mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 19:21:34 +08:00
fix(radio): properly announce radios on screen readers and resolve axe errors (#22507)
This commit is contained in:
2
core/src/components.d.ts
vendored
2
core/src/components.d.ts
vendored
@ -1708,7 +1708,7 @@ export namespace Components {
|
|||||||
*/
|
*/
|
||||||
"name": string;
|
"name": string;
|
||||||
"setButtonTabindex": (value: number) => Promise<void>;
|
"setButtonTabindex": (value: number) => Promise<void>;
|
||||||
"setFocus": () => Promise<void>;
|
"setFocus": (ev: any) => Promise<void>;
|
||||||
/**
|
/**
|
||||||
* the value of the radio.
|
* the value of the radio.
|
||||||
*/
|
*/
|
||||||
|
@ -158,7 +158,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
|||||||
|
|
||||||
// If hitting arrow down or arrow right, move to the next radio
|
// If hitting arrow down or arrow right, move to the next radio
|
||||||
// If we're on the last radio, move to the first radio
|
// If we're on the last radio, move to the first radio
|
||||||
if (['ArrowDown', 'ArrowRight'].includes(ev.key)) {
|
if (['ArrowDown', 'ArrowRight'].includes(ev.code)) {
|
||||||
nextEl = (index === radios.length - 1)
|
nextEl = (index === radios.length - 1)
|
||||||
? radios[0]
|
? radios[0]
|
||||||
: radios[index + 1];
|
: radios[index + 1];
|
||||||
@ -166,7 +166,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
|||||||
|
|
||||||
// If hitting arrow up or arrow left, move to the previous radio
|
// If hitting arrow up or arrow left, move to the previous radio
|
||||||
// If we're on the first radio, move to the last radio
|
// If we're on the first radio, move to the last radio
|
||||||
if (['ArrowUp', 'ArrowLeft'].includes(ev.key)) {
|
if (['ArrowUp', 'ArrowLeft'].includes(ev.code)) {
|
||||||
nextEl = (index === 0)
|
nextEl = (index === 0)
|
||||||
? radios[radios.length - 1]
|
? radios[radios.length - 1]
|
||||||
: radios[index - 1];
|
: radios[index - 1];
|
||||||
|
@ -10,6 +10,7 @@ export class RadioGroup implements ComponentInterface {
|
|||||||
|
|
||||||
private inputId = `ion-rg-${radioGroupIds++}`;
|
private inputId = `ion-rg-${radioGroupIds++}`;
|
||||||
private labelId = `${this.inputId}-lbl`;
|
private labelId = `${this.inputId}-lbl`;
|
||||||
|
private label?: HTMLIonLabelElement | null;
|
||||||
|
|
||||||
@Element() el!: HTMLElement;
|
@Element() el!: HTMLElement;
|
||||||
|
|
||||||
@ -68,10 +69,9 @@ export class RadioGroup implements ComponentInterface {
|
|||||||
async connectedCallback() {
|
async connectedCallback() {
|
||||||
// Get the list header if it exists and set the id
|
// Get the list header if it exists and set the id
|
||||||
// this is used to set aria-labelledby
|
// this is used to set aria-labelledby
|
||||||
const el = this.el;
|
const header = this.el.querySelector('ion-list-header') || this.el.querySelector('ion-item-divider');
|
||||||
const header = el.querySelector('ion-list-header') || el.querySelector('ion-item-divider');
|
|
||||||
if (header) {
|
if (header) {
|
||||||
const label = header.querySelector('ion-label');
|
const label = this.label = header.querySelector('ion-label');
|
||||||
if (label) {
|
if (label) {
|
||||||
this.labelId = label.id = this.name + '-lbl';
|
this.labelId = label.id = this.name + '-lbl';
|
||||||
}
|
}
|
||||||
@ -83,6 +83,9 @@ export class RadioGroup implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onClick = (ev: Event) => {
|
private onClick = (ev: Event) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
const selectedRadio = ev.target && (ev.target as HTMLElement).closest('ion-radio');
|
const selectedRadio = ev.target && (ev.target as HTMLElement).closest('ion-radio');
|
||||||
if (selectedRadio) {
|
if (selectedRadio) {
|
||||||
const currentValue = this.value;
|
const currentValue = this.value;
|
||||||
@ -110,12 +113,13 @@ export class RadioGroup implements ComponentInterface {
|
|||||||
// Only move the radio if the current focus is in the radio group
|
// Only move the radio if the current focus is in the radio group
|
||||||
if (ev.target && radios.includes(ev.target)) {
|
if (ev.target && radios.includes(ev.target)) {
|
||||||
const index = radios.findIndex(radio => radio === ev.target);
|
const index = radios.findIndex(radio => radio === ev.target);
|
||||||
|
const current = radios[index];
|
||||||
|
|
||||||
let next;
|
let next;
|
||||||
|
|
||||||
// If hitting arrow down or arrow right, move to the next radio
|
// If hitting arrow down or arrow right, move to the next radio
|
||||||
// If we're on the last radio, move to the first radio
|
// If we're on the last radio, move to the first radio
|
||||||
if (['ArrowDown', 'ArrowRight'].includes(ev.key)) {
|
if (['ArrowDown', 'ArrowRight'].includes(ev.code)) {
|
||||||
next = (index === radios.length - 1)
|
next = (index === radios.length - 1)
|
||||||
? radios[0]
|
? radios[0]
|
||||||
: radios[index + 1];
|
: radios[index + 1];
|
||||||
@ -123,29 +127,39 @@ export class RadioGroup implements ComponentInterface {
|
|||||||
|
|
||||||
// If hitting arrow up or arrow left, move to the previous radio
|
// If hitting arrow up or arrow left, move to the previous radio
|
||||||
// If we're on the first radio, move to the last radio
|
// If we're on the first radio, move to the last radio
|
||||||
if (['ArrowUp', 'ArrowLeft'].includes(ev.key)) {
|
if (['ArrowUp', 'ArrowLeft'].includes(ev.code)) {
|
||||||
next = (index === 0)
|
next = (index === 0)
|
||||||
? radios[radios.length - 1]
|
? radios[radios.length - 1]
|
||||||
: radios[index - 1];
|
: radios[index - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (next && radios.includes(next)) {
|
if (next && radios.includes(next)) {
|
||||||
next.setFocus();
|
next.setFocus(ev);
|
||||||
|
|
||||||
if (!inSelectPopover) {
|
if (!inSelectPopover) {
|
||||||
this.value = next.value;
|
this.value = next.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the radio group value when a user presses the
|
||||||
|
// space bar on top of a selected radio (only applies
|
||||||
|
// to radios in a select popover)
|
||||||
|
if (['Space'].includes(ev.code)) {
|
||||||
|
this.value = current.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { label, labelId } = this;
|
||||||
|
const mode = getIonMode(this);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Host
|
<Host
|
||||||
role="radiogroup"
|
role="radiogroup"
|
||||||
aria-labelledby={this.labelId}
|
aria-labelledby={label ? labelId : null}
|
||||||
onClick={this.onClick}
|
onClick={this.onClick}
|
||||||
class={getIonMode(this)}
|
class={mode}
|
||||||
>
|
>
|
||||||
</Host>
|
</Host>
|
||||||
);
|
);
|
||||||
|
@ -20,73 +20,132 @@
|
|||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content class="outer-content">
|
<ion-content class="outer-content">
|
||||||
<ion-radio-group id="dynamicDisabled" disabled name="tannen" id="group" value="biff">
|
<p class="ion-text-center">
|
||||||
<ion-list-header>
|
<ion-button onClick="addRadio()">Add Radio</ion-button>
|
||||||
<ion-label>Luckiest Man On Earth <span id="group-value"></span></ion-label>
|
<ion-button onClick="addChecked()">Add Checked</ion-button>
|
||||||
</ion-list-header>
|
<ion-button id="removeButton" onClick="removeRadio()">Remove Radio</ion-button>
|
||||||
|
</p>
|
||||||
|
|
||||||
<ion-item>
|
<ion-list>
|
||||||
<ion-label>Biff
|
<ion-radio-group id="dynamicDisabled" disabled name="tannen" id="group" value="biff">
|
||||||
<span id="biff"></span>
|
<ion-list-header>
|
||||||
</ion-label>
|
<ion-label>Luckiest Man On Earth <span id="group-value"></span></ion-label>
|
||||||
<ion-radio value="biff" slot="start"></ion-radio>
|
</ion-list-header>
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label>Griff
|
<ion-label>Biff
|
||||||
<span id="griff"></span>
|
<span id="biff"></span>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-radio value="griff" slot="start"></ion-radio>
|
<ion-radio value="biff" slot="start"></ion-radio>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label>Buford
|
<ion-label>Griff
|
||||||
<span id="buford"></span>
|
<span id="griff"></span>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-radio value="buford" slot="start"></ion-radio>
|
<ion-radio value="griff" slot="start"></ion-radio>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label>George</ion-label>
|
<ion-label>Buford
|
||||||
<ion-radio value="george" slot="start"></ion-radio>
|
<span id="buford"></span>
|
||||||
</ion-item>
|
</ion-label>
|
||||||
|
<ion-radio value="buford" slot="start"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
</ion-radio-group>
|
<ion-item>
|
||||||
|
<ion-label>George</ion-label>
|
||||||
|
<ion-radio value="george" slot="start"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
</ion-radio-group>
|
||||||
|
</ion-list>
|
||||||
|
|
||||||
<ion-button onClick="addSelect()">Add Select</ion-button>
|
<ion-list>
|
||||||
<ion-button onClick="addCheckedSelect()">Add Checked Select</ion-button>
|
<ion-radio-group value="huey">
|
||||||
<ion-button onClick="removeSelect()">Remove Select</ion-button>
|
<ion-item>
|
||||||
|
<ion-label>Huey</ion-label>
|
||||||
|
<ion-radio slot="start" value="huey"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Dewey</ion-label>
|
||||||
|
<ion-radio slot="start" value="dewey"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Louie</ion-label>
|
||||||
|
<ion-radio slot="start" value="louie"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
</ion-radio-group>
|
||||||
|
</ion-list>
|
||||||
|
|
||||||
|
<ion-list>
|
||||||
|
<ion-radio-group value="huey">
|
||||||
|
<ion-item-divider>
|
||||||
|
<ion-label>
|
||||||
|
Maintenance Drone
|
||||||
|
</ion-label>
|
||||||
|
</ion-item-divider>
|
||||||
|
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Huey</ion-label>
|
||||||
|
<ion-radio slot="start" value="huey"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Dewey</ion-label>
|
||||||
|
<ion-radio slot="start" value="dewey"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Louie</ion-label>
|
||||||
|
<ion-radio slot="start" value="louie"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
</ion-radio-group>
|
||||||
|
</ion-list>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.outer-content {
|
.outer-content {
|
||||||
--background: #f2f2f2;
|
--background: #f2f2f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ion-list {
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
const removeButton = document.querySelector('#removeButton');
|
||||||
|
|
||||||
const valueEl = document.querySelector('#group-value');
|
const valueEl = document.querySelector('#group-value');
|
||||||
const group = document.querySelector('ion-radio-group');
|
const group = document.querySelector('ion-radio-group');
|
||||||
|
|
||||||
group.addEventListener('ionChange', (ev) => {
|
group.addEventListener('ionChange', (ev) => {
|
||||||
valueEl.textContent = group.value;
|
valueEl.textContent = group.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
customElements.whenDefined('ion-radio-group')
|
customElements.whenDefined('ion-radio-group')
|
||||||
.then(() => group.componentOnReady())
|
.then(() => group.componentOnReady())
|
||||||
.then(() => {
|
.then(() => {
|
||||||
valueEl.textContent = group.value;
|
valueEl.textContent = group.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
function addSelect() {
|
function addRadio() {
|
||||||
const item = document.createElement('ion-item');
|
const item = document.createElement('ion-item');
|
||||||
|
|
||||||
item.innerHTML = `
|
item.innerHTML = `
|
||||||
<ion-label>Item ${count}</ion-label>
|
<ion-label>Item ${count}</ion-label>
|
||||||
<ion-radio value="item-${count}" slot="start"></ion-radio>
|
<ion-radio value="item-${count}" slot="start"></ion-radio>
|
||||||
`;
|
`;
|
||||||
group.appendChild(item);
|
group.appendChild(item);
|
||||||
count++;
|
count++;
|
||||||
|
|
||||||
|
removeButton.disabled = false;
|
||||||
}
|
}
|
||||||
function addCheckedSelect() {
|
|
||||||
|
function addChecked() {
|
||||||
const item = document.createElement('ion-item');
|
const item = document.createElement('ion-item');
|
||||||
item.innerHTML = `
|
item.innerHTML = `
|
||||||
<ion-label>Item ${count}</ion-label>
|
<ion-label>Item ${count}</ion-label>
|
||||||
@ -96,9 +155,21 @@
|
|||||||
|
|
||||||
group.value = `item-${count}`;
|
group.value = `item-${count}`;
|
||||||
count++;
|
count++;
|
||||||
|
|
||||||
|
removeButton.disabled = false;
|
||||||
}
|
}
|
||||||
function removeSelect() {
|
|
||||||
group.children[group.children.length - 1].remove();
|
function removeRadio() {
|
||||||
|
const removeEl = group.children[group.children.length - 1];
|
||||||
|
|
||||||
|
if (removeEl && removeEl.tagName === 'ION-ITEM') {
|
||||||
|
removeEl.remove();
|
||||||
|
|
||||||
|
// No more radios to remove, disable button
|
||||||
|
if (!group.querySelector('ion-item')) {
|
||||||
|
removeButton.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</ion-app>
|
</ion-app>
|
||||||
|
@ -13,28 +13,28 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<ion-radio-group value="danger">
|
<ion-radio-group value="danger">
|
||||||
<ion-radio></ion-radio>
|
<ion-radio aria-label="Default"></ion-radio>
|
||||||
|
|
||||||
<ion-radio color="primary"></ion-radio>
|
<ion-radio aria-label="Primary" color="primary"></ion-radio>
|
||||||
<ion-radio color="secondary"></ion-radio>
|
<ion-radio aria-label="Secondary" color="secondary"></ion-radio>
|
||||||
<ion-radio color="tertiary"></ion-radio>
|
<ion-radio aria-label="Tertiary" color="tertiary"></ion-radio>
|
||||||
<ion-radio color="success"></ion-radio>
|
<ion-radio aria-label="Success" color="success"></ion-radio>
|
||||||
<ion-radio color="warning"></ion-radio>
|
<ion-radio aria-label="Warning" color="warning"></ion-radio>
|
||||||
<ion-radio color="danger" value="danger"></ion-radio>
|
<ion-radio aria-label="Danger" color="danger" value="danger"></ion-radio>
|
||||||
<ion-radio color="light"></ion-radio>
|
<ion-radio aria-label="Light" color="light"></ion-radio>
|
||||||
<ion-radio color="medium"></ion-radio>
|
<ion-radio aria-label="Medium" color="medium"></ion-radio>
|
||||||
<ion-radio color="dark"></ion-radio>
|
<ion-radio aria-label="Dark" color="dark"></ion-radio>
|
||||||
|
|
||||||
<ion-radio disabled></ion-radio>
|
<ion-radio aria-label="Default" disabled></ion-radio>
|
||||||
<ion-radio color="secondary" disabled></ion-radio>
|
<ion-radio aria-label="Secondary" color="secondary" disabled></ion-radio>
|
||||||
</ion-radio-group>
|
</ion-radio-group>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
allow-empty-selection="true":
|
allow-empty-selection="true":
|
||||||
<ion-radio-group allow-empty-selection="true">
|
<ion-radio-group allow-empty-selection="true">
|
||||||
<ion-radio color="primary" value="1"></ion-radio>
|
<ion-radio aria-label="Primary" color="primary" value="1"></ion-radio>
|
||||||
<ion-radio color="secondary" value="2"></ion-radio>
|
<ion-radio aria-label="Secondary" color="secondary" value="2"></ion-radio>
|
||||||
<ion-radio color="tertiary" value="3"></ion-radio>
|
<ion-radio aria-label="Tertiary" color="tertiary" value="3"></ion-radio>
|
||||||
</ion-radio-group>
|
</ion-radio-group>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -25,7 +25,6 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.radio-icon {
|
.radio-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
@ -38,11 +37,25 @@
|
|||||||
contain: layout size style;
|
contain: layout size style;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
|
||||||
@include input-cover();
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-icon,
|
.radio-icon,
|
||||||
.radio-inner {
|
.radio-inner {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
@include input-cover();
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
@include visually-hidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(:focus) {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Meth
|
|||||||
|
|
||||||
import { getIonMode } from '../../global/ionic-global';
|
import { getIonMode } from '../../global/ionic-global';
|
||||||
import { Color, StyleEventDetail } from '../../interface';
|
import { Color, StyleEventDetail } from '../../interface';
|
||||||
import { addEventListener, findItemLabel, removeEventListener } from '../../utils/helpers';
|
import { addEventListener, getAriaLabel, removeEventListener } from '../../utils/helpers';
|
||||||
import { createColorClasses, hostContext } from '../../utils/theme';
|
import { createColorClasses, hostContext } from '../../utils/theme';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -20,11 +20,10 @@ import { createColorClasses, hostContext } from '../../utils/theme';
|
|||||||
shadow: true
|
shadow: true
|
||||||
})
|
})
|
||||||
export class Radio implements ComponentInterface {
|
export class Radio implements ComponentInterface {
|
||||||
private buttonEl?: HTMLButtonElement;
|
|
||||||
private inputId = `ion-rb-${radioButtonIds++}`;
|
private inputId = `ion-rb-${radioButtonIds++}`;
|
||||||
private radioGroup: HTMLIonRadioGroupElement | null = null;
|
private radioGroup: HTMLIonRadioGroupElement | null = null;
|
||||||
|
|
||||||
@Element() el!: HTMLElement;
|
@Element() el!: HTMLIonRadioElement;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If `true`, the radio is selected.
|
* If `true`, the radio is selected.
|
||||||
@ -77,10 +76,11 @@ export class Radio implements ComponentInterface {
|
|||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
@Method()
|
@Method()
|
||||||
async setFocus() {
|
async setFocus(ev: any) {
|
||||||
if (this.buttonEl) {
|
ev.stopPropagation();
|
||||||
this.buttonEl.focus();
|
ev.preventDefault();
|
||||||
}
|
|
||||||
|
this.el.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
@ -139,17 +139,17 @@ export class Radio implements ComponentInterface {
|
|||||||
render() {
|
render() {
|
||||||
const { inputId, disabled, checked, color, el, buttonTabindex } = this;
|
const { inputId, disabled, checked, color, el, buttonTabindex } = this;
|
||||||
const mode = getIonMode(this);
|
const mode = getIonMode(this);
|
||||||
const labelId = inputId + '-lbl';
|
const { label, labelId, labelText } = getAriaLabel(el, inputId);
|
||||||
const label = findItemLabel(el);
|
|
||||||
if (label) {
|
|
||||||
label.id = labelId;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Host
|
<Host
|
||||||
role="radio"
|
|
||||||
aria-disabled={disabled ? 'true' : null}
|
|
||||||
aria-checked={`${checked}`}
|
aria-checked={`${checked}`}
|
||||||
aria-labelledby={labelId}
|
aria-hidden={disabled ? 'true' : null}
|
||||||
|
aria-labelledby={label ? labelId : null}
|
||||||
|
role="radio"
|
||||||
|
tabindex={buttonTabindex}
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur}
|
||||||
class={createColorClasses(color, {
|
class={createColorClasses(color, {
|
||||||
[mode]: true,
|
[mode]: true,
|
||||||
'in-item': hostContext('ion-item', el),
|
'in-item': hostContext('ion-item', el),
|
||||||
@ -160,16 +160,18 @@ export class Radio implements ComponentInterface {
|
|||||||
>
|
>
|
||||||
<div class="radio-icon" part="container">
|
<div class="radio-icon" part="container">
|
||||||
<div class="radio-inner" part="mark" />
|
<div class="radio-inner" part="mark" />
|
||||||
|
<div class="radio-ripple"></div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<label htmlFor={inputId}>
|
||||||
ref={btnEl => this.buttonEl = btnEl}
|
{labelText}
|
||||||
type="button"
|
</label>
|
||||||
onFocus={this.onFocus}
|
<input
|
||||||
onBlur={this.onBlur}
|
type="radio"
|
||||||
|
checked={checked}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
tabindex={buttonTabindex}
|
tabindex="-1"
|
||||||
>
|
id={inputId}
|
||||||
</button>
|
/>
|
||||||
</Host>
|
</Host>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
19
core/src/components/radio/test/a11y/e2e.ts
Normal file
19
core/src/components/radio/test/a11y/e2e.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { newE2EPage } from '@stencil/core/testing';
|
||||||
|
|
||||||
|
test('radio: a11y', async () => {
|
||||||
|
const page = await newE2EPage({
|
||||||
|
url: '/src/components/radio/test/a11y?ionic:_testing=true'
|
||||||
|
});
|
||||||
|
|
||||||
|
const compare = await page.compareScreenshot();
|
||||||
|
expect(compare).toMatchScreenshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('radio:rtl: a11y', async () => {
|
||||||
|
const page = await newE2EPage({
|
||||||
|
url: '/src/components/radio/test/a11y?ionic:_testing=true&rtl=true'
|
||||||
|
});
|
||||||
|
|
||||||
|
const compare = await page.compareScreenshot();
|
||||||
|
expect(compare).toMatchScreenshot();
|
||||||
|
});
|
166
core/src/components/radio/test/a11y/index.html
Normal file
166
core/src/components/radio/test/a11y/index.html
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" dir="ltr">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Radio - a11y</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
|
||||||
|
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
|
||||||
|
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||||
|
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||||
|
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script></head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<ion-app>
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title>Radio - a11y</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
|
||||||
|
<ion-content id="content" class="outer-content">
|
||||||
|
<div class="native-radio-group">
|
||||||
|
<p>Select a maintenance drone (native):</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input type="radio" id="huey" name="drone" value="huey" checked>
|
||||||
|
<label for="huey">Huey</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input type="radio" id="dewey" name="drone" value="dewey">
|
||||||
|
<label for="dewey">Dewey</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input type="radio" id="fooey" value="fooey" disabled/>
|
||||||
|
<label for="fooey">Fooey</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input type="radio" id="louie" name="drone" value="louie">
|
||||||
|
<label for="louie">Louie</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ion-list>
|
||||||
|
<ion-list-header>
|
||||||
|
<ion-label>
|
||||||
|
Select a maintenance drone:
|
||||||
|
</ion-label>
|
||||||
|
</ion-list-header>
|
||||||
|
<ion-radio-group value="huey">
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Huey</ion-label>
|
||||||
|
<ion-radio slot="start" value="huey"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Dewey</ion-label>
|
||||||
|
<ion-radio slot="start" value="dewey"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Fooey</ion-label>
|
||||||
|
<ion-radio slot="start" value="fooey" color="secondary" disabled></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Louie</ion-label>
|
||||||
|
<ion-radio slot="start" value="louie"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
</ion-radio-group>
|
||||||
|
</ion-list>
|
||||||
|
|
||||||
|
<ion-list>
|
||||||
|
<ion-radio-group value="huey">
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Huey</ion-label>
|
||||||
|
<ion-radio slot="start" value="huey" color="danger"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Dewey</ion-label>
|
||||||
|
<ion-radio slot="start" value="dewey" color="secondary"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Fooey</ion-label>
|
||||||
|
<ion-radio slot="start" value="fooey" color="secondary" disabled></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Louie</ion-label>
|
||||||
|
<ion-radio slot="start" value="louie" color="tertiary"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
</ion-radio-group>
|
||||||
|
</ion-list>
|
||||||
|
|
||||||
|
<div style="padding: 10px 16px">
|
||||||
|
<ion-radio-group value="louie">
|
||||||
|
<h5>Custom Labels</h5>
|
||||||
|
<div>
|
||||||
|
<ion-radio id="custom-huey" value="huey"></ion-radio>
|
||||||
|
<label for="custom-huey">Huey</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ion-radio id="custom-dewey" value="dewey"></ion-radio>
|
||||||
|
<label for="custom-dewey">Dewey</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ion-radio id="custom-fooey" value="fooey" disabled></ion-radio>
|
||||||
|
<label for="custom-fooey">Fooey</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ion-radio id="custom-louie" value="louie"></ion-radio>
|
||||||
|
<label for="custom-louie">Louie</label>
|
||||||
|
</div>
|
||||||
|
</ion-radio-group>
|
||||||
|
</div>
|
||||||
|
</ion-content>
|
||||||
|
|
||||||
|
</ion-app>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
ion-list {
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-radio-group {
|
||||||
|
background: white;
|
||||||
|
padding: 10px 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-radio-group div {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const inputs = document.querySelectorAll('ion-radio');
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
</html>
|
@ -14,49 +14,49 @@
|
|||||||
<body>
|
<body>
|
||||||
<h1>Default</h1>
|
<h1>Default</h1>
|
||||||
<ion-radio-group value="radio">
|
<ion-radio-group value="radio">
|
||||||
<ion-radio></ion-radio>
|
<ion-radio aria-label="Default"></ion-radio>
|
||||||
<ion-radio value="radio"></ion-radio>
|
<ion-radio aria-label="Default" value="radio"></ion-radio>
|
||||||
</ion-radio-group>
|
</ion-radio-group>
|
||||||
|
|
||||||
<h1>Colors: Unchecked</h1>
|
<h1>Colors: Unchecked</h1>
|
||||||
<ion-radio color="primary"></ion-radio>
|
<ion-radio aria-label="Primary" color="primary"></ion-radio>
|
||||||
<ion-radio color="secondary"></ion-radio>
|
<ion-radio aria-label="Secondary" color="secondary"></ion-radio>
|
||||||
<ion-radio color="tertiary"></ion-radio>
|
<ion-radio aria-label="Tertiary" color="tertiary"></ion-radio>
|
||||||
<ion-radio color="success"></ion-radio>
|
<ion-radio aria-label="Success" color="success"></ion-radio>
|
||||||
<ion-radio color="warning"></ion-radio>
|
<ion-radio aria-label="Warning" color="warning"></ion-radio>
|
||||||
<ion-radio color="danger"></ion-radio>
|
<ion-radio aria-label="Danger" color="danger"></ion-radio>
|
||||||
<ion-radio color="light"></ion-radio>
|
<ion-radio aria-label="Light" color="light"></ion-radio>
|
||||||
<ion-radio color="medium"></ion-radio>
|
<ion-radio aria-label="Medium" color="medium"></ion-radio>
|
||||||
<ion-radio color="dark"></ion-radio>
|
<ion-radio aria-label="Dark" color="dark"></ion-radio>
|
||||||
|
|
||||||
<h1>Colors: Checked</h1>
|
<h1>Colors: Checked</h1>
|
||||||
<ion-radio-group value="radio">
|
<ion-radio-group value="radio">
|
||||||
<ion-radio color="primary" value="radio"></ion-radio>
|
<ion-radio aria-label="Primary" color="primary" value="radio"></ion-radio>
|
||||||
<ion-radio color="secondary" value="radio"></ion-radio>
|
<ion-radio aria-label="Secondary" color="secondary" value="radio"></ion-radio>
|
||||||
<ion-radio color="tertiary" value="radio"></ion-radio>
|
<ion-radio aria-label="Tertiary" color="tertiary" value="radio"></ion-radio>
|
||||||
<ion-radio color="success" value="radio"></ion-radio>
|
<ion-radio aria-label="Success" color="success" value="radio"></ion-radio>
|
||||||
<ion-radio color="warning" value="radio"></ion-radio>
|
<ion-radio aria-label="Warning" color="warning" value="radio"></ion-radio>
|
||||||
<ion-radio color="danger" value="radio"></ion-radio>
|
<ion-radio aria-label="Danger" color="danger" value="radio"></ion-radio>
|
||||||
<ion-radio color="light" value="radio"></ion-radio>
|
<ion-radio aria-label="Light" color="light" value="radio"></ion-radio>
|
||||||
<ion-radio color="medium" value="radio"></ion-radio>
|
<ion-radio aria-label="Medium" color="medium" value="radio"></ion-radio>
|
||||||
<ion-radio color="dark" value="radio"></ion-radio>
|
<ion-radio aria-label="Dark" color="dark" value="radio"></ion-radio>
|
||||||
</ion-radio-group>
|
</ion-radio-group>
|
||||||
|
|
||||||
<h1>Disabled</h1>
|
<h1>Disabled</h1>
|
||||||
<ion-radio-group value="radio">
|
<ion-radio-group value="radio">
|
||||||
<ion-radio disabled></ion-radio>
|
<ion-radio aria-label="Default" disabled></ion-radio>
|
||||||
<ion-radio color="secondary" disabled></ion-radio>
|
<ion-radio aria-label="Secondary" color="secondary" disabled></ion-radio>
|
||||||
<ion-radio disabled value="radio"></ion-radio>
|
<ion-radio aria-label="Default" disabled value="radio"></ion-radio>
|
||||||
<ion-radio color="secondary" disabled value="radio"></ion-radio>
|
<ion-radio aria-label="Secondary" color="secondary" disabled value="radio"></ion-radio>
|
||||||
</ion-radio-group>
|
</ion-radio-group>
|
||||||
|
|
||||||
<h1>Custom</h1>
|
<h1>Custom</h1>
|
||||||
|
|
||||||
<ion-radio-group value="radio">
|
<ion-radio-group value="radio">
|
||||||
<ion-radio class="custom"></ion-radio>
|
<ion-radio aria-label="Custom" class="custom"></ion-radio>
|
||||||
<ion-radio class="custom" value="radio"></ion-radio>
|
<ion-radio aria-label="Custom" class="custom" value="radio"></ion-radio>
|
||||||
<ion-radio class="custom" color="tertiary" value="radio"></ion-radio>
|
<ion-radio aria-label="Custom Tertiary" class="custom" color="tertiary" value="radio"></ion-radio>
|
||||||
<ion-radio class="custom-size" color="danger" value="radio"></ion-radio>
|
<ion-radio aria-label="Custom Size" class="custom-size" color="danger" value="radio"></ion-radio>
|
||||||
</ion-radio-group>
|
</ion-radio-group>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -47,14 +47,14 @@
|
|||||||
<ion-list>
|
<ion-list>
|
||||||
<ion-list-header>
|
<ion-list-header>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
Default Ionic Select
|
Custom Label Ionic Select
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-list-header>
|
</ion-list-header>
|
||||||
|
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label>Choose a Pet</ion-label>
|
<label for="ionic-select">Choose a Pet Custom</label>
|
||||||
|
|
||||||
<ion-select>
|
<ion-select id="ionic-select">
|
||||||
<ion-select-option value="dog">Dog</ion-select-option>
|
<ion-select-option value="dog">Dog</ion-select-option>
|
||||||
<ion-select-option value="cat">Cat</ion-select-option>
|
<ion-select-option value="cat">Cat</ion-select-option>
|
||||||
<ion-select-option value="hamster">Hamster</ion-select-option>
|
<ion-select-option value="hamster">Hamster</ion-select-option>
|
||||||
@ -68,14 +68,14 @@
|
|||||||
<ion-list>
|
<ion-list>
|
||||||
<ion-list-header>
|
<ion-list-header>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
Custom Label Ionic Select
|
Alert Ionic Select
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-list-header>
|
</ion-list-header>
|
||||||
|
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<label for="ionic-select">Choose a Pet</label>
|
<ion-label>Choose a Pet</ion-label>
|
||||||
|
|
||||||
<ion-select id="ionic-select">
|
<ion-select>
|
||||||
<ion-select-option value="dog">Dog</ion-select-option>
|
<ion-select-option value="dog">Dog</ion-select-option>
|
||||||
<ion-select-option value="cat">Cat</ion-select-option>
|
<ion-select-option value="cat">Cat</ion-select-option>
|
||||||
<ion-select-option value="hamster">Hamster</ion-select-option>
|
<ion-select-option value="hamster">Hamster</ion-select-option>
|
||||||
|
@ -128,7 +128,7 @@ export const getAriaLabel = (componentEl: HTMLElement, inputId: string): { label
|
|||||||
// a custom label using the label element
|
// a custom label using the label element
|
||||||
const componentId = componentEl.id;
|
const componentId = componentEl.id;
|
||||||
|
|
||||||
let labelId = labelledBy !== null
|
let labelId = labelledBy !== null && labelledBy.trim() !== ''
|
||||||
? labelledBy
|
? labelledBy
|
||||||
: inputId + '-lbl';
|
: inputId + '-lbl';
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user