Skip to content

Commit b1bc58f

Browse files
authored
fix(toggle): ensure proper visual selection when navigating via VoiceOver in Safari (#30349)
Issue number: resolves internal --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> Currently, MacOS voice over on Safari does not recognize ion-toggle correctly and fails to highlight the element properly ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> By adding the role property to the host element, we're correctly identifying the toggle so Safari knows how to handle it. ## Does this introduce a breaking change? - [ ] Yes - [X] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer for more information. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. -->
1 parent 1bc4f59 commit b1bc58f

File tree

2 files changed

+44
-9
lines changed

2 files changed

+44
-9
lines changed

core/src/components/toggle/toggle.scss

+5-3
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@
3131

3232
max-width: 100%;
3333

34-
outline: none;
35-
3634
cursor: pointer;
3735
user-select: none;
3836
z-index: $z-index-item-input;
@@ -69,8 +67,12 @@
6967
pointer-events: none;
7068
}
7169

70+
/**
71+
* The native input must be hidden with display instead of visibility or
72+
* aria-hidden to avoid accessibility issues with nested interactive elements.
73+
*/
7274
input {
73-
@include visually-hidden();
75+
display: none;
7476
}
7577

7678
// Toggle Wrapper

core/src/components/toggle/toggle.tsx

+39-6
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,16 @@ export class Toggle implements ComponentInterface {
365390

366391
return (
367392
<Host
393+
role="switch"
394+
aria-checked={`${checked}`}
368395
aria-describedby={this.getHintTextID()}
369-
aria-invalid={this.getHintTextID() === this.errorTextId}
396+
aria-invalid={this.getHintTextID() === errorTextId}
370397
onClick={this.onClick}
398+
aria-labelledby={hasLabel ? inputLabelId : null}
399+
aria-label={inheritedAttributes['aria-label'] || null}
400+
aria-disabled={disabled ? 'true' : null}
401+
tabindex={disabled ? undefined : 0}
402+
onKeyDown={this.onKeyDown}
371403
class={createColorClasses(color, {
372404
[mode]: true,
373405
'in-item': hostContext('ion-item', el),
@@ -380,7 +412,7 @@ export class Toggle implements ComponentInterface {
380412
[`toggle-${rtl}`]: true,
381413
})}
382414
>
383-
<label class="toggle-wrapper">
415+
<label class="toggle-wrapper" htmlFor={inputId}>
384416
{/*
385417
The native control must be rendered
386418
before the visible label text due to https://bugs.webkit.org/show_bug.cgi?id=251951
@@ -396,14 +428,15 @@ export class Toggle implements ComponentInterface {
396428
onBlur={() => this.onBlur()}
397429
ref={(focusEl) => (this.focusEl = focusEl)}
398430
required={required}
399-
{...this.inheritedAttributes}
431+
{...inheritedAttributes}
400432
/>
401433
<div
402434
class={{
403435
'label-text-wrapper': true,
404-
'label-text-wrapper-hidden': !this.hasLabel,
436+
'label-text-wrapper-hidden': !hasLabel,
405437
}}
406438
part="label"
439+
id={inputLabelId}
407440
>
408441
<slot></slot>
409442
{this.renderHintText()}

0 commit comments

Comments
 (0)