├── . browserslistrc ├── .babelrc ├── .gitignore ├── .npmignore ├── .npmrc ├── .stylelintrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── components │ ├── CodeFormatter │ │ ├── CodeFormatter.css │ │ └── index.js │ ├── HomePage │ │ └── index.js │ ├── Navigation │ │ ├── Desktop.css │ │ ├── Desktop.js │ │ ├── Mobile.css │ │ ├── Mobile.js │ │ ├── Shared.css │ │ ├── Shared.js │ │ └── index.js │ └── PageWrapper │ │ ├── GitHub.svg │ │ ├── PageWrapper.css │ │ └── index.js ├── index.css ├── index.html ├── index.js ├── pages │ ├── AppBar │ │ └── index.js │ ├── BottomNavigation │ │ └── index.js │ ├── Button │ │ ├── Button.css │ │ └── index.js │ ├── Collapse │ │ ├── Collapse.css │ │ └── index.js │ ├── Dialog │ │ ├── Dialog.css │ │ └── index.js │ ├── Grid │ │ └── index.js │ ├── List │ │ └── index.js │ ├── Page.css │ ├── Page.js │ ├── Paper │ │ └── index.js │ ├── SelectField │ │ └── index.js │ ├── SnackBar │ │ ├── SnackBar.css │ │ └── index.js │ ├── Switch │ │ └── index.js │ ├── Table │ │ └── index.js │ ├── Tabs │ │ └── index.js │ ├── TextField │ │ └── index.js │ └── Typography │ │ └── index.js └── utils │ └── globals.js ├── karma.conf.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── AppBar │ ├── AppBar.css │ ├── AppBar.js │ ├── AppBar.spec.js │ └── index.js ├── BottomNavigation │ ├── BottomNavigation.css │ ├── BottomNavigation.js │ ├── BottomNavigation.spec.js │ ├── BottomNavigationItem.css │ ├── BottomNavigationItem.js │ ├── BottomNavigationItem.spec.js │ └── index.js ├── Button │ ├── Button.css │ ├── Button.js │ ├── Button.spec.js │ └── index.js ├── Collapse │ ├── Collapse.css │ ├── Collapse.js │ ├── Collapse.spec.js │ └── index.js ├── Dialog │ ├── Dialog.css │ ├── Dialog.js │ ├── Dialog.spec.js │ ├── DialogInner.js │ └── index.js ├── Grid │ ├── Grid.css │ ├── Grid.js │ ├── Grid.spec.js │ ├── GridItem.css │ ├── GridItem.js │ ├── GridItem.spec.js │ └── index.js ├── List │ ├── List.css │ ├── List.js │ ├── List.spec.js │ ├── ListItem.css │ ├── ListItem.js │ ├── ListItem.spec.js │ └── index.js ├── Paper │ ├── Paper.css │ ├── Paper.js │ ├── Paper.spec.js │ └── index.js ├── Ripple │ ├── Ripple.css │ ├── Ripple.js │ ├── Ripple.spec.js │ ├── RippleItem.css │ ├── RippleItem.js │ ├── RippleItem.spec.js │ └── index.js ├── Scrollable │ ├── Scrollable.css │ ├── Scrollable.js │ ├── Scrollable.spec.js │ └── index.js ├── SelectField │ ├── SelectField.css │ ├── SelectField.js │ ├── SelectField.spec.js │ └── index.js ├── SnackBar │ ├── SnackBar.css │ ├── SnackBar.js │ ├── SnackBar.spec.js │ ├── SnackBarItem.js │ └── index.js ├── SvgIcon │ ├── SvgIcon.css │ ├── SvgIcon.js │ ├── SvgIcon.spec.js │ └── index.js ├── Switch │ ├── Switch.css │ ├── Switch.js │ ├── Switch.spec.js │ └── index.js ├── Table │ ├── Table.css │ ├── Table.js │ ├── Table.spec.js │ ├── TableBody.js │ ├── TableCell.css │ ├── TableCell.js │ ├── TableHead.js │ ├── TableRow.js │ └── index.js ├── Tabs │ ├── ScrollbarSize.js │ ├── ScrollbarSize.spec.js │ ├── Tab.css │ ├── Tab.js │ ├── Tab.spec.js │ ├── Tabs.css │ ├── Tabs.js │ ├── Tabs.spec.js │ └── index.js ├── TextField │ ├── TextField.css │ ├── TextField.js │ ├── TextField.spec.js │ ├── TextFieldAnimations.css │ └── index.js ├── Typography │ ├── Typography.css │ ├── Typography.js │ ├── Typography.spec.js │ └── index.js ├── index.js ├── utils │ ├── DOMBodyRender.js │ └── DOMBodyRender.spec.js └── variables.js ├── test ├── context.html ├── ssr.js ├── test_index.js └── utils.js ├── webpack.config.docs.js └── webpack.config.ssr.js /. browserslistrc: -------------------------------------------------------------------------------- 1 | last 4 versions 2 | Android >= 4.4 3 | Chrome >= 49 4 | Edge >= 12 5 | Firefox >= 51 6 | IE >= 11 7 | iOS >= 8.4 8 | Safari >= 8 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": { 4 | "presets": [ 5 | [ 6 | "@babel/preset-env", 7 | { 8 | "modules": "commonjs", 9 | "targets": { 10 | "browsers": ["last 2 versions"] 11 | } 12 | } 13 | ] 14 | ], 15 | "plugins": [ 16 | "babel-plugin-transform-react-remove-prop-types", 17 | [ 18 | "babel-plugin-css-modules-transform", 19 | { 20 | "generateScopedName": "[name].[local]", 21 | "prepend": [ "./postcss.config.js" ], 22 | "extractCss": { 23 | "dir": "./dist/", 24 | "relativeRoot": "./src/", 25 | "filename": "[path]/[name].css" 26 | } 27 | } 28 | ] 29 | ] 30 | }, 31 | "module": { 32 | "plugins": [ 33 | "babel-plugin-transform-react-remove-prop-types", 34 | [ 35 | "babel-plugin-css-modules-transform", 36 | { 37 | "generateScopedName": "[name].[local]", 38 | "prepend": [ "./postcss.config.js" ], 39 | "keepImport": true, 40 | "extractCss": { 41 | "dir": "./es/", 42 | "relativeRoot": "./src/", 43 | "filename": "[path]/[name].css" 44 | } 45 | } 46 | ] 47 | ] 48 | } 49 | }, 50 | "plugins": [ 51 | "@babel/plugin-proposal-class-properties", 52 | "@babel/plugin-proposal-object-rest-spread", 53 | "@babel/plugin-transform-runtime", 54 | [ 55 | "babel-plugin-transform-define", { 56 | "__TEST__": false 57 | } 58 | ] 59 | ], 60 | "presets": [ 61 | [ 62 | "@babel/preset-env", 63 | { 64 | "modules": false, 65 | "targets": { 66 | "browsers": ["last 2 versions"] 67 | } 68 | } 69 | ], 70 | "@babel/preset-react" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | .tmp 3 | dist 4 | es 5 | node_modules 6 | .coveralls.yml 7 | npm-debug.log 8 | 9 | # Documentation-generated files 10 | /index.html 11 | /main.js 12 | /main.js.map 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | !.babelrc 3 | !dist/ 4 | !es/ 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | save-exact=true 3 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": { 4 | "declaration-block-no-redundant-longhand-properties": null 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "stable" 4 | before_script: 5 | - npm run lint 6 | - npm run build 7 | install: 8 | - npm install 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.0.6](https://github.com/collegepulse/material-react-components/compare/v2.0.5...v2.0.6) (2019-09-03) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * Docs updates ([#21](https://github.com/collegepulse/material-react-components/issues/21)) ([c1439d6](https://github.com/collegepulse/material-react-components/commit/c1439d6)) 7 | 8 | 9 | 10 | ## v2.0.5 11 | 12 | - [Tabs] Implementation detail changes to reduce bundle size 13 | 14 | ## v2.0.4 15 | 16 | - [Switch] [TextField] enhancements to support Preact 17 | 18 | ## v2.0.3 19 | 20 | - [Dialog] fix for better support for Preact 21 | 22 | ## v2.0.2 23 | 24 | - Upgrade to @babel/runtime@7.0.0 25 | 26 | ## v2.0.1 27 | 28 | - Exclude require statements to CSS files in CommonJS build 29 | 30 | ## v2.0.0 31 | 32 | ### API Changes 33 | 34 | - `Switch` and `TextField` no longer autogenerate an aria-labelledby attribute for their input fields. They can be supplied via the labelId prop. This change was needed to fix server-side rendering hydration errors on the client. uuid was also removed from dependencies, so expect a slightly smaller bundle size. 35 | 36 | ### Build Changes 37 | 38 | For versions `1.2.3` (inclusive) and prior, the distributions were incorrect. 39 | 40 | - The commonjs `dist/index.js` file had module ES module import/export syntax. 41 | - The commonjs source files had their css inlined (poor for performance and causes bundle size bloat). 42 | - The module build couldn't be tree-shaken since `{sideEffects: false}` was not set in package.json. 43 | - Both builds missed other opportunities for reducing bundle size, like removing prop types. 44 | 45 | ## v1.2.3 46 | 47 | - remove `deep-assign` from dependencies 48 | - remove `keycode` from dependencies 49 | 50 | ## v1.2.2 51 | 52 | No changes. 53 | 54 | ## v1.2.1 55 | 56 | - Upgrade from babel 6 to 7 57 | - switch linter from eslint to xo 58 | - upgrade to webpack 4 59 | - move babel dependencies to devDependencies (where applicable) 60 | 61 | ## v1.2.0 62 | 63 | - `SelectField` component implementation 64 | 65 | ## v1.1.0 66 | 67 | - Added `Table`, partial implementation (https://material.io/guidelines/components/data-tables.html) 68 | - Upgraded some dependencies 69 | 70 | ## 1.0.0 71 | 72 | 🎉 1.0.0 release 🎉 73 | 74 | - **Support for React 16.** Cleaned up warnings that were present upon upgrade. 75 | - **Removed undocumented API's.** Some components accepted an undocumented domRef property; these have been removed. Out of an abundance of caution, a major release bump-- upgrading should be relatively effortless, however). 76 | 77 | ## 0.0.5 78 | 79 | - [SnackBar] fix centering on IE 11 and Safari 80 | 81 | ## 0.0.4 82 | 83 | - [GridItem] fix issue where child DOM elements could overflow outside the grid 84 | 85 | ## 0.0.3 86 | 87 | - [Tabs] a11y enhancements, fix UI bug where tabs had incorrect height 88 | 89 | ## v0.0.2-0 90 | 91 | ## v0.0.1-0 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 CollegePulse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ### ✨[Demo Site](https://collegepulse.github.io/material-react-components/): live component demos, code samples & more. ✨ 3 | material-react-components 4 | ========================= 5 | 6 | [![npm package][npm-image]][npm-url] 7 | [![Build Status][travis-image]][travis-url] 8 | [![Coverage Status][coveralls-image]][coveralls-url] 9 | [![Dependencies Status][david-image]][david-url] 10 | 11 | This is a collection of React components that implement Google's [material design specification](https://material.io/guidelines). The goals of this project include: 12 | 13 | - Minimal to zero configuration to consume components through carefully-crafted, lightweight implementations. 14 | - Be fit for enterprise consumption: components are to be performant, accessible, unit tested, cross-browser supported, support server-side rendering, etc. 15 | 16 | 17 | Examples 18 | -------- 19 | 20 | See our [material-react-components-examples](https://github.com/collegepulse/material-react-components-examples) repo for examples of small projects that demonstrate how to import the ES5 or ES6 components with Webpack. 21 | 22 | Acknowlegements 23 | --------------- 24 | 25 | [](https://www.browserstack.com/) 26 | 27 | [BrowserStack](https://www.browserstack.com/) has provided us with the tools and infrastructure necessary to build a high quality component library by testing our components on an array of browsers. Thank you. 28 | 29 | [material-ui](https://github.com/callemall/material-ui) has been an influence in some of our component implementations. We are appreciative of the effort and contributions happening over there. 30 | 31 | [npm-image]:https://img.shields.io/npm/v/material-react-components.svg 32 | [npm-url]:https://www.npmjs.com/package/material-react-components 33 | [travis-image]:https://travis-ci.org/collegepulse/material-react-components.svg?branch=master 34 | [travis-url]:https://travis-ci.org/collegepulse/material-react-components 35 | [coveralls-image]:https://coveralls.io/repos/github/collegepulse/material-react-components/badge.svg?branch=master 36 | [coveralls-url]:https://coveralls.io/github/collegepulse/material-react-components?branch=master 37 | [david-image]:https://david-dm.org/collegepulse/material-react-components/status.svg 38 | [david-url]:https://david-dm.org/collegepulse/material-react-components 39 | -------------------------------------------------------------------------------- /docs/components/CodeFormatter/CodeFormatter.css: -------------------------------------------------------------------------------- 1 | .code { 2 | font-family: monospace; 3 | white-space: pre; 4 | overflow-x: scroll; 5 | } 6 | 7 | :global(.token) { 8 | background: transparent !important; 9 | } 10 | -------------------------------------------------------------------------------- /docs/components/CodeFormatter/index.js: -------------------------------------------------------------------------------- 1 | import lightTheme from '!raw-loader!prismjs/themes/prism.css'; // eslint-disable-line 2 | import prism from 'prismjs'; 3 | import 'prismjs/components/prism-markup'; 4 | import 'prismjs/components/prism-clike'; 5 | import 'prismjs/components/prism-javascript'; 6 | import 'prismjs/components/prism-jsx'; 7 | import PropTypes from 'prop-types'; 8 | import React from 'react'; 9 | import Scrollable from '../../../src/Scrollable'; 10 | import Styles from './CodeFormatter.css'; 11 | 12 | const styleNode = window.document.createElement('style'); 13 | window.document.head.appendChild(styleNode); 14 | styleNode.textContent = lightTheme; 15 | 16 | /* Removes the fewest number of spaces present in front of every line. */ 17 | function formatCode(str) { 18 | if (!str) { 19 | return ''; 20 | } 21 | const lines = str.split('\n'); 22 | let fewestStartingSpaces = Infinity; 23 | lines.map((line) => { 24 | if (line) { 25 | fewestStartingSpaces = Math.min(fewestStartingSpaces, line.search(/\S/)); 26 | } 27 | }); 28 | return lines.reduce((acc, val) => ( 29 | `${acc}${val.substring(fewestStartingSpaces)}\n` 30 | ), ''); 31 | } 32 | 33 | class CodeFormatter extends React.Component { 34 | render() { 35 | const {code, ...other} = this.props; 36 | const formattedCode = formatCode(code); 37 | const highlightedCode = prism.highlight(formattedCode, prism.languages.jsx); 38 | return ( 39 | 40 |
45 | 46 | ); 47 | } 48 | } 49 | 50 | CodeFormatter.defaultProps = { 51 | code: null 52 | }; 53 | 54 | CodeFormatter.propTypes = { 55 | code: PropTypes.string 56 | }; 57 | 58 | export default CodeFormatter; 59 | -------------------------------------------------------------------------------- /docs/components/HomePage/index.js: -------------------------------------------------------------------------------- 1 | import CodeFormatter from '../CodeFormatter'; 2 | import React from 'react'; 3 | import Typography from '../../../src/Typography'; 4 | 5 | function Base() { 6 | return ( 7 |
8 | 12 | Installation 13 | 14 | 15 | 19 | Configuration 20 | 21 | 22 |

23 | Two methods are available for using the library: 24 | {' ES6 (recommended) or ES5. See the '} 25 | 26 | example projects 27 | 28 | {' that demonstrate both.'} 29 |

30 | 34 | ES6 Configuration (recommended) 35 | 36 | Add the following loaders to your Webpack configuration: 37 | 58 |

{'If using the material-design-icons SVG icons, or any components containing SVG\'s (e.g., SelectField), include the following loader:'}

59 | 75 | 79 | ES5 Configuration 80 | 81 |

Follow the SVG instructions in the ES6 Configuration section, if applicable.

82 |
83 | ); 84 | } 85 | 86 | export default Base; 87 | -------------------------------------------------------------------------------- /docs/components/Navigation/Desktop.css: -------------------------------------------------------------------------------- 1 | .root { 2 | display: none; 3 | flex: 0 0 auto; 4 | width: 250px; 5 | } 6 | 7 | @media (min-width: $screenMd) { 8 | .root { 9 | display: inherit; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/components/Navigation/Desktop.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Scrollable from '../../../src/Scrollable'; 3 | import Shared from './Shared'; 4 | import Styles from './Desktop.css'; 5 | 6 | function Desktop() { 7 | return ( 8 |
9 | 10 | 11 | 12 |
13 | ); 14 | } 15 | 16 | export default Desktop; 17 | -------------------------------------------------------------------------------- /docs/components/Navigation/Mobile.css: -------------------------------------------------------------------------------- 1 | .root { 2 | position: fixed; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | z-index: 2500; 8 | } 9 | 10 | .overlay { 11 | position: absolute; 12 | top: 0; 13 | left: 0; 14 | right: 0; 15 | bottom: 0; 16 | background-color: $black54; 17 | animation-duration: 350ms; 18 | animation-fill-mode: forwards; 19 | animation-name: fade-in; 20 | } 21 | 22 | @keyframes fade-in { 23 | 0% { 24 | opacity: 0; 25 | } 26 | 27 | 100% { 28 | opacity: 1; 29 | } 30 | } 31 | 32 | @keyframes fade-out { 33 | 0% { 34 | opacity: 1; 35 | } 36 | 37 | 100% { 38 | opacity: 0; 39 | } 40 | } 41 | 42 | .content { 43 | transition: all 250ms; 44 | position: fixed; 45 | animation-duration: 350ms; 46 | animation-fill-mode: forwards; 47 | animation-name: slide-in; 48 | } 49 | 50 | @keyframes slide-in { 51 | 0% { 52 | transform: translateX(-250px); 53 | } 54 | 55 | 100% { 56 | transform: translateX(0); 57 | } 58 | } 59 | 60 | @keyframes slide-out { 61 | 0% { 62 | transform: translateX(0); 63 | } 64 | 65 | 100% { 66 | transform: translateX(-250px); 67 | } 68 | } -------------------------------------------------------------------------------- /docs/components/Navigation/Mobile.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import Shared from './Shared'; 4 | import Styles from './Mobile.css'; 5 | import TransitionGroup from 'react-transition-group/TransitionGroup'; 6 | 7 | function FirstChild(props) { 8 | const childrenArray = React.Children.toArray(props.children); 9 | return childrenArray[0] || null; 10 | } 11 | 12 | function Mobile({onClose, open}) { 13 | return ( 14 | 15 | {open && } 16 | 17 | ); 18 | } 19 | 20 | Mobile.defaultProps = { 21 | open: false 22 | }; 23 | 24 | Mobile.propTypes = { 25 | onClose: PropTypes.func.isRequired, 26 | open: PropTypes.bool 27 | }; 28 | 29 | class MobileInner extends React.Component { 30 | componentWillLeave(cb) { 31 | this.content.style.animationName = Styles['slide-out']; 32 | this.overlay.style.animationName = Styles['fade-out']; 33 | setTimeout(() => { 34 | cb(); 35 | }, 350); 36 | } 37 | 38 | render() { 39 | const {onClose} = this.props; 40 | return ( 41 |
42 |
(this.overlay = c)} 46 | /> 47 |
(this.content = c)} 50 | > 51 | 52 |
53 |
54 | ); 55 | } 56 | } 57 | 58 | MobileInner.defaultProps = { 59 | onClose: () => {} 60 | }; 61 | 62 | MobileInner.propTypes = { 63 | onClose: PropTypes.func 64 | }; 65 | 66 | export default Mobile; 67 | -------------------------------------------------------------------------------- /docs/components/Navigation/Shared.css: -------------------------------------------------------------------------------- 1 | .root { 2 | border-right: 1px solid #ddd; 3 | flex: 1 0 auto; 4 | height: 100vh; 5 | position: fixed; 6 | width: 250px; 7 | box-sizing: border-box; 8 | overflow-y: auto; 9 | zindex: 1; 10 | } 11 | 12 | .title { 13 | padding: 15px; 14 | } 15 | 16 | .subtitle { 17 | margin-top: 5px; 18 | } 19 | 20 | .inactive .primary { 21 | color: $black54; 22 | } 23 | 24 | .active .primary { 25 | color: $black; 26 | } 27 | -------------------------------------------------------------------------------- /docs/components/Navigation/Shared.js: -------------------------------------------------------------------------------- 1 | import {Link, NavLink} from 'react-router-dom'; 2 | import List, {ListItem} from '../../../src/List'; 3 | import {makeURL} from '../../utils/globals'; 4 | import Paper from '../../../src/Paper'; 5 | import PropTypes from 'prop-types'; 6 | import React from 'react'; 7 | import Scrollable from '../../../src/Scrollable'; 8 | import Styles from './Shared.css'; 9 | import Typography from '../../../src/Typography'; 10 | 11 | const items = [ 12 | { 13 | label: 'AppBar', 14 | url: makeURL('/AppBar') 15 | }, 16 | { 17 | label: 'BottomNavigation', 18 | url: makeURL('/BottomNavigation') 19 | }, 20 | { 21 | label: 'Button', 22 | url: makeURL('/Button') 23 | }, 24 | { 25 | label: 'Collapse', 26 | url: makeURL('/Collapse') 27 | }, 28 | { 29 | label: 'Dialog', 30 | url: makeURL('/Dialog') 31 | }, 32 | { 33 | label: 'Grid', 34 | url: makeURL('/Grid') 35 | }, 36 | { 37 | label: 'List', 38 | url: makeURL('/List') 39 | }, 40 | { 41 | label: 'Paper', 42 | url: makeURL('/Paper') 43 | }, 44 | { 45 | label: 'SelectField', 46 | url: makeURL('/SelectField') 47 | }, 48 | { 49 | label: 'SnackBar', 50 | url: makeURL('/SnackBar') 51 | }, 52 | { 53 | label: 'Switch', 54 | url: makeURL('/Switch') 55 | }, 56 | { 57 | label: 'Table', 58 | url: makeURL('/Table') 59 | }, 60 | { 61 | label: 'Tabs', 62 | url: makeURL('/Tabs') 63 | }, 64 | { 65 | label: 'TextField', 66 | url: makeURL('/TextField') 67 | }, 68 | { 69 | label: 'Typography', 70 | url: makeURL('/Typography') 71 | } 72 | ]; 73 | 74 | function Shared({onClick}) { 75 | return ( 76 | 77 | 78 | 79 | material-react-components 80 |
By CollegePulse
81 |
82 | 83 | {items.map(item => ( 84 | 95 | {item.label} 96 |
97 | )} 98 | /> 99 | ))} 100 | 101 | 102 | 103 | ); 104 | } 105 | 106 | Shared.defaultProps = { 107 | onClick: () => {} 108 | }; 109 | 110 | Shared.propTypes = { 111 | onClick: PropTypes.func 112 | }; 113 | 114 | export default Shared; 115 | 116 | -------------------------------------------------------------------------------- /docs/components/Navigation/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Desktop'; 2 | -------------------------------------------------------------------------------- /docs/components/PageWrapper/GitHub.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/components/PageWrapper/PageWrapper.css: -------------------------------------------------------------------------------- 1 | .rootInner { 2 | width: 100%; 3 | overflow-y: auto; 4 | height: 100%; 5 | position: absolute; 6 | } 7 | 8 | @media (min-width: $screenMd) { 9 | .rootInner { 10 | width: calc(100vw - 250px); 11 | left: 250px; 12 | } 13 | } 14 | 15 | .appBar { 16 | position: fixed; 17 | z-index: 1; 18 | } 19 | 20 | @media (min-width: $screenMd) { 21 | .appBar { 22 | position: relative; 23 | } 24 | } 25 | 26 | .iconWrapper { 27 | display: inherit; 28 | } 29 | 30 | .iconLink { 31 | display: flex; 32 | } 33 | 34 | @media (min-width: $screenMd) { 35 | .iconWrapper { 36 | display: none; 37 | } 38 | } 39 | 40 | .icon { 41 | fill: $white; 42 | } 43 | 44 | .content { 45 | margin: 100px 20px 20px; 46 | } 47 | 48 | @media (min-width: $screenMd) { 49 | .content { 50 | margin: 50px; 51 | } 52 | } -------------------------------------------------------------------------------- /docs/components/PageWrapper/index.js: -------------------------------------------------------------------------------- 1 | import AppBar from '../../../src/AppBar'; 2 | import AppBarDocs from '../../pages/AppBar'; 3 | import BottomNavigation from '../../pages/BottomNavigation'; 4 | import Button from '../../pages/Button'; 5 | import Collapse from '../../pages/Collapse'; 6 | import Dialog from '../../pages/Dialog'; 7 | import GitHub from './GitHub.svg'; 8 | import Grid from '../../pages/Grid'; 9 | import HomePage from '../HomePage'; 10 | import List from '../../pages/List'; 11 | import Mobile from '../Navigation/Mobile'; 12 | import Menu from 'material-design-icons/navigation/svg/production/ic_menu_24px.svg'; 13 | import {makeURL} from '../../utils/globals'; 14 | import Paper from '../../pages/Paper'; 15 | import React from 'react'; 16 | import {Route} from 'react-router-dom'; 17 | import SelectField from '../../pages/SelectField'; 18 | import SnackBar from '../../pages/SnackBar'; 19 | import Styles from './PageWrapper.css'; 20 | import Switch from '../../pages/Switch'; 21 | import SvgIcon from '../../../src/SvgIcon'; 22 | import Table from '../../pages/Table'; 23 | import Tabs from '../../pages/Tabs'; 24 | import TextField from '../../pages/TextField'; 25 | import TypographyDocs from '../../pages/Typography'; 26 | 27 | class PageWrapper extends React.Component { 28 | constructor(props) { 29 | super(props); 30 | this.onClick = this.onClick.bind(this); 31 | this.state = { 32 | open: false 33 | }; 34 | } 35 | 36 | onClick() { 37 | this.setState({ 38 | open: !this.state.open 39 | }); 40 | } 41 | 42 | render() { 43 | const pageName = window.location.pathname.split('/').pop() || 'material-react-components'; 44 | return ( 45 |
46 | 50 | 56 | 57 | } 58 | secondary={ 59 | 64 | 69 | 70 | } 71 | style={{width: '100%', color: '#fff'}} 72 | > 73 | {`<${pageName} />`} 74 | 75 | 76 |
77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 |
94 |
95 | ); 96 | } 97 | } 98 | 99 | export default PageWrapper; 100 | -------------------------------------------------------------------------------- /docs/index.css: -------------------------------------------------------------------------------- 1 | .index { 2 | display: flex; 3 | flex-direction: row; 4 | height: 100%; 5 | } 6 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | material-react-components 8 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/index.js: -------------------------------------------------------------------------------- 1 | import {BrowserRouter} from 'react-router-dom'; 2 | import Navigation from './components/Navigation'; 3 | import PageWrapper from './components/PageWrapper'; 4 | import React from 'react'; 5 | import {render} from 'react-dom'; 6 | import Styles from './index.css'; 7 | import 'babel-polyfill'; 8 | 9 | const app = document.createElement('div'); 10 | app.style.height = '100%'; 11 | document.body.appendChild(app); 12 | 13 | function Index() { 14 | return ( 15 | 16 |
17 | 18 | 19 |
20 |
21 | ); 22 | } 23 | 24 | render(, app); 25 | -------------------------------------------------------------------------------- /docs/pages/AppBar/index.js: -------------------------------------------------------------------------------- 1 | import AppBar from '../../../src/AppBar'; 2 | import Colors from '../../../src/variables'; 3 | import CodeFormatter from '../../components/CodeFormatter'; 4 | import Menu from 'material-design-icons/navigation/svg/production/ic_menu_24px.svg'; 5 | import Page from '../Page'; 6 | import React from 'react'; 7 | import Search from 'material-design-icons/action/svg/production/ic_search_24px.svg'; 8 | import SvgIcon from '../../../src/SvgIcon'; 9 | import Switch from '../../../src/Switch'; 10 | import TextField from '../../../src/TextField'; 11 | 12 | class AppBarDocs extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.onControlPanel = this.onControlPanel.bind(this); 16 | this.state = { 17 | backgroundColor: Colors.$primary, 18 | children: 'AppBar', 19 | elevation: '2', 20 | style: '{"fill": "#FFF", "color": "#FFF"}', 21 | primary: true, 22 | secondary: true 23 | }; 24 | } 25 | 26 | onControlPanel(key, value) { 27 | this.setState({[key]: value}); 28 | } 29 | 30 | render() { 31 | const {backgroundColor, children, elevation, primary, secondary, style} = this.state; 32 | let elevationNum; 33 | let styleObj; 34 | try { 35 | elevationNum = Number(this.state.elevation); 36 | styleObj = JSON.parse(style); 37 | } catch (e) { 38 | styleObj = {}; 39 | } 40 | return ( 41 | } 49 | secondary={secondary && } 50 | > 51 | {children} 52 | 53 | } 54 | buildYourOwnCode={ 55 | ' : null}} 67 | secondary={${secondary ? '' : null}} 68 | > 69 | ${children} 70 | ` 71 | } 72 | /> 73 | } 74 | buildYourOwnControlPanel={[ 75 | (this.onControlPanel('children', e.target.value))} 77 | label="children" 78 | value={children} 79 | />, 80 | (this.onControlPanel('elevation', e.target.value))} 83 | label="elevation" 84 | value={elevation} 85 | />, 86 | (this.onControlPanel('style', e.target.value))} 88 | label="Style" 89 | value={style} 90 | />, 91 | (this.onControlPanel('primary', e.target.checked))} 93 | checked={primary} 94 | label="primary" 95 | />, 96 | (this.onControlPanel('secondary', e.target.checked))} 98 | checked={secondary} 99 | label="secondary" 100 | /> 101 | ]} 102 | examples={ 103 |
104 | 105 | AppBar 106 | 107 |
108 | } 109 | /> 110 | ); 111 | } 112 | } 113 | 114 | export default AppBarDocs; 115 | -------------------------------------------------------------------------------- /docs/pages/BottomNavigation/index.js: -------------------------------------------------------------------------------- 1 | import BottomNavigation, {BottomNavigationItem} from '../../../src/BottomNavigation'; 2 | import Page from '../Page'; 3 | import React from 'react'; 4 | import TextField from '../../../src/TextField'; 5 | 6 | class BottomNavigationDocs extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.onControlPanel = this.onControlPanel.bind(this); 10 | this.state = { 11 | label: 'label' 12 | }; 13 | } 14 | 15 | onControlPanel(key, value) { 16 | this.setState({[key]: value}); 17 | } 18 | 19 | render() { 20 | const {label} = this.state; 21 | return ( 22 | 26 | {[1, 2, 3].map(key => ( 27 | {label} 28 | ))} 29 | 30 | } 31 | buildYourOwnControlPanel={[ 32 | (this.onControlPanel('label', e.target.value))} 34 | label="label" 35 | value={label} 36 | /> 37 | ]} 38 | examples={ 39 |
40 | 41 | Foo 42 | Bar 43 | Baz 44 | 45 |
46 | } 47 | /> 48 | ); 49 | } 50 | 51 | } 52 | 53 | export default BottomNavigationDocs; 54 | -------------------------------------------------------------------------------- /docs/pages/Button/Button.css: -------------------------------------------------------------------------------- 1 | .flex { 2 | display: flex; 3 | } 4 | -------------------------------------------------------------------------------- /docs/pages/Button/index.js: -------------------------------------------------------------------------------- 1 | import Add from 'material-design-icons/content/svg/production/ic_add_24px.svg'; 2 | import Button from '../../../src/Button'; 3 | import Edit from 'material-design-icons/editor/svg/production/ic_mode_edit_24px.svg'; 4 | import Page from '../Page'; 5 | import React from 'react'; 6 | import Styles from './Button.css'; 7 | import Switch from '../../../src/Switch'; 8 | import TextField from '../../../src/TextField'; 9 | import Variables from '../../../src/variables'; 10 | 11 | class ButtonDocs extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | this.onControlPanel = this.onControlPanel.bind(this); 15 | this.state = { 16 | children: 'children', 17 | textColor: '#FFF', 18 | buttonColor: Variables.$primary, 19 | fab: false, 20 | fill: '#FFF', 21 | focusRippleDisabled: false 22 | }; 23 | } 24 | 25 | onControlPanel(key, value) { 26 | this.setState({[key]: value}); 27 | } 28 | 29 | render() { 30 | const {children, textColor, buttonColor, fill, fab, focusRippleDisabled} = this.state; 31 | return ( 32 | 41 | {fab ? : children} 42 | 43 | } 44 | buildYourOwnControlPanel={[ 45 | (this.onControlPanel('children', e.target.value))} 47 | label="children" 48 | value={children} 49 | helperText="For demo, hidden when fab is enabled" 50 | />, 51 | (this.onControlPanel('textColor', e.target.value))} 53 | label="textColor" 54 | value={textColor} 55 | helperText="Button text color" 56 | />, 57 | (this.onControlPanel('buttonColor', e.target.value))} 59 | label="buttonColor" 60 | value={buttonColor} 61 | helperText="Button background color" 62 | />, 63 | (this.onControlPanel('fill', e.target.value))} 65 | label="fill" 66 | value={fill} 67 | helperText="SVG icon color (applicable when fab mode is enabled)" 68 | />, 69 | (this.onControlPanel('fab', e.target.checked))} 71 | checked={fab} 72 | label="fab" 73 | />, 74 | (this.onControlPanel('focusRippleDisabled', e.target.checked))} 76 | checked={focusRippleDisabled} 77 | label="focusRippleDisabled" 78 | /> 79 | ]} 80 | examples={ 81 |
82 |
83 | 84 | 85 | 86 | 87 |
88 |
89 | 92 | 95 |
96 |
97 | } 98 | /> 99 | ); 100 | } 101 | } 102 | 103 | export default ButtonDocs; 104 | -------------------------------------------------------------------------------- /docs/pages/Collapse/Collapse.css: -------------------------------------------------------------------------------- 1 | .collapse { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /docs/pages/Collapse/index.js: -------------------------------------------------------------------------------- 1 | import Collapse from '../../../src/Collapse'; 2 | import Page from '../Page'; 3 | import Paper from '../../../src/Paper'; 4 | import React from 'react'; 5 | import Styles from './Collapse.css'; 6 | import Switch from '../../../src/Switch'; 7 | import TextField from '../../../src/TextField'; 8 | 9 | class CollapseDocs extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.onControlPanel = this.onControlPanel.bind(this); 13 | this.state = { 14 | collapseBaseline: false, 15 | collapsedOpenOnInitialRender: true, 16 | open: true, 17 | children: 'The\nCollapse\nComponent\nInner\nText' 18 | }; 19 | } 20 | 21 | onControlPanel(key, value) { 22 | if (value) { 23 | return this.setState({[key]: value}); 24 | } 25 | return this.setState({[key]: !this.state[key]}); 26 | } 27 | 28 | render() { 29 | return ( 30 | 34 | 35 | {this.state.children} 36 | 37 | 38 | } 39 | buildYourOwnControlPanel={[ 40 | (this.onControlPanel('open', e.target.checked))} 42 | checked={this.state.open} 43 | label="open" 44 | />, 45 | (this.onControlPanel('children', e.target.value))} 47 | label="children" 48 | value={this.state.children} 49 | multiline 50 | helperText="Wrapped in a element with white-space: pre-wrap; and 10px padding." 51 | /> 52 | ]} 53 | examples={ 54 |
55 | (this.onControlPanel('collapseBaseline'))} label={'Initially Closed'} /> 56 | 57 | 58 | This
59 | is
60 | really
61 | long
62 | content
63 | that
64 | should
65 | animate.
66 |
67 |
68 | (this.onControlPanel('collapsedOpenOnInitialRender'))} label={'Initially Opened'} /> 69 | 70 | 71 | This
72 | is
73 | really
74 | long
75 | content
76 | that
77 | should
78 | animate.
79 |
80 |
81 |
82 | } 83 | /> 84 | ); 85 | } 86 | } 87 | 88 | export default CollapseDocs; 89 | -------------------------------------------------------------------------------- /docs/pages/Dialog/Dialog.css: -------------------------------------------------------------------------------- 1 | .flex { 2 | display: flex; 3 | } 4 | -------------------------------------------------------------------------------- /docs/pages/Dialog/index.js: -------------------------------------------------------------------------------- 1 | import Button from '../../../src/Button'; 2 | import Dialog from '../../../src/Dialog'; 3 | import Page from '../Page'; 4 | import React from 'react'; 5 | import Styles from './Dialog.css'; 6 | import TextField from '../../../src/TextField'; 7 | 8 | class DialogDocs extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.onControlPanel = this.onControlPanel.bind(this); 12 | this.state = { 13 | buildYourOwnDialogOpen: false, 14 | title: 'Title', 15 | description: 'Description', 16 | dialog1Open: false 17 | }; 18 | } 19 | 20 | onControlPanel(key, value) { 21 | if (value) { 22 | return this.setState({[key]: value}); 23 | } 24 | return this.setState({[key]: !this.state[key]}); 25 | } 26 | 27 | 28 | render() { 29 | return ( 30 | 34 | 35 | (this.onControlPanel('buildYourOwnDialogOpen'))} 38 | title={this.state.title} 39 | description={this.state.description} 40 | actions={[ 41 | , 42 | 43 | ]} 44 | /> 45 |
46 | } 47 | buildYourOwnControlPanel={[ 48 | (this.onControlPanel('title', e.target.value))} 50 | label="title" 51 | value={this.state.title} 52 | />, 53 | (this.onControlPanel('description', e.target.value))} 55 | label="description" 56 | value={this.state.description} 57 | /> 58 | ]} 59 | examples={ 60 |
61 | 62 | (this.onControlPanel('dialog1Open'))} 65 | title={'Use Google\'s location service?'} 66 | description={'Let Google help apps determine location. This means sending anonymous location data to Google, even when no apps are running.'} 67 | actions={[ 68 | , 69 | 70 | ]} 71 | /> 72 |
73 | } 74 | /> 75 | ); 76 | } 77 | } 78 | 79 | export default DialogDocs; 80 | -------------------------------------------------------------------------------- /docs/pages/Grid/index.js: -------------------------------------------------------------------------------- 1 | import Grid, {GridItem} from '../../../src/Grid'; 2 | import Page from '../Page'; 3 | import Paper from '../../../src/Paper'; 4 | import React from 'react'; 5 | import Typography from '../../../src/Typography'; 6 | 7 | class GridDocs extends React.Component { 8 | render() { 9 | return ( 10 | 14 | 15 | Full-width 16 | grids use fluid columns. 17 | 18 | 19 | {[1, 2, 3, 4, 5, 6, 7, 8].map(item => ( 20 | 21 | 31 | xs=12, sm=6, md=4, lg=3 32 | 33 | 34 | ))} 35 | 36 | 37 | Centered 38 | grids use fixed columns. 39 | 40 | 41 | {[1, 2, 3, 4, 5, 6, 7].map((item) => { 42 | let xs; 43 | if (item === 1) { 44 | xs = 12; 45 | } else if (item === 2 || item === 3) { 46 | xs = 6; 47 | } else { 48 | xs = 3; 49 | } 50 | return ( 51 | 52 | 62 | {`xs=${xs}`} 63 | 64 | 65 | ); 66 | })} 67 | 68 | 69 | } 70 | /> 71 | ); 72 | } 73 | } 74 | 75 | export default GridDocs; 76 | -------------------------------------------------------------------------------- /docs/pages/List/index.js: -------------------------------------------------------------------------------- 1 | import Folder from 'material-design-icons/file/svg/design/ic_folder_24px.svg'; 2 | import Grid, {GridItem} from '../../../src/Grid'; 3 | import List, {ListItem} from '../../../src/List'; 4 | import MoreVert from 'material-design-icons/navigation/svg/production/ic_more_vert_24px.svg'; 5 | import Page from '../Page'; 6 | import React from 'react'; 7 | import Switch from '../../../src/Switch'; 8 | import SvgIcon from '../../../src/SvgIcon'; 9 | import TextField from '../../../src/TextField'; 10 | 11 | class ListDocs extends React.Component { 12 | 13 | constructor(props) { 14 | super(props); 15 | this.onControlPanel = this.onControlPanel.bind(this); 16 | this.state = { 17 | primary: 'Primary', 18 | secondary: 'Secondary', 19 | avatar: true, 20 | action: true, 21 | style: '{"width": "300px"}', 22 | arrowNavigation: false 23 | }; 24 | } 25 | 26 | onControlPanel(key, value) { 27 | this.setState({[key]: value}); 28 | } 29 | 30 | render() { 31 | const {primary, secondary, avatar, action, style, arrowNavigation} = this.state; 32 | let styleObj = {}; 33 | try { 34 | styleObj = JSON.parse(style); 35 | } catch (e) { } 36 | return ( 37 | 41 | 42 | {[1, 2, 3].map(key => ( 43 | } 48 | action={action && } 49 | /> 50 | ))} 51 | 52 | 53 | } 54 | buildYourOwnControlPanel={[ 55 | (this.onControlPanel('primary', e.target.value))} 57 | label="primary" 58 | value={primary} 59 | helperText=" " 60 | />, 61 | (this.onControlPanel('secondary', e.target.value))} 63 | label="secondary" 64 | value={secondary} 65 | helperText=" " 66 | />, 67 | (this.onControlPanel('style', e.target.value))} 69 | label="style" 70 | value={style} 71 | helperText="JSON will be converted to a style object" 72 | />, 73 | (this.onControlPanel('avatar', e.target.checked))} 75 | checked={avatar} 76 | label="avatar" 77 | />, 78 | (this.onControlPanel('action', e.target.checked))} 80 | checked={action} 81 | label="action" 82 | />, 83 | (this.onControlPanel('arrowNavigation', e.target.checked))} 85 | checked={arrowNavigation} 86 | label="arrowNavigation" 87 | /> 88 | ]} 89 | examples={ 90 | 91 | 92 | 93 | {[1, 2, 3].map(key => ( 94 | 95 | ))} 96 | 97 | 98 | 99 | 100 | {[1, 2, 3].map(key => ( 101 | } 105 | /> 106 | ))} 107 | 108 | 109 | 110 | 111 | {[1, 2, 3].map(key => ( 112 | } 115 | primary={'Primary'} 116 | secondary={'Secondary'} 117 | /> 118 | ))} 119 | 120 | 121 | 122 | 123 | {[1, 2, 3].map(key => ( 124 | } 127 | primary={'Primary'} 128 | secondary={'Secondary'} 129 | action={} 130 | /> 131 | ))} 132 | 133 | 134 | 135 | } 136 | /> 137 | ); 138 | } 139 | } 140 | 141 | export default ListDocs; 142 | -------------------------------------------------------------------------------- /docs/pages/Page.css: -------------------------------------------------------------------------------- 1 | .buildYourOwn { 2 | padding: 20px; 3 | background-color: rgba(0, 0, 0, 0.025); 4 | display: flex; 5 | justify-content: center; 6 | max-width: 100%; 7 | } 8 | 9 | @media (min-width: $screenMd) { 10 | padding: 40px 100px 40px; 11 | } 12 | 13 | .buildYourOwnCode, 14 | .buildYourOwnControlPanel { 15 | padding: 0 20px 20px; 16 | background-color: rgba(0, 0, 0, 0.025); 17 | } 18 | -------------------------------------------------------------------------------- /docs/pages/Page.js: -------------------------------------------------------------------------------- 1 | import Grid, {GridItem} from '../../src/Grid'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import Styles from './Page.css'; 5 | import Typography from '../../src/Typography'; 6 | 7 | function DocPage({ 8 | componentName, 9 | description, 10 | buildYourOwn, 11 | buildYourOwnCode, 12 | buildYourOwnControlPanel, 13 | examples 14 | }) { 15 | return ( 16 |
17 | 21 | {componentName} 22 | 23 | 24 | {description} 25 | 26 | {buildYourOwn && ( 27 | 31 | Build your own 32 | 33 | )} 34 | {buildYourOwn && ( 35 |
36 | {buildYourOwn} 37 |
38 | )} 39 | {buildYourOwnCode && ( 40 |
41 | {buildYourOwnCode} 42 |
43 | )} 44 | {buildYourOwnControlPanel && ( 45 |
46 | 47 | {/* eslint-disable react/no-array-index-key */} 48 | {buildYourOwnControlPanel.map((item, index) => { 49 | const grid = {}; 50 | if (buildYourOwnControlPanel.length >= 2) { 51 | grid.sm = 6; 52 | } 53 | if (buildYourOwnControlPanel.length >= 3) { 54 | grid.md = 4; 55 | } 56 | return ( 57 | 66 | {item} 67 | 68 | ); 69 | })} 70 | {/* eslint-enable react/no-array-index-key */} 71 | 72 |
73 | )} 74 | {examples && ( 75 | 79 | Examples 80 | 81 | )} 82 | {examples} 83 |
84 | ); 85 | } 86 | 87 | DocPage.defaultProps = { 88 | componentName: null, 89 | description: null, 90 | buildYourOwn: null, 91 | buildYourOwnCode: null, 92 | buildYourOwnControlPanel: null, 93 | examples: null 94 | }; 95 | 96 | DocPage.propTypes = { 97 | componentName: PropTypes.node, 98 | description: PropTypes.node, 99 | buildYourOwn: PropTypes.node, 100 | buildYourOwnCode: PropTypes.node, 101 | buildYourOwnControlPanel: PropTypes.arrayOf(PropTypes.node), 102 | examples: PropTypes.node 103 | }; 104 | 105 | export default DocPage; 106 | -------------------------------------------------------------------------------- /docs/pages/Paper/index.js: -------------------------------------------------------------------------------- 1 | import Grid, {GridItem} from '../../../src/Grid'; 2 | import Page from '../Page'; 3 | import Paper from '../../../src/Paper'; 4 | import React from 'react'; 5 | import TextField from '../../../src/TextField'; 6 | 7 | class PaperDocs extends React.Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | this.onControlPanel = this.onControlPanel.bind(this); 12 | this.state = { 13 | children: 'Hello World', 14 | elevation: '10', 15 | style: '{"padding": "20px", "width": "300px"}' 16 | }; 17 | } 18 | 19 | onControlPanel(key, value) { 20 | this.setState({[key]: value}); 21 | } 22 | 23 | render() { 24 | let elevation; 25 | let style; 26 | try { 27 | elevation = Number(this.state.elevation); 28 | style = JSON.parse(this.state.style); 29 | } catch (e) { 30 | style = {}; 31 | } 32 | return ( 33 | 37 | 38 | {this.state.children} 39 | 40 | 41 | } 42 | buildYourOwnControlPanel={[ 43 | (this.onControlPanel('elevation', e.target.value))} 46 | label="Elevation" 47 | value={this.state.elevation} 48 | helperText="Pick a value between 1 and 25" 49 | />, 50 | (this.onControlPanel('children', e.target.value))} 52 | label="Children" 53 | value={this.state.children} 54 | helperText="For demonstration purposes, limited to plaintext" 55 | />, 56 | (this.onControlPanel('style', e.target.value))} 58 | label="Style" 59 | value={this.state.style} 60 | helperText="JSON will be converted to a style object" 61 | /> 62 | ]} 63 | examples={ 64 | 65 | {[1, 2, 3].map((value) => { 66 | let elevationDemo; 67 | if (value === 1) { 68 | elevationDemo = 2; 69 | } else if (value === 2) { 70 | elevationDemo = 12; 71 | } else { 72 | elevationDemo = 25; 73 | } 74 | return ( 75 | 76 | 80 | Paper with elevation of {elevationDemo}. 81 | 82 | 83 | ); 84 | })} 85 | 86 | } 87 | /> 88 | ); 89 | } 90 | } 91 | 92 | export default PaperDocs; 93 | -------------------------------------------------------------------------------- /docs/pages/SelectField/index.js: -------------------------------------------------------------------------------- 1 | import Grid, {GridItem} from '../../../src/Grid'; 2 | import Page from '../Page'; 3 | import React from 'react'; 4 | import SelectField from '../../../src/SelectField'; 5 | import Switch from '../../../src/Switch'; 6 | import TextField from '../../../src/TextField'; 7 | 8 | class SelectFieldDocs extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.onControlPanel = this.onControlPanel.bind(this); 12 | this.state = { 13 | disabled: false, 14 | errorColor: '', 15 | helperText: 'Pick your favorite color', 16 | id: 'id', 17 | label: 'Color', 18 | value: '', 19 | style: '{}', 20 | }; 21 | } 22 | 23 | onControlPanel(key, value) { 24 | this.setState({[key]: value}); 25 | } 26 | 27 | render() { 28 | const {disabled, errorColor, helperText, id, label, value, style} = this.state; 29 | let styleObj = {}; 30 | try { 31 | styleObj = JSON.parse(style); 32 | } catch (e) { } 33 | 34 | const selectOptions = [ 35 | 'red', 36 | 'orange', 37 | 'yellow', 38 | 'green', 39 | 'blue', 40 | 'indigo', 41 | 'violet' 42 | ]; 43 | return ( 44 | 48 | 49 | { 58 | this.setState({value: e.target.value}) 59 | }} 60 | > 61 | 69 | ))} 70 | 71 | 72 | 73 | } 74 | buildYourOwnControlPanel={[ 75 | (this.onControlPanel('errorColor', e.target.value))} 77 | label="errorColor" 78 | helperText="If present, sets aria-invalid to true" 79 | value={errorColor} 80 | />, 81 | (this.onControlPanel('helperText', e.target.value))} 83 | label="helperText" 84 | helperText="Overflows onto a second line" 85 | value={helperText} 86 | />, 87 | (this.onControlPanel('id', e.target.value))} 89 | label="id" 90 | helperText="Binds label and select elements for a11y" 91 | value={id} 92 | />, 93 | (this.onControlPanel('label', e.target.value))} 95 | label="label" 96 | helperText="Truncated to a single line" 97 | value={label} 98 | />, 99 | (this.onControlPanel('value', e.target.value))} 101 | label="value" 102 | helperText="Value of selected option" 103 | value={value} 104 | />, 105 | (this.onControlPanel('style', e.target.value))} 107 | label="style" 108 | helperText="Applied to the select element" 109 | value={style} 110 | />, 111 | (this.onControlPanel('disabled', e.target.checked))} 113 | checked={disabled} 114 | label="disabled" 115 | /> 116 | ]} 117 | /> 118 | ); 119 | } 120 | } 121 | 122 | export default SelectFieldDocs; 123 | -------------------------------------------------------------------------------- /docs/pages/SnackBar/SnackBar.css: -------------------------------------------------------------------------------- 1 | .flex { 2 | display: flex; 3 | } 4 | -------------------------------------------------------------------------------- /docs/pages/SnackBar/index.js: -------------------------------------------------------------------------------- 1 | import Button from '../../../src/Button'; 2 | import CodeFormatter from '../../components/CodeFormatter'; 3 | import Page from '../Page'; 4 | import React from 'react'; 5 | import SnackBar, {SnackBarItem} from '../../../src/SnackBar'; 6 | import Styles from './SnackBar.css'; 7 | import TextField from '../../../src/TextField'; 8 | import Variables from '../../../src/variables'; 9 | 10 | class SnackBarDocs extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | this.addSnackBarItem = this.addSnackBarItem.bind(this); 14 | this.state = { 15 | message: 'Message sent', 16 | actionText: 'undo' 17 | }; 18 | } 19 | 20 | onControlPanel(key, value) { 21 | this.setState({[key]: value}); 22 | } 23 | 24 | addSnackBarItem({addTwo}) { 25 | const snackbaritem = ( 26 | {this.state.actionText}} 29 | /> 30 | ); 31 | this.snackbar.queue(snackbaritem); 32 | if (addTwo) { 33 | this.snackbar.queue(snackbaritem); 34 | } 35 | } 36 | 37 | render() { 38 | return ( 39 | 43 |

Compared to our other APIs, SnackBar is unique in that is uses an imperative API to schedule showing a SnackBar.

44 |

This API is necessary since only one SnackBar can be displayed at a time.

45 |

Snackbar's are managed like a first-in, first-out queue. Once a SnackBarItem is dismissed, the next scheduled one will show.

46 | 47 | } 48 | buildYourOwn={ 49 |
50 | (this.snackbar = c)} /> 51 | 58 | 65 |
66 | } 67 | buildYourOwnCode={ 68 | (this.snackbar = c)} /> 74 | 75 | this.snackbar.queue( 76 | ${this.state.actionText}} 79 | /> 80 | );` 81 | } 82 | /> 83 | } 84 | buildYourOwnControlPanel={[ 85 | (this.onControlPanel('message', e.target.value))} 87 | label="message" 88 | value={this.state.message} 89 | />, 90 | (this.onControlPanel('actionText', e.target.value))} 92 | label="actionText" 93 | value={this.state.actionText} 94 | /> 95 | ]} 96 | /> 97 | ); 98 | } 99 | } 100 | 101 | export default SnackBarDocs; 102 | -------------------------------------------------------------------------------- /docs/pages/Switch/index.js: -------------------------------------------------------------------------------- 1 | import Colors from '../../../src/variables'; 2 | import Grid, {GridItem} from '../../../src/Grid'; 3 | import Page from '../Page'; 4 | import React from 'react'; 5 | import Switch from '../../../src/Switch'; 6 | import TextField from '../../../src/TextField'; 7 | 8 | class SwitchDocs extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.onControlPanel = this.onControlPanel.bind(this); 12 | this.state = { 13 | checked: true, 14 | disabled: false, 15 | label: 'Switch Label', 16 | labelId: 'foobar', 17 | primaryColor: Colors.$primary, 18 | switch1: false, 19 | switch2: true, 20 | switch3: true, 21 | switch4: false, 22 | switch5: true 23 | }; 24 | } 25 | 26 | onControlPanel(key, value) { 27 | this.setState({[key]: value}); 28 | } 29 | 30 | render() { 31 | const {checked, disabled, label, labelId, primaryColor} = this.state; 32 | return ( 33 | 37 | (this.onControlPanel('checked', e.target.checked))} 39 | disabled={disabled} 40 | label={label} 41 | labelId={labelId} 42 | checked={checked} 43 | primaryColor={primaryColor} 44 | style={{justifyContent: 'center'}} 45 | /> 46 | 47 | } 48 | buildYourOwnControlPanel={[ 49 | (this.onControlPanel('checked', e.target.checked))} 51 | checked={checked} 52 | label="checked" 53 | />, 54 | (this.onControlPanel('disabled', e.target.checked))} 56 | checked={disabled} 57 | label="disabled" 58 | />, 59 | (this.onControlPanel('label', e.target.value))} 61 | label="label" 62 | value={label} 63 | />, 64 | (this.onControlPanel('labelId', e.target.value))} 66 | label="labelId" 67 | value={labelId} 68 | />, 69 | (this.onControlPanel('primaryColor', e.target.value))} 71 | label="primaryColor" 72 | value={primaryColor} 73 | /> 74 | ]} 75 | examples={ 76 | 77 | 78 | (this.onControlPanel('switch1', e.target.checked))} 80 | checked={this.state.switch1} 81 | label={'Switch #1'} 82 | /> 83 | 84 | 85 | (this.onControlPanel('switch2', e.target.checked))} 87 | checked={this.state.switch2} 88 | label={'Switch #2'} 89 | /> 90 | 91 | 92 | (this.onControlPanel('switch3', e.target.checked))} 94 | checked={this.state.switch3} 95 | label={'Custom Color'} 96 | primaryColor={Colors.$orange700} 97 | /> 98 | 99 | 100 | (this.onControlPanel('switch4', e.target.checked))} 102 | checked={this.state.switch4} 103 | label={'Disabled off'} 104 | disabled 105 | /> 106 | 107 | 108 | (this.onControlPanel('switch5', e.target.checked))} 110 | checked={this.state.switch5} 111 | label={'Disabled on'} 112 | disabled 113 | /> 114 | 115 | 116 | } 117 | /> 118 | ); 119 | } 120 | } 121 | 122 | export default SwitchDocs; 123 | -------------------------------------------------------------------------------- /docs/pages/Table/index.js: -------------------------------------------------------------------------------- 1 | import Grid, {GridItem} from '../../../src/Grid'; 2 | import Page from '../Page'; 3 | import Paper from '../../../src/Paper'; 4 | import React from 'react'; 5 | import Table, {TableBody, TableCell, TableHead, TableRow} from '../../../src/Table'; 6 | import Typography from '../../../src/Typography'; 7 | 8 | class TabsDocs extends React.Component { 9 | render() { 10 | return ( 11 | 15 | 16 | 17 | Simple table. 18 | 19 | 20 | 21 | 22 | 23 | Name 24 | Age 25 | Street Address 26 | ZIP Code 27 | State 28 | 29 | 30 | 31 | 32 | Johnathan Doe 33 | 25 34 | 700 1st Ave 35 | 90210 36 | CA 37 | 38 | 39 | Jane Doe 40 | 23 41 | 15 Spruce St 42 | 92101 43 | CA 44 | 45 | 46 |
47 |
48 |
49 | 50 | } 51 | /> 52 | ); 53 | } 54 | } 55 | 56 | export default TabsDocs; 57 | -------------------------------------------------------------------------------- /docs/pages/Typography/index.js: -------------------------------------------------------------------------------- 1 | import Page from '../Page'; 2 | import React from 'react'; 3 | import Scrollable from '../../../src/Scrollable'; 4 | import SelectField from '../../../src/SelectField'; 5 | import TextField from '../../../src/TextField'; 6 | import Typography from '../../../src/Typography'; 7 | 8 | class TypographyDocs extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.onControlPanel = this.onControlPanel.bind(this); 12 | this.state = { 13 | component: 'div', 14 | type: 'body1', 15 | children: 'Hello World' 16 | }; 17 | } 18 | 19 | onControlPanel(key, value) { 20 | this.setState({[key]: value}); 21 | } 22 | 23 | render() { 24 | const {component, type, children} = this.state; 25 | return ( 26 | 33 | {children} 34 | 35 | } 36 | buildYourOwnControlPanel={[ 37 | (this.onControlPanel('component', e.target.value))} 39 | label="component" 40 | value={component} 41 | />, 42 | (this.onControlPanel('type', e.target.value))} 46 | > 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | , 59 | (this.onControlPanel('children', e.target.value))} 61 | label="children" 62 | value={children} 63 | /> 64 | ]} 65 | examples={ 66 | 67 |
68 | display4 69 | display3 70 | display2 71 | display1 72 | headline 73 | title 74 | subheading 75 | body2 76 | body1 77 | caption 78 | button 79 |
80 |
81 | } 82 | /> 83 | ); 84 | } 85 | } 86 | 87 | export default TypographyDocs; 88 | -------------------------------------------------------------------------------- /docs/utils/globals.js: -------------------------------------------------------------------------------- 1 | const globals = { 2 | BASE_URL: '/material-react-components' 3 | }; 4 | 5 | export default globals; 6 | 7 | export function makeURL(path) { 8 | if (!path) { 9 | return globals.BASE_URL; 10 | } 11 | return globals.BASE_URL + path; 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "material-react-components", 3 | "version": "2.0.6", 4 | "description": "A collection of React components that implement Google's material design specification.", 5 | "homepage": "https://collegepulse.app/material-react-components", 6 | "main": "dist/index.js", 7 | "module": "es/index.js", 8 | "jsnext:main": "es/index.js", 9 | "sideEffects": false, 10 | "keywords": [ 11 | "react", 12 | "react-component", 13 | "material design", 14 | "material-ui" 15 | ], 16 | "scripts": { 17 | "build": "npm-run-all build:*", 18 | "build:commonjs": "BABEL_ENV=commonjs babel src --out-dir dist --ignore '**/*.spec.js'", 19 | "build:docs": "NODE_ENV=production webpack --config webpack.config.docs.js --env.docs", 20 | "build:module": "BABEL_ENV=module babel src --out-dir es --ignore '**/*.spec.js'", 21 | "build:ssr": "webpack --config webpack.config.ssr.js", 22 | "coverage:open": "open .coverage/lcov-report/index.html", 23 | "lint": "npm run lint:css && npm run lint:js", 24 | "lint:css": "stylelint src/**/*.css", 25 | "lint:js": "xo src/**/*.js", 26 | "start": "webpack-dev-server --config webpack.config.docs.js --env.dev --content-base docs", 27 | "test": "npm run test:unit && npm run build:ssr && npm run test:ssr", 28 | "test:ssr": "mocha .tmp/main.js", 29 | "test:unit": "karma start", 30 | "test:unit:chrome": "SOURCEMAPS=false karma start --browsers Chrome --no-single-run", 31 | "test:unit:safari": "SOURCEMAPS=false karma start --browsers Safari --no-single-run", 32 | "version": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md" 33 | }, 34 | "author": "Peter Mikitsh", 35 | "license": "MIT", 36 | "peerDependencies": { 37 | "react": "^15.5.4 || ^16.0.0", 38 | "react-dom": "^15.5.4 || ^16.0.0" 39 | }, 40 | "dependencies": { 41 | "@babel/runtime": "^7.5.5", 42 | "classnames": "2.2.6", 43 | "debounce": "1.2.0", 44 | "material-design-icons": "3.0.1", 45 | "react-event-listener": "^0.6.6", 46 | "react-transition-group": "1.2.1", 47 | "tinycolor2": "1.4.1" 48 | }, 49 | "devDependencies": { 50 | "@babel/cli": "^7.5.5", 51 | "@babel/core": "^7.5.5", 52 | "@babel/plugin-proposal-class-properties": "^7.5.5", 53 | "@babel/plugin-proposal-object-rest-spread": "7.5.5", 54 | "@babel/plugin-transform-runtime": "^7.5.5", 55 | "@babel/preset-env": "^7.5.5", 56 | "@babel/preset-react": "7.0.0", 57 | "autoprefixer": "9.6.1", 58 | "babel-core": "7.0.0-bridge.0", 59 | "babel-eslint": "9.0.0", 60 | "babel-loader": "8.0.6", 61 | "babel-plugin-css-modules-transform": "1.6.2", 62 | "babel-plugin-istanbul": "5.2.0", 63 | "babel-plugin-transform-define": "1.3.1", 64 | "babel-plugin-transform-react-remove-prop-types": "0.4.18", 65 | "babel-polyfill": "6.26.0", 66 | "case-sensitive-paths-webpack-plugin": "2.2.0", 67 | "conventional-changelog": "3.1.10", 68 | "core-js": "2.5.7", 69 | "css-loader": "1.0.0", 70 | "enzyme": "3.6.0", 71 | "enzyme-adapter-react-16": "1.5.0", 72 | "eslint-config-xo": "0.25.0", 73 | "eslint-config-xo-react": "0.17.0", 74 | "eslint-plugin-react": "7.14.3", 75 | "html-webpack-plugin": "3.2.0", 76 | "isomorphic-style-loader": "4.0.0", 77 | "karma": "3.0.0", 78 | "karma-browserstack-launcher": "1.5.1", 79 | "karma-chrome-launcher": "2.2.0", 80 | "karma-coverage": "1.1.2", 81 | "karma-coveralls": "2.1.0", 82 | "karma-mocha": "1.3.0", 83 | "karma-mocha-reporter": "2.2.5", 84 | "karma-safari-launcher": "1.0.0", 85 | "karma-sourcemap-loader": "0.3.7", 86 | "karma-webpack": "3.0.5", 87 | "keycode": "2.2.0", 88 | "lodash": "4.17.15", 89 | "mocha": "5.2.0", 90 | "npm-run-all": "4.1.5", 91 | "postcss-cli": "6.1.3", 92 | "postcss-loader": "3.0.0", 93 | "postcss-simple-vars": "5.0.2", 94 | "prismjs": "1.17.1", 95 | "prop-types": "15.7.2", 96 | "raw-loader": "0.5.1", 97 | "react": "16.5.2", 98 | "react-dom": "16.5.2", 99 | "react-hot-loader": "4.12.12", 100 | "react-router-dom": "4.3.1", 101 | "react-svg-loader": "2.1.0", 102 | "react-test-renderer": "16.9.0", 103 | "sinon": "6.3.4", 104 | "style-loader": "0.23.0", 105 | "stylelint": "9.5.0", 106 | "stylelint-config-standard": "18.3.0", 107 | "webpack": "4.39.3", 108 | "webpack-cli": "3.3.7", 109 | "webpack-dev-server": "3.8.0", 110 | "webpack-node-externals": "1.7.2", 111 | "xo": "0.24.0" 112 | }, 113 | "xo": { 114 | "envs": [ 115 | "browser", 116 | "mocha" 117 | ], 118 | "extends": [ 119 | "xo", 120 | "xo-react/space" 121 | ], 122 | "globals": [ 123 | "__TEST__" 124 | ], 125 | "parser": "babel-eslint", 126 | "rules": { 127 | "react/jsx-sort-props": false, 128 | "react/no-deprecated": false, 129 | "react/no-find-dom-node": false, 130 | "unicorn/filename-case": 0, 131 | "unicorn/prefer-node-append": 0, 132 | "unicorn/prefer-query-selector": 0 133 | }, 134 | "settings": { 135 | "react": { 136 | "version": "16.0" 137 | } 138 | }, 139 | "space": true 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const cssvariables = require('./src/variables'); 2 | 3 | module.exports = () => ({ 4 | plugins: [ 5 | require('postcss-simple-vars')({ 6 | variables: () => ( 7 | cssvariables 8 | ) 9 | }), 10 | require('autoprefixer')() 11 | ] 12 | }); 13 | -------------------------------------------------------------------------------- /src/AppBar/AppBar.css: -------------------------------------------------------------------------------- 1 | .root { 2 | background-color: $primary; 3 | height: 64px; 4 | max-height: 64px; 5 | overflow: hidden; 6 | line-height: 64px; 7 | box-sizing: border-box; 8 | width: 100%; 9 | display: flex; 10 | } 11 | 12 | .header { 13 | color: #fff; 14 | line-height: 64px; 15 | flex: 0 1 auto; 16 | overflow: hidden; 17 | white-space: nowrap; 18 | text-overflow: ellipsis; 19 | } 20 | 21 | .header.headerColor { 22 | color: inherit; 23 | } 24 | 25 | .primary, 26 | .secondary { 27 | height: 64px; 28 | display: flex; 29 | align-items: center; 30 | } 31 | 32 | .primary, 33 | .headerNoPrimary { 34 | padding-left: 16px; 35 | } 36 | 37 | .secondary { 38 | margin-left: auto; 39 | padding-right: 16px; 40 | } 41 | 42 | @media (min-width: 600px) { 43 | .headerNoPrimary { 44 | padding-right: 16px; 45 | } 46 | 47 | .secondary { 48 | padding-left: 16px; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/AppBar/AppBar.js: -------------------------------------------------------------------------------- 1 | import makeClass from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import Paper from '../Paper'; 5 | import Typography from '../Typography'; 6 | import Variables from '../variables'; 7 | import Styles from './AppBar.css'; 8 | 9 | function AppBar({ 10 | backgroundColor, 11 | className, 12 | elevation, 13 | children, 14 | primary, 15 | secondary, 16 | ...other 17 | }) { 18 | return ( 19 | 25 | {primary && ( 26 | 27 | {primary} 28 | 29 | )} 30 | 41 | {children} 42 | 43 | {secondary && ( 44 | 45 | {secondary} 46 | 47 | )} 48 | 49 | ); 50 | } 51 | 52 | AppBar.defaultProps = { 53 | backgroundColor: Variables.$primary, 54 | className: '', 55 | children: null, 56 | elevation: 4, 57 | primary: null, 58 | secondary: null 59 | }; 60 | 61 | AppBar.propTypes = { 62 | backgroundColor: PropTypes.string, 63 | className: PropTypes.string, 64 | children: PropTypes.node, 65 | elevation: PropTypes.number, 66 | primary: PropTypes.node, 67 | secondary: PropTypes.node 68 | }; 69 | 70 | export default AppBar; 71 | -------------------------------------------------------------------------------- /src/AppBar/AppBar.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import React from 'react'; 5 | import {createShallow, createMount, createTest} from '../../test/utils'; 6 | import AppBar from './AppBar'; 7 | 8 | describe('Appbar', () => { 9 | let shallow; 10 | let mount; 11 | 12 | beforeEach(() => { 13 | shallow = createShallow(); 14 | mount = createMount(); 15 | }); 16 | 17 | afterEach(() => { 18 | shallow.cleanUp(); 19 | mount.cleanUp(); 20 | }); 21 | 22 | it('should shallow render', () => { 23 | const wrapper = shallow(); 24 | assert(wrapper); 25 | }); 26 | 27 | it('should deep render primary', createTest(() => { 28 | const wrapper = mount(); 29 | assert(wrapper); 30 | })); 31 | 32 | it('should deep render children', createTest(() => { 33 | const wrapper = mount(Header); 34 | assert(wrapper); 35 | })); 36 | }); 37 | -------------------------------------------------------------------------------- /src/AppBar/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './AppBar'; 2 | -------------------------------------------------------------------------------- /src/BottomNavigation/BottomNavigation.css: -------------------------------------------------------------------------------- 1 | .root { 2 | height: 56px; 3 | display: flex; 4 | background-color: #fff; 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /src/BottomNavigation/BottomNavigation.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import Styles from './BottomNavigation.css'; 4 | 5 | function BottomNavigation({children, ...other}) { 6 | return ( 7 |
8 | {children} 9 |
10 | ); 11 | } 12 | 13 | BottomNavigation.defaultProps = { 14 | children: null 15 | }; 16 | 17 | BottomNavigation.propTypes = { 18 | children: PropTypes.node 19 | }; 20 | 21 | export default BottomNavigation; 22 | -------------------------------------------------------------------------------- /src/BottomNavigation/BottomNavigation.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import React from 'react'; 5 | import {createShallow} from '../../test/utils'; 6 | import BottomNavigation from './BottomNavigation'; 7 | 8 | describe('BottomNavigation', () => { 9 | let shallow; 10 | 11 | beforeEach(() => { 12 | shallow = createShallow(); 13 | }); 14 | 15 | afterEach(() => { 16 | shallow.cleanUp(); 17 | }); 18 | 19 | it('should shallow render', () => { 20 | const wrapper = shallow(); 21 | assert(wrapper); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/BottomNavigation/BottomNavigationItem.css: -------------------------------------------------------------------------------- 1 | .root { 2 | flex: 1; 3 | border-radius: 0; 4 | justify-content: center; 5 | margin: 0; 6 | } 7 | -------------------------------------------------------------------------------- /src/BottomNavigation/BottomNavigationItem.js: -------------------------------------------------------------------------------- 1 | import makeClass from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import Button from '../Button'; 5 | import Styles from './BottomNavigationItem.css'; 6 | 7 | function BottomNavigationItem({children, className, ...other}) { 8 | return ( 9 | 12 | ); 13 | } 14 | 15 | BottomNavigationItem.defaultProps = { 16 | children: null, 17 | className: null 18 | }; 19 | 20 | BottomNavigationItem.propTypes = { 21 | children: PropTypes.node, 22 | className: PropTypes.string 23 | }; 24 | 25 | export default BottomNavigationItem; 26 | -------------------------------------------------------------------------------- /src/BottomNavigation/BottomNavigationItem.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import React from 'react'; 5 | import {createShallow} from '../../test/utils'; 6 | import BottomNavigationItem from './BottomNavigationItem'; 7 | 8 | describe('BottomNavigationItem', () => { 9 | let shallow; 10 | 11 | beforeEach(() => { 12 | shallow = createShallow(); 13 | }); 14 | 15 | afterEach(() => { 16 | shallow.cleanUp(); 17 | }); 18 | 19 | it('should shallow render', () => { 20 | const wrapper = shallow(); 21 | assert(wrapper); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/BottomNavigation/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './BottomNavigation'; 2 | export {default as BottomNavigationItem} from './BottomNavigationItem'; 3 | -------------------------------------------------------------------------------- /src/Button/Button.css: -------------------------------------------------------------------------------- 1 | .root { 2 | align-items: center; 3 | position: relative; 4 | display: inline-flex; 5 | cursor: pointer; 6 | min-height: 36px; 7 | min-width: 88px; 8 | padding: 0 16px; 9 | justify-content: center; 10 | transition: all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms; 11 | border: 0; 12 | background: none; 13 | outline: none; 14 | text-transform: uppercase; 15 | font-family: inherit; 16 | font-weight: 500; 17 | font-size: 14px; 18 | margin: 8px; 19 | color: inherit; 20 | border-radius: 2px; 21 | user-select: none; 22 | text-decoration: none; 23 | box-sizing: border-box; 24 | -webkit-tap-highlight-color: transparent; 25 | } 26 | 27 | .root.hasBackground { 28 | box-shadow: $elevation2; 29 | } 30 | 31 | .root.hasBackground:active { 32 | box-shadow: $elevation5; 33 | } 34 | 35 | .root::-moz-focus-inner { 36 | border: 0; 37 | } 38 | 39 | .fab { 40 | height: 56px; 41 | width: 56px; 42 | min-height: 56px; 43 | min-width: 56px; 44 | border-radius: 50%; 45 | padding: 0; 46 | box-shadow: $elevation5; 47 | display: inline-flex; 48 | align-items: center; 49 | justify-content: center; 50 | } 51 | 52 | .fab:active { 53 | box-shadow: $elevation10; 54 | } 55 | 56 | .label { 57 | align-items: inherit; 58 | display: inherit; 59 | pointer-events: none; 60 | height: 100%; 61 | overflow: hidden; 62 | text-overflow: ellipsis; 63 | white-space: nowrap; 64 | width: 100%; 65 | justify-content: inherit; 66 | } 67 | 68 | .fab > .label { 69 | justify-content: center; 70 | width: 100%; 71 | } 72 | 73 | .isNotFab { 74 | padding-top: 11px; 75 | padding-bottom: 11px; 76 | } 77 | 78 | .isIcon { 79 | border-radius: 50%; 80 | } 81 | 82 | .lightText { 83 | color: $white; 84 | } 85 | 86 | .darkText { 87 | color: $black87; 88 | } 89 | 90 | -------------------------------------------------------------------------------- /src/Button/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './Button'; 2 | -------------------------------------------------------------------------------- /src/Collapse/Collapse.css: -------------------------------------------------------------------------------- 1 | .root { 2 | overflow: hidden; 3 | transition: height 350ms ease; 4 | } 5 | -------------------------------------------------------------------------------- /src/Collapse/Collapse.js: -------------------------------------------------------------------------------- 1 | import makeClass from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import Styles from './Collapse.css'; 5 | 6 | class Collapse extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | height: props.open ? null : 0 11 | }; 12 | } 13 | 14 | componentDidMount() { 15 | if (this.props.open) { 16 | this.setState({ 17 | height: `${this.rootInner.offsetHeight}px` 18 | }); 19 | } 20 | } 21 | 22 | componentWillReceiveProps({open}) { 23 | if (!this.props.open && open) { 24 | this.setState({ 25 | height: `${this.rootInner.offsetHeight}px` 26 | }); 27 | } else if (this.props.open && !open) { 28 | this.setState({ 29 | height: '0' 30 | }); 31 | } 32 | } 33 | 34 | registerRoot = c => { 35 | this.root = c; 36 | } 37 | 38 | registerInner = c => { 39 | this.rootInner = c; 40 | } 41 | 42 | render() { 43 | const { 44 | open, children, className, ...other 45 | } = this.props; 46 | return ( 47 |
56 |
59 | {children} 60 |
61 |
62 | ); 63 | } 64 | } 65 | 66 | Collapse.defaultProps = { 67 | children: null, 68 | className: null, 69 | open: false 70 | }; 71 | 72 | Collapse.propTypes = { 73 | children: PropTypes.node, 74 | className: PropTypes.string, 75 | open: PropTypes.bool 76 | }; 77 | 78 | export default Collapse; 79 | -------------------------------------------------------------------------------- /src/Collapse/Collapse.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import React from 'react'; 5 | import {createShallow, createMount} from '../../test/utils'; 6 | import variables from '../variables'; 7 | import Collapse from './Collapse'; 8 | 9 | const longDiv = ( 10 |
11 | Foo 12 | {' '} 13 |
14 | Bar 15 | {' '} 16 |
17 | Foo 18 | {' '} 19 |
20 | Bar 21 | {' '} 22 |
23 | Foo 24 | {' '} 25 |
26 | Bar 27 | {' '} 28 |
29 | Foo 30 | {' '} 31 |
32 | Bar 33 | {' '} 34 |
35 |
36 | ); 37 | 38 | describe('Collapse', () => { 39 | let shallow; 40 | let mount; 41 | 42 | beforeEach(() => { 43 | shallow = createShallow(); 44 | mount = createMount(); 45 | }); 46 | 47 | afterEach(() => { 48 | shallow.cleanUp(); 49 | mount.cleanUp(); 50 | }); 51 | 52 | it('should shallow render', () => { 53 | const wrapper = shallow(); 54 | assert(wrapper); 55 | }); 56 | 57 | it('should deep render', () => { 58 | const wrapper = mount(); 59 | assert(wrapper); 60 | }); 61 | 62 | it('should not animate on initial render if open is set to true', done => { 63 | const wrapper = mount({longDiv}); 64 | const initialHeight = wrapper.getDOMNode().style.height; 65 | assert(parseInt(initialHeight, 10) > '0'); 66 | setTimeout(() => { 67 | const heightSometimeLater = wrapper.getDOMNode().style.height; 68 | assert(initialHeight === heightSometimeLater); 69 | done(); 70 | }, 1000); 71 | }); 72 | 73 | it('should animate when open changes from false to true', done => { 74 | const wrapper = mount({longDiv}); 75 | const initialHeight = wrapper.getDOMNode().style.height; 76 | wrapper.setProps({open: true}); 77 | setTimeout(() => { 78 | const heightSometimeLater = wrapper.getDOMNode().style.height; 79 | assert(parseInt(initialHeight, 10) < parseInt(heightSometimeLater, 10)); 80 | done(); 81 | }, 1000); 82 | }); 83 | 84 | it('should animate when open changes from true to false', done => { 85 | const wrapper = mount({longDiv}); 86 | const initialHeight = wrapper.getDOMNode().style.height; 87 | setTimeout(() => { 88 | wrapper.setProps({open: false}); 89 | }, 100); 90 | setTimeout(() => { 91 | const heightSometimeLater = wrapper.getDOMNode().style.height; 92 | assert(parseInt(initialHeight, 10) > parseInt(heightSometimeLater, 10)); 93 | done(); 94 | }, 1000); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /src/Collapse/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './Collapse'; 2 | -------------------------------------------------------------------------------- /src/Dialog/Dialog.css: -------------------------------------------------------------------------------- 1 | .root { 2 | animation-duration: 225ms; 3 | animation-timing-function: $rippleEaseOutFunction; 4 | animation-fill-mode: forwards; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | position: fixed; 9 | top: 0; 10 | bottom: 0; 11 | left: 0; 12 | right: 0; 13 | z-index: 2500; 14 | pointer-events: none; 15 | opacity: 0; 16 | animation-name: fadeout; 17 | } 18 | 19 | .root.open { 20 | pointer-events: auto; 21 | animation-name: fadein; 22 | } 23 | 24 | .backdrop { 25 | justify-content: center; 26 | position: fixed; 27 | height: 100%; 28 | width: 100%; 29 | top: 0; 30 | left: 0; 31 | right: 0; 32 | bottom: 0; 33 | background-color: rgba(0, 0, 0, 0.52); 34 | -webkit-tap-highlight-color: transparent; 35 | } 36 | 37 | .dialog { 38 | width: 75%; 39 | max-width: 600px; 40 | max-height: 90vh; 41 | pointer-events: auto; 42 | z-index: 2500; 43 | position: relative; 44 | } 45 | 46 | .title { 47 | font-size: 21px; 48 | font-weight: 500; 49 | line-height: 1; 50 | padding: 24px; 51 | color: rgba(0, 0, 0, 0.87); 52 | } 53 | 54 | .description { 55 | font-size: 16px; 56 | line-height: 24px; 57 | color: rgba(0, 0, 0, 0.54); 58 | padding: 0 24px 24px; 59 | -webkit-font-smoothing: antialiased; 60 | } 61 | 62 | .actions { 63 | display: flex; 64 | justify-content: flex-end; 65 | margin: 8px 4px; 66 | } 67 | 68 | @keyframes fadein { 69 | 0% { opacity: 0; } 70 | 100% { opacity: 1; } 71 | } 72 | 73 | @keyframes fadeout { 74 | 0% { opacity: 1; } 75 | 100% { opacity: 0; } 76 | } 77 | -------------------------------------------------------------------------------- /src/Dialog/Dialog.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import TransitionGroup from 'react-transition-group/TransitionGroup'; 4 | import DialogInner from './DialogInner'; 5 | 6 | function FirstChild(props) { 7 | const childrenArray = React.Children.toArray(props.children); 8 | return childrenArray[0] || null; 9 | } 10 | 11 | class Dialog extends React.Component { 12 | register = c => { 13 | this.inner = c; 14 | } 15 | 16 | render() { 17 | return ( 18 | 19 | {this.props.open && } 20 | 21 | ); 22 | } 23 | } 24 | 25 | Dialog.defaultProps = { 26 | open: false 27 | }; 28 | 29 | Dialog.propTypes = { 30 | open: PropTypes.bool 31 | }; 32 | 33 | export default Dialog; 34 | -------------------------------------------------------------------------------- /src/Dialog/Dialog.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import keycode from 'keycode'; 5 | import React from 'react'; 6 | import {createShallow, createMount, createTest} from '../../test/utils'; 7 | import Button from '../Button'; 8 | import Dialog from './Dialog'; 9 | import Styles from './Dialog.css'; 10 | 11 | describe('Dialog', () => { 12 | let shallow; 13 | let mount; 14 | 15 | beforeEach(() => { 16 | shallow = createShallow(); 17 | mount = createMount(); 18 | }); 19 | 20 | afterEach(() => { 21 | shallow.cleanUp(); 22 | mount.cleanUp(); 23 | }); 24 | 25 | it('should shallow render', () => { 26 | const wrapper = shallow(); 27 | assert(wrapper); 28 | }); 29 | 30 | it('should mount', () => { 31 | const wrapper = mount(); 32 | assert(wrapper); 33 | }); 34 | 35 | it('should mount in an open state', () => { 36 | const wrapper = mount(); 37 | assert(wrapper); 38 | }); 39 | 40 | it('should mount in an open state, then close', createTest(() => { 41 | const wrapper = mount(); 42 | wrapper.setProps({open: false}); 43 | setTimeout(() => { 44 | assert(wrapper); 45 | }, 500); 46 | })); 47 | 48 | it('should mount in an open state, then close', createTest(() => { 49 | const wrapper = mount( { 54 | wrapper.setProps({open: false}); 55 | }} 56 | actions={[ 57 | , 58 | 59 | ]} 60 | />); 61 | assert(wrapper.find(`.${Styles.root}`).length === 1); 62 | setTimeout(() => { 63 | wrapper.find(`.${Styles.root}`).simulate('keydown', {keyCode: keycode('esc')}); 64 | }, 500); 65 | setTimeout(() => { 66 | assert(wrapper.html() === null); 67 | }, 1000); 68 | }, 2000)); 69 | 70 | it('should focus the first action on tab key press when last button has current focus', createTest(() => { 71 | const wrapper = mount( { 76 | wrapper.setProps({open: false}); 77 | }} 78 | actions={[ 79 | , 80 | 81 | ]} 82 | />); 83 | wrapper.find('button').last().instance().focus(); 84 | 85 | setTimeout(() => { 86 | wrapper.find(`.${Styles.root}`).simulate('keydown', {keyCode: keycode('tab')}); 87 | }, 500); 88 | 89 | setTimeout(() => { 90 | assert(document.activeElement === wrapper.find('button').first().instance()); 91 | }, 750); 92 | })); 93 | 94 | it('should focus the last action on shift+tab key press when first action has current focus', createTest(() => { 95 | const wrapper = mount(Foo, 101 | 102 | ]} 103 | />); 104 | wrapper.find('button').first().instance().focus(); 105 | 106 | setTimeout(() => { 107 | wrapper.find(`.${Styles.root}`).simulate('keydown', {keyCode: keycode('tab'), shiftKey: true}); 108 | }, 500); 109 | 110 | setTimeout(() => { 111 | assert(document.activeElement === wrapper.find('button').last().instance()); 112 | }, 750); 113 | })); 114 | }); 115 | -------------------------------------------------------------------------------- /src/Dialog/DialogInner.js: -------------------------------------------------------------------------------- 1 | import makeClass from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import Paper from '../Paper'; 5 | import Styles from './Dialog.css'; 6 | 7 | function ontouchmove() { 8 | return true; 9 | } 10 | 11 | class DialogInner extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | this.onBackwardTab = this.onBackwardTab.bind(this); 15 | this.onForwardTab = this.onForwardTab.bind(this); 16 | this.onKeyDown = this.onKeyDown.bind(this); 17 | } 18 | 19 | componentDidMount() { 20 | document.body.style.overflow = 'hidden'; 21 | window.addEventListener('keydown', this.onKeyDown); 22 | this.lastFocusBeforeDialog = document.activeElement; 23 | this.root.focus(); 24 | 25 | this.lastOnTouchMove = document.ontouchmove || ontouchmove; 26 | document.ontouchmove = e => { 27 | e.preventDefault(); 28 | }; 29 | } 30 | 31 | componentWillUnmount() { 32 | document.ontouchmove = this.lastOnTouchMove; 33 | } 34 | 35 | onKeyDown(e) { 36 | if (this.props.open) { 37 | const {keyCode} = e; 38 | const isTab = (keyCode === 9); 39 | const isEsc = (keyCode === 27); 40 | 41 | if (isTab) { 42 | if (this.actions.children.length <= 1) { 43 | e.preventDefault(); 44 | } else if (e.shiftKey) { 45 | this.onBackwardTab(e); 46 | } else { 47 | this.onForwardTab(e); 48 | } 49 | } 50 | if (isEsc) { 51 | this.props.onClose(); 52 | } 53 | } 54 | } 55 | 56 | onForwardTab(e) { 57 | if (document.activeElement === this.actions.lastChild) { 58 | e.preventDefault(); 59 | this.actions.firstChild.focus(); 60 | } 61 | } 62 | 63 | onBackwardTab(e) { 64 | if (document.activeElement === this.actions.firstChild) { 65 | e.preventDefault(); 66 | this.actions.lastChild.focus(); 67 | } 68 | } 69 | 70 | componentWillLeave(callback) { 71 | this.root.style.animationName = Styles.fadeout; 72 | window.removeEventListener('keydown', this.onKeyDown); 73 | if (this.lastFocusBeforeDialog.focus) { 74 | this.lastFocusBeforeDialog.focus(); 75 | } 76 | setTimeout(() => { 77 | this.root.style.animationName = ''; 78 | document.body.style.overflow = ''; 79 | callback(); 80 | }, 350); 81 | } 82 | 83 | registerRoot = c => { 84 | this.root = c; 85 | } 86 | 87 | registerActions = c => { 88 | this.actions = c; 89 | } 90 | 91 | render() { 92 | const { 93 | actions, className, description, onClose, open, title, ...other 94 | } = this.props; 95 | return ( 96 |
104 |
108 | 112 |
113 | {title} 114 |
115 |
116 | {description} 117 |
118 |
122 | {actions} 123 |
124 |
125 |
126 | ); 127 | } 128 | } 129 | 130 | DialogInner.defaultProps = { 131 | actions: [], 132 | className: null, 133 | description: null, 134 | onClose: () => {}, 135 | open: false, 136 | title: null 137 | }; 138 | 139 | DialogInner.propTypes = { 140 | actions: PropTypes.arrayOf(PropTypes.node), 141 | className: PropTypes.string, 142 | description: PropTypes.node, 143 | onClose: PropTypes.func, 144 | open: PropTypes.bool, 145 | title: PropTypes.node 146 | }; 147 | 148 | export default DialogInner; 149 | -------------------------------------------------------------------------------- /src/Dialog/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './Dialog'; 2 | -------------------------------------------------------------------------------- /src/Grid/Grid.css: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 100%; 3 | } 4 | 5 | .margin-0 { 6 | margin: 0; 7 | } 8 | 9 | .margin-8 { 10 | margin: auto 8px; 11 | max-width: calc(100% - 16px); 12 | } 13 | 14 | .margin-16 { 15 | margin: auto 16px; 16 | max-width: calc(100% - 32px); 17 | } 18 | 19 | .margin-24 { 20 | margin: auto 24px; 21 | max-width: calc(100% - 48px); 22 | } 23 | 24 | .margin-40 { 25 | margin: auto 40px; 26 | max-width: calc(100% - 80px); 27 | } 28 | 29 | .gutter { 30 | display: flex; 31 | flex-wrap: wrap; 32 | } 33 | 34 | .gutter-8 { 35 | width: calc(100% + 8px); 36 | margin: auto -4px; 37 | } 38 | 39 | .gutter-8 > .gutterChild { 40 | padding: 4px; 41 | } 42 | 43 | .gutter-16 { 44 | width: calc(100% + 16px); 45 | margin: auto -8px; 46 | } 47 | 48 | .gutter-16 > .gutterChild { 49 | padding: 8px; 50 | } 51 | 52 | .gutter-24 { 53 | width: calc(100% + 24px); 54 | margin: auto -12px; 55 | } 56 | 57 | .gutter-24 > .gutterChild { 58 | padding: 12px; 59 | } 60 | 61 | .gutter-40 { 62 | width: calc(100% + 40px); 63 | margin: auto -20px; 64 | } 65 | 66 | .gutter-40 > .gutterChild { 67 | padding: 20px; 68 | } 69 | -------------------------------------------------------------------------------- /src/Grid/Grid.js: -------------------------------------------------------------------------------- 1 | import makeClass from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import Styles from './Grid.css'; 5 | 6 | class Grid extends React.Component { 7 | render() { 8 | const { 9 | children, className, margin, gutter, ...other 10 | } = this.props; 11 | 12 | const rootClasses = makeClass( 13 | Styles.root, 14 | Styles[`margin-${margin}`], 15 | className 16 | ); 17 | 18 | const gutterClasses = makeClass( 19 | Styles.gutter, 20 | Styles[`gutter-${gutter}`] 21 | ); 22 | 23 | return ( 24 |
25 |
26 | {children} 27 |
28 |
29 | ); 30 | } 31 | } 32 | 33 | Grid.defaultProps = { 34 | children: null, 35 | className: null, 36 | gutter: 0, 37 | margin: 0 38 | }; 39 | 40 | Grid.propTypes = { 41 | children: PropTypes.node, 42 | className: PropTypes.string, 43 | gutter: PropTypes.oneOf([0, 8, 16, 24, 40]), 44 | margin: PropTypes.oneOf([0, 8, 16, 24, 40]) 45 | }; 46 | 47 | export default Grid; 48 | -------------------------------------------------------------------------------- /src/Grid/Grid.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import React from 'react'; 5 | import {createMount, createTest} from '../../test/utils'; 6 | import variables from '../variables'; 7 | import Grid from './Grid'; 8 | import GridItem from './GridItem'; 9 | 10 | describe('Grid', () => { 11 | let mount; 12 | 13 | beforeEach(() => { 14 | mount = createMount(); 15 | }); 16 | 17 | afterEach(() => { 18 | mount.cleanUp(); 19 | }); 20 | 21 | it('should render a Grid', createTest(() => { 22 | const component = ( 23 | 24 | {[1, 2, 3, 4, 5, 6, 7, 8].map(item => ( 25 | 26 |
36 | xs=12, sm=6, md=4, lg=3 37 |
38 |
39 | ))} 40 |
41 | ); 42 | const wrapper = mount(component); 43 | assert(wrapper); 44 | })); 45 | }); 46 | -------------------------------------------------------------------------------- /src/Grid/GridItem.css: -------------------------------------------------------------------------------- 1 | .root { 2 | box-sizing: border-box; 3 | } 4 | 5 | .xs-1 { 6 | flex-basis: 8.33333%; 7 | max-width: 8.33333%; 8 | } 9 | 10 | .xs-2 { 11 | flex-basis: 16.66666%; 12 | max-width: 16.66666%; 13 | } 14 | 15 | .xs-3 { 16 | flex-basis: 25%; 17 | max-width: 25%; 18 | } 19 | 20 | .xs-4 { 21 | flex-basis: 33.33333%; 22 | max-width: 33.33333%; 23 | } 24 | 25 | .xs-5 { 26 | flex-basis: 41.66666%; 27 | max-width: 41.66666%; 28 | } 29 | 30 | .xs-6 { 31 | flex-basis: 50%; 32 | max-width: 50%; 33 | } 34 | 35 | .xs-7 { 36 | flex-basis: 58.33333%; 37 | max-width: 58.33333%; 38 | } 39 | 40 | .xs-8 { 41 | flex-basis: 66.66666%; 42 | max-width: 66.66666%; 43 | } 44 | 45 | .xs-9 { 46 | flex-basis: 75%; 47 | max-width: 75%; 48 | } 49 | 50 | .xs-10 { 51 | flex-basis: 83.33333%; 52 | max-width: 83.33333%; 53 | } 54 | 55 | .xs-11 { 56 | flex-basis: 91.66666%; 57 | max-width: 91.66666%; 58 | } 59 | 60 | .xs-12 { 61 | flex-basis: 100%; 62 | max-width: 100%; 63 | } 64 | 65 | @media (min-width: $screenSm) { 66 | .sm-1 { 67 | flex-basis: 8.33333%; 68 | max-width: 8.33333%; 69 | } 70 | 71 | .sm-2 { 72 | flex-basis: 16.66666%; 73 | max-width: 16.66666%; 74 | } 75 | 76 | .sm-3 { 77 | flex-basis: 25%; 78 | max-width: 25%; 79 | } 80 | 81 | .sm-4 { 82 | flex-basis: 33.33333%; 83 | max-width: 33.33333%; 84 | } 85 | 86 | .sm-5 { 87 | flex-basis: 41.66666%; 88 | max-width: 41.66666%; 89 | } 90 | 91 | .sm-6 { 92 | flex-basis: 50%; 93 | max-width: 50%; 94 | } 95 | 96 | .sm-7 { 97 | flex-basis: 58.33333%; 98 | max-width: 58.33333%; 99 | } 100 | 101 | .sm-8 { 102 | flex-basis: 66.66666%; 103 | max-width: 66.66666%; 104 | } 105 | 106 | .sm-9 { 107 | flex-basis: 75%; 108 | max-width: 75%; 109 | } 110 | 111 | .sm-10 { 112 | flex-basis: 83.33333%; 113 | max-width: 83.33333%; 114 | } 115 | 116 | .sm-11 { 117 | flex-basis: 91.66666%; 118 | max-width: 91.66666%; 119 | } 120 | 121 | .sm-12 { 122 | flex-basis: 100%; 123 | max-width: 100%; 124 | } 125 | } 126 | 127 | @media (min-width: $screenMd) { 128 | .md-1 { 129 | flex-basis: 8.33333%; 130 | max-width: 8.33333%; 131 | } 132 | 133 | .md-2 { 134 | flex-basis: 16.66666%; 135 | max-width: 16.66666%; 136 | } 137 | 138 | .md-3 { 139 | flex-basis: 25%; 140 | max-width: 25%; 141 | } 142 | 143 | .md-4 { 144 | flex-basis: 33.33333%; 145 | max-width: 33.33333%; 146 | } 147 | 148 | .md-5 { 149 | flex-basis: 41.66666%; 150 | max-width: 41.66666%; 151 | } 152 | 153 | .md-6 { 154 | flex-basis: 50%; 155 | max-width: 50%; 156 | } 157 | 158 | .md-7 { 159 | flex-basis: 58.33333%; 160 | max-width: 58.33333%; 161 | } 162 | 163 | .md-8 { 164 | flex-basis: 66.66666%; 165 | max-width: 66.66666%; 166 | } 167 | 168 | .md-9 { 169 | flex-basis: 75%; 170 | max-width: 75%; 171 | } 172 | 173 | .md-10 { 174 | flex-basis: 83.33333%; 175 | max-width: 83.33333%; 176 | } 177 | 178 | .md-11 { 179 | flex-basis: 91.66666%; 180 | max-width: 91.66666%; 181 | } 182 | 183 | .md-12 { 184 | flex-basis: 100%; 185 | max-width: 100%; 186 | } 187 | } 188 | 189 | @media (min-width: $screenLg) { 190 | .lg-1 { 191 | flex-basis: 8.33333%; 192 | max-width: 8.33333%; 193 | } 194 | 195 | .lg-2 { 196 | flex-basis: 16.66666%; 197 | max-width: 16.66666%; 198 | } 199 | 200 | .lg-3 { 201 | flex-basis: 25%; 202 | max-width: 25%; 203 | } 204 | 205 | .lg-4 { 206 | flex-basis: 33.33333%; 207 | max-width: 33.33333%; 208 | } 209 | 210 | .lg-5 { 211 | flex-basis: 41.66666%; 212 | max-width: 41.66666%; 213 | } 214 | 215 | .lg-6 { 216 | flex-basis: 50%; 217 | max-width: 50%; 218 | } 219 | 220 | .lg-7 { 221 | flex-basis: 58.33333%; 222 | max-width: 58.33333%; 223 | } 224 | 225 | .lg-8 { 226 | flex-basis: 66.66666%; 227 | max-width: 66.66666%; 228 | } 229 | 230 | .lg-9 { 231 | flex-basis: 75%; 232 | max-width: 75%; 233 | } 234 | 235 | .lg-10 { 236 | flex-basis: 83.33333%; 237 | max-width: 83.33333%; 238 | } 239 | 240 | .lg-11 { 241 | flex-basis: 91.66666%; 242 | max-width: 91.66666%; 243 | } 244 | 245 | .lg-12 { 246 | flex-basis: 100%; 247 | max-width: 100%; 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/Grid/GridItem.js: -------------------------------------------------------------------------------- 1 | import makeClass from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import GridStyles from './Grid.css'; 5 | import Styles from './GridItem.css'; 6 | 7 | class GridItem extends React.Component { 8 | render() { 9 | const { 10 | children, className, xs, sm, md, lg, ...other 11 | } = this.props; 12 | const classes = makeClass( 13 | Styles.root, 14 | GridStyles.gutterChild, 15 | Styles[`xs-${xs}`], 16 | Styles[`sm-${sm}`], 17 | Styles[`md-${md}`], 18 | Styles[`lg-${lg}`] 19 | ); 20 | return ( 21 |
22 | {children} 23 |
24 | ); 25 | } 26 | } 27 | 28 | GridItem.defaultProps = { 29 | children: null, 30 | className: null, 31 | xs: 12, 32 | sm: 0, 33 | md: 0, 34 | lg: 0 35 | }; 36 | 37 | GridItem.propTypes = { 38 | children: PropTypes.node, 39 | className: PropTypes.string, 40 | xs: PropTypes.number, 41 | sm: PropTypes.number, 42 | md: PropTypes.number, 43 | lg: PropTypes.number 44 | }; 45 | 46 | export default GridItem; 47 | -------------------------------------------------------------------------------- /src/Grid/GridItem.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import React from 'react'; 5 | import {createMount, createTest} from '../../test/utils'; 6 | import variables from '../variables'; 7 | import GridItem from './GridItem'; 8 | 9 | describe('GridItem', () => { 10 | let mount; 11 | 12 | beforeEach(() => { 13 | mount = createMount(); 14 | }); 15 | 16 | afterEach(() => { 17 | mount.cleanUp(); 18 | }); 19 | 20 | it('should render a GridItem', createTest(() => { 21 | const component = ( 22 | 23 |
33 | xs=12 34 |
35 |
36 | ); 37 | const wrapper = mount(component); 38 | assert(wrapper); 39 | })); 40 | }); 41 | -------------------------------------------------------------------------------- /src/Grid/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './Grid'; 2 | export {default as GridItem} from './GridItem'; 3 | -------------------------------------------------------------------------------- /src/List/List.css: -------------------------------------------------------------------------------- 1 | .root { 2 | background-color: white; 3 | padding-top: 8px; 4 | padding-bottom: 8px; 5 | } 6 | -------------------------------------------------------------------------------- /src/List/List.js: -------------------------------------------------------------------------------- 1 | import makeClass from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import Styles from './List.css'; 5 | 6 | class List extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.onKeyDown = this.onKeyDown.bind(this); 10 | } 11 | 12 | onKeyDown(e, ...args) { 13 | const {keyCode} = e; 14 | const isDown = (keyCode === 40); 15 | const isUp = (keyCode === 38); 16 | 17 | if (isDown || isUp) { 18 | e.preventDefault(); 19 | const nodeOfInterest = isUp ? 'previousElementSibling' : 'nextElementSibling'; 20 | const nextListItem = e.target.parentElement; 21 | if (nextListItem && nextListItem[nodeOfInterest]) { 22 | nextListItem[nodeOfInterest].firstChild.focus(); 23 | } 24 | } 25 | this.props.onKeyDown(e, ...args); 26 | } 27 | 28 | registerRoot = c => { 29 | this.root = c; 30 | } 31 | 32 | render() { 33 | const { 34 | arrowNavigation, children, className, onKeyDown, ...other 35 | } = this.props; 36 | return ( 37 |
38 | {React.Children.map(children, child => { 39 | const props = { 40 | buttonProps: {...child.props.buttonProps, focusRippleDisabled: true} 41 | }; 42 | if (arrowNavigation) { 43 | props.onKeyDown = this.onKeyDown; 44 | } 45 | return React.cloneElement(child, props); 46 | })} 47 |
48 | ); 49 | } 50 | } 51 | 52 | List.defaultProps = { 53 | arrowNavigation: false, 54 | children: null, 55 | className: null, 56 | onKeyDown: () => {}, 57 | style: {} 58 | }; 59 | 60 | List.propTypes = { 61 | arrowNavigation: PropTypes.bool, 62 | children: PropTypes.node, 63 | className: PropTypes.string, 64 | onKeyDown: PropTypes.func, 65 | style: PropTypes.object 66 | }; 67 | 68 | export default List; 69 | -------------------------------------------------------------------------------- /src/List/List.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import keycode from 'keycode'; 5 | import React from 'react'; 6 | import {createShallow, createMount, createTest} from '../../test/utils'; 7 | import List from './List'; 8 | import ListItem from './ListItem'; 9 | 10 | describe('List', () => { 11 | let shallow; 12 | let mount; 13 | 14 | beforeEach(() => { 15 | shallow = createShallow(); 16 | mount = createMount(); 17 | }); 18 | 19 | afterEach(() => { 20 | shallow.cleanUp(); 21 | mount.cleanUp(); 22 | }); 23 | 24 | it('should shallow render', () => { 25 | const wrapper = shallow(); 26 | assert(wrapper); 27 | }); 28 | 29 | it('should deep render', () => { 30 | const wrapper = mount(); 31 | assert(wrapper); 32 | }); 33 | 34 | it('should support arrow key navigation with arrowNavigation prop', createTest(() => { 35 | const component = ( 36 | 37 | 38 | 39 | 40 | ); 41 | const wrapper = mount(component); 42 | 43 | wrapper.find('#first button').instance().focus(); 44 | 45 | setTimeout(() => { 46 | wrapper.find('#first button').simulate('keyDown', {keyCode: keycode('down')}); 47 | assert(document.activeElement === wrapper.find('#second button').instance()); 48 | }, 250); 49 | 50 | setTimeout(() => { 51 | wrapper.find('#second button').simulate('keyDown', {keyCode: keycode('up')}); 52 | assert(document.activeElement === wrapper.find('#first button').instance()); 53 | }, 750); 54 | })); 55 | }); 56 | -------------------------------------------------------------------------------- /src/List/ListItem.css: -------------------------------------------------------------------------------- 1 | .root { 2 | position: relative; 3 | } 4 | 5 | .button { 6 | align-items: center; 7 | border-radius: 0; 8 | display: flex; 9 | flex-direction: row; 10 | margin: 0; 11 | padding: 12px 16px; 12 | text-align: left; 13 | text-transform: none; 14 | width: 100%; 15 | } 16 | 17 | .button:focus, 18 | .button:hover { 19 | background-color: rgba(0, 0, 0, 0.12); 20 | } 21 | 22 | .avatar { 23 | height: 40px; 24 | width: 40px; 25 | align-items: center; 26 | justify-content: center; 27 | display: flex; 28 | fill: rgba(0, 0, 0, 0.54); 29 | } 30 | 31 | .text { 32 | display: flex; 33 | align-items: flex-start; 34 | flex-direction: column; 35 | justify-content: center; 36 | text-transform: none; 37 | width: inherit; 38 | } 39 | 40 | .textWithAvatarOrAction { 41 | max-width: calc(100% - 100px); 42 | overflow: hidden; 43 | padding: 0 16px; 44 | } 45 | 46 | .action { 47 | align-items: center; 48 | display: flex; 49 | fill: rgba(0, 0, 0, 0.54); 50 | height: 100%; 51 | position: absolute; 52 | right: 10px; 53 | top: 0; 54 | } 55 | 56 | .primary.primaryText, 57 | .secondary.secondaryText { 58 | text-overflow: ellipsis; 59 | overflow: hidden; 60 | max-width: 100%; 61 | width: 100%; 62 | } 63 | 64 | .primary.primaryText { 65 | color: #000; 66 | } 67 | 68 | .secondary.secondaryText { 69 | color: $black54; 70 | } 71 | -------------------------------------------------------------------------------- /src/List/ListItem.js: -------------------------------------------------------------------------------- 1 | import makeClass from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import Button from '../Button'; 5 | import Typography from '../Typography'; 6 | import Styles from './ListItem.css'; 7 | 8 | class ListItem extends React.Component { 9 | registerButton = c => { 10 | this.Button = c; 11 | } 12 | 13 | render() { 14 | const { 15 | action, buttonProps, avatar, className, primary, secondary, ...other 16 | } = this.props; 17 | return ( 18 |
19 | 52 | {action && ( 53 |
54 | {action} 55 |
56 | )} 57 |
58 | ); 59 | } 60 | } 61 | 62 | ListItem.defaultProps = { 63 | action: null, 64 | avatar: null, 65 | buttonProps: {}, 66 | className: null, 67 | primary: null, 68 | secondary: null 69 | }; 70 | 71 | ListItem.propTypes = { 72 | action: PropTypes.node, 73 | avatar: PropTypes.node, 74 | buttonProps: PropTypes.object, 75 | className: PropTypes.string, 76 | primary: PropTypes.node, 77 | secondary: PropTypes.node 78 | }; 79 | 80 | export default ListItem; 81 | -------------------------------------------------------------------------------- /src/List/ListItem.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import Folder from 'material-design-icons/file/svg/design/ic_folder_24px.svg'; 5 | import MoreVert from 'material-design-icons/navigation/svg/production/ic_more_vert_24px.svg'; 6 | import React from 'react'; 7 | import {createShallow, createMount, createTest} from '../../test/utils'; 8 | import ListItem from './ListItem'; 9 | import Styles from './ListItem.css'; 10 | 11 | describe('ListItem', () => { 12 | let shallow; 13 | let mount; 14 | 15 | beforeEach(() => { 16 | shallow = createShallow(); 17 | mount = createMount(); 18 | }); 19 | 20 | afterEach(() => { 21 | shallow.cleanUp(); 22 | mount.cleanUp(); 23 | }); 24 | 25 | it('should shallow render', () => { 26 | const wrapper = shallow(); 27 | assert(wrapper); 28 | }); 29 | 30 | it('should deep render', () => { 31 | const wrapper = mount(); 32 | assert(wrapper); 33 | }); 34 | 35 | it('should render with just a primary label', createTest(() => { 36 | const wrapper = mount(); 39 | assert(wrapper.find(`.${Styles.text}`).length === 1); 40 | })); 41 | 42 | it('should render with a primary and secondary label', createTest(() => { 43 | const wrapper = mount(); 47 | assert(wrapper.find(`.${Styles.text}`).length === 1); 48 | })); 49 | 50 | it('should render with an avatar, primary + secondary label', createTest(() => { 51 | const wrapper = mount(} 53 | primary="primary" 54 | secondary="secondary" 55 | />); 56 | assert(wrapper.find(`.${Styles.avatar}`).length === 1); 57 | })); 58 | 59 | it('should render with an avatar, primary + secondary labels, + an action', createTest(() => { 60 | const wrapper = mount(} 62 | primary="primary" 63 | secondary="secondary" 64 | action={} 65 | />); 66 | assert(wrapper.find(`.${Styles.action}`).length === 1); 67 | })); 68 | }); 69 | -------------------------------------------------------------------------------- /src/List/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './List'; 2 | export {default as ListItem} from './ListItem'; 3 | -------------------------------------------------------------------------------- /src/Paper/Paper.css: -------------------------------------------------------------------------------- 1 | .root { 2 | background-color: #fff; 3 | } 4 | 5 | .elevation1 { box-shadow: $elevation1; } 6 | .elevation2 { box-shadow: $elevation2; } 7 | .elevation3 { box-shadow: $elevation3; } 8 | .elevation4 { box-shadow: $elevation4; } 9 | .elevation5 { box-shadow: $elevation5; } 10 | .elevation6 { box-shadow: $elevation6; } 11 | .elevation7 { box-shadow: $elevation7; } 12 | .elevation8 { box-shadow: $elevation8; } 13 | .elevation9 { box-shadow: $elevation9; } 14 | .elevation10 { box-shadow: $elevation10; } 15 | .elevation11 { box-shadow: $elevation11; } 16 | .elevation12 { box-shadow: $elevation12; } 17 | .elevation13 { box-shadow: $elevation13; } 18 | .elevation14 { box-shadow: $elevation14; } 19 | .elevation15 { box-shadow: $elevation15; } 20 | .elevation16 { box-shadow: $elevation16; } 21 | .elevation17 { box-shadow: $elevation17; } 22 | .elevation18 { box-shadow: $elevation18; } 23 | .elevation19 { box-shadow: $elevation19; } 24 | .elevation20 { box-shadow: $elevation20; } 25 | .elevation21 { box-shadow: $elevation21; } 26 | .elevation22 { box-shadow: $elevation22; } 27 | .elevation23 { box-shadow: $elevation23; } 28 | .elevation24 { box-shadow: $elevation24; } 29 | .elevation25 { box-shadow: $elevation25; } 30 | -------------------------------------------------------------------------------- /src/Paper/Paper.js: -------------------------------------------------------------------------------- 1 | import makeClass from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import Styles from './Paper.css'; 5 | 6 | class Paper extends React.Component { 7 | render() { 8 | const { 9 | backgroundColor, children, className, elevation, style, ...other 10 | } = this.props; 11 | return ( 12 |
17 | {children} 18 |
19 | ); 20 | } 21 | } 22 | 23 | Paper.defaultProps = { 24 | backgroundColor: '#fff', 25 | children: null, 26 | className: '', 27 | elevation: 1, 28 | style: {} 29 | }; 30 | 31 | Paper.propTypes = { 32 | backgroundColor: PropTypes.string, 33 | children: PropTypes.node, 34 | className: PropTypes.string, 35 | elevation: PropTypes.number, 36 | style: PropTypes.object 37 | }; 38 | 39 | export default Paper; 40 | -------------------------------------------------------------------------------- /src/Paper/Paper.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import React from 'react'; 5 | import {createShallow, createMount, createTest} from '../../test/utils'; 6 | import Paper from './Paper'; 7 | 8 | describe('Paper', () => { 9 | let shallow; 10 | let mount; 11 | 12 | beforeEach(() => { 13 | shallow = createShallow(); 14 | mount = createMount(); 15 | }); 16 | 17 | afterEach(() => { 18 | shallow.cleanUp(); 19 | mount.cleanUp(); 20 | }); 21 | 22 | it('should shallow render', () => { 23 | const wrapper = shallow(); 24 | assert(wrapper); 25 | }); 26 | 27 | it('should deep render', createTest(() => { 28 | mount(Paper!!!); 29 | })); 30 | }); 31 | -------------------------------------------------------------------------------- /src/Paper/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './Paper'; 2 | -------------------------------------------------------------------------------- /src/Ripple/Ripple.css: -------------------------------------------------------------------------------- 1 | .root { 2 | border-radius: inherit; 3 | display: block; 4 | height: 100%; 5 | width: 100%; 6 | left: 0; 7 | top: 0; 8 | pointer-events: none; 9 | position: absolute; 10 | overflow: hidden; 11 | transition: all 250ms; 12 | z-index: 0; 13 | } 14 | -------------------------------------------------------------------------------- /src/Ripple/Ripple.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import ReactTransitionGroup from 'react-transition-group/TransitionGroup'; 4 | import RippleItem from './RippleItem'; 5 | import Styles from './Ripple.css'; 6 | 7 | /* Usage note: the parent DOM element must have relative positioning. */ 8 | class Ripple extends React.Component { 9 | static defaultProps = { 10 | color: 'rgba(0, 0, 0, 0.87)' 11 | }; 12 | 13 | static propTypes = { 14 | color: PropTypes.string 15 | }; 16 | 17 | constructor(props) { 18 | super(props); 19 | this.add = this.add.bind(this); 20 | this.remove = this.remove.bind(this); 21 | this.ignoringMouseDown = false; 22 | this.state = { 23 | nextKey: 0, 24 | ripples: [] 25 | }; 26 | } 27 | 28 | shouldComponentUpdate(nextProps, nextState) { 29 | return ( 30 | this.props.color !== nextProps.color || 31 | this.state.ripples.length !== nextState.ripples.length || 32 | this.state.nextKey !== nextState.nextKey 33 | ); 34 | } 35 | 36 | add(e, options, cb = () => {}) { 37 | if (e.type === 'mousedown' && this.ignoringMouseDown) { 38 | this.ignoringMouseDown = false; 39 | return; 40 | } 41 | 42 | if (e.type === 'touchstart') { 43 | this.ignoringMouseDown = true; 44 | } 45 | 46 | const centered = options && options.centered ? options.centered : false; 47 | const pulsate = options && options.pulsate ? options.pulsate : false; 48 | const { 49 | left, top, bottom, right, height, width 50 | } = e.target.getBoundingClientRect(); 51 | 52 | const props = {}; 53 | 54 | if (centered) { 55 | props.rippleX = width / 2; 56 | props.rippleY = height / 2; 57 | } else { 58 | const clientX = e.clientX ? e.clientX : e.touches[0].clientX; 59 | const clientY = e.clientY ? e.clientY : e.touches[0].clientY; 60 | props.rippleX = clientX - left; 61 | props.rippleY = clientY - top; 62 | } 63 | 64 | const rippleSize = centered ? 65 | Math.sqrt((((2 * (width ** 2)) + (height ** 2))) / 3) : 66 | Math.sqrt(((right - left) ** 2) + ((bottom - top) ** 2)) * 2; 67 | 68 | let {ripples} = this.state; 69 | 70 | ripples = [...ripples, ( 71 | 78 | )]; 79 | 80 | this.setState(prevState => ({ 81 | nextKey: prevState.nextKey + 1, 82 | ripples 83 | }), cb); 84 | } 85 | 86 | remove(e, opts, cb = () => {}) { 87 | const {ripples} = this.state; 88 | this.setState({ 89 | ripples: opts && opts.removeAll ? [] : ripples.slice(1) 90 | }, cb); 91 | } 92 | 93 | render() { 94 | return ( 95 | 101 | {this.state.ripples} 102 | 103 | ); 104 | } 105 | } 106 | 107 | export default Ripple; 108 | -------------------------------------------------------------------------------- /src/Ripple/Ripple.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import React from 'react'; 5 | import {createShallow} from '../../test/utils'; 6 | import Ripple from './Ripple'; 7 | 8 | describe('Ripple', () => { 9 | let shallow; 10 | 11 | beforeEach(() => { 12 | shallow = createShallow(); 13 | }); 14 | 15 | afterEach(() => { 16 | shallow.cleanUp(); 17 | }); 18 | 19 | it('should shallow render', () => { 20 | const wrapper = shallow(); 21 | assert(wrapper); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/Ripple/RippleItem.css: -------------------------------------------------------------------------------- 1 | .container { 2 | opacity: 1; 3 | position: absolute; 4 | left: 0; 5 | } 6 | 7 | .containerLeaving { 8 | animation-name: exit; 9 | max-height: 100%; 10 | max-width: 100%; 11 | animation-duration: 550ms; 12 | animation-timing-function: $rippleEaseOutFunction; 13 | animation-fill-mode: forwards; 14 | } 15 | 16 | .containerPulsating { 17 | position: absolute; 18 | left: 0; 19 | top: 0; 20 | display: block; 21 | width: 100%; 22 | height: 100%; 23 | animation-name: pulsate; 24 | animation-duration: 1500ms; 25 | animation-timing-function: $rippleEaseOutFunction; 26 | animation-iteration-count: infinite; 27 | } 28 | 29 | .ripple { 30 | width: 50px; 31 | height: 50px; 32 | left: 0; 33 | top: 0; 34 | position: absolute; 35 | border-radius: 50%; 36 | background-color: rgba(153, 153, 153, 0.6); 37 | } 38 | 39 | .rippleVisible { 40 | opacity: 0.3; 41 | animation-name: enter; 42 | animation-duration: 550ms; 43 | animation-timing-function: $rippleEaseOutFunction; 44 | animation-fill-mode: forwards; 45 | } 46 | 47 | .rippleFast { 48 | animation-duration: 200ms; 49 | } 50 | 51 | @keyframes enter { 52 | 0% { transform: scale(0); } 53 | 100% { transform: scale(1); } 54 | } 55 | 56 | @keyframes exit { 57 | 0% { opacity: 1; } 58 | 100% { opacity: 0; } 59 | } 60 | 61 | @keyframes pulsate { 62 | 0% { transform: scale(1); } 63 | 50% { transform: scale(0.9); } 64 | 100% { transform: scale(1); } 65 | } 66 | -------------------------------------------------------------------------------- /src/Ripple/RippleItem.js: -------------------------------------------------------------------------------- 1 | import makeClass from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import Styles from './RippleItem.css'; 5 | 6 | class RippleItem extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.rippleStyles = this.rippleStyles.bind(this); 10 | this.state = { 11 | rippleVisible: false, 12 | rippleLeaving: false 13 | }; 14 | } 15 | 16 | componentWillUnmount() { 17 | clearTimeout(this.leaveTimer); 18 | } 19 | 20 | componentWillLeave(cb) { 21 | this.stop(() => { 22 | this.leaveTimer = setTimeout(() => { 23 | cb(); 24 | }, 550); 25 | }); 26 | } 27 | 28 | componentWillEnter(cb) { 29 | this.start(cb); 30 | } 31 | 32 | start(cb) { 33 | this.setState({ 34 | rippleVisible: true 35 | }, cb); 36 | } 37 | 38 | stop(cb) { 39 | this.setState({ 40 | rippleLeaving: true 41 | }, cb); 42 | } 43 | 44 | rippleStyles() { 45 | const { 46 | color, rippleSize, rippleX, rippleY 47 | } = this.props; 48 | const rippleStyles = { 49 | backgroundColor: color, 50 | width: rippleSize, 51 | height: rippleSize, 52 | top: -(rippleSize / 2) + rippleY, 53 | left: -(rippleSize / 2) + rippleX 54 | }; 55 | return rippleStyles; 56 | } 57 | 58 | render() { 59 | const {pulsate} = this.props; 60 | const {rippleLeaving, rippleVisible} = this.state; 61 | 62 | const containerClasses = makeClass(Styles.container, { 63 | [Styles.containerLeaving]: rippleLeaving, 64 | [Styles.containerPulsating]: pulsate && !rippleLeaving 65 | }); 66 | 67 | const rippleClasses = makeClass(Styles.ripple, { 68 | [Styles.rippleVisible]: rippleVisible, 69 | [Styles.rippleFast]: pulsate 70 | }); 71 | 72 | return ( 73 | 74 | 75 | 76 | ); 77 | } 78 | } 79 | 80 | RippleItem.defaultProps = { 81 | color: 'rgba(0, 0, 0, 0.87)', 82 | rippleX: 0, 83 | rippleY: 0, 84 | rippleSize: 0, 85 | pulsate: false 86 | }; 87 | 88 | RippleItem.propTypes = { 89 | color: PropTypes.string, 90 | rippleX: PropTypes.number, 91 | rippleY: PropTypes.number, 92 | rippleSize: PropTypes.number, 93 | pulsate: PropTypes.bool 94 | }; 95 | 96 | export default RippleItem; 97 | -------------------------------------------------------------------------------- /src/Ripple/RippleItem.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import React from 'react'; 5 | import {createShallow} from '../../test/utils'; 6 | import RippleItem from './RippleItem'; 7 | 8 | describe('RippleItem', () => { 9 | let shallow; 10 | 11 | beforeEach(() => { 12 | shallow = createShallow(); 13 | }); 14 | 15 | afterEach(() => { 16 | shallow.cleanUp(); 17 | }); 18 | 19 | it('should shallow render', () => { 20 | const wrapper = shallow(); 21 | assert(wrapper); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/Ripple/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './Ripple'; 2 | export {default as RippleItem} from './RippleItem'; 3 | -------------------------------------------------------------------------------- /src/Scrollable/Scrollable.css: -------------------------------------------------------------------------------- 1 | .root::-webkit-scrollbar { 2 | height: 4px; 3 | width: 4px; 4 | } 5 | 6 | .root::-webkit-scrollbar-thumb { 7 | border-radius: 0; 8 | background-color: #dcdcdc; 9 | box-shadow: none; 10 | } 11 | -------------------------------------------------------------------------------- /src/Scrollable/Scrollable.js: -------------------------------------------------------------------------------- 1 | import makeClass from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import Styles from './Scrollable.css'; 5 | 6 | class Scrollable extends React.Component { 7 | render() { 8 | const firstChild = React.Children.only(this.props.children); 9 | return React.cloneElement(firstChild, { 10 | className: makeClass(Styles.root, firstChild.props.className) 11 | }); 12 | } 13 | } 14 | 15 | Scrollable.defaultProps = { 16 | children: null 17 | }; 18 | 19 | Scrollable.propTypes = { 20 | children: PropTypes.node 21 | }; 22 | 23 | export default Scrollable; 24 | -------------------------------------------------------------------------------- /src/Scrollable/Scrollable.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import React from 'react'; 5 | import {createMount, createTest} from '../../test/utils'; 6 | import Scrollable from './Scrollable'; 7 | 8 | describe('Scrollable', () => { 9 | let mount; 10 | 11 | beforeEach(() => { 12 | mount = createMount(); 13 | }); 14 | 15 | afterEach(() => { 16 | mount.cleanUp(); 17 | }); 18 | 19 | it('should render a custom scrollbar on WebKit browsers', createTest(() => { 20 | const component = ( 21 | 22 |
30 | Line 1 31 | {'\n'} 32 | Line 2 33 | {'\n'} 34 | Line 3 35 | {'\n'} 36 | Line 4 37 | {'\n'} 38 | Line 5 39 | {'\n'} 40 | Line 6 41 | {'\n'} 42 | Line 7 43 | {'\n'} 44 | Line 8 45 | {'\n'} 46 | Line 9 47 | {'\n'} 48 | Line 10 49 | {'\n'} 50 | Line 11 51 | {'\n'} 52 | Line 12 53 | {'\n'} 54 | Line 13 55 | {'\n'} 56 | Line 14 57 | {'\n'} 58 | Line 15 should cause a horizontal scrollbar to be present 59 |
60 |
61 | ); 62 | const wrapper = mount(component); 63 | setTimeout(() => { 64 | wrapper.getDOMNode().scrollTop = 200; 65 | }, 500); 66 | assert(wrapper); 67 | })); 68 | }); 69 | -------------------------------------------------------------------------------- /src/Scrollable/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './Scrollable'; 2 | -------------------------------------------------------------------------------- /src/SelectField/SelectField.css: -------------------------------------------------------------------------------- 1 | .root { 2 | position: relative; 3 | width: 100%; 4 | flex: 1; 5 | -webkit-tap-highlight-color: transparent; 6 | } 7 | 8 | .icon { 9 | position: absolute; 10 | right: 0; 11 | top: 40px; 12 | pointer-events: none; 13 | } 14 | 15 | .helperText { 16 | padding-top: 8px; 17 | } 18 | 19 | .label { 20 | color: rgba(0, 0, 0, 0.54); 21 | left: 0; 22 | line-height: 30px; 23 | max-height: 30px; 24 | overflow: hidden; 25 | pointer-events: none; 26 | position: absolute; 27 | right: 30px; 28 | text-overflow: ellipsis; 29 | top: 35px; 30 | transform-origin: left top 0; 31 | transition-duration: 250ms; 32 | } 33 | 34 | .label.hasValue { 35 | transform: $textFieldLabelFloatTransform; 36 | } 37 | 38 | .inkbar { 39 | border-bottom-width: 1px; 40 | border-bottom-color: rgba(0, 0, 0, 0.42); 41 | border-bottom-style: solid; 42 | box-sizing: border-box; 43 | height: 2px; 44 | } 45 | 46 | .select { 47 | appearance: none; 48 | background: transparent; 49 | border: none; 50 | font-family: inherit; 51 | width: 100%; 52 | font-size: 16px; 53 | padding-top: 8px; 54 | padding-bottom: 8px; 55 | padding-right: 30px; 56 | margin-top: 32px; 57 | } 58 | 59 | select::-ms-expand { 60 | display: none; 61 | } 62 | 63 | select:-moz-focusring { 64 | color: transparent; 65 | text-shadow: 0 0 0 #000; 66 | } 67 | 68 | select:focus::-ms-value { 69 | background-color: transparent; 70 | color: #000; 71 | } 72 | 73 | .select:focus { 74 | outline: none; 75 | } 76 | 77 | .select:disabled { 78 | color: rgba(0, 0, 0, 0.42); 79 | cursor: not-allowed; 80 | } 81 | 82 | .select:disabled ~ .inkbar { 83 | border-bottom-style: dashed; 84 | } 85 | 86 | .select:focus:not(:disabled) ~ .inkbar { 87 | animation-name: stretch; 88 | animation-duration: 250ms; 89 | animation-timing-function: $easeOutFunction; 90 | animation-fill-mode: forwards; 91 | border-bottom-width: 2px; 92 | border-bottom-color: $primary; 93 | max-width: 70%; 94 | } 95 | 96 | .select:hover:not(:disabled) ~ .inkbar { 97 | border-bottom-width: 2px; 98 | } 99 | 100 | @keyframes stretch { 101 | 0% { 102 | margin-left: 15%; 103 | max-width: 70%; 104 | } 105 | 106 | 100% { 107 | max-width: 100%; 108 | margin-left: 0%; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/SelectField/SelectField.js: -------------------------------------------------------------------------------- 1 | import DropDownIcon from 'material-design-icons/navigation/svg/production/ic_arrow_drop_down_24px.svg'; 2 | import makeClass from 'classnames'; 3 | import PropTypes from 'prop-types'; 4 | import React from 'react'; 5 | import Typography from '../Typography'; 6 | import Styles from './SelectField.css'; 7 | 8 | class SelectField extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.onChange = this.onChange.bind(this); 12 | } 13 | 14 | onChange(...args) { 15 | this.props.onChange(...args); 16 | } 17 | 18 | render() { 19 | const { 20 | children, 21 | disabled, 22 | errorColor, 23 | helperText, 24 | id, 25 | label, 26 | value, 27 | ...other 28 | } = this.props; 29 | return ( 30 |
31 | 40 | 51 | 55 |
59 | 64 | {helperText} 65 | 66 |
67 | ); 68 | } 69 | } 70 | 71 | SelectField.defaultProps = { 72 | children: null, 73 | disabled: false, 74 | errorColor: null, 75 | helperText: null, 76 | id: null, 77 | label: null, 78 | onChange: () => {}, 79 | value: {} 80 | }; 81 | 82 | SelectField.propTypes = { 83 | children: PropTypes.node, 84 | disabled: PropTypes.bool, 85 | errorColor: PropTypes.string, 86 | helperText: PropTypes.string, 87 | id: PropTypes.string, 88 | label: PropTypes.string, 89 | onChange: PropTypes.func, 90 | value: PropTypes.any 91 | }; 92 | 93 | export default SelectField; 94 | -------------------------------------------------------------------------------- /src/SelectField/SelectField.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import React from 'react'; 5 | import {createShallow} from '../../test/utils'; 6 | import SelectField from './SelectField'; 7 | 8 | describe('SelectField', () => { 9 | let shallow; 10 | 11 | beforeEach(() => { 12 | shallow = createShallow(); 13 | }); 14 | 15 | afterEach(() => { 16 | shallow.cleanUp(); 17 | }); 18 | 19 | it('should shallow render', () => { 20 | const wrapper = shallow(); 21 | assert(wrapper); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/SelectField/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './SelectField'; 2 | -------------------------------------------------------------------------------- /src/SnackBar/SnackBar.css: -------------------------------------------------------------------------------- 1 | .root { 2 | text-align: center; 3 | } 4 | 5 | .overlay { 6 | position: fixed; 7 | top: 0; 8 | left: 0; 9 | right: 0; 10 | bottom: 0; 11 | -webkit-tap-highlight-color: transparent; 12 | } 13 | 14 | .snackbar { 15 | background-color: #323232; 16 | bottom: 0; 17 | transform: translateY(48px); 18 | z-index: 1; 19 | color: white; 20 | width: 100%; 21 | height: 48px; 22 | box-sizing: border-box; 23 | display: inline-flex; 24 | justify-content: space-between; 25 | animation-name: show; 26 | animation-duration: 550ms; 27 | animation-fill-mode: forwards; 28 | } 29 | 30 | @media (min-width: $screenSm) { 31 | .snackbar { 32 | width: auto; 33 | max-width: 550px; 34 | } 35 | } 36 | 37 | .snackbarWrapper { 38 | bottom: 0; 39 | display: flex; 40 | justify-content: center; 41 | position: fixed; 42 | width: 100%; 43 | } 44 | 45 | @keyframes show { 46 | 0% { 47 | transform: translateY(48px); 48 | } 49 | 50 | 100% { 51 | transform: translateY(0); 52 | } 53 | } 54 | 55 | @keyframes hide { 56 | 0% { 57 | transform: translateY(0); 58 | } 59 | 60 | 100% { 61 | transform: translateY(48px); 62 | } 63 | } 64 | 65 | .message { 66 | padding: 14px 24px; 67 | color: $white; 68 | white-space: nowrap; 69 | overflow: hidden; 70 | text-overflow: ellipsis; 71 | } 72 | 73 | .button { 74 | margin: 0; 75 | } 76 | 77 | .delay { 78 | animation-delay: 550ms; 79 | } 80 | -------------------------------------------------------------------------------- /src/SnackBar/SnackBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactTransitionGroup from 'react-transition-group/TransitionGroup'; 3 | 4 | class SnackBar extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.queue = this.queue.bind(this); 8 | this.dequeue = this.dequeue.bind(this); 9 | this.handleTransition = this.handleTransition.bind(this); 10 | this.state = { 11 | delay: false, 12 | index: 0, 13 | SnackBarItems: [], 14 | transitioning: false 15 | }; 16 | } 17 | 18 | shouldComponentUpdate(nextProps, nextState) { 19 | return this.state.transitioning !== nextState.transitioning || 20 | nextState.SnackBarItems.length === 1 || 21 | (this.state.SnackBarItems.length - 1 === nextState.SnackBarItems.length); 22 | } 23 | 24 | componentWillUnmount() { 25 | clearTimeout(this.timeout); 26 | } 27 | 28 | queue(SnackBarItem) { 29 | this.timeout = setTimeout(() => { 30 | this.setState(prevState => { 31 | const newElement = React.cloneElement(SnackBarItem, { 32 | key: prevState.index 33 | }); 34 | 35 | return { 36 | delay: prevState.SnackBarItems.length !== 0, 37 | index: prevState.index + 1, 38 | SnackBarItems: [ 39 | ...prevState.SnackBarItems, 40 | newElement 41 | ] 42 | }; 43 | }); 44 | }); 45 | } 46 | 47 | dequeue() { 48 | this.setState(prevState => { 49 | const newitems = prevState.SnackBarItems.slice(1); 50 | return { 51 | delay: !newitems.length !== 0, 52 | SnackBarItems: newitems 53 | }; 54 | }); 55 | } 56 | 57 | handleTransition() { 58 | this.setState(prevState => ({ 59 | transitioning: !prevState.transitioning 60 | })); 61 | } 62 | 63 | render() { 64 | const item = this.state.SnackBarItems[0]; 65 | return ( 66 | 69 | {item && ( 70 | React.cloneElement(item, { 71 | onClose: this.dequeue, 72 | delay: this.state.delay, 73 | handleTransition: this.handleTransition, 74 | transitioning: this.state.transitioning 75 | }) 76 | )} 77 | 78 | ); 79 | } 80 | } 81 | 82 | export default SnackBar; 83 | -------------------------------------------------------------------------------- /src/SnackBar/SnackBar.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import React from 'react'; 5 | import Button from '../Button'; 6 | import {createShallow, createMount, createTest} from '../../test/utils'; 7 | import Styles from './SnackBar.css'; 8 | import SnackBar, {SnackBarItem} from '.'; 9 | 10 | describe('Snackbar', () => { 11 | let shallow; 12 | let mount; 13 | 14 | beforeEach(() => { 15 | shallow = createShallow(); 16 | mount = createMount(); 17 | }); 18 | 19 | afterEach(() => { 20 | shallow.cleanUp(); 21 | mount.cleanUp(); 22 | }); 23 | 24 | it('should shallow render a SnackBar', () => { 25 | const wrapper = shallow(); 26 | assert(wrapper); 27 | }); 28 | 29 | it('should deep render a SnackBar', () => { 30 | const wrapper = mount(); 31 | assert(wrapper); 32 | }); 33 | 34 | it('should render a snackbar with a single snackbaritem', createTest(() => { 35 | const snackbaritem = ( 36 | Test} 39 | /> 40 | ); 41 | const wrapper = mount(); 42 | wrapper.find('SnackBar').instance().queue(snackbaritem); 43 | setTimeout(() => { 44 | assert(document.body.innerHTML.indexOf('Hello World') > -1); 45 | }, 550); 46 | })); 47 | 48 | it('should queue multiple SnackBarItem', createTest(() => { 49 | const wrapper = mount(); 50 | const first = ( 51 | {}} 58 | > 59 | First 60 | 61 | )} 62 | /> 63 | ); 64 | const second = ( 65 | {}} 71 | > 72 | Second 73 | 74 | )} 75 | /> 76 | ); 77 | wrapper.find('SnackBar').instance().queue(first); 78 | wrapper.find('SnackBar').instance().queue(second); 79 | 80 | setTimeout(() => { 81 | assert(wrapper.state('SnackBarItems').length === 2); 82 | document.getElementById('firstBtn').click(); 83 | }, 500); 84 | 85 | setTimeout(() => { 86 | assert(wrapper.state('SnackBarItems').length === 1); 87 | }, 1000); 88 | 89 | setTimeout(() => { 90 | document.getElementsByClassName(Styles.overlay)[0].click(); 91 | }, 1500); 92 | 93 | setTimeout(() => { 94 | assert(wrapper.state('SnackBarItems').length === 0); 95 | }, 1750); 96 | }, 2000)); 97 | }); 98 | -------------------------------------------------------------------------------- /src/SnackBar/SnackBarItem.js: -------------------------------------------------------------------------------- 1 | import {findDOMNode} from 'react-dom'; 2 | import makeClass from 'classnames'; 3 | import PropTypes from 'prop-types'; 4 | import React from 'react'; 5 | import DOMBodyRender from '../utils/DOMBodyRender'; 6 | import Typography from '../Typography'; 7 | import Styles from './SnackBar.css'; 8 | 9 | class SnackBarItem extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.handleOverlayClick = this.handleOverlayClick.bind(this); 13 | this.makeAction = this.makeAction.bind(this); 14 | } 15 | 16 | componentDidMount() { 17 | if (this.action) { 18 | findDOMNode(this.action).focus(); 19 | } 20 | } 21 | 22 | componentWillUnmount() { 23 | clearTimeout(this.timeout); 24 | } 25 | 26 | componentWillEnter(cb) { 27 | this.timeout = setTimeout(() => { 28 | cb(); 29 | }, 550); 30 | } 31 | 32 | componentWillLeave(cb) { 33 | this.props.handleTransition(); 34 | this.snackbar.style.animationDelay = '0ms'; 35 | this.snackbar.style.animationName = Styles.hide; 36 | this.timeout = setTimeout(() => { 37 | cb(); 38 | this.props.handleTransition(); 39 | }, 550); 40 | } 41 | 42 | handleOverlayClick() { 43 | if (!this.props.transitioning) { 44 | this.props.onClose(); 45 | } 46 | } 47 | 48 | makeAction() { 49 | const {action} = this.props; 50 | return React.cloneElement(action, { 51 | className: makeClass( 52 | action.props.className, 53 | Styles.button 54 | ), 55 | onClick: (...args) => { 56 | this.handleOverlayClick(); 57 | if (action.props.onClick) { 58 | action.props.onClick(args); 59 | } 60 | }, 61 | ref: this.registerAction 62 | }); 63 | } 64 | 65 | registerAction = c => { 66 | this.action = c; 67 | } 68 | 69 | registerSnackbar = c => { 70 | this.snackbar = c; 71 | } 72 | 73 | render() { 74 | const {action, delay, message} = this.props; 75 | return ( 76 | 77 |
78 |
82 |
83 |
90 | 91 | {message} 92 | 93 | {action && this.makeAction()} 94 |
95 |
96 |
97 | 98 | ); 99 | } 100 | } 101 | 102 | SnackBarItem.defaultProps = { 103 | action: null, 104 | delay: false, 105 | handleTransition: () => {}, 106 | message: null, 107 | onClose: () => {}, 108 | transitioning: false 109 | }; 110 | 111 | SnackBarItem.propTypes = { 112 | action: PropTypes.node, 113 | delay: PropTypes.bool, 114 | handleTransition: PropTypes.func, 115 | message: PropTypes.string, 116 | onClose: PropTypes.func, 117 | transitioning: PropTypes.bool 118 | }; 119 | 120 | export default SnackBarItem; 121 | -------------------------------------------------------------------------------- /src/SnackBar/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './SnackBar'; 2 | export {default as SnackBarItem} from './SnackBarItem'; 3 | -------------------------------------------------------------------------------- /src/SvgIcon/SvgIcon.css: -------------------------------------------------------------------------------- 1 | .root { 2 | padding: 0; 3 | height: 48px; 4 | width: 48px; 5 | justify-content: center; 6 | background: none; 7 | border: none; 8 | outline: none; 9 | position: relative; 10 | min-width: 0; 11 | } 12 | -------------------------------------------------------------------------------- /src/SvgIcon/SvgIcon.js: -------------------------------------------------------------------------------- 1 | import makeClass from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import Button from '../Button'; 5 | import Styles from './SvgIcon.css'; 6 | 7 | /* This wrapper is a necessary abstraction since IE11 makes 8 | * SVG's tab key focusable, even when tabindex = -1. 9 | */ 10 | class SvgIcon extends React.Component { 11 | render() { 12 | const {buttonProps, component, ...other} = this.props; 13 | const Component = component; 14 | return ( 15 | 22 | ); 23 | } 24 | } 25 | 26 | SvgIcon.defaultProps = { 27 | buttonProps: { 28 | className: null 29 | } 30 | }; 31 | 32 | SvgIcon.propTypes = { 33 | buttonProps: PropTypes.object, 34 | component: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired 35 | }; 36 | 37 | export default SvgIcon; 38 | -------------------------------------------------------------------------------- /src/SvgIcon/SvgIcon.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import Add from 'material-design-icons/content/svg/production/ic_add_24px.svg'; 5 | import React from 'react'; 6 | import {createMount, createTest} from '../../test/utils'; 7 | import SvgIcon from './SvgIcon'; 8 | 9 | describe('SvgIcon', () => { 10 | let mount; 11 | 12 | beforeEach(() => { 13 | mount = createMount(); 14 | }); 15 | 16 | afterEach(() => { 17 | mount.cleanUp(); 18 | }); 19 | 20 | it('should accept an SVG as a component', createTest(() => { 21 | const wrapper = mount(); 22 | assert(wrapper.find(Add).length === 1); 23 | assert(wrapper.find(Add).props().focusable === 'false'); 24 | })); 25 | }); 26 | -------------------------------------------------------------------------------- /src/SvgIcon/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './SvgIcon'; 2 | -------------------------------------------------------------------------------- /src/Switch/Switch.css: -------------------------------------------------------------------------------- 1 | .root { 2 | cursor: pointer; 3 | display: flex; 4 | -webkit-tap-highlight-color: transparent; 5 | } 6 | 7 | .disabled { 8 | cursor: not-allowed; 9 | } 10 | 11 | .switchWrapper { 12 | height: 64px; 13 | width: 64px; 14 | position: relative; 15 | display: flex; 16 | } 17 | 18 | .input { 19 | position: absolute; 20 | height: 100%; 21 | width: 100%; 22 | border: 0; 23 | background: transparent; 24 | -webkit-appearance: none; 25 | outline: none; 26 | margin: 0; 27 | z-index: 10; 28 | clip: rect(1px, 1px, 1px, 1px); 29 | } 30 | 31 | .input:disabled { 32 | cursor: not-allowed; 33 | } 34 | 35 | .track { 36 | border-radius: 7px; 37 | width: 34px; 38 | height: 14px; 39 | background-color: #000; 40 | opacity: 0.5; 41 | margin: auto; 42 | position: relative; 43 | } 44 | 45 | .thumb { 46 | width: 20px; 47 | height: 20px; 48 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2), 0 1px 1px 0 rgba(0, 0, 0, 0.14), 0 2px 1px -1px rgba(0, 0, 0, 0.12); 49 | border-radius: 50%; 50 | background-color: #fff; 51 | position: absolute; 52 | top: 22px; 53 | left: 10px; 54 | transition: all 250ms $easeOutFunction; 55 | } 56 | 57 | .input:checked ~ .thumb { 58 | background-color: $primary; 59 | transform: translate(25px, 0); 60 | } 61 | 62 | .input:checked ~ .track { 63 | background-color: $primary; 64 | } 65 | 66 | .input:disabled ~ .thumb, 67 | .input:disabled ~ .track { 68 | background-color: #bdbdbd; 69 | } 70 | 71 | .input:focus ~ .thumb.thumbKeyboardFocus::before { 72 | background-color: #000; 73 | position: absolute; 74 | height: 100%; 75 | width: 100%; 76 | border-radius: 50%; 77 | opacity: 0.15; 78 | transform: scale(2); 79 | content: ' '; 80 | } 81 | 82 | .input:not(:checked) ~ .thumb { 83 | background-color: white; 84 | } 85 | 86 | .label { 87 | line-height: 64px; 88 | pointer-events: none; 89 | user-select: none; 90 | } 91 | 92 | -------------------------------------------------------------------------------- /src/Switch/Switch.js: -------------------------------------------------------------------------------- 1 | import makeClass from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import Styles from './Switch.css'; 5 | 6 | class Switch extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.onBlur = this.onBlur.bind(this); 10 | this.onMouseUp = this.onMouseUp.bind(this); 11 | this.onKeyUp = this.onKeyUp.bind(this); 12 | this.getTrackColor = this.getTrackColor.bind(this); 13 | this.getThumbColor = this.getThumbColor.bind(this); 14 | this.state = { 15 | keyboardFocused: false 16 | }; 17 | } 18 | 19 | onBlur() { 20 | this.setState({ 21 | keyboardFocused: false 22 | }); 23 | } 24 | 25 | onMouseUp() { 26 | this.input.click(); 27 | } 28 | 29 | onKeyUp({keyCode}) { 30 | const isTab = (keyCode === 9); 31 | const isSpace = (keyCode === 32); 32 | 33 | this.setState({ 34 | keyboardFocused: (isTab || isSpace) 35 | }); 36 | } 37 | 38 | registerInput = c => { 39 | this.input = c; 40 | } 41 | 42 | getTrackColor = () => { 43 | const {checked, disabled, primaryColor} = this.props; 44 | if (disabled) { 45 | return '#bdbdbd'; 46 | } 47 | if (checked) { 48 | return primaryColor || '#2196f3'; 49 | } 50 | return '#000'; 51 | } 52 | 53 | getThumbColor = () => { 54 | const {checked, disabled, primaryColor} = this.props; 55 | if (disabled && checked) { 56 | return '#bdbdbd'; 57 | } 58 | if (checked) { 59 | return primaryColor || '#2196f3'; 60 | } 61 | return '#FFF'; 62 | } 63 | 64 | render() { 65 | const { 66 | checked, 67 | disabled, 68 | label, 69 | labelId, 70 | onChange, 71 | primaryColor, 72 | style, 73 | ...props 74 | } = this.props; 75 | const {keyboardFocused} = this.state; 76 | return ( 77 |
84 |
85 | 97 |
101 |
107 |
108 | 109 |
110 | ); 111 | } 112 | } 113 | 114 | Switch.defaultProps = { 115 | checked: false, 116 | disabled: false, 117 | label: null, 118 | labelId: null, 119 | primaryColor: null, 120 | style: {} 121 | }; 122 | 123 | Switch.propTypes = { 124 | checked: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), 125 | disabled: PropTypes.bool, 126 | label: PropTypes.node, 127 | labelId: PropTypes.string, 128 | onChange: PropTypes.func.isRequired, 129 | primaryColor: PropTypes.string, 130 | style: PropTypes.object 131 | }; 132 | 133 | export default Switch; 134 | -------------------------------------------------------------------------------- /src/Switch/Switch.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import keycode from 'keycode'; 5 | import noop from 'lodash'; 6 | import React from 'react'; 7 | import {createShallow, createMount, createTest} from '../../test/utils'; 8 | import Styles from './Switch.css'; 9 | import Switch from './Switch'; 10 | 11 | describe('Switch', () => { 12 | let shallow; 13 | let mount; 14 | 15 | beforeEach(() => { 16 | shallow = createShallow(); 17 | mount = createMount(); 18 | }); 19 | 20 | afterEach(() => { 21 | shallow.cleanUp(); 22 | mount.cleanUp(); 23 | }); 24 | 25 | it('should shallow render', () => { 26 | const wrapper = shallow(); 27 | assert(wrapper); 28 | }); 29 | 30 | it('should add aria attribute', () => { 31 | const labelId = 'foo-bar-baz'; 32 | const wrapper = mount(); 33 | assert(wrapper.find('input').getDOMNode().getAttribute('aria-labelledby') === labelId); 34 | assert(wrapper.find('label').getDOMNode().getAttribute('id') === labelId); 35 | }); 36 | 37 | it('should animate when checked', createTest(() => { 38 | const wrapper = mount(); 39 | setTimeout(() => { 40 | wrapper.setProps({checked: true}); 41 | }, 500); 42 | })); 43 | 44 | it('should set keyboardFocused state to true on tab press', createTest(() => { 45 | const wrapper = mount(); 46 | wrapper.simulate('mouseup'); 47 | wrapper.find('input').simulate('keyUp', {keyCode: keycode('tab')}); 48 | assert(wrapper.state('keyboardFocused')); 49 | })); 50 | 51 | it('should set keyboardFocused state to true on space press', createTest(() => { 52 | const wrapper = mount(); 53 | wrapper.simulate('mouseup'); 54 | wrapper.find('input').simulate('keyUp', {keyCode: keycode('space')}); 55 | assert(wrapper.state('keyboardFocused')); 56 | })); 57 | 58 | it('should set keyboardFocused state to false when blurred', createTest(() => { 59 | const wrapper = mount(); 60 | wrapper.find('input').simulate('focus'); 61 | wrapper.find('input').simulate('blur'); 62 | assert(!wrapper.state('keyboardFocused')); 63 | })); 64 | 65 | it('should use custom color when checked', createTest(() => { 66 | const wrapper = mount(); 67 | assert(wrapper.find(`.${Styles.track}`).getDOMNode().style.backgroundColor === 'rgb(255, 255, 255)'); 68 | })); 69 | }); 70 | -------------------------------------------------------------------------------- /src/Switch/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './Switch'; 2 | -------------------------------------------------------------------------------- /src/Table/Table.css: -------------------------------------------------------------------------------- 1 | .container { 2 | overflow-x: auto; 3 | } 4 | 5 | .table { 6 | border-spacing: 0; 7 | border-collapse: collapse; 8 | overflow: hidden; 9 | width: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /src/Table/Table.js: -------------------------------------------------------------------------------- 1 | import makeClass from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import Styles from './Table.css'; 5 | 6 | class Table extends React.Component { 7 | render() { 8 | const {children, className, ...other} = this.props; 9 | return ( 10 |
11 | 12 | {children} 13 |
14 |
15 | ); 16 | } 17 | } 18 | 19 | Table.propTypes = { 20 | children: PropTypes.node, 21 | className: PropTypes.string 22 | }; 23 | 24 | Table.defaultProps = { 25 | children: null, 26 | className: '' 27 | }; 28 | 29 | export default Table; 30 | -------------------------------------------------------------------------------- /src/Table/Table.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import React from 'react'; 5 | import {createMount} from '../../test/utils'; 6 | import Table, { 7 | TableBody, TableCell, TableHead, TableRow 8 | } from '.'; 9 | 10 | describe('Tabs', () => { 11 | let mount; 12 | 13 | beforeEach(() => { 14 | mount = createMount(); 15 | }); 16 | 17 | afterEach(() => { 18 | mount.cleanUp(); 19 | }); 20 | 21 | it('should deep render', () => { 22 | const component = ( 23 | 24 | 25 | 26 | Name 27 | Age 28 | Street Address 29 | ZIP Code 30 | State 31 | 32 | 33 | 34 | 35 | Johnathan Doe 36 | 25 37 | 700 1st Ave 38 | 90210 39 | CA 40 | 41 | 42 | Jane Doe 43 | 23 44 | 15 Spruce St 45 | 92101 46 | CA 47 | 48 | 49 |
50 | ); 51 | const wrapper = mount(component); 52 | assert(wrapper); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/Table/TableBody.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | class TableBody extends React.Component { 5 | render() { 6 | const {children, ...other} = this.props; 7 | return ( 8 | 9 | {children} 10 | 11 | ); 12 | } 13 | } 14 | 15 | TableBody.propTypes = { 16 | children: PropTypes.node 17 | }; 18 | 19 | TableBody.defaultProps = { 20 | children: null 21 | }; 22 | 23 | export default TableBody; 24 | -------------------------------------------------------------------------------- /src/Table/TableCell.css: -------------------------------------------------------------------------------- 1 | .cell { 2 | border-bottom: 1px solid rgb(235, 235, 235); 3 | color: $black87; 4 | font-size: 13px; 5 | height: 48px; 6 | padding-left: 24px; 7 | padding-right: 56px; 8 | white-space: nowrap; 9 | } 10 | 11 | .cell:first-child { 12 | text-align: left; 13 | } 14 | 15 | .cell:not(:first-child) { 16 | text-align: right; 17 | } 18 | 19 | .head { 20 | color: $black54; 21 | font-size: 12px; 22 | font-weight: 500; 23 | height: 56px; 24 | } 25 | -------------------------------------------------------------------------------- /src/Table/TableCell.js: -------------------------------------------------------------------------------- 1 | import makeClass from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import Styles from './TableCell.css'; 5 | 6 | class TableCell extends React.Component { 7 | render() { 8 | const { 9 | children, 10 | className, 11 | head, 12 | ...other 13 | } = this.props; 14 | const Component = head ? 'th' : 'td'; 15 | const classnames = makeClass(Styles.cell, { 16 | [Styles.head]: head 17 | }, className); 18 | 19 | return ( 20 | 21 | {children} 22 | 23 | ); 24 | } 25 | } 26 | 27 | TableCell.propTypes = { 28 | children: PropTypes.node, 29 | className: PropTypes.string, 30 | head: PropTypes.bool 31 | }; 32 | 33 | TableCell.defaultProps = { 34 | children: null, 35 | className: '', 36 | head: false 37 | }; 38 | 39 | export default TableCell; 40 | -------------------------------------------------------------------------------- /src/Table/TableHead.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | class TableHead extends React.Component { 5 | renderChildren() { 6 | const {children} = this.props; 7 | return React.Children.map(children, child => ( 8 | React.cloneElement(child, {head: true}) 9 | )); 10 | } 11 | 12 | render() { 13 | const {children, ...other} = this.props; 14 | return ( 15 | 16 | {this.renderChildren()} 17 | 18 | ); 19 | } 20 | } 21 | 22 | TableHead.propTypes = { 23 | children: PropTypes.node 24 | }; 25 | 26 | TableHead.defaultProps = { 27 | children: null 28 | }; 29 | 30 | export default TableHead; 31 | -------------------------------------------------------------------------------- /src/Table/TableRow.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | class TableRow extends React.Component { 5 | renderChildren() { 6 | const {children, head} = this.props; 7 | return React.Children.map(children, child => ( 8 | React.cloneElement(child, {head}) 9 | )); 10 | } 11 | 12 | render() { 13 | const {children, head, ...other} = this.props; 14 | return ( 15 | 16 | {this.renderChildren()} 17 | 18 | ); 19 | } 20 | } 21 | 22 | TableRow.propTypes = { 23 | children: PropTypes.node, 24 | head: PropTypes.bool 25 | }; 26 | 27 | TableRow.defaultProps = { 28 | children: null, 29 | head: false 30 | }; 31 | 32 | export default TableRow; 33 | -------------------------------------------------------------------------------- /src/Table/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './Table'; 2 | export {default as TableBody} from './TableBody'; 3 | export {default as TableCell} from './TableCell'; 4 | export {default as TableHead} from './TableHead'; 5 | export {default as TableRow} from './TableRow'; 6 | -------------------------------------------------------------------------------- /src/Tabs/ScrollbarSize.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import EventListener from 'react-event-listener'; 4 | import debounce from 'debounce'; // < 1kb payload overhead when lodash/debounce is > 3kb. 5 | 6 | const styles = { 7 | width: 100, 8 | height: 100, 9 | position: 'absolute', 10 | top: -10000, 11 | overflow: 'scroll', 12 | msOverflowStyle: 'scrollbar' 13 | }; 14 | 15 | /** 16 | * @ignore - internal component. 17 | * The component is originates from https://github.com/STORIS/react-scrollbar-size. 18 | * It has been moved into the core in order to minimize the bundle size. 19 | */ 20 | class ScrollbarSize extends React.Component { 21 | handleResize = debounce(() => { 22 | const {onChange} = this.props; 23 | 24 | const prevHeight = this.scrollbarHeight; 25 | const prevWidth = this.scrollbarWidth; 26 | this.setMeasurements(); 27 | if (prevHeight !== this.scrollbarHeight || prevWidth !== this.scrollbarWidth) { 28 | onChange({scrollbarHeight: this.scrollbarHeight, scrollbarWidth: this.scrollbarWidth}); 29 | } 30 | }, 166); // Corresponds to 10 frames at 60 Hz. 31 | 32 | componentDidMount() { 33 | this.setMeasurements(); 34 | this.props.onLoad({ 35 | scrollbarHeight: this.scrollbarHeight, 36 | scrollbarWidth: this.scrollbarWidth 37 | }); 38 | } 39 | 40 | componentWillUnmount() { 41 | this.handleResize.clear(); 42 | } 43 | 44 | setMeasurements = () => { 45 | const {nodeRef} = this; 46 | 47 | if (!nodeRef) { 48 | return; 49 | } 50 | 51 | this.scrollbarHeight = nodeRef.offsetHeight - nodeRef.clientHeight; 52 | this.scrollbarWidth = nodeRef.offsetWidth - nodeRef.clientWidth; 53 | }; 54 | 55 | render() { 56 | const {onChange} = this.props; 57 | 58 | return ( 59 |
60 | {onChange ? : null} 61 |
{ 64 | this.nodeRef = ref; 65 | }} 66 | /> 67 |
68 | ); 69 | } 70 | } 71 | 72 | ScrollbarSize.propTypes = { 73 | onChange: PropTypes.func.isRequired, 74 | onLoad: PropTypes.func.isRequired 75 | }; 76 | 77 | export default ScrollbarSize; 78 | -------------------------------------------------------------------------------- /src/Tabs/ScrollbarSize.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import EventListener from 'react-event-listener'; 3 | import React from 'react'; 4 | import {spy, useFakeTimers} from 'sinon'; 5 | import {createMount, createShallow} from '../../test/utils'; 6 | import ScrollbarSize from './ScrollbarSize'; 7 | 8 | describe('', () => { 9 | const defaultProps = { 10 | onLoad: () => {}, 11 | onChange: () => {} 12 | }; 13 | let clock; 14 | let mount; 15 | let shallow; 16 | 17 | before(() => { 18 | clock = useFakeTimers(); 19 | }); 20 | 21 | beforeEach(() => { 22 | mount = createMount(); 23 | shallow = createShallow(); 24 | }); 25 | 26 | afterEach(() => { 27 | mount.cleanUp(); 28 | shallow.cleanUp(); 29 | }); 30 | 31 | after(() => { 32 | clock.restore(); 33 | }); 34 | 35 | describe('prop: onLoad', () => { 36 | it('should not call on initial load', () => { 37 | const onLoad = spy(); 38 | mount(); 39 | assert(onLoad.callCount === 0); 40 | }); 41 | 42 | it('should call on initial load', () => { 43 | const onLoad = spy(); 44 | mount(); 45 | assert(onLoad.callCount === 1); 46 | }); 47 | }); 48 | 49 | describe('prop: onChange', () => { 50 | let onChange; 51 | let wrapper; 52 | 53 | beforeEach(() => { 54 | onChange = spy(); 55 | wrapper = shallow(); 56 | const instance = wrapper.instance(); 57 | instance.nodeRef = { 58 | offsetHeight: 17, 59 | clientHeight: 0, 60 | offsetWidth: 17, 61 | clientWidth: 0 62 | }; 63 | }); 64 | 65 | it('should call on first resize event', () => { 66 | wrapper.find(EventListener).simulate('resize'); 67 | clock.tick(166); 68 | assert(onChange.callCount === 1); 69 | assert(onChange.calledWith({scrollbarHeight: 17, scrollbarWidth: 17}) === true); 70 | }); 71 | 72 | it('should not call on second resize event', () => { 73 | wrapper.find(EventListener).simulate('resize'); 74 | clock.tick(166); 75 | assert(onChange.callCount === 1); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/Tabs/Tab.css: -------------------------------------------------------------------------------- 1 | .tab { 2 | display: inline-block; 3 | } 4 | 5 | .tabFixed { 6 | flex: 1; 7 | } 8 | 9 | .button { 10 | border-radius: 0; 11 | margin: 0; 12 | padding: 0; 13 | box-shadow: none; 14 | } 15 | 16 | .label { 17 | padding: 15px 16px; 18 | } 19 | 20 | .indicator { 21 | position: absolute; 22 | bottom: 0; 23 | left: 0; 24 | right: 0; 25 | height: 2px; 26 | background-color: $accent; 27 | margin-top: -2px; 28 | } 29 | -------------------------------------------------------------------------------- /src/Tabs/Tab.js: -------------------------------------------------------------------------------- 1 | import makeClass from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import Button from '../Button'; 5 | import Variables from '../variables'; 6 | import Styles from './Tab.css'; 7 | 8 | class Tab extends React.Component { 9 | renderIndicator = () => ( 10 |
15 | ) 16 | 17 | render() { 18 | const { 19 | label, selected, type, index, indexChanged, 20 | indicatorColor, ...other 21 | } = this.props; 22 | const showInitialIndicator = selected && !indexChanged; 23 | const isFixed = type === 'fixed'; 24 | 25 | return ( 26 | 39 | ); 40 | } 41 | } 42 | 43 | Tab.defaultProps = { 44 | index: null, 45 | indexChanged: true, 46 | indicatorColor: Variables.$accent, 47 | label: null, 48 | selected: false, 49 | type: 'fixed' 50 | }; 51 | 52 | Tab.propTypes = { 53 | index: PropTypes.number, 54 | indexChanged: PropTypes.bool, 55 | indicatorColor: PropTypes.string, 56 | label: PropTypes.string, 57 | selected: PropTypes.bool, 58 | type: PropTypes.oneOf(['fixed', 'scrollable', 'centered']) 59 | }; 60 | 61 | export default Tab; 62 | -------------------------------------------------------------------------------- /src/Tabs/Tab.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import React from 'react'; 5 | import {createShallow, createMount} from '../../test/utils'; 6 | import Tab from './Tab'; 7 | 8 | describe('Tab', () => { 9 | let shallow; 10 | let mount; 11 | 12 | beforeEach(() => { 13 | shallow = createShallow(); 14 | mount = createMount(); 15 | }); 16 | 17 | afterEach(() => { 18 | shallow.cleanUp(); 19 | mount.cleanUp(); 20 | }); 21 | 22 | it('should shallow render', () => { 23 | const wrapper = shallow(); 24 | assert(wrapper); 25 | }); 26 | 27 | it('should deep render', () => { 28 | const wrapper = mount(); 29 | assert(wrapper); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/Tabs/Tabs.css: -------------------------------------------------------------------------------- 1 | .tabs { 2 | overflow: hidden; 3 | white-space: nowrap; 4 | width: 100%; 5 | } 6 | 7 | .tabsInner { 8 | overflow-x: scroll; 9 | overflow-y: hidden; 10 | padding: 0; 11 | } 12 | 13 | .tabList { 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | .tabListFixed { 19 | display: flex; 20 | } 21 | 22 | .indicator { 23 | transition: all 250ms ease; 24 | height: 2px; 25 | margin-top: -2px; 26 | } 27 | -------------------------------------------------------------------------------- /src/Tabs/Tabs.js: -------------------------------------------------------------------------------- 1 | import {findDOMNode} from 'react-dom'; 2 | import makeClass from 'classnames'; 3 | import PropTypes from 'prop-types'; 4 | import React from 'react'; 5 | import Variables from '../variables'; 6 | import ScrollbarSize from './ScrollbarSize'; 7 | import Styles from './Tabs.css'; 8 | 9 | class Tabs extends React.Component { 10 | state = { 11 | indicatorLeft: 0, 12 | indicatorWidth: 0, 13 | scrollbarHeight: 0, 14 | indexChanged: false 15 | }; 16 | 17 | componentDidMount() { 18 | this.setIndicatorStyles(); 19 | window.addEventListener('resize', this.setIndicatorStyles); 20 | } 21 | 22 | componentWillReceiveProps(nextProps) { 23 | if (this.props.index !== nextProps.index) { 24 | this.setState({indexChanged: true}); 25 | } 26 | } 27 | 28 | componentDidUpdate({index, type}) { 29 | if ( 30 | this.props.index !== index || 31 | this.props.type !== type 32 | ) { 33 | this.setIndicatorStyles(); 34 | } 35 | } 36 | 37 | componentWillUnmount() { 38 | window.removeEventListener('resize', this.setIndicatorStyles); 39 | clearTimeout(this.timeout); 40 | } 41 | 42 | onClick = (e, index) => { 43 | this.props.onChange(e, index); 44 | } 45 | 46 | getMetadata = nextIndex => { 47 | const meta = {}; 48 | if (this.tabsInner) { 49 | meta.tabsMeta = this.tabsInner.getBoundingClientRect(); 50 | meta.tabsMeta.scrollLeft = this.tabsInner.scrollLeft; 51 | } 52 | const index = typeof nextIndex === 'number' ? nextIndex : this.props.index; 53 | const currentTab = findDOMNode(this.tabs[index]); 54 | if (currentTab) { 55 | meta.tabMeta = currentTab.getBoundingClientRect(); 56 | } 57 | return meta; 58 | } 59 | 60 | setIndicatorStyles = nextIndex => { 61 | const {tabsMeta, tabMeta} = this.getMetadata(nextIndex); 62 | const indicatorLeft = `${tabMeta.left + (tabsMeta.scrollLeft - tabsMeta.left)}px`; 63 | const indicatorWidth = `${tabMeta.width}px`; 64 | if ( 65 | this.state.indicatorLeft !== indicatorLeft || 66 | this.state.indicatorWidth !== indicatorWidth 67 | ) { 68 | this.setState({ 69 | indicatorLeft, 70 | indicatorWidth 71 | }); 72 | } 73 | } 74 | 75 | tabs = {}; 76 | 77 | scrollbarSizeLoad = ({scrollbarHeight}) => { 78 | this.setState({scrollbarHeight}); 79 | } 80 | 81 | scrollbarSizeChange = scrollbarHeight => { 82 | this.setState({scrollbarHeight}); 83 | } 84 | 85 | registerTab = c => { 86 | if (c) { 87 | this.tabs[c.props.index] = c; 88 | } 89 | } 90 | 91 | registerTabsInner = c => { 92 | this.tabsInner = c; 93 | } 94 | 95 | renderTabs = () => { 96 | const { 97 | children, indicatorColor, textColor, type 98 | } = this.props; 99 | const {indexChanged} = this.state; 100 | return React.Children.map(children, (tab, i) => ( 101 | React.cloneElement(tab, { 102 | index: i, 103 | indexChanged, 104 | indicatorColor, 105 | onClick: e => (this.onClick(e, i)), 106 | ref: this.registerTab, 107 | selected: i === this.props.index, 108 | style: { 109 | boxShadow: 'none' 110 | }, 111 | textColor, 112 | type 113 | }) 114 | )); 115 | } 116 | 117 | render() { 118 | const { 119 | barColor, className, style, type, 120 | indicatorColor, textColor, index, ...other 121 | } = this.props; 122 | const isFixed = type === 'fixed'; 123 | const isCentered = type === 'centered'; 124 | const {scrollbarHeight} = this.state; 125 | return ( 126 |
133 |
141 |
147 | {this.renderTabs()} 148 |
149 | {this.state.indexChanged && ( 150 |
159 | )} 160 |
161 |
162 | 166 |
167 |
168 | ); 169 | } 170 | } 171 | 172 | Tabs.defaultProps = { 173 | barColor: Variables.$primary, 174 | children: null, 175 | className: null, 176 | index: 0, 177 | indicatorColor: Variables.$accent, 178 | onChange: () => {}, 179 | style: {}, 180 | textColor: '#fff', 181 | type: 'fixed' 182 | }; 183 | 184 | Tabs.propTypes = { 185 | barColor: PropTypes.string, 186 | children: PropTypes.node, 187 | className: PropTypes.string, 188 | index: PropTypes.number, 189 | indicatorColor: PropTypes.string, 190 | onChange: PropTypes.func, 191 | style: PropTypes.object, 192 | textColor: PropTypes.string, 193 | type: PropTypes.oneOf(['fixed', 'scrollable', 'centered']) 194 | }; 195 | 196 | export default Tabs; 197 | -------------------------------------------------------------------------------- /src/Tabs/Tabs.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import React from 'react'; 5 | import sinon from 'sinon'; 6 | import {createMount, createTest} from '../../test/utils'; 7 | import Styles from './Tabs.css'; 8 | import Tabs, {Tab} from '.'; 9 | 10 | describe('Tabs', () => { 11 | let mount; 12 | 13 | beforeEach(() => { 14 | mount = createMount(); 15 | }); 16 | 17 | afterEach(() => { 18 | mount.cleanUp(); 19 | }); 20 | 21 | it('should deep render', () => { 22 | const component = ( 23 | 24 | 25 | 26 | ); 27 | const wrapper = mount(component); 28 | assert(wrapper); 29 | }); 30 | 31 | it('should call onChange function with new index when a tab is clicked', createTest(() => { 32 | const onChange = sinon.spy(); 33 | const component = ( 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | const wrapper = mount(component); 41 | assert(wrapper); 42 | const newIndex = 2; 43 | wrapper.find('button').at(newIndex).simulate('click'); 44 | assert(onChange.calledOnce); 45 | // Second parameter should be index 46 | assert(onChange.args[0][1] === newIndex); 47 | })); 48 | 49 | it('should repaint the indicator when the index changes', createTest(() => { 50 | const component = ( 51 | {}}> 52 | 53 | 54 | 55 | 56 | ); 57 | const wrapper = mount(component); 58 | 59 | const numberPattern = /\d+/g; 60 | wrapper.setProps({index: 2}); 61 | 62 | setTimeout(() => { 63 | const node = wrapper.find(`.${Styles.indicator}`).getDOMNode(); 64 | const newTransform = node.style.transform.match(numberPattern)[0]; 65 | assert(newTransform !== 0); 66 | }, 100); 67 | })); 68 | 69 | it('should repaint the indicator on window resize', createTest(() => { 70 | const component = ( 71 | {}}> 72 | 73 | 74 | 75 | 76 | ); 77 | const wrapper = mount(component); 78 | 79 | const numberPattern = /\d+/g; 80 | wrapper.setProps({index: 2}); 81 | 82 | setTimeout(() => { 83 | wrapper.getDOMNode().style.width = '200px'; 84 | }, 400); 85 | 86 | setTimeout(() => { 87 | const event = document.createEvent('UIEvents'); 88 | event.initUIEvent('resize', true, false, window, 0); 89 | window.dispatchEvent(event); 90 | }, 700); 91 | 92 | setTimeout(() => { 93 | const node = wrapper.find(`.${Styles.indicator}`).getDOMNode(); 94 | const endLeftOffset = node.style.transform.match(numberPattern)[0]; 95 | assert(parseInt(endLeftOffset, 10) !== 0); 96 | }, 900); 97 | })); 98 | }); 99 | -------------------------------------------------------------------------------- /src/Tabs/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './Tabs'; 2 | export {default as Tab} from './Tab'; 3 | -------------------------------------------------------------------------------- /src/TextField/TextField.css: -------------------------------------------------------------------------------- 1 | .root { 2 | display: inline-block; 3 | position: relative; 4 | width: 100%; 5 | } 6 | 7 | .input { 8 | background-color: transparent; 9 | border: 0; 10 | outline: 0; 11 | margin-top: 32px; 12 | display: block; 13 | width: 100%; 14 | padding: 8px 0; 15 | font-size: 16px; 16 | font-family: inherit; 17 | -webkit-tap-highlight-color: transparent; 18 | } 19 | 20 | .input:disabled { 21 | cursor: not-allowed; 22 | } 23 | 24 | .input::placeholder { 25 | color: rgba(0, 0, 0, 0.42); 26 | } 27 | 28 | .inkbar { 29 | border-bottom-width: 1px; 30 | border-bottom-color: rgba(0, 0, 0, 0.42); 31 | border-bottom-style: solid; 32 | box-sizing: border-box; 33 | height: 2px; 34 | animation-duration: 250ms; 35 | animation-timing-function: easeOutFunction; 36 | animation-fill-mode: forwards; 37 | } 38 | 39 | .input:disabled ~ .inkbar { 40 | border-bottom-style: dashed; 41 | } 42 | 43 | .input:focus ~ .inkbar { 44 | animation-name: stretch; 45 | border-bottom-color: $primary; 46 | max-width: 70%; 47 | } 48 | 49 | .input:focus ~ .inkbar, 50 | .input:hover:not(:disabled) ~ .inkbar { 51 | border-bottom-width: 2px; 52 | } 53 | 54 | @keyframes stretch { 55 | 0% { 56 | margin-left: 15%; 57 | max-width: 70%; 58 | } 59 | 60 | 100% { 61 | max-width: 100%; 62 | margin-left: 0%; 63 | } 64 | } 65 | 66 | .label { 67 | color: rgba(0, 0, 0, 0.54); 68 | position: absolute; 69 | top: 40px; 70 | pointer-events: none; 71 | animation-duration: 250ms; 72 | animation-timing-function: $easeOutFunction; 73 | font-size: 16px; 74 | transform-origin: left top 0; 75 | animation-fill-mode: forwards; 76 | } 77 | 78 | .input.hasPlaceholder + .label, 79 | .input.hasValue + .label, 80 | .input:focus + .label { 81 | transform: $textFieldLabelFloatTransform; 82 | } 83 | 84 | .input:focus + .label { 85 | color: $primary; 86 | } 87 | 88 | .helper { 89 | padding-top: 8px; 90 | font-size: 12px; 91 | line-height: 12px; 92 | color: rgba(0, 0, 0, 0.54); 93 | } 94 | 95 | .shadow { 96 | height: auto; 97 | overflow: hidden; 98 | padding: 0; 99 | margin: 0; 100 | border: 0; 101 | box-sizing: border-box; 102 | visibility: hidden; 103 | pointer-events: none; 104 | margin-top: 40px; 105 | margin-bottom: -40px; 106 | position: absolute; 107 | resize: none; 108 | font-family: inherit; 109 | font-size: inherit; 110 | line-height: 20px; 111 | width: 100%; 112 | } 113 | 114 | .textarea { 115 | height: auto; 116 | resize: none; 117 | overflow-y: hidden; 118 | transition: all 0.3s $easeOutFunction; 119 | box-sizing: border-box; 120 | padding: 0; 121 | margin: 0; 122 | margin-top: 40px; 123 | position: relative; 124 | margin-bottom: -20px; 125 | line-height: 20px; 126 | } 127 | -------------------------------------------------------------------------------- /src/TextField/TextField.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import {noop} from 'lodash'; 5 | import React from 'react'; 6 | import {createShallow, createMount, createTest} from '../../test/utils'; 7 | import Styles from './TextField.css'; 8 | import TextField from './TextField'; 9 | import TextFieldAnimations from './TextFieldAnimations.css'; 10 | 11 | const shortText = 'Some text'; 12 | const longText = 'A really,\n really,\n really,\n really,\n really long string.'; 13 | 14 | describe('TextField', () => { 15 | let shallow; 16 | let mount; 17 | 18 | beforeEach(() => { 19 | shallow = createShallow(); 20 | mount = createMount(); 21 | }); 22 | 23 | afterEach(() => { 24 | shallow.cleanUp(); 25 | mount.cleanUp(); 26 | }); 27 | 28 | it('should shallow render', () => { 29 | const wrapper = shallow(); 34 | assert(wrapper); 35 | }); 36 | 37 | it('should add aria attribute', createTest(() => { 38 | const labelId = 'foo-bar'; 39 | const wrapper = mount(); 45 | assert(wrapper.find('input').getDOMNode().getAttribute('aria-labelledby') === labelId); 46 | assert(wrapper.find('label').getDOMNode().getAttribute('id') === labelId); 47 | })); 48 | 49 | it('should render a TextArea if multiline', createTest(() => { 50 | const wrapper = mount(); 56 | assert(wrapper.find('textarea').length === 2); 57 | })); 58 | 59 | it('should set the interal state to focused on focus', createTest(() => { 60 | const wrapper = mount(); 66 | wrapper.find('input').simulate('focus'); 67 | assert(wrapper.state('focused')); 68 | assert(wrapper.find('label').getDOMNode().style.animationName !== TextFieldAnimations.float); 69 | })); 70 | 71 | it('should add the float animation on focus when there is no placeholder and value is empty', createTest(() => { 72 | const wrapper = mount(); 77 | wrapper.find('input').simulate('focus'); 78 | assert(wrapper.find('label').getDOMNode().style.animationName === TextFieldAnimations.float); 79 | })); 80 | 81 | it('should set the interal state to not focused on blur', createTest(() => { 82 | const wrapper = mount(); 88 | wrapper.find('input').simulate('blur'); 89 | assert(!wrapper.state('focused')); 90 | assert(wrapper.find('label').getDOMNode().style.animationName !== TextFieldAnimations.sink); 91 | })); 92 | 93 | it('should add the sink animation class on blur when there is no placeholder and value is empty', createTest(() => { 94 | const wrapper = mount(); 99 | 100 | setTimeout(() => { 101 | wrapper.find('input').simulate('blur'); 102 | }, 250); 103 | 104 | setTimeout(() => { 105 | assert(!wrapper.state('focused')); 106 | assert(wrapper.find('label').getDOMNode().style.animationName === TextFieldAnimations.sink); 107 | }, 500); 108 | })); 109 | 110 | it('should increase the height of the textarea when there is more text', createTest(() => { 111 | const wrapper = mount(); 118 | const textarea = wrapper.find(`.${Styles.textarea}`); 119 | const beginningHeight = parseInt(textarea.getDOMNode().style.height, 10); 120 | setTimeout(() => { 121 | wrapper.setProps({value: longText}); 122 | textarea.simulate('change', {target: {value: longText}}); 123 | }, 500); 124 | 125 | setTimeout(() => { 126 | const height = parseInt(textarea.getDOMNode().style.height, 10); 127 | assert(beginningHeight < height); 128 | }, 750); 129 | })); 130 | 131 | it('should decrease the height of the textarea when there is less text', createTest(() => { 132 | const wrapper = mount(); 139 | const textarea = wrapper.find(`.${Styles.textarea}`); 140 | const heightWithLongText = parseInt(textarea.getDOMNode().style.height, 10); 141 | wrapper.setProps({value: shortText}); 142 | textarea.simulate('change', {target: {value: shortText}}); 143 | const heightWithShortText = parseInt(textarea.getDOMNode().style.height, 10); 144 | assert(heightWithLongText > heightWithShortText); 145 | })); 146 | }); 147 | -------------------------------------------------------------------------------- /src/TextField/TextFieldAnimations.css: -------------------------------------------------------------------------------- 1 | @keyframes sink { 2 | 0% { transform: $textFieldLabelFloatTransform; } 3 | 100% { transform: scale(1) translate(0, 0); } 4 | } 5 | 6 | @keyframes float { 7 | 0% { transform: scale(1) translate(0, 0); } 8 | 100% { transform: $textFieldLabelFloatTransform; } 9 | } 10 | -------------------------------------------------------------------------------- /src/TextField/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './TextField'; 2 | -------------------------------------------------------------------------------- /src/Typography/Typography.css: -------------------------------------------------------------------------------- 1 | /* The Material spec describes tracking/kerning in units of thousandths of an em. 2 | * 3 | * For more information on converting tracking/kerning to letter-spacing, see: 4 | * https://stackoverflow.com/questions/2760784/how-to-calculate-css-letter-spacing-v-s-tracking-in-typography 5 | */ 6 | 7 | .display4 { 8 | font-size: 112px; 9 | font-weight: $typographyWeightLight; 10 | color: $black54; 11 | letter-spacing: -0.01em; 12 | } 13 | 14 | .display3 { 15 | font-size: 56px; 16 | font-weight: $typographyWeightRegular; 17 | line-height: 1.35; 18 | color: $black54; 19 | letter-spacing: -0.005em; 20 | } 21 | 22 | .display2 { 23 | font-size: 45px; 24 | font-weight: $typographyWeightRegular; 25 | line-height: 48px; 26 | color: $black54; 27 | } 28 | 29 | .display1 { 30 | font-size: 34px; 31 | font-weight: $typographyWeightRegular; 32 | line-height: 40px; 33 | color: $black54; 34 | } 35 | 36 | .headline { 37 | font-size: 24px; 38 | font-weight: $typographyWeightRegular; 39 | line-height: 32px; 40 | color: $black87; 41 | } 42 | 43 | .title { 44 | font-size: 21px; 45 | font-weight: $typographyWeightMedium; 46 | color: $black87; 47 | letter-spacing: 0.005em; 48 | } 49 | 50 | .subheading { 51 | font-size: 16px; 52 | font-weight: $typographyWeightRegular; 53 | line-height: 24px; 54 | color: $black87; 55 | letter-spacing: 0.01em; 56 | } 57 | 58 | .body2 { 59 | font-size: 14px; 60 | font-weight: $typographyWeightMedium; 61 | line-height: 24px; 62 | color: $black87; 63 | letter-spacing: 0.01em; 64 | } 65 | 66 | .body1 { 67 | font-size: 14px; 68 | font-weight: $typographyWeightRegular; 69 | line-height: 20px; 70 | color: $black87; 71 | letter-spacing: 0.01em; 72 | } 73 | 74 | .caption { 75 | font-size: 12px; 76 | font-weight: $typographyWeightRegular; 77 | line-height: 1; 78 | color: $black54; 79 | letter-spacing: 0.02em; 80 | } 81 | 82 | .button { 83 | text-transform: uppercase; 84 | font-weight: $typographyWeightMedium; 85 | color: $black87; 86 | letter-spacing: 0.01em; 87 | } 88 | -------------------------------------------------------------------------------- /src/Typography/Typography.js: -------------------------------------------------------------------------------- 1 | import makeClass from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import Styles from './Typography.css'; 5 | 6 | function Typography({ 7 | children, className, component, type, ...other 8 | }) { 9 | const Component = component; 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | } 16 | 17 | Typography.defaultProps = { 18 | children: null, 19 | className: null, 20 | component: 'div', 21 | type: 'body1' 22 | }; 23 | 24 | Typography.propTypes = { 25 | children: PropTypes.node, 26 | className: PropTypes.string, 27 | component: PropTypes.node, 28 | type: PropTypes.oneOf([ 29 | 'display4', 30 | 'display3', 31 | 'display2', 32 | 'display1', 33 | 'headline', 34 | 'title', 35 | 'subheading', 36 | 'body2', 37 | 'body1', 38 | 'caption', 39 | 'button' 40 | ]) 41 | }; 42 | 43 | export default Typography; 44 | -------------------------------------------------------------------------------- /src/Typography/Typography.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import React from 'react'; 5 | import {createShallow} from '../../test/utils'; 6 | import Styles from './Typography.css'; 7 | import Typography from './Typography'; 8 | 9 | describe('Typography', () => { 10 | let shallow; 11 | 12 | beforeEach(() => { 13 | shallow = createShallow(); 14 | }); 15 | 16 | afterEach(() => { 17 | shallow.cleanUp(); 18 | }); 19 | 20 | it('should shallow render', () => { 21 | const wrapper = shallow(); 22 | assert(wrapper); 23 | }); 24 | 25 | it('should use custom component', () => { 26 | const wrapper = shallow(); 27 | assert(wrapper.find('span').length === 1); 28 | }); 29 | 30 | it('should apply custom typestyle class name', () => { 31 | const wrapper = shallow(); 32 | assert(wrapper.find(`.${Styles.display4}`).length === 1); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/Typography/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './Typography'; 2 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export {default as AppBar} from './AppBar'; 2 | export {default as BottomNavigation, BottomNavigationItem} from './BottomNavigation'; 3 | export {default as Button} from './Button'; 4 | export {default as Collapse} from './Collapse'; 5 | export {default as Dialog} from './Dialog'; 6 | export {default as Grid, GridItem} from './Grid'; 7 | export {default as List, ListItem} from './List'; 8 | export {default as Paper} from './Paper'; 9 | export {default as Ripple, RippleItem} from './Ripple'; 10 | export {default as Scrollable} from './Scrollable'; 11 | export {default as SnackBar, SnackBarItem} from './SnackBar'; 12 | export {default as SvgIcon} from './SvgIcon'; 13 | export {default as Switch} from './Switch'; 14 | export {default as Table, TableBody, TableCell, TableHead, TableRow} from './Table'; 15 | export {default as Tabs, Tab} from './Tabs'; 16 | export {default as TextField} from './TextField'; 17 | export {default as Typography} from './Typography'; 18 | -------------------------------------------------------------------------------- /src/utils/DOMBodyRender.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {render, unmountComponentAtNode} from 'react-dom'; 4 | 5 | class DOMBodyRender extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.node = null; 9 | this.renderNode = this.renderNode.bind(this); 10 | this.unrenderNode = this.unrenderNode.bind(this); 11 | } 12 | 13 | componentDidMount() { 14 | this.renderNode(); 15 | } 16 | 17 | componentDidUpdate() { 18 | this.renderNode(); 19 | } 20 | 21 | componentWillUnmount() { 22 | this.unrenderNode(); 23 | } 24 | 25 | unrenderNode() { 26 | if (!this.node) { 27 | return; 28 | } 29 | unmountComponentAtNode(this.node); 30 | if (document && document.body) { 31 | document.body.removeChild(this.node); 32 | } 33 | } 34 | 35 | renderNode() { 36 | if (!this.node) { 37 | this.node = document.createElement('div'); 38 | if (document && document.body) { 39 | document.body.appendChild(this.node); 40 | } 41 | } 42 | render(this.props.children, this.node); 43 | } 44 | 45 | render() { 46 | return null; 47 | } 48 | } 49 | 50 | DOMBodyRender.defaultProps = { 51 | children: null 52 | }; 53 | 54 | DOMBodyRender.propTypes = { 55 | children: PropTypes.node 56 | }; 57 | 58 | export default DOMBodyRender; 59 | -------------------------------------------------------------------------------- /src/utils/DOMBodyRender.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import React from 'react'; 5 | import {createMount} from '../../test/utils'; 6 | import DOMBodyRender from './DOMBodyRender'; 7 | 8 | describe('DOMBodyRender', () => { 9 | let mount; 10 | 11 | beforeEach(() => { 12 | mount = createMount(); 13 | }); 14 | 15 | afterEach(() => { 16 | mount.cleanUp(); 17 | }); 18 | 19 | it('should deep render', () => { 20 | const text = 'Foo bar'; 21 | const component = ( 22 | 23 |
{text}
24 |
25 | ); 26 | const wrapper = mount(component); 27 | const {node} = wrapper.instance(); 28 | assert(node.innerHTML.indexOf(text) > -1); 29 | }); 30 | 31 | it('should update DOM when children change', () => { 32 | const oldText = 'Hello, World!'; 33 | const component = ( 34 | 35 |
{oldText}
36 |
37 | ); 38 | const wrapper = mount(component); 39 | const {node} = wrapper.instance(); 40 | assert(node.innerHTML.indexOf(oldText) > -1); 41 | const newText = 'New content'; 42 | wrapper.setProps({children:
{newText}
}); 43 | assert(node.innerHTML.indexOf(oldText) === -1); 44 | assert(node.innerHTML.indexOf(newText) > -1); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/variables.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | $elevation1: 'none', 3 | $elevation2: '0 1px 3px 0 rgba(0,0,0,.2),0 1px 1px 0 rgba(0,0,0,.14),0 2px 1px -1px rgba(0,0,0,.12)', 4 | $elevation3: '0 1px 5px 0 rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.12)', 5 | $elevation4: '0 1px 8px 0 rgba(0,0,0,.2),0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.12)', 6 | $elevation5: '0 2px 4px -1px rgba(0,0,0,.2),0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12)', 7 | $elevation6: '0 3px 5px -1px rgba(0,0,0,.2),0 5px 8px 0 rgba(0,0,0,.14),0 1px 14px 0 rgba(0,0,0,.12)', 8 | $elevation7: '0 3px 5px -1px rgba(0,0,0,.2),0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12)', 9 | $elevation8: '0 4px 5px -2px rgba(0,0,0,.2),0 7px 10px 1px rgba(0,0,0,.14),0 2px 16px 1px rgba(0,0,0,.12)', 10 | $elevation9: '0 5px 5px -3px rgba(0,0,0,.2),0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12)', 11 | $elevation10: '0 5px 6px -3px rgba(0,0,0,.2),0 9px 12px 1px rgba(0,0,0,.14),0 3px 16px 2px rgba(0,0,0,.12)', 12 | $elevation11: '0 6px 6px -3px rgba(0,0,0,.2),0 10px 14px 1px rgba(0,0,0,.14),0 4px 18px 3px rgba(0,0,0,.12)', 13 | $elevation12: '0 6px 7px -4px rgba(0,0,0,.2),0 11px 15px 1px rgba(0,0,0,.14),0 4px 20px 3px rgba(0,0,0,.12)', 14 | $elevation13: '0 7px 8px -4px rgba(0,0,0,.2),0 12px 17px 2px rgba(0,0,0,.14),0 5px 22px 4px rgba(0,0,0,.12)', 15 | $elevation14: '0 7px 8px -4px rgba(0,0,0,.2),0 13px 19px 2px rgba(0,0,0,.14),0 5px 24px 4px rgba(0,0,0,.12)', 16 | $elevation15: '0 7px 9px -4px rgba(0,0,0,.2),0 14px 21px 2px rgba(0,0,0,.14),0 5px 26px 4px rgba(0,0,0,.12)', 17 | $elevation16: '0 8px 9px -5px rgba(0,0,0,.2),0 15px 22px 2px rgba(0,0,0,.14),0 6px 28px 5px rgba(0,0,0,.12)', 18 | $elevation17: '0 8px 10px -5px rgba(0,0,0,.2),0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12)', 19 | $elevation18: '0 8px 11px -5px rgba(0,0,0,.2),0 17px 26px 2px rgba(0,0,0,.14),0 6px 32px 5px rgba(0,0,0,.12)', 20 | $elevation19: '0 9px 11px -5px rgba(0,0,0,.2),0 18px 28px 2px rgba(0,0,0,.14),0 7px 34px 6px rgba(0,0,0,.12)', 21 | $elevation20: '0 9px 12px -6px rgba(0,0,0,.2),0 19px 29px 2px rgba(0,0,0,.14),0 7px 36px 6px rgba(0,0,0,.12)', 22 | $elevation21: '0 10px 13px -6px rgba(0,0,0,.2),0 20px 31px 3px rgba(0,0,0,.14),0 8px 38px 7px rgba(0,0,0,.12)', 23 | $elevation22: '0 10px 13px -6px rgba(0,0,0,.2),0 21px 33px 3px rgba(0,0,0,.14),0 8px 40px 7px rgba(0,0,0,.12)', 24 | $elevation23: '0 10px 14px -6px rgba(0,0,0,.2),0 22px 35px 3px rgba(0,0,0,.14),0 8px 42px 7px rgba(0,0,0,.12)', 25 | $elevation24: '0 11px 14px -7px rgba(0,0,0,.2),0 23px 36px 3px rgba(0,0,0,.14),0 9px 44px 8px rgba(0,0,0,.12)', 26 | $elevation25: '0 11px 15px -7px rgba(0,0,0,.2),0 24px 38px 3px rgba(0,0,0,.14),0 9px 46px 8px rgba(0,0,0,.12)', 27 | $easeOutFunction: 'cubic-bezier(.23,1,.32,1)', 28 | $rippleEaseOutFunction: 'cubic-bezier(.4,0,.2,1)', 29 | $primary: '#2196f3', 30 | $accent: '#ff4081', 31 | $borderColor: '#e0e0e0', 32 | $red500: '#f44336', 33 | $orange700: '#f57c00', 34 | $black: '#000', 35 | $black54: 'rgba(0,0,0,.54)', 36 | $black87: 'rgba(0,0,0,.87)', 37 | $white: '#fff', 38 | $textFieldLabelFloatTransform: 'scale(.75) translate(0,-25px)', 39 | $typographyWeightLight: '300', 40 | $typographyWeightRegular: '400', 41 | $typographyWeightMedium: '500', 42 | $screenSm: '600px', 43 | $screenMd: '960px', 44 | $screenLg: '1280px', 45 | $screenXl: '1920px' 46 | }; 47 | -------------------------------------------------------------------------------- /test/context.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 19 | 20 | 24 | 25 | 32 | 33 | %SCRIPTS% 34 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /test/ssr.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import assert from 'assert'; 4 | import Edit from 'material-design-icons/editor/svg/production/ic_mode_edit_24px.svg'; 5 | import React from 'react'; 6 | import ReactDOMServer from 'react-dom/server'; 7 | import SvgIcon from '../src/SvgIcon'; 8 | 9 | /* 10 | * These lightweight tests are an assurance that 11 | * components render in server-side environments 12 | */ 13 | describe('Server Side Rendering', () => { 14 | it('should render SVG', () => { 15 | const render = ReactDOMServer.renderToString( 16 | 17 | ); 18 | assert(render.indexOf('svg') > -1); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/test_index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env es5 */ 2 | 3 | // Require all modules ending in ".spec.js" from the 4 | // 'src' directory and all subdirectories 5 | 6 | var testsContext = require.context('../src', true, /\.spec\.js$/); 7 | testsContext.keys().forEach(testsContext); 8 | 9 | // Hide karma elements added to DOM 10 | parent.document.getElementById('banner').style.display = 'none'; 11 | parent.document.getElementById('browsers').style.display = 'none'; 12 | 13 | var configure = require('enzyme').configure; 14 | var Adapter = require('enzyme-adapter-react-16'); 15 | 16 | configure({adapter: new Adapter()}); 17 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | import {mount, shallow} from 'enzyme'; 2 | import {unmountComponentAtNode} from 'react-dom'; 3 | 4 | export function createWrapper(fn) { 5 | const attachTo = window.document.createElement('div'); 6 | const firstChild = window.document.body.firstChild; 7 | 8 | window.document.body.insertBefore(attachTo, firstChild); 9 | 10 | const wrapper = function enzymeWrapper(node) { 11 | return fn(node, {attachTo}); 12 | }; 13 | 14 | wrapper.cleanUp = () => { 15 | unmountComponentAtNode(attachTo); 16 | attachTo.parentNode.removeChild(attachTo); 17 | }; 18 | 19 | return wrapper; 20 | } 21 | 22 | export function createMount() { 23 | return createWrapper(mount); 24 | } 25 | 26 | export function createShallow() { 27 | return createWrapper(shallow); 28 | } 29 | 30 | /* Slow tests down a little so we can view 31 | * the visual renders in BrowserStack 32 | */ 33 | export function createTest(test, timeout) { 34 | return function (done) { 35 | test.bind(this)(); 36 | setTimeout(() => { 37 | done(); 38 | }, timeout || 1000); 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /webpack.config.docs.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); 3 | const webpack = require('webpack'); 4 | 5 | function entry(env) { 6 | let entries = []; 7 | if (env.dev) { 8 | entries = [ 9 | 'react-hot-loader/patch', 10 | 'webpack-dev-server/client?http://localhost:8080', 11 | 'webpack/hot/only-dev-server' 12 | ]; 13 | } 14 | entries.push('./docs/index'); 15 | return entries; 16 | } 17 | 18 | function getPlugins(env) { 19 | let plugins = [ 20 | new CaseSensitivePathsPlugin() 21 | ]; 22 | if (env.dev) { 23 | plugins = [ 24 | new webpack.HotModuleReplacementPlugin(), 25 | new webpack.DefinePlugin({ 26 | __TEST__: false 27 | }) 28 | ]; 29 | } 30 | if (env.docs) { 31 | plugins = [ 32 | new webpack.DefinePlugin({ 33 | __TEST__: false, 34 | 'process.env': { 35 | NODE_ENV: JSON.stringify('production') 36 | } 37 | }) 38 | ]; 39 | } 40 | return plugins; 41 | } 42 | 43 | module.exports = function config(env = {}) { 44 | return { 45 | devServer: { 46 | historyApiFallback: true, 47 | disableHostCheck: true, 48 | host: '0.0.0.0', 49 | hot: true, 50 | publicPath: '/material-react-components' 51 | }, 52 | devtool: env.docs ? 'cheap-module-source-map' : 'eval-source-map', 53 | entry: entry(env), 54 | output: { 55 | path: path.resolve(__dirname, env.docs ? '.' : './dist'), 56 | publicPath: '/', 57 | filename: '[name].js' 58 | }, 59 | mode: 'development', 60 | module: { 61 | rules: [ 62 | { 63 | test: /\.css$/, 64 | exclude: /node_modules/, 65 | use: [ 66 | 'style-loader', 67 | 'css-loader?modules&localIdentName=[name].[local]', 68 | 'postcss-loader' 69 | ] 70 | }, 71 | { 72 | test: /\.js$/, 73 | exclude: /node_modules/, 74 | loader: 'babel-loader' 75 | }, 76 | { 77 | test: /\.svg$/, 78 | use: [ 79 | { 80 | loader: 'babel-loader', 81 | options: { 82 | presets: [ 83 | [ 84 | '@babel/preset-env', 85 | { 86 | modules: false, 87 | targets: { 88 | browsers: ['last 2 versions'] 89 | } 90 | } 91 | ] 92 | ] 93 | } 94 | }, 95 | 'react-svg-loader' 96 | ] 97 | } 98 | ] 99 | }, 100 | plugins: getPlugins(env) 101 | }; 102 | }; 103 | -------------------------------------------------------------------------------- /webpack.config.ssr.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | devtool: 'eval', 5 | entry: [ 6 | './test/ssr.js' 7 | ], 8 | output: { 9 | path: path.resolve(__dirname, './.tmp'), 10 | publicPath: '/', 11 | filename: '[name].js' 12 | }, 13 | mode: 'development', 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.css$/, 18 | exclude: /node_modules/, 19 | use: [ 20 | 'isomorphic-style-loader', 21 | 'css-loader?modules&localIdentName=[name].[local]', 22 | 'postcss-loader' 23 | ] 24 | }, 25 | { 26 | test: /\.js$/, 27 | exclude: /node_modules/, 28 | use: [ 29 | 'babel-loader' 30 | ] 31 | }, 32 | { 33 | test: /\.svg$/, 34 | use: [ 35 | 'babel-loader', 36 | 'react-svg-loader' 37 | ] 38 | } 39 | ] 40 | }, 41 | target: 'node' 42 | }; 43 | --------------------------------------------------------------------------------