Accessibility for Rating

pull/2809/head
Tuğçe Küçükoğlu 2022-07-22 16:30:42 +03:00
parent 4b17571644
commit d8ec9abd14
5 changed files with 143 additions and 24 deletions

View File

@ -66,7 +66,10 @@ const defaultOptions = {
aria: {
trueLabel: 'True',
falseLabel: 'False',
nullLabel: 'Not Selected'
nullLabel: 'Not Selected',
star: 'star',
stars: 'stars',
noneStars: 'No Rating'
}
},
filterMatchModeOptions: {

View File

@ -16,6 +16,10 @@ export interface RatingProps {
* Value of the rating.
*/
modelValue?: number | undefined;
/**
* Name of the element.
*/
name?: string | undefined;
/**
* When present, it specifies that the element should be disabled.
*/

View File

@ -1,19 +1,33 @@
<template>
<div :class="containerClass">
<span class="p-rating-icon p-rating-cancel pi pi-ban" :tabindex="focusIndex" v-if="cancel" @click="onCancelClick"></span>
<span :key="i" v-for="i in stars" @click="onStarClick($event,i)" :tabindex="focusIndex" @keydown.enter.prevent="onStarClick($event,i)"
:class="['p-rating-icon', {'pi pi-star': (i > modelValue), 'pi pi-star-fill': (i <= modelValue)}]"></span>
<span class="p-hidden-accessible" v-if="cancel">
<label :for="labelFor(0)">{{labelText(0)}}</label>
<input type="radio" :id="labelFor(0)" value="0" :name="name" :checked="modelValue === 0" :disabled="disabled" :readonly="readonly" @focus="onFocus($event, 0)" @blur="onBlur" @keydown="onKeyDown($event, 0)">
</span>
<span :class="['p-rating-icon p-rating-cancel pi pi-ban', {'p-focus': focusIndex === 0}]" v-if="cancel" @click="onCancelClick" @keydown="onKeyDown"></span>
<template :key="i" v-for="i in stars">
<span class="p-hidden-accessible">
<label :for="labelFor(i)">{{labelText(i)}}</label>
<input type="radio" :id="labelFor(i)" :value="i" :name="name" :checked="modelValue === i" :disabled="disabled" :readonly="readonly" @focus="onFocus($event, i)" @blur="onBlur" @keydown="onKeyDown($event,i)">
</span>
<span :class="['p-rating-icon', {'pi pi-star': (i > modelValue), 'pi pi-star-fill': (i <= modelValue), 'p-focus': i === focusIndex}]" @click="onStarClick($event,i)"></span>
</template>
</div>
</template>
<script>
export default {
name: 'Rating',
emits: ['update:modelValue', 'change'],
emits: ['update:modelValue', 'change', 'focus', 'blur'],
props: {
modelValue: {
type: Number,
default: null
},
name: {
type: String,
default: null
},
disabled: {
type: Boolean,
@ -32,12 +46,46 @@ export default {
default: true
}
},
data() {
return {
focusIndex: null
};
},
methods: {
onStarClick(event, value) {
if (!this.readonly && !this.disabled) {
this.updateModel(event, value);
this.focusIndex = value;
}
},
onKeyDown(event, value) {
if (event.code === 'Space') {
this.updateModel(event, value);
}
if (event.code === 'Tab') {
this.focusIndex = null;
}
},
onFocus(event, index) {
if (!this.readonly) {
if (this.modelValue === null && this.focusIndex === null) {
this.cancel ? this.focusIndex = 0 : this.focusIndex = 1;
}
else if (this.modelValue !== null && this.focusIndex === null) {
this.focusIndex = this.modelValue;
this.updateModel(event, this.modelValue);
}
else {
this.focusIndex = index;
this.updateModel(event, index);
}
this.$emit('focus', event);
}
},
onBlur(event) {
this.$emit('blur', event);
},
onCancelClick(event) {
if (!this.readonly && !this.disabled) {
this.updateModel(event, null);
@ -49,6 +97,13 @@ export default {
originalEvent: event,
value: value
});
},
labelFor(index) {
return this.name + '_' + index;
},
labelText(index) {
return index === 0 ? this.$primevue.config.locale.aria.noneStars : index === 1
? index + ' ' + this.$primevue.config.locale.aria.star : index + ' ' + this.$primevue.config.locale.aria.stars;
}
},
computed: {
@ -60,9 +115,6 @@ export default {
'p-disabled': this.disabled
}
];
},
focusIndex() {
return (this.disabled || this.readonly) ? null : '0';
}
}
}
@ -76,4 +128,11 @@ export default {
.p-rating.p-rating-readonly .p-rating-icon {
cursor: default;
}
.p-rating:not(.p-disabled) .p-rating-icon.p-focus {
outline: 0 none;
outline-offset: 0;
box-shadow: 0 0 0 0.2rem #BFDBFE;
border-color: #3B82F6;
}
</style>

View File

@ -11,16 +11,16 @@
<div class="content-section implementation">
<div class="card">
<h5>Basic {{val1}}</h5>
<Rating v-model="val1" />
<Rating v-model="val1" name="basic" />
<h5>Without Cancel</h5>
<Rating v-model="val2" :cancel="false" />
<Rating v-model="val2" :cancel="false" name="cancel" />
<h5>ReadOnly</h5>
<Rating :modelValue="5" :readonly="true" :stars="10" :cancel="false" />
<Rating :modelValue="5" :readonly="true" :stars="10" :cancel="false" name="readonly" />
<h5>Disabled</h5>
<Rating :modelValue="8" :disabled="true" :stars="10" />
<Rating :modelValue="8" :disabled="true" :stars="10" name="disabled" />
</div>
</div>

View File

@ -53,6 +53,12 @@ import Rating from 'primevue/rating';
<td>null</td>
<td>Value of the rating.</td>
</tr>
<tr>
<td>name</td>
<td>string</td>
<td>null</td>
<td>Name of the element.</td>
</tr>
<tr>
<td>disabled</td>
<td>boolean</td>
@ -133,6 +139,53 @@ import Rating from 'primevue/rating';
</table>
</div>
<h5>Accessibility</h5>
<DevelopmentSection>
<h6>Screen Reader</h6>
<p>Rating component internally uses radio buttons that are only visible to screen readers. The value to read for item is retrieved from the <Link href="/locale">locale</Link> API via <i>star</i> and <i>stars</i> of the <i>aria</i> property.</p>
<h6>Keyboard Support</h6>
<p>Keyboard interaction is derived from the native browser handling of radio buttons in a group.</p>
<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 star representing the value, if there is none then first star receives the focus.</td>
</tr>
<tr>
<td>
<span class="inline-flex flex-column">
<i class="mb-1">left arrow</i>
<i>up arrow</i>
</span>
</td>
<td>Moves focus to the previous star, if there is none then last radio button receives the focus.</td>
</tr>
<tr>
<td>
<span class="inline-flex flex-column">
<i class="mb-1">right arrow</i>
<i>down arrow</i>
</span>
</td>
<td>Moves focus to the next star, if there is none then first star receives the focus.</td>
</tr>
<tr>
<td><i>space</i></td>
<td>If the focused star does not represent the value, changes the value to the star value.</td>
</tr>
</tbody>
</table>
</div>
</DevelopmentSection>
<h5>Dependencies</h5>
<p>None.</p>
</AppDoc>
@ -149,16 +202,16 @@ export default {
<template>
<div>
<h5>Basic {{val1}}</h5>
<Rating v-model="val1" />
<Rating v-model="val1" name="basic" />
<h5>Without Cancel</h5>
<Rating v-model="val2" :cancel="false" />
<Rating v-model="val2" :cancel="false" name="cancel" />
<h5>ReadOnly</h5>
<Rating :modelValue="5" :readonly="true" :stars="10" :cancel="false" />
<Rating :modelValue="5" :readonly="true" :stars="10" :cancel="false" name="readonly" />
<h5>Disabled</h5>
<Rating :modelValue="8" :disabled="true" :stars="10" />
<Rating :modelValue="8" :disabled="true" :stars="10" name="disabled" />
</div>
</template>
@ -180,16 +233,16 @@ export default {
<template>
<div>
<h5>Basic {{val1}}</h5>
<Rating v-model="val1" />
<Rating v-model="val1" name="basic" />
<h5>Without Cancel</h5>
<Rating v-model="val2" :cancel="false" />
<Rating v-model="val2" :cancel="false" name="cancel" />
<h5>ReadOnly</h5>
<Rating :modelValue="5" :readonly="true" :stars="10" :cancel="false" />
<Rating :modelValue="5" :readonly="true" :stars="10" :cancel="false" name="readonly" />
<h5>Disabled</h5>
<Rating :modelValue="8" :disabled="true" :stars="10" />
<Rating :modelValue="8" :disabled="true" :stars="10" name="disabled" />
</div>
</template>
@ -212,16 +265,16 @@ export default {
imports: `<script src="https://unpkg.com/primevue@^3/rating/rating.min.js"><\\/script>`,
content: `<div id="app">
<h5>Basic {{val1}}</h5>
<p-rating v-model="val1"></p-rating>
<p-rating v-model="val1" name="basic"></p-rating>
<h5>Without Cancel</h5>
<p-rating v-model="val2" :cancel="false"></p-rating>
<p-rating v-model="val2" :cancel="false" name="cancel"></p-rating>
<h5>ReadOnly</h5>
<p-rating :model-value="5" :readonly="true" :stars="10" :cancel="false"></p-rating>
<p-rating :modelValue="5" :readonly="true" :stars="10" :cancel="false" name="readonly"></p-rating>
<h5>Disabled</h5>
<p-rating :model-value="8" :disabled="true" :stars="10"></p-rating>
<p-rating :modelValue="8" :disabled="true" :stars="10" name="disabled"></p-rating>
</div>
<script type="module">