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

feat(chips): Move logic for calculating chip bounding rect into a foundation method #4243

Merged
merged 5 commits into from
Jan 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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