Created
August 9, 2018 20:05
-
-
Save stephenjwatkins/f063eedee626278d789788f852eb522c to your computer and use it in GitHub Desktop.
Modals that support nesting.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| <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, | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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