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.

498 lines (451 loc) 18.4 kB
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } import * as React from "react"; import styled, { css, withTheme } from "styled-components"; import defaultTheme from "../defaultTheme"; import ButtonLink, { StyledButtonLink } from "../ButtonLink"; import Close from "../icons/Close"; import { SIZES, CLOSE_BUTTON_DATA_TEST, FOCUSABLE_ELEMENT_SELECTORS } from "./consts"; import KEY_CODE_MAP from "../common/keyMaps"; import media, { getBreakpointWidth } from "../utils/mediaQuery"; import { StyledModalFooter } from "./ModalFooter"; import { MobileHeader, StyledModalHeader } from "./ModalHeader"; import { StyledModalSection } from "./ModalSection"; import { StyledHeading } from "../Heading"; import { right } from "../utils/rtl"; import transition from "../utils/transition"; import { ModalContext } from "./ModalContext"; import { QUERIES } from "../utils/mediaQuery/consts"; import randomID from "../utils/randomID"; import { DictionaryContext } from "../Dictionary"; import { pureTranslate } from "../Translate"; const getSizeToken = () => ({ size, theme }) => { const tokens = { // TODO: create tokens widthModalSmall,... [SIZES.SMALL]: theme.orbit.widthModalSmall, [SIZES.NORMAL]: theme.orbit.widthModalNormal, [SIZES.LARGE]: theme.orbit.widthModalLarge }; return tokens[size]; }; // media query only for IE 10+, not Edge const onlyIE = (style, breakpoint = "all") => css(["@media ", " and (-ms-high-contrast:none),(-ms-high-contrast:active){", ";}"], breakpoint, style); const ModalBody = styled.div.withConfig({ displayName: "Modal__ModalBody", componentId: "sc-1f0srsl-0" })(["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 }) => theme.orbit.zIndexModalOverlay, ({ theme }) => theme.orbit.fontfamily, media.largeMobile(css(["overflow-y:auto;padding:", ";"], ({ theme }) => theme.orbit.spaceXXLarge)), onlyIE(css(["position:-ms-page;"]))); ModalBody.defaultProps = { theme: defaultTheme }; const ModalWrapper = styled.div.withConfig({ displayName: "Modal__ModalWrapper", componentId: "sc-1f0srsl-1" })(["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:", ";transition:", ";top:", ";", ";", ";"], ({ isMobileFullPage }) => !isMobileFullPage && "9px", ({ isMobileFullPage }) => !isMobileFullPage && "9px", transition(["top"], "normal", "ease-in-out"), ({ loaded, isMobileFullPage }) => loaded ? !isMobileFullPage && "32px" : "100%", onlyIE(css(["height:1px;"])), media.largeMobile(css(["position:relative;top:0;max-width:", ";align-items:center;"], getSizeToken))); ModalWrapper.defaultProps = { theme: defaultTheme }; const CloseContainer = styled.div.withConfig({ displayName: "Modal__CloseContainer", componentId: "sc-1f0srsl-2" })(["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:", ";", ";& + ", ":first-of-type{padding-top:52px;border-top:0;margin:0;}", "{margin-", ":", ";& svg{transition:", ";color:", ";}&:hover svg{color:", ";}&:active svg{color:", ";}}"], ({ scrolled, fixedClose, theme }) => fixedClose || scrolled ? css(["position:fixed;", ";"], onlyIE(css(["position:-ms-page;"]), `(max-width:${+getBreakpointWidth(QUERIES.LARGEMOBILE, theme, true) - 1}px)`)) : css(["position:absolute;"]), ({ scrolled, fixedClose }) => fixedClose || scrolled ? "fixed" : "absolute", ({ scrolled, fixedClose, isMobileFullPage }) => !isMobileFullPage && (fixedClose || scrolled) ? "32px" : "0", ({ modalWidth }) => modalWidth ? `${modalWidth}px` : getSizeToken, ({ scrolled }) => scrolled && `0 2px 4px 0 rgba(23, 27, 30, 0.1)`, ({ theme, scrolled }) => scrolled && theme.orbit.paletteWhite, ({ isMobileFullPage }) => !isMobileFullPage && "9px", ({ isMobileFullPage }) => !isMobileFullPage && "9px", transition(["box-shadow", "background-color"], "fast", "ease-in-out"), media.largeMobile(css(["top:", ";right:", ";"], ({ scrolled, fixedClose }) => (fixedClose || scrolled) && "0", ({ scrolled, fixedClose }) => (fixedClose || scrolled) && "auto")), StyledModalSection, StyledButtonLink, right, ({ theme }) => theme.orbit.spaceXXSmall, transition(["color"], "fast", "ease-in-out"), ({ theme }) => theme.orbit.paletteInkLight, ({ theme }) => theme.orbit.paletteInkLightHover, ({ theme }) => theme.orbit.paletteInkLightActive); CloseContainer.defaultProps = { theme: defaultTheme }; const ModalWrapperContent = styled.div.withConfig({ displayName: "Modal__ModalWrapperContent", componentId: "sc-1f0srsl-3" })(["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 }) => !isMobileFullPage && "9px", ({ isMobileFullPage }) => !isMobileFullPage && "9px", ({ theme }) => theme.orbit.backgroundModal, ({ theme }) => theme.orbit.fontFamily, ({ theme, fixedFooter, footerHeight, isMobileFullPage }) => isMobileFullPage ? css(["max-height:100%;top:0;"]) : css(["max-height:calc( 100% - ", " - ", " );"], theme.orbit.spaceXLarge, `${fixedFooter && !!footerHeight ? footerHeight : 0}px`), ({ fixedFooter, footerHeight, isMobileFullPage, theme }) => `${(!isMobileFullPage ? parseInt(theme.orbit.spaceXLarge, 10) : 0) + (fixedFooter && !!footerHeight ? footerHeight : 0)}px`, ({ theme }) => theme.orbit.boxShadowModal, ({ fixedFooter, theme, footerHeight, fullyScrolled }) => fixedFooter && footerHeight && css(["", "{bottom:0;padding:", ";box-shadow:", ";position:fixed;transition:", ";}", ":last-of-type{padding-bottom:", ";margin-bottom:0;}"], StyledModalFooter, theme.orbit.spaceMedium, fullyScrolled ? `inset 0 1px 0 ${theme.orbit.paletteCloudNormal}, 0 -2px 4px 0 rgba(23, 27, 30, 0)` : `inset 0 0 0 transparent, 0 -2px 4px 0 rgba(23, 27, 30, 0.1)`, transition(["box-shadow"], "fast", "ease-in-out"), StyledModalSection, theme.orbit.spaceLarge), MobileHeader, ({ scrolled, theme, isMobileFullPage }) => !isMobileFullPage && scrolled && theme.orbit.spaceXLarge, ({ scrolled }) => scrolled && "1", ({ scrolled }) => scrolled && "visible", ({ scrolled, theme }) => scrolled && `top ${theme.orbit.durationNormal} ease-in-out, opacity ${theme.orbit.durationFast} ease-in-out, visibility ${theme.orbit.durationFast} ease-in-out ${theme.orbit.durationFast}`, ({ scrolled }) => scrolled && onlyIE(css(["", "{position:-ms-page;}"], MobileHeader)), StyledModalHeader, ({ hasModalSection, theme }) => !hasModalSection && theme.orbit.spaceXLarge, media.largeMobile(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 }) => !isMobileFullPage && "9px", StyledModalSection, ({ theme }) => theme.orbit.spaceXXLarge, ({ fixedFooter, footerHeight }) => fixedFooter ? `${footerHeight}px` : "0", StyledModalHeader, ({ hasModalSection, fixedFooter, footerHeight }) => !hasModalSection && fixedFooter ? `${footerHeight}px` : "0", StyledModalFooter, ({ theme, fixedFooter }) => fixedFooter ? `${theme.orbit.spaceXLarge} ${theme.orbit.spaceXXLarge}!important` : theme.orbit.spaceXXLarge, ({ modalWidth }) => modalWidth ? `${modalWidth}px` : getSizeToken, ({ fullyScrolled, fixedFooter }) => fixedFooter && fullyScrolled && "absolute", ({ fullyScrolled }) => fullyScrolled && "none", MobileHeader, ({ scrolled, theme }) => scrolled ? "0" : `-${theme.orbit.spaceXXLarge}`, ({ modalWidth, theme }) => `calc(${modalWidth}px - 48px - ${theme.orbit.spaceXXLarge})`)), onlyIE(css(["", "{position:", ";}"], StyledModalFooter, ({ fixedFooter }) => fixedFooter && "-ms-page")), ({ theme }) => onlyIE(css(["", "{position:", ";box-shadow:", ";}", ";"], StyledModalFooter, ({ fullyScrolled, fixedFooter }) => fullyScrolled && fixedFooter && "static" || fixedFooter && "fixed", ({ fixedFooter }) => !fixedFooter && `inset 0 0 0 1px ${theme.orbit.paletteWhite}`, ({ fullyScrolled, fixedFooter }) => fullyScrolled && fixedFooter && css(["", ":last-of-type{margin-bottom:0;}", "{margin-bottom:", ";}"], StyledModalSection, StyledModalHeader, ({ hasModalSection }) => !hasModalSection && "0")), getBreakpointWidth(QUERIES.LARGEMOBILE, theme))); ModalWrapperContent.defaultProps = { theme: defaultTheme }; const ModalCloseButton = ({ onClick, dataTest }) => { const dictionary = React.useContext(DictionaryContext); return React.createElement(ButtonLink, { onClick: onClick, size: "normal", icon: React.createElement(Close, null), transparent: true, dataTest: dataTest, title: pureTranslate(dictionary, "button_close") }); }; export class PureModal extends React.PureComponent { constructor(...args) { super(...args); _defineProperty(this, "state", { scrolled: false, loaded: false, fixedClose: false, fullyScrolled: false, modalWidth: 0, footerHeight: 0, hasModalSection: false }); _defineProperty(this, "modalContent", React.createRef()); _defineProperty(this, "modalBody", React.createRef()); _defineProperty(this, "offset", 40); _defineProperty(this, "focusTriggered", false); _defineProperty(this, "modalID", randomID("modalID")); _defineProperty(this, "setScrollPosition", value => { var _window; const { modalContent, modalBody } = this; if (((_window = window) === null || _window === void 0 ? void 0 : _window.innerWidth) >= getBreakpointWidth(QUERIES.LARGEMOBILE, this.props.theme, true)) { if (modalBody.current) { modalBody.current.scrollTop = value; } } else if (modalContent.current) { modalContent.current.scrollTop = value; } }); _defineProperty(this, "setDimensions", () => { const content = this.modalContent.current; if (content) { // added in 4.0.3, interpolation of styled component return static className const footerEl = content.querySelector(`${StyledModalFooter}`); const headingEl = content.querySelector(`${StyledHeading}`); this.offset = (headingEl === null || headingEl === void 0 ? void 0 : headingEl.clientHeight) + (headingEl === null || headingEl === void 0 ? void 0 : headingEl.offsetTop); const contentDimensions = content.getBoundingClientRect(); const modalWidth = contentDimensions.width; const footerHeight = footerEl === null || footerEl === void 0 ? void 0 : footerEl.clientHeight; this.setState({ modalWidth, footerHeight }); } }); _defineProperty(this, "setHasModalSection", () => { if (!this.state.hasModalSection) { this.setState({ hasModalSection: true }); } }); _defineProperty(this, "removeHasModalSection", () => { if (this.state.hasModalSection) this.setState({ hasModalSection: false }); }); _defineProperty(this, "decideFixedFooter", () => { // if the content height is smaller than window height, we need to explicitly set fullyScrolled to true const content = this.modalContent.current; const body = this.modalBody.current; // when scrollHeight + topPadding - scrollingElementHeight is smaller or even than window height const fullyScrolled = (content === null || content === void 0 ? void 0 : content.scrollHeight) + 40 - (body === null || body === void 0 ? void 0 : body.scrollTop) <= window.innerHeight; this.setState({ fullyScrolled }); }); _defineProperty(this, "handleResize", () => { this.setDimensions(); this.decideFixedFooter(); }); _defineProperty(this, "resolveAndSetStates", (target, fullScrollOffset, fixCloseOffset) => { this.setState({ scrolled: target.scrollTop >= this.offset, fixedClose: target.scrollTop >= fixCloseOffset, fullyScrolled: this.props.fixedFooter && // set fullyScrolled state sooner than the exact end of the scroll (with fullScrollOffset value) target.scrollTop >= target.scrollHeight - target.clientHeight - fullScrollOffset }); }); _defineProperty(this, "handleMobileScroll", ev => { if (ev.target instanceof HTMLDivElement && ev.target === this.modalContent.current) { this.resolveAndSetStates(ev.target, 10, 1); } }); _defineProperty(this, "handleScroll", ev => { if (ev.target instanceof HTMLDivElement && ev.target === this.modalBody.current) { this.resolveAndSetStates(ev.target, 40, 40); } }); _defineProperty(this, "handleKeyDown", ev => { const { onClose } = this.props; if (onClose && ev.key === "Escape") { ev.stopPropagation(); onClose(ev); } this.keyboardHandler(ev); }); _defineProperty(this, "handleClickOutside", ev => { const { onClose } = this.props; if (onClose && this.modalContent.current && ev.target instanceof Element && !this.modalContent.current.contains(ev.target) && /ModalBody|ModalWrapper/.test(ev.target.className)) { // If is clicked outside of modal onClose(ev); } }); _defineProperty(this, "keyboardHandler", e => { if (e.keyCode === KEY_CODE_MAP.TAB) { // Rotate Focus if (!this.focusTriggered) { this.focusTriggered = true; this.manageFocus(); } if (e.shiftKey && (document.activeElement === this.firstFocusableEl || document.activeElement === this.modalBody.current)) { e.preventDefault(); this.lastFocusableEl.focus(); } else if (!e.shiftKey && document.activeElement === this.lastFocusableEl) { e.preventDefault(); this.firstFocusableEl.focus(); } } }); _defineProperty(this, "manageFocus", () => { if (this.focusTriggered) { const focusableElements = this.modalContent.current.querySelectorAll(FOCUSABLE_ELEMENT_SELECTORS); if (focusableElements.length > 0) { const firstFocusableEl = focusableElements[0]; const lastFocusableEl = focusableElements[focusableElements.length - 1]; this.firstFocusableEl = firstFocusableEl; this.lastFocusableEl = lastFocusableEl; } } }); _defineProperty(this, "firstFocusableEl", void 0); _defineProperty(this, "lastFocusableEl", void 0); _defineProperty(this, "timeout", void 0); } componentDidMount() { this.timeout = setTimeout(() => { this.setState({ loaded: true }); this.decideFixedFooter(); this.setDimensions(); this.setFirstFocus(); }, 15); window.addEventListener("resize", this.handleResize); } componentDidUpdate(prevProps) { if (this.props.children !== prevProps.children) { this.decideFixedFooter(); this.setDimensions(); } } componentWillUnmount() { window.removeEventListener("resize", this.handleResize); if (this.timeout) { clearTimeout(this.timeout); } } setFirstFocus() { if (this.modalBody.current) this.modalBody.current.focus(); } render() { const { onClose, children, size = SIZES.NORMAL, fixedFooter = false, dataTest, isMobileFullPage = false } = this.props; const { scrolled, loaded, fixedClose, fullyScrolled, modalWidth, footerHeight, hasModalSection } = this.state; return React.createElement(ModalBody, { tabIndex: "0", onKeyDown: this.handleKeyDown, onScroll: this.handleScroll, onClick: this.handleClickOutside, "data-test": dataTest, ref: this.modalBody, role: "dialog", "aria-modal": "true", "aria-labelledby": this.modalID }, React.createElement(ModalWrapper, { size: size, loaded: loaded, onScroll: this.handleMobileScroll, fixedFooter: fixedFooter, id: this.modalID, isMobileFullPage: isMobileFullPage }, React.createElement(ModalWrapperContent, { size: size, fixedFooter: fixedFooter, scrolled: scrolled, ref: this.modalContent, fixedClose: fixedClose, fullyScrolled: fullyScrolled, modalWidth: modalWidth, footerHeight: footerHeight, hasModalSection: hasModalSection, isMobileFullPage: isMobileFullPage }, React.createElement(CloseContainer, { modalWidth: modalWidth, size: size, scrolled: scrolled, fixedClose: fixedClose, isMobileFullPage: isMobileFullPage }, onClose && React.createElement(ModalCloseButton, { onClick: onClose, dataTest: CLOSE_BUTTON_DATA_TEST })), React.createElement(ModalContext.Provider, { value: { setDimensions: this.setDimensions, decideFixedFooter: this.decideFixedFooter, setHasModalSection: this.setHasModalSection, removeHasModalSection: this.removeHasModalSection, manageFocus: this.manageFocus, hasModalSection, isMobileFullPage } }, children)))); } } _defineProperty(PureModal, "defaultProps", { theme: defaultTheme }); const ThemedModal = withTheme(PureModal); ThemedModal.displayName = "Modal"; export default ThemedModal; export { default as ModalHeader } from "./ModalHeader"; export { default as ModalSection } from "./ModalSection"; export { default as ModalFooter } from "./ModalFooter";