Skip to content
This repository was archived by the owner on Jan 13, 2025. It is now read-only.

Commit 077c809

Browse files
authored
feat(list): Automatically use appropriate aria attribute for single selection list. (#4479)
1 parent e851bae commit 077c809

File tree

17 files changed

+186
-38
lines changed

17 files changed

+186
-38
lines changed

demos/list.html

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,6 +1354,41 @@ <h3>Example - Interactive List</h3>
13541354
</a>
13551355
</nav>
13561356
</section>
1357+
<section>
1358+
<h3>Example - w/ Navigation list</h3>
1359+
<nav class="mdc-list demo-list" data-demo-interactive-list>
1360+
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-current="page" tabindex="0">
1361+
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">inbox</i>
1362+
<span class="mdc-list-item__text">Inbox<i class="test-font--redact-prev-letter"></i></span>
1363+
</a>
1364+
<a class="mdc-list-item" href="#">
1365+
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">star</i>
1366+
<span class="mdc-list-item__text">Star<i class="test-font--redact-prev-letter"></i></span>
1367+
</a>
1368+
<a class="mdc-list-item" href="#">
1369+
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">send</i>
1370+
<span class="mdc-list-item__text">Sent Mail<i class="test-font--redact-prev-letter"></i></span>
1371+
</a>
1372+
<a class="mdc-list-item" href="#">
1373+
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">drafts</i>
1374+
<span class="mdc-list-item__text">Drafts<i class="test-font--redact-prev-letter"></i></span>
1375+
</a>
1376+
1377+
<hr class="mdc-list-divider">
1378+
<h6 class="mdc-list-group__subheader">Labels</h6>
1379+
<a class="mdc-list-item" href="#">
1380+
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">bookmark</i>
1381+
<span class="mdc-list-item__text">Family<i class="test-font--redact-prev-letter"></i></span>
1382+
</a>
1383+
<a class="mdc-list-item" href="#">
1384+
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">bookmark</i>
1385+
<span class="mdc-list-item__text">Friends<i class="test-font--redact-prev-letter"></i></span>
1386+
</a>
1387+
<a class="mdc-list-item" href="#">
1388+
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">bookmark</i>
1389+
<span class="mdc-list-item__text">Work<i class="test-font--redact-prev-letter"></i></span>
1390+
</a>
1391+
</section>
13571392
</section>
13581393
</div>
13591394
</main>

packages/mdc-drawer/README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ npm install @material/drawer
3434
<aside class="mdc-drawer">
3535
<div class="mdc-drawer__content">
3636
<nav class="mdc-list">
37-
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-selected="true">
37+
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-current="page">
3838
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">inbox</i>
3939
<span class="mdc-list-item__text">Inbox</span>
4040
</a>
@@ -95,7 +95,7 @@ const drawer = MDCDrawer.attachTo(document.querySelector('.mdc-drawer'));
9595
<aside class="mdc-drawer">
9696
<div class="mdc-drawer__content">
9797
<nav class="mdc-list">
98-
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-selected="true">
98+
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-current="page">
9999
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">inbox</i>
100100
<span class="mdc-list-item__text">Inbox</span>
101101
</a>
@@ -143,7 +143,7 @@ Drawers can contain a header element which will not scroll with the rest of the
143143
</div>
144144
<div class="mdc-drawer__content">
145145
<nav class="mdc-list">
146-
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-selected="true">
146+
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-current="page">
147147
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">inbox</i>
148148
<span class="mdc-list-item__text">Inbox</span>
149149
</a>
@@ -169,7 +169,7 @@ Dismissible drawers are by default hidden off screen, and can slide into view. D
169169
<aside class="mdc-drawer mdc-drawer--dismissible">
170170
<div class="mdc-drawer__content">
171171
<nav class="mdc-list">
172-
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-selected="true">
172+
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-current="page">
173173
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">inbox</i>
174174
<span class="mdc-list-item__text">Inbox</span>
175175
</a>
@@ -206,7 +206,7 @@ In the following example, the `mdc-drawer__content` and `main-content` elements
206206
<aside class="mdc-drawer mdc-drawer--dismissible">
207207
<div class="mdc-drawer__content">
208208
<div class="mdc-list">
209-
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-selected="true">
209+
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-current="page">
210210
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">inbox</i>
211211
<span class="mdc-list-item__text">Inbox</span>
212212
</a>
@@ -258,7 +258,7 @@ In cases where the drawer appears below the top app bar you will want to follow
258258
<aside class="mdc-drawer mdc-drawer--dismissible mdc-top-app-bar--fixed-adjust">
259259
<div class="mdc-drawer__content">
260260
<div class="mdc-list">
261-
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-selected="true">
261+
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-current="page">
262262
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">inbox</i>
263263
<span class="mdc-list-item__text">Inbox</span>
264264
</a>
@@ -333,7 +333,7 @@ Modal drawers are elevated above most of the app's UI and don't affect the scree
333333
<aside class="mdc-drawer mdc-drawer--modal">
334334
<div class="mdc-drawer__content">
335335
<nav class="mdc-list">
336-
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-selected="true">
336+
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-current="page">
337337
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">inbox</i>
338338
<span class="mdc-list-item__text">Inbox</span>
339339
</a>

packages/mdc-list/README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,8 @@ list.singleSelection = true;
218218
#### Pre-selected list item
219219

220220
When rendering the list with a pre-selected list item, the list item that needs to be selected should contain
221-
the `mdc-list-item--selected` or `mdc-list-item--activated` class and `aria-selected="true"` attribute before
222-
creating the list.
221+
the `mdc-list-item--selected` or `mdc-list-item--activated` class before creating the list. Please see
222+
[Accessibility](#Accessibility) section for appropriate aria attributes.
223223

224224
```html
225225
<ul id="my-list" class="mdc-list" role="listbox">
@@ -424,6 +424,9 @@ Use `role="listbox"` only for single selection list, without this role the `ul`
424424
Do not use `aria-orientation` attribute for standard list (i.e., `role="list"`), use component's `vertical` property to set the orientation
425425
to vertical.
426426

427+
Single selection list supports `aria-selected` and `aria-current` attributes. List automatically detects the presence of these attributes
428+
and sets it to next selected list item based on which ARIA attribute you use (i.e., `aria-selected` or `aria-current`). Please see WAI-ARIA [aria-current](https://www.w3.org/TR/wai-aria-1.1/#aria-current) article for recommended usage and available attribute values.
429+
427430
As the user navigates through the list, any `button` and `a` elements within the list will receive `tabindex="-1"` when
428431
the list item is not focused. When the list item receives focus, the aforementioned elements will receive
429432
`tabIndex="0"`. This allows for the user to tab through list item elements and then tab to the first element after the
@@ -502,10 +505,10 @@ these should also receive `tabIndex="-1"`.
502505
#### Setup in `singleSelection()`
503506

504507
When implementing a component that will use the single selection variant, the HTML should be modified to include
505-
the `aria-selected` attribute, the `mdc-list-item--selected` or `mdc-list-item--activated` class should be added,
508+
the `mdc-list-item--selected` or `mdc-list-item--activated` class name,
506509
and the `tabindex` of the selected element should be `0`. The first list item should have the `tabindex` updated
507510
to `-1`. The foundation method `setSelectedIndex()` should be called with the initially selected element immediately
508-
after the foundation is instantiated.
511+
after the foundation is instantiated. Please see [Accessibility](#Accessibility) section for appropriate aria attributes.
509512

510513
```html
511514
<ul id="my-list" class="mdc-list">
@@ -529,6 +532,7 @@ Method Signature | Description
529532
`getListItemCount() => Number` | Returns the total number of list items (elements with `mdc-list-item` class) that are direct children of the `root_` element.
530533
`getFocusedElementIndex() => Number` | Returns the `index` value of the currently focused element.
531534
`getListItemIndex(ele: Element) => Number` | Returns the `index` value of the provided `ele` element.
535+
`getAttributeForElementIndex(index: number, attribute: string) => string | null` | Returns the attribute value of list item at given `index`.
532536
`setAttributeForElementIndex(index: Number, attr: String, value: String) => void` | Sets the `attr` attribute to `value` for the list item at `index`.
533537
`addClassForElementIndex(index: Number, className: String) => void` | Adds the `className` class to the list item at `index`.
534538
`removeClassForElementIndex(index: Number, className: String) => void` | Removes the `className` class to the list item at `index`.

packages/mdc-list/adapter.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
* https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md
3030
*/
3131
export interface MDCListAdapter {
32+
/**
33+
* Returns the attribute value of list item at given `index`.
34+
*/
35+
getAttributeForElementIndex(index: number, attr: string): string | null;
36+
3237
getListItemCount(): number;
3338

3439
getFocusedElementIndex(): number;

packages/mdc-list/component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ export class MDCList extends MDCComponent<MDCListFoundation> {
145145
element.focus();
146146
}
147147
},
148+
getAttributeForElementIndex: (index, attr) => this.listElements[index].getAttribute(attr),
148149
getFocusedElementIndex: () => this.listElements.indexOf(document.activeElement!),
149150
getListItemCount: () => this.listElements.length,
150151
hasCheckboxAtIndex: (index) => {

packages/mdc-list/constants.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const strings = {
3333
ARIA_CHECKED: 'aria-checked',
3434
ARIA_CHECKED_CHECKBOX_SELECTOR: '[role="checkbox"][aria-checked="true"]',
3535
ARIA_CHECKED_RADIO_SELECTOR: '[role="radio"][aria-checked="true"]',
36+
ARIA_CURRENT: 'aria-current',
3637
ARIA_ORIENTATION: 'aria-orientation',
3738
ARIA_ORIENTATION_HORIZONTAL: 'horizontal',
3839
ARIA_ROLE_CHECKBOX_SELECTOR: '[role="checkbox"]',
@@ -53,4 +54,8 @@ const strings = {
5354
RADIO_SELECTOR: 'input[type="radio"]:not(:disabled)',
5455
};
5556

56-
export {strings, cssClasses};
57+
const numbers = {
58+
UNSET_INDEX: -1,
59+
};
60+
61+
export {strings, cssClasses, numbers};

packages/mdc-list/foundation.ts

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
import {MDCFoundation} from '@material/base/foundation';
2525
import {MDCListAdapter} from './adapter';
26-
import {cssClasses, strings} from './constants';
26+
import {cssClasses, numbers, strings} from './constants';
2727
import {MDCListIndex} from './types';
2828

2929
const ELEMENTS_KEY_ALLOWED_IN = ['input', 'button', 'textarea', 'select'];
@@ -41,10 +41,15 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
4141
return cssClasses;
4242
}
4343

44+
static get numbers() {
45+
return numbers;
46+
}
47+
4448
static get defaultAdapter(): MDCListAdapter {
4549
return {
4650
addClassForElementIndex: () => undefined,
4751
focusItemAtIndex: () => undefined,
52+
getAttributeForElementIndex: () => null,
4853
getFocusedElementIndex: () => 0,
4954
getListItemCount: () => 0,
5055
hasCheckboxAtIndex: () => false,
@@ -62,9 +67,10 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
6267
private wrapFocus_ = false;
6368
private isVertical_ = true;
6469
private isSingleSelectionList_ = false;
65-
private selectedIndex_: MDCListIndex = -1;
66-
private focusedItemIndex_ = -1;
70+
private selectedIndex_: MDCListIndex = numbers.UNSET_INDEX;
71+
private focusedItemIndex_ = numbers.UNSET_INDEX;
6772
private useActivatedClass_ = false;
73+
private ariaCurrentAttrValue_: string | null = null;
6874
private isCheckboxList_ = false;
6975
private isRadioList_ = false;
7076

@@ -172,8 +178,8 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
172178
const isSpace = evt.key === 'Space' || evt.keyCode === 32;
173179

174180
let currentIndex = this.adapter_.getFocusedElementIndex();
175-
let nextIndex = -1;
176-
if (currentIndex === -1) {
181+
let nextIndex = numbers.UNSET_INDEX;
182+
if (currentIndex === numbers.UNSET_INDEX) {
177183
currentIndex = listItemIndex;
178184
if (currentIndex < 0) {
179185
// If this event doesn't have a mdc-list-item ancestor from the
@@ -223,7 +229,7 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
223229
* Click handler for the list.
224230
*/
225231
handleClick(index: number, toggleCheckbox: boolean) {
226-
if (index === -1) {
232+
if (index === numbers.UNSET_INDEX) {
227233
return;
228234
}
229235

@@ -298,29 +304,52 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
298304
}
299305

300306
private setSingleSelectionAtIndex_(index: number) {
307+
if (this.selectedIndex_ === index) {
308+
return;
309+
}
310+
301311
let selectedClassName = cssClasses.LIST_ITEM_SELECTED_CLASS;
302312
if (this.useActivatedClass_) {
303313
selectedClassName = cssClasses.LIST_ITEM_ACTIVATED_CLASS;
304314
}
305315

306-
if (this.selectedIndex_ >= 0 && this.selectedIndex_ !== index) {
316+
if (this.selectedIndex_ !== numbers.UNSET_INDEX) {
307317
this.adapter_.removeClassForElementIndex(this.selectedIndex_ as number, selectedClassName);
308-
this.adapter_.setAttributeForElementIndex(this.selectedIndex_ as number, strings.ARIA_SELECTED, 'false');
309318
}
310-
311319
this.adapter_.addClassForElementIndex(index, selectedClassName);
312-
this.adapter_.setAttributeForElementIndex(index, strings.ARIA_SELECTED, 'true');
320+
this.setAriaForSingleSelectionAtIndex_(index);
313321

314322
this.selectedIndex_ = index;
315323
}
316324

325+
/**
326+
* Sets aria attribute for single selection at given index.
327+
*/
328+
private setAriaForSingleSelectionAtIndex_(index: number) {
329+
// Detect the presence of aria-current and get the value only during list initialization when it is in unset state.
330+
if (this.selectedIndex_ === numbers.UNSET_INDEX) {
331+
this.ariaCurrentAttrValue_ =
332+
this.adapter_.getAttributeForElementIndex(index, strings.ARIA_CURRENT);
333+
}
334+
335+
const isAriaCurrent = this.ariaCurrentAttrValue_ !== null;
336+
const ariaAttribute = isAriaCurrent ? strings.ARIA_CURRENT : strings.ARIA_SELECTED;
337+
338+
if (this.selectedIndex_ !== numbers.UNSET_INDEX) {
339+
this.adapter_.setAttributeForElementIndex(this.selectedIndex_ as number, ariaAttribute, 'false');
340+
}
341+
342+
const ariaAttributeValue = isAriaCurrent ? this.ariaCurrentAttrValue_ : 'true';
343+
this.adapter_.setAttributeForElementIndex(index, ariaAttribute, ariaAttributeValue as string);
344+
}
345+
317346
/**
318347
* Toggles radio at give index. Radio doesn't change the checked state if it is already checked.
319348
*/
320349
private setRadioAtIndex_(index: number) {
321350
this.adapter_.setCheckedCheckboxOrRadioAtIndex(index, true);
322351

323-
if (this.selectedIndex_ >= 0) {
352+
if (this.selectedIndex_ !== numbers.UNSET_INDEX) {
324353
this.adapter_.setAttributeForElementIndex(this.selectedIndex_ as number, strings.ARIA_CHECKED, 'false');
325354
}
326355

@@ -344,7 +373,7 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
344373
}
345374

346375
private setTabindexAtIndex_(index: number) {
347-
if (this.focusedItemIndex_ === -1 && index !== 0) {
376+
if (this.focusedItemIndex_ === numbers.UNSET_INDEX && index !== 0) {
348377
// If no list item was selected set first list item's tabindex to -1.
349378
// Generally, tabindex is set to 0 on first list item of list that has no preselected items.
350379
this.adapter_.setAttributeForElementIndex(0, 'tabindex', '-1');
@@ -366,7 +395,7 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
366395
let targetIndex = 0;
367396

368397
if (this.isSelectableList_()) {
369-
if (typeof this.selectedIndex_ === 'number' && this.selectedIndex_ !== -1) {
398+
if (typeof this.selectedIndex_ === 'number' && this.selectedIndex_ !== numbers.UNSET_INDEX) {
370399
targetIndex = this.selectedIndex_;
371400
} else if (isNumberArray(this.selectedIndex_) && this.selectedIndex_.length > 0) {
372401
targetIndex = this.selectedIndex_.reduce((currentIndex, minIndex) => Math.min(currentIndex, minIndex));
@@ -421,7 +450,7 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
421450
this.adapter_.setAttributeForElementIndex(index, strings.ARIA_CHECKED, isChecked ? 'true' : 'false');
422451

423452
// If none of the checkbox items are selected and selectedIndex is not initialized then provide a default value.
424-
let selectedIndexes = this.selectedIndex_ === -1 ? [] : (this.selectedIndex_ as number[]).slice();
453+
let selectedIndexes = this.selectedIndex_ === numbers.UNSET_INDEX ? [] : (this.selectedIndex_ as number[]).slice();
425454

426455
if (isChecked) {
427456
selectedIndexes.push(index);

test/screenshot/spec/mdc-drawer/classes/dismissible-below-top-app-bar.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
</header>
6363
<div class="mdc-drawer__content">
6464
<nav class="mdc-list">
65-
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-selected="true" tabindex="0">
65+
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-current="page" tabindex="0">
6666
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">inbox</i>
6767
<span class="mdc-list-item__text">Inbox<i class="test-font--redact-prev-letter"></i></span>
6868
</a>

test/screenshot/spec/mdc-drawer/classes/dismissible.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
</header>
5151
<div class="mdc-drawer__content">
5252
<nav class="mdc-list">
53-
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-selected="true" tabindex="0">
53+
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-current="page" tabindex="0">
5454
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">inbox</i>
5555
<span class="mdc-list-item__text">Inbox<i class="test-font--redact-prev-letter"></i></span>
5656
</a>

test/screenshot/spec/mdc-drawer/classes/modal.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
</header>
5151
<div class="mdc-drawer__content">
5252
<nav class="mdc-list">
53-
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-selected="true" tabindex="0">
53+
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-current="page" tabindex="0">
5454
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">inbox</i>
5555
<span class="mdc-list-item__text">Inbox<i class="test-font--redact-prev-letter"></i></span>
5656
</a>

0 commit comments

Comments
 (0)