import type { PropsWithChildren } from "react"; import React, { useCallback, useMemo, useState } from "react"; import { StyleSheet, View } from "react-native"; import isEqual from "lodash.isequal"; import type { AddElement, ElementsRecord, GetCurrentActiveOverlay, OverlayData, RemoveElement, RootRefGetter, } from "./context"; import HighlightableElementContext from "./context"; export type HighlightableElementProviderProps = PropsWithChildren<{ /** * The reference to the root of the DOM. If (and only if) this is left as undefined, * this component will instantiate a View as a child and use that as the root instead. * You usually don't need to set this, unless either: * - The wrapper we provide is making your app look weird. This can happen when you use * tab bars / headers / etc. * - You have several providers for whatever reason (you probably shouldn't). * * @since 1.0 */ rootRef?: React.Component | null; }>; /** * The context provider that provides `HighlightOverlay` with the id's of all the * `HighlightableElement`s. * * If the `rootRef` prop is not set this provider must be placed top-level since it also serves as * the relative measuring point for the highlights. * * If the `rootRef` prop **is** set it only has to be above the `rootRef` and all `HighlightOverlay` * and `HighlightableElement` that is being used. * * @since 1.0 */ function HighlightableElementProvider({ rootRef: externalRootRef, children, }: HighlightableElementProviderProps): JSX.Element { const [rootRef, setRootRef] = useState | null>( externalRootRef ?? null ); const [elements, setElements] = useState({}); const [currentActiveOverlay, setCurrentActiveOverlay] = useState(null); const addElement = useCallback( (id, node, bounds, options) => { if ( elements[id] == null || !isEqual(bounds, elements[id].bounds) || !isEqual(options, elements[id].options) ) { setElements((oldElements) => ({ ...oldElements, [id]: { node, bounds, options } })); } }, [elements] ); const removeElement: RemoveElement = useCallback((id) => { setElements((oldElements) => { delete oldElements[id]; return { ...oldElements }; }); }, []); const getRootRef = useCallback( () => externalRootRef ?? rootRef, [externalRootRef, rootRef] ); const getCurrentActiveOverlay = useCallback( () => currentActiveOverlay, [currentActiveOverlay] ); const contextValue = useMemo( () => Object.freeze([ elements, { addElement, removeElement, rootRef: externalRootRef ?? rootRef, getRootRef, setCurrentActiveOverlay, getCurrentActiveOverlay, }, ] as const), [ addElement, elements, externalRootRef, getCurrentActiveOverlay, getRootRef, removeElement, rootRef, ] ); if (externalRootRef == null) { return ( {children} ); } else { return ( {children} ); } } const styles = StyleSheet.create({ rootWrapper: { flex: 1, }, }); export default HighlightableElementProvider;