From d0476991a61376f2e4c94496ae68d52008ec6d2f Mon Sep 17 00:00:00 2001 From: Lyubo Marinov Date: Mon, 18 Sep 2017 02:01:14 -0500 Subject: [PATCH] [RN] Support children in Dialog --- .../base/dialog/components/AbstractDialog.js | 43 ++++--- .../base/dialog/components/Dialog.native.js | 119 +++++++++++++++--- .../base/dialog/components/Dialog.web.js | 23 ++-- react/features/base/dialog/constants.js | 20 +-- 4 files changed, 140 insertions(+), 65 deletions(-) diff --git a/react/features/base/dialog/components/AbstractDialog.js b/react/features/base/dialog/components/AbstractDialog.js index 1549e17ec..d9633f51f 100644 --- a/react/features/base/dialog/components/AbstractDialog.js +++ b/react/features/base/dialog/components/AbstractDialog.js @@ -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. + * AbstractDialog React Component's prop types. * * @static */ static propTypes = { ...DIALOG_PROP_TYPES, + /** + * The React Component children of AbstractDialog + * 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 AbstractDialog instance. * - * @param {Object} props - The read-only properties with which the new - * instance is to be initialized. + * @param {Object} props - The read-only React Component 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()); } } diff --git a/react/features/base/dialog/components/Dialog.native.js b/react/features/base/dialog/components/Dialog.native.js index e2637fc96..fbc102562 100644 --- a/react/features/base/dialog/components/Dialog.native.js +++ b/react/features/base/dialog/components/Dialog.native.js @@ -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 AbstractDialog on react-native using Prompt. */ class Dialog extends AbstractDialog { - /** - * Native sialog component's property types. + * AbstractDialog's React Component 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 ( - - ); + 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 ReactElement with the results + * of calling a specific function on every node of a specific + * ReactElement tree. + * + * @param {ReactElement} element - The ReactElement to clone and + * call the specified f on. + * @param {Function} f - The function to call on every node of the + * ReactElement tree represented by the specified element. + * @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 ReactElement of a specific type found in a + * specific ReactElement tree with a specific replacement + * ReactElement. + * + * @param {ReactElement} element - The ReactElement tree to search + * through and replace in. + * @param {*} type - The type of the ReactElement to be replaced. + * @param {ReactElement} replacement - The ReactElement to replace + * the first ReactElement in element of the specified + * type. + * @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; + }); } } diff --git a/react/features/base/dialog/components/Dialog.web.js b/react/features/base/dialog/components/Dialog.web.js index 006fc345d..f7790e4e0 100644 --- a/react/features/base/dialog/components/Dialog.web.js +++ b/react/features/base/dialog/components/Dialog.web.js @@ -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(); } } diff --git a/react/features/base/dialog/constants.js b/react/features/base/dialog/constants.js index ec112b730..676add1d0 100644 --- a/react/features/base/dialog/constants.js +++ b/react/features/base/dialog/constants.js @@ -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 };