refactor(list)!: move list aria to host

BREAKING CHANGE: Aria and roles on List have been moved to the host element. list-tabindex attribute should be migrated to tabindex attribute. type attribute should be migrated to role attribute.

PiperOrigin-RevId: 565767899
This commit is contained in:
Elliott Marquez 2023-09-15 13:44:49 -07:00 committed by Copybara-Service
parent cfd053c397
commit 9447ec7d72
7 changed files with 59 additions and 41 deletions

View File

@ -56,7 +56,7 @@
<!-- unrelated change to components -->
{% block content %}{{ content | safe }}{% endblock %}
</main>
<md-list list-tabindex="-1">
<md-list>
{% for component in collections.component|filtersort('data.name') %}
<li>
<md-list-item

View File

@ -233,11 +233,15 @@ Videos can also be slotted into list-items' `start-video"` or
## Accessibility
List and List Items can have their internal `role` attribute set via the `type`
attribute which by default is set to
[`'list'`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/list_role)<!-- {.external} -->
and[ `'listitem'`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listitem_role)<!-- {.external} -->
respectively.
List can have its `role` and `tabindex` set via the `role` and `tabindex`
attributes, and list items can have their internal `role` and `tabindex` set via
the `type` and `item-tabindex` attributes respectively.
By default these values are set to
[`role="list"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/list_role)<!-- {.external} -->
and `tabindex="-1"` for list, and
[`role="listitem"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listitem_role)<!-- {.external} -->
and `tabindex="0"` for list items.
The following example sets
[`role="listbox"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role)<!-- {.external} -->
@ -246,7 +250,7 @@ on the internal list and
on the internal list item nodes.
```html
<md-list type="listbox">
<md-list role="listbox" tabindex="0">
<md-list-item type="option" headline="icon">
<md-icon slot="start-icon">account_circle</md-icon>
</md-list-item>

View File

@ -8,7 +8,7 @@ import './index.js';
import './material-collection.js';
import {KnobTypesToKnobs, MaterialCollection, materialInitsToStoryInits, setUpDemo, title} from './material-collection.js';
import {boolInput, Knob, numberInput, textInput} from './index.js';
import {boolInput, Knob, textInput} from './index.js';
import {stories, StoryKnobs} from './stories.js';
@ -36,7 +36,6 @@ export const VIDEO_URL =
const collection =
new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>('List', [
new Knob('listTabIndex', {ui: numberInput(), defaultValue: -1}),
new Knob('md-list-item', {ui: title()}),
new Knob('disabled', {ui: boolInput(), defaultValue: false}),
new Knob('noninteractive', {ui: boolInput(), defaultValue: false}),

View File

@ -14,8 +14,6 @@ import {css, html} from 'lit';
/** Knob types for list stories. */
export interface StoryKnobs {
listTabIndex: number;
'md-list-item': void;
disabled: boolean;
noninteractive: boolean;
@ -59,7 +57,6 @@ const standard: MaterialStoryInit<StoryKnobs> = {
}`,
render(knobs) {
const {
listTabIndex,
disabled,
noninteractive,
multiLineSupportingText,
@ -72,9 +69,7 @@ const standard: MaterialStoryInit<StoryKnobs> = {
} = knobs;
return html`
<div class="list-demo">
<md-list
.listTabIndex=${listTabIndex}
class="list">
<md-list class="list" role="listbox">
<md-list-item
.headline=${headline}
.supportingText=${supportingText}

View File

@ -4,11 +4,10 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {html, LitElement, nothing} from 'lit';
import {property, query, queryAssignedElements} from 'lit/decorators.js';
import {html, isServer, LitElement} from 'lit';
import {query, queryAssignedElements} from 'lit/decorators.js';
import {ARIAMixinStrict} from '../../internal/aria/aria.js';
import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js';
import {polyfillElementInternalsAria, setupHostAria} from '../../internal/aria/aria.js';
import {ListItem} from './listitem/list-item.js';
@ -44,20 +43,9 @@ function isNavigableKey(key: string): key is NavigatableValues {
// tslint:disable-next-line:enforce-comments-on-exported-symbols
export class List extends LitElement {
static {
requestUpdateOnAriaChange(List);
setupHostAria(List, {focusable: false});
}
/** @nocollapse */
static override shadowRootOptions:
ShadowRootInit = {mode: 'open', delegatesFocus: true};
@property() type: 'menu'|'menubar'|'listbox'|'list'|'' = 'list';
/**
* The tabindex of the underlying list.
*/
@property({type: Number, attribute: 'list-tabindex'}) listTabIndex = -1;
@query('.list') private listRoot!: HTMLElement|null;
/**
@ -71,6 +59,17 @@ export class List extends LitElement {
@queryAssignedElements({flatten: true, selector: '[md-list-item]'})
items!: ListItem[];
private readonly internals = polyfillElementInternalsAria(
this, (this as HTMLElement /* needed for closure */).attachInternals());
constructor() {
super();
if (!isServer) {
this.internals.role = 'list';
this.addEventListener('keydown', this.handleKeydown.bind(this));
}
}
protected override render() {
return this.renderList();
}
@ -79,15 +78,8 @@ export class List extends LitElement {
* Renders the main list element.
*/
private renderList() {
// Needed for closure conformance
const {ariaLabel} = this as ARIAMixinStrict;
return html`
<ul class="list"
aria-label=${ariaLabel || nothing}
tabindex=${this.listTabIndex}
role=${this.type || nothing}
@keydown=${this.handleKeydown}
>
<ul class="list" role="presentation">
${this.renderContent()}
</ul>
`;

View File

@ -937,6 +937,34 @@ describe('<md-list>', () => {
expect(document.activeElement).toEqual(first);
});
});
describe('aria', () => {
it('Sets default role to list', async () => {
const root = env.render(html`<md-list></md-list>`);
const listEl = root.querySelector('md-list')!;
await env.waitForStability();
const internals =
(listEl as unknown as {internals: {role: string | null}}).internals;
expect(internals.role).toEqual('list');
});
it('Does not override user given role attribute', async () => {
const root = env.render(html`<md-list role="listbox"></md-list>`);
const listEl = root.querySelector('md-list')!;
await env.waitForStability();
expect(listEl.getAttribute('role')).toBe('listbox');
});
it('Does not override user given role property', async () => {
const root = env.render(html`<md-list .role=${'listbox'}></md-list>`);
const listEl = root.querySelector('md-list')!;
await env.waitForStability();
expect(listEl.role).toBe('listbox');
});
});
});
describe('<md-list-item>', () => {

View File

@ -306,8 +306,8 @@ export abstract class Menu extends LitElement {
part="list"
id="list"
aria-label=${ariaLabel || nothing}
type=${this.type}
listTabIndex=${this.listTabIndex}
role=${this.type}
tabindex=${this.listTabIndex}
@keydown=${this.handleListKeydown}>
${this.renderMenuItems()}
</md-list>`;