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

Commit

Permalink
feat(tab): Move event registration to component (#3331)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Removes handleTransitionEnd foundation API. Removes [de]registerEventHandler adapter APIs. Event registration is now the component's responsibility.
  • Loading branch information
kfranqueiro authored Aug 13, 2018
1 parent 1a7f6e7 commit f2ac793
Show file tree
Hide file tree
Showing 8 changed files with 41 additions and 156 deletions.
11 changes: 8 additions & 3 deletions packages/mdc-tab/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,6 @@ Method Signature | Description
`addClass(className: string) => void` | Adds a class to the root element.
`removeClass(className: string) => void` | Removes a class from the root element.
`hasClass(className: string) => boolean` | Returns true if the root element contains the given class.
`registerEventHandler(evtType: string, handler: EventListener) => void` | Registers an event listener on the root element.
`deregisterEventHandler(evtType: string, handler: EventListener) => void` | Deregisters an event listener on the root element.
`setAttr(attr: string, value: string) => void` | Sets the given attribute on the root element to the given value.
`activateIndicator(previousIndicatorClientRect: ClientRect=) => void` | Activates the tab indicator subcomponent. `previousIndicatorClientRect` is an optional argument.
`deactivateIndicator() => void` | Deactivates the tab indicator subcomponent.
Expand All @@ -153,10 +151,17 @@ Method Signature | Description

Method Signature | Description
--- | ---
`handleTransitionEnd(evt: Event) => void` | Handles the logic for the `"transitionend"` event.
`handleClick() => void` | Handles the logic for the `"click"` event.
`isActive() => boolean` | Returns whether the tab is active.
`activate(previousIndicatorClientRect: ClientRect=) => void` | Activates the tab. `previousIndicatorClientRect` is an optional argument.
`deactivate() => void` | Deactivates the tab.
`computeIndicatorClientRect() => ClientRect` | Returns the tab indicator subcomponent's content bounding client rect.
`computeDimensions() => MDCTabDimensions` | Returns the dimensions of the tab.

### `MDCTabFoundation` Event Handlers

When wrapping the Tab component, it is necessary to register the following event handler. For an example of this, see the [MDCTab](index.js) component's `initialSyncWithDOM` method.

Event | Element | Foundation Handler
--- | --- | ---
`click` | Root element | `handleClick()`
14 changes: 0 additions & 14 deletions packages/mdc-tab/adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,6 @@ let MDCTabDimensions;
* @record
*/
class MDCTabAdapter {
/**
* Registers an event listener on the root element for a given event.
* @param {string} evtType
* @param {function(!Event): undefined} handler
*/
registerEventHandler(evtType, handler) {}

/**
* Deregisters an event listener on the root element for a given event.
* @param {string} evtType
* @param {function(!Event): undefined} handler
*/
deregisterEventHandler(evtType, handler) {}

/**
* Adds the given className to the root element.
* @param {string} className The className to add
Expand Down
2 changes: 0 additions & 2 deletions packages/mdc-tab/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
/** @enum {string} */
const cssClasses = {
ACTIVE: 'mdc-tab--active',
ANIMATING_ACTIVATE: 'mdc-tab--animating-activate',
ANIMATING_DEACTIVATE: 'mdc-tab--animating-deactivate',
};

/** @enum {string} */
Expand Down
27 changes: 0 additions & 27 deletions packages/mdc-tab/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ class MDCTabFoundation extends MDCFoundation {
*/
static get defaultAdapter() {
return /** @type {!MDCTabAdapter} */ ({
registerEventHandler: () => {},
deregisterEventHandler: () => {},
addClass: () => {},
removeClass: () => {},
hasClass: () => {},
Expand All @@ -69,31 +67,10 @@ class MDCTabFoundation extends MDCFoundation {
constructor(adapter) {
super(Object.assign(MDCTabFoundation.defaultAdapter, adapter));

/** @private {function(!Event): undefined} */
this.handleTransitionEnd_ = (evt) => this.handleTransitionEnd(evt);

/** @private {function(?Event): undefined} */
this.handleClick_ = () => this.handleClick();
}

init() {
this.adapter_.registerEventHandler('click', this.handleClick_);
}

/**
* Handles the "transitionend" event
* @param {!Event} evt A browser event
*/
handleTransitionEnd(evt) {
// Early exit for ripple
if (evt.pseudoElement) {
return;
}
this.adapter_.deregisterEventHandler('transitionend', this.handleTransitionEnd_);
this.adapter_.removeClass(cssClasses.ANIMATING_ACTIVATE);
this.adapter_.removeClass(cssClasses.ANIMATING_DEACTIVATE);
}

/**
* Handles the "click" event
*/
Expand Down Expand Up @@ -121,8 +98,6 @@ class MDCTabFoundation extends MDCFoundation {
return;
}

this.adapter_.registerEventHandler('transitionend', this.handleTransitionEnd_);
this.adapter_.addClass(cssClasses.ANIMATING_ACTIVATE);
this.adapter_.addClass(cssClasses.ACTIVE);
this.adapter_.setAttr(strings.ARIA_SELECTED, 'true');
this.adapter_.setAttr(strings.TABINDEX, '0');
Expand All @@ -139,8 +114,6 @@ class MDCTabFoundation extends MDCFoundation {
return;
}

this.adapter_.registerEventHandler('transitionend', this.handleTransitionEnd_);
this.adapter_.addClass(cssClasses.ANIMATING_DEACTIVATE);
this.adapter_.removeClass(cssClasses.ACTIVE);
this.adapter_.setAttr(strings.ARIA_SELECTED, 'false');
this.adapter_.setAttr(strings.TABINDEX, '-1');
Expand Down
11 changes: 9 additions & 2 deletions packages/mdc-tab/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ class MDCTab extends MDCComponent {
this.tabIndicator_;
/** @private {?Element} */
this.content_;

/** @private {?Function} */
this.handleClick_;
}

/**
Expand Down Expand Up @@ -69,7 +72,13 @@ class MDCTab extends MDCComponent {
this.content_ = this.root_.querySelector(MDCTabFoundation.strings.CONTENT_SELECTOR);
}

initialSyncWithDOM() {
this.handleClick_ = this.foundation_.handleClick.bind(this.foundation_);
this.listen('click', this.handleClick_);
}

destroy() {
this.unlisten('click', /** @type {!Function} */ (this.handleClick_));
this.ripple_.destroy();
super.destroy();
}
Expand All @@ -81,8 +90,6 @@ class MDCTab extends MDCComponent {
return new MDCTabFoundation(
/** @type {!MDCTabAdapter} */ ({
setAttr: (attr, value) => this.root_.setAttribute(attr, value),
registerEventHandler: (evtType, handler) => this.root_.addEventListener(evtType, handler),
deregisterEventHandler: (evtType, handler) => this.root_.removeEventListener(evtType, handler),
addClass: (className) => this.root_.classList.add(className),
removeClass: (className) => this.root_.classList.remove(className),
hasClass: (className) => this.root_.classList.contains(className),
Expand Down
16 changes: 2 additions & 14 deletions packages/mdc-tab/mdc-tab.scss
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@

.mdc-tab__text-label,
.mdc-tab__icon {
transition: 150ms color linear, 150ms opacity linear;
z-index: 2;
}

Expand Down Expand Up @@ -112,26 +113,13 @@
padding-bottom: 16px;
}

// The [de]activation animation affects color for text label and icon
.mdc-tab--animating-activate .mdc-tab__text-label,
.mdc-tab--animating-activate .mdc-tab__icon,
.mdc-tab--animating-deactivate .mdc-tab__text-label,
.mdc-tab--animating-deactivate .mdc-tab__icon {
transition: 150ms color linear, 150ms opacity linear;
}

// The activation animation has a delay of 100ms
.mdc-tab--animating-activate .mdc-tab__text-label,
.mdc-tab--animating-activate .mdc-tab__icon {
transition-delay: 100ms;
}

.mdc-tab--active {
@include mdc-tab-text-label-color(primary);
@include mdc-tab-icon-color(primary);

.mdc-tab__text-label,
.mdc-tab__icon {
transition-delay: 100ms;
opacity: 1;
}
}
Expand Down
75 changes: 1 addition & 74 deletions test/unit/mdc-tab/foundation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import {assert} from 'chai';
import td from 'testdouble';

import {captureHandlers, verifyDefaultAdapter} from '../helpers/foundation';
import {verifyDefaultAdapter} from '../helpers/foundation';
import {setupFoundationTest} from '../helpers/setup';
import MDCTabFoundation from '../../../packages/mdc-tab/foundation';

Expand All @@ -33,7 +33,6 @@ test('exports strings', () => {

test('defaultAdapter returns a complete adapter implementation', () => {
verifyDefaultAdapter(MDCTabFoundation, [
'registerEventHandler', 'deregisterEventHandler',
'addClass', 'removeClass', 'hasClass',
'setAttr',
'activateIndicator', 'deactivateIndicator', 'computeIndicatorClientRect',
Expand All @@ -50,13 +49,6 @@ test('#activate does nothing if already active', () => {
td.when(mockAdapter.hasClass(MDCTabFoundation.cssClasses.ACTIVE)).thenReturn(true);
foundation.activate();
td.verify(mockAdapter.addClass(MDCTabFoundation.cssClasses.ACTIVE), {times: 0});
td.verify(mockAdapter.registerEventHandler('transitionend', td.matchers.isA(Function)), {times: 0});
});

test('#activate registers a transitionend listener on the root element', () => {
const {foundation, mockAdapter} = setupTest();
foundation.activate();
td.verify(mockAdapter.registerEventHandler('transitionend', td.matchers.isA(Function)));
});

test('#activate adds mdc-tab--active class to the root element', () => {
Expand All @@ -65,12 +57,6 @@ test('#activate adds mdc-tab--active class to the root element', () => {
td.verify(mockAdapter.addClass(MDCTabFoundation.cssClasses.ACTIVE));
});

test('#activate adds mdc-tab--animating-activate class to the root element', () => {
const {foundation, mockAdapter} = setupTest();
foundation.activate();
td.verify(mockAdapter.addClass(MDCTabFoundation.cssClasses.ANIMATING_ACTIVATE));
});

test('#activate sets the root element aria-selected attribute to true', () => {
const {foundation, mockAdapter} = setupTest();
foundation.activate();
Expand Down Expand Up @@ -105,14 +91,6 @@ test('#deactivate does nothing if not active', () => {
const {foundation, mockAdapter} = setupTest();
foundation.deactivate();
td.verify(mockAdapter.addClass, {times: 0});
td.verify(mockAdapter.registerEventHandler('transitionend', td.matchers.isA(Function)), {times: 0});
});

test('#deactivate registers a transitionend listener on the root element', () => {
const {foundation, mockAdapter} = setupTest();
td.when(mockAdapter.hasClass(MDCTabFoundation.cssClasses.ACTIVE)).thenReturn(true);
foundation.deactivate();
td.verify(mockAdapter.registerEventHandler('transitionend', td.matchers.isA(Function)));
});

test('#deactivate removes mdc-tab--active class to the root element', () => {
Expand All @@ -122,13 +100,6 @@ test('#deactivate removes mdc-tab--active class to the root element', () => {
td.verify(mockAdapter.removeClass(MDCTabFoundation.cssClasses.ACTIVE));
});

test('#deactivate adds mdc-tab--animating-deactivate class to the root element', () => {
const {foundation, mockAdapter} = setupTest();
td.when(mockAdapter.hasClass(MDCTabFoundation.cssClasses.ACTIVE)).thenReturn(true);
foundation.deactivate();
td.verify(mockAdapter.addClass(MDCTabFoundation.cssClasses.ANIMATING_DEACTIVATE));
});

test('#deactivate sets the root element aria-selected attribute to false', () => {
const {foundation, mockAdapter} = setupTest();
td.when(mockAdapter.hasClass(MDCTabFoundation.cssClasses.ACTIVE)).thenReturn(true);
Expand All @@ -150,56 +121,12 @@ test('#deactivate sets the root element tabindex to -1', () => {
td.verify(mockAdapter.setAttr(MDCTabFoundation.strings.TABINDEX, '-1'));
});

test('#handleTransitionEnd removes mdc-tab--animating-activate class', () => {
const {foundation, mockAdapter} = setupTest();
foundation.handleTransitionEnd({pseudoElement: ''});
td.verify(mockAdapter.removeClass(MDCTabFoundation.cssClasses.ANIMATING_ACTIVATE));
});

test('#handleTransitionEnd removes mdc-tab--animating-deactivate class', () => {
const {foundation, mockAdapter} = setupTest();
foundation.handleTransitionEnd({pseudoElement: ''});
td.verify(mockAdapter.removeClass(MDCTabFoundation.cssClasses.ANIMATING_DEACTIVATE));
});

test('#handleTransitionEnd deregisters the transitionend event listener', () => {
const {foundation, mockAdapter} = setupTest();
foundation.handleTransitionEnd({pseudoElement: ''});
td.verify(mockAdapter.deregisterEventHandler('transitionend', td.matchers.isA(Function)));
});

test('#handleTransitionEnd does nothing when triggered by a pseudo element', () => {
const {foundation, mockAdapter} = setupTest();
foundation.handleTransitionEnd({pseudoElement: '::before'});
td.verify(mockAdapter.removeClass(MDCTabFoundation.cssClasses.ANIMATING_ACTIVATE), {times: 0});
td.verify(mockAdapter.removeClass(MDCTabFoundation.cssClasses.ANIMATING_DEACTIVATE), {times: 0});
td.verify(mockAdapter.deregisterEventHandler('transitionend', td.matchers.isA(Function)), {times: 0});
});

test('on transitionend, call #handleTransitionEnd', () => {
const {foundation, mockAdapter} = setupTest();
const handlers = captureHandlers(mockAdapter, 'registerEventHandler');
foundation.handleTransitionEnd = td.function('handles transitionend');
foundation.activate();
handlers.transitionend();
td.verify(foundation.handleTransitionEnd(td.matchers.anything()), {times: 1});
});

test(`#handleClick emits the ${MDCTabFoundation.strings.INTERACTED_EVENT} event`, () => {
const {foundation, mockAdapter} = setupTest();
foundation.handleClick();
td.verify(mockAdapter.notifyInteracted(), {times: 1});
});

test('on click, call #handleClick', () => {
const {foundation, mockAdapter} = setupTest();
const handlers = captureHandlers(mockAdapter, 'registerEventHandler');
foundation.handleClick = td.function('handles click');
foundation.init();
handlers.click();
td.verify(foundation.handleClick(), {times: 1});
});

test('#computeDimensions() returns the dimensions of the tab', () => {
const {foundation, mockAdapter} = setupTest();
td.when(mockAdapter.getOffsetLeft()).thenReturn(10);
Expand Down
41 changes: 21 additions & 20 deletions test/unit/mdc-tab/mdc-tab.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,31 @@ test('attachTo returns an MDCTab instance', () => {
assert.isTrue(MDCTab.attachTo(getFixture()) instanceof MDCTab);
});

function setupTest() {
function setupTest({createMockFoundation = false} = {}) {
const root = getFixture();
const content = root.querySelector(MDCTabFoundation.strings.CONTENT_SELECTOR);
const component = new MDCTab(root);
return {root, content, component};
const mockFoundation = createMockFoundation ? new (td.constructor(MDCTabFoundation))() : undefined;
const component = new MDCTab(root, mockFoundation);
return {root, content, component, mockFoundation};
}

test('click handler is added during initialSyncWithDOM', () => {
const {component, root, mockFoundation} = setupTest({createMockFoundation: true});

domEvents.emit(root, 'click');
td.verify(mockFoundation.handleClick(td.matchers.anything()), {times: 1});

component.destroy();
});

test('click handler is removed during destroy', () => {
const {component, root, mockFoundation} = setupTest({createMockFoundation: true});

component.destroy();
domEvents.emit(root, 'click');
td.verify(mockFoundation.handleClick(td.matchers.anything()), {times: 0});
});

test('#destroy removes the ripple', () => {
const raf = createMockRaf();
const {component, root} = setupTest();
Expand Down Expand Up @@ -84,23 +102,6 @@ test('#adapter.setAttr adds a given attribute to the root element', () => {
assert.equal(root.getAttribute('foo'), 'bar');
});

test('#adapter.registerEventHandler adds an event listener to the root element for a given event', () => {
const {root, component} = setupTest();
const handler = td.func('transitionend handler');
component.getDefaultFoundation().adapter_.registerEventHandler('transitionend', handler);
domEvents.emit(root, 'transitionend');
td.verify(handler(td.matchers.anything()));
});

test('#adapter.deregisterEventHandler removes an event listener from the root element for a given event', () => {
const {root, component} = setupTest();
const handler = td.func('transitionend handler');
root.addEventListener('transitionend', handler);
component.getDefaultFoundation().adapter_.deregisterEventHandler('transitionend', handler);
domEvents.emit(root, 'transitionend');
td.verify(handler(td.matchers.anything()), {times: 0});
});

test('#adapter.activateIndicator activates the indicator subcomponent', () => {
const {root, component} = setupTest();
component.getDefaultFoundation().adapter_.activateIndicator();
Expand Down

0 comments on commit f2ac793

Please sign in to comment.