Skip to content

Commit

Permalink
feat(list): Add customizable aria-label/role attributes to list, …
Browse files Browse the repository at this point in the history
…and customizable `role` to list item.

PiperOrigin-RevId: 466358466
  • Loading branch information
joyzhong authored and copybara-github committed Aug 9, 2022
1 parent 77cc80e commit 8f63406
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 28 deletions.
3 changes: 2 additions & 1 deletion decorators/aria-property.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ import {ReactiveElement} from 'lit';
* @ExportDecoratedItems
*/
export function ariaProperty<E extends ReactiveElement, K extends keyof E&
`aria${string}`>(prototype: E, property: K) {
(`aria${string}` | 'role')>(
prototype: E, property: K) {
// Replace the ARIAMixin property with data-* attribute syncing instead of
// using the native aria-* attribute reflection. This preserves the attribute
// for SSR and avoids screenreader conflicts after delegating the attribute
Expand Down
28 changes: 16 additions & 12 deletions list/lib/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/

import {ariaProperty} from '@material/web/decorators/aria-property';
import {ARIARole} from '@material/web/types/aria';
import {html, LitElement, PropertyValues, TemplateResult} from 'lit';
import {property, query, queryAssignedElements} from 'lit/decorators';
import {ifDefined} from 'lit/directives/if-defined';

import {ListItemInteractionEvent} from './listitem/constants';
import {ListItem} from './listitem/list-item';
Expand All @@ -23,13 +25,23 @@ export class List extends LitElement {
static override shadowRootOptions:
ShadowRootInit = {mode: 'open', delegatesFocus: true};

@ariaProperty // tslint:disable-line:no-new-decorators
@property({type: String, attribute: 'data-aria-label', noAccessor: true})
override ariaLabel!: string;

@ariaProperty // tslint:disable-line:no-new-decorators
@property({type: String, attribute: 'data-role', noAccessor: true})
role: ARIARole = 'list';

@property({type: Number}) listTabIndex: number = 0;

items: ListItem[] = [];
activeListItem: ListItem|null = null;

@query('.md3-list') listRoot!: HTMLElement;

@property({type: String}) listItemTagName = 'md-list-item';

@queryAssignedElements({flatten: true})
protected assignedElements!: HTMLElement[]|null;

Expand All @@ -39,17 +51,13 @@ export class List extends LitElement {
this.updateItems();
}

/** @soyTemplate */
protected getAriaRole(): ARIARole {
return 'list';
}

/** @soyTemplate */
override render(): TemplateResult {
return html`
<ul class="md3-list"
aria-label="${ifDefined(this.ariaLabel)}"
tabindex=${this.listTabIndex}
role=${this.getAriaRole()}
role=${this.role}
@list-item-interaction=${this.handleItemInteraction}
@keydown=${this.handleKeydown}
>
Expand Down Expand Up @@ -124,13 +132,9 @@ export class List extends LitElement {
this.items = elements.filter(this.isListItem, this);
}

protected getListItemTagName() {
return 'md-list-item';
}

/** @return Whether the given element is an <md-list-item> element. */
/** @return Whether the given element is a list item element. */
private isListItem(element: Element): element is ListItem {
return element.tagName.toLowerCase() === this.getListItemTagName();
return element.tagName.toLowerCase() === this.listItemTagName;
}

private getFirstItem(): ListItem {
Expand Down
7 changes: 6 additions & 1 deletion list/lib/listitem/list-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import '@material/web/ripple/ripple';
import '@material/web/focus/focus-ring';

import {ActionElement, BeginPressConfig, EndPressConfig} from '@material/web/actionelement/action-element';
import {ariaProperty} from '@material/web/decorators/aria-property';
import {pointerPress, shouldShowStrongFocus} from '@material/web/focus/strong-focus';
import {MdRipple} from '@material/web/ripple/ripple';
import {ARIARole} from '@material/web/types/aria';
Expand All @@ -17,6 +18,10 @@ import {ClassInfo, classMap} from 'lit/directives/class-map';

/** @soyCompatible */
export class ListItem extends ActionElement {
@ariaProperty // tslint:disable-line:no-new-decorators
@property({type: String, attribute: 'data-role', noAccessor: true})
role: ARIARole = 'listitem';

@property({type: String}) supportingText = '';
@property({type: String}) multiLineSupportingText = '';
@property({type: String}) trailingSupportingText = '';
Expand All @@ -32,7 +37,7 @@ export class ListItem extends ActionElement {
return html`
<li
tabindex=${this.itemTabIndex}
role=${this.getAriaRole()}
role=${this.role}
data-query-md3-list-item
class="md3-list-item ${classMap(this.getRenderClasses())}"
@pointerdown=${this.handlePointerDown}
Expand Down
5 changes: 1 addition & 4 deletions list/lib/listitem/option-list-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ import {ListItem} from './list-item';

/** @soyCompatible */
export class OptionListItem extends ListItem {
/** @soyTemplate */
protected override getAriaRole(): ARIARole {
return 'option';
}
override role: ARIARole = 'option';

override handleClick(e: MouseEvent) {
this.dispatchEvent(new CustomEvent(
Expand Down
5 changes: 1 addition & 4 deletions list/lib/option-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,5 @@ import {List} from './list';

/** @soyCompatible */
export class OptionList extends List {
/** @soyTemplate */
protected override getAriaRole(): ARIARole {
return 'listbox';
}
override role: ARIARole = 'listbox';
}
10 changes: 10 additions & 0 deletions list/list-item_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,14 @@ describe('list item tests', () => {
'tabindex'))
.toBe('-1');
});

it('setting `role` attribute sets role on <li> element', async () => {
const listItem =
env.render(html`<md-list-item role="menuitem">One</md-list-item>`)
.querySelector('md-list-item')!;
await env.waitForStability();

expect(listItem.shadowRoot!.querySelector('li')!.getAttribute('role'))
.toBe('menuitem');
});
});
32 changes: 26 additions & 6 deletions list/list_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,33 +22,53 @@ describe('list tests', () => {
const env = new Environment();

it('`items` property returns correct items', async () => {
const element = env.render(LIST_TEMPLATE).querySelector('md-list')!;
await env.waitForStability();
const list = env.render(LIST_TEMPLATE).querySelector('md-list')!;
await list.updateComplete;

expect(element.items.length).toBe(3);
expect(list.items.length).toBe(3);
});

it('focusListRoot() should focus on the list element', async () => {
const list = env.render(LIST_TEMPLATE).querySelector('md-list')!;
await env.waitForStability();
await list.updateComplete;

list.focusListRoot();
expect(document.activeElement).toEqual(list);
});

it('activateFirstItem() should focus on the first list item', async () => {
const list = env.render(LIST_TEMPLATE).querySelector('md-list')!;
await env.waitForStability();
await list.updateComplete;

list.activateFirstItem();
expect(document.activeElement).toEqual(list.items[0]);
});

it('activateLastItem() should focus on the last list item', async () => {
const list = env.render(LIST_TEMPLATE).querySelector('md-list')!;
await env.waitForStability();
await list.updateComplete;

list.activateLastItem();
expect(document.activeElement).toEqual(list.items[list.items.length - 1]);
});

it('setting `role` attribute sets role on <ul> element', async () => {
const listItem = env.render(html`<md-list role="menu"></md-list>`)
.querySelector('md-list')!;
await env.waitForStability();

expect(listItem.shadowRoot!.querySelector('ul')!.getAttribute('role'))
.toBe('menu');
});

it('setting `aria-label` attribute sets aria-label on <ul> element',
async () => {
const listItem = env.render(html`<md-list aria-label="foo"></md-list>`)
.querySelector('md-list')!;
await env.waitForStability();

expect(
listItem.shadowRoot!.querySelector('ul')!.getAttribute('aria-label'))
.toBe('foo');
});
});

0 comments on commit 8f63406

Please sign in to comment.