fix(app): isolate bottom sheet modal scopes
This commit is contained in:
@@ -10,7 +10,6 @@ import {
|
|||||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||||
import { KeyboardProvider } from "react-native-keyboard-controller";
|
import { KeyboardProvider } from "react-native-keyboard-controller";
|
||||||
import { GestureHandlerRootView, Gesture, GestureDetector } from "react-native-gesture-handler";
|
import { GestureHandlerRootView, Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||||
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
|
|
||||||
import { PortalProvider } from "@gorhom/portal";
|
import { PortalProvider } from "@gorhom/portal";
|
||||||
import { VoiceProvider } from "@/contexts/voice-context";
|
import { VoiceProvider } from "@/contexts/voice-context";
|
||||||
import { useAppSettings } from "@/hooks/use-settings";
|
import { useAppSettings } from "@/hooks/use-settings";
|
||||||
@@ -898,23 +897,21 @@ export default function RootLayout() {
|
|||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<KeyboardProvider>
|
<KeyboardProvider>
|
||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
<BottomSheetModalProvider>
|
<HostRuntimeBootstrapProvider>
|
||||||
<HostRuntimeBootstrapProvider>
|
<PushNotificationRouter />
|
||||||
<PushNotificationRouter />
|
<ToastProvider>
|
||||||
<ToastProvider>
|
<ProvidersWrapper>
|
||||||
<ProvidersWrapper>
|
<SidebarAnimationProvider>
|
||||||
<SidebarAnimationProvider>
|
<HorizontalScrollProvider>
|
||||||
<HorizontalScrollProvider>
|
<OpenProjectListener />
|
||||||
<OpenProjectListener />
|
<AppWithSidebar>
|
||||||
<AppWithSidebar>
|
<RootStack />
|
||||||
<RootStack />
|
</AppWithSidebar>
|
||||||
</AppWithSidebar>
|
</HorizontalScrollProvider>
|
||||||
</HorizontalScrollProvider>
|
</SidebarAnimationProvider>
|
||||||
</SidebarAnimationProvider>
|
</ProvidersWrapper>
|
||||||
</ProvidersWrapper>
|
</ToastProvider>
|
||||||
</ToastProvider>
|
</HostRuntimeBootstrapProvider>
|
||||||
</HostRuntimeBootstrapProvider>
|
|
||||||
</BottomSheetModalProvider>
|
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
</KeyboardProvider>
|
</KeyboardProvider>
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { forwardRef, useCallback, useEffect, useMemo, useRef } from "react";
|
import { forwardRef, useCallback, useEffect, useMemo } from "react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { Modal, Pressable, ScrollView, Text, TextInput, View } from "react-native";
|
import { Modal, Pressable, ScrollView, Text, TextInput, View } from "react-native";
|
||||||
@@ -7,7 +7,6 @@ import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|||||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { getOverlayRoot, OVERLAY_Z } from "../lib/overlay-root";
|
import { getOverlayRoot, OVERLAY_Z } from "../lib/overlay-root";
|
||||||
import {
|
import {
|
||||||
BottomSheetModal,
|
|
||||||
BottomSheetBackdrop,
|
BottomSheetBackdrop,
|
||||||
BottomSheetScrollView,
|
BottomSheetScrollView,
|
||||||
BottomSheetTextInput,
|
BottomSheetTextInput,
|
||||||
@@ -16,6 +15,10 @@ import {
|
|||||||
import { X } from "lucide-react-native";
|
import { X } from "lucide-react-native";
|
||||||
import { FileDropZone } from "@/components/file-drop-zone";
|
import { FileDropZone } from "@/components/file-drop-zone";
|
||||||
import type { ImageAttachment } from "@/components/message-input";
|
import type { ImageAttachment } from "@/components/message-input";
|
||||||
|
import {
|
||||||
|
IsolatedBottomSheetModal,
|
||||||
|
useIsolatedBottomSheetVisibility,
|
||||||
|
} from "@/components/ui/isolated-bottom-sheet-modal";
|
||||||
import { isWeb } from "@/constants/platform";
|
import { isWeb } from "@/constants/platform";
|
||||||
|
|
||||||
type EscHandler = () => void;
|
type EscHandler = () => void;
|
||||||
@@ -163,7 +166,6 @@ export interface AdaptiveModalSheetProps {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
headerActions?: ReactNode;
|
headerActions?: ReactNode;
|
||||||
snapPoints?: string[];
|
snapPoints?: string[];
|
||||||
stackBehavior?: "push" | "switch" | "replace";
|
|
||||||
testID?: string;
|
testID?: string;
|
||||||
/** Override the max width of the desktop card. */
|
/** Override the max width of the desktop card. */
|
||||||
desktopMaxWidth?: number;
|
desktopMaxWidth?: number;
|
||||||
@@ -180,7 +182,6 @@ export function AdaptiveModalSheet({
|
|||||||
children,
|
children,
|
||||||
headerActions,
|
headerActions,
|
||||||
snapPoints,
|
snapPoints,
|
||||||
stackBehavior,
|
|
||||||
testID,
|
testID,
|
||||||
desktopMaxWidth,
|
desktopMaxWidth,
|
||||||
onFilesDropped,
|
onFilesDropped,
|
||||||
@@ -188,33 +189,13 @@ export function AdaptiveModalSheet({
|
|||||||
}: AdaptiveModalSheetProps) {
|
}: AdaptiveModalSheetProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const isMobile = useIsCompactFormFactor();
|
const isMobile = useIsCompactFormFactor();
|
||||||
const sheetRef = useRef<BottomSheetModal>(null);
|
const titleColor = theme.colors.foreground;
|
||||||
const dismissingForVisibilityRef = useRef(false);
|
|
||||||
const resolvedSnapPoints = useMemo(() => snapPoints ?? ["65%", "90%"], [snapPoints]);
|
const resolvedSnapPoints = useMemo(() => snapPoints ?? ["65%", "90%"], [snapPoints]);
|
||||||
|
const { sheetRef, handleSheetChange } = useIsolatedBottomSheetVisibility({
|
||||||
useEffect(() => {
|
visible,
|
||||||
if (!isMobile) return;
|
isEnabled: isMobile,
|
||||||
if (visible) {
|
onClose,
|
||||||
dismissingForVisibilityRef.current = false;
|
});
|
||||||
sheetRef.current?.present();
|
|
||||||
} else {
|
|
||||||
dismissingForVisibilityRef.current = true;
|
|
||||||
sheetRef.current?.dismiss();
|
|
||||||
}
|
|
||||||
}, [visible, isMobile]);
|
|
||||||
|
|
||||||
const handleSheetChange = useCallback(
|
|
||||||
(index: number) => {
|
|
||||||
if (index === -1) {
|
|
||||||
if (dismissingForVisibilityRef.current) {
|
|
||||||
dismissingForVisibilityRef.current = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onClose],
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderBackdrop = useCallback(
|
const renderBackdrop = useCallback(
|
||||||
(props: React.ComponentProps<typeof BottomSheetBackdrop>) => (
|
(props: React.ComponentProps<typeof BottomSheetBackdrop>) => (
|
||||||
@@ -230,7 +211,7 @@ export function AdaptiveModalSheet({
|
|||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return (
|
return (
|
||||||
<BottomSheetModal
|
<IsolatedBottomSheetModal
|
||||||
ref={sheetRef}
|
ref={sheetRef}
|
||||||
snapPoints={resolvedSnapPoints}
|
snapPoints={resolvedSnapPoints}
|
||||||
index={0}
|
index={0}
|
||||||
@@ -238,7 +219,6 @@ export function AdaptiveModalSheet({
|
|||||||
onChange={handleSheetChange}
|
onChange={handleSheetChange}
|
||||||
backdropComponent={renderBackdrop}
|
backdropComponent={renderBackdrop}
|
||||||
enablePanDownToClose
|
enablePanDownToClose
|
||||||
stackBehavior={stackBehavior}
|
|
||||||
backgroundComponent={SheetBackground}
|
backgroundComponent={SheetBackground}
|
||||||
handleIndicatorStyle={{ backgroundColor: theme.colors.surface2 }}
|
handleIndicatorStyle={{ backgroundColor: theme.colors.surface2 }}
|
||||||
keyboardBehavior="extend"
|
keyboardBehavior="extend"
|
||||||
@@ -247,7 +227,7 @@ export function AdaptiveModalSheet({
|
|||||||
>
|
>
|
||||||
<View style={styles.bottomSheetHeader} testID={testID}>
|
<View style={styles.bottomSheetHeader} testID={testID}>
|
||||||
<View style={styles.headerTitleGroup}>
|
<View style={styles.headerTitleGroup}>
|
||||||
<Text style={[styles.title, { color: theme.colors.foreground }]} numberOfLines={1}>
|
<Text key={titleColor} style={[styles.title, { color: titleColor }]} numberOfLines={1}>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
{subtitle}
|
{subtitle}
|
||||||
@@ -268,7 +248,7 @@ export function AdaptiveModalSheet({
|
|||||||
) : (
|
) : (
|
||||||
<View style={styles.bottomSheetStaticContent}>{children}</View>
|
<View style={styles.bottomSheetStaticContent}>{children}</View>
|
||||||
)}
|
)}
|
||||||
</BottomSheetModal>
|
</IsolatedBottomSheetModal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +256,7 @@ export function AdaptiveModalSheet({
|
|||||||
<>
|
<>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View style={styles.headerTitleGroup}>
|
<View style={styles.headerTitleGroup}>
|
||||||
<Text style={[styles.title, { color: theme.colors.foreground }]} numberOfLines={1}>
|
<Text key={titleColor} style={[styles.title, { color: titleColor }]} numberOfLines={1}>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
{subtitle}
|
{subtitle}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { View, Text, Pressable, TextInput, ActivityIndicator } from "react-nativ
|
|||||||
import type { StyleProp, ViewStyle, TextProps } from "react-native";
|
import type { StyleProp, ViewStyle, TextProps } from "react-native";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import {
|
import {
|
||||||
BottomSheetModal,
|
|
||||||
BottomSheetScrollView,
|
BottomSheetScrollView,
|
||||||
BottomSheetBackdrop,
|
BottomSheetBackdrop,
|
||||||
BottomSheetBackgroundProps,
|
BottomSheetBackgroundProps,
|
||||||
@@ -30,6 +29,10 @@ import type {
|
|||||||
import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest";
|
import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest";
|
||||||
import { getModeVisuals, type AgentModeIcon } from "@server/server/agent/provider-manifest";
|
import { getModeVisuals, type AgentModeIcon } from "@server/server/agent/provider-manifest";
|
||||||
import { Combobox, ComboboxItem, ComboboxEmpty } from "@/components/ui/combobox";
|
import { Combobox, ComboboxItem, ComboboxEmpty } from "@/components/ui/combobox";
|
||||||
|
import {
|
||||||
|
IsolatedBottomSheetModal,
|
||||||
|
useIsolatedBottomSheetVisibility,
|
||||||
|
} from "@/components/ui/isolated-bottom-sheet-modal";
|
||||||
import { baseColors } from "@/styles/theme";
|
import { baseColors } from "@/styles/theme";
|
||||||
import { isNative } from "@/constants/platform";
|
import { isNative } from "@/constants/platform";
|
||||||
|
|
||||||
@@ -256,31 +259,17 @@ export function DropdownSheet({
|
|||||||
children,
|
children,
|
||||||
}: DropdownSheetProps): ReactElement {
|
}: DropdownSheetProps): ReactElement {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const bottomSheetRef = useRef<BottomSheetModal>(null);
|
const titleColor = theme.colors.foreground;
|
||||||
const snapPoints = useMemo(() => ["60%", "90%"], []);
|
const snapPoints = useMemo(() => ["60%", "90%"], []);
|
||||||
|
const { sheetRef, handleSheetChange } = useIsolatedBottomSheetVisibility({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
});
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
bottomSheetRef.current?.dismiss();
|
|
||||||
onClose();
|
onClose();
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (visible) {
|
|
||||||
bottomSheetRef.current?.present();
|
|
||||||
} else {
|
|
||||||
bottomSheetRef.current?.dismiss();
|
|
||||||
}
|
|
||||||
}, [visible]);
|
|
||||||
|
|
||||||
const handleSheetChange = useCallback(
|
|
||||||
(index: number) => {
|
|
||||||
if (index === -1) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onClose],
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderBackdrop = useCallback(
|
const renderBackdrop = useCallback(
|
||||||
(props: React.ComponentProps<typeof BottomSheetBackdrop>) => (
|
(props: React.ComponentProps<typeof BottomSheetBackdrop>) => (
|
||||||
<BottomSheetBackdrop {...props} disappearsOnIndex={-1} appearsOnIndex={0} opacity={0.45} />
|
<BottomSheetBackdrop {...props} disappearsOnIndex={-1} appearsOnIndex={0} opacity={0.45} />
|
||||||
@@ -289,8 +278,8 @@ export function DropdownSheet({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BottomSheetModal
|
<IsolatedBottomSheetModal
|
||||||
ref={bottomSheetRef}
|
ref={sheetRef}
|
||||||
snapPoints={snapPoints}
|
snapPoints={snapPoints}
|
||||||
index={0}
|
index={0}
|
||||||
enableDynamicSizing={false}
|
enableDynamicSizing={false}
|
||||||
@@ -303,7 +292,9 @@ export function DropdownSheet({
|
|||||||
keyboardBlurBehavior="restore"
|
keyboardBlurBehavior="restore"
|
||||||
>
|
>
|
||||||
<View style={styles.bottomSheetHeader}>
|
<View style={styles.bottomSheetHeader}>
|
||||||
<Text style={[styles.dropdownSheetTitle, { color: theme.colors.foreground }]}>{title}</Text>
|
<Text key={titleColor} style={[styles.dropdownSheetTitle, { color: titleColor }]}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Close sheet"
|
accessibilityLabel="Close sheet"
|
||||||
@@ -321,7 +312,7 @@ export function DropdownSheet({
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</BottomSheetScrollView>
|
</BottomSheetScrollView>
|
||||||
</BottomSheetModal>
|
</IsolatedBottomSheetModal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1229,7 +1220,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
dropdownSheetTitle: {
|
dropdownSheetTitle: {
|
||||||
fontSize: theme.fontSize.lg,
|
fontSize: theme.fontSize.lg,
|
||||||
fontWeight: theme.fontWeight.semibold,
|
fontWeight: theme.fontWeight.semibold,
|
||||||
color: theme.colors.foreground,
|
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
dropdownSheetScrollContent: {
|
dropdownSheetScrollContent: {
|
||||||
|
|||||||
@@ -630,7 +630,6 @@ function ControlledStatusBar({
|
|||||||
title="Preferences"
|
title="Preferences"
|
||||||
visible={prefsOpen}
|
visible={prefsOpen}
|
||||||
onClose={() => setPrefsOpen(false)}
|
onClose={() => setPrefsOpen(false)}
|
||||||
stackBehavior="replace"
|
|
||||||
testID="agent-preferences-sheet"
|
testID="agent-preferences-sheet"
|
||||||
>
|
>
|
||||||
{canSelectModel ? (
|
{canSelectModel ? (
|
||||||
|
|||||||
@@ -657,7 +657,6 @@ export function CombinedModelSelector({
|
|||||||
onSelect={() => {}}
|
onSelect={() => {}}
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
stackBehavior="push"
|
|
||||||
anchorRef={anchorRef}
|
anchorRef={anchorRef}
|
||||||
desktopPlacement="top-start"
|
desktopPlacement="top-start"
|
||||||
desktopMinWidth={360}
|
desktopMinWidth={360}
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ import { View, Text, Pressable } from "react-native";
|
|||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import Animated from "react-native-reanimated";
|
import Animated from "react-native-reanimated";
|
||||||
import {
|
import {
|
||||||
BottomSheetModal,
|
|
||||||
BottomSheetScrollView,
|
BottomSheetScrollView,
|
||||||
BottomSheetBackdrop,
|
BottomSheetBackdrop,
|
||||||
BottomSheetBackgroundProps,
|
BottomSheetBackgroundProps,
|
||||||
} from "@gorhom/bottom-sheet";
|
} from "@gorhom/bottom-sheet";
|
||||||
import { X } from "lucide-react-native";
|
import { X } from "lucide-react-native";
|
||||||
import type { ToolCallDetail } from "@server/server/agent/agent-sdk-types";
|
import type { ToolCallDetail } from "@server/server/agent/agent-sdk-types";
|
||||||
|
import {
|
||||||
|
IsolatedBottomSheetModal,
|
||||||
|
type IsolatedBottomSheetModalRef,
|
||||||
|
} from "@/components/ui/isolated-bottom-sheet-modal";
|
||||||
import { resolveToolCallIcon } from "@/utils/tool-call-icon";
|
import { resolveToolCallIcon } from "@/utils/tool-call-icon";
|
||||||
import { ToolCallDetailsContent } from "./tool-call-details";
|
import { ToolCallDetailsContent } from "./tool-call-details";
|
||||||
|
|
||||||
@@ -60,18 +63,25 @@ interface ToolCallSheetProviderProps {
|
|||||||
|
|
||||||
export function ToolCallSheetProvider({ children }: ToolCallSheetProviderProps) {
|
export function ToolCallSheetProvider({ children }: ToolCallSheetProviderProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const bottomSheetRef = useRef<BottomSheetModal>(null);
|
const bottomSheetRef = useRef<IsolatedBottomSheetModalRef>(null);
|
||||||
|
const hasPresentedRef = useRef(false);
|
||||||
const [sheetData, setSheetData] = React.useState<ToolCallSheetData | null>(null);
|
const [sheetData, setSheetData] = React.useState<ToolCallSheetData | null>(null);
|
||||||
|
|
||||||
const snapPoints = useMemo(() => ["60%", "95%"], []);
|
const snapPoints = useMemo(() => ["60%", "95%"], []);
|
||||||
|
|
||||||
const openToolCall = useCallback((data: ToolCallSheetData) => {
|
const openToolCall = useCallback((data: ToolCallSheetData) => {
|
||||||
setSheetData(data);
|
setSheetData(data);
|
||||||
|
if (hasPresentedRef.current) {
|
||||||
|
requestAnimationFrame(() => bottomSheetRef.current?.snapToIndex(0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPresentedRef.current = true;
|
||||||
bottomSheetRef.current?.present();
|
bottomSheetRef.current?.present();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const closeToolCall = useCallback(() => {
|
const closeToolCall = useCallback(() => {
|
||||||
bottomSheetRef.current?.dismiss();
|
bottomSheetRef.current?.close();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSheetChange = useCallback((index: number) => {
|
const handleSheetChange = useCallback((index: number) => {
|
||||||
@@ -95,11 +105,10 @@ export function ToolCallSheetProvider({ children }: ToolCallSheetProviderProps)
|
|||||||
return (
|
return (
|
||||||
<ToolCallSheetContext.Provider value={contextValue}>
|
<ToolCallSheetContext.Provider value={contextValue}>
|
||||||
{children}
|
{children}
|
||||||
<BottomSheetModal
|
<IsolatedBottomSheetModal
|
||||||
ref={bottomSheetRef}
|
ref={bottomSheetRef}
|
||||||
snapPoints={snapPoints}
|
snapPoints={snapPoints}
|
||||||
index={0}
|
index={0}
|
||||||
stackBehavior="replace"
|
|
||||||
enableDynamicSizing={false}
|
enableDynamicSizing={false}
|
||||||
onChange={handleSheetChange}
|
onChange={handleSheetChange}
|
||||||
backdropComponent={renderBackdrop}
|
backdropComponent={renderBackdrop}
|
||||||
@@ -108,7 +117,7 @@ export function ToolCallSheetProvider({ children }: ToolCallSheetProviderProps)
|
|||||||
handleIndicatorStyle={{ backgroundColor: theme.colors.palette.zinc[600] }}
|
handleIndicatorStyle={{ backgroundColor: theme.colors.palette.zinc[600] }}
|
||||||
>
|
>
|
||||||
{sheetData && <ToolCallSheetContent data={sheetData} onClose={closeToolCall} />}
|
{sheetData && <ToolCallSheetContent data={sheetData} onClose={closeToolCall} />}
|
||||||
</BottomSheetModal>
|
</IsolatedBottomSheetModal>
|
||||||
</ToolCallSheetContext.Provider>
|
</ToolCallSheetContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import {
|
import {
|
||||||
BottomSheetModal,
|
|
||||||
BottomSheetScrollView,
|
BottomSheetScrollView,
|
||||||
BottomSheetBackdrop,
|
BottomSheetBackdrop,
|
||||||
BottomSheetTextInput,
|
BottomSheetTextInput,
|
||||||
@@ -38,6 +37,10 @@ import {
|
|||||||
} from "./combobox-options";
|
} from "./combobox-options";
|
||||||
import type { ComboboxOptionModel } from "./combobox-options";
|
import type { ComboboxOptionModel } from "./combobox-options";
|
||||||
import { isWeb } from "@/constants/platform";
|
import { isWeb } from "@/constants/platform";
|
||||||
|
import {
|
||||||
|
IsolatedBottomSheetModal,
|
||||||
|
useIsolatedBottomSheetVisibility,
|
||||||
|
} from "./isolated-bottom-sheet-modal";
|
||||||
|
|
||||||
const IS_WEB = isWeb;
|
const IS_WEB = isWeb;
|
||||||
|
|
||||||
@@ -66,8 +69,6 @@ export interface ComboboxProps {
|
|||||||
title?: string;
|
title?: string;
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
onOpenChange?: (open: boolean) => void;
|
onOpenChange?: (open: boolean) => void;
|
||||||
enableDismissOnClose?: boolean;
|
|
||||||
stackBehavior?: "push" | "switch" | "replace";
|
|
||||||
desktopPlacement?: "top-start" | "bottom-start";
|
desktopPlacement?: "top-start" | "bottom-start";
|
||||||
/**
|
/**
|
||||||
* Prevents an initial frame at 0,0 by hiding desktop content until floating
|
* Prevents an initial frame at 0,0 by hiding desktop content until floating
|
||||||
@@ -271,8 +272,6 @@ export function Combobox({
|
|||||||
title = "Select",
|
title = "Select",
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
enableDismissOnClose,
|
|
||||||
stackBehavior,
|
|
||||||
desktopPlacement = "top-start",
|
desktopPlacement = "top-start",
|
||||||
desktopPreventInitialFlash = true,
|
desktopPreventInitialFlash = true,
|
||||||
desktopMinWidth,
|
desktopMinWidth,
|
||||||
@@ -284,11 +283,10 @@ export function Combobox({
|
|||||||
}: ComboboxProps): ReactElement {
|
}: ComboboxProps): ReactElement {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const isMobile = useIsCompactFormFactor();
|
const isMobile = useIsCompactFormFactor();
|
||||||
|
const titleColor = theme.colors.foreground;
|
||||||
const effectiveOptionsPosition = isMobile ? "below-search" : optionsPosition;
|
const effectiveOptionsPosition = isMobile ? "below-search" : optionsPosition;
|
||||||
const isDesktopAboveSearch = !isMobile && isWeb && effectiveOptionsPosition === "above-search";
|
const isDesktopAboveSearch = !isMobile && isWeb && effectiveOptionsPosition === "above-search";
|
||||||
const { height: windowHeight, width: windowWidth } = useWindowDimensions();
|
const { height: windowHeight, width: windowWidth } = useWindowDimensions();
|
||||||
const bottomSheetRef = useRef<BottomSheetModal>(null);
|
|
||||||
const hasPresentedBottomSheetRef = useRef(false);
|
|
||||||
const snapPoints = useMemo(() => ["60%", "90%"], []);
|
const snapPoints = useMemo(() => ["60%", "90%"], []);
|
||||||
const [availableSize, setAvailableSize] = useState<{ width?: number; height?: number } | null>(
|
const [availableSize, setAvailableSize] = useState<{ width?: number; height?: number } | null>(
|
||||||
null,
|
null,
|
||||||
@@ -474,32 +472,11 @@ export function Combobox({
|
|||||||
}
|
}
|
||||||
: floatingStyles;
|
: floatingStyles;
|
||||||
|
|
||||||
useEffect(() => {
|
const { sheetRef: bottomSheetRef, handleSheetChange } = useIsolatedBottomSheetVisibility({
|
||||||
if (!isMobile) return;
|
visible: isOpen,
|
||||||
if (isOpen) {
|
isEnabled: isMobile,
|
||||||
if (enableDismissOnClose === false && hasPresentedBottomSheetRef.current) {
|
onClose: handleClose,
|
||||||
bottomSheetRef.current?.snapToIndex(0);
|
});
|
||||||
} else {
|
|
||||||
hasPresentedBottomSheetRef.current = true;
|
|
||||||
bottomSheetRef.current?.present();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (enableDismissOnClose === false) {
|
|
||||||
bottomSheetRef.current?.close();
|
|
||||||
} else {
|
|
||||||
bottomSheetRef.current?.dismiss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [enableDismissOnClose, isOpen, isMobile]);
|
|
||||||
|
|
||||||
const handleSheetChange = useCallback(
|
|
||||||
(index: number) => {
|
|
||||||
if (index === -1) {
|
|
||||||
handleClose();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[handleClose],
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderBackdrop = useCallback(
|
const renderBackdrop = useCallback(
|
||||||
(props: React.ComponentProps<typeof BottomSheetBackdrop>) => (
|
(props: React.ComponentProps<typeof BottomSheetBackdrop>) => (
|
||||||
@@ -718,7 +695,7 @@ export function Combobox({
|
|||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return (
|
return (
|
||||||
<BottomSheetModal
|
<IsolatedBottomSheetModal
|
||||||
ref={bottomSheetRef}
|
ref={bottomSheetRef}
|
||||||
snapPoints={snapPoints}
|
snapPoints={snapPoints}
|
||||||
index={0}
|
index={0}
|
||||||
@@ -726,15 +703,15 @@ export function Combobox({
|
|||||||
onChange={handleSheetChange}
|
onChange={handleSheetChange}
|
||||||
backdropComponent={renderBackdrop}
|
backdropComponent={renderBackdrop}
|
||||||
enablePanDownToClose
|
enablePanDownToClose
|
||||||
enableDismissOnClose={enableDismissOnClose}
|
|
||||||
stackBehavior={stackBehavior}
|
|
||||||
backgroundComponent={ComboboxSheetBackground}
|
backgroundComponent={ComboboxSheetBackground}
|
||||||
handleIndicatorStyle={{ backgroundColor: theme.colors.palette.zinc[600] }}
|
handleIndicatorStyle={{ backgroundColor: theme.colors.palette.zinc[600] }}
|
||||||
keyboardBehavior="extend"
|
keyboardBehavior="extend"
|
||||||
keyboardBlurBehavior="restore"
|
keyboardBlurBehavior="restore"
|
||||||
>
|
>
|
||||||
<View style={styles.bottomSheetHeader}>
|
<View style={styles.bottomSheetHeader}>
|
||||||
<Text style={[styles.comboboxTitle, { color: theme.colors.foreground }]}>{title}</Text>
|
<Text key={titleColor} style={[styles.comboboxTitle, { color: titleColor }]}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{stickyHeader}
|
{stickyHeader}
|
||||||
{!children && searchable ? searchInput : null}
|
{!children && searchable ? searchInput : null}
|
||||||
@@ -745,7 +722,7 @@ export function Combobox({
|
|||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</BottomSheetScrollView>
|
</BottomSheetScrollView>
|
||||||
</BottomSheetModal>
|
</IsolatedBottomSheetModal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -931,7 +908,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
comboboxTitle: {
|
comboboxTitle: {
|
||||||
fontSize: theme.fontSize.lg,
|
fontSize: theme.fontSize.lg,
|
||||||
fontWeight: theme.fontWeight.medium,
|
fontWeight: theme.fontWeight.medium,
|
||||||
color: theme.colors.foreground,
|
|
||||||
textAlign: "left",
|
textAlign: "left",
|
||||||
},
|
},
|
||||||
comboboxScrollContent: {
|
comboboxScrollContent: {
|
||||||
|
|||||||
@@ -30,8 +30,12 @@ import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
|
|||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { Check, CheckCircle } from "lucide-react-native";
|
import { Check, CheckCircle } from "lucide-react-native";
|
||||||
import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from "@gorhom/bottom-sheet";
|
import { BottomSheetBackdrop, BottomSheetScrollView } from "@gorhom/bottom-sheet";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import {
|
||||||
|
IsolatedBottomSheetModal,
|
||||||
|
useIsolatedBottomSheetVisibility,
|
||||||
|
} from "@/components/ui/isolated-bottom-sheet-modal";
|
||||||
import { isWeb, isNative } from "@/constants/platform";
|
import { isWeb, isNative } from "@/constants/platform";
|
||||||
import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style";
|
import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style";
|
||||||
|
|
||||||
@@ -354,7 +358,6 @@ export function ContextMenuContent({
|
|||||||
const isMobile = useIsCompactFormFactor();
|
const isMobile = useIsCompactFormFactor();
|
||||||
const useMobileSheet = isMobile && mobileMode === "sheet";
|
const useMobileSheet = isMobile && mobileMode === "sheet";
|
||||||
const { open, setOpen, triggerRef, anchorRect } = context;
|
const { open, setOpen, triggerRef, anchorRect } = context;
|
||||||
const bottomSheetRef = useRef<BottomSheetModal>(null);
|
|
||||||
const sheetSnapPoints = useMemo(() => ["30%", "55%"], []);
|
const sheetSnapPoints = useMemo(() => ["30%", "55%"], []);
|
||||||
const [triggerRect, setTriggerRect] = useState<Rect | null>(null);
|
const [triggerRect, setTriggerRect] = useState<Rect | null>(null);
|
||||||
const [contentSize, setContentSize] = useState<{ width: number; height: number } | null>(null);
|
const [contentSize, setContentSize] = useState<{ width: number; height: number } | null>(null);
|
||||||
@@ -364,23 +367,11 @@ export function ContextMenuContent({
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
}, [setOpen]);
|
}, [setOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
const { sheetRef: bottomSheetRef, handleSheetChange } = useIsolatedBottomSheetVisibility({
|
||||||
if (!useMobileSheet) return;
|
visible: open,
|
||||||
if (open) {
|
isEnabled: useMobileSheet,
|
||||||
bottomSheetRef.current?.present();
|
onClose: handleClose,
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
bottomSheetRef.current?.dismiss();
|
|
||||||
}, [open, useMobileSheet]);
|
|
||||||
|
|
||||||
const handleSheetChange = useCallback(
|
|
||||||
(index: number) => {
|
|
||||||
if (index === -1) {
|
|
||||||
handleClose();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[handleClose],
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderSheetBackdrop = useCallback(
|
const renderSheetBackdrop = useCallback(
|
||||||
(props: ComponentProps<typeof BottomSheetBackdrop>) => (
|
(props: ComponentProps<typeof BottomSheetBackdrop>) => (
|
||||||
@@ -468,7 +459,7 @@ export function ContextMenuContent({
|
|||||||
if (useMobileSheet) {
|
if (useMobileSheet) {
|
||||||
return (
|
return (
|
||||||
<ContextMenuContext.Provider value={context}>
|
<ContextMenuContext.Provider value={context}>
|
||||||
<BottomSheetModal
|
<IsolatedBottomSheetModal
|
||||||
ref={bottomSheetRef}
|
ref={bottomSheetRef}
|
||||||
index={0}
|
index={0}
|
||||||
snapPoints={sheetSnapPoints}
|
snapPoints={sheetSnapPoints}
|
||||||
@@ -495,7 +486,7 @@ export function ContextMenuContent({
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</BottomSheetScrollView>
|
</BottomSheetScrollView>
|
||||||
</BottomSheetModal>
|
</IsolatedBottomSheetModal>
|
||||||
</ContextMenuContext.Provider>
|
</ContextMenuContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
import { cleanup, render } from "@testing-library/react";
|
||||||
|
import React from "react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
IsolatedBottomSheetModal,
|
||||||
|
useIsolatedBottomSheetVisibility,
|
||||||
|
} from "./isolated-bottom-sheet-modal";
|
||||||
|
|
||||||
|
const { modalMethods, modalProps } = vi.hoisted(() => ({
|
||||||
|
modalMethods: {
|
||||||
|
present: vi.fn(),
|
||||||
|
close: vi.fn(),
|
||||||
|
snapToIndex: vi.fn(),
|
||||||
|
dismiss: vi.fn(),
|
||||||
|
},
|
||||||
|
modalProps: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@gorhom/bottom-sheet", async () => {
|
||||||
|
const React = await import("react");
|
||||||
|
const MockBottomSheetModal = React.forwardRef(
|
||||||
|
(props: Record<string, unknown>, ref: React.ForwardedRef<unknown>) => {
|
||||||
|
modalProps(props);
|
||||||
|
React.useImperativeHandle(ref, () => modalMethods);
|
||||||
|
return React.createElement(
|
||||||
|
"div",
|
||||||
|
{ "data-testid": "bottom-sheet" },
|
||||||
|
props.children as ReactNode,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
BottomSheetModal: MockBottomSheetModal,
|
||||||
|
BottomSheetModalProvider: ({ children }: { children: ReactNode }) =>
|
||||||
|
React.createElement("div", { "data-testid": "bottom-sheet-provider" }, children),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@gorhom/portal", async () => {
|
||||||
|
const React = await import("react");
|
||||||
|
return {
|
||||||
|
Portal: ({ children, hostName }: { children: ReactNode; hostName?: string }) =>
|
||||||
|
React.createElement("div", { "data-host": hostName, "data-testid": "app-portal" }, children),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function Harness({ visible, onClose }: { visible: boolean; onClose: () => void }) {
|
||||||
|
const { sheetRef, handleSheetChange } = useIsolatedBottomSheetVisibility({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IsolatedBottomSheetModal
|
||||||
|
ref={sheetRef}
|
||||||
|
index={0}
|
||||||
|
snapPoints={["50%"]}
|
||||||
|
onChange={handleSheetChange}
|
||||||
|
>
|
||||||
|
<div>Sheet content</div>
|
||||||
|
</IsolatedBottomSheetModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("IsolatedBottomSheetModal", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("forces sheet isolation and keeps modal content mounted while hidden", () => {
|
||||||
|
const onClose = vi.fn();
|
||||||
|
const { getByTestId, rerender } = render(<Harness visible={false} onClose={onClose} />);
|
||||||
|
|
||||||
|
expect(getByTestId("app-portal").getAttribute("data-host")).toBe("root");
|
||||||
|
expect(modalProps).toHaveBeenLastCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
enableDismissOnClose: false,
|
||||||
|
stackBehavior: "replace",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(modalMethods.present).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
rerender(<Harness visible onClose={onClose} />);
|
||||||
|
expect(modalMethods.present).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
rerender(<Harness visible={false} onClose={onClose} />);
|
||||||
|
expect(modalMethods.close).toHaveBeenCalledTimes(1);
|
||||||
|
expect(modalMethods.dismiss).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
rerender(<Harness visible onClose={onClose} />);
|
||||||
|
expect(modalMethods.present).toHaveBeenCalledTimes(1);
|
||||||
|
expect(modalMethods.snapToIndex).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("only reports a user close when the sheet was visible", () => {
|
||||||
|
const onClose = vi.fn();
|
||||||
|
const { rerender } = render(<Harness visible onClose={onClose} />);
|
||||||
|
|
||||||
|
const latestProps = modalProps.mock.lastCall?.[0] as { onChange: (index: number) => void };
|
||||||
|
latestProps.onChange(-1);
|
||||||
|
expect(onClose).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
rerender(<Harness visible={false} onClose={onClose} />);
|
||||||
|
const closedProps = modalProps.mock.lastCall?.[0] as { onChange: (index: number) => void };
|
||||||
|
closedProps.onChange(-1);
|
||||||
|
expect(onClose).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows nested sheets inside a parent sheet without creating a sibling provider", () => {
|
||||||
|
const { getAllByTestId } = render(
|
||||||
|
<IsolatedBottomSheetModal index={0} snapPoints={["90%"]}>
|
||||||
|
<IsolatedBottomSheetModal index={0} snapPoints={["60%"]} onChange={() => {}}>
|
||||||
|
<div>Nested model picker</div>
|
||||||
|
</IsolatedBottomSheetModal>
|
||||||
|
</IsolatedBottomSheetModal>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getAllByTestId("bottom-sheet-provider")).toHaveLength(1);
|
||||||
|
expect(modalProps).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expect.objectContaining({
|
||||||
|
stackBehavior: "replace",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(modalProps).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
expect.objectContaining({
|
||||||
|
stackBehavior: "push",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import {
|
||||||
|
BottomSheetModal as GorhomBottomSheetModal,
|
||||||
|
BottomSheetModalProvider,
|
||||||
|
type BottomSheetModalProps,
|
||||||
|
} from "@gorhom/bottom-sheet";
|
||||||
|
import { Portal } from "@gorhom/portal";
|
||||||
|
import React, { createContext, useContext } from "react";
|
||||||
|
import { forwardRef, useCallback, useEffect, useRef } from "react";
|
||||||
|
import type { ElementRef } from "react";
|
||||||
|
|
||||||
|
type GorhomBottomSheetModalMethods = ElementRef<typeof GorhomBottomSheetModal>;
|
||||||
|
|
||||||
|
type IsolatedBottomSheetModalProps = Omit<
|
||||||
|
BottomSheetModalProps,
|
||||||
|
"enableDismissOnClose" | "stackBehavior"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type IsolatedBottomSheetModalRef = GorhomBottomSheetModalMethods;
|
||||||
|
|
||||||
|
const IsolatedBottomSheetScopeContext = createContext(false);
|
||||||
|
|
||||||
|
export const IsolatedBottomSheetModal = forwardRef<
|
||||||
|
IsolatedBottomSheetModalRef,
|
||||||
|
IsolatedBottomSheetModalProps
|
||||||
|
>(function IsolatedBottomSheetModal(props, ref) {
|
||||||
|
const isNestedSheet = useContext(IsolatedBottomSheetScopeContext);
|
||||||
|
const { children, ...bottomSheetProps } = props;
|
||||||
|
const scopedChildren =
|
||||||
|
typeof children === "function" ? (
|
||||||
|
(input: { data?: unknown }) => (
|
||||||
|
<IsolatedBottomSheetScopeContext.Provider value={true}>
|
||||||
|
{children(input) as React.ReactNode}
|
||||||
|
</IsolatedBottomSheetScopeContext.Provider>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<IsolatedBottomSheetScopeContext.Provider value={true}>
|
||||||
|
{children}
|
||||||
|
</IsolatedBottomSheetScopeContext.Provider>
|
||||||
|
);
|
||||||
|
const modal = (
|
||||||
|
<GorhomBottomSheetModal
|
||||||
|
{...bottomSheetProps}
|
||||||
|
ref={ref}
|
||||||
|
enableDismissOnClose={false}
|
||||||
|
stackBehavior={isNestedSheet ? "push" : "replace"}
|
||||||
|
>
|
||||||
|
{scopedChildren}
|
||||||
|
</GorhomBottomSheetModal>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isNestedSheet) {
|
||||||
|
return modal;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal hostName="root">
|
||||||
|
<BottomSheetModalProvider>{modal}</BottomSheetModalProvider>
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useIsolatedBottomSheetVisibility({
|
||||||
|
visible,
|
||||||
|
isEnabled,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
visible: boolean;
|
||||||
|
isEnabled?: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const sheetRef = useRef<IsolatedBottomSheetModalRef>(null);
|
||||||
|
const hasPresentedRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEnabled === false) return;
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
if (hasPresentedRef.current) {
|
||||||
|
sheetRef.current?.snapToIndex(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPresentedRef.current = true;
|
||||||
|
sheetRef.current?.present();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasPresentedRef.current) {
|
||||||
|
sheetRef.current?.close();
|
||||||
|
}
|
||||||
|
}, [isEnabled, visible]);
|
||||||
|
|
||||||
|
const handleSheetChange = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
if (index === -1 && visible) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onClose, visible],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
sheetRef,
|
||||||
|
handleSheetChange,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -471,7 +471,6 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
|
|||||||
searchPlaceholder="Search tabs"
|
searchPlaceholder="Search tabs"
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
onOpenChange={setIsOpen}
|
onOpenChange={setIsOpen}
|
||||||
enableDismissOnClose={false}
|
|
||||||
anchorRef={anchorRef}
|
anchorRef={anchorRef}
|
||||||
renderOption={({ option, selected, active, onPress }) => {
|
renderOption={({ option, selected, active, onPress }) => {
|
||||||
const tab = tabByKey.get(option.id);
|
const tab = tabByKey.get(option.id);
|
||||||
|
|||||||
Reference in New Issue
Block a user