Skip to content

Commit

Permalink
Implemented modal element
Browse files Browse the repository at this point in the history
  • Loading branch information
mmsbrggr committed Aug 24, 2017
1 parent e8970ff commit a844fa8
Show file tree
Hide file tree
Showing 20 changed files with 532 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ type Props = {
isOpen: boolean,
/** When set to false the backdrop renders transparent. */
isVisible: boolean,
inPortal: boolean,
onClick?: () => void,
};

export default class Backdrop extends React.PureComponent<Props> {
static defaultProps = {
isOpen: true,
isVisible: true,
inPortal: true,
};

handleClick = () => {
Expand All @@ -31,13 +34,10 @@ export default class Backdrop extends React.PureComponent<Props> {
[backdropStyles.backdrop]: true,
[backdropStyles.isVisible]: isVisible,
});

return (
<Portal isOpened={isOpen}>
<div
onClick={this.handleClick}
className={backdropClasses} />
</Portal>
);
const backdrop = <div onClick={this.handleClick} className={backdropClasses} />;
if (!this.props.inPortal) {
return backdrop;
}
return <Portal isOpened={isOpen}>{backdrop}</Portal>;
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
@import '../../containers/Application/colors.scss';

$backdropBackground: $black;
$backdropBackground: rgba($dustyGrey, .9);

.backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: $backdropBackground;
opacity: 0;
background: transparent;

&.is-visible {
opacity: .4;
background: $backdropBackground;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ test('The component should render in body when open', () => {
expect(pretty(body.innerHTML)).toMatchSnapshot();
});

test('The component should not render in body when property is set', () => {
const view = mount(<Backdrop inPortal={false} />).render();
expect(view).toMatchSnapshot();
});

test('The component should not render in the body when closed', () => {
const body = document.body;
const view = mount(<Backdrop isOpen={false} />).render();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`The component should not render in body when property is set 1`] = `
<div
class="backdrop isVisible"
/>
`;

exports[`The component should render in body when open 1`] = `
"<div>
<div data-reactroot=\\"\\" class=\\"backdrop isVisible\\"></div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,11 @@ type Props = {

export default class Icon extends React.PureComponent<Props> {
render() {
const className = classNames(
this.props.className,
'fa',
'fa-' + this.props.name
);
const {className, name, ...otherProps} = this.props;
const classes = classNames(className, 'fa', 'fa-' + name);

return (
<span className={className} aria-hidden={true} />
<span className={classes} aria-hidden={true} {...otherProps} />
);
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// @flow
import React from 'react';
import type {Element, Node} from 'react';
import type {Element} from 'react';
import {action, observable} from 'mobx';
import {observer} from 'mobx-react';
import type {ModalProps} from './types';
import Modal from './Modal';

type Props = {
type Props = ModalProps & {
className?: string,
clickElement: Element<*>,
children: Node,
};

@observer
Expand All @@ -24,10 +24,14 @@ export default class ClickModal extends React.PureComponent<Props> {
};

render() {
const {className, clickElement, ...modalProps} = this.props;
return (
<div className={this.props.className}>
{React.cloneElement(this.props.clickElement, {onClick: this.handleElementClick})}
<Modal isOpen={this.modalOpen} onRequestClose={this.handleRequestClose}>
<div className={className}>
{React.cloneElement(clickElement, {onClick: this.handleElementClick})}
<Modal
isOpen={this.modalOpen}
onRequestClose={this.handleRequestClose}
{...modalProps} >
{this.props.children}
</Modal>
</div>
Expand Down
83 changes: 71 additions & 12 deletions src/Sulu/Bundle/AdminBundle/Resources/js/components/Modal/Modal.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,97 @@
// @flow
import React from 'react';
import type {Node} from 'react';
import {observable, action} from 'mobx';
import {observer} from 'mobx-react';
import Portal from 'react-portal';
import classnames from 'classnames';
import {afterElementsRendered} from '../../services/DOM';
import Backdrop from '../Backdrop';
import modalStyle from './modal.scss';
import ModalBox from './ModalBox';
import type {ModalProps} from './types';
import modalStyles from './modal.scss';

type Props = {
type Props = ModalProps & {
isOpen: boolean,
children: Node,
onRequestClose?: () => void,
onRequestClose: () => void,
};

const ESC_KEY = 27;

@observer
export default class Modal extends React.PureComponent<Props> {
static defaultProps = {
isOpen: false,
};

requestClose = () => {
if (this.props.onRequestClose) {
@observable isVisible: boolean = false;
@observable isOpenHasChanged: boolean = false;

@action componentWillMount() {
this.isOpenHasChanged = this.props.isOpen;
}

componentDidMount() {
window.addEventListener('keydown', this.handleKeyDown);
this.toggleModal();
}

componentWillUnmount() {
window.removeEventListener('keydown', this.handleKeyDown);
}

@action componentWillReceiveProps(newProps: Props) {
this.isOpenHasChanged = newProps.isOpen !== this.props.isOpen;
}

componentDidUpdate() {
this.toggleModal();
}

@action toggleModal() {
afterElementsRendered(action(() => {
if (this.isOpenHasChanged) {
this.isVisible = this.props.isOpen;
}
}));
}

handleKeyDown = (event: KeyboardEvent) => {
if (event.keyCode === ESC_KEY) {
this.props.onRequestClose();
}
};

handleBackdropClick = this.requestClose;
@action handleTransitionEnd = () => {
afterElementsRendered(action(() => {
this.isOpenHasChanged = false;
}));
};

render() {
const containerClasses = classnames({
[modalStyles.container]: true,
[modalStyles.isDown]: this.isVisible,
});

return (
<div>
<Portal isOpened={this.props.isOpen}>
<div className={modalStyle.container}>
<div className={modalStyle.modal}>{this.props.children}</div>
<Portal isOpened={this.props.isOpen || this.isOpenHasChanged}>
<div
className={containerClasses}
onTransitionEnd={this.handleTransitionEnd} >
<div className={modalStyles.box}>
<ModalBox
title={this.props.title}
actions={this.props.actions}
onRequestClose={this.props.onRequestClose}
onConfirm={this.props.onConfirm}
confirmText={this.props.confirmText} >
{this.props.children}
</ModalBox>
</div>
<Backdrop inPortal={false} onClick={this.props.onRequestClose} />
</div>
</Portal>
<Backdrop isOpen={this.props.isOpen} onClick={this.handleBackdropClick} />
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// @flow
import React from 'react';
import Icon from '../Icon';
import type {ModalProps, Action} from './types';
import modalBoxStyles from './modalBox.scss';

type Props = ModalProps & {
onRequestClose: () => void,
}

const CLOSE_ICON = 'times';

export default class ModalBox extends React.PureComponent<Props> {
static defaultProps = {
actions: [],
};

render() {
return (
<div className={modalBoxStyles.box}>
<div className={modalBoxStyles.header}>
{this.props.title}
<Icon
name={CLOSE_ICON}
className={modalBoxStyles.icon}
onClick={this.props.onRequestClose} />
</div>
<div className={modalBoxStyles.content}>
{this.props.children}
</div>
<div className={modalBoxStyles.footer}>
{ModalBox.renderActions(this.props.actions)}
<button
className={modalBoxStyles.confirmButton}
onClick={this.props.onConfirm}>{this.props.confirmText}</button>
</div>
</div>
);
}

static renderActions(actions: Array<Action>) {
if (actions.length > 0) {
return (
<div className={modalBoxStyles.actions}>
{actions.map(ModalBox.renderAction)}
</div>
);
}
}

static renderAction(action: Action, index: number) {
return (
<button
key={index}
className={modalBoxStyles.action}
onClick={action.handleAction}>{action.title}</button>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,22 @@ It renders depending on the passed property and request being closed through a c

```
initialState = {open: false};
const actions = [
{title: 'Destroy world', handleAction: () => {/* destroy world */}},
{title: 'Save world', handleAction: () => {/* save world */}},
];
<div>
<button onClick={() => setState({open: true})}>Open modal</button>
<Modal onRequestClose={() => setState({open: false})} isOpen={state.open}>
<div style={{width: '500px', height: '500px'}}>My modal content</div>
<Modal
title="Njan Njan Njan"
onRequestClose={() => setState({open: false})}
actions={actions}
confirmText="Apply"
isOpen={state.open} >
<div style={{width: '900px', height: '500px', display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
<img src="http://www.nyan.cat/cats/original.gif" />
</div>
</Modal>
</div>
```
Expand All @@ -19,9 +30,18 @@ of the modal internally.

```
const ClickModal = require('./ClickModal').default;
const actions = [
{title: 'Save Gotham', handleAction: () => {/* save gotham */}},
];
const button = (<button>Open modal</button>);
<ClickModal clickElement={button}>
<div style={{width: '500px', height: '1000px'}}>My modal content</div>
<ClickModal
clickElement={button}
title="Nana Nana Nana"
actions={actions}
confirmText="Ok" >
<div style={{width: '900px', height: '500px', display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
<img src="https://media.giphy.com/media/NmhVw98IHkQtq/source.gif" />
</div>
</ClickModal>
```

0 comments on commit a844fa8

Please sign in to comment.