SelectButton | hidden checkbox and radio roles added

pull/2835/head
Tuğçe Küçükoğlu 2022-08-04 17:17:49 +03:00
parent af37fb5510
commit 79803baf11
5 changed files with 137 additions and 45 deletions

View File

@ -46,6 +46,12 @@ const SelectButtonProps = [
type: "string",
default: "null",
description: "A property to uniquely identify an option."
},
{
name: "aria-labelledby",
type: "string",
default: "null",
description: "Identifier of the underlying element."
}
];

View File

@ -51,6 +51,10 @@ export interface SelectButtonProps {
* A property to uniquely identify an option.
*/
dataKey?: string | undefined;
/**
* Identifier of the underlying element.
*/
"aria-labelledby"?: string | undefined;
}
export interface SelectButtonSlots {

View File

@ -1,8 +1,7 @@
<template>
<div :class="containerClass" role="group">
<div v-for="(option, i) of options" :key="getOptionRenderKey(option)" :aria-label="getOptionLabel(option)" role="button" :aria-pressed="isSelected(option)"
:class="getButtonClass(option)" :tabindex="isOptionDisabled(option) ? null : '0'" v-ripple
@click="onOptionSelect($event, option)" @keydown="onKeydown($event, option)" @focus="onFocus($event)" @blur="onBlur($event)">
<div ref="container" :class="containerClass" role="group" :aria-labelledby="ariaLabelledby">
<div v-for="(option, i) of options" :key="getOptionRenderKey(option)" :tabindex="i === focusedIndex ? '0' : '-1'" :aria-label="getOptionLabel(option)" :role="multiple ? 'checkbox' : 'radio'" :aria-checked="isSelected(option)" :aria-disabled="optionDisabled"
:class="getButtonClass(option, i)" @click="onOptionSelect($event, option, i)" @keydown="onKeydown($event, option, i)" @focus="onFocus($event)" @blur="onBlur($event, option)" v-ripple>
<slot name="option" :option="option" :index="i">
<span class="p-button-label">{{getOptionLabel(option)}}</span>
</slot>
@ -11,7 +10,7 @@
</template>
<script>
import {ObjectUtils} from 'primevue/utils';
import {ObjectUtils,DomHandler} from 'primevue/utils';
import Ripple from 'primevue/ripple';
export default {
@ -25,9 +24,31 @@ export default {
optionDisabled: null,
multiple: Boolean,
disabled: Boolean,
dataKey: null
dataKey: null,
'aria-labelledby': {
type: String,
default: null
}
},
data() {
return {
focusedIndex: 0
}
},
mounted() {
this.defaultTabIndexes();
},
methods: {
defaultTabIndexes() {
let opts = DomHandler.find(this.$refs.container, '.p-button');
let firstHighlight = DomHandler.findSingle(this.$refs.container, '.p-highlight');
for (let i = 0; i < opts.length; i++) {
if ((DomHandler.hasClass(opts[i], 'p-highlight') && ObjectUtils.equals(opts[i], firstHighlight)) || (firstHighlight === null && i == 0)) {
this.focusedIndex = i;
}
}
},
getOptionLabel(option) {
return this.optionLabel ? ObjectUtils.resolveFieldData(option, this.optionLabel) : option;
},
@ -40,7 +61,7 @@ export default {
isOptionDisabled(option) {
return this.optionDisabled ? ObjectUtils.resolveFieldData(option, this.optionDisabled) : false;
},
onOptionSelect(event, option) {
onOptionSelect(event, option, index) {
if (this.disabled || this.isOptionDisabled(option)) {
return;
}
@ -49,7 +70,7 @@ export default {
let optionValue = this.getOptionValue(option);
let newValue;
if(this.multiple) {
if (this.multiple) {
if (selected)
newValue = this.modelValue.filter(val => !ObjectUtils.equals(val, optionValue, this.equalityKey));
else
@ -59,16 +80,10 @@ export default {
newValue = selected ? null : optionValue;
}
this.focusedIndex = index;
this.$emit('update:modelValue', newValue);
this.$emit('change', {event: event, value: newValue});
},
onKeydown(event, option) {
//space
if (event.which === 32) {
this.onOptionSelect(event, option);
event.preventDefault();
}
},
isSelected(option) {
let selected = false;
let optionValue = this.getOptionValue(option);
@ -89,11 +104,60 @@ export default {
return selected;
},
onKeydown(event, option, index) {
switch (event.code) {
case 'Space': {
this.onOptionSelect(event, option, index);
event.preventDefault();
break;
}
case 'ArrowDown':
case 'ArrowRight': {
this.changeTabIndexes(event, 'next');
event.preventDefault();
break;
}
case 'ArrowUp':
case 'ArrowLeft': {
this.changeTabIndexes(event, 'prev');
event.preventDefault();
break;
}
default:
//no op
break;
}
},
changeTabIndexes(event, direction) {
let firstTabableChild, index;
for (let i = 0; i <= this.$refs.container.children.length - 1; i++) {
if (this.$refs.container.children[i].getAttribute('tabindex') === '0')
firstTabableChild = {elem: this.$refs.container.children[i], index: i};
}
if (direction === 'prev') {
if (firstTabableChild.index === 0) index = this.$refs.container.children.length - 1;
else index = firstTabableChild.index - 1;
}
else {
if (firstTabableChild.index === this.$refs.container.children.length - 1) index = 0;
else index = firstTabableChild.index + 1;
}
this.focusedIndex = index;
this.$refs.container.children[index].focus();
},
onFocus(event) {
this.$emit('focus', event);
},
onBlur(event) {
this.$emit('blur', event);
onBlur(event, option) {
if (event.target && event.target.parentElement && event.relatedTarget && event.relatedTarget.parentElement && event.target.parentElement.getAttribute('aria-labelledby') !== event.relatedTarget.parentElement.getAttribute('aria-labelledby')) {
this.defaultTabIndexes();
}
this.$emit('blur', event, option);
},
getButtonClass(option) {
return ['p-button p-component', {

View File

@ -10,14 +10,14 @@
<div class="content-section implementation">
<div class="card">
<h5>Single Selection</h5>
<SelectButton v-model="value1" :options="options" />
<h5 id="single">Single Selection</h5>
<SelectButton v-model="value1" :options="options" aria-labelledby="single" />
<h5>Multiple Selection</h5>
<SelectButton v-model="value2" :options="paymentOptions" optionLabel="name" multiple />
<h5 id="multiple">Multiple Selection</h5>
<SelectButton v-model="value2" :options="paymentOptions" optionLabel="name" multiple aria-labelledby="multiple" />
<h5>Custom Content</h5>
<SelectButton v-model="value3" :options="justifyOptions" optionLabel="value" dataKey="value" >
<h5 id="custom">Custom Content</h5>
<SelectButton v-model="value3" :options="justifyOptions" optionLabel="value" dataKey="value" aria-labelledby="custom" >
<template #option="slotProps">
<i :class="slotProps.option.icon"></i>
</template>

View File

@ -175,11 +175,11 @@ export default {
<h5>Accessibility</h5>
<DevelopmentSection>
<h6>Screen Reader</h6>
<p>The container element that wraps the buttons has a <i>group</i> role whereas each button element uses <i>button</i> role and <i>aria-pressed</i> is updated depending on selection state.
Value to describe an option is automatically set using the <i>aria-label</i> property that refers to the label of an option so it is still suggested to define a label even the option display
consists of presentational content like icons only.</p>
<p>SelectButton component uses hidden native checkbox role for multiple selection and hidden radio role for single selection that is only visible to screen readers.
Value to describe the component can be provided via <i>aria-labelledby</i> property.</p>
<h6>Keyboard Support</h6>
<p>Keyboard interaction is derived from the native browser handling of checkboxs in a group.</p>
<div class="doc-tablewrapper">
<table class="doc-table">
<thead>
@ -191,7 +191,25 @@ export default {
<tbody>
<tr>
<td><i>tab</i></td>
<td>Moves focus to the buttons.</td>
<td>Moves focus to the first selected option, if there is none then first option receives the focus.</td>
</tr>
<tr>
<td>
<span class="inline-flex flex-column">
<i class="mb-1">right arrow</i>
<i>up arrow</i>
</span>
</td>
<td>Moves focus to the previous option.</td>
</tr>
<tr>
<td>
<span class="inline-flex flex-column">
<i class="mb-1">left arrow</i>
<i>down arrow</i>
</span>
</td>
<td>Moves focus to the next option.</td>
</tr>
<tr>
<td><i>space</i></td>
@ -217,14 +235,14 @@ export default {
content: `
<template>
<div>
<h5>Single Selection</h5>
<SelectButton v-model="value1" :options="options" />
<h5 id="single">Single Selection</h5>
<SelectButton v-model="value1" :options="options" aria-labelledby="single" />
<h5>Multiple Selection</h5>
<SelectButton v-model="value2" :options="paymentOptions" optionLabel="name" multiple />
<h5 id="multiple">Multiple Selection</h5>
<SelectButton v-model="value2" :options="paymentOptions" optionLabel="name" multiple aria-labelledby="multiple" />
<h5>Custom Content</h5>
<SelectButton v-model="value3" :options="justifyOptions" optionLabel="value" dataKey="value">
<h5 id="custom">Custom Content</h5>
<SelectButton v-model="value3" :options="justifyOptions" optionLabel="value" dataKey="value" aria-labelledby="custom">
<template #option="slotProps">
<i :class="slotProps.option.icon"></i>
</template>
@ -261,14 +279,14 @@ export default {
content: `
<template>
<div>
<h5>Single Selection</h5>
<SelectButton v-model="value1" :options="options" />
<h5 id="single">Single Selection</h5>
<SelectButton v-model="value1" :options="options" aria-labelledby="single" />
<h5>Multiple Selection</h5>
<SelectButton v-model="value2" :options="paymentOptions" optionLabel="name" multiple />
<h5 id="multiple">Multiple Selection</h5>
<SelectButton v-model="value2" :options="paymentOptions" optionLabel="name" multiple aria-labelledby="multiple" />
<h5>Custom Content</h5>
<SelectButton v-model="value3" :options="justifyOptions" optionLabel="value" dataKey="value">
<h5 id="custom">Custom Content</h5>
<SelectButton v-model="value3" :options="justifyOptions" optionLabel="value" dataKey="value" aria-labelledby="custom">
<template #option="slotProps">
<i :class="slotProps.option.icon"></i>
</template>
@ -307,14 +325,14 @@ export default {
tabName: 'Browser Source',
imports: `<script src="https://unpkg.com/primevue@^3/selectbutton/selectbutton.min.js"><\\/script>`,
content: `<div id="app">
<h5>Single Selection</h5>
<p-selectbutton v-model="value1" :options="options"></p-selectbutton>
<h5 id="single">Single Selection</h5>
<p-selectbutton v-model="value1" :options="options" aria-labelledby="single"></p-selectbutton>
<h5>Multiple Selection</h5>
<p-selectbutton v-model="value2" :options="paymentOptions" option-label="name" multiple></p-selectbutton>
<h5 id="multiple">Multiple Selection</h5>
<p-selectbutton v-model="value2" :options="paymentOptions" option-label="name" multiple aria-labelledby="multiple"></p-selectbutton>
<h5>Custom Content</h5>
<p-selectbutton v-model="value3" :options="justifyOptions" option-label="value" data-key="value">
<h5 id="custom">Custom Content</h5>
<p-selectbutton v-model="value3" :options="justifyOptions" option-label="value" data-key="value" aria-labelledby="custom">
<template #option="slotProps">
<i :class="slotProps.option.icon"></i>
</template>