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

feat(chips): Make chip exit on trailing icon click optional #2893

Merged
merged 15 commits into from
Jun 15, 2018
Merged
47 changes: 42 additions & 5 deletions demos/chips.html
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ <h2>Input Chips</h2>
<i class="material-icons mdc-chip__icon mdc-chip__icon--trailing" tabindex="0" role="button">cancel</i>
</div>
</div>

<div class="mdc-form-field">
<div class="mdc-checkbox">
<input type="checkbox" class="mdc-checkbox__native-control" id="toggle-trailing-icon" aria-labelledby="toggle-disabled-label">
<div class="mdc-checkbox__background">
<svg class="mdc-checkbox__checkmark" viewBox="0 0 24 24">
<path class="mdc-checkbox__checkmark-path" fill="none" stroke="white" d="M1.73,12.91 8.1,19.28 22.79,4.59">
</svg>
<div class="mdc-checkbox__mixedmark"></div>
</div>
</div>
<label for="toggle-trailing-icon" id="toggle-trailing-icon-label">Require confirmation before removing chip</label>
</div>
</section>

<section class="example">
Expand Down Expand Up @@ -243,12 +256,34 @@ <h2>Custom theme</h2>
var input = document.getElementById('input-chip-set-input');
var inputButton = document.getElementById('input-chip-set-button');
var deleteButton = document.getElementById('input-chip-set-delete-button');
var trailingIconToggle = document.getElementById('toggle-trailing-icon');

[].forEach.call(chipSets, function(chipSet) {
mdc.chips.MDCChipSet.attachTo(chipSet);
});
var inputChipSetComponent = mdc.chips.MDCChipSet.attachTo(inputChipSetEl);

inputButton.addEventListener('click', addInputChip);
input.addEventListener('keydown', addInputChip);
deleteButton.addEventListener('click', deleteLastChip);
inputChipSetEl.addEventListener('MDCChip:removal', removeChip);
trailingIconToggle.addEventListener('change', resetInputChipSet);

function resetInputChipSet() {
inputChipSetComponent.destroy();
if (this.checked) {
inputChipSetComponent = new mdc.chips.MDCChipSet(inputChipSetEl, undefined, (chipEl) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use function, not => (IE 11)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

argh, nice catch

const chip = new mdc.chips.MDCChip(chipEl);
chip.shouldRemoveOnTrailingIconClick = false;
return chip;
});
inputChipSetEl.addEventListener('MDCChip:trailingIconInteraction', confirmChipRemoval);
} else {
inputChipSetComponent = mdc.chips.MDCChipSet.attachTo(inputChipSetEl);
inputChipSetEl.removeEventListener('MDCChip:trailingIconInteraction', confirmChipRemoval);
}
}

function createChipEl(text, leadingIcon, trailingIcon) {
const chipTextEl = document.createElement('div');
chipTextEl.classList.add('mdc-chip__text');
Expand Down Expand Up @@ -280,6 +315,13 @@ <h2>Custom theme</h2>
}
};

function confirmChipRemoval(evt) {
const {chip} = evt.detail;
if (confirm('Are you sure you want to remove this chip?')) {
chip.beginExit();
}
}

function removeChip(evt) {
const root = evt.detail.root;
root && inputChipSetEl.removeChild(root);
Expand All @@ -290,11 +332,6 @@ <h2>Custom theme</h2>
const lastChip = inputChipSetComponent.chips[lastChipIndex];
lastChip.beginExit();
};

inputButton.addEventListener('click', addInputChip);
input.addEventListener('keydown', addInputChip);
deleteButton.addEventListener('click', deleteLastChip);
inputChipSetEl.addEventListener('MDCChip:removal', removeChip);
});
</script>
</body>
Expand Down
2 changes: 2 additions & 0 deletions demos/chips.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
@import "./common";
@import "../packages/mdc-chips/mdc-chips";
@import "../packages/mdc-button/mdc-button";
@import "../packages/mdc-checkbox/mdc-checkbox";
@import "../packages/mdc-form-field/mdc-form-field";

#input-chip-icon-clones {
display: none;
Expand Down
5 changes: 4 additions & 1 deletion packages/mdc-chips/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,13 @@ To use the `MDCChip` and `MDCChipSet` classes, [import](../../docs/importing-js.

Method Signature | Description
--- | ---
`get foundation() => MDCChipFoundation` | Returns the foundation
`isSelected() => boolean` | Proxies to the foundation's `isSelected` method
`beginExit() => void` | Begins the exit animation which leads to removal of the chip

Property | Value Type | Description
--- | --- | ---
`foundation` | MDCChipFoundation | The foundation
`shouldRemoveOnTrailingIconClick` | Boolean | Proxies to the foundation's `shouldRemoveOnTrailingIconClick`/`setShouldRemoveOnTrailingIconClick` methods
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to add get to the foundation method name in the description.

Also, an additional thought: add a sentence here explaining that if this is set to false, manually call beginExit on the chip component to control when to remove it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, added as a NOTE to stick with having one line per table entry

`ripple` | `MDCRipple` | The `MDCRipple` instance for the root element that `MDCChip` initializes

#### `MDCChipSet`
Expand Down Expand Up @@ -276,6 +277,8 @@ Method Signature | Description
--- | ---
`isSelected() => boolean` | Returns true if the chip is selected
`setSelected(selected: boolean) => void` | Sets the chip's selected state
`shouldRemoveOnTrailingIconClick() => boolean` | Returns whether a trailing icon click should trigger exit/removal of the chip
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be getShouldRemove... to complement setShouldRemove...?

`setShouldRemoveOnTrailingIconClick(shouldRemove: boolean) => void` | Sets whether a trailing icon click should trigger exit/removal of the chip

#### `MDCChipSetFoundation`

Expand Down
24 changes: 23 additions & 1 deletion packages/mdc-chips/chip/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ class MDCChipFoundation extends MDCFoundation {
constructor(adapter) {
super(Object.assign(MDCChipFoundation.defaultAdapter, adapter));

/**
* Whether a trailing icon click should immediately trigger exit/removal of the chip.
* @private {boolean}
* */
this.shouldRemoveOnTrailingIconClick_ = true;
/** @private {function(!Event): undefined} */
this.interactionHandler_ = (evt) => this.handleInteraction_(evt);
/** @private {function(!Event): undefined} */
Expand Down Expand Up @@ -112,6 +117,20 @@ class MDCChipFoundation extends MDCFoundation {
}
}

/**
* @return {boolean}
*/
shouldRemoveOnTrailingIconClick() {
return this.shouldRemoveOnTrailingIconClick_;
}

/**
* @param {boolean} shouldRemove
*/
setShouldRemoveOnTrailingIconClick(shouldRemove) {
this.shouldRemoveOnTrailingIconClick_ = shouldRemove;
}

/**
* Handles an interaction event on the root element.
* @param {!Event} evt
Expand Down Expand Up @@ -175,7 +194,10 @@ class MDCChipFoundation extends MDCFoundation {
evt.stopPropagation();
if (evt.type === 'click' || evt.key === 'Enter' || evt.keyCode === 13) {
this.adapter_.notifyTrailingIconInteraction();
this.adapter_.addClass(cssClasses.CHIP_EXIT);
if (this.shouldRemoveOnTrailingIconClick_) {
// Begins the exit animation which leads to removal of the chip
this.adapter_.addClass(cssClasses.CHIP_EXIT);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It occurs to me that this is effectively doing the same thing that beginExit on the component is doing. Do you think we should create a beginExit foundation method and proxy to it from the component's method, so that both can reuse the same code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that but realized that's actually more code?
...But I guess it makes sense pattern-wise. Ok I'll update.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It probably is more code ultimately, but in this case the trade-off is avoiding repeating the same logic across modules which risks skew later.

}
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions packages/mdc-chips/chip/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,22 @@ class MDCChip extends MDCComponent {
return this.foundation_;
}

/**
* Returns whether a trailing icon click should trigger exit/removal of the chip.
* @return {boolean}
*/
get shouldRemoveOnTrailingIconClick() {
return this.foundation_.shouldRemoveOnTrailingIconClick();
}

/**
* Sets whether a trailing icon click should trigger exit/removal of the chip.
* @param {boolean} shouldRemove
*/
set shouldRemoveOnTrailingIconClick(shouldRemove) {
return this.foundation_.setShouldRemoveOnTrailingIconClick(shouldRemove);
}

/**
* @return {!MDCChipFoundation}
*/
Expand Down
34 changes: 34 additions & 0 deletions test/unit/mdc-chips/mdc-chip.foundation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,40 @@ test('on click in trailing icon, emit custom event', () => {
handlers.click(mockEvt);

td.verify(mockAdapter.notifyTrailingIconInteraction());
td.verify(mockEvt.stopPropagation());
});

test(`on click in trailing icon, add ${cssClasses.CHIP_EXIT} class by default`, () => {
const {foundation, mockAdapter} = setupTest();
const handlers = captureHandlers(mockAdapter, 'registerTrailingIconInteractionHandler');
const mockEvt = {
type: 'click',
stopPropagation: td.func('stopPropagation'),
};

foundation.init();
handlers.click(mockEvt);

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

test(`on click in trailing icon, do not add ${cssClasses.CHIP_EXIT} class if shouldRemoveOnTrailingIconClick_ is false`,
() => {
const {foundation, mockAdapter} = setupTest();
const handlers = captureHandlers(mockAdapter, 'registerTrailingIconInteractionHandler');
const mockEvt = {
type: 'click',
stopPropagation: td.func('stopPropagation'),
};

foundation.init();
foundation.setShouldRemoveOnTrailingIconClick(false);
handlers.click(mockEvt);

assert.isFalse(foundation.shouldRemoveOnTrailingIconClick());
td.verify(mockAdapter.addClass(cssClasses.CHIP_EXIT), {times: 0});
td.verify(mockEvt.stopPropagation());
}
);
11 changes: 11 additions & 0 deletions test/unit/mdc-chips/mdc-chip.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,17 @@ test('#isSelected proxies to foundation', () => {
td.verify(mockFoundation.isSelected());
});

test('#get shouldRemoveOnTrailingIconClick proxies to foundation', () => {
const {component, mockFoundation} = setupMockFoundationTest();
assert.equal(component.shouldRemoveOnTrailingIconClick, mockFoundation.shouldRemoveOnTrailingIconClick());
});

test('#set shouldRemoveOnTrailingIconClick proxies to foundation', () => {
const {component, mockFoundation} = setupMockFoundationTest();
component.shouldRemoveOnTrailingIconClick = false;
td.verify(mockFoundation.setShouldRemoveOnTrailingIconClick(false));
});

test(`#beginExit adds ${MDCChipFoundation.cssClasses.CHIP_EXIT} class`, () => {
const {component, root} = setupMockFoundationTest();
component.beginExit();
Expand Down