",
"homepage": "https://vue-select.org",
diff --git a/src/components/Select.vue b/src/components/Select.vue
index 1ef3891..0145c9c 100644
--- a/src/components/Select.vue
+++ b/src/components/Select.vue
@@ -7,10 +7,11 @@
{{ selected.label }}
@@ -22,6 +23,7 @@
typeof option === 'object' ? {...option} : {};
+
/**
* @name VueSelect
*/
@@ -215,10 +219,11 @@
},
/**
- * Decides wether an option is selectable or not. Not selectable options
+ * Decides whether an option is selectable or not. Not selectable options
* are displayed but disabled and cannot be selected.
*
* @type {Function}
+ * @since 3.3.0
* @param {Object|String} option
* @return {Boolean}
*/
@@ -292,10 +297,8 @@
getOptionScope: {
type: Function,
default (option, index) {
- const optionProperties = typeof option === 'object' ? {...option} : {};
-
return {
- ...optionProperties,
+ ...spreadableOptionProperties(option),
label: this.getOptionLabel(option),
attributes: {
key: this.getOptionKey(option),
@@ -322,6 +325,7 @@
type: Function,
default (option, index) {
return {
+ ...spreadableOptionProperties(option),
label: this.getOptionLabel(option),
deselect: this.getOptionDeselectScope(option),
bindings: {
@@ -354,6 +358,7 @@
'aria-label': `Deselect ${this.getOptionLabel(option)}`,
'disabled': this.disabled,
'multiple': this.multiple,
+ 'ref': 'deselectButtons'
},
events: {
'click': () => this.deselect(option),
@@ -467,12 +472,20 @@
},
/**
- * When false, updating the options will not reset the select value
- * @type {Boolean}
+ * When false, updating the options will not reset the selected value. Accepts
+ * a `boolean` or `function` that returns a `boolean`. If defined as a function,
+ * it will receive the params listed below.
+ *
+ * @since 3.4 - Type changed to {Boolean|Function}
+ *
+ * @type {Boolean|Function}
+ * @param {Array} newOptions
+ * @param {Array} oldOptions
+ * @param {Array} selectedValue
*/
resetOnOptionsChange: {
- type: Boolean,
- default: false
+ default: false,
+ validator: (value) => ['function', 'boolean'].includes(typeof value)
},
/**
@@ -571,13 +584,17 @@
* is correct.
* @return {[type]} [description]
*/
- options(val) {
- if (!this.taggable && this.resetOnOptionsChange) {
- this.clearSelection()
+ options (newOptions, oldOptions) {
+ let shouldReset = () => typeof this.resetOnOptionsChange === 'function'
+ ? this.resetOnOptionsChange(newOptions, oldOptions, this.selectedValue)
+ : this.resetOnOptionsChange;
+
+ if (!this.taggable && shouldReset()) {
+ this.clearSelection();
}
if (this.value && this.isTrackingValues) {
- this.setInternalValueFromOptions(this.value)
+ this.setInternalValueFromOptions(this.value);
}
},
@@ -712,33 +729,23 @@
* @param {Event} e
* @return {void}
*/
- toggleDropdown (e) {
- const target = e.target;
- const toggleTargets = [
- this.$el,
- this.searchEl,
- this.$refs.toggle,
- this.$refs.actions,
- this.$refs.selectedOptions,
+ toggleDropdown ({target}) {
+ // don't react to click on deselect/clear buttons,
+ // they dropdown state will be set in their click handlers
+ const ignoredButtons = [
+ ...(this.$refs['deselectButtons'] || []),
+ ...([this.$refs['clearButton']] || [])
];
- if (typeof this.$refs.openIndicator !== 'undefined') {
- toggleTargets.push(
- this.$refs.openIndicator.$el,
- // the line below is a bit gross, but required to support IE11 without adding polyfills
- ...Array.prototype.slice.call(this.$refs.openIndicator.$el.childNodes),
- );
+ if (ignoredButtons.some(ref => ref.contains(target) || ref === target)) {
+ return;
}
- if (toggleTargets.indexOf(target) > -1 || target.classList.contains('vs__selected')) {
- if (this.open) {
- this.searchEl.blur(); // dropdown will close on blur
- } else {
- if (!this.disabled) {
- this.open = true;
- this.searchEl.focus();
- }
- }
+ if (this.open) {
+ this.searchEl.blur();
+ } else if (!this.disabled) {
+ this.open = true;
+ this.searchEl.focus();
}
},
@@ -1061,6 +1068,7 @@
clear: {
component: this.childComponents.Deselect,
bindings: {
+ 'ref': 'clearButton',
'disabled': this.disabled,
'type': 'button',
'class': 'vs__clear',
diff --git a/tests/helpers.js b/tests/helpers.js
index 77c5988..3e2c176 100755
--- a/tests/helpers.js
+++ b/tests/helpers.js
@@ -1,5 +1,5 @@
-import { shallowMount } from "@vue/test-utils";
-import VueSelect from "../src/components/Select.vue";
+import { mount, shallowMount } from '@vue/test-utils';
+import VueSelect from '../src/components/Select.vue';
import Vue from 'vue';
/**
@@ -13,7 +13,7 @@ export const searchSubmit = (Wrapper, searchText = false) => {
if (searchText) {
Wrapper.vm.search = searchText;
}
- Wrapper.find({ ref: "search" }).trigger("keydown.enter")
+ Wrapper.find({ref: 'search'}).trigger('keydown.enter');
};
/**
@@ -22,9 +22,11 @@ export const searchSubmit = (Wrapper, searchText = false) => {
* @param propsData
* @returns {Wrapper}
*/
-export const selectWithProps = (propsData = {}) => {
- return shallowMount(VueSelect, { propsData });
-};
+export const selectWithProps = (propsData = {}) =>
+ shallowMount(VueSelect, {propsData});
+
+export const mountWithProps = (propsData = {}) =>
+ mount(VueSelect, {propsData});
/**
* Returns a Wrapper with a v-select component.
@@ -42,7 +44,6 @@ export const mountDefault = (props = {}, options = {}) => {
});
};
-
/**
* Returns a v-select component directly.
* @param props
@@ -54,7 +55,7 @@ export const mountWithoutTestUtils = (props = {}, options = {}) => {
render: createEl => createEl('vue-select', {
ref: 'select',
props: {options: ['one', 'two', 'three'], ...props},
- ...options
+ ...options,
}),
components: {VueSelect},
}).$mount().$refs.select;
diff --git a/tests/unit/Dropdown.spec.js b/tests/unit/Dropdown.spec.js
index 6ae22a4..fe72a8d 100755
--- a/tests/unit/Dropdown.spec.js
+++ b/tests/unit/Dropdown.spec.js
@@ -1,25 +1,37 @@
-import { selectWithProps } from "../helpers";
+import { mountWithProps } from "../helpers";
import OpenIndicator from "../../src/components/OpenIndicator";
describe("Toggling Dropdown", () => {
- it("should not open the dropdown when the el is clicked but the component is disabled", () => {
- const Select = selectWithProps({ disabled: true });
- Select.vm.toggleDropdown({ target: Select.vm.$refs.search });
- expect(Select.vm.open).toEqual(false);
- });
-
- it("should open the dropdown when the el is clicked", () => {
- const Select = selectWithProps({
+ it("should open the dropdown when the el is clicked", async () => {
+ const Select = mountWithProps({
value: [{ label: "one" }],
options: [{ label: "one" }]
});
+ const spy = jest.spyOn(Select.vm, 'toggleDropdown');
- Select.vm.toggleDropdown({ target: Select.vm.$refs.search });
+ Select.find({ref: 'toggle'}).trigger('mousedown');
+
+ await Select.vm.$nextTick();
+
+ expect(spy).toHaveBeenCalled();
expect(Select.vm.open).toEqual(true);
});
+ it("should not open the dropdown when the el is clicked but the component is disabled", async () => {
+ const Select = mountWithProps({ disabled: true });
+
+ const spy = jest.spyOn(Select.vm, 'toggleDropdown');
+
+ Select.find({ref: 'toggle'}).trigger('mousedown');
+
+ await Select.vm.$nextTick();
+
+ expect(spy).toHaveBeenCalled();
+ expect(Select.vm.open).toEqual(false);
+ });
+
it("should open the dropdown when the selected tag is clicked", () => {
- const Select = selectWithProps({
+ const Select = mountWithProps({
value: [{ label: "one" }],
options: [{ label: "one" }]
});
@@ -31,7 +43,7 @@ describe("Toggling Dropdown", () => {
});
it("can close the dropdown when the el is clicked", () => {
- const Select = selectWithProps();
+ const Select = mountWithProps();
const spy = jest.spyOn(Select.vm.$refs.search, "blur");
Select.vm.open = true;
@@ -41,7 +53,7 @@ describe("Toggling Dropdown", () => {
});
it("closes the dropdown when an option is selected, multiple is true, and closeOnSelect option is true", () => {
- const Select = selectWithProps({
+ const Select = mountWithProps({
value: [],
options: ["one", "two", "three"],
multiple: true
@@ -54,7 +66,7 @@ describe("Toggling Dropdown", () => {
});
it("does not close the dropdown when the el is clicked, multiple is true, and closeOnSelect option is false", () => {
- const Select = selectWithProps({
+ const Select = mountWithProps({
value: [],
options: ["one", "two", "three"],
multiple: true,
@@ -68,7 +80,7 @@ describe("Toggling Dropdown", () => {
});
it("should close the dropdown on search blur", () => {
- const Select = selectWithProps({
+ const Select = mountWithProps({
options: [{ label: "one" }]
});
@@ -79,7 +91,7 @@ describe("Toggling Dropdown", () => {
});
it("will close the dropdown and emit the search:blur event from onSearchBlur", () => {
- const Select = selectWithProps();
+ const Select = mountWithProps();
const spy = jest.spyOn(Select.vm, "$emit");
Select.vm.open = true;
@@ -90,7 +102,7 @@ describe("Toggling Dropdown", () => {
});
it("will open the dropdown and emit the search:focus event from onSearchFocus", () => {
- const Select = selectWithProps();
+ const Select = mountWithProps();
const spy = jest.spyOn(Select.vm, "$emit");
Select.vm.onSearchFocus();
@@ -100,7 +112,7 @@ describe("Toggling Dropdown", () => {
});
it("will close the dropdown on escape, if search is empty", () => {
- const Select = selectWithProps();
+ const Select = mountWithProps();
const spy = jest.spyOn(Select.vm.$refs.search, "blur");
Select.vm.open = true;
@@ -110,7 +122,7 @@ describe("Toggling Dropdown", () => {
});
it("should remove existing search text on escape keydown", () => {
- const Select = selectWithProps({
+ const Select = mountWithProps({
value: [{ label: "one" }],
options: [{ label: "one" }]
});
@@ -121,7 +133,7 @@ describe("Toggling Dropdown", () => {
});
it("should have an open class when dropdown is active", () => {
- const Select = selectWithProps();
+ const Select = mountWithProps();
expect(Select.vm.stateClasses['vs--open']).toEqual(false);
@@ -130,7 +142,7 @@ describe("Toggling Dropdown", () => {
});
it("should not display the dropdown if noDrop is true", () => {
- const Select = selectWithProps({
+ const Select = mountWithProps({
noDrop: true,
});
@@ -141,21 +153,21 @@ describe("Toggling Dropdown", () => {
});
it("should hide the open indicator if noDrop is true", () => {
- const Select = selectWithProps({
+ const Select = mountWithProps({
noDrop: true,
});
expect(Select.contains(OpenIndicator)).toBeFalsy();
});
it("should not add the searchable state class when noDrop is true", () => {
- const Select = selectWithProps({
+ const Select = mountWithProps({
noDrop: true,
});
expect(Select.classes('vs--searchable')).toBeFalsy();
});
it("should not add the searching state class when noDrop is true", () => {
- const Select = selectWithProps({
+ const Select = mountWithProps({
noDrop: true,
});
diff --git a/tests/unit/ReactiveOptions.spec.js b/tests/unit/ReactiveOptions.spec.js
index 04b6a91..efefd37 100755
--- a/tests/unit/ReactiveOptions.spec.js
+++ b/tests/unit/ReactiveOptions.spec.js
@@ -1,5 +1,6 @@
-import { shallowMount } from "@vue/test-utils";
+import { mount, shallowMount } from '@vue/test-utils';
import VueSelect from "../../src/components/Select";
+import { mountDefault } from '../helpers';
describe("Reset on options change", () => {
it("should not reset the selected value by default when the options property changes", () => {
@@ -13,6 +14,73 @@ describe("Reset on options change", () => {
expect(Select.vm.selectedValue).toEqual(["one"]);
});
+ describe('resetOnOptionsChange as a function', () => {
+ it('will yell at you if resetOnOptionsChange is not a function or boolean', () => {
+ const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ mountDefault({resetOnOptionsChange: 1});
+ expect(spy.mock.calls[0][0]).toContain('Invalid prop: custom validator check failed for prop "resetOnOptionsChange"')
+
+ mountDefault({resetOnOptionsChange: 'one'});
+ expect(spy.mock.calls[1][0]).toContain('Invalid prop: custom validator check failed for prop "resetOnOptionsChange"')
+
+ mountDefault({resetOnOptionsChange: []});
+ expect(spy.mock.calls[2][0]).toContain('Invalid prop: custom validator check failed for prop "resetOnOptionsChange"')
+
+ mountDefault({resetOnOptionsChange: {}});
+ expect(spy.mock.calls[3][0]).toContain('Invalid prop: custom validator check failed for prop "resetOnOptionsChange"')
+ });
+
+ it('should receive the new options, old options, and current value', () => {
+ let resetOnOptionsChange = jest.fn(option => option);
+ const Select = mountDefault(
+ {resetOnOptionsChange, options: ['bear'], value: 'selected'},
+ );
+
+ Select.setProps({options: ['lake', 'kite']});
+
+ expect(resetOnOptionsChange).toHaveBeenCalledTimes(1);
+ expect(resetOnOptionsChange)
+ .toHaveBeenCalledWith(['lake', 'kite'], ['bear'], ['selected']);
+ });
+
+ it('should allow resetOnOptionsChange to be a function that returns true', () => {
+ let resetOnOptionsChange = () => true;
+ const Select = shallowMount(VueSelect, {
+ propsData: {resetOnOptionsChange, options: ['one'], value: 'one'},
+ });
+ const spy = jest.spyOn(Select.vm, 'clearSelection');
+
+ Select.setProps({options: ['one', 'two']});
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+
+ it('should allow resetOnOptionsChange to be a function that returns false', () => {
+ let resetOnOptionsChange = () => false;
+ const Select = shallowMount(VueSelect, {
+ propsData: {resetOnOptionsChange, options: ['one'], value: 'one'},
+ });
+ const spy = jest.spyOn(Select.vm, 'clearSelection');
+
+ Select.setProps({options: ['one', 'two']});
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('should reset the options if the selectedValue does not exist in the new options', async () => {
+ let resetOnOptionsChange = (options, old, val) => val.some(val => options.includes(val));
+ const Select = shallowMount(VueSelect, {
+ propsData: {resetOnOptionsChange, options: ['one'], value: 'one'},
+ });
+ const spy = jest.spyOn(Select.vm, 'clearSelection');
+
+ Select.setProps({options: ['one', 'two']});
+ expect(Select.vm.selectedValue).toEqual(['one']);
+
+ Select.setProps({options: ['two']});
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+ });
+
it("should reset the selected value when the options property changes", () => {
const Select = shallowMount(VueSelect, {
propsData: { resetOnOptionsChange: true, options: ["one"] }
diff --git a/tests/unit/Slots.spec.js b/tests/unit/Slots.spec.js
index 66d11c2..2e45cb5 100644
--- a/tests/unit/Slots.spec.js
+++ b/tests/unit/Slots.spec.js
@@ -1,29 +1,53 @@
import { mountDefault } from '../helpers';
+/**
+ * Breaking change to the slot signature: {option} is no longer a valid key
+ * Breaking change: removed selected-option-container
+ */
+
describe('Scoped Slots', () => {
- it('receives an option object to the selected-option slot', () => {
- const Select = mountDefault(
- {value: 'one'},
- {
- scopedSlots: {
- 'selected-option': `{{ option.label }}`,
- },
- });
+ let receiveProps = props => receivedSlotProps = props;
+ let receivedSlotProps;
- expect(Select.find('.vs__selected').text()).toEqual('one')
+ beforeEach(() => receivedSlotProps = null);
+
+ describe('Slot: selected-option', () => {
+ it('receives an option object in the selected-option slot', () => {
+ mountDefault(
+ {value: 'one'},
+ {scopedSlots: {'selected-option': receiveProps}},
+ );
+ expect(receivedSlotProps.label).toEqual('one');
+ expect(receivedSlotProps.hasOwnProperty('bindings')).toBeTruthy();
+ expect(receivedSlotProps.hasOwnProperty('events')).toBeTruthy();
+ expect(receivedSlotProps.hasOwnProperty('deselect')).toBeTruthy();
+ });
+
+ xit('opens the dropdown when clicked', () => {
+ const Select = mountDefault(
+ {value: 'one'},
+ {
+ scopedSlots: {
+ 'selected-option': `{{ option.label }}`,
+ },
+ });
+
+ Select.find('.my-option').trigger('mousedown');
+ expect(Select.vm.open).toEqual(true);
+ });
});
- it('receives an option object to the option slot in the dropdown menu', () => {
- const Select = mountDefault(
- {value: 'one'},
- {
- scopedSlots: {
- 'option': `{{ option.label }}`,
- },
- });
-
- Select.vm.open = true;
-
- expect(Select.find({ref: 'dropdownMenu'}).text()).toEqual('onetwothree')
+ describe('Slot: option', () => {
+ it('receives an option object in the option slot', () => {
+ const {vm} = mountDefault(
+ {value: 'one', options: ['one']},
+ {scopedSlots: {option: receiveProps}},
+ );
+ vm.open = true;
+ expect(receivedSlotProps.label).toEqual('one');
+ expect(receivedSlotProps.hasOwnProperty('attributes')).toBeTruthy();
+ expect(receivedSlotProps.hasOwnProperty('events')).toBeTruthy();
+ });
});
+
});