[RN] Power to Dialog
* Implement disabling buttons (like Web had the ability). * Use consistent colors (e.g. for the buttons) like the rest of the app (e.g. WelcomePage). * Enable AbstractDialog to await a thenable from onSubmit and Dialog to render a LoadingIndicator in place of the OK/submit button text.
This commit is contained in:
parent
1ea62215f6
commit
a12984ed6f
|
@ -37,8 +37,33 @@ export default class AbstractDialog extends Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
};
|
||||
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
this._onSubmitFulfilled = this._onSubmitFulfilled.bind(this);
|
||||
this._onSubmitRejected = this._onSubmitRejected.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentWillMount()}. Invoked
|
||||
* immediately before mounting occurs.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillMount() {
|
||||
this._mounted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentWillUnmount()}. Invoked
|
||||
* immediately before this component is unmounted and destroyed.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._mounted = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -48,25 +73,79 @@ export default class AbstractDialog extends Component {
|
|||
* @returns {void}
|
||||
*/
|
||||
_onCancel() {
|
||||
const { onCancel } = this.props;
|
||||
const { cancelDisabled, onCancel } = this.props;
|
||||
|
||||
if (!onCancel || onCancel()) {
|
||||
if ((typeof cancelDisabled === 'undefined' || !cancelDisabled)
|
||||
&& (!onCancel || onCancel())) {
|
||||
this.props.dispatch(hideDialog());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a redux action to hide this dialog when it's submitted.
|
||||
* Submits this dialog. If the React <tt>Component</tt> prop
|
||||
* <tt>onSubmit</tt> is defined, the function that is the value of the prop
|
||||
* is invoked. If the function returns a <tt>thenable</tt>, then the
|
||||
* resolution of the <tt>thenable</tt> is awaited. If the submission
|
||||
* completes successfully, a redux action will be dispatched to hide this
|
||||
* dialog.
|
||||
*
|
||||
* @private
|
||||
* @param {string} value - The submitted value if any.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSubmit(value) {
|
||||
const { onSubmit } = this.props;
|
||||
const { okDisabled, onSubmit } = this.props;
|
||||
|
||||
if (!onSubmit || onSubmit(value)) {
|
||||
this.props.dispatch(hideDialog());
|
||||
if (typeof okDisabled === 'undefined' || !okDisabled) {
|
||||
this.setState({ submitting: true });
|
||||
|
||||
// Invoke the React Compnent prop onSubmit if any.
|
||||
const r = !onSubmit || onSubmit(value);
|
||||
|
||||
// If the invocation returns a thenable, await its resolution;
|
||||
// otherwise, treat the return value as a boolean indicating whether
|
||||
// the submission has completed successfully.
|
||||
let then;
|
||||
|
||||
if (r) {
|
||||
switch (typeof r) {
|
||||
case 'function':
|
||||
case 'object':
|
||||
then = r.then;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (typeof then === 'function' && then.length === 2) {
|
||||
then.call(r, this._onSubmitFulfilled, this._onSubmitRejected);
|
||||
} else if (r) {
|
||||
this._onSubmitFulfilled();
|
||||
} else {
|
||||
this._onSubmitRejected();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies this <tt>AbstractDialog</tt> that it has been submitted
|
||||
* successfully. Dispatches a redux action to hide this dialog after it has
|
||||
* been submitted.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSubmitFulfilled() {
|
||||
this._mounted && this.setState({ submitting: false });
|
||||
|
||||
this.props.dispatch(hideDialog());
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies this <tt>AbstractDialog</tt> that its submission has failed.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSubmitRejected() {
|
||||
this._mounted && this.setState({ submitting: false });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,31 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { TextInput } from 'react-native';
|
||||
import { StyleSheet, TextInput } from 'react-native';
|
||||
import Prompt from 'react-native-prompt';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../i18n';
|
||||
import { LoadingIndicator } from '../../react';
|
||||
import { set } from '../../redux';
|
||||
|
||||
import AbstractDialog from './AbstractDialog';
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* The value of the style property {@link _TAG_KEY} which identifies the
|
||||
* OK/submit button of <tt>Prompt</tt>.
|
||||
*/
|
||||
const _SUBMIT_TEXT_TAG_VALUE = '_SUBMIT_TEXT_TAG_VALUE';
|
||||
|
||||
/**
|
||||
* The name of the style property which identifies ancestors of <tt>Prompt</tt>
|
||||
* such as its OK/submit button for the purposes of workarounds implemented by
|
||||
* <tt>Dialog</tt>.
|
||||
*
|
||||
* XXX The value may trigger a react-native warning in the Debug configuration
|
||||
* but, unfortunately, I couldn't find a value that wouldn't.
|
||||
*/
|
||||
const _TAG_KEY = '_TAG_KEY';
|
||||
|
||||
/**
|
||||
* Implements <tt>AbstractDialog</tt> on react-native using <tt>Prompt</tt>.
|
||||
|
@ -37,7 +56,6 @@ class Dialog extends AbstractDialog {
|
|||
bodyKey,
|
||||
cancelDisabled,
|
||||
cancelTitleKey = 'dialog.Cancel',
|
||||
children,
|
||||
okDisabled,
|
||||
okTitleKey = 'dialog.Ok',
|
||||
t,
|
||||
|
@ -45,30 +63,82 @@ class Dialog extends AbstractDialog {
|
|||
titleString
|
||||
} = this.props;
|
||||
|
||||
/* eslint-disable react/jsx-wrap-multilines */
|
||||
const cancelButtonTextStyle
|
||||
= cancelDisabled ? styles.disabledButtonText : styles.buttonText;
|
||||
let submitButtonTextStyle
|
||||
= okDisabled ? styles.disabledButtonText : styles.buttonText;
|
||||
|
||||
let element
|
||||
= <Prompt
|
||||
cancelText = { cancelDisabled ? undefined : t(cancelTitleKey) }
|
||||
submitButtonTextStyle = {
|
||||
...submitButtonTextStyle,
|
||||
[_TAG_KEY]: _SUBMIT_TEXT_TAG_VALUE
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-extra-parens
|
||||
let element = (
|
||||
<Prompt
|
||||
cancelButtonTextStyle = { cancelButtonTextStyle }
|
||||
cancelText = { t(cancelTitleKey) }
|
||||
onCancel = { this._onCancel }
|
||||
onSubmit = { this._onSubmit }
|
||||
placeholder = { t(bodyKey) }
|
||||
submitText = { okDisabled ? undefined : t(okTitleKey) }
|
||||
submitButtonTextStyle = { submitButtonTextStyle }
|
||||
submitText = { t(okTitleKey) }
|
||||
title = { titleString || t(titleKey) }
|
||||
visible = { true } />;
|
||||
visible = { true } />
|
||||
);
|
||||
|
||||
/* eslint-enable react/jsx-wrap-multilines */
|
||||
// XXX The following implements workarounds with knowledge of
|
||||
// react-native-prompt/Prompt's implementation.
|
||||
|
||||
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);
|
||||
}
|
||||
// eslint-disable-next-line no-extra-parens, new-cap
|
||||
element = (new (element.type)(element.props)).render();
|
||||
|
||||
let { children } = this.props;
|
||||
|
||||
children = React.Children.count(children) ? children : undefined;
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
element = this._mapReactElement(element, element => {
|
||||
// * If this Dialog has children, they are to be rendered instead of
|
||||
// Prompt's TextInput.
|
||||
if (element.type === TextInput) {
|
||||
if (children) {
|
||||
element = children; // eslint-disable-line no-param-reassign
|
||||
children = undefined;
|
||||
}
|
||||
} else {
|
||||
let { style } = element.props;
|
||||
|
||||
if (style
|
||||
&& (style = StyleSheet.flatten(style))
|
||||
&& _TAG_KEY in style) {
|
||||
switch (style[_TAG_KEY]) {
|
||||
case _SUBMIT_TEXT_TAG_VALUE:
|
||||
if (this.state.submitting) {
|
||||
// * If this Dialog is submitting, render a
|
||||
// LoadingIndicator.
|
||||
return (
|
||||
<LoadingIndicator
|
||||
color = { submitButtonTextStyle.color }
|
||||
size = { 'small' } />
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
element
|
||||
= React.cloneElement(
|
||||
element,
|
||||
/* props */ {
|
||||
style: set(style, _TAG_KEY, undefined)
|
||||
},
|
||||
...React.Children.toArray(element.props.children));
|
||||
}
|
||||
}
|
||||
|
||||
return element;
|
||||
});
|
||||
|
||||
return element;
|
||||
}
|
||||
|
@ -108,36 +178,6 @@ class Dialog extends AbstractDialog {
|
|||
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect()(Dialog));
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { ColorPalette, createStyleSheet } from '../../styles';
|
||||
|
||||
/**
|
||||
* The React <tt>Component</tt> styles of the feature base/dialog.
|
||||
*/
|
||||
export default createStyleSheet({
|
||||
/**
|
||||
* The style of the <tt>Text</tt> in a <tt>Dialog</tt> button.
|
||||
*/
|
||||
buttonText: {
|
||||
color: ColorPalette.blue
|
||||
},
|
||||
|
||||
/**
|
||||
* The style of the <tt>Text</tt> in a <tt>Dialog</tt> button which is
|
||||
* disabled.
|
||||
*/
|
||||
disabledButtonText: {
|
||||
color: ColorPalette.darkGrey
|
||||
}
|
||||
});
|
Loading…
Reference in New Issue