Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
amsheehan committed Jun 29, 2017
1 parent 0f3a059 commit e563eca
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 35 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
"stylelint-order": "^0.4.3",
"stylelint-scss": "^1.4.1",
"stylelint-selector-bem-pattern": "^1.0.0",
"testdouble": "^3.0.0",
"testdouble": "3.0.0",
"to-slug-case": "^1.0.0",
"validate-commit-msg": "^2.6.1",
"webpack": "^2.2.1",
Expand Down
10 changes: 6 additions & 4 deletions packages/mdc-snackbar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,16 +198,18 @@ The adapter for snackbars must provide the following functions, with correct sig
| `removeClass(className: string) => void` | Removes a class from the root element. |
| `setAriaHidden() => void` | Sets `aria-hidden="true"` on the root element. |
| `unsetAriaHidden() => void` | Removes the `aria-hidden` attribute from the root element. |
| `setMessageText(message: string) => void` | Set the text content of the message element. |
| `setActionText(actionText: string) => void` | Set the text content of the action element. |
| `setActionAriaHidden() => void` | Sets `aria-hidden="true"` on the action element. |
| `unsetActionAriaHidden() => void` | Removes the `aria-hidden` attribute from the action element. |
| `registerFocusHandler(handler: EventListener) => void` | Registers an event handler to be called when a `focus` event is triggered on the `body` |
| `deregisterFocusHandler(handler: EventListener) => void` | Deregisters a `focus` event handler from the `body` |
| `setActionText(actionText: string) => void` | Set the text content of the action element. |
| `setMessageText(message: string) => void` | Set the text content of the message element. |
| `setFocus() => void` | Sets focus on the action button. |
| `visibilityIsHidden() => boolean` | Returns document.hidden property. |
| `registerBlurHandler(handler: EventListener) => void` | Registers an event handler to be called when a `blur` event is triggered on the action button |
| `deregisterBlurHandler(handler: EventListener) => void` | Deregisters a `blur` event handler from the actionButton |
| `registerVisibilityChangeHandler(handler: EventListener) => void` | Registers an event handler to be called when a 'visibilitychange' event occurs |
| `deregisterVisibilityChangeHandler(handler: EventListener) => void` | Deregisters an event handler to be called when a 'visibilitychange' event occurs |
| `registerCapturedInteractionHandler(evtType: string, handler: EventListener) => void` | Registers an event handler to be called when the given event type is triggered on the `body` |
| `deregisterCapturedInteractionHandler(evtType: string, handler: EventListener) => void` | Deregisters an event handler from the `body` |
| `registerActionClickHandler(handler: EventListener) => void` | Registers an event handler to be called when a `click` event is triggered on the action element. |
| `deregisterActionClickHandler(handler: EventListener) => void` | Deregisters an event handler from a `click` event on the action element. This will only be called with handlers that have previously been passed to `registerActionClickHandler` calls. |
| `registerTransitionEndHandler(handler: EventListener) => void` | Registers an event handler to be called when an `transitionend` event is triggered on the root element. Note that you must account for vendor prefixes in order for this to work correctly. |
Expand Down
20 changes: 10 additions & 10 deletions packages/mdc-snackbar/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
removeClass: (/* className: string */) => {},
setAriaHidden: () => {},
unsetAriaHidden: () => {},
setMessageText: (/* message: string */) => {},
setActionText: (/* actionText: string */) => {},
setActionAriaHidden: () => {},
unsetActionAriaHidden: () => {},
setActionText: (/* actionText: string */) => {},
setMessageText: (/* message: string */) => {},
setFocus: () => {},
visibilityIsHidden: () => /* boolean */ false,
registerBlurHandler: (/* handler: EventListener */) => {},
deregisterBlurHandler: (/* handler: EventListener */) => {},
registerCapturedBlurHandler: (/* handler: EventListener */) => {},
deregisterCapturedBlurHandler: (/* handler: EventListener */) => {},
registerVisibilityChangeHandler: (/* handler: EventListener */) => {},
deregisterVisibilityChangeHandler: (/* handler: EventListener */) => {},
registerCapturedInteractionHandler: (/* evtType: string, handler: EventListener */) => {},
Expand Down Expand Up @@ -74,7 +75,7 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
this.snackbarHasFocus_ = true;

if (!this.adapter_.visibilityIsHidden()) {
setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || MESSAGE_TIMEOUT);
setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || numbers.MESSAGE_TIMEOUT);
}
};
this.interactionHandler_ = (evt) => {
Expand All @@ -90,7 +91,7 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
this.blurHandler_ = () => {
clearTimeout(this.timeoutId_);
this.snackbarHasFocus_ = false;
this.timeoutId_ = setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || MESSAGE_TIMEOUT);
this.timeoutId_ = setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || numbers.MESSAGE_TIMEOUT);
};
}

Expand All @@ -102,7 +103,7 @@ export default class MDCSnackbarFoundation extends MDCFoundation {

destroy() {
this.adapter_.deregisterActionClickHandler(this.actionClickHandler_);
this.adapter_.deregisterBlurHandler(this.focusHandler_);
this.adapter_.deregisterCapturedBlurHandler(this.blurHandler_);
this.adapter_.deregisterVisibilityChangeHandler(this.visibilitychangeHandler_);
['touchstart', 'mousedown', 'focus'].forEach((evtType) => {
this.adapter_.deregisterCapturedInteractionHandler(evtType, this.interactionHandler_);
Expand All @@ -122,7 +123,7 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
this.snackbarData_ = data;
this.firstFocus_ = true;
this.adapter_.registerVisibilityChangeHandler(this.visibilitychangeHandler_);
this.adapter_.registerBlurHandler(this.blurHandler_);
this.adapter_.registerCapturedBlurHandler(this.blurHandler_);
['touchstart', 'mousedown', 'focus'].forEach((evtType) => {
this.adapter_.registerCapturedInteractionHandler(evtType, this.interactionHandler_);
});
Expand All @@ -142,7 +143,6 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
}

const {ACTIVE, MULTILINE, ACTION_ON_BOTTOM} = cssClasses;
const {MESSAGE_TIMEOUT} = numbers;

this.adapter_.setMessageText(this.snackbarData_.message);

Expand All @@ -167,7 +167,7 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
this.adapter_.addClass(ACTIVE);
this.adapter_.unsetAriaHidden();

this.timeoutId_ = setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || MESSAGE_TIMEOUT);
this.timeoutId_ = setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || numbers.MESSAGE_TIMEOUT);
}

handlePossibleTabKeyboardFocus_() {
Expand Down
4 changes: 2 additions & 2 deletions packages/mdc-snackbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ export class MDCSnackbar extends MDCComponent {
setMessageText: (text) => { getText().textContent = text; },
setFocus: () => getActionButton().focus(),
visibilityIsHidden: () => document.hidden,
registerBlurHandler: (handler) => getActionButton().addEventListener('blur', handler, true),
deregisterBlurHandler: (handler) => getActionButton().removeEventListener('blur', handler, true),
registerCapturedBlurHandler: (handler) => getActionButton().addEventListener('blur', handler, true),
deregisterCapturedBlurHandler: (handler) => getActionButton().removeEventListener('blur', handler, true),
registerVisibilityChangeHandler: (handler) => document.addEventListener('visibilitychange', handler),
deregisterVisibilityChangeHandler: (handler) => document.removeEventListener('visibilitychange', handler),
registerCapturedInteractionHandler: (evt, handler) =>
Expand Down
155 changes: 150 additions & 5 deletions test/unit/mdc-snackbar/foundation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,19 @@ test('defaultAdapter returns a complete adapter implementation', () => {

assert.equal(methods.length, Object.keys(defaultAdapter).length, 'Every adapter key must be a function');
assert.deepEqual(methods, [
'addClass', 'removeClass', 'setAriaHidden', 'unsetAriaHidden', 'setMessageText',
'setActionText', 'setActionAriaHidden', 'unsetActionAriaHidden', 'visibilityIsHidden',
'registerBlurHandler', 'deregisterBlurHandler', 'registerVisibilityChangeHandler',
'addClass', 'removeClass', 'setAriaHidden', 'unsetAriaHidden', 'setActionAriaHidden',
'unsetActionAriaHidden', 'setActionText', 'setMessageText', 'setFocus', 'visibilityIsHidden',
'registerCapturedBlurHandler', 'deregisterCapturedBlurHandler', 'registerVisibilityChangeHandler',
'deregisterVisibilityChangeHandler', 'registerCapturedInteractionHandler',
'deregisterCapturedInteractionHandler', 'registerActionClickHandler',
'deregisterActionClickHandler', 'registerTransitionEndHandler', 'deregisterTransitionEndHandler',
'deregisterActionClickHandler', 'registerTransitionEndHandler',
'deregisterTransitionEndHandler',
]);
// Test default methods
methods.forEach((m) => assert.doesNotThrow(defaultAdapter[m]));
});

test('#init calls adapter.registerActionClickHandler() with a action click handler function', () => {
test('#init calls adapter.registerActionClickHandler() with an action click handler function', () => {
const {foundation, mockAdapter} = setupTest();
const {isA} = td.matchers;

Expand All @@ -76,6 +77,30 @@ test('#destroy calls adapter.deregisterActionClickHandler() with a registerActio
td.verify(mockAdapter.deregisterActionClickHandler(changeHandler));
});

test('#destroy calls adapter.deregisterVisibilityChangeHandler() with a function', () => {
const {foundation, mockAdapter} = setupTest();
const {isA} = td.matchers;

foundation.destroy();
td.verify(mockAdapter.deregisterVisibilityChangeHandler(isA(Function)));
});

test('#destroy calls adapter.deregisterCapturedBlurHandler() with a function', () => {
const {foundation, mockAdapter} = setupTest();
const {isA} = td.matchers;

foundation.destroy();
td.verify(mockAdapter.deregisterCapturedBlurHandler(isA(Function)));
});

test('#destroy calls adapter.deregisterCapturedInteractionHandler() with an event type and function 3 times', () => {
const {foundation, mockAdapter} = setupTest();
const {isA} = td.matchers;

foundation.destroy();
td.verify(mockAdapter.deregisterCapturedInteractionHandler(isA(String), isA(Function)), {times: 3});
});

test('#init calls adapter.setAriaHidden to ensure snackbar starts hidden', () => {
const {foundation, mockAdapter} = setupTest();

Expand Down Expand Up @@ -313,6 +338,30 @@ test('#show will clean up snackbar after the timeout and transition end', () =>
clock.uninstall();
});

test('#show calls adapter.registerVisibilityChangeHandler() with a function', () => {
const {foundation, mockAdapter} = setupTest();
const {isA} = td.matchers;

foundation.show({message: 'foo'});
td.verify(mockAdapter.registerVisibilityChangeHandler(isA(Function)));
});

test('#show calls adapter.registerCapturedBlurHandler() with a function', () => {
const {foundation, mockAdapter} = setupTest();
const {isA} = td.matchers;

foundation.show({message: 'foo'});
td.verify(mockAdapter.registerCapturedBlurHandler(isA(Function)));
});

test('#show calls adapter.registerCapturedInteractionHandler() with an event type and function 3 times', () => {
const {foundation, mockAdapter} = setupTest();
const {isA} = td.matchers;

foundation.show({message: 'foo'});
td.verify(mockAdapter.registerCapturedInteractionHandler(isA(String), isA(Function)), {times: 3});
});

test('snackbar is dismissed after action button is pressed', () => {
const {foundation, mockAdapter} = setupTest();
const {isA} = td.matchers;
Expand Down Expand Up @@ -363,3 +412,99 @@ test('snackbar is not dismissed after action button is pressed if setDismissOnAc

td.verify(mockAdapter.removeClass(cssClasses.ACTIVE), {times: 0});
});

test('snackbar is not dismissed if action button gets focus', () => {
const {foundation, mockAdapter} = setupTest();
const evtType = 'focus';
const mockEvent = {type: 'focus'};
let focusEvent;

td.when(mockAdapter.registerCapturedInteractionHandler(evtType, td.matchers.isA(Function)))
.thenDo((evtType, handler) => {
focusEvent = handler;
});

foundation.init();
foundation.show({message: 'foo'});
focusEvent(mockEvent);

foundation.show({message: 'foo'});

td.verify(mockAdapter.removeClass(cssClasses.ACTIVE), {times: 0});
});

test('focus hijacks the snackbar timeout if no click or touchstart occurs', () => {
const {foundation, mockAdapter} = setupTest();
const mockEvent = {type: 'focus'};
let tabEvent;

td.when(mockAdapter.registerCapturedInteractionHandler(mockEvent.type, td.matchers.isA(Function)))
.thenDo((evt, handler) => {
tabEvent = handler;
});

foundation.init();
foundation.show({message: 'foo'});
tabEvent(mockEvent);

td.verify(mockAdapter.removeClass(cssClasses.ACTIVE), {times: 0});
});

test('focus does not hijack the snackbar timeout if it occurs as a result' +
'of a mousedown or touchstart', () => {
const clock = lolex.install();
const {foundation, mockAdapter} = setupTest();
const mockFocusEvent = {type: 'focus'};
const mockMouseEvent = {type: 'mousedown'};
let focusEvent;
let mouseEvent;

td.when(mockAdapter.registerCapturedInteractionHandler(mockFocusEvent.type, td.matchers.isA(Function)))
.thenDo((evt, handler) => {
focusEvent = handler;
});
td.when(mockAdapter.registerCapturedInteractionHandler(mockMouseEvent.type, td.matchers.isA(Function)))
.thenDo((evt, handler) => {
mouseEvent = handler;
});

foundation.init();
foundation.show({message: 'foo'});
mouseEvent(mockMouseEvent);
focusEvent(mockFocusEvent);
clock.tick(numbers.MESSAGE_TIMEOUT);

td.verify(mockAdapter.removeClass(cssClasses.ACTIVE));
clock.uninstall();
});

test('blur resets the snackbar timeout', () => {
const clock = lolex.install();
const {foundation, mockAdapter} = setupTest();
const {isA} = td.matchers;
const mockBlurEvent = {type: 'blur'};
const mockFocusEvent = {type: 'focus'};
let focusEvent;
let blurEvent;

td.when(mockAdapter.registerCapturedInteractionHandler(mockFocusEvent.type, td.matchers.isA(Function)))
.thenDo((evt, handler) => {
focusEvent = handler;
});
td.when(mockAdapter.registerCapturedBlurHandler(isA(Function)))
.thenDo((handler) => {
blurEvent = handler;
});

foundation.init();
foundation.show({message: 'foo'});
focusEvent(mockFocusEvent);
// Sanity Check
td.verify(mockAdapter.removeClass(cssClasses.ACTIVE), {times: 0});

blurEvent(mockBlurEvent);
clock.tick(numbers.MESSAGE_TIMEOUT);
td.verify(mockAdapter.removeClass(cssClasses.ACTIVE));

clock.uninstall();
});
30 changes: 17 additions & 13 deletions test/unit/mdc-snackbar/mdc-snackbar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,41 +118,45 @@ test('foundationAdapter#unsetActionAriaHidden removes "aria-hidden" from the act
assert.isNotOk(actionButton.getAttribute('aria-hidden'));
});

// TODO: return to this
test('adapter#setFocus sets focus on the action button', () => {
const {actionButton, component} = setupTest();
const {root, actionButton, component} = setupTest();
const handler = td.func('fixture focus handler');
root.addEventListener('focus', handler);
document.body.appendChild(root);

component.getDefaultFoundation().adapter_.setFocus();

assert.equal(document.activeElement, actionButton);
document.body.removeChild(root);
});

// TODO: return to this
test.only('adapter#visibilityIsHidden returns the document.hidden property', () => {
test('adapter#visibilityIsHidden returns the document.hidden property', () => {
const {component} = setupTest();
assert.isTrue(component.getDefaultFoundation().adapter_.visibilityIsHidden());
assert.equal(component.getDefaultFoundation().adapter_.visibilityIsHidden(), document.hidden);
});

test.only('adapter#registerBlurHandler adds a handler to be called on a blur event', () => {
test('adapter#registerCapturedBlurHandler adds a handler to be called on a blur event', () => {
const {actionButton, component} = setupTest();
const handler = td.func('blurHandler');

component.getDefaultFoundation().adapter_.registerBlurHandler(handler);
component.getDefaultFoundation().adapter_.registerCapturedBlurHandler(handler);
domEvents.emit(actionButton, 'blur');

td.verify(handler(td.matchers.anything()));
});

test.only('adapter#deregisterBlurHandler removes a handler to be called on a blur event', () => {
test('adapter#deregisterCapturedBlurHandler removes a handler to be called on a blur event', () => {
const {actionButton, component} = setupTest();
const handler = td.func('blurHandler');

actionButton.addEventListener('blur', handler, true);
component.getDefaultFoundation().adapter_.deregisterBlurHandler(handler);
component.getDefaultFoundation().adapter_.deregisterCapturedBlurHandler(handler);
domEvents.emit(actionButton, 'blur');

td.verify(handler(td.matchers.anything()), {times: 0});
});

test.only('adapter#registerVisibilityChangeHandler adds a handler to be called on a visibilitychange event', () => {
test('adapter#registerVisibilityChangeHandler adds a handler to be called on a visibilitychange event', () => {
const {component} = setupTest();
const handler = td.func('visibilitychangeHandler');

Expand All @@ -162,7 +166,7 @@ test.only('adapter#registerVisibilityChangeHandler adds a handler to be called o
td.verify(handler(td.matchers.anything()));
});

test.only('adapter#deregisterVisibilityChangeHandler removes a handler to be called on a visibilitychange event', () => {
test('adapter#deregisterVisibilityChangeHandler removes a handler to be called on a visibilitychange event', () => {
const {component} = setupTest();
const handler = td.func('visibilitychangeHandler');

Expand All @@ -173,7 +177,7 @@ test.only('adapter#deregisterVisibilityChangeHandler removes a handler to be cal
td.verify(handler(td.matchers.anything()), {times: 0});
});

test.only('adapter#registerCapturedInteractionHandler adds a handler to be called when a given event occurs', () => {
test('adapter#registerCapturedInteractionHandler adds a handler to be called when a given event occurs', () => {
const {component} = setupTest();
const handler = td.func('interactionHandler');
const mockEvent = 'click';
Expand All @@ -184,7 +188,7 @@ test.only('adapter#registerCapturedInteractionHandler adds a handler to be calle
td.verify(handler(td.matchers.anything()));
});

test.only('adapter#deregisterCapturedInteractionHandler removes a handler to be called when a given event occurs', () => {
test('adapter#deregisterCapturedInteractionHandler removes a handler to be called when a given event occurs', () => {
const {component} = setupTest();
const handler = td.func('interactionHandler');
const mockEvent = 'click';
Expand Down

0 comments on commit e563eca

Please sign in to comment.