From aea81a6f5ca115bb3f124638c484ea7622e1fc49 Mon Sep 17 00:00:00 2001 From: Markus Date: Fri, 25 Oct 2019 22:11:50 +0200 Subject: [PATCH 1/9] Allow to disable options with selectable function (#921) * allow to disable options with selectable function * add simple spec for new selectable option * Prevent non-selectable options from being keyboard navigatable --- src/components/Select.vue | 19 +++++++-- src/mixins/typeAheadPointer.js | 33 ++++++++++------ src/scss/modules/_dropdown-option.scss | 9 +++++ tests/unit/Selectable.spec.js | 53 ++++++++++++++++++++++++++ tests/unit/TypeAhead.spec.js | 8 ++-- 5 files changed, 104 insertions(+), 18 deletions(-) create mode 100644 tests/unit/Selectable.spec.js diff --git a/src/components/Select.vue b/src/components/Select.vue index a80efdd..2d8c5f3 100644 --- a/src/components/Select.vue +++ b/src/components/Select.vue @@ -57,9 +57,9 @@ v-for="(option, index) in filteredOptions" :key="getOptionKey(option)" class="vs__dropdown-option" - :class="{ 'vs__dropdown-option--selected': isOptionSelected(option), 'vs__dropdown-option--highlight': index === typeAheadPointer }" - @mouseover="typeAheadPointer = index" - @mousedown.prevent.stop="select(option)" + :class="{ 'vs__dropdown-option--selected': isOptionSelected(option), 'vs__dropdown-option--highlight': index === typeAheadPointer, 'vs__dropdown-option--disabled': !selectable(option) }" + @mouseover="selectable(option) ? typeAheadPointer = index : null" + @mousedown.prevent.stop="selectable(option) ? select(option) : null" > {{ getOptionLabel(option) }} @@ -224,6 +224,19 @@ default: option => option, }, + /** + * Decides wether an option is selectable or not. Not selectable options + * are displayed but disabled and cannot be selected. + * + * @type {Function} + * @param {Object|String} option + * @return {Boolean} + */ + selectable: { + type: Function, + default: option => true, + }, + /** * Callback to generate the label text. If {option} * is an object, returns option[this.label] by default. diff --git a/src/mixins/typeAheadPointer.js b/src/mixins/typeAheadPointer.js index f6f9467..4eb2636 100644 --- a/src/mixins/typeAheadPointer.js +++ b/src/mixins/typeAheadPointer.js @@ -7,35 +7,46 @@ export default { watch: { filteredOptions() { - this.typeAheadPointer = 0 + for (let i = 0; i < this.filteredOptions.length; i++) { + if (this.selectable(this.filteredOptions[i])) { + this.typeAheadPointer = i; + break; + } + } } }, methods: { /** * Move the typeAheadPointer visually up the list by - * subtracting the current index by one. + * setting it to the previous selectable option. * @return {void} */ typeAheadUp() { - if (this.typeAheadPointer > 0) { - this.typeAheadPointer-- - if( this.maybeAdjustScroll ) { - this.maybeAdjustScroll() + for (let i = this.typeAheadPointer - 1; i >= 0; i--) { + if (this.selectable(this.filteredOptions[i])) { + this.typeAheadPointer = i; + if( this.maybeAdjustScroll ) { + this.maybeAdjustScroll() + } + break; } } }, /** * Move the typeAheadPointer visually down the list by - * adding the current index by one. + * setting it to the next selectable option. * @return {void} */ typeAheadDown() { - if (this.typeAheadPointer < this.filteredOptions.length - 1) { - this.typeAheadPointer++ - if( this.maybeAdjustScroll ) { - this.maybeAdjustScroll() + for (let i = this.typeAheadPointer + 1; i < this.filteredOptions.length; i++) { + if (this.selectable(this.filteredOptions[i])) { + this.typeAheadPointer = i; + if( this.maybeAdjustScroll ) { + this.maybeAdjustScroll() + } + break; } } }, diff --git a/src/scss/modules/_dropdown-option.scss b/src/scss/modules/_dropdown-option.scss index e25de00..474746d 100644 --- a/src/scss/modules/_dropdown-option.scss +++ b/src/scss/modules/_dropdown-option.scss @@ -16,3 +16,12 @@ background: $vs-state-active-bg; color: $vs-state-active-color; } + +.vs__dropdown-option--disabled { + background: inherit; + color: $vs-state-disabled-color; + + &:hover { + cursor: inherit; + } +} diff --git a/tests/unit/Selectable.spec.js b/tests/unit/Selectable.spec.js new file mode 100644 index 0000000..8812834 --- /dev/null +++ b/tests/unit/Selectable.spec.js @@ -0,0 +1,53 @@ +import { selectWithProps } from "../helpers"; + +describe("Selectable prop", () => { + it("should select selectable option if clicked", () => { + const Select = selectWithProps({ + options: ["one", "two", "three"], + selectable: (option) => option == "one" + }); + + Select.vm.$data.open = true; + + Select.find(".vs__dropdown-menu li:first-child").trigger("mousedown"); + expect(Select.vm.selectedValue).toEqual(["one"]); + }) + + it("should not select not selectable option if clicked", () => { + const Select = selectWithProps({ + options: ["one", "two", "three"], + selectable: (option) => option == "one" + }); + + Select.vm.$data.open = true; + + Select.find(".vs__dropdown-menu li:last-child").trigger("mousedown"); + expect(Select.vm.selectedValue).toEqual([]); + }); + + it("should skip non-selectable option on down arrow keyUp", () => { + const Select = selectWithProps({ + options: ["one", "two", "three"], + selectable: (option) => option !== "two" + }); + + Select.vm.typeAheadPointer = 1; + + Select.find({ ref: "search" }).trigger("keyup.down"); + + expect(Select.vm.typeAheadPointer).toEqual(2); + }) + + it("should skip non-selectable option on up arrow keyUp", () => { + const Select = selectWithProps({ + options: ["one", "two", "three"], + selectable: (option) => option !== "two" + }); + + Select.vm.typeAheadPointer = 2; + + Select.find({ ref: "search" }).trigger("keyup.up"); + + expect(Select.vm.typeAheadPointer).toEqual(0); + }) +}) diff --git a/tests/unit/TypeAhead.spec.js b/tests/unit/TypeAhead.spec.js index 31a2f57..6dcc6ab 100755 --- a/tests/unit/TypeAhead.spec.js +++ b/tests/unit/TypeAhead.spec.js @@ -18,7 +18,7 @@ describe("Moving the Typeahead Pointer", () => { expect(Select.vm.typeAheadPointer).toEqual(0); }); - it("should move the pointer visually up the list on up arrow keyDown", () => { + it("should move the pointer visually up the list on up arrow keyUp", () => { const Select = mountDefault(); Select.vm.typeAheadPointer = 1; @@ -28,7 +28,7 @@ describe("Moving the Typeahead Pointer", () => { expect(Select.vm.typeAheadPointer).toEqual(0); }); - it("should move the pointer visually down the list on down arrow keyDown", () => { + it("should move the pointer visually down the list on down arrow keyUp", () => { const Select = mountDefault(); Select.vm.typeAheadPointer = 1; @@ -47,7 +47,7 @@ describe("Moving the Typeahead Pointer", () => { }); describe("Automatic Scrolling", () => { - it("should check if the scroll position needs to be adjusted on up arrow keyDown", () => { + it("should check if the scroll position needs to be adjusted on up arrow keyUp", () => { const Select = mountDefault(); const spy = jest.spyOn(Select.vm, "maybeAdjustScroll"); @@ -57,7 +57,7 @@ describe("Moving the Typeahead Pointer", () => { expect(spy).toHaveBeenCalled(); }); - it("should check if the scroll position needs to be adjusted on down arrow keyDown", () => { + it("should check if the scroll position needs to be adjusted on down arrow keyUp", () => { const Select = mountDefault(); const spy = jest.spyOn(Select.vm, "maybeAdjustScroll"); From ceb42b49502a750b4971e3e7990ae87c88a4134f Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Sat, 26 Oct 2019 06:42:59 +1030 Subject: [PATCH 2/9] Change all keyup events to keydown (#935) For a few reasons: - event.preventDefault() for the Enter key (to stop it from submitting the form when you select an item) is only effective if it's a keydown event. - Using keydown for up/down navigation means you can hold them down to rapidly scroll through a lot of items. - Keydown events make the UX feel more responsive, and is consistent with how most apps/operating systems handle key presses. --- src/components/Select.vue | 19 ++++--------------- tests/helpers.js | 2 +- tests/unit/Dropdown.spec.js | 4 ++-- tests/unit/TypeAhead.spec.js | 8 ++++---- 4 files changed, 11 insertions(+), 22 deletions(-) diff --git a/src/components/Select.vue b/src/components/Select.vue index 2d8c5f3..4fdd2c5 100644 --- a/src/components/Select.vue +++ b/src/components/Select.vue @@ -852,16 +852,10 @@ case 9: // tab return this.onTab(); - } - }, - - /** - * Search 'input' KeyBoardEvent handler. - * @param e {KeyboardEvent} - * @return {Function} - */ - onSearchKeyUp (e) { - switch (e.keyCode) { + case 13: + // enter.prevent + e.preventDefault(); + return this.typeAheadSelect(); case 27: // esc return this.onEscape(); @@ -873,10 +867,6 @@ // down.prevent e.preventDefault(); return this.typeAheadDown(); - case 13: - // enter.prevent - e.preventDefault(); - return this.typeAheadSelect(); } } }, @@ -955,7 +945,6 @@ }, events: { 'keydown': this.onSearchKeyDown, - 'keyup': this.onSearchKeyUp, 'blur': this.onSearchBlur, 'focus': this.onSearchFocus, 'input': (e) => this.search = e.target.value diff --git a/tests/helpers.js b/tests/helpers.js index fd45c88..77c5988 100755 --- a/tests/helpers.js +++ b/tests/helpers.js @@ -13,7 +13,7 @@ export const searchSubmit = (Wrapper, searchText = false) => { if (searchText) { Wrapper.vm.search = searchText; } - Wrapper.find({ ref: "search" }).trigger("keyup.enter") + Wrapper.find({ ref: "search" }).trigger("keydown.enter") }; /** diff --git a/tests/unit/Dropdown.spec.js b/tests/unit/Dropdown.spec.js index 58b01e2..6ae22a4 100755 --- a/tests/unit/Dropdown.spec.js +++ b/tests/unit/Dropdown.spec.js @@ -109,14 +109,14 @@ describe("Toggling Dropdown", () => { expect(spy).toHaveBeenCalled(); }); - it("should remove existing search text on escape keyup", () => { + it("should remove existing search text on escape keydown", () => { const Select = selectWithProps({ value: [{ label: "one" }], options: [{ label: "one" }] }); Select.vm.search = "foo"; - Select.find('.vs__search').trigger('keyup.esc') + Select.find('.vs__search').trigger('keydown.esc') expect(Select.vm.search).toEqual(""); }); diff --git a/tests/unit/TypeAhead.spec.js b/tests/unit/TypeAhead.spec.js index 6dcc6ab..a1a4755 100755 --- a/tests/unit/TypeAhead.spec.js +++ b/tests/unit/TypeAhead.spec.js @@ -23,7 +23,7 @@ describe("Moving the Typeahead Pointer", () => { Select.vm.typeAheadPointer = 1; - Select.find({ ref: "search" }).trigger("keyup.up"); + Select.find({ ref: "search" }).trigger("keydown.up"); expect(Select.vm.typeAheadPointer).toEqual(0); }); @@ -33,7 +33,7 @@ describe("Moving the Typeahead Pointer", () => { Select.vm.typeAheadPointer = 1; - Select.find({ ref: "search" }).trigger("keyup.down"); + Select.find({ ref: "search" }).trigger("keydown.down"); expect(Select.vm.typeAheadPointer).toEqual(2); }); @@ -53,7 +53,7 @@ describe("Moving the Typeahead Pointer", () => { Select.vm.typeAheadPointer = 1; - Select.find({ ref: "search" }).trigger("keyup.up"); + Select.find({ ref: "search" }).trigger("keydown.up"); expect(spy).toHaveBeenCalled(); }); @@ -63,7 +63,7 @@ describe("Moving the Typeahead Pointer", () => { Select.vm.typeAheadPointer = 1; - Select.find({ ref: "search" }).trigger("keyup.down"); + Select.find({ ref: "search" }).trigger("keydown.down"); expect(spy).toHaveBeenCalled(); }); From b8fbefdc2d66485d0bd57393d2aa54fb6c8a0fbc Mon Sep 17 00:00:00 2001 From: Markus Date: Sat, 26 Oct 2019 20:42:50 +0200 Subject: [PATCH 3/9] fix unclosable select dropdown when clicking on dropdown (#949) The reason for the bug was: clicking on the area blurred the search input. vue-slect relies on this event to close the dropdown. Since the click happend inside the dropdown, it did not close (which is correct). Though now the search input was blurred already, so clicking outside of the dropdown had no effect. Be preventing event propagation, the input does not get blurred anymore when clicking inside the dropdown and everything still works. --- src/components/Select.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Select.vue b/src/components/Select.vue index 4fdd2c5..f2aba4e 100644 --- a/src/components/Select.vue +++ b/src/components/Select.vue @@ -51,7 +51,7 @@ -
    +
    • Date: Sat, 26 Oct 2019 12:43:57 -0600 Subject: [PATCH 4/9] Update slots.md (#959) remove deprecated slot="" scope-slot="" syntax --- docs/guide/slots.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/guide/slots.md b/docs/guide/slots.md index 10d3c50..7bfe9b1 100644 --- a/docs/guide/slots.md +++ b/docs/guide/slots.md @@ -10,14 +10,13 @@ vue-select provides the scoped `option` slot in order to create custom dropdown ```html -