From b30f5e2e5b6e0012be45f23791133e34fceae74b Mon Sep 17 00:00:00 2001 From: Rachel Friedman Date: Mon, 14 Jan 2019 11:19:21 -0800 Subject: [PATCH] feat(chips): Move logic for calculating chip bounding rect into a foundation method (#4243) BREAKING CHANGE: Adds 3 new chips adapter methods: hasLeadingIcon, getRootBoundingClientRect, and getCheckmarkBoundingClientRect. Also adds a new foundation method: getDimensions. --- packages/mdc-chips/README.md | 4 ++ packages/mdc-chips/chip/adapter.js | 18 +++++++++ packages/mdc-chips/chip/foundation.js | 19 ++++++++++ packages/mdc-chips/chip/index.js | 27 +++++--------- .../mdc-chips/mdc-chip.foundation.test.js | 36 +++++++++++++++++- test/unit/mdc-chips/mdc-chip.test.js | 37 +++++++++++++++++++ 6 files changed, 123 insertions(+), 18 deletions(-) diff --git a/packages/mdc-chips/README.md b/packages/mdc-chips/README.md index a339c2cebea..2e43038124c 100644 --- a/packages/mdc-chips/README.md +++ b/packages/mdc-chips/README.md @@ -334,6 +334,9 @@ Method Signature | Description `notifyRemoval() => void` | Notifies the Chip Set that the chip will be removed\*\*\* `getComputedStyleValue(propertyName: string) => string` | Returns the computed property value of the given style property on the root element `setStyleProperty(propertyName: string, value: string) => void` | Sets the property value of the given style property on the root element +`hasLeadingIcon() => boolean` | Returns whether the chip has a leading icon +`getRootBoundingClientRect() => ClientRect` | Returns the bounding client rect of the root element +`getCheckmarkBoundingClientRect() => ?ClientRect` | Returns the bounding client rect of the checkmark element or null if it doesn't exist > \*_NOTE_: `notifyInteraction` and `notifyTrailingIconInteraction` must pass along the target chip's ID, and must be observable by the parent `mdc-chip-set` element (e.g. via DOM event bubbling). @@ -359,6 +362,7 @@ Method Signature | Description `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 +`getDimensions() => ClientRect` | Returns the dimensions of the chip. This is used for applying ripple to the chip. `beginExit() => void` | Begins the exit animation which leads to removal of the chip `handleInteraction(evt: Event) => void` | Handles an interaction event on the root element `handleTransitionEnd(evt: Event) => void` | Handles a transition end event on the root element diff --git a/packages/mdc-chips/chip/adapter.js b/packages/mdc-chips/chip/adapter.js index 603a35fcd0b..a27b5520aea 100644 --- a/packages/mdc-chips/chip/adapter.js +++ b/packages/mdc-chips/chip/adapter.js @@ -109,6 +109,24 @@ class MDCChipAdapter { * @param {string} value */ setStyleProperty(propertyName, value) {} + + /** + * Returns whether the chip has a leading icon. + * @return {boolean} + */ + hasLeadingIcon() {} + + /** + * Returns the bounding client rect of the root element. + * @return {!ClientRect} + */ + getRootBoundingClientRect() {} + + /** + * Returns the bounding client rect of the checkmark element or null if it doesn't exist. + * @return {?ClientRect} + */ + getCheckmarkBoundingClientRect() {} } export default MDCChipAdapter; diff --git a/packages/mdc-chips/chip/foundation.js b/packages/mdc-chips/chip/foundation.js index 1090305aa96..54b53be4490 100644 --- a/packages/mdc-chips/chip/foundation.js +++ b/packages/mdc-chips/chip/foundation.js @@ -60,6 +60,9 @@ class MDCChipFoundation extends MDCFoundation { notifyRemoval: () => {}, getComputedStyleValue: () => {}, setStyleProperty: () => {}, + hasLeadingIcon: () => {}, + getRootBoundingClientRect: () => {}, + getCheckmarkBoundingClientRect: () => {}, }); } @@ -109,6 +112,22 @@ class MDCChipFoundation extends MDCFoundation { this.shouldRemoveOnTrailingIconClick_ = shouldRemove; } + /** @return {!ClientRect} */ + getDimensions() { + // When a chip has a checkmark and not a leading icon, the bounding rect changes in size depending on the current + // size of the checkmark. + if (!this.adapter_.hasLeadingIcon() && this.adapter_.getCheckmarkBoundingClientRect() !== null) { + const height = this.adapter_.getRootBoundingClientRect().height; + // The checkmark's width is initially set to 0, so use the checkmark's height as a proxy since the checkmark + // should always be square. + const width = + this.adapter_.getRootBoundingClientRect().width + this.adapter_.getCheckmarkBoundingClientRect().height; + return /** @type {!ClientRect} */ ({height, width}); + } else { + return this.adapter_.getRootBoundingClientRect(); + } + } + /** * Begins the exit animation which leads to removal of the chip. */ diff --git a/packages/mdc-chips/chip/index.js b/packages/mdc-chips/chip/index.js index ea3dba757de..3d29d710aa1 100644 --- a/packages/mdc-chips/chip/index.js +++ b/packages/mdc-chips/chip/index.js @@ -47,6 +47,8 @@ class MDCChip extends MDCComponent { this.leadingIcon_; /** @private {?Element} */ this.trailingIcon_; + /** @private {?Element} */ + this.checkmark_; /** @private {!MDCRipple} */ this.ripple_; @@ -71,24 +73,12 @@ class MDCChip extends MDCComponent { this.id = this.root_.id; this.leadingIcon_ = this.root_.querySelector(strings.LEADING_ICON_SELECTOR); this.trailingIcon_ = this.root_.querySelector(strings.TRAILING_ICON_SELECTOR); + this.checkmark_ = this.root_.querySelector(strings.CHECKMARK_SELECTOR); - // Adjust ripple size for chips with animated growing width. This applies when filter chips without - // a leading icon are selected, and a leading checkmark will cause the chip width to expand. - const checkmarkEl = this.root_.querySelector(strings.CHECKMARK_SELECTOR); - if (checkmarkEl && !this.leadingIcon_) { - const adapter = Object.assign(MDCRipple.createAdapter(this), { - computeBoundingRect: () => { - const height = this.root_.getBoundingClientRect().height; - // The checkmark's width is initially set to 0, so use the checkmark's height as a proxy since the - // checkmark should always be square. - const width = this.root_.getBoundingClientRect().width + checkmarkEl.getBoundingClientRect().height; - return {height, width}; - }, - }); - this.ripple_ = rippleFactory(this.root_, new MDCRippleFoundation(adapter)); - } else { - this.ripple_ = rippleFactory(this.root_); - } + const adapter = Object.assign(MDCRipple.createAdapter(this), { + computeBoundingRect: () => this.foundation_.getDimensions(), + }); + this.ripple_ = rippleFactory(this.root_, new MDCRippleFoundation(adapter)); } initialSyncWithDOM() { @@ -192,6 +182,9 @@ class MDCChip extends MDCComponent { this.emit(strings.REMOVAL_EVENT, {chipId: this.id, root: this.root_}, true /* shouldBubble */), getComputedStyleValue: (propertyName) => window.getComputedStyle(this.root_).getPropertyValue(propertyName), setStyleProperty: (propertyName, value) => this.root_.style.setProperty(propertyName, value), + hasLeadingIcon: () => !!this.leadingIcon_, + getRootBoundingClientRect: () => this.root_.getBoundingClientRect(), + getCheckmarkBoundingClientRect: () => this.checkmark_ ? this.checkmark_.getBoundingClientRect() : null, }))); } diff --git a/test/unit/mdc-chips/mdc-chip.foundation.test.js b/test/unit/mdc-chips/mdc-chip.foundation.test.js index 1f18ef616a5..7d221897f53 100644 --- a/test/unit/mdc-chips/mdc-chip.foundation.test.js +++ b/test/unit/mdc-chips/mdc-chip.foundation.test.js @@ -46,7 +46,8 @@ test('defaultAdapter returns a complete adapter implementation', () => { 'addClass', 'removeClass', 'hasClass', 'addClassToLeadingIcon', 'removeClassFromLeadingIcon', 'eventTargetHasClass', 'notifyInteraction', 'notifyTrailingIconInteraction', 'notifyRemoval', 'notifySelection', - 'getComputedStyleValue', 'setStyleProperty', + 'getComputedStyleValue', 'setStyleProperty', 'hasLeadingIcon', + 'getRootBoundingClientRect', 'getCheckmarkBoundingClientRect', ]); }); @@ -88,6 +89,39 @@ test('#setSelected removes calls adapter.notifySelection when selected is false' td.verify(mockAdapter.notifySelection(false)); }); +test('#getDimensions returns adapter.getRootBoundingClientRect when there is no checkmark bounding rect', () => { + const {foundation, mockAdapter} = setupTest(); + td.when(mockAdapter.getCheckmarkBoundingClientRect()).thenReturn(null); + const boundingRect = {width: 10, height: 10}; + td.when(mockAdapter.getRootBoundingClientRect()).thenReturn(boundingRect); + + assert.strictEqual(foundation.getDimensions(), boundingRect); +}); + +test('#getDimensions factors in the checkmark bounding rect when it exists and there is no leading icon', () => { + const {foundation, mockAdapter} = setupTest(); + const boundingRect = {width: 10, height: 10}; + const checkmarkBoundingRect = {width: 5, height: 5}; + td.when(mockAdapter.getCheckmarkBoundingClientRect()).thenReturn(checkmarkBoundingRect); + td.when(mockAdapter.getRootBoundingClientRect()).thenReturn(boundingRect); + td.when(mockAdapter.hasLeadingIcon()).thenReturn(false); + + const dimensions = foundation.getDimensions(); + assert.equal(dimensions.height, boundingRect.height); + assert.equal(dimensions.width, boundingRect.width + checkmarkBoundingRect.height); +}); + +test('#getDimensions returns adapter.getRootBoundingClientRect when there is a checkmark and a leading icon', () => { + const {foundation, mockAdapter} = setupTest(); + const checkmarkBoundingRect = {width: 5, height: 5}; + td.when(mockAdapter.getCheckmarkBoundingClientRect()).thenReturn(checkmarkBoundingRect); + const boundingRect = {width: 10, height: 10}; + td.when(mockAdapter.getRootBoundingClientRect()).thenReturn(boundingRect); + td.when(mockAdapter.hasLeadingIcon()).thenReturn(true); + + assert.strictEqual(foundation.getDimensions(), boundingRect); +}); + test(`#beginExit adds ${cssClasses.CHIP_EXIT} class`, () => { const {foundation, mockAdapter} = setupTest(); foundation.beginExit(); diff --git a/test/unit/mdc-chips/mdc-chip.test.js b/test/unit/mdc-chips/mdc-chip.test.js index 899a4c599b1..a88cc5c4611 100644 --- a/test/unit/mdc-chips/mdc-chip.test.js +++ b/test/unit/mdc-chips/mdc-chip.test.js @@ -29,6 +29,8 @@ import td from 'testdouble'; import {MDCRipple} from '../../../packages/mdc-ripple/index'; import {MDCChip, MDCChipFoundation} from '../../../packages/mdc-chips/chip/index'; +const {CHECKMARK_SELECTOR} = MDCChipFoundation.strings; + const getFixture = () => bel`
Chip content
@@ -266,6 +268,41 @@ test('adapter#setStyleProperty sets a style property on the root element', () => assert.equal(root.style.getPropertyValue('color'), color); }); +test('adapter#hasLeadingIcon returns true if the chip has a leading icon', () => { + const root = getFixtureWithCheckmark(); + addLeadingIcon(root); + const component = new MDCChip(root); + + assert.isTrue(component.getDefaultFoundation().adapter_.hasLeadingIcon()); +}); + +test('adapter#hasLeadingIcon returns false if the chip does not have a leading icon', () => { + const {component} = setupTest(); + assert.isFalse(component.getDefaultFoundation().adapter_.hasLeadingIcon()); +}); + +test('adapter#getRootBoundingClientRect calls getBoundingClientRect on the root element', () => { + const {root, component} = setupTest(); + root.getBoundingClientRect = td.func(); + component.getDefaultFoundation().adapter_.getRootBoundingClientRect(); + td.verify(root.getBoundingClientRect(), {times: 1}); +}); + +test('adapter#getCheckmarkBoundingClientRect calls getBoundingClientRect on the checkmark element if it exists', () => { + const root = getFixtureWithCheckmark(); + const component = new MDCChip(root); + const checkmark = root.querySelector(CHECKMARK_SELECTOR); + + checkmark.getBoundingClientRect = td.func(); + component.getDefaultFoundation().adapter_.getCheckmarkBoundingClientRect(); + td.verify(checkmark.getBoundingClientRect(), {times: 1}); +}); + +test('adapter#getCheckmarkBoundingClientRect returns null when there is no checkmark element', () => { + const {component} = setupTest(); + assert.isNull(component.getDefaultFoundation().adapter_.getCheckmarkBoundingClientRect()); +}); + test('#get selected proxies to foundation', () => { const {component, mockFoundation} = setupMockFoundationTest(); assert.equal(component.selected, mockFoundation.isSelected());