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

Commit

Permalink
feat(chips): Announce when chips are removed
Browse files Browse the repository at this point in the history
- Update MDCChipAdapter to include a `getAttribute` method
- Update MDCChipRemovalEventDetail to include a nullable `removedAnnouncement` property
- Update MDCChipsetAdapter to include a `announceMessage` method

BREAKING CHANGE: Both `MDCChipAdapter` and `MDCChipSetAdapter` have new methods. `MDCChipSetFoundation` event handlers now accept the corresponding chip event detail interface as the sole argument. The `root` property has been removed from the `MDCChipRemovalEventDetail` interface.
  • Loading branch information
patrickrodee authored Jan 31, 2020
1 parent ed9e4ac commit b3f70eb
Show file tree
Hide file tree
Showing 13 changed files with 205 additions and 92 deletions.
12 changes: 7 additions & 5 deletions packages/mdc-chips/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ Event Name | `event.detail` | Description
--- | --- | ---
`MDCChip:interaction` | `{chipId: string}` | Indicates the chip was interacted with (via click/tap or Enter key)
`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:removal` | `{chipId: string, removedAnnouncement: string|null}` | 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

Expand Down Expand Up @@ -410,6 +410,7 @@ Method Signature | Description
`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
`getAttribute(attr: string) => string|null` | Returns the string value of the attribute if it exists, otherwise `null`


> \*_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 @@ -431,6 +432,7 @@ Method Signature | Description
`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`
`announceMessage(message: string) => void` | Announces the message via [an `aria-live` region](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions)

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

Expand Down Expand Up @@ -468,10 +470,10 @@ Method Signature | Description
--- | ---
`getSelectedChipIds() => ReadonlyArray<string>` | Returns an array of the IDs of all selected chips
`select(chipId: string) => void` | Selects the chip with the given id
`handleChipInteraction(chipId: string) => void` | Handles a custom `MDCChip:interaction` event on the root element
`handleChipSelection(chipId: string, selected: boolean, chipSetShouldIgnore: boolean) => void` | Handles a custom `MDCChip:selection` event on the root element. When `chipSetShouldIgnore` is true, the chip set does not process the event.
`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
`handleChipInteraction(detail: MDCChipInteractionEventDetail) => void` | Handles a custom `MDCChip:interaction` event on the root element
`handleChipSelection(detail: MDCChipSelectionEventDetail) => void` | Handles a custom `MDCChip:selection` event on the root element. When `chipSetShouldIgnore` is true, the chip set does not process the event.
`handleChipRemoval(detail: MDCChipRemovalEventDetail) => void` | Handles a custom `MDCChip:removal` event on the root element
`handleChipNavigation(detail: MDCChipNavigationEventDetail) => void` | Handles a custom `MDCChip:navigation` event on the root element

#### `MDCChipSetFoundation` Event Handlers

Expand Down
5 changes: 5 additions & 0 deletions packages/mdc-chips/chip-set/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,9 @@ export interface MDCChipSetAdapter {
* @return the number of chips in the chip set.
*/
getChipListCount(): number;

/**
* Announces the message via an aria-live region.
*/
announceMessage(message: string): void;
}
23 changes: 15 additions & 8 deletions packages/mdc-chips/chip-set/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
*/

import {MDCComponent} from '@material/base/component';
import {announce} from '@material/dom/announce';
import {MDCChip, MDCChipFactory} from '../chip/component';
import {MDCChipFoundation} from '../chip/foundation';
import {MDCChipInteractionEvent, MDCChipNavigationEvent, MDCChipRemovalEvent,
Expand Down Expand Up @@ -72,13 +73,14 @@ export class MDCChipSet extends MDCComponent<MDCChipSetFoundation> {
}
});

this.handleChipInteraction_ = (evt) => this.foundation_.handleChipInteraction(evt.detail.chipId);
this.handleChipSelection_ = (evt) => {
this.foundation_.handleChipSelection(evt.detail.chipId, evt.detail.selected, evt.detail.shouldIgnore);
};
this.handleChipRemoval_ = (evt) => this.foundation_.handleChipRemoval(evt.detail.chipId);
this.handleChipNavigation_ = (evt) => this.foundation_.handleChipNavigation(
evt.detail.chipId, evt.detail.key, evt.detail.source);
this.handleChipInteraction_ = (evt) =>
this.foundation_.handleChipInteraction(evt.detail);
this.handleChipSelection_ = (evt) =>
this.foundation_.handleChipSelection(evt.detail);
this.handleChipRemoval_ = (evt) =>
this.foundation_.handleChipRemoval(evt.detail);
this.handleChipNavigation_ = (evt) =>
this.foundation_.handleChipNavigation(evt.detail);
this.listen(INTERACTION_EVENT, this.handleChipInteraction_);
this.listen(SELECTION_EVENT, this.handleChipSelection_);
this.listen(REMOVAL_EVENT, this.handleChipRemoval_);
Expand Down Expand Up @@ -110,6 +112,9 @@ export class MDCChipSet extends MDCComponent<MDCChipSetFoundation> {
// DO NOT INLINE this variable. For backward compatibility, foundations take a Partial<MDCFooAdapter>.
// To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable.
const adapter: MDCChipSetAdapter = {
announceMessage: (message) => {
announce(message);
},
focusChipPrimaryActionAtIndex: (index) => {
this.chips_[index].focusPrimaryAction();
},
Expand All @@ -121,7 +126,9 @@ export class MDCChipSet extends MDCComponent<MDCChipSetFoundation> {
return this.findChipIndex_(chipId);
},
hasClass: (className) => this.root_.classList.contains(className),
isRTL: () => window.getComputedStyle(this.root_).getPropertyValue('direction') === 'rtl',
isRTL: () =>
window.getComputedStyle(this.root_).getPropertyValue('direction') ===
'rtl',
removeChipAtIndex: (index) => {
if (index >= 0 && index < this.chips_.length) {
this.chips_[index].destroy();
Expand Down
17 changes: 13 additions & 4 deletions packages/mdc-chips/chip-set/foundation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
*/

import {MDCFoundation} from '@material/base/foundation';

import {Direction, EventSource, jumpChipKeys, navigationKeys, strings as chipStrings} from '../chip/constants';
import {MDCChipInteractionEventDetail, MDCChipNavigationEventDetail, MDCChipRemovalEventDetail, MDCChipSelectionEventDetail} from '../chip/types';

import {MDCChipSetAdapter} from './adapter';
import {cssClasses, strings} from './constants';

Expand All @@ -37,6 +40,7 @@ export class MDCChipSetFoundation extends MDCFoundation<MDCChipSetAdapter> {

static get defaultAdapter(): MDCChipSetAdapter {
return {
announceMessage: () => undefined,
focusChipPrimaryActionAtIndex: () => undefined,
focusChipTrailingActionAtIndex: () => undefined,
getChipListCount: () => -1,
Expand Down Expand Up @@ -76,7 +80,7 @@ export class MDCChipSetFoundation extends MDCFoundation<MDCChipSetAdapter> {
/**
* Handles a chip interaction event
*/
handleChipInteraction(chipId: string) {
handleChipInteraction({chipId}: MDCChipInteractionEventDetail) {
const index = this.adapter_.getIndexOfChipById(chipId);
this.removeFocusFromChipsExcept_(index);
if (this.adapter_.hasClass(cssClasses.CHOICE) || this.adapter_.hasClass(cssClasses.FILTER)) {
Expand All @@ -87,7 +91,8 @@ export class MDCChipSetFoundation extends MDCFoundation<MDCChipSetAdapter> {
/**
* Handles a chip selection event, used to handle discrepancy when selection state is set directly on the Chip.
*/
handleChipSelection(chipId: string, selected: boolean, shouldIgnore: boolean) {
handleChipSelection({chipId, selected, shouldIgnore}:
MDCChipSelectionEventDetail) {
// Early exit if we should ignore the event
if (shouldIgnore) {
return;
Expand All @@ -104,7 +109,11 @@ export class MDCChipSetFoundation extends MDCFoundation<MDCChipSetAdapter> {
/**
* Handles the event when a chip is removed.
*/
handleChipRemoval(chipId: string) {
handleChipRemoval({chipId, removedAnnouncement}: MDCChipRemovalEventDetail) {
if (removedAnnouncement) {
this.adapter_.announceMessage(removedAnnouncement);
}

const index = this.adapter_.getIndexOfChipById(chipId);
this.deselectAndNotifyClients_(chipId);
this.adapter_.removeChipAtIndex(index);
Expand All @@ -118,7 +127,7 @@ export class MDCChipSetFoundation extends MDCFoundation<MDCChipSetAdapter> {
/**
* Handles a chip navigation event.
*/
handleChipNavigation(chipId: string, key: string, source: EventSource) {
handleChipNavigation({chipId, key, source}: MDCChipNavigationEventDetail) {
const maxIndex = this.adapter_.getChipListCount() - 1;
let index = this.adapter_.getIndexOfChipById(chipId);
// Early exit if the index is out of range or the key is unusable
Expand Down
81 changes: 56 additions & 25 deletions packages/mdc-chips/chip-set/test/component.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,36 +119,67 @@ describe('MDCChipSet', () => {
REMOVAL_EVENT,
SELECTION_EVENT
} = MDCChipFoundation.strings;
const evtData = {

emitEvent(root, INTERACTION_EVENT, {
bubbles: true,
cancelable: true,
detail: {
chipId: 'chipA',
},
});

expect(mockFoundation.handleChipInteraction).toHaveBeenCalledWith({
chipId: 'chipA'
});
expect(mockFoundation.handleChipInteraction).toHaveBeenCalledTimes(1);

emitEvent(root, SELECTION_EVENT, {
bubbles: true,
cancelable: true,
detail: {
chipId: 'chipA',
selected: true,
shouldIgnore: false,
},
});

expect(mockFoundation.handleChipSelection).toHaveBeenCalledWith({
chipId: 'chipA',
selected: true,
key: ARROW_LEFT_KEY,
source: 1,
shouldIgnore: false,
};
const evt1 = document.createEvent('CustomEvent');
const evt2 = document.createEvent('CustomEvent');
const evt3 = document.createEvent('CustomEvent');
const evt4 = document.createEvent('CustomEvent');
evt1.initCustomEvent(INTERACTION_EVENT, true, true, evtData);
evt2.initCustomEvent(REMOVAL_EVENT, true, true, evtData);
evt3.initCustomEvent(SELECTION_EVENT, true, true, evtData);
evt4.initCustomEvent(NAVIGATION_EVENT, true, true, evtData);

root.dispatchEvent(evt1);
root.dispatchEvent(evt2);
root.dispatchEvent(evt3);
root.dispatchEvent(evt4);

expect(mockFoundation.handleChipInteraction).toHaveBeenCalledWith('chipA');
expect(mockFoundation.handleChipInteraction).toHaveBeenCalledTimes(1);
expect(mockFoundation.handleChipSelection)
.toHaveBeenCalledWith('chipA', true, false);
});
expect(mockFoundation.handleChipSelection).toHaveBeenCalledTimes(1);
expect(mockFoundation.handleChipRemoval).toHaveBeenCalledWith('chipA');

emitEvent(root, REMOVAL_EVENT, {
bubbles: true,
cancelable: true,
detail: {
chipId: 'chipA',
removedAnnouncement: 'Removed foo',
},
});

expect(mockFoundation.handleChipRemoval).toHaveBeenCalledWith({
chipId: 'chipA',
removedAnnouncement: 'Removed foo'
});
expect(mockFoundation.handleChipRemoval).toHaveBeenCalledTimes(1);
expect(mockFoundation.handleChipNavigation)
.toHaveBeenCalledWith('chipA', ARROW_LEFT_KEY, 1);

emitEvent(root, NAVIGATION_EVENT, {
bubbles: true,
cancelable: true,
detail: {
chipId: 'chipA',
key: ARROW_LEFT_KEY,
source: 1,
},
});

expect(mockFoundation.handleChipNavigation).toHaveBeenCalledWith({
chipId: 'chipA',
key: ARROW_LEFT_KEY,
source: 1,
});
expect(mockFoundation.handleChipNavigation).toHaveBeenCalledTimes(1);
});

Expand Down
Loading

0 comments on commit b3f70eb

Please sign in to comment.