mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-28 12:12:47 +00:00
* perf(app): keep large diffs responsive Render file headers on the canvas and bound expensive text shaping to the active viewport while preserving sticky, hover, and horizontal-scroll behavior. Let native scroll cancellation arbitrate header presses so finger scrolling cannot expand files. * test(app): assert canvas diff header accessibility * test(app): assert canvas diff header semantics
74 lines
2.4 KiB
TypeScript
74 lines
2.4 KiB
TypeScript
import { useCallback, useRef } from "react";
|
|
import type { LayoutChangeEvent } from "react-native";
|
|
|
|
interface FileHeaderInteractionInput {
|
|
path: string;
|
|
enabled: boolean;
|
|
onSelect?: (path: string) => void;
|
|
onActivate?: (path: string) => void;
|
|
onLayout?: (height: number) => void;
|
|
}
|
|
|
|
export interface FileHeaderInteraction {
|
|
activate: () => void;
|
|
select: () => void;
|
|
onLayout: (event: LayoutChangeEvent) => void;
|
|
onPressIn: () => void;
|
|
onLongPress: () => void;
|
|
}
|
|
|
|
export type FileHeaderPressPhase = "idle" | "pressed" | "long" | "handled";
|
|
export type FileHeaderPressEvent =
|
|
| { type: "press-in" }
|
|
| { type: "press" }
|
|
| { type: "long-press" };
|
|
export type FileHeaderPressEffect = "none" | "select" | "activate";
|
|
|
|
export function reduceFileHeaderPress(
|
|
phase: FileHeaderPressPhase,
|
|
event: FileHeaderPressEvent,
|
|
): { phase: FileHeaderPressPhase; effect: FileHeaderPressEffect } {
|
|
if (event.type === "press-in") return { phase: "pressed", effect: "none" };
|
|
if (event.type === "long-press") return { phase: "long", effect: "select" };
|
|
return phase === "long" || phase === "handled"
|
|
? { phase: "handled", effect: "none" }
|
|
: { phase: "handled", effect: "activate" };
|
|
}
|
|
|
|
export function useFileHeaderInteraction(input: FileHeaderInteractionInput): FileHeaderInteraction {
|
|
const { path, enabled, onSelect, onActivate, onLayout: notifyLayout } = input;
|
|
const phaseRef = useRef<FileHeaderPressPhase>("idle");
|
|
|
|
const select = useCallback(() => {
|
|
if (enabled) onSelect?.(path);
|
|
}, [enabled, onSelect, path]);
|
|
|
|
const activate = useCallback(() => {
|
|
if (!enabled) return;
|
|
const transition = reduceFileHeaderPress(phaseRef.current, { type: "press" });
|
|
phaseRef.current = transition.phase;
|
|
if (transition.effect !== "activate") return;
|
|
select();
|
|
onActivate?.(path);
|
|
}, [enabled, onActivate, path, select]);
|
|
|
|
const onLayout = useCallback(
|
|
(event: LayoutChangeEvent) => {
|
|
notifyLayout?.(event.nativeEvent.layout.height);
|
|
},
|
|
[notifyLayout],
|
|
);
|
|
|
|
const onPressIn = useCallback(() => {
|
|
phaseRef.current = reduceFileHeaderPress(phaseRef.current, { type: "press-in" }).phase;
|
|
}, []);
|
|
|
|
const onLongPress = useCallback(() => {
|
|
const transition = reduceFileHeaderPress(phaseRef.current, { type: "long-press" });
|
|
phaseRef.current = transition.phase;
|
|
if (transition.effect === "select") select();
|
|
}, [select]);
|
|
|
|
return { activate, select, onLayout, onPressIn, onLongPress };
|
|
}
|