From 68006fb75fbf66f0ace18be6dde6a53707892765 Mon Sep 17 00:00:00 2001 From: Bonnie Zhou Date: Thu, 12 Apr 2018 17:46:35 -0700 Subject: [PATCH] feat(chips): Add animation for entry chips (#2543) BREAKING CHANGE: layout() method added to MDCChipAdapter. --- demos/chips.html | 5 ++--- packages/mdc-chips/README.md | 2 ++ packages/mdc-chips/chip-set/mdc-chip-set.scss | 17 +++++++++++++++++ packages/mdc-chips/chip/adapter.js | 5 +++++ packages/mdc-chips/chip/constants.js | 3 ++- packages/mdc-chips/chip/foundation.js | 16 ++++++++++++++++ packages/mdc-chips/chip/index.js | 1 + test/unit/mdc-chips/mdc-chip-set.test.js | 18 ++++++++++++++---- .../unit/mdc-chips/mdc-chip.foundation.test.js | 16 +++++++++++++++- 9 files changed, 74 insertions(+), 9 deletions(-) diff --git a/demos/chips.html b/demos/chips.html index 55b577c99fa..37725cc4caa 100644 --- a/demos/chips.html +++ b/demos/chips.html @@ -254,9 +254,8 @@

Custom theme

entryInput.value = ''; } }; - ['click', 'keydown'].forEach(function(evtType) { - entryButton.addEventListener(evtType, addChip); - }); + entryButton.addEventListener('click', addChip); + entryInput.addEventListener('keydown', addChip); }); diff --git a/packages/mdc-chips/README.md b/packages/mdc-chips/README.md index 3f1ca1df544..7742c2ad560 100644 --- a/packages/mdc-chips/README.md +++ b/packages/mdc-chips/README.md @@ -135,6 +135,7 @@ To pre-select filter chips that have a leading icon, also add the class `mdc-chi CSS Class | Description --- | --- `mdc-chip-set` | Mandatory. Indicates the set that the chip belongs to. +`mdc-chip-set--entry` | Optional. Indicates that the chips in the set are entry chips, which enable user input by converting text into chips. `mdc-chip-set--choice` | Optional. Indicates that the chips in the set are choice chips, which allow a single selection from a set of options. `mdc-chip-set--filter` | Optional. Indicates that the chips in the set are filter chips, which allow multiple selection from a set of options. `mdc-chip` | Mandatory. @@ -216,6 +217,7 @@ Method Signature | Description `deregisterTrailingIconInteractionHandler(evtType: string, handler: EventListener) => void` | Deregisters an event listener on the trailing icon element `notifyInteraction() => void` | Emits a custom event `MDCChip:interaction` denoting the chip has been interacted with `notifyTrailingIconInteraction() => void` | Emits a custom event `MDCChip:trailingIconInteraction` denoting the chip's trailing icon has been interacted with +`layout() => void` | Recomputes all dimensions and positions for the ripple element > _NOTE_: The custom events emitted by `notifyInteraction` and `notifyTrailingIconInteraction` must pass along the target chip in its event `detail`, as well as bubble to the parent `mdc-chip-set` element. diff --git a/packages/mdc-chips/chip-set/mdc-chip-set.scss b/packages/mdc-chips/chip-set/mdc-chip-set.scss index 7a72cd20971..7b2bae02d53 100644 --- a/packages/mdc-chips/chip-set/mdc-chip-set.scss +++ b/packages/mdc-chips/chip-set/mdc-chip-set.scss @@ -14,8 +14,21 @@ // limitations under the License. // +@import "@material/animation/variables"; @import "../mixins"; +@keyframes mdc-chip-entry { + from { + transform: scale(.8); + opacity: .4; + } + + to { + transform: scale(1); + opacity: 1; + } +} + .mdc-chip-set { @include mdc-chip-set-spacing(8px); @@ -23,3 +36,7 @@ flex-wrap: wrap; box-sizing: border-box; } + +.mdc-chip-set--entry .mdc-chip { + animation: mdc-chip-entry 100ms $mdc-animation-deceleration-curve-timing-function; +} diff --git a/packages/mdc-chips/chip/adapter.js b/packages/mdc-chips/chip/adapter.js index 3fb59d6cc82..459ad6f5380 100644 --- a/packages/mdc-chips/chip/adapter.js +++ b/packages/mdc-chips/chip/adapter.js @@ -106,6 +106,11 @@ class MDCChipAdapter { * interacted with (typically on click or keydown). */ notifyTrailingIconInteraction() {} + + /** + * Recomputes all dimensions and positions for the ripple element. + */ + layout() {} } export default MDCChipAdapter; diff --git a/packages/mdc-chips/chip/constants.js b/packages/mdc-chips/chip/constants.js index 059abaa5850..511f5ce2482 100644 --- a/packages/mdc-chips/chip/constants.js +++ b/packages/mdc-chips/chip/constants.js @@ -17,10 +17,11 @@ /** @enum {string} */ const strings = { + ENTRY_ANIMATION_NAME: 'mdc-chip-entry', INTERACTION_EVENT: 'MDCChip:interaction', + TRAILING_ICON_INTERACTION_EVENT: 'MDCChip:trailingIconInteraction', CHECKMARK_SELECTOR: '.mdc-chip__checkmark', LEADING_ICON_SELECTOR: '.mdc-chip__icon--leading', - TRAILING_ICON_INTERACTION_EVENT: 'MDCChip:trailingIconInteraction', TRAILING_ICON_SELECTOR: '.mdc-chip__icon--trailing', }; diff --git a/packages/mdc-chips/chip/foundation.js b/packages/mdc-chips/chip/foundation.js index a3729cc584c..b6dd3a4f443 100644 --- a/packages/mdc-chips/chip/foundation.js +++ b/packages/mdc-chips/chip/foundation.js @@ -54,6 +54,7 @@ class MDCChipFoundation extends MDCFoundation { deregisterTrailingIconInteractionHandler: () => {}, notifyInteraction: () => {}, notifyTrailingIconInteraction: () => {}, + layout: () => {}, }); } @@ -68,6 +69,8 @@ class MDCChipFoundation extends MDCFoundation { /** @private {function(!Event): undefined} */ this.transitionEndHandler_ = (evt) => this.handleTransitionEnd_(evt); /** @private {function(!Event): undefined} */ + this.animationEndHandler_ = (evt) => this.handleAnimationEnd_(evt); + /** @private {function(!Event): undefined} */ this.trailingIconInteractionHandler_ = (evt) => this.handleTrailingIconInteraction_(evt); } @@ -76,6 +79,7 @@ class MDCChipFoundation extends MDCFoundation { this.adapter_.registerEventHandler(evtType, this.interactionHandler_); }); this.adapter_.registerEventHandler('transitionend', this.transitionEndHandler_); + this.adapter_.registerEventHandler('animationend', this.animationEndHandler_); ['click', 'keydown', 'touchstart', 'pointerdown', 'mousedown'].forEach((evtType) => { this.adapter_.registerTrailingIconInteractionHandler(evtType, this.trailingIconInteractionHandler_); }); @@ -86,6 +90,7 @@ class MDCChipFoundation extends MDCFoundation { this.adapter_.deregisterEventHandler(evtType, this.interactionHandler_); }); this.adapter_.deregisterEventHandler('transitionend', this.transitionEndHandler_); + this.adapter_.deregisterEventHandler('animationend', this.animationEndHandler_); ['click', 'keydown', 'touchstart', 'pointerdown', 'mousedown'].forEach((evtType) => { this.adapter_.deregisterTrailingIconInteractionHandler(evtType, this.trailingIconInteractionHandler_); }); @@ -138,6 +143,17 @@ class MDCChipFoundation extends MDCFoundation { } } + /** + * Handles an animation end event on the root element. + * @param {!Event} evt + */ + handleAnimationEnd_(evt) { + // The chip's ripple size must be recalculated after the entry animation. + if (evt.animationName === strings.ENTRY_ANIMATION_NAME) { + this.adapter_.layout(); + } + } + /** * Handles an interaction event on the trailing icon element. This is used to * prevent the ripple from activating on interaction with the trailing icon. diff --git a/packages/mdc-chips/chip/index.js b/packages/mdc-chips/chip/index.js index ddde612ad0a..5dcbae9341d 100644 --- a/packages/mdc-chips/chip/index.js +++ b/packages/mdc-chips/chip/index.js @@ -125,6 +125,7 @@ class MDCChip extends MDCComponent { notifyInteraction: () => this.emit(strings.INTERACTION_EVENT, {chip: this}, true /* shouldBubble */), notifyTrailingIconInteraction: () => this.emit( strings.TRAILING_ICON_INTERACTION_EVENT, {chip: this}, true /* shouldBubble */), + layout: () => this.ripple_.layout(), }))); } diff --git a/test/unit/mdc-chips/mdc-chip-set.test.js b/test/unit/mdc-chips/mdc-chip-set.test.js index 9c5c14bc082..513fc2481ef 100644 --- a/test/unit/mdc-chips/mdc-chip-set.test.js +++ b/test/unit/mdc-chips/mdc-chip-set.test.js @@ -51,10 +51,10 @@ class FakeChip { test('#constructor instantiates child chip components', () => { const root = getFixture(); const component = new MDCChipSet(root, undefined, (el) => new FakeChip(el)); - assert.isOk(component.chips.length === 3 && - component.chips[0] instanceof FakeChip && - component.chips[1] instanceof FakeChip && - component.chips[2] instanceof FakeChip); + assert.equal(component.chips.length, 3); + assert.instanceOf(component.chips[0], FakeChip); + assert.instanceOf(component.chips[1], FakeChip); + assert.instanceOf(component.chips[2], FakeChip); }); test('#destroy cleans up child chip components', () => { @@ -66,6 +66,16 @@ test('#destroy cleans up child chip components', () => { td.verify(component.chips[2].destroy()); }); +test('#addChip creates and adds a new chip to the DOM', () => { + const root = getFixture(); + const component = new MDCChipSet(root, undefined, (el) => new FakeChip(el)); + component.initialSyncWithDOM(); + component.addChip('hello world'); + td.verify(component.foundation_.addChip('hello world')); + assert.equal(component.chips.length, 4); + assert.instanceOf(component.chips[3], FakeChip); +}); + class FakeSelectedChip { constructor() { this.foundation = td.object({ diff --git a/test/unit/mdc-chips/mdc-chip.foundation.test.js b/test/unit/mdc-chips/mdc-chip.foundation.test.js index c2e7dd454ef..80663876ff5 100644 --- a/test/unit/mdc-chips/mdc-chip.foundation.test.js +++ b/test/unit/mdc-chips/mdc-chip.foundation.test.js @@ -38,7 +38,7 @@ test('defaultAdapter returns a complete adapter implementation', () => { 'addClass', 'removeClass', 'hasClass', 'addClassToLeadingIcon', 'removeClassFromLeadingIcon', 'eventTargetHasClass', 'registerEventHandler', 'deregisterEventHandler', 'registerTrailingIconInteractionHandler', 'deregisterTrailingIconInteractionHandler', - 'notifyInteraction', 'notifyTrailingIconInteraction', + 'notifyInteraction', 'notifyTrailingIconInteraction', 'layout', ]); }); @@ -109,6 +109,20 @@ test('on click, emit custom event', () => { td.verify(mockAdapter.notifyInteraction()); }); +test('on entry animation end, call layout', () => { + const {foundation, mockAdapter} = setupTest(); + const handlers = captureHandlers(mockAdapter, 'registerEventHandler'); + const mockEvt = { + type: 'animationend', + animationName: 'mdc-chip-entry', + }; + + foundation.init(); + handlers.animationend(mockEvt); + + td.verify(mockAdapter.layout()); +}); + test(`on leading icon opacity transition end, add ${cssClasses.HIDDEN_LEADING_ICON}` + 'class to leading icon if chip is selected', () => { const {foundation, mockAdapter} = setupTest();