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

Commit e849937

Browse files
authored
fix(chips): Emit custom event from trailing icon (#2286)
Also don't trigger a ripple when interacting with the trailing icon BREAKING CHANGE: New MDCChipAdapter methods for handling trailing icons must be implemented.
1 parent 1d131b4 commit e849937

File tree

7 files changed

+138
-10
lines changed

7 files changed

+138
-10
lines changed

packages/mdc-chips/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,10 @@ Method Signature | Description
144144
`hasClass(className: string) => boolean` | Returns true if the root element contains the given class
145145
`registerInteractionHandler(evtType: string, handler: EventListener) => void` | Registers an event listener on the root element
146146
`deregisterInteractionHandler(evtType: string, handler: EventListener) => void` | Deregisters an event listener on the root element
147-
`notifyInteraction() => void` | Emits a custom event "MDCChip:interaction" denoting the chip has been interacted with, which bubbles to the parent `mdc-chip-set` element
147+
`registerTrailingIconInteractionHandler(evtType: string, handler: EventListener) => void` | Registers an event listener on the trailing icon element
148+
`deregisterTrailingIconInteractionHandler(evtType: string, handler: EventListener) => void` | Deregisters an event listener on the trailing icon element
149+
`notifyInteraction() => void` | Emits a custom event `MDCChip:interaction` denoting the chip has been interacted with, which bubbles to the parent `mdc-chip-set` element
150+
`notifyTrailingIconInteraction() => void` | Emits a custom event `MDCChip:trailingIconInteraction` denoting the chip's trailing icon has been interacted with, which bubbles to the parent `mdc-chip-set` element
148151

149152
#### `MDCChipSetAdapter`
150153

packages/mdc-chips/chip/adapter.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,31 @@ class MDCChipAdapter {
6161
*/
6262
deregisterInteractionHandler(evtType, handler) {}
6363

64+
/**
65+
* Registers an event listener on the trailing icon element for a given event.
66+
* @param {string} evtType
67+
* @param {function(!Event): undefined} handler
68+
*/
69+
registerTrailingIconInteractionHandler(evtType, handler) {}
70+
71+
/**
72+
* Deregisters an event listener on the trailing icon element for a given event.
73+
* @param {string} evtType
74+
* @param {function(!Event): undefined} handler
75+
*/
76+
deregisterTrailingIconInteractionHandler(evtType, handler) {}
77+
6478
/**
6579
* Emits a custom "MDCChip:interaction" event denoting the chip has been
6680
* interacted with (typically on click or keydown).
6781
*/
6882
notifyInteraction() {}
83+
84+
/**
85+
* Emits a custom "MDCChip:trailingIconInteraction" event denoting the trailing icon has been
86+
* interacted with (typically on click or keydown).
87+
*/
88+
notifyTrailingIconInteraction() {}
6989
}
7090

7191
export default MDCChipAdapter;

packages/mdc-chips/chip/constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
/** @enum {string} */
1919
const strings = {
2020
INTERACTION_EVENT: 'MDCChip:interaction',
21+
TRAILING_ICON_INTERACTION_EVENT: 'MDCChip:trailingIconInteraction',
22+
TRAILING_ICON_SELECTOR: '.mdc-chip__icon--trailing',
2123
};
2224

2325
/** @enum {string} */

packages/mdc-chips/chip/foundation.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ class MDCChipFoundation extends MDCFoundation {
4747
hasClass: () => {},
4848
registerInteractionHandler: () => {},
4949
deregisterInteractionHandler: () => {},
50+
registerTrailingIconInteractionHandler: () => {},
51+
deregisterTrailingIconInteractionHandler: () => {},
5052
notifyInteraction: () => {},
53+
notifyTrailingIconInteraction: () => {},
5154
});
5255
}
5356

@@ -59,17 +62,27 @@ class MDCChipFoundation extends MDCFoundation {
5962

6063
/** @private {function(!Event): undefined} */
6164
this.interactionHandler_ = (evt) => this.handleInteraction_(evt);
65+
/** @private {function(!Event): undefined} */
66+
this.trailingIconInteractionHandler_ = (evt) => this.handleTrailingIconInteraction_(evt);
6267
}
6368

6469
init() {
6570
['click', 'keydown'].forEach((evtType) => {
6671
this.adapter_.registerInteractionHandler(evtType, this.interactionHandler_);
72+
this.adapter_.registerTrailingIconInteractionHandler(evtType, this.trailingIconInteractionHandler_);
73+
});
74+
['touchstart', 'pointerdown', 'mousedown'].forEach((evtType) => {
75+
this.adapter_.registerTrailingIconInteractionHandler(evtType, this.trailingIconInteractionHandler_);
6776
});
6877
}
6978

7079
destroy() {
7180
['click', 'keydown'].forEach((evtType) => {
7281
this.adapter_.deregisterInteractionHandler(evtType, this.interactionHandler_);
82+
this.adapter_.deregisterTrailingIconInteractionHandler(evtType, this.trailingIconInteractionHandler_);
83+
});
84+
['touchstart', 'pointerdown', 'mousedown'].forEach((evtType) => {
85+
this.adapter_.deregisterTrailingIconInteractionHandler(evtType, this.trailingIconInteractionHandler_);
7386
});
7487
}
7588

@@ -93,6 +106,18 @@ class MDCChipFoundation extends MDCFoundation {
93106
this.adapter_.notifyInteraction();
94107
}
95108
}
109+
110+
/**
111+
* Handles an interaction event on the trailing icon element. This is used to
112+
* prevent the ripple from activating on interaction with the trailing icon.
113+
* @param {!Event} evt
114+
*/
115+
handleTrailingIconInteraction_(evt) {
116+
evt.stopPropagation();
117+
if (evt.type === 'click' || evt.key === 'Enter' || evt.keyCode === 13) {
118+
this.adapter_.notifyTrailingIconInteraction();
119+
}
120+
}
96121
}
97122

98123
export default MDCChipFoundation;

packages/mdc-chips/chip/index.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,21 @@ class MDCChip extends MDCComponent {
6767
hasClass: (className) => this.root_.classList.contains(className),
6868
registerInteractionHandler: (evtType, handler) => this.root_.addEventListener(evtType, handler),
6969
deregisterInteractionHandler: (evtType, handler) => this.root_.removeEventListener(evtType, handler),
70+
registerTrailingIconInteractionHandler: (evtType, handler) => {
71+
const trailingIconEl = this.root_.querySelector(strings.TRAILING_ICON_SELECTOR);
72+
if (trailingIconEl) {
73+
trailingIconEl.addEventListener(evtType, handler);
74+
}
75+
},
76+
deregisterTrailingIconInteractionHandler: (evtType, handler) => {
77+
const trailingIconEl = this.root_.querySelector(strings.TRAILING_ICON_SELECTOR);
78+
if (trailingIconEl) {
79+
trailingIconEl.removeEventListener(evtType, handler);
80+
}
81+
},
7082
notifyInteraction: () => this.emit(strings.INTERACTION_EVENT, {chip: this}, true /* shouldBubble */),
83+
notifyTrailingIconInteraction: () => this.emit(
84+
strings.TRAILING_ICON_INTERACTION_EVENT, {chip: this}, true /* shouldBubble */),
7185
})));
7286
}
7387

test/unit/mdc-chips/mdc-chip.foundation.test.js

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import {assert} from 'chai';
1818
import td from 'testdouble';
1919

20-
import {verifyDefaultAdapter} from '../helpers/foundation';
20+
import {verifyDefaultAdapter, captureHandlers} from '../helpers/foundation';
2121
import {setupFoundationTest} from '../helpers/setup';
2222
import MDCChipFoundation from '../../../packages/mdc-chips/chip/foundation';
2323

@@ -36,7 +36,9 @@ test('exports cssClasses', () => {
3636
test('defaultAdapter returns a complete adapter implementation', () => {
3737
verifyDefaultAdapter(MDCChipFoundation, [
3838
'addClass', 'removeClass', 'hasClass',
39+
'registerTrailingIconInteractionHandler', 'deregisterTrailingIconInteractionHandler',
3940
'registerInteractionHandler', 'deregisterInteractionHandler', 'notifyInteraction',
41+
'notifyTrailingIconInteraction',
4042
]);
4143
});
4244

@@ -48,6 +50,11 @@ test('#init adds event listeners', () => {
4850

4951
td.verify(mockAdapter.registerInteractionHandler('click', td.matchers.isA(Function)));
5052
td.verify(mockAdapter.registerInteractionHandler('keydown', td.matchers.isA(Function)));
53+
td.verify(mockAdapter.registerTrailingIconInteractionHandler('click', td.matchers.isA(Function)));
54+
td.verify(mockAdapter.registerTrailingIconInteractionHandler('keydown', td.matchers.isA(Function)));
55+
td.verify(mockAdapter.registerTrailingIconInteractionHandler('touchstart', td.matchers.isA(Function)));
56+
td.verify(mockAdapter.registerTrailingIconInteractionHandler('pointerdown', td.matchers.isA(Function)));
57+
td.verify(mockAdapter.registerTrailingIconInteractionHandler('mousedown', td.matchers.isA(Function)));
5158
});
5259

5360
test('#destroy removes event listeners', () => {
@@ -56,6 +63,11 @@ test('#destroy removes event listeners', () => {
5663

5764
td.verify(mockAdapter.deregisterInteractionHandler('click', td.matchers.isA(Function)));
5865
td.verify(mockAdapter.deregisterInteractionHandler('keydown', td.matchers.isA(Function)));
66+
td.verify(mockAdapter.deregisterTrailingIconInteractionHandler('click', td.matchers.isA(Function)));
67+
td.verify(mockAdapter.deregisterTrailingIconInteractionHandler('keydown', td.matchers.isA(Function)));
68+
td.verify(mockAdapter.deregisterTrailingIconInteractionHandler('touchstart', td.matchers.isA(Function)));
69+
td.verify(mockAdapter.deregisterTrailingIconInteractionHandler('pointerdown', td.matchers.isA(Function)));
70+
td.verify(mockAdapter.deregisterTrailingIconInteractionHandler('mousedown', td.matchers.isA(Function)));
5971
});
6072

6173
test('#toggleActive adds mdc-chip--activated class if the class does not exist', () => {
@@ -76,17 +88,28 @@ test('#toggleActive removes mdc-chip--activated class if the class exists', () =
7688

7789
test('on click, emit custom event', () => {
7890
const {foundation, mockAdapter} = setupTest();
91+
const handlers = captureHandlers(mockAdapter, 'registerInteractionHandler');
7992
const mockEvt = {
8093
type: 'click',
8194
};
82-
let click;
83-
84-
td.when(mockAdapter.registerInteractionHandler('click', td.matchers.isA(Function))).thenDo((evtType, handler) => {
85-
click = handler;
86-
});
8795

8896
foundation.init();
89-
click(mockEvt);
97+
handlers.click(mockEvt);
9098

9199
td.verify(mockAdapter.notifyInteraction());
92100
});
101+
102+
test('on click in trailing icon, emit custom event', () => {
103+
const {foundation, mockAdapter} = setupTest();
104+
const handlers = captureHandlers(mockAdapter, 'registerTrailingIconInteractionHandler');
105+
const mockEvt = {
106+
type: 'click',
107+
stopPropagation: td.func('stopPropagation'),
108+
};
109+
110+
foundation.init();
111+
handlers.click(mockEvt);
112+
113+
td.verify(mockAdapter.notifyTrailingIconInteraction());
114+
td.verify(mockEvt.stopPropagation());
115+
});

test/unit/mdc-chips/mdc-chip.test.js

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,37 @@ test('#adapter.deregisterInteractionHandler removes event listener for a given e
7878
td.verify(handler(td.matchers.anything()), {times: 0});
7979
});
8080

81-
test('#adapter.notifyInteraction emits ' +
82-
`${MDCChipFoundation.strings.INTERACTION_EVENT}`, () => {
81+
test('#adapter.registerTrailingIconInteractionHandler adds event listener for a given event to the trailing' +
82+
'icon element', () => {
83+
const {root, component} = setupTest();
84+
const icon = bel`
85+
<i class="material-icons mdc-chip__icon mdc-chip__icon--trailing" tabindex="0" role="button">cancel</i>
86+
`;
87+
root.appendChild(icon);
88+
const handler = td.func('click handler');
89+
component.getDefaultFoundation().adapter_.registerTrailingIconInteractionHandler('click', handler);
90+
domEvents.emit(icon, 'click');
91+
92+
td.verify(handler(td.matchers.anything()));
93+
});
94+
95+
test('#adapter.deregisterTrailingIconInteractionHandler removes event listener for a given event from the trailing ' +
96+
'icon element', () => {
97+
const {root, component} = setupTest();
98+
const icon = bel`
99+
<i class="material-icons mdc-chip__icon mdc-chip__icon--trailing" tabindex="0" role="button">cancel</i>
100+
`;
101+
root.appendChild(icon);
102+
const handler = td.func('click handler');
103+
104+
icon.addEventListener('click', handler);
105+
component.getDefaultFoundation().adapter_.deregisterTrailingIconInteractionHandler('click', handler);
106+
domEvents.emit(icon, 'click');
107+
108+
td.verify(handler(td.matchers.anything()), {times: 0});
109+
});
110+
111+
test('#adapter.notifyInteraction emits ' + MDCChipFoundation.strings.INTERACTION_EVENT, () => {
83112
const {component} = setupTest();
84113
const handler = td.func('interaction handler');
85114

@@ -90,6 +119,18 @@ test('#adapter.notifyInteraction emits ' +
90119
td.verify(handler(td.matchers.anything()));
91120
});
92121

122+
test('#adapter.notifyTrailingIconInteraction emits ' +
123+
MDCChipFoundation.strings.TRAILING_ICON_INTERACTION_EVENT, () => {
124+
const {component} = setupTest();
125+
const handler = td.func('interaction handler');
126+
127+
component.listen(
128+
MDCChipFoundation.strings.TRAILING_ICON_INTERACTION_EVENT, handler);
129+
component.getDefaultFoundation().adapter_.notifyTrailingIconInteraction();
130+
131+
td.verify(handler(td.matchers.anything()));
132+
});
133+
93134
function setupMockFoundationTest(root = getFixture()) {
94135
const MockFoundationConstructor = td.constructor(MDCChipFoundation);
95136
const mockFoundation = new MockFoundationConstructor();

0 commit comments

Comments
 (0)