Skip to content

Commit

Permalink
feat: expose ScrollView ref
Browse files Browse the repository at this point in the history
  • Loading branch information
pierpo committed Apr 19, 2024
1 parent 2af2901 commit 5f2bb3c
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 77 deletions.
169 changes: 92 additions & 77 deletions packages/lib/src/spatial-navigation/components/ScrollView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import React, { useCallback, RefObject, useRef, ReactElement, ReactNode, useMemo } from 'react';
import React, {
useCallback,
RefObject,
useRef,
ReactElement,
ReactNode,
useMemo,
forwardRef,
} from 'react';
import {
ScrollView,
View,
Expand All @@ -14,6 +22,7 @@ import {
} from '../context/ParentScrollContext';
import { scrollToNewlyFocusedElement } from '../helpers/scrollToNewlyfocusedElement';
import { useSpatialNavigationDeviceType } from '../context/DeviceContext';
import { mergeRefs } from '../helpers/mergeRefs';

type Props = {
horizontal?: boolean;
Expand Down Expand Up @@ -122,85 +131,91 @@ const getNodeRef = (node: ScrollView | null | undefined) => {
return node;
};

export const SpatialNavigationScrollView = ({
horizontal = false,
style,
offsetFromStart = 0,
children,
ascendingArrow,
ascendingArrowContainerStyle,
descendingArrow,
descendingArrowContainerStyle,
pointerScrollSpeed = 10,
}: Props) => {
const { scrollToNodeIfNeeded: makeParentsScrollToNodeIfNeeded } =
useSpatialNavigatorParentScroll();
const scrollViewRef = useRef<ScrollView>(null);

const scrollY = useRef<number>(0);

const { ascendingArrowProps, descendingArrowProps, deviceType, deviceTypeRef } =
useRemotePointerScrollviewScrollProps({ pointerScrollSpeed, scrollY, scrollViewRef });

const scrollToNode = useCallback(
(newlyFocusedElementRef: RefObject<View>, additionalOffset = 0) => {
try {
if (deviceTypeRef.current === 'remoteKeys') {
newlyFocusedElementRef?.current?.measureLayout(
getNodeRef(scrollViewRef?.current),
(left, top) =>
scrollToNewlyFocusedElement({
newlyFocusedElementDistanceToLeftRelativeToLayout: left,
newlyFocusedElementDistanceToTopRelativeToLayout: top,
horizontal,
offsetFromStart: offsetFromStart + additionalOffset,
scrollViewRef,
}),
() => {},
);
}
} catch {
// A crash can happen when calling measureLayout when a page unmounts. No impact on focus detected in regular use cases.
}
makeParentsScrollToNodeIfNeeded(newlyFocusedElementRef, additionalOffset); // We need to propagate the scroll event for parents if we have nested ScrollViews/VirtualizedLists.
export const SpatialNavigationScrollView = forwardRef<ScrollView, Props>(
(
{
horizontal = false,
style,
offsetFromStart = 0,
children,
ascendingArrow,
ascendingArrowContainerStyle,
descendingArrow,
descendingArrowContainerStyle,
pointerScrollSpeed = 10,
},
[makeParentsScrollToNodeIfNeeded, horizontal, offsetFromStart, deviceTypeRef],
);
ref,
) => {
const { scrollToNodeIfNeeded: makeParentsScrollToNodeIfNeeded } =
useSpatialNavigatorParentScroll();
const scrollViewRef = useRef<ScrollView>(null);

const onScroll = useCallback(
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
scrollY.current = event.nativeEvent.contentOffset.y;
},
[scrollY],
);
const scrollY = useRef<number>(0);

return (
<SpatialNavigatorParentScrollContext.Provider value={scrollToNode}>
<ScrollView
ref={scrollViewRef}
horizontal={horizontal}
style={style}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
scrollEnabled={false}
onScroll={onScroll}
scrollEventThrottle={16}
>
{children}
</ScrollView>
{deviceType === 'remotePointer' ? (
<PointerScrollArrows
descendingArrow={descendingArrow}
ascendingArrow={ascendingArrow}
descendingArrowContainerStyle={descendingArrowContainerStyle}
ascendingArrowContainerStyle={ascendingArrowContainerStyle}
ascendingArrowProps={ascendingArrowProps}
descendingArrowProps={descendingArrowProps}
/>
) : undefined}
</SpatialNavigatorParentScrollContext.Provider>
);
};
const { ascendingArrowProps, descendingArrowProps, deviceType, deviceTypeRef } =
useRemotePointerScrollviewScrollProps({ pointerScrollSpeed, scrollY, scrollViewRef });

const scrollToNode = useCallback(
(newlyFocusedElementRef: RefObject<View>, additionalOffset = 0) => {
try {
if (deviceTypeRef.current === 'remoteKeys') {
newlyFocusedElementRef?.current?.measureLayout(
getNodeRef(scrollViewRef?.current),
(left, top) =>
scrollToNewlyFocusedElement({
newlyFocusedElementDistanceToLeftRelativeToLayout: left,
newlyFocusedElementDistanceToTopRelativeToLayout: top,
horizontal,
offsetFromStart: offsetFromStart + additionalOffset,
scrollViewRef,
}),
() => {},
);
}
} catch {
// A crash can happen when calling measureLayout when a page unmounts. No impact on focus detected in regular use cases.
}
makeParentsScrollToNodeIfNeeded(newlyFocusedElementRef, additionalOffset); // We need to propagate the scroll event for parents if we have nested ScrollViews/VirtualizedLists.
},
[makeParentsScrollToNodeIfNeeded, horizontal, offsetFromStart, deviceTypeRef],
);

const onScroll = useCallback(
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
scrollY.current = event.nativeEvent.contentOffset.y;
},
[scrollY],
);

return (
<SpatialNavigatorParentScrollContext.Provider value={scrollToNode}>
<ScrollView
ref={mergeRefs([ref, scrollViewRef])}
horizontal={horizontal}
style={style}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
scrollEnabled={false}
onScroll={onScroll}
scrollEventThrottle={16}
>
{children}
</ScrollView>
{deviceType === 'remotePointer' ? (
<PointerScrollArrows
descendingArrow={descendingArrow}
ascendingArrow={ascendingArrow}
descendingArrowContainerStyle={descendingArrowContainerStyle}
ascendingArrowContainerStyle={ascendingArrowContainerStyle}
ascendingArrowProps={ascendingArrowProps}
descendingArrowProps={descendingArrowProps}
/>
) : undefined}
</SpatialNavigatorParentScrollContext.Provider>
);
},
);
SpatialNavigationScrollView.displayName = 'SpatialNavigationScrollView';

const PointerScrollArrows = React.memo(
({
Expand Down
16 changes: 16 additions & 0 deletions packages/lib/src/spatial-navigation/helpers/mergeRefs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// copy-paste from react-merge-refs lib
import type * as React from 'react';

export function mergeRefs<T>(
refs: Array<React.MutableRefObject<T> | React.LegacyRef<T> | undefined | null>,
): React.RefCallback<T> {
return (value) => {
refs.forEach((ref) => {
if (typeof ref === 'function') {
ref(value);
} else if (ref != null) {
(ref as React.MutableRefObject<T | null>).current = value;
}
});
};
}

0 comments on commit 5f2bb3c

Please sign in to comment.