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

Commit c5e87a6

Browse files
mgr34Matt Goo
authored and
Matt Goo
committed
feat(chips): implements missing adapter method notifyTrailingIconInteraction (#653)
BREAKING CHANGE: renamed chip.props.removeIcon --> chips.props.trailingIcon
1 parent e0b0946 commit c5e87a6

File tree

6 files changed

+113
-46
lines changed

6 files changed

+113
-46
lines changed

packages/chips/Chip.tsx

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@ export interface ChipProps extends Ripple.InjectedProps<HTMLDivElement> {
3333
handleSelect?: (id: string, selected: boolean) => void;
3434
handleRemove?: (id: string) => void;
3535
handleInteraction?: (id: string) => void;
36+
handleTrailingIconInteraction?: (id: string) => void;
3637
onClick?: React.MouseEventHandler<HTMLDivElement>;
3738
onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
3839
onTransitionEnd?: React.TransitionEventHandler<HTMLDivElement>;
3940
chipCheckmark?: React.ReactElement<HTMLElement>;
4041
leadingIcon?: React.ReactElement<HTMLElement>;
41-
removeIcon?: React.ReactElement<HTMLElement>;
42+
shouldRemoveOnTrailingIconClick?: boolean;
43+
trailingIcon?: React.ReactElement<HTMLElement>;
4244
initRipple: (surface: HTMLElement | null) => void;
4345
};
4446

@@ -63,6 +65,8 @@ export class Chip extends React.Component<ChipProps, ChipState> {
6365
handleSelect: () => {},
6466
handleRemove: () => {},
6567
handleInteraction: () => {},
68+
handleTrailingIconInteraction: () => {},
69+
shouldRemoveOnTrailingIconClick: true,
6670
};
6771

6872
state = {
@@ -71,14 +75,24 @@ export class Chip extends React.Component<ChipProps, ChipState> {
7175
};
7276

7377
componentDidMount() {
78+
const {selected, shouldRemoveOnTrailingIconClick} = this.props;
7479
this.foundation = new MDCChipFoundation(this.adapter);
7580
this.foundation.init();
76-
this.foundation.setSelected(this.props.selected);
81+
this.foundation.setSelected(selected);
82+
if (shouldRemoveOnTrailingIconClick !== this.foundation.getShouldRemoveOnTrailingIconClick()) {
83+
this.foundation.setShouldRemoveOnTrailingIconClick(shouldRemoveOnTrailingIconClick);
84+
}
7785
}
7886

7987
componentDidUpdate(prevProps: ChipProps) {
80-
if (this.props.selected !== prevProps.selected) {
81-
this.foundation.setSelected(this.props.selected);
88+
const {selected, shouldRemoveOnTrailingIconClick} = this.props;
89+
90+
if (selected !== prevProps.selected) {
91+
this.foundation.setSelected(selected);
92+
}
93+
94+
if (shouldRemoveOnTrailingIconClick !== prevProps.shouldRemoveOnTrailingIconClick) {
95+
this.foundation.setShouldRemoveOnTrailingIconClick(shouldRemoveOnTrailingIconClick);
8296
}
8397
}
8498

@@ -126,6 +140,7 @@ export class Chip extends React.Component<ChipProps, ChipState> {
126140
notifyInteraction: () => this.props.handleInteraction!(this.props.id!),
127141
notifySelection: (selected: boolean) =>
128142
this.props.handleSelect!(this.props.id!, selected),
143+
notifyTrailingIconInteraction: () => this.props.handleTrailingIconInteraction!(this.props.id!),
129144
addClassToLeadingIcon: (className: string) => {
130145
const leadingIconClassList = new Set(this.state.leadingIconClassList);
131146
leadingIconClassList.add(className);
@@ -149,7 +164,7 @@ export class Chip extends React.Component<ChipProps, ChipState> {
149164
this.foundation.handleInteraction(e);
150165
};
151166

152-
handleRemoveIconClick = (e: React.MouseEvent) => this.foundation.handleTrailingIconInteraction(e);
167+
handleTrailingIconClick = (e: React.MouseEvent) => this.foundation.handleTrailingIconInteraction(e);
153168

154169
handleTransitionEnd = (e: React.TransitionEvent<HTMLDivElement>) => {
155170
this.props.onTransitionEnd!(e);
@@ -171,21 +186,21 @@ export class Chip extends React.Component<ChipProps, ChipState> {
171186
return React.cloneElement(leadingIcon, props);
172187
};
173188

174-
renderRemoveIcon = (removeIcon: React.ReactElement<HTMLElement>) => {
175-
const {className, ...otherProps} = removeIcon.props;
189+
renderTrailingIcon = (trailingIcon: React.ReactElement<HTMLElement>) => {
190+
const {className, ...otherProps} = trailingIcon.props;
176191
const props = {
177192
className: classnames(
178193
className,
179194
'mdc-chip__icon',
180195
'mdc-chip__icon--trailing'
181196
),
182-
onClick: this.handleRemoveIconClick,
183-
onKeyDown: this.handleRemoveIconClick,
197+
onClick: this.handleTrailingIconClick,
198+
onKeyDown: this.handleTrailingIconClick,
184199
tabIndex: 0,
185200
role: 'button',
186201
...otherProps,
187202
};
188-
return React.cloneElement(removeIcon, props);
203+
return React.cloneElement(trailingIcon, props);
189204
};
190205

191206
render() {
@@ -197,16 +212,18 @@ export class Chip extends React.Component<ChipProps, ChipState> {
197212
handleSelect,
198213
handleInteraction,
199214
handleRemove,
215+
handleTrailingIconInteraction,
200216
onClick,
201217
onKeyDown,
202218
onTransitionEnd,
203219
computeBoundingRect,
204220
initRipple,
205221
unbounded,
222+
shouldRemoveOnTrailingIconClick,
206223
/* eslint-enable no-unused-vars */
207224
chipCheckmark,
208225
leadingIcon,
209-
removeIcon,
226+
trailingIcon,
210227
label,
211228
...otherProps
212229
} = this.props;
@@ -220,10 +237,10 @@ export class Chip extends React.Component<ChipProps, ChipState> {
220237
ref={this.init}
221238
{...otherProps}
222239
>
223-
{leadingIcon ? this.renderLeadingIcon(leadingIcon) : null}
240+
{leadingIcon && this.renderLeadingIcon(leadingIcon)}
224241
{chipCheckmark}
225242
<div className='mdc-chip__text'>{label}</div>
226-
{removeIcon ? this.renderRemoveIcon(removeIcon) : null}
243+
{trailingIcon && this.renderTrailingIcon(trailingIcon)}
227244
</div>
228245
);
229246
}

packages/chips/README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ class MyInputChips extends React.Component {
137137
<Chip
138138
key={chip.id} // The chip's key cannot be its index, because its index may change.
139139
label={chip.label}
140-
removeIcon={<MaterialIcon icon='cancel' />}
140+
trailingIcon={<MaterialIcon icon='cancel' />}
141141
/>
142142
)}
143143
</ChipSet>
@@ -170,12 +170,15 @@ className | String | Classes to be applied to the chip element
170170
id | Number | Required. Unique identifier for the chip
171171
label | String | Text to be shown on the chip
172172
leadingIcon | Element | An icon element that appears as the leading icon.
173-
removeIcon | Element | An icon element that appears as the remove icon. Clicking on it should remove the chip.
173+
trailingIcon | Element | An icon element that appears as the remove icon. Clicking on it should remove the chip.
174174
selected | Boolean | Indicates whether the chip is selected
175175
handleSelect | Function(id: string, selected: boolean) => void | Callback for selecting the chip with the given id
176-
handleRemove | Function(id: string) => void | Callback for removing the chip with the given id
177176
handleInteraction | Function(id: string) => void | Callback for interaction of chip (`onClick` | `onKeyDown`)
178-
177+
handleTrailingIconInteraction | Function(id: string) => void | Callback for interaction with trailing icon
178+
shouldRemoveOnTrailingIconClick | Boolean | indicates if interaction with trailing icon should remove chip. defaults to `true`
179+
> Note: `handleTrailingIconInteraction` will execute before `handleRemove`.
180+
> Without explicitly setting shouldRemoveOnTrailingIconClick to false both
181+
> callbacks will fire on trailingIcon interaction
179182
180183
## Sass Mixins
181184

test/screenshot/chips/index.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ type InputChipsTestState = {
6262

6363
class InputChipsTest extends React.Component<InputChipsTestProps, InputChipsTestState> {
6464
state = {
65-
chips: this.props.labels.map((label) => {
66-
return {label: label, id: uuidv1()};
67-
}),
65+
chips: this.props.labels.reduce((a, label) => (
66+
[...a, {label, id: uuidv1()}]
67+
), [{label: 'Name Chips (dont remove)', id: uuidv1()}] ),
6868
};
6969

7070
addChip(label: string) {
@@ -90,13 +90,14 @@ class InputChipsTest extends React.Component<InputChipsTestProps, InputChipsTest
9090
<div>
9191
<input type='text' onKeyDown={this.handleKeyDown} />
9292
<ChipSet input updateChips={this.updateChips}>
93-
{this.state.chips.map((chip) => (
93+
{this.state.chips.map((chip, i: number ) => (
9494
<Chip
9595
id={chip.id}
9696
key={chip.id} // The chip s key cannot be its index, because its index may change
9797
label={chip.label}
98-
leadingIcon={<MaterialIcon icon='face' />}
99-
removeIcon={<MaterialIcon icon='cancel' />}
98+
leadingIcon={i === 0 ? undefined : <MaterialIcon icon='face' />}
99+
trailingIcon={<MaterialIcon icon={i === 0 ? 'announcement' : 'cancel'} />}
100+
shouldRemoveOnTrailingIconClick={i !== 0}
100101
/>
101102
))}
102103
</ChipSet>

test/screenshot/golden.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"button": "57cb8769600669ec4d44dd23fcf2f6ded528ce8eec612d832c2b9e0271a640c3",
33
"card": "b2fd82763c383be438ff6578083bf9009711c7470333d07eb916ab690fc42d31",
44
"checkbox": "9c61177f0f927e178e7c6687d74cdfa08abc15ea8fc3c381f570b7c7d1f46d2a",
5-
"chips": "e100a23df0b92c37920127c62d7d694ce3fe40c101c0ed05d535f5cafee62b27",
5+
"chips": "f5973cc5f1961464cbbbe152ca25b9a989e7e5a54b6d64cb28f0c25788f7df44",
66
"fab": "db36f52195c420062d91dd5ebe5432ad87247b3c1146fd547b0a195079bbce2f",
77
"floating-label": "1d4d4f2e57e1769b14fc84985d1e6f53410c49aef41c9cf4fde94f938adefe57",
88
"icon-button": "5ffb1f7fbd06d2c0533f6ba8d4d9ea170cec1a248a61de1cc1bb626cb58ebcd2",

test/unit/chips/Chip.test.tsx

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ test('creates foundation', () => {
1313
assert.exists(wrapper.instance().foundation);
1414
});
1515

16+
test('#componentWillUnmount destroys foundation', () => {
17+
const wrapper = shallow<Chip>(<Chip id='2' />);
18+
const foundation = wrapper.instance().foundation;
19+
foundation.destroy = td.func();
20+
wrapper.unmount();
21+
td.verify(foundation.destroy(), {times: 1});
22+
});
23+
1624
test('calls setSelected if props.selected is true (#foundation.setSelected)', () => {
1725
const wrapper = mount<Chip>(
1826
<Chip id='1' selected>
@@ -22,6 +30,36 @@ test('calls setSelected if props.selected is true (#foundation.setSelected)', ()
2230
assert.isTrue(wrapper.state().classList.has('mdc-chip--selected'));
2331
});
2432

33+
34+
test('renders a Chip with foundation.shouldRemoveOnTrailingIconClick set to true', () => {
35+
const wrapper = shallow<Chip>(<Chip id='3' />);
36+
assert.isTrue(wrapper.instance().foundation.getShouldRemoveOnTrailingIconClick());
37+
});
38+
39+
test('#componentDidMount sets #foundation.shouldRemoveOnTrailingIconClick to false if prop false', () => {
40+
const wrapper = shallow<Chip>(<Chip id='4' shouldRemoveOnTrailingIconClick={false} />);
41+
assert.isFalse(wrapper.instance().foundation.getShouldRemoveOnTrailingIconClick());
42+
});
43+
44+
test('when props.shouldRemoveOnTrailingIconClick updates to false, ' +
45+
' #foundation.setShouldRemoveOnTrailingIconClick is called ', () => {
46+
const wrapper = shallow<Chip>(<Chip id='5' shouldRemoveOnTrailingIconClick />);
47+
assert.isTrue(wrapper.instance().foundation.getShouldRemoveOnTrailingIconClick());
48+
wrapper.instance().foundation.setShouldRemoveOnTrailingIconClick = td.func();
49+
wrapper.setProps({shouldRemoveOnTrailingIconClick: false});
50+
td.verify(wrapper.instance().foundation.setShouldRemoveOnTrailingIconClick(false), {times: 1});
51+
});
52+
53+
54+
test('when props.shouldRemoveOnTrailingIconClick updates to true, ' +
55+
' #foundation.setShouldRemoveOnTrailingIconClick is called ', () => {
56+
const wrapper = shallow<Chip>(<Chip id='6' shouldRemoveOnTrailingIconClick={false} />);
57+
assert.isFalse(wrapper.instance().foundation.getShouldRemoveOnTrailingIconClick());
58+
wrapper.instance().foundation.setShouldRemoveOnTrailingIconClick = td.func();
59+
wrapper.setProps({shouldRemoveOnTrailingIconClick: true});
60+
td.verify(wrapper.instance().foundation.setShouldRemoveOnTrailingIconClick(true), {times: 1});
61+
});
62+
2563
test('classNames adds classes', () => {
2664
const wrapper = shallow(<Chip id='1' className='test-class-name' />);
2765
assert.isTrue(wrapper.hasClass('test-class-name'));
@@ -139,6 +177,13 @@ test('#adapter.notifySelection calls #props.handleSelect w/ chipId and selected
139177
td.verify(handleSelect('123', true), {times: 1});
140178
});
141179

180+
test('#adapter.notifyTrailingIconInteraction calls #props.handleTrailingIconInteraction w/ chipId', () => {
181+
const handleTrailingIconInteraction = coerceForTesting<(id: string) => void>(td.func());
182+
const wrapper = shallow<Chip>(<Chip id='123' handleTrailingIconInteraction={handleTrailingIconInteraction} />);
183+
wrapper.instance().foundation.adapter_.notifyTrailingIconInteraction();
184+
td.verify(handleTrailingIconInteraction('123'), {times: 1});
185+
});
186+
142187
test('on click calls #props.onClick', () => {
143188
const onClick = coerceForTesting<(event: React.MouseEvent) => void>(td.func());
144189
const wrapper = shallow<Chip>(<Chip id='1' onClick={onClick} />);
@@ -229,9 +274,9 @@ test('renders leadingIcon with state.leadingIconClassList', () => {
229274
);
230275
});
231276

232-
test('renders remove icon', () => {
233-
const removeIcon = <i className='remove-icon' />;
234-
const wrapper = shallow(<Chip id='1' removeIcon={removeIcon} />);
277+
test('renders trailing icon', () => {
278+
const trailingIcon = <i className='remove-icon' />;
279+
const wrapper = shallow(<Chip id='1' trailingIcon={trailingIcon} />);
235280
assert.equal(
236281
wrapper
237282
.children()
@@ -241,9 +286,9 @@ test('renders remove icon', () => {
241286
);
242287
});
243288

244-
test('renders remove icon with class name', () => {
245-
const removeIcon = <i className='remove-icon' />;
246-
const wrapper = shallow(<Chip id='1' removeIcon={removeIcon} />);
289+
test('renders trailing icon with class name', () => {
290+
const trailingIcon = <i className='remove-icon' />;
291+
const wrapper = shallow(<Chip id='1' trailingIcon={trailingIcon} />);
247292
assert.isTrue(
248293
wrapper
249294
.children()
@@ -252,9 +297,9 @@ test('renders remove icon with class name', () => {
252297
);
253298
});
254299

255-
test('renders remove icon with base class names', () => {
256-
const removeIcon = <i className='remove-icon' />;
257-
const wrapper = shallow(<Chip id='1' removeIcon={removeIcon} />);
300+
test('renders trailing icon with base class names', () => {
301+
const trailingIcon = <i className='remove-icon' />;
302+
const wrapper = shallow(<Chip id='1' trailingIcon={trailingIcon} />);
258303
assert.isTrue(
259304
wrapper
260305
.children()
@@ -269,9 +314,9 @@ test('renders remove icon with base class names', () => {
269314
);
270315
});
271316

272-
test('remove icon click calls #foundation.handleTrailingIconInteraction', () => {
273-
const removeIcon = <i className='remove-icon' />;
274-
const wrapper = shallow<Chip>(<Chip id='1' removeIcon={removeIcon} />);
317+
test('trailing icon click calls #foundation.handleTrailingIconInteraction', () => {
318+
const trailingIcon = <i className='remove-icon' />;
319+
const wrapper = shallow<Chip>(<Chip id='1' trailingIcon={trailingIcon} />);
275320
wrapper.instance().foundation.handleTrailingIconInteraction = td.func();
276321
const evt = {};
277322
wrapper
@@ -283,9 +328,9 @@ test('remove icon click calls #foundation.handleTrailingIconInteraction', () =>
283328
});
284329
});
285330

286-
test('remove icon keydown calls #foundation.handleTrailingIconInteraction', () => {
287-
const removeIcon = <i className='remove-icon' />;
288-
const wrapper = shallow<Chip>(<Chip id='1' removeIcon={removeIcon} />);
331+
test('trailing icon keydown calls #foundation.handleTrailingIconInteraction', () => {
332+
const trailingIcon = <i className='remove-icon' />;
333+
const wrapper = shallow<Chip>(<Chip id='1' trailingIcon={trailingIcon} />);
289334
wrapper.instance().foundation.handleTrailingIconInteraction = td.func();
290335
const evt = {};
291336
wrapper
@@ -297,6 +342,7 @@ test('remove icon keydown calls #foundation.handleTrailingIconInteraction', () =
297342
});
298343
});
299344

345+
300346
test('calls #foundation.handleTransitionEnd on transitionend event', () => {
301347
const wrapper = shallow<Chip>(<Chip id='1' />);
302348
wrapper.instance().foundation.handleTransitionEnd = td.func();

test/unit/chips/ChipSet.test.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ suite('ChipSet');
1111

1212

1313
test('creates foundation', () => {
14-
const wrapper = mount<ChipSet>(<ChipSet><Chip /></ChipSet>);
14+
const wrapper = mount<ChipSet>(<ChipSet><Chip id='1' /></ChipSet>);
1515
assert.exists(wrapper.state().foundation);
1616
});
1717

@@ -277,13 +277,13 @@ test('#removeChip calls #props.updateChips with array of remaining chips', () =>
277277
});
278278

279279
test('#setCheckmarkWidth sets checkmark width', () => {
280-
const wrapper = shallow<ChipSet>(<ChipSet><Chip /></ChipSet>);
280+
const wrapper = shallow<ChipSet>(<ChipSet><Chip id='1' /></ChipSet>);
281281
wrapper.instance().setCheckmarkWidth(coerceForTesting<ChipCheckmark>({width: 20}));
282282
assert.equal(wrapper.instance().checkmarkWidth, 20);
283283
});
284284

285285
test('#setCheckmarkWidth does not set checkmark width if checkmark width is already set', () => {
286-
const wrapper = shallow<ChipSet>(<ChipSet><Chip /></ChipSet>);
286+
const wrapper = shallow<ChipSet>(<ChipSet><Chip id='1' /></ChipSet>);
287287
wrapper.instance().checkmarkWidth = 20;
288288
wrapper.instance().setCheckmarkWidth(coerceForTesting<ChipCheckmark>({width: 40}));
289289
assert.equal(wrapper.instance().checkmarkWidth, 20);
@@ -296,7 +296,7 @@ test('#setCheckmarkWidth does not set checkmark width if checkmark is null', ()
296296
});
297297

298298
test('#computeBoundingRect returns width and height', () => {
299-
const wrapper = shallow<ChipSet>(<ChipSet><Chip /></ChipSet>);
299+
const wrapper = shallow<ChipSet>(<ChipSet><Chip id='1' /></ChipSet>);
300300
const chipWidth = 20;
301301
const chipHeight = 50;
302302
const chipElement = coerceForTesting<HTMLDivElement>({
@@ -308,7 +308,7 @@ test('#computeBoundingRect returns width and height', () => {
308308
});
309309

310310
test('#computeBoundingRect returns width and height', () => {
311-
const wrapper = shallow<ChipSet>(<ChipSet><Chip /></ChipSet>);
311+
const wrapper = shallow<ChipSet>(<ChipSet><Chip id='1' /></ChipSet>);
312312
const chipWidth = 20;
313313
const chipHeight = 50;
314314
wrapper.instance().checkmarkWidth = 20;
@@ -355,7 +355,7 @@ test('#chip.props.handleSelect calls #foundation.handleChipSelection', () => {
355355
});
356356

357357
test('chip is rendered with handleRemove method', () => {
358-
const wrapper = mount<ChipSet>(<ChipSet><Chip /></ChipSet>);
358+
const wrapper = mount<ChipSet>(<ChipSet><Chip id='1' /></ChipSet>);
359359
wrapper.instance().handleRemove = coerceForTesting<(chipId: string) => void>(td.func());
360360
wrapper.setProps({children: <Chip id='1' />});
361361
const chip = wrapper.children().props().children[0];
@@ -421,7 +421,7 @@ test('chip is rendered with computeBoundingRect method prop if is not filter var
421421
});
422422

423423
test('#componentWillUnmount destroys foundation', () => {
424-
const wrapper = shallow<ChipSet>(<ChipSet><Chip /></ChipSet>);
424+
const wrapper = shallow<ChipSet>(<ChipSet><Chip id='1' /></ChipSet>);
425425
const foundation = wrapper.state().foundation;
426426
foundation.destroy = td.func();
427427
wrapper.unmount();

0 commit comments

Comments
 (0)