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

Commit 0f3a059

Browse files
committed
WIP
1 parent 0947a02 commit 0f3a059

File tree

6 files changed

+150
-42
lines changed

6 files changed

+150
-42
lines changed

demos/snackbar.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ <h2 class="mdc-typography--title">Basic Example</h2>
227227
message: messageInput.value,
228228
actionOnBottom: actionOnBottomInput.checked,
229229
multiline: multilineInput.checked,
230-
timeout: 3000
230+
timeout: 2750
231231
};
232232

233233
if (actionInput.value) {

packages/mdc-snackbar/README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,6 @@ import {MDCSnackbar} from 'mdc-snackbar';
126126
const snackbar = new MDCSnackbar(document.querySelector('.mdc-snackbar'));
127127
```
128128

129-
130129
### Showing a message and action
131130

132131
Once you have obtained an MDCSnackbar instance attached to the DOM, you can use
@@ -203,6 +202,12 @@ The adapter for snackbars must provide the following functions, with correct sig
203202
| `setActionText(actionText: string) => void` | Set the text content of the action element. |
204203
| `setActionAriaHidden() => void` | Sets `aria-hidden="true"` on the action element. |
205204
| `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 |
208+
| `deregisterBlurHandler(handler: EventListener) => void` | Deregisters a `blur` event handler from the actionButton |
209+
| `registerVisibilityChangeHandler(handler: EventListener) => void` | Registers an event handler to be called when a 'visibilitychange' event occurs |
210+
| `deregisterVisibilityChangeHandler(handler: EventListener) => void` | Deregisters an event handler to be called when a 'visibilitychange' event occurs |
206211
| `registerActionClickHandler(handler: EventListener) => void` | Registers an event handler to be called when a `click` event is triggered on the action element. |
207212
| `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. |
208213
| `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: 48 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
3636
setActionText: (/* actionText: string */) => {},
3737
setActionAriaHidden: () => {},
3838
unsetActionAriaHidden: () => {},
39-
registerFocusHandler: () => {},
40-
deregisterFocusHandler: () => {},
41-
registerBlurHandler: () => {},
42-
deregisterBlurHandler: () => {},
39+
visibilityIsHidden: () => /* boolean */ false,
40+
registerBlurHandler: (/* handler: EventListener */) => {},
41+
deregisterBlurHandler: (/* handler: EventListener */) => {},
42+
registerVisibilityChangeHandler: (/* handler: EventListener */) => {},
43+
deregisterVisibilityChangeHandler: (/* handler: EventListener */) => {},
44+
registerCapturedInteractionHandler: (/* evtType: string, handler: EventListener */) => {},
45+
deregisterCapturedInteractionHandler: (/* evtType: string, handler: EventListener */) => {},
4346
registerActionClickHandler: (/* handler: EventListener */) => {},
4447
deregisterActionClickHandler: (/* handler: EventListener */) => {},
4548
registerTransitionEndHandler: (/* handler: EventListener */) => {},
@@ -58,26 +61,14 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
5861
this.actionWasClicked_ = false;
5962
this.dismissOnAction_ = true;
6063
this.firstFocus_ = true;
64+
this.pointerDownRecognized_ = false;
6165
this.snackbarHasFocus_ = false;
6266
this.snackbarData_ = null;
6367
this.queue_ = [];
64-
6568
this.actionClickHandler_ = () => {
6669
this.actionWasClicked_ = true;
6770
this.invokeAction_();
6871
};
69-
this.focusHandler_ = () => {
70-
if (this.firstFocus_) {
71-
this.setFocusOnAction_();
72-
}
73-
74-
this.firstFocus_ = false;
75-
};
76-
this.blurHandler_ = () => {
77-
this.snackbarHasFocus_ = false;
78-
clearTimeout(this.timeoutId_);
79-
this.timeoutId_ = setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || MESSAGE_TIMEOUT);
80-
};
8172
this.visibilitychangeHandler_ = () => {
8273
clearTimeout(this.timeoutId_);
8374
this.snackbarHasFocus_ = true;
@@ -86,6 +77,21 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
8677
setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || MESSAGE_TIMEOUT);
8778
}
8879
};
80+
this.interactionHandler_ = (evt) => {
81+
if (evt.type == 'touchstart' || evt.type == 'mousedown') {
82+
this.pointerDownRecognized_ = true;
83+
}
84+
this.handlePossibleTabKeyboardFocus_(evt);
85+
86+
if (evt.type == 'focus') {
87+
this.pointerDownRecognized_ = false;
88+
}
89+
};
90+
this.blurHandler_ = () => {
91+
clearTimeout(this.timeoutId_);
92+
this.snackbarHasFocus_ = false;
93+
this.timeoutId_ = setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || MESSAGE_TIMEOUT);
94+
};
8995
}
9096

9197
init() {
@@ -96,8 +102,11 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
96102

97103
destroy() {
98104
this.adapter_.deregisterActionClickHandler(this.actionClickHandler_);
99-
this.adapter_.deregisterFocusHandler(this.focusHandler_);
100105
this.adapter_.deregisterBlurHandler(this.focusHandler_);
106+
this.adapter_.deregisterVisibilityChangeHandler(this.visibilitychangeHandler_);
107+
['touchstart', 'mousedown', 'focus'].forEach((evtType) => {
108+
this.adapter_.deregisterCapturedInteractionHandler(evtType, this.interactionHandler_);
109+
});
101110
}
102111

103112
dismissesOnAction() {
@@ -109,11 +118,14 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
109118
}
110119

111120
show(data) {
121+
clearTimeout(this.timeoutId_);
112122
this.snackbarData_ = data;
113123
this.firstFocus_ = true;
114-
this.adapter_.registerVisbilityChangeHandler(this.visibilitychangeHandler_);
115-
this.adapter_.registerFocusHandler(this.focusHandler_);
124+
this.adapter_.registerVisibilityChangeHandler(this.visibilitychangeHandler_);
116125
this.adapter_.registerBlurHandler(this.blurHandler_);
126+
['touchstart', 'mousedown', 'focus'].forEach((evtType) => {
127+
this.adapter_.registerCapturedInteractionHandler(evtType, this.interactionHandler_);
128+
});
117129

118130
if (!this.snackbarData_) {
119131
throw new Error(
@@ -125,10 +137,8 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
125137
if (this.snackbarData_.actionHandler && !this.snackbarData_.actionText) {
126138
throw new Error('Please provide action text with the handler.');
127139
}
128-
129140
if (this.active) {
130141
this.queue_.push(this.snackbarData_);
131-
return;
132142
}
133143

134144
const {ACTIVE, MULTILINE, ACTION_ON_BOTTOM} = cssClasses;
@@ -160,9 +170,21 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
160170
this.timeoutId_ = setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || MESSAGE_TIMEOUT);
161171
}
162172

173+
handlePossibleTabKeyboardFocus_() {
174+
const hijackFocus =
175+
this.firstFocus_ && !this.pointerDownRecognized_;
176+
177+
if (hijackFocus) {
178+
this.setFocusOnAction_();
179+
}
180+
181+
this.firstFocus_ = false;
182+
}
183+
163184
setFocusOnAction_() {
164185
this.adapter_.setFocus();
165186
this.snackbarHasFocus_ = true;
187+
this.firstFocus_ = false;
166188
}
167189

168190
invokeAction_() {
@@ -174,27 +196,28 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
174196
this.actionHandler_();
175197
} finally {
176198
if (this.dismissOnAction_) {
177-
clearTimeout(this.timeoutId_);
178199
this.cleanup_();
179200
}
180201
}
181202
}
182203

183204
cleanup_() {
184-
if (!this.snackbarHasFocus_ || this.actionWasClicked_) {
205+
const allowDismissal = !this.snackbarHasFocus_ || this.actionWasClicked_;
206+
207+
if (allowDismissal) {
185208
const {ACTIVE, MULTILINE, ACTION_ON_BOTTOM} = cssClasses;
186209

187210
this.adapter_.removeClass(ACTIVE);
188211

189212
const handler = () => {
213+
clearTimeout(this.timeoutId_);
190214
this.adapter_.deregisterTransitionEndHandler(handler);
191215
this.adapter_.removeClass(MULTILINE);
192216
this.adapter_.removeClass(ACTION_ON_BOTTOM);
193217
this.setActionHidden_(true);
194218
this.adapter_.setAriaHidden();
195219
this.active_ = false;
196220
this.snackbarHasFocus_ = false;
197-
clearTimeout(this.timeoutId_);
198221
this.showNext_();
199222
};
200223

@@ -206,7 +229,6 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
206229
if (!this.queue_.length) {
207230
return;
208231
}
209-
210232
this.show(this.queue_.shift());
211233
}
212234

packages/mdc-snackbar/index.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,14 @@ export class MDCSnackbar extends MDCComponent {
4949
setMessageText: (text) => { getText().textContent = text; },
5050
setFocus: () => getActionButton().focus(),
5151
visibilityIsHidden: () => document.hidden,
52-
registerFocusHandler: (handler) => document.body.addEventListener('focus', handler, true),
53-
deregisterFocusHandler: (handler) => document.body.addEventListener('focus', handler, true),
5452
registerBlurHandler: (handler) => getActionButton().addEventListener('blur', handler, true),
55-
deregisterBlurHandler: (handler) => getActionButton().addEventListener('blur', handler, true),
56-
registerVisbilityChangeHandler: (handler) => document.addEventListener('visibilitychange', handler),
57-
deregisterVisbilityChangeHandler: (handler) => document.addEventListener('visibilitychange', handler),
53+
deregisterBlurHandler: (handler) => getActionButton().removeEventListener('blur', handler, true),
54+
registerVisibilityChangeHandler: (handler) => document.addEventListener('visibilitychange', handler),
55+
deregisterVisibilityChangeHandler: (handler) => document.removeEventListener('visibilitychange', handler),
56+
registerCapturedInteractionHandler: (evt, handler) =>
57+
document.body.addEventListener(evt, handler, true),
58+
deregisterCapturedInteractionHandler: (evt, handler) =>
59+
document.body.removeEventListener(evt, handler, true),
5860
registerActionClickHandler: (handler) => getActionButton().addEventListener('click', handler),
5961
deregisterActionClickHandler: (handler) => getActionButton().removeEventListener('click', handler),
6062
registerTransitionEndHandler:

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,11 @@ test('defaultAdapter returns a complete adapter implementation', () => {
4444
assert.equal(methods.length, Object.keys(defaultAdapter).length, 'Every adapter key must be a function');
4545
assert.deepEqual(methods, [
4646
'addClass', 'removeClass', 'setAriaHidden', 'unsetAriaHidden', 'setMessageText',
47-
'setActionText', 'setActionAriaHidden', 'unsetActionAriaHidden',
48-
'registerActionClickHandler', 'deregisterActionClickHandler',
49-
'registerTransitionEndHandler', 'deregisterTransitionEndHandler',
47+
'setActionText', 'setActionAriaHidden', 'unsetActionAriaHidden', 'visibilityIsHidden',
48+
'registerBlurHandler', 'deregisterBlurHandler', 'registerVisibilityChangeHandler',
49+
'deregisterVisibilityChangeHandler', 'registerCapturedInteractionHandler',
50+
'deregisterCapturedInteractionHandler', 'registerActionClickHandler',
51+
'deregisterActionClickHandler', 'registerTransitionEndHandler', 'deregisterTransitionEndHandler',
5052
]);
5153
// Test default methods
5254
methods.forEach((m) => assert.doesNotThrow(defaultAdapter[m]));
@@ -216,22 +218,22 @@ test('#show while snackbar is already showing will queue the data object.', () =
216218
message: 'Message Archived',
217219
});
218220

219-
td.verify(mockAdapter.setMessageText('Message Archived'), {times: 0});
221+
td.verify(mockAdapter.setMessageText('Message Deleted'));
222+
td.verify(mockAdapter.setMessageText('Message Archived'));
220223
});
221224

222225
test('#show while snackbar is already showing will show after the timeout and transition end', () => {
223226
const clock = lolex.install();
224227
const {foundation, mockAdapter} = setupTest();
225228
const {isA} = td.matchers;
226229

227-
foundation.init();
228-
229230
let transEndHandler;
230231
td.when(mockAdapter.registerTransitionEndHandler(isA(Function)))
231232
.thenDo((handler) => {
232233
transEndHandler = handler;
233234
});
234235

236+
foundation.init();
235237
foundation.show({
236238
message: 'Message Deleted',
237239
});
@@ -304,8 +306,6 @@ test('#show will clean up snackbar after the timeout and transition end', () =>
304306
clock.tick(numbers.MESSAGE_TIMEOUT);
305307
transEndHandler();
306308

307-
td.verify(mockAdapter.setMessageText(null));
308-
td.verify(mockAdapter.setActionText(null));
309309
td.verify(mockAdapter.removeClass(cssClasses.MULTILINE));
310310
td.verify(mockAdapter.removeClass(cssClasses.ACTION_ON_BOTTOM));
311311
td.verify(mockAdapter.deregisterTransitionEndHandler(transEndHandler));

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

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ function getFixture() {
3636

3737
function setupTest() {
3838
const root = getFixture();
39+
const actionButton = root.querySelector(strings.ACTION_BUTTON_SELECTOR);
3940
const component = new MDCSnackbar(root);
40-
return {root, component};
41+
return {root, actionButton, component};
4142
}
4243

4344
suite('MDCSnackbar');
@@ -117,6 +118,84 @@ test('foundationAdapter#unsetActionAriaHidden removes "aria-hidden" from the act
117118
assert.isNotOk(actionButton.getAttribute('aria-hidden'));
118119
});
119120

121+
// TODO: return to this
122+
test('adapter#setFocus sets focus on the action button', () => {
123+
const {actionButton, component} = setupTest();
124+
component.getDefaultFoundation().adapter_.setFocus();
125+
assert.equal(document.activeElement, actionButton);
126+
});
127+
128+
// TODO: return to this
129+
test.only('adapter#visibilityIsHidden returns the document.hidden property', () => {
130+
const {component} = setupTest();
131+
assert.isTrue(component.getDefaultFoundation().adapter_.visibilityIsHidden());
132+
});
133+
134+
test.only('adapter#registerBlurHandler adds a handler to be called on a blur event', () => {
135+
const {actionButton, component} = setupTest();
136+
const handler = td.func('blurHandler');
137+
138+
component.getDefaultFoundation().adapter_.registerBlurHandler(handler);
139+
domEvents.emit(actionButton, 'blur');
140+
141+
td.verify(handler(td.matchers.anything()));
142+
});
143+
144+
test.only('adapter#deregisterBlurHandler removes a handler to be called on a blur event', () => {
145+
const {actionButton, component} = setupTest();
146+
const handler = td.func('blurHandler');
147+
148+
actionButton.addEventListener('blur', handler, true);
149+
component.getDefaultFoundation().adapter_.deregisterBlurHandler(handler);
150+
domEvents.emit(actionButton, 'blur');
151+
152+
td.verify(handler(td.matchers.anything()), {times: 0});
153+
});
154+
155+
test.only('adapter#registerVisibilityChangeHandler adds a handler to be called on a visibilitychange event', () => {
156+
const {component} = setupTest();
157+
const handler = td.func('visibilitychangeHandler');
158+
159+
component.getDefaultFoundation().adapter_.registerVisibilityChangeHandler(handler);
160+
domEvents.emit(document, 'visibilitychange');
161+
162+
td.verify(handler(td.matchers.anything()));
163+
});
164+
165+
test.only('adapter#deregisterVisibilityChangeHandler removes a handler to be called on a visibilitychange event', () => {
166+
const {component} = setupTest();
167+
const handler = td.func('visibilitychangeHandler');
168+
169+
document.addEventListener('visibilitychange', handler);
170+
component.getDefaultFoundation().adapter_.deregisterVisibilityChangeHandler(handler);
171+
domEvents.emit(document, 'visibilitychange');
172+
173+
td.verify(handler(td.matchers.anything()), {times: 0});
174+
});
175+
176+
test.only('adapter#registerCapturedInteractionHandler adds a handler to be called when a given event occurs', () => {
177+
const {component} = setupTest();
178+
const handler = td.func('interactionHandler');
179+
const mockEvent = 'click';
180+
181+
component.getDefaultFoundation().adapter_.registerCapturedInteractionHandler(mockEvent, handler);
182+
domEvents.emit(document.body, mockEvent);
183+
184+
td.verify(handler(td.matchers.anything()));
185+
});
186+
187+
test.only('adapter#deregisterCapturedInteractionHandler removes a handler to be called when a given event occurs', () => {
188+
const {component} = setupTest();
189+
const handler = td.func('interactionHandler');
190+
const mockEvent = 'click';
191+
192+
document.body.addEventListener(mockEvent, handler, true);
193+
component.getDefaultFoundation().adapter_.deregisterCapturedInteractionHandler(mockEvent, handler);
194+
domEvents.emit(document.body, mockEvent);
195+
196+
td.verify(handler(td.matchers.anything()), {times: 0});
197+
});
198+
120199
test('foundationAdapter#registerActionClickHandler adds the handler to be called when action is clicked', () => {
121200
const {root, component} = setupTest();
122201
const handler = td.func('clickHandler');

0 commit comments

Comments
 (0)