Skip to content

Instantly share code, notes, and snippets.

@stephenjwatkins
Created August 9, 2018 20:05
Show Gist options
  • Save stephenjwatkins/f063eedee626278d789788f852eb522c to your computer and use it in GitHub Desktop.
Save stephenjwatkins/f063eedee626278d789788f852eb522c to your computer and use it in GitHub Desktop.
Modals that support nesting.
const crossBrowser = {
transitionstart: ["transitionstart", "webkitTransitionStart"],
transitionend: ["transitionend", "webkitTransitionEnd"],
animationstart: ["animationstart", "webkitAnimationStart"],
animationend: ["animationend", "webkitAnimationEnd"],
animationiteration: ["animationiteration", "webkitAnimationIteration"],
};
const on = (el, event, handler) => {
const events = crossBrowser[event] || [event];
events.forEach(e => {
el.addEventListener(e, handler, false);
});
};
const off = (el, event, handler) => {
const events = crossBrowser[event] || [event];
events.forEach(e => {
el.removeEventListener(e, handler, false);
});
};
const events = { on, off };
export { events };
/*
<ModalContainer>
<ModalBackdrop />
<ModalFrames>
<ModalFrame>
<ModalGutter />
<ModalWindow>
<ModalHeader />
<ModalBody />
<ModalFooter />
</ModalWindow>
<ModalGutter />
</ModalFrame>
</ModalFrames>
</ModalContainer>
*/
import React from "react";
import { events } from "utilities/events";
import { Modal } from "../RenderModal";
const transition = props => props.map(p => `${p} .15s ease-out`).join(",");
class ModalTransition extends React.Component {
constructor(...args) {
super(...args);
this.state = { isMounted: false };
this.attachTransitionEl = this.attachTransitionEl.bind(this);
this.handleTransitionEnd = this.handleTransitionEnd.bind(this);
}
attachTransitionEl(el) {
this.transitionEl = el;
}
handleTransitionEnd(e) {
if (e.propertyName === "opacity" && this.props.isRequestingUnmount) {
this.props.actuallyUnmountModal();
}
}
componentDidMount() {
events.on(this.transitionEl, "transitionend", this.handleTransitionEnd);
if (!this.props.isLoading) {
setTimeout(() => {
this.setState({ isMounted: true });
});
}
}
componentDidUpdate(prevProps) {
if (!this.props.isLoading && prevProps.isLoading) {
setTimeout(() => {
this.setState({ isMounted: true });
});
}
}
componentWillUnmount() {
events.off(this.transitionEl, "transitionend", this.handleTransitionEnd);
}
render() {
const { isMounted } = this.state;
return this.props.render({
isMounted,
transitionRef: this.attachTransitionEl,
});
}
}
const ModalContainer = ({ css, isOpen, children, ...rest }) => (
<div
style={{
display: "flex",
position: "fixed",
top: 0,
left: 0,
width: "100%",
height: "100%",
pointerEvents: isOpen ? "auto" : "none",
}}
{...rest}
>
{children}
</div>
);
const ModalBackdrop = ({ css, isVisible, ...rest }) => (
<div
style={{
display: "flex",
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
backgroundColor: "rgba(0,0,0,.1)",
opacity: isVisible ? 1 : 0,
pointerEvents: isVisible ? "auto" : "none",
transition: transition(["opacity"]),
}}
{...rest}
/>
);
const ModalFrames = ({ ...props }) => <Elements.Block {...props} />;
const ModalFrame = ({ css, isLoading, children, ...rest }) => (
<Modal>
{({
ModalId,
ModalPosition,
ModalCount,
isRequestingUnmount,
unmount,
}) => {
const offsetInStack = ModalCount - ModalPosition;
return (
<ModalTransition
isLoading={isLoading}
isRequestingUnmount={isRequestingUnmount}
actuallyUnmountModal={unmount}
render={({ isMounted, transitionRef }) => (
<div
style={{
display: "flex",
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
overflow: "auto",
WebkitOverflowScrolling: "touch",
MsOverflowStyle: "none",
overscrollBehavior: "none",
display: "flex",
flexDirection: "column",
alignItems: "center",
opacity: isRequestingUnmount ? 0 : isMounted ? 1 : 0,
pointerEvents: offsetInStack === 0 ? "auto" : "none",
transformOrigin: "top center",
// prettier-ignore
transform: `
scale(${isRequestingUnmount ? 1.0 : 1.0 - (0.05 * offsetInStack)})
translateY(${isRequestingUnmount ? 16 : (isMounted ? -16 * offsetInStack : 16)}px)
`,
transition: transition(["opacity", "transform"]),
WebkitBackfaceVisibility: "hidden",
}}
{...rest}
ref={transitionRef}
>
{children}
</div>
)}
/>
);
}}
</Modal>
);
const ModalGutter = props => <Elements.Flex {...props} />;
const ModalWindow = ({
component: Component = "div",
css,
children,
...rest
}) => (
<Component
style={{
display: "flex",
flexDirection: "column",
position: "relative",
width: "100%",
}}
{...rest}
>
{children}
</Component>
);
const ModalHeader = ({ css, children, ...rest }) => (
<div
style={{
position: "sticky",
flex: "0 0 auto",
top: 0,
}}
{...rest}
>
{children}
</div>
);
const ModalBody = ({ css, children, ...rest }) => (
<div
style={{ display: "flex", flex: "1 0 auto" }}
{...rest}
>
{children}
</div>
);
const ModalFooter = ({ css, children, ...rest }) => (
<div
style={{
display: "flex",
flex: "0 0 auto",
position: getChromeVersion() === 65 ? "relative" : "sticky",
bottom: 0,
}}
{...rest}
>
{children}
</div>
);
export {
ModalContainer,
ModalBackdrop,
ModalFrames,
ModalFrame,
ModalGutter,
ModalWindow,
ModalHeader,
ModalBody,
ModalFooter,
};
import React from "react";
import { createPortal } from "react-dom";
let globalModalIds = 0;
const RenderModalContext = React.createContext({});
const ModalContext = React.createContext({});
class RenderModalContainer extends React.Component {
constructor(...args) {
super(...args);
this.state = {
allModals: [],
mountedModals: [],
};
this.mountModal = this.mountModal.bind(this);
this.unmountingModal = this.unmountingModal.bind(this);
this.unmountModal = this.unmountModal.bind(this);
this.getModalCount = this.getModalCount.bind(this);
this.getModalPosition = this.getModalPosition.bind(this);
}
mountModal(id) {
this.setState(prevState => {
return {
allModals: [...prevState.allModals, id],
mountedModals: [...prevState.mountedModals, id],
};
});
}
unmountingModal(id) {
this.setState(prevState => {
return {
mountedModals: prevState.mountedModals.filter(d => d !== id),
};
});
}
unmountModal(id) {
this.setState(prevState => {
return {
mountedModals: prevState.mountedModals.filter(d => d !== id),
allModals: prevState.allModals.filter(d => d !== id),
};
});
}
getModalCount(id) {
return this.state.mountedModals.length;
}
getModalPosition(id) {
return this.state.mountedModals.findIndex(d => d === id) + 1;
}
render() {
return (
<RenderModalContext.Provider
value={{
containerEl: this.containerEl,
mountModal: this.mountModal,
unmountingModal: this.unmountingModal,
unmountModal: this.unmountModal,
getModalCount: this.getModalCount,
getModalPosition: this.getModalPosition,
}}
>
{this.props.children({
allModals: this.state.allModals,
mountedModals: this.state.mountedModals,
containerRef: el => {
this.containerEl = el;
},
})}
</RenderModalContext.Provider>
);
}
}
class RenderModalConsumerHandler extends React.Component {
constructor(...args) {
super(...args);
this.id = `modal-${globalModalIds + 1}`;
globalModalIds++;
this.state = {
childrenToRender: this.props.children,
isRequestingUnmount: false,
isActuallyUnmounted: false,
};
this.unmount = this.unmount.bind(this);
}
unmount() {
this.setState({ isActuallyUnmounted: true });
}
componentDidMount() {
if (this.props.children) {
this.props.context.mountModal(this.id);
}
}
componentWillReceiveProps(nextProps) {
if (!nextProps.children && this.props.children) {
this.setState({ isRequestingUnmount: true });
}
if (nextProps.children && !this.props.children) {
this.setState({
isRequestingUnmount: false,
isActuallyUnmounted: false,
});
}
if (nextProps.children && nextProps.children !== this.props.children) {
this.setState({ childrenToRender: nextProps.children });
}
}
componentDidUpdate(prevProps, prevState) {
if (this.state.isRequestingUnmount && !prevState.isRequestingUnmount) {
this.props.context.unmountingModal(this.id);
}
if (this.state.isActuallyUnmounted && !prevState.isActuallyUnmounted) {
this.props.context.unmountModal(this.id);
}
if (this.props.children && !prevProps.children) {
this.props.context.mountModal(this.id);
}
}
render() {
const { children, context } = this.props;
const {
childrenToRender,
isRequestingUnmount,
isActuallyUnmounted,
} = this.state;
if (isActuallyUnmounted || (!children && !isRequestingUnmount)) {
return null;
}
if (!context.containerEl) {
return null;
}
return (
<ModalContext.Provider
value={{
modalId: this.id,
modalCount: context.getModalCount(this.id),
modalPosition: context.getModalPosition(this.id),
isRequestingUnmount: isRequestingUnmount,
unmount: this.unmount,
}}
>
{createPortal(childrenToRender, context.containerEl)}
</ModalContext.Provider>
);
}
}
const RenderModal = props => (
<RenderModalContext.Consumer>
{context => <RenderModalConsumerHandler context={context} {...props} />}
</RenderModalContext.Consumer>
);
const Modal = ModalContext.Consumer;
export { RenderModalContainer, RenderModal, Modal };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment