Last active
June 20, 2023 09:33
-
-
Save sdjnes/3a1ce8b48d54cc39b49e8775266c7969 to your computer and use it in GitHub Desktop.
Modal system idea
This file contains 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
// To use it, you simply define a Modal and then render it | |
type MyComponentProps = { | |
description: string; | |
links: Array<{ href: string, label: string}> | |
} | |
const MyComponent = (props: MyComponentProps) => ... | |
const Page = () => { | |
// This returns an object with a Component, and 3 functions: toggle, open, close | |
const myModal = useModal<MyComponentProps>( | |
MyComponent, | |
"an-id-for-this-modal", | |
() => { console.log('onOpen callback') }, | |
() => { console.log('onClose callback') } | |
); | |
// To open a modal, you trigger its `open` function | |
useEffect(() => { | |
myModal.open() | |
}); | |
// We always render the Modal out to the tree, but whether it shows something is determined by the open/close | |
// The props of myModal.Component align with the props of MyComponent (the thing you are rendering in the modal) | |
return <> | |
<myModal.Component description="This is my modal" links={[ { href: '/', label: 'Link one' ]} /> | |
</> | |
} | |
// This is where we implement how the stack works, pop/push | |
const Wrapper = () => { | |
const [modalStack, setModalStack] = useState<Array<string>>([]); | |
return <ModalStackContext.Provider | |
value={{ | |
stack: modalStack, | |
push: (modalId: string) => { | |
setModalStack((modals) => [...modals, modalId]); | |
}, | |
pop: () => { | |
let newStack = [...modalStack]; | |
const poppedItem = newStack.pop(); | |
setModalStack(newStack); | |
return poppedItem; | |
}, | |
}} | |
> | |
<Page /> | |
</ModalStackContext.Provider> | |
} | |
This file contains 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
// This is a specific type of Stack for modals | |
import { useContext } from "react"; | |
import { createStackContext } from "./StackContext"; | |
type ModalStackItem = string; | |
const ModalStackContext = createStackContext<ModalStackItem>(); | |
const useModalStack = () => { | |
const context = useContext(ModalStackContext); | |
return context; | |
}; | |
export { ModalStackContext, useModalStack }; |
This file contains 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
// Using a stack means we can open modals one on top of another if needed | |
// A stack needs to implement a push and a pop | |
import { createContext } from "react"; | |
type Props<T> = { | |
stack: Array<T>; | |
push: (modal: T) => void; | |
pop: () => T | undefined; | |
}; | |
function createStackContext<T>() { | |
return createContext<Props<T>>({ | |
stack: [], | |
push: (x: T) => {}, | |
pop: () => undefined | |
}); | |
} | |
export { createStackContext }; |
This file contains 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
// This hook returns the open/close for a specific modal, and the component to render | |
import { useCallback, useMemo, useState, type FC } from "react"; | |
import ReactDOM from "react-dom"; | |
import { Modal } from "~/components"; | |
import { useModalStack } from "~/contexts"; | |
function useModal<T>( | |
ModalContent: FC<T>, | |
id: string, | |
onOpen?: () => void, | |
onClose?: () => void | |
): { | |
Component: FC<T & { title: string }>; | |
toggle: (instanceProps?: Partial<T>) => void; | |
open: (instanceProps?: Partial<T>) => void; | |
close: () => void; | |
} { | |
const modalStack = useModalStack(); | |
const [instanceProps, setInstanceProps] = useState<Partial<T>>(); | |
const isInStack = useMemo( | |
() => modalStack.stack.includes(id), | |
[modalStack, id] | |
); | |
const open = useCallback( | |
(instanceProps?: Partial<T>) => { | |
modalStack.push(id); | |
setInstanceProps(instanceProps); | |
onOpen?.(); | |
}, | |
[id, modalStack, onOpen] | |
); | |
const close = useCallback(() => { | |
modalStack.pop(); | |
onClose?.(); | |
}, [modalStack, onClose]); | |
const toggle = useCallback( | |
(instanceProps?: Partial<T>) => { | |
isInStack ? close() : open(instanceProps); | |
}, | |
[close, isInStack, open] | |
); | |
const Component = useCallback( | |
(props: T & { title: string }) => | |
isInStack | |
? ReactDOM.createPortal( | |
// <Modal> is a wrapper that centers a white box on a darkened background | |
<Modal | |
title={props.title} | |
onClose={() => { | |
close(); | |
}} | |
> | |
<ModalContent {...props} {...instanceProps} /> | |
</Modal>, | |
document.getElementById("modal-container")! | |
) | |
: null, | |
[ModalContent, close, instanceProps, isInStack] | |
); | |
return useMemo( | |
() => ({ | |
Component, | |
toggle, | |
open, | |
close, | |
}), | |
[Component, toggle, open, close] | |
); | |
} | |
export { useModal }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment