mirror of
https://github.com/material-components/material-web.git
synced 2026-01-09 07:21:09 +08:00
feat(chips): swap to toolbar a11y pattern
BREAKING CHANGE: chips now follow the [aria toolbar pattern](https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/examples/toolbar/). Chip sets are toolbars and chips are buttons or links. Filter chips are toggle buttons. What to change: - Remove `type` attribute from `<md-chip-set>` (you can mix and match chip types!) - Remove `single-select` from `<md-chip-set>`. Use JS to control filter chips if single selection is required. Radio filter chips will come in a future update. - Disabled chips CAN be focused with the keyboard if `always-focusable` is set. - Filter chips no longer dispatch a `"selected"` event. Listen to `"click"` and use `event.target.selected` instead. - ArrowUp and ArrowDown no longer navigate between chips. These are reserved for chip actions, like dropdown menu chips. PiperOrigin-RevId: 566352830
This commit is contained in:
parent
1f31df818b
commit
16bfac1343
@ -17,7 +17,6 @@ const collection =
|
||||
new Knob('label', {defaultValue: '', ui: textInput()}),
|
||||
new Knob('elevated', {defaultValue: false, ui: boolInput()}),
|
||||
new Knob('disabled', {defaultValue: false, ui: boolInput()}),
|
||||
new Knob('singleSelect', {defaultValue: false, ui: boolInput()}),
|
||||
new Knob('scrolling', {defaultValue: false, ui: boolInput()}),
|
||||
]);
|
||||
|
||||
|
||||
@ -20,7 +20,6 @@ export interface StoryKnobs {
|
||||
label: string;
|
||||
elevated: boolean;
|
||||
disabled: boolean;
|
||||
singleSelect: boolean;
|
||||
scrolling: boolean;
|
||||
}
|
||||
|
||||
@ -44,14 +43,13 @@ const styles = css`
|
||||
}
|
||||
`;
|
||||
|
||||
const standard: MaterialStoryInit<StoryKnobs> = {
|
||||
const assist: MaterialStoryInit<StoryKnobs> = {
|
||||
name: 'Assist chips',
|
||||
styles,
|
||||
render({label, elevated, disabled, scrolling}) {
|
||||
const classes = {scrolling};
|
||||
const classes = {'scrolling': scrolling};
|
||||
return html`
|
||||
<md-chip-set type="assist" class=${classMap(classes)}
|
||||
aria-label="Assist chips">
|
||||
<md-chip-set class=${classMap(classes)} aria-label="Assist chips">
|
||||
<md-assist-chip
|
||||
label=${label || 'Assist chip'}
|
||||
?disabled=${disabled}
|
||||
@ -64,26 +62,18 @@ const standard: MaterialStoryInit<StoryKnobs> = {
|
||||
>
|
||||
<md-icon slot="icon">local_laundry_service</md-icon>
|
||||
</md-assist-chip>
|
||||
</md-chip-set>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
const links: MaterialStoryInit<StoryKnobs> = {
|
||||
name: 'Assist link chips',
|
||||
styles,
|
||||
render({label, elevated, disabled, scrolling}) {
|
||||
const classes = {scrolling};
|
||||
return html`
|
||||
<md-chip-set type="assist" class=${classMap(classes)}
|
||||
aria-label="Assist link chips">
|
||||
<md-assist-chip
|
||||
label=${label || 'Assist link chip'}
|
||||
?disabled=${disabled}
|
||||
?elevated=${elevated}
|
||||
href="https://google.com"
|
||||
target="_blank"
|
||||
>${GOOGLE_LOGO}</md-assist-chip>
|
||||
<md-assist-chip
|
||||
label=${label || 'Disabled assist chip (focusable)'}
|
||||
disabled
|
||||
always-focusable
|
||||
?elevated=${elevated}
|
||||
></md-assist-chip>
|
||||
</md-chip-set>
|
||||
`;
|
||||
}
|
||||
@ -92,12 +82,11 @@ const links: MaterialStoryInit<StoryKnobs> = {
|
||||
const filters: MaterialStoryInit<StoryKnobs> = {
|
||||
name: 'Filter chips',
|
||||
styles,
|
||||
render({label, elevated, disabled, scrolling, singleSelect}) {
|
||||
const classes = {scrolling};
|
||||
render({label, elevated, disabled, scrolling}) {
|
||||
const classes = {'scrolling': scrolling};
|
||||
return html`
|
||||
<md-chip-set type="filter" class=${classMap(classes)}
|
||||
aria-label="Filter chips"
|
||||
?single-select=${singleSelect}>
|
||||
<md-chip-set class=${classMap(classes)}
|
||||
aria-label="Filter chips">
|
||||
<md-filter-chip
|
||||
label=${label || 'Filter chip'}
|
||||
?disabled=${disabled}
|
||||
@ -116,6 +105,13 @@ const filters: MaterialStoryInit<StoryKnobs> = {
|
||||
?elevated=${elevated}
|
||||
removable
|
||||
></md-filter-chip>
|
||||
<md-filter-chip
|
||||
label=${label || 'Disabled filter chip (focusable)'}
|
||||
disabled
|
||||
always-focusable
|
||||
?elevated=${elevated}
|
||||
removable
|
||||
></md-filter-chip>
|
||||
</md-chip-set>
|
||||
`;
|
||||
}
|
||||
@ -125,10 +121,9 @@ const inputs: MaterialStoryInit<StoryKnobs> = {
|
||||
name: 'Input chips',
|
||||
styles,
|
||||
render({label, disabled, scrolling}) {
|
||||
const classes = {scrolling};
|
||||
const classes = {'scrolling': scrolling};
|
||||
return html`
|
||||
<md-chip-set type="input" class=${classMap(classes)}
|
||||
aria-label="Input chips">
|
||||
<md-chip-set class=${classMap(classes)} aria-label="Input chips">
|
||||
<md-input-chip
|
||||
label=${label || 'Input chip'}
|
||||
?disabled=${disabled}
|
||||
@ -146,30 +141,21 @@ const inputs: MaterialStoryInit<StoryKnobs> = {
|
||||
>
|
||||
<img slot="icon" src="https://lh3.googleusercontent.com/a/default-user=s48">
|
||||
</md-input-chip>
|
||||
<md-input-chip
|
||||
label=${label || 'Input link chip'}
|
||||
href="https://google.com"
|
||||
target="_blank"
|
||||
>${GOOGLE_LOGO}</md-input-chip>
|
||||
<md-input-chip
|
||||
label=${label || 'Remove-only input chip'}
|
||||
?disabled=${disabled}
|
||||
remove-only
|
||||
></md-input-chip>
|
||||
</md-chip-set>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
const inputLinks: MaterialStoryInit<StoryKnobs> = {
|
||||
name: 'Input link chips',
|
||||
styles,
|
||||
render({label, disabled, scrolling}) {
|
||||
const classes = {scrolling};
|
||||
return html`
|
||||
<md-chip-set type="input" class=${classMap(classes)}
|
||||
aria-label="Input link chips">
|
||||
<md-input-chip
|
||||
label=${label || 'Input link chip'}
|
||||
?disabled=${disabled}
|
||||
href="https://google.com"
|
||||
target="_blank"
|
||||
>${GOOGLE_LOGO}</md-input-chip>
|
||||
label=${label || 'Disabled input chip (focusable)'}
|
||||
disabled
|
||||
always-focusable
|
||||
></md-input-chip>
|
||||
</md-chip-set>
|
||||
`;
|
||||
}
|
||||
@ -179,10 +165,9 @@ const suggestions: MaterialStoryInit<StoryKnobs> = {
|
||||
name: 'Suggestion chips',
|
||||
styles,
|
||||
render({label, elevated, disabled, scrolling}) {
|
||||
const classes = {scrolling};
|
||||
const classes = {'scrolling': scrolling};
|
||||
return html`
|
||||
<md-chip-set type="suggestion" class=${classMap(classes)}
|
||||
aria-label="Suggestion chips">
|
||||
<md-chip-set class=${classMap(classes)} aria-label="Suggestion chips">
|
||||
<md-suggestion-chip
|
||||
label=${label || 'Suggestion chip'}
|
||||
?disabled=${disabled}
|
||||
@ -195,32 +180,22 @@ const suggestions: MaterialStoryInit<StoryKnobs> = {
|
||||
>
|
||||
<md-icon slot="icon">local_laundry_service</md-icon>
|
||||
</md-suggestion-chip>
|
||||
</md-chip-set>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
const suggestionLinks: MaterialStoryInit<StoryKnobs> = {
|
||||
name: 'Suggestion link chips',
|
||||
styles,
|
||||
render({label, elevated, disabled, scrolling}) {
|
||||
const classes = {scrolling};
|
||||
return html`
|
||||
<md-chip-set type="suggestion" class=${classMap(classes)}
|
||||
aria-label="Suggestion link chips">
|
||||
<md-suggestion-chip
|
||||
label=${label || 'Suggestion link chip'}
|
||||
?disabled=${disabled}
|
||||
?elevated=${elevated}
|
||||
href="https://google.com"
|
||||
target="_blank"
|
||||
>${GOOGLE_LOGO}</md-suggestion-chip>
|
||||
<md-suggestion-chip
|
||||
label=${label || 'Disabled suggestion chip (focusable)'}
|
||||
disabled
|
||||
always-focusable
|
||||
?elevated=${elevated}
|
||||
></md-suggestion-chip>
|
||||
</md-chip-set>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
/** Chips stories. */
|
||||
export const stories = [
|
||||
standard, links, filters, inputs, inputLinks, suggestions, suggestionLinks
|
||||
];
|
||||
export const stories = [assist, filters, inputs, suggestions];
|
||||
|
||||
@ -9,10 +9,4 @@
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-wrap: inherit;
|
||||
gap: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ import {property} from 'lit/decorators.js';
|
||||
|
||||
import {ARIAMixinStrict} from '../../internal/aria/aria.js';
|
||||
|
||||
import {Chip, renderGridAction, renderGridContainer} from './chip.js';
|
||||
import {Chip} from './chip.js';
|
||||
|
||||
/**
|
||||
* An assist chip component.
|
||||
@ -30,10 +30,6 @@ export class AssistChip extends Chip {
|
||||
return !this.href && this.disabled;
|
||||
}
|
||||
|
||||
protected override renderContainer(content: unknown) {
|
||||
return renderGridContainer(content, this.getContainerClasses());
|
||||
}
|
||||
|
||||
protected override getContainerClasses() {
|
||||
return {
|
||||
...super.getContainerClasses(),
|
||||
@ -47,24 +43,24 @@ export class AssistChip extends Chip {
|
||||
protected override renderPrimaryAction(content: unknown) {
|
||||
const {ariaLabel} = this as ARIAMixinStrict;
|
||||
if (this.href) {
|
||||
return renderGridAction(html`
|
||||
return html`
|
||||
<a class="primary action"
|
||||
id="link"
|
||||
aria-label=${ariaLabel || nothing}
|
||||
href=${this.href}
|
||||
target=${this.target || nothing}
|
||||
>${content}</a>
|
||||
`);
|
||||
`;
|
||||
}
|
||||
|
||||
return renderGridAction(html`
|
||||
return html`
|
||||
<button class="primary action"
|
||||
id="button"
|
||||
aria-label=${ariaLabel || nothing}
|
||||
?disabled=${this.disabled}
|
||||
?disabled=${this.disabled && !this.alwaysFocusable}
|
||||
type="button"
|
||||
>${content}</button>
|
||||
`);
|
||||
`;
|
||||
}
|
||||
|
||||
protected override renderOutline() {
|
||||
|
||||
@ -4,25 +4,19 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {html, isServer, LitElement, nothing, PropertyValues} from 'lit';
|
||||
import {property, queryAssignedElements} from 'lit/decorators.js';
|
||||
import {html, isServer, LitElement} from 'lit';
|
||||
import {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 {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(ChipSet);
|
||||
setupHostAria(ChipSet, {focusable: false});
|
||||
}
|
||||
|
||||
get chips() {
|
||||
@ -30,60 +24,31 @@ export class ChipSet extends LitElement {
|
||||
(child): child is Chip => child instanceof Chip);
|
||||
}
|
||||
|
||||
@property() type: ChipSetType = '';
|
||||
@property({type: Boolean, attribute: 'single-select'}) singleSelect = false;
|
||||
|
||||
@queryAssignedElements() private readonly childElements!: HTMLElement[];
|
||||
private readonly internals = polyfillElementInternalsAria(
|
||||
this, (this as HTMLElement /* needed for closure */).attachInternals());
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
if (!isServer) {
|
||||
this.addEventListener('focusin', this.updateTabIndices.bind(this));
|
||||
this.addEventListener('update-focus', this.updateTabIndices.bind(this));
|
||||
this.addEventListener('keydown', this.handleKeyDown.bind(this));
|
||||
this.addEventListener('selected', this.handleSelected.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
protected override updated(changed: PropertyValues<ChipSet>) {
|
||||
if (changed.has('singleSelect') && this.singleSelect) {
|
||||
let hasSelectedChip = false;
|
||||
for (const chip of this.chips as MaybeSelectableChip[]) {
|
||||
if (chip.selected === true) {
|
||||
if (!hasSelectedChip) {
|
||||
hasSelectedChip = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
chip.selected = false;
|
||||
}
|
||||
}
|
||||
this.internals.role = 'toolbar';
|
||||
}
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
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>
|
||||
`;
|
||||
return html`<slot @slotchange=${this.updateTabIndices}></slot>`;
|
||||
}
|
||||
|
||||
private handleKeyDown(event: KeyboardEvent) {
|
||||
const isDown = event.key === 'ArrowDown';
|
||||
const isUp = event.key === 'ArrowUp';
|
||||
const isLeft = event.key === 'ArrowLeft';
|
||||
const isRight = event.key === 'ArrowRight';
|
||||
const isHome = event.key === 'Home';
|
||||
const isEnd = event.key === 'End';
|
||||
// Ignore non-navigation keys
|
||||
if (!isLeft && !isRight && !isDown && !isUp && !isHome && !isEnd) {
|
||||
if (!isLeft && !isRight && !isHome && !isEnd) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -105,7 +70,7 @@ export class ChipSet extends LitElement {
|
||||
|
||||
// Check if moving forwards or backwards
|
||||
const isRtl = getComputedStyle(this).direction === 'rtl';
|
||||
const forwards = isRtl ? isLeft || isUp : isRight || isDown;
|
||||
const forwards = isRtl ? isLeft : isRight;
|
||||
const focusedChip = chips.find(chip => chip.matches(':focus-within'));
|
||||
if (!focusedChip) {
|
||||
// If there is not already a chip focused, select the first or last chip
|
||||
@ -131,8 +96,11 @@ export class ChipSet extends LitElement {
|
||||
|
||||
// Check if the next sibling is disabled. If so,
|
||||
// move the index and continue searching.
|
||||
//
|
||||
// Some toolbar items may be focusable when disabled for increased
|
||||
// visibility.
|
||||
const nextChip = chips[nextIndex];
|
||||
if (nextChip.disabled) {
|
||||
if (nextChip.disabled && !nextChip.alwaysFocusable) {
|
||||
if (forwards) {
|
||||
nextIndex++;
|
||||
} else {
|
||||
@ -149,33 +117,31 @@ export class ChipSet extends LitElement {
|
||||
}
|
||||
|
||||
private updateTabIndices() {
|
||||
// The chip that should be focusable is either the chip that currently has
|
||||
// focus or the first chip that can be focused.
|
||||
const {chips} = this;
|
||||
let hasFocusedChip = false;
|
||||
let chipToFocus: Chip|undefined;
|
||||
for (const chip of chips) {
|
||||
if (chip.matches(':focus-within')) {
|
||||
chip.removeAttribute('tabindex');
|
||||
hasFocusedChip = true;
|
||||
} else {
|
||||
chip.tabIndex = -1;
|
||||
const isChipFocusable = chip.alwaysFocusable || !chip.disabled;
|
||||
const chipIsFocused = chip.matches(':focus-within');
|
||||
if (chipIsFocused && isChipFocusable) {
|
||||
// Found the first chip that is actively focused. This overrides the
|
||||
// first focusable chip found.
|
||||
chipToFocus = chip;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasFocusedChip) {
|
||||
chips[0]?.removeAttribute('tabindex');
|
||||
}
|
||||
}
|
||||
|
||||
private handleSelected(event: Event) {
|
||||
if (!this.singleSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((event.target as MaybeSelectableChip).selected === true) {
|
||||
for (const chip of this.chips as MaybeSelectableChip[]) {
|
||||
if (chip !== event.target && chip.selected) {
|
||||
chip.selected = false;
|
||||
}
|
||||
if (isChipFocusable && !chipToFocus) {
|
||||
chipToFocus = chip;
|
||||
}
|
||||
|
||||
// Disable non-focused chips. If we disable all of them, we'll grant focus
|
||||
// to the first focusable child that was found.
|
||||
chip.tabIndex = -1;
|
||||
}
|
||||
|
||||
if (chipToFocus) {
|
||||
chipToFocus.tabIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -183,7 +149,3 @@ export class ChipSet extends LitElement {
|
||||
interface MaybeMultiActionChip extends Chip {
|
||||
focus(options?: FocusOptions&{trailing?: boolean}): void;
|
||||
}
|
||||
|
||||
interface MaybeSelectableChip extends Chip {
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
@ -16,7 +16,6 @@ import {ChipHarness} from '../harness.js';
|
||||
import {AssistChip} from './assist-chip.js';
|
||||
import {Chip} from './chip.js';
|
||||
import {ChipSet} from './chip-set.js';
|
||||
import {FilterChip} from './filter-chip.js';
|
||||
import {InputChip} from './input-chip.js';
|
||||
|
||||
@customElement('test-chip-set')
|
||||
@ -28,9 +27,6 @@ class TestAssistChip extends AssistChip {
|
||||
@customElement('test-chip-set-input-chip')
|
||||
class TestInputChip extends InputChip {
|
||||
}
|
||||
@customElement('test-chip-set-filter-chip')
|
||||
class TestFilterChip extends FilterChip {
|
||||
}
|
||||
|
||||
describe('Chip set', () => {
|
||||
const env = new Environment();
|
||||
@ -68,9 +64,9 @@ describe('Chip set', () => {
|
||||
const chipSet = await setupTest(
|
||||
[new TestAssistChip(), new TestAssistChip(), new TestAssistChip()]);
|
||||
|
||||
expect(chipSet.chips[0].hasAttribute('tabindex'))
|
||||
.withContext('first chip does not have tabindex')
|
||||
.toBeFalse();
|
||||
expect(chipSet.chips[0].getAttribute('tabindex'))
|
||||
.withContext('first tabindex')
|
||||
.toBe('0');
|
||||
expect(chipSet.chips[1].getAttribute('tabindex'))
|
||||
.withContext('second tabindex')
|
||||
.toBe('-1');
|
||||
@ -117,20 +113,6 @@ describe('Chip set', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should navigate forward on vertical arrow keys', async () => {
|
||||
const first = new TestAssistChip();
|
||||
const second = new TestAssistChip();
|
||||
const third = new TestAssistChip();
|
||||
const chipSet = await setupTest([first, second, third]);
|
||||
await testNavigation({
|
||||
chipSet,
|
||||
ltrKey: 'ArrowDown',
|
||||
rtlKey: 'ArrowUp',
|
||||
current: first,
|
||||
next: second
|
||||
});
|
||||
});
|
||||
|
||||
it('should navigate backward on horizontal keys', async () => {
|
||||
const first = new TestAssistChip();
|
||||
const second = new TestAssistChip();
|
||||
@ -145,20 +127,6 @@ describe('Chip set', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should navigate backward on vertical keys', async () => {
|
||||
const first = new TestAssistChip();
|
||||
const second = new TestAssistChip();
|
||||
const third = new TestAssistChip();
|
||||
const chipSet = await setupTest([first, second, third]);
|
||||
await testNavigation({
|
||||
chipSet,
|
||||
ltrKey: 'ArrowUp',
|
||||
rtlKey: 'ArrowDown',
|
||||
current: second,
|
||||
next: first
|
||||
});
|
||||
});
|
||||
|
||||
it('should navigate to the first chip on Home', async () => {
|
||||
const first = new TestAssistChip();
|
||||
const second = new TestAssistChip();
|
||||
@ -232,6 +200,22 @@ describe('Chip set', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT skip over disabled always focusable chips', async () => {
|
||||
const first = new TestAssistChip();
|
||||
const second = new TestAssistChip();
|
||||
second.disabled = true;
|
||||
second.alwaysFocusable = true;
|
||||
const third = new TestAssistChip();
|
||||
const chipSet = await setupTest([first, second, third]);
|
||||
await testNavigation({
|
||||
chipSet,
|
||||
ltrKey: 'ArrowRight',
|
||||
rtlKey: 'ArrowLeft',
|
||||
current: first,
|
||||
next: second
|
||||
});
|
||||
});
|
||||
|
||||
it('should focus trailing actions when navigating backwards', async () => {
|
||||
const first = new TestInputChip();
|
||||
const second = new TestInputChip();
|
||||
@ -277,93 +261,4 @@ describe('Chip set', () => {
|
||||
.toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('single selection', () => {
|
||||
it('should allow multi-selection if singleSelect is false', async () => {
|
||||
const first = new TestFilterChip();
|
||||
const second = new TestFilterChip();
|
||||
const chipSet = await setupTest([first, second]);
|
||||
chipSet.singleSelect = false;
|
||||
|
||||
await new ChipHarness(first).clickWithMouse();
|
||||
await new ChipHarness(second).clickWithMouse();
|
||||
|
||||
expect(first.selected).withContext('first chip is selected').toBeTrue();
|
||||
expect(second.selected).withContext('second chip is selected').toBeTrue();
|
||||
});
|
||||
|
||||
it('should deselect other chips when another chip is selected',
|
||||
async () => {
|
||||
const first = new TestFilterChip();
|
||||
const second = new TestFilterChip();
|
||||
second.selected = true;
|
||||
const chipSet = await setupTest([first, second]);
|
||||
chipSet.singleSelect = true;
|
||||
|
||||
await new ChipHarness(first).clickWithMouse();
|
||||
|
||||
expect(first.selected)
|
||||
.withContext('first chip is selected')
|
||||
.toBeTrue();
|
||||
expect(second.selected)
|
||||
.withContext('second chip is not selected')
|
||||
.toBeFalse();
|
||||
});
|
||||
|
||||
it('should not do anything if all chips are deselected and the property changes',
|
||||
async () => {
|
||||
const first = new TestFilterChip();
|
||||
const second = new TestFilterChip();
|
||||
|
||||
const chipSet = await setupTest([first, second]);
|
||||
chipSet.singleSelect = true;
|
||||
await env.waitForStability();
|
||||
|
||||
expect(first.selected)
|
||||
.withContext('first chip is still unselected')
|
||||
.toBeFalse();
|
||||
expect(second.selected)
|
||||
.withContext('second chip is still unselected')
|
||||
.toBeFalse();
|
||||
});
|
||||
|
||||
it('should ensure one chip is selected when property changes', async () => {
|
||||
const first = new TestFilterChip();
|
||||
first.selected = true;
|
||||
const second = new TestFilterChip();
|
||||
second.selected = true;
|
||||
|
||||
const chipSet = await setupTest([first, second]);
|
||||
chipSet.singleSelect = true;
|
||||
await env.waitForStability();
|
||||
|
||||
expect(first.selected).withContext('first chip is selected').toBeTrue();
|
||||
expect(second.selected)
|
||||
.withContext('second chip is deselected')
|
||||
.toBeFalse();
|
||||
});
|
||||
|
||||
it('should prefer setting the first selected chip as the single selected chip when property changes',
|
||||
async () => {
|
||||
const first = new TestFilterChip();
|
||||
const second = new TestFilterChip();
|
||||
second.selected = true;
|
||||
const third = new TestFilterChip();
|
||||
second.selected = true;
|
||||
|
||||
const chipSet = await setupTest([first, second, third]);
|
||||
chipSet.singleSelect = true;
|
||||
await env.waitForStability();
|
||||
|
||||
expect(first.selected)
|
||||
.withContext('first chip is still unselected')
|
||||
.toBeFalse();
|
||||
expect(second.selected)
|
||||
.withContext('second chip remains selected')
|
||||
.toBeTrue();
|
||||
expect(third.selected)
|
||||
.withContext('third chip is deselected')
|
||||
.toBeFalse();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
import '../../focus/md-focus-ring.js';
|
||||
import '../../ripple/ripple.js';
|
||||
|
||||
import {html, LitElement, nothing, TemplateResult} from 'lit';
|
||||
import {html, LitElement, PropertyValues, TemplateResult} from 'lit';
|
||||
import {property} from 'lit/decorators.js';
|
||||
import {ClassInfo, classMap} from 'lit/directives/class-map.js';
|
||||
|
||||
@ -27,7 +27,26 @@ export abstract class Chip extends LitElement {
|
||||
delegatesFocus: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether or not the chip is disabled.
|
||||
*
|
||||
* Disabled chips are not focusable, unless `always-focusable` is set.
|
||||
*/
|
||||
@property({type: Boolean}) disabled = false;
|
||||
|
||||
/**
|
||||
* When true, allow disabled chips to be focused with arrow keys.
|
||||
*
|
||||
* Add this when a chip needs increased visibility when disabled. See
|
||||
* https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_disabled_controls
|
||||
* for more guidance on when this is needed.
|
||||
*/
|
||||
@property({type: Boolean, attribute: 'always-focusable'})
|
||||
alwaysFocusable = false;
|
||||
|
||||
/**
|
||||
* The label of the chip.
|
||||
*/
|
||||
@property() label = '';
|
||||
|
||||
/**
|
||||
@ -43,15 +62,31 @@ export abstract class Chip extends LitElement {
|
||||
return this.disabled;
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
return this.renderContainer(this.renderContainerContent());
|
||||
override focus(options?: FocusOptions) {
|
||||
if (this.disabled && !this.alwaysFocusable) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.focus(options);
|
||||
}
|
||||
|
||||
protected abstract renderContainer(content: unknown): unknown;
|
||||
protected override render() {
|
||||
return html`
|
||||
<div class="container ${classMap(this.getContainerClasses())}">
|
||||
${this.renderContainerContent()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected getContainerClasses() {
|
||||
protected override updated(changed: PropertyValues<Chip>) {
|
||||
if (changed.has('disabled') && changed.get('disabled') !== undefined) {
|
||||
this.dispatchEvent(new Event('update-focus', {bubbles: true}));
|
||||
}
|
||||
}
|
||||
|
||||
protected getContainerClasses(): ClassInfo {
|
||||
return {
|
||||
disabled: this.disabled,
|
||||
'disabled': this.disabled,
|
||||
};
|
||||
}
|
||||
|
||||
@ -86,27 +121,3 @@ export abstract class Chip extends LitElement {
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a chip container that follows the grid/row/cell a11y pattern.
|
||||
*
|
||||
* This renders the container with `role="row"`.
|
||||
*/
|
||||
export function renderGridContainer(content: unknown, classes: ClassInfo) {
|
||||
return html`
|
||||
<div class="container ${classMap(classes)}" role="row">${content}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a chip action that follows the grid/row/cell a11y pattern.
|
||||
*
|
||||
* This wraps actions in a `role="cell"` div.
|
||||
*/
|
||||
export function renderGridAction(content: unknown) {
|
||||
if (content === nothing) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return html`<div class="cell" role="cell">${content}</div>`;
|
||||
}
|
||||
|
||||
46
chips/internal/chip_test.ts
Normal file
46
chips/internal/chip_test.ts
Normal file
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// import 'jasmine'; (google3-only)
|
||||
|
||||
import {html} from 'lit';
|
||||
import {customElement} from 'lit/decorators.js';
|
||||
|
||||
import {Environment} from '../../testing/environment.js';
|
||||
import {ChipHarness} from '../harness.js';
|
||||
|
||||
import {Chip} from './chip.js';
|
||||
|
||||
@customElement('test-chip')
|
||||
class TestChip extends Chip {
|
||||
primaryId = 'button';
|
||||
|
||||
override renderPrimaryAction() {
|
||||
return html`<button id=${this.primaryId}>Chip</button>`;
|
||||
}
|
||||
}
|
||||
|
||||
describe('Chip', () => {
|
||||
const env = new Environment();
|
||||
|
||||
async function setupTest() {
|
||||
const chip = new TestChip();
|
||||
env.render(html`${chip}`);
|
||||
await env.waitForStability();
|
||||
return {chip, harness: new ChipHarness(chip)};
|
||||
}
|
||||
|
||||
it('should dispatch `update-focus` for chip set when disabled changes',
|
||||
async () => {
|
||||
const {chip} = await setupTest();
|
||||
const updateFocusListener = jasmine.createSpy('updateFocusListener');
|
||||
chip.addEventListener('update-focus', updateFocusListener);
|
||||
|
||||
chip.disabled = true;
|
||||
await env.waitForStability();
|
||||
expect(updateFocusListener).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -6,9 +6,8 @@
|
||||
|
||||
import '../../elevation/elevation.js';
|
||||
|
||||
import {html, nothing, PropertyValues, svg} from 'lit';
|
||||
import {html, nothing} from 'lit';
|
||||
import {property, query} from 'lit/decorators.js';
|
||||
import {classMap} from 'lit/directives/class-map.js';
|
||||
|
||||
import {ARIAMixinStrict} from '../../internal/aria/aria.js';
|
||||
import {redispatchEvent} from '../../internal/controller/events.js';
|
||||
@ -25,28 +24,13 @@ export class FilterChip extends MultiActionChip {
|
||||
@property({type: Boolean, reflect: true}) selected = false;
|
||||
|
||||
protected get primaryId() {
|
||||
return 'option';
|
||||
return 'button';
|
||||
}
|
||||
|
||||
@query('.primary.action') protected readonly primaryAction!: HTMLElement|null;
|
||||
@query('.trailing.action')
|
||||
protected readonly trailingAction!: HTMLElement|null;
|
||||
|
||||
protected override updated(changed: PropertyValues<FilterChip>) {
|
||||
if (changed.has('selected') && changed.get('selected') !== undefined) {
|
||||
// Dispatch when `selected` changes, except for the first update.
|
||||
this.dispatchEvent(new Event('selected', {bubbles: true}));
|
||||
}
|
||||
}
|
||||
|
||||
protected override renderContainer(content: unknown) {
|
||||
const classes = this.getContainerClasses();
|
||||
// Note: an explicit `role="presentation"` is needed for VoiceOver. Without
|
||||
// it, linear navigation gets stuck on the first filter chip.
|
||||
return html`<div class="container ${
|
||||
classMap(classes)}" role="presentation">${content}</div>`;
|
||||
}
|
||||
|
||||
protected override getContainerClasses() {
|
||||
return {
|
||||
...super.getContainerClasses(),
|
||||
@ -60,11 +44,10 @@ export class FilterChip extends MultiActionChip {
|
||||
const {ariaLabel} = this as ARIAMixinStrict;
|
||||
return html`
|
||||
<button class="primary action"
|
||||
id="option"
|
||||
id="button"
|
||||
aria-label=${ariaLabel || nothing}
|
||||
aria-selected=${this.selected}
|
||||
?disabled=${this.disabled || nothing}
|
||||
role="option"
|
||||
aria-pressed=${this.selected}
|
||||
?disabled=${this.disabled && !this.alwaysFocusable}
|
||||
@click=${this.handleClick}
|
||||
>${content}</button>
|
||||
`;
|
||||
@ -75,17 +58,20 @@ export class FilterChip extends MultiActionChip {
|
||||
return super.renderLeadingIcon();
|
||||
}
|
||||
|
||||
return svg`
|
||||
return html`
|
||||
<svg class="checkmark" viewBox="0 0 18 18" aria-hidden="true">
|
||||
<path d="M6.75012 12.1274L3.62262 8.99988L2.55762 10.0574L6.75012 14.2499L15.7501 5.24988L14.6926 4.19238L6.75012 12.1274Z" />
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
protected override renderTrailingAction() {
|
||||
protected override renderTrailingAction(focusListener: EventListener) {
|
||||
if (this.removable) {
|
||||
return renderRemoveButton(
|
||||
{ariaLabel: this.ariaLabelRemove, disabled: this.disabled});
|
||||
return renderRemoveButton({
|
||||
focusListener,
|
||||
ariaLabel: this.ariaLabelRemove,
|
||||
disabled: this.disabled
|
||||
});
|
||||
}
|
||||
|
||||
return nothing;
|
||||
|
||||
@ -53,30 +53,6 @@ describe('Filter chip', () => {
|
||||
expect(chip.selected).withContext('chip.selected').toBeFalse();
|
||||
});
|
||||
|
||||
it('should dispatch "selected" event when selected changes programmatically',
|
||||
async () => {
|
||||
const {chip} = await setupTest();
|
||||
const handler = jasmine.createSpy();
|
||||
chip.addEventListener('selected', handler);
|
||||
|
||||
chip.selected = true;
|
||||
await env.waitForStability();
|
||||
chip.selected = false;
|
||||
await env.waitForStability();
|
||||
expect(handler).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should dispatch "selected" event when selected changes by click',
|
||||
async () => {
|
||||
const {chip, harness} = await setupTest();
|
||||
const handler = jasmine.createSpy();
|
||||
chip.addEventListener('selected', handler);
|
||||
|
||||
await harness.clickWithMouse();
|
||||
await harness.clickWithMouse();
|
||||
expect(handler).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('can prevent default', async () => {
|
||||
const {chip, harness} = await setupTest();
|
||||
const handler = jasmine.createSpy();
|
||||
|
||||
@ -9,7 +9,6 @@ import {property, query} from 'lit/decorators.js';
|
||||
|
||||
import {ARIAMixinStrict} from '../../internal/aria/aria.js';
|
||||
|
||||
import {renderGridAction, renderGridContainer} from './chip.js';
|
||||
import {MultiActionChip} from './multi-action-chip.js';
|
||||
import {renderRemoveButton} from './trailing-icons.js';
|
||||
|
||||
@ -53,10 +52,6 @@ export class InputChip extends MultiActionChip {
|
||||
@query('.trailing.action')
|
||||
protected readonly trailingAction!: HTMLElement|null;
|
||||
|
||||
protected override renderContainer(content: unknown) {
|
||||
return renderGridContainer(content, this.getContainerClasses());
|
||||
}
|
||||
|
||||
protected override getContainerClasses() {
|
||||
return {
|
||||
...super.getContainerClasses(),
|
||||
@ -72,39 +67,40 @@ export class InputChip extends MultiActionChip {
|
||||
protected override renderPrimaryAction(content: unknown) {
|
||||
const {ariaLabel} = this as ARIAMixinStrict;
|
||||
if (this.href) {
|
||||
return renderGridAction(html`
|
||||
return html`
|
||||
<a class="primary action"
|
||||
id="link"
|
||||
aria-label=${ariaLabel || nothing}
|
||||
href=${this.href}
|
||||
target=${this.target || nothing}
|
||||
>${content}</a>
|
||||
`);
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.removeOnly) {
|
||||
return renderGridAction(html`
|
||||
return html`
|
||||
<span class="primary action" aria-label=${ariaLabel || nothing}>
|
||||
${content}
|
||||
</span>
|
||||
`);
|
||||
`;
|
||||
}
|
||||
|
||||
return renderGridAction(html`
|
||||
return html`
|
||||
<button class="primary action"
|
||||
id="button"
|
||||
aria-label=${ariaLabel || nothing}
|
||||
?disabled=${this.disabled}
|
||||
?disabled=${this.disabled && !this.alwaysFocusable}
|
||||
type="button"
|
||||
>${content}</button>
|
||||
`);
|
||||
`;
|
||||
}
|
||||
|
||||
protected override renderTrailingAction() {
|
||||
return renderGridAction(renderRemoveButton({
|
||||
protected override renderTrailingAction(focusListener: EventListener) {
|
||||
return renderRemoveButton({
|
||||
focusListener,
|
||||
ariaLabel: this.ariaLabelRemove,
|
||||
disabled: !this.href && this.disabled,
|
||||
tabbable: this.removeOnly,
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,28 +44,31 @@ export abstract class MultiActionChip extends Chip {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.handleTrailingActionFocus = this.handleTrailingActionFocus.bind(this);
|
||||
if (!isServer) {
|
||||
this.addEventListener('keydown', this.handleKeyDown.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
override focus(options?: FocusOptions&{trailing?: boolean}) {
|
||||
if (options?.trailing && this.trailingAction) {
|
||||
const isFocusable = this.alwaysFocusable || !this.disabled;
|
||||
if (isFocusable && options?.trailing && this.trailingAction) {
|
||||
this.trailingAction.focus(options);
|
||||
return;
|
||||
}
|
||||
|
||||
super.focus(options);
|
||||
super.focus(options as FocusOptions);
|
||||
}
|
||||
|
||||
protected override renderContainerContent() {
|
||||
return html`
|
||||
${super.renderContainerContent()}
|
||||
${this.renderTrailingAction()}
|
||||
${this.renderTrailingAction(this.handleTrailingActionFocus)}
|
||||
`;
|
||||
}
|
||||
|
||||
protected abstract renderTrailingAction(): unknown;
|
||||
protected abstract renderTrailingAction(focusListener: EventListener):
|
||||
unknown;
|
||||
|
||||
private handleKeyDown(event: KeyboardEvent) {
|
||||
const isLeft = event.key === 'ArrowLeft';
|
||||
@ -98,4 +101,19 @@ export abstract class MultiActionChip extends Chip {
|
||||
const actionToFocus = forwards ? this.trailingAction : this.primaryAction;
|
||||
actionToFocus.focus();
|
||||
}
|
||||
|
||||
private handleTrailingActionFocus() {
|
||||
const {primaryAction, trailingAction} = this;
|
||||
if (!primaryAction || !trailingAction) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Temporarily turn off the primary action's focusability. This allows
|
||||
// shift+tab from the trailing action to move to the previous chip rather
|
||||
// than the primary action in the same chip.
|
||||
primaryAction.tabIndex = -1;
|
||||
trailingAction.addEventListener('focusout', () => {
|
||||
primaryAction.tabIndex = 0;
|
||||
}, {once: true});
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,21 +25,20 @@ class TestMultiActionChip extends MultiActionChip {
|
||||
|
||||
protected primaryId = 'primary';
|
||||
|
||||
protected override renderContainer(content: unknown) {
|
||||
return html`<div>${content}</div>`;
|
||||
}
|
||||
|
||||
protected override renderPrimaryAction() {
|
||||
return html`<button id="primary"></button>`;
|
||||
}
|
||||
|
||||
protected override renderTrailingAction() {
|
||||
protected override renderTrailingAction(focusListener: EventListener) {
|
||||
if (this.noTrailingAction) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return renderRemoveButton(
|
||||
{ariaLabel: this.ariaLabelRemove, disabled: this.disabled});
|
||||
return renderRemoveButton({
|
||||
focusListener,
|
||||
ariaLabel: this.ariaLabelRemove,
|
||||
disabled: this.disabled
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -14,22 +14,24 @@ import {Chip} from './chip.js';
|
||||
interface RemoveButtonProperties {
|
||||
ariaLabel: string;
|
||||
disabled: boolean;
|
||||
focusListener: EventListener;
|
||||
tabbable?: boolean;
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
export function renderRemoveButton(
|
||||
{ariaLabel, disabled, tabbable = false}: RemoveButtonProperties) {
|
||||
{ariaLabel, disabled, focusListener, tabbable = false}:
|
||||
RemoveButtonProperties) {
|
||||
return html`
|
||||
<button class="trailing action"
|
||||
aria-label=${ariaLabel}
|
||||
?disabled=${disabled}
|
||||
tabindex=${!tabbable ? -1 : nothing}
|
||||
@click=${handleRemoveClick}
|
||||
@focus=${focusListener}
|
||||
>
|
||||
<md-focus-ring part="trailing-focus-ring"></md-focus-ring>
|
||||
<md-ripple></md-ripple>
|
||||
<svg class="trailing icon" viewBox="0 96 960 960">
|
||||
<md-ripple ?disabled=${disabled}></md-ripple>
|
||||
<svg class="trailing icon" viewBox="0 96 960 960" aria-hidden="true">
|
||||
<path d="m249 849-42-42 231-231-231-231 42-42 231 231 231-231 42 42-231 231 231 231-42 42-231-231-231 231Z" />
|
||||
</svg>
|
||||
<span class="touch"></span>
|
||||
|
||||
@ -83,18 +83,21 @@ Choose the type of chip based on its purpose and author.
|
||||
<!-- TODO: catalog-include "figures/<component>/usage.html" -->
|
||||
|
||||
```html
|
||||
<md-assist-chip label="Assist"></md-assist-chip>
|
||||
<md-filter-chip label="Filter"></md-filter-chip>
|
||||
<md-input-chip label="Input"></md-input-chip>
|
||||
<md-suggestion-chip label="Suggestion"></md-suggestion-chip>
|
||||
<md-chip-set>
|
||||
<md-assist-chip label="Assist"></md-assist-chip>
|
||||
<md-filter-chip label="Filter"></md-filter-chip>
|
||||
<md-input-chip label="Input"></md-input-chip>
|
||||
<md-suggestion-chip label="Suggestion"></md-suggestion-chip>
|
||||
</md-chip-set>
|
||||
```
|
||||
|
||||
### Chip sets
|
||||
|
||||
<!-- go/md-chip-set -->
|
||||
|
||||
Chips should always appear in a set. Use the same type of chip for chip set
|
||||
children.
|
||||
Chips should always appear in a set. Chip sets are
|
||||
[toolbars](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/toolbar_role)<!-- {.external} -->
|
||||
that can display any type of chip or other toolbar items.
|
||||
|
||||
<!-- no-catalog-start -->
|
||||
<!-- TODO: add image -->
|
||||
@ -103,17 +106,11 @@ children.
|
||||
|
||||
```html
|
||||
<h3>New event</h3>
|
||||
<md-chip-set type="assist">
|
||||
<md-chip-set>
|
||||
<md-filter-chip label="All day"></md-filter-chip>
|
||||
<md-assist-chip label="Add to calendar"></md-assist-chip>
|
||||
<md-assist-chip label="Set a reminder"></md-assist-chip>
|
||||
</md-chip-set>
|
||||
|
||||
<h3>Favorite foods</h3>
|
||||
<md-chip-set type="filter" single-select>
|
||||
<md-filter-chip label="Pizza"></md-filter-chip>
|
||||
<md-filter-chip label="Ice cream"></md-filter-chip>
|
||||
<md-filter-chip label="Sandwich"></md-filter-chip>
|
||||
</md-chip-set>
|
||||
```
|
||||
|
||||
### Icons
|
||||
@ -128,13 +125,15 @@ picture is displayed.
|
||||
<!-- catalog-only-end -->
|
||||
|
||||
```html
|
||||
<md-assist-chip label="Add to calendar">
|
||||
<md-icon slot="icon">event</md-icon>
|
||||
</md-assist-chip>
|
||||
<md-chip-set>
|
||||
<md-assist-chip label="Add to calendar">
|
||||
<md-icon slot="icon">event</md-icon>
|
||||
</md-assist-chip>
|
||||
|
||||
<md-input-chip label="Ping Qiang" avatar>
|
||||
<img slot="icon" src="...">
|
||||
</md-input-chip>
|
||||
<md-input-chip label="Ping Qiang" avatar>
|
||||
<img slot="icon" src="...">
|
||||
</md-input-chip>
|
||||
</md-chip-set>
|
||||
```
|
||||
|
||||
### Elevated
|
||||
@ -151,7 +150,7 @@ protection, such as on top of an image.
|
||||
```html
|
||||
<div>
|
||||
<img src="...">
|
||||
<md-chip-set type="suggestion">
|
||||
<md-chip-set>
|
||||
<md-suggestion-chip label="Share" elevated></md-suggestion-chip>
|
||||
<md-suggestion-chip label="Favorite" elevated></md-suggestion-chip>
|
||||
</md-chip-set>
|
||||
@ -162,18 +161,41 @@ protection, such as on top of an image.
|
||||
|
||||
Add an
|
||||
[`aria-label`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-label)<!-- {.external} -->
|
||||
attribute to chip sets without labels or chips whose labels need to be more
|
||||
descriptive.
|
||||
attribute to chip sets or reference a label with
|
||||
[`aria-labelledby`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-labelledby)<!-- {.external} -->.
|
||||
Add an `aria-label` to chips whose labels need to be more descriptive.
|
||||
|
||||
```html
|
||||
<md-chip-set type="filter" aria-label="Select dates">
|
||||
<h3 id="dates-label">Dates</h3>
|
||||
<md-chip-set aria-labelledby="dates-label">
|
||||
<md-filter-chip label="Mon" aria-label="Monday"></md-filter-chip>
|
||||
<md-filter-chip label="Tue" aria-label="Tuesday"></md-filter-chip>
|
||||
<md-filter-chip label="Wed" aria-label="Wednesday"></md-filter-chip>
|
||||
<!-- ... -->
|
||||
</md-chip-set>
|
||||
```
|
||||
|
||||
### Focusable and disabled
|
||||
|
||||
By default, disabled chips are not focusable with the keyboard. Some use cases
|
||||
encourage focusability of disabled toolbar items to increase their
|
||||
discoverability.
|
||||
|
||||
See the
|
||||
[ARIA guidelines on focusability of disabled controls](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_disabled_controls)<!-- {.external} -->
|
||||
for guidance on when this is recommended.
|
||||
|
||||
```html
|
||||
<md-chip-set aria-label="Actions">
|
||||
<!--
|
||||
Disabled until text is selected. Since both are disabled by default, keep
|
||||
them focusable so that screen readers can discover the actions available.
|
||||
-->
|
||||
<md-assist-chip label="Copy" disabled always-focusable></md-assist-chip>
|
||||
<md-assist-chip label="Paste" disabled always-focusable></md-assist-chip>
|
||||
</md-chip-set>
|
||||
<md-outlined-text-field type="textarea"></md-outlined-text-field>
|
||||
```
|
||||
|
||||
## Assist chip
|
||||
|
||||
<!-- go/md-assist-chip -->
|
||||
@ -193,7 +215,7 @@ action. They should appear dynamically and contextually in a UI.
|
||||
|
||||
```html
|
||||
<h3>A restaraunt location</h3>
|
||||
<md-chip-set type="assist">
|
||||
<md-chip-set>
|
||||
<md-assist-chip label="Add to my itinerary">
|
||||
<md-icon slot="icon">calendar</md-icon>
|
||||
</md-assist-chip>
|
||||
@ -219,7 +241,7 @@ to toggle buttons or checkboxes.
|
||||
|
||||
```html
|
||||
<h3>Choose where to share</h3>
|
||||
<md-chip-set type="filter">
|
||||
<md-chip-set>
|
||||
<md-filter-chip label="Docs"></md-filter-chip>
|
||||
<md-filter-chip label="Slides" selected></md-filter-chip>
|
||||
<md-filter-chip label="Sheets" selected></md-filter-chip>
|
||||
@ -227,26 +249,6 @@ to toggle buttons or checkboxes.
|
||||
</md-chip-set>
|
||||
```
|
||||
|
||||
### Single select
|
||||
|
||||
Filter chip sets can add a `single-select` attribute to only allow a single
|
||||
filter chip to be selected at one time.
|
||||
|
||||
<!-- no-catalog-start -->
|
||||
<!-- TODO: add image -->
|
||||
<!-- no-catalog-end -->
|
||||
<!-- TODO: catalog-include "figures/<component>/usage-scenario-one.html" -->
|
||||
<!-- catalog-only-end -->
|
||||
|
||||
```html
|
||||
<h3>Shopping category</h3>
|
||||
<md-chip-set type="filter" single-select>
|
||||
<md-filter-chip label="Cameras" selected></md-filter-chip>
|
||||
<md-filter-chip label="Laptops"></md-filter-chip>
|
||||
<md-filter-chip label="Phones"></md-filter-chip>
|
||||
</md-chip-set>
|
||||
```
|
||||
|
||||
### Removable
|
||||
|
||||
Filter chips can optionally be removable from the chip set. Removable chips have
|
||||
@ -260,7 +262,7 @@ a trailing remove icon.
|
||||
|
||||
```html
|
||||
<h3>Colors</h3>
|
||||
<md-chip-set type="filter">
|
||||
<md-chip-set>
|
||||
<md-filter-chip label="Red" removable selected></md-filter-chip>
|
||||
<md-filter-chip label="Yellow" removable></md-filter-chip>
|
||||
<md-filter-chip label="Blue" removable></md-filter-chip>
|
||||
@ -288,7 +290,7 @@ display the image in a larger circle.
|
||||
```html
|
||||
<md-outlined-text-field label="Attendees" type="email"></md-outlined-text-field>
|
||||
|
||||
<md-chip-set type="input">
|
||||
<md-chip-set>
|
||||
<md-input-chip label="Ping Qiang" avatar>
|
||||
<img slot="icon" src="...">
|
||||
</md-input-chip>
|
||||
@ -311,7 +313,7 @@ associated with clicking on it, it may be marked as `remove-only`.
|
||||
|
||||
```html
|
||||
<h3>Favorite movies</h3>
|
||||
<md-chip-set type="input">
|
||||
<md-chip-set>
|
||||
<md-input-chip label="Star Wars" remove-only></md-input-chip>
|
||||
<md-input-chip label="Star Trek" remove-only></md-input-chip>
|
||||
</md-chip-set>
|
||||
@ -333,7 +335,7 @@ such as possible responses or search filters.
|
||||
|
||||
```html
|
||||
<h3>Suggested reply</h3>
|
||||
<md-chip-set type="suggestion">
|
||||
<md-chip-set>
|
||||
<md-suggestion-chip label="I agree"></md-suggestion-chip>
|
||||
<md-suggestion-chip label="Looks good to me"></md-suggestion-chip>
|
||||
<md-suggestion-chip label="Thank you"></md-suggestion-chip>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user