Skip to content

Commit

Permalink
feat(chips): Move logic for calculating chip bounding rect into a fou…
Browse files Browse the repository at this point in the history
…ndation method (#4243)

BREAKING CHANGE: Adds 3 new chips adapter methods: hasLeadingIcon, getRootBoundingClientRect, and getCheckmarkBoundingClientRect. Also adds a new foundation method: getDimensions.
  • Loading branch information
rlfriedman committed Jan 14, 2019
1 parent 4688971 commit b30f5e2
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 18 deletions.
4 changes: 4 additions & 0 deletions packages/mdc-chips/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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
Expand Down
18 changes: 18 additions & 0 deletions packages/mdc-chips/chip/adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
19 changes: 19 additions & 0 deletions packages/mdc-chips/chip/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ class MDCChipFoundation extends MDCFoundation {
notifyRemoval: () => {},
getComputedStyleValue: () => {},
setStyleProperty: () => {},
hasLeadingIcon: () => {},
getRootBoundingClientRect: () => {},
getCheckmarkBoundingClientRect: () => {},
});
}

Expand Down Expand Up @@ -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.
*/
Expand Down
27 changes: 10 additions & 17 deletions packages/mdc-chips/chip/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ class MDCChip extends MDCComponent {
this.leadingIcon_;
/** @private {?Element} */
this.trailingIcon_;
/** @private {?Element} */
this.checkmark_;
/** @private {!MDCRipple} */
this.ripple_;

Expand All @@ -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() {
Expand Down Expand Up @@ -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,
})));
}

Expand Down
36 changes: 35 additions & 1 deletion test/unit/mdc-chips/mdc-chip.foundation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
]);
});

Expand Down Expand Up @@ -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();
Expand Down
37 changes: 37 additions & 0 deletions test/unit/mdc-chips/mdc-chip.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`
<div class="mdc-chip">
<div class="mdc-chip__text">Chip content</div>
Expand Down Expand Up @@ -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());
Expand Down

0 comments on commit b30f5e2

Please sign in to comment.