diff --git a/packages/chips/Chip.tsx b/packages/chips/Chip.tsx index bd8478b8c..398203e1f 100644 --- a/packages/chips/Chip.tsx +++ b/packages/chips/Chip.tsx @@ -33,12 +33,14 @@ export interface ChipProps extends Ripple.InjectedProps { handleSelect?: (id: string, selected: boolean) => void; handleRemove?: (id: string) => void; handleInteraction?: (id: string) => void; + handleTrailingIconInteraction?: (id: string) => void; onClick?: React.MouseEventHandler; onKeyDown?: React.KeyboardEventHandler; onTransitionEnd?: React.TransitionEventHandler; chipCheckmark?: React.ReactElement; leadingIcon?: React.ReactElement; - removeIcon?: React.ReactElement; + shouldRemoveOnTrailingIconClick?: boolean; + trailingIcon?: React.ReactElement; initRipple: (surface: HTMLElement | null) => void; }; @@ -63,6 +65,8 @@ export class Chip extends React.Component { handleSelect: () => {}, handleRemove: () => {}, handleInteraction: () => {}, + handleTrailingIconInteraction: () => {}, + shouldRemoveOnTrailingIconClick: true, }; state = { @@ -71,14 +75,24 @@ export class Chip extends React.Component { }; componentDidMount() { + const {selected, shouldRemoveOnTrailingIconClick} = this.props; this.foundation = new MDCChipFoundation(this.adapter); this.foundation.init(); - this.foundation.setSelected(this.props.selected); + this.foundation.setSelected(selected); + if (shouldRemoveOnTrailingIconClick !== this.foundation.getShouldRemoveOnTrailingIconClick()) { + this.foundation.setShouldRemoveOnTrailingIconClick(shouldRemoveOnTrailingIconClick); + } } componentDidUpdate(prevProps: ChipProps) { - if (this.props.selected !== prevProps.selected) { - this.foundation.setSelected(this.props.selected); + const {selected, shouldRemoveOnTrailingIconClick} = this.props; + + if (selected !== prevProps.selected) { + this.foundation.setSelected(selected); + } + + if (shouldRemoveOnTrailingIconClick !== prevProps.shouldRemoveOnTrailingIconClick) { + this.foundation.setShouldRemoveOnTrailingIconClick(shouldRemoveOnTrailingIconClick); } } @@ -126,6 +140,7 @@ export class Chip extends React.Component { notifyInteraction: () => this.props.handleInteraction!(this.props.id!), notifySelection: (selected: boolean) => this.props.handleSelect!(this.props.id!, selected), + notifyTrailingIconInteraction: () => this.props.handleTrailingIconInteraction!(this.props.id!), addClassToLeadingIcon: (className: string) => { const leadingIconClassList = new Set(this.state.leadingIconClassList); leadingIconClassList.add(className); @@ -149,7 +164,7 @@ export class Chip extends React.Component { this.foundation.handleInteraction(e); }; - handleRemoveIconClick = (e: React.MouseEvent) => this.foundation.handleTrailingIconInteraction(e); + handleTrailingIconClick = (e: React.MouseEvent) => this.foundation.handleTrailingIconInteraction(e); handleTransitionEnd = (e: React.TransitionEvent) => { this.props.onTransitionEnd!(e); @@ -171,21 +186,21 @@ export class Chip extends React.Component { return React.cloneElement(leadingIcon, props); }; - renderRemoveIcon = (removeIcon: React.ReactElement) => { - const {className, ...otherProps} = removeIcon.props; + renderTrailingIcon = (trailingIcon: React.ReactElement) => { + const {className, ...otherProps} = trailingIcon.props; const props = { className: classnames( className, 'mdc-chip__icon', 'mdc-chip__icon--trailing' ), - onClick: this.handleRemoveIconClick, - onKeyDown: this.handleRemoveIconClick, + onClick: this.handleTrailingIconClick, + onKeyDown: this.handleTrailingIconClick, tabIndex: 0, role: 'button', ...otherProps, }; - return React.cloneElement(removeIcon, props); + return React.cloneElement(trailingIcon, props); }; render() { @@ -197,16 +212,18 @@ export class Chip extends React.Component { handleSelect, handleInteraction, handleRemove, + handleTrailingIconInteraction, onClick, onKeyDown, onTransitionEnd, computeBoundingRect, initRipple, unbounded, + shouldRemoveOnTrailingIconClick, /* eslint-enable no-unused-vars */ chipCheckmark, leadingIcon, - removeIcon, + trailingIcon, label, ...otherProps } = this.props; @@ -220,10 +237,10 @@ export class Chip extends React.Component { ref={this.init} {...otherProps} > - {leadingIcon ? this.renderLeadingIcon(leadingIcon) : null} + {leadingIcon && this.renderLeadingIcon(leadingIcon)} {chipCheckmark}
{label}
- {removeIcon ? this.renderRemoveIcon(removeIcon) : null} + {trailingIcon && this.renderTrailingIcon(trailingIcon)} ); } diff --git a/packages/chips/README.md b/packages/chips/README.md index 39b0f7574..c7f3af9c1 100644 --- a/packages/chips/README.md +++ b/packages/chips/README.md @@ -137,7 +137,7 @@ class MyInputChips extends React.Component { } + trailingIcon={} /> )} @@ -170,12 +170,15 @@ className | String | Classes to be applied to the chip element id | Number | Required. Unique identifier for the chip label | String | Text to be shown on the chip leadingIcon | Element | An icon element that appears as the leading icon. -removeIcon | Element | An icon element that appears as the remove icon. Clicking on it should remove the chip. +trailingIcon | Element | An icon element that appears as the remove icon. Clicking on it should remove the chip. selected | Boolean | Indicates whether the chip is selected handleSelect | Function(id: string, selected: boolean) => void | Callback for selecting the chip with the given id -handleRemove | Function(id: string) => void | Callback for removing the chip with the given id handleInteraction | Function(id: string) => void | Callback for interaction of chip (`onClick` | `onKeyDown`) - +handleTrailingIconInteraction | Function(id: string) => void | Callback for interaction with trailing icon +shouldRemoveOnTrailingIconClick | Boolean | indicates if interaction with trailing icon should remove chip. defaults to `true` +> Note: `handleTrailingIconInteraction` will execute before `handleRemove`. +> Without explicitly setting shouldRemoveOnTrailingIconClick to false both +> callbacks will fire on trailingIcon interaction ## Sass Mixins diff --git a/test/screenshot/chips/index.tsx b/test/screenshot/chips/index.tsx index b74721e34..7c5c3df95 100644 --- a/test/screenshot/chips/index.tsx +++ b/test/screenshot/chips/index.tsx @@ -62,9 +62,9 @@ type InputChipsTestState = { class InputChipsTest extends React.Component { state = { - chips: this.props.labels.map((label) => { - return {label: label, id: uuidv1()}; - }), + chips: this.props.labels.reduce((a, label) => ( + [...a, {label, id: uuidv1()}] + ), [{label: 'Name Chips (dont remove)', id: uuidv1()}] ), }; addChip(label: string) { @@ -90,13 +90,14 @@ class InputChipsTest extends React.Component - {this.state.chips.map((chip) => ( + {this.state.chips.map((chip, i: number ) => ( } - removeIcon={} + leadingIcon={i === 0 ? undefined : } + trailingIcon={} + shouldRemoveOnTrailingIconClick={i !== 0} /> ))} diff --git a/test/screenshot/golden.json b/test/screenshot/golden.json index c5c0c3671..3b4610792 100644 --- a/test/screenshot/golden.json +++ b/test/screenshot/golden.json @@ -2,7 +2,7 @@ "button": "57cb8769600669ec4d44dd23fcf2f6ded528ce8eec612d832c2b9e0271a640c3", "card": "b2fd82763c383be438ff6578083bf9009711c7470333d07eb916ab690fc42d31", "checkbox": "9c61177f0f927e178e7c6687d74cdfa08abc15ea8fc3c381f570b7c7d1f46d2a", - "chips": "e100a23df0b92c37920127c62d7d694ce3fe40c101c0ed05d535f5cafee62b27", + "chips": "f5973cc5f1961464cbbbe152ca25b9a989e7e5a54b6d64cb28f0c25788f7df44", "fab": "db36f52195c420062d91dd5ebe5432ad87247b3c1146fd547b0a195079bbce2f", "floating-label": "1d4d4f2e57e1769b14fc84985d1e6f53410c49aef41c9cf4fde94f938adefe57", "icon-button": "5ffb1f7fbd06d2c0533f6ba8d4d9ea170cec1a248a61de1cc1bb626cb58ebcd2", diff --git a/test/unit/chips/Chip.test.tsx b/test/unit/chips/Chip.test.tsx index 9ecff902d..45f976796 100644 --- a/test/unit/chips/Chip.test.tsx +++ b/test/unit/chips/Chip.test.tsx @@ -13,6 +13,14 @@ test('creates foundation', () => { assert.exists(wrapper.instance().foundation); }); +test('#componentWillUnmount destroys foundation', () => { + const wrapper = shallow(); + const foundation = wrapper.instance().foundation; + foundation.destroy = td.func(); + wrapper.unmount(); + td.verify(foundation.destroy(), {times: 1}); +}); + test('calls setSelected if props.selected is true (#foundation.setSelected)', () => { const wrapper = mount( @@ -22,6 +30,36 @@ test('calls setSelected if props.selected is true (#foundation.setSelected)', () assert.isTrue(wrapper.state().classList.has('mdc-chip--selected')); }); + +test('renders a Chip with foundation.shouldRemoveOnTrailingIconClick set to true', () => { + const wrapper = shallow(); + assert.isTrue(wrapper.instance().foundation.getShouldRemoveOnTrailingIconClick()); +}); + +test('#componentDidMount sets #foundation.shouldRemoveOnTrailingIconClick to false if prop false', () => { + const wrapper = shallow(); + assert.isFalse(wrapper.instance().foundation.getShouldRemoveOnTrailingIconClick()); +}); + +test('when props.shouldRemoveOnTrailingIconClick updates to false, ' + + ' #foundation.setShouldRemoveOnTrailingIconClick is called ', () => { + const wrapper = shallow(); + assert.isTrue(wrapper.instance().foundation.getShouldRemoveOnTrailingIconClick()); + wrapper.instance().foundation.setShouldRemoveOnTrailingIconClick = td.func(); + wrapper.setProps({shouldRemoveOnTrailingIconClick: false}); + td.verify(wrapper.instance().foundation.setShouldRemoveOnTrailingIconClick(false), {times: 1}); +}); + + +test('when props.shouldRemoveOnTrailingIconClick updates to true, ' + + ' #foundation.setShouldRemoveOnTrailingIconClick is called ', () => { + const wrapper = shallow(); + assert.isFalse(wrapper.instance().foundation.getShouldRemoveOnTrailingIconClick()); + wrapper.instance().foundation.setShouldRemoveOnTrailingIconClick = td.func(); + wrapper.setProps({shouldRemoveOnTrailingIconClick: true}); + td.verify(wrapper.instance().foundation.setShouldRemoveOnTrailingIconClick(true), {times: 1}); +}); + test('classNames adds classes', () => { const wrapper = shallow(); assert.isTrue(wrapper.hasClass('test-class-name')); @@ -139,6 +177,13 @@ test('#adapter.notifySelection calls #props.handleSelect w/ chipId and selected td.verify(handleSelect('123', true), {times: 1}); }); +test('#adapter.notifyTrailingIconInteraction calls #props.handleTrailingIconInteraction w/ chipId', () => { + const handleTrailingIconInteraction = coerceForTesting<(id: string) => void>(td.func()); + const wrapper = shallow(); + wrapper.instance().foundation.adapter_.notifyTrailingIconInteraction(); + td.verify(handleTrailingIconInteraction('123'), {times: 1}); +}); + test('on click calls #props.onClick', () => { const onClick = coerceForTesting<(event: React.MouseEvent) => void>(td.func()); const wrapper = shallow(); @@ -229,9 +274,9 @@ test('renders leadingIcon with state.leadingIconClassList', () => { ); }); -test('renders remove icon', () => { - const removeIcon = ; - const wrapper = shallow(); +test('renders trailing icon', () => { + const trailingIcon = ; + const wrapper = shallow(); assert.equal( wrapper .children() @@ -241,9 +286,9 @@ test('renders remove icon', () => { ); }); -test('renders remove icon with class name', () => { - const removeIcon = ; - const wrapper = shallow(); +test('renders trailing icon with class name', () => { + const trailingIcon = ; + const wrapper = shallow(); assert.isTrue( wrapper .children() @@ -252,9 +297,9 @@ test('renders remove icon with class name', () => { ); }); -test('renders remove icon with base class names', () => { - const removeIcon = ; - const wrapper = shallow(); +test('renders trailing icon with base class names', () => { + const trailingIcon = ; + const wrapper = shallow(); assert.isTrue( wrapper .children() @@ -269,9 +314,9 @@ test('renders remove icon with base class names', () => { ); }); -test('remove icon click calls #foundation.handleTrailingIconInteraction', () => { - const removeIcon = ; - const wrapper = shallow(); +test('trailing icon click calls #foundation.handleTrailingIconInteraction', () => { + const trailingIcon = ; + const wrapper = shallow(); wrapper.instance().foundation.handleTrailingIconInteraction = td.func(); const evt = {}; wrapper @@ -283,9 +328,9 @@ test('remove icon click calls #foundation.handleTrailingIconInteraction', () => }); }); -test('remove icon keydown calls #foundation.handleTrailingIconInteraction', () => { - const removeIcon = ; - const wrapper = shallow(); +test('trailing icon keydown calls #foundation.handleTrailingIconInteraction', () => { + const trailingIcon = ; + const wrapper = shallow(); wrapper.instance().foundation.handleTrailingIconInteraction = td.func(); const evt = {}; wrapper @@ -297,6 +342,7 @@ test('remove icon keydown calls #foundation.handleTrailingIconInteraction', () = }); }); + test('calls #foundation.handleTransitionEnd on transitionend event', () => { const wrapper = shallow(); wrapper.instance().foundation.handleTransitionEnd = td.func(); diff --git a/test/unit/chips/ChipSet.test.tsx b/test/unit/chips/ChipSet.test.tsx index 82e35dc21..63bbf7a3b 100644 --- a/test/unit/chips/ChipSet.test.tsx +++ b/test/unit/chips/ChipSet.test.tsx @@ -11,7 +11,7 @@ suite('ChipSet'); test('creates foundation', () => { - const wrapper = mount(); + const wrapper = mount(); assert.exists(wrapper.state().foundation); }); @@ -277,20 +277,20 @@ test('#removeChip calls #props.updateChips with array of remaining chips', () => }); test('#setCheckmarkWidth sets checkmark width', () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper.instance().setCheckmarkWidth(coerceForTesting({width: 20})); assert.equal(wrapper.instance().checkmarkWidth, 20); }); test('#setCheckmarkWidth does not set checkmark width if checkmark width is already set', () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper.instance().checkmarkWidth = 20; wrapper.instance().setCheckmarkWidth(coerceForTesting({width: 40})); assert.equal(wrapper.instance().checkmarkWidth, 20); }); test('#computeBoundingRect returns width and height', () => { - const wrapper = shallow(); + const wrapper = shallow(); const chipWidth = 20; const chipHeight = 50; const chipElement = coerceForTesting({ @@ -302,7 +302,7 @@ test('#computeBoundingRect returns width and height', () => { }); test('#computeBoundingRect returns width and height', () => { - const wrapper = shallow(); + const wrapper = shallow(); const chipWidth = 20; const chipHeight = 50; wrapper.instance().checkmarkWidth = 20; @@ -349,7 +349,7 @@ test('#chip.props.handleSelect calls #foundation.handleChipSelection', () => { }); test('chip is rendered with handleRemove method', () => { - const wrapper = mount(); + const wrapper = mount(); wrapper.instance().handleRemove = coerceForTesting<(chipId: string) => void>(td.func()); wrapper.setProps({children: }); const chip = wrapper.children().props().children[0]; @@ -415,7 +415,7 @@ test('chip is rendered with computeBoundingRect method prop if is not filter var }); test('#componentWillUnmount destroys foundation', () => { - const wrapper = shallow(); + const wrapper = shallow(); const foundation = wrapper.state().foundation; foundation.destroy = td.func(); wrapper.unmount();