fix(checkbox): use a native input to fix a11y issues with axe and screen readers (#22402)

fixes #21644
fixes #20517
fixes #17796
This commit is contained in:
Brandy Carney
2020-11-12 11:25:33 -05:00
committed by GitHub
parent 0956f8bc55
commit 7214a8401b
11 changed files with 347 additions and 106 deletions

View File

@ -9,6 +9,7 @@
* [Ripple Effect](#ripple-effect)
* [Example Components](#example-components)
* [References](#references)
- [Accessibility](#accessibility)
- [Rendering Anchor or Button](#rendering-anchor-or-button)
* [Example Components](#example-components-1)
* [Component Structure](#component-structure-1)
@ -362,6 +363,130 @@ ion-ripple-effect {
- [iOS Buttons](https://developer.apple.com/design/human-interface-guidelines/ios/controls/buttons/)
## Accessibility
### Checkbox
#### Example Components
- [ion-checkbox](https://github.com/ionic-team/ionic/tree/master/core/src/components/checkbox)
#### 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:
```tsx
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:
```tsx
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:
```tsx
const { label, labelId, labelText } = getAriaLabel(el, inputId);
```
where `el` and `inputId` are the following:
```tsx
private inputId = `ion-cb-${checkboxIds++}`;
@Element() el!: HTMLElement;
```
This can then be added to the `Host` like the following:
```tsx
<Host
aria-labelledby={label ? labelId : null}
aria-checked={`${checked}`}
aria-hidden={disabled ? 'true' : null}
role="checkbox"
>
```
In addition to that, the checkbox should have a label added:
```tsx
<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`:
```tsx
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.
## Rendering Anchor or Button
Certain components can render an `<a>` or a `<button>` depending on the presence of an `href` attribute.

View File

@ -75,7 +75,7 @@
"css.sass": "sass src/css:./css",
"lint": "npm run lint.ts && npm run lint.sass",
"lint.fix": "npm run lint.ts.fix && npm run lint.sass.fix",
"lint.sass": "stylelint 'src/**/*.scss'",
"lint.sass": "stylelint \"src/**/*.scss\"",
"lint.sass.fix": "npm run lint.sass -- --fix",
"lint.ts": "tslint --project .",
"lint.ts.fix": "tslint --project . --fix",

View File

@ -394,7 +394,7 @@ export namespace Components {
*/
"name": string;
/**
* The value of the toggle does not mean if it's checked or not, use the `checked` property for that. The value of a toggle is analogous to the value of a `<input type="checkbox">`, it's only used when the toggle participates in a native `<form>`.
* The value of the checkbox does not mean if it's checked or not, use the `checked` property for that. The value of a checkbox is analogous to the value of an `<input type="checkbox">`, it's only used when the checkbox participates in a native `<form>`.
*/
"value": string;
}
@ -3705,7 +3705,7 @@ declare namespace LocalJSX {
*/
"name"?: string;
/**
* Emitted when the toggle loses focus.
* Emitted when the checkbox loses focus.
*/
"onIonBlur"?: (event: CustomEvent<void>) => void;
/**
@ -3713,7 +3713,7 @@ declare namespace LocalJSX {
*/
"onIonChange"?: (event: CustomEvent<CheckboxChangeEventDetail>) => void;
/**
* Emitted when the toggle has focus.
* Emitted when the checkbox has focus.
*/
"onIonFocus"?: (event: CustomEvent<void>) => void;
/**
@ -3721,7 +3721,7 @@ declare namespace LocalJSX {
*/
"onIonStyle"?: (event: CustomEvent<StyleEventDetail>) => void;
/**
* The value of the toggle does not mean if it's checked or not, use the `checked` property for that. The value of a toggle is analogous to the value of a `<input type="checkbox">`, it's only used when the toggle participates in a native `<form>`.
* The value of the checkbox does not mean if it's checked or not, use the `checked` property for that. The value of a checkbox is analogous to the value of an `<input type="checkbox">`, it's only used when the checkbox participates in a native `<form>`.
*/
"value"?: string;
}

View File

@ -40,8 +40,18 @@
--checkmark-color: #{current-color(contrast)};
}
button {
label {
@include input-cover();
display: flex;
align-items: center;
opacity: 0;
}
input {
@include visually-hidden();
}
.checkbox-icon {

View File

@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop
import { getIonMode } from '../../global/ionic-global';
import { CheckboxChangeEventDetail, Color, StyleEventDetail } from '../../interface';
import { findItemLabel, renderHiddenInput } from '../../utils/helpers';
import { getAriaLabel, renderHiddenInput } from '../../utils/helpers';
import { createColorClasses, hostContext } from '../../utils/theme';
/**
@ -22,7 +22,7 @@ import { createColorClasses, hostContext } from '../../utils/theme';
export class Checkbox implements ComponentInterface {
private inputId = `ion-cb-${checkboxIds++}`;
private buttonEl?: HTMLElement;
private focusEl?: HTMLElement;
@Element() el!: HTMLElement;
@ -54,11 +54,11 @@ export class Checkbox implements ComponentInterface {
@Prop() disabled = false;
/**
* The value of the toggle does not mean if it's checked or not, use the `checked`
* The value of the checkbox does not mean if it's checked or not, use the `checked`
* property for that.
*
* The value of a toggle is analogous to the value of a `<input type="checkbox">`,
* it's only used when the toggle participates in a native `<form>`.
* The value of a checkbox is analogous to the value of an `<input type="checkbox">`,
* it's only used when the checkbox participates in a native `<form>`.
*/
@Prop() value = 'on';
@ -68,12 +68,12 @@ export class Checkbox implements ComponentInterface {
@Event() ionChange!: EventEmitter<CheckboxChangeEventDetail>;
/**
* Emitted when the toggle has focus.
* Emitted when the checkbox has focus.
*/
@Event() ionFocus!: EventEmitter<void>;
/**
* Emitted when the toggle loses focus.
* Emitted when the checkbox loses focus.
*/
@Event() ionBlur!: EventEmitter<void>;
@ -109,12 +109,15 @@ export class Checkbox implements ComponentInterface {
}
private setFocus() {
if (this.buttonEl) {
this.buttonEl.focus();
if (this.focusEl) {
this.focusEl.focus();
}
}
private onClick = () => {
private onClick = (ev: Event) => {
ev.preventDefault();
ev.stopPropagation();
this.setFocus();
this.checked = !this.checked;
this.indeterminate = false;
@ -129,14 +132,11 @@ export class Checkbox implements ComponentInterface {
}
render() {
const { inputId, indeterminate, disabled, checked, value, color, el } = this;
const labelId = inputId + '-lbl';
const { color, checked, disabled, el, indeterminate, inputId, name, value } = this;
const mode = getIonMode(this);
const label = findItemLabel(el);
if (label) {
label.id = labelId;
}
renderHiddenInput(true, el, this.name, (checked ? value : ''), disabled);
const { label, labelId, labelText } = getAriaLabel(el, inputId);
renderHiddenInput(true, el, name, (checked ? value : ''), disabled);
let path = indeterminate
? <path d="M6 12L18 12" part="mark" />
@ -151,10 +151,10 @@ export class Checkbox implements ComponentInterface {
return (
<Host
onClick={this.onClick}
role="checkbox"
aria-disabled={disabled ? 'true' : null}
aria-labelledby={label ? labelId : null}
aria-checked={`${checked}`}
aria-labelledby={labelId}
aria-hidden={disabled ? 'true' : null}
role="checkbox"
class={createColorClasses(color, {
[mode]: true,
'in-item': hostContext('ion-item', el),
@ -167,14 +167,18 @@ export class Checkbox implements ComponentInterface {
<svg class="checkbox-icon" viewBox="0 0 24 24" part="container">
{path}
</svg>
<button
type="button"
onFocus={this.onFocus}
onBlur={this.onBlur}
disabled={this.disabled}
ref={btnEl => this.buttonEl = btnEl}
>
</button>
<label htmlFor={inputId}>
{labelText}
</label>
<input
type="checkbox"
aria-checked={`${checked}`}
disabled={disabled}
id={inputId}
onFocus={() => this.onFocus()}
onBlur={() => this.onBlur()}
ref={focusEl => this.focusEl = focusEl}
/>
</Host>
);
}

View File

@ -266,16 +266,16 @@ export default defineComponent({
| `indeterminate` | `indeterminate` | If `true`, the checkbox will visually appear as indeterminate. | `boolean` | `false` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
| `name` | `name` | The name of the control, which is submitted with the form data. | `string` | `this.inputId` |
| `value` | `value` | The value of the toggle does not mean if it's checked or not, use the `checked` property for that. The value of a toggle is analogous to the value of a `<input type="checkbox">`, it's only used when the toggle participates in a native `<form>`. | `string` | `'on'` |
| `value` | `value` | The value of the checkbox does not mean if it's checked or not, use the `checked` property for that. The value of a checkbox is analogous to the value of an `<input type="checkbox">`, it's only used when the checkbox participates in a native `<form>`. | `string` | `'on'` |
## Events
| Event | Description | Type |
| ----------- | ---------------------------------------------- | ---------------------------------------- |
| `ionBlur` | Emitted when the toggle loses focus. | `CustomEvent<void>` |
| `ionBlur` | Emitted when the checkbox loses focus. | `CustomEvent<void>` |
| `ionChange` | Emitted when the checked property has changed. | `CustomEvent<CheckboxChangeEventDetail>` |
| `ionFocus` | Emitted when the toggle has focus. | `CustomEvent<void>` |
| `ionFocus` | Emitted when the checkbox has focus. | `CustomEvent<void>` |
## Shadow Parts

View File

@ -38,7 +38,7 @@
<ion-item>
<ion-label>Secondary</ion-label>
<ion-checkbox checked color="secondary"></ion-checkbox>
<ion-checkbox disabled checked color="secondary"></ion-checkbox>
</ion-item>
<ion-item>
@ -103,6 +103,31 @@
</ion-content>
</ion-app>
<script>
const inputs = document.querySelectorAll('ion-checkbox');
for (var i = 0; i < inputs.length; i++) {
const input = inputs[i];
input.addEventListener('ionBlur', function() {
console.log('Listen ionBlur: fired');
});
input.addEventListener('ionFocus', function() {
console.log('Listen ionFocus: fired');
});
input.addEventListener('ionChange', function(ev) {
console.log('Listen ionChange: fired', ev.detail);
});
input.addEventListener('click', function() {
console.log('Listen click: fired');
});
}
</script>
</body>
</html>

View File

@ -31,22 +31,22 @@
<div class="ion-padding-start">
<!-- Default to unchecked -->
<label for="unchecked">Unchecked</label>
<input name="unchecked" type="checkbox">
<input name="unchecked" id="unchecked" type="checkbox">
<br>
<!-- Default to checked -->
<label for="checked">Checked</label>
<input name="checked" type="checkbox" checked />
<input name="checked" id="checked" type="checkbox" checked />
<br>
<!-- Default to indeterminate -->
<label for="indeterminate">Indeterminate</label>
<input name="indeterminate" type="checkbox" class="indeterminate">
<input name="indeterminate" id="indeterminate" type="checkbox" class="indeterminate">
<br>
<!-- Default to checked / indeterminate -->
<label for="both">Checked / Indeterminate</label>
<input name="both" type="checkbox" checked class="indeterminate">
<input name="both" id="both" type="checkbox" checked class="indeterminate">
<br>
</div>
@ -81,15 +81,15 @@
</ion-label>
</ion-list-header>
<div class="ion-padding-start">
<ion-checkbox indeterminate></ion-checkbox>
<ion-checkbox indeterminate color="secondary"></ion-checkbox>
<ion-checkbox indeterminate color="tertiary"></ion-checkbox>
<ion-checkbox indeterminate color="success"></ion-checkbox>
<ion-checkbox indeterminate color="warning"></ion-checkbox>
<ion-checkbox indeterminate color="danger"></ion-checkbox>
<ion-checkbox indeterminate color="dark"></ion-checkbox>
<ion-checkbox indeterminate color="medium"></ion-checkbox>
<ion-checkbox indeterminate color="light"></ion-checkbox>
<ion-checkbox aria-label="Default Indeterminate" indeterminate></ion-checkbox>
<ion-checkbox aria-label="Secondary Indeterminate" indeterminate color="secondary"></ion-checkbox>
<ion-checkbox aria-label="Tertiary Indeterminate" indeterminate color="tertiary"></ion-checkbox>
<ion-checkbox aria-label="Success Indeterminate" indeterminate color="success"></ion-checkbox>
<ion-checkbox aria-label="Warning Indeterminate" indeterminate color="warning"></ion-checkbox>
<ion-checkbox aria-label="Danger Indeterminate" indeterminate color="danger"></ion-checkbox>
<ion-checkbox aria-label="Dark Indeterminate" indeterminate color="dark"></ion-checkbox>
<ion-checkbox aria-label="Medium Indeterminate" indeterminate color="medium"></ion-checkbox>
<ion-checkbox aria-label="Light Indeterminate" indeterminate color="light"></ion-checkbox>
</div>
<ion-list-header>
@ -100,20 +100,20 @@
<ul>
<li>
<ion-checkbox name="tall" id="tall" indeterminate></ion-checkbox>
<label for="tall">Tall Things</label>
<ion-checkbox aria-labelledby="tall-label-0" indeterminate></ion-checkbox>
<label id="tall-label-0">Tall Things</label>
<ul>
<li>
<ion-checkbox name="tall-1" id="tall-1" checked></ion-checkbox>
<label for="tall-1">Skyscrapers</label>
<ion-checkbox aria-labelledby="tall-label-1" checked></ion-checkbox>
<label id="tall-label-1">Skyscrapers</label>
</li>
<li>
<ion-checkbox name="tall-2" id="tall-2"></ion-checkbox>
<label for="tall-2">Trees</label>
<ion-checkbox aria-labelledby="tall-label-2"></ion-checkbox>
<label id="tall-label-2">Trees</label>
</li>
<li>
<ion-checkbox name="tall-2" id="tall-2"></ion-checkbox>
<label for="tall-2">Giants</label>
<ion-checkbox aria-labelledby="tall-label-3"></ion-checkbox>
<label id="tall-label-3">Giants</label>
</li>
</ul>
</li>

View File

@ -13,67 +13,67 @@
<body class="ion-padding">
<h1>Default</h1>
<ion-checkbox></ion-checkbox>
<ion-checkbox checked></ion-checkbox>
<ion-checkbox disabled></ion-checkbox>
<ion-checkbox disabled checked></ion-checkbox>
<ion-checkbox aria-label="Default Checkbox"></ion-checkbox>
<ion-checkbox aria-label="Default Checkbox" checked></ion-checkbox>
<ion-checkbox aria-label="Default Checkbox" disabled></ion-checkbox>
<ion-checkbox aria-label="Default Checkbox" disabled checked></ion-checkbox>
<h1>Colors</h1>
<ion-checkbox color="primary"></ion-checkbox>
<ion-checkbox color="secondary"></ion-checkbox>
<ion-checkbox color="tertiary"></ion-checkbox>
<ion-checkbox color="success"></ion-checkbox>
<ion-checkbox color="warning"></ion-checkbox>
<ion-checkbox color="danger"></ion-checkbox>
<ion-checkbox color="light"></ion-checkbox>
<ion-checkbox color="medium"></ion-checkbox>
<ion-checkbox color="dark"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Primary" color="primary"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Secondary" color="secondary"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Tertiary" color="tertiary"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Success" color="success"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Warning" color="warning"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Danger" color="danger"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Light" color="light"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Medium" color="medium"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Dark" color="dark"></ion-checkbox>
<hr>
<ion-checkbox checked color="primary"></ion-checkbox>
<ion-checkbox checked color="secondary"></ion-checkbox>
<ion-checkbox checked color="tertiary"></ion-checkbox>
<ion-checkbox checked color="success"></ion-checkbox>
<ion-checkbox checked color="warning"></ion-checkbox>
<ion-checkbox checked color="danger"></ion-checkbox>
<ion-checkbox checked color="light"></ion-checkbox>
<ion-checkbox checked color="medium"></ion-checkbox>
<ion-checkbox checked color="dark"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Primary" checked color="primary"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Secondary" checked color="secondary"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Tertiary" checked color="tertiary"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Success" checked color="success"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Warning" checked color="warning"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Danger" checked color="danger"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Light" checked color="light"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Medium" checked color="medium"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Dark" checked color="dark"></ion-checkbox>
<hr>
<ion-checkbox checked disabled color="primary"></ion-checkbox>
<ion-checkbox checked disabled color="secondary"></ion-checkbox>
<ion-checkbox checked disabled color="tertiary"></ion-checkbox>
<ion-checkbox checked disabled color="success"></ion-checkbox>
<ion-checkbox checked disabled color="warning"></ion-checkbox>
<ion-checkbox checked disabled color="danger"></ion-checkbox>
<ion-checkbox checked disabled color="light"></ion-checkbox>
<ion-checkbox checked disabled color="medium"></ion-checkbox>
<ion-checkbox checked disabled color="dark"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Primary" checked disabled color="primary"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Secondary" checked disabled color="secondary"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Tertiary" checked disabled color="tertiary"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Success" checked disabled color="success"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Warning" checked disabled color="warning"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Danger" checked disabled color="danger"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Light" checked disabled color="light"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Medium" checked disabled color="medium"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Dark" checked disabled color="dark"></ion-checkbox>
<h1>Custom</h1>
<ion-checkbox class="custom"></ion-checkbox>
<ion-checkbox class="custom" checked></ion-checkbox>
<ion-checkbox class="custom" disabled></ion-checkbox>
<ion-checkbox class="custom" disabled checked></ion-checkbox>
<ion-checkbox aria-label="Checkbox Custom" class="custom"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Custom" class="custom" checked></ion-checkbox>
<ion-checkbox aria-label="Checkbox Custom" class="custom" disabled></ion-checkbox>
<ion-checkbox aria-label="Checkbox Custom" class="custom" disabled checked></ion-checkbox>
<h1>Custom: checked</h1>
<ion-checkbox class="custom-checked"></ion-checkbox>
<ion-checkbox class="custom-checked" checked></ion-checkbox>
<ion-checkbox class="custom-checked" disabled></ion-checkbox>
<ion-checkbox class="custom-checked" disabled checked></ion-checkbox>
<ion-checkbox aria-label="Checkbox Custom" class="custom-checked"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Custom" class="custom-checked" checked></ion-checkbox>
<ion-checkbox aria-label="Checkbox Custom" class="custom-checked" disabled></ion-checkbox>
<ion-checkbox aria-label="Checkbox Custom" class="custom-checked" disabled checked></ion-checkbox>
<h1>Custom: light</h1>
<ion-checkbox class="custom-light"></ion-checkbox>
<ion-checkbox class="custom-light" checked></ion-checkbox>
<ion-checkbox class="custom-light" disabled></ion-checkbox>
<ion-checkbox class="custom-light" disabled checked></ion-checkbox>
<ion-checkbox aria-label="Checkbox Custom Light" class="custom-light"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Custom Light" class="custom-light" checked></ion-checkbox>
<ion-checkbox aria-label="Checkbox Custom Light" class="custom-light" disabled></ion-checkbox>
<ion-checkbox aria-label="Checkbox Custom Light" class="custom-light" disabled checked></ion-checkbox>
<h1>Custom: transition</h1>
<ion-checkbox class="custom-transition"></ion-checkbox>
<ion-checkbox class="custom-transition" checked></ion-checkbox>
<ion-checkbox aria-label="Checkbox Custom Transition" class="custom-transition"></ion-checkbox>
<ion-checkbox aria-label="Checkbox Custom Transition" class="custom-transition" checked></ion-checkbox>
<style>
.custom {

View File

@ -21,6 +21,31 @@
}
}
@mixin visually-hidden() {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
border: 0;
outline: 0;
clip: rect(0 0 0 0);
opacity: 0;
overflow: hidden;
-webkit-appearance: none;
-moz-appearance: none;
}
@mixin text-inherit() {
font-family: inherit;
font-size: inherit;

View File

@ -70,7 +70,7 @@ export const hasShadowDom = (el: HTMLElement) => {
return !!el.shadowRoot && !!(el as any).attachShadow;
};
export const findItemLabel = (componentEl: HTMLElement) => {
export const findItemLabel = (componentEl: HTMLElement): HTMLIonLabelElement | null => {
const itemEl = componentEl.closest('ion-item');
if (itemEl) {
return itemEl.querySelector('ion-label');
@ -78,6 +78,58 @@ export const findItemLabel = (componentEl: HTMLElement) => {
return null;
};
/**
* This method is used for Ionic's input components that use Shadow DOM. In
* order to properly label the inputs to work with screen readers, we need
* to get the text content of the label outside of the shadow root and pass
* it to the input inside of the shadow root.
*
* Referencing label elements by id from outside of the component is
* impossible due to the shadow boundary, read more here:
* https://developer.salesforce.com/blogs/2020/01/accessibility-for-web-components.html
*
* @param componentEl The shadow element that needs the aria label
* @param inputId The unique identifier for the input
*/
export const getAriaLabel = (componentEl: HTMLElement, inputId: string): { label: Element | null, labelId: string, labelText: string | null | undefined } => {
let labelText;
// If the user provides their own label via the aria-labelledby attr
// we should use that instead of looking for an ion-label
const labelledBy = componentEl.getAttribute('aria-labelledby');
const labelId = labelledBy !== null
? labelledBy
: inputId + '-lbl';
const label = labelledBy !== null
? document.querySelector(`#${labelledBy}`)
: findItemLabel(componentEl);
if (label) {
if (labelledBy === null) {
label.id = labelId;
}
labelText = label.textContent;
label.setAttribute('aria-hidden', 'true');
}
return { label, labelId, labelText };
};
/**
* This method is used to add a hidden input to a host element that contains
* a Shadow DOM. It does not add the input inside of the Shadow root which
* allows it to be picked up inside of forms. It should contain the same
* values as the host element.
*
* @param always Add a hidden input even if the container does not use Shadow
* @param container The element where the input will be added
* @param name The name of the input
* @param value The value of the input
* @param disabled If true, the input is disabled
*/
export const renderHiddenInput = (always: boolean, container: HTMLElement, name: string, value: string | undefined | null, disabled: boolean) => {
if (always || hasShadowDom(container)) {
let input = container.querySelector('input.aux-input') as HTMLInputElement | null;