mirror of
https://github.com/tenrok/vue-select.git
synced 2026-06-22 10:30:34 +03:00
fix: Add and update WAI-ARIA 1.1 combobox propeties (#1068)
* Add aria 1.1 combobox properties * Update dropdown test to account for v-show changes * test: fix dropdown tests * test: fix pointer scroll warnings * refactor: implement uniqueId function * fix: close #1072 remove empty button Co-authored-by: Jeff Sagal <sagalbot@gmail.com>
This commit is contained in:
+31
-23
@@ -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="Search for option">
|
||||||
|
|
||||||
<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="Deselect option" ref="deselectButtons">
|
<button v-if="multiple" :disabled="disabled" @click="deselect(option)" type="button" class="vs__deselect" :title="`Deselect ${getOptionLabel(option)}`" :aria-label="`Deselect ${getOptionLabel(option)}`" ref="deselectButtons">
|
||||||
<component :is="childComponents.Deselect" />
|
<component :is="childComponents.Deselect" />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
@@ -35,7 +35,8 @@
|
|||||||
@click="clearSelection"
|
@click="clearSelection"
|
||||||
type="button"
|
type="button"
|
||||||
class="vs__clear"
|
class="vs__clear"
|
||||||
title="Clear selection"
|
title="Clear Selected"
|
||||||
|
aria-label="Clear Selected"
|
||||||
ref="clearButton"
|
ref="clearButton"
|
||||||
>
|
>
|
||||||
<component :is="childComponents.Deselect" />
|
<component :is="childComponents.Deselect" />
|
||||||
@@ -52,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">Sorry, no matching options.</slot>
|
</slot>
|
||||||
</li>
|
</li>
|
||||||
|
<li v-if="!filteredOptions.length" class="vs__no-options" @mousedown.stop="">
|
||||||
|
<slot name="no-options">Sorry, no matching options.</slot>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,6 +84,7 @@
|
|||||||
import typeAheadPointer from '../mixins/typeAheadPointer'
|
import typeAheadPointer from '../mixins/typeAheadPointer'
|
||||||
import ajax from '../mixins/ajax'
|
import ajax from '../mixins/ajax'
|
||||||
import childComponents from './childComponents';
|
import childComponents from './childComponents';
|
||||||
|
import uniqueId from '../utility/uniqueId';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name VueSelect
|
* @name VueSelect
|
||||||
@@ -516,6 +522,7 @@
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
uid: uniqueId(),
|
||||||
search: '',
|
search: '',
|
||||||
open: false,
|
open: false,
|
||||||
isComposing: false,
|
isComposing: false,
|
||||||
@@ -985,10 +992,11 @@
|
|||||||
'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': 'Search for option',
|
'aria-labelledby': `vs${this.uid}__combobox`,
|
||||||
|
'aria-controls': `vs${this.uid}__listbox`,
|
||||||
|
'aria-activedescendant': this.typeAheadPointer > -1 ? `vs${this.uid}__option-${this.typeAheadPointer}` : '',
|
||||||
'ref': 'search',
|
'ref': 'search',
|
||||||
'role': 'combobox',
|
|
||||||
'type': 'search',
|
'type': 'search',
|
||||||
'autocomplete': this.autocomplete,
|
'autocomplete': this.autocomplete,
|
||||||
'value': this.search,
|
'value': this.search,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
let idCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dead simple unique ID implementation.
|
||||||
|
* Thanks lodash!
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
function uniqueId() {
|
||||||
|
return ++idCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default uniqueId;
|
||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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