Skip to content

Commit

Permalink
Merge pull request #39 from PocketDerm/add_modals
Browse files Browse the repository at this point in the history
added immersive and regular modals
  • Loading branch information
ZeMunchkin committed Jan 25, 2019
2 parents c7d1ee5 + b49e857 commit 40fd5be
Show file tree
Hide file tree
Showing 22 changed files with 925 additions and 1,403 deletions.
3 changes: 3 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"extends": "curology",
"globals": {
"document": false,
},
"overrides": [
{
"files": ["test.js"],
Expand Down
89 changes: 89 additions & 0 deletions docs/immersiveModal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# ImmersiveModal
### Usage

```jsx
import React from 'react';

import { ImmersiveModal, Button } from 'radiance-ui';

const HeaderImage = () => (
<div>
This is a placeholder for an optional header image.
</div>
);

class DefaultImmersiveModal extends React.Component {
state = {
defaultIsOpen: false,
headerIsOpen: false,
};

onOpenDefaultModal = () => this.setState({ defaultIsOpen: true });

onOpenHeaderModal = () => this.setState({ headerIsOpen: true });

onClose = () => this.setState({
defaultIsOpen: false,
headerIsOpen: false,
});

render() {
const { defaultIsOpen, headerIsOpen } = this.state;

return (
<div>
<Button onClick={this.onOpenDefaultModal}>Open ImmersiveModal</Button>
{defaultIsOpen && (
<ImmersiveModal onClose={this.onClose}>
<ImmersiveModal.Title>This is styled with ImmersiveModal.Title</ImmersiveModal.Title>
<ImmersiveModal.Body>This is styled with ImmersiveModal.Body</ImmersiveModal.Body>
<ImmersiveModal.Footer>
This is styled with ImmersiveModal.Footer. It gives us a padding to separate
from the body.
<Button.Container>
<Button onClick={this.onClose}>Close ImmersiveModal</Button>
</Button.Container>
</ImmersiveModal.Footer>
</ImmersiveModal>
)}

<Button onClick={this.onOpenHeaderModal}>Open ImmersiveModal with Header</Button>
{headerIsOpen && (
<ImmersiveModal
onClose={this.onClose}
header={<HeaderImage />}
>
<ImmersiveModal.Title>This is styled with ImmersiveModal.Title</ImmersiveModal.Title>
<ImmersiveModal.Body>This is styled with ImmersiveModal.Body.</ImmersiveModal.Body>
<ImmersiveModal.Footer>
This is styled with ImmersiveModal.Footer. It gives us a padding to separate
from the body.
<Button.Container>
<Button onClick={this.onClose}>Close ImmersiveModal</Button>
</Button.Container>
</ImmersiveModal.Footer>
</ImmersiveModal>
)}
</div>
);
}
}
```

<!-- STORY -->

### Proptypes
| prop | propType | required | default | description |
|----------|--------------------|----------|---------|------------------------------------------------------------------------------------------------------------------------------|
| onClose | func | no | () => {} | Function that is executed when either the close icon is clicked or any part of the page outside the modal is clicked |
| canBeClosed | bool | no | true | If false, the close icon does not render and the onClose function does not execute |
| children | node | yes | - | Node that will render when the modal is visible |
| header | node | no | - | Node that will render at the top of the modal and without padding, most commonly used for images |

### Notes

Available subcomponents through dot notation:

`<ImmersiveModal.Title>`
`<ImmersiveModal.Body>`
`<ImmersiveModal.Footer>`
62 changes: 62 additions & 0 deletions docs/modal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Modal
### Usage

```jsx
import React from 'react';

import { Modal, Button } from 'radiance-ui';

class DefaultModal extends React.Component {
state = {
isOpen: false,
};

onOpenModal = () => this.setState({ isOpen: true });

onClose = () => this.setState({ isOpen: false });

render() {
const { isOpen } = this.state;

return (
<div>
<Button onClick={this.onOpenModal}>Open Modal</Button>
<Modal isOpen={isOpen} onClose={this.onClose}>
<Modal.ContentContainer>
<Modal.Title>This is styled with Modal.Title</Modal.Title>
<Modal.Body>This is styled with Modal.Body.</Modal.Body>
<Modal.Footer>
This is styled with Modal.Footer. It gives us a padding to separate
from the body.
<Button.Container>
<Button onClick={this.onClose}>Close Modal</Button>
</Button.Container>
</Modal.Footer>
</Modal.ContentContainer>
</Modal>
<br />
</div>
);
}
}
```

<!-- STORY -->

### Proptypes
| prop | propType | required | default | description |
|----------|--------------------|----------|---------|------------------------------------------------------------------------------------------------------------------------------|
| onClose | func | no | () => {} | Function that is executed when the close icon is clicked |
| isOpen | bool | no | false | Determines if the modal is open (visible) |
| canBeClosed | bool | no | true | If false, the close icon does not render |
| className | string | no | '' | Class name that will be passed to the modal |
| children | node | no | '' | Node that will render when the modal is open |

### Notes

Available subcomponents through dot notation:

`<Modal.ContentContainer>`
`<Modal.Title>`
`<Modal.Body>`
`<Modal.Footer>`
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
"prop-types": "^15.6.2",
"react": "^16.6.0",
"react-emotion": "^9.2.12",
"react-modal": "^3.8.1",
"tinycolor2": "^1.4.1"
},
"husky": {
Expand Down
7 changes: 7 additions & 0 deletions src/constants/keycodes/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import throwOnUndefinedProperty from '../../utils/throwOnUndefinedProperty';

const keycodes = throwOnUndefinedProperty({
escape: 27,
});

export default keycodes;
98 changes: 98 additions & 0 deletions src/shared-components/immersiveModal/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import PropTypes from 'prop-types';
import React from 'react';
import ReactDOM from 'react-dom';

import KEYCODES from '../../constants/keycodes';
import keyPressMatch from '../../utils/keyPressMatch';
import OffClickWrapper from '../offClickWrapper';
import CloseIcon from '../../svgs/icons/close-icon.svg';
import {
ModalContainer,
Overlay,
CloseIconContainer,
CopyContainer,
Title,
Body,
Footer,
} from './style';

class ImmersiveModal extends React.Component {
static propTypes = {
children: PropTypes.node.isRequired,
canBeClosed: PropTypes.bool,
onClose: PropTypes.func,
header: PropTypes.node,
};

static defaultProps = {
canBeClosed: true,
onClose: () => {},
};

static Title = Title;

static Body = Body;

static Footer = Footer;

constructor(props) {
super(props);

this.htmlNode = document.querySelector('html');
this.domNode =
document.querySelector('#reactPortalSection') || document.body;
}

componentDidMount() {
this.htmlNode.classList.add('no-scroll');

document
.getElementsByTagName('body')[0]
.addEventListener('keydown', this.handleEscapeKey);
}

componentWillUnmount() {
this.htmlNode.classList.remove('no-scroll');

document
.getElementsByTagName('body')[0]
.removeEventListener('keydown', this.handleEscapeKey);
}

handleEscapeKey = event => {
if (keyPressMatch(event, KEYCODES.escape)) {
this.closeModal();
}
};

closeModal = () => {
const { canBeClosed, onClose } = this.props;
if (canBeClosed) {
onClose();
}
};

render() {
const {
children, canBeClosed, onClose, header,
} = this.props;
return ReactDOM.createPortal(
<Overlay>
<ModalContainer>
<OffClickWrapper onOffClick={this.closeModal}>
{canBeClosed && (
<CloseIconContainer onClick={onClose}>
<CloseIcon />
</CloseIconContainer>
)}
{header}
<CopyContainer>{children}</CopyContainer>
</OffClickWrapper>
</ModalContainer>
</Overlay>,
this.domNode
);
}
}

export default ImmersiveModal;
85 changes: 85 additions & 0 deletions src/shared-components/immersiveModal/style.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import styled from 'react-emotion';

import Typography from '../typography';
import {
ANIMATION,
BREAKPOINTS,
COLORS,
MEDIA_QUERIES,
SPACING,
Z_SCALE,
} from '../../constants';

export const Overlay = styled.div`
background-color: ${COLORS.overlay};
bottom: 0;
left: 0;
position: fixed;
right: 0;
top: 0;
z-index: ${Z_SCALE.modal};
overflow-y: auto;
`;

export const ModalContainer = styled.div`
background-color: ${COLORS.white};
height: 100vh;
margin: 0 auto;
max-width: ${BREAKPOINTS.sm}px;
overflow-y: auto;
position: relative;
width: 100%;
${MEDIA_QUERIES.smUp} {
height: auto;
margin-bottom: ${SPACING.base};
margin-top: ${SPACING.base};
}
`;

export const CloseIconContainer = styled.div`
cursor: pointer;
position: absolute;
right: ${SPACING.small};
top: ${SPACING.small};
transform: scale(1, 1);
transition: all ${ANIMATION.defaultTiming};
z-index: 2000;
padding: ${SPACING.small};
background-color: ${COLORS.white};
border-radius: 50%;
&:hover {
transform: scale(1.1, 1.1);
}
`;

export const CopyContainer = styled.div`
padding: ${SPACING.medium} ${SPACING.base};
${MEDIA_QUERIES.mdUp} {
padding: ${SPACING.large} ${SPACING.medium};
}
`;

export const Title = styled(Typography.Title)`
margin-bottom: ${SPACING.small};
text-align: left;
`;

export const Body = styled.div`
color: ${COLORS.purple80};
text-align: left;
&:not(:last-child) {
margin-bottom: ${SPACING.base};
}
p > a {
text-transform: none;
}
`;

export const Footer = styled.div`
margin-bottom: ${SPACING.xsmall};
`;
23 changes: 23 additions & 0 deletions src/shared-components/immersiveModal/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import { shallow } from 'enzyme';

import ImmersiveModal from './index';

const childComponent = <div />;

describe('<ImmersiveModal />', () => {
describe('ImmersiveModal closure', () => {
it('invokes the onClose prop', () => {
const spy = jest.fn();
const wrapper = shallow(
<ImmersiveModal onClose={spy}>
{childComponent}
</ImmersiveModal>
);

wrapper.instance().closeModal();

expect(spy).toHaveBeenCalled();
});
});
});
Loading

0 comments on commit 40fd5be

Please sign in to comment.