Split diff horizontal scroll into platform-specific components
Use Metro .web.tsx resolution to separate native gesture arbitration (RNGH ScrollView, waitFor, closeGestureRef) from web (plain RN ScrollView), fixing text selection drag on desktop web.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { useState, useCallback, useEffect, useId, useRef } from "react";
|
||||
import {
|
||||
type LayoutChangeEvent,
|
||||
type NativeSyntheticEvent,
|
||||
type NativeScrollEvent,
|
||||
type StyleProp,
|
||||
type ViewStyle,
|
||||
} from "react-native";
|
||||
import { ScrollView, type ScrollView as ScrollViewType } from "react-native-gesture-handler";
|
||||
import { useHorizontalScrollOptional } from "@/contexts/horizontal-scroll-context";
|
||||
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
|
||||
|
||||
interface DiffScrollProps {
|
||||
children: React.ReactNode;
|
||||
scrollViewWidth: number;
|
||||
onScrollViewWidthChange: (width: number) => void;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
contentContainerStyle?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
export function DiffScroll({
|
||||
children,
|
||||
scrollViewWidth,
|
||||
onScrollViewWidthChange,
|
||||
style,
|
||||
contentContainerStyle,
|
||||
}: DiffScrollProps) {
|
||||
const [isAtLeftEdge, setIsAtLeftEdge] = useState(true);
|
||||
const horizontalScroll = useHorizontalScrollOptional();
|
||||
const scrollId = useId();
|
||||
const scrollViewRef = useRef<ScrollViewType>(null);
|
||||
|
||||
// Get the close gesture ref from animation context (may not be available outside sidebar)
|
||||
let closeGestureRef: React.MutableRefObject<any> | undefined;
|
||||
try {
|
||||
const animation = useExplorerSidebarAnimation();
|
||||
closeGestureRef = animation.closeGestureRef;
|
||||
} catch {
|
||||
// Not inside ExplorerSidebarAnimationProvider, which is fine
|
||||
}
|
||||
|
||||
// Register/unregister scroll offset tracking
|
||||
useEffect(() => {
|
||||
if (!horizontalScroll) return;
|
||||
// Start at 0 (not scrolled)
|
||||
horizontalScroll.registerScrollOffset(scrollId, 0);
|
||||
return () => {
|
||||
horizontalScroll.unregisterScrollOffset(scrollId);
|
||||
};
|
||||
}, [horizontalScroll, scrollId]);
|
||||
|
||||
const handleScroll = useCallback(
|
||||
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const offsetX = event.nativeEvent.contentOffset.x;
|
||||
// Track if we're at the left edge (with small threshold for float precision)
|
||||
setIsAtLeftEdge(offsetX <= 1);
|
||||
if (horizontalScroll) {
|
||||
horizontalScroll.registerScrollOffset(scrollId, offsetX);
|
||||
}
|
||||
},
|
||||
[horizontalScroll, scrollId],
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
horizontal
|
||||
nestedScrollEnabled
|
||||
showsHorizontalScrollIndicator
|
||||
bounces={false}
|
||||
style={style}
|
||||
contentContainerStyle={contentContainerStyle}
|
||||
onScroll={handleScroll}
|
||||
scrollEventThrottle={16}
|
||||
onLayout={(e: LayoutChangeEvent) => onScrollViewWidthChange(e.nativeEvent.layout.width)}
|
||||
// When at left edge, wait for close gesture to fail before scrolling.
|
||||
// The close gesture fails quickly on leftward swipes (failOffsetX=-10),
|
||||
// so scrolling left works normally. On rightward swipes, close gesture
|
||||
// activates and closes the sidebar.
|
||||
waitFor={isAtLeftEdge && closeGestureRef?.current ? closeGestureRef : undefined}
|
||||
>
|
||||
{children}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ScrollView, type LayoutChangeEvent, type StyleProp, type ViewStyle } from "react-native";
|
||||
|
||||
interface DiffScrollProps {
|
||||
children: React.ReactNode;
|
||||
scrollViewWidth: number;
|
||||
onScrollViewWidthChange: (width: number) => void;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
contentContainerStyle?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
export function DiffScroll({
|
||||
children,
|
||||
onScrollViewWidthChange,
|
||||
style,
|
||||
contentContainerStyle,
|
||||
}: DiffScrollProps) {
|
||||
return (
|
||||
<ScrollView
|
||||
horizontal
|
||||
nestedScrollEnabled
|
||||
showsHorizontalScrollIndicator
|
||||
style={style}
|
||||
contentContainerStyle={contentContainerStyle}
|
||||
onLayout={(e: LayoutChangeEvent) => onScrollViewWidthChange(e.nativeEvent.layout.width)}
|
||||
>
|
||||
{children}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useId,
|
||||
useMemo,
|
||||
useRef,
|
||||
memo,
|
||||
@@ -20,7 +19,6 @@ import {
|
||||
type NativeSyntheticEvent,
|
||||
type NativeScrollEvent,
|
||||
} from "react-native";
|
||||
import { ScrollView, type ScrollView as ScrollViewType } from "react-native-gesture-handler";
|
||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import {
|
||||
@@ -44,8 +42,7 @@ import {
|
||||
} from "@/hooks/use-checkout-diff-query";
|
||||
import { useCheckoutStatusQuery } from "@/hooks/use-checkout-status-query";
|
||||
import { useCheckoutPrStatusQuery } from "@/hooks/use-checkout-pr-status-query";
|
||||
import { useHorizontalScrollOptional } from "@/contexts/horizontal-scroll-context";
|
||||
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
|
||||
import { DiffScroll } from "./diff-scroll";
|
||||
import {
|
||||
darkHighlightColors,
|
||||
lightHighlightColors,
|
||||
@@ -262,41 +259,6 @@ function DiffFileBody({
|
||||
testID?: string;
|
||||
}) {
|
||||
const [scrollViewWidth, setScrollViewWidth] = useState(0);
|
||||
const [isAtLeftEdge, setIsAtLeftEdge] = useState(true);
|
||||
const horizontalScroll = useHorizontalScrollOptional();
|
||||
const scrollId = useId();
|
||||
const scrollViewRef = useRef<ScrollViewType>(null);
|
||||
|
||||
// Get the close gesture ref from animation context (may not be available outside sidebar)
|
||||
let closeGestureRef: React.MutableRefObject<any> | undefined;
|
||||
try {
|
||||
const animation = useExplorerSidebarAnimation();
|
||||
closeGestureRef = animation.closeGestureRef;
|
||||
} catch {
|
||||
// Not inside ExplorerSidebarAnimationProvider, which is fine
|
||||
}
|
||||
|
||||
// Register/unregister scroll offset tracking
|
||||
useEffect(() => {
|
||||
if (!horizontalScroll) return;
|
||||
// Start at 0 (not scrolled)
|
||||
horizontalScroll.registerScrollOffset(scrollId, 0);
|
||||
return () => {
|
||||
horizontalScroll.unregisterScrollOffset(scrollId);
|
||||
};
|
||||
}, [horizontalScroll, scrollId]);
|
||||
|
||||
const handleScroll = useCallback(
|
||||
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const offsetX = event.nativeEvent.contentOffset.x;
|
||||
// Track if we're at the left edge (with small threshold for float precision)
|
||||
setIsAtLeftEdge(offsetX <= 1);
|
||||
if (horizontalScroll) {
|
||||
horizontalScroll.registerScrollOffset(scrollId, offsetX);
|
||||
}
|
||||
},
|
||||
[horizontalScroll, scrollId],
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -364,29 +326,18 @@ function DiffFileBody({
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
horizontal
|
||||
nestedScrollEnabled
|
||||
showsHorizontalScrollIndicator
|
||||
bounces={false}
|
||||
<DiffScroll
|
||||
scrollViewWidth={scrollViewWidth}
|
||||
onScrollViewWidthChange={setScrollViewWidth}
|
||||
style={styles.diffContent}
|
||||
contentContainerStyle={styles.diffContentInner}
|
||||
onScroll={handleScroll}
|
||||
scrollEventThrottle={16}
|
||||
onLayout={(e) => setScrollViewWidth(e.nativeEvent.layout.width)}
|
||||
// When at left edge, wait for close gesture to fail before scrolling.
|
||||
// The close gesture fails quickly on leftward swipes (failOffsetX=-10),
|
||||
// so scrolling left works normally. On rightward swipes, close gesture
|
||||
// activates and closes the sidebar.
|
||||
waitFor={isAtLeftEdge && closeGestureRef?.current ? closeGestureRef : undefined}
|
||||
>
|
||||
<View
|
||||
style={[styles.linesContainer, scrollViewWidth > 0 && { minWidth: scrollViewWidth }]}
|
||||
>
|
||||
{linesContent}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</DiffScroll>
|
||||
);
|
||||
})()}
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user