mirror of
				https://github.com/ionic-team/ionic-framework.git
				synced 2025-11-04 21:30:00 +08:00 
			
		
		
		
	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:
		@ -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 {
 | 
				
			||||||
 | 
				
			|||||||
@ -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()}
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user