Accessibility for Chips

pull/2809/head
Tuğçe Küçükoğlu 2022-07-21 21:45:24 +03:00
parent 091d26e7e3
commit 081b9414f9
4 changed files with 170 additions and 36 deletions

View File

@ -30,16 +30,28 @@ const ChipsProps = [
description: "Whether to allow duplicate values or not."
},
{
name: "class",
name: "inputId",
type: "string",
default: "null",
description: "Style class of the component."
description: "Identifier of the focus input to match a label defined for the chips."
},
{
name: "style",
type: "any",
name: "disabled",
type: "boolean",
default: "false",
description: "When present, it specifies that the element should be disabled."
},
{
name: "'aria-labelledby'",
type: "string",
default: "null",
description: "Inline of the component."
description: "Establishes relationships between the component and label(s) where its value should be one or more element IDs."
},
{
name: "'aria-label'",
type: "string",
default: "null",
description: "Establishes a string value that labels the component."
}
];

View File

@ -40,13 +40,25 @@ export interface ChipsProps {
*/
separator?: string | undefined;
/**
* Style class of the component input field.
* Identifier of the focus input to match a label defined for the chips.
*/
class?: any;
inputId?: string | undefined;
/**
* Inline style of the component.
*
*/
style?: any;
inputProps?: object | undefined;
/**
* When present, it specifies that the element should be disabled.
*/
disabled?: boolean | undefined;
/**
* Establishes relationships between the component and label(s) where its value should be one or more element IDs.
*/
'aria-labelledby'?: string | undefined;
/**
* Establishes a string value that labels the component.
*/
'aria-label'?: string | undefined;
}
export interface ChipsSlots {

View File

@ -1,15 +1,15 @@
<template>
<div :class="containerClass" :style="style">
<ul :class="['p-inputtext p-chips-multiple-container', {'p-disabled': $attrs.disabled, 'p-focus': focused}]" @click="onWrapperClick()">
<li v-for="(val,i) of modelValue" :key="`${i}_${val}`" class="p-chips-token">
<slot name="chip" :value="val">
<div :class="containerClass">
<ul :class="['p-inputtext p-chips-multiple-container', {'p-disabled': disabled, 'p-focus': focused}]" role="listbox" aria-orientation="horizontal" @click="onWrapperClick()">
<li v-for="(val,i) of modelValue" :key="`${i}_${val}`" role="option" :class="['p-chips-token', {'p-focus': focusedIndex === i}]">
<slot name="chip" :value="val" :aria-label="val">
<span class="p-chips-token-label">{{val}}</span>
</slot>
<span class="p-chips-token-icon pi pi-times-circle" @click="removeItem($event, i)"></span>
</li>
<li class="p-chips-input-token">
<input ref="input" type="text" v-bind="$attrs" @focus="onFocus" @blur="onBlur($event)" @input="onInput" @keydown="onKeyDown($event)" @paste="onPaste($event)"
:disabled="$attrs.disabled || maxedOut">
<input ref="input" type="text" :id="inputId" :disabled="disabled || maxedOut" :aria-labelledby="ariaLabelledby" :aria-label="ariaLabel"
@focus="onFocus($event)" @blur="onBlur($event)" @input="onInput" @keydown="onKeyDown($event)" @paste="onPaste($event)" v-bind="inputProps">
</li>
</ul>
</div>
@ -18,8 +18,7 @@
<script>
export default {
name: 'Chips',
inheritAttrs: false,
emits: ['update:modelValue', 'add', 'remove'],
emits: ['update:modelValue', 'add', 'remove', 'focus', 'blur'],
props: {
modelValue: {
type: Array,
@ -41,13 +40,26 @@ export default {
type: Boolean,
default: true
},
class: null,
style: null
inputId: null,
inputProps: null,
disabled: {
type: Boolean,
default: false
},
'aria-labelledby': {
type: String,
default: null
},
'aria-label': {
type: String,
default: null
}
},
data() {
return {
inputValue: null,
focused: false
focused: false,
focusedIndex: null
};
},
methods: {
@ -57,36 +69,55 @@ export default {
onInput(event) {
this.inputValue = event.target.value;
},
onFocus() {
onFocus(event) {
this.focused = true;
this.$emit('focus', event);
},
onBlur(event) {
this.focused = false;
this.focusedIndex = null;
if (this.addOnBlur) {
this.addItem(event, event.target.value, false);
}
this.$emit('blur', event);
},
onKeyDown(event) {
const inputValue = event.target.value;
switch(event.which) {
//backspace
case 8:
switch(event.code) {
case 'Backspace':
if (inputValue.length === 0 && this.modelValue && this.modelValue.length > 0) {
this.removeItem(event, this.modelValue.length - 1);
if (this.focusedIndex !== null) {
this.removeItem(event, this.focusedIndex);
}
else this.removeItem(event, this.modelValue.length - 1);
}
break;
//enter
case 13:
case 'Enter':
if (inputValue && inputValue.trim().length && !this.maxedOut) {
this.addItem(event, inputValue, true);
}
break;
case 'ArrowLeft':
if (inputValue.length === 0 && this.modelValue && this.modelValue.length > 0) {
if (this.focusedIndex === 0 || this.focusedIndex === null) this.focusedIndex = this.modelValue.length-1;
else this.focusedIndex--;
}
break;
case 'ArrowRight':
if (inputValue.length === 0 && this.modelValue && this.modelValue.length > 0) {
if (this.focusedIndex === null || this.focusedIndex === this.modelValue.length-1) this.focusedIndex = 0;
else this.focusedIndex++;
}
break;
default:
if (this.separator) {
if (this.separator === ',' && (event.which === 188 || event.which === 110)) {
if (this.separator === ',' && event.key === ',') {
this.addItem(event, inputValue, true);
}
}
@ -128,12 +159,13 @@ export default {
}
},
removeItem(event, index) {
if (this.$attrs.disabled) {
if (this.disabled) {
return;
}
let values = [...this.modelValue];
const removedItem = values.splice(index, 1);
if (values.length === 0) this.focusedIndex = null;
this.$emit('update:modelValue', values);
this.$emit('remove', {
originalEvent: event,
@ -146,7 +178,7 @@ export default {
return this.max && this.modelValue && this.max === this.modelValue.length;
},
containerClass() {
return ['p-chips p-component p-inputwrapper', this.class, {
return ['p-chips p-component p-inputwrapper', {
'p-inputwrapper-filled': ((this.modelValue && this.modelValue.length) || (this.inputValue && this.inputValue.length)),
'p-inputwrapper-focus': this.focused
}];
@ -201,4 +233,8 @@ export default {
.p-fluid .p-chips {
display: flex;
}
.p-chips .p-chips-multiple-container .p-chips-token.p-focus {
background-color: var(--primary-color);
}
</style>

View File

@ -78,16 +78,16 @@ import Chips from 'primevue/chips';
<td>Whether to allow duplicate values or not.</td>
</tr>
<tr>
<td>style</td>
<td>any</td>
<td>inputId</td>
<td>string</td>
<td>null</td>
<td>Style class of the component input field.</td>
</tr>
<tr>
<td>class</td>
<td>string</td>
<td>null</td>
<td>Inline style of the component.</td>
<td>disabled</td>
<td>boolean</td>
<td>false</td>
<td>When present, it specifies that the element should be disabled.</td>
</tr>
</tbody>
</table>
@ -174,6 +174,80 @@ import Chips from 'primevue/chips';
</table>
</div>
<h5>Accessibility</h5>
<DevelopmentSection>
<h6>Screen Reader</h6>
<p>Value to describe the component can either be provided via <i>label</i> tag combined with <i>inputId</i> prop or using <i>aria-labelledby</i>, <i>aria-label</i> props.
Chip list uses <i>listbox</i> role with <i>aria-orientation</i> set to horizontal whereas each chip has the <i>option</i> role with <i>aria-label</i> set to the label of the chip.</p>
<pre v-code><code>
&lt;label for="chips1"&gt;Tags&lt;/label&gt;
&lt;Chips inputId="chips1" /&gt;
&lt;span id="chips2"&gt;Tags&lt;/span&gt;
&lt;Chips aria-labelledby="chips2" /&gt;
&lt;Chips aria-label="Tags" /&gt;
</code></pre>
<h6>Input Field 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 input element</td>
</tr>
<tr>
<td><i>enter</i></td>
<td>Adds a new chips using the input field value.</td>
</tr>
<tr>
<td><i>backspace</i></td>
<td>Deletes the previous chip if the input field is empty.</td>
</tr>
<tr>
<td><i>left arrow</i></td>
<td>Moves focus to the previous chip if available and input field is empty.</td>
</tr>
</tbody>
</table>
</div>
<h6>Chip 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>left arrow</i></td>
<td>Moves focus to the previous chip if available.</td>
</tr>
<tr>
<td><i>right arrow</i></td>
<td>Moves focus to the next chip, if there is none then input field receives the focus.</td>
</tr>
<tr>
<td><i>backspace</i></td>
<td>Deletes the chips and adds focus to the input field.</td>
</tr>
</tbody>
</table>
</div>
</DevelopmentSection>
<h5>Dependencies</h5>
<p>None.</p>
</AppDoc>