[RN] Support children in Dialog

This commit is contained in:
Lyubo Marinov 2017-09-18 02:01:14 -05:00
parent 2496b3ec02
commit d0476991a6
4 changed files with 140 additions and 65 deletions

View File

@ -1,32 +1,38 @@
import React, { Component } from 'react'; import PropTypes from 'prop-types';
import { Component } from 'react';
import { hideDialog } from '../actions'; import { hideDialog } from '../actions';
import { DIALOG_PROP_TYPES } from '../constants'; import { DIALOG_PROP_TYPES } from '../constants';
/** /**
* Abstract dialog to display dialogs. * An abstract implementation of a dialog on Web/React and mobile/react-native.
*/ */
export default class AbstractDialog extends Component { export default class AbstractDialog extends Component {
/** /**
* Abstract Dialog component's property types. * <tt>AbstractDialog</tt> React <tt>Component</tt>'s prop types.
* *
* @static * @static
*/ */
static propTypes = { static propTypes = {
...DIALOG_PROP_TYPES, ...DIALOG_PROP_TYPES,
/**
* The React <tt>Component</tt> children of <tt>AbstractDialog</tt>
* which represents the dialog's body.
*/
children: PropTypes.node,
/** /**
* Used to show/hide the dialog on cancel. * Used to show/hide the dialog on cancel.
*/ */
dispatch: React.PropTypes.func dispatch: PropTypes.func
}; };
/** /**
* Initializes a new Dialog instance. * Initializes a new <tt>AbstractDialog</tt> instance.
* *
* @param {Object} props - The read-only properties with which the new * @param {Object} props - The read-only React <tt>Component</tt> props with
* instance is to be initialized. * which the new instance is to be initialized.
*/ */
constructor(props) { constructor(props) {
super(props); super(props);
@ -36,37 +42,30 @@ export default class AbstractDialog extends Component {
} }
/** /**
* Dispatches action to hide the dialog. * Dispatches a redux action to hide this dialog when it's canceled.
* *
* @protected
* @returns {void} * @returns {void}
*/ */
_onCancel() { _onCancel() {
let hide = true; const { onCancel } = this.props;
if (this.props.onCancel) { if (!onCancel || onCancel()) {
hide = this.props.onCancel();
}
if (hide) {
this.props.dispatch(hideDialog()); this.props.dispatch(hideDialog());
} }
} }
/** /**
* Dispatches the action when submitting the dialog. * Dispatches a redux action to hide this dialog when it's submitted.
* *
* @private * @private
* @param {string} value - The submitted value if any. * @param {string} value - The submitted value if any.
* @returns {void} * @returns {void}
*/ */
_onSubmit(value) { _onSubmit(value) {
let hide = true; const { onSubmit } = this.props;
if (this.props.onSubmit) { if (!onSubmit || onSubmit(value)) {
hide = this.props.onSubmit(value);
}
if (hide) {
this.props.dispatch(hideDialog()); this.props.dispatch(hideDialog());
} }
} }

View File

@ -1,4 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { TextInput } from 'react-native';
import Prompt from 'react-native-prompt'; import Prompt from 'react-native-prompt';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -7,20 +9,21 @@ import { translate } from '../../i18n';
import AbstractDialog from './AbstractDialog'; import AbstractDialog from './AbstractDialog';
/** /**
* Native dialog using Prompt. * Implements <tt>AbstractDialog</tt> on react-native using <tt>Prompt</tt>.
*/ */
class Dialog extends AbstractDialog { class Dialog extends AbstractDialog {
/** /**
* Native sialog component's property types. * <tt>AbstractDialog</tt>'s React <tt>Component</tt> prop types.
* *
* @static * @static
*/ */
static propTypes = { static propTypes = {
...AbstractDialog.propTypes,
/** /**
* I18n key to put as body title. * I18n key to put as body title.
*/ */
bodyKey: React.PropTypes.string bodyKey: PropTypes.string
}; };
/** /**
@ -31,27 +34,109 @@ class Dialog extends AbstractDialog {
*/ */
render() { render() {
const { const {
cancelDisabled,
cancelTitleKey,
bodyKey, bodyKey,
cancelDisabled,
cancelTitleKey = 'dialog.Cancel',
children,
okDisabled, okDisabled,
okTitleKey, okTitleKey = 'dialog.Ok',
t, t,
titleKey titleKey,
titleString
} = this.props; } = this.props;
return ( /* eslint-disable react/jsx-wrap-multilines */
<Prompt
cancelText = { cancelDisabled let element
? undefined : t(cancelTitleKey || 'dialog.Cancel') } = <Prompt
cancelText = { cancelDisabled ? undefined : t(cancelTitleKey) }
onCancel = { this._onCancel } onCancel = { this._onCancel }
onSubmit = { this._onSubmit } onSubmit = { this._onSubmit }
placeholder = { t(bodyKey) } placeholder = { t(bodyKey) }
submitText = { okDisabled submitText = { okDisabled ? undefined : t(okTitleKey) }
? undefined : t(okTitleKey || 'dialog.Ok') } title = { titleString || t(titleKey) }
title = { t(titleKey) } visible = { true } />;
visible = { true } />
); /* eslint-enable react/jsx-wrap-multilines */
if (React.Children.count(children)) {
// XXX The following implements a workaround with knowledge of the
// implementation of react-native-prompt.
element
= this._replaceFirstElementOfType(
// eslint-disable-next-line no-extra-parens, new-cap
(new (element.type)(element.props)).render(),
TextInput,
children);
}
return element;
}
/**
* Creates a deep clone of a specific <tt>ReactElement</tt> with the results
* of calling a specific function on every node of a specific
* <tt>ReactElement</tt> tree.
*
* @param {ReactElement} element - The <tt>ReactElement</tt> to clone and
* call the specified <tt>f</tt> on.
* @param {Function} f - The function to call on every node of the
* <tt>ReactElement</tt> tree represented by the specified <tt>element</tt>.
* @private
* @returns {ReactElement}
*/
_mapReactElement(element, f) {
if (!element || !element.props || !element.type) {
return element;
}
let mapped = f(element);
if (mapped === element) {
mapped
= React.cloneElement(
element,
/* props */ undefined,
...React.Children.toArray(React.Children.map(
element.props.children,
function(element) { // eslint-disable-line no-shadow
// eslint-disable-next-line no-invalid-this
return this._mapReactElement(element, f);
},
this)));
}
return mapped;
}
/**
* Replaces the first <tt>ReactElement</tt> of a specific type found in a
* specific <tt>ReactElement</tt> tree with a specific replacement
* <tt>ReactElement</tt>.
*
* @param {ReactElement} element - The <tt>ReactElement</tt> tree to search
* through and replace in.
* @param {*} type - The type of the <tt>ReactElement</tt> to be replaced.
* @param {ReactElement} replacement - The <tt>ReactElement</tt> to replace
* the first <tt>ReactElement</tt> in <tt>element</tt> of the specified
* <tt>type</tt>.
* @private
* @returns {ReactElement}
*/
_replaceFirstElementOfType(element, type, replacement) {
// eslint-disable-next-line no-shadow
return this._mapReactElement(element, element => {
if (replacement && element.type === type) {
/* eslint-disable no-param-reassign */
element = replacement;
replacement = undefined;
/* eslint-enable no-param-reassign */
}
return element;
});
} }
} }

View File

@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -8,7 +9,6 @@ import StatelessDialog from './StatelessDialog';
* Web dialog that uses atlaskit modal-dialog to display dialogs. * Web dialog that uses atlaskit modal-dialog to display dialogs.
*/ */
class Dialog extends AbstractDialog { class Dialog extends AbstractDialog {
/** /**
* Web dialog component's property types. * Web dialog component's property types.
* *
@ -17,21 +17,16 @@ class Dialog extends AbstractDialog {
static propTypes = { static propTypes = {
...AbstractDialog.propTypes, ...AbstractDialog.propTypes,
/**
* This is the body of the dialog, the component children.
*/
children: React.PropTypes.node,
/** /**
* Whether the dialog is modal. This means clicking on the blanket will * Whether the dialog is modal. This means clicking on the blanket will
* leave the dialog open. No cancel button. * leave the dialog open. No cancel button.
*/ */
isModal: React.PropTypes.bool, isModal: PropTypes.bool,
/** /**
* Disables rendering of the submit button. * Disables rendering of the submit button.
*/ */
submitDisabled: React.PropTypes.bool, submitDisabled: PropTypes.bool,
/** /**
* Width of the dialog, can be: * Width of the dialog, can be:
@ -40,7 +35,7 @@ class Dialog extends AbstractDialog {
* - integer value for pixel width * - integer value for pixel width
* - string value for percentage * - string value for percentage
*/ */
width: React.PropTypes.string width: PropTypes.string
}; };
/** /**
@ -65,8 +60,8 @@ class Dialog extends AbstractDialog {
render() { render() {
const props = { const props = {
...this.props, ...this.props,
onSubmit: this._onSubmit, onCancel: this._onCancel,
onCancel: this._onCancel onSubmit: this._onSubmit
}; };
delete props.dispatch; delete props.dispatch;
@ -80,11 +75,7 @@ class Dialog extends AbstractDialog {
* @returns {void} * @returns {void}
*/ */
_onCancel() { _onCancel() {
if (this.props.isModal) { this.props.isModal || super._onCancel();
return;
}
super._onCancel();
} }
} }

View File

@ -1,50 +1,50 @@
import React from 'react'; import PropTypes from 'prop-types';
export const DIALOG_PROP_TYPES = { export const DIALOG_PROP_TYPES = {
/** /**
* Whether cancel button is disabled. Enabled by default. * Whether cancel button is disabled. Enabled by default.
*/ */
cancelDisabled: React.PropTypes.bool, cancelDisabled: PropTypes.bool,
/** /**
* Optional i18n key to change the cancel button title. * Optional i18n key to change the cancel button title.
*/ */
cancelTitleKey: React.PropTypes.string, cancelTitleKey: PropTypes.string,
/** /**
* Is ok button enabled/disabled. Enabled by default. * Is ok button enabled/disabled. Enabled by default.
*/ */
okDisabled: React.PropTypes.bool, okDisabled: PropTypes.bool,
/** /**
* Optional i18n key to change the ok button title. * Optional i18n key to change the ok button title.
*/ */
okTitleKey: React.PropTypes.string, okTitleKey: PropTypes.string,
/** /**
* The handler for onCancel event. * The handler for onCancel event.
*/ */
onCancel: React.PropTypes.func, onCancel: PropTypes.func,
/** /**
* The handler for the event when submitting the dialog. * The handler for the event when submitting the dialog.
*/ */
onSubmit: React.PropTypes.func, onSubmit: PropTypes.func,
/** /**
* Used to obtain translations in children classes. * Used to obtain translations in children classes.
*/ */
t: React.PropTypes.func, t: PropTypes.func,
/** /**
* Key to use for showing a title. * Key to use for showing a title.
*/ */
titleKey: React.PropTypes.string, titleKey: PropTypes.string,
/** /**
* The string to use as a title instead of {@code titleKey}. If a truthy * The string to use as a title instead of {@code titleKey}. If a truthy
* value is specified, it takes precedence over {@code titleKey} i.e. * value is specified, it takes precedence over {@code titleKey} i.e.
* the latter is unused. * the latter is unused.
*/ */
titleString: React.PropTypes.string titleString: PropTypes.string
}; };