SelectButton | hidden checkbox and radio roles added
parent
af37fb5510
commit
79803baf11
|
@ -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."
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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', {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue