UNPKG

@kiwicom/orbit-components

Version:

Orbit-components is a React component library which provides developers with the easiest possible way of building Kiwi.com’s products.

400 lines (395 loc) 19.8 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default; exports.__esModule = true; exports.default = exports.ModalWrapperContent = exports.ModalSection = exports.ModalHeader = exports.ModalFooter = void 0; var React = _interopRequireWildcard(require("react")); var _styledComponents = _interopRequireWildcard(require("styled-components")); var _ModalContext = require("./ModalContext"); var _ModalHeader = _interopRequireWildcard(require("./ModalHeader")); exports.ModalHeading = _ModalHeader.ModalHeading; exports.ModalHeader = _ModalHeader.default; var _ModalFooter = _interopRequireWildcard(require("./ModalFooter")); exports.ModalFooter = _ModalFooter.default; var _ModalSection = _interopRequireWildcard(require("./ModalSection")); exports.ModalSection = _ModalSection.default; var _ModalCloseButton = _interopRequireDefault(require("./ModalCloseButton")); var _consts = require("./consts"); var _keyMaps = _interopRequireDefault(require("../common/keyMaps")); var _defaultTheme = _interopRequireDefault(require("../defaultTheme")); var _ButtonPrimitive = require("../primitives/ButtonPrimitive"); var _mediaQuery = _interopRequireDefault(require("../utils/mediaQuery")); var _rtl = require("../utils/rtl"); var _transition = _interopRequireDefault(require("../utils/transition")); var _useRandomId = _interopRequireDefault(require("../hooks/useRandomId")); var _useMediaQuery = _interopRequireDefault(require("../hooks/useMediaQuery")); var _consts2 = _interopRequireDefault(require("../hooks/useFocusTrap/consts")); var _usePrevious = _interopRequireDefault(require("../hooks/usePrevious")); var _useLockScrolling = _interopRequireDefault(require("../hooks/useLockScrolling")); const getSizeToken = ({ size, theme }) => { const tokens = { [_consts.SIZES.EXTRASMALL]: "360px", [_consts.SIZES.SMALL]: theme.orbit.widthModalSmall, [_consts.SIZES.NORMAL]: theme.orbit.widthModalNormal, [_consts.SIZES.LARGE]: theme.orbit.widthModalLarge, [_consts.SIZES.EXTRALARGE]: theme.orbit.widthModalExtraLarge }; if (!size) return null; return tokens[size]; }; const ModalBody = _styledComponents.default.div.withConfig({ displayName: "Modal__ModalBody", componentId: "sc-1uug5dx-0" })(["", ""], ({ theme, isMobileFullPage }) => (0, _styledComponents.css)(["width:100%;height:100%;position:fixed;top:0;right:0;bottom:0;left:0;z-index:", ";box-sizing:border-box;outline:none;overflow-x:hidden;background-color:", ";font-family:", ";-webkit-overflow-scrolling:auto;", ";"], theme.orbit.zIndexModalOverlay, !isMobileFullPage && "rgba(0, 0, 0, 0.5)", theme.orbit.fontFamily, _mediaQuery.default.largeMobile((0, _styledComponents.css)(["overflow-y:auto;padding:", ";background-color:rgba(0,0,0,0.5);"], theme.orbit.spaceXXLarge)))); ModalBody.defaultProps = { theme: _defaultTheme.default }; const ModalWrapper = _styledComponents.default.div.withConfig({ displayName: "Modal__ModalWrapper", componentId: "sc-1uug5dx-1" })(["", ""], ({ isMobileFullPage, disableAnimation, loaded }) => (0, _styledComponents.css)(["box-sizing:border-box;min-height:100%;display:flex;align-items:flex-start;margin:0 auto;position:fixed;width:100%;border-top-left-radius:", ";border-top-right-radius:", ";", " ", ";"], !isMobileFullPage && "12px", !isMobileFullPage && "12px", disableAnimation ? (0, _styledComponents.css)(["top:", ";"], !isMobileFullPage && "32px") : (0, _styledComponents.css)(["transition:", ";top:", ";"], (0, _transition.default)(["top"], "normal", "ease-in-out"), loaded ? !isMobileFullPage && "32px" : "100%"), _mediaQuery.default.largeMobile((0, _styledComponents.css)(["position:relative;top:0;max-width:", ";align-items:center;"], getSizeToken)))); ModalWrapper.defaultProps = { theme: _defaultTheme.default }; const CloseContainer = _styledComponents.default.div.withConfig({ displayName: "Modal__CloseContainer", componentId: "sc-1uug5dx-2" })(["", ""], ({ theme, scrolled, fixedClose, isMobileFullPage, modalWidth, size }) => (0, _styledComponents.css)(["display:flex;", ";position:", ";top:", ";right:0;z-index:800;justify-content:flex-end;align-items:center;box-sizing:border-box;height:52px;width:100%;max-width:", ";box-shadow:", ";background-color:", ";border-top-left-radius:", ";border-top-right-radius:", ";transition:", ";pointer-events:none;", ";& + ", ":first-of-type{padding-top:52px;border-top:0;margin:0;}", "{pointer-events:auto;margin-", ":", ";& svg{transition:", ";color:", ";}&:hover svg{color:", ";}&:active svg{color:", ";}}"], fixedClose || scrolled ? (0, _styledComponents.css)(["position:fixed;"]) : (0, _styledComponents.css)(["position:absolute;"]), fixedClose || scrolled ? "fixed" : "absolute", !isMobileFullPage && (fixedClose || scrolled) ? "32px" : "0", modalWidth ? `${modalWidth}px` : getSizeToken({ size, theme }), scrolled && theme.orbit.boxShadowFixed, scrolled && theme.orbit.paletteWhite, !isMobileFullPage && "12px", !isMobileFullPage && "12px", (0, _transition.default)(["box-shadow", "background-color"], "fast", "ease-in-out"), _mediaQuery.default.largeMobile((0, _styledComponents.css)(["top:", ";right:", ";border-radius:0;"], (fixedClose || scrolled) && "0", (fixedClose || scrolled) && "auto")), _ModalSection.StyledModalSection, _ButtonPrimitive.StyledButtonPrimitive, _rtl.right, theme.orbit.spaceXXSmall, (0, _transition.default)(["color"], "fast", "ease-in-out"), theme.orbit.paletteInkNormal, theme.orbit.paletteInkLightHover, theme.orbit.paletteInkLightActive)); CloseContainer.defaultProps = { theme: _defaultTheme.default }; const ModalWrapperContent = _styledComponents.default.div.withConfig({ displayName: "Modal__ModalWrapperContent", componentId: "sc-1uug5dx-3" })(["", ""], ({ theme, isMobileFullPage, fixedFooter, footerHeight, size, fullyScrolled, scrolled, modalWidth, hasModalSection }) => (0, _styledComponents.css)(["position:absolute;box-sizing:border-box;border-top-left-radius:", ";border-top-right-radius:", ";background-color:", ";font-family:", ";width:100%;", ";bottom:", ";box-shadow:", ";overflow-y:auto;overflow-x:hidden;", ";", "{top:", ";opacity:", ";visibility:", ";transition:", ";}", "{margin-bottom:", ";}", ";"], !isMobileFullPage && "12px", !isMobileFullPage && "12px", theme.orbit.backgroundModal, theme.orbit.fontFamily, isMobileFullPage ? (0, _styledComponents.css)(["max-height:100%;top:0;"]) : (0, _styledComponents.css)(["max-height:calc( 100% - ", " - ", " );"], theme.orbit.spaceXLarge, `${fixedFooter && Boolean(footerHeight) ? footerHeight : 0}px`), `${(!isMobileFullPage ? parseInt(theme.orbit.spaceXLarge, 10) : 0) + (fixedFooter && Boolean(footerHeight) ? footerHeight : 0)}px`, theme.orbit.boxShadowOverlay, fixedFooter && footerHeight && (0, _styledComponents.css)(["", "{bottom:0;padding:", ";box-shadow:", ";position:fixed;transition:", ";}", ":last-of-type{padding-bottom:", ";margin-bottom:0;}"], _ModalFooter.StyledModalFooter, theme.orbit.spaceMedium, fullyScrolled ? `inset 0 1px 0 ${theme.orbit.paletteCloudNormal}, ${theme.orbit.boxShadowFixedReverse}` : `inset 0 0 0 transparent, ${theme.orbit.boxShadowFixedReverse}`, (0, _transition.default)(["box-shadow"], "fast", "ease-in-out"), _ModalSection.StyledModalSection, theme.orbit.spaceLarge), _ModalHeader.MobileHeader, !isMobileFullPage && scrolled && theme.orbit.spaceXLarge, scrolled && "1", scrolled && "visible", scrolled && (0, _styledComponents.css)(["", ",", ""], (0, _transition.default)(["top"], "normal", "ease-in-out"), (0, _transition.default)(["opacity", "visibility"], "fast", "ease-in-out")), _ModalHeader.StyledModalHeader, !hasModalSection && theme.orbit.spaceXLarge, _mediaQuery.default.largeMobile((0, _styledComponents.css)(["position:relative;bottom:auto;border-radius:", ";padding-bottom:0;height:auto;overflow:visible;max-height:100%;", ":last-of-type{padding-bottom:", ";margin-bottom:", ";&::after{content:none;}}", "{margin-bottom:", ";}", "{padding:", ";max-width:", ";position:", ";box-shadow:", ";}", "{top:", ";width:", ";}"], !isMobileFullPage && "9px", _ModalSection.StyledModalSection, theme.orbit.spaceXXLarge, fixedFooter ? `${footerHeight}px` : "0", _ModalHeader.StyledModalHeader, !hasModalSection && fixedFooter ? `${footerHeight}px` : "0", _ModalFooter.StyledModalFooter, fixedFooter ? `${theme.orbit.spaceXLarge} ${theme.orbit.spaceXLarge}!important` : theme.orbit.spaceXLarge, modalWidth ? `${modalWidth}px` : getSizeToken({ size, theme }), fixedFooter && fullyScrolled && "absolute", fullyScrolled && "none", _ModalHeader.MobileHeader, scrolled ? "0" : `-${theme.orbit.spaceXXLarge}`, `calc(${modalWidth}px - 48px - ${theme.orbit.spaceXXLarge})`)))); exports.ModalWrapperContent = ModalWrapperContent; ModalWrapperContent.defaultProps = { theme: _defaultTheme.default }; const OFFSET = 40; const Modal = /*#__PURE__*/React.forwardRef(({ size = _consts.SIZES.NORMAL, scrollingElementRef, children, onClose, autoFocus = true, fixedFooter = false, isMobileFullPage = false, preventOverlayClose = false, onScroll, hasCloseButton = true, mobileHeader = true, disableAnimation = false, dataTest, id, labelClose = "Close", lockScrolling = true }, ref) => { const [loaded, setLoaded] = React.useState(false); const [scrolled, setScrolled] = React.useState(false); const [fullyScrolled, setFullyScrolled] = React.useState(false); const [hasModalTitle, setHasModalTitle] = React.useState(false); const [hasModalSection, setHasModalSection] = React.useState(false); const [clickedModalBody, setClickedModalBody] = React.useState(false); const [fixedClose, setFixedClose] = React.useState(false); const [focusTriggered, setFocusTriggered] = React.useState(false); const [modalWidth, setModalWidth] = React.useState(0); const [footerHeight, setFooterHeight] = React.useState(0); const [firstFocusableEl, setFirstFocusableEl] = React.useState(null); const [lastFocusableEl, setLastFocusableEl] = React.useState(null); const modalContent = React.useRef(null); const modalBody = React.useRef(null); const modalTitleID = (0, _useRandomId.default)(); const { isLargeMobile } = (0, _useMediaQuery.default)(); const scrollingElement = React.useRef(null); const setScrollingElementRefs = React.useCallback(node => { scrollingElement.current = node; if (scrollingElementRef) { if (typeof scrollingElementRef === "function") { scrollingElementRef(node); } else { // @ts-expect-error TODO // eslint-disable-next-line no-param-reassign scrollingElementRef.current = node; } } }, [scrollingElementRef]); (0, _useLockScrolling.default)(scrollingElement, lockScrolling, [isLargeMobile]); const modalContentRef = React.useCallback(node => { modalContent.current = node; if (!isLargeMobile) setScrollingElementRefs(node); }, [isLargeMobile, setScrollingElementRefs]); const modalBodyRef = React.useCallback(node => { modalBody.current = node; if (isLargeMobile) setScrollingElementRefs(node); }, [isLargeMobile, setScrollingElementRefs]); const prevChildren = (0, _usePrevious.default)(children); const setDimensions = () => { const content = modalContent.current; if (!content) return; // added in 4.0.3, interpolation of styled component return static className const footerEl = content.querySelector(`${_ModalFooter.StyledModalFooter}`); const contentDimensions = content.getBoundingClientRect(); setModalWidth(contentDimensions.width); if (footerEl?.clientHeight) { setFooterHeight(footerEl.clientHeight); } }; const setFirstFocus = () => { if (modalBody.current && autoFocus) { modalBody.current.focus(); } }; const decideFixedFooter = () => { if (!modalContent.current || !modalBody.current) return; // if the content height is smaller than window height, we need to explicitly set fullyScrolled to true const content = modalContent.current; const body = modalBody.current; const contentHeight = content.scrollHeight > content.offsetHeight + OFFSET ? content.offsetHeight : content.scrollHeight; // when scrollHeight + topPadding - scrollingElementHeight is smaller than or equal to window height setFullyScrolled(contentHeight + OFFSET - body.scrollTop <= window.innerHeight); }; const manageFocus = React.useCallback(() => { if (!focusTriggered || !modalContent.current) return; const focusableElements = modalContent.current.querySelectorAll(_consts2.default); if (focusableElements.length > 0) { setFirstFocusableEl(focusableElements[0]); setLastFocusableEl(focusableElements[focusableElements.length - 1]); } }, [focusTriggered]); const keyboardHandler = event => { if (event.keyCode !== _keyMaps.default.TAB) return; if (!focusTriggered) { setFocusTriggered(true); manageFocus(); } if (event.shiftKey && (document.activeElement === firstFocusableEl || document.activeElement === modalBody.current)) { event.preventDefault(); lastFocusableEl?.focus(); } else if (!event.shiftKey && document.activeElement === lastFocusableEl) { event.preventDefault(); firstFocusableEl?.focus(); } }; const handleKeyDown = event => { if (onClose && event.key === "Escape") { event.stopPropagation(); onClose(event); } keyboardHandler(event); }; const handleClickOutside = event => { const clickedOutside = onClose && preventOverlayClose === false && !clickedModalBody && modalContent.current && event.target instanceof Element && !modalContent.current.contains(event.target) && /ModalBody|ModalWrapper/.test(event.target.className); if (clickedOutside && onClose) onClose(event); setClickedModalBody(false); }; const setScrollStates = (target, fullScrollOffset, fixCloseOffset, scrollBegin, mobile) => { const content = modalContent.current; if (!content) return; const { height: contentHeight } = content.getBoundingClientRect(); /* Only for desktop, we need to check if the scrollHeight of content is bigger than actual height if so, we need to you use the contentHeight + padding as bottom scroll point, otherwise actual scrollHeight of the target is enough. */ const scrollHeight = !mobile && target.scrollHeight > contentHeight + 80 ? contentHeight + 80 : target.scrollHeight; // @ts-expect-error TODO setScrolled(target.scrollTop >= scrollBegin + (!mobile ? target.scrollTop : 0)); setFixedClose(target.scrollTop >= fixCloseOffset); // set fullyScrolled state sooner than the exact end of the scroll (with fullScrollOffset value) setFullyScrolled(fixedFooter && target.scrollTop >= scrollHeight - target.clientHeight - fullScrollOffset); }; const getScrollTopPoint = mobile => { const content = modalContent.current; if (!content) return null; const headingEl = content.querySelector(`${_ModalHeader.ModalHeading}`); if (headingEl) { const { top } = headingEl.getBoundingClientRect(); return top; } if (mobile) return OFFSET; const { top } = content.getBoundingClientRect(); return top; }; const handleScroll = event => { if (event.target instanceof HTMLDivElement && event.target === modalBody.current) { if (onScroll) onScroll(event); setScrollStates(event.target, OFFSET, OFFSET, getScrollTopPoint()); } }; const handleMobileScroll = event => { if (onScroll) onScroll(event); if (event.target instanceof HTMLDivElement && event.target === modalContent.current) { setScrollStates(event.target, 10, 1, getScrollTopPoint(true), true); } }; const handleMouseDown = () => { /* This is due to issue where it was possible to close Modal, even though click started (onMouseDown) in ModalWrapper. */ setClickedModalBody(true); }; const callContextFunctions = React.useCallback(() => { setDimensions(); decideFixedFooter(); manageFocus(); }, [manageFocus]); const getScrollPosition = () => { if (scrollingElement.current) { return scrollingElement.current.scrollTop; } return null; }; const setScrollPosition = value => { if (scrollingElement.current) { scrollingElement.current.scrollTop = value; } }; React.useImperativeHandle(ref, () => ({ getScrollPosition, setScrollPosition, modalBody, modalContent })); // eslint-disable-next-line consistent-return React.useEffect(() => { if (disableAnimation) { decideFixedFooter(); setDimensions(); setFirstFocus(); } else { const timer = setTimeout(() => { setLoaded(true); decideFixedFooter(); setDimensions(); setFirstFocus(); }, 15); return () => { clearTimeout(timer); }; } // the Modal can only transition in on mount // eslint-disable-next-line react-hooks/exhaustive-deps }, []); React.useEffect(() => { const handleResize = () => { setDimensions(); decideFixedFooter(); }; window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize", handleResize); }; }, []); React.useEffect(() => { if (children !== prevChildren) { decideFixedFooter(); setDimensions(); } }, [children, prevChildren]); const hasCloseContainer = mobileHeader && (hasModalTitle || onClose && hasCloseButton); const value = React.useMemo(() => ({ setHasModalTitle, setHasModalSection: () => setHasModalSection(true), removeHasModalSection: () => setHasModalSection(false), callContextFunctions, setFooterHeight, hasModalSection, hasMobileHeader: mobileHeader, isMobileFullPage, closable: Boolean(onClose), isInsideModal: true, titleID: modalTitleID }), [callContextFunctions, hasModalSection, isMobileFullPage, mobileHeader, onClose, modalTitleID]); return /*#__PURE__*/React.createElement(ModalBody, { tabIndex: 0, onKeyDown: handleKeyDown, onScroll: handleScroll, onClick: handleClickOutside, "data-test": dataTest, id: id, ref: modalBodyRef, role: "dialog", isMobileFullPage: isMobileFullPage, autoFocus: autoFocus, "aria-modal": "true", "aria-labelledby": hasModalTitle ? modalTitleID : "" }, /*#__PURE__*/React.createElement(ModalWrapper, { size: size, loaded: loaded, fixedFooter: fixedFooter, isMobileFullPage: isMobileFullPage, disableAnimation: disableAnimation }, /*#__PURE__*/React.createElement(ModalWrapperContent, { size: size, fixedFooter: fixedFooter, onScroll: handleMobileScroll, scrolled: scrolled, ref: modalContentRef, fixedClose: fixedClose, fullyScrolled: fullyScrolled, modalWidth: modalWidth, footerHeight: footerHeight, hasModalSection: hasModalSection, isMobileFullPage: isMobileFullPage, onMouseDown: handleMouseDown }, hasCloseContainer && /*#__PURE__*/React.createElement(CloseContainer, { "data-test": "CloseContainer", modalWidth: modalWidth, size: size, scrolled: scrolled, fixedClose: fixedClose, isMobileFullPage: isMobileFullPage }, onClose && hasCloseButton && /*#__PURE__*/React.createElement(_ModalCloseButton.default, { onClick: onClose, dataTest: _consts.CLOSE_BUTTON_DATA_TEST, title: labelClose })), /*#__PURE__*/React.createElement(_ModalContext.ModalContext.Provider, { value: value }, children)))); }); Modal.displayName = "Modal"; var _default = Modal; exports.default = _default;