Skip to content

Commit

Permalink
Add Modal
Browse files Browse the repository at this point in the history
  • Loading branch information
Joozty committed Sep 14, 2020
1 parent 3a391d5 commit 79a93d5
Show file tree
Hide file tree
Showing 14 changed files with 507 additions and 4 deletions.
66 changes: 64 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,13 @@
"@mdi/svg": "^5.5.55",
"@tippyjs/react": "^4.1.0",
"babel-plugin-add-react-displayname": "0.0.5",
"body-scroll-lock": "^3.1.3",
"classnames": "^2.2.6",
"cosmiconfig": "^7.0.0",
"deepmerge": "^4.2.2",
"prop-types": "^15.7.2",
"react-focus-lock": "^2.4.1",
"react-scrolllock": "^5.0.1",
"sanitize.css": "^12.0.1",
"svgo": "^1.3.2",
"svgstore": "^3.0.0-2",
Expand Down
1 change: 1 addition & 0 deletions src/modules/index.css
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
@import url('select/select.css');
@import url('popover/popover.css');
@import url('modal/modal.css');
1 change: 1 addition & 0 deletions src/modules/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export Select from './select'
export Popover from './popover'
export Modal from './modal'
21 changes: 21 additions & 0 deletions src/modules/modal/content.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react'

import styles from './modal.css'

import { classNames } from 'utils'

const ModalContent = ({
children,
className,
tag: Tag = 'div',
...passProps
}) => (
<Tag
className={classNames.use(styles.content).join(className)}
{...passProps}
>
{children}
</Tag>
)

export default ModalContent
18 changes: 18 additions & 0 deletions src/modules/modal/footer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react'

import styles from './modal.css'

import { classNames } from 'utils'

const ModalFooter = ({
children,
className,
tag: Tag = 'footer',
...passProps
}) => (
<Tag className={classNames.use(styles.footer).join(className)} {...passProps}>
{children}
</Tag>
)

export default ModalFooter
1 change: 1 addition & 0 deletions src/modules/modal/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default from './modal'
53 changes: 53 additions & 0 deletions src/modules/modal/modal.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
:root {
--modal-padding-x: var(--component-padding-x, 1rem);
--modal-padding-y: var(--component-padding-y, 1rem);
--modal-background: var(--white, #fff);
--modal-corner-radius: var(--component-corner-radius, 2px);
--modal-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
--modal-title-color: var(--gray-800);
--modal-overlay-color: rgba(0, 0, 0, 0.5);
}

.overlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1000;
background: var(--modal-overlay-color);
}

.title {
padding-bottom: 1rem;
margin: 0;
color: var(--modal-title-color);
border-bottom: 1px solid var(--gray-200);
}

.footer {
padding-top: 1rem;
margin: 0;
border-top: 1px solid var(--gray-200);
}

.content {
overflow-y: auto;
}

.modal {
position: relative;
top: 10vh;
display: flex;
flex-direction: column;
width: 100%;
max-width: 60rem;
max-height: 80vh;
padding: var(--modal-padding-y) var(--modal-padding-x);
margin-right: auto;
margin-left: auto;
overflow: auto;
background: var(--modal-background);
border-radius: var(--modal-corner-radius);
box-shadow: var(--modal-box-shadow);
}
134 changes: 134 additions & 0 deletions src/modules/modal/modal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import React, { useRef, useCallback, useEffect } from 'react'
import FocusLock from 'react-focus-lock'
import PropTypes from 'prop-types'
import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock'

import styles from './modal.css'
import ModalTitle from './title'
import ModalFooter from './footer'
import ModalContent from './content'
import ModalPortal from './portal'

import { classNames } from 'utils'

const checkAccessibility = (props, propName, componentName) => {
if (!props['aria-label'] && !props['aria-labelledby']) {
return new Error(
`One of props 'aria-label' or 'aria-labelledby' isRequired in '${componentName}'.`
)
}

if (!props['aria-label'] && !props['aria-labelledby']) {
return new Error(
`One of props 'aria-label' or 'aria-labelledby' was not specified in '${componentName}'.`
)
}

if (props['aria-label'] && props['aria-labelledby']) {
return new Error(
`Props 'aria-label' and 'aria-labelledby' in '${componentName}' mutually exclusive.`
)
}

return true
}

const Modal = ({
children,
onClose,
className,
hideManually = false,
selector = 'body',
...restProps
}) => {
const modalRef = useRef(null)

const handleKeyDown = useCallback(
(event) => {
if (hideManually || !onClose) return
if (event.key === 'Escape' || event.key === 'Esc' || event.keyCode === 27)
onClose(event)
},
[hideManually]
)

const handleClick = useCallback(
(event) => {
if (hideManually || !onClose) return
if (
(modalRef.current && modalRef.current.contains(event.target)) ||
// If the click is on the scrollbar we don't want to close the modal.
event.pageX > event.target.ownerDocument.documentElement.offsetWidth ||
event.pageY > event.target.ownerDocument.documentElement.offsetHeight
)
return
onClose(event)
},
[hideManually]
)

useEffect(() => enableBodyScroll(modalRef.current), [])

return (
<ModalPortal selector={selector}>
<FocusLock returnFocus>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div
className={styles.overlay}
onClick={handleClick}
onKeyDown={handleKeyDown}
{...restProps}
>
<div
ref={(ref) => {
modalRef.current = ref
disableBodyScroll(modalRef.current)
}}
role="dialog"
className={classNames.use(styles.modal).join(className)}
{...restProps}
>
{children}
</div>
</div>
</FocusLock>
</ModalPortal>
)
}

Modal.Title = ModalTitle
Modal.Content = ModalContent
Modal.Footer = ModalFooter

Modal.propTypes = {
'id': PropTypes.string,
'children': PropTypes.node,
/**
* Callback called when modal is being closed
*/
'onClose': PropTypes.func,
/**
* Describes the title of the modal
*/
'aria-label': checkAccessibility,
/**
* Points to modal label.
* Ideally it should be used in conjunction with Modal.Title
*/
'aria-labelledby': checkAccessibility,
/**
* If set to true onClose callback is not triggered on overlay click
* or hitting ESC button.
*/
'hideManually': PropTypes.bool,
/**
* The location where the modal is rendered
*/
'selector': PropTypes.string,
/**
* className applied on modal
*/
'className': PropTypes.string,
}

export default Modal
Loading

0 comments on commit 79a93d5

Please sign in to comment.