4 Accessibility
Liam DeBeasi edited this page 2021-07-09 09:38:23 -04:00

Accessibility

Checkbox

Example Components

VoiceOver

In order for VoiceOver to work properly with a checkbox component there must be a native input with type="checkbox", and aria-checked and role="checkbox" must be on the host element. The aria-hidden attribute needs to be added if the checkbox is disabled, preventing iOS users from selecting it:

render() {
  const { checked, disabled } = this;

  return (
    <Host
      aria-checked={`${checked}`}
      aria-hidden={disabled ? 'true' : null}
      role="checkbox"
    >
      <input
        type="checkbox"
      />
      ...
    </Host>
  );
}

NVDA

It is required to have aria-checked on the native input for checked to read properly and disabled to prevent tabbing to the input:

render() {
  const { checked, disabled } = this;

  return (
    <Host
      aria-checked={`${checked}`}
      aria-hidden={disabled ? 'true' : null}
      role="checkbox"
    >
      <input
        type="checkbox"
        aria-checked={`${checked}`}
        disabled={disabled}
      />
      ...
    </Host>
  );
}

Labels

A helper function has been created to get the proper aria-label for the checkbox. This can be imported as getAriaLabel like the following:

const { label, labelId, labelText } = getAriaLabel(el, inputId);

where el and inputId are the following:

export class Checkbox implements ComponentInterface {
  private inputId = `ion-cb-${checkboxIds++}`;

  @Element() el!: HTMLElement;

  ...
}

This can then be added to the Host like the following:

<Host
  aria-labelledby={label ? labelId : null}
  aria-checked={`${checked}`}
  aria-hidden={disabled ? 'true' : null}
  role="checkbox"
>

In addition to that, the checkbox input should have a label added:

<Host
  aria-labelledby={label ? labelId : null}
  aria-checked={`${checked}`}
  aria-hidden={disabled ? 'true' : null}
  role="checkbox"
>
  <label htmlFor={inputId}>
    {labelText}
  </label>
  <input
    type="checkbox"
    aria-checked={`${checked}`}
    disabled={disabled}
    id={inputId}
  />

Hidden Input

A helper function to render a hidden input has been added, it can be added in the render:

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

This is required for the checkbox to work with forms.

Known Issues

When using VoiceOver on macOS, Chrome will announce the following when you are focused on a checkbox:

currently on a checkbox inside of a checkbox

This is a compromise we have to make in order for it to work with the other screen readers & Safari.

Item

Example Components

Usage with Form Components

When using form components such as ion-input or ion-toggle inside of ion-item, setting disabled="true" on the form component will disable the entire ion-item. Both the form component and the label will have a lower opacity and will not receive pointer events.

However, just doing that is not enough as screen readers do not know that the form component associated with the ion-label is disabled. The reason for this is the disabled property on a Web Component such as ion-toggle is not the same as the disabled Attribute. This disabled attribute is only supported on certain HTML elements, not Web Components. As a result, we need to set aria-disabled="true" on the ion-item so that the screen reader knows that the item as well as its label and form component are disabled.

How does ion-item know when to apply aria-disabled? ion-item listens for the ionStyle event emitted from any of the form components. The form components indicate whether or not the component is disabled. For example, here is the relevant code for ion-toggle: https://github.com/ionic-team/ionic-framework/blob/master/core/src/components/toggle/toggle.tsx#L127-L131. ion-item looks for this to determine whether or not to set aria-disabled.

Select

Example Components

Labels

When used inside of ion-item, most form components are automatically associated with the corresponding ion-label by adding an id to the label and then setting aria-labelledby on the form component. ion-select is a different story as we need to have screen readers announce both the label value as well as the text of any selected options within ion-select. As a result, we set an aria-label on the ion-select which contains the label text as well as the text of any selected options. If there are no selected options, the placeholder text is used instead.

Switch

Example Components

Voiceover

In order for VoiceOver to work properly with a switch component there must be a native input with type="checkbox" and role="switch", and aria-checked and role="switch" must be on the host element. The aria-hidden attribute needs to be added if the switch is disabled, preventing iOS users from selecting it:

render() {
  const { checked, disabled } = this;

  return (
    <Host
      aria-checked={`${checked}`}
      aria-hidden={disabled ? 'true' : null}
      role="switch"
    >
      <input
        type="checkbox"
        role="switch"
      />
      ...
    </Host>
  );
}

NVDA

It is required to have aria-checked on the native input for checked to read properly and disabled to prevent tabbing to the input:

render() {
  const { checked, disabled } = this;

  return (
    <Host
      aria-checked={`${checked}`}
      aria-hidden={disabled ? 'true' : null}
      role="switch"
    >
      <input
        type="checkbox"
        role="switch"
        aria-checked={`${checked}`}
        disabled={disabled}
      />
      ...
    </Host>
  );
}

Labels

A helper function has been created to get the proper aria-label for the switch. This can be imported as getAriaLabel like the following:

const { label, labelId, labelText } = getAriaLabel(el, inputId);

where el and inputId are the following:

export class Toggle implements ComponentInterface {
  private inputId = `ion-tg-${toggleIds++}`;

  @Element() el!: HTMLElement;

  ...
}

This can then be added to the Host like the following:

<Host
  aria-labelledby={label ? labelId : null}
  aria-checked={`${checked}`}
  aria-hidden={disabled ? 'true' : null}
  role="switch"
>

In addition to that, the checkbox input should have a label added:

<Host
  aria-labelledby={label ? labelId : null}
  aria-checked={`${checked}`}
  aria-hidden={disabled ? 'true' : null}
  role="switch"
>
  <label htmlFor={inputId}>
    {labelText}
  </label>
  <input
    type="checkbox"
    role="switch"
    aria-checked={`${checked}`}
    disabled={disabled}
    id={inputId}
  />

Hidden Input

A helper function to render a hidden input has been added, it can be added in the render:

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

This is required for the switch to work with forms.

Known Issues

When using VoiceOver on macOS or iOS, Chrome will announce the switch as a checked or unchecked checkbox:

You are currently on a switch. To select or deselect this checkbox, press Control-Option-Space.

There is a WebKit bug open for this: https://bugs.webkit.org/show_bug.cgi?id=196354