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 @@ - +