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

Commit c638e79

Browse files
author
Matt Goo
authored
fix(chips): add chip leading icon class management (#281)
1 parent 8435079 commit c638e79

File tree

7 files changed

+81
-34
lines changed

7 files changed

+81
-34
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,4 @@ matrix:
3737
- docker pull mdcreact/screenshots
3838
- docker image ls
3939
script:
40-
- docker run -it --rm --cap-add=SYS_ADMIN -e MDC_GCLOUD_SERVICE_ACCOUNT_KEY="${MDC_GCLOUD_SERVICE_ACCOUNT_KEY}" mdcreact/screenshots /bin/sh -c "git fetch; git checkout \"${CURRENT_BRANCH}\"; git pull; npm i; /home/pptruser/material-components-web-react/test/screenshot/start.sh; sleep 30s; npm run test:image-diff"
40+
- docker run -it --rm --cap-add=SYS_ADMIN -e MDC_GCLOUD_SERVICE_ACCOUNT_KEY="${MDC_GCLOUD_SERVICE_ACCOUNT_KEY}" mdcreact/screenshots /bin/sh -c "git checkout .; git fetch; git checkout \"${CURRENT_BRANCH}\"; git pull; npm i; /home/pptruser/material-components-web-react/test/screenshot/start.sh; sleep 30s; npm run test:image-diff"

packages/chips/Chip.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export class Chip extends Component {
3131
foundation_ = null;
3232
state = {
3333
classList: new Set(),
34+
leadingIconClassList: new Set(),
3435
};
3536

3637
componentDidMount() {
@@ -79,6 +80,16 @@ export class Chip extends Component {
7980
setStyleProperty: (propertyName, value) => this.chipElement_.style.setProperty(propertyName, value),
8081
notifyRemoval: () => this.props.handleRemove(this.props.id),
8182
notifyInteraction: () => this.props.handleSelect(this.props.id),
83+
addClassToLeadingIcon: (className) => {
84+
const leadingIconClassList = new Set(this.state.leadingIconClassList);
85+
leadingIconClassList.add(className);
86+
this.setState({leadingIconClassList});
87+
},
88+
removeClassFromLeadingIcon: (className) => {
89+
const leadingIconClassList = new Set(this.state.leadingIconClassList);
90+
leadingIconClassList.delete(className);
91+
this.setState({leadingIconClassList});
92+
},
8293
};
8394
}
8495

@@ -96,9 +107,13 @@ export class Chip extends Component {
96107

97108
handleRemoveIconClick = (e) => this.foundation_.handleTrailingIconInteraction(e);
98109

99-
handleTransitionEnd = (e) => this.foundation_.handleTransitionEnd(e);
110+
handleTransitionEnd = (e) => {
111+
this.props.onTransitionEnd(e);
112+
this.foundation_.handleTransitionEnd(e);
113+
};
100114

101115
renderLeadingIcon = (leadingIcon) => {
116+
const {leadingIconClassList} = this.state;
102117
const {
103118
className,
104119
...otherProps
@@ -107,6 +122,7 @@ export class Chip extends Component {
107122
const props = {
108123
className: classnames(
109124
className,
125+
Array.from(leadingIconClassList),
110126
'mdc-chip__icon',
111127
'mdc-chip__icon--leading',
112128
),
@@ -148,6 +164,7 @@ export class Chip extends Component {
148164
handleRemove,
149165
onClick,
150166
onKeyDown,
167+
onTransitionEnd,
151168
computeBoundingRect,
152169
initRipple,
153170
unbounded,
@@ -187,6 +204,7 @@ Chip.propTypes = {
187204
handleRemove: PropTypes.func,
188205
onClick: PropTypes.func,
189206
onKeyDown: PropTypes.func,
207+
onTransitionEnd: PropTypes.func,
190208
initRipple: PropTypes.func,
191209
unbounded: PropTypes.bool,
192210
chipCheckmark: PropTypes.node,
@@ -201,6 +219,7 @@ Chip.defaultProps = {
201219
selected: false,
202220
onClick: () => {},
203221
onKeyDown: () => {},
222+
onTransitionEnd: () => {},
204223
initRipple: () => {},
205224
handleSelect: () => {},
206225
handleRemove: () => {},

packages/chips/ChipSet.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ export default class ChipSet extends Component {
4242

4343
componentDidUpdate(prevProps) {
4444
if (this.props.selectedChipIds !== prevProps.selectedChipIds) {
45-
this.updateChipSelection();
45+
const selectedChipIds = new Set(this.props.selectedChipIds);
46+
this.setState({selectedChipIds});
47+
this.updateChipSelection(selectedChipIds);
4648
}
4749
}
4850

@@ -74,10 +76,10 @@ export default class ChipSet extends Component {
7476
};
7577
}
7678

77-
updateChipSelection() {
79+
updateChipSelection(ids = this.state.selectedChipIds) {
7880
React.Children.forEach(this.props.children, (child) => {
7981
const {id} = child.props;
80-
if (this.props.selectedChipIds.indexOf(id) > -1) {
82+
if (ids.has(id)) {
8183
this.foundation_.select(id);
8284
} else {
8385
// remove deselect when MDC Web issue 3612 is fixed
@@ -90,6 +92,7 @@ export default class ChipSet extends Component {
9092
const {handleSelect, choice, filter} = this.props;
9193
// update when mdc web issue is fix
9294
// https://github.com/material-components/material-components-web/issues/3613
95+
// filter || choice is duplicate logic found in MDC Web foundation code
9396
if (filter || choice) {
9497
this.foundation_.toggleSelect(chipId);
9598
}

test/screenshot/chips/index.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,14 @@ const sizes = ['Small', 'Medium', 'Large'];
104104
const clothes = ['Tops', 'Bottoms', 'Shoes'];
105105
const contacts = ['Jane Smith', 'John Doe'];
106106

107-
const renderChips = (list) => {
107+
const renderChips = (list, hasLeadingIcon = false) => {
108108
return list.map((label, index) => (
109-
<Chip id={`${index}chip`} key={index} label={label} />
109+
<Chip
110+
id={`${index}chip`}
111+
key={index}
112+
label={label}
113+
leadingIcon={hasLeadingIcon ? <MaterialIcon icon='shopping_basket' /> : null}
114+
/>
110115
));
111116
};
112117

@@ -123,7 +128,12 @@ const ChipsScreenshotTest = () => {
123128
{renderChips(sizes)}
124129
</ChoiceChipsTest>
125130

126-
Filter Chips
131+
Filter Chips with Leading Icon
132+
<FilterChipsTest selectedChipIds={['1chip', '2chip']}>
133+
{renderChips(clothes, true)}
134+
</FilterChipsTest>
135+
136+
Filter Chips no Leading Icon
127137
<FilterChipsTest selectedChipIds={['1chip', '2chip']}>
128138
{renderChips(clothes)}
129139
</FilterChipsTest>

test/screenshot/golden.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
"button": "a0cf6b2a5d09400657303bb2a7b220dbe33642a8f44fd7571cdfcf5517fb3586",
44
"card": "b2fd82763c383be438ff6578083bf9009711c7470333d07eb916ab690fc42d31",
5-
"chips": "9cdf56c10f4badd9555433e84a52d52028f286f42392f4902251dcb8eb748e33",
5+
"chips": "89fb955abe09193af4e2b0f8cb9f0df06495508d2321fee247fa1a50cfe16a61",
66
"line-ripple": "56b136db2dc7e09260849447e6bde9b55a837af332a05d9f52506ab1c95e2e57",
77
"fab": "db36f52195c420062d91dd5ebe5432ad87247b3c1146fd547b0a195079bbce2f",
88
"floating-label": "1d4d4f2e57e1769b14fc84985d1e6f53410c49aef41c9cf4fde94f938adefe57",
@@ -12,6 +12,7 @@
1212
"select": "ed9c021c8347f18e6ece48d55a8a3b30bb2380ec94276b002e9c064e7674e1c8",
1313
"switch": "dd8a3ec00447e0c586b5bbefdc633681d29e6f04ff8b517a68209bd1f4a6a4e4",
1414
"tab": "0e53fa0ca9b2de4ff7941169a9b6a929a83b18e517c18404adeb40f7e644a2f1",
15+
"tab-bar": "b5cc3a623212f39be0a10001bb7f7e8d33f899ba44688e62b4ea449a8c849027",
1516
"tab-indicator": "7ce7ce8fd50301c67d7ebfb0ba953208260ce2881bee0c7e640c46bf60dc90b6",
1617
"tab-scroller": "468866dd0c222b36b55485ab44a5760133a4ddfb2a6cf81e6ae4672d7e02a447",
1718
"text-field": "3467c006063732dbc46fad4dbf3ea806b97772be3c13f798bc74475c4ec63b21",

test/unit/chips/Chip.test.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,19 @@ test('#adapter.setStyleProperty should add styles to chip', () => {
7777
assert.equal(chipElement.style.width, width);
7878
});
7979

80+
test('#adapter.addClassToLeadingIcon adds to state.leadingIconClassList', () => {
81+
const wrapper = shallow(<Chip id='123' />);
82+
wrapper.instance().foundation_.adapter_.addClassToLeadingIcon('test-leading-icon-class');
83+
assert.isTrue(wrapper.state().leadingIconClassList.has('test-leading-icon-class'));
84+
});
85+
86+
test('#adapter.removeClassFromLeadingIcon removes from state.leadingIconClassList', () => {
87+
const wrapper = shallow(<Chip id='123' />);
88+
wrapper.setState({leadingIconClassList: new Set('test-leading-icon-class')});
89+
wrapper.instance().foundation_.adapter_.removeClassFromLeadingIcon('test-leading-icon-class');
90+
assert.isFalse(wrapper.state().leadingIconClassList.has('test-leading-icon-class'));
91+
});
92+
8093
test('#adapter.notifyInteraction calls #props.handleSelect w/ chipId', () => {
8194
const handleSelect = td.func();
8295
const wrapper = shallow(<Chip id='123' handleSelect={handleSelect} />);
@@ -142,6 +155,13 @@ test('renders leading icon with base class names', () => {
142155
assert.isTrue(wrapper.children().first().hasClass('mdc-chip__icon--leading'));
143156
});
144157

158+
test('renders leadingIcon with state.leadingIconClassList', () => {
159+
const leadingIcon = <i className='leading-icon'></i>;
160+
const wrapper = shallow(<Chip id='1' leadingIcon={leadingIcon} />);
161+
wrapper.setState({leadingIconClassList: new Set(['test-leading-icon-class'])});
162+
assert.isTrue(wrapper.children().first().hasClass('test-leading-icon-class'));
163+
});
164+
145165
test('renders remove icon', () => {
146166
const removeIcon = <i className='remove-icon'></i>;
147167
const wrapper = shallow(<Chip id='1' removeIcon={removeIcon} />);
@@ -182,11 +202,20 @@ test('remove icon keydown calls #foundation.handleTrailingIconInteraction', () =
182202
test('calls #foundation.handleTransitionEnd on transitionend event', () => {
183203
const wrapper = shallow(<Chip id='1' />);
184204
wrapper.instance().foundation_.handleTransitionEnd = td.func();
185-
const evt = {};
205+
const evt = {target: {}};
186206
wrapper.simulate('transitionend', evt);
187207
td.verify(wrapper.instance().foundation_.handleTransitionEnd(evt), {times: 1});
188208
});
189209

210+
211+
test('calls #props.onTransitionEnd on transitionend event', () => {
212+
const onTransitionEnd = td.func();
213+
const wrapper = shallow(<Chip id='1' onTransitionEnd={onTransitionEnd} />);
214+
const evt = {target: {classList: {contains: () => {}}}};
215+
wrapper.simulate('transitionend', evt);
216+
td.verify(onTransitionEnd(evt), {times: 1});
217+
});
218+
190219
test('renders chip checkmark if it exists', () => {
191220
const wrapper = mount(<Chip id='1' chipCheckmark={<ChipCheckmark/>} />);
192221
assert.equal(wrapper.find('.mdc-chip__checkmark').length, 1);

test/unit/chips/ChipSet.test.js

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,11 @@ test('creates foundation', () => {
1313
assert.exists(wrapper.instance().foundation_);
1414
});
1515

16-
test('calls #foundation.select when the selectedChipIds change', () => {
16+
test('updates state.selectedChipIds when the props.selectedChipIds change', () => {
1717
const wrapper = shallow(<ChipSet><div id='1' /></ChipSet>);
18-
wrapper.instance().updateChipSelection = td.func();
1918
const selectedChipIds = ['1'];
2019
wrapper.setProps({selectedChipIds});
21-
td.verify(wrapper.instance().updateChipSelection(), {times: 1});
20+
assert.isTrue(wrapper.state().selectedChipIds.has('1'));
2221
});
2322

2423
test('filter classname is added if is filter variant', () => {
@@ -66,38 +65,24 @@ test('#adapter.setSelected removes selectedChipId from state', () => {
6665
assert.isFalse(wrapper.state().selectedChipIds.has('1'));
6766
});
6867

69-
test('#foundation.select is called when #updateChipSelection is called', () => {
70-
const wrapper = shallow(<ChipSet selectedChipIds={['1']}><div id='1' /></ChipSet>);
68+
test('#foundation.select is called when #updateChipSelection is called and ' +
69+
'state.selectedChipIds has a selected Id', () => {
70+
const wrapper = shallow(<ChipSet><div id='1' /></ChipSet>);
7171
wrapper.instance().foundation_.select = td.func();
72+
const selectedChipIds = new Set(['1']);
73+
wrapper.setState({selectedChipIds});
7274
wrapper.instance().updateChipSelection();
7375
td.verify(wrapper.instance().foundation_.select('1'), {times: 1});
7476
});
7577

76-
test('#foundation.deselect is called when #updateChipSelection is called', () => {
78+
test('#foundation.deselect is called when #updateChipSelection is called and ' +
79+
'state.selectedChipIds does not have selected Id', () => {
7780
const wrapper = shallow(<ChipSet><div id='1' /></ChipSet>);
7881
wrapper.instance().foundation_.deselect = td.func();
7982
wrapper.instance().updateChipSelection();
8083
td.verify(wrapper.instance().foundation_.deselect('1'), {times: 1});
8184
});
8285

83-
test('#foundation.select is called when the selectedChipIds change', () => {
84-
const wrapper = shallow(<ChipSet><div id='1' /></ChipSet>);
85-
wrapper.instance().foundation_.select = td.func();
86-
const selectedChipIds = ['1'];
87-
wrapper.setProps({selectedChipIds});
88-
td.verify(wrapper.instance().foundation_.select('1'), {times: 1});
89-
});
90-
91-
test('#foundation.deselect is called when the selectedChipIds change', () => {
92-
const wrapper = shallow(<ChipSet><div id='1' /></ChipSet>);
93-
const selectedChipIds = ['1'];
94-
wrapper.setProps({selectedChipIds});
95-
wrapper.instance().foundation_.deselect = td.func();
96-
wrapper.setProps({selectedChipIds: []});
97-
98-
td.verify(wrapper.instance().foundation_.deselect('1'), {times: 1});
99-
});
100-
10186
test('#handleSelect calls props.handleSelect', () => {
10287
const handleSelect = td.func();
10388
const wrapper = shallow(<ChipSet handleSelect={handleSelect}></ChipSet>);

0 commit comments

Comments
 (0)