// This is in a separate file to avoid circular dependency with `Layout`. import React from 'react' import { useValue } from '../helpers/react' export type OrderHintKey = 'top' | 'high' | 'center' | 'low' | 'bottom' export type OrderHint = OrderHintKey | number const HINT_ORDER: { [K in OrderHintKey]: number } = { top: 0, high: 25, center: 50, low: 75, bottom: 100, } interface Action { element: React.ReactElement order: number } type ActionMap = { [K in React.Key]: Action } type AddAction = (k: React.Key, e: React.ReactElement, o?: OrderHint) => void type RemoveAction = (k: React.Key) => void interface ActionControl { addAction?: AddAction removeAction?: RemoveAction hasConsumer: boolean } type ActionCtx = [ActionMap, (cb: React.SetStateAction) => void] const ActionContext = React.createContext(undefined) const ActionControlContext = React.createContext({ hasConsumer: false }) export function useAction( element: React.ReactElement | null, orderHint?: OrderHint, key?: React.Key ): React.ReactElement | null { const { addAction, removeAction, hasConsumer } = React.useContext(ActionControlContext) if (element && !key && key !== 0) { key = element.key || element.key === 0 ? element.key : undefined } // TODO It'd be great if I could come up with some sort of hash that indicated // if an element needs a re-render, instead of having to re-add it every time. // My initial thought is that it would depend on `type`, `key`, and a shallow // `props` comparison, except for children where we recurse. Shouldn't need to // worry about re-renders triggered by hooks, since that's handled internally // regardless of where the element ends up in the tree. React.useEffect(() => { if (element && addAction) { if (key === undefined) { throw new Error('Cannot add action bar item without a key.') } addAction(key, element, orderHint) } }, [element, orderHint, key, addAction]) React.useEffect(() => { if (key && removeAction) { return () => removeAction(key as React.Key) } }, [key, removeAction]) return !hasConsumer ? element : null } export function useActionBarItems(): React.ReactElement[] { const actionCtx = React.useContext(ActionContext) const actions: ActionMap = actionCtx ? actionCtx[0] : {} const setControl = actionCtx && actionCtx[1] React.useEffect(() => { if (setControl) { setControl((c) => (c.hasConsumer ? c : { ...c, hasConsumer: true })) return () => setControl((c) => (!c.hasConsumer ? c : { ...c, hasConsumer: false })) } }, [setControl]) const actionList = Object.keys(actions).map((k) => actions[k]) actionList.sort((a, b) => a.order - b.order) return actionList.map((a) => a.element) } const ActionProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => { const [actions, setActions] = React.useState({}) const addAction: AddAction = (key, element, orderHint) => { if (element.key === null) { element = React.cloneElement(element, { key }) } setActions((existingActions) => { const action = { element, order: HINT_ORDER.center } if (existingActions[key] === undefined) { let $order = action.order if (typeof orderHint === 'number') { $order = orderHint } else if (orderHint && HINT_ORDER.hasOwnProperty(orderHint)) { $order = HINT_ORDER[orderHint] } // Because Javascript sort is not guaranteed stable, we want `order` to be unique. const orders = new Set(Object.keys(existingActions).map((k) => existingActions[k].order)) while (orders.has($order)) { $order++ } action.order = $order } else { action.order = existingActions[key].order } return { ...existingActions, [key]: action } }) } const removeAction: RemoveAction = (key) => setActions((existingActions) => { if (existingActions[key] !== undefined) { existingActions = { ...existingActions } delete existingActions[key] } return existingActions }) const [control, setControl] = React.useState({ addAction, removeAction, hasConsumer: false, }) const actionCtx = useValue([actions, setControl], [actions]) return ( {children} ) } export default ActionProvider