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

Commit 04d992e

Browse files
committed
WIP
1 parent 0f3a059 commit 04d992e

File tree

4 files changed

+181
-28
lines changed

4 files changed

+181
-28
lines changed

packages/mdc-snackbar/README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ properties and their usage.
144144

145145
### Responding to a Snackbar Action
146146

147-
To respond to a snackbar action, assign a function to the optional `actionHandler` property in the object that gets passed to the `show` method. If you choose to set this property, you *must _also_* set the `actionText` property.
147+
To respond to a snackbar action, assign a function to the optional `actionHandler` property in the object that gets passed to the `show` method. If you choose to set this property, you *must _also_* set the `actionText` property, or the component will complain.
148148

149149
```html
150150
<div class="mdc-snackbar"
@@ -198,16 +198,18 @@ The adapter for snackbars must provide the following functions, with correct sig
198198
| `removeClass(className: string) => void` | Removes a class from the root element. |
199199
| `setAriaHidden() => void` | Sets `aria-hidden="true"` on the root element. |
200200
| `unsetAriaHidden() => void` | Removes the `aria-hidden` attribute from the root element. |
201-
| `setMessageText(message: string) => void` | Set the text content of the message element. |
202-
| `setActionText(actionText: string) => void` | Set the text content of the action element. |
203201
| `setActionAriaHidden() => void` | Sets `aria-hidden="true"` on the action element. |
204202
| `unsetActionAriaHidden() => void` | Removes the `aria-hidden` attribute from the action element. |
205-
| `registerFocusHandler(handler: EventListener) => void` | Registers an event handler to be called when a `focus` event is triggered on the `body` |
206-
| `deregisterFocusHandler(handler: EventListener) => void` | Deregisters a `focus` event handler from the `body` |
207-
| `registerBlurHandler(handler: EventListener) => void` | Registers an event handler to be called when a `blur` event is triggered on the action button |
203+
| `setActionText(actionText: string) => void` | Sets the text content of the action element. |
204+
| `setMessageText(message: string) => void` | Sets the text content of the message element. |
205+
| `setFocus() => void` | Sets focus on the action button. |
206+
| `visibilityIsHidden() => boolean` | Returns document.hidden property. |
207+
| `registerBlurHandler(handler: EventListener) => void` | Registers an event handler to be called when a `blur` event is triggered on the action button. This happens during the capture phase of the event. |
208208
| `deregisterBlurHandler(handler: EventListener) => void` | Deregisters a `blur` event handler from the actionButton |
209209
| `registerVisibilityChangeHandler(handler: EventListener) => void` | Registers an event handler to be called when a 'visibilitychange' event occurs |
210210
| `deregisterVisibilityChangeHandler(handler: EventListener) => void` | Deregisters an event handler to be called when a 'visibilitychange' event occurs |
211+
| `registerCapturedInteractionHandler(evtType: string, handler: EventListener) => void` | Registers an event handler to be called when the given event type is triggered on the `body`. This happens during the capture phase of an event. |
212+
| `deregisterCapturedInteractionHandler(evtType: string, handler: EventListener) => void` | Deregisters an event handler from the `body` |
211213
| `registerActionClickHandler(handler: EventListener) => void` | Registers an event handler to be called when a `click` event is triggered on the action element. |
212214
| `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. |
213215
| `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. |

packages/mdc-snackbar/foundation.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,11 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
3232
removeClass: (/* className: string */) => {},
3333
setAriaHidden: () => {},
3434
unsetAriaHidden: () => {},
35-
setMessageText: (/* message: string */) => {},
36-
setActionText: (/* actionText: string */) => {},
3735
setActionAriaHidden: () => {},
3836
unsetActionAriaHidden: () => {},
37+
setActionText: (/* actionText: string */) => {},
38+
setMessageText: (/* message: string */) => {},
39+
setFocus: () => {},
3940
visibilityIsHidden: () => /* boolean */ false,
4041
registerBlurHandler: (/* handler: EventListener */) => {},
4142
deregisterBlurHandler: (/* handler: EventListener */) => {},
@@ -74,7 +75,7 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
7475
this.snackbarHasFocus_ = true;
7576

7677
if (!this.adapter_.visibilityIsHidden()) {
77-
setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || MESSAGE_TIMEOUT);
78+
setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || numbers.MESSAGE_TIMEOUT);
7879
}
7980
};
8081
this.interactionHandler_ = (evt) => {
@@ -90,7 +91,7 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
9091
this.blurHandler_ = () => {
9192
clearTimeout(this.timeoutId_);
9293
this.snackbarHasFocus_ = false;
93-
this.timeoutId_ = setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || MESSAGE_TIMEOUT);
94+
this.timeoutId_ = setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || numbers.MESSAGE_TIMEOUT);
9495
};
9596
}
9697

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

103104
destroy() {
104105
this.adapter_.deregisterActionClickHandler(this.actionClickHandler_);
105-
this.adapter_.deregisterBlurHandler(this.focusHandler_);
106+
this.adapter_.deregisterBlurHandler(this.blurHandler_);
106107
this.adapter_.deregisterVisibilityChangeHandler(this.visibilitychangeHandler_);
107108
['touchstart', 'mousedown', 'focus'].forEach((evtType) => {
108109
this.adapter_.deregisterCapturedInteractionHandler(evtType, this.interactionHandler_);
@@ -142,7 +143,6 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
142143
}
143144

144145
const {ACTIVE, MULTILINE, ACTION_ON_BOTTOM} = cssClasses;
145-
const {MESSAGE_TIMEOUT} = numbers;
146146

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

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

170-
this.timeoutId_ = setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || MESSAGE_TIMEOUT);
170+
this.timeoutId_ = setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || numbers.MESSAGE_TIMEOUT);
171171
}
172172

173173
handlePossibleTabKeyboardFocus_() {

test/unit/mdc-snackbar/foundation.test.js

Lines changed: 151 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,19 @@ test('defaultAdapter returns a complete adapter implementation', () => {
4343

4444
assert.equal(methods.length, Object.keys(defaultAdapter).length, 'Every adapter key must be a function');
4545
assert.deepEqual(methods, [
46-
'addClass', 'removeClass', 'setAriaHidden', 'unsetAriaHidden', 'setMessageText',
47-
'setActionText', 'setActionAriaHidden', 'unsetActionAriaHidden', 'visibilityIsHidden',
46+
'addClass', 'removeClass', 'setAriaHidden', 'unsetAriaHidden', 'setActionAriaHidden',
47+
'unsetActionAriaHidden', 'setActionText', 'setMessageText', 'setFocus', 'visibilityIsHidden',
4848
'registerBlurHandler', 'deregisterBlurHandler', 'registerVisibilityChangeHandler',
4949
'deregisterVisibilityChangeHandler', 'registerCapturedInteractionHandler',
5050
'deregisterCapturedInteractionHandler', 'registerActionClickHandler',
51-
'deregisterActionClickHandler', 'registerTransitionEndHandler', 'deregisterTransitionEndHandler',
51+
'deregisterActionClickHandler', 'registerTransitionEndHandler',
52+
'deregisterTransitionEndHandler',
5253
]);
5354
// Test default methods
5455
methods.forEach((m) => assert.doesNotThrow(defaultAdapter[m]));
5556
});
5657

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

@@ -76,6 +77,32 @@ test('#destroy calls adapter.deregisterActionClickHandler() with a registerActio
7677
td.verify(mockAdapter.deregisterActionClickHandler(changeHandler));
7778
});
7879

80+
test('#destroy calls adapter.deregisterVisibilityChangeHandler() with a function', () => {
81+
const {foundation, mockAdapter} = setupTest();
82+
const {isA} = td.matchers;
83+
84+
foundation.destroy();
85+
td.verify(mockAdapter.deregisterVisibilityChangeHandler(isA(Function)));
86+
});
87+
88+
test('#destroy calls adapter.deregisterBlurHandler() with a function', () => {
89+
const {foundation, mockAdapter} = setupTest();
90+
const {isA} = td.matchers;
91+
92+
foundation.destroy();
93+
td.verify(mockAdapter.deregisterBlurHandler(isA(Function)));
94+
});
95+
96+
test('#destroy calls adapter.deregisterCapturedInteractionHandler() with an event type and function 3 times', () => {
97+
const {foundation, mockAdapter} = setupTest();
98+
const {isA} = td.matchers;
99+
100+
foundation.destroy();
101+
td.verify(mockAdapter.deregisterCapturedInteractionHandler('touchstart', isA(Function)));
102+
td.verify(mockAdapter.deregisterCapturedInteractionHandler('mousedown', isA(Function)));
103+
td.verify(mockAdapter.deregisterCapturedInteractionHandler('focus', isA(Function)));
104+
});
105+
79106
test('#init calls adapter.setAriaHidden to ensure snackbar starts hidden', () => {
80107
const {foundation, mockAdapter} = setupTest();
81108

@@ -313,6 +340,30 @@ test('#show will clean up snackbar after the timeout and transition end', () =>
313340
clock.uninstall();
314341
});
315342

343+
test('#show calls adapter.registerVisibilityChangeHandler() with a function', () => {
344+
const {foundation, mockAdapter} = setupTest();
345+
const {isA} = td.matchers;
346+
347+
foundation.show({message: 'foo'});
348+
td.verify(mockAdapter.registerVisibilityChangeHandler(isA(Function)));
349+
});
350+
351+
test('#show calls adapter.registerBlurHandler() with a function', () => {
352+
const {foundation, mockAdapter} = setupTest();
353+
const {isA} = td.matchers;
354+
355+
foundation.show({message: 'foo'});
356+
td.verify(mockAdapter.registerBlurHandler(isA(Function)));
357+
});
358+
359+
test('#show calls adapter.registerCapturedInteractionHandler() with an event type and function 3 times', () => {
360+
const {foundation, mockAdapter} = setupTest();
361+
const {isA} = td.matchers;
362+
363+
foundation.show({message: 'foo'});
364+
td.verify(mockAdapter.registerCapturedInteractionHandler(isA(String), isA(Function)), {times: 3});
365+
});
366+
316367
test('snackbar is dismissed after action button is pressed', () => {
317368
const {foundation, mockAdapter} = setupTest();
318369
const {isA} = td.matchers;
@@ -363,3 +414,99 @@ test('snackbar is not dismissed after action button is pressed if setDismissOnAc
363414

364415
td.verify(mockAdapter.removeClass(cssClasses.ACTIVE), {times: 0});
365416
});
417+
418+
test('snackbar is not dismissed if action button gets focus', () => {
419+
const {foundation, mockAdapter} = setupTest();
420+
const evtType = 'focus';
421+
const mockEvent = {type: 'focus'};
422+
let focusEvent;
423+
424+
td.when(mockAdapter.registerCapturedInteractionHandler(evtType, td.matchers.isA(Function)))
425+
.thenDo((evtType, handler) => {
426+
focusEvent = handler;
427+
});
428+
429+
foundation.init();
430+
foundation.show({message: 'foo'});
431+
focusEvent(mockEvent);
432+
433+
foundation.show({message: 'foo'});
434+
435+
td.verify(mockAdapter.removeClass(cssClasses.ACTIVE), {times: 0});
436+
});
437+
438+
test('focus hijacks the snackbar timeout if no click or touchstart occurs', () => {
439+
const {foundation, mockAdapter} = setupTest();
440+
const mockEvent = {type: 'focus'};
441+
let tabEvent;
442+
443+
td.when(mockAdapter.registerCapturedInteractionHandler(mockEvent.type, td.matchers.isA(Function)))
444+
.thenDo((evt, handler) => {
445+
tabEvent = handler;
446+
});
447+
448+
foundation.init();
449+
foundation.show({message: 'foo'});
450+
tabEvent(mockEvent);
451+
452+
td.verify(mockAdapter.removeClass(cssClasses.ACTIVE), {times: 0});
453+
});
454+
455+
test('focus does not hijack the snackbar timeout if it occurs as a result' +
456+
'of a mousedown or touchstart', () => {
457+
const clock = lolex.install();
458+
const {foundation, mockAdapter} = setupTest();
459+
const mockFocusEvent = {type: 'focus'};
460+
const mockMouseEvent = {type: 'mousedown'};
461+
let focusEvent;
462+
let mouseEvent;
463+
464+
td.when(mockAdapter.registerCapturedInteractionHandler(mockFocusEvent.type, td.matchers.isA(Function)))
465+
.thenDo((evt, handler) => {
466+
focusEvent = handler;
467+
});
468+
td.when(mockAdapter.registerCapturedInteractionHandler(mockMouseEvent.type, td.matchers.isA(Function)))
469+
.thenDo((evt, handler) => {
470+
mouseEvent = handler;
471+
});
472+
473+
foundation.init();
474+
foundation.show({message: 'foo'});
475+
mouseEvent(mockMouseEvent);
476+
focusEvent(mockFocusEvent);
477+
clock.tick(numbers.MESSAGE_TIMEOUT);
478+
479+
td.verify(mockAdapter.removeClass(cssClasses.ACTIVE));
480+
clock.uninstall();
481+
});
482+
483+
test('blur resets the snackbar timeout', () => {
484+
const clock = lolex.install();
485+
const {foundation, mockAdapter} = setupTest();
486+
const {isA} = td.matchers;
487+
const mockBlurEvent = {type: 'blur'};
488+
const mockFocusEvent = {type: 'focus'};
489+
let focusEvent;
490+
let blurEvent;
491+
492+
td.when(mockAdapter.registerCapturedInteractionHandler(mockFocusEvent.type, td.matchers.isA(Function)))
493+
.thenDo((evt, handler) => {
494+
focusEvent = handler;
495+
});
496+
td.when(mockAdapter.registerBlurHandler(isA(Function)))
497+
.thenDo((handler) => {
498+
blurEvent = handler;
499+
});
500+
501+
foundation.init();
502+
foundation.show({message: 'foo'});
503+
focusEvent(mockFocusEvent);
504+
// Sanity Check
505+
td.verify(mockAdapter.removeClass(cssClasses.ACTIVE), {times: 0});
506+
507+
blurEvent(mockBlurEvent);
508+
clock.tick(numbers.MESSAGE_TIMEOUT);
509+
td.verify(mockAdapter.removeClass(cssClasses.ACTIVE));
510+
511+
clock.uninstall();
512+
});

test/unit/mdc-snackbar/mdc-snackbar.test.js

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -118,20 +118,24 @@ test('foundationAdapter#unsetActionAriaHidden removes "aria-hidden" from the act
118118
assert.isNotOk(actionButton.getAttribute('aria-hidden'));
119119
});
120120

121-
// TODO: return to this
122121
test('adapter#setFocus sets focus on the action button', () => {
123-
const {actionButton, component} = setupTest();
122+
const {root, actionButton, component} = setupTest();
123+
const handler = td.func('fixture focus handler');
124+
root.addEventListener('focus', handler);
125+
document.body.appendChild(root);
126+
124127
component.getDefaultFoundation().adapter_.setFocus();
128+
125129
assert.equal(document.activeElement, actionButton);
130+
document.body.removeChild(root);
126131
});
127132

128-
// TODO: return to this
129-
test.only('adapter#visibilityIsHidden returns the document.hidden property', () => {
133+
test('adapter#visibilityIsHidden returns the document.hidden property', () => {
130134
const {component} = setupTest();
131-
assert.isTrue(component.getDefaultFoundation().adapter_.visibilityIsHidden());
135+
assert.equal(component.getDefaultFoundation().adapter_.visibilityIsHidden(), document.hidden);
132136
});
133137

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

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

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

@@ -152,7 +156,7 @@ test.only('adapter#deregisterBlurHandler removes a handler to be called on a blu
152156
td.verify(handler(td.matchers.anything()), {times: 0});
153157
});
154158

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

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

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

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

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

187-
test.only('adapter#deregisterCapturedInteractionHandler removes a handler to be called when a given event occurs', () => {
191+
test('adapter#deregisterCapturedInteractionHandler removes a handler to be called when a given event occurs', () => {
188192
const {component} = setupTest();
189193
const handler = td.func('interactionHandler');
190194
const mockEvent = 'click';

0 commit comments

Comments
 (0)