mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-11 02:46:29 +00:00
* fix(app): prevent text selection during Android swipes Gesture interception did not cancel the native view where the touch began, leaving Android's pending text-selection action alive. * fix(app): restore iOS sidebar close swipes Fabric could fail to attach close handlers when their target lived inside retained but display-hidden panel content. Keep a registered native gesture host mounted outside the presentation wrapper so close swipes work over nested lists without activating rows. * fix(app): recover sidebar drags after cancellation Cancelled native drags could leave a lifted row and the shared outer list locked. Scope terminal cleanup to the owning gesture session and release the lock on cancellation or unmount. * test(app): clean up partial drag fixtures
126 lines
4.2 KiB
TypeScript
126 lines
4.2 KiB
TypeScript
import { useMemo, type ComponentProps, type ReactNode } from "react";
|
|
import { Pressable, StyleSheet, View } from "react-native";
|
|
import { GestureDetector, type GestureType } from "react-native-gesture-handler";
|
|
import Animated, { useAnimatedStyle } from "react-native-reanimated";
|
|
import { isWeb } from "@/constants/platform";
|
|
import { WindowChromeRootRegion } from "@/utils/desktop-window";
|
|
import { usePanelStore, type MobilePanelView } from "@/stores/panel-store";
|
|
import { getMobilePanelFrame } from "./model";
|
|
import { useIsMobilePanelPresented, useMobilePanelsRuntime } from "./provider";
|
|
|
|
type OverlayPanel = Exclude<MobilePanelView, "agent">;
|
|
|
|
interface MobilePanelOverlayProps {
|
|
children: ReactNode;
|
|
closeGesture: GestureType;
|
|
panel: OverlayPanel;
|
|
panelStyle?: ComponentProps<typeof Animated.View>["style"];
|
|
}
|
|
|
|
export function MobilePanelOverlay({
|
|
children,
|
|
closeGesture,
|
|
panel,
|
|
panelStyle,
|
|
}: MobilePanelOverlayProps) {
|
|
const { position, windowWidth } = useMobilePanelsRuntime();
|
|
const target = usePanelStore((state) => state.mobilePanel.target);
|
|
const showMobileAgent = usePanelStore((state) => state.showMobileAgent);
|
|
const isOpen = target === panel;
|
|
const isPresented = useIsMobilePanelPresented(panel);
|
|
const isLeft = panel === "agent-list";
|
|
|
|
const sidebarAnimatedStyle = useAnimatedStyle(() => {
|
|
const frame = getMobilePanelFrame(position.value, windowWidth);
|
|
return {
|
|
transform: [{ translateX: isLeft ? frame.leftTranslateX : frame.rightTranslateX }],
|
|
};
|
|
}, [isLeft, windowWidth]);
|
|
|
|
const backdropAnimatedStyle = useAnimatedStyle(() => {
|
|
const frame = getMobilePanelFrame(position.value, windowWidth);
|
|
return { opacity: isLeft ? frame.leftBackdropOpacity : frame.rightBackdropOpacity };
|
|
}, [isLeft, windowWidth]);
|
|
|
|
const overlayStyle = useMemo(
|
|
() => [styles.overlay, { display: isPresented ? ("flex" as const) : ("none" as const) }],
|
|
[isPresented],
|
|
);
|
|
const positionedPanelStyle = isLeft ? styles.leftPanel : styles.rightPanel;
|
|
const backdropStyle = useMemo(
|
|
() => [styles.backdrop, backdropAnimatedStyle],
|
|
[backdropAnimatedStyle],
|
|
);
|
|
const combinedPanelStyle = useMemo(
|
|
() => [
|
|
styles.panel,
|
|
positionedPanelStyle,
|
|
{ width: windowWidth },
|
|
panelStyle,
|
|
sidebarAnimatedStyle,
|
|
],
|
|
[panelStyle, positionedPanelStyle, sidebarAnimatedStyle, windowWidth],
|
|
);
|
|
let overlayPointerEvents: "auto" | "box-none" | "none";
|
|
if (!isWeb) {
|
|
overlayPointerEvents = "box-none";
|
|
} else {
|
|
overlayPointerEvents = isOpen ? "auto" : "none";
|
|
}
|
|
|
|
return (
|
|
<GestureDetector gesture={closeGesture} touchAction="pan-y">
|
|
{/* Fabric needs an always-mounted native host to attach the close handler while the
|
|
retained panel content is hidden. nativeID keeps that host registered. */}
|
|
<View
|
|
collapsable={false}
|
|
nativeID={`${panel}-gesture-host`}
|
|
pointerEvents={overlayPointerEvents}
|
|
style={styles.overlay}
|
|
>
|
|
<View style={overlayStyle} pointerEvents={overlayPointerEvents}>
|
|
<Pressable
|
|
accessibilityElementsHidden
|
|
importantForAccessibility="no-hide-descendants"
|
|
onPress={showMobileAgent}
|
|
pointerEvents={isOpen ? "auto" : "none"}
|
|
style={StyleSheet.absoluteFillObject}
|
|
testID={`${panel}-backdrop`}
|
|
>
|
|
<Animated.View pointerEvents="none" style={backdropStyle} />
|
|
</Pressable>
|
|
|
|
<Animated.View pointerEvents={isOpen ? "auto" : "none"} style={combinedPanelStyle}>
|
|
<WindowChromeRootRegion corners="both">{children}</WindowChromeRootRegion>
|
|
</Animated.View>
|
|
</View>
|
|
</View>
|
|
</GestureDetector>
|
|
);
|
|
}
|
|
|
|
// Reanimated owns only these static native styles and the derived transform.
|
|
// Theme values stay inline at call sites, avoiding Unistyles patching the same
|
|
// native node after Fabric commits.
|
|
const styles = StyleSheet.create({
|
|
overlay: {
|
|
...StyleSheet.absoluteFillObject,
|
|
},
|
|
backdrop: {
|
|
...StyleSheet.absoluteFillObject,
|
|
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
},
|
|
panel: {
|
|
position: "absolute",
|
|
top: 0,
|
|
bottom: 0,
|
|
overflow: "hidden",
|
|
},
|
|
leftPanel: {
|
|
left: 0,
|
|
},
|
|
rightPanel: {
|
|
right: 0,
|
|
},
|
|
});
|