Skip to content

Commit

Permalink
fix(chips): add aria grid/listbox models to chip set
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 540384126
  • Loading branch information
asyncLiz authored and copybara-github committed Jun 14, 2023
1 parent b1172d8 commit fcdb126
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 27 deletions.
22 changes: 15 additions & 7 deletions chips/demo/stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ const standard: MaterialStoryInit<StoryKnobs> = {
render({label, elevated, disabled, scrolling}) {
const classes = {scrolling};
return html`
<md-chip-set class=${classMap(classes)}>
<md-chip-set type="assist" class=${classMap(classes)}
aria-label="Assist chips">
<md-assist-chip
label=${label || 'Assist chip'}
?disabled=${disabled}
Expand All @@ -74,7 +75,8 @@ const links: MaterialStoryInit<StoryKnobs> = {
render({label, elevated, disabled, scrolling}) {
const classes = {scrolling};
return html`
<md-chip-set class=${classMap(classes)}>
<md-chip-set type="assist" class=${classMap(classes)}
aria-label="Assist link chips">
<md-assist-chip
label=${label || 'Assist link chip'}
?disabled=${disabled}
Expand All @@ -93,7 +95,9 @@ const filters: MaterialStoryInit<StoryKnobs> = {
render({label, elevated, disabled, scrolling, singleSelect}) {
const classes = {scrolling};
return html`
<md-chip-set class=${classMap(classes)} ?single-select=${singleSelect}>
<md-chip-set type="filter" class=${classMap(classes)}
aria-label="Filter chips"
?single-select=${singleSelect}>
<md-filter-chip
label=${label || 'Filter chip'}
?disabled=${disabled}
Expand Down Expand Up @@ -123,7 +127,8 @@ const inputs: MaterialStoryInit<StoryKnobs> = {
render({label, disabled, scrolling}) {
const classes = {scrolling};
return html`
<md-chip-set class=${classMap(classes)}>
<md-chip-set type="input" class=${classMap(classes)}
aria-label="Input chips">
<md-input-chip
label=${label || 'Input chip'}
?disabled=${disabled}
Expand Down Expand Up @@ -157,7 +162,8 @@ const inputLinks: MaterialStoryInit<StoryKnobs> = {
render({label, disabled, scrolling}) {
const classes = {scrolling};
return html`
<md-chip-set class=${classMap(classes)}>
<md-chip-set type="input" class=${classMap(classes)}
aria-label="Input link chips">
<md-input-chip
label=${label || 'Input link chip'}
?disabled=${disabled}
Expand All @@ -175,7 +181,8 @@ const suggestions: MaterialStoryInit<StoryKnobs> = {
render({label, elevated, disabled, scrolling}) {
const classes = {scrolling};
return html`
<md-chip-set class=${classMap(classes)}>
<md-chip-set type="suggestion" class=${classMap(classes)}
aria-label="Suggestion chips">
<md-suggestion-chip
label=${label || 'Suggestion chip'}
?disabled=${disabled}
Expand All @@ -199,7 +206,8 @@ const suggestionLinks: MaterialStoryInit<StoryKnobs> = {
render({label, elevated, disabled, scrolling}) {
const classes = {scrolling};
return html`
<md-chip-set class=${classMap(classes)}>
<md-chip-set type="suggestion" class=${classMap(classes)}
aria-label="Suggestion link chips">
<md-suggestion-chip
label=${label || 'Suggestion link chip'}
?disabled=${disabled}
Expand Down
6 changes: 6 additions & 0 deletions chips/lib/_chip-set.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,10 @@
flex-wrap: wrap;
gap: 8px;
}

.content {
display: flex;
flex-wrap: inherit;
gap: inherit;
}
}
4 changes: 4 additions & 0 deletions chips/lib/_shared.scss
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@
pointer-events: none;
}

.cell {
display: flex;
}

.action {
align-items: baseline;
appearance: none;
Expand Down
28 changes: 26 additions & 2 deletions chips/lib/chip-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,33 @@
* SPDX-License-Identifier: Apache-2.0
*/

import {html, isServer, LitElement, PropertyValues} from 'lit';
import {html, isServer, LitElement, nothing, PropertyValues} from 'lit';
import {property, queryAssignedElements} from 'lit/decorators.js';

import {ARIAMixinStrict} from '../../internal/aria/aria.js';
import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js';

import {Chip} from './chip.js';

/**
* The type of chip a chip set controls.
*/
export type ChipSetType = 'assist'|'suggestion'|'filter'|'input'|'';

/**
* A chip set component.
*/
export class ChipSet extends LitElement {
static {
requestUpdateOnAriaChange(this);
}

get chips() {
return this.childElements.filter(
(child): child is Chip => child instanceof Chip);
}

@property() type: ChipSetType = '';
@property({type: Boolean, attribute: 'single-select'}) singleSelect = false;

@queryAssignedElements({flatten: true})
Expand Down Expand Up @@ -49,7 +62,18 @@ export class ChipSet extends LitElement {
}

protected override render() {
return html`<slot @slotchange=${this.updateTabIndices}></slot>`;
const {ariaLabel} = this as ARIAMixinStrict;
const isFilter = this.type === 'filter';
const role = isFilter ? 'listbox' : 'grid';
const multiselectable = isFilter ? !this.singleSelect : nothing;
return html`
<div class="content"
role=${role}
aria-label=${ariaLabel || nothing}
aria-multiselectable=${multiselectable}>
<slot @slotchange=${this.updateTabIndices}></slot>
</div>
`;
}

private handleKeyDown(event: KeyboardEvent) {
Expand Down
34 changes: 27 additions & 7 deletions chips/lib/chip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import '../../focus/focus-ring.js';
import '../../ripple/ripple.js';

import {html, LitElement, TemplateResult} from 'lit';
import {property} from 'lit/decorators.js';
import {html, LitElement, nothing, TemplateResult} from 'lit';
import {property, state} from 'lit/decorators.js';
import {classMap} from 'lit/directives/class-map.js';

import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js';
Expand Down Expand Up @@ -43,14 +43,21 @@ export abstract class Chip extends LitElement {
return this.disabled;
}

/**
* The aria role of the container. Defaults to `row` for grid chip sets.
* Listbox chip sets should remove this since they do not contain cells.
*/
@state() protected containerRole?: 'row' = 'row';

protected override render() {
return html`
<div class="container ${classMap(this.getContainerClasses())}">
<div class="container ${classMap(this.getContainerClasses())}"
role=${this.containerRole || nothing}>
${this.renderOutline()}
<md-focus-ring for=${this.primaryId}></md-focus-ring>
<md-ripple for=${this.primaryId}
?disabled=${this.rippleDisabled}></md-ripple>
${this.renderAction()}
${this.renderActions()}
</div>
`;
}
Expand All @@ -61,18 +68,31 @@ export abstract class Chip extends LitElement {
};
}

protected renderActions() {
return this.renderActionCell(this.renderAction());
}

protected renderActionCell(content: TemplateResult|
typeof nothing): TemplateResult|typeof nothing {
if (content === nothing) {
return content;
}

return html`<div class="cell" role="cell">${content}</div>`;
}

protected abstract renderAction(): TemplateResult;

protected renderContent() {
return html`
<span class="leading icon">
<span class="leading icon" aria-hidden="true">
${this.renderLeadingIcon()}
</span>
<span class="label">${this.label}</span>
<span class="touch"></span>
`;
}

protected abstract renderAction(): TemplateResult;

protected renderOutline() {
return html`<span class="outline"></span>`;
}
Expand Down
16 changes: 13 additions & 3 deletions chips/lib/filter-chip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import '../../elevation/elevation.js';

import {html, nothing, PropertyValues, svg} from 'lit';
import {html, nothing, PropertyValues, svg, TemplateResult} from 'lit';
import {property, query} from 'lit/decorators.js';

import {ARIAMixinStrict} from '../../internal/aria/aria.js';
Expand All @@ -32,6 +32,9 @@ export class FilterChip extends MultiActionChip {

constructor() {
super();
// Remove the `row` role from the container, since filter chips do not use a
// `grid` navigation model.
this.containerRole = undefined;
this.addEventListener('click', () => {
if (this.disabled) {
return;
Expand All @@ -56,7 +59,13 @@ export class FilterChip extends MultiActionChip {
};
}

protected override renderPrimaryAction() {
protected override renderActionCell(content: TemplateResult|typeof nothing) {
// Filter chips use a `listbox`/`option` model, and do not need `gridcell`
// wrappers around their actions.
return content;
}

protected override renderAction() {
const {ariaLabel} = this as ARIAMixinStrict;
return html`
<button class="primary action"
Expand All @@ -83,7 +92,8 @@ export class FilterChip extends MultiActionChip {

protected override renderTrailingAction() {
if (this.removable) {
return renderRemoveButton({disabled: this.disabled});
return renderRemoveButton(
{ariaLabel: this.ariaLabelRemove, disabled: this.disabled});
}

return nothing;
Expand Down
7 changes: 5 additions & 2 deletions chips/lib/input-chip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export class InputChip extends MultiActionChip {
};
}

protected override renderPrimaryAction() {
protected override renderAction() {
const {ariaLabel} = this as ARIAMixinStrict;
if (this.href) {
return html`
Expand Down Expand Up @@ -103,6 +103,9 @@ export class InputChip extends MultiActionChip {
}

protected override renderTrailingAction() {
return renderRemoveButton({disabled: this.disabled});
return renderRemoveButton({
ariaLabel: this.ariaLabelRemove,
disabled: this.disabled,
});
}
}
35 changes: 30 additions & 5 deletions chips/lib/multi-action-chip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,39 @@

import {html, isServer, nothing, TemplateResult} from 'lit';

import {ARIAMixinStrict} from '../../internal/aria/aria.js';

import {Chip} from './chip.js';

const ARIA_LABEL_REMOVE = 'aria-label-remove';

/**
* A chip component with multiple actions.
*/
export abstract class MultiActionChip extends Chip {
get ariaLabelRemove(): string {
if (this.hasAttribute(ARIA_LABEL_REMOVE)) {
return this.getAttribute(ARIA_LABEL_REMOVE)!;
}

const {ariaLabel} = this as ARIAMixinStrict;
return `Remove ${ariaLabel || this.label}`;
}
set ariaLabelRemove(ariaLabel: string|null) {
const prev = this.ariaLabelRemove;
if (ariaLabel === prev) {
return;
}

if (ariaLabel === null) {
this.removeAttribute(ARIA_LABEL_REMOVE);
} else {
this.setAttribute(ARIA_LABEL_REMOVE, ariaLabel);
}

this.requestUpdate();
}

protected abstract readonly primaryAction: HTMLElement|null;
protected abstract readonly trailingAction: HTMLElement|null;

Expand All @@ -37,15 +64,13 @@ export abstract class MultiActionChip extends Chip {
this.updateTabIndices();
}

protected override renderAction() {
protected override renderActions() {
return html`
${this.renderPrimaryAction()}
${this.renderTrailingAction()}
${super.renderActions()}
${this.renderActionCell(this.renderTrailingAction())}
`;
}

protected abstract renderPrimaryAction(): TemplateResult;

protected abstract renderTrailingAction(): TemplateResult|typeof nothing;

private handleKeyDown(event: KeyboardEvent) {
Expand Down
9 changes: 8 additions & 1 deletion chips/lib/trailing-icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@ import {html} from 'lit';

import {Chip} from './chip.js';

interface RemoveButtonProperties {
ariaLabel: string;
disabled: boolean;
}

/** @protected */
export function renderRemoveButton({disabled}: {disabled: boolean}) {
export function renderRemoveButton(
{ariaLabel, disabled}: RemoveButtonProperties) {
return html`
<button class="trailing action"
aria-label=${ariaLabel}
?disabled=${disabled}
@click=${handleRemoveClick}
>
Expand Down

0 comments on commit fcdb126

Please sign in to comment.