mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-19 03:32:21 +08:00
fix(toggle): use a native input to fix a11y issues with axe and screen readers (#22477)
fixes #22011 references #21552
This commit is contained in:
2
.github/COMPONENT-GUIDE.md
vendored
2
.github/COMPONENT-GUIDE.md
vendored
@ -10,6 +10,7 @@
|
|||||||
* [Example Components](#example-components)
|
* [Example Components](#example-components)
|
||||||
* [References](#references)
|
* [References](#references)
|
||||||
- [Accessibility](#accessibility)
|
- [Accessibility](#accessibility)
|
||||||
|
* [Checkbox](#checkbox)
|
||||||
- [Rendering Anchor or Button](#rendering-anchor-or-button)
|
- [Rendering Anchor or Button](#rendering-anchor-or-button)
|
||||||
* [Example Components](#example-components-1)
|
* [Example Components](#example-components-1)
|
||||||
* [Component Structure](#component-structure-1)
|
* [Component Structure](#component-structure-1)
|
||||||
@ -370,6 +371,7 @@ ion-ripple-effect {
|
|||||||
#### Example Components
|
#### Example Components
|
||||||
|
|
||||||
- [ion-checkbox](https://github.com/ionic-team/ionic/tree/master/core/src/components/checkbox)
|
- [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
|
#### VoiceOver
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<ion-title>Toggle - Basic</ion-title>
|
<ion-title>Toggle - Basic</ion-title>
|
||||||
<ion-buttons slot="primary">
|
<ion-buttons slot="primary">
|
||||||
<ion-toggle></ion-toggle>
|
<ion-toggle aria-label="Toggle"></ion-toggle>
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
@ -86,7 +86,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<ion-toggle id="standAloneChecked"></ion-toggle>
|
<ion-toggle aria-label="Stand-alone toggle" id="standAloneChecked"></ion-toggle>
|
||||||
Stand-alone toggle:
|
Stand-alone toggle:
|
||||||
<span id="standAloneCheckedSpan"></span>
|
<span id="standAloneCheckedSpan"></span>
|
||||||
</p>
|
</p>
|
||||||
|
@ -25,53 +25,53 @@
|
|||||||
|
|
||||||
<ion-content class="ion-padding-horizontal">
|
<ion-content class="ion-padding-horizontal">
|
||||||
<h1>Default</h1>
|
<h1>Default</h1>
|
||||||
<ion-toggle></ion-toggle>
|
<ion-toggle aria-label="Default"></ion-toggle>
|
||||||
<ion-toggle checked></ion-toggle>
|
<ion-toggle aria-label="Default" checked></ion-toggle>
|
||||||
<ion-toggle color="danger"></ion-toggle>
|
<ion-toggle aria-label="Default Danger" color="danger"></ion-toggle>
|
||||||
<ion-toggle color="danger" checked></ion-toggle>
|
<ion-toggle aria-label="Default Danger" color="danger" checked></ion-toggle>
|
||||||
<ion-toggle color="tertiary" class="toggle-activated"></ion-toggle>
|
<ion-toggle aria-label="Default Tertiary" color="tertiary" class="toggle-activated"></ion-toggle>
|
||||||
<ion-toggle color="tertiary" checked class="toggle-activated"></ion-toggle>
|
<ion-toggle aria-label="Default Tertiary Activated" color="tertiary" checked class="toggle-activated"></ion-toggle>
|
||||||
|
|
||||||
<h1>Custom Widths</h1>
|
<h1>Custom Widths</h1>
|
||||||
<ion-toggle color="secondary" class="width-small"></ion-toggle>
|
<ion-toggle aria-label="Secondary Small Width" color="secondary" class="width-small"></ion-toggle>
|
||||||
<ion-toggle color="secondary" checked class="width-small"></ion-toggle>
|
<ion-toggle aria-label="Secondary Small Width" color="secondary" checked class="width-small"></ion-toggle>
|
||||||
<ion-toggle color="secondary" class="width-large"></ion-toggle>
|
<ion-toggle aria-label="Secondary Large Width" color="secondary" class="width-large"></ion-toggle>
|
||||||
<ion-toggle color="secondary" checked class="width-large"></ion-toggle>
|
<ion-toggle aria-label="Secondary Large Width" color="secondary" checked class="width-large"></ion-toggle>
|
||||||
<ion-toggle color="tertiary" class="width-large toggle-activated"></ion-toggle>
|
<ion-toggle aria-label="Tertiary Large Width Activated" 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="Tertiary Large Width Activated" color="tertiary" checked class="width-large toggle-activated"></ion-toggle>
|
||||||
|
|
||||||
<h1>Custom Heights</h1>
|
<h1>Custom Heights</h1>
|
||||||
<div style="display: flex; flex-flow: column; float: left;">
|
<div style="display: flex; flex-flow: column; float: left;">
|
||||||
<ion-toggle class="height-small"></ion-toggle>
|
<ion-toggle aria-label="Small Height" class="height-small"></ion-toggle>
|
||||||
<ion-toggle checked class="height-small"></ion-toggle>
|
<ion-toggle aria-label="Small Height" checked class="height-small"></ion-toggle>
|
||||||
</div>
|
</div>
|
||||||
<ion-toggle class="height-large"></ion-toggle>
|
<ion-toggle aria-label="Large Height" class="height-large"></ion-toggle>
|
||||||
<ion-toggle checked class="height-large"></ion-toggle>
|
<ion-toggle aria-label="Large Height" checked class="height-large"></ion-toggle>
|
||||||
<ion-toggle class="handle-height-large"></ion-toggle>
|
<ion-toggle aria-label="Large Height" class="handle-height-large"></ion-toggle>
|
||||||
<ion-toggle checked class="handle-height-large"></ion-toggle>
|
<ion-toggle aria-label="Large Height" checked class="handle-height-large"></ion-toggle>
|
||||||
<ion-toggle checked class="handle-height-large toggle-activated"></ion-toggle>
|
<ion-toggle aria-label="Large Height Activated" checked class="handle-height-large toggle-activated"></ion-toggle>
|
||||||
|
|
||||||
<h1>Dynamic Sizes</h1>
|
<h1>Dynamic Sizes</h1>
|
||||||
<ion-toggle color="tertiary" class="dynamic-small width-small height-small"></ion-toggle>
|
<ion-toggle aria-label="Tertiary Small Width Small Height" 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 aria-label="Tertiary Small Width Small Height" 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 aria-label="Tertiary Large Width Large Height" 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 Large Width Large Height" color="tertiary" checked class="dynamic-large width-large height-large"></ion-toggle>
|
||||||
|
|
||||||
<h1>Complex Custom Toggles</h1>
|
<h1>Complex Custom Toggles</h1>
|
||||||
<ion-toggle mode="ios" class="all-custom"></ion-toggle>
|
<ion-toggle aria-label="Custom" mode="ios" class="all-custom"></ion-toggle>
|
||||||
<ion-toggle mode="ios" checked 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 aria-label="Custom Overflow" class="custom-overflow"></ion-toggle>
|
||||||
<ion-toggle checked 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 aria-label="Custom Spacing iOS" 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" checked class="custom-spacing"></ion-toggle>
|
||||||
|
|
||||||
<ion-toggle mode="md" color="dark" class="custom-spacing"></ion-toggle>
|
<ion-toggle aria-label="Custom Spacing MD" 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" checked class="custom-spacing"></ion-toggle>
|
||||||
|
|
||||||
<ion-toggle mode="ios" class="icon-custom"></ion-toggle>
|
<ion-toggle aria-label="Custom Icon iOS" 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" checked class="icon-custom"></ion-toggle>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
</ion-app>
|
</ion-app>
|
||||||
|
|
||||||
|
@ -13,23 +13,23 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<!-- Default -->
|
<!-- Default -->
|
||||||
<ion-toggle></ion-toggle>
|
<ion-toggle aria-label="Default Toggle"></ion-toggle>
|
||||||
|
|
||||||
<!-- Colors -->
|
<!-- Colors -->
|
||||||
<ion-toggle checked color="primary"></ion-toggle>
|
<ion-toggle aria-label="Primary Toggle" checked color="primary"></ion-toggle>
|
||||||
<ion-toggle checked color="secondary"></ion-toggle>
|
<ion-toggle aria-label="Secondary Toggle" checked color="secondary"></ion-toggle>
|
||||||
<ion-toggle checked color="tertiary"></ion-toggle>
|
<ion-toggle aria-label="Tertiary Toggle" checked color="tertiary"></ion-toggle>
|
||||||
<ion-toggle checked color="success"></ion-toggle>
|
<ion-toggle aria-label="Success Toggle" checked color="success"></ion-toggle>
|
||||||
<ion-toggle checked color="warning"></ion-toggle>
|
<ion-toggle aria-label="Warning Toggle" checked color="warning"></ion-toggle>
|
||||||
<ion-toggle checked color="danger"></ion-toggle>
|
<ion-toggle aria-label="Danger Toggle" checked color="danger"></ion-toggle>
|
||||||
<ion-toggle checked color="light"></ion-toggle>
|
<ion-toggle aria-label="Light Toggle" checked color="light"></ion-toggle>
|
||||||
<ion-toggle checked color="medium"></ion-toggle>
|
<ion-toggle aria-label="Medium Toggle" checked color="medium"></ion-toggle>
|
||||||
<ion-toggle checked color="dark"></ion-toggle>
|
<ion-toggle aria-label="Dark Toggle" checked color="dark"></ion-toggle>
|
||||||
<ion-toggle checked class="custom"></ion-toggle>
|
<ion-toggle aria-label="Custom Toggle" checked class="custom"></ion-toggle>
|
||||||
|
|
||||||
<!-- Disabled -->
|
<!-- Disabled -->
|
||||||
<ion-toggle checked disabled></ion-toggle>
|
<ion-toggle aria-label="Disabled Default Toggle" checked disabled></ion-toggle>
|
||||||
<ion-toggle checked disabled color="secondary"></ion-toggle>
|
<ion-toggle aria-label="Disabled Secondary Toggle" checked disabled color="secondary"></ion-toggle>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.custom {
|
.custom {
|
||||||
|
@ -45,8 +45,18 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
label {
|
||||||
@include input-cover();
|
@include input-cover();
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
@include visually-hidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle Background Track: Unchecked
|
// Toggle Background Track: Unchecked
|
||||||
|
@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop
|
|||||||
|
|
||||||
import { getIonMode } from '../../global/ionic-global';
|
import { getIonMode } from '../../global/ionic-global';
|
||||||
import { Color, Gesture, GestureDetail, StyleEventDetail, ToggleChangeEventDetail } from '../../interface';
|
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 { hapticSelection } from '../../utils/native/haptic';
|
||||||
import { createColorClasses, hostContext } from '../../utils/theme';
|
import { createColorClasses, hostContext } from '../../utils/theme';
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ export class Toggle implements ComponentInterface {
|
|||||||
|
|
||||||
private inputId = `ion-tg-${toggleIds++}`;
|
private inputId = `ion-tg-${toggleIds++}`;
|
||||||
private gesture?: Gesture;
|
private gesture?: Gesture;
|
||||||
private buttonEl?: HTMLElement;
|
private focusEl?: HTMLElement;
|
||||||
private lastDrag = 0;
|
private lastDrag = 0;
|
||||||
|
|
||||||
@Element() el!: HTMLElement;
|
@Element() el!: HTMLElement;
|
||||||
@ -156,12 +156,15 @@ export class Toggle implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setFocus() {
|
private setFocus() {
|
||||||
if (this.buttonEl) {
|
if (this.focusEl) {
|
||||||
this.buttonEl.focus();
|
this.focusEl.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onClick = () => {
|
private onClick = (ev: Event) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
if (this.lastDrag + 300 < Date.now()) {
|
if (this.lastDrag + 300 < Date.now()) {
|
||||||
this.checked = !this.checked;
|
this.checked = !this.checked;
|
||||||
}
|
}
|
||||||
@ -176,23 +179,20 @@ export class Toggle implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { inputId, disabled, checked, activated, color, el } = this;
|
const { activated, color, checked, disabled, el, inputId, name } = this;
|
||||||
const mode = getIonMode(this);
|
const mode = getIonMode(this);
|
||||||
const labelId = inputId + '-lbl';
|
const { label, labelId, labelText } = getAriaLabel(el, inputId);
|
||||||
const label = findItemLabel(el);
|
|
||||||
const value = this.getValue();
|
const value = this.getValue();
|
||||||
if (label) {
|
|
||||||
label.id = labelId;
|
renderHiddenInput(true, el, name, (checked ? value : ''), disabled);
|
||||||
}
|
|
||||||
renderHiddenInput(true, el, this.name, (checked ? value : ''), disabled);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Host
|
<Host
|
||||||
onClick={this.onClick}
|
onClick={this.onClick}
|
||||||
role="checkbox"
|
aria-labelledby={label ? labelId : null}
|
||||||
aria-disabled={disabled ? 'true' : null}
|
|
||||||
aria-checked={`${checked}`}
|
aria-checked={`${checked}`}
|
||||||
aria-labelledby={labelId}
|
aria-hidden={disabled ? 'true' : null}
|
||||||
|
role="switch"
|
||||||
class={createColorClasses(color, {
|
class={createColorClasses(color, {
|
||||||
[mode]: true,
|
[mode]: true,
|
||||||
'in-item': hostContext('ion-item', el),
|
'in-item': hostContext('ion-item', el),
|
||||||
@ -207,15 +207,19 @@ export class Toggle implements ComponentInterface {
|
|||||||
<div class="toggle-inner" part="handle" />
|
<div class="toggle-inner" part="handle" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<label htmlFor={inputId}>
|
||||||
type="button"
|
{labelText}
|
||||||
onFocus={this.onFocus}
|
</label>
|
||||||
onBlur={this.onBlur}
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={`${checked}`}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
ref={btnEl => this.buttonEl = btnEl}
|
id={inputId}
|
||||||
aria-hidden="true"
|
onFocus={() => this.onFocus()}
|
||||||
>
|
onBlur={() => this.onBlur()}
|
||||||
</button>
|
ref={focusEl => this.focusEl = focusEl}
|
||||||
|
/>
|
||||||
</Host>
|
</Host>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user