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/components/CustomModalSheet.tsx b/packages/pluggableWidgets/bottom-sheet-native/src/components/CustomModalSheet.tsx
index 4d1f59bd3..9d0d6ba28 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 { ReactElement, ReactNode, useCallback, useRef, useState } from "react";
+import { Modal, Pressable, useWindowDimensions } from "react-native";
import BottomSheet, {
BottomSheetBackdrop,
BottomSheetBackdropProps,
@@ -16,36 +16,50 @@ interface CustomModalSheetProps {
export const CustomModalSheet = (props: CustomModalSheetProps): ReactElement => {
const bottomSheetRef = useRef(null);
- const [contentHeight, setContentHeight] = useState(0);
- const [currentStatus, setCurrentStatus] = useState(false);
+ 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);
- }
- },
- [contentHeight]
- );
+ if (externalOpen && !mounted) {
+ setMounted(true);
+ }
const close = useCallback(() => {
bottomSheetRef.current?.close();
}, []);
+ const handleModalShow = useCallback(() => {
+ setReady(true);
+ }, []);
+
+ const handleChange = useCallback(
+ (index: number) => {
+ if (index === 0) {
+ didOpenRef.current = true;
+ return;
+ }
+
+ if (index === -1 && didOpenRef.current) {
+ didOpenRef.current = false;
+ setReady(false);
+ props.triggerAttribute?.setValue(false);
+ setMounted(false);
+ }
+ },
+ [props.triggerAttribute]
+ );
+
const renderBackdrop = useCallback(
(backdropProps: BottomSheetBackdropProps) => (
[close]
);
- 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 handleSheetChanges = useCallback(
- (index: number) => {
- if (!isAvailable) {
- return;
- }
-
- const hasClosed = index === -1;
- if (hasClosed && props.triggerAttribute?.value) {
- props.triggerAttribute?.setValue(false);
- setCurrentStatus(false);
- }
- },
- [isAvailable, props.triggerAttribute]
- );
-
- useEffect(() => {
- if (!isAvailable) {
- return;
- }
-
- const shouldBeOpen = props.triggerAttribute?.value === true;
-
- if (shouldBeOpen && !currentStatus) {
- requestAnimationFrame(() => {
- setCurrentStatus(true);
- });
- } else if (!shouldBeOpen && currentStatus) {
- bottomSheetRef.current?.close();
- setCurrentStatus(false);
- }
- }, [props.triggerAttribute?.value, currentStatus, isAvailable]);
+ const maxHeight = windowHeight * 0.9;
return (
-
- handleSheetChanges(-1)}
- onChange={handleSheetChanges}
- backdropComponent={renderBackdrop}
- style={[props.styles.modal]}
- backgroundStyle={props.styles.container}
- enablePanDownToClose={false}
- handleComponent={null}
- handleStyle={{ display: "none" }}
- >
-
+ {ready && (
+ handleChange(-1)}
+ backdropComponent={renderBackdrop}
+ style={[props.styles.modal]}
+ backgroundStyle={props.styles.container}
+ handleComponent={null}
+ handleStyle={{ display: "none" }}
>
- {props.content}
-
-
+
+ {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..71e8f9868 100644
--- a/packages/pluggableWidgets/bottom-sheet-native/src/components/NativeBottomSheet.tsx
+++ b/packages/pluggableWidgets/bottom-sheet-native/src/components/NativeBottomSheet.tsx
@@ -2,14 +2,13 @@ import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from
import {
ActionSheetIOS,
Appearance,
- Dimensions,
- LayoutChangeEvent,
Modal,
Platform,
Pressable,
StyleSheet,
Text,
TouchableHighlight,
+ useWindowDimensions,
View
} from "react-native";
import BottomSheet, {
@@ -22,6 +21,12 @@ import { ItemsBasicType } from "../../typings/BottomSheetProps";
import { BottomSheetStyle, ModalItemContainerStyle } from "../ui/Styles";
import { executeAction } from "@mendix/piw-utils-internal";
+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;
triggerAttribute?: EditableValue;
@@ -30,38 +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 { 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) {
- bottomSheetRef.current?.snapToIndex(0);
- } else {
- bottomSheetRef.current?.close();
- }
- }
- }, [props.triggerAttribute]);
+ 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;
@@ -73,22 +68,37 @@ 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 handleChange = useCallback(
+ (index: number) => {
+ if (index === 0) {
+ didOpenRef.current = true;
+ return;
+ }
+
+ if (index === -1 && didOpenRef.current) {
+ didOpenRef.current = false;
+ setReady(false);
+ props.triggerAttribute?.setValue(false);
+ setMounted(false);
+ }
+ },
+ [props.triggerAttribute]
+ );
+
const renderBackdrop = useCallback(
(backdropProps: BottomSheetBackdropProps) => (
@@ -104,16 +114,6 @@ export const NativeBottomSheet = (props: NativeBottomSheetProps): ReactElement =
[close]
);
- const onLayoutHandler = useCallback(
- (event: LayoutChangeEvent) => {
- const height = event.nativeEvent.layout.height;
- if (height > 0 && height !== contentHeight) {
- setContentHeight(height);
- }
- },
- [contentHeight]
- );
-
const actionHandler = useCallback(
(index: number) => {
setTimeout(() => {
@@ -135,7 +135,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 (
@@ -170,61 +169,37 @@ 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 (
-
- 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" }}
- >
-
- {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))}
+
+
+ )}
);
};
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 @@
-
+