fix(select): improvements for announcing placeholder and value on screenreaders (#22556)

- Hides select text from screen readers so it isn't announced twice (Android talkback needs this)
- Adds the placeholder text to be announced if there is no value
- Don't add a comma if there is no value/placeholder (NVDA speech viewer)
- Don't announce alert label twice
This commit is contained in:
Brandy Carney
2020-11-25 15:47:14 -05:00
committed by GitHub
parent c45c8d5564
commit ea52db66f0
3 changed files with 54 additions and 30 deletions

View File

@ -373,22 +373,24 @@ export class Alert implements ComponentInterface, OverlayInterface {
return values; return values;
} }
private renderAlertInputs(labelledBy: string | undefined) { private renderAlertInputs() {
switch (this.inputType) { switch (this.inputType) {
case 'checkbox': return this.renderCheckbox(labelledBy); case 'checkbox': return this.renderCheckbox();
case 'radio': return this.renderRadio(labelledBy); case 'radio': return this.renderRadio();
default: return this.renderInput(labelledBy); default: return this.renderInput();
} }
} }
private renderCheckbox(labelledby: string | undefined) { private renderCheckbox() {
const inputs = this.processedInputs; const inputs = this.processedInputs;
const mode = getIonMode(this); const mode = getIonMode(this);
if (inputs.length === 0) { if (inputs.length === 0) {
return null; return null;
} }
return ( return (
<div class="alert-checkbox-group" aria-labelledby={labelledby}> <div class="alert-checkbox-group">
{ inputs.map(i => ( { inputs.map(i => (
<button <button
type="button" type="button"
@ -422,13 +424,15 @@ export class Alert implements ComponentInterface, OverlayInterface {
); );
} }
private renderRadio(labelledby: string | undefined) { private renderRadio() {
const inputs = this.processedInputs; const inputs = this.processedInputs;
if (inputs.length === 0) { if (inputs.length === 0) {
return null; return null;
} }
return ( return (
<div class="alert-radio-group" role="radiogroup" aria-labelledby={labelledby} aria-activedescendant={this.activeId}> <div class="alert-radio-group" role="radiogroup" aria-activedescendant={this.activeId}>
{ inputs.map(i => ( { inputs.map(i => (
<button <button
type="button" type="button"
@ -459,13 +463,13 @@ export class Alert implements ComponentInterface, OverlayInterface {
); );
} }
private renderInput(labelledby: string | undefined) { private renderInput() {
const inputs = this.processedInputs; const inputs = this.processedInputs;
if (inputs.length === 0) { if (inputs.length === 0) {
return null; return null;
} }
return ( return (
<div class="alert-input-group" aria-labelledby={labelledby}> <div class="alert-input-group">
{ inputs.map(i => { { inputs.map(i => {
if (i.type === 'textarea') { if (i.type === 'textarea') {
return ( return (
@ -552,13 +556,6 @@ export class Alert implements ComponentInterface, OverlayInterface {
const subHdrId = `alert-${overlayIndex}-sub-hdr`; const subHdrId = `alert-${overlayIndex}-sub-hdr`;
const msgId = `alert-${overlayIndex}-msg`; const msgId = `alert-${overlayIndex}-msg`;
let labelledById: string | undefined;
if (header !== undefined) {
labelledById = hdrId;
} else if (subHeader !== undefined) {
labelledById = subHdrId;
}
return ( return (
<Host <Host
role="dialog" role="dialog"
@ -589,7 +586,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
<div id={msgId} class="alert-message" innerHTML={sanitizeDOMString(this.message)}></div> <div id={msgId} class="alert-message" innerHTML={sanitizeDOMString(this.message)}></div>
{this.renderAlertInputs(labelledById)} {this.renderAlertInputs()}
{this.renderAlertButtons()} {this.renderAlertButtons()}
</div> </div>

View File

@ -458,11 +458,12 @@ export class Select implements ComponentInterface {
const textPart = addPlaceholderClass ? 'placeholder' : 'text'; const textPart = addPlaceholderClass ? 'placeholder' : 'text';
// If there is a label then we need to concatenate it with the // If there is a label then we need to concatenate it with the
// current value and a comma so it separates nicely when the screen reader // current value (or placeholder) and a comma so it separates
// announces it, otherwise just announce the value // nicely when the screen reader announces it, otherwise just
// announce the value / placeholder
const displayLabel = labelText !== undefined const displayLabel = labelText !== undefined
? `${displayValue}, ${labelText}` ? (selectText !== '' ? `${selectText}, ${labelText}` : labelText)
: displayValue; : selectText;
return ( return (
<Host <Host
@ -477,7 +478,7 @@ export class Select implements ComponentInterface {
'select-disabled': disabled, 'select-disabled': disabled,
}} }}
> >
<div class={selectTextClasses} part={textPart}> <div aria-hidden="true" class={selectTextClasses} part={textPart}>
{selectText} {selectText}
</div> </div>
<div class="select-icon" role="presentation" part="icon"> <div class="select-icon" role="presentation" part="icon">

View File

@ -31,7 +31,7 @@
</ion-list-header> </ion-list-header>
<div class="native-select-wrapper"> <div class="native-select-wrapper">
<label for="pet-select">Choose a Pet</label> <label for="pet-select">Favorite Pet</label>
<select name="pets" id="pet-select"> <select name="pets" id="pet-select">
<option value="dog">Dog</option> <option value="dog">Dog</option>
@ -52,9 +52,35 @@
</ion-list-header> </ion-list-header>
<ion-item> <ion-item>
<label for="ionic-select">Choose a Pet Custom</label> <ion-label>Favorite Pet</ion-label>
<ion-select id="ionic-select"> <ion-select>
<ion-select-option value="dog">Dog</ion-select-option>
<ion-select-option value="cat">Cat</ion-select-option>
<ion-select-option value="hamster">Hamster</ion-select-option>
<ion-select-option value="parrot">Parrot</ion-select-option>
<ion-select-option value="spider">Spider</ion-select-option>
<ion-select-option value="goldfish">Goldfish</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label>Favorite Pet</ion-label>
<ion-select placeholder="Select a Pet">
<ion-select-option value="dog">Dog</ion-select-option>
<ion-select-option value="cat">Cat</ion-select-option>
<ion-select-option value="hamster">Hamster</ion-select-option>
<ion-select-option value="parrot">Parrot</ion-select-option>
<ion-select-option value="spider">Spider</ion-select-option>
<ion-select-option value="goldfish">Goldfish</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label>Favorite Pet</ion-label>
<ion-select value="spider">
<ion-select-option value="dog">Dog</ion-select-option> <ion-select-option value="dog">Dog</ion-select-option>
<ion-select-option value="cat">Cat</ion-select-option> <ion-select-option value="cat">Cat</ion-select-option>
<ion-select-option value="hamster">Hamster</ion-select-option> <ion-select-option value="hamster">Hamster</ion-select-option>
@ -73,9 +99,9 @@
</ion-list-header> </ion-list-header>
<ion-item> <ion-item>
<ion-label>Choose a Pet</ion-label> <label for="ionic-select">Favorite Pet</label>
<ion-select> <ion-select id="ionic-select">
<ion-select-option value="dog">Dog</ion-select-option> <ion-select-option value="dog">Dog</ion-select-option>
<ion-select-option value="cat">Cat</ion-select-option> <ion-select-option value="cat">Cat</ion-select-option>
<ion-select-option value="hamster">Hamster</ion-select-option> <ion-select-option value="hamster">Hamster</ion-select-option>
@ -94,7 +120,7 @@
</ion-list-header> </ion-list-header>
<ion-item> <ion-item>
<ion-label>Choose a Pet</ion-label> <ion-label>Favorite Pet</ion-label>
<ion-select interface="popover"> <ion-select interface="popover">
<ion-select-option value="dog">Dog</ion-select-option> <ion-select-option value="dog">Dog</ion-select-option>
@ -115,7 +141,7 @@
</ion-list-header> </ion-list-header>
<ion-item> <ion-item>
<ion-label>Choose a Pet</ion-label> <ion-label>Favorite Pet</ion-label>
<ion-select interface="action-sheet"> <ion-select interface="action-sheet">
<ion-select-option value="dog">Dog</ion-select-option> <ion-select-option value="dog">Dog</ion-select-option>