Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
amsheehan committed Jun 23, 2017
1 parent 0f3a059 commit 188d102
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 26 deletions.
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
14 changes: 7 additions & 7 deletions packages/mdc-snackbar/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ 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 */) => {},
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_.deregisterBlurHandler(this.blurHandler_);
this.adapter_.deregisterVisibilityChangeHandler(this.visibilitychangeHandler_);
['touchstart', 'mousedown', 'focus'].forEach((evtType) => {
this.adapter_.deregisterCapturedInteractionHandler(evtType, this.interactionHandler_);
Expand Down Expand Up @@ -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
153 changes: 149 additions & 4 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',
'addClass', 'removeClass', 'setAriaHidden', 'unsetAriaHidden', 'setActionAriaHidden',
'unsetActionAriaHidden', 'setActionText', 'setMessageText', 'setFocus', 'visibilityIsHidden',
'registerBlurHandler', 'deregisterBlurHandler', '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.deregisterBlurHandler() with a function', () => {
const {foundation, mockAdapter} = setupTest();
const {isA} = td.matchers;

foundation.destroy();
td.verify(mockAdapter.deregisterBlurHandler(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.registerBlurHandler() with a function', () => {
const {foundation, mockAdapter} = setupTest();
const {isA} = td.matchers;

foundation.show({message: 'foo'});
td.verify(mockAdapter.registerBlurHandler(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.registerBlurHandler(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();
});
26 changes: 15 additions & 11 deletions test/unit/mdc-snackbar/mdc-snackbar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,20 +118,24 @@ 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.isFalse(component.getDefaultFoundation().adapter_.visibilityIsHidden());
});

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

Expand All @@ -141,7 +145,7 @@ test.only('adapter#registerBlurHandler adds a handler to be called on a blur eve
td.verify(handler(td.matchers.anything()));
});

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

Expand All @@ -152,7 +156,7 @@ test.only('adapter#deregisterBlurHandler removes a handler to be called on a blu
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 188d102

Please sign in to comment.