forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {
2 createContext,
3 Fragment,
4 useCallback,
5 useContext,
6 useEffect,
7 useId,
8 useMemo,
9 useRef,
10 useState,
11} from 'react'
12
13type Component = React.ReactElement<any>
14
15type ContextType = {
16 outlet: Component | null
17 append(id: string, component: Component): void
18 remove(id: string): void
19}
20
21type ComponentMap = {
22 [id: string]: Component | null
23}
24
25export function createPortalGroup() {
26 const Context = createContext<ContextType>({
27 outlet: null,
28 append: () => {},
29 remove: () => {},
30 })
31 Context.displayName = 'PortalContext'
32
33 function Provider(props: React.PropsWithChildren<{}>) {
34 const map = useRef<ComponentMap>({})
35 const [outlet, setOutlet] = useState<ContextType['outlet']>(null)
36
37 const append = useCallback<ContextType['append']>((id, component) => {
38 if (map.current[id]) return
39 map.current[id] = <Fragment key={id}>{component}</Fragment>
40 setOutlet(<>{Object.values(map.current)}</>)
41 }, [])
42
43 const remove = useCallback<ContextType['remove']>(id => {
44 map.current[id] = null
45 setOutlet(<>{Object.values(map.current)}</>)
46 }, [])
47
48 const contextValue = useMemo(
49 () => ({
50 outlet,
51 append,
52 remove,
53 }),
54 [outlet, append, remove],
55 )
56
57 return (
58 <Context.Provider value={contextValue}>{props.children}</Context.Provider>
59 )
60 }
61
62 function Outlet() {
63 const ctx = useContext(Context)
64 return ctx.outlet
65 }
66
67 function Portal({children}: React.PropsWithChildren<{}>) {
68 const {append, remove} = useContext(Context)
69 const id = useId()
70 useEffect(() => {
71 append(id, children as Component)
72 return () => remove(id)
73 }, [id, children, append, remove])
74 return null
75 }
76
77 return {Provider, Outlet, Portal}
78}
79
80const DefaultPortal = createPortalGroup()
81export const Provider = DefaultPortal.Provider
82export const Outlet = DefaultPortal.Outlet
83export const Portal = DefaultPortal.Portal