Accessibility for TreeSelect and Tree
parent
35a8bf636a
commit
cc4517a8f9
|
@ -41,12 +41,6 @@ const TreeSelectProps = [
|
|||
default: "null",
|
||||
description: "Identifier of the underlying input element."
|
||||
},
|
||||
{
|
||||
name: "ariaLabelledBy",
|
||||
type: "string",
|
||||
default: "null",
|
||||
description: "Establishes relationships between the component and label(s) where its value should be one or more element IDs."
|
||||
},
|
||||
{
|
||||
name: "selectionMode",
|
||||
type: "string",
|
||||
|
|
|
@ -10,9 +10,9 @@
|
|||
@keydown="onFilterKeydown" v-model="filterValue" />
|
||||
<span class="p-tree-filter-icon pi pi-search"></span>
|
||||
</div>
|
||||
<div class="p-tree-wrapper" :style="{maxHeight: scrollHeight}">
|
||||
<ul class="p-tree-container" role="tree">
|
||||
<TreeNode v-for="node of valueToRender" :key="node.key" :node="node" :templates="$slots"
|
||||
<div class="p-tree-wrapper" role="tree" :style="{maxHeight: scrollHeight}">
|
||||
<ul class="p-tree-container" role="group">
|
||||
<TreeNode v-for="(node, index) of valueToRender" :key="node.key" :node="node" :templates="$slots" :level="level + 1" :index="index"
|
||||
:expandedKeys="d_expandedKeys" @node-toggle="onNodeToggle" @node-click="onNodeClick"
|
||||
:selectionMode="selectionMode" :selectionKeys="selectionKeys" @checkbox-change="onCheckboxChange"></TreeNode>
|
||||
</ul>
|
||||
|
@ -79,6 +79,10 @@ export default {
|
|||
scrollHeight: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<li :class="containerClass">
|
||||
<li :class="containerClass" role="treeitem" :aria-label="label(node)" :aria-selected="selected" :aria-expanded="expanded"
|
||||
:aria-setsize="node.children ? node.children.length : 0" :aria-posinset="index + 1" :aria-level="level">
|
||||
<div :class="contentClass" tabindex="0" role="treeitem" :aria-expanded="expanded"
|
||||
@click="onClick" @keydown="onKeyDown" @touchend="onTouchEnd" :style="node.style">
|
||||
<button type="button" class="p-tree-toggler p-link" @click="toggle" tabindex="-1" v-ripple>
|
||||
|
@ -17,7 +18,7 @@
|
|||
</span>
|
||||
</div>
|
||||
<ul class="p-treenode-children" role="group" v-if="hasChildren && expanded">
|
||||
<TreeNode v-for="childNode of node.children" :key="childNode.key" :node="childNode" :templates="templates"
|
||||
<TreeNode v-for="childNode of node.children" :key="childNode.key" :node="childNode" :templates="templates" :level="level + 1"
|
||||
:expandedKeys="expandedKeys" @node-toggle="onChildNodeToggle" @node-click="onChildNodeClick"
|
||||
:selectionMode="selectionMode" :selectionKeys="selectionKeys"
|
||||
@checkbox-change="propagateUp" />
|
||||
|
@ -52,6 +53,14 @@ export default {
|
|||
templates: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
nodeTouched: false,
|
||||
|
@ -92,9 +101,8 @@ export default {
|
|||
onKeyDown(event) {
|
||||
const nodeElement = event.target.parentElement;
|
||||
|
||||
switch (event.which) {
|
||||
//down arrow
|
||||
case 40:
|
||||
switch (event.code) {
|
||||
case 'ArrowDown':
|
||||
var listElement = nodeElement.children[1];
|
||||
if (listElement) {
|
||||
this.focusNode(listElement.children[0]);
|
||||
|
@ -111,12 +119,9 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
//up arrow
|
||||
case 38:
|
||||
case 'ArrowUp':
|
||||
if (nodeElement.previousElementSibling) {
|
||||
this.focusNode(this.findLastVisibleDescendant(nodeElement.previousElementSibling));
|
||||
}
|
||||
|
@ -126,28 +131,24 @@ export default {
|
|||
this.focusNode(parentNodeElement);
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
//right-left arrows
|
||||
case 37:
|
||||
case 39:
|
||||
case 'ArrowRight':
|
||||
case 'ArrowLeft':
|
||||
this.$emit('node-toggle', this.node);
|
||||
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
//enter
|
||||
case 13:
|
||||
case 'Enter':
|
||||
case 'Space':
|
||||
this.onClick(event);
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
default:
|
||||
//no op
|
||||
break;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
toggleCheckbox() {
|
||||
let _selectionKeys = this.selectionKeys ? {...this.selectionKeys} : {};
|
||||
|
|
|
@ -38,10 +38,6 @@ export interface TreeSelectProps {
|
|||
* Identifier of the underlying input element.
|
||||
*/
|
||||
inputId?: string | undefined;
|
||||
/**
|
||||
* Establishes relationships between the component and label(s) where its value should be one or more element IDs.
|
||||
*/
|
||||
ariaLabelledBy?: string | undefined;
|
||||
/**
|
||||
* Defines the selection mode.
|
||||
* @see TreeSelectSelectionModeType
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div ref="container" :class="containerClass" @click="onClick">
|
||||
<div class="p-hidden-accessible">
|
||||
<input ref="focusInput" type="text" role="listbox" :id="inputId" readonly :disabled="disabled" @focus="onFocus" @blur="onBlur" @keydown="onKeyDown" :tabindex="tabindex"
|
||||
aria-haspopup="true" :aria-expanded="overlayVisible" :aria-labelledby="ariaLabelledBy"/>
|
||||
<input ref="focusInput" type="text" role="combobox" :id="inputId" readonly :disabled="disabled" :tabindex="tabindex" :aria-labelledby="ariaLabelledby" :aria-label="ariaLabel"
|
||||
aria-haspopup="listbox" :aria-expanded="overlayVisible" :aria-controls="listId" @focus="onFocus($event)" @blur="onBlur($event)" @keydown="onKeyDown($event)" v-bind="inputProps" />
|
||||
</div>
|
||||
<div class="p-treeselect-label-container">
|
||||
<div :class="labelClass">
|
||||
|
@ -19,7 +19,7 @@
|
|||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-treeselect-trigger">
|
||||
<div class="p-treeselect-trigger" role="button" aria-haspopup="listbox" :aria-expanded="overlayVisible">
|
||||
<slot name="indicator">
|
||||
<span class="p-treeselect-trigger-icon pi pi-chevron-down"></span>
|
||||
</slot>
|
||||
|
@ -29,10 +29,10 @@
|
|||
<div :ref="overlayRef" v-if="overlayVisible" @click="onOverlayClick" :class="panelStyleClass">
|
||||
<slot name="header" :value="modelValue" :options="options"></slot>
|
||||
<div class="p-treeselect-items-wrapper" :style="{'max-height': scrollHeight}">
|
||||
<TSTree :value="options" :selectionMode="selectionMode" @update:selectionKeys="onSelectionChange" :selectionKeys="modelValue"
|
||||
<TSTree :id="listId" :value="options" :selectionMode="selectionMode" @update:selectionKeys="onSelectionChange" :selectionKeys="modelValue"
|
||||
:expandedKeys="expandedKeys" @update:expandedKeys="onNodeToggle" :metaKeySelection="metaKeySelection"
|
||||
@node-expand="$emit('node-expand', $event)" @node-collapse="$emit('node-collapse', $event)"
|
||||
@node-select="onNodeSelect" @node-unselect="onNodeUnselect" />
|
||||
@node-select="onNodeSelect" @node-unselect="onNodeUnselect" :level="0" />
|
||||
<div v-if="emptyOptions" class="p-treeselect-empty-message">
|
||||
<slot name="empty">{{emptyMessageText}}</slot>
|
||||
</div>
|
||||
|
@ -45,7 +45,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import {ConnectedOverlayScrollHandler,DomHandler,ZIndexUtils} from 'primevue/utils';
|
||||
import {ConnectedOverlayScrollHandler,DomHandler,ZIndexUtils,UniqueComponentId} from 'primevue/utils';
|
||||
import OverlayEventBus from 'primevue/overlayeventbus';
|
||||
import Tree from 'primevue/tree';
|
||||
import Ripple from 'primevue/ripple';
|
||||
|
@ -53,7 +53,7 @@ import Portal from 'primevue/portal';
|
|||
|
||||
export default {
|
||||
name: 'TreeSelect',
|
||||
emits: ['update:modelValue', 'before-show', 'before-hide', 'change', 'show', 'hide', 'node-select', 'node-unselect', 'node-expand', 'node-collapse'],
|
||||
emits: ['update:modelValue', 'before-show', 'before-hide', 'change', 'show', 'hide', 'node-select', 'node-unselect', 'node-expand', 'node-collapse', 'focus', 'blur'],
|
||||
props: {
|
||||
modelValue: null,
|
||||
options: Array,
|
||||
|
@ -65,7 +65,7 @@ export default {
|
|||
disabled: Boolean,
|
||||
tabindex: String,
|
||||
inputId: String,
|
||||
ariaLabelledBy: null,
|
||||
inputProps: null,
|
||||
selectionMode: {
|
||||
type: String,
|
||||
default: 'single'
|
||||
|
@ -89,6 +89,14 @@ export default {
|
|||
metaKeySelection: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
'aria-labelledby': {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
'aria-label': {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -143,11 +151,13 @@ export default {
|
|||
this.$emit('before-hide');
|
||||
this.overlayVisible = false;
|
||||
},
|
||||
onFocus() {
|
||||
onFocus(event) {
|
||||
this.focused = true;
|
||||
this.$emit('focus', event);
|
||||
},
|
||||
onBlur() {
|
||||
onBlur(event) {
|
||||
this.focused = false;
|
||||
this.$emit('blur', event);
|
||||
},
|
||||
onClick(event) {
|
||||
if (!this.disabled && (!this.overlay || !this.overlay.contains(event.target)) && !DomHandler.hasClass(event.target, 'p-treeselect-close')) {
|
||||
|
@ -178,37 +188,38 @@ export default {
|
|||
this.expandedKeys = keys;
|
||||
},
|
||||
onKeyDown(event) {
|
||||
switch(event.which) {
|
||||
//down
|
||||
case 40:
|
||||
if (!this.overlayVisible && event.altKey) {
|
||||
this.show();
|
||||
event.preventDefault();
|
||||
switch(event.code) {
|
||||
case 'Down':
|
||||
case 'ArrowDown':
|
||||
if (this.overlayVisible) {
|
||||
if (DomHandler.findSingle(this.overlay, '.p-highlight')) {
|
||||
DomHandler.findSingle(this.overlay, '.p-highlight').focus();
|
||||
}
|
||||
else DomHandler.findSingle(this.overlay, '.p-treenode').children[0].focus();
|
||||
}
|
||||
else {
|
||||
this.show();
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
//space
|
||||
case 32:
|
||||
case 'Space':
|
||||
case 'Enter':
|
||||
if (!this.overlayVisible) {
|
||||
this.show();
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
|
||||
//enter and escape
|
||||
case 13:
|
||||
case 27:
|
||||
case 'Escape':
|
||||
case 'Tab':
|
||||
if (this.overlayVisible) {
|
||||
this.hide();
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
|
||||
//tab
|
||||
case 9:
|
||||
this.hide();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -415,6 +426,9 @@ export default {
|
|||
},
|
||||
emptyOptions() {
|
||||
return !this.options || this.options.length === 0;
|
||||
},
|
||||
listId() {
|
||||
return UniqueComponentId() + '_list';
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
|
@ -292,12 +292,6 @@ data() {
|
|||
<td>null</td>
|
||||
<td>Identifier of the underlying input element.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ariaLabelledBy</td>
|
||||
<td>string</td>
|
||||
<td>null</td>
|
||||
<td>Establishes relationships between the component and label(s) where its value should be one or more element IDs.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>selectionMode</td>
|
||||
<td>string</td>
|
||||
|
@ -501,6 +495,100 @@ 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 treeselect 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.
|
||||
In checkbox selection, <i>aria-checked</i> is used instead of <i>aria-selected</i>. Checkbox and toggle icons are hidden from screen readers as their parent element with <i>treeitem</i> role and attributes are used instead for readers and keyboard support.
|
||||
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>
|
||||
|
||||
<pre v-code><code>
|
||||
<span id="dd1">Options</span>
|
||||
<TreeSelect aria-labelledby="dd1" />
|
||||
|
||||
<TreeSelect 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 treeselect element.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i>space</i></td>
|
||||
<td>Opens the popup and moves visual focus to the selected treenode, if there is none then first treenode 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>Moves focus to the next focusable element in the popup, if there is none then first focusable element receives the focus.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i>shift</i> + <i>tab</i></td>
|
||||
<td>Moves focus to the previous focusable element in the popup, if there is none then last focusable element receives the focus.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i>enter</i></td>
|
||||
<td>Selects the focused option, closes the popup if selection mode is single.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i>space</i></td>
|
||||
<td>Selects the focused option, closes the popup if selection mode is single.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i>escape</i></td>
|
||||
<td>Closes the popup, moves focus to the treeselect element.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i>down arrow</i></td>
|
||||
<td>Moves focus to the next treenode.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i>up arrow</i></td>
|
||||
<td>Moves focus to the previous treenode.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i>right arrow</i></td>
|
||||
<td>If node is closed, opens the node otherwise moves focus to the first child node.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i>left arrow</i></td>
|
||||
<td>If node is open, closes the node otherwise moves focus to the parent node.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</DevelopmentSection>
|
||||
|
||||
<h5>Dependencies</h5>
|
||||
<p>None.</p>
|
||||
</AppDoc>
|
||||
|
|
Loading…
Reference in New Issue