diff --git a/package.json b/package.json
index 6ab2d31dd..d29147d54 100644
--- a/package.json
+++ b/package.json
@@ -78,6 +78,7 @@
"@material/notched-outline": "^0.41.0",
"@material/ripple": "^0.41.0",
"@material/select": "^0.40.1",
+ "@material/snackbar": "^0.43.0",
"@material/switch": "^0.41.0",
"@material/tab": "^0.41.0",
"@material/tab-bar": "^0.41.0",
diff --git a/packages/snackbar/README.md b/packages/snackbar/README.md
new file mode 100644
index 000000000..dc4246dab
--- /dev/null
+++ b/packages/snackbar/README.md
@@ -0,0 +1,91 @@
+# React Snackbar
+
+A React version of an [MDC Snackbar](https://github.com/material-components/material-components-web/tree/master/packages/mdc-snackbar).
+
+## Installation
+
+```
+npm install @material/react-snackbar
+```
+
+## Usage
+
+### Styles
+
+with Sass:
+```js
+import '@material/react-snackbar/index.scss';
+```
+
+with CSS:
+```js
+import '@material/react-snackbar/dist/snackbar.css';
+```
+
+### Javascript Instantiation
+```js
+import React from 'react';
+import Snackbar from '@material/react-snackbar';
+
+class MyApp extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+}
+```
+
+## Props
+
+Prop Name | Type | Description
+--- | --- | ---
+message | String | Message to show in the snackbar
+className | String | Classes to be applied to the root element.
+timeoutMs | Number | Timeout in milliseconds when to close snackbar.
+closeOnEscape | Boolean | Closes popup on "Esc" button if true.
+actionText | String | Text for action button
+leading | Boolean | Shows snackbar on the left if true (or right for rtl languages)
+stacked | Boolean | Shows buttons under text if true
+onAnnounce | Function() => void | Callback for handling screenreader announce event
+onOpening | Function() => void | Callback for handling event, which happens before opening
+onOpen | Function(evt: Event) => void | Callback for handling event, which happens after opening
+onClosing | Function() => void | Callback for handling event, which happens before closing
+onClose | Function() => void | Callback for handling event, which happens after closing
+
+## Getting snackbar parameters
+
+If you need to get the `timeoutMs`, `closeOnEscape`, or `open` value, then you can use a ref like so:
+
+```js
+import React from 'react';
+import Snackbar from '@material/react-snackbar';
+ class MyApp extends React.Component {
+ getSnackbarInfo = (snackbar) => {
+ if (!snackbar) return;
+ console.log(snackbar.getTimeoutMs());
+ console.log(snackbar.isOpen());
+ console.log(snackbar.getCloseOnEscape());
+ }
+ render() {
+ return (
+
+ );
+ }
+}
+```
+
+## Sass Mixins
+
+Sass mixins may be available to customize various aspects of the Components. Please refer to the
+MDC Web repository for more information on what mixins are available, and how to use them.
+
+[Advanced Sass Mixins](https://github.com/material-components/material-components-web/blob/master/packages/mdc-snackbar/README.md#sass-mixins)
+
+## Usage with Icons
+
+Please see our [Best Practices doc](../../docs/best-practices.md#importing-font-icons) when importing or using icon fonts.
diff --git a/packages/snackbar/index.scss b/packages/snackbar/index.scss
new file mode 100644
index 000000000..2eefaf7ef
--- /dev/null
+++ b/packages/snackbar/index.scss
@@ -0,0 +1,23 @@
+// The MIT License
+//
+// Copyright (c) 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/snackbar/mdc-snackbar";
diff --git a/packages/snackbar/index.tsx b/packages/snackbar/index.tsx
new file mode 100644
index 000000000..34c1d2543
--- /dev/null
+++ b/packages/snackbar/index.tsx
@@ -0,0 +1,179 @@
+// The MIT License
+//
+// Copyright (c) 2019 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 * as React from 'react';
+import classnames from 'classnames';
+
+// TODO: replace with MDC Web types when available
+import {IMDCSnackbarAdapter, IMDCSnackbarFoundation} from './types';
+
+// @ts-ignore no .d.ts file
+import {MDCSnackbarFoundation} from '@material/snackbar';
+
+export interface Props {
+ message: string;
+ className?: string;
+ timeoutMs?: number;
+ closeOnEscape?: boolean;
+ actionText?: string;
+ leading?: boolean;
+ stacked?: boolean;
+ open?: boolean;
+ onOpening?: () => void;
+ onOpen?: () => void;
+ onClosing?: (reason: string) => void;
+ onClose?: (reason: string) => void;
+ onAnnounce?: () => void;
+};
+
+type State = {
+ classes: Set,
+};
+
+export class Snackbar extends React.Component {
+ foundation: IMDCSnackbarFoundation
+
+ static defaultProps: Partial = {
+ open: true,
+ stacked: false,
+ leading: false,
+ }
+
+ constructor(props: Props) {
+ super(props);
+ const {timeoutMs, closeOnEscape, leading, stacked} = this.props;
+ const classes = new Set();
+ if (leading) {
+ classes.add('mdc-snackbar--leading');
+ }
+
+ if (stacked) {
+ classes.add('mdc-snackbar--stacked');
+ }
+
+ this.state = {
+ classes,
+ };
+
+ this.foundation = new MDCSnackbarFoundation(this.adapter);
+ if (timeoutMs) {
+ this.foundation.setTimeoutMs(timeoutMs);
+ }
+
+ if (closeOnEscape) {
+ this.foundation.setCloseOnEscape(closeOnEscape);
+ }
+ }
+ get adapter(): IMDCSnackbarAdapter {
+ return {
+ addClass: (className: string) => {
+ const {classes} = this.state;
+ classes.add(className);
+ this.setState({
+ classes,
+ });
+ },
+ removeClass: (className: string) => {
+ const {classes} = this.state;
+ classes.delete(className);
+ this.setState({
+ classes,
+ });
+ },
+ announce: () => {
+ // Usually it works automatically if this component uses conditional rendering
+ this.props.onAnnounce && this.props.onAnnounce();
+ },
+ notifyOpening: () => {
+ const {onOpening} = this.props;
+ if (onOpening) {
+ onOpening();
+ }
+ },
+ notifyOpened: () => {
+ const {onOpen} = this.props;
+ if (onOpen) {
+ onOpen();
+ }
+ },
+ notifyClosing: (reason: string) => {
+ const {onClosing} = this.props;
+ if (onClosing) {
+ onClosing(reason);
+ }
+ },
+ notifyClosed: (reason: string) => {
+ const {onClose} = this.props;
+ if (onClose) {
+ onClose(reason);
+ }
+ },
+ };
+ }
+ close(action: string) {
+ this.foundation.close(action);
+ }
+ getTimeoutMs() {
+ return this.foundation.getTimeoutMs();
+ }
+ getCloseOnEscape() {
+ return this.foundation.getCloseOnEscape();
+ }
+ isOpen() {
+ return this.foundation.isOpen();
+ }
+ handleKeyDown = (e: React.KeyboardEvent) => {
+ this.foundation.handleKeyDown(e.nativeEvent);
+ }
+ handleActionClick = (e: React.MouseEvent) => {
+ this.foundation.handleActionButtonClick(e.nativeEvent);
+ }
+ componentDidMount() {
+ this.foundation.init();
+ if (this.props.open) {
+ this.foundation.open();
+ }
+ }
+ componentWillUnmount() {
+ this.foundation.destroy();
+ }
+ get classes() {
+ return classnames(this.props.className, 'mdc-snackbar', ...Array.from(this.state.classes));
+ }
+ render() {
+ return
+
+
+ {this.props.message}
+
+ {this.props.actionText ?
+
+
+
: null}
+
+
;
+ }
+}
diff --git a/packages/snackbar/package.json b/packages/snackbar/package.json
new file mode 100644
index 000000000..10f0ffefb
--- /dev/null
+++ b/packages/snackbar/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "@material/react-snackbar",
+ "version": "0.0.0",
+ "description": "Material Components React Snackbar",
+ "license": "MIT",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "keywords": [
+ "mdc web react",
+ "material components react",
+ "material design",
+ "material snackbar",
+ "materialsnackbar"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/material-components/material-components-web-react.git"
+ },
+ "dependencies": {
+ "classnames": "^2.2.5",
+ "react": "^16.4.2"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/snackbar/types.tsx b/packages/snackbar/types.tsx
new file mode 100644
index 000000000..31964ea71
--- /dev/null
+++ b/packages/snackbar/types.tsx
@@ -0,0 +1,49 @@
+// The MIT License
+//
+// Copyright (c) 2019 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.
+
+// TODO: remove this when MDC Web types are added.
+
+export interface IMDCSnackbarAdapter {
+ addClass(className: string): void
+ removeClass(className: string): void
+ announce(): void
+ notifyOpening(): void
+ notifyOpened(): void
+ notifyClosing(reason: string): void
+ notifyClosed(reason: string): void
+}
+
+export interface IMDCSnackbarFoundation {
+ open(): void;
+ close(action: string): void;
+ isOpen(): boolean
+ getTimeoutMs(): number
+ setTimeoutMs(timeoutMs: number): void
+ getCloseOnEscape(): boolean
+ setCloseOnEscape(closeOnEscape: boolean): void
+ handleKeyDown(event: KeyboardEvent): void
+ handleActionButtonClick(event: MouseEvent): void
+ handleActionIconClick(event: MouseEvent): void
+ init(): void
+ destroy(): void
+ adapter_: IMDCSnackbarAdapter
+}
diff --git a/scripts/package-json-reader.js b/scripts/package-json-reader.js
index 994e555a7..7d3b1da27 100644
--- a/scripts/package-json-reader.js
+++ b/scripts/package-json-reader.js
@@ -15,7 +15,7 @@ const readMaterialPackages = () => {
}
}
return dependencies;
-}
+};
module.exports = {readMaterialPackages};
diff --git a/test/screenshot/golden.json b/test/screenshot/golden.json
index c7bb42e82..c5c0c3671 100644
--- a/test/screenshot/golden.json
+++ b/test/screenshot/golden.json
@@ -15,6 +15,7 @@
"notched-outline": "7770dd381c27608a1f43b6f83da92507fe53963f5e4409bd73184b86275538fe",
"radio": "adfce0bbfa2711c67a52e1c3c3c7e980314be0147e340dac733152c80c385765",
"select": "10b82843806ddc961e85192d231ba5c3bc5aef7c20c14ffda7dd1e857b5a8c9f",
+ "snackbar": "fb7f6f61f37095d7e7c92c9305eb797145d8e72c0c8dbc4e531f9aca91b0fd28",
"switch": "dd8a3ec00447e0c586b5bbefdc633681d29e6f04ff8b517a68209bd1f4a6a4e4",
"tab": "0e53fa0ca9b2de4ff7941169a9b6a929a83b18e517c18404adeb40f7e644a2f1",
"tab-bar": "6c28ec268b2baf308459e7df9d7471fb7907b6473240b9a28a81be54a335f932",
diff --git a/test/screenshot/screenshot-test-urls.tsx b/test/screenshot/screenshot-test-urls.tsx
index 07b190964..672044b87 100644
--- a/test/screenshot/screenshot-test-urls.tsx
+++ b/test/screenshot/screenshot-test-urls.tsx
@@ -19,6 +19,7 @@ const urls = [
'notched-outline',
'radio',
'select',
+ 'snackbar',
'tab',
'tab-bar',
'tab-indicator',
diff --git a/test/screenshot/snackbar/index.scss b/test/screenshot/snackbar/index.scss
new file mode 100644
index 000000000..b17367c8d
--- /dev/null
+++ b/test/screenshot/snackbar/index.scss
@@ -0,0 +1,8 @@
+.snackbar-container {
+ margin-bottom: 16px;
+ // by default snackbar is displayed in the screen bottom.
+ // for screenshots we need to display snackbar inside its parent
+ .mdc-snackbar.mdc-snackbar {
+ position: static;
+ }
+}
diff --git a/test/screenshot/snackbar/index.tsx b/test/screenshot/snackbar/index.tsx
new file mode 100644
index 000000000..5e02ef739
--- /dev/null
+++ b/test/screenshot/snackbar/index.tsx
@@ -0,0 +1,24 @@
+import * as React from 'react';
+import '../../../packages/snackbar/index.scss';
+import './index.scss';
+import {Snackbar} from '../../../packages/snackbar/index';
+
+const ButtonScreenshotTest = () => {
+ return (
+