Skip to content

Commit

Permalink
fix(toggle): use a native input to fix a11y issues with axe and scree…
Browse files Browse the repository at this point in the history
…n readers (#22477)

fixes #22011
references #21552
  • Loading branch information
brandyscarney committed Nov 12, 2020
1 parent 96d6012 commit 813611a
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 72 deletions.
2 changes: 2 additions & 0 deletions .github/COMPONENT-GUIDE.md
Expand Up @@ -10,6 +10,7 @@
* [Example Components](#example-components)
* [References](#references)
- [Accessibility](#accessibility)
* [Checkbox](#checkbox)
- [Rendering Anchor or Button](#rendering-anchor-or-button)
* [Example Components](#example-components-1)
* [Component Structure](#component-structure-1)
Expand Down Expand Up @@ -370,6 +371,7 @@ ion-ripple-effect {
#### Example Components
- [ion-checkbox](https://github.com/ionic-team/ionic/tree/master/core/src/components/checkbox)
- [ion-toggle](https://github.com/ionic-team/ionic/tree/master/core/src/components/toggle)
#### VoiceOver
Expand Down
4 changes: 2 additions & 2 deletions core/src/components/toggle/test/basic/index.html
Expand Up @@ -18,7 +18,7 @@
<ion-toolbar>
<ion-title>Toggle - Basic</ion-title>
<ion-buttons slot="primary">
<ion-toggle></ion-toggle>
<ion-toggle aria-label="Toggle"></ion-toggle>
</ion-buttons>
</ion-toolbar>
</ion-header>
Expand Down Expand Up @@ -86,7 +86,7 @@
</p>

<p>
<ion-toggle id="standAloneChecked"></ion-toggle>
<ion-toggle aria-label="Stand-alone toggle" id="standAloneChecked"></ion-toggle>
Stand-alone toggle:
<span id="standAloneCheckedSpan"></span>
</p>
Expand Down
66 changes: 33 additions & 33 deletions core/src/components/toggle/test/sizes/index.html
Expand Up @@ -25,53 +25,53 @@

<ion-content class="ion-padding-horizontal">
<h1>Default</h1>
<ion-toggle></ion-toggle>
<ion-toggle checked></ion-toggle>
<ion-toggle color="danger"></ion-toggle>
<ion-toggle color="danger" checked></ion-toggle>
<ion-toggle color="tertiary" class="toggle-activated"></ion-toggle>
<ion-toggle color="tertiary" checked class="toggle-activated"></ion-toggle>
<ion-toggle aria-label="Default"></ion-toggle>
<ion-toggle aria-label="Default" checked></ion-toggle>
<ion-toggle aria-label="Default Danger" color="danger"></ion-toggle>
<ion-toggle aria-label="Default Danger" color="danger" checked></ion-toggle>
<ion-toggle aria-label="Default Tertiary" color="tertiary" class="toggle-activated"></ion-toggle>
<ion-toggle aria-label="Default Tertiary Activated" color="tertiary" checked class="toggle-activated"></ion-toggle>

<h1>Custom Widths</h1>
<ion-toggle color="secondary" class="width-small"></ion-toggle>
<ion-toggle color="secondary" checked class="width-small"></ion-toggle>
<ion-toggle color="secondary" class="width-large"></ion-toggle>
<ion-toggle color="secondary" checked class="width-large"></ion-toggle>
<ion-toggle color="tertiary" class="width-large toggle-activated"></ion-toggle>
<ion-toggle color="tertiary" checked class="width-large toggle-activated"></ion-toggle>
<ion-toggle aria-label="Secondary Small Width" color="secondary" class="width-small"></ion-toggle>
<ion-toggle aria-label="Secondary Small Width" color="secondary" checked class="width-small"></ion-toggle>
<ion-toggle aria-label="Secondary Large Width" color="secondary" class="width-large"></ion-toggle>
<ion-toggle aria-label="Secondary Large Width" color="secondary" checked class="width-large"></ion-toggle>
<ion-toggle aria-label="Tertiary Large Width Activated" color="tertiary" class="width-large toggle-activated"></ion-toggle>
<ion-toggle aria-label="Tertiary Large Width Activated" color="tertiary" checked class="width-large toggle-activated"></ion-toggle>

<h1>Custom Heights</h1>
<div style="display: flex; flex-flow: column; float: left;">
<ion-toggle class="height-small"></ion-toggle>
<ion-toggle checked class="height-small"></ion-toggle>
<ion-toggle aria-label="Small Height" class="height-small"></ion-toggle>
<ion-toggle aria-label="Small Height" checked class="height-small"></ion-toggle>
</div>
<ion-toggle class="height-large"></ion-toggle>
<ion-toggle checked class="height-large"></ion-toggle>
<ion-toggle class="handle-height-large"></ion-toggle>
<ion-toggle checked class="handle-height-large"></ion-toggle>
<ion-toggle checked class="handle-height-large toggle-activated"></ion-toggle>
<ion-toggle aria-label="Large Height" class="height-large"></ion-toggle>
<ion-toggle aria-label="Large Height" checked class="height-large"></ion-toggle>
<ion-toggle aria-label="Large Height" class="handle-height-large"></ion-toggle>
<ion-toggle aria-label="Large Height" checked class="handle-height-large"></ion-toggle>
<ion-toggle aria-label="Large Height Activated" checked class="handle-height-large toggle-activated"></ion-toggle>

<h1>Dynamic Sizes</h1>
<ion-toggle color="tertiary" class="dynamic-small width-small height-small"></ion-toggle>
<ion-toggle color="tertiary" checked class="dynamic-small width-small height-small"></ion-toggle>
<ion-toggle color="tertiary" class="dynamic-large width-large height-large"></ion-toggle>
<ion-toggle color="tertiary" checked class="dynamic-large width-large height-large"></ion-toggle>
<ion-toggle aria-label="Tertiary Small Width Small Height" color="tertiary" class="dynamic-small width-small height-small"></ion-toggle>
<ion-toggle aria-label="Tertiary Small Width Small Height" color="tertiary" checked class="dynamic-small width-small height-small"></ion-toggle>
<ion-toggle aria-label="Tertiary Large Width Large Height" color="tertiary" class="dynamic-large width-large height-large"></ion-toggle>
<ion-toggle aria-label="Tertiary Large Width Large Height" color="tertiary" checked class="dynamic-large width-large height-large"></ion-toggle>

<h1>Complex Custom Toggles</h1>
<ion-toggle mode="ios" class="all-custom"></ion-toggle>
<ion-toggle mode="ios" checked class="all-custom"></ion-toggle>
<ion-toggle aria-label="Custom" mode="ios" class="all-custom"></ion-toggle>
<ion-toggle aria-label="Custom" mode="ios" checked class="all-custom"></ion-toggle>

<ion-toggle class="custom-overflow"></ion-toggle>
<ion-toggle checked class="custom-overflow"></ion-toggle>
<ion-toggle aria-label="Custom Overflow" class="custom-overflow"></ion-toggle>
<ion-toggle aria-label="Custom Overflow" checked class="custom-overflow"></ion-toggle>

<ion-toggle mode="ios" color="dark" class="custom-spacing"></ion-toggle>
<ion-toggle mode="ios" color="dark" checked class="custom-spacing"></ion-toggle>
<ion-toggle aria-label="Custom Spacing iOS" mode="ios" color="dark" class="custom-spacing"></ion-toggle>
<ion-toggle aria-label="Custom Spacing iOS" mode="ios" color="dark" checked class="custom-spacing"></ion-toggle>

<ion-toggle mode="md" color="dark" class="custom-spacing"></ion-toggle>
<ion-toggle mode="md" color="dark" checked class="custom-spacing"></ion-toggle>
<ion-toggle aria-label="Custom Spacing MD" mode="md" color="dark" class="custom-spacing"></ion-toggle>
<ion-toggle aria-label="Custom Spacing MD" mode="md" color="dark" checked class="custom-spacing"></ion-toggle>

<ion-toggle mode="ios" class="icon-custom"></ion-toggle>
<ion-toggle mode="ios" checked class="icon-custom"></ion-toggle>
<ion-toggle aria-label="Custom Icon iOS" mode="ios" class="icon-custom"></ion-toggle>
<ion-toggle aria-label="Custom Icon iOS" mode="ios" checked class="icon-custom"></ion-toggle>
</ion-content>
</ion-app>

Expand Down
26 changes: 13 additions & 13 deletions core/src/components/toggle/test/standalone/index.html
Expand Up @@ -13,23 +13,23 @@

<body>
<!-- Default -->
<ion-toggle></ion-toggle>
<ion-toggle aria-label="Default Toggle"></ion-toggle>

<!-- Colors -->
<ion-toggle checked color="primary"></ion-toggle>
<ion-toggle checked color="secondary"></ion-toggle>
<ion-toggle checked color="tertiary"></ion-toggle>
<ion-toggle checked color="success"></ion-toggle>
<ion-toggle checked color="warning"></ion-toggle>
<ion-toggle checked color="danger"></ion-toggle>
<ion-toggle checked color="light"></ion-toggle>
<ion-toggle checked color="medium"></ion-toggle>
<ion-toggle checked color="dark"></ion-toggle>
<ion-toggle checked class="custom"></ion-toggle>
<ion-toggle aria-label="Primary Toggle" checked color="primary"></ion-toggle>
<ion-toggle aria-label="Secondary Toggle" checked color="secondary"></ion-toggle>
<ion-toggle aria-label="Tertiary Toggle" checked color="tertiary"></ion-toggle>
<ion-toggle aria-label="Success Toggle" checked color="success"></ion-toggle>
<ion-toggle aria-label="Warning Toggle" checked color="warning"></ion-toggle>
<ion-toggle aria-label="Danger Toggle" checked color="danger"></ion-toggle>
<ion-toggle aria-label="Light Toggle" checked color="light"></ion-toggle>
<ion-toggle aria-label="Medium Toggle" checked color="medium"></ion-toggle>
<ion-toggle aria-label="Dark Toggle" checked color="dark"></ion-toggle>
<ion-toggle aria-label="Custom Toggle" checked class="custom"></ion-toggle>

<!-- Disabled -->
<ion-toggle checked disabled></ion-toggle>
<ion-toggle checked disabled color="secondary"></ion-toggle>
<ion-toggle aria-label="Disabled Default Toggle" checked disabled></ion-toggle>
<ion-toggle aria-label="Disabled Secondary Toggle" checked disabled color="secondary"></ion-toggle>

<style>
.custom {
Expand Down
12 changes: 11 additions & 1 deletion core/src/components/toggle/toggle.scss
Expand Up @@ -45,8 +45,18 @@
pointer-events: none;
}

button {
label {
@include input-cover();

display: flex;

align-items: center;

opacity: 0;
}

input {
@include visually-hidden();
}

// Toggle Background Track: Unchecked
Expand Down
50 changes: 27 additions & 23 deletions core/src/components/toggle/toggle.tsx
Expand Up @@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop

import { getIonMode } from '../../global/ionic-global';
import { Color, Gesture, GestureDetail, StyleEventDetail, ToggleChangeEventDetail } from '../../interface';
import { findItemLabel, renderHiddenInput } from '../../utils/helpers';
import { getAriaLabel, renderHiddenInput } from '../../utils/helpers';
import { hapticSelection } from '../../utils/native/haptic';
import { createColorClasses, hostContext } from '../../utils/theme';

Expand All @@ -24,7 +24,7 @@ export class Toggle implements ComponentInterface {

private inputId = `ion-tg-${toggleIds++}`;
private gesture?: Gesture;
private buttonEl?: HTMLElement;
private focusEl?: HTMLElement;
private lastDrag = 0;

@Element() el!: HTMLElement;
Expand Down Expand Up @@ -156,12 +156,15 @@ export class Toggle 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();

if (this.lastDrag + 300 < Date.now()) {
this.checked = !this.checked;
}
Expand All @@ -176,23 +179,20 @@ export class Toggle implements ComponentInterface {
}

render() {
const { inputId, disabled, checked, activated, color, el } = this;
const { activated, color, checked, disabled, el, inputId, name } = this;
const mode = getIonMode(this);
const labelId = inputId + '-lbl';
const label = findItemLabel(el);
const { label, labelId, labelText } = getAriaLabel(el, inputId);
const value = this.getValue();
if (label) {
label.id = labelId;
}
renderHiddenInput(true, el, this.name, (checked ? value : ''), disabled);

renderHiddenInput(true, el, name, (checked ? value : ''), disabled);

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="switch"
class={createColorClasses(color, {
[mode]: true,
'in-item': hostContext('ion-item', el),
Expand All @@ -207,15 +207,19 @@ export class Toggle implements ComponentInterface {
<div class="toggle-inner" part="handle" />
</div>
</div>
<button
type="button"
onFocus={this.onFocus}
onBlur={this.onBlur}
<label htmlFor={inputId}>
{labelText}
</label>
<input
type="checkbox"
role="switch"
aria-checked={`${checked}`}
disabled={disabled}
ref={btnEl => this.buttonEl = btnEl}
aria-hidden="true"
>
</button>
id={inputId}
onFocus={() => this.onFocus()}
onBlur={() => this.onBlur()}
ref={focusEl => this.focusEl = focusEl}
/>
</Host>
);
}
Expand Down

0 comments on commit 813611a

Please sign in to comment.