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,
+}) => (
+
+);
+
+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,
+}) => (
+
+);
+
+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,
+}) => (
+
+);
+
+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 (
+
+ );
+ }
+}
+
+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,
+}) => (
+
+);
+
+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 (
+ *
+ *
+ *
+ *
+ * );
+ * }
+ * }
+ * ```
+ */
+
+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';