Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add overlay component #3466

Merged
merged 17 commits into from Aug 29, 2017
Merged

Add overlay component #3466

merged 17 commits into from Aug 29, 2017

Conversation

mmsbrggr
Copy link
Contributor

@mmsbrggr mmsbrggr commented Aug 3, 2017

Q A
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Dependent PR #3468
License MIT

What's in this PR?

This PR contains a modal components which displays content above everything else.

Example Usage

<button onClick={() => setState({open: true})}>Open overlay</button>
<Overlay onClose={() => setState({open: false})} isOpen={state.open}>
    <p>My overlay content</p>
</Overlay >

@mmsbrggr mmsbrggr changed the title Feature/modal Modal component Aug 3, 2017
@@ -27,6 +28,7 @@
"enzyme-to-json": "^1.5.1",
"identity-obj-proxy": "^3.0.0",
"jest": "^20.0.4",
"pretty": "^2.0.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pretty is just a dev-dependency which beautifies a string of html. This is used for testing the html of the overlay which is ported to the body.

import {observer} from 'mobx-react';

@observer
export default class ClickModal extends React.PureComponent {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would wait with the merge until we know how we name this.

render() {
return (
<div className={this.props.className}>
{React.cloneElement(this.props.clickElement, {onClick: this.handleElementClick})}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to clone something here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To pass an extra property

this.modalOpen = true;
};

@action handleModalRequestClose = () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just handleRequestClose?

<div className={modalStyle.modal}>
{this.props.children}
</div>
<div onClick={this.handleBackdropClick} className={modalStyle.backdrop} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't the idea to move that into an own Backdrop component, so that it can also be used for the dropdowns?

$backgroundColor: $white;
$modalShadow: 0 0 25px 0 rgba($black, 0.25);

.container {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.modal-container

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the major advantage of css-modules that we can use the same classes in multiple files without caution? modal is mentioned in the name of the file.

</ClickModal>
).render();
expect(view).toMatchSnapshot();
expect(body.innerHTML).toBe('');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't that contain at least the button with the Open modal text? Or am I missing something.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The view is rendered in a virtual document as far as I know. Only the stuff in the portal is put in the body mentioned here.


expect(body.innerHTML).toBe('');
view.find('button').simulate('click');
expect(pretty(body.innerHTML)).toMatchSnapshot();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to the question above: Don't you make a far to big snapshot this way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned, the only thing in the body is the stuff of the portal and that's exactly the things a snapshot should be taken of.

const body = document.body;
const view = mount(<Modal isOpen={true}><p>Modal content</p></Modal>).render();
expect(view.html()).toBe(null);
expect(pretty(body.innerHTML)).toMatchSnapshot();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned, the only thing in the body is the stuff of the portal and that's exactly the things a snapshot should be taken of.

const backdrop = view.find('.backdrop');
expect(backdrop.length).toBe(1);

expect(requestCloseSpy).toHaveBeenCalledTimes(0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have used toBeCalledTimes until now, which is an alias for that. Would be nice to be consistent here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this function situated? If I replace toHaveBeenCalledTimes with toBeCalledTimes, I get that toBeCalledTimes is not a function.

@mmsbrggr mmsbrggr force-pushed the feature/modal branch 2 times, most recently from 41f9dcf to 47ee3f7 Compare August 3, 2017 13:45
@mmsbrggr mmsbrggr force-pushed the feature/modal branch 7 times, most recently from 5894bb7 to a844fa8 Compare August 24, 2017 08:38
@@ -8,12 +8,15 @@ type Props = {
isOpen: boolean,
/** When set to false the backdrop renders transparent. */
isVisible: boolean,
inPortal: boolean,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really get what this prop is doing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

local

@@ -1,17 +1,16 @@
@import '../../containers/Application/colors.scss';

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this color decided by the designers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

'fa',
'fa-' + this.props.name
);
const {className, name, ...otherProps} = this.props;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you need the otherProps for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create explicit onclick

};

@observer
export default class ClickModal extends React.PureComponent<Props> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still not a big fan of this component... Don't you think it would make more sense to implement this as a high order component?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't see the advantages of using a HOC here.

onRequestClose: () => void,
};

const ESC_KEY = 27;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that should go somewhere else, or we should include a library handling this for us. I don't want to define the ESC keycode in every component 😉

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remember correctly @chirimoya found a react library handling that using decorators.

Copy link
Contributor Author

@mmsbrggr mmsbrggr Aug 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

react-keydown only listens to keydowns within the component elements. Because we use portals in the modal it doesn't really work. I created an issue in react-keydown: glortho/react-keydown#68

<Portal isOpened={this.props.isOpen || this.isOpenHasChanged}>
<div
className={containerClasses}
onTransitionEnd={this.handleTransitionEnd} >
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No space before >

<div
className={containerClasses}
onTransitionEnd={this.handleTransitionEnd} >
<div className={modalStyles.box}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not moving that class to the ModalBox?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it handles the transition. The modal box is just the element which doesn't care where its placed and how it appears.

actions={this.props.actions}
onRequestClose={this.props.onRequestClose}
onConfirm={this.props.onConfirm}
confirmText={this.props.confirmText} >
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use destructuring for all these variables.

className={modalBoxStyles.confirmButton}
onClick={this.props.onConfirm}>{this.props.confirmText}</button>
</div>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should make use of the section, header and footer tags here, shouldn't we?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And maybe also put them in separate components? Having a render* method is usually a sign that it might be better to put into a separate component.


export type Action = {
title: string,
handleAction: () => void,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not onClick?

@danrot
Copy link
Contributor

danrot commented Aug 24, 2017

I think the name of the component is a bit controversial... We'll have two different kind of overlays, and both of them will be modals. But we are already occupying this term for one of these overlays...

@chirimoya
Copy link
Member

@danrot are we not able to manage the different kind of overlays in one component?

@mmsbrggr
Copy link
Contributor Author

@danrot The other kind of modal you talk about are "Dialogs" for me.

@mmsbrggr mmsbrggr force-pushed the feature/modal branch 2 times, most recently from 57ab1c7 to 475b110 Compare August 24, 2017 15:48
@danrot
Copy link
Contributor

danrot commented Aug 24, 2017

@chirimoya TBH I don't know yet... There are no designs 🙈 It's again the question if we build it using some configuration, or if the differences are so big that they justify a separate component.

@mmsbrggr Both of them are modals per definitions. A modal is something that requires immediate attention and blocks the rest of the application, which is true for both components (if there will be two)

@@ -7,7 +7,7 @@ intialState = {open: false};

<div>
<button onClick={() => setState({open: true})}>Open Backdrop</button>
<Backdrop isOpen={state.open} onClick={() => setState({open: false})} />
<Backdrop isOpen={!!state.open} onClick={() => setState({open: false})} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the double bang?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because initially the state.open is undefined in styleguidist. As the default value for isOpen is true the Backdrop opens automatically. That's why the backdrops were open on page load all the time.

@@ -18,6 +18,6 @@ intialState = {open: false};

<div>
<button onClick={() => setState({open: true})}>Open Backdrop</button>
<Backdrop isVisible={false} isOpen={state.open} onClick={() => setState({open: false})} />
<Backdrop isVisible={false} isOpen={!!state.open} onClick={() => setState({open: false})} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the double bang? We actually said we don't do them anymore.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The make sense sometimes. Especially at the points where you really expect a boolean and not just a truthy type. At this point undefined and false make a difference.

@@ -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', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when local property

<Modal
isOpen={this.modalOpen}
onRequestClose={this.handleRequestClose}
{...modalProps} >
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No space before > (there has to be an eslint rule for that...)

className={modalBoxStyles.icon}
onClick={this.props.onRequestClose} />
</header>
<section>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have left this one a div and the outer div would be the section for me, since a section is allowed to have headers and footers (if I am not mistaken).

return (
<div className={modalBoxStyles.box}>
<header>
{this.props.title}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use destructuring

@mmsbrggr mmsbrggr force-pushed the feature/modal branch 4 times, most recently from 2fc28e0 to bbc5a24 Compare August 25, 2017 06:20

export default class Actions extends React.PureComponent<Props> {
render() {
if (this.props.actions.length > 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use destructuring? At first I thought it doesn't make any sense, but you are using it twice, so it does for me.

actions: [],
};

@keydown('esc') requestClose() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename to handleRequestClose?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The component doesn't handle a request close call, but really requests to be closed.

<section className={modalBoxStyles.box}>
<header>
{title}
<Icon name={CLOSE_ICON} className={modalBoxStyles.icon} onClick={onRequestClose} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use requestClose resp. handleRequestClose directly? What if we have to do more than just calling one callback?

title="Njan Njan Njan"
onRequestClose={() => setState({open: false})}
actions={actions}
confirmText="Apply"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also add a onConfirm callback?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it to both examples

clickElement={button}
title="Nana Nana Nana"
actions={actions}
confirmText="Ok">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also add a onConfirm callback?

@mmsbrggr
Copy link
Contributor Author

mmsbrggr commented Aug 25, 2017

@danrot I couldn't find a possibility to forbid spaces before ">" in the starting tag via es-lint. I created an issue jsx-eslint/eslint-plugin-react#1396

export default class Actions extends React.PureComponent<Props> {
render() {
const {actions} = this.props;
if (actions.length > 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

early return

};

@observer
export default class ClickModal extends React.PureComponent<Props> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to ClickOverlay?


@observer
export default class ClickModal extends React.PureComponent<Props> {
@observable modalOpen = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to isOpen?


const CLOSE_ICON = 'times';

export default class ModalBox extends React.PureComponent<Props> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OverlayBox? Box is strange somehow, because it doesn't transport a real message for me... Maybe there is a better alternative.

import Icon from '../Icon';
import type {ModalProps} from './types';
import Actions from './Actions';
import modalBoxStyles from './modalBox.scss';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to overlayBoxStyles and overlayBox.scss?

import Modal from './Modal';

export {ClickModal};
export default Modal;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How should these imports be handled?

  1. Move the ClickModal to its own component folder?
  2. Have two named exports?
  3. Decide on which one is the more "default" one, and the other is the named export?
  4. Is there an actual need for both kind of modals? Could imagine that we always only open the modals when an element is clicked.

import Backdrop from '../Backdrop';
import ModalBox from './ModalBox';
import type {ModalProps} from './types';
import modalStyles from './modal.scss';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to overlayStyles?

@danrot danrot changed the title Modal component Add overlay component Aug 28, 2017
@chirimoya chirimoya merged commit 1b2f955 into sulu:develop Aug 29, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants