Skip to content

Commit

Permalink
Merge pull request #971 from sagalbot/fix-956-event-delegation
Browse files Browse the repository at this point in the history
WIP Event delegation for #956
  • Loading branch information
sagalbot committed Nov 8, 2019
2 parents 99f2dfd + 1e6b0e9 commit 92658a3
Show file tree
Hide file tree
Showing 8 changed files with 247 additions and 25 deletions.
2 changes: 1 addition & 1 deletion dev/Dev.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div id="app">
<sandbox hide-help v-slot="config">
<v-select v-bind="config" />
<v-select v-bind="config"/>
</sandbox>
</div>
</template>
Expand Down
25 changes: 25 additions & 0 deletions docs/.vuepress/components/CustomHandlers.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<template>
<v-select
taggable
multiple
no-drop
:map-keydown="handlers"
placeholder="enter an email"
/>
</template>

<script>
export default {
name: 'CustomHandlers',
methods: {
handlers: (map, vm) => ({
...map, 50: e => {
e.preventDefault();
if( e.key === '@' && vm.search.length > 0 ) {
vm.search = `${vm.search}@gmail.com`;
}
},
}),
},
};
</script>
4 changes: 4 additions & 0 deletions docs/.vuepress/components/TagOnComma.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<template>
<!-- tag on 188/comma & 13/return -->
<v-select no-drop taggable multiple :select-on-key-codes="[188, 13]" />
</template>
7 changes: 7 additions & 0 deletions docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ module.exports = {
['guide/loops', 'Using in Loops'],
],
},
{
title: 'Customizing',
collapsable: false,
children: [
['guide/keydown', 'Keydown Events'],
],
},
{
title: 'API',
collapsable: false,
Expand Down
73 changes: 73 additions & 0 deletions docs/guide/keydown.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
### Customizing Keydown Behaviour
---

## selectOnKeyCodes <Badge text="v3.3.0+" />

`selectOnKeyCodes {Array}` is an array of keyCodes that will trigger a typeAheadSelect. Any keyCodes
in this array will prevent the default event action and trigger a typeahead select. By default,
it's just `[13]` for return. For example, maybe you want to tag on a comma keystroke:

<TagOnComma />

<<< @/.vuepress/components/TagOnComma.vue

## mapKeyDown <Badge text="v3.3.0+" />

Vue Select provides the `map-keydown` Function prop to allow for customizing the components response to
keydown events while the search input has focus.

```js
/**
* @param map {Object} Mapped keyCode to handlers { <keyCode>:<callback> }
* @param vm {VueSelect}
* @return {Object}
*/
(map, vm) => map,
```

By default, the prop is a no–op returning the same object `map` object it receives. This object
maps keyCodes to handlers: `{ <keyCode>: <callback> }`. Modifying this object can override default
functionality, or add handlers for different keys that the component doesn't normally listen for.

Note that any keyCodes you've added to `selectOnKeyCodes` will be passed to `map-keydown` as well,
so `map-keydown` will always take precedence.

**Default Handlers**

```js
// delete
8: e => this.maybeDeleteValue()

// tab
9: e => this.onTab()

// enter
13: e => {
e.preventDefault();
return this.typeAheadSelect();
}

// esc
27: e => this.onEscape()

// up
38: e => {
e.preventDefault();
return this.typeAheadUp();
}

// down
40: e => {
e.preventDefault();
return this.typeAheadDown();
}
```

### Example: Autocomplete Email Addresses

This is example listens for the `@` key, and autocompletes an email address with `@gmail.com`.

<CustomHandlers />

<<< @/.vuepress/components/CustomHandlers.vue

83 changes: 61 additions & 22 deletions src/components/Select.vue
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@
import ajax from '../mixins/ajax'
import childComponents from './childComponents';
/**
* @name VueSelect
*/
export default {
components: {...childComponents},
Expand Down Expand Up @@ -302,11 +305,12 @@
/**
* Select the current value if selectOnTab is enabled
* @deprecated since 3.3
*/
onTab: {
type: Function,
default: function () {
if (this.selectOnTab) {
if (this.selectOnTab && !this.isComposing) {
this.typeAheadSelect();
}
},
Expand Down Expand Up @@ -449,12 +453,22 @@
/**
* When true, hitting the 'tab' key will select the current select value
* @type {Boolean}
* @deprecated since 3.3 - use selectOnKeyCodes instead
*/
selectOnTab: {
type: Boolean,
default: false
},
/**
* Keycodes that will select the current option.
* @type Array
*/
selectOnKeyCodes: {
type: Array,
default: () => [13],
},
/**
* Query Selector used to find the search input
* when the 'search' scoped slot is used.
Expand All @@ -467,13 +481,29 @@
searchInputQuerySelector: {
type: String,
default: '[type=search]'
},
/**
* Used to modify the default keydown events map
* for the search input. Can be used to implement
* custom behaviour for key presses.
*/
mapKeydown: {
type: Function,
/**
* @param map {Object}
* @param vm {VueSelect}
* @return {Object}
*/
default: (map, vm) => map,
}
},
data() {
return {
search: '',
open: false,
isComposing: false,
pushedTags: [],
_value: [] // Internal value managed by Vue Select if no `value` prop is passed
}
Expand Down Expand Up @@ -840,39 +870,46 @@
},
/**
* Search 'input' KeyBoardEvent handler.
* Search <input> KeyBoardEvent handler.
* @param e {KeyboardEvent}
* @return {Function}
*/
onSearchKeyDown (e) {
switch (e.keyCode) {
case 8:
// delete
return this.maybeDeleteValue();
case 9:
// tab
return this.onTab();
case 13:
// enter.prevent
e.preventDefault();
return this.typeAheadSelect();
case 27:
// esc
return this.onEscape();
case 38:
// up.prevent
const preventAndSelect = e => {
e.preventDefault();
return !this.isComposing && this.typeAheadSelect();
};
const defaults = {
// delete
8: e => this.maybeDeleteValue(),
// tab
9: e => this.onTab(),
// esc
27: e => this.onEscape(),
// up.prevent
38: e => {
e.preventDefault();
return this.typeAheadUp();
case 40:
// down.prevent
},
// down.prevent
40: e => {
e.preventDefault();
return this.typeAheadDown();
},
};
this.selectOnKeyCodes.forEach(keyCode => defaults[keyCode] = preventAndSelect);
const handlers = this.mapKeydown(defaults, this);
if (typeof handlers[e.keyCode] === 'function') {
return handlers[e.keyCode](e);
}
}
},
computed: {
/**
* Determine if the component needs to
* track the state of values internally.
Expand Down Expand Up @@ -944,10 +981,12 @@
'value': this.search,
},
events: {
'compositionstart': () => this.isComposing = true,
'compositionend': () => this.isComposing = false,
'keydown': this.onSearchKeyDown,
'blur': this.onSearchBlur,
'focus': this.onSearchFocus,
'input': (e) => this.search = e.target.value
'input': (e) => this.search = e.target.value,
},
},
spinner: {
Expand Down
74 changes: 74 additions & 0 deletions tests/unit/Keydown.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { mountDefault } from '../helpers';

describe('Custom Keydown Handlers', () => {

it('can use the map-keydown prop to trigger custom behaviour', () => {
const onKeyDown = jest.fn();
const Select = mountDefault({
mapKeydown: (defaults, vm) => ({...defaults, 32: onKeyDown}),
});

Select.find({ref: 'search'}).trigger('keydown.space');

expect(onKeyDown.mock.calls.length).toBe(1);
});

it('selectOnKeyCodes should trigger a selection for custom keycodes', () => {
const Select = mountDefault({
selectOnKeyCodes: [32],
});

const spy = jest.spyOn(Select.vm, 'typeAheadSelect');

Select.find({ref: 'search'}).trigger('keydown.space');

expect(spy).toHaveBeenCalledTimes(1);
});

it('even works when combining selectOnKeyCodes with map-keydown', () => {
const onKeyDown = jest.fn();
const Select = mountDefault({
mapKeydown: (defaults, vm) => ({...defaults, 32: onKeyDown}),
selectOnKeyCodes: [9],
});

const spy = jest.spyOn(Select.vm, 'typeAheadSelect');

Select.find({ref: 'search'}).trigger('keydown.space');
expect(onKeyDown.mock.calls.length).toBe(1);

Select.find({ref: 'search'}).trigger('keydown.tab');
expect(spy).toHaveBeenCalledTimes(1);
});

describe('CompositionEvent support', () => {

it('will not select a value with enter if the user is composing', () => {
const Select = mountDefault();
const spy = jest.spyOn(Select.vm, 'typeAheadSelect');

Select.find({ref: 'search'}).trigger('compositionstart');
Select.find({ref: 'search'}).trigger('keydown.enter');
expect(spy).toHaveBeenCalledTimes(0);

Select.find({ref: 'search'}).trigger('compositionend');
Select.find({ref: 'search'}).trigger('keydown.enter');
expect(spy).toHaveBeenCalledTimes(1);
});

it('will not select a value with tab if the user is composing', () => {
const Select = mountDefault({selectOnTab: true});
const spy = jest.spyOn(Select.vm, 'typeAheadSelect');

Select.find({ref: 'search'}).trigger('compositionstart');
Select.find({ref: 'search'}).trigger('keydown.tab');
expect(spy).toHaveBeenCalledTimes(0);

Select.find({ref: 'search'}).trigger('compositionend');
Select.find({ref: 'search'}).trigger('keydown.tab');
expect(spy).toHaveBeenCalledTimes(1);
});

});

});
4 changes: 2 additions & 2 deletions tests/unit/Selectable.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ describe("Selectable prop", () => {
it("should select selectable option if clicked", () => {
const Select = selectWithProps({
options: ["one", "two", "three"],
selectable: (option) => option == "one"
selectable: (option) => option === "one"
});

Select.vm.$data.open = true;
Expand All @@ -16,7 +16,7 @@ describe("Selectable prop", () => {
it("should not select not selectable option if clicked", () => {
const Select = selectWithProps({
options: ["one", "two", "three"],
selectable: (option) => option == "one"
selectable: (option) => option === "one"
});

Select.vm.$data.open = true;
Expand Down

0 comments on commit 92658a3

Please sign in to comment.