@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
JavaScript
;
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;