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>
This commit is contained in:
Shane
2025-03-28 09:15:03 -07:00
committed by GitHub
parent 23b7a29b27
commit bb40a1efe7
2 changed files with 27 additions and 3 deletions

View File

@ -111,8 +111,12 @@
display: none; display: none;
} }
/**
* The native input must be hidden with display instead of visibility or
* aria-hidden to avoid accessibility issues with nested interactive elements.
*/
input { input {
@include visually-hidden(); display: none;
} }
.native-wrapper { .native-wrapper {

View File

@ -31,6 +31,7 @@ import type { CheckboxChangeEventDetail } from './checkbox-interface';
}) })
export class Checkbox implements ComponentInterface { export class Checkbox implements ComponentInterface {
private inputId = `ion-cb-${checkboxIds++}`; private inputId = `ion-cb-${checkboxIds++}`;
private inputLabelId = `${this.inputId}-lbl`;
private helperTextId = `${this.inputId}-helper-text`; private helperTextId = `${this.inputId}-helper-text`;
private errorTextId = `${this.inputId}-error-text`; private errorTextId = `${this.inputId}-error-text`;
private focusEl?: HTMLElement; private focusEl?: HTMLElement;
@ -181,6 +182,15 @@ export class Checkbox implements ComponentInterface {
this.ionBlur.emit(); this.ionBlur.emit();
}; };
private onKeyDown = (ev: KeyboardEvent) => {
if (ev.key === ' ') {
ev.preventDefault();
if (!this.disabled) {
this.toggleChecked(ev);
}
}
};
private onClick = (ev: MouseEvent) => { private onClick = (ev: MouseEvent) => {
if (this.disabled) { if (this.disabled) {
return; return;
@ -250,14 +260,23 @@ export class Checkbox implements ComponentInterface {
} = this; } = this;
const mode = getIonMode(this); const mode = getIonMode(this);
const path = getSVGPath(mode, indeterminate); const path = getSVGPath(mode, indeterminate);
const hasLabelContent = el.textContent !== '';
renderHiddenInput(true, el, name, checked ? value : '', disabled); renderHiddenInput(true, el, name, checked ? value : '', disabled);
// The host element must have a checkbox role to ensure proper VoiceOver
// support in Safari for accessibility.
return ( return (
<Host <Host
role="checkbox"
aria-checked={indeterminate ? 'mixed' : `${checked}`} aria-checked={indeterminate ? 'mixed' : `${checked}`}
aria-describedby={this.getHintTextID()} aria-describedby={this.getHintTextID()}
aria-invalid={this.getHintTextID() === this.errorTextId} aria-invalid={this.getHintTextID() === this.errorTextId}
aria-labelledby={hasLabelContent ? this.inputLabelId : null}
aria-label={inheritedAttributes['aria-label'] || null}
aria-disabled={disabled ? 'true' : null}
tabindex={disabled ? undefined : 0}
onKeyDown={this.onKeyDown}
class={createColorClasses(color, { class={createColorClasses(color, {
[mode]: true, [mode]: true,
'in-item': hostContext('ion-item', el), 'in-item': hostContext('ion-item', el),
@ -271,7 +290,7 @@ export class Checkbox implements ComponentInterface {
})} })}
onClick={this.onClick} onClick={this.onClick}
> >
<label class="checkbox-wrapper"> <label class="checkbox-wrapper" htmlFor={inputId}>
{/* {/*
The native control must be rendered The native control must be rendered
before the visible label text due to https://bugs.webkit.org/show_bug.cgi?id=251951 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 {
<div <div
class={{ class={{
'label-text-wrapper': true, 'label-text-wrapper': true,
'label-text-wrapper-hidden': el.textContent === '', 'label-text-wrapper-hidden': !hasLabelContent,
}} }}
part="label" part="label"
id={this.inputLabelId}
> >
<slot></slot> <slot></slot>
{this.renderHintText()} {this.renderHintText()}