orcs-design-system
Version:
TeamForm's Design System, aka: ORCS
283 lines (279 loc) • 13.4 kB
JavaScript
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;