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", type: "string",
default: "null", default: "null",
description: "A property to uniquely identify an option." 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. * A property to uniquely identify an option.
*/ */
dataKey?: string | undefined; dataKey?: string | undefined;
/**
* Identifier of the underlying element.
*/
"aria-labelledby"?: string | undefined;
} }
export interface SelectButtonSlots { export interface SelectButtonSlots {

View File

@ -1,8 +1,7 @@
<template> <template>
<div :class="containerClass" role="group"> <div ref="container" :class="containerClass" role="group" :aria-labelledby="ariaLabelledby">
<div v-for="(option, i) of options" :key="getOptionRenderKey(option)" :aria-label="getOptionLabel(option)" role="button" :aria-pressed="isSelected(option)" <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)" :tabindex="isOptionDisabled(option) ? null : '0'" v-ripple :class="getButtonClass(option, i)" @click="onOptionSelect($event, option, i)" @keydown="onKeydown($event, option, i)" @focus="onFocus($event)" @blur="onBlur($event, option)" v-ripple>
@click="onOptionSelect($event, option)" @keydown="onKeydown($event, option)" @focus="onFocus($event)" @blur="onBlur($event)">
<slot name="option" :option="option" :index="i"> <slot name="option" :option="option" :index="i">
<span class="p-button-label">{{getOptionLabel(option)}}</span> <span class="p-button-label">{{getOptionLabel(option)}}</span>
</slot> </slot>
@ -11,7 +10,7 @@
</template> </template>
<script> <script>
import {ObjectUtils} from 'primevue/utils'; import {ObjectUtils,DomHandler} from 'primevue/utils';
import Ripple from 'primevue/ripple'; import Ripple from 'primevue/ripple';
export default { export default {
@ -25,9 +24,31 @@ export default {
optionDisabled: null, optionDisabled: null,
multiple: Boolean, multiple: Boolean,
disabled: Boolean, disabled: Boolean,
dataKey: null dataKey: null,
'aria-labelledby': {
type: String,
default: null
}
},
data() {
return {
focusedIndex: 0
}
},
mounted() {
this.defaultTabIndexes();
}, },
methods: { 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) { getOptionLabel(option) {
return this.optionLabel ? ObjectUtils.resolveFieldData(option, this.optionLabel) : option; return this.optionLabel ? ObjectUtils.resolveFieldData(option, this.optionLabel) : option;
}, },
@ -40,7 +61,7 @@ export default {
isOptionDisabled(option) { isOptionDisabled(option) {
return this.optionDisabled ? ObjectUtils.resolveFieldData(option, this.optionDisabled) : false; return this.optionDisabled ? ObjectUtils.resolveFieldData(option, this.optionDisabled) : false;
}, },
onOptionSelect(event, option) { onOptionSelect(event, option, index) {
if (this.disabled || this.isOptionDisabled(option)) { if (this.disabled || this.isOptionDisabled(option)) {
return; return;
} }
@ -49,7 +70,7 @@ export default {
let optionValue = this.getOptionValue(option); let optionValue = this.getOptionValue(option);
let newValue; let newValue;
if(this.multiple) { if (this.multiple) {
if (selected) if (selected)
newValue = this.modelValue.filter(val => !ObjectUtils.equals(val, optionValue, this.equalityKey)); newValue = this.modelValue.filter(val => !ObjectUtils.equals(val, optionValue, this.equalityKey));
else else
@ -59,16 +80,10 @@ export default {
newValue = selected ? null : optionValue; newValue = selected ? null : optionValue;
} }
this.focusedIndex = index;
this.$emit('update:modelValue', newValue); this.$emit('update:modelValue', newValue);
this.$emit('change', {event: event, value: newValue}); this.$emit('change', {event: event, value: newValue});
}, },
onKeydown(event, option) {
//space
if (event.which === 32) {
this.onOptionSelect(event, option);
event.preventDefault();
}
},
isSelected(option) { isSelected(option) {
let selected = false; let selected = false;
let optionValue = this.getOptionValue(option); let optionValue = this.getOptionValue(option);
@ -89,11 +104,60 @@ export default {
return selected; 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) { onFocus(event) {
this.$emit('focus', event); this.$emit('focus', event);
}, },
onBlur(event) { onBlur(event, option) {
this.$emit('blur', event); 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) { getButtonClass(option) {
return ['p-button p-component', { return ['p-button p-component', {

View File

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

View File

@ -175,11 +175,11 @@ export default {
<h5>Accessibility</h5> <h5>Accessibility</h5>
<DevelopmentSection> <DevelopmentSection>
<h6>Screen Reader</h6> <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. <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 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 Value to describe the component can be provided via <i>aria-labelledby</i> property.</p>
consists of presentational content like icons only.</p>
<h6>Keyboard Support</h6> <h6>Keyboard Support</h6>
<p>Keyboard interaction is derived from the native browser handling of checkboxs in a group.</p>
<div class="doc-tablewrapper"> <div class="doc-tablewrapper">
<table class="doc-table"> <table class="doc-table">
<thead> <thead>
@ -191,7 +191,25 @@ export default {
<tbody> <tbody>
<tr> <tr>
<td><i>tab</i></td> <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>
<tr> <tr>
<td><i>space</i></td> <td><i>space</i></td>
@ -217,14 +235,14 @@ export default {
content: ` content: `
<template> <template>
<div> <div>
<h5>Single Selection</h5> <h5 id="single">Single Selection</h5>
<SelectButton v-model="value1" :options="options" /> <SelectButton v-model="value1" :options="options" aria-labelledby="single" />
<h5>Multiple Selection</h5> <h5 id="multiple">Multiple Selection</h5>
<SelectButton v-model="value2" :options="paymentOptions" optionLabel="name" multiple /> <SelectButton v-model="value2" :options="paymentOptions" optionLabel="name" multiple aria-labelledby="multiple" />
<h5>Custom Content</h5> <h5 id="custom">Custom Content</h5>
<SelectButton v-model="value3" :options="justifyOptions" optionLabel="value" dataKey="value"> <SelectButton v-model="value3" :options="justifyOptions" optionLabel="value" dataKey="value" aria-labelledby="custom">
<template #option="slotProps"> <template #option="slotProps">
<i :class="slotProps.option.icon"></i> <i :class="slotProps.option.icon"></i>
</template> </template>
@ -261,14 +279,14 @@ export default {
content: ` content: `
<template> <template>
<div> <div>
<h5>Single Selection</h5> <h5 id="single">Single Selection</h5>
<SelectButton v-model="value1" :options="options" /> <SelectButton v-model="value1" :options="options" aria-labelledby="single" />
<h5>Multiple Selection</h5> <h5 id="multiple">Multiple Selection</h5>
<SelectButton v-model="value2" :options="paymentOptions" optionLabel="name" multiple /> <SelectButton v-model="value2" :options="paymentOptions" optionLabel="name" multiple aria-labelledby="multiple" />
<h5>Custom Content</h5> <h5 id="custom">Custom Content</h5>
<SelectButton v-model="value3" :options="justifyOptions" optionLabel="value" dataKey="value"> <SelectButton v-model="value3" :options="justifyOptions" optionLabel="value" dataKey="value" aria-labelledby="custom">
<template #option="slotProps"> <template #option="slotProps">
<i :class="slotProps.option.icon"></i> <i :class="slotProps.option.icon"></i>
</template> </template>
@ -307,14 +325,14 @@ export default {
tabName: 'Browser Source', tabName: 'Browser Source',
imports: `<script src="https://unpkg.com/primevue@^3/selectbutton/selectbutton.min.js"><\\/script>`, imports: `<script src="https://unpkg.com/primevue@^3/selectbutton/selectbutton.min.js"><\\/script>`,
content: `<div id="app"> content: `<div id="app">
<h5>Single Selection</h5> <h5 id="single">Single Selection</h5>
<p-selectbutton v-model="value1" :options="options"></p-selectbutton> <p-selectbutton v-model="value1" :options="options" aria-labelledby="single"></p-selectbutton>
<h5>Multiple Selection</h5> <h5 id="multiple">Multiple Selection</h5>
<p-selectbutton v-model="value2" :options="paymentOptions" option-label="name" multiple></p-selectbutton> <p-selectbutton v-model="value2" :options="paymentOptions" option-label="name" multiple aria-labelledby="multiple"></p-selectbutton>
<h5>Custom Content</h5> <h5 id="custom">Custom Content</h5>
<p-selectbutton v-model="value3" :options="justifyOptions" option-label="value" data-key="value"> <p-selectbutton v-model="value3" :options="justifyOptions" option-label="value" data-key="value" aria-labelledby="custom">
<template #option="slotProps"> <template #option="slotProps">
<i :class="slotProps.option.icon"></i> <i :class="slotProps.option.icon"></i>
</template> </template>