diff --git a/package-lock.json b/package-lock.json index cc570de2..fae0dea8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4774,6 +4774,11 @@ } } }, + "body-scroll-lock": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-3.1.3.tgz", + "integrity": "sha512-KMV9MT96y2PFUL2C98e2nx/Gs2mhCAzYP6Gsu/9r7Rhn27qxu1yTnQBqHogUuvwVSbstEHNXTaToPNsL7oBZ9g==" + }, "bonjour": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", @@ -6743,6 +6748,11 @@ "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", "dev": true }, + "detect-node-es": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.0.0.tgz", + "integrity": "sha512-S4AHriUkTX9FoFvL4G8hXDcx6t3gp2HpfCza3Q0v6S78gul2hKWifLQbeW+ZF89+hSm2ZIc/uF3J97ZgytgTRg==" + }, "detect-port-alt": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", @@ -7790,6 +7800,11 @@ "clone-regexp": "^2.1.0" } }, + "exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=" + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -8281,6 +8296,11 @@ "readable-stream": "^2.3.6" } }, + "focus-lock": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.7.0.tgz", + "integrity": "sha512-LI7v2mH02R55SekHYdv9pRHR9RajVNyIJ2N5IEkWbg7FT5ZmJ9Hw4mWxHeEUcd+dJo0QmzztHvDvWcc7prVFsw==" + }, "follow-redirects": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.11.0.tgz", @@ -14542,6 +14562,14 @@ "prop-types": "^15.6.2" } }, + "react-clientside-effect": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.2.tgz", + "integrity": "sha512-nRmoyxeok5PBO6ytPvSjKp9xwXg9xagoTK1mMjwnQxqM9Hd7MNPl+LS1bOSOe+CV2+4fnEquc7H/S8QD3q697A==", + "requires": { + "@babel/runtime": "^7.0.0" + } + }, "react-dev-utils": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-9.1.0.tgz", @@ -14837,6 +14865,19 @@ "integrity": "sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA==", "dev": true }, + "react-focus-lock": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.4.1.tgz", + "integrity": "sha512-c5ZP56KSpj9EAxzScTqQO7bQQNPltf/W1ZEBDqNDOV1XOIwvAyHX0O7db9ekiAtxyKgnqZjQlLppVg94fUeL9w==", + "requires": { + "@babel/runtime": "^7.0.0", + "focus-lock": "^0.7.0", + "prop-types": "^15.6.2", + "react-clientside-effect": "^1.2.2", + "use-callback-ref": "^1.2.1", + "use-sidecar": "^1.0.1" + } + }, "react-group": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/react-group/-/react-group-3.0.2.tgz", @@ -14860,6 +14901,14 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==" }, + "react-scrolllock": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-scrolllock/-/react-scrolllock-5.0.1.tgz", + "integrity": "sha512-poeEsjnZAlpA6fJlaNo4rZtcip2j6l5mUGU/SJe1FFlicEudS943++u7ZSdA7lk10hoyYK3grOD02/qqt5Lxhw==", + "requires": { + "exenv": "^1.2.2" + } + }, "react-simple-code-editor": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/react-simple-code-editor/-/react-simple-code-editor-0.10.0.tgz", @@ -17987,8 +18036,7 @@ "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", - "dev": true + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" }, "tty-browserify": { "version": "0.0.0", @@ -18382,6 +18430,20 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, + "use-callback-ref": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.4.tgz", + "integrity": "sha512-rXpsyvOnqdScyied4Uglsp14qzag1JIemLeTWGKbwpotWht57hbP78aNT+Q4wdFKQfQibbUX4fb6Qb4y11aVOQ==" + }, + "use-sidecar": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.0.3.tgz", + "integrity": "sha512-ygJwGUBeQfWgDls7uTrlEDzJUUR67L8Rm14v/KfFtYCdHhtjHZx1Krb3DIQl3/Q5dJGfXLEQ02RY8BdNBv87SQ==", + "requires": { + "detect-node-es": "^1.0.0", + "tslib": "^1.9.3" + } + }, "util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", diff --git a/package.json b/package.json index 2d5c1d15..a6674f32 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/modules/index.css b/src/modules/index.css index c0304540..4d884ce3 100644 --- a/src/modules/index.css +++ b/src/modules/index.css @@ -1,2 +1,3 @@ @import url('select/select.css'); @import url('popover/popover.css'); +@import url('modal/modal.css'); diff --git a/src/modules/index.js b/src/modules/index.js index a00c890d..2f180637 100644 --- a/src/modules/index.js +++ b/src/modules/index.js @@ -1,2 +1,3 @@ export Select from './select' export Popover from './popover' +export Modal from './modal' diff --git a/src/modules/modal/content.jsx b/src/modules/modal/content.jsx new file mode 100644 index 00000000..cd2d593d --- /dev/null +++ b/src/modules/modal/content.jsx @@ -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 +}) => ( + + {children} + +) + +export default ModalContent diff --git a/src/modules/modal/footer.jsx b/src/modules/modal/footer.jsx new file mode 100644 index 00000000..d332cdb5 --- /dev/null +++ b/src/modules/modal/footer.jsx @@ -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 +}) => ( + + {children} + +) + +export default ModalFooter diff --git a/src/modules/modal/index.js b/src/modules/modal/index.js new file mode 100644 index 00000000..4e7a561f --- /dev/null +++ b/src/modules/modal/index.js @@ -0,0 +1 @@ +export default from './modal' diff --git a/src/modules/modal/modal.css b/src/modules/modal/modal.css new file mode 100644 index 00000000..1bac1869 --- /dev/null +++ b/src/modules/modal/modal.css @@ -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); +} diff --git a/src/modules/modal/modal.jsx b/src/modules/modal/modal.jsx new file mode 100644 index 00000000..92403706 --- /dev/null +++ b/src/modules/modal/modal.jsx @@ -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 ( + + + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} +
+
{ + modalRef.current = ref + disableBodyScroll(modalRef.current) + }} + role="dialog" + className={classNames.use(styles.modal).join(className)} + {...restProps} + > + {children} +
+
+
+
+ ) +} + +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 diff --git a/src/modules/modal/modal.md b/src/modules/modal/modal.md new file mode 100644 index 00000000..02b2cef4 --- /dev/null +++ b/src/modules/modal/modal.md @@ -0,0 +1,169 @@ +## Accessible Modal + +Modal is built according to [Modal Dialog Example from WAI ARIA practices](https://www.w3.org/TR/wai-aria-practices-1.1/examples/dialog-modal/dialog.html). +When modal is open focus is trapped withing the modal's focusable nodes. +The dialog can be closed automatically (by hitting ESC key or clicking outside of content area) or manually. +Focus is restored after dialog is closed. + + +```jsx +import React from 'react' + +import Modal from './modal' +import { Button } from '../../elements' + +const DemoOne = () => { + const [isModalActive, setModalActive] = React.useState(false) + const modal = isModalActive ? ( + setModalActive(false)} + > + Demo One + + Modal gets closed automatically on overlay click, by hitting ESC key, or by clicking on button. + + + + + + ) : null + + return ( +

+ + {modal} +

+ ) +} + +const DemoTwo = () => { + const [isModalActive, setModalActive] = React.useState(false) + const modal = isModalActive ? ( + + + Demo Two + + + Modal gets hidden only by clicking on button. + + + + + + ) : null + + return ( +

+ + {modal} +

+ ) +} + +const DemoThree = () => { + const [isModalActive, setModalActive] = React.useState(false) + const modal = isModalActive ? ( + setModalActive(false)} + > + + Demo Three + + +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer facilisis, massa eget ornare bibendum, nulla est porta tellus, eget malesuada leo augue vel ligula. Praesent blandit nisi id enim ultrices, quis gravida sapien pretium. Donec bibendum augue eu lacus dapibus consectetur. Vestibulum fringilla turpis sed tortor dapibus, sit amet commodo ante interdum. Duis bibendum at lacus non volutpat. Proin at lacinia massa, a fermentum metus. Proin at tortor ac justo vehicula iaculis. Praesent sollicitudin dui leo. Cras ac tortor nec nisl gravida porta. Quisque gravida magna sed risus auctor, sed fermentum risus suscipit. Fusce cursus nisi sed ipsum tristique, feugiat tempus lorem eleifend. Curabitur non venenatis justo. Maecenas massa diam, varius quis dolor eu, mollis posuere erat. Phasellus neque diam, mollis ut venenatis at, molestie porttitor nisi. Morbi sed ex libero. +

+

+ Nam varius tincidunt hendrerit. Ut dictum molestie lorem eu placerat. Mauris at ex et nibh volutpat ullamcorper. Donec sit amet diam velit. Aenean at ante non massa scelerisque vehicula. Nunc scelerisque neque sem, non tincidunt nulla molestie et. Nulla at pharetra turpis. Vestibulum aliquam elit id erat malesuada, ut tempus elit rhoncus. Suspendisse sit amet tristique ante. Quisque volutpat pretium sagittis. In accumsan, arcu fringilla venenatis suscipit, elit leo consequat tortor, sit amet consequat est dolor sit amet neque. Curabitur accumsan augue enim, in egestas felis placerat et. +

+

+ Nulla consectetur auctor dignissim. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Quisque eu pulvinar libero, nec cursus leo. Maecenas maximus, urna laoreet tincidunt laoreet, lorem nulla aliquet justo, id scelerisque dui libero a lectus. Mauris quis felis ac orci tempus tempus. Fusce blandit bibendum mi, eu maximus quam sodales quis. Morbi nec luctus nisl. Maecenas suscipit placerat gravida. Morbi viverra magna non risus lobortis, at sodales enim consequat. Vivamus fringilla dignissim quam, id euismod turpis blandit eget. +

+

+ Praesent id orci ac nunc sollicitudin facilisis. Fusce et fringilla metus, a accumsan enim. Ut consectetur est ac ante consequat laoreet. Praesent vitae eros eleifend, congue tellus at, maximus nunc. Vestibulum sed nunc id risus volutpat volutpat. Pellentesque vehicula, metus sed efficitur accumsan, lacus dui commodo turpis, sed consectetur tellus neque non sem. Ut pellentesque enim ut sodales sagittis. In mi lorem, finibus fringilla efficitur vel, ultricies eget erat. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer facilisis, massa eget ornare bibendum, nulla est porta tellus, eget malesuada leo augue vel ligula. Praesent blandit nisi id enim ultrices, quis gravida sapien pretium. Donec bibendum augue eu lacus dapibus consectetur. Vestibulum fringilla turpis sed tortor dapibus, sit amet commodo ante interdum. Duis bibendum at lacus non volutpat. Proin at lacinia massa, a fermentum metus. Proin at tortor ac justo vehicula iaculis. Praesent sollicitudin dui leo. Cras ac tortor nec nisl gravida porta. Quisque gravida magna sed risus auctor, sed fermentum risus suscipit. Fusce cursus nisi sed ipsum tristique, feugiat tempus lorem eleifend. Curabitur non venenatis justo. Maecenas massa diam, varius quis dolor eu, mollis posuere erat. Phasellus neque diam, mollis ut venenatis at, molestie porttitor nisi. Morbi sed ex libero. +

+

+ Nam varius tincidunt hendrerit. Ut dictum molestie lorem eu placerat. Mauris at ex et nibh volutpat ullamcorper. Donec sit amet diam velit. Aenean at ante non massa scelerisque vehicula. Nunc scelerisque neque sem, non tincidunt nulla molestie et. Nulla at pharetra turpis. Vestibulum aliquam elit id erat malesuada, ut tempus elit rhoncus. Suspendisse sit amet tristique ante. Quisque volutpat pretium sagittis. In accumsan, arcu fringilla venenatis suscipit, elit leo consequat tortor, sit amet consequat est dolor sit amet neque. Curabitur accumsan augue enim, in egestas felis placerat et. +

+

+ Nulla consectetur auctor dignissim. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Quisque eu pulvinar libero, nec cursus leo. Maecenas maximus, urna laoreet tincidunt laoreet, lorem nulla aliquet justo, id scelerisque dui libero a lectus. Mauris quis felis ac orci tempus tempus. Fusce blandit bibendum mi, eu maximus quam sodales quis. Morbi nec luctus nisl. Maecenas suscipit placerat gravida. Morbi viverra magna non risus lobortis, at sodales enim consequat. Vivamus fringilla dignissim quam, id euismod turpis blandit eget. +

+

+ Praesent id orci ac nunc sollicitudin facilisis. Fusce et fringilla metus, a accumsan enim. Ut consectetur est ac ante consequat laoreet. Praesent vitae eros eleifend, congue tellus at, maximus nunc. Vestibulum sed nunc id risus volutpat volutpat. Pellentesque vehicula, metus sed efficitur accumsan, lacus dui commodo turpis, sed consectetur tellus neque non sem. Ut pellentesque enim ut sodales sagittis. In mi lorem, finibus fringilla efficitur vel, ultricies eget erat. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer facilisis, massa eget ornare bibendum, nulla est porta tellus, eget malesuada leo augue vel ligula. Praesent blandit nisi id enim ultrices, quis gravida sapien pretium. Donec bibendum augue eu lacus dapibus consectetur. Vestibulum fringilla turpis sed tortor dapibus, sit amet commodo ante interdum. Duis bibendum at lacus non volutpat. Proin at lacinia massa, a fermentum metus. Proin at tortor ac justo vehicula iaculis. Praesent sollicitudin dui leo. Cras ac tortor nec nisl gravida porta. Quisque gravida magna sed risus auctor, sed fermentum risus suscipit. Fusce cursus nisi sed ipsum tristique, feugiat tempus lorem eleifend. Curabitur non venenatis justo. Maecenas massa diam, varius quis dolor eu, mollis posuere erat. Phasellus neque diam, mollis ut venenatis at, molestie porttitor nisi. Morbi sed ex libero. +

+

+ Nam varius tincidunt hendrerit. Ut dictum molestie lorem eu placerat. Mauris at ex et nibh volutpat ullamcorper. Donec sit amet diam velit. Aenean at ante non massa scelerisque vehicula. Nunc scelerisque neque sem, non tincidunt nulla molestie et. Nulla at pharetra turpis. Vestibulum aliquam elit id erat malesuada, ut tempus elit rhoncus. Suspendisse sit amet tristique ante. Quisque volutpat pretium sagittis. In accumsan, arcu fringilla venenatis suscipit, elit leo consequat tortor, sit amet consequat est dolor sit amet neque. Curabitur accumsan augue enim, in egestas felis placerat et. +

+

+ Nulla consectetur auctor dignissim. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Quisque eu pulvinar libero, nec cursus leo. Maecenas maximus, urna laoreet tincidunt laoreet, lorem nulla aliquet justo, id scelerisque dui libero a lectus. Mauris quis felis ac orci tempus tempus. Fusce blandit bibendum mi, eu maximus quam sodales quis. Morbi nec luctus nisl. Maecenas suscipit placerat gravida. Morbi viverra magna non risus lobortis, at sodales enim consequat. Vivamus fringilla dignissim quam, id euismod turpis blandit eget. +

+

+ Praesent id orci ac nunc sollicitudin facilisis. Fusce et fringilla metus, a accumsan enim. Ut consectetur est ac ante consequat laoreet. Praesent vitae eros eleifend, congue tellus at, maximus nunc. Vestibulum sed nunc id risus volutpat volutpat. Pellentesque vehicula, metus sed efficitur accumsan, lacus dui commodo turpis, sed consectetur tellus neque non sem. Ut pellentesque enim ut sodales sagittis. In mi lorem, finibus fringilla efficitur vel, ultricies eget erat. +

+
+ + + +
+ ) : null + + return ( +

+ + {modal} +

+ ) +} + + +<> + + + + +``` diff --git a/src/modules/modal/portal.jsx b/src/modules/modal/portal.jsx new file mode 100644 index 00000000..71f636c4 --- /dev/null +++ b/src/modules/modal/portal.jsx @@ -0,0 +1,16 @@ +import { useRef, useEffect, useState } from 'react' +import { createPortal } from 'react-dom' + +const ModalPortal = ({ children, selector }) => { + const ref = useRef() + const [mounted, setMounted] = useState(false) + + useEffect(() => { + ref.current = document.querySelector(selector) + setMounted(true) + }, [selector]) + + return mounted ? createPortal(children, ref.current) : null +} + +export default ModalPortal diff --git a/src/modules/modal/title.jsx b/src/modules/modal/title.jsx new file mode 100644 index 00000000..8e67117a --- /dev/null +++ b/src/modules/modal/title.jsx @@ -0,0 +1,15 @@ +import React from 'react' + +import styles from './modal.css' + +import { classNames } from 'utils' + +const ModalTitle = ({ children, className, tag: Tag = 'h1', ...restProps }) => ( + + {children} + +) + +ModalTitle.displayName = 'ModalTitle' + +export default ModalTitle diff --git a/src/modules/popover/popover.jsx b/src/modules/popover/popover.jsx index 3ffd3f20..70114a80 100644 --- a/src/modules/popover/popover.jsx +++ b/src/modules/popover/popover.jsx @@ -89,7 +89,7 @@ Popover.propTypes = { /** * Indicated whether the popover contents should be hidden on blur. */ - hideOnBlur: PropTypes.bool.isRequired, + hideOnBlur: PropTypes.bool, /** * Preferred position of the content. The position may not be reflected * if opposite placement has more space or overflow occurs. diff --git a/styleguide.config.js b/styleguide.config.js index d0c68050..5b8c4213 100644 --- a/styleguide.config.js +++ b/styleguide.config.js @@ -52,7 +52,7 @@ module.exports = { }, { name: 'Modules', - components: 'src/modules/!(select)/**/*.{js,jsx,ts,tsx}', + components: 'src/modules/!(select|modal)/**/*.{js,jsx,ts,tsx}', sections: [ { name: 'Select', @@ -61,6 +61,15 @@ module.exports = { 'src/modules/select/option.jsx', ], }, + { + name: 'Modal', + components: () => [ + 'src/modules/modal/modal.jsx', + 'src/modules/modal/title.jsx', + 'src/modules/modal/content.jsx', + 'src/modules/modal/footer.jsx', + ], + }, ], }, ],