diff --git a/dev/Dev.vue b/dev/Dev.vue index d180b1a..880c8c2 100644 --- a/dev/Dev.vue +++ b/dev/Dev.vue @@ -1,7 +1,7 @@ diff --git a/docs/.vuepress/components/CustomHandlers.vue b/docs/.vuepress/components/CustomHandlers.vue new file mode 100644 index 0000000..8fa43e8 --- /dev/null +++ b/docs/.vuepress/components/CustomHandlers.vue @@ -0,0 +1,25 @@ + + + diff --git a/docs/.vuepress/components/TagOnComma.vue b/docs/.vuepress/components/TagOnComma.vue new file mode 100644 index 0000000..59e1d6a --- /dev/null +++ b/docs/.vuepress/components/TagOnComma.vue @@ -0,0 +1,4 @@ + diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 583ba3d..1f2e6a0 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -123,6 +123,13 @@ module.exports = { ['guide/loops', 'Using in Loops'], ], }, + { + title: 'Customizing', + collapsable: false, + children: [ + ['guide/keydown', 'Keydown Events'], + ], + }, { title: 'API', collapsable: false, diff --git a/docs/guide/keydown.md b/docs/guide/keydown.md new file mode 100644 index 0000000..b353960 --- /dev/null +++ b/docs/guide/keydown.md @@ -0,0 +1,73 @@ +### Customizing Keydown Behaviour +--- + +## selectOnKeyCodes + +`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: + + + +<<< @/.vuepress/components/TagOnComma.vue + +## mapKeyDown + +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 { : } + * @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: `{ : }`. 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`. + + + +<<< @/.vuepress/components/CustomHandlers.vue + diff --git a/src/components/Select.vue b/src/components/Select.vue index f2aba4e..ba34341 100644 --- a/src/components/Select.vue +++ b/src/components/Select.vue @@ -79,6 +79,9 @@ import ajax from '../mixins/ajax' import childComponents from './childComponents'; + /** + * @name VueSelect + */ export default { components: {...childComponents}, @@ -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(); } }, @@ -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. @@ -467,6 +481,21 @@ 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, } }, @@ -474,6 +503,7 @@ return { search: '', open: false, + isComposing: false, pushedTags: [], _value: [] // Internal value managed by Vue Select if no `value` prop is passed } @@ -840,39 +870,46 @@ }, /** - * Search 'input' KeyBoardEvent handler. + * Search 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. @@ -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: { diff --git a/tests/unit/Keydown.spec.js b/tests/unit/Keydown.spec.js new file mode 100644 index 0000000..4721719 --- /dev/null +++ b/tests/unit/Keydown.spec.js @@ -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); + }); + + }); + +}); diff --git a/tests/unit/Selectable.spec.js b/tests/unit/Selectable.spec.js index b8158eb..11d7663 100644 --- a/tests/unit/Selectable.spec.js +++ b/tests/unit/Selectable.spec.js @@ -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; @@ -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;