Skip to content

Conversation

@adam-sajko
Copy link

Motivation

The BottomSheetModal component has a critical issue when bottomSheetRef.current?.present() and bottomSheetRef.current?.close() are called programmatically through useEffect hooks triggered by state changes, causing the modal to remain open instead of closing properly.

Problem:
When open changes rapidly (e.g., in Storybook controls or programmatic state changes), the useEffect calls bottomSheetRef.current?.present() and bottomSheetRef.current?.close() in quick succession. The animateOnMount call from the present action overrides the handleForceClose animation from the close action, causing the modal to stay open and become frozen - unable to be closed afterward.

Example:

const [open, setOpen] = useState(false);
const bottomSheetRef = useRef<BottomSheetModal>(null);

useEffect(() => {
  if (open) {
    bottomSheetRef.current?.present();
  } else {
    bottomSheetRef.current?.close();
  }
}, [open]);

// When setOpen(true) followed quickly by setOpen(false) and setOpen(true)
// The modal becomes frozen and cannot be closed

Root Cause:
In the evaluatePosition function, when !isAnimatedOnMount.value is true (which happens when the modal is being presented), the code immediately calls animateToPosition without checking if there's already a running animation that should be respected. This allows mount animations to override ongoing close animations, especially problematic when React's state batching and effect scheduling cause rapid successive calls.

Expected vs Actual Behavior:

  • Expected: open = truebottomSheetRef.current?.present()open = falsebottomSheetRef.current?.close() → Modal closes properly
  • Actual: open = truebottomSheetRef.current?.present()open = falsebottomSheetRef.current?.close()open = true (rapid) → bottomSheetRef.current?.present() → Modal stays open and becomes frozen

Technical Details:
The issue occurs in BottomSheet.tsx lines 965-985 where the mount animation logic doesn't respect running animations. The fix adds a check for animationStatus === ANIMATION_STATUS.RUNNING before handling mount animations, ensuring that force close operations are not interrupted by subsequent present calls triggered by React's state update timing.

Impact:
This affects programmatic usage of BottomSheetModal where state-driven present/dismiss calls are common, such as in Storybook controls, form workflows, conditional rendering, or user interaction handlers. The fix ensures consistent and predictable modal behavior regardless of React's state update timing and prevents the modal from becoming permanently frozen.

@adam-sajko
Copy link
Author

Patch:

diff --git a/lib/commonjs/components/bottomSheet/BottomSheet.js b/lib/commonjs/components/bottomSheet/BottomSheet.js
index 40725c071f23498d5a3053c0b307ab3f17566e18..5c840e29ae0ab8a6c64156d662f736ccc893329e 100644
--- a/lib/commonjs/components/bottomSheet/BottomSheet.js
+++ b/lib/commonjs/components/bottomSheet/BottomSheet.js
@@ -686,6 +686,14 @@ const BottomSheetComponent = /*#__PURE__*/(0, _react.forwardRef)(function Bottom
      * then we evaluate on mount use cases.
      */
     if (!isAnimatedOnMount.value) {
+      /**
+       * if there's a running animation (like force close), respect it and don't
+       * override with mount animation
+       */
+      if (animationStatus === _constants.ANIMATION_STATUS.RUNNING) {
+        return;
+      }
+
       /**
        * if animate on mount is set to true, then we animate to the propose position,
        * else, we set the position with out animation.
diff --git a/lib/module/components/bottomSheet/BottomSheet.js b/lib/module/components/bottomSheet/BottomSheet.js
index 561ad524ac6ed22e9e2284097b964923751ee9aa..fdb33f8b87d3104c553d77b8894374b59d6b893a 100644
--- a/lib/module/components/bottomSheet/BottomSheet.js
+++ b/lib/module/components/bottomSheet/BottomSheet.js
@@ -679,6 +679,14 @@ const BottomSheetComponent = /*#__PURE__*/forwardRef(function BottomSheet(props,
      * then we evaluate on mount use cases.
      */
     if (!isAnimatedOnMount.value) {
+      /**
+       * if there's a running animation (like force close), respect it and don't
+       * override with mount animation
+       */
+      if (animationStatus === ANIMATION_STATUS.RUNNING) {
+        return;
+      }
+
       /**
        * if animate on mount is set to true, then we animate to the propose position,
        * else, we set the position with out animation.

@adam-sajko
Copy link
Author

adam-sajko commented Oct 15, 2025

It's also worth mentioning that the consumer should always update the state in onDismiss when using a CONTROLLED bottom sheet

@github-actions
Copy link

This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 10 days.

@iffa
Copy link

iffa commented Nov 15, 2025

Not stale. Would love to see this and other critical fixes being merged as 5.2.X is currently very broken 😐

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants