import type { ReactNode } from 'react'; import { createContext, useCallback, useContext, useState } from 'react'; import type { InteractionEntry, ModifierKey } from './models'; import type { MouseButton } from './models'; export interface InteractionsContextValue { registerInteraction: (id: string, value: InteractionEntry) => void; unregisterInteraction: (id: string) => void; shouldInteract: (id: string, event: MouseEvent) => boolean; } interface MapEntry extends InteractionEntry { id: string; } const InteractionsContext = createContext({} as InteractionsContextValue); export function useInteractionsContext() { return useContext(InteractionsContext); } function InteractionsProvider(props: { children: ReactNode }) { const { children } = props; const [interactionMap] = useState(new Map()); const registerInteraction = useCallback( (id: string, value: InteractionEntry) => { if (interactionMap.has(id)) { console.warn(`An interaction with ID "${id}" is already registered.`); // eslint-disable-line no-console } else { interactionMap.set(id, { id, ...value }); } }, [interactionMap] ); const unregisterInteraction = useCallback( (id: string) => { interactionMap.delete(id); }, [interactionMap] ); const shouldInteract = useCallback( (interactionId: string, event: MouseEvent | WheelEvent) => { const registeredInteractions = [...interactionMap.values()]; function isButtonPressed(button: MouseButton | MouseButton[] | 'Wheel') { if (event instanceof WheelEvent) { return button === 'Wheel'; } return Array.isArray(button) ? button.includes(event.button) : event.button === button; } function areKeysPressed(keys: ModifierKey[]) { return keys.every((k) => event.getModifierState(k)); } if (!interactionMap.has(interactionId)) { throw new Error(`Interaction ${interactionId} is not registered`); } const matchingInteractions = registeredInteractions.filter( ({ modifierKeys: keys, button, disabled }) => !disabled && isButtonPressed(button) && areKeysPressed(keys) ); if (matchingInteractions.length === 0) { return false; } if (matchingInteractions.length === 1) { return matchingInteractions[0].id === interactionId; } // If conflicting interactions, the one with the most modifier keys take precedence const [maxKeyInteraction] = matchingInteractions .sort((a, b) => a.modifierKeys.length - b.modifierKeys.length) .reverse(); return maxKeyInteraction.id === interactionId; }, [interactionMap] ); return ( {children} ); } export default InteractionsProvider;