import { logger } from '@codeleap/logger' import React, { MutableRefObject, useCallback, useContext, useImperativeHandle, useRef } from 'react' import { NativeScrollEvent, NativeSyntheticEvent, ScrollView, ScrollViewProps } from "react-native" const scrollProperties = [ 'scrollTo', ] satisfies (keyof ScrollView)[] type ScrollProperty = (typeof scrollProperties)[number] type ScrollEvents = Pick export type Scrollable = Pick & { subscribe(e: T, cb: ScrollEvents[T]): () => void } type ScrollRef = React.MutableRefObject type ScrollContextValue = { ref: ScrollRef } const noOpScrollMethods = Object.fromEntries( scrollProperties.map(p => [p, () => { logger.warn(`"${p}" was called from a ScrollProvider ref without a bound scrollable component. This does not cause errors but may impact user experience and indicate an unhandled edge case in calling code`) }]) ) const noOpScrollable = { ...noOpScrollMethods, subscribe(e, cb){ return () => {} } } as Scrollable const ScrollContext = React.createContext({} as ScrollContextValue) /** * Augments a plain `ScrollView` ref with a pub/sub `subscribe` method so that sibling components (e.g. a floating header) can react to scroll events without prop-drilling. The augmented ref is exposed via `useImperativeHandle` so the parent sees the extended interface; the underlying `ScrollView` ref is called through directly so no native bridge call is duplicated. */ export const useScrollPubSub = (ref: MutableRefObject>) => { const listeners = useRef(new Map()) const augmentedRef = useRef(null) const emit = useCallback((event: keyof ScrollEvents, e: NativeSyntheticEvent) => { listeners.current.forEach((cb, key) => { if(key.startsWith(`${event}-`)){ cb(e) } }) }, []) useImperativeHandle(augmentedRef, () => ({ scrollTo(...args){ ref?.current.scrollTo(...args) }, subscribe(e, cb){ const id = `${e}-${listeners.current.size}` listeners.current.set(id, cb) return () => { listeners.current.delete(id) } } }), [ref]) return { ref: augmentedRef, emit } } export const ScrollProvider = React.forwardRef(({children}, ref) => { return {children} }) export function useWrappingScrollable(){ const ctx = useContext(ScrollContext) if(!ctx){ return { current: noOpScrollable } } return ctx.ref }