├── .eslintrc ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── lib ├── Avatar.js ├── Button.js ├── Card │ ├── Actions.js │ ├── Body.js │ ├── Media.js │ └── index.js ├── Checkbox.js ├── CheckboxGroup.js ├── Divider.js ├── Drawer │ ├── Header.js │ ├── Section.js │ └── index.js ├── Icon.js ├── IconToggle.js ├── List.js ├── RadioButton.js ├── RadioButtonGroup.js ├── Ripple.js ├── Subheader.js ├── Toolbar.js ├── VectorIconComponent.js ├── config.js ├── helpers.js ├── index.js └── polyfill │ ├── Elevation.js │ └── Ripple.js └── package.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "react" 5 | ], 6 | "ecmaFeatures": { 7 | "jsx": true 8 | }, 9 | "rules": { 10 | // Strict mode 11 | "strict": [2, "never"], 12 | 13 | // Code style 14 | "indent": 2, 15 | "quotes": [2, "single"], 16 | "no-unused-vars": 1, 17 | "no-undef": 1, 18 | "object-curly-spacing": [2, "always"], 19 | 20 | // JSX 21 | "jsx-quotes": 1, 22 | 23 | // React 24 | "react/display-name": 0, 25 | "react/jsx-boolean-value": 1, 26 | "react/jsx-closing-bracket-location": 1, 27 | "react/jsx-curly-spacing": 1, 28 | "react/jsx-max-props-per-line": 0, 29 | "react/jsx-indent-props": 0, 30 | "react/jsx-no-duplicate-props": 1, 31 | "react/jsx-no-undef": 1, 32 | "react/jsx-sort-prop-types": 0, 33 | "react/jsx-sort-props": 0, 34 | "react/jsx-uses-react": 1, 35 | "react/jsx-uses-vars": 1, 36 | "react/no-danger": 0, 37 | "react/no-set-state": 0, 38 | "react/no-did-mount-set-state": 1, 39 | "react/no-did-update-set-state": 1, 40 | "react/no-multi-comp": 1, 41 | "react/no-unknown-property": 1, 42 | "react/require-extension": 1, 43 | "react/self-closing-comp": 1, 44 | "react/wrap-multilines": 1 45 | } 46 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node template 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 29 | node_modules 30 | 31 | .idea 32 | .DS_Store -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/invertase/react-native-material-design/e3909c4e5deaeb45bcde5d0338c4e8a05dace78f/CONTRIBUTING.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Invertase Limited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > This library is **no longer maintained**. As an alternative we highly recommend checking out [React Native Paper](https://github.com/callstack/react-native-paper) by the awesome folks over at @callstack. ✨ 2 | 3 | # React Native Material Design 4 | 5 | [![npm](https://img.shields.io/npm/v/react-native-material-design.svg)](https://www.npmjs.com/package/react-native-material-design) 6 | [![Dependency Status](https://david-dm.org/react-native-material-design/react-native-material-design.svg)](https://david-dm.org/react-native-material-design/react-native-material-design.svg) 7 | [![GitHub issues](https://img.shields.io/github/issues/react-native-material-design/react-native-material-design.svg)](https://github.com/react-native-material-design/react-native-material-design/issues) 8 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/react-native-material-design/react-native-material-design/master/LICENSE) 9 | [![Join the chat at https://gitter.im/react-native-material-design/react-native-material-design](https://badges.gitter.im/react-native-material-design/react-native-material-design.svg)](https://gitter.im/react-native-material-design/react-native-material-design?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 10 | 11 | React Native components which implement [Material Design](https://www.google.com/design/spec/material-design/introduction.html). 12 | 13 | This repository has been heavily developed on top of the [mrn](https://github.com/binggg/mrn) project started by **@binggg**. Improvements include support for the latest React Native versions, 14 | many bug fixes, extra components, backward compatibility to Android SDK API 16 and more. 15 | 16 | > Please keep in mind this is still a work in progress. The master branch is subject to breaking changes. 17 | 18 | Looking for a demo app? [Check out the repo](https://github.com/react-native-material-design/demo-app). 19 | 20 | Or view it online [here](https://appetize.io/app/hyp1m20y515c16cj5yw2karcjg)! (Credits to Appetize for free hosting). 21 | 22 | ## Installation 23 | 24 | ``` 25 | npm i react-native-material-design --save 26 | ``` 27 | 28 | Follow the [Android installation instructions](https://github.com/oblador/react-native-vector-icons#android) on the [react-native-vector-icons](https://github.com/oblador/react-native-vector-icons) library, which consists of adding the following to your `./android/app/build.gradle`: 29 | 30 | ```gradle 31 | apply from: "../../node_modules/react-native-vector-icons/fonts.gradle" 32 | ``` 33 | 34 | _Note: It's possible that the instructions on the react-native-vector-icons page differ from what is above. If that's the case, follow those instructions, and please file an issue with us to update it here._ 35 | 36 | Import any required components into your project, for example: 37 | 38 | ``` 39 | import { Button, Card } from 'react-native-material-design'; 40 | ``` 41 | 42 | > You may need to restart your packager in order for the icons to render. 43 | 44 | ## Documentation 45 | 46 | Documentation & full installation instructions are available at http://react-native-material-design.github.io 47 | 48 | ## React Native 0.16+ 49 | 50 | This library only works with React Native 0.16+ due to the breaking changes with Babel and font loading it introduced. 51 | 52 | ## Examples 53 | 54 | ![Example 1](https://raw.githubusercontent.com/react-native-material-design/demo-app/master/resources/examples-1.jpg "Example 1") 55 | ![Example 2](https://raw.githubusercontent.com/react-native-material-design/demo-app/master/resources/examples-2.jpg "Example 2") 56 | ![Example 3](https://raw.githubusercontent.com/react-native-material-design/demo-app/master/resources/examples-3.jpg "Example 3") 57 | 58 | ## Contributing 59 | 60 | Full contributing guidelines are to be written, however please ensure you follow the points when sending in PRs: 61 | 62 | - Ensure no lint warns occur via `npm run lint`. 63 | - Follow the Material Design [guidelines](https://www.google.com/design/spec/layout/metrics-keylines.html#metrics-keylines-baseline-grids). 64 | -------------------------------------------------------------------------------- /lib/Avatar.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from "react"; 2 | import {View, Image, Text} from "react-native"; 3 | import Icon from './Icon'; 4 | import { getColor } from './helpers'; 5 | 6 | export default class Avatar extends Component { 7 | 8 | static propTypes = { 9 | image: PropTypes.shape({ type: PropTypes.oneOf([Image]) }), 10 | icon: PropTypes.string, 11 | size: PropTypes.number, 12 | color: PropTypes.string, 13 | backgroundColor: PropTypes.string, 14 | text: PropTypes.string, 15 | borderRadius: PropTypes.number, 16 | borderColor: PropTypes.string, 17 | borderWidth: PropTypes.number 18 | }; 19 | 20 | static defaultProps = { 21 | size: 40, 22 | color: '#ffffff', 23 | backgroundColor: getColor('paperGrey500'), 24 | borderRadius: 40 / 2, 25 | borderColor: 'rgba(0,0,0,.1)', 26 | borderWidth: 1 27 | }; 28 | 29 | render() { 30 | const { 31 | image, 32 | icon, 33 | size, 34 | color, 35 | backgroundColor, 36 | text, 37 | borderRadius, 38 | borderColor, 39 | borderWidth 40 | } = this.props; 41 | 42 | if (image) { 43 | return React.cloneElement(image, { 44 | style: { 45 | width: size, 46 | height: size, 47 | borderRadius: borderRadius, 48 | borderColor: borderColor, 49 | borderWidth: borderWidth 50 | } 51 | }); 52 | } 53 | 54 | if (icon) { 55 | return ( 56 | 57 | 58 | 63 | 64 | 65 | ); 66 | } 67 | 68 | if (text) { 69 | return ( 70 | 71 | 72 | {text} 73 | 74 | 75 | ); 76 | } 77 | 78 | return null; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/Button.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from "react"; 2 | import {ActivityIndicator, View, Text, TouchableNativeFeedback, Platform} from "react-native"; 3 | import Ripple from './polyfill/Ripple'; 4 | import { TYPO, PRIMARY, THEME_NAME, PRIMARY_COLORS } from './config'; 5 | import { getColor, isCompatible } from './helpers'; 6 | 7 | export default class Button extends Component { 8 | 9 | static propTypes = { 10 | text: PropTypes.string.isRequired, 11 | theme: PropTypes.oneOf(THEME_NAME), 12 | primary: PropTypes.oneOf(PRIMARY_COLORS), 13 | overrides: PropTypes.shape({ 14 | textColor: PropTypes.string, 15 | backgroundColor: PropTypes.string, 16 | rippleColor: PropTypes.string 17 | }), 18 | disabled: PropTypes.bool, 19 | loading: PropTypes.bool, 20 | activityIndicatorColor: PropTypes.string, 21 | raised: PropTypes.bool, 22 | styles: PropTypes.shape({ 23 | button: View.propTypes.style, 24 | disabled: View.propTypes.style, 25 | text: Text.propTypes.style 26 | }), 27 | onPress: PropTypes.func, 28 | onLongPress: PropTypes.func, 29 | }; 30 | 31 | static defaultProps = { 32 | theme: 'light', 33 | primary: PRIMARY, 34 | disabled: false, 35 | loading: false, 36 | raised: false, 37 | activityIndicatorColor: 'black', 38 | styles: { 39 | disabled: {opacity: 0.5} 40 | } 41 | }; 42 | 43 | constructor(props) { 44 | super(props); 45 | this.state = { 46 | elevation: 2 47 | }; 48 | } 49 | 50 | shouldComponentUpdate(nextProps, nextState) { 51 | return ( 52 | nextProps.text !== this.props.text || 53 | nextProps.value !== this.props.value || 54 | nextProps.loading !== this.props.loading || 55 | nextProps.disabled !== this.props.disabled 56 | ); 57 | } 58 | 59 | setElevation = () => { 60 | this.setState({ 61 | elevation: 4 62 | }); 63 | }; 64 | 65 | removeElevation = () => { 66 | this.setState({ 67 | elevation: 2 68 | }); 69 | }; 70 | 71 | render() { 72 | const { elevation } = this.state; 73 | const { text, value, theme, primary, overrides, disabled, loading, raised, styles, onPress, onLongPress } = this.props; 74 | 75 | const textStyleMap = { 76 | flat: { 77 | light: { 78 | normal: { 79 | color: getColor(primary) 80 | }, 81 | disabled: { 82 | color: 'rgba(0,0,0,.26)' 83 | } 84 | }, 85 | dark: { 86 | normal: { 87 | color: getColor(primary) 88 | }, 89 | disabled: { 90 | color: 'rgba(255,255,255,.3)' 91 | } 92 | } 93 | }, 94 | raised: { 95 | light: { 96 | normal: { 97 | color: getColor(primary) 98 | }, 99 | disabled: { 100 | color: 'rgba(0,0,0,.26)' 101 | } 102 | }, 103 | dark: { 104 | normal: { 105 | color: '#fff' 106 | }, 107 | disabled: { 108 | color: 'rgba(255,255,255,.3)' 109 | } 110 | } 111 | } 112 | }; 113 | 114 | const buttonStyleMap = { 115 | raised: { 116 | light: { 117 | normal: { 118 | backgroundColor: '#fff', 119 | borderColor: 'rgba(0,0,0,.12)', 120 | }, 121 | disabled: { 122 | backgroundColor: 'rgba(0,0,0,.12)', 123 | borderWidth: 1, 124 | borderColor: 'rgba(0,0,0,.12)' 125 | } 126 | }, 127 | dark: { 128 | normal: { 129 | backgroundColor: getColor(primary), 130 | borderWidth: 1, 131 | borderColor: 'rgba(0,0,0,.12)' 132 | }, 133 | disabled: { 134 | backgroundColor: 'rgba(255,255,255,.12)', 135 | borderWidth: 1, 136 | borderColor: 'rgba(0,0,0,.12)' 137 | } 138 | } 139 | } 140 | }; 141 | 142 | const rippleColorMap = { 143 | flat: { 144 | light: { 145 | normal: 'rgba(153,153,153,.4)', 146 | disabled: 'rgba(0,0,0,0.06)' 147 | }, 148 | dark: { 149 | normal: 'rgba(204,204,204,.25)', 150 | disabled: 'rgba(255,255,255,0.06)' 151 | } 152 | }, 153 | raised: { 154 | light: { 155 | normal: 'rgba(153,153,153,.4)', 156 | disabled: 'rgba(0,0,0,.06)' 157 | }, 158 | dark: { 159 | normal: getColor(`${primary}700`), 160 | disabled: 'rgba(255,255,255,.06)' 161 | } 162 | } 163 | }; 164 | 165 | const type = disabled ? 'disabled' : 'normal'; 166 | const shape = raised ? 'raised' : 'flat'; 167 | 168 | const textStyle = (() => { 169 | if (disabled || !(overrides && overrides.textColor)) { 170 | return textStyleMap[shape][theme][type]; 171 | } 172 | 173 | return { color: getColor(overrides.textColor) }; 174 | })(); 175 | 176 | const buttonStyle = (() => { 177 | if (raised) { 178 | if (disabled || !(overrides && overrides.backgroundColor)) { 179 | return buttonStyleMap[shape][theme][type]; 180 | } 181 | 182 | return Object.assign(buttonStyleMap[shape][theme][type], { backgroundColor: getColor(overrides.backgroundColor) }); 183 | } 184 | 185 | return null; 186 | })(); 187 | 188 | const rippleColor = (() => { 189 | if (disabled || !(overrides && overrides.rippleColor)) { 190 | return rippleColorMap[shape][theme][type]; 191 | } 192 | 193 | return getColor(overrides.rippleColor) 194 | })(); 195 | 196 | if (disabled || loading) { 197 | return ( 198 | 208 | {loading ? 209 | 210 | : 211 | 212 | {text || value} 213 | {this.props.children} 214 | 215 | } 216 | 217 | ); 218 | } 219 | 220 | if (!isCompatible('TouchableNativeFeedback')) { 221 | return ( 222 | 238 | 239 | {text || value} 240 | {this.props.children} 241 | 242 | 243 | ) 244 | } 245 | 246 | return ( 247 | 254 | 263 | 264 | {text || value} 265 | 266 | 267 | 268 | ); 269 | }; 270 | } 271 | 272 | const componentStyles = { 273 | button: { 274 | alignItems: 'center', 275 | justifyContent: 'center', 276 | paddingVertical: 6, 277 | paddingHorizontal: 16, 278 | margin: 6, 279 | borderRadius: 2 280 | }, 281 | text: { 282 | lineHeight: 20 283 | }, 284 | spinner: { 285 | alignSelf: 'center' 286 | } 287 | }; 288 | -------------------------------------------------------------------------------- /lib/Card/Actions.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from "react"; 2 | import {StyleSheet, View} from "react-native"; 3 | 4 | export default class Actions extends Component { 5 | 6 | static propTypes = { 7 | position: PropTypes.oneOf(['left', 'right']), 8 | children: PropTypes.node.isRequired 9 | }; 10 | 11 | static defaultProps = { 12 | position: 'left' 13 | }; 14 | 15 | render() { 16 | const { position, children } = this.props; 17 | 18 | return ( 19 | 20 | 21 | {children} 22 | 23 | 24 | ); 25 | } 26 | } 27 | 28 | const styles = StyleSheet.create({ 29 | container: { 30 | flex: 1, 31 | paddingRight: -16, 32 | paddingLeft: -16 33 | }, 34 | actions: { 35 | flexDirection: 'row' 36 | } 37 | }); -------------------------------------------------------------------------------- /lib/Card/Body.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from "react"; 2 | import {StyleSheet, View} from "react-native"; 3 | 4 | export default class Body extends Component { 5 | 6 | static propTypes = { 7 | children: PropTypes.node.isRequired 8 | }; 9 | 10 | render() { 11 | const { theme, children } = this.props; 12 | 13 | return ( 14 | 15 | {children} 16 | 17 | ); 18 | } 19 | } 20 | 21 | const styles = StyleSheet.create({ 22 | container: { 23 | paddingTop: 16, 24 | paddingBottom: 16 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /lib/Card/Media.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from "react"; 2 | import {StyleSheet, Image, View} from "react-native"; 3 | 4 | export default class Media extends Component { 5 | 6 | static propTypes = { 7 | image: PropTypes.shape({ type: PropTypes.oneOf([Image]) }).isRequired, 8 | height: PropTypes.number, 9 | overlay: PropTypes.bool, 10 | children: PropTypes.node 11 | }; 12 | 13 | static defaultProps = { 14 | height: 150, 15 | overlay: false 16 | }; 17 | 18 | render() { 19 | const { image, height, overlay, children } = this.props; 20 | 21 | return ( 22 | 23 | {React.cloneElement(image, { 24 | style: [styles.media, { height }] 25 | })} 26 | {children && 27 | 28 | {children} 29 | 30 | } 31 | 32 | ); 33 | } 34 | 35 | } 36 | 37 | const styles = StyleSheet.create({ 38 | media: { 39 | position: 'absolute', 40 | left: -16, 41 | right: -16, 42 | }, 43 | content: { 44 | position: 'absolute', 45 | left: -16, 46 | right: -16, 47 | bottom: 0, 48 | paddingTop: 24, 49 | paddingBottom: 16, 50 | paddingLeft: 16, 51 | paddingRight: 16 52 | } 53 | }); -------------------------------------------------------------------------------- /lib/Card/index.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from "react"; 2 | import {View, TouchableNativeFeedback} from "react-native"; 3 | import Ripple from '../polyfill/Ripple'; 4 | import { getColor, isCompatible } from '../helpers'; 5 | 6 | import Media from './Media'; 7 | import Body from './Body'; 8 | import Actions from './Actions'; 9 | 10 | import { COLOR } from '../config'; 11 | 12 | export default class Card extends Component { 13 | 14 | static propTypes = { 15 | theme: PropTypes.string, 16 | overrides: PropTypes.shape({ 17 | backgroundColor: PropTypes.string, 18 | rippleColor: PropTypes.string 19 | }), 20 | elevation: PropTypes.number, 21 | disabled: PropTypes.bool, 22 | onPress: PropTypes.func, 23 | children: PropTypes.node.isRequired, 24 | style: View.propTypes.style 25 | }; 26 | 27 | static defaultProps = { 28 | elevation: 2, 29 | disabled: false 30 | }; 31 | 32 | static Media = Media; 33 | 34 | static Body = Body; 35 | 36 | static Actions = Actions; 37 | 38 | render() { 39 | const { theme, overrides, elevation, disabled, onPress, children, style } = this.props; 40 | 41 | const cardStyle = (() => { 42 | return [ 43 | styles.container, { 44 | elevation 45 | }, !isCompatible('elevation') && { 46 | borderWidth: 1, 47 | borderColor: 'rgba(0,0,0,.12)' 48 | }, theme && { 49 | backgroundColor: COLOR[theme].color 50 | }, overrides && overrides.backgroundColor && { 51 | backgroundColor: overrides.backgroundColor 52 | }, style 53 | ]; 54 | })(); 55 | 56 | 57 | if (onPress == null || disabled) { 58 | return ( 59 | 60 | {children} 61 | 62 | ); 63 | } 64 | 65 | const defaultRippleColor = 'rgba(153,153,153,.4)'; 66 | const rippleColor = (() => { 67 | if (disabled || !(overrides && overrides.rippleColor)) { 68 | return defaultRippleColor; 69 | } 70 | 71 | return getColor(overrides.rippleColor) 72 | })(); 73 | 74 | if (!isCompatible('TouchableNativeFeedback')) { 75 | return ( 76 | 80 | 81 | {children} 82 | 83 | 84 | ) 85 | } 86 | 87 | return ( 88 | 92 | 93 | {children} 94 | 95 | 96 | ); 97 | 98 | } 99 | 100 | } 101 | 102 | const styles = { 103 | container: { 104 | backgroundColor: '#ffffff', 105 | borderRadius: 2, 106 | margin: 8, 107 | paddingLeft: 16, 108 | paddingRight: 16 109 | } 110 | }; 111 | -------------------------------------------------------------------------------- /lib/Checkbox.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from "react"; 2 | import {StyleSheet, Text, View, TouchableHighlight} from "react-native"; 3 | import { TYPO, PRIMARY, COLOR, PRIMARY_COLORS, THEME_NAME } from './config'; 4 | import Icon from './Icon'; 5 | import IconToggle from './IconToggle'; 6 | 7 | const typos = StyleSheet.create(TYPO); 8 | 9 | export default class Checkbox extends Component { 10 | 11 | static propTypes = { 12 | label: PropTypes.string, 13 | theme: PropTypes.oneOf(THEME_NAME), 14 | primary: PropTypes.oneOfType([PropTypes.oneOf(PRIMARY_COLORS), PropTypes.string]), 15 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, 16 | checked: PropTypes.bool, 17 | disabled: PropTypes.bool, 18 | onCheck: PropTypes.func 19 | }; 20 | 21 | static defaultProps = { 22 | theme: 'light', 23 | primary: PRIMARY, 24 | disabled: false 25 | }; 26 | 27 | render() { 28 | const { theme, primary, checked, disabled, value, onCheck } = this.props; 29 | 30 | const status = (()=> { 31 | if (disabled) { 32 | return 'disabled' 33 | } else if (checked) { 34 | return 'checked' 35 | } else { 36 | return 'default' 37 | } 38 | })(); 39 | 40 | const colorMap = { 41 | light: { 42 | disabled: '#000000', 43 | checked: (COLOR[`${primary}500`] || {}).color || primary, 44 | default: '#000000' 45 | }, 46 | dark: { 47 | disabled: '#ffffff', 48 | checked: (COLOR[`${primary}500`] || {}).color || primary, 49 | default: '#ffffff' 50 | } 51 | }; 52 | 53 | const opacityMap = { 54 | light: { 55 | checked: 1, 56 | default: 0.54, 57 | disabled: 0.26 58 | }, 59 | dark: { 60 | checked: 1, 61 | default: 0.7, 62 | disabled: 0.3 63 | } 64 | }; 65 | 66 | const underlayMap = { 67 | light: 'rgba(0,0,0,.12)', 68 | dark: 'rgba(255,255,255,.12)' 69 | } 70 | 71 | const labelColorMap = { 72 | light: '#000000', 73 | dark: '#ffffff' 74 | }; 75 | 76 | const CURR_COLOR = colorMap[theme][status]; 77 | const OPACITY = opacityMap[theme][status]; 78 | const LABEL_COLOR = labelColorMap[theme]; 79 | const UNDERLAY_COLOR = underlayMap[theme]; 80 | 81 | return ( 82 | { disabled ? null : onCheck(!checked, value) }} 84 | underlayColor={disabled ? 'rgba(0,0,0,0)' : UNDERLAY_COLOR} 85 | activeOpacity={1} 86 | > 87 | 88 | { disabled ? null : onCheck(!checked, value) }} 92 | > 93 | 102 | 103 | onCheck(!checked, value)} 108 | > 109 | 119 | {this.props.label} 120 | 121 | 122 | 123 | 124 | ); 125 | }; 126 | } 127 | 128 | const styles = StyleSheet.create({ 129 | container: { 130 | flexDirection: 'row', 131 | alignItems: 'center', 132 | backgroundColor: 'rgba(0,0,0,0)' 133 | }, 134 | label: { 135 | marginLeft: 16, 136 | opacity: COLOR.darkPrimaryOpacity.opacity, 137 | alignItems: 'center', 138 | flex: 1 139 | }, 140 | labelContainer :{ 141 | justifyContent: 'center', 142 | flexDirection:'row' 143 | } 144 | }); 145 | -------------------------------------------------------------------------------- /lib/CheckboxGroup.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from "react"; 2 | import {View} from "react-native"; 3 | import Checkbox from './Checkbox'; 4 | import { THEME_NAME, PRIMARY, PRIMARY_COLORS } from './config'; 5 | 6 | export default class CheckboxGroup extends Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | selected: [] 12 | }; 13 | } 14 | 15 | static propTypes = { 16 | theme: PropTypes.oneOf(THEME_NAME), 17 | primary: PropTypes.oneOf(PRIMARY_COLORS), 18 | onSelect: PropTypes.func, 19 | checked: PropTypes.array, 20 | items: PropTypes.arrayOf(PropTypes.shape({ 21 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, 22 | label: PropTypes.string, 23 | disabled: PropTypes.bool 24 | })) 25 | }; 26 | 27 | static defaultProps = { 28 | theme: 'light', 29 | primary: PRIMARY 30 | }; 31 | 32 | componentWillMount = () => { 33 | const { checked } = this.props; 34 | 35 | if (checked && checked.length) { 36 | this.value = checked; 37 | } 38 | }; 39 | 40 | render() { 41 | const { items, theme, primary } = this.props; 42 | return ( 43 | 44 | { 45 | items && items.length && items.map((item) => { 46 | const { value } = item; 47 | return ( 48 | 58 | ); 59 | }) 60 | } 61 | 62 | ); 63 | }; 64 | 65 | _onChange = (checked, value) => { 66 | const { selected } = this.state; 67 | 68 | var newSelected; 69 | if (checked) { 70 | newSelected = [...selected, value] 71 | } else { 72 | let index = selected.indexOf(value); 73 | newSelected = [ 74 | ...selected.slice(0, index), 75 | ...selected.slice(index + 1) 76 | ] 77 | } 78 | 79 | this.setState({ 80 | selected: newSelected 81 | }); 82 | 83 | const { onSelect } = this.props; 84 | onSelect && onSelect(newSelected); 85 | }; 86 | 87 | /** 88 | * Get the value of checked Checkbox in CheckboxGroup. Often use in form. 89 | * @returns {Array} 90 | */ 91 | get value() { 92 | return this.state.selected 93 | } 94 | 95 | /** 96 | * Make CheckboxGroup set some checkbox checked 97 | * @param {string[]} value - An array of values of some Checkbox in CheckboxGroup 98 | */ 99 | set value(value) { 100 | this.setState({ 101 | selected: value 102 | }); 103 | 104 | const { onSelect } = this.props; 105 | onSelect && onSelect(value); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/Divider.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from "react"; 2 | import {View} from "react-native"; 3 | import { THEME_NAME } from './config'; 4 | 5 | export default class Divider extends Component { 6 | 7 | static propTypes = { 8 | inset: PropTypes.bool, 9 | theme: PropTypes.oneOf(THEME_NAME), 10 | style: View.propTypes.style 11 | }; 12 | 13 | static defaultProps = { 14 | inset: false, 15 | theme: 'light' 16 | }; 17 | 18 | render() { 19 | const { inset, theme, style } = this.props; 20 | 21 | return ( 22 | 31 | ); 32 | } 33 | } 34 | 35 | const styles = { 36 | divider: { 37 | height: 1 38 | } 39 | }; -------------------------------------------------------------------------------- /lib/Drawer/Header.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from "react"; 2 | import {View, Image} from "react-native"; 3 | 4 | export default class Header extends Component { 5 | 6 | static propTypes = { 7 | image: PropTypes.shape({ type: PropTypes.oneOf([Image]) }), 8 | backgroundColor: PropTypes.string, 9 | height: PropTypes.number, 10 | children: PropTypes.node 11 | }; 12 | 13 | static defaultProps = { 14 | height: 150, 15 | backgroundColor: '#333333' 16 | }; 17 | 18 | render() { 19 | const { image, height, backgroundColor, children } = this.props; 20 | 21 | if (image) { 22 | return React.cloneElement(image, { 23 | style: [styles.header, { height: height }] 24 | }, children); 25 | } 26 | 27 | return ( 28 | 29 | {children} 30 | 31 | ); 32 | } 33 | } 34 | 35 | const styles = { 36 | header: { 37 | flexWrap: 'wrap', 38 | justifyContent:'flex-start', 39 | paddingHorizontal: 16, 40 | marginBottom: 8 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /lib/Drawer/Section.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from "react"; 2 | import {View, Text, TouchableNativeFeedback} from "react-native"; 3 | import Icon from '../Icon'; 4 | import Ripple from '../polyfill/Ripple'; 5 | import { TYPO } from '../config'; 6 | import { isCompatible } from '../helpers'; 7 | 8 | export default class Section extends Component { 9 | 10 | static propTypes = { 11 | title: PropTypes.string, 12 | items: PropTypes.arrayOf(PropTypes.shape({ 13 | icon: PropTypes.string, 14 | value: PropTypes.string.isRequired, 15 | label: PropTypes.string, 16 | onPress: PropTypes.func, 17 | onLongPress: PropTypes.func, 18 | active: PropTypes.bool, 19 | disabled: PropTypes.bool 20 | })) 21 | }; 22 | 23 | renderRow = (item, index, color) => { 24 | return ( 25 | 29 | {item.icon && 30 | 36 | } 37 | 38 | 39 | {item.value} 40 | 41 | 42 | {item.label && 43 | 44 | 45 | {item.label} 46 | 47 | 48 | } 49 | 50 | ); 51 | }; 52 | 53 | render() { 54 | const { theme, title, items } = this.props; 55 | 56 | const textStyleMap = { 57 | light: { 58 | 'default': 'rgba(0,0,0,.87)', 59 | disabled: 'rgba(0,0,0,.38)' 60 | }, 61 | dark: { 62 | 'default': '#ffffff', 63 | disabled: 'rgba(255,255,255,.30)' 64 | } 65 | }; 66 | 67 | const subheaderStyleMap = { 68 | light: 'rgba(0,0,0,.54)', 69 | dark: 'rgba(255,255,255,.70)', 70 | }; 71 | 72 | const activeStyleMap = { 73 | light: '#f5f5f5', 74 | dark: '#212121', 75 | }; 76 | 77 | const TEXT_COLOR = textStyleMap[theme]['default']; 78 | const SUB_TEXT_COLOR = subheaderStyleMap[theme]; 79 | const ACTIVE_COLOR = activeStyleMap[theme]; 80 | 81 | return ( 82 | 83 | {title && 84 | 85 | 86 | {title} 87 | 88 | 89 | } 90 | {items && items.map((item, i) => { 91 | if (item.disabled) { 92 | return this.renderRow(item, i, textStyleMap[theme]['disabled']); 93 | } 94 | 95 | if (!isCompatible('TouchableNativeFeedback')) { 96 | return ( 97 | 108 | {this.renderRow(item, i, TEXT_COLOR)} 109 | 110 | ); 111 | } 112 | 113 | return ( 114 | 120 | 121 | {this.renderRow(item, i, TEXT_COLOR)} 122 | 123 | 124 | ); 125 | })} 126 | 127 | ); 128 | } 129 | } 130 | 131 | const styles = { 132 | section: { 133 | marginTop: 8, 134 | flexWrap: 'wrap', 135 | justifyContent:'flex-start' 136 | }, 137 | item: { 138 | flex: 1, 139 | flexDirection: 'row', 140 | alignItems: 'center', 141 | height: 48, 142 | paddingLeft: 16, 143 | flexWrap: 'wrap' 144 | }, 145 | subheader: { 146 | flex: 1, 147 | flexWrap: 'wrap', 148 | justifyContent:'flex-start' 149 | }, 150 | icon: { 151 | position: 'absolute', 152 | top: 13 153 | }, 154 | value: { 155 | flex: 1, 156 | paddingLeft: 56, 157 | top: 2 158 | }, 159 | label: { 160 | paddingRight: 16, 161 | top: 2 162 | } 163 | }; 164 | -------------------------------------------------------------------------------- /lib/Drawer/index.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from "react"; 2 | import {ScrollView} from "react-native"; 3 | import { THEME_NAME, PRIMARY_COLORS } from '../config'; 4 | import { getColor } from '../helpers'; 5 | 6 | import Header from './Header'; 7 | import Section from './Section'; 8 | 9 | export default class Drawer extends Component { 10 | 11 | static propTypes = { 12 | theme: PropTypes.oneOf(THEME_NAME), 13 | primary: PropTypes.oneOf(PRIMARY_COLORS), 14 | overrides: PropTypes.shape({ 15 | backgroundColor: PropTypes.string 16 | }), 17 | children: PropTypes.node.isRequired 18 | }; 19 | 20 | static defaultProps = { 21 | theme: 'light', 22 | primary: 'paperGrey' 23 | }; 24 | 25 | static Header = Header; 26 | 27 | static Section = Section; 28 | 29 | render() { 30 | const { theme, overrides, children } = this.props; 31 | 32 | const backgroundColorMap = { 33 | light: '#ffffff', 34 | dark: '#333333' 35 | }; 36 | 37 | const backgroundColor = (() => { 38 | if (overrides && overrides.backgroundColor) { 39 | return getColor(overrides.backgroundColor); 40 | } 41 | return backgroundColorMap[theme]; 42 | })(); 43 | 44 | return ( 45 | 46 | {React.Children.map(children, (child) => { 47 | return React.cloneElement(child, { 48 | theme 49 | }); 50 | })} 51 | 52 | ); 53 | } 54 | 55 | } 56 | 57 | const styles = { 58 | container: { 59 | flex: 1 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /lib/Icon.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from "react"; 2 | import {View} from "react-native"; 3 | import { getColor } from './helpers'; 4 | import VectorIconComponent from './VectorIconComponent'; 5 | 6 | export default class Icon extends Component { 7 | 8 | static propTypes = { 9 | name: PropTypes.string.isRequired, 10 | style: View.propTypes.style, 11 | size: PropTypes.number, 12 | color: PropTypes.string, 13 | allowFontScaling: PropTypes.bool 14 | }; 15 | 16 | static defaultProps = { 17 | size: 30, 18 | color: '#757575', 19 | allowFontScaling: true 20 | }; 21 | 22 | render() { 23 | const { name, style, size, color, allowFontScaling} = this.props; 24 | const VectorIcon = VectorIconComponent.get(); 25 | 26 | return ( 27 | 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/IconToggle.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from "react"; 2 | import {Text, View, Animated} from "react-native"; 3 | import { getColor } from './helpers'; 4 | 5 | export default class IconToggle extends Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | scaleValue: new Animated.Value(0.01), 11 | opacityValue: new Animated.Value(0.1), 12 | maxOpacity: props.opacity, 13 | size: null 14 | }; 15 | this._responder = { 16 | onStartShouldSetResponder: (e) => true, 17 | onResponderGrant: this._highlight, 18 | onResponderRelease: this._handleResponderEnd, 19 | onResponderTerminate: this._unHighlight 20 | }; 21 | } 22 | 23 | componentDidUpdate(prevProps) { 24 | if (this.props && this.props.badge) { 25 | if (prevProps.badge && this.props.badge.value !== prevProps.badge.value) { 26 | this._animateBadgeCounter(); 27 | } 28 | } 29 | }; 30 | 31 | static propTypes = { 32 | color: PropTypes.string.isRequired, 33 | opacity: PropTypes.number, 34 | disabled: PropTypes.bool, 35 | onPress: PropTypes.func, 36 | percent: PropTypes.number, 37 | children: PropTypes.element.isRequired, 38 | badge: PropTypes.shape({ 39 | value: PropTypes.number, 40 | animate: PropTypes.bool, 41 | backgroundColor: PropTypes.string, 42 | textColor: PropTypes.string 43 | }), 44 | onLayout: PropTypes.func 45 | }; 46 | 47 | static defaultProps = { 48 | opacity: .1, 49 | disabled: false, 50 | percent: 90 51 | }; 52 | 53 | setSize = (event) => { 54 | const { width, height } = event.nativeEvent.layout; 55 | 56 | this.setState({ 57 | size: width > height ? width : height 58 | }); 59 | }; 60 | 61 | render() { 62 | const { scaleValue, opacityValue, size } = this.state; 63 | const { color, onLayout, children } = this.props; 64 | 65 | let { badge } = this.props; 66 | let { percent } = this.props; 67 | 68 | if (percent < 0) { 69 | percent = 0; 70 | } 71 | if (percent > 100) { 72 | percent = 100; 73 | } 74 | 75 | percent = percent / 100; 76 | 77 | if (badge && typeof badge.value === 'number') { 78 | badge = Object.assign({}, 79 | {value: badge.value}, 80 | badge.backgroundColor ? {backgroundColor: badge.backgroundColor} : {backgroundColor: 'paperRed'}, 81 | badge.textColor ? {textColor: badge.textColor} : {textColor: '#ffffff'} 82 | ); 83 | } 84 | 85 | this.badgeAnim = this.badgeAnim || new Animated.Value(0); 86 | 87 | return ( 88 | 89 | 90 | {size && 91 | 104 | } 105 | { this.setSize(event) }}> 106 | {children} 107 | 108 | {size && badge && typeof badge.value === 'number' && 109 | 135 | 136 | 99 ? { fontSize: 8 } : { fontSize: 10 }]}> 137 | {badge.value} 138 | 139 | 140 | 141 | } 142 | 143 | 144 | ); 145 | }; 146 | 147 | /** 148 | * 149 | * @private 150 | */ 151 | _highlight = () => { 152 | if (!this.props.disabled) { 153 | Animated.timing(this.state.scaleValue, { 154 | toValue: 1, 155 | duration: 150 156 | }).start(); 157 | Animated.timing(this.state.opacityValue, { 158 | toValue: this.state.maxOpacity, 159 | duration: 100 160 | }).start(); 161 | } 162 | }; 163 | 164 | 165 | /** 166 | * 167 | * @private 168 | */ 169 | _unHighlight = () => { 170 | if (!this.props.disabled) { 171 | Animated.timing(this.state.scaleValue, { 172 | toValue: 0.01, 173 | duration: 1500 174 | }).start(); 175 | Animated.timing(this.state.opacityValue, { 176 | toValue: 0 177 | }).start(); 178 | } 179 | }; 180 | 181 | /** 182 | * 183 | * @private 184 | */ 185 | _animateBadgeCounter = () => { 186 | if (this.badgeAnim && this.props.badge && this.props.badge.animate !== false) { 187 | Animated.spring(this.badgeAnim, { 188 | toValue: 0, // Returns to the start 189 | velocity: 8, // Velocity makes it move 190 | tension: -5, // Slow 191 | friction: 1, // Oscillate a lot 192 | duration: 300 193 | }).start(); 194 | } 195 | }; 196 | 197 | /** 198 | * 199 | * @private 200 | */ 201 | _setMainRef = (ref) => { 202 | this.main = ref; 203 | }; 204 | 205 | /** 206 | * 207 | * @private 208 | */ 209 | _handleResponderEnd = () => { 210 | const { disabled, onPress } = this.props; 211 | 212 | if (!disabled) { 213 | this._unHighlight(); 214 | onPress && onPress(); 215 | } 216 | }; 217 | 218 | // Proxy the measurement methods from the main view 219 | measure = (...args) => this.main.measure(...args); 220 | measureInWindow = (...args) => this.main.measureInWindow(...args); 221 | measureLayout = (...args) => this.main.measureLayout(...args); 222 | } 223 | 224 | const styles = { 225 | badgeContainer: { 226 | position: 'absolute', 227 | borderRadius: 7.5, 228 | width: 15, 229 | height: 15 230 | }, 231 | badgeText: { 232 | textAlign: 'center' 233 | } 234 | }; 235 | -------------------------------------------------------------------------------- /lib/List.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from "react"; 2 | import {StyleSheet, View, Text, TouchableWithoutFeedback} from "react-native"; 3 | import { TYPO } from './config'; 4 | 5 | export default class List extends Component { 6 | 7 | static propTypes = { 8 | primaryText: PropTypes.string.isRequired, 9 | secondaryText: PropTypes.string, 10 | captionText: PropTypes.string, 11 | secondaryTextMoreLine: PropTypes.arrayOf(PropTypes.shape({ 12 | text: PropTypes.string.isRequired, 13 | style: PropTypes.object 14 | })), 15 | leftIcon: PropTypes.element, 16 | rightIcon: PropTypes.element, 17 | leftAvatar: PropTypes.element, 18 | rightAvatar: PropTypes.element, 19 | lines: PropTypes.number, 20 | primaryColor: PropTypes.string, 21 | onPress: PropTypes.func, 22 | onLeftIconClick: PropTypes.func, 23 | onRightIconClick: PropTypes.func 24 | }; 25 | 26 | static defaultProps = { 27 | lines: 1, 28 | primaryColor: 'rgba(0,0,0,.87)' 29 | }; 30 | 31 | render() { 32 | const { 33 | primaryText, 34 | secondaryText, 35 | leftIcon, 36 | leftAvatar, 37 | rightAvatar, 38 | rightIcon, 39 | lines, 40 | onPress, 41 | primaryColor, 42 | onLeftIconClicked, 43 | onRightIconClicked, 44 | secondaryTextMoreLine, 45 | captionText 46 | } = this.props; 47 | 48 | return ( 49 | 2 ? ((lines -1) * 16 + 56) : (secondaryText ? 72 : (leftAvatar || rightAvatar ) ? 56 : 48) }]}> 50 | {leftIcon && 51 | 52 | 2 && { paddingTop: 16, alignSelf: 'flex-start' }]}> 53 | {leftIcon} 54 | 55 | 56 | } 57 | {leftAvatar && 58 | 2 && { paddingTop: 16, alignSelf: 'flex-start' }]}> 59 | {leftAvatar} 60 | 61 | } 62 | 63 | 64 | 65 | 66 | {primaryText} 67 | 68 | 69 | {(lines > 2 && !!rightIcon) || 70 | 71 | 72 | {captionText} 73 | 74 | 75 | } 76 | 77 | {secondaryText && 78 | 79 | 2 && { height: 22 * (lines - 1) -4 }, styles.secondaryText]}> 80 | {secondaryText} 81 | 82 | 83 | } 84 | {secondaryTextMoreLine && 85 | 2 && { height: 22 * (lines - 1) - 4 }]}> 86 | {secondaryTextMoreLine.map((line) => ( 87 | 88 | {line.text} 89 | 90 | ))} 91 | 92 | } 93 | 94 | 95 | {rightAvatar && 96 | 2 && { 98 | paddingTop: 16, 99 | alignSelf: 'flex-start' 100 | }]} 101 | > 102 | {rightAvatar} 103 | 104 | } 105 | 106 | {lines > 2 && !!rightIcon && !!captionText && 107 | 108 | {captionText} 109 | } 110 | 111 | {rightIcon && 112 | 113 | 2 && { 115 | paddingTop: 16, 116 | alignSelf: 'flex-end', 117 | justifyContent:'flex-end' 118 | }]} 119 | onPress={onRightIconClicked} 120 | > 121 | {rightIcon} 122 | 123 | 124 | } 125 | 126 | 127 | ) 128 | } 129 | } 130 | 131 | const styles = StyleSheet.create({ 132 | listContainer: { 133 | flexDirection: 'row', 134 | paddingLeft: 16, 135 | paddingRight: 16, 136 | height: 48, 137 | alignItems: 'center' 138 | }, 139 | leftIcon: { 140 | width: 56, 141 | position: 'relative', 142 | top: -6 143 | }, 144 | rightIcon: { 145 | paddingLeft: 16, 146 | position: 'relative', 147 | top: -3, 148 | left: -8 149 | }, 150 | leftAvatar: { 151 | width: 56 152 | }, 153 | rightAvatar: { 154 | width: 56 155 | }, 156 | primaryText: Object.assign({}, TYPO.paperFontSubhead, { lineHeight: 24 }), 157 | secondaryText: Object.assign({}, TYPO.paperFontBody1, { 158 | lineHeight: 22, 159 | fontSize: 14, 160 | color: 'rgba(0,0,0,.54)' 161 | }), 162 | firstLine: { 163 | flexDirection: 'row' 164 | }, 165 | primaryTextContainer: { 166 | flex: 1, 167 | paddingRight: 16 168 | }, 169 | captionTextContainer: { 170 | alignSelf: 'flex-start', 171 | alignItems: 'flex-start' 172 | }, 173 | captionText: Object.assign({}, TYPO.paperFontCaption), 174 | captionTextContainer2: { 175 | alignSelf: 'flex-end', 176 | alignItems: 'flex-end' 177 | } 178 | }); -------------------------------------------------------------------------------- /lib/RadioButton.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from "react"; 2 | import {StyleSheet, Text, View, TouchableHighlight} from "react-native"; 3 | import Icon from './Icon'; 4 | import IconToggle from './IconToggle'; 5 | import { TYPO, PRIMARY, COLOR, THEME_NAME, PRIMARY_COLORS } from './config'; 6 | 7 | const typos = StyleSheet.create(TYPO); 8 | 9 | export default class RadioButton extends Component { 10 | 11 | static propTypes = { 12 | label: PropTypes.string, 13 | theme: PropTypes.oneOf(THEME_NAME), 14 | primary: PropTypes.oneOf(PRIMARY_COLORS), 15 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, 16 | checked: PropTypes.bool, 17 | disabled: PropTypes.bool, 18 | onSelect: PropTypes.func 19 | }; 20 | 21 | static defaultProps = { 22 | theme: 'light', 23 | primary: PRIMARY, 24 | disabled: false 25 | }; 26 | 27 | render() { 28 | const { theme, primary, value, checked, disabled, onSelect } = this.props; 29 | const primaryColor = COLOR[`${primary}500`].color; 30 | 31 | let status = (()=> { 32 | if (disabled) { 33 | return 'disabled' 34 | } else if (checked) { 35 | return 'checked' 36 | } else { 37 | return 'default' 38 | } 39 | })(); 40 | 41 | const colorMap = { 42 | light: { 43 | disabled: '#000', 44 | checked: primaryColor, 45 | default: '#000' 46 | }, 47 | dark: { 48 | disabled: '#fff', 49 | checked: primaryColor, 50 | default: '#fff' 51 | } 52 | }; 53 | 54 | const opacityMap = { 55 | light: { 56 | checked: 1, 57 | default: 0.54, 58 | disabled: 0.26 59 | }, 60 | dark: { 61 | checked: 1, 62 | default: 0.7, 63 | disabled: 0.3 64 | } 65 | }; 66 | 67 | const underlayMap = { 68 | light: 'rgba(0,0,0,.12)', 69 | dark: 'rgba(255,255,255,.12)' 70 | } 71 | 72 | const labelColorMap = { 73 | light: '#000', 74 | dark: '#fff' 75 | }; 76 | 77 | const CURR_COLOR = colorMap[theme][status]; 78 | const OPACITY = opacityMap[theme][status]; 79 | const LABEL_COLOR = labelColorMap[theme]; 80 | const UNDERLAY_COLOR = underlayMap[theme]; 81 | 82 | return ( 83 | { disabled && !checked ? null : onSelect(value) }} 85 | underlayColor={disabled ? 'rgba(0,0,0,0)' : UNDERLAY_COLOR} 86 | activeOpacity={1} 87 | > 88 | 89 | { disabled && !checked ? null : onSelect(value) }} 93 | > 94 | 102 | 103 | 104 | 114 | {this.props.label} 115 | 116 | 117 | 118 | 119 | ); 120 | }; 121 | } 122 | 123 | const styles = StyleSheet.create({ 124 | container: { 125 | flexDirection: 'row', 126 | alignItems: 'center', 127 | backgroundColor: 'rgba(0,0,0,0)' 128 | }, 129 | labelContainer: { 130 | alignItems: 'center', 131 | flexDirection: 'row', 132 | flex: 1 133 | }, 134 | label: { 135 | marginLeft: 16, 136 | opacity: COLOR.darkPrimaryOpacity.opacity 137 | } 138 | }); 139 | -------------------------------------------------------------------------------- /lib/RadioButtonGroup.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from "react"; 2 | import {View} from "react-native"; 3 | import RadioButton from './RadioButton'; 4 | import { PRIMARY, PRIMARY_COLORS, THEME_NAME } from './config'; 5 | 6 | export default class RadioButtonGroup extends Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | selected: props.selected || null 12 | }; 13 | } 14 | 15 | static propTypes = { 16 | theme: PropTypes.oneOf(THEME_NAME), 17 | primary: PropTypes.oneOf(PRIMARY_COLORS), 18 | onSelect: PropTypes.func, 19 | selected: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 20 | items: PropTypes.arrayOf(PropTypes.shape({ 21 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, 22 | label: PropTypes.string, 23 | disabled: PropTypes.bool 24 | })) 25 | }; 26 | 27 | static defaultProps = { 28 | theme: 'light', 29 | primary: PRIMARY 30 | }; 31 | 32 | componentDidMount = () => { 33 | const { selected } = this.state; 34 | 35 | if (selected) { 36 | this.value = selected; 37 | } 38 | }; 39 | 40 | render() { 41 | const { items, theme, primary } = this.props; 42 | return ( 43 | 44 | { 45 | items && items.length && items.map((item) => { 46 | const { value } = item; 47 | return ( 48 | 58 | ); 59 | }) 60 | } 61 | 62 | ); 63 | }; 64 | 65 | onSelect = (value) => { 66 | const { onSelect } = this.props; 67 | 68 | this.setState({ 69 | selected: value 70 | }); 71 | 72 | onSelect && onSelect(value); 73 | }; 74 | 75 | /** 76 | * Get the value of checked RadioButton in RadioButtonGroup. Often use in form. 77 | * @returns {string} 78 | */ 79 | get value() { 80 | return this.state.selected 81 | } 82 | 83 | /** 84 | * Specifies that which RadioButton should be pre-selected 85 | * @param {string} value 86 | */ 87 | set value(value) { 88 | this.onSelect(value); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/Ripple.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from "react"; 2 | import {View, Text, TouchableNativeFeedback} from "react-native"; 3 | import { default as PolyfillRipple } from './polyfill/Ripple'; 4 | import { isCompatible } from './helpers'; 5 | 6 | export default class Ripple extends Component { 7 | 8 | static propTypes = { 9 | color: PropTypes.string, 10 | onPress: PropTypes.func, 11 | onLongPress: PropTypes.func, 12 | children: PropTypes.node.isRequired 13 | }; 14 | 15 | static defaultProps = { 16 | color: 'rgba(0,0,0,.2)' 17 | }; 18 | 19 | render() { 20 | const { color, onPress, onLongPress, children, ...other } = this.props; 21 | 22 | if (!isCompatible('TouchableNativeFeedback')) { 23 | return ( 24 | 30 | {/* Stops fatal crash with out of bounds animation */} 31 | 32 | {children} 33 | 34 | 35 | ) 36 | } 37 | 38 | return ( 39 | 44 | 45 | {children} 46 | 47 | 48 | ); 49 | } 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /lib/Subheader.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from "react"; 2 | import {StyleSheet, View, Text} from "react-native"; 3 | import { TYPO, THEME_NAME } from './config'; 4 | import { getColor } from './helpers'; 5 | 6 | export default class Subheader extends Component { 7 | 8 | static propTypes = { 9 | text: PropTypes.string.isRequired, 10 | color: PropTypes.string, 11 | inset: PropTypes.bool, 12 | theme: PropTypes.oneOf(THEME_NAME), 13 | lines: PropTypes.number 14 | }; 15 | 16 | static defaultProps = { 17 | color: 'rgba(0,0,0,.54)', 18 | inset: false, 19 | theme: 'light', 20 | lines: 1 21 | }; 22 | 23 | render() { 24 | const { text, color, inset, lines } = this.props; 25 | 26 | return ( 27 | 32 | 39 | {text} 40 | 41 | 42 | ); 43 | } 44 | } 45 | 46 | const styles = StyleSheet.create({ 47 | container: { 48 | padding: 16 49 | }, 50 | text: TYPO.paperFontBody1 51 | }); -------------------------------------------------------------------------------- /lib/Toolbar.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from "react"; 2 | import {View, Text} from "react-native"; 3 | import { TYPO, PRIMARY, THEME_NAME, PRIMARY_COLORS } from './config'; 4 | import { getColor } from './helpers'; 5 | import Icon from './Icon'; 6 | import IconToggle from './IconToggle'; 7 | 8 | export default class Toolbar extends Component { 9 | 10 | static propTypes = { 11 | title: PropTypes.string, 12 | theme: PropTypes.oneOf(THEME_NAME), 13 | primary: PropTypes.oneOf(PRIMARY_COLORS), 14 | style: View.propTypes.style, 15 | leftIconStyle: PropTypes.object, 16 | rightIconStyle: PropTypes.object, 17 | elevation: PropTypes.number, 18 | overrides: PropTypes.shape({ 19 | backgroundColor: PropTypes.string, 20 | titleColor: PropTypes.string, 21 | leftIconColor: PropTypes.string, 22 | rightIconColor: PropTypes.string 23 | }), 24 | icon: PropTypes.string, 25 | onIconPress: PropTypes.func, 26 | actions: PropTypes.arrayOf(PropTypes.shape({ 27 | icon: PropTypes.string.isRequired, 28 | onPress: PropTypes.func, 29 | counter: PropTypes.shape(), 30 | onLayout: PropTypes.func 31 | })), 32 | onLayout: PropTypes.func 33 | }; 34 | 35 | static defaultProps = { 36 | theme: 'dark', 37 | primary: PRIMARY, 38 | elevation: 4 39 | }; 40 | 41 | render() { 42 | const { title, theme, primary, style, leftIconStyle, rightIconStyle, elevation, overrides, icon, onIconPress, actions, onLayout } = this.props; 43 | 44 | const themeMap = { 45 | light: { 46 | backgroundColor: '#ffffff', 47 | color: 'rgba(0,0,0,.87)', 48 | leftIconColor: 'rgba(0,0,0,.54)', 49 | rightIconColor: 'rgba(0,0,0,.54)' 50 | }, 51 | dark: { 52 | backgroundColor: getColor(primary), 53 | color: 'rgba(255,255,255,.87)', 54 | leftIconColor: 'rgba(255,255,255,.87)', 55 | rightIconColor: 'rgba(255,255,255,.87)' 56 | } 57 | }; 58 | 59 | const opacityMap = { 60 | light: .38, 61 | dark: .30 62 | }; 63 | 64 | const styleMap = { 65 | backgroundColor: overrides && overrides.backgroundColor ? getColor(overrides.backgroundColor) : themeMap[theme].backgroundColor, 66 | color: overrides && overrides.color ? getColor(overrides.color) : themeMap[theme].color, 67 | leftIconColor: overrides && overrides.leftIconColor ? getColor(overrides.leftIconColor) : themeMap[theme].leftIconColor, 68 | rightIconColor: overrides && overrides.rightIconColor ? getColor(overrides.rightIconColor) : themeMap[theme].rightIconColor 69 | }; 70 | 71 | return ( 72 | 73 | { 74 | icon && ( 75 | 79 | 84 | 85 | ) 86 | } 87 | { 88 | !title ? this.props.children : ( 89 | 96 | {title} 97 | 98 | ) 99 | } 100 | { 101 | actions && 102 | actions.map(function (action, i) { 103 | return ( 104 | 112 | 117 | 118 | ); 119 | }) 120 | } 121 | 122 | ); 123 | } 124 | } 125 | 126 | const styles = { 127 | toolbar: { 128 | position: 'absolute', 129 | top: 0, 130 | left: 0, 131 | right: 0, 132 | height: 56, 133 | flexDirection: 'row', 134 | alignItems: 'center' 135 | }, 136 | title: { 137 | flex: 1, 138 | marginLeft: 16 139 | }, 140 | leftIcon: { 141 | margin: 16 142 | }, 143 | rightIcon: { 144 | margin: 16 145 | } 146 | }; 147 | -------------------------------------------------------------------------------- /lib/VectorIconComponent.js: -------------------------------------------------------------------------------- 1 | import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; 2 | 3 | let _iconComponent = MaterialIcons; 4 | 5 | export default { 6 | set(component) { 7 | _iconComponent = component; 8 | }, 9 | 10 | get() { 11 | return _iconComponent; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | import { typography, color } from 'react-native-material-design-styles'; 2 | 3 | /** 4 | * Paper style color 5 | * @type {object} 6 | */ 7 | export const COLOR = color; 8 | 9 | /** 10 | * Paper style typography 11 | * @type {object} 12 | */ 13 | export const TYPO = typography; 14 | 15 | /** 16 | * Primary color name 17 | * @type {string} 18 | */ 19 | export const PRIMARY = 'paperBlue'; 20 | 21 | /** 22 | * Paper style color names 23 | * @type {string[]} 24 | */ 25 | export const PRIMARY_COLORS = ['googleBlue', 'googleGreen', 'googleGrey', 'googleRed', 'googleYellow', 'paperAmber', 26 | 'paperBlue', 'paperBlueGrey', 'paperBrown', 'paperCyan', 'paperDeepOrange', 'paperDeepPurple', 'paperGreen', 27 | 'paperGrey', 'paperIndigo', 'paperLightBlue', 'paperLightGreen', 'paperLime', 'paperOrange', 'paperPink', 28 | 'paperPurple', 'paperRed', 'paperTeal', 'paperYellow']; 29 | 30 | /** 31 | * Themes 32 | * @type {string[]} 33 | */ 34 | export const THEME_NAME = ['light', 'dark']; 35 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | import { Platform } from 'react-native'; 2 | import { PRIMARY, COLOR } from './config'; 3 | 4 | /** 5 | * Detect whether a color is a hex code/rgba or a paper element style 6 | * @param string 7 | * @returns {*} 8 | */ 9 | export function getColor(string) { 10 | if (string) { 11 | if (string.indexOf('#') > -1 || string.indexOf('rgba') > -1) { 12 | return string; 13 | } 14 | 15 | if (COLOR[string]) { 16 | return COLOR[string].color; 17 | } 18 | 19 | if (COLOR[`${string}500`]) { 20 | return COLOR[`${string}500`].color; 21 | } 22 | } 23 | 24 | return COLOR[`${PRIMARY}500`].color; 25 | } 26 | 27 | /** 28 | * Detect whether a specific feature is compatible with the device 29 | * @param feature 30 | * @returns bool 31 | */ 32 | export function isCompatible(feature) { 33 | const version = Platform.Version; 34 | 35 | switch (feature) { 36 | case 'TouchableNativeFeedback': 37 | return version >= 21; 38 | break; 39 | case 'elevation': 40 | return version >= 21; 41 | break; 42 | default: 43 | return true; 44 | break; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | export { COLOR, TYPO, PRIMARY_COLORS, THEME_NAME } from './config'; 2 | export { default as Avatar } from './Avatar'; 3 | export { default as Button } from './Button'; 4 | export { default as Card } from './Card'; 5 | export { default as Checkbox } from './Checkbox'; 6 | export { default as CheckboxGroup } from './CheckboxGroup'; 7 | export { default as Drawer } from './Drawer'; 8 | export { default as Divider } from './Divider'; 9 | export { default as Icon } from './Icon'; 10 | export { default as IconToggle } from './IconToggle'; 11 | //export { default as List } from './List'; 12 | export { default as RadioButton } from './RadioButton'; 13 | export { default as Ripple } from './Ripple'; 14 | export { default as RadioButtonGroup } from './RadioButtonGroup'; 15 | export { default as Subheader } from './Subheader'; 16 | export { default as Toolbar } from './Toolbar'; 17 | export { default as VectorIconComponent } from './VectorIconComponent'; 18 | -------------------------------------------------------------------------------- /lib/polyfill/Elevation.js: -------------------------------------------------------------------------------- 1 | import {Platform} from 'react-native' 2 | 3 | export default function (elevation){ 4 | if(Platform.OS == 'ios'){ 5 | if(elevation !== 0){ 6 | return { 7 | shadowColor: "black", 8 | shadowOpacity: 0.3, 9 | shadowRadius: elevation, 10 | shadowOffset: { 11 | height: 2, 12 | width: 0 13 | } 14 | } 15 | }else{ 16 | return {} 17 | } 18 | }else{ 19 | return { 20 | elevation: elevation, 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/polyfill/Ripple.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from "react"; 2 | import {View, Animated, TouchableOpacity, Platform} from "react-native"; 3 | 4 | import elevationPolyfill from './Elevation'; 5 | 6 | export default class Ripple extends Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | scaleValue: new Animated.Value(0.001), 12 | fadeValue: new Animated.Value(0.001), 13 | pageX: null, 14 | pageY: null, 15 | rippling: false, 16 | size: null, 17 | location: null, 18 | longPress: false, 19 | elevation: props.elevation ? props.elevation[0] : null 20 | }; 21 | } 22 | 23 | static propTypes = { 24 | rippleColor: PropTypes.string, 25 | elevation: PropTypes.array, 26 | onPress: PropTypes.func, 27 | onLongPress: PropTypes.func, 28 | style: PropTypes.oneOfType([View.propTypes.style, PropTypes.object, PropTypes.array]), 29 | children: PropTypes.element.isRequired 30 | }; 31 | 32 | static defaultProps = { 33 | rippleColor: 'rgba(0,0,0,.2)', 34 | elevation: null 35 | }; 36 | 37 | render() { 38 | const { rippleColor, onPress, onLongPress, children, style } = this.props; 39 | const { fadeValue ,size, pageX, pageY, rippling, scaleValue, location, elevation } = this.state; 40 | let outerStyle = {}, innerStyle = style; 41 | 42 | // Extract padding, margin from style since IOS overflow: hidden has issues with the shadow 43 | if(Platform.OS == 'ios') { 44 | ({outerStyle, innerStyle} = (style instanceof Array ? style : [style]).reduce((obj, styles) => { 45 | if (styles instanceof Object) { 46 | Object.entries(styles).forEach( 47 | ([name, value]) => 48 | [ 49 | 'marginLeft', 50 | 'marginRight', 51 | 'marginTop', 52 | 'marginBottom', 53 | 'marginHorizontal', 54 | 'marginVertical', 55 | 'margin', 56 | 'borderRadius', 57 | 'backgroundColor' 58 | ].indexOf(name) !== -1 ? obj.outerStyle[name] = value : obj.innerStyle[name] = value 59 | ) 60 | } 61 | return obj 62 | }, {outerStyle: {}, innerStyle: {}})); 63 | } 64 | 65 | return ( 66 | 73 | 76 | 77 | {children} 78 | 84 | 99 | 100 | 101 | 102 | 103 | ); 104 | }; 105 | 106 | _highlight = (e) => { 107 | const { elevation } = this.props; 108 | 109 | if (elevation) { 110 | this.setState({ 111 | elevation: elevation[1] 112 | }); 113 | } 114 | 115 | const { pageX, pageY } = e.nativeEvent; 116 | 117 | this.setState({ 118 | rippling: true, 119 | pageX, 120 | pageY 121 | }); 122 | 123 | this._getContainerDimensions(() => { 124 | const duration = (this.state.size / 100) * 110; 125 | 126 | Animated.timing(this.state.scaleValue, { 127 | toValue: 1, 128 | duration: duration < 500 || duration >= 1500 ? 500 : duration 129 | }).start(); 130 | 131 | Animated.timing(this.state.fadeValue, { 132 | toValue: .2, 133 | duration: 200 134 | }).start(); 135 | }); 136 | }; 137 | 138 | _unHighlight = () => { 139 | const { elevation } = this.props; 140 | 141 | if (elevation) { 142 | this.setState({ 143 | elevation: elevation[0] 144 | }); 145 | } 146 | 147 | this.setState({ 148 | rippling: false 149 | }); 150 | 151 | Animated.timing(this.state.scaleValue, { 152 | toValue: 0.001, 153 | duration: 100 154 | }).start(); 155 | 156 | Animated.timing(this.state.fadeValue, { 157 | toValue: 0.001, 158 | duration: 100, 159 | }).start(); 160 | }; 161 | 162 | _getContainerDimensions = (next) => { 163 | this.refs.container.measure((x, y, width, height, pageX, pageY) => { 164 | this.setState({ 165 | size: 3 * (width > height ? width : height), 166 | location: { pageX, pageY } 167 | }, next); 168 | }) 169 | }; 170 | 171 | } 172 | 173 | const styles = { 174 | container: { 175 | overflow: 'hidden' 176 | }, 177 | background: { 178 | flex: 1, 179 | position: 'absolute', 180 | top: 0, 181 | right: 0, 182 | bottom: 0, 183 | left: 0 184 | }, 185 | ripple: { 186 | position: 'absolute' 187 | } 188 | }; 189 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-material-design", 3 | "version": "0.3.8", 4 | "description": "React Native Material Design Components", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "lint": "./node_modules/.bin/eslint ./lib" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/react-native-material-design/react-native-material-design" 13 | }, 14 | "keywords": [ 15 | "react-native", 16 | "material", 17 | "design", 18 | "ui", 19 | "components", 20 | "react-component", 21 | "android" 22 | ], 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/react-native-material-design/react-native-material-design/issues" 26 | }, 27 | "homepage": "https://github.com/react-native-material-design/react-native-material-design", 28 | "dependencies": { 29 | "react-native-material-design-styles": "git+https://github.com/react-native-material-design/react-native-material-design-styles.git", 30 | "react-native-vector-icons": "^2.0.1" 31 | }, 32 | "devDependencies": { 33 | "babel-eslint": "^4.1.6", 34 | "eslint": "^1.6.0", 35 | "eslint-plugin-react": "^3.5.1" 36 | } 37 | } 38 | --------------------------------------------------------------------------------