From 0cff3481d310823089afd306eb09c6288561498d Mon Sep 17 00:00:00 2001 From: Jozef Harag <32jojo32@gmail.com> Date: Thu, 10 Sep 2020 13:49:38 +0200 Subject: [PATCH] Add Modal --- package-lock.json | 87 ++++++++++++++++- package.json | 4 + src/modules/index.css | 1 + src/modules/modal/content.jsx | 21 +++++ src/modules/modal/footer.jsx | 18 ++++ src/modules/modal/modal.css | 65 +++++++++++++ src/modules/modal/modal.jsx | 132 ++++++++++++++++++++++++++ src/modules/modal/modal.md | 169 ++++++++++++++++++++++++++++++++++ src/modules/modal/portal.jsx | 16 ++++ src/modules/modal/title.jsx | 15 +++ styleguide.config.js | 10 +- 11 files changed, 535 insertions(+), 3 deletions(-) create mode 100644 src/modules/modal/content.jsx create mode 100644 src/modules/modal/footer.jsx create mode 100644 src/modules/modal/modal.css create mode 100644 src/modules/modal/modal.jsx create mode 100644 src/modules/modal/modal.md create mode 100644 src/modules/modal/portal.jsx create mode 100644 src/modules/modal/title.jsx diff --git a/package-lock.json b/package-lock.json index cc570de2..1605b5cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6743,6 +6743,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 +7795,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 +8291,27 @@ "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==" + }, + "focus-trap": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-6.0.1.tgz", + "integrity": "sha512-BOksLMPK/jlXD389jYPlZHAqiDdy9W63EBQfVIouME8s3UZsCEmq3NA53qt30S4i72sRcDjc1FHtast0TmqhRA==", + "requires": { + "tabbable": "^5.0.0" + } + }, + "focus-trap-react": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-8.0.0.tgz", + "integrity": "sha512-wWIfr2vU92YTS5MssxU93/qSKPQImqxQZiBaU6Vfh0QKczJ1C1+hszCPhhj5Pm3k1Gr+fdDlshDw3IAB7wBZXA==", + "requires": { + "focus-trap": "^6.0.1" + } + }, "follow-redirects": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.11.0.tgz", @@ -10848,6 +10879,11 @@ "lower-case": "^1.1.1" } }, + "no-scroll": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/no-scroll/-/no-scroll-2.1.1.tgz", + "integrity": "sha512-YTzGAJOo/B6hkodeT5SKKHpOhAzjMfkUCCXjLJwjWk2F4/InIg+HbdH9kmT7bKpleDuqLZDTRy2OdNtAj0IVyQ==" + }, "node-dir": { "version": "0.1.17", "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", @@ -14542,6 +14578,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 +14881,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 +14917,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", @@ -17502,6 +17567,11 @@ "object-assign": "^4.1.1" } }, + "tabbable": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.0.0.tgz", + "integrity": "sha512-+TJTMpkHRCWkMGczHHVEfzBYCsVOiBjd3vle55AT4H299BhdJDLFqcYmr7S6kt5EGhT8gAywSC5gPUBDNvtl7w==" + }, "table": { "version": "5.4.6", "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", @@ -17987,8 +18057,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 +18451,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..e9f3cf5e 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,11 @@ "classnames": "^2.2.6", "cosmiconfig": "^7.0.0", "deepmerge": "^4.2.2", + "focus-trap-react": "^8.0.0", + "no-scroll": "^2.1.1", "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/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/modal.css b/src/modules/modal/modal.css new file mode 100644 index 00000000..d341db6f --- /dev/null +++ b/src/modules/modal/modal.css @@ -0,0 +1,65 @@ +: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; + color: var(--modal-title-color); + border-bottom: 1px solid var(--gray-200); +} + +.footer { + padding-top: 1rem; + border-top: 1px solid var(--gray-200); +} + +.content { + overflow-y: scroll; +} + +.modal { + position: relative; + top: 10vh; + display: flex; + flex-direction: column; + max-height: 80vh; + padding: var(--modal-padding-y) var(--modal-padding-x); + margin-right: auto; + margin-left: auto; + overflow-y: scroll; + background: var(--modal-background); + border-radius: var(--modal-corner-radius); + box-shadow: var(--modal-box-shadow); +} + +@media (min-width: 768px) { + .modal { + width: 750px; + } +} +@media (min-width: 992px) { + .modal { + width: 970px; + } +} +@media (min-width: 1200px) { + .modal { + width: 1170px; + } +} diff --git a/src/modules/modal/modal.jsx b/src/modules/modal/modal.jsx new file mode 100644 index 00000000..0df10cfb --- /dev/null +++ b/src/modules/modal/modal.jsx @@ -0,0 +1,132 @@ +import React, { useRef, useCallback } from 'react' +import FocusLock from 'react-focus-lock' +import ScrollLock from 'react-scrolllock' +import PropTypes from 'prop-types' + +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, + onExit, + className, + hideManually = false, + selector = 'body', + ...restProps +}) => { + const modalRef = useRef(null) + + const handleKeyDown = useCallback( + (event) => { + if (hideManually || !onExit) return + if (event.key === 'Escape' || event.key === 'Esc' || event.keyCode === 27) + onExit(event) + }, + [hideManually] + ) + + const handleClick = useCallback( + (event) => { + if (hideManually || !onExit) 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 + onExit(event) + }, + [hideManually] + ) + + return ( + + + + {/* eslint-disable-next-line max-len */} + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} +
+
+ {children} +
+
+
+
+
+ ) +} + +Modal.Title = ModalTitle +Modal.Content = ModalContent +Modal.Footer = ModalFooter + +Modal.propTypes = { + 'id': PropTypes.string, + 'children': PropTypes.node, + /** + * Callback called when modal should be closed + */ + 'onExit': 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 onExit 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..ab2fecd3 --- /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/styleguide.config.js b/styleguide.config.js index d0c68050..cdde5f13 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,14 @@ module.exports = { 'src/modules/select/option.jsx', ], }, + { + name: 'Modal', + components: () => [ + 'src/modules/modal/modal.jsx', + 'src/modules/modal/title.jsx', + 'src/modules/modal/footer.jsx', + ], + }, ], }, ],