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

Commit 42065fe

Browse files
authored
feat(chips): Add keyboard navigation (#4844)
Add support for keyboard navigation for chips. Update ripple focus-within mixin behavior. Demos: action chips, choice chips, filter chips, and input chips. Fixes #2259 BREAKING CHANGE: Chips markup, adapters, foundations, and events have changed.
1 parent c5738ed commit 42065fe

File tree

20 files changed

+1454
-263
lines changed

20 files changed

+1454
-263
lines changed

packages/mdc-chips/README.md

Lines changed: 86 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,20 @@ npm install @material/chips
3939

4040
### HTML Structure
4141

42+
>**Note**: Due to IE11 and Edge's lack of support for the `:focus-within` selector, keyboard navigation of the chip set will not be visually obvious.
43+
4244
```html
43-
<div class="mdc-chip-set">
44-
<button class="mdc-chip">
45-
<span class="mdc-chip__text">Chip content</span>
46-
</button>
45+
<div class="mdc-chip-set" role="grid">
46+
<div class="mdc-chip" role="row">
47+
<span role="gridcell">
48+
<span role="button" tabindex="0" class="mdc-chip__text">Chip One</span>
49+
</span>
50+
</div>
51+
<div class="mdc-chip" role="row">
52+
<span role="gridcell">
53+
<span role="button" tabindex="-1" class="mdc-chip__text">Chip Two</span>
54+
</span>
55+
</div>
4756
...
4857
</div>
4958
```
@@ -83,29 +92,35 @@ However, you can also use SVG, [Font Awesome](https://fontawesome.com/), or any
8392
#### Leading icon
8493

8594
```html
86-
<button class="mdc-chip">
95+
<div class="mdc-chip" role="row">
8796
<i class="material-icons mdc-chip__icon mdc-chip__icon--leading">event</i>
88-
<span class="mdc-chip__text">Add to calendar</span>
89-
</button>
97+
<span role="gridcell">
98+
<span role="button" tabindex="0" class="mdc-chip__text">Add to calendar</span>
99+
</span>
100+
</div>
90101
```
91102

92103
#### Trailing icon
93104

94105
A trailing icon comes with the functionality to remove the chip from the set. If you're adding a trailing icon, also set `tabindex="0"` and `role="button"` to make it accessible by keyboard and screenreader. Trailing icons should only be added to [input chips](#input-chips).
95106

96107
```html
97-
<button class="mdc-chip">
98-
<span class="mdc-chip__text">Jane Smith</span>
99-
<i class="material-icons mdc-chip__icon mdc-chip__icon--trailing" tabindex="0" role="button">cancel</i>
100-
</button>
108+
<div class="mdc-chip" role="row">
109+
<span role="gridcell">
110+
<span role="button" tabindex="0" class="mdc-chip__text">Jane Smith</span>
111+
</span>
112+
<span role="gridcell">
113+
<i class="material-icons mdc-chip__icon mdc-chip__icon--trailing" tabindex="-1" role="button">cancel</i>
114+
</span>
115+
</div>
101116
```
102117

103118
### Choice Chips
104119

105120
Choice chips are a variant of chips which allow single selection from a set of options. To define a set of chips as choice chips, add the class `mdc-chip-set--choice` to the chip set element.
106121

107122
```html
108-
<div class="mdc-chip-set mdc-chip-set--choice">
123+
<div class="mdc-chip-set mdc-chip-set--choice" role="grid">
109124
...
110125
</div>
111126
```
@@ -115,15 +130,17 @@ Choice chips are a variant of chips which allow single selection from a set of o
115130
Filter chips are a variant of chips which allow multiple selection from a set of options. To define a set of chips as filter chips, add the class `mdc-chip-set--filter` to the chip set element. When a filter chip is selected, a checkmark appears as the leading icon. If the chip already has a leading icon, the checkmark replaces it. This requires the HTML structure of a filter chip to differ from other chips:
116131

117132
```html
118-
<div class="mdc-chip-set mdc-chip-set--filter">
119-
<button class="mdc-chip">
133+
<div class="mdc-chip-set mdc-chip-set--filter" role="grid">
134+
<div class="mdc-chip" role="row">
120135
<span class="mdc-chip__checkmark" >
121136
<svg class="mdc-chip__checkmark-svg" viewBox="-2 -3 30 30">
122137
<path class="mdc-chip__checkmark-path" fill="none" stroke="black"
123138
d="M1.73,12.91 8.1,19.28 22.79,4.59"/>
124139
</svg>
125140
</span>
126-
<span class="mdc-chip__text">Filterable content</span>
141+
<span role="gridcell">
142+
<span role="checkbox" tabindex="0" aria-checked="false" class="mdc-chip__text">Filterable content</span>
143+
</span>
127144
</button>
128145
...
129146
</div>
@@ -132,17 +149,19 @@ Filter chips are a variant of chips which allow multiple selection from a set of
132149
To use a leading icon in a filter chip, put the `mdc-chip__icon--leading` element _before_ the `mdc-chip__checkmark` element:
133150

134151
```html
135-
<div class="mdc-chip-set mdc-chip-set--filter">
136-
<button class="mdc-chip">
152+
<div class="mdc-chip-set mdc-chip-set--filter" role="grid">
153+
<div class="mdc-chip" role="row">
137154
<i class="material-icons mdc-chip__icon mdc-chip__icon--leading">face</i>
138155
<span class="mdc-chip__checkmark" >
139156
<svg class="mdc-chip__checkmark-svg" viewBox="-2 -3 30 30">
140157
<path class="mdc-chip__checkmark-path" fill="none" stroke="black"
141158
d="M1.73,12.91 8.1,19.28 22.79,4.59"/>
142159
</svg>
143160
</span>
144-
<span class="mdc-chip__text">Filterable content</span>
145-
</button>
161+
<span role="gridcell">
162+
<span role="checkbox" tabindex="0" aria-checked="false" class="mdc-chip__text">Filterable content</span>
163+
</span>
164+
</div>
146165
...
147166
</div>
148167
```
@@ -152,7 +171,7 @@ To use a leading icon in a filter chip, put the `mdc-chip__icon--leading` elemen
152171
Input chips are a variant of chips which enable user input by converting text into chips. To define a set of chips as input chips, add the class `mdc-chip-set--input` to the chip set element.
153172

154173
```html
155-
<div class="mdc-chip-set mdc-chip-set--input">
174+
<div class="mdc-chip-set mdc-chip-set--input" role="grid">
156175
...
157176
</div>
158177
```
@@ -194,24 +213,32 @@ chipSet.listen('MDCChip:removal', function(event) {
194213
To display a pre-selected filter or choice chip, add the class `mdc-chip--selected` to the root chip element.
195214

196215
```html
197-
<button class="mdc-chip mdc-chip--selected">
198-
<span class="mdc-chip__text">Add to calendar</span>
199-
</button>
216+
<div class="mdc-chip-set mdc-chip-set--choice" role="grid">
217+
<div class="mdc-chip mdc-chip--selected" role="row">
218+
<span role="gridcell">
219+
<span role="radio" tabindex="0" aria-checked="true" class="mdc-chip__text">Add to calendar</span>
220+
</span>
221+
</div>
222+
</div>
200223
```
201224

202225
To pre-select filter chips that have a leading icon, also add the class `mdc-chip__icon--leading-hidden` to the `mdc-chip__icon--leading` element. This will ensure that the checkmark displaces the leading icon.
203226

204227
```html
205-
<button class="mdc-chip mdc-chip--selected">
206-
<i class="material-icons mdc-chip__icon mdc-chip__icon--leading mdc-chip__icon--leading-hidden">face</i>
207-
<span class="mdc-chip__checkmark">
208-
<svg class="mdc-chip__checkmark-svg" viewBox="-2 -3 30 30">
209-
<path class="mdc-chip__checkmark-path" fill="none" stroke="black"
210-
d="M1.73,12.91 8.1,19.28 22.79,4.59"/>
211-
</svg>
212-
</span>
213-
<span class="mdc-chip__text">Filterable content</span>
214-
</button>
228+
<div class="mdc-chip-set mdc-chip-set--filter" role="grid">
229+
<div class="mdc-chip mdc-chip--selected" role="row">
230+
<i class="material-icons mdc-chip__icon mdc-chip__icon--leading mdc-chip__icon--leading-hidden">face</i>
231+
<span class="mdc-chip__checkmark">
232+
<svg class="mdc-chip__checkmark-svg" viewBox="-2 -3 30 30">
233+
<path class="mdc-chip__checkmark-path" fill="none" stroke="black"
234+
d="M1.73,12.91 8.1,19.28 22.79,4.59"/>
235+
</svg>
236+
</span>
237+
<span role="gridcell">
238+
<span role="checkbox" tabindex="0" aria-checked="true" class="mdc-chip__text">Filterable content</span>
239+
</span>
240+
</div>
241+
</div>
215242
```
216243

217244
## Style Customization
@@ -237,6 +264,10 @@ CSS Class | Description
237264

238265
> _NOTE_: Every element that has an `mdc-chip__icon` class must also have either the `mdc-chip__icon--leading` or `mdc-chip__icon--trailing` class.
239266
267+
`mdc-chip__action--primary` | Mandatory. Placed on the `mdc-chip__text` element.
268+
`mdc-chip__action--trailing` | Optinoal. Placed on the `mdc-chip__icon--trailing` when it should be accessible via keyboard navigation.
269+
`mdc-chip--deletable` | Optional. Indicates that the chip should be removable by the delete or backspace key.
270+
240271
### Sass Mixins
241272

242273
Mixin | Description
@@ -276,6 +307,9 @@ To use the `MDCChip` and `MDCChipSet` classes, [import](../../docs/importing-js.
276307
Method Signature | Description
277308
--- | ---
278309
`beginExit() => void` | Proxies to the foundation's `beginExit` method
310+
`focusPrimaryAction() => void` | Proxies to the foundation's `focusPrimaryAction` method
311+
`focusTrailingAction() => void` | Proxies to the foundation's `focusTrailingAction` method
312+
`removeFocus() => void` | Proxies to the foundation's `removeFocus` method
279313

280314
Property | Value Type | Description
281315
--- | --- | ---
@@ -296,6 +330,7 @@ Event Name | `event.detail` | Description
296330
`MDCChip:selection` | `{chipId: string, selected: boolean}` | Indicates the chip's selection state has changed (for choice/filter chips)
297331
`MDCChip:removal` | `{chipId: string, root: Element}` | Indicates the chip is ready to be removed from the DOM
298332
`MDCChip:trailingIconInteraction` | `{chipId: string}` | Indicates the chip's trailing icon was interacted with (via click/tap or Enter key)
333+
`MDCChip:navigation` | `{chipId: string, key: string, source: FocusSource}` | Indicates a navigation event has occurred on a chip
299334

300335
> _NOTE_: All of `MDCChip`'s emitted events bubble up through the DOM.
301336
@@ -337,7 +372,13 @@ Method Signature | Description
337372
`hasLeadingIcon() => boolean` | Returns whether the chip has a leading icon
338373
`getRootBoundingClientRect() => ClientRect` | Returns the bounding client rect of the root element
339374
`getCheckmarkBoundingClientRect() => ClientRect \| null` | Returns the bounding client rect of the checkmark element or null if it doesn't exist
340-
`setAttr(attr: string, value: string) => void` | Sets the value of the attribute on the root element.
375+
`notifyNavigation(key: string, source: EventSource) => void` | Notifies the Chip Set that a navigation event has occurred
376+
`setPrimaryActionAttr(attr: string, value: string) => void` | Sets an attribute on the primary action element to the given value
377+
`focusPrimaryAction() => void` | Gives focus to the primary action element
378+
`hasTrailingAction() => boolean` | Returns `true` if the chip has a trailing action element
379+
`setTrailingActionAttr(attr: string, value: string) => void` | Sets an attribute on the trailing action element to the given value, if the element exists
380+
`focusTrailingAction() => void` | Gives focus to the trailing action element if present
381+
341382

342383
> \*_NOTE_: `notifyInteraction` and `notifyTrailingIconInteraction` must pass along the target chip's ID, and must be observable by the parent `mdc-chip-set` element (e.g. via DOM event bubbling).
343384
@@ -352,6 +393,12 @@ Method Signature | Description
352393
`hasClass(className: string) => boolean` | Returns whether the chip set element has the given class
353394
`removeChip(chipId: string) => void` | Removes the chip with the given id from the chip set
354395
`setSelected(chipId: string, selected: boolean) => void` | Sets the selected state of the chip with the given id
396+
`getIndexOfChipById(id: string) => number` | Returns the index of the chip with the matching `id` or -1
397+
`focusChipPrimaryActionAtIndex(index: number) => void` | Calls `MDCChip#focusPrimaryAction()` on the chip at the given `index`
398+
`focusChipTrailingActionAtIndex(index: number) => void` | Calls `MDCChip#focusTrailingAction()` on the chip at the given `index`
399+
`isRTL() => boolean` | Returns `true` if the text direction is RTL
400+
`getChipListCount() => number` | Returns the number of chips inside the chip set
401+
`removeFocusFromChipAtIndex(index: number) => void` | Calls `MDCChip#removeFocus()` on the chip at the given `index`
355402

356403
### Foundations: `MDCChipFoundation` and `MDCChipSetFoundation`
357404

@@ -368,6 +415,8 @@ Method Signature | Description
368415
`handleInteraction(evt: Event) => void` | Handles an interaction event on the root element
369416
`handleTransitionEnd(evt: Event) => void` | Handles a transition end event on the root element
370417
`handleTrailingIconInteraction(evt: Event) => void` | Handles an interaction event on the trailing icon element
418+
`handleKeydown(evt: Event) => void` | Handles a keydown event on the root element
419+
`removeFocus() => void` | Removes focusability from the chip
371420

372421
#### `MDCChipFoundation` Event Handlers
373422

@@ -378,6 +427,7 @@ Events | Element Selector | Foundation Handler
378427
`click`, `keydown` | `.mdc-chip` (root) | `handleInteraction()`
379428
`click`, `keydown` | `.mdc-chip__icon--trailing` (if present) | `handleTrailingIconInteraction()`
380429
`transitionend` | `.mdc-chip` (root) | `handleTransitionEnd()`
430+
`keydown` | `.mdc-chip` (root) | `handleKeydown()`
381431

382432
#### `MDCChipSetFoundation`
383433

@@ -388,6 +438,7 @@ Method Signature | Description
388438
`handleChipInteraction(chipId: string) => void` | Handles a custom `MDCChip:interaction` event on the root element
389439
`handleChipSelection(chipId: string, selected: boolean) => void` | Handles a custom `MDCChip:selection` event on the root element
390440
`handleChipRemoval(chipId: string) => void` | Handles a custom `MDCChip:removal` event on the root element
441+
`handleChipNavigation(chipId: string, key: string) => void` | Handles a custom `MDCChip:navigation` event on the root element
391442

392443
#### `MDCChipSetFoundation` Event Handlers
393444

@@ -398,3 +449,4 @@ Events | Element Selector | Foundation Handler
398449
`MDCChip:interaction` | `.mdc-chip-set` (root) | `handleChipInteraction`
399450
`MDCChip:selection` | `.mdc-chip-set` (root) | `handleChipSelection`
400451
`MDCChip:removal` | `.mdc-chip-set` (root) | `handleChipRemoval`
452+
`MDCChip:navigation` | `.mdc-chip-set` (root) | `handleChipNavigation`

packages/mdc-chips/_mixins.scss

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,12 @@
132132
}
133133
}
134134

135+
.mdc-chip__text:focus {
136+
@include mdc-feature-targets($feat-structure) {
137+
outline: none;
138+
}
139+
}
140+
135141
.mdc-chip--selected .mdc-chip__checkmark-path {
136142
@include mdc-feature-targets($feat-structure) {
137143
stroke-dashoffset: 0;
@@ -345,7 +351,7 @@
345351
}
346352

347353
@mixin mdc-chip-ink-color-ripple_($color, $query) {
348-
@include mdc-states($color, $query: $query);
354+
@include mdc-states($color, true, $query: $query);
349355
}
350356

351357
@mixin mdc-chip-selected-ink-color($color, $query: mdc-feature-all()) {
@@ -379,7 +385,7 @@
379385

380386
@mixin mdc-chip-selected-ink-color-ripple_($color, $query) {
381387
&.mdc-chip {
382-
@include mdc-states-selected($color, $query: $query);
388+
@include mdc-states-selected($color, $has-nested-focusable-element: true, $query: $query);
383389
}
384390
}
385391

@@ -538,8 +544,7 @@
538544

539545
.mdc-chip__icon--trailing {
540546
@include mdc-feature-targets($feat-structure) {
541-
margin-right: $right-margin;
542-
margin-left: $left-margin;
547+
@include mdc-rtl-reflexive-property(margin, $left-margin, $right-margin);
543548
}
544549
}
545550
}

packages/mdc-chips/chip-set/adapter.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,30 @@ export interface MDCChipSetAdapter {
4343
* Sets the selected state of the chip with the given id.
4444
*/
4545
setSelected(chipId: string, selected: boolean): void;
46+
47+
/**
48+
* @param chipId the unique ID of the chip
49+
* @return the numerical index of the chip with the matching id or -1.
50+
*/
51+
getIndexOfChipById(chipId: string): number;
52+
53+
focusChipPrimaryActionAtIndex(index: number): void;
54+
55+
focusChipTrailingActionAtIndex(index: number): void;
56+
57+
/**
58+
* Removes focus from the chip at the given index.
59+
* @param index the index of the chip
60+
*/
61+
removeFocusFromChipAtIndex(index: number): void;
62+
63+
/**
64+
* @return true if the text direction is RTL.
65+
*/
66+
isRTL(): boolean;
67+
68+
/**
69+
* @return the number of chips in the chip set.
70+
*/
71+
getChipListCount(): number;
4672
}

0 commit comments

Comments
 (0)