mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 03:00:58 +08:00
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:
125
.github/COMPONENT-GUIDE.md
vendored
125
.github/COMPONENT-GUIDE.md
vendored
@ -9,6 +9,7 @@
|
|||||||
* [Ripple Effect](#ripple-effect)
|
* [Ripple Effect](#ripple-effect)
|
||||||
* [Example Components](#example-components)
|
* [Example Components](#example-components)
|
||||||
* [References](#references)
|
* [References](#references)
|
||||||
|
- [Accessibility](#accessibility)
|
||||||
- [Rendering Anchor or Button](#rendering-anchor-or-button)
|
- [Rendering Anchor or Button](#rendering-anchor-or-button)
|
||||||
* [Example Components](#example-components-1)
|
* [Example Components](#example-components-1)
|
||||||
* [Component Structure](#component-structure-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/)
|
- [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
|
## Rendering Anchor or Button
|
||||||
|
|
||||||
Certain components can render an `<a>` or a `<button>` depending on the presence of an `href` attribute.
|
Certain components can render an `<a>` or a `<button>` depending on the presence of an `href` attribute.
|
||||||
|
@ -75,7 +75,7 @@
|
|||||||
"css.sass": "sass src/css:./css",
|
"css.sass": "sass src/css:./css",
|
||||||
"lint": "npm run lint.ts && npm run lint.sass",
|
"lint": "npm run lint.ts && npm run lint.sass",
|
||||||
"lint.fix": "npm run lint.ts.fix && npm run lint.sass.fix",
|
"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.sass.fix": "npm run lint.sass -- --fix",
|
||||||
"lint.ts": "tslint --project .",
|
"lint.ts": "tslint --project .",
|
||||||
"lint.ts.fix": "tslint --project . --fix",
|
"lint.ts.fix": "tslint --project . --fix",
|
||||||
|
8
core/src/components.d.ts
vendored
8
core/src/components.d.ts
vendored
@ -394,7 +394,7 @@ export namespace Components {
|
|||||||
*/
|
*/
|
||||||
"name": string;
|
"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;
|
"value": string;
|
||||||
}
|
}
|
||||||
@ -3705,7 +3705,7 @@ declare namespace LocalJSX {
|
|||||||
*/
|
*/
|
||||||
"name"?: string;
|
"name"?: string;
|
||||||
/**
|
/**
|
||||||
* Emitted when the toggle loses focus.
|
* Emitted when the checkbox loses focus.
|
||||||
*/
|
*/
|
||||||
"onIonBlur"?: (event: CustomEvent<void>) => void;
|
"onIonBlur"?: (event: CustomEvent<void>) => void;
|
||||||
/**
|
/**
|
||||||
@ -3713,7 +3713,7 @@ declare namespace LocalJSX {
|
|||||||
*/
|
*/
|
||||||
"onIonChange"?: (event: CustomEvent<CheckboxChangeEventDetail>) => void;
|
"onIonChange"?: (event: CustomEvent<CheckboxChangeEventDetail>) => void;
|
||||||
/**
|
/**
|
||||||
* Emitted when the toggle has focus.
|
* Emitted when the checkbox has focus.
|
||||||
*/
|
*/
|
||||||
"onIonFocus"?: (event: CustomEvent<void>) => void;
|
"onIonFocus"?: (event: CustomEvent<void>) => void;
|
||||||
/**
|
/**
|
||||||
@ -3721,7 +3721,7 @@ declare namespace LocalJSX {
|
|||||||
*/
|
*/
|
||||||
"onIonStyle"?: (event: CustomEvent<StyleEventDetail>) => void;
|
"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;
|
"value"?: string;
|
||||||
}
|
}
|
||||||
|
@ -40,8 +40,18 @@
|
|||||||
--checkmark-color: #{current-color(contrast)};
|
--checkmark-color: #{current-color(contrast)};
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
label {
|
||||||
@include input-cover();
|
@include input-cover();
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
@include visually-hidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-icon {
|
.checkbox-icon {
|
||||||
|
@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop
|
|||||||
|
|
||||||
import { getIonMode } from '../../global/ionic-global';
|
import { getIonMode } from '../../global/ionic-global';
|
||||||
import { CheckboxChangeEventDetail, Color, StyleEventDetail } from '../../interface';
|
import { CheckboxChangeEventDetail, Color, StyleEventDetail } from '../../interface';
|
||||||
import { findItemLabel, renderHiddenInput } from '../../utils/helpers';
|
import { getAriaLabel, renderHiddenInput } from '../../utils/helpers';
|
||||||
import { createColorClasses, hostContext } from '../../utils/theme';
|
import { createColorClasses, hostContext } from '../../utils/theme';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -22,7 +22,7 @@ import { createColorClasses, hostContext } from '../../utils/theme';
|
|||||||
export class Checkbox implements ComponentInterface {
|
export class Checkbox implements ComponentInterface {
|
||||||
|
|
||||||
private inputId = `ion-cb-${checkboxIds++}`;
|
private inputId = `ion-cb-${checkboxIds++}`;
|
||||||
private buttonEl?: HTMLElement;
|
private focusEl?: HTMLElement;
|
||||||
|
|
||||||
@Element() el!: HTMLElement;
|
@Element() el!: HTMLElement;
|
||||||
|
|
||||||
@ -54,11 +54,11 @@ export class Checkbox implements ComponentInterface {
|
|||||||
@Prop() disabled = false;
|
@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.
|
* property for that.
|
||||||
*
|
*
|
||||||
* The value of a toggle is analogous to the value of a `<input type="checkbox">`,
|
* The value of a checkbox is analogous to the value of an `<input type="checkbox">`,
|
||||||
* it's only used when the toggle participates in a native `<form>`.
|
* it's only used when the checkbox participates in a native `<form>`.
|
||||||
*/
|
*/
|
||||||
@Prop() value = 'on';
|
@Prop() value = 'on';
|
||||||
|
|
||||||
@ -68,12 +68,12 @@ export class Checkbox implements ComponentInterface {
|
|||||||
@Event() ionChange!: EventEmitter<CheckboxChangeEventDetail>;
|
@Event() ionChange!: EventEmitter<CheckboxChangeEventDetail>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when the toggle has focus.
|
* Emitted when the checkbox has focus.
|
||||||
*/
|
*/
|
||||||
@Event() ionFocus!: EventEmitter<void>;
|
@Event() ionFocus!: EventEmitter<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when the toggle loses focus.
|
* Emitted when the checkbox loses focus.
|
||||||
*/
|
*/
|
||||||
@Event() ionBlur!: EventEmitter<void>;
|
@Event() ionBlur!: EventEmitter<void>;
|
||||||
|
|
||||||
@ -109,12 +109,15 @@ export class Checkbox implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setFocus() {
|
private setFocus() {
|
||||||
if (this.buttonEl) {
|
if (this.focusEl) {
|
||||||
this.buttonEl.focus();
|
this.focusEl.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onClick = () => {
|
private onClick = (ev: Event) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
this.setFocus();
|
this.setFocus();
|
||||||
this.checked = !this.checked;
|
this.checked = !this.checked;
|
||||||
this.indeterminate = false;
|
this.indeterminate = false;
|
||||||
@ -129,14 +132,11 @@ export class Checkbox implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { inputId, indeterminate, disabled, checked, value, color, el } = this;
|
const { color, checked, disabled, el, indeterminate, inputId, name, value } = this;
|
||||||
const labelId = inputId + '-lbl';
|
|
||||||
const mode = getIonMode(this);
|
const mode = getIonMode(this);
|
||||||
const label = findItemLabel(el);
|
const { label, labelId, labelText } = getAriaLabel(el, inputId);
|
||||||
if (label) {
|
|
||||||
label.id = labelId;
|
renderHiddenInput(true, el, name, (checked ? value : ''), disabled);
|
||||||
}
|
|
||||||
renderHiddenInput(true, el, this.name, (checked ? value : ''), disabled);
|
|
||||||
|
|
||||||
let path = indeterminate
|
let path = indeterminate
|
||||||
? <path d="M6 12L18 12" part="mark" />
|
? <path d="M6 12L18 12" part="mark" />
|
||||||
@ -151,10 +151,10 @@ export class Checkbox implements ComponentInterface {
|
|||||||
return (
|
return (
|
||||||
<Host
|
<Host
|
||||||
onClick={this.onClick}
|
onClick={this.onClick}
|
||||||
role="checkbox"
|
aria-labelledby={label ? labelId : null}
|
||||||
aria-disabled={disabled ? 'true' : null}
|
|
||||||
aria-checked={`${checked}`}
|
aria-checked={`${checked}`}
|
||||||
aria-labelledby={labelId}
|
aria-hidden={disabled ? 'true' : null}
|
||||||
|
role="checkbox"
|
||||||
class={createColorClasses(color, {
|
class={createColorClasses(color, {
|
||||||
[mode]: true,
|
[mode]: true,
|
||||||
'in-item': hostContext('ion-item', el),
|
'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">
|
<svg class="checkbox-icon" viewBox="0 0 24 24" part="container">
|
||||||
{path}
|
{path}
|
||||||
</svg>
|
</svg>
|
||||||
<button
|
<label htmlFor={inputId}>
|
||||||
type="button"
|
{labelText}
|
||||||
onFocus={this.onFocus}
|
</label>
|
||||||
onBlur={this.onBlur}
|
<input
|
||||||
disabled={this.disabled}
|
type="checkbox"
|
||||||
ref={btnEl => this.buttonEl = btnEl}
|
aria-checked={`${checked}`}
|
||||||
>
|
disabled={disabled}
|
||||||
</button>
|
id={inputId}
|
||||||
|
onFocus={() => this.onFocus()}
|
||||||
|
onBlur={() => this.onBlur()}
|
||||||
|
ref={focusEl => this.focusEl = focusEl}
|
||||||
|
/>
|
||||||
</Host>
|
</Host>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -266,16 +266,16 @@ export default defineComponent({
|
|||||||
| `indeterminate` | `indeterminate` | If `true`, the checkbox will visually appear as indeterminate. | `boolean` | `false` |
|
| `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` |
|
| `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` |
|
| `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
|
## Events
|
||||||
|
|
||||||
| Event | Description | Type |
|
| 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>` |
|
| `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
|
## Shadow Parts
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label>Secondary</ion-label>
|
<ion-label>Secondary</ion-label>
|
||||||
<ion-checkbox checked color="secondary"></ion-checkbox>
|
<ion-checkbox disabled checked color="secondary"></ion-checkbox>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-item>
|
<ion-item>
|
||||||
@ -103,6 +103,31 @@
|
|||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
||||||
</ion-app>
|
</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>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
@ -31,22 +31,22 @@
|
|||||||
<div class="ion-padding-start">
|
<div class="ion-padding-start">
|
||||||
<!-- Default to unchecked -->
|
<!-- Default to unchecked -->
|
||||||
<label for="unchecked">Unchecked</label>
|
<label for="unchecked">Unchecked</label>
|
||||||
<input name="unchecked" type="checkbox">
|
<input name="unchecked" id="unchecked" type="checkbox">
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<!-- Default to checked -->
|
<!-- Default to checked -->
|
||||||
<label for="checked">Checked</label>
|
<label for="checked">Checked</label>
|
||||||
<input name="checked" type="checkbox" checked />
|
<input name="checked" id="checked" type="checkbox" checked />
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<!-- Default to indeterminate -->
|
<!-- Default to indeterminate -->
|
||||||
<label for="indeterminate">Indeterminate</label>
|
<label for="indeterminate">Indeterminate</label>
|
||||||
<input name="indeterminate" type="checkbox" class="indeterminate">
|
<input name="indeterminate" id="indeterminate" type="checkbox" class="indeterminate">
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<!-- Default to checked / indeterminate -->
|
<!-- Default to checked / indeterminate -->
|
||||||
<label for="both">Checked / Indeterminate</label>
|
<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>
|
<br>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -81,15 +81,15 @@
|
|||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-list-header>
|
</ion-list-header>
|
||||||
<div class="ion-padding-start">
|
<div class="ion-padding-start">
|
||||||
<ion-checkbox indeterminate></ion-checkbox>
|
<ion-checkbox aria-label="Default Indeterminate" indeterminate></ion-checkbox>
|
||||||
<ion-checkbox indeterminate color="secondary"></ion-checkbox>
|
<ion-checkbox aria-label="Secondary Indeterminate" indeterminate color="secondary"></ion-checkbox>
|
||||||
<ion-checkbox indeterminate color="tertiary"></ion-checkbox>
|
<ion-checkbox aria-label="Tertiary Indeterminate" indeterminate color="tertiary"></ion-checkbox>
|
||||||
<ion-checkbox indeterminate color="success"></ion-checkbox>
|
<ion-checkbox aria-label="Success Indeterminate" indeterminate color="success"></ion-checkbox>
|
||||||
<ion-checkbox indeterminate color="warning"></ion-checkbox>
|
<ion-checkbox aria-label="Warning Indeterminate" indeterminate color="warning"></ion-checkbox>
|
||||||
<ion-checkbox indeterminate color="danger"></ion-checkbox>
|
<ion-checkbox aria-label="Danger Indeterminate" indeterminate color="danger"></ion-checkbox>
|
||||||
<ion-checkbox indeterminate color="dark"></ion-checkbox>
|
<ion-checkbox aria-label="Dark Indeterminate" indeterminate color="dark"></ion-checkbox>
|
||||||
<ion-checkbox indeterminate color="medium"></ion-checkbox>
|
<ion-checkbox aria-label="Medium Indeterminate" indeterminate color="medium"></ion-checkbox>
|
||||||
<ion-checkbox indeterminate color="light"></ion-checkbox>
|
<ion-checkbox aria-label="Light Indeterminate" indeterminate color="light"></ion-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ion-list-header>
|
<ion-list-header>
|
||||||
@ -100,20 +100,20 @@
|
|||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<ion-checkbox name="tall" id="tall" indeterminate></ion-checkbox>
|
<ion-checkbox aria-labelledby="tall-label-0" indeterminate></ion-checkbox>
|
||||||
<label for="tall">Tall Things</label>
|
<label id="tall-label-0">Tall Things</label>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<ion-checkbox name="tall-1" id="tall-1" checked></ion-checkbox>
|
<ion-checkbox aria-labelledby="tall-label-1" checked></ion-checkbox>
|
||||||
<label for="tall-1">Skyscrapers</label>
|
<label id="tall-label-1">Skyscrapers</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<ion-checkbox name="tall-2" id="tall-2"></ion-checkbox>
|
<ion-checkbox aria-labelledby="tall-label-2"></ion-checkbox>
|
||||||
<label for="tall-2">Trees</label>
|
<label id="tall-label-2">Trees</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<ion-checkbox name="tall-2" id="tall-2"></ion-checkbox>
|
<ion-checkbox aria-labelledby="tall-label-3"></ion-checkbox>
|
||||||
<label for="tall-2">Giants</label>
|
<label id="tall-label-3">Giants</label>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
@ -13,67 +13,67 @@
|
|||||||
|
|
||||||
<body class="ion-padding">
|
<body class="ion-padding">
|
||||||
<h1>Default</h1>
|
<h1>Default</h1>
|
||||||
<ion-checkbox></ion-checkbox>
|
<ion-checkbox aria-label="Default Checkbox"></ion-checkbox>
|
||||||
<ion-checkbox checked></ion-checkbox>
|
<ion-checkbox aria-label="Default Checkbox" checked></ion-checkbox>
|
||||||
<ion-checkbox disabled></ion-checkbox>
|
<ion-checkbox aria-label="Default Checkbox" disabled></ion-checkbox>
|
||||||
<ion-checkbox disabled checked></ion-checkbox>
|
<ion-checkbox aria-label="Default Checkbox" disabled checked></ion-checkbox>
|
||||||
|
|
||||||
<h1>Colors</h1>
|
<h1>Colors</h1>
|
||||||
<ion-checkbox color="primary"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Primary" color="primary"></ion-checkbox>
|
||||||
<ion-checkbox color="secondary"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Secondary" color="secondary"></ion-checkbox>
|
||||||
<ion-checkbox color="tertiary"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Tertiary" color="tertiary"></ion-checkbox>
|
||||||
<ion-checkbox color="success"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Success" color="success"></ion-checkbox>
|
||||||
<ion-checkbox color="warning"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Warning" color="warning"></ion-checkbox>
|
||||||
<ion-checkbox color="danger"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Danger" color="danger"></ion-checkbox>
|
||||||
<ion-checkbox color="light"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Light" color="light"></ion-checkbox>
|
||||||
<ion-checkbox color="medium"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Medium" color="medium"></ion-checkbox>
|
||||||
<ion-checkbox color="dark"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Dark" color="dark"></ion-checkbox>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<ion-checkbox checked color="primary"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Primary" checked color="primary"></ion-checkbox>
|
||||||
<ion-checkbox checked color="secondary"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Secondary" checked color="secondary"></ion-checkbox>
|
||||||
<ion-checkbox checked color="tertiary"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Tertiary" checked color="tertiary"></ion-checkbox>
|
||||||
<ion-checkbox checked color="success"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Success" checked color="success"></ion-checkbox>
|
||||||
<ion-checkbox checked color="warning"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Warning" checked color="warning"></ion-checkbox>
|
||||||
<ion-checkbox checked color="danger"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Danger" checked color="danger"></ion-checkbox>
|
||||||
<ion-checkbox checked color="light"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Light" checked color="light"></ion-checkbox>
|
||||||
<ion-checkbox checked color="medium"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Medium" checked color="medium"></ion-checkbox>
|
||||||
<ion-checkbox checked color="dark"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Dark" checked color="dark"></ion-checkbox>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<ion-checkbox checked disabled color="primary"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Primary" checked disabled color="primary"></ion-checkbox>
|
||||||
<ion-checkbox checked disabled color="secondary"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Secondary" checked disabled color="secondary"></ion-checkbox>
|
||||||
<ion-checkbox checked disabled color="tertiary"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Tertiary" checked disabled color="tertiary"></ion-checkbox>
|
||||||
<ion-checkbox checked disabled color="success"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Success" checked disabled color="success"></ion-checkbox>
|
||||||
<ion-checkbox checked disabled color="warning"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Warning" checked disabled color="warning"></ion-checkbox>
|
||||||
<ion-checkbox checked disabled color="danger"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Danger" checked disabled color="danger"></ion-checkbox>
|
||||||
<ion-checkbox checked disabled color="light"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Light" checked disabled color="light"></ion-checkbox>
|
||||||
<ion-checkbox checked disabled color="medium"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Medium" checked disabled color="medium"></ion-checkbox>
|
||||||
<ion-checkbox checked disabled color="dark"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Dark" checked disabled color="dark"></ion-checkbox>
|
||||||
|
|
||||||
<h1>Custom</h1>
|
<h1>Custom</h1>
|
||||||
<ion-checkbox class="custom"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Custom" class="custom"></ion-checkbox>
|
||||||
<ion-checkbox class="custom" checked></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Custom" class="custom" checked></ion-checkbox>
|
||||||
<ion-checkbox class="custom" disabled></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Custom" class="custom" disabled></ion-checkbox>
|
||||||
<ion-checkbox class="custom" disabled checked></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Custom" class="custom" disabled checked></ion-checkbox>
|
||||||
|
|
||||||
<h1>Custom: checked</h1>
|
<h1>Custom: checked</h1>
|
||||||
<ion-checkbox class="custom-checked"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Custom" class="custom-checked"></ion-checkbox>
|
||||||
<ion-checkbox class="custom-checked" checked></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Custom" class="custom-checked" checked></ion-checkbox>
|
||||||
<ion-checkbox class="custom-checked" disabled></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Custom" class="custom-checked" disabled></ion-checkbox>
|
||||||
<ion-checkbox class="custom-checked" disabled checked></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Custom" class="custom-checked" disabled checked></ion-checkbox>
|
||||||
|
|
||||||
<h1>Custom: light</h1>
|
<h1>Custom: light</h1>
|
||||||
<ion-checkbox class="custom-light"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Custom Light" class="custom-light"></ion-checkbox>
|
||||||
<ion-checkbox class="custom-light" checked></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Custom Light" class="custom-light" checked></ion-checkbox>
|
||||||
<ion-checkbox class="custom-light" disabled></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Custom Light" 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" disabled checked></ion-checkbox>
|
||||||
|
|
||||||
<h1>Custom: transition</h1>
|
<h1>Custom: transition</h1>
|
||||||
<ion-checkbox class="custom-transition"></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Custom Transition" class="custom-transition"></ion-checkbox>
|
||||||
<ion-checkbox class="custom-transition" checked></ion-checkbox>
|
<ion-checkbox aria-label="Checkbox Custom Transition" class="custom-transition" checked></ion-checkbox>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.custom {
|
.custom {
|
||||||
|
@ -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() {
|
@mixin text-inherit() {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
@ -509,4 +534,4 @@
|
|||||||
transform: $rtl-translate $extra;
|
transform: $rtl-translate $extra;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,7 @@ export const hasShadowDom = (el: HTMLElement) => {
|
|||||||
return !!el.shadowRoot && !!(el as any).attachShadow;
|
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');
|
const itemEl = componentEl.closest('ion-item');
|
||||||
if (itemEl) {
|
if (itemEl) {
|
||||||
return itemEl.querySelector('ion-label');
|
return itemEl.querySelector('ion-label');
|
||||||
@ -78,6 +78,58 @@ export const findItemLabel = (componentEl: HTMLElement) => {
|
|||||||
return null;
|
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) => {
|
export const renderHiddenInput = (always: boolean, container: HTMLElement, name: string, value: string | undefined | null, disabled: boolean) => {
|
||||||
if (always || hasShadowDom(container)) {
|
if (always || hasShadowDom(container)) {
|
||||||
let input = container.querySelector('input.aux-input') as HTMLInputElement | null;
|
let input = container.querySelector('input.aux-input') as HTMLInputElement | null;
|
||||||
|
Reference in New Issue
Block a user