Skip to content

Commit bb40a1e

Browse files
ShaneKbrandyscarneythetaPC
authored
fix(checkbox): ensure proper visual selection when navigating via VoiceOver in Safari (#30300)
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-checkbox 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 ion-checkbox as a checkbox 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. --> --------- Co-authored-by: Brandy Smith <brandyscarney@users.noreply.github.com> Co-authored-by: Maria Hutt <thetaPC@users.noreply.github.com>
1 parent 23b7a29 commit bb40a1e

File tree

2 files changed

+27
-3
lines changed

2 files changed

+27
-3
lines changed

core/src/components/checkbox/checkbox.scss

+5-1
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,12 @@
111111
display: none;
112112
}
113113

114+
/**
115+
* The native input must be hidden with display instead of visibility or
116+
* aria-hidden to avoid accessibility issues with nested interactive elements.
117+
*/
114118
input {
115-
@include visually-hidden();
119+
display: none;
116120
}
117121

118122
.native-wrapper {

core/src/components/checkbox/checkbox.tsx

+22-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import type { CheckboxChangeEventDetail } from './checkbox-interface';
3131
})
3232
export class Checkbox implements ComponentInterface {
3333
private inputId = `ion-cb-${checkboxIds++}`;
34+
private inputLabelId = `${this.inputId}-lbl`;
3435
private helperTextId = `${this.inputId}-helper-text`;
3536
private errorTextId = `${this.inputId}-error-text`;
3637
private focusEl?: HTMLElement;
@@ -181,6 +182,15 @@ export class Checkbox implements ComponentInterface {
181182
this.ionBlur.emit();
182183
};
183184

185+
private onKeyDown = (ev: KeyboardEvent) => {
186+
if (ev.key === ' ') {
187+
ev.preventDefault();
188+
if (!this.disabled) {
189+
this.toggleChecked(ev);
190+
}
191+
}
192+
};
193+
184194
private onClick = (ev: MouseEvent) => {
185195
if (this.disabled) {
186196
return;
@@ -250,14 +260,23 @@ export class Checkbox implements ComponentInterface {
250260
} = this;
251261
const mode = getIonMode(this);
252262
const path = getSVGPath(mode, indeterminate);
263+
const hasLabelContent = el.textContent !== '';
253264

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

267+
// The host element must have a checkbox role to ensure proper VoiceOver
268+
// support in Safari for accessibility.
256269
return (
257270
<Host
271+
role="checkbox"
258272
aria-checked={indeterminate ? 'mixed' : `${checked}`}
259273
aria-describedby={this.getHintTextID()}
260274
aria-invalid={this.getHintTextID() === this.errorTextId}
275+
aria-labelledby={hasLabelContent ? this.inputLabelId : null}
276+
aria-label={inheritedAttributes['aria-label'] || null}
277+
aria-disabled={disabled ? 'true' : null}
278+
tabindex={disabled ? undefined : 0}
279+
onKeyDown={this.onKeyDown}
261280
class={createColorClasses(color, {
262281
[mode]: true,
263282
'in-item': hostContext('ion-item', el),
@@ -271,7 +290,7 @@ export class Checkbox implements ComponentInterface {
271290
})}
272291
onClick={this.onClick}
273292
>
274-
<label class="checkbox-wrapper">
293+
<label class="checkbox-wrapper" htmlFor={inputId}>
275294
{/*
276295
The native control must be rendered
277296
before the visible label text due to https://bugs.webkit.org/show_bug.cgi?id=251951
@@ -291,9 +310,10 @@ export class Checkbox implements ComponentInterface {
291310
<div
292311
class={{
293312
'label-text-wrapper': true,
294-
'label-text-wrapper-hidden': el.textContent === '',
313+
'label-text-wrapper-hidden': !hasLabelContent,
295314
}}
296315
part="label"
316+
id={this.inputLabelId}
297317
>
298318
<slot></slot>
299319
{this.renderHintText()}

0 commit comments

Comments
 (0)