2
0
mirror of https://github.com/tenrok/vue-select.git synced 2026-06-19 09:50:33 +03:00

Merge branch 'master' into ft/el-as-a-button-better-focusing

This commit is contained in:
Jeff
2020-03-05 19:39:27 -08:00
27 changed files with 4757 additions and 1279 deletions
+12
View File
@@ -0,0 +1,12 @@
## Pull Requests
Looks like you want to help out on vue-select.. awesome! Here's a few things to keep in mind when contributing.
- Vue Select uses semantic release to automate the release process. This means that good commit
messages are critical. If you're unfamiliar with [conventional changelog](https://github.com/ajoslin/conventional-changelog) you can run `yarn commit` to generate a commit message.
- It's almost always better to ask before you jump into a PR if you want to add new functionality
. It's not fun for anyone when you spend time on something that doesn't end up in the component.
- If your PR fixes or references an open issue, be sure to reference it in your message.
- If you're adding new functionality, make sure your code has good test coverage.
:tada: Thanks for contributing, and an even bigger thanks for reading these guidelines!
+28
View File
@@ -0,0 +1,28 @@
name: Release
on:
push:
branches:
- master
jobs:
release:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
with:
node-version: 12
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Test with Coverage
run: yarn test
- name: Build
run: yarn build
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx semantic-release
+38
View File
@@ -0,0 +1,38 @@
name: Test & Build
on: [pull_request]
jobs:
test:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
with:
node-version: 12
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Test with Coverage
run: yarn test --coverage --coverageReporters=lcov
- name: Report Coverage
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
build:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
with:
node-version: 12
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build
run: yarn build
- name: Bundlewatch
run: npx bundlewatch
-9
View File
@@ -1,9 +0,0 @@
language: node_js
cache: yarn
node_js:
- "8"
script:
- yarn test --coverage --coverageReporters=text-lcov | coveralls
- yarn build && bundlewatch --max-size 20kb ./dist/!(*.map)
-10
View File
@@ -1,10 +0,0 @@
## Pull Requests
Looks like you want to help out on vue-select.. awesome! Here's a few things to keep in mind when contributing.
1. If your PR contains multiple commits, try to keep those commits succinct, with descriptive messages. This makes it easier to understand your thought process.
2. **Don't run the build** before submitting. The build is only run and committed immediately before a new release.
3. If your PR fixes or references an open issue, be sure to reference it in your message.
4. If you're adding new functionality, make sure your code has good test coverage.
:tada: Thanks for contributing, and an even bigger thanks for reading these guidelines!
@@ -0,0 +1,16 @@
<template>
<v-select>
<template v-slot:no-options="{ search, searching }">
<template v-if="searching">
No results found for <em>{{ search }}</em>.
</template>
<em style="opacity: 0.5;" v-else>Start typing to search for a country.</em>
</template>
</v-select>
</template>
<script>
export default {
name: 'BetterNoOptions',
};
</script>
+13
View File
@@ -120,6 +120,19 @@ clearSearchOnSelect: {
}, },
``` ```
## clearSearchOnBlur
Enables/disables clearing the search text when the search input is blurred.
```js
clearSearchOnBlur: {
type: Function,
default: function ({ clearSearchOnSelect, multiple }) {
return clearSearchOnSelect && !multiple
}
},
```
## closeOnSelect ## closeOnSelect
Close a dropdown when an option is chosen. Set to false to keep the dropdown Close a dropdown when an option is chosen. Set to false to keep the dropdown
+72 -19
View File
@@ -7,13 +7,16 @@ Slots can be used to change the look and feel of the UI, or to simply swap out t
### `selected-option` ### `selected-option`
#### Scope: #### Scope:
- `option {Object}` - A selected option - `option {Object}` - A selected option
```html ```html
<slot name="selected-option" v-bind="(typeof option === 'object')?option:{[label]: option}"> <slot
{{ getOptionLabel(option) }} name="selected-option"
v-bind="(typeof option === 'object')?option:{[label]: option}"
>
{{ getOptionLabel(option) }}
</slot> </slot>
``` ```
@@ -27,16 +30,32 @@ Slots can be used to change the look and feel of the UI, or to simply swap out t
- `multiple {Boolean}` - If the component supports the selection of multiple values - `multiple {Boolean}` - If the component supports the selection of multiple values
```html ```html
<slot v-for="option in valueAsArray" name="selected-option-container" <slot
:option="(typeof option === 'object')?option:{[label]: option}" :deselect="deselect" :multiple="multiple" :disabled="disabled"> v-for="option in valueAsArray"
<span class="selected-tag" v-bind:key="option.index"> name="selected-option-container"
<slot name="selected-option" v-bind="(typeof option === 'object')?option:{[label]: option}"> :option="(typeof option === 'object')?option:{[label]: option}"
{{ getOptionLabel(option) }} :deselect="deselect"
</slot> :multiple="multiple"
<button v-if="multiple" :disabled="disabled" @click="deselect(option)" type="button" class="close" aria-label="Remove option"> :disabled="disabled"
<span aria-hidden="true">&times;</span> >
</button> <span class="selected-tag" v-bind:key="option.index">
</span> <slot
name="selected-option"
v-bind="(typeof option === 'object')?option:{[label]: option}"
>
{{ getOptionLabel(option) }}
</slot>
<button
v-if="multiple"
:disabled="disabled"
@click="deselect(option)"
type="button"
class="close"
aria-label="Remove option"
>
<span aria-hidden="true">&times;</span>
</button>
</span>
</slot> </slot>
``` ```
@@ -44,9 +63,29 @@ Slots can be used to change the look and feel of the UI, or to simply swap out t
### `spinner` ### `spinner`
#### Scope:
- `loading {Boolean}` - if the component is in a loading state
```html ```html
<slot name="spinner"> <slot name="spinner" v-bind="scope.spinner">
<div class="spinner" v-show="mutableLoading">Loading...</div> <div class="vs__spinner" v-show="mutableLoading">Loading...</div>
</slot>
```
### `open-indicator`
```js
attributes : {
'ref': 'openIndicator',
'role': 'presentation',
'class': 'vs__open-indicator',
}
```
```vue
<slot name="open-indicator" v-bind="scope.openIndicator">
<component :is="childComponents.OpenIndicator" v-if="!noDrop" v-bind="scope.openIndicator.attributes"/>
</slot> </slot>
``` ```
@@ -54,12 +93,26 @@ Slots can be used to change the look and feel of the UI, or to simply swap out t
### `option` ### `option`
#### Scope:
- `option {Object}` - The currently iterated option from `filteredOptions` - `option {Object}` - The currently iterated option from `filteredOptions`
```html ```html
<slot name="option" v-bind="(typeof option === 'object')?option:{[label]: option}"> <slot
{{ getOptionLabel(option) }} name="option"
v-bind="(typeof option === 'object')?option:{[label]: option}"
>
{{ getOptionLabel(option) }}
</slot>
```
### `no-options`
The no options slot is displayed in the dropdown when `filteredOptions.length === 0`.
- `search {String}` - the current search text
- `searching {Boolean}` - if the component has search text
```vue
<slot name="no-options" v-bind="scope.noOptions">
Sorry, no matching options.
</slot> </slot>
``` ```
+18 -8
View File
@@ -1,22 +1,32 @@
::: tip 🚧 ::: tip 🚧
This section of the guide is a work in progress! Check back soon for an update. This section of the guide is a work in progress! Check back soon for an update.
Vue Select currently offers quite a few scoped slots, and you can check out the Vue Select currently offers quite a few scoped slots, and you can check out the
[API Docs for Slots](../api/slots.md) in the meantime while a good guide is put together. [API Docs for Slots](../api/slots.md) in the meantime while a good guide is put together.
::: :::
#### Scoped Slot `option` ### Scoped Slot `option`
vue-select provides the scoped `option` slot in order to create custom dropdown templates. vue-select provides the scoped `option` slot in order to create custom dropdown templates.
```html ```html
<v-select :options="options" label="title"> <v-select :options="options" label="title">
<template v-slot:option="option"> <template v-slot:option="option">
<span :class="option.icon"></span> <span :class="option.icon"></span>
{{ option.title }} {{ option.title }}
</template> </template>
</v-select> </v-select>
``` ```
Using the `option` slot with props `"option"` provides the current option variable to the template. Using the `option` slot with props `"option"` provides the current option variable to the template.
<CodePen url="NXBwYG" height="500"/> <CodePen url="NXBwYG" height="500"/>
### Improving the default `no-options` text
The `no-options` slot is displayed in the dropdown when `filteredOptions === 0`. By default, it
displays _Sorry, no matching options_. You can add more contextual information by using the slot
in your own apps.
<BetterNoOptions />
<<< @/.vuepress/components/BetterNoOptions.vue
+30 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "vue-select", "name": "vue-select",
"version": "3.4.0", "version": "3.6.0",
"description": "Everything you wish the HTML <select> element could do, wrapped up into a lightweight, extensible Vue component.", "description": "Everything you wish the HTML <select> element could do, wrapped up into a lightweight, extensible Vue component.",
"author": "Jeff Sagal <sagalbot@gmail.com>", "author": "Jeff Sagal <sagalbot@gmail.com>",
"homepage": "https://vue-select.org", "homepage": "https://vue-select.org",
@@ -18,7 +18,9 @@
"build": "cross-env NODE_ENV=production webpack --config build/webpack.prod.conf.js --progress", "build": "cross-env NODE_ENV=production webpack --config build/webpack.prod.conf.js --progress",
"build:docs": "cd docs && yarn build", "build:docs": "cd docs && yarn build",
"build:preview": "cd docs && yarn build", "build:preview": "cd docs && yarn build",
"test": "jest" "test": "jest",
"semantic-release": "semantic-release",
"commit": "git-cz"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -35,16 +37,20 @@
"@babel/plugin-transform-runtime": "^7.4.0", "@babel/plugin-transform-runtime": "^7.4.0",
"@babel/preset-env": "^7.4.2", "@babel/preset-env": "^7.4.2",
"@babel/runtime": "^7.4.2", "@babel/runtime": "^7.4.2",
"@vue/test-utils": "^1.0.0-beta.29", "@semantic-release/git": "^9.0.0",
"@semantic-release/github": "^7.0.4",
"@vue/test-utils": "^1.0.0-beta.31",
"autoprefixer": "^9.4.7", "autoprefixer": "^9.4.7",
"babel-core": "^7.0.0-bridge.0", "babel-core": "^7.0.0-bridge.0",
"babel-loader": "^8.0.5", "babel-loader": "^8.0.5",
"bundlewatch": "^0.2.5", "bundlewatch": "^0.2.5",
"chokidar": "^2.1.5", "chokidar": "^2.1.5",
"commitizen": "^4.0.3",
"coveralls": "^3.0.2", "coveralls": "^3.0.2",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"css-loader": "^2.1.0", "css-loader": "^2.1.0",
"cssnano": "^4.1.10", "cssnano": "^4.1.10",
"cz-conventional-changelog": "3.1.0",
"html-loader": "^0.5.5", "html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"jest": "^24.1.0", "jest": "^24.1.0",
@@ -55,6 +61,7 @@
"postcss-loader": "^3.0.0", "postcss-loader": "^3.0.0",
"postcss-scss": "^2.0.0", "postcss-scss": "^2.0.0",
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",
"semantic-release": "^17.0.4",
"terser-webpack-plugin": "^1.2.3", "terser-webpack-plugin": "^1.2.3",
"url-loader": "^1.1.2", "url-loader": "^1.1.2",
"vue": "^2.6.10", "vue": "^2.6.10",
@@ -99,8 +106,26 @@
"!**/node_modules/**" "!**/node_modules/**"
], ],
"coverageReporters": [ "coverageReporters": [
"html", "text"
"text-summary" ]
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
},
"bundlewatch": {
"files": [
{
"path": "./dist/vue-select.js",
"compression": "none",
"maxSize": "21 KB"
},
{
"path": "./dist/vue-select.css",
"compression": "none",
"maxSize": "6 KB"
}
] ]
} }
} }
+20
View File
@@ -0,0 +1,20 @@
module.exports = {
release: {
branch: "master"
},
plugins: [
"@semantic-release/npm",
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
["@semantic-release/github", {
"assets": ["dist/**"]
}],
[
"@semantic-release/git",
{
assets: ["package.json"],
message: "chore(🚀): ${nextRelease.version}"
}
]
]
};
+62 -33
View File
@@ -3,8 +3,21 @@
</style> </style>
<template> <template>
<button :tabindex="tabindex" type="button" @keydown="keypressWhileToggleIsFocused" ref="toggle" @mousedown.prevent="maybeToggleDropdown" class="v-select vs__dropdown-toggle" :dir="dir" :class="stateClasses"> <button
type="button"
ref="toggle"
:dir="dir"
role="combobox"
:id="`vs${uid}__combobox`"
:tabindex="tabindex"
:aria-expanded="dropdownOpen.toString()"
:aria-owns="`vs${uid}__listbox`"
aria-label="Search for option"
class="v-select vs__dropdown-toggle"
:class="stateClasses"
@keydown="keypressWhileToggleIsFocused"
@mousedown.prevent="maybeToggleDropdown"
>
<div class="vs__selected-options" ref="selectedOptions"> <div class="vs__selected-options" ref="selectedOptions">
<slot v-for="option in selectedValue" <slot v-for="option in selectedValue"
name="selected-option-container" name="selected-option-container"
@@ -16,7 +29,7 @@
<slot name="selected-option" v-bind="normalizeOptionForSlot(option)"> <slot name="selected-option" v-bind="normalizeOptionForSlot(option)">
{{ getOptionLabel(option) }} {{ getOptionLabel(option) }}
</slot> </slot>
<button v-if="multiple" :disabled="disabled" @click="deselect(option)" type="button" class="vs__deselect" aria-label="Deselect option" ref="deselectButtons"> <button v-if="multiple" :disabled="disabled" @click="deselect(option)" type="button" class="vs__deselect" :title="`Deselect ${getOptionLabel(option)}`" :aria-label="`Deselect ${getOptionLabel(option)}`" ref="deselectButtons">
<component :is="childComponents.Deselect" /> <component :is="childComponents.Deselect" />
</button> </button>
</span> </span>
@@ -34,7 +47,8 @@
@click="clearSelection" @click="clearSelection"
type="button" type="button"
class="vs__clear" class="vs__clear"
title="Clear selection" title="Clear Selected"
aria-label="Clear Selected"
ref="clearButton" ref="clearButton"
> >
<component :is="childComponents.Deselect" /> <component :is="childComponents.Deselect" />
@@ -49,23 +63,27 @@
</slot> </slot>
</div> </div>
<transition :name="transition"> <transition :name="transition">
<ul ref="dropdownMenu" v-if="dropdownOpen" class="vs__dropdown-menu" role="listbox" @mousedown.prevent="onMousedown" @mouseup="onMouseUp"> <ul ref="dropdownMenu" v-show="dropdownOpen" :id="`vs${uid}__listbox`" class="vs__dropdown-menu" role="listbox" @mousedown.prevent="onMousedown" @mouseup="onMouseUp">
<li <template v-if="dropdownOpen">
role="option" <li
v-for="(option, index) in filteredOptions" role="option"
:key="getOptionKey(option)" v-for="(option, index) in filteredOptions"
class="vs__dropdown-option" :key="getOptionKey(option)"
:class="{ 'vs__dropdown-option--selected': isOptionSelected(option), 'vs__dropdown-option--highlight': index === typeAheadPointer, 'vs__dropdown-option--disabled': !selectable(option) }" :id="`vs${uid}__option-${index}`"
@mouseover="selectable(option) ? typeAheadPointer = index : null" class="vs__dropdown-option"
@mousedown.prevent.stop="selectable(option) ? select(option) : null" :class="{ 'vs__dropdown-option--selected': isOptionSelected(option), 'vs__dropdown-option--highlight': index === typeAheadPointer, 'vs__dropdown-option--disabled': !selectable(option) }"
> :aria-selected="index === typeAheadPointer ? true : null"
<slot name="option" v-bind="normalizeOptionForSlot(option)"> @mouseover="selectable(option) ? typeAheadPointer = index : null"
{{ getOptionLabel(option) }} @mousedown.prevent.stop="selectable(option) ? select(option) : null"
</slot> >
</li> <slot name="option" v-bind="normalizeOptionForSlot(option)">
<li v-if="!filteredOptions.length" class="vs__no-options" @mousedown.stop=""> {{ getOptionLabel(option) }}
<slot name="no-options">Sorry, no matching options.</slot> </slot>
</li> </li>
<li v-if="filteredOptions.length === 0" class="vs__no-options" @mousedown.stop="">
<slot name="no-options" v-bind="scope.noOptions">Sorry, no matching options.</slot>
</li>
</template>
</ul> </ul>
</transition> </transition>
</button> </button>
@@ -77,6 +95,7 @@
import typeAheadPointer from '../mixins/typeAheadPointer' import typeAheadPointer from '../mixins/typeAheadPointer'
import ajax from '../mixins/ajax' import ajax from '../mixins/ajax'
import childComponents from './childComponents'; import childComponents from './childComponents';
import uniqueId from '../utility/uniqueId';
/** /**
* @name VueSelect * @name VueSelect
@@ -423,6 +442,17 @@
validator: (value) => ['function', 'boolean'].includes(typeof value) validator: (value) => ['function', 'boolean'].includes(typeof value)
}, },
/**
* If search text should clear on blur
* @return {Boolean} True when single and clearSearchOnSelect
*/
clearSearchOnBlur: {
type: Function,
default: function ({ clearSearchOnSelect, multiple }) {
return clearSearchOnSelect && !multiple
}
},
/** /**
* Disable the dropdown entirely. * Disable the dropdown entirely.
* @type {Boolean} * @type {Boolean}
@@ -503,6 +533,7 @@
data() { data() {
return { return {
uid: uniqueId(),
search: '', search: '',
open: false, open: false,
isComposing: false, isComposing: false,
@@ -829,7 +860,8 @@
if (this.mousedown && !this.searching) { if (this.mousedown && !this.searching) {
this.mousedown = false this.mousedown = false
} else { } else {
if (this.clearSearchOnBlur) { const { clearSearchOnSelect, multiple } = this;
if (this.clearSearchOnBlur({ clearSearchOnSelect, multiple })) {
this.search = '' this.search = ''
} }
this.closeSearchOptions(); this.closeSearchOptions();
@@ -1002,10 +1034,11 @@
'tabindex': '-1', 'tabindex': '-1',
'readonly': !this.searchable, 'readonly': !this.searchable,
'id': this.inputId, 'id': this.inputId,
'aria-expanded': this.dropdownOpen, 'aria-autocomplete': 'list',
'aria-label': 'Search for option', 'aria-labelledby': `vs${this.uid}__combobox`,
'aria-controls': `vs${this.uid}__listbox`,
'aria-activedescendant': this.typeAheadPointer > -1 ? `vs${this.uid}__option-${this.typeAheadPointer}` : '',
'ref': 'search', 'ref': 'search',
'role': 'combobox',
'type': 'search', 'type': 'search',
'autocomplete': this.autocomplete, 'autocomplete': this.autocomplete,
'value': this.search, 'value': this.search,
@@ -1022,6 +1055,10 @@
spinner: { spinner: {
loading: this.mutableLoading loading: this.mutableLoading
}, },
noOptions: {
search: this.search,
searching: this.searching,
},
openIndicator: { openIndicator: {
attributes: { attributes: {
'ref': 'openIndicator', 'ref': 'openIndicator',
@@ -1062,14 +1099,6 @@
} }
}, },
/**
* If search text should clear on blur
* @return {Boolean} True when single and clearSearchOnSelect
*/
clearSearchOnBlur() {
return this.clearSearchOnSelect && !this.multiple
},
/** /**
* Return the current state of the * Return the current state of the
* search input * search input
+1 -1
View File
@@ -30,7 +30,7 @@ export default {
*/ */
pixelsToPointerTop() { pixelsToPointerTop() {
let pixelsToPointerTop = 0; let pixelsToPointerTop = 0;
if (this.$refs.dropdownMenu) { if (this.$refs.dropdownMenu && this.dropdownOpen) {
for (let i = 0; i < this.typeAheadPointer; i++) { for (let i = 0; i < this.typeAheadPointer; i++) {
pixelsToPointerTop += this.$refs.dropdownMenu.children[i] pixelsToPointerTop += this.$refs.dropdownMenu.children[i]
.offsetHeight; .offsetHeight;
+1
View File
@@ -23,6 +23,7 @@ $transition-duration: .15s;
/* Dropdown Default Transition */ /* Dropdown Default Transition */
.vs__fade-enter-active, .vs__fade-enter-active,
.vs__fade-leave-active { .vs__fade-leave-active {
pointer-events: none;
transition: opacity $transition-duration $transition-timing-function; transition: opacity $transition-duration $transition-timing-function;
} }
.vs__fade-enter, .vs__fade-enter,
+12
View File
@@ -0,0 +1,12 @@
let idCount = 0;
/**
* Dead simple unique ID implementation.
* Thanks lodash!
* @return {number}
*/
function uniqueId() {
return ++idCount;
}
export default uniqueId;
+10 -4
View File
@@ -13,10 +13,11 @@ describe("Asynchronous Loading", () => {
expect(Select.vm.mutableLoading).toEqual(true); expect(Select.vm.mutableLoading).toEqual(true);
}); });
it("should trigger the search event when the search text changes", () => { it("should trigger the search event when the search text changes", async () => {
const Select = selectWithProps(); const Select = selectWithProps();
Select.vm.search = "foo"; Select.vm.search = "foo";
await Select.vm.$nextTick();
const events = Select.emitted("search"); const events = Select.emitted("search");
@@ -24,11 +25,13 @@ describe("Asynchronous Loading", () => {
expect(events.length).toEqual(1); expect(events.length).toEqual(1);
}); });
it("should trigger the search event if the search text is empty", () => { it("should trigger the search event if the search text is empty", async () => {
const Select = selectWithProps(); const Select = selectWithProps();
Select.vm.search = "foo"; Select.vm.search = "foo";
await Select.vm.$nextTick();
Select.vm.search = ""; Select.vm.search = "";
await Select.vm.$nextTick();
const events = Select.emitted("search"); const events = Select.emitted("search");
@@ -36,7 +39,7 @@ describe("Asynchronous Loading", () => {
expect(events.length).toEqual(2); expect(events.length).toEqual(2);
}); });
it("can set loading to false from the @search event callback", () => { it("can set loading to false from the @search event callback", async () => {
const Select = shallowMount(vSelect, { const Select = shallowMount(vSelect, {
listeners: { listeners: {
search: (search, loading) => { search: (search, loading) => {
@@ -47,13 +50,16 @@ describe("Asynchronous Loading", () => {
Select.vm.mutableLoading = true; Select.vm.mutableLoading = true;
Select.vm.search = 'foo'; Select.vm.search = 'foo';
await Select.vm.$nextTick();
expect(Select.vm.mutableLoading).toEqual(false); expect(Select.vm.mutableLoading).toEqual(false);
}); });
it("will sync mutable loading with the loading prop", () => { it('will sync mutable loading with the loading prop', async () => {
const Select = selectWithProps({ loading: false }); const Select = selectWithProps({ loading: false });
Select.setProps({ loading: true }); Select.setProps({ loading: true });
await Select.vm.$nextTick();
expect(Select.vm.mutableLoading).toEqual(true); expect(Select.vm.mutableLoading).toEqual(true);
}); });
}); });
+2 -1
View File
@@ -1,9 +1,10 @@
import { selectWithProps } from "../helpers"; import { selectWithProps } from "../helpers";
describe("Removing values", () => { describe("Removing values", () => {
it("can remove the given tag when its close icon is clicked", () => { it("can remove the given tag when its close icon is clicked", async () => {
const Select = selectWithProps({ multiple: true }); const Select = selectWithProps({ multiple: true });
Select.vm.$data._value = 'one'; Select.vm.$data._value = 'one';
await Select.vm.$nextTick();
Select.find(".vs__deselect").trigger("click"); Select.find(".vs__deselect").trigger("click");
expect(Select.emitted().input).toEqual([[[]]]); expect(Select.emitted().input).toEqual([[[]]]);
+7 -3
View File
@@ -2,7 +2,7 @@ import { mountDefault, selectWithProps } from '../helpers';
import OpenIndicator from "../../src/components/OpenIndicator"; import OpenIndicator from "../../src/components/OpenIndicator";
describe("Toggling Dropdown", () => { describe("Toggling Dropdown", () => {
fdescribe('focusing on the button wrapper', () => { describe('focusing on the button wrapper', () => {
test('when the search is focused buttonIsFocused is false', () => { test('when the search is focused buttonIsFocused is false', () => {
const Select = mountDefault(); const Select = mountDefault();
Select.vm.$refs.search.focus(); Select.vm.$refs.search.focus();
@@ -144,14 +144,18 @@ describe("Toggling Dropdown", () => {
expect(Select.vm.stateClasses['vs--open']).toEqual(true); expect(Select.vm.stateClasses['vs--open']).toEqual(true);
}); });
it("should not display the dropdown if noDrop is true", () => { it("should not display the dropdown if noDrop is true", async () => {
const Select = selectWithProps({ const Select = selectWithProps({
noDrop: true, noDrop: true,
}); });
Select.vm.maybeToggleDropdown({ target: Select.vm.$refs.search }); Select.vm.maybeToggleDropdown({ target: Select.vm.$refs.search });
expect(Select.vm.open).toEqual(true); expect(Select.vm.open).toEqual(true);
expect(Select.contains('.vs__dropdown-menu')).toBeFalsy(); await Select.vm.$nextTick();
expect(Select.find('.vs__dropdown-menu').element.style['display']).toEqual('none');
expect(Select.contains('.vs__dropdown-option')).toBeFalsy();
expect(Select.contains('.vs__no-options')).toBeFalsy();
expect(Select.vm.stateClasses['vs--open']).toBeFalsy(); expect(Select.vm.stateClasses['vs--open']).toBeFalsy();
}); });
+3 -1
View File
@@ -12,13 +12,15 @@ describe("Labels", () => {
expect(Select.find(".vs__selected").text()).toBe("Foo"); expect(Select.find(".vs__selected").text()).toBe("Foo");
}); });
it("will console.warn when options contain objects without a valid label key", () => { it("will console.warn when options contain objects without a valid label key", async () => {
const spy = jest.spyOn(console, "warn").mockImplementation(() => {}); const spy = jest.spyOn(console, "warn").mockImplementation(() => {});
const Select = selectWithProps({ const Select = selectWithProps({
options: [{}] options: [{}]
}); });
Select.vm.open = true; Select.vm.open = true;
await Select.vm.$nextTick();
expect(spy).toHaveBeenCalledWith( expect(spy).toHaveBeenCalledWith(
'[vue-select warn]: Label key "option.label" does not exist in options object {}.' + '[vue-select warn]: Label key "option.label" does not exist in options object {}.' +
"\nhttps://vue-select.org/api/props.html#getoptionlabel" "\nhttps://vue-select.org/api/props.html#getoptionlabel"
+42 -4
View File
@@ -31,20 +31,21 @@ describe("Reset on options change", () => {
expect(spy.mock.calls[3][0]).toContain('Invalid prop: custom validator check failed for prop "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', () => { it('should receive the new options, old options, and current value', async () => {
let resetOnOptionsChange = jest.fn(option => option); let resetOnOptionsChange = jest.fn(option => option);
const Select = mountDefault( const Select = mountDefault(
{resetOnOptionsChange, options: ['bear'], value: 'selected'}, {resetOnOptionsChange, options: ['bear'], value: 'selected'},
); );
Select.setProps({options: ['lake', 'kite']}); Select.setProps({options: ['lake', 'kite']});
await Select.vm.$nextTick();
expect(resetOnOptionsChange).toHaveBeenCalledTimes(1); expect(resetOnOptionsChange).toHaveBeenCalledTimes(1);
expect(resetOnOptionsChange) expect(resetOnOptionsChange)
.toHaveBeenCalledWith(['lake', 'kite'], ['bear'], ['selected']); .toHaveBeenCalledWith(['lake', 'kite'], ['bear'], ['selected']);
}); });
it('should allow resetOnOptionsChange to be a function that returns true', () => { it('should allow resetOnOptionsChange to be a function that returns true', async () => {
let resetOnOptionsChange = () => true; let resetOnOptionsChange = () => true;
const Select = shallowMount(VueSelect, { const Select = shallowMount(VueSelect, {
propsData: {resetOnOptionsChange, options: ['one'], value: 'one'}, propsData: {resetOnOptionsChange, options: ['one'], value: 'one'},
@@ -52,6 +53,8 @@ describe("Reset on options change", () => {
const spy = jest.spyOn(Select.vm, 'clearSelection'); const spy = jest.spyOn(Select.vm, 'clearSelection');
Select.setProps({options: ['one', 'two']}); Select.setProps({options: ['one', 'two']});
await Select.vm.$nextTick();
expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledTimes(1);
}); });
@@ -74,14 +77,18 @@ describe("Reset on options change", () => {
const spy = jest.spyOn(Select.vm, 'clearSelection'); const spy = jest.spyOn(Select.vm, 'clearSelection');
Select.setProps({options: ['one', 'two']}); Select.setProps({options: ['one', 'two']});
await Select.vm.$nextTick();
expect(Select.vm.selectedValue).toEqual(['one']); expect(Select.vm.selectedValue).toEqual(['one']);
Select.setProps({options: ['two']}); Select.setProps({options: ['two']});
await Select.vm.$nextTick();
expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledTimes(1);
}); });
}); });
it("should reset the selected value when the options property changes", () => { it("should reset the selected value when the options property changes", async () => {
const Select = shallowMount(VueSelect, { const Select = shallowMount(VueSelect, {
propsData: { resetOnOptionsChange: true, options: ["one"] } propsData: { resetOnOptionsChange: true, options: ["one"] }
}); });
@@ -89,15 +96,46 @@ describe("Reset on options change", () => {
Select.vm.$data._value = 'one'; Select.vm.$data._value = 'one';
Select.setProps({options: ["four", "five", "six"]}); Select.setProps({options: ["four", "five", "six"]});
await Select.vm.$nextTick();
expect(Select.vm.selectedValue).toEqual([]); expect(Select.vm.selectedValue).toEqual([]);
}); });
it("should return correct selected value when the options property changes and a new option matches", () => { it("should return correct selected value when the options property changes and a new option matches", async () => {
const Select = shallowMount(VueSelect, { const Select = shallowMount(VueSelect, {
propsData: { value: "one", options: [], reduce(option) { return option.value } } propsData: { value: "one", options: [], reduce(option) { return option.value } }
}); });
Select.setProps({options: [{ label: "oneLabel", value: "one" }]}); Select.setProps({options: [{ label: "oneLabel", value: "one" }]});
await Select.vm.$nextTick();
expect(Select.vm.selectedValue).toEqual([{ label: "oneLabel", value: "one" }]); expect(Select.vm.selectedValue).toEqual([{ label: "oneLabel", value: "one" }]);
}); });
it('clearSearchOnBlur returns false when multiple is true', () => {
const Select = mountDefault({});
let clearSearchOnBlur = jest.spyOn(Select.vm, 'clearSearchOnBlur');
Select.find({ref: 'search'}).trigger('click');
Select.setData({search: 'one'});
Select.find({ref: 'search'}).trigger('blur');
expect(clearSearchOnBlur).toHaveBeenCalledTimes(1);
expect(clearSearchOnBlur).toHaveBeenCalledWith({
clearSearchOnSelect: true,
multiple: false,
});
expect(Select.vm.search).toBe('');
});
it('clearSearchOnBlur accepts a function', () => {
let clearSearchOnBlur = jest.fn(() => false);
const Select = mountDefault({clearSearchOnBlur});
Select.find({ref: 'search'}).trigger('click');
Select.setData({search: 'one'});
Select.find({ref: 'search'}).trigger('blur');
expect(clearSearchOnBlur).toHaveBeenCalledTimes(1);
expect(Select.vm.search).toBe('one');
});
}); });
+2 -1
View File
@@ -211,7 +211,7 @@ describe("When reduce prop is defined", () => {
}); });
it("reacts correctly when value property changes", () => { it("reacts correctly when value property changes", async () => {
const optionToChangeTo = { id: 1, label: "Foo" }; const optionToChangeTo = { id: 1, label: "Foo" };
const Select = shallowMount(VueSelect, { const Select = shallowMount(VueSelect, {
propsData: { propsData: {
@@ -222,6 +222,7 @@ describe("When reduce prop is defined", () => {
}); });
Select.setProps({ value: optionToChangeTo.id }); Select.setProps({ value: optionToChangeTo.id });
await Select.vm.$nextTick();
expect(Select.vm.selectedValue).toEqual([optionToChangeTo]); expect(Select.vm.selectedValue).toEqual([optionToChangeTo]);
}); });
+8 -2
View File
@@ -1,27 +1,33 @@
import { selectWithProps } from "../helpers"; import { selectWithProps } from "../helpers";
describe("Selectable prop", () => { describe("Selectable prop", () => {
it("should select selectable option if clicked", () => { it("should select selectable option if clicked", async () => {
const Select = selectWithProps({ const Select = selectWithProps({
options: ["one", "two", "three"], options: ["one", "two", "three"],
selectable: (option) => option === "one" selectable: (option) => option === "one"
}); });
Select.vm.$data.open = true; Select.vm.$data.open = true;
await Select.vm.$nextTick();
Select.find(".vs__dropdown-menu li:first-child").trigger("mousedown"); Select.find(".vs__dropdown-menu li:first-child").trigger("mousedown");
await Select.vm.$nextTick();
expect(Select.vm.selectedValue).toEqual(["one"]); expect(Select.vm.selectedValue).toEqual(["one"]);
}) })
it("should not select not selectable option if clicked", () => { it("should not select not selectable option if clicked", async () => {
const Select = selectWithProps({ const Select = selectWithProps({
options: ["one", "two", "three"], options: ["one", "two", "three"],
selectable: (option) => option === "one" selectable: (option) => option === "one"
}); });
Select.vm.$data.open = true; Select.vm.$data.open = true;
await Select.vm.$nextTick();
Select.find(".vs__dropdown-menu li:last-child").trigger("mousedown"); Select.find(".vs__dropdown-menu li:last-child").trigger("mousedown");
await Select.vm.$nextTick();
expect(Select.vm.selectedValue).toEqual([]); expect(Select.vm.selectedValue).toEqual([]);
}); });
+18 -1
View File
@@ -42,7 +42,7 @@ describe('Scoped Slots', () => {
}); });
it('receives an option object to the option slot in the dropdown menu', it('receives an option object to the option slot in the dropdown menu',
() => { async () => {
const Select = mountDefault( const Select = mountDefault(
{value: 'one'}, {value: 'one'},
{ {
@@ -52,7 +52,24 @@ describe('Scoped Slots', () => {
}); });
Select.vm.open = true; Select.vm.open = true;
await Select.vm.$nextTick();
expect(Select.find({ref: 'dropdownMenu'}).text()).toEqual('onetwothree'); expect(Select.find({ref: 'dropdownMenu'}).text()).toEqual('onetwothree');
}); });
it('noOptions slot receives the current search text', async () => {
const noOptions = jest.fn();
const Select = mountDefault({}, {
scopedSlots: {'no-options': noOptions},
});
Select.vm.search = 'something not there';
Select.vm.open = true;
await Select.vm.$nextTick();
expect(noOptions).toHaveBeenCalledWith({
search: 'something not there',
searching: true,
})
});
}); });
+4 -2
View File
@@ -153,7 +153,7 @@ describe("When Tagging Is Enabled", () => {
expect(Select.vm.selectedValue).toEqual([two]); expect(Select.vm.selectedValue).toEqual([two]);
}); });
it("should select an existing option if the search string matches an objects label from options", () => { it("should select an existing option if the search string matches an objects label from options", async () => {
let two = { label: "two" }; let two = { label: "two" };
const Select = selectWithProps({ const Select = selectWithProps({
taggable: true, taggable: true,
@@ -161,12 +161,13 @@ describe("When Tagging Is Enabled", () => {
}); });
Select.vm.search = "two"; Select.vm.search = "two";
await Select.vm.$nextTick();
searchSubmit(Select); searchSubmit(Select);
expect(Select.vm.selectedValue).toEqual([two]); expect(Select.vm.selectedValue).toEqual([two]);
}); });
it("should select an existing option if the search string matches an objects label from options when filter-options is false", () => { it("should select an existing option if the search string matches an objects label from options when filter-options is false", async () => {
let two = { label: "two" }; let two = { label: "two" };
const Select = selectWithProps({ const Select = selectWithProps({
taggable: true, taggable: true,
@@ -175,6 +176,7 @@ describe("When Tagging Is Enabled", () => {
}); });
Select.vm.search = "two"; Select.vm.search = "two";
await Select.vm.$nextTick();
searchSubmit(Select); searchSubmit(Select);
expect(Select.vm.selectedValue).toEqual([two]); expect(Select.vm.selectedValue).toEqual([two]);
+2 -1
View File
@@ -120,11 +120,12 @@ describe("Moving the Typeahead Pointer", () => {
}); });
describe("Measuring pixel distances", () => { describe("Measuring pixel distances", () => {
it("should calculate pointerHeight as the offsetHeight of the pointer element if it exists", () => { it("should calculate pointerHeight as the offsetHeight of the pointer element if it exists", async () => {
const Select = mountDefault(); const Select = mountDefault();
// Drop down must be open for $refs to exist // Drop down must be open for $refs to exist
Select.vm.open = true; Select.vm.open = true;
await Select.vm.$nextTick();
/** /**
* Since JSDom doesn't render layouts, set the offsetHeight explicitly * Since JSDom doesn't render layouts, set the offsetHeight explicitly
+5
View File
@@ -0,0 +1,5 @@
import uniqueId from '../../../src/utility/uniqueId';
test('it generates a unique number', () => {
expect(uniqueId()).not.toEqual(uniqueId());
});
+4331 -1174
View File
File diff suppressed because it is too large Load Diff