Skip to content

Commit 2330f59

Browse files
authored
Merge pull request #112 from bamlab/feat/expose-scrollview-ref
feat: expose scrollview ref
2 parents 2af2901 + 5f2bb3c commit 2330f59

File tree

2 files changed

+108
-77
lines changed

2 files changed

+108
-77
lines changed

packages/lib/src/spatial-navigation/components/ScrollView.tsx

Lines changed: 92 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import React, { useCallback, RefObject, useRef, ReactElement, ReactNode, useMemo } from 'react';
1+
import React, {
2+
useCallback,
3+
RefObject,
4+
useRef,
5+
ReactElement,
6+
ReactNode,
7+
useMemo,
8+
forwardRef,
9+
} from 'react';
210
import {
311
ScrollView,
412
View,
@@ -14,6 +22,7 @@ import {
1422
} from '../context/ParentScrollContext';
1523
import { scrollToNewlyFocusedElement } from '../helpers/scrollToNewlyfocusedElement';
1624
import { useSpatialNavigationDeviceType } from '../context/DeviceContext';
25+
import { mergeRefs } from '../helpers/mergeRefs';
1726

1827
type Props = {
1928
horizontal?: boolean;
@@ -122,85 +131,91 @@ const getNodeRef = (node: ScrollView | null | undefined) => {
122131
return node;
123132
};
124133

125-
export const SpatialNavigationScrollView = ({
126-
horizontal = false,
127-
style,
128-
offsetFromStart = 0,
129-
children,
130-
ascendingArrow,
131-
ascendingArrowContainerStyle,
132-
descendingArrow,
133-
descendingArrowContainerStyle,
134-
pointerScrollSpeed = 10,
135-
}: Props) => {
136-
const { scrollToNodeIfNeeded: makeParentsScrollToNodeIfNeeded } =
137-
useSpatialNavigatorParentScroll();
138-
const scrollViewRef = useRef<ScrollView>(null);
139-
140-
const scrollY = useRef<number>(0);
141-
142-
const { ascendingArrowProps, descendingArrowProps, deviceType, deviceTypeRef } =
143-
useRemotePointerScrollviewScrollProps({ pointerScrollSpeed, scrollY, scrollViewRef });
144-
145-
const scrollToNode = useCallback(
146-
(newlyFocusedElementRef: RefObject<View>, additionalOffset = 0) => {
147-
try {
148-
if (deviceTypeRef.current === 'remoteKeys') {
149-
newlyFocusedElementRef?.current?.measureLayout(
150-
getNodeRef(scrollViewRef?.current),
151-
(left, top) =>
152-
scrollToNewlyFocusedElement({
153-
newlyFocusedElementDistanceToLeftRelativeToLayout: left,
154-
newlyFocusedElementDistanceToTopRelativeToLayout: top,
155-
horizontal,
156-
offsetFromStart: offsetFromStart + additionalOffset,
157-
scrollViewRef,
158-
}),
159-
() => {},
160-
);
161-
}
162-
} catch {
163-
// A crash can happen when calling measureLayout when a page unmounts. No impact on focus detected in regular use cases.
164-
}
165-
makeParentsScrollToNodeIfNeeded(newlyFocusedElementRef, additionalOffset); // We need to propagate the scroll event for parents if we have nested ScrollViews/VirtualizedLists.
134+
export const SpatialNavigationScrollView = forwardRef<ScrollView, Props>(
135+
(
136+
{
137+
horizontal = false,
138+
style,
139+
offsetFromStart = 0,
140+
children,
141+
ascendingArrow,
142+
ascendingArrowContainerStyle,
143+
descendingArrow,
144+
descendingArrowContainerStyle,
145+
pointerScrollSpeed = 10,
166146
},
167-
[makeParentsScrollToNodeIfNeeded, horizontal, offsetFromStart, deviceTypeRef],
168-
);
147+
ref,
148+
) => {
149+
const { scrollToNodeIfNeeded: makeParentsScrollToNodeIfNeeded } =
150+
useSpatialNavigatorParentScroll();
151+
const scrollViewRef = useRef<ScrollView>(null);
169152

170-
const onScroll = useCallback(
171-
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
172-
scrollY.current = event.nativeEvent.contentOffset.y;
173-
},
174-
[scrollY],
175-
);
153+
const scrollY = useRef<number>(0);
176154

177-
return (
178-
<SpatialNavigatorParentScrollContext.Provider value={scrollToNode}>
179-
<ScrollView
180-
ref={scrollViewRef}
181-
horizontal={horizontal}
182-
style={style}
183-
showsHorizontalScrollIndicator={false}
184-
showsVerticalScrollIndicator={false}
185-
scrollEnabled={false}
186-
onScroll={onScroll}
187-
scrollEventThrottle={16}
188-
>
189-
{children}
190-
</ScrollView>
191-
{deviceType === 'remotePointer' ? (
192-
<PointerScrollArrows
193-
descendingArrow={descendingArrow}
194-
ascendingArrow={ascendingArrow}
195-
descendingArrowContainerStyle={descendingArrowContainerStyle}
196-
ascendingArrowContainerStyle={ascendingArrowContainerStyle}
197-
ascendingArrowProps={ascendingArrowProps}
198-
descendingArrowProps={descendingArrowProps}
199-
/>
200-
) : undefined}
201-
</SpatialNavigatorParentScrollContext.Provider>
202-
);
203-
};
155+
const { ascendingArrowProps, descendingArrowProps, deviceType, deviceTypeRef } =
156+
useRemotePointerScrollviewScrollProps({ pointerScrollSpeed, scrollY, scrollViewRef });
157+
158+
const scrollToNode = useCallback(
159+
(newlyFocusedElementRef: RefObject<View>, additionalOffset = 0) => {
160+
try {
161+
if (deviceTypeRef.current === 'remoteKeys') {
162+
newlyFocusedElementRef?.current?.measureLayout(
163+
getNodeRef(scrollViewRef?.current),
164+
(left, top) =>
165+
scrollToNewlyFocusedElement({
166+
newlyFocusedElementDistanceToLeftRelativeToLayout: left,
167+
newlyFocusedElementDistanceToTopRelativeToLayout: top,
168+
horizontal,
169+
offsetFromStart: offsetFromStart + additionalOffset,
170+
scrollViewRef,
171+
}),
172+
() => {},
173+
);
174+
}
175+
} catch {
176+
// A crash can happen when calling measureLayout when a page unmounts. No impact on focus detected in regular use cases.
177+
}
178+
makeParentsScrollToNodeIfNeeded(newlyFocusedElementRef, additionalOffset); // We need to propagate the scroll event for parents if we have nested ScrollViews/VirtualizedLists.
179+
},
180+
[makeParentsScrollToNodeIfNeeded, horizontal, offsetFromStart, deviceTypeRef],
181+
);
182+
183+
const onScroll = useCallback(
184+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
185+
scrollY.current = event.nativeEvent.contentOffset.y;
186+
},
187+
[scrollY],
188+
);
189+
190+
return (
191+
<SpatialNavigatorParentScrollContext.Provider value={scrollToNode}>
192+
<ScrollView
193+
ref={mergeRefs([ref, scrollViewRef])}
194+
horizontal={horizontal}
195+
style={style}
196+
showsHorizontalScrollIndicator={false}
197+
showsVerticalScrollIndicator={false}
198+
scrollEnabled={false}
199+
onScroll={onScroll}
200+
scrollEventThrottle={16}
201+
>
202+
{children}
203+
</ScrollView>
204+
{deviceType === 'remotePointer' ? (
205+
<PointerScrollArrows
206+
descendingArrow={descendingArrow}
207+
ascendingArrow={ascendingArrow}
208+
descendingArrowContainerStyle={descendingArrowContainerStyle}
209+
ascendingArrowContainerStyle={ascendingArrowContainerStyle}
210+
ascendingArrowProps={ascendingArrowProps}
211+
descendingArrowProps={descendingArrowProps}
212+
/>
213+
) : undefined}
214+
</SpatialNavigatorParentScrollContext.Provider>
215+
);
216+
},
217+
);
218+
SpatialNavigationScrollView.displayName = 'SpatialNavigationScrollView';
204219

205220
const PointerScrollArrows = React.memo(
206221
({
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// copy-paste from react-merge-refs lib
2+
import type * as React from 'react';
3+
4+
export function mergeRefs<T>(
5+
refs: Array<React.MutableRefObject<T> | React.LegacyRef<T> | undefined | null>,
6+
): React.RefCallback<T> {
7+
return (value) => {
8+
refs.forEach((ref) => {
9+
if (typeof ref === 'function') {
10+
ref(value);
11+
} else if (ref != null) {
12+
(ref as React.MutableRefObject<T | null>).current = value;
13+
}
14+
});
15+
};
16+
}

0 commit comments

Comments
 (0)