Accessibility for CascadeSelect

pull/2770/head
Tuğçe Küçükoğlu 2022-07-08 12:28:23 +03:00
parent 638d132c21
commit 35556009ca
4 changed files with 204 additions and 97 deletions

View File

@ -1,8 +1,8 @@
<template>
<div ref="container" :class="containerClass" @click="onClick($event)">
<div class="p-hidden-accessible">
<input ref="focusInput" type="text" :id="inputId" readonly :disabled="disabled" @focus="onFocus" @blur="onBlur" @keydown="onKeyDown" :tabindex="tabindex"
aria-haspopup="listbox" :aria-expanded="overlayVisible" :aria-labelledby="ariaLabelledBy" />
<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-controls="listId" v-bind="inputProps" />
</div>
<span :class="labelClass">
<slot name="value" :value="modelValue" :placeholder="placeholder">
@ -16,9 +16,9 @@
</div>
<Portal :appendTo="appendTo">
<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">
<CascadeSelectSub :options="options" :selectionPath="selectionPath"
<CascadeSelectSub :id="listId" role="tree" :options="options" :selectionPath="selectionPath"
:optionLabel="optionLabel" :optionValue="optionValue" :level="0" :templates="$slots"
:optionGroupLabel="optionGroupLabel" :optionGroupChildren="optionGroupChildren"
@option-select="onOptionSelect" @optiongroup-select="onOptionGroupSelect" :dirty="dirty" :root="true" />
@ -30,7 +30,7 @@
</template>
<script>
import {ConnectedOverlayScrollHandler,ObjectUtils,DomHandler,ZIndexUtils} from 'primevue/utils';
import {ConnectedOverlayScrollHandler,ObjectUtils,DomHandler,ZIndexUtils,UniqueComponentId} from 'primevue/utils';
import OverlayEventBus from 'primevue/overlayeventbus';
import CascadeSelectSub from './CascadeSelectSub.vue';
import Portal from 'primevue/portal';
@ -71,7 +71,8 @@ export default {
loadingIcon: {
type: String,
default: 'pi pi-spinner pi-spin'
}
},
inputProps: null
},
outsideClickListener: null,
scrollHandler: null,
@ -260,7 +261,12 @@ export default {
this.overlay = el;
},
onKeyDown(event) {
switch(event.key) {
if (this.disabled || this.loading) {
event.preventDefault();
return;
}
switch(event.code) {
case 'Down':
case 'ArrowDown':
if (this.overlayVisible) {
@ -272,11 +278,14 @@ export default {
event.preventDefault();
break;
case 'Escape':
case 'Space':
if (this.overlayVisible) {
this.hide();
event.preventDefault();
}
else {
this.show();
}
event.preventDefault();
break;
case 'Tab':
@ -326,6 +335,9 @@ export default {
},
dropdownIconClass() {
return ['p-cascadeselect-trigger-icon', this.loading ? this.loadingIcon : 'pi pi-chevron-down'];
},
listId() {
return UniqueComponentId() + '_list';
}
},
components: {

View File

@ -1,8 +1,9 @@
<template>
<ul class="p-cascadeselect-panel p-cascadeselect-items" role="listbox" aria-orientation="horizontal">
<template v-for="(option,i) of options" :key="getOptionLabelToRender(option)">
<li :class="getItemClass(option)" role="none">
<div class="p-cascadeselect-item-content" @click="onOptionClick($event, option)" tabindex="0" @keydown="onKeyDown($event, option, i)" v-ripple>
<ul class="p-cascadeselect-panel p-cascadeselect-items" aria-orientation="horizontal">
<template v-for="(option,index) of options" :key="getOptionLabelToRender(option)">
<li :class="getItemClass(option)" role="treeitem" :aria-label="getOptionLabelToRender(option)" :aria-selected="isOptionActive(option)" :aria-expanded="isOptionActive(option)"
: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']"/>
<template v-else>
<span class="p-cascadeselect-item-text">{{getOptionLabelToRender(option)}}</span>
@ -11,7 +12,7 @@
</div>
<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"
: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>
</template>
</ul>
@ -117,7 +118,7 @@ export default {
return this.activeOption === option;
},
onKeyDown(event, option, index) {
switch (event.key) {
switch (event.code) {
case 'Down':
case 'ArrowDown':
var nextItem = this.$el.children[index + 1];
@ -157,6 +158,7 @@ export default {
break;
case 'Enter':
case 'Space':
this.onOptionClick(event, option);
break;
}

View File

@ -3,7 +3,7 @@
<div class="content-section introduction">
<div class="feature-intro">
<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>
<AppDemoActions />
</div>

View File

@ -25,87 +25,85 @@ import CascadeSelect from 'primevue/cascadeselect';
</code></pre>
<pre v-code.script><code>
data() {
data() &#123;
return &#123;
selectedCity: null,
countries: [
&#123;
name: 'Australia',
code: 'AU',
states: [
&#123;
name: 'New South Wales',
cities: [
&#123;cname: 'Sydney', code: 'A-SY'&#125;,
&#123;cname: 'Newcastle', code: 'A-NE'&#125;,
&#123;cname: 'Wollongong', code: 'A-WO'&#125;
]
&#125;,
&#123;
name: 'Queensland',
cities: [
&#123;cname: 'Brisbane', code: 'A-BR'&#125;,
&#123;cname: 'Townsville', code: 'A-TO'&#125;
]
&#125;,
]
&#125;,
&#123;
name: 'Canada',
code: 'CA',
states: [
&#123;
name: 'Quebec',
cities: [
&#123;cname: 'Montreal', code: 'C-MO'&#125;,
&#123;cname: 'Quebec City', code: 'C-QU'&#125;
]
&#125;,
&#123;
name: 'Ontario',
cities: [
&#123;cname: 'Ottawa', code: 'C-OT'&#125;,
&#123;cname: 'Toronto', code: 'C-TO'&#125;
]
&#125;,
]
&#125;,
&#123;
name: 'United States',
code: 'US',
states: [
&#123;
name: 'California',
cities: [
&#123;cname: 'Los Angeles', code: 'US-LA'&#125;,
&#123;cname: 'San Diego', code: 'US-SD'&#125;,
&#123;cname: 'San Francisco', code: 'US-SF'&#125;
]
&#125;,
&#123;
name: 'Florida',
cities: [
&#123;cname: 'Jacksonville', code: 'US-JA'&#125;,
&#123;cname: 'Miami', code: 'US-MI'&#125;,
&#123;cname: 'Tampa', code: 'US-TA'&#125;,
&#123;cname: 'Orlando', code: 'US-OR'&#125;
]
&#125;,
&#123;
name: 'Texas',
cities: [
&#123;cname: 'Austin', code: 'US-AU'&#125;,
&#123;cname: 'Dallas', code: 'US-DA'&#125;,
&#123;cname: 'Houston', code: 'US-HO'&#125;
]
&#125;
]
&#125;
]
&#125;
data() &#123;
return &#123;
selectedCity: null,
countries: [
&#123;
name: 'Australia',
code: 'AU',
states: [
&#123;
name: 'New South Wales',
cities: [
&#123;cname: 'Sydney', code: 'A-SY'&#125;,
&#123;cname: 'Newcastle', code: 'A-NE'&#125;,
&#123;cname: 'Wollongong', code: 'A-WO'&#125;
]
&#125;,
&#123;
name: 'Queensland',
cities: [
&#123;cname: 'Brisbane', code: 'A-BR'&#125;,
&#123;cname: 'Townsville', code: 'A-TO'&#125;
]
&#125;,
]
&#125;,
&#123;
name: 'Canada',
code: 'CA',
states: [
&#123;
name: 'Quebec',
cities: [
&#123;cname: 'Montreal', code: 'C-MO'&#125;,
&#123;cname: 'Quebec City', code: 'C-QU'&#125;
]
&#125;,
&#123;
name: 'Ontario',
cities: [
&#123;cname: 'Ottawa', code: 'C-OT'&#125;,
&#123;cname: 'Toronto', code: 'C-TO'&#125;
]
&#125;,
]
&#125;,
&#123;
name: 'United States',
code: 'US',
states: [
&#123;
name: 'California',
cities: [
&#123;cname: 'Los Angeles', code: 'US-LA'&#125;,
&#123;cname: 'San Diego', code: 'US-SD'&#125;,
&#123;cname: 'San Francisco', code: 'US-SF'&#125;
]
&#125;,
&#123;
name: 'Florida',
cities: [
&#123;cname: 'Jacksonville', code: 'US-JA'&#125;,
&#123;cname: 'Miami', code: 'US-MI'&#125;,
&#123;cname: 'Tampa', code: 'US-TA'&#125;,
&#123;cname: 'Orlando', code: 'US-OR'&#125;
]
&#125;,
&#123;
name: 'Texas',
cities: [
&#123;cname: 'Austin', code: 'US-AU'&#125;,
&#123;cname: 'Dallas', code: 'US-DA'&#125;,
&#123;cname: 'Houston', code: 'US-HO'&#125;
]
&#125;
]
&#125;
]
&#125;
}
@ -360,6 +358,101 @@ data() {
</table>
</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>
&lt;span id="dd1"&gt;Options&lt;/span&gt;
&lt;CascadeSelect aria-labelledby="dd1" /&gt;
&lt;CascadeSelect aria-label="Options" /&gt;
</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>
<p>None.</p>
</AppDoc>