orcs-design-system
Version:
TeamForm's Design System, aka: ORCS
239 lines (236 loc) • 10.1 kB
JavaScript
import React, { useCallback, useEffect, useMemo, 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 = keyframes(["0%{transform:scale(0);}100%{transform:scale(1);}"]);
const fadeIn = keyframes(["0%{opacity:0;}100%{opacity:1;}"]);
const Overlay = styled(Flex).withConfig({
displayName: "Modal__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 = styled(Box).withConfig({
displayName: "Modal__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 = styled(Button).withConfig({
displayName: "Modal__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 = styled.div.withConfig({
displayName: "Modal__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 = styled.div.withConfig({
displayName: "Modal__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 = styled.div.withConfig({
displayName: "Modal__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,
height,
maxWidth,
maxHeight,
minWidth,
minHeight,
overflow,
onClose,
theme,
visible,
overlayID,
modalID,
headerContent,
footerContent,
...restProps
} = _ref;
const [lastActiveElement, setLastActiveElement] = useState(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?.dataset.actionMenuId && isHidden(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(() => {
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 vida overlayID.
*/
const handler = e => {
if (e.target.id === overlayID) {
onClose();
}
};
const eventTypes = ["touchstart", "click"];
eventTypes.map(type => {
window.addEventListener(type, handler, true);
});
return () => {
eventTypes.map(type => {
window.removeEventListener(type, handler, true);
});
};
}, [visible, onClose, overlayID]);
if (!visible) return null;
const component = /*#__PURE__*/_jsx(Overlay, {
alignItems: "center",
justifyContent: "center",
id: overlayID,
...restProps,
children: visible && /*#__PURE__*/_jsx(FocusTrap, {
focusTrapOptions: {
onDeactivate: onClose
},
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: [headerContent ? /*#__PURE__*/_jsxs(HeaderContent, {
children: [/*#__PURE__*/_jsx(CloseButton, {
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, {
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
};
Modal.defaultProps = {
width: "350px",
height: "auto",
maxHeight: "90vh",
maxWidth: "90vw",
overlayID: "modal-overlay",
modalID: "modal-container"
};
export default Modal;