diff --git a/mobile/src/components/BottomDrawer.tsx b/mobile/src/components/BottomDrawer.tsx index e96f035c2..a0d154c42 100644 --- a/mobile/src/components/BottomDrawer.tsx +++ b/mobile/src/components/BottomDrawer.tsx @@ -22,6 +22,7 @@ import Animated, { Extrapolation } from 'react-native-reanimated' import { colors, spacing } from '../theme/mobile-theme' +import { resolveBottomDrawerMounted } from './bottom-drawer-mount-state' import { useResponsiveLayout } from '../layout/responsive-layout' const DISMISS_THRESHOLD = 80 @@ -50,16 +51,17 @@ export function BottomDrawer({ zIndex }: Props) { const [mounted, setMounted] = useState(visible) + const resolvedMounted = resolveBottomDrawerMounted(visible, mounted) - useEffect(() => { - if (visible) { - setMounted(true) - } - }, [visible]) + // Why: opening drawers should mount before commit; waiting for a passive + // Effect adds a null render before every drawer can animate in. + if (resolvedMounted !== mounted) { + setMounted(resolvedMounted) + } // Why: hidden drawers are rendered by parent screens even while closed; keep // their Reanimated/Gesture setup out of hot paths like commit-message typing. - if (!mounted) return null + if (!resolvedMounted) return null return ( { + it('mounts before opening and stays mounted while closing', () => { + expect(resolveBottomDrawerMounted(true, false)).toBe(true) + expect(resolveBottomDrawerMounted(true, true)).toBe(true) + expect(resolveBottomDrawerMounted(false, true)).toBe(true) + expect(resolveBottomDrawerMounted(false, false)).toBe(false) + }) +}) diff --git a/mobile/src/components/bottom-drawer-mount-state.ts b/mobile/src/components/bottom-drawer-mount-state.ts new file mode 100644 index 000000000..3e9a7f110 --- /dev/null +++ b/mobile/src/components/bottom-drawer-mount-state.ts @@ -0,0 +1,3 @@ +export function resolveBottomDrawerMounted(visible: boolean, mounted: boolean): boolean { + return visible || mounted +}