mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Add oxlint-tsgolint and configure typescript/no-unnecessary-type-assertion to flag redundant `!` and `as Foo` casts. Type-aware mode is left off by default to keep `npm run lint` fast; the rule sits configured for when we turn type-aware on intentionally. Auto-fix removed ~283 redundant casts; two manual touch-ups: a real tsgolint false positive in split-container.tsx and a stale ChildProcess import after a double-cast collapsed.
162 lines
5.2 KiB
TypeScript
162 lines
5.2 KiB
TypeScript
import { useCallback, useEffect, useState, type ReactNode, type RefObject } from "react";
|
|
import {
|
|
type FlatList,
|
|
type LayoutChangeEvent,
|
|
type NativeScrollEvent,
|
|
type NativeSyntheticEvent,
|
|
type ScrollView,
|
|
} from "react-native";
|
|
import {
|
|
WebDesktopScrollbarOverlay,
|
|
useWebDesktopScrollbarMetrics,
|
|
type ScrollbarMetrics,
|
|
} from "./web-desktop-scrollbar";
|
|
import { isWeb as platformIsWeb } from "@/constants/platform";
|
|
|
|
const METRICS_EPSILON = 0.5;
|
|
const HIDE_SCROLLBAR_STYLE_ID = "paseo-hide-scrollbar";
|
|
|
|
function ensureHideScrollbarStyle(): void {
|
|
if (typeof document === "undefined") return;
|
|
if (document.getElementById(HIDE_SCROLLBAR_STYLE_ID)) return;
|
|
const style = document.createElement("style");
|
|
style.id = HIDE_SCROLLBAR_STYLE_ID;
|
|
style.textContent =
|
|
"[data-hide-scrollbar]::-webkit-scrollbar { display: none; width: 0; height: 0; }";
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
function metricsChanged(a: ScrollbarMetrics, b: ScrollbarMetrics): boolean {
|
|
return (
|
|
Math.abs(a.offset - b.offset) > METRICS_EPSILON ||
|
|
Math.abs(a.viewportSize - b.viewportSize) > METRICS_EPSILON ||
|
|
Math.abs(a.contentSize - b.contentSize) > METRICS_EPSILON
|
|
);
|
|
}
|
|
|
|
// ── DOM element scrollbar ────────────────────────────────────────────
|
|
// Fully automatic: listens to scroll/input/resize events on the element,
|
|
// hides the native scrollbar, and returns a themed overlay or null.
|
|
|
|
export function useWebElementScrollbar(
|
|
elementRef: RefObject<HTMLElement | null>,
|
|
options?: {
|
|
enabled?: boolean;
|
|
contentRef?: RefObject<HTMLElement | null>;
|
|
},
|
|
): ReactNode {
|
|
const enabled = (options?.enabled ?? true) && platformIsWeb;
|
|
const contentRef = options?.contentRef;
|
|
|
|
const [metrics, setMetrics] = useState<ScrollbarMetrics>({
|
|
offset: 0,
|
|
viewportSize: 0,
|
|
contentSize: 0,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!enabled) return;
|
|
const element = elementRef.current;
|
|
if (!element) return;
|
|
|
|
element.setAttribute("data-hide-scrollbar", "");
|
|
(
|
|
element.style as CSSStyleDeclaration & { scrollbarWidth: string; msOverflowStyle: string }
|
|
).scrollbarWidth = "none";
|
|
(
|
|
element.style as CSSStyleDeclaration & { scrollbarWidth: string; msOverflowStyle: string }
|
|
).msOverflowStyle = "none";
|
|
ensureHideScrollbarStyle();
|
|
|
|
function update() {
|
|
const el = elementRef.current;
|
|
if (!el) return;
|
|
const next: ScrollbarMetrics = {
|
|
offset: el.scrollTop,
|
|
viewportSize: el.clientHeight,
|
|
contentSize: el.scrollHeight,
|
|
};
|
|
setMetrics((prev) => (metricsChanged(prev, next) ? next : prev));
|
|
}
|
|
|
|
element.addEventListener("scroll", update, { passive: true });
|
|
|
|
const resizeObserver = new ResizeObserver(update);
|
|
resizeObserver.observe(element);
|
|
const contentElement = contentRef?.current;
|
|
if (contentElement) {
|
|
resizeObserver.observe(contentElement);
|
|
}
|
|
|
|
update();
|
|
|
|
return () => {
|
|
element.removeEventListener("scroll", update);
|
|
resizeObserver.disconnect();
|
|
element.removeAttribute("data-hide-scrollbar");
|
|
(
|
|
element.style as CSSStyleDeclaration & { scrollbarWidth: string; msOverflowStyle: string }
|
|
).scrollbarWidth = "";
|
|
(
|
|
element.style as CSSStyleDeclaration & { scrollbarWidth: string; msOverflowStyle: string }
|
|
).msOverflowStyle = "";
|
|
};
|
|
}, [contentRef, elementRef, enabled]);
|
|
|
|
const onScrollToOffset = useCallback(
|
|
(offset: number) => {
|
|
elementRef.current?.scrollTo({ top: offset, behavior: "auto" });
|
|
},
|
|
[elementRef],
|
|
);
|
|
|
|
if (!enabled) return null;
|
|
|
|
return (
|
|
<WebDesktopScrollbarOverlay enabled metrics={metrics} onScrollToOffset={onScrollToOffset} />
|
|
);
|
|
}
|
|
|
|
// ── RN ScrollView / FlatList scrollbar ───────────────────────────────
|
|
// Returns event handlers to wire onto your ScrollView/FlatList plus
|
|
// a renderable overlay. The overlay is null when disabled.
|
|
|
|
interface WebScrollViewScrollbar {
|
|
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
|
onLayout: (event: LayoutChangeEvent) => void;
|
|
onContentSizeChange: (width: number, height: number) => void;
|
|
overlay: ReactNode;
|
|
}
|
|
|
|
export function useWebScrollViewScrollbar(
|
|
scrollableRef: RefObject<ScrollView | FlatList | null>,
|
|
options?: { enabled?: boolean },
|
|
): WebScrollViewScrollbar {
|
|
const enabled = (options?.enabled ?? true) && platformIsWeb;
|
|
const metricsHook = useWebDesktopScrollbarMetrics();
|
|
|
|
const onScrollToOffset = useCallback(
|
|
(offset: number) => {
|
|
const scrollable = scrollableRef.current;
|
|
if (!scrollable) return;
|
|
if ("scrollToOffset" in scrollable) {
|
|
scrollable.scrollToOffset({ offset, animated: false });
|
|
} else {
|
|
scrollable.scrollTo({ y: offset, animated: false });
|
|
}
|
|
},
|
|
[scrollableRef],
|
|
);
|
|
|
|
const overlay: ReactNode = enabled ? (
|
|
<WebDesktopScrollbarOverlay enabled metrics={metricsHook} onScrollToOffset={onScrollToOffset} />
|
|
) : null;
|
|
|
|
return {
|
|
onScroll: metricsHook.onScroll,
|
|
onLayout: metricsHook.onLayout,
|
|
onContentSizeChange: metricsHook.onContentSizeChange,
|
|
overlay,
|
|
};
|
|
}
|