2
0
mirror of https://github.com/tenrok/vue-select.git synced 2026-06-10 07:52:23 +03:00

Merge branch 'master' into ft/prettier

This commit is contained in:
Jeff
2020-03-08 15:16:45 -07:00
33 changed files with 4959 additions and 1187 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>
@@ -0,0 +1,77 @@
<template>
<div>
<v-select :options="countries" append-to-body :calculate-position="withPopper" />
<label for="position" style="display: block; margin: 1rem 0;">
<input
type="checkbox"
id="position"
v-model="placement"
true-value="top"
false-value="bottom"
>
Position dropdown above
</label>
</div>
</template>
<script>
import countries from '../data/countries'
import { createPopper } from '@popperjs/core';
export default {
data: () => ({countries, placement: 'top'}),
methods: {
withPopper (dropdownList, component, {width},) {
/**
* We need to explicitly define the dropdown width since
* it is usually inherited from the parent with CSS.
*/
dropdownList.style.width = width;
/**
* Here we position the dropdownList relative to the $refs.toggle Element.
*
* The 'offset' modifier aligns the dropdown so that the $refs.toggle and
* the dropdownList overlap by 1 pixel.
*
* The 'toggleClass' modifier adds a 'drop-up' class to the Vue Select
* wrapper so that we can set some styles for when the dropdown is placed
* above.
*/
createPopper(component.$refs.toggle, dropdownList, {
placement: this.placement,
modifiers: [
{
name: 'offset', options: {
offset: [0, -1]
}
},
{
name: 'toggleClass',
enabled: true,
phase: 'write',
fn ({state}) {
component.$el.classList.toggle('drop-up', state.placement === 'top')
},
}]
});
}
}
};
</script>
<style>
.v-select.drop-up.vs--open .vs__dropdown-toggle {
border-radius: 0 0 4px 4px;
border-top-color: transparent;
border-bottom-color: rgba(60, 60, 60, 0.26);
}
[data-popper-placement='top'] {
border-radius: 4px 4px 0 0;
border-top-style: solid;
border-bottom-style: none;
box-shadow: 0 -3px 6px rgba(0, 0, 0, 0.15)
}
</style>
+1
View File
@@ -129,6 +129,7 @@ module.exports = {
collapsable: false,
children: [
['guide/keydown', 'Keydown Events'],
['guide/positioning', 'Dropdown Position']
],
},
{
+56 -3
View File
@@ -1,3 +1,18 @@
## appendToBody <Badge text="v3.7.0+" />
Append the dropdown element to the end of the body
and size/position it dynamically. Use it if you have
overflow or z-index issues.
See [Dropdown Position](../guide/positioning.md) for more details.
```js
appendToBody: {
type: Boolean,
default: false
},
```
## value
Contains the currently selected value. Very similar to a
@@ -109,6 +124,30 @@ transition: {
},
```
## calculatePosition <Badge text="v3.7.0+" />
When `appendToBody` is true, this function is responsible for positioning the drop down list.
See [Dropdown Position](../guide/positioning.md) for more details.
```js
calculatePosition: {
type: Function,
/**
* @param dropdownList {HTMLUListElement}
* @param component {Vue} current instance of vue select
* @param width {string} calculated width in pixels of the dropdown menu
* @param top {string} absolute position top value in pixels relative to the document
* @param left {string} absolute position left value in pixels relative to the document
*/
default(dropdownList, component, {width, top, left}) {
dropdownList.style.top = top;
dropdownList.style.left = left;
dropdownList.style.width = width;
}
}
```
## clearSearchOnSelect
Enables/disables clearing the search text when an option is selected.
@@ -120,6 +159,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
Close a dropdown when an option is chosen. Set to false to keep the dropdown
@@ -340,10 +392,10 @@ createOption: {
## resetOnOptionsChange
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.
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
/**
@@ -399,3 +451,4 @@ selectOnTab: {
type: Boolean,
default: false
}
```
+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`
#### Scope:
#### Scope:
- `option {Object}` - A selected option
```html
<slot name="selected-option" v-bind="(typeof option === 'object')?option:{[label]: option}">
{{ getOptionLabel(option) }}
<slot
name="selected-option"
v-bind="(typeof option === 'object')?option:{[label]: option}"
>
{{ getOptionLabel(option) }}
</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
```html
<slot v-for="option in valueAsArray" name="selected-option-container"
:option="(typeof option === 'object')?option:{[label]: option}" :deselect="deselect" :multiple="multiple" :disabled="disabled">
<span class="selected-tag" v-bind:key="option.index">
<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
v-for="option in valueAsArray"
name="selected-option-container"
:option="(typeof option === 'object')?option:{[label]: option}"
:deselect="deselect"
:multiple="multiple"
:disabled="disabled"
>
<span class="selected-tag" v-bind:key="option.index">
<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>
```
@@ -44,9 +63,29 @@ Slots can be used to change the look and feel of the UI, or to simply swap out t
### `spinner`
#### Scope:
- `loading {Boolean}` - if the component is in a loading state
```html
<slot name="spinner">
<div class="spinner" v-show="mutableLoading">Loading...</div>
<slot name="spinner" v-bind="scope.spinner">
<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>
```
@@ -54,12 +93,26 @@ Slots can be used to change the look and feel of the UI, or to simply swap out t
### `option`
#### Scope:
- `option {Object}` - The currently iterated option from `filteredOptions`
```html
<slot name="option" v-bind="(typeof option === 'object')?option:{[label]: option}">
{{ getOptionLabel(option) }}
<slot
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>
```
+33
View File
@@ -0,0 +1,33 @@
## Default
With the default CSS, Vue Select uses absolute positioning to render the dropdown menu. The root
`.v-select` container (the components `$el`) is used as the `relative` parent for the dropdown. The
dropdown will be displayed below the `$el` regardless of the available space.
This works for most cases, but you might run into issues placing into a modal or near the bottom of
the viewport. If you need more fine grain control, you can use calculated positioning.
## Calculated <Badge text="v3.7.0+" />
If you want more control over how the dropdown is rendered, or if you're running into z-index issues,
you may use the `appendToBody` boolean prop. When enabled, Vue Select will append the dropdown to
the document, outside of the `.v-select` container, and position it with Javscript.
When `appendToBody` is true, the positioning will be handled by the `calculatePosition` prop. This
function is responsible for setting top/left absolute positioning values for the dropdown. The
default implementation places the dropdown in the same position that it would normally appear.
## Popper.js Integration <Badge text="v3.7.0+" />
[Popper.js](https://popper.js.org/) is an awesome, 3kb utility for calculating positions of just
about any DOM element relative to another.
By using the `appendToBody` and `calculatePosition` props, we're able to integrate directly with
popper to calculate positioning for us.
<PositionedWithPopper />
Check out the [Popper Docs](https://popper.js.org/docs/v2/modifiers/) to see the full `modifiers`
API being used below.
<<< @/.vuepress/components/PositionedWithPopper.vue{25-59}
+18 -8
View File
@@ -1,22 +1,32 @@
::: tip 🚧
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.
:::
#### Scoped Slot `option`
### Scoped Slot `option`
vue-select provides the scoped `option` slot in order to create custom dropdown templates.
```html
<v-select :options="options" label="title">
<template v-slot:option="option">
<span :class="option.icon"></span>
{{ option.title }}
</template>
</v-select>
```
<template v-slot:option="option">
<span :class="option.icon"></span>
{{ option.title }}
</template>
</v-select>
```
Using the `option` slot with props `"option"` provides the current option variable to the template.
<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
+1
View File
@@ -8,6 +8,7 @@
"build:preview": "cross-env DEPLOY_PREVIEW=true vuepress build"
},
"devDependencies": {
"@popperjs/core": "^2.1.0",
"@vuepress/plugin-active-header-links": "^1.0.0-alpha.47",
"@vuepress/plugin-google-analytics": "^1.0.0-alpha.47",
"@vuepress/plugin-nprogress": "^1.0.0-alpha.47",
+5
View File
@@ -715,6 +715,11 @@
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
"@popperjs/core@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.1.0.tgz#09a7a352a40508156e1256efdc015593feca28e0"
integrity sha512-ntN5t5spqhQv28cLfmmt1dYabsudzR5A7PU15gr/gzcT/gzqAOnYFQPaLPFraDa7ZCJG2eJ1JsO7pgXbYXGIrw==
"@types/events@*":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
+30 -5
View File
@@ -1,6 +1,6 @@
{
"name": "vue-select",
"version": "3.4.0",
"version": "3.7.0",
"description": "Everything you wish the HTML <select> element could do, wrapped up into a lightweight, extensible Vue component.",
"author": "Jeff Sagal <sagalbot@gmail.com>",
"homepage": "https://vue-select.org",
@@ -20,7 +20,9 @@
"build:preview": "cd docs && yarn build",
"test": "jest",
"lint": "npx eslint '**/*.{js,vue}'",
"prettier": "npx prettier '**/*.{js,vue}'"
"prettier": "npx prettier '**/*.{js,vue}'",
"semantic-release": "semantic-release",
"commit": "git-cz"
},
"repository": {
"type": "git",
@@ -37,17 +39,21 @@
"@babel/plugin-transform-runtime": "^7.4.0",
"@babel/preset-env": "^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",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^10.0.3",
"babel-loader": "^8.0.5",
"bundlewatch": "^0.2.5",
"chokidar": "^2.1.5",
"commitizen": "^4.0.3",
"coveralls": "^3.0.2",
"cross-env": "^5.2.0",
"css-loader": "^2.1.0",
"cssnano": "^4.1.10",
"cz-conventional-changelog": "3.1.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.0",
"eslint-loader": "^3.0.3",
@@ -64,6 +70,7 @@
"postcss-scss": "^2.0.0",
"prettier": "^1.19.1",
"sass-loader": "^7.1.0",
"semantic-release": "^17.0.4",
"terser-webpack-plugin": "^1.2.3",
"url-loader": "^1.1.2",
"vue": "^2.6.10",
@@ -108,8 +115,26 @@
"!**/node_modules/**"
],
"coverageReporters": [
"html",
"text-summary"
"text"
]
},
"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}"
}
]
]
};
+70 -18
View File
@@ -4,7 +4,7 @@
<template>
<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="Search for option">
<div class="vs__selected-options" ref="selectedOptions">
<slot v-for="option in selectedValue"
@@ -17,7 +17,7 @@
<slot name="selected-option" v-bind="normalizeOptionForSlot(option)">
{{ getOptionLabel(option) }}
</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" />
</button>
</span>
@@ -35,7 +35,8 @@
@click="clearSelection"
type="button"
class="vs__clear"
title="Clear selection"
title="Clear Selected"
aria-label="Clear Selected"
ref="clearButton"
>
<component :is="childComponents.Deselect" />
@@ -52,13 +53,15 @@
</div>
<transition :name="transition">
<ul ref="dropdownMenu" v-if="dropdownOpen" class="vs__dropdown-menu" role="listbox" @mousedown.prevent="onMousedown" @mouseup="onMouseUp">
<ul ref="dropdownMenu" v-if="dropdownOpen" :id="`vs${uid}__listbox`" class="vs__dropdown-menu" role="listbox" @mousedown.prevent="onMousedown" @mouseup="onMouseUp" v-append-to-body>
<li
role="option"
v-for="(option, index) in filteredOptions"
:key="getOptionKey(option)"
:id="`vs${uid}__option-${index}`"
class="vs__dropdown-option"
: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"
@mouseover="selectable(option) ? typeAheadPointer = index : null"
@mousedown.prevent.stop="selectable(option) ? select(option) : null"
>
@@ -66,10 +69,11 @@
{{ getOptionLabel(option) }}
</slot>
</li>
<li v-if="!filteredOptions.length" class="vs__no-options" @mousedown.stop="">
<slot name="no-options">Sorry, no matching options.</slot>
<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>
</ul>
<ul v-else :id="`vs${uid}__listbox`" role="listbox" style="display: none; visibility: hidden;"></ul>
</transition>
</div>
</template>
@@ -79,6 +83,8 @@
import typeAheadPointer from '../mixins/typeAheadPointer'
import ajax from '../mixins/ajax'
import childComponents from './childComponents';
import appendToBody from '../directives/appendToBody';
import uniqueId from '../utility/uniqueId';
/**
* @name VueSelect
@@ -88,6 +94,8 @@
mixins: [pointerScroll, typeAheadPointer, ajax],
directives: {appendToBody},
props: {
/**
* Contains the currently selected value. Very similar to a
@@ -425,6 +433,17 @@
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.
* @type {Boolean}
@@ -500,11 +519,46 @@
* @return {Object}
*/
default: (map, vm) => map,
},
/**
* Append the dropdown element to the end of the body
* and size/position it dynamically. Use it if you have
* overflow or z-index issues.
* @type {Boolean}
*/
appendToBody: {
type: Boolean,
default: false
},
/**
* When `appendToBody` is true, this function
* is responsible for positioning the drop
* down list.
* @since v3.7.0
* @see http://vue-select.org/guide/positioning.html
*/
calculatePosition: {
type: Function,
/**
* @param dropdownList {HTMLUListElement}
* @param component {Vue} current instance of vue select
* @param width {string} calculated width in pixels of the dropdown menu
* @param top {string} absolute position top value in pixels relative to the document
* @param left {string} absolute position left value in pixels relative to the document
*/
default(dropdownList, component, {width, top, left}) {
dropdownList.style.top = top;
dropdownList.style.left = left;
dropdownList.style.width = width;
}
}
},
data() {
return {
uid: uniqueId(),
search: '',
open: false,
isComposing: false,
@@ -826,7 +880,8 @@
if (this.mousedown && !this.searching) {
this.mousedown = false
} else {
if (this.clearSearchOnBlur) {
const { clearSearchOnSelect, multiple } = this;
if (this.clearSearchOnBlur({ clearSearchOnSelect, multiple })) {
this.search = ''
}
this.closeSearchOptions()
@@ -973,10 +1028,11 @@
'tabindex': this.tabindex,
'readonly': !this.searchable,
'id': this.inputId,
'aria-expanded': this.dropdownOpen,
'aria-label': 'Search for option',
'aria-autocomplete': 'list',
'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',
'role': 'combobox',
'type': 'search',
'autocomplete': this.autocomplete,
'value': this.search,
@@ -993,6 +1049,10 @@
spinner: {
loading: this.mutableLoading
},
noOptions: {
search: this.search,
searching: this.searching,
},
openIndicator: {
attributes: {
'ref': 'openIndicator',
@@ -1033,14 +1093,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
* search input
+21
View File
@@ -0,0 +1,21 @@
export default {
inserted (el, bindings, {context}) {
if (context.appendToBody) {
const {height, top, left} = context.$refs.toggle.getBoundingClientRect();
context.calculatePosition(el, context, {
width: context.$refs.toggle.clientWidth + 'px',
top: (window.scrollY + top + height) + 'px',
left: (window.scrollX + left) + 'px',
});
document.body.appendChild(el);
}
},
unbind (el, bindings, vnode) {
if (vnode.context.appendToBody && el.parentNode) {
el.parentNode.removeChild(el);
}
},
}
+1 -1
View File
@@ -30,7 +30,7 @@ export default {
*/
pixelsToPointerTop() {
let pixelsToPointerTop = 0;
if (this.$refs.dropdownMenu) {
if (this.$refs.dropdownMenu && this.dropdownOpen) {
for (let i = 0; i < this.typeAheadPointer; i++) {
pixelsToPointerTop += this.$refs.dropdownMenu.children[i]
.offsetHeight;
+1
View File
@@ -23,6 +23,7 @@ $transition-duration: .15s;
/* Dropdown Default Transition */
.vs__fade-enter-active,
.vs__fade-leave-active {
pointer-events: none;
transition: opacity $transition-duration $transition-timing-function;
}
.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);
});
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();
Select.vm.search = "foo";
await Select.vm.$nextTick();
const events = Select.emitted("search");
@@ -24,11 +25,13 @@ describe("Asynchronous Loading", () => {
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();
Select.vm.search = "foo";
await Select.vm.$nextTick();
Select.vm.search = "";
await Select.vm.$nextTick();
const events = Select.emitted("search");
@@ -36,7 +39,7 @@ describe("Asynchronous Loading", () => {
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, {
listeners: {
search: (search, loading) => {
@@ -47,13 +50,16 @@ describe("Asynchronous Loading", () => {
Select.vm.mutableLoading = true;
Select.vm.search = 'foo';
await Select.vm.$nextTick();
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 });
Select.setProps({ loading: true });
await Select.vm.$nextTick();
expect(Select.vm.mutableLoading).toEqual(true);
});
});
+2 -1
View File
@@ -1,9 +1,10 @@
import { selectWithProps } from "../helpers";
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 });
Select.vm.$data._value = 'one';
await Select.vm.$nextTick();
Select.find(".vs__deselect").trigger("click");
expect(Select.emitted().input).toEqual([[[]]]);
+5 -2
View File
@@ -129,14 +129,17 @@ describe("Toggling Dropdown", () => {
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({
noDrop: true,
});
Select.vm.toggleDropdown({ target: Select.vm.$refs.search });
expect(Select.vm.open).toEqual(true);
await Select.vm.$nextTick();
expect(Select.contains('.vs__dropdown-menu')).toBeFalsy();
expect(Select.contains('.vs__dropdown-option')).toBeFalsy();
expect(Select.contains('.vs__no-options')).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");
});
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 Select = selectWithProps({
options: [{}]
});
Select.vm.open = true;
await Select.vm.$nextTick();
expect(spy).toHaveBeenCalledWith(
'[vue-select warn]: Label key "option.label" does not exist in options object {}.' +
"\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"')
});
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);
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', () => {
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'},
@@ -52,6 +53,8 @@ describe("Reset on options change", () => {
const spy = jest.spyOn(Select.vm, 'clearSelection');
Select.setProps({options: ['one', 'two']});
await Select.vm.$nextTick();
expect(spy).toHaveBeenCalledTimes(1);
});
@@ -74,14 +77,18 @@ describe("Reset on options change", () => {
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", () => {
it("should reset the selected value when the options property changes", async () => {
const Select = shallowMount(VueSelect, {
propsData: { resetOnOptionsChange: true, options: ["one"] }
});
@@ -89,15 +96,46 @@ describe("Reset on options change", () => {
Select.vm.$data._value = 'one';
Select.setProps({options: ["four", "five", "six"]});
await Select.vm.$nextTick();
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, {
propsData: { value: "one", options: [], reduce(option) { return option.value } }
});
Select.setProps({options: [{ label: "oneLabel", value: "one" }]});
await Select.vm.$nextTick();
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 Select = shallowMount(VueSelect, {
propsData: {
@@ -222,6 +222,7 @@ describe("When reduce prop is defined", () => {
});
Select.setProps({ value: optionToChangeTo.id });
await Select.vm.$nextTick();
expect(Select.vm.selectedValue).toEqual([optionToChangeTo]);
});
+8 -2
View File
@@ -1,27 +1,33 @@
import { selectWithProps } from "../helpers";
describe("Selectable prop", () => {
it("should select selectable option if clicked", () => {
it("should select selectable option if clicked", async () => {
const Select = selectWithProps({
options: ["one", "two", "three"],
selectable: (option) => option === "one"
});
Select.vm.$data.open = true;
await Select.vm.$nextTick();
Select.find(".vs__dropdown-menu li:first-child").trigger("mousedown");
await Select.vm.$nextTick();
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({
options: ["one", "two", "three"],
selectable: (option) => option === "one"
});
Select.vm.$data.open = true;
await Select.vm.$nextTick();
Select.find(".vs__dropdown-menu li:last-child").trigger("mousedown");
await Select.vm.$nextTick();
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',
() => {
async () => {
const Select = mountDefault(
{value: 'one'},
{
@@ -52,7 +52,24 @@ describe('Scoped Slots', () => {
});
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;
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]);
});
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" };
const Select = selectWithProps({
taggable: true,
@@ -161,12 +161,13 @@ describe("When Tagging Is Enabled", () => {
});
Select.vm.search = "two";
await Select.vm.$nextTick();
searchSubmit(Select);
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" };
const Select = selectWithProps({
taggable: true,
@@ -175,6 +176,7 @@ describe("When Tagging Is Enabled", () => {
});
Select.vm.search = "two";
await Select.vm.$nextTick();
searchSubmit(Select);
expect(Select.vm.selectedValue).toEqual([two]);
+2 -1
View File
@@ -120,11 +120,12 @@ describe("Moving the Typeahead Pointer", () => {
});
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();
// Drop down must be open for $refs to exist
Select.vm.open = true;
await Select.vm.$nextTick();
/**
* 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());
});
+4346 -1095
View File
File diff suppressed because it is too large Load Diff