@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.
527 lines (474 loc) • 19.1 kB
JavaScript
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 useTranslate from "../hooks/useTranslate";
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,
theme
}) => scrolled && theme.orbit.boxShadowFixed, ({
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.boxShadowOverlay, ({
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}, ${theme.orbit.boxShadowFixedReverse}` : `inset 0 0 0 transparent, ${theme.orbit.boxShadowFixedReverse}`, 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 translate = useTranslate();
return React.createElement(ButtonLink, {
onClick: onClick,
size: "normal",
icon: React.createElement(Close, null),
transparent: true,
dataTest: dataTest,
type: "secondary",
title: translate("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, "firstFocusableEl", void 0);
_defineProperty(this, "lastFocusableEl", void 0);
_defineProperty(this, "timeout", void 0);
_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, scrollBegin, mobile) => {
this.setState({
scrolled: target.scrollTop >= scrollBegin + (!mobile ? target.scrollTop : 0),
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, "getScrollTopPoint", mobile => {
const content = this.modalContent.current;
if (content) {
const headingEl = content.querySelector(`${StyledHeading}`);
if (headingEl) {
const {
top
} = headingEl.getBoundingClientRect();
return top;
}
if (mobile) {
return 40;
}
const {
top
} = content.getBoundingClientRect();
return top;
}
return null;
});
_defineProperty(this, "handleMobileScroll", ev => {
if (ev.target instanceof HTMLDivElement && ev.target === this.modalContent.current) {
this.resolveAndSetStates(ev.target, 10, 1, this.getScrollTopPoint(true), true);
}
});
_defineProperty(this, "handleScroll", ev => {
if (ev.target instanceof HTMLDivElement && ev.target === this.modalBody.current) {
this.resolveAndSetStates(ev.target, 40, 40, this.getScrollTopPoint());
}
});
_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,
preventOverlayClose = false
} = this.props;
if (onClose && preventOverlayClose === false && 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;
}
}
});
}
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,
isInsideModal: true
}
}, 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";