Skip to content

Commit

Permalink
feat(chips): Add keyboard navigation (#4844)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
patrickrodee committed Jul 1, 2019
1 parent c5738ed commit 42065fe
Show file tree
Hide file tree
Showing 20 changed files with 1,454 additions and 263 deletions.
120 changes: 86 additions & 34 deletions packages/mdc-chips/README.md
Expand Up @@ -39,11 +39,20 @@ npm install @material/chips

### HTML Structure

>**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.
```html
<div class="mdc-chip-set">
<button class="mdc-chip">
<span class="mdc-chip__text">Chip content</span>
</button>
<div class="mdc-chip-set" role="grid">
<div class="mdc-chip" role="row">
<span role="gridcell">
<span role="button" tabindex="0" class="mdc-chip__text">Chip One</span>
</span>
</div>
<div class="mdc-chip" role="row">
<span role="gridcell">
<span role="button" tabindex="-1" class="mdc-chip__text">Chip Two</span>
</span>
</div>
...
</div>
```
Expand Down Expand Up @@ -83,29 +92,35 @@ However, you can also use SVG, [Font Awesome](https://fontawesome.com/), or any
#### Leading icon

```html
<button class="mdc-chip">
<div class="mdc-chip" role="row">
<i class="material-icons mdc-chip__icon mdc-chip__icon--leading">event</i>
<span class="mdc-chip__text">Add to calendar</span>
</button>
<span role="gridcell">
<span role="button" tabindex="0" class="mdc-chip__text">Add to calendar</span>
</span>
</div>
```

#### Trailing icon

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).

```html
<button class="mdc-chip">
<span class="mdc-chip__text">Jane Smith</span>
<i class="material-icons mdc-chip__icon mdc-chip__icon--trailing" tabindex="0" role="button">cancel</i>
</button>
<div class="mdc-chip" role="row">
<span role="gridcell">
<span role="button" tabindex="0" class="mdc-chip__text">Jane Smith</span>
</span>
<span role="gridcell">
<i class="material-icons mdc-chip__icon mdc-chip__icon--trailing" tabindex="-1" role="button">cancel</i>
</span>
</div>
```

### Choice Chips

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.

```html
<div class="mdc-chip-set mdc-chip-set--choice">
<div class="mdc-chip-set mdc-chip-set--choice" role="grid">
...
</div>
```
Expand All @@ -115,15 +130,17 @@ Choice chips are a variant of chips which allow single selection from a set of o
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:

```html
<div class="mdc-chip-set mdc-chip-set--filter">
<button class="mdc-chip">
<div class="mdc-chip-set mdc-chip-set--filter" role="grid">
<div class="mdc-chip" role="row">
<span class="mdc-chip__checkmark" >
<svg class="mdc-chip__checkmark-svg" viewBox="-2 -3 30 30">
<path class="mdc-chip__checkmark-path" fill="none" stroke="black"
d="M1.73,12.91 8.1,19.28 22.79,4.59"/>
</svg>
</span>
<span class="mdc-chip__text">Filterable content</span>
<span role="gridcell">
<span role="checkbox" tabindex="0" aria-checked="false" class="mdc-chip__text">Filterable content</span>
</span>
</button>
...
</div>
Expand All @@ -132,17 +149,19 @@ Filter chips are a variant of chips which allow multiple selection from a set of
To use a leading icon in a filter chip, put the `mdc-chip__icon--leading` element _before_ the `mdc-chip__checkmark` element:

```html
<div class="mdc-chip-set mdc-chip-set--filter">
<button class="mdc-chip">
<div class="mdc-chip-set mdc-chip-set--filter" role="grid">
<div class="mdc-chip" role="row">
<i class="material-icons mdc-chip__icon mdc-chip__icon--leading">face</i>
<span class="mdc-chip__checkmark" >
<svg class="mdc-chip__checkmark-svg" viewBox="-2 -3 30 30">
<path class="mdc-chip__checkmark-path" fill="none" stroke="black"
d="M1.73,12.91 8.1,19.28 22.79,4.59"/>
</svg>
</span>
<span class="mdc-chip__text">Filterable content</span>
</button>
<span role="gridcell">
<span role="checkbox" tabindex="0" aria-checked="false" class="mdc-chip__text">Filterable content</span>
</span>
</div>
...
</div>
```
Expand All @@ -152,7 +171,7 @@ To use a leading icon in a filter chip, put the `mdc-chip__icon--leading` elemen
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.

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

```html
<button class="mdc-chip mdc-chip--selected">
<span class="mdc-chip__text">Add to calendar</span>
</button>
<div class="mdc-chip-set mdc-chip-set--choice" role="grid">
<div class="mdc-chip mdc-chip--selected" role="row">
<span role="gridcell">
<span role="radio" tabindex="0" aria-checked="true" class="mdc-chip__text">Add to calendar</span>
</span>
</div>
</div>
```

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.

```html
<button class="mdc-chip mdc-chip--selected">
<i class="material-icons mdc-chip__icon mdc-chip__icon--leading mdc-chip__icon--leading-hidden">face</i>
<span class="mdc-chip__checkmark">
<svg class="mdc-chip__checkmark-svg" viewBox="-2 -3 30 30">
<path class="mdc-chip__checkmark-path" fill="none" stroke="black"
d="M1.73,12.91 8.1,19.28 22.79,4.59"/>
</svg>
</span>
<span class="mdc-chip__text">Filterable content</span>
</button>
<div class="mdc-chip-set mdc-chip-set--filter" role="grid">
<div class="mdc-chip mdc-chip--selected" role="row">
<i class="material-icons mdc-chip__icon mdc-chip__icon--leading mdc-chip__icon--leading-hidden">face</i>
<span class="mdc-chip__checkmark">
<svg class="mdc-chip__checkmark-svg" viewBox="-2 -3 30 30">
<path class="mdc-chip__checkmark-path" fill="none" stroke="black"
d="M1.73,12.91 8.1,19.28 22.79,4.59"/>
</svg>
</span>
<span role="gridcell">
<span role="checkbox" tabindex="0" aria-checked="true" class="mdc-chip__text">Filterable content</span>
</span>
</div>
</div>
```

## Style Customization
Expand All @@ -237,6 +264,10 @@ CSS Class | Description

> _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.
`mdc-chip__action--primary` | Mandatory. Placed on the `mdc-chip__text` element.
`mdc-chip__action--trailing` | Optinoal. Placed on the `mdc-chip__icon--trailing` when it should be accessible via keyboard navigation.
`mdc-chip--deletable` | Optional. Indicates that the chip should be removable by the delete or backspace key.

### Sass Mixins

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

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

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


> \*_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).
Expand All @@ -352,6 +393,12 @@ Method Signature | Description
`hasClass(className: string) => boolean` | Returns whether the chip set element has the given class
`removeChip(chipId: string) => void` | Removes the chip with the given id from the chip set
`setSelected(chipId: string, selected: boolean) => void` | Sets the selected state of the chip with the given id
`getIndexOfChipById(id: string) => number` | Returns the index of the chip with the matching `id` or -1
`focusChipPrimaryActionAtIndex(index: number) => void` | Calls `MDCChip#focusPrimaryAction()` on the chip at the given `index`
`focusChipTrailingActionAtIndex(index: number) => void` | Calls `MDCChip#focusTrailingAction()` on the chip at the given `index`
`isRTL() => boolean` | Returns `true` if the text direction is RTL
`getChipListCount() => number` | Returns the number of chips inside the chip set
`removeFocusFromChipAtIndex(index: number) => void` | Calls `MDCChip#removeFocus()` on the chip at the given `index`

### Foundations: `MDCChipFoundation` and `MDCChipSetFoundation`

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

#### `MDCChipFoundation` Event Handlers

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

#### `MDCChipSetFoundation`

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

#### `MDCChipSetFoundation` Event Handlers

Expand All @@ -398,3 +449,4 @@ Events | Element Selector | Foundation Handler
`MDCChip:interaction` | `.mdc-chip-set` (root) | `handleChipInteraction`
`MDCChip:selection` | `.mdc-chip-set` (root) | `handleChipSelection`
`MDCChip:removal` | `.mdc-chip-set` (root) | `handleChipRemoval`
`MDCChip:navigation` | `.mdc-chip-set` (root) | `handleChipNavigation`
13 changes: 9 additions & 4 deletions packages/mdc-chips/_mixins.scss
Expand Up @@ -132,6 +132,12 @@
}
}

.mdc-chip__text:focus {
@include mdc-feature-targets($feat-structure) {
outline: none;
}
}

.mdc-chip--selected .mdc-chip__checkmark-path {
@include mdc-feature-targets($feat-structure) {
stroke-dashoffset: 0;
Expand Down Expand Up @@ -345,7 +351,7 @@
}

@mixin mdc-chip-ink-color-ripple_($color, $query) {
@include mdc-states($color, $query: $query);
@include mdc-states($color, true, $query: $query);
}

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

@mixin mdc-chip-selected-ink-color-ripple_($color, $query) {
&.mdc-chip {
@include mdc-states-selected($color, $query: $query);
@include mdc-states-selected($color, $has-nested-focusable-element: true, $query: $query);
}
}

Expand Down Expand Up @@ -538,8 +544,7 @@

.mdc-chip__icon--trailing {
@include mdc-feature-targets($feat-structure) {
margin-right: $right-margin;
margin-left: $left-margin;
@include mdc-rtl-reflexive-property(margin, $left-margin, $right-margin);
}
}
}
Expand Down
26 changes: 26 additions & 0 deletions packages/mdc-chips/chip-set/adapter.ts
Expand Up @@ -43,4 +43,30 @@ export interface MDCChipSetAdapter {
* Sets the selected state of the chip with the given id.
*/
setSelected(chipId: string, selected: boolean): void;

/**
* @param chipId the unique ID of the chip
* @return the numerical index of the chip with the matching id or -1.
*/
getIndexOfChipById(chipId: string): number;

focusChipPrimaryActionAtIndex(index: number): void;

focusChipTrailingActionAtIndex(index: number): void;

/**
* Removes focus from the chip at the given index.
* @param index the index of the chip
*/
removeFocusFromChipAtIndex(index: number): void;

/**
* @return true if the text direction is RTL.
*/
isRTL(): boolean;

/**
* @return the number of chips in the chip set.
*/
getChipListCount(): number;
}

0 comments on commit 42065fe

Please sign in to comment.