octicon-rss(16/)
You've already forked ionic-framework
mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-10 22:44:13 +08:00
fix(radio): properly announce radios on screen readers and resolve axe errors (#22507)
This commit is contained in:
octicon-git-branch(16/)
octicon-tag(16/)
committed by
GitHub
gitea-unlock(16/)
parent
4e23aad3d9
commit
afcc46e1cc
octicon-diff(16/tw-mr-1) 12 changed files with 409 additions and 124 deletions
@@ -25,7 +25,6 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
.radio-icon {
|
||||
display: flex;
|
||||
|
||||
@@ -38,11 +37,25 @@
|
||||
contain: layout size style;
|
||||
}
|
||||
|
||||
button {
|
||||
@include input-cover();
|
||||
}
|
||||
|
||||
.radio-icon,
|
||||
.radio-inner {
|
||||
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 { Color, StyleEventDetail } from '../../interface';
|
||||
import { addEventListener, findItemLabel, removeEventListener } from '../../utils/helpers';
|
||||
import { addEventListener, getAriaLabel, removeEventListener } from '../../utils/helpers';
|
||||
import { createColorClasses, hostContext } from '../../utils/theme';
|
||||
|
||||
/**
|
||||
@@ -20,11 +20,10 @@ import { createColorClasses, hostContext } from '../../utils/theme';
|
||||
shadow: true
|
||||
})
|
||||
export class Radio implements ComponentInterface {
|
||||
private buttonEl?: HTMLButtonElement;
|
||||
private inputId = `ion-rb-${radioButtonIds++}`;
|
||||
private radioGroup: HTMLIonRadioGroupElement | null = null;
|
||||
|
||||
@Element() el!: HTMLElement;
|
||||
@Element() el!: HTMLIonRadioElement;
|
||||
|
||||
/**
|
||||
* If `true`, the radio is selected.
|
||||
@@ -77,10 +76,11 @@ export class Radio implements ComponentInterface {
|
||||
|
||||
/** @internal */
|
||||
@Method()
|
||||
async setFocus() {
|
||||
if (this.buttonEl) {
|
||||
this.buttonEl.focus();
|
||||
}
|
||||
async setFocus(ev: any) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
this.el.focus();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@@ -139,17 +139,17 @@ export class Radio implements ComponentInterface {
|
||||
render() {
|
||||
const { inputId, disabled, checked, color, el, buttonTabindex } = this;
|
||||
const mode = getIonMode(this);
|
||||
const labelId = inputId + '-lbl';
|
||||
const label = findItemLabel(el);
|
||||
if (label) {
|
||||
label.id = labelId;
|
||||
}
|
||||
const { label, labelId, labelText } = getAriaLabel(el, inputId);
|
||||
|
||||
return (
|
||||
<Host
|
||||
role="radio"
|
||||
aria-disabled={disabled ? 'true' : null}
|
||||
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, {
|
||||
[mode]: true,
|
||||
'in-item': hostContext('ion-item', el),
|
||||
@@ -160,16 +160,18 @@ export class Radio implements ComponentInterface {
|
||||
>
|
||||
<div class="radio-icon" part="container">
|
||||
<div class="radio-inner" part="mark" />
|
||||
<div class="radio-ripple"></div>
|
||||
</div>
|
||||
<button
|
||||
ref={btnEl => this.buttonEl = btnEl}
|
||||
type="button"
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
<label htmlFor={inputId}>
|
||||
{labelText}
|
||||
</label>
|
||||
<input
|
||||
type="radio"
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
tabindex={buttonTabindex}
|
||||
>
|
||||
</button>
|
||||
tabindex="-1"
|
||||
id={inputId}
|
||||
/>
|
||||
</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>
|
||||
<h1>Default</h1>
|
||||
<ion-radio-group value="radio">
|
||||
<ion-radio></ion-radio>
|
||||
<ion-radio value="radio"></ion-radio>
|
||||
<ion-radio aria-label="Default"></ion-radio>
|
||||
<ion-radio aria-label="Default" value="radio"></ion-radio>
|
||||
</ion-radio-group>
|
||||
|
||||
<h1>Colors: Unchecked</h1>
|
||||
<ion-radio color="primary"></ion-radio>
|
||||
<ion-radio color="secondary"></ion-radio>
|
||||
<ion-radio color="tertiary"></ion-radio>
|
||||
<ion-radio color="success"></ion-radio>
|
||||
<ion-radio color="warning"></ion-radio>
|
||||
<ion-radio color="danger"></ion-radio>
|
||||
<ion-radio color="light"></ion-radio>
|
||||
<ion-radio color="medium"></ion-radio>
|
||||
<ion-radio color="dark"></ion-radio>
|
||||
<ion-radio aria-label="Primary" color="primary"></ion-radio>
|
||||
<ion-radio aria-label="Secondary" color="secondary"></ion-radio>
|
||||
<ion-radio aria-label="Tertiary" color="tertiary"></ion-radio>
|
||||
<ion-radio aria-label="Success" color="success"></ion-radio>
|
||||
<ion-radio aria-label="Warning" color="warning"></ion-radio>
|
||||
<ion-radio aria-label="Danger" color="danger"></ion-radio>
|
||||
<ion-radio aria-label="Light" color="light"></ion-radio>
|
||||
<ion-radio aria-label="Medium" color="medium"></ion-radio>
|
||||
<ion-radio aria-label="Dark" color="dark"></ion-radio>
|
||||
|
||||
<h1>Colors: Checked</h1>
|
||||
<ion-radio-group value="radio">
|
||||
<ion-radio color="primary" value="radio"></ion-radio>
|
||||
<ion-radio color="secondary" value="radio"></ion-radio>
|
||||
<ion-radio color="tertiary" value="radio"></ion-radio>
|
||||
<ion-radio color="success" value="radio"></ion-radio>
|
||||
<ion-radio color="warning" value="radio"></ion-radio>
|
||||
<ion-radio color="danger" value="radio"></ion-radio>
|
||||
<ion-radio color="light" value="radio"></ion-radio>
|
||||
<ion-radio color="medium" value="radio"></ion-radio>
|
||||
<ion-radio color="dark" value="radio"></ion-radio>
|
||||
<ion-radio aria-label="Primary" color="primary" value="radio"></ion-radio>
|
||||
<ion-radio aria-label="Secondary" color="secondary" value="radio"></ion-radio>
|
||||
<ion-radio aria-label="Tertiary" color="tertiary" value="radio"></ion-radio>
|
||||
<ion-radio aria-label="Success" color="success" value="radio"></ion-radio>
|
||||
<ion-radio aria-label="Warning" color="warning" value="radio"></ion-radio>
|
||||
<ion-radio aria-label="Danger" color="danger" value="radio"></ion-radio>
|
||||
<ion-radio aria-label="Light" color="light" value="radio"></ion-radio>
|
||||
<ion-radio aria-label="Medium" color="medium" value="radio"></ion-radio>
|
||||
<ion-radio aria-label="Dark" color="dark" value="radio"></ion-radio>
|
||||
</ion-radio-group>
|
||||
|
||||
<h1>Disabled</h1>
|
||||
<ion-radio-group value="radio">
|
||||
<ion-radio disabled></ion-radio>
|
||||
<ion-radio color="secondary" disabled></ion-radio>
|
||||
<ion-radio disabled value="radio"></ion-radio>
|
||||
<ion-radio color="secondary" disabled value="radio"></ion-radio>
|
||||
<ion-radio aria-label="Default" disabled></ion-radio>
|
||||
<ion-radio aria-label="Secondary" color="secondary" disabled></ion-radio>
|
||||
<ion-radio aria-label="Default" disabled value="radio"></ion-radio>
|
||||
<ion-radio aria-label="Secondary" color="secondary" disabled value="radio"></ion-radio>
|
||||
</ion-radio-group>
|
||||
|
||||
<h1>Custom</h1>
|
||||
|
||||
<ion-radio-group value="radio">
|
||||
<ion-radio class="custom"></ion-radio>
|
||||
<ion-radio class="custom" value="radio"></ion-radio>
|
||||
<ion-radio class="custom" color="tertiary" value="radio"></ion-radio>
|
||||
<ion-radio class="custom-size" color="danger" value="radio"></ion-radio>
|
||||
<ion-radio aria-label="Custom" class="custom"></ion-radio>
|
||||
<ion-radio aria-label="Custom" class="custom" value="radio"></ion-radio>
|
||||
<ion-radio aria-label="Custom Tertiary" class="custom" color="tertiary" value="radio"></ion-radio>
|
||||
<ion-radio aria-label="Custom Size" class="custom-size" color="danger" value="radio"></ion-radio>
|
||||
</ion-radio-group>
|
||||
|
||||
<style>
|
||||
|
||||
Reference in New Issue
Block a user