From 188d1022c09039fb875b0dccce8b6011eb1c975b Mon Sep 17 00:00:00 2001 From: Alex Sheehan Date: Thu, 22 Jun 2017 16:46:52 -0400 Subject: [PATCH] WIP --- packages/mdc-snackbar/README.md | 10 +- packages/mdc-snackbar/foundation.js | 14 +- test/unit/mdc-snackbar/foundation.test.js | 153 +++++++++++++++++++- test/unit/mdc-snackbar/mdc-snackbar.test.js | 26 ++-- 4 files changed, 177 insertions(+), 26 deletions(-) diff --git a/packages/mdc-snackbar/README.md b/packages/mdc-snackbar/README.md index 4c0bc7f3bc0..96589ae83bd 100644 --- a/packages/mdc-snackbar/README.md +++ b/packages/mdc-snackbar/README.md @@ -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. | diff --git a/packages/mdc-snackbar/foundation.js b/packages/mdc-snackbar/foundation.js index f5beaf93ddf..b2d7b8fbb1b 100644 --- a/packages/mdc-snackbar/foundation.js +++ b/packages/mdc-snackbar/foundation.js @@ -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 */) => {}, @@ -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) => { @@ -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); }; } @@ -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_); @@ -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); @@ -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_() { diff --git a/test/unit/mdc-snackbar/foundation.test.js b/test/unit/mdc-snackbar/foundation.test.js index 14e61f29e3c..2dc72af09ad 100644 --- a/test/unit/mdc-snackbar/foundation.test.js +++ b/test/unit/mdc-snackbar/foundation.test.js @@ -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; @@ -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(); @@ -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; @@ -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(); +}); diff --git a/test/unit/mdc-snackbar/mdc-snackbar.test.js b/test/unit/mdc-snackbar/mdc-snackbar.test.js index 58297208994..0ab4f046831 100644 --- a/test/unit/mdc-snackbar/mdc-snackbar.test.js +++ b/test/unit/mdc-snackbar/mdc-snackbar.test.js @@ -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'); @@ -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'); @@ -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'); @@ -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'); @@ -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'; @@ -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';