```
@@ -59,11 +63,8 @@ npm install @material/snackbar
### JavaScript Instantiation
-MDC Snackbar ships with a Component / Foundation combo which provides the API for showing snackbar messages with optional action.
-
```js
import {MDCSnackbar} from '@material/snackbar';
-
const snackbar = new MDCSnackbar(document.querySelector('.mdc-snackbar'));
```
@@ -71,35 +72,59 @@ const snackbar = new MDCSnackbar(document.querySelector('.mdc-snackbar'));
## Variants
-### Start Aligned Snackbars (tablet and desktop only)
+### Stacked
-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.
+Action buttons with long text should be positioned _below_ the label instead of alongside it. This can be accomplished by adding the `mdc-snackbar--stacked` modifier class to the root element:
```html
-
-
-
-
-
+
+ ...
+
+```
+
+Alternatively, you can call the `mdc-snackbar-layout-stacked` mixin from Sass:
+
+```scss
+@media (max-width: $mdc-snackbar-mobile-breakpoint) {
+ .my-snackbar {
+ @include mdc-snackbar-layout-stacked;
+ }
+}
+```
+
+### Leading (tablet and desktop only)
+
+By default, snackbars are centered horizontally within the viewport.
+
+On larger screens, they can optionally be displayed on the _leading_ edge of the screen (the left side in LTR, or the right side in RTL) by adding the `mdc-snackbar--leading` modifier class to the root element:
+
+```html
+
+ ...
```
-### Additional Information
+Alternatively, you can call the `mdc-snackbar-position-leading` mixin from Sass:
-#### Avoiding Flash-Of-Unstyled-Content (FOUC)
+```scss
+@media (min-width: $mdc-snackbar-mobile-breakpoint) {
+ .my-snackbar {
+ @include mdc-snackbar-position-leading;
+ }
+}
+```
-If you are loading the `mdc-snackbar` CSS asynchronously, you may experience a brief flash-of-unstyled-content (FOUC) due to the
-snackbar's translate transition running once the CSS loads. To avoid this temporary FOUC, you can add the following simple style
-before the `mdc-snackbar` CSS is loaded:
+### Wide (tablet and desktop only)
-```css
-.mdc-snackbar { transform: translateY(100%); }
+To increase the margins between the snackbar and the viewport on larger screens, call the `mdc-snackbar-viewport-margin` mixin inside a media query:
+
+```scss
+@media (min-width: $mdc-snackbar-mobile-breakpoint) {
+ .my-snackbar {
+ @include mdc-snackbar-viewport-margin($mdc-snackbar-viewport-margin-wide);
+ }
+}
```
-This will move the snackbar offscreen until the CSS is fully loaded and avoids a translate transition upon load.
## Style Customization
@@ -108,69 +133,150 @@ This will move the snackbar offscreen until the CSS is fully loaded and avoids a
CSS Class | Description
--- | ---
`mdc-snackbar` | Mandatory. Container for the snackbar elements.
-`mdc-snackbar__action-wrapper` | Mandatory. Wraps the action button.
-`mdc-snackbar__action-button` | Mandatory. The action button.
-`mdc-snackbar__text` | Mandtory. The next of the snackbar.
-`mdc-snackbar--align-start` | Optional. Class to align snackbar to start, ltr dependent.
-`mdc-snackbar--action-on-bottom` | Optional on the mdc-snackbar element. Moves action to bottom of snackbar. Can be applied in js.
-`mdc-snackbar--multiline` | Optional on the mdc-snackbar element. Makes the snackbar multiple lines. Can be applied in js.
+`mdc-snackbar__label` | Mandatory. Message text.
+`mdc-snackbar__actions` | Optional. Wraps the action button/icon elements, if present.
+`mdc-snackbar__action-button` | Optional. The action button.
+`mdc-snackbar__action-icon` | Optional. The dismiss ("X") icon.
+`mdc-snackbar--opening` | Optional. Applied automatically when the snackbar is in the process of animating open.
+`mdc-snackbar--open` | Optional. Indicates that the snackbar is open and visible.
+`mdc-snackbar--closing` | Optional. Applied automatically when the snackbar is in the process of animating closed.
+`mdc-snackbar--leading` | Optional. Positions the snackbar on the leading edge of the screen (left in LTR, right in RTL) instead of centered.
+`mdc-snackbar--stacked` | Optional. Positions the action button/icon below the label instead of alongside it.
+
+### Sass Mixins
+
+Mixin | Description
+--- | ---
+`mdc-snackbar-fill-color($color)` | Sets the fill color of the snackbar.
+`mdc-snackbar-label-ink-color($color)` | Sets the color of the snackbar's label text.
+`mdc-snackbar-shape-radius($radius, $rtl-reflexive)` | Sets the rounded shape to snackbar surface with given radius size. Set `$rtl-reflexive` to true to flip radius values in RTL context, defaults to false.
+`mdc-snackbar-min-width($min-width, $mobile-breakpoint)` | Sets the `min-width` of the surface on tablet/desktop devices. On mobile, the width is automatically set to 100%.
+`mdc-snackbar-max-width($max-width)` | Sets the `max-width` of the snackbar.
+`mdc-snackbar-elevation($z-index)` | Sets the elevation of the snackbar.
+`mdc-snackbar-viewport-margin($margin)` | Sets the distance between the snackbar and the viewport.
+`mdc-snackbar-z-index($z-index)` | Sets the `z-index` of the snackbar.
+`mdc-snackbar-position-leading()` | Positions the snackbar on the leading edge of the screen (left in LTR, right in RTL) instead of centered.
+`mdc-snackbar-layout-stacked()` | Positions the action button/icon below the label instead of alongside it.
+
+> **NOTE**: The `mdc-snackbar__action-button` and `mdc-snackbar__action-icon` elements can be customized with [`mdc-button`](../mdc-button) and [`mdc-icon-button`](../mdc-icon-button) mixins.
+
+## JavaScript API
-## `MDCSnackbar` Properties and Methods
+### `MDCSnackbar` Properties
Property | Value Type | Description
--- | --- | ---
-`dismissesOnAction` | `boolean` | Whether the snackbar dismisses when the action is clicked, or if it waits for the timeout anyway. Defaults to `true`.
+`isOpen` | `boolean` (read-only) | Gets whether the snackbar is currently open.
+`timeoutMs` | `number` | Gets/sets the automatic dismiss timeout in milliseconds. Value must be between `4000` and `10000` or an error will be thrown. Defaults to `5000` (5 seconds).
+`closeOnEscape` | `boolean` | Gets/sets whether the snackbar closes when it is focused and the user presses the ESC key. Defaults to `true`.
+`labelText` | `string` | Gets/sets the `textContent` of the label element.
+`actionButtonText` | `string` | Gets/sets the `textContent` of the action button element.
-Method Signature | Description
---- | ---
-`show(data: DataObject=) => void` | Displays the snackbar. `data` populates the snackbar and sets options (see below).
+> **NOTE**: Setting `labelText` while the snackbar is open will cause screen readers to announce the new label. See [Screen Readers](#screen-readers) below for more information.
-### DataObject Properties
+### `MDCSnackbar` Methods
- Property | Type | Description
---- | --- | ---
- `message` | string | Mandatory. The text message to display.
- `timeout` | number | The amount of time in milliseconds to show the snackbar. Defaults to `2750`.
- `actionHandler` | function | The function to execute when the action is clicked.
- `actionText` | string | Mandatory if `actionHandler` is set. The text to display for the action button.
- `multiline` | boolean | Whether to show the snackbar with space for multiple lines of text.
- `actionOnBottom` | boolean | Whether to show the action below the multiple lines of text (only applicable when `multiline` is true).
+Method Signature | Description
+--- | ---
+`open() => void` | Opens the snackbar.
+`close(reason: string=) => void` | Closes the snackbar, optionally with the specified reason indicating why it was closed.
### Events
Event Name | `event.detail` | Description
--- | --- | ---
-`MDCSnackbar:hide` | `{}` | Emitted when the Snackbar is hidden.
-`MDCSnackbar:show` | `{}` | Emitted when the Snackbar is shown.
+`MDCSnackbar:opening` | `{}` | Indicates when the snackbar begins its opening animation.
+`MDCSnackbar:opened` | `{}` | Indicates when the snackbar finishes its opening animation.
+`MDCSnackbar:closing` | `{reason: ?string}` | Indicates when the snackbar begins its closing animation. `reason` contains the reason why the snackbar closed (`dismiss` or `action`).
+`MDCSnackbar:closed` | `{reason: ?string}` | Indicates when the snackbar finishes its closing animation. `reason` contains the reason why the snackbar closed (`dismiss` or `action`).
-## Usage Within Frameworks
+### Usage Within Frameworks
If you are using a JavaScript framework, such as React or Angular, you can create a Snackbar for your framework. Depending on your needs, you can use the _Simple Approach: Wrapping MDC Web Vanilla Components_, or the _Advanced Approach: Using Foundations and Adapters_. Please follow the instructions [here](../../docs/integrating-into-frameworks.md).
-### `MDCSnackbarAdapter`
+#### `MDCSnackbarAdapter` Methods
Method Signature | Description
--- | ---
`addClass(className: string) => void` | Adds a class to the root element.
`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.
-`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.
-`isFocused() => boolean` | Detects 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.
-`deregisterTransitionEndHandler(handler: EventListener) => void` | Deregisters an event handler from an `transitionend` event listener. This will only be called with handlers that have previously been passed to `registerTransitionEndHandler` calls.
-`notifyShow() => void` | Dispatches an event notifying listeners that the snackbar has been shown.
-`notifyHide() => void` | Dispatches an event notifying listeners that the snackbar has been hidden.
+`announce() => void` | Announces the snackbar's label text to screen reader users.
+`notifyOpening() => void` | Broadcasts an event denoting that the snackbar has just started opening.
+`notifyOpened() => void` | Broadcasts an event denoting that the snackbar has finished opening.
+`notifyClosing(reason: string) {}` | Broadcasts an event denoting that the snackbar has just started closing. If a non-empty `reason` is passed, the event's `detail` object should include its value in the `reason` property.
+`notifyClosed(reason: string) {}` | Broadcasts an event denoting that the snackbar has finished closing. If a non-empty `reason` is passed, the event's `detail` object should include its value in the `reason` property.
+
+#### `MDCSnackbarFoundation` Methods
+
+Method Signature | Description
+--- | ---
+`open()` | Opens the snackbar.
+`close(action: string)` | Closes the snackbar, optionally with the specified action indicating why it was closed.
+`isOpen() => boolean` | Returns whether the snackbar is open.
+`getTimeoutMs() => number` | Returns the automatic dismiss timeout in milliseconds.
+`setTimeoutMs(timeoutMs: number)` | Sets the automatic dismiss timeout in milliseconds. Value must be between `4000` and `10000` or an error will be thrown.
+`getCloseOnEscape() => boolean` | Returns whether the snackbar closes when it is focused and the user presses the ESC key.
+`setCloseOnEscape(closeOnEscape: boolean) => void` | Sets whether the snackbar closes when it is focused and the user presses the ESC key.
+`handleKeyDown(event: !KeyEvent)` | Handles `keydown` events on or within the snackbar's root element.
+`handleActionButtonClick(event: !MouseEvent)` | Handles `click` events on or within the action button.
+`handleActionIconClick(event: !MouseEvent)` | Handles `click` events on or within the dismiss icon.
+
+#### Event Handlers
+
+When wrapping the Snackbar foundation, the following events must be bound to the indicated foundation methods:
+
+Event | Target | Foundation Handler | Register | Deregister
+--- | --- | --- | --- | ---
+`keydown` | `.mdc-snackbar` | `handleKeyDown` | During initialization | During destruction
+`click` | `.mdc-snackbar__action-button` | `handleActionButtonClick` | During initialization | During destruction
+`click` | `.mdc-snackbar__action-icon` | `handleActionIconClick` | During initialization | During destruction
+
+#### The Util API
+
+External frameworks and libraries can use the following utility methods from the `util` module when implementing their own component.
+
+Method Signature | Description
+--- | ---
+`announce(ariaEl: !HTMLElement, labelEl: !HTMLElement=) => void` | Announces the label text to screen reader users.
+
+> Alternatively, frameworks can use [Closure Library's `goog.a11y.aria.Announcer#say()` method](https://github.com/google/closure-library/blob/bee9ced776b4700e8076a3466bd9d3f9ade2fb54/closure/goog/a11y/aria/announcer.js#L80).
+
+## Accessibility
+
+### Screen Readers
+
+Snackbars automatically announce their label text to screen reader users with a ["polite" notification](https://www.w3.org/TR/wai-aria-1.1/#aria-live) when `open()` is called.
+
+However, screen readers only announce [ARIA Live Regions](https://mdn.io/ARIA_Live_Regions) when the element's `textContent` _changes_, so MDC Snackbar provides a `util.announce()` method to temporarily clear and then restore the label element's `textContent`.
+
+> **NOTE**: Setting `labelText` while the snackbar is open will cause screen readers to announce the new label.
+
+`util.announce()` supports the latest versions of the following screen readers and browsers:
+
+* [ChromeVox](https://chrome.google.com/webstore/detail/chromevox/kgejglhpjiefppelpmljglcjbhoiplfn)
+* [NVDA](https://www.nvaccess.org/):
+ - Chrome
+ - Firefox
+ - IE 11
+* [JAWS](https://www.freedomscientific.com/Products/Blindness/JAWS):
+ - Chrome
+ - Firefox
+ - IE 11
+
+macOS VoiceOver is _not_ supported at this time.
+
+### Dismiss Icon
+
+A dedicated dismiss icon is optional, but **strongly** recommended. If the snackbar gets permanently "stuck" on the screen for any reason (e.g., #1398), the user needs to be able to manually dismiss it.
+
+### Dismiss Key
+
+Pressing the ESC key while one of the snackbar's subelements has focus (e.g., the action button) will dismiss the snackbar.
+
+To disable this behavior, set `closeOnEscape` to `false`.
+
+### No JS Ripples
+
+The `mdc-snackbar__action-button` and `mdc-snackbar__action-icon` elements should _**not**_ have JavaScript-enabled [`MDCRipple`](../mdc-ripple) behavior.
+
+When combined with the snackbar's exit animation, ripples cause too much motion, which can be distracting or disorienting for users.
diff --git a/packages/mdc-snackbar/_mixins.scss b/packages/mdc-snackbar/_mixins.scss
new file mode 100644
index 00000000000..55c8f91ffbc
--- /dev/null
+++ b/packages/mdc-snackbar/_mixins.scss
@@ -0,0 +1,92 @@
+//
+// Copyright 2018 Google Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+@import "@material/elevation/mixins";
+@import "@material/shape/mixins";
+@import "@material/theme/mixins";
+@import "./variables";
+
+@mixin mdc-snackbar-fill-color($color) {
+ .mdc-snackbar__surface {
+ @include mdc-theme-prop(background-color, $color);
+ }
+}
+
+@mixin mdc-snackbar-label-ink-color($color) {
+ .mdc-snackbar__label {
+ @include mdc-theme-prop(color, $color);
+ }
+}
+
+@mixin mdc-snackbar-shape-radius($radius, $rtl-reflexive: false) {
+ .mdc-snackbar__surface {
+ @include mdc-shape-radius($radius, $rtl-reflexive);
+ }
+}
+
+@mixin mdc-snackbar-min-width($min-width, $mobile-breakpoint: $mdc-snackbar-mobile-breakpoint) {
+ .mdc-snackbar__surface {
+ min-width: $min-width;
+
+ // The first media query ensures that snackbars are always 100% width on mobile devices, as required by the spec.
+ // The second media query prevents snackbars from being wider than the viewport for large min-width values.
+ @media (max-width: $mobile-breakpoint), (max-width: $min-width) {
+ min-width: 100%;
+ }
+ }
+}
+
+@mixin mdc-snackbar-max-width($max-width) {
+ .mdc-snackbar__surface {
+ max-width: $max-width;
+ }
+}
+
+@mixin mdc-snackbar-elevation($z-index) {
+ .mdc-snackbar__surface {
+ @include mdc-elevation($z-index);
+ }
+}
+
+@mixin mdc-snackbar-viewport-margin($margin) {
+ margin: $margin;
+}
+
+@mixin mdc-snackbar-z-index($z-index) {
+ z-index: $z-index;
+}
+
+@mixin mdc-snackbar-position-leading {
+ justify-content: flex-start;
+}
+
+@mixin mdc-snackbar-layout-stacked {
+ .mdc-snackbar__surface {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .mdc-snackbar__actions {
+ align-self: flex-end;
+ margin-bottom: $mdc-snackbar-padding;
+ }
+}
diff --git a/packages/mdc-snackbar/_variables.scss b/packages/mdc-snackbar/_variables.scss
index 608c8aa1d5d..ac7efd222e1 100644
--- a/packages/mdc-snackbar/_variables.scss
+++ b/packages/mdc-snackbar/_variables.scss
@@ -1,5 +1,5 @@
//
-// Copyright 2017 Google Inc.
+// Copyright 2018 Google Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
@@ -20,9 +20,27 @@
// THE SOFTWARE.
//
-// Hard coded since the color is not present in any palette.
-$mdc-snackbar-background-color: #323232 !default;
-$mdc-snackbar-foreground-color: white !default;
-// TODO: Better spot to pull this breakpoint?
-//$snackbar-tablet-breakpoint: $grid-tablet-breakpoint;
-$mdc-snackbar-tablet-breakpoint: 600px !default;
+@import "@material/theme/functions";
+@import "@material/theme/variables"; // for mdc-theme-prop-value() function
+
+$mdc-snackbar-fill-color: mix(mdc-theme-prop-value(on-surface), mdc-theme-prop-value(surface), 80%) !default;
+$mdc-snackbar-label-ink-color: rgba(mdc-theme-prop-value(surface), mdc-theme-text-emphasis(high)) !default;
+$mdc-snackbar-action-icon-ink-color: rgba(mdc-theme-prop-value(surface), mdc-theme-text-emphasis(high)) !default;
+$mdc-snackbar-action-button-ink-color: #bb86fc !default;
+
+$mdc-snackbar-label-type-scale: body2 !default;
+$mdc-snackbar-action-icon-size: 18px !default;
+$mdc-snackbar-min-width: 344px !default;
+$mdc-snackbar-max-width: 672px !default;
+$mdc-snackbar-mobile-breakpoint: 480px !default;
+$mdc-snackbar-viewport-margin-narrow: 8px !default;
+$mdc-snackbar-viewport-margin-wide: 24px !default;
+$mdc-snackbar-padding: 8px !default;
+
+$mdc-snackbar-elevation: 6 !default;
+$mdc-snackbar-shape-radius: small !default; // Key from $mdc-shape-category-values or CSS length value (e.g., 4px)
+$mdc-snackbar-z-index: 8 !default; // One above mdc-dialog
+
+// These variables need to be kept in sync with the values in constants.js.
+$mdc-snackbar-enter-duration: 150ms !default;
+$mdc-snackbar-exit-duration: 75ms !default;
diff --git a/packages/mdc-snackbar/adapter.js b/packages/mdc-snackbar/adapter.js
new file mode 100644
index 00000000000..3fa8e867d2f
--- /dev/null
+++ b/packages/mdc-snackbar/adapter.js
@@ -0,0 +1,64 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/* eslint no-unused-vars: [2, {"args": "none"}] */
+
+/**
+ * Adapter for MDC Snackbar. Provides an interface for managing:
+ * - CSS classes
+ * - Event handlers
+ *
+ * Additionally, provides type information for the adapter to the Closure
+ * compiler.
+ *
+ * Implement this adapter for your framework of choice to delegate updates to
+ * the component in your framework of choice. See architecture documentation
+ * for more details.
+ * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md
+ *
+ * @record
+ */
+class MDCSnackbarAdapter {
+ /** @param {string} className */
+ addClass(className) {}
+
+ /** @param {string} className */
+ removeClass(className) {}
+
+ announce() {}
+
+ notifyOpening() {}
+ notifyOpened() {}
+
+ /**
+ * @param {string} reason
+ */
+ notifyClosing(reason) {}
+
+ /**
+ * @param {string} reason
+ */
+ notifyClosed(reason) {}
+}
+
+export default MDCSnackbarAdapter;
diff --git a/packages/mdc-snackbar/constants.js b/packages/mdc-snackbar/constants.js
index 8e5a033523e..b2ea630ef20 100644
--- a/packages/mdc-snackbar/constants.js
+++ b/packages/mdc-snackbar/constants.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright 2016 Google Inc.
+ * Copyright 2018 Google Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -20,24 +20,45 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
-export const cssClasses = {
- ROOT: 'mdc-snackbar',
- TEXT: 'mdc-snackbar__text',
- ACTION_WRAPPER: 'mdc-snackbar__action-wrapper',
- ACTION_BUTTON: 'mdc-snackbar__action-button',
- ACTIVE: 'mdc-snackbar--active',
- MULTILINE: 'mdc-snackbar--multiline',
- ACTION_ON_BOTTOM: 'mdc-snackbar--action-on-bottom',
+
+const cssClasses = {
+ OPENING: 'mdc-snackbar--opening',
+ OPEN: 'mdc-snackbar--open',
+ CLOSING: 'mdc-snackbar--closing',
};
-export const strings = {
- TEXT_SELECTOR: '.mdc-snackbar__text',
- ACTION_WRAPPER_SELECTOR: '.mdc-snackbar__action-wrapper',
+const strings = {
+ SURFACE_SELECTOR: '.mdc-snackbar__surface',
+ LABEL_SELECTOR: '.mdc-snackbar__label',
ACTION_BUTTON_SELECTOR: '.mdc-snackbar__action-button',
- SHOW_EVENT: 'MDCSnackbar:show',
- HIDE_EVENT: 'MDCSnackbar:hide',
+ ACTION_ICON_SELECTOR: '.mdc-snackbar__action-icon',
+
+ OPENING_EVENT: 'MDCSnackbar:opening',
+ OPENED_EVENT: 'MDCSnackbar:opened',
+ CLOSING_EVENT: 'MDCSnackbar:closing',
+ CLOSED_EVENT: 'MDCSnackbar:closed',
+
+ REASON_ACTION: 'action',
+ REASON_DISMISS: 'dismiss',
+
+ ARIA_LIVE_LABEL_TEXT_ATTR: 'data-mdc-snackbar-label-text',
};
-export const numbers = {
- MESSAGE_TIMEOUT: 2750,
+const numbers = {
+ MIN_AUTO_DISMISS_TIMEOUT_MS: 4000,
+ MAX_AUTO_DISMISS_TIMEOUT_MS: 10000,
+ DEFAULT_AUTO_DISMISS_TIMEOUT_MS: 5000,
+
+ // These variables need to be kept in sync with the values in _variables.scss.
+ SNACKBAR_ANIMATION_OPEN_TIME_MS: 150,
+ SNACKBAR_ANIMATION_CLOSE_TIME_MS: 75,
+
+ /**
+ * Number of milliseconds to wait between temporarily clearing the label text
+ * in the DOM and subsequently restoring it. This is necessary to force IE 11
+ * to pick up the `aria-live` content change and announce it to the user.
+ */
+ ARIA_LIVE_DELAY_MS: 1000,
};
+
+export {cssClasses, strings, numbers};
diff --git a/packages/mdc-snackbar/foundation.js b/packages/mdc-snackbar/foundation.js
index 1f6e7bebba0..7778d406146 100644
--- a/packages/mdc-snackbar/foundation.js
+++ b/packages/mdc-snackbar/foundation.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright 2016 Google Inc.
+ * Copyright 2018 Google Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -21,10 +21,16 @@
* THE SOFTWARE.
*/
+/* eslint no-unused-vars: ["error", {"argsIgnorePattern": "evt", "varsIgnorePattern": "Adapter$"}] */
+
import {MDCFoundation} from '@material/base/index';
-import {cssClasses, strings, numbers} from './constants';
+import MDCSnackbarAdapter from './adapter';
+import {cssClasses, numbers, strings} from './constants';
+
+const {OPENING, OPEN, CLOSING} = cssClasses;
+const {REASON_ACTION, REASON_DISMISS} = strings;
-export default class MDCSnackbarFoundation extends MDCFoundation {
+class MDCSnackbarFoundation extends MDCFoundation {
static get cssClasses() {
return cssClasses;
}
@@ -33,225 +39,203 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
return strings;
}
+ static get numbers() {
+ return numbers;
+ }
+
+ /**
+ * @return {!MDCSnackbarAdapter}
+ */
static get defaultAdapter() {
- return {
+ return /** @type {!MDCSnackbarAdapter} */ ({
addClass: (/* className: string */) => {},
removeClass: (/* className: string */) => {},
- setAriaHidden: () => {},
- unsetAriaHidden: () => {},
- setActionAriaHidden: () => {},
- unsetActionAriaHidden: () => {},
- setActionText: (/* actionText: string */) => {},
- setMessageText: (/* message: string */) => {},
- setFocus: () => {},
- isFocused: () => /* boolean */ false,
- 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 */) => {},
- deregisterTransitionEndHandler: (/* handler: EventListener */) => {},
- notifyShow: () => {},
- notifyHide: () => {},
- };
- }
-
- get active() {
- return this.active_;
+ announce: () => {},
+ notifyOpening: () => {},
+ notifyOpened: () => {},
+ notifyClosing: (/* reason: string */) => {},
+ notifyClosed: (/* reason: string */) => {},
+ });
}
+ /**
+ * @param {!MDCSnackbarAdapter=} adapter
+ */
constructor(adapter) {
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.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 === 'focus' && !this.adapter_.isFocused()) {
- return;
- }
- 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);
- };
- }
+ /** @private {boolean} */
+ this.isOpen_ = false;
- init() {
- this.adapter_.registerActionClickHandler(this.actionClickHandler_);
- this.adapter_.setAriaHidden();
- this.adapter_.setActionAriaHidden();
- }
+ /** @private {number} */
+ this.animationFrame_ = 0;
- 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_);
- });
- }
+ /** @private {number} */
+ this.animationTimer_ = 0;
+
+ /** @private {number} */
+ this.autoDismissTimer_ = 0;
- dismissesOnAction() {
- return this.dismissOnAction_;
+ /** @private {number} */
+ this.autoDismissTimeoutMs_ = numbers.DEFAULT_AUTO_DISMISS_TIMEOUT_MS;
+
+ /** @private {boolean} */
+ this.closeOnEscape_ = true;
}
- setDismissOnAction(dismissOnAction) {
- this.dismissOnAction_ = !!dismissOnAction;
+ destroy() {
+ this.clearAutoDismissTimer_();
+ cancelAnimationFrame(this.animationFrame_);
+ this.animationFrame_ = 0;
+ clearTimeout(this.animationTimer_);
+ this.animationTimer_ = 0;
+ this.adapter_.removeClass(OPENING);
+ this.adapter_.removeClass(OPEN);
+ this.adapter_.removeClass(CLOSING);
+ }
+
+ open() {
+ this.clearAutoDismissTimer_();
+ this.isOpen_ = true;
+ this.adapter_.notifyOpening();
+ this.adapter_.removeClass(CLOSING);
+ this.adapter_.addClass(OPENING);
+ this.adapter_.announce();
+
+ // Wait a frame once display is no longer "none", to establish basis for animation
+ this.runNextAnimationFrame_(() => {
+ this.adapter_.addClass(OPEN);
+
+ this.animationTimer_ = setTimeout(() => {
+ this.handleAnimationTimerEnd_();
+ this.adapter_.notifyOpened();
+ this.autoDismissTimer_ = setTimeout(() => {
+ this.close(REASON_DISMISS);
+ }, this.getTimeoutMs());
+ }, numbers.SNACKBAR_ANIMATION_OPEN_TIME_MS);
+ });
}
- show(data) {
- if (!data) {
- throw new Error(
- 'Please provide a data object with at least a message to display.');
- }
- if (!data.message) {
- throw new Error('Please provide a message to be displayed.');
- }
- if (data.actionHandler && !data.actionText) {
- throw new Error('Please provide action text with the handler.');
- }
- if (this.active) {
- this.queue_.push(data);
+ /**
+ * @param {string=} reason Why the snackbar was closed. Value will be passed to CLOSING_EVENT and CLOSED_EVENT via the
+ * `event.detail.reason` property. Standard values are REASON_ACTION and REASON_DISMISS, but custom
+ * client-specific values may also be used if desired.
+ */
+ close(reason = '') {
+ if (!this.isOpen_) {
+ // Avoid redundant close calls (and events), e.g. repeated interactions as the snackbar is animating closed
return;
}
- 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_);
- });
- const {ACTIVE, MULTILINE, ACTION_ON_BOTTOM} = cssClasses;
+ cancelAnimationFrame(this.animationFrame_);
+ this.animationFrame_ = 0;
+ this.clearAutoDismissTimer_();
- this.adapter_.setMessageText(this.snackbarData_.message);
+ this.isOpen_ = false;
+ this.adapter_.notifyClosing(reason);
+ this.adapter_.addClass(cssClasses.CLOSING);
+ this.adapter_.removeClass(cssClasses.OPEN);
+ this.adapter_.removeClass(cssClasses.OPENING);
- if (this.snackbarData_.multiline) {
- this.adapter_.addClass(MULTILINE);
- if (this.snackbarData_.actionOnBottom) {
- this.adapter_.addClass(ACTION_ON_BOTTOM);
- }
- }
-
- if (this.snackbarData_.actionHandler) {
- this.adapter_.setActionText(this.snackbarData_.actionText);
- this.actionHandler_ = this.snackbarData_.actionHandler;
- this.setActionHidden_(false);
- } else {
- this.setActionHidden_(true);
- this.actionHandler_ = null;
- this.adapter_.setActionText(null);
- }
+ clearTimeout(this.animationTimer_);
+ this.animationTimer_ = setTimeout(() => {
+ this.handleAnimationTimerEnd_();
+ this.adapter_.notifyClosed(reason);
+ }, numbers.SNACKBAR_ANIMATION_CLOSE_TIME_MS);
+ }
- this.active_ = true;
- this.adapter_.addClass(ACTIVE);
- this.adapter_.unsetAriaHidden();
- this.adapter_.notifyShow();
+ /**
+ * @return {boolean}
+ */
+ isOpen() {
+ return this.isOpen_;
+ }
- this.timeoutId_ = setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || numbers.MESSAGE_TIMEOUT);
+ /**
+ * @return {number}
+ */
+ getTimeoutMs() {
+ return this.autoDismissTimeoutMs_;
}
- handlePossibleTabKeyboardFocus_() {
- const hijackFocus =
- this.firstFocus_ && !this.pointerDownRecognized_;
+ /**
+ * @param {number} timeoutMs
+ */
+ setTimeoutMs(timeoutMs) {
+ // Use shorter variable names to make the code more readable
+ const minValue = numbers.MIN_AUTO_DISMISS_TIMEOUT_MS;
+ const maxValue = numbers.MAX_AUTO_DISMISS_TIMEOUT_MS;
- if (hijackFocus) {
- this.setFocusOnAction_();
+ if (timeoutMs <= maxValue && timeoutMs >= minValue) {
+ this.autoDismissTimeoutMs_ = timeoutMs;
+ } else {
+ throw new Error(`timeoutMs must be an integer in the range ${minValue}–${maxValue}, but got '${timeoutMs}'`);
}
+ }
- this.firstFocus_ = false;
+ /**
+ * @return {boolean}
+ */
+ getCloseOnEscape() {
+ return this.closeOnEscape_;
}
- setFocusOnAction_() {
- this.adapter_.setFocus();
- this.snackbarHasFocus_ = true;
- this.firstFocus_ = false;
+ /**
+ * @param {boolean} closeOnEscape
+ */
+ setCloseOnEscape(closeOnEscape) {
+ this.closeOnEscape_ = closeOnEscape;
}
- invokeAction_() {
- try {
- if (!this.actionHandler_) {
- return;
- }
-
- this.actionHandler_();
- } finally {
- if (this.dismissOnAction_) {
- this.cleanup_();
- }
+ /**
+ * @param {!KeyboardEvent} evt
+ */
+ handleKeyDown(evt) {
+ if (this.getCloseOnEscape() && (evt.key === 'Escape' || evt.keyCode === 27)) {
+ this.close(REASON_DISMISS);
}
}
- cleanup_() {
- const allowDismissal = !this.snackbarHasFocus_ || this.actionWasClicked_;
-
- if (allowDismissal) {
- const {ACTIVE, MULTILINE, ACTION_ON_BOTTOM} = cssClasses;
-
- this.adapter_.removeClass(ACTIVE);
+ /**
+ * @param {!MouseEvent} evt
+ */
+ handleActionButtonClick(evt) {
+ this.close(REASON_ACTION);
+ }
- 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.adapter_.notifyHide();
- this.showNext_();
- };
+ /**
+ * @param {!MouseEvent} evt
+ */
+ handleActionIconClick(evt) {
+ this.close(REASON_DISMISS);
+ }
- this.adapter_.registerTransitionEndHandler(handler);
- }
+ /** @private */
+ clearAutoDismissTimer_() {
+ clearTimeout(this.autoDismissTimer_);
+ this.autoDismissTimer_ = 0;
}
- showNext_() {
- if (!this.queue_.length) {
- return;
- }
- this.show(this.queue_.shift());
+ /** @private */
+ handleAnimationTimerEnd_() {
+ this.animationTimer_ = 0;
+ this.adapter_.removeClass(cssClasses.OPENING);
+ this.adapter_.removeClass(cssClasses.CLOSING);
}
- setActionHidden_(isHidden) {
- if (isHidden) {
- this.adapter_.setActionAriaHidden();
- } else {
- this.adapter_.unsetActionAriaHidden();
- }
+ /**
+ * Runs the given logic on the next animation frame, using setTimeout to factor in Firefox reflow behavior.
+ * @param {Function} callback
+ * @private
+ */
+ runNextAnimationFrame_(callback) {
+ cancelAnimationFrame(this.animationFrame_);
+ this.animationFrame_ = requestAnimationFrame(() => {
+ this.animationFrame_ = 0;
+ clearTimeout(this.animationTimer_);
+ this.animationTimer_ = setTimeout(callback, 0);
+ });
}
}
+
+export default MDCSnackbarFoundation;
diff --git a/packages/mdc-snackbar/index.js b/packages/mdc-snackbar/index.js
index a0f7dd41412..876fea94b86 100644
--- a/packages/mdc-snackbar/index.js
+++ b/packages/mdc-snackbar/index.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright 2016 Google Inc.
+ * Copyright 2018 Google Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -23,64 +23,214 @@
import {MDCComponent} from '@material/base/index';
import MDCSnackbarFoundation from './foundation';
-import {getCorrectEventName} from '@material/animation/index';
+import {strings} from './constants';
+import * as util from './util';
+import * as ponyfill from '@material/dom/ponyfill';
-export {MDCSnackbarFoundation};
+const {
+ SURFACE_SELECTOR, LABEL_SELECTOR, ACTION_BUTTON_SELECTOR, ACTION_ICON_SELECTOR,
+ OPENING_EVENT, OPENED_EVENT, CLOSING_EVENT, CLOSED_EVENT,
+} = strings;
-export class MDCSnackbar extends MDCComponent {
+class MDCSnackbar extends MDCComponent {
static attachTo(root) {
return new MDCSnackbar(root);
}
- show(data) {
- this.foundation_.show(data);
+ constructor(...args) {
+ super(...args);
+
+ /** @type {!HTMLElement} */
+ this.surfaceEl_;
+
+ /** @type {!HTMLElement} */
+ this.labelEl_;
+
+ /** @type {!HTMLElement} */
+ this.actionButtonEl_;
+
+ /** @type {function(!HTMLElement, !HTMLElement=): void} */
+ this.announce_;
+
+ /** @private {!Function} */
+ this.handleKeyDown_;
+
+ /** @private {!Function} */
+ this.handleSurfaceClick_;
}
- getDefaultFoundation() {
- const {
- TEXT_SELECTOR,
- ACTION_BUTTON_SELECTOR,
- } = MDCSnackbarFoundation.strings;
- const getText = () => this.root_.querySelector(TEXT_SELECTOR);
- const getActionButton = () => this.root_.querySelector(ACTION_BUTTON_SELECTOR);
+ /**
+ * @param {function(): function(!HTMLElement, !HTMLElement=):void} announceFactory
+ */
+ initialize(announceFactory = () => util.announce) {
+ this.announce_ = announceFactory();
+ }
+
+ initialSyncWithDOM() {
+ this.surfaceEl_ = /** @type {!HTMLElement} */ (this.root_.querySelector(SURFACE_SELECTOR));
+ this.labelEl_ = /** @type {!HTMLElement} */ (this.root_.querySelector(LABEL_SELECTOR));
+ this.actionButtonEl_ = /** @type {!HTMLElement} */ (this.root_.querySelector(ACTION_BUTTON_SELECTOR));
+
+ this.handleKeyDown_ = (evt) => this.foundation_.handleKeyDown(evt);
+ this.handleSurfaceClick_ = (evt) => {
+ if (this.isActionButton_(evt.target)) {
+ this.foundation_.handleActionButtonClick(evt);
+ } else if (this.isActionIcon_(evt.target)) {
+ this.foundation_.handleActionIconClick(evt);
+ }
+ };
+
+ this.registerKeyDownHandler_(this.handleKeyDown_);
+ this.registerSurfaceClickHandler_(this.handleSurfaceClick_);
+ }
+ destroy() {
+ super.destroy();
+ this.deregisterKeyDownHandler_(this.handleKeyDown_);
+ this.deregisterSurfaceClickHandler_(this.handleSurfaceClick_);
+ }
+
+ open() {
+ this.foundation_.open();
+ }
+
+ /**
+ * @param {string=} reason Why the snackbar was closed. Value will be passed to CLOSING_EVENT and CLOSED_EVENT via the
+ * `event.detail.reason` property. Standard values are REASON_ACTION and REASON_DISMISS, but custom
+ * client-specific values may also be used if desired.
+ */
+ close(reason = '') {
+ this.foundation_.close(reason);
+ }
+
+ /**
+ * @return {!MDCSnackbarFoundation}
+ */
+ getDefaultFoundation() {
/* eslint brace-style: "off" */
return new MDCSnackbarFoundation({
addClass: (className) => this.root_.classList.add(className),
removeClass: (className) => this.root_.classList.remove(className),
- setAriaHidden: () => this.root_.setAttribute('aria-hidden', 'true'),
- unsetAriaHidden: () => this.root_.removeAttribute('aria-hidden'),
- setActionAriaHidden: () => getActionButton().setAttribute('aria-hidden', 'true'),
- unsetActionAriaHidden: () => getActionButton().removeAttribute('aria-hidden'),
- setActionText: (text) => {getActionButton().textContent = text;},
- setMessageText: (text) => {getText().textContent = text;},
- setFocus: () => getActionButton().focus(),
- isFocused: () => document.activeElement === getActionButton(),
- 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:
- (handler) => this.root_.addEventListener(getCorrectEventName(window, 'transitionend'), handler),
- deregisterTransitionEndHandler:
- (handler) => this.root_.removeEventListener(getCorrectEventName(window, 'transitionend'), handler),
- notifyShow: () => this.emit(MDCSnackbarFoundation.strings.SHOW_EVENT),
- notifyHide: () => this.emit(MDCSnackbarFoundation.strings.HIDE_EVENT),
+ announce: () => this.announce_(this.labelEl_),
+ notifyOpening: () => this.emit(OPENING_EVENT, {}),
+ notifyOpened: () => this.emit(OPENED_EVENT, {}),
+ notifyClosing: (reason) => this.emit(CLOSING_EVENT, reason ? {reason} : {}),
+ notifyClosed: (reason) => this.emit(CLOSED_EVENT, reason ? {reason} : {}),
});
}
- get dismissesOnAction() {
- return this.foundation_.dismissesOnAction();
+ /**
+ * @return {number}
+ */
+ get timeoutMs() {
+ return this.foundation_.getTimeoutMs();
+ }
+
+ /**
+ * @param {number} timeoutMs
+ */
+ set timeoutMs(timeoutMs) {
+ this.foundation_.setTimeoutMs(timeoutMs);
+ }
+
+ /**
+ * @return {boolean}
+ */
+ get closeOnEscape() {
+ return this.foundation_.getCloseOnEscape();
}
- set dismissesOnAction(dismissesOnAction) {
- this.foundation_.setDismissOnAction(dismissesOnAction);
+ /**
+ * @param {boolean} closeOnEscape
+ */
+ set closeOnEscape(closeOnEscape) {
+ this.foundation_.setCloseOnEscape(closeOnEscape);
+ }
+
+ /**
+ * @return {boolean}
+ */
+ get isOpen() {
+ return this.foundation_.isOpen();
+ }
+
+ /**
+ * @return {string}
+ */
+ get labelText() {
+ return this.labelEl_.textContent;
+ }
+
+ /**
+ * @param {string} labelText
+ */
+ set labelText(labelText) {
+ this.labelEl_.textContent = labelText;
+ }
+
+ /**
+ * @return {string}
+ */
+ get actionButtonText() {
+ return this.actionButtonEl_.textContent;
+ }
+
+ /**
+ * @param {string} actionButtonText
+ */
+ set actionButtonText(actionButtonText) {
+ this.actionButtonEl_.textContent = actionButtonText;
+ }
+
+ /**
+ * @param {!Function} handler
+ * @private
+ */
+ registerKeyDownHandler_(handler) {
+ this.listen('keydown', handler);
+ }
+
+ /**
+ * @param {!Function} handler
+ * @private
+ */
+ deregisterKeyDownHandler_(handler) {
+ this.unlisten('keydown', handler);
+ }
+
+ /**
+ * @param {!Function} handler
+ * @private
+ */
+ registerSurfaceClickHandler_(handler) {
+ this.surfaceEl_.addEventListener('click', handler);
+ }
+
+ /**
+ * @param {!Function} handler
+ * @private
+ */
+ deregisterSurfaceClickHandler_(handler) {
+ this.surfaceEl_.removeEventListener('click', handler);
+ }
+
+ /**
+ * @param {!Element} target
+ * @return {boolean}
+ * @private
+ */
+ isActionButton_(target) {
+ return Boolean(ponyfill.closest(target, ACTION_BUTTON_SELECTOR));
+ }
+
+ /**
+ * @param {!Element} target
+ * @return {boolean}
+ * @private
+ */
+ isActionIcon_(target) {
+ return Boolean(ponyfill.closest(target, ACTION_ICON_SELECTOR));
}
}
+
+export {MDCSnackbar, MDCSnackbarFoundation, util};
diff --git a/packages/mdc-snackbar/mdc-snackbar.scss b/packages/mdc-snackbar/mdc-snackbar.scss
index ea80ba52833..a4928538de7 100644
--- a/packages/mdc-snackbar/mdc-snackbar.scss
+++ b/packages/mdc-snackbar/mdc-snackbar.scss
@@ -1,5 +1,5 @@
//
-// Copyright 2017 Google Inc.
+// Copyright 2018 Google Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
@@ -21,152 +21,132 @@
//
@import "@material/animation/functions";
+@import "@material/button/mixins";
+@import "@material/icon-button/mixins";
+@import "@material/ripple/mixins";
@import "@material/rtl/mixins";
-@import "@material/theme/mixins";
@import "@material/typography/mixins";
+@import "./mixins";
@import "./variables";
// postcss-bem-linter: define snackbar
.mdc-snackbar {
- display: flex;
+ @include mdc-snackbar-z-index($mdc-snackbar-z-index);
+ @include mdc-snackbar-viewport-margin($mdc-snackbar-viewport-margin-narrow);
+
+ display: none;
position: fixed;
+ right: 0;
bottom: 0;
- left: 50%;
+ left: 0;
align-items: center;
- justify-content: flex-start;
+ justify-content: center;
box-sizing: border-box;
- padding-right: 24px;
- padding-left: 24px;
- transform: translate(-50%, 100%);
- transition: mdc-animation-exit-permanent(transform, .25s);
- background-color: $mdc-snackbar-background-color;
- pointer-events: none;
- will-change: transform;
- @media (max-width: ($mdc-snackbar-tablet-breakpoint - 1)) {
- left: 0;
- width: 100%;
- transform: translate(0, 100%);
- }
+ // Ignore mouse events on the root layout element.
+ pointer-events: none;
- @media (min-width: $mdc-snackbar-tablet-breakpoint) {
- min-width: 288px;
- max-width: 568px;
- border-radius: 2px;
- }
+ // For some reason, iOS Safari displays a tap highlight on the entire snackbar element.
+ // Mobile Safari only supports `rgba` values for this property; named values like `transparent` are ignored.
+ // From Apple's docs:
+ // > This property obeys the alpha value, if specified.
+ // > If you don’t specify an alpha value, Safari on iOS applies a default alpha value to the color.
+ // > To disable tap highlighting, set the alpha value to 0 (invisible).
+ // > If you set the alpha value to 1.0 (opaque), the element is not visible when tapped.
+ // See https://github.com/ben-eb/postcss-colormin/issues/1
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
-.mdc-snackbar--align-start {
- @media (min-width: $mdc-snackbar-tablet-breakpoint) {
- @include mdc-rtl-reflexive-position(left, 24px);
-
- bottom: 24px;
- transform: translate(0, 200%);
- }
-
- @media (max-width: ($mdc-snackbar-tablet-breakpoint - 1)) {
- bottom: 0;
- left: 0;
- width: 100%;
- transform: translate(0, 100%);
- }
+@at-root {
+ @include mdc-snackbar-fill-color($mdc-snackbar-fill-color);
+ @include mdc-snackbar-label-ink-color($mdc-snackbar-label-ink-color);
+ @include mdc-snackbar-min-width($mdc-snackbar-min-width);
+ @include mdc-snackbar-max-width($mdc-snackbar-max-width);
+ @include mdc-snackbar-elevation($mdc-snackbar-elevation);
+ @include mdc-snackbar-shape-radius($mdc-snackbar-shape-radius);
}
-.mdc-snackbar--active {
- transform: translate(0);
- transition: mdc-animation-enter(transform, .25s);
- pointer-events: auto;
-
- &:not(.mdc-snackbar--align-start) {
- transform: translate(-50%, 0);
-
- @media (max-width: ($mdc-snackbar-tablet-breakpoint - 1)) {
- bottom: 0;
- left: 0;
- width: 100%;
- transform: translate(0);
- }
- }
+.mdc-snackbar--opening,
+.mdc-snackbar--open,
+.mdc-snackbar--closing {
+ display: flex;
}
-.mdc-snackbar__action-wrapper {
- @include mdc-rtl-reflexive-property(padding, 24px, 0);
+.mdc-snackbar--leading {
+ @include mdc-snackbar-position-leading;
}
-.mdc-snackbar--action-on-bottom {
- flex-direction: column;
+.mdc-snackbar--stacked {
+ @include mdc-snackbar-layout-stacked;
}
-.mdc-snackbar__text {
- @include mdc-typography(body1);
- @include mdc-rtl-reflexive-box(margin, right, auto, ".mdc-snackbar");
-
+.mdc-snackbar__surface {
display: flex;
align-items: center;
- height: 48px;
- transition: mdc-animation-exit-permanent(opacity, .3s);
+ justify-content: flex-start;
+ box-sizing: border-box;
+ transform: scale(.8);
opacity: 0;
- color: $mdc-snackbar-foreground-color;
- @media (min-width: $mdc-snackbar-tablet-breakpoint) {
- @include mdc-rtl-reflexive-property(padding, 0, 24px);
+ .mdc-snackbar--open & {
+ transform: scale(1);
+ transition:
+ mdc-animation-enter(opacity, $mdc-snackbar-enter-duration),
+ mdc-animation-enter(transform, $mdc-snackbar-enter-duration);
+ opacity: 1;
+ pointer-events: auto; // Allow mouse events on surface element while snackbar is open
}
-}
-.mdc-snackbar--action-on-bottom .mdc-snackbar__text {
- margin-right: inherit;
+ .mdc-snackbar--closing & {
+ transform: scale(1);
+ transition: mdc-animation-exit-permanent(opacity, $mdc-snackbar-exit-duration);
+ }
}
-.mdc-snackbar--action-on-bottom .mdc-snackbar__action-wrapper {
- @include mdc-rtl-reflexive-box(margin, left, auto);
+.mdc-snackbar__label {
+ @include mdc-typography($mdc-snackbar-label-type-scale);
- flex-direction: column;
- justify-content: flex-start;
- margin-top: -12px;
- margin-bottom: 8px;
+ flex-grow: 1;
+ box-sizing: border-box;
+ margin: 0;
+
+ // 14px top/bottom padding needed to make the height 48px.
+ padding: 14px 16px;
}
-.mdc-snackbar--multiline .mdc-snackbar__text {
- height: 80px;
+// Used to prevent visual jank when announcing label text to screen readers.
+// See the `announce()` function in util.js for details.
+.mdc-snackbar__label::before {
+ display: inline;
+ content: attr(data-mdc-snackbar-label-text);
}
-.mdc-snackbar__action-button {
- @include mdc-typography(button);
- @include mdc-theme-prop(color, secondary);
-
- padding: 0;
- transition: mdc-animation-exit-permanent(opacity, .3s);
- border: none;
- outline: none;
- background-color: transparent;
- opacity: 0;
- user-select: none;
- -webkit-appearance: none;
- visibility: hidden;
+.mdc-snackbar__actions {
+ @include mdc-rtl-reflexive-property(margin, 0, $mdc-snackbar-padding);
- &::-moz-focus-inner {
- border: 0;
- }
+ display: flex;
+ flex-shrink: 0;
+ align-items: center;
+ box-sizing: border-box;
+}
- &:hover {
- cursor: pointer;
- }
+.mdc-snackbar__action-button {
+ @include mdc-button-ink-color($mdc-snackbar-action-button-ink-color);
+ @include mdc-states($mdc-snackbar-action-button-ink-color);
+}
- &:not([aria-hidden]) {
- visibility: inherit;
- }
+.mdc-snackbar__action-icon {
+ @include mdc-icon-button-ink-color($mdc-snackbar-action-icon-ink-color);
}
-.mdc-snackbar--active .mdc-snackbar__text,
-.mdc-snackbar--active .mdc-snackbar__action-button:not([aria-hidden]) {
- transition: mdc-animation-exit-permanent(opacity, .3s);
- opacity: 1;
+// Two selectors are needed to increase specificity above `.material-icons`.
+// stylelint-disable-next-line selector-class-pattern
+.mdc-snackbar__action-icon.mdc-snackbar__action-icon {
+ @include mdc-icon-button-size($mdc-snackbar-action-icon-size);
}
-// stylelint-disable plugin/selector-bem-pattern
-.mdc-snackbar--multiline.mdc-snackbar--action-on-bottom .mdc-snackbar__text {
- margin: 0;
+.mdc-snackbar__action-button + .mdc-snackbar__action-icon {
+ @include mdc-rtl-reflexive-property(margin, $mdc-snackbar-padding, 0);
}
-// stylelint-enable plugin/selector-bem-pattern
// postcss-bem-linter: end
diff --git a/packages/mdc-snackbar/package.json b/packages/mdc-snackbar/package.json
index 139c334f0e6..2caf2292efb 100644
--- a/packages/mdc-snackbar/package.json
+++ b/packages/mdc-snackbar/package.json
@@ -16,7 +16,12 @@
"dependencies": {
"@material/animation": "^0.41.0",
"@material/base": "^0.41.0",
+ "@material/button": "^0.41.0",
+ "@material/dom": "^0.41.0",
+ "@material/icon-button": "^0.41.0",
+ "@material/ripple": "^0.41.0",
"@material/rtl": "^0.42.0",
+ "@material/shape": "^0.40.1",
"@material/theme": "^0.41.0",
"@material/typography": "^0.42.0"
}
diff --git a/packages/mdc-snackbar/util.js b/packages/mdc-snackbar/util.js
new file mode 100644
index 00000000000..addbe60b293
--- /dev/null
+++ b/packages/mdc-snackbar/util.js
@@ -0,0 +1,91 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import {numbers, strings} from './constants';
+
+const {ARIA_LIVE_DELAY_MS} = numbers;
+const {ARIA_LIVE_LABEL_TEXT_ATTR} = strings;
+
+/**
+ * @param {!HTMLElement} ariaEl
+ * @param {!HTMLElement=} labelEl
+ */
+function announce(ariaEl, labelEl = ariaEl) {
+ const priority = ariaEl.getAttribute('aria-live');
+ const labelText = labelEl.textContent.trim(); // Ignore ` ` (see below)
+ if (!labelText) {
+ return;
+ }
+
+ // Temporarily disable `aria-live` to prevent JAWS+Firefox from announcing the message twice.
+ ariaEl.setAttribute('aria-live', 'off');
+
+ // Temporarily clear `textContent` to force a DOM mutation event that will be detected by screen readers.
+ // `aria-live` elements are only announced when the element's `textContent` *changes*, so snackbars
+ // sent to the browser in the initial HTML response won't be read unless we clear the element's `textContent` first.
+ // Similarly, displaying the same snackbar message twice in a row doesn't trigger a DOM mutation event,
+ // so screen readers won't announce the second message unless we first clear `textContent`.
+ //
+ // We have to clear the label text two different ways to make it work in all browsers and screen readers:
+ //
+ // 1. `textContent = ''` is required for IE11 + JAWS
+ // 2. `innerHTML = ' '` is required for Chrome + JAWS and NVDA
+ //
+ // All other browser/screen reader combinations support both methods.
+ //
+ // The wrapper `` visually hides the space character so that it doesn't cause jank when added/removed.
+ // N.B.: Setting `position: absolute`, `opacity: 0`, or `height: 0` prevents Chrome from detecting the DOM change.
+ //
+ // This technique has been tested in:
+ //
+ // * JAWS 2019:
+ // - Chrome 70
+ // - Firefox 60 (ESR)
+ // - IE 11
+ // * NVDA 2018:
+ // - Chrome 70
+ // - Firefox 60 (ESR)
+ // - IE 11
+ // * ChromeVox 53
+ labelEl.textContent = '';
+ labelEl.innerHTML = '';
+
+ // Prevent visual jank by temporarily displaying the label text in the ::before pseudo-element.
+ // CSS generated content is normally announced by screen readers
+ // (except in IE 11; see https://tink.uk/accessibility-support-for-css-generated-content/);
+ // however, `aria-live` is turned off, so this DOM update will be ignored by screen readers.
+ labelEl.setAttribute(ARIA_LIVE_LABEL_TEXT_ATTR, labelText);
+
+ setTimeout(() => {
+ // Allow screen readers to announce changes to the DOM again.
+ ariaEl.setAttribute('aria-live', priority);
+
+ // Remove the message from the ::before pseudo-element.
+ labelEl.removeAttribute(ARIA_LIVE_LABEL_TEXT_ATTR);
+
+ // Restore the original label text, which will be announced by screen readers.
+ labelEl.textContent = labelText;
+ }, ARIA_LIVE_DELAY_MS);
+}
+
+export {announce};
diff --git a/test/screenshot/golden.json b/test/screenshot/golden.json
index 2532df5d7c5..054f6ef252c 100644
--- a/test/screenshot/golden.json
+++ b/test/screenshot/golden.json
@@ -1167,6 +1167,105 @@
"desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/travis/2018/11/21/19_18_38_795/spec/mdc-select/mixins/shape-radius.html.windows_ie_11.png"
}
},
+ "spec/mdc-snackbar/classes/baseline-with-action.html": {
+ "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/12/05/21_03_57_990/spec/mdc-snackbar/classes/baseline-with-action.html?utm_source=golden_json",
+ "screenshots": {
+ "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/classes/baseline-with-action.html.windows_chrome_69.png",
+ "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/02_34_48_408/spec/mdc-snackbar/classes/baseline-with-action.html.windows_edge_17.png",
+ "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/12/05/21_03_57_990/spec/mdc-snackbar/classes/baseline-with-action.html.windows_firefox_63.png",
+ "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/classes/baseline-with-action.html.windows_ie_11.png"
+ }
+ },
+ "spec/mdc-snackbar/classes/baseline-without-action.html": {
+ "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/12/05/21_03_57_990/spec/mdc-snackbar/classes/baseline-without-action.html?utm_source=golden_json",
+ "screenshots": {
+ "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/classes/baseline-without-action.html.windows_chrome_69.png",
+ "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/02_34_48_408/spec/mdc-snackbar/classes/baseline-without-action.html.windows_edge_17.png",
+ "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/12/05/21_03_57_990/spec/mdc-snackbar/classes/baseline-without-action.html.windows_firefox_63.png",
+ "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/classes/baseline-without-action.html.windows_ie_11.png"
+ }
+ },
+ "spec/mdc-snackbar/classes/leading.html": {
+ "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/12/05/21_03_57_990/spec/mdc-snackbar/classes/leading.html?utm_source=golden_json",
+ "screenshots": {
+ "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/classes/leading.html.windows_chrome_69.png",
+ "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/02_34_48_408/spec/mdc-snackbar/classes/leading.html.windows_edge_17.png",
+ "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/12/05/21_03_57_990/spec/mdc-snackbar/classes/leading.html.windows_firefox_63.png",
+ "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/classes/leading.html.windows_ie_11.png"
+ }
+ },
+ "spec/mdc-snackbar/classes/stacked.html": {
+ "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/12/05/21_03_57_990/spec/mdc-snackbar/classes/stacked.html?utm_source=golden_json",
+ "screenshots": {
+ "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/classes/stacked.html.windows_chrome_69.png",
+ "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/02_34_48_408/spec/mdc-snackbar/classes/stacked.html.windows_edge_17.png",
+ "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/12/05/21_03_57_990/spec/mdc-snackbar/classes/stacked.html.windows_firefox_63.png",
+ "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/classes/stacked.html.windows_ie_11.png"
+ }
+ },
+ "spec/mdc-snackbar/mixins/elevation.html": {
+ "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/12/05/21_03_57_990/spec/mdc-snackbar/mixins/elevation.html?utm_source=golden_json",
+ "screenshots": {
+ "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/mixins/elevation.html.windows_chrome_69.png",
+ "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/02_34_48_408/spec/mdc-snackbar/mixins/elevation.html.windows_edge_17.png",
+ "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/12/05/21_03_57_990/spec/mdc-snackbar/mixins/elevation.html.windows_firefox_63.png",
+ "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/mixins/elevation.html.windows_ie_11.png"
+ }
+ },
+ "spec/mdc-snackbar/mixins/fill-color.html": {
+ "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/12/05/21_03_57_990/spec/mdc-snackbar/mixins/fill-color.html?utm_source=golden_json",
+ "screenshots": {
+ "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/mixins/fill-color.html.windows_chrome_69.png",
+ "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/02_34_48_408/spec/mdc-snackbar/mixins/fill-color.html.windows_edge_17.png",
+ "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/12/05/21_03_57_990/spec/mdc-snackbar/mixins/fill-color.html.windows_firefox_63.png",
+ "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/mixins/fill-color.html.windows_ie_11.png"
+ }
+ },
+ "spec/mdc-snackbar/mixins/label-ink-color.html": {
+ "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/12/05/21_03_57_990/spec/mdc-snackbar/mixins/label-ink-color.html?utm_source=golden_json",
+ "screenshots": {
+ "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/mixins/label-ink-color.html.windows_chrome_69.png",
+ "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/02_34_48_408/spec/mdc-snackbar/mixins/label-ink-color.html.windows_edge_17.png",
+ "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/12/05/21_03_57_990/spec/mdc-snackbar/mixins/label-ink-color.html.windows_firefox_63.png",
+ "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/mixins/label-ink-color.html.windows_ie_11.png"
+ }
+ },
+ "spec/mdc-snackbar/mixins/max-width.html": {
+ "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/12/05/21_03_57_990/spec/mdc-snackbar/mixins/max-width.html?utm_source=golden_json",
+ "screenshots": {
+ "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/mixins/max-width.html.windows_chrome_69.png",
+ "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/02_34_48_408/spec/mdc-snackbar/mixins/max-width.html.windows_edge_17.png",
+ "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/12/05/21_03_57_990/spec/mdc-snackbar/mixins/max-width.html.windows_firefox_63.png",
+ "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/mixins/max-width.html.windows_ie_11.png"
+ }
+ },
+ "spec/mdc-snackbar/mixins/min-width.html": {
+ "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/12/05/21_03_57_990/spec/mdc-snackbar/mixins/min-width.html?utm_source=golden_json",
+ "screenshots": {
+ "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/mixins/min-width.html.windows_chrome_69.png",
+ "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/02_34_48_408/spec/mdc-snackbar/mixins/min-width.html.windows_edge_17.png",
+ "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/12/05/21_03_57_990/spec/mdc-snackbar/mixins/min-width.html.windows_firefox_63.png",
+ "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/mixins/min-width.html.windows_ie_11.png"
+ }
+ },
+ "spec/mdc-snackbar/mixins/shape-radius.html": {
+ "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/12/05/21_03_57_990/spec/mdc-snackbar/mixins/shape-radius.html?utm_source=golden_json",
+ "screenshots": {
+ "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/mixins/shape-radius.html.windows_chrome_69.png",
+ "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/02_34_48_408/spec/mdc-snackbar/mixins/shape-radius.html.windows_edge_17.png",
+ "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/12/05/21_03_57_990/spec/mdc-snackbar/mixins/shape-radius.html.windows_firefox_63.png",
+ "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/mixins/shape-radius.html.windows_ie_11.png"
+ }
+ },
+ "spec/mdc-snackbar/mixins/viewport-margin.html": {
+ "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/02_34_48_408/spec/mdc-snackbar/mixins/viewport-margin.html?utm_source=golden_json",
+ "screenshots": {
+ "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/mixins/viewport-margin.html.windows_chrome_69.png",
+ "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/02_34_48_408/spec/mdc-snackbar/mixins/viewport-margin.html.windows_edge_17.png",
+ "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/mixins/viewport-margin.html.windows_firefox_62.png",
+ "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/11/13/01_26_33_478/spec/mdc-snackbar/mixins/viewport-margin.html.windows_ie_11.png"
+ }
+ },
"spec/mdc-switch/classes/baseline.html": {
"public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/travis/2018/09/10/17_42_51_602/spec/mdc-switch/classes/baseline.html?utm_source=golden_json",
"screenshots": {
diff --git a/test/screenshot/spec/mdc-snackbar/classes/baseline-with-action.html b/test/screenshot/spec/mdc-snackbar/classes/baseline-with-action.html
new file mode 100644
index 00000000000..6f95196bc65
--- /dev/null
+++ b/test/screenshot/spec/mdc-snackbar/classes/baseline-with-action.html
@@ -0,0 +1,72 @@
+
+
+
+
+
+ Baseline Snackbar with Action - MDC Web Screenshot Test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
This item already has the label "travel". You can add a new label.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/screenshot/spec/mdc-snackbar/fixture.js b/test/screenshot/spec/mdc-snackbar/fixture.js
new file mode 100644
index 00000000000..577b50ccea6
--- /dev/null
+++ b/test/screenshot/spec/mdc-snackbar/fixture.js
@@ -0,0 +1,72 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+window.mdc.testFixture.fontsLoaded.then(() => {
+ /** @type {!Array} */
+ const queue = [];
+
+ /** @param {function(): void} fn */
+ function enqueue(fn) {
+ queue.push(fn);
+ if (queue.length === 1) {
+ fn();
+ }
+ }
+
+ function dequeue() {
+ queue.shift();
+ const nextFn = queue[0];
+ if (nextFn) {
+ setTimeout(nextFn, 250); // Insert a brief delay between queued snackbars (it's less visually jarring)
+ }
+ }
+
+ // Export snackbar instances to `window` for manual testing/debugging in dev tools
+ window.mdc.testFixture.snackbars = [];
+
+ [].forEach.call(document.querySelectorAll('.mdc-snackbar'), (rootEl) => {
+ /** @type {!MDCSnackbar} */
+ const snackbar = mdc.snackbar.MDCSnackbar.attachTo(rootEl);
+
+ const openButtonEl = document.querySelector(`[data-test-snackbar-id="${rootEl.id}"]`);
+ if (openButtonEl) {
+ openButtonEl.addEventListener('click', () => enqueue(() => snackbar.open()));
+ }
+
+ const {OPENING_EVENT, OPENED_EVENT, CLOSING_EVENT, CLOSED_EVENT} = mdc.snackbar.MDCSnackbarFoundation.strings;
+ [OPENING_EVENT, OPENED_EVENT, CLOSING_EVENT, CLOSED_EVENT].forEach((eventName) => {
+ snackbar.listen(eventName, (evt) => console.log(evt.type, evt.detail));
+ });
+
+ snackbar.listen(CLOSED_EVENT, dequeue);
+
+ const timeoutMs = parseInt(rootEl.getAttribute('data-test-snackbar-timeout-ms'), 10);
+ if (timeoutMs > 0) {
+ snackbar.timeoutMs = timeoutMs;
+ }
+
+ window.mdc.testFixture.snackbars.push(snackbar);
+ });
+
+ window.mdc.testFixture.notifyDomReady();
+});
diff --git a/demos/snackbar.scss b/test/screenshot/spec/mdc-snackbar/fixture.scss
similarity index 58%
rename from demos/snackbar.scss
rename to test/screenshot/spec/mdc-snackbar/fixture.scss
index e44c6093951..04b3aafd5e6 100644
--- a/demos/snackbar.scss
+++ b/test/screenshot/spec/mdc-snackbar/fixture.scss
@@ -1,5 +1,5 @@
//
-// Copyright 2017 Google Inc.
+// Copyright 2018 Google Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
@@ -20,9 +20,35 @@
// THE SOFTWARE.
//
-@import "./common";
-@import "../packages/mdc-button/mdc-button";
-@import "../packages/mdc-checkbox/mdc-checkbox";
-@import "../packages/mdc-form-field/mdc-form-field";
-@import "../packages/mdc-snackbar/mdc-snackbar";
-@import "../packages/mdc-textfield/mdc-text-field";
+@import "../../../../packages/mdc-snackbar/mixins";
+@import "../../../../packages/mdc-theme/color-palette";
+
+$custom-snackbar-color: $material-color-red-500;
+
+.custom-snackbar--elevation {
+ @include mdc-snackbar-elevation(16);
+}
+
+.custom-snackbar--fill-color {
+ @include mdc-snackbar-fill-color($custom-snackbar-color);
+}
+
+.custom-snackbar--label-ink-color {
+ @include mdc-snackbar-label-ink-color($custom-snackbar-color);
+}
+
+.custom-snackbar--max-width {
+ @include mdc-snackbar-max-width(200px);
+}
+
+.custom-snackbar--min-width {
+ @include mdc-snackbar-min-width(800px);
+}
+
+.custom-snackbar--shape-radius {
+ @include mdc-snackbar-shape-radius(10px);
+}
+
+.custom-snackbar--viewport-margin {
+ @include mdc-snackbar-viewport-margin(50px);
+}
diff --git a/test/screenshot/spec/mdc-snackbar/mixins/elevation.html b/test/screenshot/spec/mdc-snackbar/mixins/elevation.html
new file mode 100644
index 00000000000..7f38ea048d0
--- /dev/null
+++ b/test/screenshot/spec/mdc-snackbar/mixins/elevation.html
@@ -0,0 +1,72 @@
+
+
+
+
+
+ elevation Snackbar Mixin - MDC Web Screenshot Test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+