-
+
@@ -171,28 +205,52 @@
Basic Example
var MDCSnackbar = global.mdc.snackbar.MDCSnackbar;
var snackbar = new MDCSnackbar(document.getElementById('mdc-js-snackbar'));
var rtlSnackbar = new MDCSnackbar(document.getElementById('mdc-rtl-js-snackbar'));
+ var alignStartSnackbar = new MDCSnackbar(document.getElementById('mdc-align-start-js-snackbar'));
+ var alignStartRTLSnackbar = new MDCSnackbar(document.getElementById('mdc-align-start-rtl-js-snackbar'));
var messageInput = document.getElementById('message');
var actionInput = document.getElementById('action');
var multilineInput = document.getElementById('multiline');
var actionOnBottomInput = document.getElementById('action-on-bottom');
+ var actionOnBottomCheckbox = document.getElementById('action-on-bottom-checkbox');
var dismissOnActionInput = document.getElementById('dismiss-on-action');
+ var textFields = document.querySelectorAll('.mdc-textfield');
+
+ // Since Action on Bottom cannot be checked if Multi-line Input
+ // is not, we start with a disabled Action on Bottom option
+ actionOnBottomCheckbox.classList.add('mdc-checkbox--disabled');
+ actionOnBottomInput.disabled = true;
+ actionOnBottomInput.checked = false;
var show = function(sb) {
snackbar.dismissesOnAction = dismissOnActionInput.checked;
var data = {
message: messageInput.value,
actionOnBottom: actionOnBottomInput.checked,
- multiline: multilineInput.checked
+ multiline: multilineInput.checked,
+ timeout: 2750
};
+
if (actionInput.value) {
data.actionText = actionInput.value;
data.actionHandler = function() {
console.log(data);
}
}
+
sb.show(data);
};
+ multilineInput.addEventListener('click', function () {
+ if (!multilineInput.checked) {
+ actionOnBottomCheckbox.classList.add('mdc-checkbox--disabled');
+ actionOnBottomInput.disabled = true;
+ actionOnBottomInput.checked = false;
+ } else {
+ actionOnBottomCheckbox.classList.remove('mdc-checkbox--disabled');
+ actionOnBottomInput.disabled = false;
+ }
+ });
+
document.getElementById('show-snackbar').addEventListener('click', function() {
show(snackbar);
});
@@ -201,6 +259,22 @@
Basic Example
show(rtlSnackbar);
});
+ document.getElementById('show-start-aligned-snackbar').addEventListener('click', function() {
+ show(alignStartSnackbar);
+ });
+
+ document.getElementById('show-start-aligned-rtl-snackbar').addEventListener('click', function() {
+ show(alignStartRTLSnackbar);
+ });
+
+ document.getElementById('toggle-dark-theme').addEventListener('click', function(evt) {
+ document.body.classList.contains('mdc-theme--dark') ? document.body.classList.remove('mdc-theme--dark') : document.body.classList.add('mdc-theme--dark');
+ });
+
+ textFields.forEach(function(tf) {
+ mdc.textfield.MDCTextfield.attachTo(tf);
+ })
+
// Remove any element hiding after loading.
window.onload = function() { document.body.className = ''; };
})(this);
diff --git a/packages/mdc-snackbar/README.md b/packages/mdc-snackbar/README.md
index 1cc9b36cb20..96589ae83bd 100644
--- a/packages/mdc-snackbar/README.md
+++ b/packages/mdc-snackbar/README.md
@@ -18,7 +18,7 @@ path: /catalog/snackbars/
The MDC Snackbar component is a spec-aligned snackbar/toast component adhering to the
[Material Design snackbars & toasts requirements](https://material.io/guidelines/components/snackbars-toasts.html#snackbars-toasts-specs).
-It requires JavaScript the trigger the display and hide of the snackbar.
+It requires JavaScript to show and hide itself.
## Design & API Documentation
@@ -53,6 +53,23 @@ npm install --save @material/snackbar
```
+### Start Aligned Snackbars (tablet and desktop only)
+
+MDC Snackbar can be start aligned (including in RTL contexts). To create a start-aligned
+snackbar, add the `mdc-snackbar--align-start` modifier class to the root element.
+
+```html
+
+```
+
### Using the JS Component
MDC Snackbar ships with a Component / Foundation combo which provides the API for showing snackbar
@@ -125,6 +142,38 @@ properties and their usage.
| multiline | Whether to show the snackbar with space for multiple lines of text | Optional | Boolean |
| actionOnBottom | Whether to show the action below the multiple lines of text | Optional, applies when multiline is true | Boolean |
+### Responding to a Snackbar Action
+
+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.
+
+```html
+
+```
+
+```js
+import {MDCSnackbar} from 'mdc-snackbar';
+
+const snackbar = new MDCSnackbar(document.querySelector('.mdc-snackbar'));
+const dataObj = {
+ message: messageInput.value,
+ actionText: 'Undo',
+ actionHandler: function () {
+ console.log('my cool function');
+ }
+};
+
+snackbar.show(dataObj);
+```
+
+
### Keep snackbar when the action button is pressed
By default the snackbar will be dimissed when the user presses the action button.
@@ -149,10 +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. |
+| `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/_variables.scss b/packages/mdc-snackbar/_variables.scss
index 86e25edab04..163c7396f36 100644
--- a/packages/mdc-snackbar/_variables.scss
+++ b/packages/mdc-snackbar/_variables.scss
@@ -16,6 +16,7 @@
// Hard coded since the color is not present in any palette.
$mdc-snackbar-background-color: #323232;
+$mdc-snackbar-background-color-on-dark: #fafafa;
$mdc-snackbar-foreground-color: white;
// TODO: Better spot to pull this breakpoint?
//$snackbar-tablet-breakpoint: $grid-tablet-breakpoint;
diff --git a/packages/mdc-snackbar/foundation.js b/packages/mdc-snackbar/foundation.js
index 2f4f48f2bf5..3b02f8a8f9e 100644
--- a/packages/mdc-snackbar/foundation.js
+++ b/packages/mdc-snackbar/foundation.js
@@ -32,10 +32,18 @@ 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,
+ registerCapturedBlurHandler: (/* handler: EventListener */) => {},
+ deregisterCapturedBlurHandler: (/* handler: EventListener */) => {},
+ registerVisibilityChangeHandler: (/* handler: EventListener */) => {},
+ deregisterVisibilityChangeHandler: (/* handler: EventListener */) => {},
+ registerCapturedInteractionHandler: (/* evtType: string, handler: EventListener */) => {},
+ deregisterCapturedInteractionHandler: (/* evtType: string, handler: EventListener */) => {},
registerActionClickHandler: (/* handler: EventListener */) => {},
deregisterActionClickHandler: (/* handler: EventListener */) => {},
registerTransitionEndHandler: (/* handler: EventListener */) => {},
@@ -51,9 +59,40 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
super(Object.assign(MDCSnackbarFoundation.defaultAdapter, adapter));
this.active_ = false;
+ this.actionWasClicked_ = false;
this.dismissOnAction_ = true;
+ this.firstFocus_ = true;
+ this.pointerDownRecognized_ = false;
+ this.snackbarHasFocus_ = false;
+ this.snackbarData_ = null;
this.queue_ = [];
- this.actionClickHandler_ = () => this.invokeAction_();
+ this.actionClickHandler_ = () => {
+ this.actionWasClicked_ = true;
+ this.invokeAction_();
+ };
+ this.visibilitychangeHandler_ = () => {
+ clearTimeout(this.timeoutId_);
+ this.snackbarHasFocus_ = true;
+
+ if (!this.adapter_.visibilityIsHidden()) {
+ setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || numbers.MESSAGE_TIMEOUT);
+ }
+ };
+ this.interactionHandler_ = (evt) => {
+ if (evt.type == 'touchstart' || evt.type == 'mousedown') {
+ this.pointerDownRecognized_ = true;
+ }
+ this.handlePossibleTabKeyboardFocus_(evt);
+
+ if (evt.type == 'focus') {
+ this.pointerDownRecognized_ = false;
+ }
+ };
+ this.blurHandler_ = () => {
+ clearTimeout(this.timeoutId_);
+ this.snackbarHasFocus_ = false;
+ this.timeoutId_ = setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || numbers.MESSAGE_TIMEOUT);
+ };
}
init() {
@@ -64,6 +103,11 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
destroy() {
this.adapter_.deregisterActionClickHandler(this.actionClickHandler_);
+ this.adapter_.deregisterCapturedBlurHandler(this.blurHandler_);
+ this.adapter_.deregisterVisibilityChangeHandler(this.visibilitychangeHandler_);
+ ['touchstart', 'mousedown', 'focus'].forEach((evtType) => {
+ this.adapter_.deregisterCapturedInteractionHandler(evtType, this.interactionHandler_);
+ });
}
dismissesOnAction() {
@@ -75,37 +119,43 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
}
show(data) {
- if (!data) {
+ clearTimeout(this.timeoutId_);
+ this.snackbarData_ = data;
+ this.firstFocus_ = true;
+ this.adapter_.registerVisibilityChangeHandler(this.visibilitychangeHandler_);
+ this.adapter_.registerCapturedBlurHandler(this.blurHandler_);
+ ['touchstart', 'mousedown', 'focus'].forEach((evtType) => {
+ this.adapter_.registerCapturedInteractionHandler(evtType, this.interactionHandler_);
+ });
+
+ if (!this.snackbarData_) {
throw new Error(
'Please provide a data object with at least a message to display.');
}
- if (!data.message) {
+ if (!this.snackbarData_.message) {
throw new Error('Please provide a message to be displayed.');
}
- if (data.actionHandler && !data.actionText) {
+ if (this.snackbarData_.actionHandler && !this.snackbarData_.actionText) {
throw new Error('Please provide action text with the handler.');
}
-
if (this.active) {
- this.queue_.push(data);
- return;
+ this.queue_.push(this.snackbarData_);
}
const {ACTIVE, MULTILINE, ACTION_ON_BOTTOM} = cssClasses;
- const {MESSAGE_TIMEOUT} = numbers;
- this.adapter_.setMessageText(data.message);
+ this.adapter_.setMessageText(this.snackbarData_.message);
- if (data.multiline) {
+ if (this.snackbarData_.multiline) {
this.adapter_.addClass(MULTILINE);
- if (data.actionOnBottom) {
+ if (this.snackbarData_.actionOnBottom) {
this.adapter_.addClass(ACTION_ON_BOTTOM);
}
}
- if (data.actionHandler) {
- this.adapter_.setActionText(data.actionText);
- this.actionHandler_ = data.actionHandler;
+ if (this.snackbarData_.actionHandler) {
+ this.adapter_.setActionText(this.snackbarData_.actionText);
+ this.actionHandler_ = this.snackbarData_.actionHandler;
this.setActionHidden_(false);
} else {
this.setActionHidden_(true);
@@ -117,7 +167,24 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
this.adapter_.addClass(ACTIVE);
this.adapter_.unsetAriaHidden();
- this.timeoutId_ = setTimeout(this.cleanup_.bind(this), data.timeout || MESSAGE_TIMEOUT);
+ this.timeoutId_ = setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || numbers.MESSAGE_TIMEOUT);
+ }
+
+ handlePossibleTabKeyboardFocus_() {
+ const hijackFocus =
+ this.firstFocus_ && !this.pointerDownRecognized_;
+
+ if (hijackFocus) {
+ this.setFocusOnAction_();
+ }
+
+ this.firstFocus_ = false;
+ }
+
+ setFocusOnAction_() {
+ this.adapter_.setFocus();
+ this.snackbarHasFocus_ = true;
+ this.firstFocus_ = false;
}
invokeAction_() {
@@ -129,37 +196,39 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
this.actionHandler_();
} finally {
if (this.dismissOnAction_) {
- clearTimeout(this.timeoutId_);
this.cleanup_();
}
}
}
cleanup_() {
- const {ACTIVE, MULTILINE, ACTION_ON_BOTTOM} = cssClasses;
+ const allowDismissal = !this.snackbarHasFocus_ || this.actionWasClicked_;
- this.adapter_.removeClass(ACTIVE);
+ if (allowDismissal) {
+ const {ACTIVE, MULTILINE, ACTION_ON_BOTTOM} = cssClasses;
- const handler = () => {
- this.adapter_.deregisterTransitionEndHandler(handler);
- this.adapter_.removeClass(MULTILINE);
- this.adapter_.removeClass(ACTION_ON_BOTTOM);
- this.setActionHidden_(true);
- this.adapter_.setMessageText(null);
- this.adapter_.setActionText(null);
- this.adapter_.setAriaHidden();
- this.active_ = false;
- this.showNext_();
- };
+ this.adapter_.removeClass(ACTIVE);
- this.adapter_.registerTransitionEndHandler(handler);
+ const handler = () => {
+ clearTimeout(this.timeoutId_);
+ this.adapter_.deregisterTransitionEndHandler(handler);
+ this.adapter_.removeClass(MULTILINE);
+ this.adapter_.removeClass(ACTION_ON_BOTTOM);
+ this.setActionHidden_(true);
+ this.adapter_.setAriaHidden();
+ this.active_ = false;
+ this.snackbarHasFocus_ = false;
+ this.showNext_();
+ };
+
+ this.adapter_.registerTransitionEndHandler(handler);
+ }
}
showNext_() {
if (!this.queue_.length) {
return;
}
-
this.show(this.queue_.shift());
}
diff --git a/packages/mdc-snackbar/index.js b/packages/mdc-snackbar/index.js
index 6121f08cf59..7fe7d3da567 100644
--- a/packages/mdc-snackbar/index.js
+++ b/packages/mdc-snackbar/index.js
@@ -47,6 +47,16 @@ export class MDCSnackbar extends MDCComponent {
unsetActionAriaHidden: () => getActionButton().removeAttribute('aria-hidden'),
setActionText: (text) => { getActionButton().textContent = text; },
setMessageText: (text) => { getText().textContent = text; },
+ setFocus: () => getActionButton().focus(),
+ visibilityIsHidden: () => document.hidden,
+ 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) =>
+ document.body.addEventListener(evt, handler, true),
+ deregisterCapturedInteractionHandler: (evt, handler) =>
+ document.body.removeEventListener(evt, handler, true),
registerActionClickHandler: (handler) => getActionButton().addEventListener('click', handler),
deregisterActionClickHandler: (handler) => getActionButton().removeEventListener('click', handler),
registerTransitionEndHandler:
diff --git a/packages/mdc-snackbar/mdc-snackbar.scss b/packages/mdc-snackbar/mdc-snackbar.scss
index b7ccf0ff3b2..9a2034094c5 100644
--- a/packages/mdc-snackbar/mdc-snackbar.scss
+++ b/packages/mdc-snackbar/mdc-snackbar.scss
@@ -30,31 +30,60 @@
justify-content: flex-start;
padding-right: 24px;
padding-left: 24px;
- transform: translate(0, 100%);
+ transform: translate(-50%, 100%);
transition: mdc-animation-exit(transform, .25s);
background-color: $mdc-snackbar-background-color;
+
+ @include mdc-theme-dark(".mdc-snackbar") {
+ background-color: $mdc-snackbar-background-color-on-dark;
+ }
+
will-change: transform;
pointer-events: none;
@media (max-width: ($mdc-snackbar-tablet-breakpoint - 1)) {
left: 0;
width: calc(100% - 48px);
+ transform: translate(0, 100%);
}
@media (min-width: $mdc-snackbar-tablet-breakpoint) {
min-width: 288px;
max-width: 568px;
- transform: translate(-50%, 100%);
border-radius: 2px;
}
+ &--align-start {
+ @media (min-width: $mdc-snackbar-tablet-breakpoint) {
+ bottom: 24px;
+
+ @include mdc-rtl-reflexive-position(left, 24px);
+
+ transform: translate(0, 200%);
+ }
+
+ @media (max-width: ($mdc-snackbar-tablet-breakpoint - 1)) {
+ bottom: 0;
+ left: 0;
+ width: calc(100% - 48px);
+ transform: translate(0, 100%);
+ }
+ }
+
&--active {
- transform: translate(0, 0);
+ transform: translate(0);
pointer-events: auto;
transition: mdc-animation-enter(transform, .25s);
- @media (min-width: $mdc-snackbar-tablet-breakpoint) {
+ &:not(.mdc-snackbar--align-start) {
transform: translate(-50%, 0);
+
+ @media (max-width: ($mdc-snackbar-tablet-breakpoint - 1)) {
+ bottom: 0;
+ left: 0;
+ width: calc(100% - 48px);
+ transform: translate(0);
+ }
}
}
@@ -84,6 +113,11 @@
height: 48px;
transition: mdc-animation-exit(opacity, .3s);
color: $mdc-snackbar-foreground-color;
+
+ @include mdc-theme-dark(".mdc-snackbar") {
+ @include mdc-theme-prop(color, text-primary-on-light);
+ }
+
opacity: 0;
}
@@ -99,6 +133,11 @@
&__action-button {
@include mdc-theme-prop(color, accent);
+
+ @include mdc-theme-dark(".mdc-snackbar") {
+ @include mdc-theme-prop(color, primary);
+ }
+
@include mdc-rtl-reflexive-box(margin, right, -16px, ".mdc-snackbar");
min-width: auto;
diff --git a/test/unit/mdc-snackbar/foundation.test.js b/test/unit/mdc-snackbar/foundation.test.js
index b98acf9cec6..51241b58e2d 100644
--- a/test/unit/mdc-snackbar/foundation.test.js
+++ b/test/unit/mdc-snackbar/foundation.test.js
@@ -43,16 +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',
- 'registerActionClickHandler', 'deregisterActionClickHandler',
- 'registerTransitionEndHandler', 'deregisterTransitionEndHandler',
+ 'addClass', 'removeClass', 'setAriaHidden', 'unsetAriaHidden', 'setActionAriaHidden',
+ 'unsetActionAriaHidden', 'setActionText', 'setMessageText', 'setFocus', 'visibilityIsHidden',
+ 'registerCapturedBlurHandler', 'deregisterCapturedBlurHandler', 'registerVisibilityChangeHandler',
+ 'deregisterVisibilityChangeHandler', 'registerCapturedInteractionHandler',
+ 'deregisterCapturedInteractionHandler', 'registerActionClickHandler',
+ '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;
@@ -74,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();
@@ -216,7 +243,8 @@ test('#show while snackbar is already showing will queue the data object.', () =
message: 'Message Archived',
});
- td.verify(mockAdapter.setMessageText('Message Archived'), {times: 0});
+ td.verify(mockAdapter.setMessageText('Message Deleted'));
+ td.verify(mockAdapter.setMessageText('Message Archived'));
});
test('#show while snackbar is already showing will show after the timeout and transition end', () => {
@@ -224,14 +252,13 @@ test('#show while snackbar is already showing will show after the timeout and tr
const {foundation, mockAdapter} = setupTest();
const {isA} = td.matchers;
- foundation.init();
-
let transEndHandler;
td.when(mockAdapter.registerTransitionEndHandler(isA(Function)))
.thenDo((handler) => {
transEndHandler = handler;
});
+ foundation.init();
foundation.show({
message: 'Message Deleted',
});
@@ -304,8 +331,6 @@ test('#show will clean up snackbar after the timeout and transition end', () =>
clock.tick(numbers.MESSAGE_TIMEOUT);
transEndHandler();
- td.verify(mockAdapter.setMessageText(null));
- td.verify(mockAdapter.setActionText(null));
td.verify(mockAdapter.removeClass(cssClasses.MULTILINE));
td.verify(mockAdapter.removeClass(cssClasses.ACTION_ON_BOTTOM));
td.verify(mockAdapter.deregisterTransitionEndHandler(transEndHandler));
@@ -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;
@@ -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();
+});
diff --git a/test/unit/mdc-snackbar/mdc-snackbar.test.js b/test/unit/mdc-snackbar/mdc-snackbar.test.js
index 4cf5978f9b3..18054db97f1 100644
--- a/test/unit/mdc-snackbar/mdc-snackbar.test.js
+++ b/test/unit/mdc-snackbar/mdc-snackbar.test.js
@@ -36,8 +36,9 @@ function getFixture() {
function setupTest() {
const root = getFixture();
+ const actionButton = root.querySelector(strings.ACTION_BUTTON_SELECTOR);
const component = new MDCSnackbar(root);
- return {root, component};
+ return {root, actionButton, component};
}
suite('MDCSnackbar');
@@ -117,6 +118,88 @@ test('foundationAdapter#unsetActionAriaHidden removes "aria-hidden" from the act
assert.isNotOk(actionButton.getAttribute('aria-hidden'));
});
+test('adapter#setFocus sets focus on the action button', () => {
+ 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);
+});
+
+test('adapter#visibilityIsHidden returns the document.hidden property', () => {
+ const {component} = setupTest();
+ assert.equal(component.getDefaultFoundation().adapter_.visibilityIsHidden(), document.hidden);
+});
+
+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_.registerCapturedBlurHandler(handler);
+ domEvents.emit(actionButton, 'blur');
+
+ td.verify(handler(td.matchers.anything()));
+});
+
+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_.deregisterCapturedBlurHandler(handler);
+ domEvents.emit(actionButton, 'blur');
+
+ td.verify(handler(td.matchers.anything()), {times: 0});
+});
+
+test('adapter#registerVisibilityChangeHandler adds a handler to be called on a visibilitychange event', () => {
+ const {component} = setupTest();
+ const handler = td.func('visibilitychangeHandler');
+
+ component.getDefaultFoundation().adapter_.registerVisibilityChangeHandler(handler);
+ domEvents.emit(document, 'visibilitychange');
+
+ td.verify(handler(td.matchers.anything()));
+});
+
+test('adapter#deregisterVisibilityChangeHandler removes a handler to be called on a visibilitychange event', () => {
+ const {component} = setupTest();
+ const handler = td.func('visibilitychangeHandler');
+
+ document.addEventListener('visibilitychange', handler);
+ component.getDefaultFoundation().adapter_.deregisterVisibilityChangeHandler(handler);
+ domEvents.emit(document, 'visibilitychange');
+
+ td.verify(handler(td.matchers.anything()), {times: 0});
+});
+
+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';
+
+ component.getDefaultFoundation().adapter_.registerCapturedInteractionHandler(mockEvent, handler);
+ domEvents.emit(document.body, mockEvent);
+
+ td.verify(handler(td.matchers.anything()));
+});
+
+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';
+
+ document.body.addEventListener(mockEvent, handler, true);
+ component.getDefaultFoundation().adapter_.deregisterCapturedInteractionHandler(mockEvent, handler);
+ domEvents.emit(document.body, mockEvent);
+
+ td.verify(handler(td.matchers.anything()), {times: 0});
+});
+
test('foundationAdapter#registerActionClickHandler adds the handler to be called when action is clicked', () => {
const {root, component} = setupTest();
const handler = td.func('clickHandler');