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

Commit

Permalink
feat(icon-button): update event handling to new standard (#3165)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Removed some adapter APIs (registerInteractionHandler, deregisterInteractionHandler) and shifted responsibility of event handling out of the foundation and into the component.
  • Loading branch information
Matt Goo authored Jul 26, 2018
1 parent 547a980 commit 531867e
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 84 deletions.
9 changes: 3 additions & 6 deletions packages/mdc-icon-button/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,16 +173,13 @@ Method Signature | Description
--- | ---
`addClass(className: string) => void` | Adds a class to the root element, or the inner icon element.
`removeClass(className: string) => void` | Removes a class from the root element, or the inner icon element.
`registerInteractionHandler(type: string, handler: EventListener) => void` | Registers an event handler for an interaction event, such as `click` or `keydown`.
`deregisterInteractionHandler(type: string, handler: EventListener) => void` | Removes an event handler for an interaction event, such as `click` or `keydown`.
`setText(text: string) => void` | Sets the text content of the root element, or the inner icon element.
`getTabIndex() => number` | Returns the tab index of the root element.
`setTabIndex(tabIndex: number) => void` | Sets the tab index of the root element.
`getAttr(name: string) => string` | Returns the value of the attribute `name` on the root element. Can also return `null`, similar to `getAttribute()`.
`setAttr(name: string, value: string) => void` | Sets the attribute `name` to `value` on the root element.
`removeAttr(name: string) => void` | Removes the attribute `name` on the root element.
`notifyChange(evtData: {isOn: boolean}) => void` | Broadcasts a change notification, passing along the `evtData` to the environment's event handling system. In our vanilla implementation, Custom Events are used for this.

### Foundation: `MDCIconButtonToggleFoundation`

The foundation does not contain any public properties or methods aside from those inherited from MDCFoundation.
Method Signature | Description
--- | ---
`handleClick()` | Event handler triggered on the click event. It will toggle the icon from on/off and update aria attributes.
13 changes: 1 addition & 12 deletions packages/mdc-icon-button/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ class MDCIconButtonToggleFoundation extends MDCFoundation {
return {
addClass: (/* className: string */) => {},
removeClass: (/* className: string */) => {},
registerInteractionHandler: (/* type: string, handler: EventListener */) => {},
deregisterInteractionHandler: (/* type: string, handler: EventListener */) => {},
setText: (/* text: string */) => {},
getAttr: (/* name: string */) => /* string */ '',
setAttr: (/* name: string, value: string */) => {},
Expand All @@ -61,18 +59,10 @@ class MDCIconButtonToggleFoundation extends MDCFoundation {

/** @private {?IconButtonToggleState} */
this.toggleOffData_ = null;

this.clickHandler_ = /** @private {!EventListener} */ (
() => this.toggleFromEvt_());
}

init() {
this.refreshToggleData();
this.adapter_.registerInteractionHandler('click', this.clickHandler_);
}

destroy() {
this.adapter_.deregisterInteractionHandler('click', this.clickHandler_);
}

refreshToggleData() {
Expand All @@ -88,8 +78,7 @@ class MDCIconButtonToggleFoundation extends MDCFoundation {
};
}

/** @private */
toggleFromEvt_() {
handleClick() {
this.toggle();
const {on_: isOn} = this;
this.adapter_.notifyChange(/** @type {!IconButtonToggleEvent} */ ({isOn}));
Expand Down
7 changes: 5 additions & 2 deletions packages/mdc-icon-button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class MDCIconButtonToggle extends MDCComponent {

/** @private {!MDCRipple} */
this.ripple_ = this.initRipple_();
/** @private {!Function} */
this.handleClick_;
}

/** @return {!Element} */
Expand All @@ -52,6 +54,7 @@ class MDCIconButtonToggle extends MDCComponent {
}

destroy() {
this.root_.removeEventListener('click', this.handleClick_);
this.ripple_.destroy();
super.destroy();
}
Expand All @@ -61,8 +64,6 @@ class MDCIconButtonToggle extends MDCComponent {
return new MDCIconButtonToggleFoundation({
addClass: (className) => this.iconEl_.classList.add(className),
removeClass: (className) => this.iconEl_.classList.remove(className),
registerInteractionHandler: (type, handler) => this.root_.addEventListener(type, handler),
deregisterInteractionHandler: (type, handler) => this.root_.removeEventListener(type, handler),
setText: (text) => this.iconEl_.textContent = text,
getAttr: (name) => this.root_.getAttribute(name),
setAttr: (name, value) => this.root_.setAttribute(name, value),
Expand All @@ -71,7 +72,9 @@ class MDCIconButtonToggle extends MDCComponent {
}

initialSyncWithDOM() {
this.handleClick_ = this.foundation_.handleClick.bind(this.foundation_);
this.on = this.root_.getAttribute(MDCIconButtonToggleFoundation.strings.ARIA_PRESSED) === 'true';
this.root_.addEventListener('click', this.handleClick_);
}

/** @return {!MDCRipple} */
Expand Down
53 changes: 17 additions & 36 deletions test/unit/mdc-icon-button/foundation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {assert} from 'chai';
import td from 'testdouble';

import {setupFoundationTest} from '../helpers/setup';
import {verifyDefaultAdapter, captureHandlers as baseCaptureHandlers} from '../helpers/foundation';
import {verifyDefaultAdapter} from '../helpers/foundation';
import MDCIconButtonToggleFoundation from '../../../packages/mdc-icon-button/foundation';

const {strings} = MDCIconButtonToggleFoundation;
Expand All @@ -35,7 +35,7 @@ test('exports cssClasses', () => {

test('defaultAdapter returns a complete adapter implementation', () => {
verifyDefaultAdapter(MDCIconButtonToggleFoundation, [
'addClass', 'removeClass', 'registerInteractionHandler', 'deregisterInteractionHandler',
'addClass', 'removeClass',
'setText', 'getAttr', 'setAttr', 'notifyChange',
]);
});
Expand All @@ -56,6 +56,21 @@ test('#constructor sets on to true if the toggle is pressed', () => {
assert.isTrue(foundation.isOn());
});

test('#handleClick calls #toggle', () => {
const {foundation} = setupTest();
foundation.init();
foundation.toggle = td.func();
foundation.handleClick();
td.verify(foundation.toggle(), {times: 1});
});

test('#handleClick calls notifyChange', () => {
const {foundation, mockAdapter} = setupTest();
foundation.init();
foundation.handleClick();
td.verify(mockAdapter.notifyChange({isOn: true}), {times: 1});
});

test('#toggle flips on', () => {
const {foundation} = setupTest();
foundation.init();
Expand Down Expand Up @@ -190,37 +205,3 @@ test('#refreshToggleData syncs the foundation state with data-toggle-on, data-to
td.verify(mockAdapter.addClass('second-class-on'));
td.verify(mockAdapter.removeClass('second-class-off'));
});

test('#destroy deregisters all interaction handlers', () => {
const {foundation, mockAdapter} = setupTest();
const {isA} = td.matchers;
foundation.destroy();
td.verify(mockAdapter.deregisterInteractionHandler('click', isA(Function)));
});

const captureHandlers = (adapter) => baseCaptureHandlers(adapter, 'registerInteractionHandler');

test('updates toggle state on click', () => {
const {foundation, mockAdapter} = setupTest();
const handlers = captureHandlers(mockAdapter);
foundation.init();

handlers.click();
assert.isOk(foundation.isOn());
td.verify(mockAdapter.setAttr(strings.ARIA_PRESSED, 'true'));

handlers.click();
assert.isNotOk(foundation.isOn());
td.verify(mockAdapter.setAttr(strings.ARIA_PRESSED, 'false'));
});

test('broadcasts change notification on click', () => {
const {foundation, mockAdapter} = setupTest();
const handlers = captureHandlers(mockAdapter);
foundation.init();

handlers.click();
td.verify(mockAdapter.notifyChange({isOn: true}));
handlers.click();
td.verify(mockAdapter.notifyChange({isOn: false}));
});
63 changes: 35 additions & 28 deletions test/unit/mdc-icon-button/mdc-icon-button-toggle.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,35 @@ import {MDCIconButtonToggle, MDCIconButtonToggleFoundation} from '../../../packa
import {MDCRipple} from '../../../packages/mdc-ripple';
import {cssClasses} from '../../../packages/mdc-ripple/constants';

function setupTest({tabIndex = undefined, useInnerIconElement = false} = {}) {
const root = document.createElement('button');
function getFixture() {
return bel`
<button></button>
`;
}

function getIconFixture() {
return bel`
<i id="icon"></i>
`;
}

function setupTest({tabIndex = undefined, useInnerIconElement = false, createMockFoundation = false} = {}) {
const root = getFixture();
if (useInnerIconElement) {
const icon = document.createElement('i');
icon.id = 'icon';
const icon = getIconFixture();
root.dataset.iconInnerSelector = `#${icon.id}`;
root.appendChild(icon);
}
if (tabIndex !== undefined) {
root.tabIndex = tabIndex;
}
const component = new MDCIconButtonToggle(root);
return {root, component};
let mockFoundation;
if (createMockFoundation) {
const MockFoundationCtor = td.constructor(MDCIconButtonToggleFoundation);
mockFoundation = new MockFoundationCtor();
}
const component = new MDCIconButtonToggle(root, mockFoundation);
return {root, component, mockFoundation};
}

suite('MDCIconButtonToggle');
Expand Down Expand Up @@ -124,28 +140,6 @@ test('#adapter.removeClass adds a class to the inner icon element when used', ()
assert.isNotOk(root.querySelector('#icon').classList.contains('foo'));
});

test('#adapter.registerInteractionHandler adds an event listener for (type, handler)', () => {
const {root, component} = setupTest();
document.body.appendChild(root);
const handler = td.func('clickHandler');
component.getDefaultFoundation().adapter_.registerInteractionHandler('click', handler);
domEvents.emit(root, 'click');
td.verify(handler(td.matchers.anything()));
document.body.removeChild(root);
});

test('#adapter.deregisterInteractionHandler removes an event listener for (type, hander)', () => {
const {root, component} = setupTest();
document.body.appendChild(root);
const handler = td.func('clickHandler');

root.addEventListener('click', handler);
component.getDefaultFoundation().adapter_.deregisterInteractionHandler('click', handler);
domEvents.emit(root, 'click');
td.verify(handler(td.matchers.anything()), {times: 0});
document.body.removeChild(root);
});

test('#adapter.setText sets the text content of the root element', () => {
const {root, component} = setupTest();
component.getDefaultFoundation().adapter_.setText('foo');
Expand Down Expand Up @@ -189,3 +183,16 @@ test('assert keyup does not trigger ripple', () => {
domEvents.emit(root, 'keyup');
assert.isNotOk(root.classList.contains(cssClasses.FG_ACTIVATION));
});

test('click handler is added to root element', () => {
const {root, mockFoundation} = setupTest({createMockFoundation: true});
domEvents.emit(root, 'click');
td.verify(mockFoundation.handleClick(td.matchers.anything()), {times: 1});
});

test('click handler is removed from the root element on destroy', () => {
const {root, component, mockFoundation} = setupTest({createMockFoundation: true});
component.destroy();
domEvents.emit(root, 'click');
td.verify(mockFoundation.handleClick(td.matchers.anything()), {times: 0});
});

0 comments on commit 531867e

Please sign in to comment.