diff --git a/example/src/DialogExample.js b/example/src/DialogExample.js new file mode 100644 index 0000000..65947fa --- /dev/null +++ b/example/src/DialogExample.js @@ -0,0 +1,75 @@ +/* @flow */ + +import React, { Component } from 'react'; +import { View, StyleSheet } from 'react-native'; +import { Colors, Button } from 'react-native-paper'; +import { + DialogWithCustomColors, + DialogWithLoadingIndicator, + DialogWithLongText, + DialogWithRadioBtns, + UndismissableDialog, +} from './Dialogs'; + +export default class DialogExample extends Component { + static title = 'Dialog'; + + state = { + visible1: false, + visible2: false, + visible3: false, + visible4: false, + visible5: false, + }; + + _openDialog1 = () => this.setState({ visible1: true }); + _openDialog2 = () => this.setState({ visible2: true }); + _openDialog3 = () => this.setState({ visible3: true }); + _openDialog4 = () => this.setState({ visible4: true }); + _openDialog5 = () => this.setState({ visible5: true }); + + _closeDialog1 = () => this.setState({ visible1: false }); + _closeDialog2 = () => this.setState({ visible2: false }); + _closeDialog3 = () => this.setState({ visible3: false }); + _closeDialog4 = () => this.setState({ visible4: false }); + _closeDialog5 = () => this.setState({ visible5: false }); + + render() { + const { visible1, visible2, visible3, visible4, visible5 } = this.state; + return ( + + + + + + + + + + + + + ); + } +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: Colors.grey200, + padding: 16, + }, +}); diff --git a/example/src/Dialogs/DialogWithCustomColors.js b/example/src/Dialogs/DialogWithCustomColors.js new file mode 100644 index 0000000..266f495 --- /dev/null +++ b/example/src/Dialogs/DialogWithCustomColors.js @@ -0,0 +1,37 @@ +/* @flow */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Paragraph, Button, Dialog, Colors } from 'react-native-paper'; + +const DialogWithCustomColors = ({ + visible, + close, +}: { + visible: boolean, + close: Function, +}) => ( + + Alert + + + This is a dialog with custom colors + + + + + + +); + +DialogWithCustomColors.propTypes = { + visible: PropTypes.bool, + close: PropTypes.func, +}; + +export default DialogWithCustomColors; diff --git a/example/src/Dialogs/DialogWithLoadingIndicator.js b/example/src/Dialogs/DialogWithLoadingIndicator.js new file mode 100644 index 0000000..3c88f99 --- /dev/null +++ b/example/src/Dialogs/DialogWithLoadingIndicator.js @@ -0,0 +1,36 @@ +/* @flow */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { ActivityIndicator, Platform, View } from 'react-native'; +import { Paragraph, Colors, Dialog } from 'react-native-paper'; + +const isIOS = Platform.OS === 'ios'; + +const DialogWithLoadingIndicator = ({ + visible, + close, +}: { + visible: boolean, + close: Function, +}) => ( + + Progress Dialog + + + + Loading..... + + + +); + +DialogWithLoadingIndicator.propTypes = { + visible: PropTypes.bool, + close: PropTypes.func, +}; + +export default DialogWithLoadingIndicator; diff --git a/example/src/Dialogs/DialogWithLongText.js b/example/src/Dialogs/DialogWithLongText.js new file mode 100644 index 0000000..5022aca --- /dev/null +++ b/example/src/Dialogs/DialogWithLongText.js @@ -0,0 +1,55 @@ +/* @flow */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { ScrollView } from 'react-native'; +import { Paragraph, Button, Dialog } from 'react-native-paper'; + +const DialogWithLongText = ({ + visible, + close, +}: { + visible: boolean, + close: Function, +}) => ( + + Alert + + + + Material is the metaphor + {'\n'} + {'\n'} + A material metaphor is the unifying theory of a rationalized space and + a system of motion. The material is grounded in tactile reality, + inspired by the study of paper and ink, yet technologically advanced + and open to imagination and magic. + {'\n'} + {'\n'} + Surfaces and edges of the material provide visual cues that are + grounded in reality. The use of familiar tactile attributes helps + users quickly understand affordances. Yet the flexibility of the + material creates new affordances that supersede those in the physical + world, without breaking the rules of physics. + {'\n'} + {'\n'} + The fundamentals of light, surface, and movement are key to conveying + how objects move, interact, and exist in space and in relation to each + other. Realistic lighting shows seams, divides space, and indicates + moving parts. + + + + + + + +); + +DialogWithLongText.propTypes = { + visible: PropTypes.bool, + close: PropTypes.func, +}; + +export default DialogWithLongText; diff --git a/example/src/Dialogs/DialogWithRadioBtns.js b/example/src/Dialogs/DialogWithRadioBtns.js new file mode 100644 index 0000000..8066856 --- /dev/null +++ b/example/src/Dialogs/DialogWithRadioBtns.js @@ -0,0 +1,84 @@ +/* @flow */ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { ScrollView, View, StyleSheet } from 'react-native'; +import { Paragraph, Button, Dialog, RadioButton } from 'react-native-paper'; + +type Props = { + visible: boolean, + close: Function, +}; + +type State = { + checked: number, +}; + +export default class extends Component { + static propTypes = { + visible: PropTypes.bool, + close: PropTypes.func, + }; + state = { + checked: 0, + }; + + state: State; + + render() { + const { checked } = this.state; + const { visible, close } = this.props; + return ( + + Choose an option + + + + + this.setState({ checked: 0 })} + /> + Option 1 + + + this.setState({ checked: 1 })} + /> + Option 2 + + + this.setState({ checked: 2 })} + /> + Option 3 + + + this.setState({ checked: 3 })} + /> + Option 4 + + + + + + + + + ); + } +} + +const styles = StyleSheet.create({ + checkBoxRow: { + flexDirection: 'row', + alignItems: 'center', + height: 56, + }, + paragraph: { marginLeft: 16 }, +}); diff --git a/example/src/Dialogs/UndismissableDialog.js b/example/src/Dialogs/UndismissableDialog.js new file mode 100644 index 0000000..5807ccd --- /dev/null +++ b/example/src/Dialogs/UndismissableDialog.js @@ -0,0 +1,34 @@ +/* @flow */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Paragraph, Button, Dialog, Colors } from 'react-native-paper'; + +const DialogWithLongText = ({ + visible, + close, +}: { + visible: boolean, + close: Function, +}) => ( + + Alert + + This is an undismissable dialog!! + + + + + + +); + +DialogWithLongText.propTypes = { + visible: PropTypes.bool, + close: PropTypes.func, +}; + +export default DialogWithLongText; diff --git a/example/src/Dialogs/index.js b/example/src/Dialogs/index.js new file mode 100644 index 0000000..542e899 --- /dev/null +++ b/example/src/Dialogs/index.js @@ -0,0 +1,9 @@ +/* @flow */ + +export { default as DialogWithCustomColors } from './DialogWithCustomColors'; +export { + default as DialogWithLoadingIndicator, +} from './DialogWithLoadingIndicator'; +export { default as DialogWithLongText } from './DialogWithLongText'; +export { default as DialogWithRadioBtns } from './DialogWithRadioBtns'; +export { default as UndismissableDialog } from './UndismissableDialog'; diff --git a/example/src/ExampleList.js b/example/src/ExampleList.js index 26d9698..f2363bf 100644 --- a/example/src/ExampleList.js +++ b/example/src/ExampleList.js @@ -8,6 +8,7 @@ import ButtonExample from './ButtonExample'; import FABExample from './FABExample'; import CardExample from './CardExample'; import CheckboxExample from './CheckboxExample'; +import DialogExample from './DialogExample'; import DividerExample from './DividerExample'; import GridViewExample from './GridViewExample'; import PaperExample from './PaperExample'; @@ -24,6 +25,7 @@ export const examples = { fab: FABExample, card: CardExample, checkbox: CheckboxExample, + dialog: DialogExample, divider: DividerExample, grid: GridViewExample, paper: PaperExample, diff --git a/src/components/Dialog/Actions.js b/src/components/Dialog/Actions.js new file mode 100644 index 0000000..e2e7a3f --- /dev/null +++ b/src/components/Dialog/Actions.js @@ -0,0 +1,38 @@ +/* @flow */ + +import React, { Children } from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet, View, ViewPropTypes } from 'react-native'; + +type Props = { + children?: any, + style?: any, +}; + +const DialogActions = (props: Props) => { + return ( + + {Children.map(props.children, child => + React.cloneElement(child, { + compact: true, + }) + )} + + ); +}; + +DialogActions.propTypes = { + children: PropTypes.node.isRequired, + style: ViewPropTypes.style, +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-end', + padding: 4, + }, +}); + +export default DialogActions; diff --git a/src/components/Dialog/Content.js b/src/components/Dialog/Content.js new file mode 100644 index 0000000..aec9f81 --- /dev/null +++ b/src/components/Dialog/Content.js @@ -0,0 +1,22 @@ +/* @flow */ + +import React from 'react'; +import { View, StyleSheet } from 'react-native'; + +type Props = { + children?: any, + style?: any, +}; + +const DialogContent = ({ children, style }: Props) => ( + {children} +); + +const styles = StyleSheet.create({ + container: { + paddingBottom: 24, + paddingHorizontal: 24, + }, +}); + +export default DialogContent; diff --git a/src/components/Dialog/ScrollArea.js b/src/components/Dialog/ScrollArea.js new file mode 100644 index 0000000..2ae1fa1 --- /dev/null +++ b/src/components/Dialog/ScrollArea.js @@ -0,0 +1,25 @@ +/* @flow */ + +import React from 'react'; +import { View, StyleSheet } from 'react-native'; +import { grey400 } from '../../styles/colors'; + +type Props = { + children?: any, + style?: any, +}; + +const DialogScrollArea = ({ children, style }: Props) => ( + {children} +); + +const styles = StyleSheet.create({ + container: { + borderColor: grey400, + borderTopWidth: StyleSheet.hairlineWidth, + borderBottomWidth: StyleSheet.hairlineWidth, + paddingHorizontal: 24, + }, +}); + +export default DialogScrollArea; diff --git a/src/components/Dialog/Title.js b/src/components/Dialog/Title.js new file mode 100644 index 0000000..1b1145e --- /dev/null +++ b/src/components/Dialog/Title.js @@ -0,0 +1,40 @@ +/* @flow */ + +import React from 'react'; +import { StyleSheet } from 'react-native'; +import PropTypes from 'prop-types'; +import PaperTitle from '../Typography/Title'; +import withTheme from '../../core/withTheme'; +import type { Theme } from '../../types/Theme'; + +type Props = { + children: any, + style?: any, + theme: Theme, +}; + +const DialogTitle = ({ + children, + theme: { colors: { text } }, + style, +}: Props) => ( + + {children} + +); + +const styles = StyleSheet.create({ + text: { + marginTop: 22, + marginBottom: 18, + marginHorizontal: 24, + }, +}); + +DialogTitle.propTypes = { + children: PropTypes.node.isRequired, + style: PropTypes.object, + theme: PropTypes.object.isRequired, +}; + +export default withTheme(DialogTitle); diff --git a/src/components/Dialog/index.js b/src/components/Dialog/index.js new file mode 100644 index 0000000..2fc2cc0 --- /dev/null +++ b/src/components/Dialog/index.js @@ -0,0 +1,143 @@ +/* @flow */ + +import React, { Children } from 'react'; +import { StyleSheet, Platform, Animated } from 'react-native'; +import PropTypes from 'prop-types'; +import Modal from '../Modal'; +import { black, white } from '../../styles/colors'; +import Paper from '../Paper'; +import DialogActions from './Actions'; +import DialogTitle from './Title'; +import DialogContent from './Content'; +import DialogScrollArea from './ScrollArea'; + +const AnimatedPaper = Animated.createAnimatedComponent(Paper); + +type Props = { + children?: any, + dismissable?: boolean, + onRequestClose?: Function, + style?: any, + visible: boolean, +}; + +/** + * Dialogs inform users about a specific task and may contain critical information, require decisions, or involve multiple tasks. + * + * ``` + * export default class MyComponent extends Component { + * state = { + * visible: false, + * }; + * + * _showDialog = () => this.setState({ visble: true }); + * _hideDialog = () => this.setState({ visble: false }); + * + * render() { + * const { visible } = this.state; + * return ( + * + * + * + * Alert + * + * This is simple dialog + * + * + * + * + * + * + * ); + * } + * } + * ``` + */ + +const Dialog = (props: Props) => { + const { children, dismissable, onRequestClose, visible, style } = props; + const childrenArray = Children.toArray(children); + const title = childrenArray.find(child => child.type === DialogTitle); + const actionBtnsChildren = childrenArray.filter( + child => child.type === DialogActions + ); + const restOfChildren = childrenArray.filter( + child => child.type !== DialogActions && child.type !== DialogTitle + ); + let restOfChildrenWithoutTitle = restOfChildren; + if (!title) { + let found = false; + restOfChildrenWithoutTitle = restOfChildren.map(child => { + if (child.type === DialogContent && !found) { + found = true; + return React.cloneElement(child, { + style: { paddingTop: 24 }, + }); + } else { + return child; + } + }); + } + return ( + + + {title} + {restOfChildrenWithoutTitle} + {actionBtnsChildren} + + + ); +}; + +Dialog.Actions = DialogActions; +Dialog.Title = DialogTitle; +Dialog.Content = DialogContent; +Dialog.ScrollArea = DialogScrollArea; + +Dialog.propTypes = { + children: PropTypes.node.isRequired, + /** + * Determines whether clicking outside the dialog dismiss it, true by default + */ + dismissable: PropTypes.bool, + /** + * Callback that is called when the user dismisses the dialog + */ + onRequestClose: PropTypes.func.isRequired, + style: PropTypes.object, + /** + * Determines Whether the dialog is visible + */ + visible: PropTypes.bool, +}; + +Dialog.defaultProps = { + dismissable: true, + titleColor: black, + visible: false, +}; + +export default Dialog; + +const styles = StyleSheet.create({ + container: { + /** + * This prevents the shadow from being clipped on Android since Android + * doesn't support `overflow: visible`. + * One downside for this fix is that it will disable clicks on the area + * of the shadow around the dialog, consequently, if you click around the + * dialog (44 pixel from the top and bottom) it won't be dismissed. + */ + marginVertical: Platform.OS === 'android' ? 44 : 0, + marginHorizontal: 26, + borderRadius: 2, + backgroundColor: white, + }, +}); diff --git a/src/components/Modal.js b/src/components/Modal.js new file mode 100644 index 0000000..674f449 --- /dev/null +++ b/src/components/Modal.js @@ -0,0 +1,178 @@ +/* @flow */ + +import React, { PureComponent } from 'react'; +import { + Animated, + View, + Easing, + StyleSheet, + TouchableWithoutFeedback, + BackHandler, +} from 'react-native'; +import PropTypes from 'prop-types'; +import ThemedPortal from './Portal/ThemedPortal'; + +type Props = { + children?: any, + dismissable?: boolean, + onRequestClose?: Function, + visible: boolean, +}; + +type DefaultProps = { + dismissable: boolean, + visible: boolean, +}; + +type State = { + opacity: Animated.Value, + rendered: boolean, +}; + +/** + * The Modal component is a simple way to present content above an enclosing view. + * + * ``` + * export default class MyComponent extends Component { + * state = { + * visible: false, + * }; + * + * _showModal = () => this.setState({ visble: true }); + * _hideModal = () => this.setState({ visble: false }); + * + * render() { + * const { visible } = this.state; + * return ( + * + * Example Modal + * + * ); + * } + * } + * ``` + */ + +export default class Modal extends PureComponent { + static propTypes = { + children: PropTypes.node.isRequired, + /** + * Determines whether clicking outside the dialog dismiss it, true by default + */ + dismissable: PropTypes.bool, + /** + * Callback that is called when the user dismisses the dialog + */ + onRequestClose: PropTypes.func.isRequired, + /** + * Determines Whether the dialog is visible + */ + visible: PropTypes.bool, + }; + + static defaultProps = { + dismissable: true, + visible: false, + }; + + constructor(props: Props) { + super(props); + this.state = { + opacity: new Animated.Value(props.visible ? 1 : 0), + rendered: props.visible, + }; + } + + state: State; + + componentWillReceiveProps({ visible }: Props) { + if (this.props.visible !== visible) { + if (visible) { + this.setState({ + rendered: true, + }); + } + } + } + + componentDidUpdate({ visible }: Props) { + if (visible !== this.props.visible) { + if (this.props.visible) { + this._showModal(); + } else { + this._hideModal(); + } + } + } + + _handleBack = () => { + if (this.props.dismissable) { + this._hideModal(); + } + return true; + }; + + _showModal = () => { + BackHandler.addEventListener('hardwareBackPress', this._handleBack); + Animated.timing(this.state.opacity, { + toValue: 1, + duration: 280, + easing: Easing.ease, + }).start(); + }; + + _hideModal = () => { + BackHandler.removeEventListener('hardwareBackPress', this._handleBack); + Animated.timing(this.state.opacity, { + toValue: 0, + duration: 280, + easing: Easing.ease, + }).start(() => { + if (this.props.visible && this.props.onRequestClose) { + this.props.onRequestClose(); + } + if (this.props.visible) { + this._showModal(); + } else { + this.setState({ + rendered: false, + }); + } + }); + }; + + render() { + if (!this.state.rendered) return null; + + const { children, dismissable } = this.props; + return ( + + + + {dismissable && ( + + + + )} + + {children} + + + + ); + } +} + +const styles = StyleSheet.create({ + wrapper: { + ...StyleSheet.absoluteFillObject, + justifyContent: 'center', + }, +}); diff --git a/src/index.js b/src/index.js index 0cb3e5b..effb3ea 100644 --- a/src/index.js +++ b/src/index.js @@ -13,6 +13,7 @@ export { default as Button } from './components/Button'; export { default as FAB } from './components/FAB'; export { default as Card } from './components/Card'; export { default as Checkbox } from './components/Checkbox'; +export { default as Dialog } from './components/Dialog'; export { default as Paper } from './components/Paper'; export { default as RadioButton } from './components/RadioButton'; export { default as TouchableRipple } from './components/TouchableRipple'; @@ -29,6 +30,7 @@ export { default as DrawerItem } from './components/DrawerItem'; export { default as DrawerSection } from './components/DrawerSection'; export { default as GridView } from './components/GridView'; export { default as SearchBar } from './components/SearchBar'; +export { default as Modal } from './components/Modal'; export { default as ProgressBar } from './components/ProgressBar'; export { default as Toolbar } from './components/Toolbar';