// CODE FROM https://github.com/rheng001/react-native-wheel-scrollview-picker/tree/master import * as React from 'react'; import { View, Text, ScrollView as NativeScrollView, FlatList as NativeFlatList, FlatListProps, Pressable, Platform } from 'react-native'; import type { ComponentProps, MutableRefObject, LegacyRef, ReactNode, RefObject, RefCallback } from 'react'; const { createContext, forwardRef, useContext, useMemo, useRef, useImperativeHandle } = React; // from react-merge-refs (avoid dependency) function mergeRefs( refs: Array | LegacyRef> ): RefCallback { return (value) => { refs.forEach((ref) => { if (typeof ref === 'function') { ref(value); } else if (ref != null) { (ref as MutableRefObject).current = value; } }); }; } // type Props = { // anchors: Anchors; // }; type Unpromisify = T extends Promise ? R : T; /** * The following is taken/edited from `useScrollToTop` from `@react-navigation/native` */ type ScrollOptions = { y?: number; animated?: boolean }; type ScrollableView = | { scrollToTop(): void } | { scrollTo(options: ScrollOptions): void } | { scrollToOffset(options: { offset?: number; animated?: boolean }): void } | { scrollResponderScrollTo(options: ScrollOptions): void }; type ScrollableWrapper = | { getScrollResponder(): ReactNode } | { getNode(): ScrollableView } | ScrollableView; function getScrollableNode(ref: RefObject) { if (ref.current == null) { return null; } if (typeof ref.current !== 'object' && typeof ref.current !== 'function') { return null; } if ( 'scrollTo' in ref.current || 'scrollToOffset' in ref.current || 'scrollResponderScrollTo' in ref.current ) { // This is already a scrollable node. return ref.current; } else if ('getScrollResponder' in ref.current) { // If the view is a wrapper like FlatList, SectionList etc. // We need to use `getScrollResponder` to get access to the scroll responder return ref.current.getScrollResponder(); } else if ('getNode' in ref.current) { // When a `ScrollView` is wraped in `Animated.createAnimatedComponent` // we need to use `getNode` to get the ref to the actual scrollview. // Note that `getNode` is deprecated in newer versions of react-native // this is why we check if we already have a scrollable node above. return ref.current.getNode(); } else { return ref.current; } } /** * End of react-navigation code. */ type ScrollToOptions = { animated?: boolean; /** * A number that determines how far from the content you want to scroll. * * Default: `-10`, which means it scrolls to 10 pixels before the content. */ offset?: number; /** * If you're using a `ScrollView` or `FlatList` imported from this library, you can ignore this field. */ // horizontal?: boolean; }; export interface Anchors {} export type AnchorsRef = { scrollTo: ( name: Anchor, options?: ScrollToOptions ) => Promise<{ success: true } | { success: false; message: string }>; }; /** * If you need to control a `ScrollView` or `FlatList` from outside of their scope: * * ```jsx * import React from 'react' * import { useAnchors, ScrollView } from '@nandorojo/anchor' * * export default function App() { * const anchors = useAnchors() * * const onPress = () => { * anchors.current?.scrollTo('list') * } * * return ( * * * * ) * } * ``` */ const useAnchors = () => { const ref = useRef(null); return ref; }; // @ts-expect-error type Anchor = Anchors['anchor'] extends string ? Anchors['anchor'] : string; // export default function createAnchors() { type AnchorsContext = { targetRefs: RefObject>; scrollRef: RefObject; registerTargetRef: (name: Anchor, ref: View | Text) => void; registerScrollRef: (ref: ScrollableWrapper | null) => void; horizontal: ComponentProps['horizontal']; scrollTo: AnchorsRef['scrollTo']; }; const AnchorsContext = createContext({ targetRefs: { current: {} as any }, scrollRef: { current: null as any }, registerTargetRef: () => { // no-op }, registerScrollRef: () => { // no-op }, horizontal: false, scrollTo: () => { return new Promise((resolve) => resolve({ success: false, message: 'Missing @nandorojo/anchor provider.' }) ); } }); const useAnchorsContext = () => useContext(AnchorsContext); const useCreateAnchorsContext = ({ horizontal }: Pick): AnchorsContext => { const targetRefs = useRef>({}); const scrollRef = useRef(null); return useMemo(() => { return { targetRefs, scrollRef: scrollRef as RefObject, registerTargetRef: (target, ref) => { targetRefs.current = { ...targetRefs.current, [target]: ref }; }, registerScrollRef: (ref) => { if (ref) { scrollRef.current = ref; } }, horizontal, scrollTo: ( name: Anchor, { animated = true, offset = -10 }: ScrollToOptions = {} ) => { return new Promise< { success: true } | { success: false; message: string } >((resolve) => { try { const node = Platform.select({ default: scrollRef.current, web: scrollRef.current && // @ts-ignore scrollRef.current.getInnerViewNode && // @ts-ignore scrollRef.current.getInnerViewNode() }); if (!node) { return resolve({ success: false, message: 'Scroll ref does not exist. Will not scroll to view.' }); } if (!targetRefs.current?.[name]) { resolve({ success: false, message: 'Anchor ref ' + name + ' does not exist. It will not scroll. Please make sure to use the ScrollView provided by @nandorojo/anchors, or use the registerScrollRef function for your own ScrollView.' }); } targetRefs.current?.[name].measureLayout( node, (left, top) => { requestAnimationFrame(() => { const scrollY = top; const scrollX = left; const scrollable = getScrollableNode( scrollRef as RefObject ) as ScrollableWrapper; let scrollTo = horizontal ? scrollX : scrollY; scrollTo += offset; scrollTo = Math.max(scrollTo, 0); const key = horizontal ? 'x' : 'y'; if (!scrollable) { return resolve({ success: false, message: 'Scrollable not detected. Will not scroll.' }); } try { if ('scrollTo' in scrollable) { scrollable.scrollTo({ [key]: scrollTo, animated }); } else if ('scrollToOffset' in scrollable) { scrollable.scrollToOffset({ offset: scrollTo, animated }); } else if ('scrollResponderScrollTo' in scrollable) { scrollable.scrollResponderScrollTo({ [key]: scrollTo, animated }); } } catch (error) { return resolve({ success: false, message: 'Failed to scroll for an unknown reason.' }); } resolve({ success: true }); }); }, () => { resolve({ success: false, message: 'Failed to measure target node.' }); } ); } catch (error: any) { resolve({ success: false, message: [ 'Failed to measure target node.', error && 'message' in error && error.message ] .filter(Boolean) .join(' ') }); } }); } }; }, [horizontal]); }; function useRegisterTarget() { const { registerTargetRef } = useAnchorsContext(); return useMemo( () => ({ register: (name: Anchor) => { return (ref: View) => registerTargetRef(name, ref); } }), [registerTargetRef] ); } function useScrollTo() { const { scrollTo } = useAnchorsContext(); return useMemo( () => ({ scrollTo }), [scrollTo] ); } function useRegisterScroller() { const { registerScrollRef } = useAnchorsContext(); return { registerScrollRef }; } function AnchorProvider({ children, horizontal, anchors }: { children: ReactNode; anchors?: RefObject } & Pick< AnchorsContext, 'horizontal' >) { const value = useCreateAnchorsContext({ horizontal }); useImperativeHandle(anchors, () => ({ scrollTo: (...props) => { return value.scrollTo(...props); } })); return ( {children} ); } /** * Identical to the normal React Native `ScrollView`, except that it allows scrolling to anchor links. * * If you use this component, you don't need to use the `AnchorProvider`. It implements it for you. */ const ScrollView = forwardRef< NativeScrollView, ComponentProps & { children?: ReactNode; } & Pick, 'anchors'> >(function ScrollView({ horizontal = false, anchors, ...props }, ref) { return ( {({ registerScrollRef }) => ( )} ); }); /** * Identical to the normal React Native flatlist, except that it allows scrolling to anchor links. * * If you use this component, you don't need to use the `AnchorProvider`. * * One important difference: if you want to use the `ref`, pass it to `flatListRef` instead of `ref`. */ function FlatList({ flatListRef, horizontal = false, anchors, ...props }: FlatListProps & { flatListRef?: RefObject } & Pick< ComponentProps, 'anchors' >) { return ( {({ registerScrollRef }) => ( )} ); } function ScrollTo({ target, onPress, options, onRequestScrollTo, ...props }: { children?: ReactNode; target: Anchor; options?: ScrollToOptions; onRequestScrollTo?: ( props: Unpromisify['scrollTo']>> ) => void; } & ComponentProps) { const { scrollTo } = useScrollTo(); return ( { onPress?.(e); const result = await scrollTo(target, options); onRequestScrollTo?.(result); }} /> ); } const Target = forwardRef< View, { name: Anchor; children?: ReactNode } & ComponentProps >(function Target({ name, ...props }, ref) { const { register } = useRegisterTarget(); return ; }); const AnchorsConsumer = AnchorsContext.Consumer; export { AnchorProvider, ScrollView, FlatList, useRegisterTarget, useScrollTo, ScrollTo, Target, ScrollTo as Anchor, useRegisterScroller, useAnchors, AnchorsConsumer };