Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP Event delegation for #956 #971

Merged
merged 11 commits into from
Nov 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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>
9 changes: 8 additions & 1 deletion docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,21 @@ module.exports = {
],
},
{
title: 'Digging Deeper',
title: 'Use Cases',
collapsable: false,
children: [
['guide/validation', 'Validation'],
['guide/vuex', 'Vuex'],
['guide/ajax', 'AJAX'],
],
},
{
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);
});

});

});