Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

revert: feat(chips): Consolidate interaction event handlers #5301

Merged
merged 1 commit into from
Dec 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/mdc-chips/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -445,9 +445,10 @@ Method Signature | Description
`setShouldRemoveOnTrailingIconClick(shouldRemove: boolean) => void` | Sets whether a trailing icon click should trigger exit/removal of the chip
`getDimensions() => ClientRect` | Returns the dimensions of the chip. This is used for applying ripple to the chip.
`beginExit() => void` | Begins the exit animation which leads to removal of the chip
`handleClick(evt: Event) => void` | Handles a click event on the root element
`handleKeydown(evt: Event) => void` | Handles a keydown event on the root element
`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 @@ -456,9 +457,10 @@ When wrapping the Chip foundation, the following events must be bound to the ind

Events | Element Selector | Foundation Handler
--- | --- | ---
`click` | `.mdc-chip` (root) | `handleClick()`
`keydown` | `.mdc-chip` (root) | `handleKeydown()`
`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 Down
34 changes: 30 additions & 4 deletions packages/mdc-chips/chip/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ import {MDCChipFoundation} from './foundation';
import {MDCChipInteractionEventDetail, MDCChipNavigationEventDetail, MDCChipRemovalEventDetail,
MDCChipSelectionEventDetail} from './types';

type InteractionType = 'click' | 'keydown';

const INTERACTION_EVENTS: InteractionType[] = ['click', 'keydown'];

export type MDCChipFactory = (el: Element, foundation?: MDCChipFoundation) => MDCChip;

export class MDCChip extends MDCComponent<MDCChipFoundation> implements MDCRippleCapableSurface {
Expand Down Expand Up @@ -80,17 +84,20 @@ export class MDCChip extends MDCComponent<MDCChipFoundation> implements MDCRippl
root_!: HTMLElement; // assigned in MDCComponent constructor

private leadingIcon_!: Element | null; // assigned in initialize()
private trailingIcon_!: Element | null; // assigned in initialize()
private checkmark_!: Element | null; // assigned in initialize()
private ripple_!: MDCRipple; // assigned in initialize()
private primaryAction_!: Element | null; // assigned in initialize()
private trailingAction_!: Element | null; // assigned in initialize()

private handleClick_!: SpecificEventListener<'click'>; // assigned in initialSyncWithDOM()
private handleInteraction_!: SpecificEventListener<InteractionType>; // assigned in initialSyncWithDOM()
private handleTransitionEnd_!: SpecificEventListener<'transitionend'>; // assigned in initialSyncWithDOM()
private handleTrailingIconInteraction_!: SpecificEventListener<InteractionType>; // assigned in initialSyncWithDOM()
private handleKeydown_!: SpecificEventListener<'keydown'>; // assigned in initialSyncWithDOM()

initialize(rippleFactory: MDCRippleFactory = (el, foundation) => new MDCRipple(el, foundation)) {
this.leadingIcon_ = this.root_.querySelector(strings.LEADING_ICON_SELECTOR);
this.trailingIcon_ = this.root_.querySelector(strings.TRAILING_ICON_SELECTOR);
this.checkmark_ = this.root_.querySelector(strings.CHECKMARK_SELECTOR);
this.primaryAction_ = this.root_.querySelector(strings.PRIMARY_ACTION_SELECTOR);
this.trailingAction_ = this.root_.querySelector(strings.TRAILING_ACTION_SELECTOR);
Expand All @@ -105,21 +112,40 @@ export class MDCChip extends MDCComponent<MDCChipFoundation> implements MDCRippl
}

initialSyncWithDOM() {
this.handleClick_ = (evt: MouseEvent) => this.foundation_.handleClick(evt);
this.handleInteraction_ = (evt: MouseEvent | KeyboardEvent) => this.foundation_.handleInteraction(evt);
this.handleTransitionEnd_ = (evt: TransitionEvent) => this.foundation_.handleTransitionEnd(evt);
this.handleTrailingIconInteraction_ = (evt: MouseEvent | KeyboardEvent) =>
this.foundation_.handleTrailingIconInteraction(evt);
this.handleKeydown_ = (evt: KeyboardEvent) => this.foundation_.handleKeydown(evt);

this.listen('click', this.handleClick_);
INTERACTION_EVENTS.forEach((evtType) => {
this.listen(evtType, this.handleInteraction_);
});
this.listen('transitionend', this.handleTransitionEnd_);
this.listen('keydown', this.handleKeydown_);

if (this.trailingIcon_) {
INTERACTION_EVENTS.forEach((evtType) => {
this.trailingIcon_!.addEventListener(evtType, this.handleTrailingIconInteraction_ as EventListener);
});
}
}

destroy() {
this.ripple_.destroy();
this.unlisten('click', this.handleClick_);

INTERACTION_EVENTS.forEach((evtType) => {
this.unlisten(evtType, this.handleInteraction_);
});
this.unlisten('transitionend', this.handleTransitionEnd_);
this.unlisten('keydown', this.handleKeydown_);

if (this.trailingIcon_) {
INTERACTION_EVENTS.forEach((evtType) => {
this.trailingIcon_!.removeEventListener(evtType, this.handleTrailingIconInteraction_ as EventListener);
});
}

super.destroy();
}

Expand Down
52 changes: 24 additions & 28 deletions packages/mdc-chips/chip/foundation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,11 @@ export class MDCChipFoundation extends MDCFoundation<MDCChipAdapter> {
/**
* Handles an interaction event on the root element.
*/
handleClick(evt: MouseEvent) {
const trailingIconIsSource = this.adapter_.eventTargetHasClass(evt.target, cssClasses.TRAILING_ICON);
if (trailingIconIsSource) {
return this.notifyTrailingIconInteractionAndRemove_(evt);
handleInteraction(evt: MouseEvent | KeyboardEvent) {
if (this.shouldHandleInteraction_(evt)) {
this.adapter_.notifyInteraction();
this.focusPrimaryAction_();
}

this.notifyInteractionAndFocus_();
}

/**
Expand Down Expand Up @@ -205,24 +203,27 @@ export class MDCChipFoundation extends MDCFoundation<MDCChipAdapter> {
}

/**
* Handles a keydown event from the root element.
* Handles an interaction event on the trailing icon element. This is used to
* prevent the ripple from activating on interaction with the trailing icon.
*/
handleKeydown(evt: KeyboardEvent) {
const trailingIconIsSource = this.adapter_.eventTargetHasClass(evt.target, cssClasses.TRAILING_ICON);
if (trailingIconIsSource && this.shouldProcessKeydownAsClick_(evt)) {
return this.notifyTrailingIconInteractionAndRemove_(evt);
}

if (this.shouldProcessKeydownAsClick_(evt)) {
return this.notifyInteractionAndFocus_();
handleTrailingIconInteraction(evt: MouseEvent | KeyboardEvent) {
if (this.shouldHandleInteraction_(evt)) {
this.adapter_.notifyTrailingIconInteraction();
this.removeChip_(evt);
}
}

/**
* Handles a keydown event from the root element.
*/
handleKeydown(evt: KeyboardEvent) {
if (this.shouldRemoveChip_(evt)) {
return this.removeChip_(evt);
}

const key = evt.key;
// Early exit if the key is not usable
if (!navigationKeys.has(evt.key)) {
if (!navigationKeys.has(key)) {
return;
}

Expand Down Expand Up @@ -307,20 +308,20 @@ export class MDCChipFoundation extends MDCFoundation<MDCChipAdapter> {
this.adapter_.setPrimaryActionAttr(strings.TAB_INDEX, '-1');
}

private removeChip_(evt: Event) {
private removeChip_(evt: MouseEvent|KeyboardEvent) {
evt.stopPropagation();
if (this.shouldRemoveOnTrailingIconClick_) {
this.beginExit();
}
}

private notifyTrailingIconInteractionAndRemove_(evt: Event) {
this.adapter_.notifyTrailingIconInteraction();
this.removeChip_(evt);
}
private shouldHandleInteraction_(evt: MouseEvent|KeyboardEvent): boolean {
if (evt.type === 'click') {
return true;
}

private shouldProcessKeydownAsClick_(evt: KeyboardEvent): boolean {
return evt.key === strings.ENTER_KEY || evt.key === strings.SPACEBAR_KEY;
const keyEvt = evt as KeyboardEvent;
return keyEvt.key === strings.ENTER_KEY || keyEvt.key === strings.SPACEBAR_KEY;
}

private shouldRemoveChip_(evt: KeyboardEvent): boolean {
Expand All @@ -345,11 +346,6 @@ export class MDCChipFoundation extends MDCFoundation<MDCChipAdapter> {
private notifyIgnoredSelection_(selected: boolean) {
this.adapter_.notifySelection(selected, true);
}

private notifyInteractionAndFocus_() {
this.adapter_.notifyInteraction();
this.focusPrimaryAction_();
}
}

// tslint:disable-next-line:no-default-export Needed for backward compatibility with MDC Web v0.44.0 and earlier.
Expand Down
110 changes: 26 additions & 84 deletions test/unit/mdc-chips/mdc-chip.foundation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,19 +149,21 @@ test(`#beginExit adds ${cssClasses.CHIP_EXIT} class`, () => {
td.verify(mockAdapter.addClass(cssClasses.CHIP_EXIT));
});

test('#handleKeydown does not emit event on invalid key', () => {
test('#handleInteraction does not emit event on invalid key', () => {
const {foundation, mockAdapter} = setupTest();
const mockKeydown = {
const mockEvt = {
type: 'keydown',
key: 'Shift',
};

foundation.handleKeydown(mockKeydown);
foundation.handleInteraction(mockEvt);
td.verify(mockAdapter.notifyInteraction(), {times: 0});
});

const validEvents = [
{
type: 'click',
}, {
type: 'keydown',
key: 'Enter',
}, {
Expand All @@ -171,39 +173,23 @@ const validEvents = [
];

validEvents.forEach((evt) => {
test(`#handleKeydown(${evt}) notifies interaction`, () => {
test(`#handleInteraction(${evt}) notifies interaction`, () => {
const {foundation, mockAdapter} = setupTest();

foundation.handleKeydown(evt);
foundation.handleInteraction(evt);
td.verify(mockAdapter.notifyInteraction());
});

test(`#handleKeydown(${evt}) focuses the primary action`, () => {
test(`#handleInteraction(${evt}) focuses the primary action`, () => {
const {foundation, mockAdapter} = setupTest();

foundation.handleKeydown(evt);
foundation.handleInteraction(evt);
td.verify(mockAdapter.setPrimaryActionAttr(strings.TAB_INDEX, '0'));
td.verify(mockAdapter.setTrailingActionAttr(strings.TAB_INDEX, '-1'));
td.verify(mockAdapter.focusPrimaryAction());
});
});

test('#handleClick(evt) notifies interaction', () => {
const {foundation, mockAdapter} = setupTest();

foundation.handleClick({type: 'click'});
td.verify(mockAdapter.notifyInteraction());
});

test('#handleClick(evt) focuses the primary action', () => {
const {foundation, mockAdapter} = setupTest();

foundation.handleClick({type: 'click'});
td.verify(mockAdapter.setPrimaryActionAttr(strings.TAB_INDEX, '0'));
td.verify(mockAdapter.setTrailingActionAttr(strings.TAB_INDEX, '-1'));
td.verify(mockAdapter.focusPrimaryAction());
});

test('#handleTransitionEnd notifies removal of chip on width transition end', () => {
const {foundation, mockAdapter} = setupTest();
const mockEvt = {
Expand Down Expand Up @@ -318,106 +304,62 @@ test('#handleTransitionEnd does nothing for width property when not exiting', ()
td.verify(mockAdapter.removeClassFromLeadingIcon(cssClasses.HIDDEN_LEADING_ICON), {times: 0});
});

test('#handleKeydown emits no custom event on invalid keys', () => {
test('#handleTrailingIconInteraction emits no event on invalid keys', () => {
const {foundation, mockAdapter} = setupTest();
const mockEvt = {
type: 'keydown',
type: 'keydowb',
key: 'Shift',
stopPropagation: td.func('stopPropagation'),
target: {},
};

td.when(mockAdapter.eventTargetHasClass(mockEvt.target, cssClasses.TRAILING_ICON)).thenReturn(true);

foundation.handleKeydown(mockEvt);
foundation.handleTrailingIconInteraction(mockEvt);
td.verify(mockAdapter.notifyTrailingIconInteraction(), {times: 0});
});

const validKeys = [
' ', // Space
'Enter',
];

validKeys.forEach((key) => {
test(`#handleKeydown() from trailing icon emits custom event on "${key}"`, () => {
const {foundation, mockAdapter} = setupTest();
const mockEvt = {
type: 'keydown',
stopPropagation: td.func('stopPropagation'),
target: {},
key,
};

td.when(mockAdapter.eventTargetHasClass(mockEvt.target, cssClasses.TRAILING_ICON)).thenReturn(true);

foundation.handleKeydown(mockEvt);
td.verify(mockAdapter.notifyTrailingIconInteraction(), {times: 1});
});
});

test('#handleClick() from trailing icon emits custom event', () => {
test('#handleTrailingIconInteraction emits custom event on click or enter key in trailing icon', () => {
const {foundation, mockAdapter} = setupTest();
const mockEvt = {
type: 'click',
stopPropagation: td.func('stopPropagation'),
target: {},
};

td.when(mockAdapter.eventTargetHasClass(mockEvt.target, cssClasses.TRAILING_ICON)).thenReturn(true);

foundation.handleClick(mockEvt);
foundation.handleTrailingIconInteraction(mockEvt);
td.verify(mockAdapter.notifyTrailingIconInteraction(), {times: 1});
td.verify(mockEvt.stopPropagation(), {times: 1});

foundation.handleTrailingIconInteraction(Object.assign(mockEvt, {type: 'keydown', key: ' '}));
td.verify(mockAdapter.notifyTrailingIconInteraction(), {times: 2});
td.verify(mockEvt.stopPropagation(), {times: 2});

foundation.handleTrailingIconInteraction(Object.assign(mockEvt, {type: 'keydown', key: 'Enter'}));
td.verify(mockAdapter.notifyTrailingIconInteraction(), {times: 3});
td.verify(mockEvt.stopPropagation(), {times: 3});
});

test(`#handleClick() from trailing icon adds ${cssClasses.CHIP_EXIT} class by default`, () => {
test(`#handleTrailingIconInteraction adds ${cssClasses.CHIP_EXIT} class by default on click in trailing icon`, () => {
const {foundation, mockAdapter} = setupTest();
const mockEvt = {
type: 'click',
stopPropagation: td.func('stopPropagation'),
target: {},
};

td.when(mockAdapter.eventTargetHasClass(mockEvt.target, cssClasses.TRAILING_ICON)).thenReturn(true);
foundation.handleTrailingIconInteraction(mockEvt);

foundation.handleClick(mockEvt);
assert.isTrue(foundation.getShouldRemoveOnTrailingIconClick());
td.verify(mockAdapter.addClass(cssClasses.CHIP_EXIT));
td.verify(mockEvt.stopPropagation());
});

validKeys.forEach((key) => {
test(`#handleKeydown({key: "${key}"}) from trailing icon adds ${cssClasses.CHIP_EXIT} class by default`, () => {
const {foundation, mockAdapter} = setupTest();
const mockEvt = {
type: 'keydown',
stopPropagation: td.func('stopPropagation'),
target: {},
key,
};

td.when(mockAdapter.eventTargetHasClass(mockEvt.target, cssClasses.TRAILING_ICON)).thenReturn(true);

foundation.handleKeydown(mockEvt);
assert.isTrue(foundation.getShouldRemoveOnTrailingIconClick());
td.verify(mockAdapter.addClass(cssClasses.CHIP_EXIT));
td.verify(mockEvt.stopPropagation());
});
});

test(`#handleClick() from trailing icon does not add ${cssClasses.CHIP_EXIT} class to trailing icon ` +
test(`#handleTrailingIconInteraction does not add ${cssClasses.CHIP_EXIT} class on click in trailing icon ` +
'if shouldRemoveOnTrailingIconClick_ is false', () => {
const {foundation, mockAdapter} = setupTest();
const mockEvt = {
type: 'click',
stopPropagation: td.func('stopPropagation'),
target: {},
};

td.when(mockAdapter.eventTargetHasClass(mockEvt.target, cssClasses.TRAILING_ICON)).thenReturn(true);

foundation.setShouldRemoveOnTrailingIconClick(false);
foundation.handleClick(mockEvt);
foundation.handleTrailingIconInteraction(mockEvt);

assert.isFalse(foundation.getShouldRemoveOnTrailingIconClick());
td.verify(mockAdapter.addClass(cssClasses.CHIP_EXIT), {times: 0});
Expand Down
Loading