[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 { 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 {
/**
* Abstract Dialog component's property types.
* <tt>AbstractDialog</tt> React <tt>Component</tt>'s prop types.
*
* @static
*/
static propTypes = {
...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.
*/
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
* instance is to be initialized.
* @param {Object} props - The read-only React <tt>Component</tt> props with
* which the new instance is to be initialized.
*/
constructor(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}
*/
_onCancel() {
let hide = true;
const { onCancel } = this.props;
if (this.props.onCancel) {
hide = this.props.onCancel();
}
if (hide) {
if (!onCancel || onCancel()) {
this.props.dispatch(hideDialog());
}
}
/**
* Dispatches the action when submitting the dialog.
* Dispatches a redux action to hide this dialog when it's submitted.
*
* @private
* @param {string} value - The submitted value if any.
* @returns {void}
*/
_onSubmit(value) {
let hide = true;
const { onSubmit } = this.props;
if (this.props.onSubmit) {
hide = this.props.onSubmit(value);
}
if (hide) {
if (!onSubmit || onSubmit(value)) {
this.props.dispatch(hideDialog());
}
}

View File

@ -1,4 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import { TextInput } from 'react-native';
import Prompt from 'react-native-prompt';
import { connect } from 'react-redux';
@ -7,20 +9,21 @@ import { translate } from '../../i18n';
import AbstractDialog from './AbstractDialog';
/**
* Native dialog using Prompt.
* Implements <tt>AbstractDialog</tt> on react-native using <tt>Prompt</tt>.
*/
class Dialog extends AbstractDialog {
/**
* Native sialog component's property types.
* <tt>AbstractDialog</tt>'s React <tt>Component</tt> prop types.
*
* @static
*/
static propTypes = {
...AbstractDialog.propTypes,
/**
* I18n key to put as body title.
*/
bodyKey: React.PropTypes.string
bodyKey: PropTypes.string
};
/**
@ -31,27 +34,109 @@ class Dialog extends AbstractDialog {
*/
render() {
const {
cancelDisabled,
cancelTitleKey,
bodyKey,
cancelDisabled,
cancelTitleKey = 'dialog.Cancel',
children,
okDisabled,
okTitleKey,
okTitleKey = 'dialog.Ok',
t,
titleKey
titleKey,
titleString
} = this.props;
return (
<Prompt
cancelText = { cancelDisabled
? undefined : t(cancelTitleKey || 'dialog.Cancel') }
/* eslint-disable react/jsx-wrap-multilines */
let element
= <Prompt
cancelText = { cancelDisabled ? undefined : t(cancelTitleKey) }
onCancel = { this._onCancel }
onSubmit = { this._onSubmit }
placeholder = { t(bodyKey) }
submitText = { okDisabled
? undefined : t(okTitleKey || 'dialog.Ok') }
title = { t(titleKey) }
visible = { true } />
);
submitText = { okDisabled ? undefined : t(okTitleKey) }
title = { titleString || t(titleKey) }
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 { connect } from 'react-redux';
@ -8,7 +9,6 @@ import StatelessDialog from './StatelessDialog';
* Web dialog that uses atlaskit modal-dialog to display dialogs.
*/
class Dialog extends AbstractDialog {
/**
* Web dialog component's property types.
*
@ -17,21 +17,16 @@ class Dialog extends AbstractDialog {
static 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
* leave the dialog open. No cancel button.
*/
isModal: React.PropTypes.bool,
isModal: PropTypes.bool,
/**
* Disables rendering of the submit button.
*/
submitDisabled: React.PropTypes.bool,
submitDisabled: PropTypes.bool,
/**
* Width of the dialog, can be:
@ -40,7 +35,7 @@ class Dialog extends AbstractDialog {
* - integer value for pixel width
* - string value for percentage
*/
width: React.PropTypes.string
width: PropTypes.string
};
/**
@ -65,8 +60,8 @@ class Dialog extends AbstractDialog {
render() {
const props = {
...this.props,
onSubmit: this._onSubmit,
onCancel: this._onCancel
onCancel: this._onCancel,
onSubmit: this._onSubmit
};
delete props.dispatch;
@ -80,11 +75,7 @@ class Dialog extends AbstractDialog {
* @returns {void}
*/
_onCancel() {
if (this.props.isModal) {
return;
}
super._onCancel();
this.props.isModal || super._onCancel();
}
}

View File

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