diff --git a/demos/chips.html b/demos/chips.html index 4c06dad05cf..6bcdcb7369b 100644 --- a/demos/chips.html +++ b/demos/chips.html @@ -78,6 +78,19 @@

Input Chips

cancel + +
+
+ +
+ + + +
+
+
+ +
@@ -243,12 +256,34 @@

Custom theme

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, function(chipEl) { + 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'); @@ -267,7 +302,7 @@

Custom theme

} function addInputChip(evt) { - if ((evt.type === 'click' || evt.key === 'Enter' || evt.keyCode === 13) && + if ((evt.type === 'click' || evt.key === 'Enter' || evt.keyCode === 13) && input.value !== '') { var leadingIcon = document.querySelector('.input-leading-icon').cloneNode(true); var trailingIcon = document.querySelector('.input-trailing-icon').cloneNode(true); @@ -280,6 +315,12 @@

Custom theme

} }; + function confirmChipRemoval(evt) { + if (confirm('Are you sure you want to remove this chip?')) { + evt.detail.chip.beginExit(); + } + } + function removeChip(evt) { const root = evt.detail.root; root && inputChipSetEl.removeChild(root); @@ -290,11 +331,6 @@

Custom theme

const lastChip = inputChipSetComponent.chips[lastChipIndex]; lastChip.beginExit(); }; - - inputButton.addEventListener('click', addInputChip); - input.addEventListener('keydown', addInputChip); - deleteButton.addEventListener('click', deleteLastChip); - inputChipSetEl.addEventListener('MDCChip:removal', removeChip); }); diff --git a/demos/chips.scss b/demos/chips.scss index cecfd4725cd..65c92f334ea 100644 --- a/demos/chips.scss +++ b/demos/chips.scss @@ -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; diff --git a/packages/mdc-chips/README.md b/packages/mdc-chips/README.md index 0d5c941f311..6b71742373f 100644 --- a/packages/mdc-chips/README.md +++ b/packages/mdc-chips/README.md @@ -211,14 +211,17 @@ 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 +`beginExit() => void` | Proxies to the foundation's `beginExit` method Property | Value Type | Description --- | --- | --- +`foundation` | MDCChipFoundation | The foundation +`shouldRemoveOnTrailingIconClick` | Boolean | Proxies to the foundation's `getShouldRemoveOnTrailingIconClick`/`setShouldRemoveOnTrailingIconClick` methods `ripple` | `MDCRipple` | The `MDCRipple` instance for the root element that `MDCChip` initializes +>_NOTE_: If `shouldRemoveOnTrailingIconClick` is set to false, you must manually call `beginExit()` on the chip to remove it. + #### `MDCChipSet` Method Signature | Description @@ -276,6 +279,9 @@ Method Signature | Description --- | --- `isSelected() => boolean` | Returns true if the chip is selected `setSelected(selected: boolean) => void` | Sets the chip's selected state +`getShouldRemoveOnTrailingIconClick() => boolean` | Returns whether a trailing icon click should trigger exit/removal of the chip +`setShouldRemoveOnTrailingIconClick(shouldRemove: boolean) => void` | Sets whether a trailing icon click should trigger exit/removal of the chip +`beginExit() => void` | Begins the exit animation which leads to removal of the chip #### `MDCChipSetFoundation` diff --git a/packages/mdc-chips/chip/foundation.js b/packages/mdc-chips/chip/foundation.js index fcffccf76ee..c452162e402 100644 --- a/packages/mdc-chips/chip/foundation.js +++ b/packages/mdc-chips/chip/foundation.js @@ -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} */ @@ -112,6 +117,27 @@ class MDCChipFoundation extends MDCFoundation { } } + /** + * @return {boolean} + */ + getShouldRemoveOnTrailingIconClick() { + return this.shouldRemoveOnTrailingIconClick_; + } + + /** + * @param {boolean} shouldRemove + */ + setShouldRemoveOnTrailingIconClick(shouldRemove) { + this.shouldRemoveOnTrailingIconClick_ = shouldRemove; + } + + /** + * Begins the exit animation which leads to removal of the chip. + */ + beginExit() { + this.adapter_.addClass(cssClasses.CHIP_EXIT); + } + /** * Handles an interaction event on the root element. * @param {!Event} evt @@ -175,7 +201,9 @@ 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_) { + this.beginExit(); + } } } } diff --git a/packages/mdc-chips/chip/index.js b/packages/mdc-chips/chip/index.js index 43223c27ee0..310cb9bc00f 100644 --- a/packages/mdc-chips/chip/index.js +++ b/packages/mdc-chips/chip/index.js @@ -20,7 +20,7 @@ import {MDCRipple, MDCRippleFoundation} from '@material/ripple/index'; import MDCChipAdapter from './adapter'; import MDCChipFoundation from './foundation'; -import {strings, cssClasses} from './constants'; +import {strings} from './constants'; /** * @extends {MDCComponent} @@ -86,7 +86,7 @@ class MDCChip extends MDCComponent { * Begins the exit animation which leads to removal of the chip. */ beginExit() { - this.root_.classList.add(cssClasses.CHIP_EXIT); + this.foundation_.beginExit(); } /** @@ -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_.getShouldRemoveOnTrailingIconClick(); + } + + /** + * 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} */ diff --git a/test/unit/mdc-chips/mdc-chip.foundation.test.js b/test/unit/mdc-chips/mdc-chip.foundation.test.js index 3c6feaddef5..908475fecf9 100644 --- a/test/unit/mdc-chips/mdc-chip.foundation.test.js +++ b/test/unit/mdc-chips/mdc-chip.foundation.test.js @@ -99,6 +99,12 @@ test('#setSelected removes mdc-chip--selected class if false', () => { td.verify(mockAdapter.removeClass(cssClasses.SELECTED)); }); +test(`#beginExit adds ${cssClasses.CHIP_EXIT} class`, () => { + const {foundation, mockAdapter} = setupTest(); + foundation.beginExit(); + td.verify(mockAdapter.addClass(cssClasses.CHIP_EXIT)); +}); + test('on click, emit custom event', () => { const {foundation, mockAdapter} = setupTest(); const handlers = captureHandlers(mockAdapter, 'registerEventHandler'); @@ -234,6 +240,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.getShouldRemoveOnTrailingIconClick()); 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.getShouldRemoveOnTrailingIconClick()); + td.verify(mockAdapter.addClass(cssClasses.CHIP_EXIT), {times: 0}); + td.verify(mockEvt.stopPropagation()); + } +); diff --git a/test/unit/mdc-chips/mdc-chip.test.js b/test/unit/mdc-chips/mdc-chip.test.js index 08143fbf517..1b5298609e1 100644 --- a/test/unit/mdc-chips/mdc-chip.test.js +++ b/test/unit/mdc-chips/mdc-chip.test.js @@ -203,8 +203,19 @@ test('#isSelected proxies to foundation', () => { td.verify(mockFoundation.isSelected()); }); -test(`#beginExit adds ${MDCChipFoundation.cssClasses.CHIP_EXIT} class`, () => { - const {component, root} = setupMockFoundationTest(); +test('#get shouldRemoveOnTrailingIconClick proxies to foundation', () => { + const {component, mockFoundation} = setupMockFoundationTest(); + assert.equal(component.shouldRemoveOnTrailingIconClick, mockFoundation.getShouldRemoveOnTrailingIconClick()); +}); + +test('#set shouldRemoveOnTrailingIconClick proxies to foundation', () => { + const {component, mockFoundation} = setupMockFoundationTest(); + component.shouldRemoveOnTrailingIconClick = false; + td.verify(mockFoundation.setShouldRemoveOnTrailingIconClick(false)); +}); + +test('#beginExit proxies to foundation', () => { + const {component, mockFoundation} = setupMockFoundationTest(); component.beginExit(); - assert.isTrue(root.classList.contains(MDCChipFoundation.cssClasses.CHIP_EXIT)); + td.verify(mockFoundation.beginExit()); });