@kiwicom/orbit-components
Version:
<div align="center"> <a href="https://orbit.kiwi" target="_blank"> <img alt="orbit-components" src="https://orbit.kiwi/wp-content/uploads/2018/08/orbit-components.png" srcset="https://orbit.kiwi/wp-content/uploads/2018/08/orbit-components@2x.png 2x"
315 lines (278 loc) • 8.17 kB
JavaScript
// @flow
import * as React from "react";
import styled from "styled-components";
import defaultTokens from "../defaultTokens";
import ButtonLink, { StyledButtonLink } from "../ButtonLink";
import Close from "../icons/Close";
import SIZES from "./consts";
import media from "../utils/media";
import { StyledModalFooter } from "./ModalFooter";
import { MobileHeader } from "./ModalHeader";
import ClickOutside from "../ClickOutside";
import { StyledModalSection } from "./ModalSection";
import { StyledHeading } from "../Heading";
import type { Props, State, ContentType, CloseElementType } from "./index";
const getToken = (theme, type, name) => {
const tokens = {
// TODO: create tokens widthModalSmall,...
modalWidth: {
[SIZES.SMALL]: "540px",
[SIZES.NORMAL]: "740px",
[SIZES.LARGE]: "1280px",
},
};
return tokens[name][type];
};
const ModalBody = styled.div`
width: 100%;
height: 100%;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: ${({ theme }) => theme.orbit.zIndexModalOverlay};
box-sizing: border-box;
outline: none;
overflow-x: hidden;
background-color: rgba(0, 0, 0, 0.5);
font-family: ${({ theme }) => theme.orbit.fontfamily};
${media.desktop`
overflow-y: auto;
`};
`;
ModalBody.defaultProps = {
theme: defaultTokens,
};
const ModalWrapper = styled.div`
box-sizing: border-box;
min-height: 100%;
display: flex;
align-items: flex-start;
margin: 0 auto;
position: fixed;
width: 100%;
border-top-left-radius: ${({ theme }) => theme.orbit.borderRadiusNormal};
border-top-right-radius: ${({ theme }) => theme.orbit.borderRadiusNormal};
transition: top ${({ theme }) => theme.orbit.durationNormal} ease-in-out;
top: ${({ loaded }) => (loaded ? "32px" : "100%")};
${media.desktop`
padding: ${({ theme }) => theme.orbit.spaceXXLarge};
position: relative;
top: 0;
max-width: ${({ theme, size }) => getToken(theme, size, "modalWidth")};
align-items: center;
`};
`;
ModalWrapper.defaultProps = {
theme: defaultTokens,
};
const CloseContainer = styled.div`
display: flex;
position: fixed;
top: 32px;
right: 0;
z-index: 1;
justify-content: flex-end;
align-items: center;
box-sizing: border-box;
// TODO create tokens
height: 52px;
width: 100%;
transition: all ${({ theme }) => theme.orbit.durationNormal} ease-in-out;
border-top-left-radius: ${({ theme }) => theme.orbit.borderRadiusNormal};
border-top-right-radius: ${({ theme }) => theme.orbit.borderRadiusNormal};
opacity: 1;
& + ${StyledModalSection}:first-of-type {
padding-top: 52px;
border-top: 0;
margin: 0;
}
${StyledButtonLink} {
margin-right: 4px;
& svg {
transition: color ${({ theme }) => theme.orbit.durationFast} ease-in-out;
color: ${({ theme }) => theme.orbit.paletteInkLight};
}
&:hover svg {
color: ${({ theme }) => theme.orbit.paletteInkLightHover};
}
&:active svg {
color: ${({ theme }) => theme.orbit.paletteInkLightActive};
}
}
`;
CloseContainer.defaultProps = {
theme: defaultTokens,
};
const ModalWrapperContent = styled.div`
position: absolute;
box-sizing: border-box;
border-top-left-radius: ${({ theme }) => theme.orbit.borderRadiusNormal};
border-top-right-radius: ${({ theme }) => theme.orbit.borderRadiusNormal};
background-color: ${({ theme }) => theme.orbit.backgroundModal};
font-family: ${({ theme }) => theme.orbit.fontFamily};
width: 100%;
height: calc(
100% - ${({ theme }) => theme.orbit.spaceXLarge} -
${({ fixedFooter }) => (fixedFooter ? "76px" : "0px")}
);
box-shadow: ${({ theme }) => theme.orbit.boxShadowModal};
overflow-y: auto;
overflow-x: hidden;
${StyledModalSection}:last-of-type {
padding-bottom: ${({ theme, fixedFooter }) => fixedFooter && theme.orbit.spaceLarge};
margin-bottom: ${({ fixedFooter }) => fixedFooter && "0"};
}
${StyledModalFooter} {
padding: ${({ theme, fixedFooter }) => fixedFooter && theme.orbit.spaceMedium};
box-shadow: ${({ fixedFooter }) => fixedFooter && `0 -2px 4px 0 rgba(23, 27, 30, 0.1)`};
position: ${({ fixedFooter }) => fixedFooter && "fixed"};
}
${MobileHeader} {
top: ${({ scrolled, theme }) => scrolled && theme.orbit.spaceXLarge};
opacity: ${({ scrolled }) => scrolled && "1"};
}
${CloseContainer} {
top: ${({ scrolled }) => scrolled && "32px"};
position: ${({ scrolled }) => scrolled && "fixed"};
box-shadow: ${({ scrolled }) => scrolled && `0 2px 4px 0 rgba(23, 27, 30, 0.1)`};
background-color: ${({ theme, scrolled }) => scrolled && theme.orbit.paletteWhite};
opacity: ${({ scrolled }) => scrolled && "1"};
}
${media.desktop`
position: relative;
border-radius: ${({ theme }) => theme.orbit.borderRadiusNormal};
padding-bottom: 0;
${StyledModalSection}:last-of-type {
padding-bottom: ${({ theme }) => theme.orbit.spaceXXLarge};
margin-bottom: 0;
&::after {
content: none;
}
}
${StyledModalFooter} {
position: relative;
box-shadow: none;
padding: ${({ theme }) => `0 ${theme.orbit.spaceXXLarge} ${theme.orbit.spaceXXLarge}`};
}
${CloseContainer} {
position: absolute;
top: 0;
}
`};
`;
ModalWrapperContent.defaultProps = {
theme: defaultTokens,
};
const CloseElement = ({ onClose }: CloseElementType) => (
<CloseContainer>
<ButtonLink onClick={onClose} size="normal" icon={<Close />} transparent />
</CloseContainer>
);
const Content = ({ closable, onClose, size, children, scrolled, fixedFooter }: ContentType) =>
closable ? (
<ClickOutside onClickOutside={onClose}>
<ModalWrapperContent size={size} fixedFooter={fixedFooter} scrolled={scrolled}>
<CloseElement onClose={onClose} />
{children}
</ModalWrapperContent>
</ClickOutside>
) : (
<ModalWrapperContent size={size} fixedFooter={fixedFooter} scrolled={scrolled}>
{children}
</ModalWrapperContent>
);
class Modal extends React.PureComponent<Props, State> {
constructor() {
super();
// $FlowExpected
this.handleScroll = this.handleScroll.bind(this);
}
state = {
scrolled: false,
loaded: false,
};
componentDidMount() {
// eslint-disable-next-line
setTimeout(
() =>
this.setState({
loaded: true,
}),
150,
);
}
componentDidUpdate() {
if (!this.state.scrolled) {
this.setScrollPoint();
}
}
setScrollPoint() {
setTimeout(() => {
if (!this.state.scrolled) {
const { node } = this;
if (node instanceof HTMLElement) {
const el = node.querySelector(`.${<StyledHeading />.type.styledComponentId}`);
if (el) {
this.offset = el.clientHeight + el.offsetTop;
}
}
}
}, 550);
}
handleScroll(ev: Event) {
if (ev.target instanceof HTMLDivElement) {
this.setState({
scrolled: ev.target.scrollTop >= this.offset,
});
}
}
handleKeyDown = (ev: SyntheticKeyboardEvent<HTMLDivElement>) => {
const { onClose } = this.props;
if (ev.key === "Escape" && onClose) {
onClose(ev);
}
};
node: ?HTMLElement;
offset = 40;
render() {
const {
onClose,
children,
size = SIZES.NORMAL,
closable = true,
fixedFooter = false,
dataTest,
} = this.props;
const { scrolled, loaded } = this.state;
return (
<ModalBody
tabIndex="0"
onKeyDown={closable ? this.handleKeyDown : undefined}
data-test={dataTest}
>
<ModalWrapper
size={size}
loaded={loaded}
onScroll={this.handleScroll}
innerRef={node => {
this.node = node;
}}
fixedFooter={fixedFooter}
>
<Content
closable={closable}
onClose={onClose}
size={size}
scrolled={scrolled}
fixedFooter={fixedFooter}
>
{children}
</Content>
</ModalWrapper>
</ModalBody>
);
}
}
export default Modal;