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.

462 lines (388 loc) 20.7 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default; Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "ModalHeader", { enumerable: true, get: function () { return _ModalHeader.default; } }); Object.defineProperty(exports, "ModalFooter", { enumerable: true, get: function () { return _ModalFooter.default; } }); Object.defineProperty(exports, "ModalSection", { enumerable: true, get: function () { return _ModalSection.default; } }); exports.default = void 0; var React = _interopRequireWildcard(require("react")); var _styledComponents = _interopRequireWildcard(require("styled-components")); var _ModalContext = require("./ModalContext"); var _ModalHeader = _interopRequireWildcard(require("./ModalHeader")); var _ModalFooter = _interopRequireWildcard(require("./ModalFooter")); var _ModalSection = _interopRequireWildcard(require("./ModalSection")); 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 = _interopRequireWildcard(require("../utils/mediaQuery")); var _consts2 = require("../utils/mediaQuery/consts"); var _rtl = require("../utils/rtl"); var _transition = _interopRequireDefault(require("../utils/transition")); var _useRandomId = _interopRequireDefault(require("../hooks/useRandomId")); var _onlyIE = _interopRequireDefault(require("../utils/onlyIE")); var _useMediaQuery = _interopRequireDefault(require("../hooks/useMediaQuery")); var _consts3 = _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 }; return tokens[size]; }; const ModalBody = _styledComponents.default.div.withConfig({ displayName: "Modal__ModalBody", componentId: "sc-15ie1vv-0" })(["", ""], ({ theme }) => (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:rgba(0,0,0,0.5);font-family:", ";-webkit-overflow-scrolling:auto;", ";", ";"], theme.orbit.zIndexModalOverlay, theme.orbit.fontFamily, _mediaQuery.default.largeMobile((0, _styledComponents.css)(["overflow-y:auto;padding:", ";"], theme.orbit.spaceXXLarge)), (0, _onlyIE.default)((0, _styledComponents.css)(["position:-ms-page;"])))); // $FlowFixMe: https://github.com/flow-typed/flow-typed/issues/3653#issuecomment-568539198 ModalBody.defaultProps = { theme: _defaultTheme.default }; const ModalWrapper = _styledComponents.default.div.withConfig({ displayName: "Modal__ModalWrapper", componentId: "sc-15ie1vv-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%"), (0, _onlyIE.default)((0, _styledComponents.css)(["height:1px;"])), _mediaQuery.default.largeMobile((0, _styledComponents.css)(["position:relative;top:0;max-width:", ";align-items:center;"], getSizeToken)))); // $FlowFixMe: https://github.com/flow-typed/flow-typed/issues/3653#issuecomment-568539198 ModalWrapper.defaultProps = { theme: _defaultTheme.default }; const CloseContainer = _styledComponents.default.div.withConfig({ displayName: "Modal__CloseContainer", componentId: "sc-15ie1vv-2" })(["", ""], ({ theme, scrolled, fixedClose, isMobileFullPage, modalWidth }) => (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, _onlyIE.default)((0, _styledComponents.css)(["position:-ms-page;"]), `(max-width:${+(0, _mediaQuery.getBreakpointWidth)(_consts2.QUERIES.LARGEMOBILE, theme, true) - 1}px)`)) : (0, _styledComponents.css)(["position:absolute;"]), fixedClose || scrolled ? "fixed" : "absolute", !isMobileFullPage && (fixedClose || scrolled) ? "32px" : "0", modalWidth ? `${modalWidth}px` : getSizeToken, 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.paletteInkLight, theme.orbit.paletteInkLightHover, theme.orbit.paletteInkLightActive)); // $FlowFixMe: https://github.com/flow-typed/flow-typed/issues/3653#issuecomment-568539198 CloseContainer.defaultProps = { theme: _defaultTheme.default }; const ModalWrapperContent = _styledComponents.default.div.withConfig({ displayName: "Modal__ModalWrapperContent", componentId: "sc-15ie1vv-3" })(["", ""], ({ theme, isMobileFullPage, fixedFooter, footerHeight, 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")), scrolled && (0, _onlyIE.default)((0, _styledComponents.css)(["", "{position:-ms-page;}"], _ModalHeader.MobileHeader)), _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, fixedFooter && fullyScrolled && "absolute", fullyScrolled && "none", _ModalHeader.MobileHeader, scrolled ? "0" : `-${theme.orbit.spaceXXLarge}`, `calc(${modalWidth}px - 48px - ${theme.orbit.spaceXXLarge})`)), (0, _onlyIE.default)((0, _styledComponents.css)(["", "{position:", ";}"], _ModalFooter.StyledModalFooter, fixedFooter && "-ms-page")))); // $FlowFixMe: https://github.com/flow-typed/flow-typed/issues/3653#issuecomment-568539198 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, hasCloseButton = true, mobileHeader = true, disableAnimation = false, dataTest, 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 { // 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 // $FlowFixMe const footerEl = content.querySelector(`${_ModalFooter.StyledModalFooter}`); const contentDimensions = content.getBoundingClientRect(); setModalWidth(contentDimensions.width); if (footerEl !== null && footerEl !== void 0 && 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 = () => { if (!focusTriggered || !modalContent.current) return; const focusableElements = modalContent.current.querySelectorAll(_consts3.default); if (focusableElements.length > 0) { setFirstFocusableEl(focusableElements[0]); setLastFocusableEl(focusableElements[focusableElements.length - 1]); } }; 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 === null || lastFocusableEl === void 0 ? void 0 : lastFocusableEl.focus(); } else if (!event.shiftKey && document.activeElement === lastFocusableEl) { event.preventDefault(); firstFocusableEl === null || firstFocusableEl === void 0 ? void 0 : 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; // $FlowFixMe 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; // $FlowFixMe 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) { setScrollStates(event.target, OFFSET, OFFSET, getScrollTopPoint()); } }; const handleMobileScroll = 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 = () => { setDimensions(); decideFixedFooter(); 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); return /*#__PURE__*/React.createElement(ModalBody, { tabIndex: "0", onKeyDown: handleKeyDown, onScroll: handleScroll, onClick: handleClickOutside, "data-test": dataTest, ref: modalBodyRef, role: "dialog", autoFocus: autoFocus, "aria-modal": "true", "aria-labelledby": hasModalTitle ? modalTitleID : null }, /*#__PURE__*/React.createElement(ModalWrapper, { size: size, loaded: loaded, onScroll: handleMobileScroll, fixedFooter: fixedFooter, isMobileFullPage: isMobileFullPage, disableAnimation: disableAnimation }, /*#__PURE__*/React.createElement(ModalWrapperContent, { size: size, fixedFooter: fixedFooter, 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 })), /*#__PURE__*/React.createElement(_ModalContext.ModalContext.Provider, { value: { setHasModalTitle, setHasModalSection: () => setHasModalSection(true), removeHasModalSection: () => setHasModalSection(false), callContextFunctions, setFooterHeight, hasModalSection, hasMobileHeader: mobileHeader, isMobileFullPage, closable: Boolean(onClose), isInsideModal: true, titleID: modalTitleID } }, children)))); }); Modal.displayName = "Modal"; var _default = Modal; exports.default = _default;