Skip to content

Commit

Permalink
feat(modal-pages): add delay before calling onClose to let animations…
Browse files Browse the repository at this point in the history
… finish (#766)

* feat(modal-pages): add delay before calling onClose to let animations finish

* Update packages/application-components/src/components/modal-pages/form-modal-page/README.md

Co-Authored-By: Malcolm Laing <malcolmclaing@gmail.com>

* refactor(components/modal-pages): to not use hooks yet

* refactor(components/modal-pages): simplify controlled state

* fix(website): small bug in modal-page examples
  • Loading branch information
jonnybel committed Jun 20, 2019
1 parent 731dff4 commit ae0a843
Show file tree
Hide file tree
Showing 11 changed files with 152 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Form Modal pages are controlled components used to render a page with a form or
| `dataAttributesPrimaryButton` | `object` | - | - | - | Use this prop to pass `data-` attributes to the primary button |
| `getParentSelector` | `function` | - | - | - | The function should return an HTML element that will be used as the parent container to hold the modal DOM tree. If no function is provided, it's expected that an HTML element with the `id="parent-container"` is present in the DOM. In `NODE_ENV=test` environment, the default HTML element is `body`. |
| `customControls` | `node` | - | - | Pass a React.node to be used in place of the pre-determined controls. This can be useful if you need actions other than Cancel & Confirm, or other types of buttons, while keeping the same modal header layout |
| `shouldDelayOnClose` | `bool` | - | `true` | Sets whether the ModalPage should delay calling its onClose function to allow the closing animation time to finish. This can be turned off if the developer is controlling the ModalPage only through the `isOpen` prop, and not abruptly mounting/unmounting it or one of its parent elements. |

> NOTE: If `customControls` are passed, the `on*Click` props will no longer be required, and among with the `label*Button`, `isPrimaryButtonDisabled` and `dataAttributes*` props will no longer have any effect.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const FormModalPageExample = () => (
{ value: 'custom', label: 'Custom (Icon Buttons example)' },
{ value: 'none', label: 'None' },
],
initialValue: 'none',
initialValue: 'default',
},
]}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import buttonMessages from '../../../utils/button-messages';
import ModalPage from '../internals/modal-page';
import ModalPageTopBar from '../internals/modal-page-top-bar';
import ModalPageHeader from '../internals/modal-page-header';
import { ContentWrapper } from '../internals/modal-page.styles';

Expand All @@ -14,13 +13,11 @@ const FormModalPage = props => (
zIndex={props.zIndex}
onClose={props.onClose}
baseZIndex={props.baseZIndex}
currentPathLabel={props.topBarCurrentPathLabel || props.title}
previousPathLabel={props.topBarPreviousPathLabel}
getParentSelector={props.getParentSelector}
shouldDelayOnClose={props.shouldDelayOnClose}
>
<ModalPageTopBar
onClose={props.onClose}
currentPathLabel={props.topBarCurrentPathLabel || props.title}
previousPathLabel={props.topBarPreviousPathLabel}
/>
<ModalPageHeader
title={props.title}
subtitle={props.subtitle}
Expand All @@ -46,6 +43,7 @@ FormModalPage.propTypes = {
children: PropTypes.node.isRequired,
baseZIndex: PropTypes.number,
getParentSelector: PropTypes.string,
shouldDelayOnClose: PropTypes.bool,
// TopBar props
topBarCurrentPathLabel: PropTypes.string,
topBarPreviousPathLabel: PropTypes.string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ Info Modal pages are controlled components used to render a page using a modal c
| `topBarPreviousPathLabel` | `string` | | `"Go Back"` (translated) | The label to appear as the previous path of the top bar of the modal |
| `children` | `node` || - | Content rendered within the page. If the content is long in height (depending on the screen size) a scrollbar will appear. |
| `getParentSelector` | `function` | - | - | The function should return an HTML element that will be used as the parent container to hold the modal DOM tree. If no function is provided, it's expected that an HTML element with the `id="parent-container"` is present in the DOM. In `NODE_ENV=test` environment, the default HTML element is `body`. |
| `shouldDelayOnClose` | `bool` | - | `true` | Sets wether the ModalPage should delay calling its onClose function to allow the closing animation to finish. This can be turned off if the developer is controlling the ModalPage only through the `isOpen` prop, and not abruptly mounting/unmounting it or one of its parent elements. |
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import ModalPage from '../internals/modal-page';
import ModalPageTopBar from '../internals/modal-page-top-bar';
import ModalPageHeader from '../internals/modal-page-header';
import { ContentWrapper } from '../internals/modal-page.styles';

Expand All @@ -13,13 +12,11 @@ const InfoModalPage = props => (
zIndex={props.zIndex}
onClose={props.onClose}
baseZIndex={props.baseZIndex}
currentPathLabel={props.topBarCurrentPathLabel || props.title}
previousPathLabel={props.topBarPreviousPathLabel}
shouldDelayOnClose={props.shouldDelayOnClose}
getParentSelector={props.getParentSelector}
>
<ModalPageTopBar
onClose={props.onClose}
currentPathLabel={props.topBarCurrentPathLabel || props.title}
previousPathLabel={props.topBarPreviousPathLabel}
/>
<ModalPageHeader
title={props.title}
subtitle={props.subtitle}
Expand All @@ -38,6 +35,7 @@ InfoModalPage.propTypes = {
children: PropTypes.node.isRequired,
baseZIndex: PropTypes.number,
getParentSelector: PropTypes.string,
shouldDelayOnClose: PropTypes.bool,
// TopBar Props
topBarCurrentPathLabel: PropTypes.string,
topBarPreviousPathLabel: PropTypes.string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,5 @@ ModalPageTopBar.defaultProps = {
color: 'surface',
previousPathLabel: messages.back,
};
ModalPageTopBar.Intl = { back: messages.back };

export default injectIntl(ModalPageTopBar);
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import {
getBeforeCloseOverlayAnimation,
getBeforeCloseContainerAnimation,
} from './modal-page.styles';
import ModalPageTopBar from './modal-page-top-bar';

const TRANSITION_DURATION = 200;

// When running tests, we don't render the AppShell. Instead we mock the
// application context to make the data available to the application under
Expand All @@ -30,56 +33,120 @@ const getDefaultParentSelector = () =>
? document.body
: document.querySelector(`#${PORTALS_CONTAINER_ID}`);

const ModalPage = props => (
<ClassNames>
{({ css: makeClassName }) => (
<Modal
isOpen={props.isOpen}
onRequestClose={props.onClose}
shouldCloseOnOverlayClick={Boolean(props.onClose)}
shouldCloseOnEsc={Boolean(props.onClose)}
overlayClassName={{
base: makeClassName(getOverlayStyles(props)),
afterOpen: makeClassName(getAfterOpenOverlayAnimation()),
beforeClose: makeClassName(getBeforeCloseOverlayAnimation()),
}}
className={{
base: makeClassName(getContainerStyles(props)),
afterOpen: makeClassName(getAfterOpenContainerAnimation()),
beforeClose: makeClassName(getBeforeCloseContainerAnimation()),
}}
contentLabel={props.title}
parentSelector={props.getParentSelector}
ariaHideApp={false}
// Adjust this value if the (beforeClose) animation duration is changed
closeTimeoutMS={200}
style={{
// stylelint-disable-next-line selector-type-no-unknown
overlay: {
zIndex: props.zIndex,
},
}}
>
{props.children}
</Modal>
)}
</ClassNames>
);
ModalPage.displayName = 'ModalPage';
ModalPage.propTypes = {
level: PropTypes.number,
title: PropTypes.string.isRequired,
zIndex: PropTypes.number,
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func,
children: PropTypes.node.isRequired,
baseZIndex: PropTypes.number,
getParentSelector: PropTypes.func,
};
ModalPage.defaultProps = {
level: 1,
baseZIndex: 1000,
getParentSelector: getDefaultParentSelector,
};
class ModalPage extends React.Component {
static displayName = 'ModalPage';
static propTypes = {
level: PropTypes.number,
title: PropTypes.string.isRequired,
zIndex: PropTypes.number,
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func,
children: PropTypes.node.isRequired,
baseZIndex: PropTypes.number,
getParentSelector: PropTypes.func,
shouldDelayOnClose: PropTypes.bool,
// TopBar props:
topBarColor: PropTypes.oneOf(['surface', 'neutral']),
currentPathLabel: PropTypes.string,
previousPathLabel: PropTypes.oneOfType([
PropTypes.string,
// default intl message
PropTypes.shape({
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string.isRequired,
}),
]).isRequired,
// injected
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}),
};
static defaultProps = {
level: 1,
baseZIndex: 1000,
getParentSelector: getDefaultParentSelector,
shouldDelayOnClose: true,
};

state = { forceIsOpen: null };

componentDidUpdate(prevProps) {
if (prevProps.isOpen === false && this.props.isOpen === true) {
this.setState({ forceIsOpen: null });
}
}

componentWillUnmount() {
if (this.closingTimer) {
clearTimeout(this.closingTimer);
}
}

handleClose = () => {
if (this.props.shouldDelayOnClose) {
// In this case we want the closing animation to be shown
// and therefore we need wait for it to be completed
// before calling `onClose`.
this.setState({ forceIsOpen: false });
this.closingTimer = setTimeout(() => {
this.props.onClose();
}, TRANSITION_DURATION);
return;
}
this.props.onClose();
};

render() {
return (
<ClassNames>
{({ css: makeClassName }) => (
<Modal
isOpen={
this.state.forceIsOpen !== null
? this.state.forceIsOpen
: this.props.isOpen
}
onRequestClose={this.handleClose}
shouldCloseOnOverlayClick={Boolean(this.props.onClose)}
shouldCloseOnEsc={Boolean(this.props.onClose)}
overlayClassName={{
base: makeClassName(
getOverlayStyles(this.props, TRANSITION_DURATION)
),
afterOpen: makeClassName(getAfterOpenOverlayAnimation()),
beforeClose: makeClassName(getBeforeCloseOverlayAnimation()),
}}
className={{
base: makeClassName(
getContainerStyles(this.props, TRANSITION_DURATION)
),
afterOpen: makeClassName(getAfterOpenContainerAnimation()),
beforeClose: makeClassName(getBeforeCloseContainerAnimation()),
}}
contentLabel={this.props.title}
parentSelector={this.props.getParentSelector}
ariaHideApp={false}
// Adjust this value if the (beforeClose) animation duration is changed
closeTimeoutMS={TRANSITION_DURATION}
style={{
// stylelint-disable-next-line selector-type-no-unknown
overlay: {
zIndex: this.props.zIndex,
},
}}
>
<ModalPageTopBar
color={this.props.topBarColor}
onClose={this.handleClose}
currentPathLabel={this.props.currentPathLabel}
previousPathLabel={this.props.previousPathLabel}
/>
{this.props.children}
</Modal>
)}
</ClassNames>
);
}
}

export default ModalPage;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { css } from '@emotion/core';
import styled from '@emotion/styled';
import { customProperties } from '@commercetools-frontend/ui-kit';

export const getContainerStyles = props => css`
export const getContainerStyles = (props, transitionDuration) => css`
position: absolute;
top: 0;
right: 0;
Expand All @@ -14,7 +14,21 @@ export const getContainerStyles = props => css`
box-shadow: ${customProperties.shadow4}, ${customProperties.shadow6};
outline: 0;
transform: translate3d(30px, 0, 0);
transition: transform 0.2s ease;
transition: transform ${transitionDuration}ms ease;
`;

export const getOverlayStyles = (props, transitionDuration) => css`
position: absolute;
z-index: ${typeof props.zIndex === 'number'
? props.zIndex
: props.baseZIndex + props.level};
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(32, 62, 72, 0.5);
opacity: 0;
transition: opacity ${transitionDuration}ms ease;
`;

export const getAfterOpenContainerAnimation = () => css`
Expand All @@ -33,20 +47,6 @@ export const getBeforeCloseOverlayAnimation = () => css`
opacity: 0 !important;
`;

export const getOverlayStyles = props => css`
position: absolute;
z-index: ${typeof props.zIndex === 'number'
? props.zIndex
: props.baseZIndex + props.level};
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(32, 62, 72, 0.5);
opacity: 0;
transition: opacity 0.2s ease;
`;

export const ContentWrapper = styled.div`
flex: 1;
padding: ${customProperties.spacingS} ${customProperties.spacingM};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Tabular Modal pages are controlled components used to render a page with custom
| `isPrimaryButtonDisabled` | `boolean` | - | - | false | Indicates whether primary button is disabled or not |
| `dataAttributesSecondaryButton` | `object` | - | - | - | Use this prop to pass `data-` attributes to the secondary button |
| `dataAttributesPrimaryButton` | `object` | - | - | - | Use this prop to pass `data-` attributes to the primary button |
| `shouldDelayOnClose` | `bool` | - | `true` | Sets wether the ModalPage should delay calling its onClose function to allow the closing animation to finish. This can be turned off if the developer is controlling the ModalPage only through the `isOpen` prop, and not abruptly mounting/unmounting it or one of its parent elements. |

## Static properties

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ const TabularModalPageExample = () => (
</div>
}
customTitleRow={
values.useCustomTitleRow && exampleCustomTitleRow
values.useCustomTitleRow === 'custom' && exampleCustomTitleRow
}
customControls={customControls(values.useCustomControls)}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { css } from '@emotion/core';
import { customProperties, Spacings } from '@commercetools-frontend/ui-kit';
import buttonMessages from '../../../utils/button-messages';
import ModalPage from '../internals/modal-page';
import ModalPageTopBar from '../internals/modal-page-top-bar';
import ModalPageHeaderTitle from '../internals/modal-page-header-title';
import ModalPageHeaderDefaultControls from '../internals/modal-page-header-default-controls';
import { ContentWrapper } from '../internals/modal-page.styles';
Expand All @@ -18,14 +17,12 @@ const TabularModalPage = props => (
zIndex={props.zIndex}
onClose={props.onClose}
baseZIndex={props.baseZIndex}
topBarColor="neutral"
currentPathLabel={props.topBarCurrentPathLabel || props.title}
previousPathLabel={props.topBarPreviousPathLabel}
getParentSelector={props.getParentSelector}
shouldDelayOnClose={props.shouldDelayOnClose}
>
<ModalPageTopBar
color="neutral"
onClose={props.onClose}
currentPathLabel={props.topBarCurrentPathLabel || props.title}
previousPathLabel={props.topBarPreviousPathLabel}
/>
<div
css={css`
background-color: ${customProperties.colorNeutral95};
Expand Down Expand Up @@ -80,6 +77,7 @@ TabularModalPage.propTypes = {
children: PropTypes.node.isRequired,
baseZIndex: PropTypes.number,
getParentSelector: PropTypes.string,
shouldDelayOnClose: PropTypes.bool,
// For topbar
topBarCurrentPathLabel: PropTypes.string,
topBarPreviousPathLabel: PropTypes.string,
Expand Down

0 comments on commit ae0a843

Please sign in to comment.