From b1bc58f1c8ffdc859e3f4349040bb1ad6e383d1e Mon Sep 17 00:00:00 2001 From: Shane Date: Fri, 11 Apr 2025 10:18:35 -0700 Subject: [PATCH] fix(toggle): ensure proper visual selection when navigating via VoiceOver in Safari (#30349) Issue number: resolves internal --------- ## What is the current behavior? Currently, MacOS voice over on Safari does not recognize ion-toggle correctly and fails to highlight the element properly ## What is the new behavior? 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 ## Other information --- core/src/components/toggle/toggle.scss | 8 +++-- core/src/components/toggle/toggle.tsx | 45 ++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/core/src/components/toggle/toggle.scss b/core/src/components/toggle/toggle.scss index ad78e471c0..b64107e66b 100644 --- a/core/src/components/toggle/toggle.scss +++ b/core/src/components/toggle/toggle.scss @@ -31,8 +31,6 @@ max-width: 100%; - outline: none; - cursor: pointer; user-select: none; z-index: $z-index-item-input; @@ -69,8 +67,12 @@ pointer-events: none; } +/** + * The native input must be hidden with display instead of visibility or + * aria-hidden to avoid accessibility issues with nested interactive elements. + */ input { - @include visually-hidden(); + display: none; } // Toggle Wrapper diff --git a/core/src/components/toggle/toggle.tsx b/core/src/components/toggle/toggle.tsx index 8ccdb60d3d..57a902c941 100644 --- a/core/src/components/toggle/toggle.tsx +++ b/core/src/components/toggle/toggle.tsx @@ -35,6 +35,7 @@ import type { ToggleChangeEventDetail } from './toggle-interface'; }) export class Toggle implements ComponentInterface { private inputId = `ion-tg-${toggleIds++}`; + private inputLabelId = `${this.inputId}-lbl`; private helperTextId = `${this.inputId}-helper-text`; private errorTextId = `${this.inputId}-error-text`; private gesture?: Gesture; @@ -246,6 +247,15 @@ export class Toggle implements ComponentInterface { } } + private onKeyDown = (ev: KeyboardEvent) => { + if (ev.key === ' ') { + ev.preventDefault(); + if (!this.disabled) { + this.toggleChecked(); + } + } + }; + private onClick = (ev: MouseEvent) => { if (this.disabled) { return; @@ -355,8 +365,23 @@ export class Toggle implements ComponentInterface { } render() { - const { activated, color, checked, disabled, el, justify, labelPlacement, inputId, name, alignment, required } = - this; + const { + activated, + alignment, + checked, + color, + disabled, + el, + errorTextId, + hasLabel, + inheritedAttributes, + inputId, + inputLabelId, + justify, + labelPlacement, + name, + required, + } = this; const mode = getIonMode(this); const value = this.getValue(); @@ -365,9 +390,16 @@ export class Toggle implements ComponentInterface { return ( -