mirror of
https://github.com/tenrok/vue-select.git
synced 2026-06-22 10:30:34 +03:00
Merge branch 'master' into customizable-text
This commit is contained in:
@@ -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!
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
github: [sagalbot]
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
*
|
||||||
|
!src/**/*
|
||||||
|
!dist/**/*
|
||||||
|
.DS_Store
|
||||||
@@ -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)
|
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<v-select
|
||||||
|
multiple
|
||||||
|
placeholder="Choose up to 3 books!"
|
||||||
|
label="title"
|
||||||
|
v-model="selected"
|
||||||
|
:options="books"
|
||||||
|
:selectable="() => selected.length < 3"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import books from '../data/books';
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return { selected: [] }
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
books: () => books,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<v-select
|
||||||
|
placeholder="Choose a book to read"
|
||||||
|
label="title"
|
||||||
|
:options="books"
|
||||||
|
:selectable="option => ! option.author.lastName.includes('Woodhouse')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import books from '../data/books';
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
books: () => books,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -118,6 +118,7 @@ module.exports = {
|
|||||||
collapsable: false,
|
collapsable: false,
|
||||||
children: [
|
children: [
|
||||||
['guide/validation', 'Validation'],
|
['guide/validation', 'Validation'],
|
||||||
|
['guide/selectable', 'Limiting Selections'],
|
||||||
['guide/vuex', 'Vuex'],
|
['guide/vuex', 'Vuex'],
|
||||||
['guide/ajax', 'AJAX'],
|
['guide/ajax', 'AJAX'],
|
||||||
['guide/loops', 'Using in Loops'],
|
['guide/loops', 'Using in Loops'],
|
||||||
|
|||||||
+26
-3
@@ -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
|
||||||
@@ -339,12 +352,22 @@ createOption: {
|
|||||||
|
|
||||||
## resetOnOptionsChange
|
## resetOnOptionsChange
|
||||||
|
|
||||||
When false, updating the options will not reset the select value
|
When false, updating the options will not reset the selected value.
|
||||||
|
|
||||||
|
Since `v3.4+` the prop accepts either a `boolean` or `function` that returns a `boolean`.
|
||||||
|
|
||||||
|
If defined as a function, it will receive the params listed below.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
/**
|
||||||
|
* @type {Boolean|Function}
|
||||||
|
* @param {Array} newOptions
|
||||||
|
* @param {Array} oldOptions
|
||||||
|
* @param {Array} selectedValue
|
||||||
|
*/
|
||||||
resetOnOptionsChange: {
|
resetOnOptionsChange: {
|
||||||
type: Boolean,
|
default: false,
|
||||||
default: false
|
validator: (value) => ['function', 'boolean'].includes(typeof value)
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+72
-19
@@ -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">×</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">×</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>
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ Vue Select aims to follow the WAI-ARIA best practices for the
|
|||||||
[combobox](https://www.w3.org/TR/wai-aria-practices-1.1/#combobox) and
|
[combobox](https://www.w3.org/TR/wai-aria-practices-1.1/#combobox) and
|
||||||
[listbox](https://www.w3.org/TR/wai-aria-practices-1.1/#Listbox) widgets.
|
[listbox](https://www.w3.org/TR/wai-aria-practices-1.1/#Listbox) widgets.
|
||||||
|
|
||||||
The UX of the component isdesigned around the HTML `<select>` element, while following the WAI-ARIA
|
The UX of the component is designed around the HTML `<select>` element, while following the WAI-ARIA
|
||||||
specifications and best practices for creating accessible components.
|
specifications and best practices for creating accessible components.
|
||||||
|
|
||||||
## Combobox
|
## Combobox
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
## Selectable Prop <Badge text="v3.3.0+" />
|
||||||
|
|
||||||
|
The `selectable` prop determines if an option is selectable or not. If `selectable` returns false
|
||||||
|
for a given option, it will be displayed with a `vs__dropdown-option--disabled` class. The option
|
||||||
|
will be disabled and unable to be selected.
|
||||||
|
|
||||||
|
```js
|
||||||
|
selectable: {
|
||||||
|
type: Function,
|
||||||
|
/**
|
||||||
|
* @param {Object|String} option
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
default: option => true,
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
Here `selectable` is used to prevent books by a certain author from being chosen. In this case,
|
||||||
|
the options passed to the component are objects:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
title: "Right Ho Jeeves",
|
||||||
|
author: { firstName: "P.D", lastName: "Woodhouse" },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This object will be passed to `selectable`, so we can check if the author should be selectable or not.
|
||||||
|
|
||||||
|
<UnselectableExample />
|
||||||
|
|
||||||
|
<<< @/.vuepress/components/UnselectableExample.vue{6}
|
||||||
|
|
||||||
|
## Limiting the Number of Selections
|
||||||
|
|
||||||
|
`selectable` can also be used a bit more creatively to limit the number selections that can be made
|
||||||
|
within the component. In this case, the user can select any author, but may only select a maximum
|
||||||
|
of three books.
|
||||||
|
|
||||||
|
<LimitSelectionQuantity />
|
||||||
|
|
||||||
|
<<< @/.vuepress/components/LimitSelectionQuantity.vue{8}
|
||||||
+18
-8
@@ -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
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ To allow input that's not present within the options, set the `taggable` prop to
|
|||||||
If you want added tags to be pushed to the options array, set `push-tags` to true.
|
If you want added tags to be pushed to the options array, set `push-tags` to true.
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<v-select taggable multiple />
|
<v-select taggable multiple push-tags />
|
||||||
```
|
```
|
||||||
|
|
||||||
<v-select taggable multiple push-tags />
|
<v-select taggable multiple push-tags />
|
||||||
|
|||||||
+30
-5
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vue-select",
|
"name": "vue-select",
|
||||||
"version": "3.2.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"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
};
|
||||||
+92
-75
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :dir="dir" class="v-select" :class="stateClasses">
|
<div :dir="dir" class="v-select" :class="stateClasses">
|
||||||
<div ref="toggle" @mousedown.prevent="toggleDropdown" class="vs__dropdown-toggle">
|
<div :id="`vs${uid}__combobox`" ref="toggle" @mousedown.prevent="toggleDropdown" class="vs__dropdown-toggle" role="combobox" :aria-expanded="dropdownOpen.toString()" :aria-owns="`vs${uid}__listbox`" :aria-label="i18n.search.ariaLabel">
|
||||||
|
|
||||||
<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"
|
||||||
@@ -17,7 +17,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="i18n.deselect.ariaLabel">
|
<button v-if="multiple" :disabled="disabled" @click="deselect(option)" type="button" class="vs__deselect" :title="i18n.deselectButton.ariaLabel(getOptionLabel(option))" :aria-label="i18n.deselectButton.ariaLabel(getOptionLabel(option))" ref="deselectButtons">
|
||||||
<component :is="childComponents.Deselect" />
|
<component :is="childComponents.Deselect" />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
@@ -28,14 +28,16 @@
|
|||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="vs__actions">
|
<div class="vs__actions" ref="actions">
|
||||||
<button
|
<button
|
||||||
v-show="showClearButton"
|
v-show="showClearButton"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@click="clearSelection"
|
@click="clearSelection"
|
||||||
type="button"
|
type="button"
|
||||||
class="vs__clear"
|
class="vs__clear"
|
||||||
title="Clear selection"
|
title="i18n.clearButton.ariaLabel"
|
||||||
|
aria-label="i18n.clearButton.ariaLabel"
|
||||||
|
ref="clearButton"
|
||||||
>
|
>
|
||||||
<component :is="childComponents.Deselect" />
|
<component :is="childComponents.Deselect" />
|
||||||
</button>
|
</button>
|
||||||
@@ -51,23 +53,27 @@
|
|||||||
</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">{{ i18n.noOptions.text }}</slot>
|
</slot>
|
||||||
</li>
|
</li>
|
||||||
|
<li v-if="filteredOptions.length === 0" class="vs__no-options" @mousedown.stop="">
|
||||||
|
<slot name="no-options" v-bind="scope.noOptions">{{ i18n.noOptions.text }}</slot>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,6 +85,7 @@
|
|||||||
import ajax from '../mixins/ajax'
|
import ajax from '../mixins/ajax'
|
||||||
import i18n from '../mixins/i18n'
|
import i18n from '../mixins/i18n'
|
||||||
import childComponents from './childComponents';
|
import childComponents from './childComponents';
|
||||||
|
import uniqueId from '../utility/uniqueId';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name VueSelect
|
* @name VueSelect
|
||||||
@@ -229,10 +236,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.
|
* are displayed but disabled and cannot be selected.
|
||||||
*
|
*
|
||||||
* @type {Function}
|
* @type {Function}
|
||||||
|
* @since 3.3.0
|
||||||
* @param {Object|String} option
|
* @param {Object|String} option
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
@@ -297,8 +305,7 @@
|
|||||||
`to generate unique key. Please provide'getOptionKey' prop ` +
|
`to generate unique key. Please provide'getOptionKey' prop ` +
|
||||||
`to return a unique key for each option.\n` +
|
`to return a unique key for each option.\n` +
|
||||||
'https://vue-select.org/api/props.html#getoptionkey'
|
'https://vue-select.org/api/props.html#getoptionkey'
|
||||||
)
|
);
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -403,23 +410,37 @@
|
|||||||
*/
|
*/
|
||||||
createOption: {
|
createOption: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default(newOption) {
|
default (option) {
|
||||||
if (typeof this.optionList[0] === 'object') {
|
return (typeof this.optionList[0] === 'object') ? {[this.label]: option} : option;
|
||||||
newOption = {[this.label]: newOption}
|
},
|
||||||
}
|
|
||||||
|
|
||||||
this.$emit('option:created', newOption)
|
|
||||||
return newOption
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When false, updating the options will not reset the select value
|
* When false, updating the options will not reset the selected value. Accepts
|
||||||
* @type {Boolean}
|
* 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: {
|
resetOnOptionsChange: {
|
||||||
type: Boolean,
|
default: false,
|
||||||
default: false
|
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
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -502,6 +523,7 @@
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
uid: uniqueId(),
|
||||||
search: '',
|
search: '',
|
||||||
open: false,
|
open: false,
|
||||||
isComposing: false,
|
isComposing: false,
|
||||||
@@ -518,13 +540,17 @@
|
|||||||
* is correct.
|
* is correct.
|
||||||
* @return {[type]} [description]
|
* @return {[type]} [description]
|
||||||
*/
|
*/
|
||||||
options(val) {
|
options (newOptions, oldOptions) {
|
||||||
if (!this.taggable && this.resetOnOptionsChange) {
|
let shouldReset = () => typeof this.resetOnOptionsChange === 'function'
|
||||||
this.clearSelection()
|
? this.resetOnOptionsChange(newOptions, oldOptions, this.selectedValue)
|
||||||
|
: this.resetOnOptionsChange;
|
||||||
|
|
||||||
|
if (!this.taggable && shouldReset()) {
|
||||||
|
this.clearSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.value && this.isTrackingValues) {
|
if (this.value && this.isTrackingValues) {
|
||||||
this.setInternalValueFromOptions(this.value)
|
this.setInternalValueFromOptions(this.value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -582,7 +608,8 @@
|
|||||||
select(option) {
|
select(option) {
|
||||||
if (!this.isOptionSelected(option)) {
|
if (!this.isOptionSelected(option)) {
|
||||||
if (this.taggable && !this.optionExists(option)) {
|
if (this.taggable && !this.optionExists(option)) {
|
||||||
option = this.createOption(option)
|
option = this.createOption(option);
|
||||||
|
this.$emit('option:created', option);
|
||||||
}
|
}
|
||||||
if (this.multiple) {
|
if (this.multiple) {
|
||||||
option = this.selectedValue.concat(option)
|
option = this.selectedValue.concat(option)
|
||||||
@@ -658,31 +685,23 @@
|
|||||||
* @param {Event} e
|
* @param {Event} e
|
||||||
* @return {void}
|
* @return {void}
|
||||||
*/
|
*/
|
||||||
toggleDropdown (e) {
|
toggleDropdown ({target}) {
|
||||||
const target = e.target;
|
// don't react to click on deselect/clear buttons,
|
||||||
const toggleTargets = [
|
// they dropdown state will be set in their click handlers
|
||||||
this.$el,
|
const ignoredButtons = [
|
||||||
this.searchEl,
|
...(this.$refs['deselectButtons'] || []),
|
||||||
this.$refs.toggle,
|
...([this.$refs['clearButton']] || [])
|
||||||
];
|
];
|
||||||
|
|
||||||
if (typeof this.$refs.openIndicator !== 'undefined') {
|
if (ignoredButtons.some(ref => ref.contains(target) || ref === target)) {
|
||||||
toggleTargets.push(
|
return;
|
||||||
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 (toggleTargets.indexOf(target) > -1 || target.classList.contains('vs__selected')) {
|
if (this.open) {
|
||||||
if (this.open) {
|
this.searchEl.blur();
|
||||||
this.searchEl.blur(); // dropdown will close on blur
|
} else if (!this.disabled) {
|
||||||
} else {
|
this.open = true;
|
||||||
if (!this.disabled) {
|
this.searchEl.focus();
|
||||||
this.open = true;
|
|
||||||
this.searchEl.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -826,7 +845,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()
|
||||||
@@ -973,12 +993,13 @@
|
|||||||
'tabindex': this.tabindex,
|
'tabindex': this.tabindex,
|
||||||
'readonly': !this.searchable,
|
'readonly': !this.searchable,
|
||||||
'id': this.inputId,
|
'id': this.inputId,
|
||||||
'aria-expanded': this.dropdownOpen,
|
'aria-autocomplete': 'list',
|
||||||
'aria-label': this.i18n.search.ariaLabel,
|
'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': 'off',
|
'autocomplete': this.autocomplete,
|
||||||
'value': this.search,
|
'value': this.search,
|
||||||
},
|
},
|
||||||
events: {
|
events: {
|
||||||
@@ -993,6 +1014,10 @@
|
|||||||
spinner: {
|
spinner: {
|
||||||
loading: this.mutableLoading
|
loading: this.mutableLoading
|
||||||
},
|
},
|
||||||
|
noOptions: {
|
||||||
|
search: this.search,
|
||||||
|
searching: this.searching,
|
||||||
|
},
|
||||||
openIndicator: {
|
openIndicator: {
|
||||||
attributes: {
|
attributes: {
|
||||||
'ref': 'openIndicator',
|
'ref': 'openIndicator',
|
||||||
@@ -1033,14 +1058,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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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([[[]]]);
|
||||||
|
|||||||
@@ -129,14 +129,17 @@ 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.toggleDropdown({ target: Select.vm.$refs.search });
|
Select.vm.toggleDropdown({ 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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { shallowMount } from "@vue/test-utils";
|
import { mount, shallowMount } from '@vue/test-utils';
|
||||||
import VueSelect from "../../src/components/Select";
|
import VueSelect from "../../src/components/Select";
|
||||||
|
import { mountDefault } from '../helpers';
|
||||||
|
|
||||||
describe("Reset on options change", () => {
|
describe("Reset on options change", () => {
|
||||||
it("should not reset the selected value by default when the options property changes", () => {
|
it("should not reset the selected value by default when the options property changes", () => {
|
||||||
@@ -13,7 +14,81 @@ describe("Reset on options change", () => {
|
|||||||
expect(Select.vm.selectedValue).toEqual(["one"]);
|
expect(Select.vm.selectedValue).toEqual(["one"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reset the selected value when the options property changes", () => {
|
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', async () => {
|
||||||
|
let resetOnOptionsChange = jest.fn(option => option);
|
||||||
|
const Select = mountDefault(
|
||||||
|
{resetOnOptionsChange, options: ['bear'], value: 'selected'},
|
||||||
|
);
|
||||||
|
|
||||||
|
Select.setProps({options: ['lake', 'kite']});
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
|
expect(resetOnOptionsChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(resetOnOptionsChange)
|
||||||
|
.toHaveBeenCalledWith(['lake', 'kite'], ['bear'], ['selected']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow resetOnOptionsChange to be a function that returns true', async () => {
|
||||||
|
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']});
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
|
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']});
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
|
expect(Select.vm.selectedValue).toEqual(['one']);
|
||||||
|
|
||||||
|
Select.setProps({options: ['two']});
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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"] }
|
||||||
});
|
});
|
||||||
@@ -21,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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+53
-19
@@ -10,32 +10,66 @@ describe('Scoped Slots', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(Select.find({ ref: 'selectedOptions' }).text()).toEqual('one')
|
expect(Select.find({ref: 'selectedOptions'}).text()).toEqual('one');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('receives an option object to the selected-option slot', () => {
|
describe('Slot: selected-option', () => {
|
||||||
const Select = mountDefault(
|
it('receives an option object to the selected-option slot', () => {
|
||||||
{value: 'one'},
|
const Select = mountDefault(
|
||||||
{
|
{value: 'one'},
|
||||||
scopedSlots: {
|
{
|
||||||
'selected-option': `<span slot="selected-option" slot-scope="option">{{ option.label }}</span>`,
|
scopedSlots: {
|
||||||
},
|
'selected-option': `<span slot="selected-option" slot-scope="option">{{ option.label }}</span>`,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
expect(Select.find('.vs__selected').text()).toEqual('one')
|
expect(Select.find('.vs__selected').text()).toEqual('one');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens the dropdown when clicking an option in selected-option slot',
|
||||||
|
() => {
|
||||||
|
const Select = mountDefault(
|
||||||
|
{value: 'one'},
|
||||||
|
{
|
||||||
|
scopedSlots: {
|
||||||
|
'selected-option': `<span class="my-option" slot-scope="option">{{ option.label }}</span>`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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', () => {
|
it('receives an option object to the option slot in the dropdown menu',
|
||||||
const Select = mountDefault(
|
async () => {
|
||||||
{value: 'one'},
|
const Select = mountDefault(
|
||||||
{
|
{value: 'one'},
|
||||||
scopedSlots: {
|
{
|
||||||
'option': `<span slot="option" slot-scope="option">{{ option.label }}</span>`,
|
scopedSlots: {
|
||||||
},
|
'option': `<span slot="option" slot-scope="option">{{ option.label }}</span>`,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Select.vm.open = true;
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
|
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;
|
Select.vm.open = true;
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
expect(Select.find({ref: 'dropdownMenu'}).text()).toEqual('onetwothree')
|
expect(noOptions).toHaveBeenCalledWith({
|
||||||
|
search: 'something not there',
|
||||||
|
searching: true,
|
||||||
|
})
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -80,6 +80,20 @@ describe("When Tagging Is Enabled", () => {
|
|||||||
expect(Select.vm.optionList).toEqual(["one", "two", "three"]);
|
expect(Select.vm.optionList).toEqual(["one", "two", "three"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should pushTags even if the consumer has defined a createOption callback", () => {
|
||||||
|
const Select = selectWithProps({
|
||||||
|
pushTags: true,
|
||||||
|
taggable: true,
|
||||||
|
createOption: option => option,
|
||||||
|
options: ["one", "two"]
|
||||||
|
});
|
||||||
|
|
||||||
|
searchSubmit(Select, "three");
|
||||||
|
|
||||||
|
expect(Select.vm.pushedTags).toEqual(["three"]);
|
||||||
|
expect(Select.vm.optionList).toEqual(["one", "two", "three"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("should add a freshly created option/tag to the options list when pushTags is true and filterable is false", () => {
|
it("should add a freshly created option/tag to the options list when pushTags is true and filterable is false", () => {
|
||||||
const Select = selectWithProps({
|
const Select = selectWithProps({
|
||||||
filterable: false,
|
filterable: false,
|
||||||
@@ -139,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,
|
||||||
@@ -147,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,
|
||||||
@@ -161,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]);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import uniqueId from '../../../src/utility/uniqueId';
|
||||||
|
|
||||||
|
test('it generates a unique number', () => {
|
||||||
|
expect(uniqueId()).not.toEqual(uniqueId());
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user