Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(radio): properly announce radios on screen readers #22507

Merged
merged 88 commits into from Nov 24, 2020
Merged
Show file tree
Hide file tree
Changes from 87 commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
51a5459
fix(item): only add click events to clickable items
brandyscarney Oct 19, 2020
55d7cac
style: lint
Oct 19, 2020
8e82313
style(item): lint
Oct 19, 2020
37895dc
fix(checkbox): changing button to label
brandyscarney Oct 26, 2020
59f5fe4
fix(checkbox): make input type checkbox
brandyscarney Oct 29, 2020
c60d897
style(lint): fix lint errors, accidentally removed code, type issues
brandyscarney Oct 29, 2020
6025a9a
fix(checkbox): aXe errors
brandyscarney Oct 29, 2020
5c933c5
Merge branch 'master' into fix-checkbox-nvda
brandyscarney Oct 29, 2020
d6f9a04
Merge branch 'master' into fix-item-clickable-a11y
brandyscarney Oct 29, 2020
8a30f8d
test(item): include item with an input added later
brandyscarney Oct 29, 2020
30f6484
fix(item): add the event listener on update in case the input is delayed
brandyscarney Oct 29, 2020
6d4c51a
style(lint): fix lint errors with trailing spaces
brandyscarney Oct 29, 2020
b5318ec
chore: use rimraf
brandyscarney Oct 30, 2020
9918fd7
Merge branch 'fix-item-clickable-a11y' into fix-checkbox-nvda
brandyscarney Oct 30, 2020
b70554a
fix(checkbox): move onClick to prevent double calls
brandyscarney Oct 30, 2020
d40bb3d
fix(checkbox): hidden input is required for form submission
brandyscarney Oct 30, 2020
3016d9a
come onnnnn
brandyscarney Oct 30, 2020
87a2164
trailing white space
brandyscarney Oct 30, 2020
0ef56e0
chore: build
Nov 2, 2020
c274a96
Merge branch 'master' into fix-checkbox-nvda
Nov 2, 2020
d0d8037
chore: build
Nov 2, 2020
d052d7b
fix(checkbox): move onClick back to host for click events to work wit…
Nov 2, 2020
bacd5e1
Merge branch 'fix-checkbox-nvda' into fix-toggle-nvda
Nov 3, 2020
2aca1f4
fix(checkbox): don't add aria-labelledby if there is no label
Nov 3, 2020
41cdb5a
Merge branch 'fix-checkbox-nvda' into fix-toggle-nvda
Nov 3, 2020
8dafd02
Merge branch 'master' into fix-checkbox-nvda
Nov 3, 2020
ac8f502
fix(checkbox): use the user's aria-labelledby if it exists
Nov 3, 2020
b2fea1c
test(checkbox): resolve all axe errors
Nov 3, 2020
e2bd28b
fix(checkbox): work with voiceover
Nov 3, 2020
d79a511
style(helpers): fix lint errors
Nov 3, 2020
af83c6c
fix(item): properly remove event listener
Nov 4, 2020
449b5dc
Merge branch 'fix-item-clickable-a11y' into fix-checkbox-nvda
Nov 4, 2020
57375b0
fix(item): check if listener function exists instead
Nov 4, 2020
5aefd99
Merge branch 'fix-item-clickable-a11y' into fix-checkbox-nvda
Nov 4, 2020
225cfde
style: update type
Nov 4, 2020
7c5033b
Merge branch 'fix-item-clickable-a11y' into fix-checkbox-nvda
Nov 4, 2020
157d045
Merge branch 'master' into fix-checkbox-nvda
Nov 4, 2020
c91e3c2
style: shorten code comments
Nov 4, 2020
aecaf10
Merge branch 'master' into fix-checkbox-nvda
Nov 9, 2020
81c11bd
fix(checkbox): checked required on host for safari to work
Nov 10, 2020
f204a0f
fix(helpers): update labelled by id
Nov 10, 2020
9de2b7e
fix(checkbox): don't select disabled in safari on iOS
Nov 10, 2020
f8b84ad
fix(checkbox): reduce code to hide disabled from voiceover
Nov 10, 2020
598d639
fix(checkbox): add role back for checkbox always since aria-hidden
Nov 10, 2020
fb288d2
fix(checkbox): add aria-checked on the inner input for nvda
Nov 10, 2020
2d6056e
docs(github): update component guide to include a11y
Nov 10, 2020
c194729
Merge branch 'fix-checkbox-nvda' into fix-toggle-nvda
Nov 11, 2020
4d76963
fix(toggle): update to pass a11y guidelines
Nov 11, 2020
68d645b
test(toggle): resolve axe errors
Nov 11, 2020
e9aa27c
test(toggle): update for a11y
Nov 11, 2020
c04be5e
fix(toggle): role for toggle should be a switch
Nov 11, 2020
7c95d89
docs(component-guide): add toggle as a checkbox type component
Nov 11, 2020
9cf6cc3
Merge branch 'master' into fix-toggle-nvda
Nov 12, 2020
353c30c
Merge branch 'master' into fix-select-nvda
Nov 12, 2020
f645337
Merge branch 'master' into fix-select-nvda
Nov 12, 2020
f88e6fa
fix(select): add native input and include value in the label
Nov 12, 2020
b37ee50
style: lint
Nov 12, 2020
562f8b9
fix(select): updates for screen readers
Nov 13, 2020
64aeadb
style(select): remove TODOs, tested and working
Nov 16, 2020
0f7cd93
fix(select): only fire click event once
Nov 16, 2020
c0cef96
fix(select): remove other readonly props
Nov 16, 2020
d562504
use button
liamdebeasi Nov 16, 2020
f4787b5
remove htmlfor
liamdebeasi Nov 16, 2020
e259553
fix(radio): add native radio to announce via screenreaders properly
Nov 16, 2020
85be6f9
fix(radio-group): don't propagate onClick
Nov 17, 2020
b44bb63
fix(radio-group): do not add aria-labelledby if label does not exist
Nov 18, 2020
6ad1011
test(radio): resolve axe issues
Nov 18, 2020
ba7bd53
test(radio): revert back
Nov 18, 2020
5aa3dd9
test(radio): add a11y test
Nov 18, 2020
b91168b
fix(radio): properly focus radios
Nov 18, 2020
5fe44bc
fix(radio): announce total radio buttons, focus properly, use custom …
Nov 19, 2020
c596c2b
Merge branch 'master' into fix-radio-nvda
Nov 19, 2020
1744aa2
git(revert): revert only changes made to toggle
Nov 19, 2020
bd13525
fix(helpers): make sure that id / aria-labelledby exist and are not e…
Nov 20, 2020
7579d29
Merge branch 'master' into fix-select-nvda
Nov 20, 2020
4426f31
fix(helpers): update to look for custom labels
Nov 20, 2020
357b793
test(select): add a11y test for select
Nov 20, 2020
0a14314
Merge branch 'fix-select-nvda' into fix-radio-nvda
Nov 20, 2020
487a8a1
pls work
Nov 20, 2020
898da02
maintaining two different code bases is fun
Nov 20, 2020
cda9beb
Merge branch 'fix-select-nvda' into fix-radio-nvda
Nov 20, 2020
544db8b
fix(helpers): check for empty string on aria-labelledby
Nov 20, 2020
813a3a3
Merge branch 'master' into fix-radio-nvda
Nov 20, 2020
ee28b52
Merge branch 'master' into fix-radio-nvda
Nov 20, 2020
e4bb383
fix(radio): allow users to select a radio on space
Nov 23, 2020
5e12fc8
Merge branch 'master' into fix-radio-nvda
Nov 23, 2020
0176d6d
test(radio): use divs instead of ps to group properly
Nov 23, 2020
ebe85c5
Update core/src/components/radio/radio.scss
brandyscarney Nov 23, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/src/components.d.ts
Expand Up @@ -1708,7 +1708,7 @@ export namespace Components {
*/
"name": string;
"setButtonTabindex": (value: number) => Promise<void>;
"setFocus": () => Promise<void>;
"setFocus": (ev: any) => Promise<void>;
/**
* the value of the radio.
*/
Expand Down
4 changes: 2 additions & 2 deletions core/src/components/alert/alert.tsx
Expand Up @@ -158,15 +158,15 @@ export class Alert implements ComponentInterface, OverlayInterface {

// 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 (['ArrowDown', 'ArrowRight'].includes(ev.key)) {
if (['ArrowDown', 'ArrowRight'].includes(ev.code)) {
nextEl = (index === radios.length - 1)
? radios[0]
: radios[index + 1];
}

// 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 (['ArrowUp', 'ArrowLeft'].includes(ev.key)) {
if (['ArrowUp', 'ArrowLeft'].includes(ev.code)) {
nextEl = (index === 0)
? radios[radios.length - 1]
: radios[index - 1];
Expand Down
30 changes: 22 additions & 8 deletions core/src/components/radio-group/radio-group.tsx
Expand Up @@ -10,6 +10,7 @@ export class RadioGroup implements ComponentInterface {

private inputId = `ion-rg-${radioGroupIds++}`;
private labelId = `${this.inputId}-lbl`;
private label?: HTMLIonLabelElement | null;

@Element() el!: HTMLElement;

Expand Down Expand Up @@ -68,10 +69,9 @@ export class RadioGroup implements ComponentInterface {
async connectedCallback() {
// Get the list header if it exists and set the id
// this is used to set aria-labelledby
const el = this.el;
const header = el.querySelector('ion-list-header') || el.querySelector('ion-item-divider');
const header = this.el.querySelector('ion-list-header') || this.el.querySelector('ion-item-divider');
if (header) {
const label = header.querySelector('ion-label');
const label = this.label = header.querySelector('ion-label');
if (label) {
this.labelId = label.id = this.name + '-lbl';
}
Expand All @@ -83,6 +83,9 @@ export class RadioGroup implements ComponentInterface {
}

private onClick = (ev: Event) => {
ev.preventDefault();
ev.stopPropagation();

const selectedRadio = ev.target && (ev.target as HTMLElement).closest('ion-radio');
if (selectedRadio) {
const currentValue = this.value;
Expand Down Expand Up @@ -110,42 +113,53 @@ export class RadioGroup implements ComponentInterface {
// Only move the radio if the current focus is in the radio group
if (ev.target && radios.includes(ev.target)) {
const index = radios.findIndex(radio => radio === ev.target);
const current = radios[index];

let next;

// 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 (['ArrowDown', 'ArrowRight'].includes(ev.key)) {
if (['ArrowDown', 'ArrowRight'].includes(ev.code)) {
next = (index === radios.length - 1)
? radios[0]
: radios[index + 1];
}

// 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 (['ArrowUp', 'ArrowLeft'].includes(ev.key)) {
if (['ArrowUp', 'ArrowLeft'].includes(ev.code)) {
next = (index === 0)
? radios[radios.length - 1]
: radios[index - 1];
}

if (next && radios.includes(next)) {
next.setFocus();
next.setFocus(ev);

if (!inSelectPopover) {
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() {
const { label, labelId } = this;
const mode = getIonMode(this);

return (
<Host
role="radiogroup"
aria-labelledby={this.labelId}
aria-labelledby={label ? labelId : null}
onClick={this.onClick}
class={getIonMode(this)}
class={mode}
>
</Host>
);
Expand Down
153 changes: 112 additions & 41 deletions core/src/components/radio-group/test/basic/index.html
Expand Up @@ -20,85 +20,156 @@
</ion-header>

<ion-content class="outer-content">
<ion-radio-group id="dynamicDisabled" disabled name="tannen" id="group" value="biff">
<ion-list-header>
<ion-label>Luckiest Man On Earth <span id="group-value"></span></ion-label>
</ion-list-header>

<ion-item>
<ion-label>Biff
<span id="biff"></span>
</ion-label>
<ion-radio value="biff" slot="start"></ion-radio>
</ion-item>

<ion-item>
<ion-label>Griff
<span id="griff"></span>
</ion-label>
<ion-radio value="griff" slot="start"></ion-radio>
</ion-item>

<ion-item>
<ion-label>Buford
<span id="buford"></span>
</ion-label>
<ion-radio value="buford" slot="start"></ion-radio>
</ion-item>

<ion-item>
<ion-label>George</ion-label>
<ion-radio value="george" slot="start"></ion-radio>
</ion-item>

</ion-radio-group>

<ion-button onClick="addSelect()">Add Select</ion-button>
<ion-button onClick="addCheckedSelect()">Add Checked Select</ion-button>
<ion-button onClick="removeSelect()">Remove Select</ion-button>
<p class="ion-text-center">
<ion-button onClick="addRadio()">Add Radio</ion-button>
<ion-button onClick="addChecked()">Add Checked</ion-button>
<ion-button id="removeButton" onClick="removeRadio()">Remove Radio</ion-button>
</p>

<ion-list>
<ion-radio-group id="dynamicDisabled" disabled name="tannen" id="group" value="biff">
<ion-list-header>
<ion-label>Luckiest Man On Earth <span id="group-value"></span></ion-label>
</ion-list-header>

<ion-item>
<ion-label>Biff
<span id="biff"></span>
</ion-label>
<ion-radio value="biff" slot="start"></ion-radio>
</ion-item>

<ion-item>
<ion-label>Griff
<span id="griff"></span>
</ion-label>
<ion-radio value="griff" slot="start"></ion-radio>
</ion-item>

<ion-item>
<ion-label>Buford
<span id="buford"></span>
</ion-label>
<ion-radio value="buford" slot="start"></ion-radio>
</ion-item>

<ion-item>
<ion-label>George</ion-label>
<ion-radio value="george" slot="start"></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"></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>

<style>
.outer-content {
--background: #f2f2f2;
}

ion-list {
margin-bottom: 10px !important;
}
</style>

<script>
let count = 0;
const removeButton = document.querySelector('#removeButton');

const valueEl = document.querySelector('#group-value');
const group = document.querySelector('ion-radio-group');

group.addEventListener('ionChange', (ev) => {
valueEl.textContent = group.value;
});

customElements.whenDefined('ion-radio-group')
.then(() => group.componentOnReady())
.then(() => {
valueEl.textContent = group.value;
});

function addSelect() {
function addRadio() {
const item = document.createElement('ion-item');

item.innerHTML = `
<ion-label>Item ${count}</ion-label>
<ion-radio value="item-${count}" slot="start"></ion-radio>
`;
group.appendChild(item);
count++;

removeButton.disabled = false;
}
function addCheckedSelect() {

function addChecked() {
const item = document.createElement('ion-item');
item.innerHTML = `
<ion-label>Item ${count}</ion-label>
<ion-radio value="item-${count}" slot="start"></ion-radio>
`;
group.appendChild(item);

group.value = `item-${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>
</ion-app>
Expand Down
30 changes: 15 additions & 15 deletions core/src/components/radio-group/test/standalone/index.html
Expand Up @@ -13,28 +13,28 @@

<body>
<ion-radio-group value="danger">
<ion-radio></ion-radio>
<ion-radio aria-label="Default"></ion-radio>

<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" value="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" value="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>

<ion-radio disabled></ion-radio>
<ion-radio color="secondary" disabled></ion-radio>
<ion-radio aria-label="Default" disabled></ion-radio>
<ion-radio aria-label="Secondary" color="secondary" disabled></ion-radio>
</ion-radio-group>

<p>
allow-empty-selection="true":&nbsp;
<ion-radio-group allow-empty-selection="true">
<ion-radio color="primary" value="1"></ion-radio>
<ion-radio color="secondary" value="2"></ion-radio>
<ion-radio color="tertiary" value="3"></ion-radio>
<ion-radio aria-label="Primary" color="primary" value="1"></ion-radio>
<ion-radio aria-label="Secondary" color="secondary" value="2"></ion-radio>
<ion-radio aria-label="Tertiary" color="tertiary" value="3"></ion-radio>
</ion-radio-group>
</p>

Expand Down
23 changes: 18 additions & 5 deletions core/src/components/radio/radio.scss
Expand Up @@ -25,7 +25,6 @@
pointer-events: none;
}


.radio-icon {
display: flex;

Expand All @@ -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;
brandyscarney marked this conversation as resolved.
Show resolved Hide resolved
}