Merge pull request #3157 from SergioCrisostomo/refactor-select

Refactor select, fix tab/keyboard navigation
This commit is contained in:
Aresn 2018-05-04 10:28:44 +08:00 committed by GitHub
commit 4ce11f84c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 1088 additions and 779 deletions

View file

@ -1,6 +1,8 @@
const path = require('path');
const webpack = require('webpack');
const entry = require('./locale');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
process.env.NODE_ENV = 'production';
module.exports = {
@ -40,7 +42,7 @@ module.exports = {
NODE_ENV: '"production"'
}
}),
new webpack.optimize.UglifyJsPlugin({
new UglifyJsPlugin({
parallel: true,
sourceMap: true,
})

View file

@ -3,6 +3,7 @@ const webpack = require('webpack');
const merge = require('webpack-merge');
const webpackBaseConfig = require('./webpack.base.config.js');
const CompressionPlugin = require('compression-webpack-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
process.env.NODE_ENV = 'production';
@ -32,7 +33,7 @@ module.exports = merge(webpackBaseConfig, {
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
}),
new webpack.optimize.UglifyJsPlugin({
new UglifyJsPlugin({
parallel: true,
sourceMap: true,
}),

101
package-lock.json generated
View file

@ -13387,6 +13387,30 @@
"integrity": "sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g==",
"dev": true
},
"uglify-es": {
"version": "3.3.9",
"resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz",
"integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==",
"dev": true,
"requires": {
"commander": "2.13.0",
"source-map": "0.6.1"
},
"dependencies": {
"commander": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz",
"integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"uglify-js": {
"version": "2.8.29",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
@ -13405,14 +13429,54 @@
"dev": true
},
"uglifyjs-webpack-plugin": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz",
"integrity": "sha1-uVH0q7a9YX5m9j64kUmOORdj4wk=",
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.3.tgz",
"integrity": "sha512-as/50351uuJGiQbhVvE510SCqM/YOWghCzIFJeEOu5oVE0QOZ3/vu2QcnVvu0Lz+vNd0rKsiCFAlbcw0i/YH2w==",
"dev": true,
"requires": {
"source-map": "0.5.7",
"uglify-js": "2.8.29",
"webpack-sources": "1.1.0"
"cacache": "10.0.4",
"find-cache-dir": "1.0.0",
"schema-utils": "0.4.5",
"serialize-javascript": "1.4.0",
"source-map": "0.6.1",
"uglify-es": "3.3.9",
"webpack-sources": "1.1.0",
"worker-farm": "1.6.0"
},
"dependencies": {
"ajv": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.2.1.tgz",
"integrity": "sha1-KKarxJOiq+D7TIUHrK7bQ/pVBnE=",
"dev": true,
"requires": {
"fast-deep-equal": "1.1.0",
"fast-json-stable-stringify": "2.0.0",
"json-schema-traverse": "0.3.1"
}
},
"ajv-keywords": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.1.0.tgz",
"integrity": "sha1-rCsnk5xUPpXSwG5/f1wnvkqlQ74=",
"dev": true
},
"schema-utils": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.5.tgz",
"integrity": "sha512-yYrjb9TX2k/J1Y5UNy3KYdZq10xhYcF8nMpAW6o3hy6Q8WSIEf9lJHG/ePnOBfziPM3fvQwfOwa13U/Fh8qTfA==",
"dev": true,
"requires": {
"ajv": "6.2.1",
"ajv-keywords": "3.1.0"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"ultron": {
@ -13850,6 +13914,11 @@
"dev": true,
"optional": true
},
"v-click-outside-x": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/v-click-outside-x/-/v-click-outside-x-2.4.0.tgz",
"integrity": "sha512-xAouyFRaMDD074px+J3PoxhU5nGQsIj8yxXRYyFd0/PRhY1ob3F55L9mGsd35KzXkQteajEhap6SClaMB0MENg=="
},
"v8flags": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz",
@ -14807,6 +14876,17 @@
"integrity": "sha1-mTcqXJmb8t8WCvwNdL7U9HlIzSI=",
"dev": true
},
"uglifyjs-webpack-plugin": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz",
"integrity": "sha1-uVH0q7a9YX5m9j64kUmOORdj4wk=",
"dev": true,
"requires": {
"source-map": "0.5.7",
"uglify-js": "2.8.29",
"webpack-sources": "1.1.0"
}
},
"y18n": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
@ -15446,6 +15526,15 @@
"integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
"dev": true
},
"worker-farm": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz",
"integrity": "sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==",
"dev": true,
"requires": {
"errno": "0.1.7"
}
},
"wrap-ansi": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",

View file

@ -47,7 +47,8 @@
"js-calendar": "^1.2.3",
"lodash.throttle": "^4.1.1",
"popper.js": "^1.14.1",
"tinycolor2": "^1.4.1"
"tinycolor2": "^1.4.1",
"v-click-outside-x": "^2.4.0"
},
"peerDependencies": {
"vue": "^2.5.2"
@ -99,6 +100,7 @@
"sinon": "^4.4.2",
"sinon-chai": "^3.0.0",
"style-loader": "^0.20.2",
"uglifyjs-webpack-plugin": "^1.2.3",
"url-loader": "^1.0.1",
"vue": "^2.5.16",
"vue-hot-reload-api": "^2.3.0",

View file

@ -0,0 +1,29 @@
<script>
const returnArrayFn = () => [];
export default {
props: {
options: {
type: Array,
default: returnArrayFn
},
slotOptions: {
type: Array,
default: returnArrayFn
},
slotUpdateHook: {
type: Function,
default: () => {}
},
},
functional: true,
render(h, {props, parent}){
// to detect changes in the $slot children/options we do this hack
// so we can trigger the parents computed properties and have everything reactive
// although $slot.default is not
if (props.slotOptions !== parent.$slots.default) props.slotUpdateHook();
return props.options;
}
};
</script>

View file

@ -1,5 +1,10 @@
<template>
<li :class="classes" @click.stop="select" @mouseout.stop="blur" v-show="!hidden"><slot>{{ showLabel }}</slot></li>
<li
:class="classes"
@click.stop="select"
@mousedown.prevent
@touchstart.prevent
><slot>{{ showLabel }}</slot></li>
</template>
<script>
import Emitter from '../../mixins/emitter';
@ -22,15 +27,19 @@
disabled: {
type: Boolean,
default: false
},
selected: {
type: Boolean,
default: false
},
isFocused: {
type: Boolean,
default: false
}
},
data () {
return {
selected: false,
index: 0, // for up and down to focus
isFocus: false,
hidden: false, // for search
searchLabel: '', // the value is slot,only for search
searchLabel: '', // the slot value (textContent)
autoComplete: false
};
},
@ -41,53 +50,34 @@
{
[`${prefixCls}-disabled`]: this.disabled,
[`${prefixCls}-selected`]: this.selected && !this.autoComplete,
[`${prefixCls}-focus`]: this.isFocus
[`${prefixCls}-focus`]: this.isFocused
}
];
},
showLabel () {
return (this.label) ? this.label : this.value;
},
optionLabel(){
return (this.$el && this.$el.textContent) || this.label;
}
},
methods: {
select () {
if (this.disabled) {
return false;
}
if (this.disabled) return false;
this.dispatch('iSelect', 'on-select-selected', this.value);
this.dispatch('iSelect', 'on-select-selected', {
value: this.value,
label: this.optionLabel,
});
this.$emit('on-select-selected', {
value: this.value,
label: this.optionLabel,
});
},
blur () {
this.isFocus = false;
},
queryChange (val) {
const parsedQuery = val.replace(/(\^|\(|\)|\[|\]|\$|\*|\+|\.|\?|\\|\{|\}|\|)/g, '\\$1');
this.hidden = !new RegExp(parsedQuery, 'i').test(this.searchLabel);
},
// 使 key SearchLabel #1865
updateSearchLabel () {
this.searchLabel = this.$el.textContent;
},
onSelectClose(){
this.isFocus = false;
},
onQueryChange(val){
this.queryChange(val);
}
},
mounted () {
this.updateSearchLabel();
this.dispatch('iSelect', 'append');
this.$on('on-select-close', this.onSelectClose);
this.$on('on-query-change',this.onQueryChange);
const Select = findComponentUpward(this, 'iSelect');
if (Select) this.autoComplete = Select.autoComplete;
},
beforeDestroy () {
this.dispatch('iSelect', 'remove');
this.$off('on-select-close', this.onSelectClose);
this.$off('on-query-change',this.onQueryChange);
}
};
</script>

View file

@ -0,0 +1,196 @@
<template>
<div @click="onHeaderClick">
<div class="ivu-tag ivu-tag-checked" v-for="item in selectedMultiple">
<span class="ivu-tag-text">{{ item.label }}</span>
<Icon type="ios-close-empty" @click.native.stop="removeTag(item)"></Icon>
</div>
<span
:class="singleDisplayClasses"
v-show="singleDisplayValue"
>{{ singleDisplayValue }}</span>
<input
:id="inputElementId"
type="text"
v-if="filterable"
v-model="query"
:disabled="disabled"
:class="[prefixCls + '-input']"
:placeholder="showPlaceholder ? localePlaceholder : ''"
:style="inputStyle"
autocomplete="off"
spellcheck="false"
@keydown="resetInputState"
@keydown.delete="handleInputDelete"
@focus="onInputFocus"
@blur="onInputFocus"
ref="input">
<Icon type="ios-close" :class="[prefixCls + '-arrow']" v-if="resetSelect" @click.native.stop="resetSelect"></Icon>
<Icon type="arrow-down-b" :class="[prefixCls + '-arrow']" v-if="!resetSelect && !remote && !disabled"></Icon>
</div>
</template>
<script>
import Icon from '../icon';
import Emitter from '../../mixins/emitter';
import Locale from '../../mixins/locale';
const prefixCls = 'ivu-select';
export default {
name: 'iSelectHead',
mixins: [ Emitter, Locale ],
components: { Icon },
props: {
disabled: {
type: Boolean,
default: false
},
filterable: {
type: Boolean,
default: false
},
multiple: {
type: Boolean,
default: false
},
remote: {
type: Boolean,
default: false
},
initialLabel: {
type: String,
},
values: {
type: Array,
default: () => []
},
clearable: {
type: [Function, Boolean],
default: false,
},
inputElementId: {
type: String
},
placeholder: {
type: String
},
queryProp: {
type: String,
default: ''
}
},
data () {
return {
prefixCls: prefixCls,
query: '',
inputLength: 20,
remoteInitialLabel: this.initialLabel,
preventRemoteCall: false,
};
},
computed: {
singleDisplayClasses(){
const {filterable, multiple, showPlaceholder} = this;
return [{
[prefixCls + '-placeholder']: showPlaceholder && !filterable,
[prefixCls + '-selected-value']: !showPlaceholder && !multiple && !filterable,
}];
},
singleDisplayValue(){
if ((this.multiple && this.values.length > 0) || this.filterable) return '';
return `${this.selectedSingle}` || this.localePlaceholder;
},
showPlaceholder () {
let status = false;
if (!this.multiple) {
const value = this.values[0];
if (typeof value === 'undefined' || String(value).trim() === ''){
status = !this.remoteInitialLabel;
}
} else {
if (!this.values.length > 0) {
status = true;
}
}
return status;
},
resetSelect(){
return !this.showPlaceholder && this.clearable;
},
inputStyle () {
let style = {};
if (this.multiple) {
if (this.showPlaceholder) {
style.width = '100%';
} else {
style.width = `${this.inputLength}px`;
}
}
return style;
},
localePlaceholder () {
if (this.placeholder === undefined) {
return this.t('i.select.placeholder');
} else {
return this.placeholder;
}
},
selectedSingle(){
const selected = this.values[0];
return selected ? selected.label : (this.remoteInitialLabel || '');
},
selectedMultiple(){
return this.multiple ? this.values : [];
}
},
methods: {
onInputFocus(e){
this.$emit(e.type === 'focus' ? 'on-input-focus' : 'on-input-blur');
},
removeTag (value) {
if (this.disabled) return false;
this.dispatch('iSelect', 'on-select-selected', value);
},
resetInputState () {
this.inputLength = this.$refs.input.value.length * 12 + 20;
},
handleInputDelete () {
if (this.multiple && this.selectedMultiple.length && this.query === '') {
this.removeTag(this.selectedMultiple[this.selectedMultiple.length - 1]);
}
},
onHeaderClick(e){
if (this.filterable && e.target === this.$el){
this.$refs.input.focus();
}
}
},
watch: {
values ([value]) {
if (!this.filterable) return;
this.preventRemoteCall = true;
if (this.multiple){
this.query = '';
this.preventRemoteCall = false; // this should be after the query change setter above
return;
}
// #982
if (typeof value === 'undefined' || value === '' || value === null) this.query = '';
else this.query = value.label;
},
query (val) {
if (this.preventRemoteCall) {
this.preventRemoteCall = false;
return;
}
this.$emit('on-query-change', val);
},
queryProp(query){
if (query !== this.query) this.query = query;
},
}
};
</script>

File diff suppressed because it is too large Load diff

View file

@ -25,23 +25,14 @@
border: 1px solid @border-color-base;
transition: all @transition-time @ease-in-out;
.@{select-prefix-cls}-arrow:nth-of-type(1) {
display: none;
cursor: pointer;
}
&:hover {
&:hover, &-focused {
.hover();
.@{select-prefix-cls}-arrow:nth-of-type(1) {
.@{select-prefix-cls}-arrow {
display: inline-block;
}
}
}
&-show-clear &-selection:hover .@{select-prefix-cls}-arrow:nth-of-type(2){
display: none;
}
&-arrow {
.inner-arrow();
}
@ -51,14 +42,9 @@
.active();
}
.@{select-prefix-cls}-arrow:nth-of-type(2) {
.@{select-prefix-cls}-arrow {
transform: rotate(180deg);
}
}
&:focus{
outline: 0;
.@{select-prefix-cls}-selection{
.active();
display: inline-block;
}
}
@ -66,7 +52,7 @@
.@{select-prefix-cls}-selection {
.disabled();
.@{select-prefix-cls}-arrow:nth-of-type(1) {
.@{select-prefix-cls}-arrow {
display: none;
}
@ -74,7 +60,7 @@
border-color: @border-color-base;
box-shadow: none;
.@{select-prefix-cls}-arrow:nth-of-type(2) {
.@{select-prefix-cls}-arrow {
display: inline-block;
}
}

View file

@ -26,34 +26,40 @@ describe('Select.vue', () => {
const placeholderSpan = vm.$el.querySelector('.ivu-select-placeholder');
expect(placeholderSpan.textContent).to.equal(placeholder);
expect(placeholderSpan.style.display).to.not.equal('none');
expect(vm.$children[0].showPlaceholder).to.equal(true);
done();
});
});
it('should create a Select component and take a pre-selected value', done => {
vm = createVue({
template: `
<Select :value="2">
<Option v-for="item in options" :value="item.value" :key="item.value">{{ item.label }}</Option>
</Select>
`,
data() {
return {
value: '',
options: [{value: 1, label: 'Foo'}, {value: 2, label: 'Bar'}]
};
}
});
vm.$nextTick(() => {
const selectedValueSpan = vm.$el.querySelector('.ivu-select-selected-value');
expect(selectedValueSpan.textContent).to.equal('Bar');
expect(selectedValueSpan.style.display).to.not.equal('none');
expect(vm.$children[0].selectedSingle).to.equal('Bar');
expect(vm.$children[0].model).to.equal(2);
done();
template: `
<Select :value="value">
<Option v-for="item in options" :value="item.value" :key="item.value">{{ item.label }}</Option>
</Select>
`,
data() {
return {
value: 2,
options: [{value: 1, label: 'Foo'}, {value: 2, label: 'Bar'}]
};
}
});
waitForIt(
() => {
const selectedValueSpan = vm.$el.querySelector('.ivu-select-selected-value');
return selectedValueSpan.textContent === 'Bar';
},
() => {
const selectedValueSpan = vm.$el.querySelector('.ivu-select-selected-value');
const {label, value} = vm.$children[0].values[0];
expect(selectedValueSpan.textContent).to.equal('Bar');
expect(selectedValueSpan.style.display).to.not.equal('none');
expect(label).to.equal('Bar');
expect(value).to.equal(2);
done();
}
);
});
it('should accept normal characters', done => {
@ -112,13 +118,20 @@ describe('Select.vue', () => {
};
}
});
vm.$nextTick(() => {
const placeholderSpan = vm.$el.querySelector('.ivu-select-placeholder');
const selectedValueSpan = vm.$el.querySelector('.ivu-select-selected-value');
expect(placeholderSpan.style.display).to.equal('none');
expect(selectedValueSpan.style.display).to.not.equal('none');
done();
});
waitForIt(
() => {
const selectedValueSpan = vm.$el.querySelector('.ivu-select-selected-value');
return selectedValueSpan.textContent === 'Bar';
},
() => {
const placeholderSpan = vm.$el.querySelector('.ivu-select-placeholder');
const selectedValueSpan = vm.$el.querySelector('.ivu-select-selected-value');
expect(placeholderSpan).to.equal(null);
expect(!!selectedValueSpan.style.display).to.not.equal('none');
expect(selectedValueSpan.textContent).to.equal('Bar');
done();
}
);
});
it('should set different classes for different sizes', done => {
@ -158,12 +171,10 @@ describe('Select.vue', () => {
}
});
const condition = function() {
return vm.$children[0].options.length > 0;
const componentOptions = vm.$children[0].flatOptions;
return componentOptions && componentOptions.length > 0;
};
const callback = function() {
if (vm.$children[0].options == 0) return setTimeout(waitForIt.bind(null, done), 50);
expect(JSON.stringify(vm.$children[0].options)).to.equal(JSON.stringify(laterOptions));
const renderedOptions = vm.$el.querySelectorAll('.ivu-select-dropdown-list li');
expect(renderedOptions.length).to.equal(laterOptions.length);
@ -177,79 +188,234 @@ describe('Select.vue', () => {
});
describe('Behavior tests', () => {
it('should create different and independent instances', done => {
const options = [
{value: 'beijing', label: 'Beijing'},
{value: 'stockholm', label: 'Stockholm'},
{value: 'lisboa', label: 'Lisboa'}
];
it('should create different and independent instances', done => {
const options = [
{value: 'beijing', label: 'Beijing'},
{value: 'stockholm', label: 'Stockholm'},
{value: 'lisboa', label: 'Lisboa'}
];
vm = createVue({
template: `
<div>
<i-select v-model="modelA" multiple style="width:260px">
<i-option v-for="item in cityList" :value="item.value" :key="item.value">{{ item.label }}</i-option>
</i-select>
<i-select v-model="modelB" multiple style="width:260px">
<i-option v-for="item in cityList" :value="item.value" :key="item.value">{{ item.label }}</i-option>
</i-select>
</div>
`,
data() {
return {
cityList: [],
modelA: [],
modelB: []
};
},
mounted() {
setTimeout(() => (this.cityList = options), 200);
}
vm = createVue({
template: `
<div>
<i-select v-model="modelA" multiple style="width:260px">
<i-option v-for="item in cityList" :value="item.value" :key="item.value">{{ item.label }}</i-option>
</i-select>
<i-select v-model="modelB" multiple style="width:260px">
<i-option v-for="item in cityList" :value="item.value" :key="item.value">{{ item.label }}</i-option>
</i-select>
</div>
`,
data() {
return {
cityList: [],
modelA: [],
modelB: []
};
},
mounted() {
setTimeout(() => (this.cityList = options), 200);
}
});
const [SelectA, SelectB] = vm.$children;
SelectA.toggleMenu(null, true);
SelectB.toggleMenu(null, true);
new Promise(resolve => {
const condition = function() {
const optionsA = SelectA.$el.querySelectorAll('.ivu-select-item');
const optionsB = SelectB.$el.querySelectorAll('.ivu-select-item');
return optionsA.length > 0 && optionsB.length > 0;
};
waitForIt(condition, resolve);
})
.then(() => {
// click in A options
const optionsA = SelectA.$el.querySelectorAll('.ivu-select-item');
optionsA[0].click();
return promissedTick(SelectA);
})
.then(() => {
expect(SelectA.value[0]).to.equal(options[0].value);
expect(SelectA.value.length).to.equal(1);
expect(SelectB.value.length).to.equal(0);
// click in B options
const optionsB = SelectB.$el.querySelectorAll('.ivu-select-item');
optionsB[1].click();
optionsB[2].click();
return promissedTick(SelectB);
})
.then(() => {
// lets check the values!
const getSelections = component => {
const tags = component.$el.querySelectorAll('.ivu-select-selection .ivu-tag');
return [...tags].map(el => el.textContent.trim()).join(',');
};
const selectAValue = getSelections(SelectA);
const selectBValue = getSelections(SelectB);
expect(selectAValue).to.equal(options[0].label);
expect(selectBValue).to.equal(options.slice(1, 3).map(obj => obj.label.trim()).join(','));
done();
}).catch(err => {
console.log(err);
done(false);
});
});
const [SelectA, SelectB] = vm.$children;
SelectA.toggleMenu();
SelectB.toggleMenu();
new Promise(resolve => {
const condition = function() {
const optionsA = SelectA.$el.querySelectorAll('.ivu-select-item');
const optionsB = SelectB.$el.querySelectorAll('.ivu-select-item');
return optionsA.length > 0 && optionsB.length > 0;
};
waitForIt(condition, resolve);
})
.then(() => {
// click in A options
const optionsA = SelectA.$el.querySelectorAll('.ivu-select-item');
optionsA[0].click();
return promissedTick(SelectA);
})
.then(() => {
expect(SelectA.value[0]).to.equal(options[0].value);
expect(SelectA.value.length).to.equal(1);
expect(SelectB.value.length).to.equal(0);
it('should create update model with value, and label when asked', done => {
const options = [
{value: 'beijing', label: 'Beijing'},
{value: 'stockholm', label: 'Stockholm'},
{value: 'lisboa', label: 'Lisboa'}
];
let onChangeValueA, onChangeValueB;
// click in B options
const optionsB = SelectB.$el.querySelectorAll('.ivu-select-item');
optionsB[1].click();
optionsB[2].click();
return promissedTick(SelectB);
})
.then(() => {
// lets check the values!
const getSelections = component => {
const tags = component.$el.querySelectorAll('.ivu-select-selection .ivu-tag');
return [...tags].map(el => el.textContent.trim()).join(',');
};
const selectAValue = getSelections(SelectA);
const selectBValue = getSelections(SelectB);
expect(selectAValue).to.equal(options[0].label);
expect(selectBValue).to.equal(options.slice(1, 3).map(obj => obj.label.trim()).join(','));
vm = createVue({
template: `
<div>
<i-select v-model="modelA" style="width:260px" @on-change="onChangeA">
<i-option v-for="item in cityList" :value="item.value" :key="item.value">{{ item.label }}</i-option>
</i-select>
<i-select v-model="modelB" label-in-value style="width:260px" @on-change="onChangeB">
<i-option v-for="item in cityList" :value="item.value" :key="item.value">{{ item.label }}</i-option>
</i-select>
</div>
`,
data() {
return {
cityList: options,
modelA: [],
modelB: []
};
},
methods: {
onChangeA(val){
onChangeValueA = val;
},
onChangeB(val){
onChangeValueB = val;
}
}
});
const [SelectA, SelectB] = vm.$children;
SelectA.toggleMenu(null, true);
SelectB.toggleMenu(null, true);
done();
});
});
new Promise(resolve => {
const condition = function() {
const optionsA = SelectA.$el.querySelectorAll('.ivu-select-item');
const optionsB = SelectB.$el.querySelectorAll('.ivu-select-item');
return optionsA.length > 0 && optionsB.length > 0;
};
waitForIt(condition, resolve);
})
.then(() => {
// click in A options
const optionsA = SelectA.$el.querySelectorAll('.ivu-select-item');
optionsA[0].click();
return promissedTick(SelectA);
})
.then(() => {
expect(vm.modelA).to.equal(options[0].value);
expect(onChangeValueA).to.equal(options[0].value);
// click in B options
const optionsB = SelectB.$el.querySelectorAll('.ivu-select-item');
optionsB[2].click();
return promissedTick(SelectB);
})
.then(() => {
expect(vm.modelB).to.equal(options[2].value);
expect(JSON.stringify(onChangeValueB)).to.equal(JSON.stringify(options[2]));
done();
});
});
});
describe('Public API', () => {
it('The "setQuery" method should behave as expected', (done) => {
const options = [
{value: 'beijing', label: 'Beijing'},
{value: 'stockholm', label: 'Stockholm'},
{value: 'lisboa', label: 'Lisboa'}
];
vm = createVue({
template: `
<Select v-model="value" filterable>
<Option v-for="item in options" :value="item.value" :key="item.value">{{ item.label }}</Option>
</Select>
`,
data() {
return {
value: '',
options: options
};
}
});
const [Select] = vm.$children;
Select.setQuery('i');
vm.$nextTick(() => {
const query = 'i';
const input = vm.$el.querySelector('.ivu-select-input');
expect(input.value).to.equal(query);
const renderedOptions = [...vm.$el.querySelectorAll('.ivu-select-item')].map(el => el.textContent);
const filteredOptions = options.filter(option => JSON.stringify(option).includes(query)).map(({label}) => label);
expect(JSON.stringify(renderedOptions)).to.equal(JSON.stringify(filteredOptions));
// reset query
// setQuery(null) should clear the select
Select.setQuery(null);
vm.$nextTick(() => {
const input = vm.$el.querySelector('.ivu-select-input');
expect(input.value).to.equal('');
const renderedOptions = [...vm.$el.querySelectorAll('.ivu-select-item')].map(el => el.textContent);
expect(JSON.stringify(renderedOptions)).to.equal(JSON.stringify(options.map(({label}) => label)));
done();
});
});
});
it('The "clearSingleSelect" method should behave as expected', (done) => {
// clearSingleSelect
const options = [
{value: 'beijing', label: 'Beijing'},
{value: 'stockholm', label: 'Stockholm'},
{value: 'lisboa', label: 'Lisboa'}
];
const preSelected = 'lisboa';
vm = createVue({
template: `
<Select v-model="value" clearable>
<Option v-for="item in options" :value="item.value" :key="item.value">{{ item.label }}</Option>
</Select>
`,
data() {
return {
value: preSelected,
options: options
};
}
});
const [Select] = vm.$children;
vm.$nextTick(() => {
expect(Select.publicValue).to.equal(preSelected);
Select.clearSingleSelect();
expect(typeof Select.publicValue).to.equal('undefined');
done();
});
});
});
describe('Performance tests', () => {
@ -278,7 +444,8 @@ describe('Select.vue', () => {
}
});
const condition = function() {
return vm.$children[0].options.length == manyLaterOptions.length;
const componentOptions = vm.$children[0].flatOptions;
return componentOptions && componentOptions.length === manyLaterOptions.length;
};
const callback = function() {
const end = +new Date();