Accessibility for CascadeSelect
parent
638d132c21
commit
35556009ca
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="container" :class="containerClass" @click="onClick($event)">
|
<div ref="container" :class="containerClass" @click="onClick($event)">
|
||||||
<div class="p-hidden-accessible">
|
<div class="p-hidden-accessible">
|
||||||
<input ref="focusInput" type="text" :id="inputId" readonly :disabled="disabled" @focus="onFocus" @blur="onBlur" @keydown="onKeyDown" :tabindex="tabindex"
|
<input ref="focusInput" role="combobox" type="text" :id="inputId" readonly :disabled="disabled" @focus="onFocus" @blur="onBlur" @keydown="onKeyDown" :tabindex="tabindex"
|
||||||
aria-haspopup="listbox" :aria-expanded="overlayVisible" :aria-labelledby="ariaLabelledBy" />
|
aria-haspopup="listbox" :aria-expanded="overlayVisible" :aria-labelledby="ariaLabelledBy" :aria-controls="listId" v-bind="inputProps" />
|
||||||
</div>
|
</div>
|
||||||
<span :class="labelClass">
|
<span :class="labelClass">
|
||||||
<slot name="value" :value="modelValue" :placeholder="placeholder">
|
<slot name="value" :value="modelValue" :placeholder="placeholder">
|
||||||
|
@ -16,9 +16,9 @@
|
||||||
</div>
|
</div>
|
||||||
<Portal :appendTo="appendTo">
|
<Portal :appendTo="appendTo">
|
||||||
<transition name="p-connected-overlay" @enter="onOverlayEnter" @leave="onOverlayLeave" @after-leave="onOverlayAfterLeave">
|
<transition name="p-connected-overlay" @enter="onOverlayEnter" @leave="onOverlayLeave" @after-leave="onOverlayAfterLeave">
|
||||||
<div :ref="overlayRef" :class="panelStyleClass" v-if="overlayVisible" @click="onOverlayClick">
|
<div :ref="overlayRef" :class="panelStyleClass" v-if="overlayVisible" @click="onOverlayClick" role="group">
|
||||||
<div class="p-cascadeselect-items-wrapper">
|
<div class="p-cascadeselect-items-wrapper">
|
||||||
<CascadeSelectSub :options="options" :selectionPath="selectionPath"
|
<CascadeSelectSub :id="listId" role="tree" :options="options" :selectionPath="selectionPath"
|
||||||
:optionLabel="optionLabel" :optionValue="optionValue" :level="0" :templates="$slots"
|
:optionLabel="optionLabel" :optionValue="optionValue" :level="0" :templates="$slots"
|
||||||
:optionGroupLabel="optionGroupLabel" :optionGroupChildren="optionGroupChildren"
|
:optionGroupLabel="optionGroupLabel" :optionGroupChildren="optionGroupChildren"
|
||||||
@option-select="onOptionSelect" @optiongroup-select="onOptionGroupSelect" :dirty="dirty" :root="true" />
|
@option-select="onOptionSelect" @optiongroup-select="onOptionGroupSelect" :dirty="dirty" :root="true" />
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {ConnectedOverlayScrollHandler,ObjectUtils,DomHandler,ZIndexUtils} from 'primevue/utils';
|
import {ConnectedOverlayScrollHandler,ObjectUtils,DomHandler,ZIndexUtils,UniqueComponentId} from 'primevue/utils';
|
||||||
import OverlayEventBus from 'primevue/overlayeventbus';
|
import OverlayEventBus from 'primevue/overlayeventbus';
|
||||||
import CascadeSelectSub from './CascadeSelectSub.vue';
|
import CascadeSelectSub from './CascadeSelectSub.vue';
|
||||||
import Portal from 'primevue/portal';
|
import Portal from 'primevue/portal';
|
||||||
|
@ -71,7 +71,8 @@ export default {
|
||||||
loadingIcon: {
|
loadingIcon: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'pi pi-spinner pi-spin'
|
default: 'pi pi-spinner pi-spin'
|
||||||
}
|
},
|
||||||
|
inputProps: null
|
||||||
},
|
},
|
||||||
outsideClickListener: null,
|
outsideClickListener: null,
|
||||||
scrollHandler: null,
|
scrollHandler: null,
|
||||||
|
@ -260,7 +261,12 @@ export default {
|
||||||
this.overlay = el;
|
this.overlay = el;
|
||||||
},
|
},
|
||||||
onKeyDown(event) {
|
onKeyDown(event) {
|
||||||
switch(event.key) {
|
if (this.disabled || this.loading) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(event.code) {
|
||||||
case 'Down':
|
case 'Down':
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
if (this.overlayVisible) {
|
if (this.overlayVisible) {
|
||||||
|
@ -272,11 +278,14 @@ export default {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Escape':
|
case 'Space':
|
||||||
if (this.overlayVisible) {
|
if (this.overlayVisible) {
|
||||||
this.hide();
|
this.hide();
|
||||||
event.preventDefault();
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Tab':
|
case 'Tab':
|
||||||
|
@ -326,6 +335,9 @@ export default {
|
||||||
},
|
},
|
||||||
dropdownIconClass() {
|
dropdownIconClass() {
|
||||||
return ['p-cascadeselect-trigger-icon', this.loading ? this.loadingIcon : 'pi pi-chevron-down'];
|
return ['p-cascadeselect-trigger-icon', this.loading ? this.loadingIcon : 'pi pi-chevron-down'];
|
||||||
|
},
|
||||||
|
listId() {
|
||||||
|
return UniqueComponentId() + '_list';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<ul class="p-cascadeselect-panel p-cascadeselect-items" role="listbox" aria-orientation="horizontal">
|
<ul class="p-cascadeselect-panel p-cascadeselect-items" aria-orientation="horizontal">
|
||||||
<template v-for="(option,i) of options" :key="getOptionLabelToRender(option)">
|
<template v-for="(option,index) of options" :key="getOptionLabelToRender(option)">
|
||||||
<li :class="getItemClass(option)" role="none">
|
<li :class="getItemClass(option)" role="treeitem" :aria-label="getOptionLabelToRender(option)" :aria-selected="isOptionActive(option)" :aria-expanded="isOptionActive(option)"
|
||||||
<div class="p-cascadeselect-item-content" @click="onOptionClick($event, option)" tabindex="0" @keydown="onKeyDown($event, option, i)" v-ripple>
|
:aria-setsize="options.length" :aria-posinset="index + 1" :aria-level="level + 1">
|
||||||
|
<div class="p-cascadeselect-item-content" @click="onOptionClick($event, option)" tabindex="0" @keydown="onKeyDown($event, option, index)" v-ripple>
|
||||||
<component :is="templates['option']" :option="option" v-if="templates['option']"/>
|
<component :is="templates['option']" :option="option" v-if="templates['option']"/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span class="p-cascadeselect-item-text">{{getOptionLabelToRender(option)}}</span>
|
<span class="p-cascadeselect-item-text">{{getOptionLabelToRender(option)}}</span>
|
||||||
|
@ -11,7 +12,7 @@
|
||||||
</div>
|
</div>
|
||||||
<CascadeSelectSub v-if="isOptionGroup(option) && isOptionActive(option)" class="p-cascadeselect-sublist" :selectionPath="selectionPath" :options="getOptionGroupChildren(option)"
|
<CascadeSelectSub v-if="isOptionGroup(option) && isOptionActive(option)" class="p-cascadeselect-sublist" :selectionPath="selectionPath" :options="getOptionGroupChildren(option)"
|
||||||
:optionLabel="optionLabel" :optionValue="optionValue" :level="level + 1" @option-select="onOptionSelect" @optiongroup-select="onOptionGroupSelect"
|
:optionLabel="optionLabel" :optionValue="optionValue" :level="level + 1" @option-select="onOptionSelect" @optiongroup-select="onOptionGroupSelect"
|
||||||
:optionGroupLabel="optionGroupLabel" :optionGroupChildren="optionGroupChildren" :parentActive="isOptionActive(option)" :dirty="dirty" :templates="templates"/>
|
:optionGroupLabel="optionGroupLabel" :optionGroupChildren="optionGroupChildren" :parentActive="isOptionActive(option)" :dirty="dirty" :templates="templates" :aria-level="level + 2"/>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -117,7 +118,7 @@ export default {
|
||||||
return this.activeOption === option;
|
return this.activeOption === option;
|
||||||
},
|
},
|
||||||
onKeyDown(event, option, index) {
|
onKeyDown(event, option, index) {
|
||||||
switch (event.key) {
|
switch (event.code) {
|
||||||
case 'Down':
|
case 'Down':
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
var nextItem = this.$el.children[index + 1];
|
var nextItem = this.$el.children[index + 1];
|
||||||
|
@ -157,6 +158,7 @@ export default {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
|
case 'Space':
|
||||||
this.onOptionClick(event, option);
|
this.onOptionClick(event, option);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="content-section introduction">
|
<div class="content-section introduction">
|
||||||
<div class="feature-intro">
|
<div class="feature-intro">
|
||||||
<h1>CascadeSelect</h1>
|
<h1>CascadeSelect</h1>
|
||||||
<p>CascadeSelect displays a nested structure of options.</p>
|
<p>CascadeSelect is a form component to select a value from a nested structure of options.</p>
|
||||||
</div>
|
</div>
|
||||||
<AppDemoActions />
|
<AppDemoActions />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,7 +25,6 @@ import CascadeSelect from 'primevue/cascadeselect';
|
||||||
</code></pre>
|
</code></pre>
|
||||||
|
|
||||||
<pre v-code.script><code>
|
<pre v-code.script><code>
|
||||||
data() {
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selectedCity: null,
|
selectedCity: null,
|
||||||
|
@ -106,7 +105,6 @@ data() {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</code></pre>
|
</code></pre>
|
||||||
|
@ -360,6 +358,101 @@ data() {
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h5>Accessibility</h5>
|
||||||
|
<DevelopmentSection>
|
||||||
|
<h6>Screen Reader</h6>
|
||||||
|
<p>Value to describe the component can either be provided with <i>aria-labelledby</i> or <i>aria-label</i> props. The cascadeselect element has a <i>combobox</i> role
|
||||||
|
in addition to <i>aria-haspopup</i> and <i>aria-expanded</i> attributes. The relation between the combobox and the popup is created with <i>aria-controls</i> that refers to the id of the popup.</p>
|
||||||
|
<p>The popup list has an id that refers to the <i>aria-controls</i> attribute of the <i>combobox</i> element and uses <i>tree</i> as the role. Each list item has a <i>treeitem</i> role along with <i>aria-label</i>, <i>aria-selected</i> and <i>aria-expanded</i> attributes. The container
|
||||||
|
element of a treenode has the <i>group</i> role. The <i>aria-setsize</i>, <i>aria-posinset</i> and <i>aria-level</i> attributes are calculated implicitly and added to each treeitem.</p>
|
||||||
|
|
||||||
|
<p>If filtering is enabled, <i>filterInputProps</i> can be defined to give <i>aria-*</i> props to the filter input element.</p>
|
||||||
|
|
||||||
|
<pre v-code><code>
|
||||||
|
<span id="dd1">Options</span>
|
||||||
|
<CascadeSelect aria-labelledby="dd1" />
|
||||||
|
|
||||||
|
<CascadeSelect aria-label="Options" />
|
||||||
|
|
||||||
|
</code></pre>
|
||||||
|
|
||||||
|
<h6>Closed State Keyboard Support</h6>
|
||||||
|
<div class="doc-tablewrapper">
|
||||||
|
<table class="doc-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Key</th>
|
||||||
|
<th>Function</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><i>tab</i></td>
|
||||||
|
<td>Moves focus to the cascadeselect element.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i>space</i></td>
|
||||||
|
<td>Opens the popup and moves visual focus to the selected option, if there is none then first option receives the focus.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i>down arrow</i></td>
|
||||||
|
<td>Opens the popup and moves visual focus to the selected option, if there is none then first option receives the focus.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6>Popup Keyboard Support</h6>
|
||||||
|
<div class="doc-tablewrapper">
|
||||||
|
<table class="doc-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Key</th>
|
||||||
|
<th>Function</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><i>tab</i></td>
|
||||||
|
<td>Hides the popup and moves focus to the next tabbable element.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i>tab</i></td>
|
||||||
|
<td>Hides the popup and moves focus to the previous tabbable element.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i>enter</i></td>
|
||||||
|
<td>Selects the focused option and closes the popup.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i>space</i></td>
|
||||||
|
<td>Selects the focused option and closes the popup.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i>escape</i></td>
|
||||||
|
<td>Closes the popup, moves focus to the cascadeselect element.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i>down arrow</i></td>
|
||||||
|
<td>Moves focus to the next option.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i>up arrow</i></td>
|
||||||
|
<td>Moves focus to the previous option.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i>right arrow</i></td>
|
||||||
|
<td>If option is closed, opens the option otherwise moves focus to the first child option.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i>left arrow</i></td>
|
||||||
|
<td>If option is open, closes the option otherwise moves focus to the parent option.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</DevelopmentSection>
|
||||||
|
|
||||||
<h5>Dependencies</h5>
|
<h5>Dependencies</h5>
|
||||||
<p>None.</p>
|
<p>None.</p>
|
||||||
</AppDoc>
|
</AppDoc>
|
||||||
|
|
Loading…
Reference in New Issue