| 11 | * + Slot component that is used by LeadingVisual, Description |
| 12 | */ |
| 13 | const createSlots = <SlotNames extends string>(slotNames: SlotNames[]) => { |
| 14 | type Slots = { |
| 15 | [key in SlotNames]?: React.ReactNode |
| 16 | } |
| 17 | |
| 18 | type ContextProps = { |
| 19 | registerSlot: (name: SlotNames, contents: React.ReactNode) => void |
| 20 | unregisterSlot: (name: SlotNames) => void |
| 21 | context: Record<string, unknown> |
| 22 | } |
| 23 | const SlotsContext = React.createContext<ContextProps>({ |
| 24 | registerSlot: () => null, |
| 25 | unregisterSlot: () => null, |
| 26 | context: {}, |
| 27 | }) |
| 28 | |
| 29 | // maintain a static reference to avoid infinite render loop |
| 30 | const defaultContext = Object.freeze({}) |
| 31 | |
| 32 | /** Slots uses a Double render strategy inspired by [reach-ui/descendants](https://github.com/reach/reach-ui/tree/develop/packages/descendants) |
| 33 | * Slot registers themself with the Slots parent. |
| 34 | * When all the children have mounted = registered themselves in slot, |
| 35 | * we re-render the parent component to render with slots |
| 36 | */ |
| 37 | const Slots: React.FC<{ |
| 38 | context?: ContextProps['context'] |
| 39 | children: (slots: Slots) => React.ReactNode |
| 40 | }> = ({context = defaultContext, children}) => { |
| 41 | // initialise slots |
| 42 | const slotsDefinition: Slots = {} |
| 43 | slotNames.map(name => (slotsDefinition[name] = null)) |
| 44 | const slotsRef = React.useRef<Slots>(slotsDefinition) |
| 45 | |
| 46 | const rerenderWithSlots = useForceUpdate() |
| 47 | const [isMounted, setIsMounted] = React.useState(false) |
| 48 | |
| 49 | // fires after all the effects in children |
| 50 | useLayoutEffect(() => { |
| 51 | rerenderWithSlots() |
| 52 | setIsMounted(true) |
| 53 | }, [rerenderWithSlots]) |
| 54 | |
| 55 | const registerSlot = React.useCallback( |
| 56 | (name: SlotNames, contents: React.ReactNode) => { |
| 57 | slotsRef.current[name] = contents |
| 58 | |
| 59 | // don't render until the component mounts = all slots are registered |
| 60 | if (isMounted) rerenderWithSlots() |
| 61 | }, |
| 62 | [isMounted, rerenderWithSlots], |
| 63 | ) |
| 64 | |
| 65 | // Slot can be removed from the tree as well, |
| 66 | // we need to unregister them from the slot |
| 67 | const unregisterSlot = React.useCallback( |
| 68 | (name: SlotNames) => { |
| 69 | slotsRef.current[name] = null |
| 70 | rerenderWithSlots() |