mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-19 11:41:20 +08:00
fix(segment, segment-button): use correct tablist and tab roles for screen readers (#23145)
* fix(segment, segment-button): change aria attributes for segment and segment-button * add axe test * Add tests, screen reader doc * add updated screen reader * fix(segment-button): move aria tags to host * verify nvda and talkback behavior * fix(segment-button): remove outline on focus * Update core/src/components/segment/test/basic/index.html Co-authored-by: Liam DeBeasi <liamdebeasi@icloud.com>
This commit is contained in:
40
core/package-lock.json
generated
40
core/package-lock.json
generated
@ -14,6 +14,7 @@
|
|||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@axe-core/puppeteer": "^4.1.1",
|
||||||
"@jest/core": "^26.6.3",
|
"@jest/core": "^26.6.3",
|
||||||
"@rollup/plugin-node-resolve": "^8.4.0",
|
"@rollup/plugin-node-resolve": "^8.4.0",
|
||||||
"@rollup/plugin-virtual": "^2.0.3",
|
"@rollup/plugin-virtual": "^2.0.3",
|
||||||
@ -43,6 +44,21 @@
|
|||||||
"typescript": "^4.0.5"
|
"typescript": "^4.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@axe-core/puppeteer": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@axe-core/puppeteer/-/puppeteer-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-Ao9N7HL//s26hdasx3Ba18tlJgxpoO+1SmIN6eSx5vC50dqYhiRU0xp6wBKWqzo10u1jpzl/s4RFsOAuolFMBA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"axe-core": "^4.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"puppeteer": ">=1.10.0 < 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.10.1",
|
"version": "7.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
|
||||||
@ -2029,6 +2045,15 @@
|
|||||||
"integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==",
|
"integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/axe-core": {
|
||||||
|
"version": "4.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.1.4.tgz",
|
||||||
|
"integrity": "sha512-Pdgfv6iP0gNx9ejRGa3zE7Xgkj/iclXqLfe7BnatdZz0QnLZ3jrRHUVH8wNSdN68w05Sk3ShGTb3ydktMTooig==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/babel-plugin-istanbul": {
|
"node_modules/babel-plugin-istanbul": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz",
|
||||||
@ -13802,6 +13827,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@axe-core/puppeteer": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@axe-core/puppeteer/-/puppeteer-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-Ao9N7HL//s26hdasx3Ba18tlJgxpoO+1SmIN6eSx5vC50dqYhiRU0xp6wBKWqzo10u1jpzl/s4RFsOAuolFMBA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"axe-core": "^4.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@babel/code-frame": {
|
"@babel/code-frame": {
|
||||||
"version": "7.10.1",
|
"version": "7.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
|
||||||
@ -15481,6 +15515,12 @@
|
|||||||
"integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==",
|
"integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"axe-core": {
|
||||||
|
"version": "4.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.1.4.tgz",
|
||||||
|
"integrity": "sha512-Pdgfv6iP0gNx9ejRGa3zE7Xgkj/iclXqLfe7BnatdZz0QnLZ3jrRHUVH8wNSdN68w05Sk3ShGTb3ydktMTooig==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"babel-plugin-istanbul": {
|
"babel-plugin-istanbul": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz",
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@axe-core/puppeteer": "^4.1.1",
|
||||||
"@jest/core": "^26.6.3",
|
"@jest/core": "^26.6.3",
|
||||||
"@rollup/plugin-node-resolve": "^8.4.0",
|
"@rollup/plugin-node-resolve": "^8.4.0",
|
||||||
"@rollup/plugin-virtual": "^2.0.3",
|
"@rollup/plugin-virtual": "^2.0.3",
|
||||||
|
@ -166,6 +166,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host(:focus) {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
// Segment Button: Hover
|
// Segment Button: Hover
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
@ -86,13 +86,28 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get tabIndex() {
|
||||||
|
if (this.disabled) { return -1; }
|
||||||
|
|
||||||
|
const hasTabIndex = this.el.hasAttribute('tabindex');
|
||||||
|
|
||||||
|
if (hasTabIndex) {
|
||||||
|
return this.el.getAttribute('tabindex');
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { checked, type, disabled, hasIcon, hasLabel, layout, segmentEl } = this;
|
const { checked, type, disabled, hasIcon, hasLabel, layout, segmentEl, tabIndex } = this;
|
||||||
const mode = getIonMode(this);
|
const mode = getIonMode(this);
|
||||||
const hasSegmentColor = () => segmentEl !== null && segmentEl.color !== undefined;
|
const hasSegmentColor = () => segmentEl !== null && segmentEl.color !== undefined;
|
||||||
return (
|
return (
|
||||||
<Host
|
<Host
|
||||||
|
role="tab"
|
||||||
|
aria-selected={checked ? 'true' : 'false'}
|
||||||
aria-disabled={disabled ? 'true' : null}
|
aria-disabled={disabled ? 'true' : null}
|
||||||
|
tabIndex={tabIndex}
|
||||||
class={{
|
class={{
|
||||||
[mode]: true,
|
[mode]: true,
|
||||||
'in-toolbar': hostContext('ion-toolbar', this.el),
|
'in-toolbar': hostContext('ion-toolbar', this.el),
|
||||||
@ -113,7 +128,7 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type={type}
|
type={type}
|
||||||
aria-pressed={checked ? 'true' : 'false'}
|
tabIndex={-1}
|
||||||
class="button-native"
|
class="button-native"
|
||||||
part="native"
|
part="native"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -425,6 +425,7 @@ export class Segment implements ComponentInterface {
|
|||||||
const mode = getIonMode(this);
|
const mode = getIonMode(this);
|
||||||
return (
|
return (
|
||||||
<Host
|
<Host
|
||||||
|
role="tablist"
|
||||||
onClick={this.onClick}
|
onClick={this.onClick}
|
||||||
class={createColorClasses(this.color, {
|
class={createColorClasses(this.color, {
|
||||||
[mode]: true,
|
[mode]: true,
|
||||||
|
11
core/src/components/segment/test/a11y/e2e.ts
Normal file
11
core/src/components/segment/test/a11y/e2e.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { newE2EPage } from '@stencil/core/testing';
|
||||||
|
import { AxePuppeteer } from '@axe-core/puppeteer';
|
||||||
|
|
||||||
|
test('segment: axe', async () => {
|
||||||
|
const page = await newE2EPage({
|
||||||
|
url: '/src/components/segment/test/a11y?ionic:_testing=true'
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await new AxePuppeteer(page).analyze();
|
||||||
|
expect(results.violations.length).toEqual(0);
|
||||||
|
});
|
31
core/src/components/segment/test/a11y/index.html
Normal file
31
core/src/components/segment/test/a11y/index.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Segment - a11y</title>
|
||||||
|
<meta name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
|
||||||
|
<link href="../../../../../css/core.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>
|
||||||
|
<main>
|
||||||
|
<h1>Segment</h1>
|
||||||
|
<ion-segment aria-label="Tab Options" color="dark" value="reading-list">
|
||||||
|
<ion-segment-button value="bookmarks">
|
||||||
|
<ion-label>Bookmarks</ion-label>
|
||||||
|
</ion-segment-button>
|
||||||
|
<ion-segment-button value="reading-list">
|
||||||
|
<ion-label>Reading List</ion-label>
|
||||||
|
</ion-segment-button>
|
||||||
|
<ion-segment-button value="shared-links">
|
||||||
|
<ion-label>Shared Links</ion-label>
|
||||||
|
</ion-segment-button>
|
||||||
|
</ion-segment>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
24
core/src/components/segment/test/a11y/screen-readers.md
Normal file
24
core/src/components/segment/test/a11y/screen-readers.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"native" refers to this sample: https://w3c.github.io/aria-practices/examples/tabs/tabs-2/tabs.html
|
||||||
|
|
||||||
|
### Tabbing to Segment Button
|
||||||
|
|
||||||
|
| | native | Ionic |
|
||||||
|
| ------------------------ | ------------------------------------------------ | ------------------------------------------------ |
|
||||||
|
| VoiceOver macOS - Chrome | BOOKMARKS, tab, 1 of 3, Tab Options, tab group | BOOKMARKS, tab, 1 of 3, Tab Options, tab group |
|
||||||
|
| VoiceOver macOS - Safari | BOOKMARKS, tab, 1 of 3, Tab Options, tab group | BOOKMARKS, tab, 1 of 3, Tab Options, tab group |
|
||||||
|
| VoiceOver iOS | Bookmarks, tab | Bookmarks, tab |
|
||||||
|
| Android TalkBack | Bookmarks, tab | Bookmarks, tab |
|
||||||
|
| Windows NVDA | Tab Options, tab control, BOOKMARKS, tab, 1 of 3 | Tab Options, tab control, BOOKMARKS, tab, 1 of 3 |
|
||||||
|
|
||||||
|
### Selecting Segment Button
|
||||||
|
|
||||||
|
| | native | Ionic |
|
||||||
|
| ------------------------ | -------------------------------------------------------- | ------------------------ |
|
||||||
|
| VoiceOver macOS - Chrome | BOOKMARKS, selected, tab, 1 of 3, Tab Options, tab group | BOOKMARKS, selected, tab, 1 of 3, Tab Options, tab group |
|
||||||
|
| VoiceOver macOS - Safari | BOOKMARKS, selected, tab, 1 of 3, Tab Options, tab group | BOOKMARKS, selected, tab, 1 of 3, Tab Options, tab group |
|
||||||
|
| VoiceOver iOS | selected, Bookmarks, tab | selected, Bookmarks, tab |
|
||||||
|
| Android TalkBack | selected | selected |
|
||||||
|
| Windows NVDA | BOOKMARKS, tab, 1 of 3, selected | BOOKMARKS, tab, 1 of 3, selected |
|
||||||
|
|
||||||
|
Note: The `aria-label` for tablist is typically only read on the first interaction.
|
||||||
|
|
Reference in New Issue
Block a user