Skip to content

Commit ac6eaae

Browse files
committed
fix(toggle): ensure proper visual selection when navigating via VoiceOver in Safari
1 parent 1bc4f59 commit ac6eaae

File tree

2 files changed

+43
-7
lines changed

2 files changed

+43
-7
lines changed

core/src/components/toggle/toggle.scss

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,12 @@
6969
pointer-events: none;
7070
}
7171

72+
/**
73+
* The native input must be hidden with display instead of visibility or
74+
* aria-hidden to avoid accessibility issues with nested interactive elements.
75+
*/
7276
input {
73-
@include visually-hidden();
77+
display: none;
7478
}
7579

7680
// Toggle Wrapper

core/src/components/toggle/toggle.tsx

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import type { ToggleChangeEventDetail } from './toggle-interface';
3535
})
3636
export class Toggle implements ComponentInterface {
3737
private inputId = `ion-tg-${toggleIds++}`;
38+
private inputLabelId = `${this.inputId}-lbl`;
3839
private helperTextId = `${this.inputId}-helper-text`;
3940
private errorTextId = `${this.inputId}-error-text`;
4041
private gesture?: Gesture;
@@ -246,6 +247,15 @@ export class Toggle implements ComponentInterface {
246247
}
247248
}
248249

250+
private onKeyDown = (ev: KeyboardEvent) => {
251+
if (ev.key === ' ') {
252+
ev.preventDefault();
253+
if (!this.disabled) {
254+
this.toggleChecked();
255+
}
256+
}
257+
};
258+
249259
private onClick = (ev: MouseEvent) => {
250260
if (this.disabled) {
251261
return;
@@ -355,8 +365,23 @@ export class Toggle implements ComponentInterface {
355365
}
356366

357367
render() {
358-
const { activated, color, checked, disabled, el, justify, labelPlacement, inputId, name, alignment, required } =
359-
this;
368+
const {
369+
activated,
370+
alignment,
371+
checked,
372+
color,
373+
disabled,
374+
el,
375+
errorTextId,
376+
hasLabel,
377+
inheritedAttributes,
378+
inputId,
379+
inputLabelId,
380+
justify,
381+
labelPlacement,
382+
name,
383+
required,
384+
} = this;
360385

361386
const mode = getIonMode(this);
362387
const value = this.getValue();
@@ -365,9 +390,15 @@ export class Toggle implements ComponentInterface {
365390

366391
return (
367392
<Host
393+
role="switch"
368394
aria-describedby={this.getHintTextID()}
369-
aria-invalid={this.getHintTextID() === this.errorTextId}
395+
aria-invalid={this.getHintTextID() === errorTextId}
370396
onClick={this.onClick}
397+
aria-labelledby={hasLabel ? inputLabelId : null}
398+
aria-label={inheritedAttributes['aria-label'] || null}
399+
aria-disabled={disabled ? 'true' : null}
400+
tabindex={disabled ? undefined : 0}
401+
onKeyDown={this.onKeyDown}
371402
class={createColorClasses(color, {
372403
[mode]: true,
373404
'in-item': hostContext('ion-item', el),
@@ -380,7 +411,7 @@ export class Toggle implements ComponentInterface {
380411
[`toggle-${rtl}`]: true,
381412
})}
382413
>
383-
<label class="toggle-wrapper">
414+
<label class="toggle-wrapper" htmlFor={inputId}>
384415
{/*
385416
The native control must be rendered
386417
before the visible label text due to https://bugs.webkit.org/show_bug.cgi?id=251951
@@ -396,14 +427,15 @@ export class Toggle implements ComponentInterface {
396427
onBlur={() => this.onBlur()}
397428
ref={(focusEl) => (this.focusEl = focusEl)}
398429
required={required}
399-
{...this.inheritedAttributes}
430+
{...inheritedAttributes}
400431
/>
401432
<div
402433
class={{
403434
'label-text-wrapper': true,
404-
'label-text-wrapper-hidden': !this.hasLabel,
435+
'label-text-wrapper-hidden': !hasLabel,
405436
}}
406437
part="label"
438+
id={inputLabelId}
407439
>
408440
<slot></slot>
409441
{this.renderHintText()}

0 commit comments

Comments
 (0)