UNPKG

orcs-design-system

Version:
283 lines (279 loc) 13.4 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties"; const _excluded = ["children", "width", "height", "maxWidth", "maxHeight", "minWidth", "minHeight", "overflow", "onClose", "theme", "visible", "overlayID", "modalID", "headerContent", "footerContent"]; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import ReactDOM from "react-dom"; import PropTypes from "prop-types"; import styled, { keyframes, ThemeProvider } from "styled-components"; import FocusTrap from "focus-trap-react"; import { css } from "@styled-system/css"; import { themeGet } from "@styled-system/theme-get"; import Icon from "../Icon"; import Button from "../Button"; import Flex from "../Flex"; import Box from "../Box"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; const scaleIn = /*#__PURE__*/keyframes(["0%{transform:scale(0);}100%{transform:scale(1);}"]); const fadeIn = /*#__PURE__*/keyframes(["0%{opacity:0;}100%{opacity:1;}"]); const Overlay = /*#__PURE__*/styled(Flex).withConfig({ displayName: "Overlay", componentId: "sc-1v5puuo-0" })(["position:fixed;background:rgba(0,0,0,0.7);top:0;right:0;bottom:0;left:0;margin:0;opacity:0;transition:all 300ms ease-in-out;opacity:1;z-index:900;visibility:visible;animation:300ms ", " ease-in-out;justify-content:center;align-items:center;"], fadeIn); const Container = /*#__PURE__*/styled(Box).withConfig({ displayName: "Container", componentId: "sc-1v5puuo-1" })(["position:relative;z-index:1000;animation:300ms ", " ease-in-out,300ms ", " ease-in-out;display:flex;flex-direction:column;resize:both;overflow:", ";"], fadeIn, scaleIn, props => props.overflow === "visible" ? "visible" : "hidden"); const CloseButton = /*#__PURE__*/styled(Button).withConfig({ displayName: "CloseButton", componentId: "sc-1v5puuo-2" })(props => css({ position: "absolute", top: "s", right: "s", bg: "transparent", color: themeGet("colors.greyDark")(props), borderColor: "transparent", "&:hover": { bg: themeGet("colors.greyLighter")(props), borderColor: themeGet("colors.greyLighter")(props) }, "&:focus": { outline: "0", boxShadow: themeGet("shadows.thinOutline")(props) + " " + themeGet("colors.black20")(props) } })); const HeaderContent = /*#__PURE__*/styled.div.withConfig({ displayName: "HeaderContent", componentId: "sc-1v5puuo-3" })(["display:flex;align-items:flex-start;flex-direction:row-reverse;justify-content:space-between;width:100%;padding-bottom:", ";border-bottom:solid 1px ", ";flex:0 0 auto;"], props => themeGet("space.r")(props), props => themeGet("colors.greyLighter")(props)); const FooterContent = /*#__PURE__*/styled.div.withConfig({ displayName: "FooterContent", componentId: "sc-1v5puuo-4" })(["display:flex;align-items:flex-start;justify-content:space-between;width:100%;padding-top:", ";border-top:solid 1px ", ";flex:0 0 auto;"], props => themeGet("space.r")(props), props => themeGet("colors.greyLighter")(props)); const ScrollableContent = /*#__PURE__*/styled.div.withConfig({ displayName: "ScrollableContent", componentId: "sc-1v5puuo-5" })(["--scrollbar-size:8px;--scrollbar-minlength:30px;--scrollbar-ff-width:thin;--scrollbar-track-color:rgba(0,0,0,0.05);--scrollbar-color:rgba(0,0,0,0.2);--scrollbar-color-hover:rgba(0,0,0,0.35);--scrollbar-color-active:rgba(0,0,0,0.5);height:100%;flex:1 1 auto;overflow-y:", ";margin-top:", ";-webkit-overflow-scrolling:touch;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;scrollbar-width:var(--scrollbar-ff-width);scrollbar-color:var(--scrollbar-color) var(--scrollbar-track-color);&::-webkit-scrollbar{height:var(--scrollbar-size);width:var(--scrollbar-size);}&::-webkit-scrollbar-track{background-color:var(--scrollbar-track-color);}&::-webkit-scrollbar-thumb{background-color:var(--scrollbar-color);border-radius:4px;}&::-webkit-scrollbar-thumb:hover{background-color:var(--scrollbar-color-hover);}&::-webkit-scrollbar-thumb:active{background-color:var(--scrollbar-color-active);}&::-webkit-scrollbar-thumb:vertical{min-height:var(--scrollbar-minlength);}&::-webkit-scrollbar-thumb:horizontal{min-width:var(--scrollbar-minlength);}"], props => props.overflow === "visible" ? "visible" : "auto", props => props.headerContent ? "0" : "20px"); const isHidden = el => window.getComputedStyle(el).display === "none"; const Modal = _ref => { let { children, width = "350px", height = "auto", maxWidth = "90vw", maxHeight = "90vh", minWidth, minHeight, overflow, onClose, theme, visible, overlayID = "modal-overlay", modalID = "modal-container", headerContent, footerContent } = _ref, restProps = _objectWithoutProperties(_ref, _excluded); const [lastActiveElement, setLastActiveElement] = useState(null); const [isSelectingText, setIsSelectingText] = useState(false); const isSelectingTextRef = useRef(false); const closeButtonRef = useRef(null); const ariaLabel = useMemo(() => { if (restProps.ariaLabel) { return restProps.ariaLabel; } else if (typeof headerContent === "string") { return headerContent; } }, [restProps.ariaLabel, headerContent]); const focusLastActiveElement = useCallback(() => { if (!lastActiveElement) return; if (lastActiveElement !== null && lastActiveElement !== void 0 && lastActiveElement.dataset.actionMenuId && isHidden(lastActiveElement === null || lastActiveElement === void 0 ? void 0 : lastActiveElement.parentNode)) { const actionMenu = document.getElementById(lastActiveElement.dataset.actionMenuId); actionMenu.focus(); } else { lastActiveElement.focus(); } }, [lastActiveElement]); // On becoming visible focus the top useEffect(() => { if (visible && !lastActiveElement) { // Keep track of last clicked element to refocus to after dialog closes setLastActiveElement(document.activeElement); } else if (!visible) { setLastActiveElement(null); // Focus the last active element before modal open when modal is closed focusLastActiveElement(); } }, [visible, focusLastActiveElement, lastActiveElement]); useEffect(() => { isSelectingTextRef.current = isSelectingText; }, [isSelectingText]); useEffect(() => { if (!visible) return; /* Adding onClick handler on the Overlay does not work. * So we add a global listener and check if the element clicked is the * overlay via overlayID. */ const handleMouseDown = e => { // Check if the user is starting to select text if (e.target.closest("#".concat(modalID))) { setIsSelectingText(true); } }; const handleMouseUp = () => { // Reset text selection state after a short delay setTimeout(() => { setIsSelectingText(false); }, 100); }; const handleClick = e => { if (e.target.id === overlayID && !isSelectingTextRef.current) { onClose(); } }; const handleTouchStart = e => { if (e.target.id === overlayID) { onClose(); } }; // Add event listeners window.addEventListener("mousedown", handleMouseDown, true); window.addEventListener("mouseup", handleMouseUp, true); window.addEventListener("click", handleClick, true); window.addEventListener("touchstart", handleTouchStart, true); return () => { window.removeEventListener("mousedown", handleMouseDown, true); window.removeEventListener("mouseup", handleMouseUp, true); window.removeEventListener("click", handleClick, true); window.removeEventListener("touchstart", handleTouchStart, true); }; }, [visible, onClose, overlayID, modalID]); if (!visible) return null; const component = /*#__PURE__*/_jsx(Overlay, _objectSpread(_objectSpread({ alignItems: "center", justifyContent: "center", id: overlayID }, restProps), {}, { children: visible && /*#__PURE__*/_jsx(FocusTrap, { focusTrapOptions: { onDeactivate: onClose, // Use initialFocus to point to close button, which is always present // This ensures FocusTrap has a valid focus target from the start, preventing errors // in test environments where content might not be fully rendered initialFocus: () => closeButtonRef.current || document.querySelector(".modal-close"), // Use fallbackFocus as backup in case initialFocus fails fallbackFocus: () => closeButtonRef.current || document.querySelector(".modal-close") }, children: /*#__PURE__*/_jsx("div", { id: "modal", role: "dialog", "aria-modal": "true", "aria-label": ariaLabel, children: /*#__PURE__*/_jsxs(Container, { width: width, height: height, maxWidth: maxWidth, maxHeight: maxHeight, minWidth: minWidth, minHeight: minHeight, overflow: overflow, borderRadius: "2", bg: "white", p: "r", id: modalID, children: [/*#__PURE__*/_jsx("input", { type: "text", tabIndex: 0, "aria-hidden": "true", style: { position: "absolute", left: "-9999px", width: "1px", height: "1px", opacity: 0, pointerEvents: "none" }, readOnly: true }), headerContent ? /*#__PURE__*/_jsxs(HeaderContent, { children: [/*#__PURE__*/_jsx(CloseButton, { ref: closeButtonRef, onClick: onClose, className: "modal-close", small: true, px: "6px", "aria-label": "Close dialog", children: /*#__PURE__*/_jsx(Icon, { icon: ["fas", "times"], color: "greyDark", size: "lg" }) }), /*#__PURE__*/_jsx(Box, { mr: "xl", width: "100%", children: headerContent })] }) : /*#__PURE__*/_jsx(CloseButton, { ref: closeButtonRef, onClick: onClose, className: "modal-close", small: true, px: "6px", "aria-label": "Close dialog", children: /*#__PURE__*/_jsx(Icon, { icon: ["fas", "times"], color: "greyDark", size: "lg" }) }), /*#__PURE__*/_jsx(ScrollableContent, { headerContent: headerContent, overflow: overflow, children: children }), footerContent && /*#__PURE__*/_jsx(FooterContent, { children: footerContent })] }) }) }) })); return /*#__PURE__*/ReactDOM.createPortal(theme ? /*#__PURE__*/_jsx(ThemeProvider, { theme: theme, children: component }) : component, document.body); }; Modal.propTypes = { /** Specifies the children of the Modal */ children: PropTypes.oneOfType([PropTypes.element, PropTypes.string, PropTypes.node]), /** Specifies content for the header of the modal */ headerContent: PropTypes.oneOfType([PropTypes.element, PropTypes.node]), /** Specifies content for the header of the modal */ footerContent: PropTypes.oneOfType([PropTypes.element, PropTypes.node]), /** Specifies the width of the Modal */ width: PropTypes.string, /** Specifies the max width of the Modal */ maxWidth: PropTypes.string, /** Specifies the min width of the Modal */ minWidth: PropTypes.string, /** Specifies the height of the Modal */ height: PropTypes.string, /** Specifies the max height of the Modal */ maxHeight: PropTypes.string, /** Specifies the min height of the Modal */ minHeight: PropTypes.string, /** Specifies the visibility of the Modal */ visible: PropTypes.bool, /** Specifies the function to run on clicking X icon. Ensure that this function will close Modal through the `visible` prop */ onClose: PropTypes.func, /** Specifies whether the Modal overflow is visible or not, default is `hidden`. If height is not enough, vertical scrollbar will be displayed (`overflow-y: auto`) */ overflow: PropTypes.string, /** Specifies the id of the overlay element for targeting */ overlayID: PropTypes.string, /** Specifies the id of the modal element for targeting */ modalID: PropTypes.string, /** Sets the theme for the Modal */ theme: PropTypes.object, /** Specifies the aria-label for the modal. Set this if headerContent is not defined as a string. */ ariaLabel: PropTypes.string }; export default Modal;