From e7518c4a218c61e6ea65aca68b7c0f5b0095698a Mon Sep 17 00:00:00 2001 From: MxKevinBeqo Date: Fri, 19 Jun 2026 14:37:39 +0200 Subject: [PATCH 1/5] fix(bottom-sheet-native): improve toggle animations and initial render --- .../src/components/CustomModalSheet.tsx | 121 +++++++++++++----- .../src/components/NativeBottomSheet.tsx | 120 ++++++++++++----- 2 files changed, 176 insertions(+), 65 deletions(-) diff --git a/packages/pluggableWidgets/bottom-sheet-native/src/components/CustomModalSheet.tsx b/packages/pluggableWidgets/bottom-sheet-native/src/components/CustomModalSheet.tsx index 4d1f59bd3..eb8882081 100644 --- a/packages/pluggableWidgets/bottom-sheet-native/src/components/CustomModalSheet.tsx +++ b/packages/pluggableWidgets/bottom-sheet-native/src/components/CustomModalSheet.tsx @@ -1,5 +1,5 @@ import { ReactElement, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Dimensions, LayoutChangeEvent, Modal, Pressable } from "react-native"; +import { Animated, Dimensions, LayoutChangeEvent, Modal, Pressable, View } from "react-native"; import BottomSheet, { BottomSheetBackdrop, BottomSheetBackdropProps, @@ -8,6 +8,20 @@ import BottomSheet, { import { EditableValue, ValueStatus } from "mendix"; import { BottomSheetStyle } from "../ui/Styles"; +const BACKDROP_FADE_IN_DURATION = 200; +const BACKDROP_FADE_OUT_DURATION = 150; +// Delay before animating sheet open to ensure Modal and BottomSheet are fully laid out +const SHEET_ANIMATION_DELAY = 50; + +// Styles for off-screen measurement container +const MEASUREMENT_CONTAINER_STYLE = { + position: "absolute" as const, + opacity: 0, + top: -10000, // Position far off-screen to avoid any layout interference + left: 0, + pointerEvents: "none" as const +}; + interface CustomModalSheetProps { triggerAttribute?: EditableValue; content?: ReactNode; @@ -18,6 +32,8 @@ export const CustomModalSheet = (props: CustomModalSheetProps): ReactElement => const bottomSheetRef = useRef(null); const [contentHeight, setContentHeight] = useState(0); const [currentStatus, setCurrentStatus] = useState(false); + const [isMeasured, setIsMeasured] = useState(false); + const backdropOpacity = useRef(new Animated.Value(0)).current; const isAvailable = props.triggerAttribute && props.triggerAttribute.status === ValueStatus.Available; @@ -31,6 +47,7 @@ export const CustomModalSheet = (props: CustomModalSheetProps): ReactElement => const layoutHeight = event.nativeEvent.layout.height; if (layoutHeight > 0 && layoutHeight !== contentHeight) { setContentHeight(layoutHeight); + setIsMeasured(true); } }, [contentHeight] @@ -42,17 +59,29 @@ export const CustomModalSheet = (props: CustomModalSheetProps): ReactElement => const renderBackdrop = useCallback( (backdropProps: BottomSheetBackdropProps) => ( - - - + + + + + ), - [close] + [close, backdropOpacity] ); const snapPoints = useMemo(() => { @@ -89,38 +118,62 @@ export const CustomModalSheet = (props: CustomModalSheetProps): ReactElement => const shouldBeOpen = props.triggerAttribute?.value === true; if (shouldBeOpen && !currentStatus) { + setCurrentStatus(true); requestAnimationFrame(() => { - setCurrentStatus(true); + // Fade in backdrop - this helps smooth the transition as the sheet opens, reducing the perception of any initial stuttering. + Animated.timing(backdropOpacity, { + toValue: 1, + duration: BACKDROP_FADE_IN_DURATION, + useNativeDriver: true + }).start(); + // Delay animation to ensure Modal and BottomSheet are fully mounted and laid out + setTimeout(() => { + bottomSheetRef.current?.snapToIndex(0); + }, SHEET_ANIMATION_DELAY); }); } else if (!shouldBeOpen && currentStatus) { bottomSheetRef.current?.close(); - setCurrentStatus(false); + Animated.timing(backdropOpacity, { + toValue: 0, + duration: BACKDROP_FADE_OUT_DURATION, + useNativeDriver: true + }).start(() => { + setCurrentStatus(false); + }); } - }, [props.triggerAttribute?.value, currentStatus, isAvailable]); + }, [props.triggerAttribute?.value, currentStatus, isAvailable, backdropOpacity]); return ( - - handleSheetChanges(-1)} - onChange={handleSheetChanges} - backdropComponent={renderBackdrop} - style={[props.styles.modal]} - backgroundStyle={props.styles.container} - enablePanDownToClose={false} - handleComponent={null} - handleStyle={{ display: "none" }} - > - + {/* Off-screen measurement - measure content before showing Modal to prevent sudden jumps in layout */} + {!isMeasured && ( + {props.content} - - - + + )} + + + handleSheetChanges(-1)} + onChange={handleSheetChanges} + backdropComponent={renderBackdrop} + style={[props.styles.modal]} + backgroundStyle={props.styles.container} + enablePanDownToClose={false} + handleComponent={null} + handleStyle={{ display: "none" }} + > + + {props.content} + + + + ); }; diff --git a/packages/pluggableWidgets/bottom-sheet-native/src/components/NativeBottomSheet.tsx b/packages/pluggableWidgets/bottom-sheet-native/src/components/NativeBottomSheet.tsx index 966418537..579b785df 100644 --- a/packages/pluggableWidgets/bottom-sheet-native/src/components/NativeBottomSheet.tsx +++ b/packages/pluggableWidgets/bottom-sheet-native/src/components/NativeBottomSheet.tsx @@ -1,6 +1,7 @@ import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ActionSheetIOS, + Animated, Appearance, Dimensions, LayoutChangeEvent, @@ -22,6 +23,20 @@ import { ItemsBasicType } from "../../typings/BottomSheetProps"; import { BottomSheetStyle, ModalItemContainerStyle } from "../ui/Styles"; import { executeAction } from "@mendix/piw-utils-internal"; +const BACKDROP_FADE_IN_DURATION = 200; +const BACKDROP_FADE_OUT_DURATION = 150; +// Delay before animating sheet open to ensure Modal and BottomSheet are fully laid out +const SHEET_ANIMATION_DELAY = 50; + +// Styles for off-screen measurement container +const MEASUREMENT_CONTAINER_STYLE = { + position: "absolute" as const, + opacity: 0, + top: -10000, // Position far off-screen to avoid any layout interference + left: 0, + pointerEvents: "none" as const +}; + interface NativeBottomSheetProps { name: string; triggerAttribute?: EditableValue; @@ -35,6 +50,8 @@ let lastIndexRef = -1; export const NativeBottomSheet = (props: NativeBottomSheetProps): ReactElement => { const bottomSheetRef = useRef(null); const [contentHeight, setContentHeight] = useState(0); + const [isMeasured, setIsMeasured] = useState(false); + const backdropOpacity = useRef(new Animated.Value(0)).current; const isAvailable = props.triggerAttribute && props.triggerAttribute.status === ValueStatus.Available; const isOpen = @@ -45,12 +62,28 @@ export const NativeBottomSheet = (props: NativeBottomSheetProps): ReactElement = const manageBottomSheet = useCallback(() => { if (props.triggerAttribute && props.triggerAttribute.status === ValueStatus.Available) { if (props.triggerAttribute.value) { - bottomSheetRef.current?.snapToIndex(0); + requestAnimationFrame(() => { + // Fade in backdrop - this helps smooth the transition as the sheet opens, reducing the perception of any initial stuttering. + Animated.timing(backdropOpacity, { + toValue: 1, + duration: BACKDROP_FADE_IN_DURATION, + useNativeDriver: true + }).start(); + // Delay animation to ensure Modal and BottomSheet are fully mounted and laid out + setTimeout(() => { + bottomSheetRef.current?.snapToIndex(0); + }, SHEET_ANIMATION_DELAY); + }); } else { bottomSheetRef.current?.close(); + Animated.timing(backdropOpacity, { + toValue: 0, + duration: BACKDROP_FADE_OUT_DURATION, + useNativeDriver: true + }).start(); } } - }, [props.triggerAttribute]); + }, [props.triggerAttribute, backdropOpacity]); useEffect(() => { manageBottomSheet(); @@ -91,17 +124,29 @@ export const NativeBottomSheet = (props: NativeBottomSheetProps): ReactElement = const renderBackdrop = useCallback( (backdropProps: BottomSheetBackdropProps) => ( - - - + + + + + ), - [close] + [close, backdropOpacity] ); const onLayoutHandler = useCallback( @@ -109,6 +154,7 @@ export const NativeBottomSheet = (props: NativeBottomSheetProps): ReactElement = const height = event.nativeEvent.layout.height; if (height > 0 && height !== contentHeight) { setContentHeight(height); + setIsMeasured(true); } }, [contentHeight] @@ -206,26 +252,38 @@ export const NativeBottomSheet = (props: NativeBottomSheetProps): ReactElement = } return ( - - 0 ? 0 : -1} - snapPoints={snapPoints} - enablePanDownToClose - animateOnMount={false} - onClose={() => handleSheetChanges(-1)} - onChange={handleSheetChanges} - style={getContainerStyle()} - backdropComponent={renderBackdrop} - backgroundStyle={props.styles.container} - handleComponent={null} - handleStyle={{ display: "none" }} - > - + <> + {/* Off-screen measurement - measure content before showing Modal to prevent sudden jumps in layout */} + {!isMeasured && ( + {props.itemsBasic.map((item, index) => renderItem(item, index))} - - - + + )} + + + handleSheetChanges(-1)} + onChange={handleSheetChanges} + style={getContainerStyle()} + backdropComponent={renderBackdrop} + backgroundStyle={props.styles.container} + handleComponent={null} + handleStyle={{ display: "none" }} + > + + {props.itemsBasic.map((item, index) => renderItem(item, index))} + + + + ); }; From 29ff8729546aac60f843bc13cee20bb4e01c3323 Mon Sep 17 00:00:00 2001 From: MxKevinBeqo Date: Fri, 19 Jun 2026 14:50:57 +0200 Subject: [PATCH 2/5] chore: add changelog and bump version --- packages/pluggableWidgets/bottom-sheet-native/CHANGELOG.md | 5 +++++ packages/pluggableWidgets/bottom-sheet-native/package.json | 2 +- .../pluggableWidgets/bottom-sheet-native/src/package.xml | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/pluggableWidgets/bottom-sheet-native/CHANGELOG.md b/packages/pluggableWidgets/bottom-sheet-native/CHANGELOG.md index aefdb3c49..81d90b439 100644 --- a/packages/pluggableWidgets/bottom-sheet-native/CHANGELOG.md +++ b/packages/pluggableWidgets/bottom-sheet-native/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Fixed + +- Fixed flickering issue on Android when opening bottom sheet (both basic and custom render types). +- Improved backdrop animation with smooth fade-in/fade-out transitions. + ## [5.3.0] - 2026-6-10 ### Changed diff --git a/packages/pluggableWidgets/bottom-sheet-native/package.json b/packages/pluggableWidgets/bottom-sheet-native/package.json index e9f810b84..ce8f2f345 100644 --- a/packages/pluggableWidgets/bottom-sheet-native/package.json +++ b/packages/pluggableWidgets/bottom-sheet-native/package.json @@ -1,7 +1,7 @@ { "name": "bottom-sheet-native", "widgetName": "BottomSheet", - "version": "5.3.0", + "version": "5.3.1", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/pluggableWidgets/bottom-sheet-native/src/package.xml b/packages/pluggableWidgets/bottom-sheet-native/src/package.xml index 968d2a9f5..0bd9676f9 100644 --- a/packages/pluggableWidgets/bottom-sheet-native/src/package.xml +++ b/packages/pluggableWidgets/bottom-sheet-native/src/package.xml @@ -1,6 +1,6 @@ - + From e3f4168103446ec2d9aae0618df201a1976ee332 Mon Sep 17 00:00:00 2001 From: MxKevinBeqo Date: Fri, 19 Jun 2026 15:06:14 +0200 Subject: [PATCH 3/5] chore(bottom-sheet-native): update snapshots --- .../__snapshots__/BottomSheet.spec.tsx.snap | 139 +++++++++++++++++- 1 file changed, 137 insertions(+), 2 deletions(-) diff --git a/packages/pluggableWidgets/bottom-sheet-native/src/__tests__/__snapshots__/BottomSheet.spec.tsx.snap b/packages/pluggableWidgets/bottom-sheet-native/src/__tests__/__snapshots__/BottomSheet.spec.tsx.snap index 512486a56..533021945 100644 --- a/packages/pluggableWidgets/bottom-sheet-native/src/__tests__/__snapshots__/BottomSheet.spec.tsx.snap +++ b/packages/pluggableWidgets/bottom-sheet-native/src/__tests__/__snapshots__/BottomSheet.spec.tsx.snap @@ -1,8 +1,143 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Bottom sheet renders a custom bottom action sheet for ios (Basic modal) with custom style 1`] = `null`; +exports[`Bottom sheet renders a custom bottom action sheet for ios (Basic modal) with custom style 1`] = ` + + + + + Item 1 + + + + + + + Item 2 + + + + +`; -exports[`Bottom sheet renders a custom modal 1`] = `null`; +exports[`Bottom sheet renders a custom modal 1`] = ` + + + +`; exports[`Bottom sheet renders a expanding 1`] = ` Date: Mon, 22 Jun 2026 15:07:14 +0200 Subject: [PATCH 4/5] fix(bottom-sheet-native): simplify modal and bottom sheet implementation --- .../__snapshots__/BottomSheet.spec.tsx.snap | 139 +---------- .../src/components/CustomModalSheet.tsx | 202 +++++---------- .../src/components/NativeBottomSheet.tsx | 231 ++++++------------ 3 files changed, 138 insertions(+), 434 deletions(-) diff --git a/packages/pluggableWidgets/bottom-sheet-native/src/__tests__/__snapshots__/BottomSheet.spec.tsx.snap b/packages/pluggableWidgets/bottom-sheet-native/src/__tests__/__snapshots__/BottomSheet.spec.tsx.snap index 533021945..512486a56 100644 --- a/packages/pluggableWidgets/bottom-sheet-native/src/__tests__/__snapshots__/BottomSheet.spec.tsx.snap +++ b/packages/pluggableWidgets/bottom-sheet-native/src/__tests__/__snapshots__/BottomSheet.spec.tsx.snap @@ -1,143 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Bottom sheet renders a custom bottom action sheet for ios (Basic modal) with custom style 1`] = ` - - - - - Item 1 - - - - - - - Item 2 - - - - -`; +exports[`Bottom sheet renders a custom bottom action sheet for ios (Basic modal) with custom style 1`] = `null`; -exports[`Bottom sheet renders a custom modal 1`] = ` - - - -`; +exports[`Bottom sheet renders a custom modal 1`] = `null`; exports[`Bottom sheet renders a expanding 1`] = ` ; content?: ReactNode; @@ -30,150 +17,85 @@ interface CustomModalSheetProps { export const CustomModalSheet = (props: CustomModalSheetProps): ReactElement => { const bottomSheetRef = useRef(null); - const [contentHeight, setContentHeight] = useState(0); - const [currentStatus, setCurrentStatus] = useState(false); - const [isMeasured, setIsMeasured] = useState(false); - const backdropOpacity = useRef(new Animated.Value(0)).current; + const { height: windowHeight } = useWindowDimensions(); - const isAvailable = props.triggerAttribute && props.triggerAttribute.status === ValueStatus.Available; + const externalOpen = + props.triggerAttribute?.status === ValueStatus.Available && props.triggerAttribute.value === true; - const isOpen = - props.triggerAttribute && - props.triggerAttribute.status === ValueStatus.Available && - props.triggerAttribute.value; + const [mounted, setMounted] = useState(externalOpen); + const [ready, setReady] = useState(false); + const didOpenRef = useRef(false); - const onContentLayoutHandler = useCallback( - (event: LayoutChangeEvent): void => { - const layoutHeight = event.nativeEvent.layout.height; - if (layoutHeight > 0 && layoutHeight !== contentHeight) { - setContentHeight(layoutHeight); - setIsMeasured(true); - } - }, - [contentHeight] - ); + if (externalOpen && !mounted) { + setMounted(true); + } const close = useCallback(() => { bottomSheetRef.current?.close(); }, []); - const renderBackdrop = useCallback( - (backdropProps: BottomSheetBackdropProps) => ( - - - - - - ), - [close, backdropOpacity] - ); - - const snapPoints = useMemo(() => { - if (contentHeight === 0) { - return [1]; // During measurement - } - - // Use actual measured content height, cap at 90% screen - const maxHeight = Dimensions.get("screen").height * 0.9; - const snapHeight = Math.min(contentHeight, maxHeight); - return [snapHeight]; - }, [contentHeight]); + const handleModalShow = useCallback(() => { + setReady(true); + }, []); - const handleSheetChanges = useCallback( + const handleChange = useCallback( (index: number) => { - if (!isAvailable) { + if (index === 0) { + didOpenRef.current = true; return; } - const hasClosed = index === -1; - if (hasClosed && props.triggerAttribute?.value) { + if (index === -1 && didOpenRef.current) { + didOpenRef.current = false; + setReady(false); props.triggerAttribute?.setValue(false); - setCurrentStatus(false); + setMounted(false); } }, - [isAvailable, props.triggerAttribute] + [props.triggerAttribute] ); - useEffect(() => { - if (!isAvailable) { - return; - } - - const shouldBeOpen = props.triggerAttribute?.value === true; + const renderBackdrop = useCallback( + (backdropProps: BottomSheetBackdropProps) => ( + + ), + [] + ); - if (shouldBeOpen && !currentStatus) { - setCurrentStatus(true); - requestAnimationFrame(() => { - // Fade in backdrop - this helps smooth the transition as the sheet opens, reducing the perception of any initial stuttering. - Animated.timing(backdropOpacity, { - toValue: 1, - duration: BACKDROP_FADE_IN_DURATION, - useNativeDriver: true - }).start(); - // Delay animation to ensure Modal and BottomSheet are fully mounted and laid out - setTimeout(() => { - bottomSheetRef.current?.snapToIndex(0); - }, SHEET_ANIMATION_DELAY); - }); - } else if (!shouldBeOpen && currentStatus) { - bottomSheetRef.current?.close(); - Animated.timing(backdropOpacity, { - toValue: 0, - duration: BACKDROP_FADE_OUT_DURATION, - useNativeDriver: true - }).start(() => { - setCurrentStatus(false); - }); - } - }, [props.triggerAttribute?.value, currentStatus, isAvailable, backdropOpacity]); + const maxHeight = windowHeight * 0.9; return ( - <> - {/* Off-screen measurement - measure content before showing Modal to prevent sudden jumps in layout */} - {!isMeasured && ( - - {props.content} - - )} - - - handleSheetChanges(-1)} - onChange={handleSheetChanges} - backdropComponent={renderBackdrop} - style={[props.styles.modal]} - backgroundStyle={props.styles.container} - enablePanDownToClose={false} - handleComponent={null} - handleStyle={{ display: "none" }} - > - - {props.content} - - - - + + + {ready && ( + handleChange(-1)} + backdropComponent={renderBackdrop} + style={[props.styles.modal]} + backgroundStyle={props.styles.container} + handleComponent={null} + handleStyle={{ display: "none" }} + > + + {props.content} + + + )} + + ); }; diff --git a/packages/pluggableWidgets/bottom-sheet-native/src/components/NativeBottomSheet.tsx b/packages/pluggableWidgets/bottom-sheet-native/src/components/NativeBottomSheet.tsx index 579b785df..c90a6d083 100644 --- a/packages/pluggableWidgets/bottom-sheet-native/src/components/NativeBottomSheet.tsx +++ b/packages/pluggableWidgets/bottom-sheet-native/src/components/NativeBottomSheet.tsx @@ -1,18 +1,16 @@ import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ActionSheetIOS, - Animated, Appearance, - Dimensions, - LayoutChangeEvent, Modal, Platform, - Pressable, StyleSheet, Text, TouchableHighlight, + useWindowDimensions, View } from "react-native"; +import { GestureHandlerRootView } from "react-native-gesture-handler"; import BottomSheet, { BottomSheetBackdrop, BottomSheetBackdropProps, @@ -23,19 +21,11 @@ import { ItemsBasicType } from "../../typings/BottomSheetProps"; import { BottomSheetStyle, ModalItemContainerStyle } from "../ui/Styles"; import { executeAction } from "@mendix/piw-utils-internal"; -const BACKDROP_FADE_IN_DURATION = 200; -const BACKDROP_FADE_OUT_DURATION = 150; -// Delay before animating sheet open to ensure Modal and BottomSheet are fully laid out -const SHEET_ANIMATION_DELAY = 50; - -// Styles for off-screen measurement container -const MEASUREMENT_CONTAINER_STYLE = { - position: "absolute" as const, - opacity: 0, - top: -10000, // Position far off-screen to avoid any layout interference - left: 0, - pointerEvents: "none" as const -}; +const ITEM_ROW_HEIGHT = 44; +const CONTAINER_PADDING_TOP = 12; +const CONTAINER_PADDING_BOTTOM = 8; +const SCROLL_PADDING_BOTTOM = 16; +const VERTICAL_PADDING = CONTAINER_PADDING_TOP + CONTAINER_PADDING_BOTTOM + SCROLL_PADDING_BOTTOM; interface NativeBottomSheetProps { name: string; @@ -45,56 +35,28 @@ interface NativeBottomSheetProps { styles: BottomSheetStyle; } -let lastIndexRef = -1; - export const NativeBottomSheet = (props: NativeBottomSheetProps): ReactElement => { const bottomSheetRef = useRef(null); - const [contentHeight, setContentHeight] = useState(0); - const [isMeasured, setIsMeasured] = useState(false); - const backdropOpacity = useRef(new Animated.Value(0)).current; + const { height: windowHeight } = useWindowDimensions(); - const isAvailable = props.triggerAttribute && props.triggerAttribute.status === ValueStatus.Available; - const isOpen = - props.triggerAttribute && - props.triggerAttribute.status === ValueStatus.Available && - props.triggerAttribute.value; + const externalOpen = + props.triggerAttribute?.status === ValueStatus.Available && props.triggerAttribute.value === true; - const manageBottomSheet = useCallback(() => { - if (props.triggerAttribute && props.triggerAttribute.status === ValueStatus.Available) { - if (props.triggerAttribute.value) { - requestAnimationFrame(() => { - // Fade in backdrop - this helps smooth the transition as the sheet opens, reducing the perception of any initial stuttering. - Animated.timing(backdropOpacity, { - toValue: 1, - duration: BACKDROP_FADE_IN_DURATION, - useNativeDriver: true - }).start(); - // Delay animation to ensure Modal and BottomSheet are fully mounted and laid out - setTimeout(() => { - bottomSheetRef.current?.snapToIndex(0); - }, SHEET_ANIMATION_DELAY); - }); - } else { - bottomSheetRef.current?.close(); - Animated.timing(backdropOpacity, { - toValue: 0, - duration: BACKDROP_FADE_OUT_DURATION, - useNativeDriver: true - }).start(); - } - } - }, [props.triggerAttribute, backdropOpacity]); + const [mounted, setMounted] = useState(externalOpen); + const [ready, setReady] = useState(false); + const didOpenRef = useRef(false); - useEffect(() => { - manageBottomSheet(); - }, [manageBottomSheet]); + if (externalOpen && !mounted) { + setMounted(true); + } + + const handleModalShow = useCallback(() => { + setReady(true); + }, []); useEffect(() => { - // Only show the ActionSheet if using native on iOS and the trigger is active. - if (props.useNative && Platform.OS === "ios" && isOpen) { - // Create the options from props.itemsBasic captions. + if (props.useNative && Platform.OS === "ios" && externalOpen) { const options = props.itemsBasic.map(item => item.caption); - // Append a cancel option. options.push("Cancel"); const cancelButtonIndex = options.length - 1; @@ -106,58 +68,48 @@ export const NativeBottomSheet = (props: NativeBottomSheetProps): ReactElement = }, buttonIndex => { if (buttonIndex !== cancelButtonIndex) { - // Execute the corresponding action from itemsBasic. executeAction(props.itemsBasic[buttonIndex].action); } - // Reset the trigger so the ActionSheet will not show again until triggered. if (props.triggerAttribute && !props.triggerAttribute.readOnly) { props.triggerAttribute.setValue(false); } } ); } - }, [isOpen]); + }, [externalOpen]); const close = useCallback(() => { bottomSheetRef.current?.close(); }, []); - const renderBackdrop = useCallback( - (backdropProps: BottomSheetBackdropProps) => ( - - - - - - ), - [close, backdropOpacity] - ); + const handleChange = useCallback( + (index: number) => { + if (index === 0) { + didOpenRef.current = true; + return; + } - const onLayoutHandler = useCallback( - (event: LayoutChangeEvent) => { - const height = event.nativeEvent.layout.height; - if (height > 0 && height !== contentHeight) { - setContentHeight(height); - setIsMeasured(true); + if (index === -1 && didOpenRef.current) { + didOpenRef.current = false; + setReady(false); + props.triggerAttribute?.setValue(false); + setMounted(false); } }, - [contentHeight] + [props.triggerAttribute] + ); + + const renderBackdrop = useCallback( + (backdropProps: BottomSheetBackdropProps) => ( + + ), + [] ); const actionHandler = useCallback( @@ -181,7 +133,6 @@ export const NativeBottomSheet = (props: NativeBottomSheetProps): ReactElement = return [styles.buttonContainer, buttonContainerStyle]; }; - // Render items with conditional style based on theme and platform. const renderItem = (item: ItemsBasicType, index: number) => { if (Platform.OS === "android" || !props.useNative) { return ( @@ -216,74 +167,40 @@ export const NativeBottomSheet = (props: NativeBottomSheetProps): ReactElement = return [styles.sheetContainer, props.styles.container]; }; - const handleSheetChanges = useCallback( - (index: number) => { - if (!isAvailable) { - return; - } - const hasOpened = lastIndexRef === -1 && index === 0; - const hasClosed = index === -1; - lastIndexRef = index; - - if (hasOpened) { - props.triggerAttribute?.setValue(true); - } - if (hasClosed) { - props.triggerAttribute?.setValue(false); - } - }, - [isAvailable, props.triggerAttribute] - ); - const snapPoints = useMemo(() => { - if (contentHeight === 0) { - return [1]; // During measurement - } - - // Use actual measured height, cap at 90% screen - const maxHeight = Dimensions.get("screen").height * 0.9; - - const snapHeight = Math.min(contentHeight, maxHeight); - return [snapHeight]; - }, [contentHeight]); + const maxHeight = windowHeight * 0.9; + return [Math.min(props.itemsBasic.length * ITEM_ROW_HEIGHT + VERTICAL_PADDING, maxHeight)]; + }, [props.itemsBasic.length, windowHeight]); if (props.useNative && Platform.OS === "ios") { return ; } return ( - <> - {/* Off-screen measurement - measure content before showing Modal to prevent sudden jumps in layout */} - {!isMeasured && ( - - {props.itemsBasic.map((item, index) => renderItem(item, index))} - - )} - - - handleSheetChanges(-1)} - onChange={handleSheetChanges} - style={getContainerStyle()} - backdropComponent={renderBackdrop} - backgroundStyle={props.styles.container} - handleComponent={null} - handleStyle={{ display: "none" }} - > - - {props.itemsBasic.map((item, index) => renderItem(item, index))} - - - - + + + {ready && ( + handleChange(-1)} + style={getContainerStyle()} + backdropComponent={renderBackdrop} + backgroundStyle={props.styles.container} + handleComponent={null} + handleStyle={{ display: "none" }} + > + + {props.itemsBasic.map((item, index) => renderItem(item, index))} + + + )} + + ); }; From 78e66c2965452eb2a53d036eb73f7ac93c8f5eb9 Mon Sep 17 00:00:00 2001 From: MxKevinBeqo Date: Tue, 23 Jun 2026 13:52:41 +0200 Subject: [PATCH 5/5] fix(bottom-sheet-native): re-introduce Pressable as 'close when click outside' mechanism --- .../src/components/CustomModalSheet.tsx | 67 +++++++++---------- .../src/components/NativeBottomSheet.tsx | 62 ++++++++--------- 2 files changed, 64 insertions(+), 65 deletions(-) diff --git a/packages/pluggableWidgets/bottom-sheet-native/src/components/CustomModalSheet.tsx b/packages/pluggableWidgets/bottom-sheet-native/src/components/CustomModalSheet.tsx index 43bf844d5..9d0d6ba28 100644 --- a/packages/pluggableWidgets/bottom-sheet-native/src/components/CustomModalSheet.tsx +++ b/packages/pluggableWidgets/bottom-sheet-native/src/components/CustomModalSheet.tsx @@ -1,6 +1,5 @@ import { ReactElement, ReactNode, useCallback, useRef, useState } from "react"; -import { Modal, useWindowDimensions } from "react-native"; -import { GestureHandlerRootView } from "react-native-gesture-handler"; +import { Modal, Pressable, useWindowDimensions } from "react-native"; import BottomSheet, { BottomSheetBackdrop, BottomSheetBackdropProps, @@ -57,45 +56,45 @@ export const CustomModalSheet = (props: CustomModalSheetProps): ReactElement => const renderBackdrop = useCallback( (backdropProps: BottomSheetBackdropProps) => ( - + + + ), - [] + [close] ); const maxHeight = windowHeight * 0.9; return ( - - {ready && ( - handleChange(-1)} - backdropComponent={renderBackdrop} - style={[props.styles.modal]} - backgroundStyle={props.styles.container} - handleComponent={null} - handleStyle={{ display: "none" }} - > - - {props.content} - - - )} - + {ready && ( + handleChange(-1)} + backdropComponent={renderBackdrop} + style={[props.styles.modal]} + backgroundStyle={props.styles.container} + handleComponent={null} + handleStyle={{ display: "none" }} + > + + {props.content} + + + )} ); }; diff --git a/packages/pluggableWidgets/bottom-sheet-native/src/components/NativeBottomSheet.tsx b/packages/pluggableWidgets/bottom-sheet-native/src/components/NativeBottomSheet.tsx index c90a6d083..71e8f9868 100644 --- a/packages/pluggableWidgets/bottom-sheet-native/src/components/NativeBottomSheet.tsx +++ b/packages/pluggableWidgets/bottom-sheet-native/src/components/NativeBottomSheet.tsx @@ -4,13 +4,13 @@ import { Appearance, Modal, Platform, + Pressable, StyleSheet, Text, TouchableHighlight, useWindowDimensions, View } from "react-native"; -import { GestureHandlerRootView } from "react-native-gesture-handler"; import BottomSheet, { BottomSheetBackdrop, BottomSheetBackdropProps, @@ -101,15 +101,17 @@ export const NativeBottomSheet = (props: NativeBottomSheetProps): ReactElement = const renderBackdrop = useCallback( (backdropProps: BottomSheetBackdropProps) => ( - + + + ), - [] + [close] ); const actionHandler = useCallback( @@ -178,28 +180,26 @@ export const NativeBottomSheet = (props: NativeBottomSheetProps): ReactElement = return ( - - {ready && ( - handleChange(-1)} - style={getContainerStyle()} - backdropComponent={renderBackdrop} - backgroundStyle={props.styles.container} - handleComponent={null} - handleStyle={{ display: "none" }} - > - - {props.itemsBasic.map((item, index) => renderItem(item, index))} - - - )} - + {ready && ( + handleChange(-1)} + style={getContainerStyle()} + backdropComponent={renderBackdrop} + backgroundStyle={props.styles.container} + handleComponent={null} + handleStyle={{ display: "none" }} + > + + {props.itemsBasic.map((item, index) => renderItem(item, index))} + + + )} ); };