Skip to content

Commit

Permalink
Merge d7193a8 into a8b7aa0
Browse files Browse the repository at this point in the history
  • Loading branch information
cema-sp committed Feb 27, 2017
2 parents a8b7aa0 + d7193a8 commit 75c557e
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 25 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ node_modules/
.idea/
_book
coverage
npm-debug.log
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,33 @@ You can use this to remove scrolling on the the body while the modal is open.
}
```

### Overriding overlay
You can override default overlay component providing `overlay` prop to `Modal`.
Note that overlay component should be a class (not function) due to internal refs usage.

```js
import Modal, { ModalOverlay } from 'react-modal';

class MyOverlay extends ModalOverlay {
render () {
return (
<div {...restProps}>
{children}
</div>
);
}
}

<Modal
{...modalProps}
overlay={<MyOverlay />}
>
{...modalContent}
<Modal>
```

See another example in [overlay example](examples/overlay/app.js).

## Examples
Inside an app:

Expand Down
31 changes: 31 additions & 0 deletions examples/overlay/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.ReactModal__Overlay {
-webkit-perspective: 600;
perspective: 600;
opacity: 0;
}

.ReactModal__Overlay--after-open {
opacity: 1;
transition: opacity 150ms ease-out;
}

.ReactModal__Content {
-webkit-transform: scale(0.5) rotateX(-30deg);
transform: scale(0.5) rotateX(-30deg);
}

.ReactModal__Content--after-open {
-webkit-transform: scale(1) rotateX(0deg);
transform: scale(1) rotateX(0deg);
transition: all 150ms ease-in;
}

.ReactModal__Overlay--before-close {
opacity: 0;
}

.ReactModal__Content--before-close {
-webkit-transform: scale(0.5) rotateX(30deg);
transform: scale(0.5) rotateX(30deg);
transition: all 150ms ease-in;
}
75 changes: 75 additions & 0 deletions examples/overlay/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Modal from '../../lib/index';

const appElement = document.getElementById('example');

Modal.setAppElement('#example');

const MyOverlay = React.createClass({
render: function() {
const { children, content, ...restProps } = this.props;

return (
<div {...restProps} >
{children}
<p
style={{
position: 'absolute',
bottom: '5px',
right: '20px',
margin: '3px'
}}
>
{content}
</p>
</div>
);
}
});

const App = React.createClass({
getInitialState: function() {
return { modalIsOpen: false };
},

openModal: function() {
this.setState({modalIsOpen: true});
},

closeModal: function() {
this.setState({modalIsOpen: false});
},

handleModalCloseRequest: function() {
// opportunity to validate something and keep the modal open even if it
// requested to be closed
this.setState({modalIsOpen: false});
},

handleInputChange: function() {
this.setState({foo: 'bar'});
},

render: function() {
return (
<div>
<button onClick={this.openModal}>Open Modal</button>
<Modal
ref="mymodal"
closeTimeoutMS={150}
isOpen={this.state.modalIsOpen}
onRequestClose={this.handleModalCloseRequest}
overlay={
<MyOverlay content="Some special content in overlay" />
}
>
<h1>Note overlay text</h1>
<button onClick={this.closeModal}>close</button>
</Modal>
</div>
);
}
});

ReactDOM.render(<App/>, appElement);
14 changes: 14 additions & 0 deletions examples/overlay/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!doctype html public "embarassment">
<title>Overlay Example</title>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<link rel="stylesheet" href="http://instructure-react.github.io/library/shared.css"/>
<link rel="stylesheet" href="app.css"/>
<body>
<header class="branding padbox">
<h1>react-modal</h1>
<h2>an accessible React modal dialog component</h2>
</header>
<div id="example" class="padbox"></div>
<a target="_top" href="https://github.com/reactjs/react-modal"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://github-camo.global.ssl.fastly.net/a6677b08c955af8400f44c6298f40e7d19cc5b2d/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f677261795f3664366436642e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png"></a>
<script src="../__build__/shared.js"></script>
<script src="../__build__/overlay.js"></script>
6 changes: 4 additions & 2 deletions lib/components/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export default class Modal extends Component {
shouldCloseOnOverlayClick: React.PropTypes.bool,
parentSelector: React.PropTypes.func,
role: React.PropTypes.string,
contentLabel: React.PropTypes.string.isRequired
contentLabel: React.PropTypes.string.isRequired,
overlay: React.PropTypes.element
};
/* eslint-enable react/no-unused-prop-types */

Expand All @@ -41,7 +42,8 @@ export default class Modal extends Component {
ariaHideApp: true,
closeTimeoutMS: 0,
shouldCloseOnOverlayClick: true,
parentSelector () { return document.body; }
parentSelector () { return document.body; },
overlay: null
};

static defaultStyles = {
Expand Down
27 changes: 27 additions & 0 deletions lib/components/ModalOverlay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { Component, PropTypes } from 'react';

export default class ModalOverlay extends Component {
static propTypes = {
className: PropTypes.string,
style: PropTypes.objectOf(
PropTypes.oneOfType([PropTypes.string, PropTypes.number])
),
onMouseDown: PropTypes.func,
onMouseUp: PropTypes.func,
children: PropTypes.element
};

static defaultProps = {
style: {},
};

render () {
const { children, ...restProps } = this.props;

return (
<div {...restProps}>
{children}
</div>
);
}
}
61 changes: 42 additions & 19 deletions lib/components/ModalPortal.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, { Component, PropTypes } from 'react';
import Assign from 'lodash.assign';
import ModalOverlay from './ModalOverlay';

import scopeTab from '../helpers/scopeTab';
import {
returnFocus,
Expand Down Expand Up @@ -42,14 +44,15 @@ export default class ModalPortal extends Component {
}),
role: PropTypes.string,
children: PropTypes.node,
contentLabel: PropTypes.string
contentLabel: PropTypes.string,
overlay: PropTypes.element
};

static defaultProps = {
style: {
overlay: {},
content: {}
}
},
};

constructor () {
Expand Down Expand Up @@ -204,30 +207,50 @@ export default class ModalPortal extends Component {
const contentStyles = (this.props.className) ? {} : this.props.defaultStyles.content;
const overlayStyles = (this.props.overlayClassName) ? {} : this.props.defaultStyles.overlay;

const overlayProps = {
ref: (c) => { this.overlay = c; },
className: this.buildClassName('overlay', this.props.overlayClassName),
style: Assign({}, overlayStyles, this.props.style.overlay || {}),
onClick: this.handleOverlayOnClick,
onMouseDown: this.handleOverlayMouseDown,
onMouseUp: this.handleOverlayMouseUp,
};

// Disabling this rule is okay, since we know what is going on here, that being said
// longterm we should probably do this better.
/* eslint-disable jsx-a11y/no-static-element-interactions */
return this.shouldBeClosed() ? <div /> : (
const overlayContent = (
<div
ref={(c) => { this.overlay = c; }}
className={this.buildClassName('overlay', this.props.overlayClassName)}
style={Assign({}, overlayStyles, this.props.style.overlay || {})}
onClick={this.handleOverlayOnClick}
ref={(c) => { this.content = c; }}
style={Assign({}, contentStyles, this.props.style.content || {})}
className={this.buildClassName('content', this.props.className)}
tabIndex={-1}
onKeyDown={this.handleKeyDown}
onClick={this.handleContentOnClick}
role={this.props.role}
aria-label={this.props.contentLabel}
>
<div
ref={(c) => { this.content = c; }}
style={Assign({}, contentStyles, this.props.style.content || {})}
className={this.buildClassName('content', this.props.className)}
tabIndex={-1}
onKeyDown={this.handleKeyDown}
onClick={this.handleContentOnClick}
role={this.props.role}
aria-label={this.props.contentLabel}
>
{this.props.children}
</div>
{this.props.children}
</div>
);
/* eslint-enable jsx-a11y/no-static-element-interactions */

if (this.shouldBeClosed()) {
return <div />;
}

if (this.props.overlay && typeof this.props.overlay === 'object') {
return React.cloneElement(
this.props.overlay,
overlayProps,
overlayContent
);
}

return (
<ModalOverlay {...overlayProps}>
{overlayContent}
</ModalOverlay>
);
}
}
2 changes: 2 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Modal from './components/Modal';
import ModalOverlay from './components/ModalOverlay';

export default Modal;
export { ModalOverlay };
29 changes: 25 additions & 4 deletions specs/Modal.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import sinon from 'sinon';
import expect from 'expect';
import ReactDOM from 'react-dom';
import Modal from '../lib/components/Modal';
import ModalOverlay from '../lib/components/ModalOverlay';
import * as ariaAppHider from '../lib/helpers/ariaAppHider';
import { renderModal, unmountModal, emptyDOM } from './helper';

Expand Down Expand Up @@ -182,7 +183,8 @@ describe('Modal', () => {

it('supports overlayClassName', () => {
const modal = renderModal({ isOpen: true, overlayClassName: 'myOverlayClass' });
expect(modal.portal.overlay.className.indexOf('myOverlayClass')).toNotEqual(-1);
expect(modal.portal.overlay.props.className.indexOf('myOverlayClass')).toNotEqual(-1);
unmountModal();
});

it('overrides the default styles when a custom classname is used', () => {
Expand All @@ -192,7 +194,7 @@ describe('Modal', () => {

it('overrides the default styles when a custom overlayClassName is used', () => {
const modal = renderModal({ isOpen: true, overlayClassName: 'myOverlayClass' });
expect(modal.portal.overlay.style.backgroundColor).toEqual('');
expect(modal.portal.overlay.props.style.backgroundColor).toNotExist();
});

it('supports adding style to the modal contents', () => {
Expand All @@ -207,12 +209,12 @@ describe('Modal', () => {

it('supports adding style on the modal overlay', () => {
const modal = renderModal({ isOpen: true, style: { overlay: { width: '75px' } } });
expect(modal.portal.overlay.style.width).toEqual('75px');
expect(modal.portal.overlay.props.style.width).toEqual('75px');
});

it('supports overriding style on the modal overlay', () => {
const modal = renderModal({ isOpen: true, style: { overlay: { position: 'static' } } });
expect(modal.portal.overlay.style.position).toEqual('static');
expect(modal.portal.overlay.props.style.position).toEqual('static');
});

it('supports overriding the default styles', () => {
Expand All @@ -225,6 +227,25 @@ describe('Modal', () => {
Modal.defaultStyles.content.position = previousStyle;
});

it('supports overriding overlay component', () => {
class MyOverlay extends ModalOverlay {
render () {
const { children, text, ...restProps } = this.props;

return (
<div {...restProps}>
{children}
<p>{text}</p>
</div>
);
}
}

const text = 'Extra text in overlay';
const modal = renderModal({ isOpen: true, overlay: <MyOverlay text={text} /> });
expect(modal.portal.overlay.props.text).toEqual(text);
});

it('adds class to body when open', () => {
renderModal({ isOpen: false });
expect(document.body.className.indexOf('ReactModal__Body--open') !== -1).toEqual(false);
Expand Down

0 comments on commit 75c557e

Please sign in to comment.