fix(checkbox): use a native input to fix a11y issues with axe and screen readers (#22402)

fixes #21644
fixes #20517
fixes #17796
This commit is contained in:
Brandy Carney
2020-11-12 11:25:33 -05:00
committed by GitHub
parent 0956f8bc55
commit 7214a8401b
11 changed files with 347 additions and 106 deletions

View File

@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop
import { getIonMode } from '../../global/ionic-global';
import { CheckboxChangeEventDetail, Color, StyleEventDetail } from '../../interface';
import { findItemLabel, renderHiddenInput } from '../../utils/helpers';
import { getAriaLabel, renderHiddenInput } from '../../utils/helpers';
import { createColorClasses, hostContext } from '../../utils/theme';
/**
@ -22,7 +22,7 @@ import { createColorClasses, hostContext } from '../../utils/theme';
export class Checkbox implements ComponentInterface {
private inputId = `ion-cb-${checkboxIds++}`;
private buttonEl?: HTMLElement;
private focusEl?: HTMLElement;
@Element() el!: HTMLElement;
@ -54,11 +54,11 @@ export class Checkbox implements ComponentInterface {
@Prop() disabled = false;
/**
* The value of the toggle does not mean if it's checked or not, use the `checked`
* The value of the checkbox does not mean if it's checked or not, use the `checked`
* property for that.
*
* The value of a toggle is analogous to the value of a `<input type="checkbox">`,
* it's only used when the toggle participates in a native `<form>`.
* The value of a checkbox is analogous to the value of an `<input type="checkbox">`,
* it's only used when the checkbox participates in a native `<form>`.
*/
@Prop() value = 'on';
@ -68,12 +68,12 @@ export class Checkbox implements ComponentInterface {
@Event() ionChange!: EventEmitter<CheckboxChangeEventDetail>;
/**
* Emitted when the toggle has focus.
* Emitted when the checkbox has focus.
*/
@Event() ionFocus!: EventEmitter<void>;
/**
* Emitted when the toggle loses focus.
* Emitted when the checkbox loses focus.
*/
@Event() ionBlur!: EventEmitter<void>;
@ -109,12 +109,15 @@ export class Checkbox implements ComponentInterface {
}
private setFocus() {
if (this.buttonEl) {
this.buttonEl.focus();
if (this.focusEl) {
this.focusEl.focus();
}
}
private onClick = () => {
private onClick = (ev: Event) => {
ev.preventDefault();
ev.stopPropagation();
this.setFocus();
this.checked = !this.checked;
this.indeterminate = false;
@ -129,14 +132,11 @@ export class Checkbox implements ComponentInterface {
}
render() {
const { inputId, indeterminate, disabled, checked, value, color, el } = this;
const labelId = inputId + '-lbl';
const { color, checked, disabled, el, indeterminate, inputId, name, value } = this;
const mode = getIonMode(this);
const label = findItemLabel(el);
if (label) {
label.id = labelId;
}
renderHiddenInput(true, el, this.name, (checked ? value : ''), disabled);
const { label, labelId, labelText } = getAriaLabel(el, inputId);
renderHiddenInput(true, el, name, (checked ? value : ''), disabled);
let path = indeterminate
? <path d="M6 12L18 12" part="mark" />
@ -151,10 +151,10 @@ export class Checkbox implements ComponentInterface {
return (
<Host
onClick={this.onClick}
role="checkbox"
aria-disabled={disabled ? 'true' : null}
aria-labelledby={label ? labelId : null}
aria-checked={`${checked}`}
aria-labelledby={labelId}
aria-hidden={disabled ? 'true' : null}
role="checkbox"
class={createColorClasses(color, {
[mode]: true,
'in-item': hostContext('ion-item', el),
@ -167,14 +167,18 @@ export class Checkbox implements ComponentInterface {
<svg class="checkbox-icon" viewBox="0 0 24 24" part="container">
{path}
</svg>
<button
type="button"
onFocus={this.onFocus}
onBlur={this.onBlur}
disabled={this.disabled}
ref={btnEl => this.buttonEl = btnEl}
>
</button>
<label htmlFor={inputId}>
{labelText}
</label>
<input
type="checkbox"
aria-checked={`${checked}`}
disabled={disabled}
id={inputId}
onFocus={() => this.onFocus()}
onBlur={() => this.onBlur()}
ref={focusEl => this.focusEl = focusEl}
/>
</Host>
);
}