Skip to content

fix(Drawer): prevent nested drawer mask stacking#666

Merged
epolikrati merged 7 commits into
mainfrom
fix/drawer-nested-mask-overlay
Jul 15, 2026
Merged

fix(Drawer): prevent nested drawer mask stacking#666
epolikrati merged 7 commits into
mainfrom
fix/drawer-nested-mask-overlay

Conversation

@epolikrati

@epolikrati epolikrati commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Problem

When multiple <Drawer> components are open simultaneously (e.g. opening a drawer from within another drawer), each instance independently renders its own semi-transparent backdrop mask. The masks stack on top of each other, causing the background to become progressively darker with each additional drawer. Navigating back and forth compounds the issue that the accumulated backdrop layers are never cleaned up, making the left side of the screen increasingly opaque.

Changes

  • New useDrawerStackPosition hook: Maintains a module-level openDrawerStack array tracking all open drawer instances by useId(). Uses useSyncExternalStore for tear-free reads and useLayoutEffect for synchronous registration/cleanup. Only the drawer at index 0 (the first opened) returns isBottomMostOpenDrawer: true — all subsequent drawers render a transparent mask instead.
  • Removed showMask prop from DrawerProps: Mask visibility is now fully automatic based on stack position — consumers no longer need to pass showMask. The Mask component accepts visible (controls background opacity) and onClick (optional close handler) instead of the previous onClose.
  • Conditional mask rendering and click behaviour: shouldRenderMask = closeOnOutsideClick || isBottomMostOpenDrawer ensures the mask element exists when needed for click interception or backdrop display. shouldShowVisibleMask = isBottomMostOpenDrawer controls whether the backdrop is opaque or transparent. When closeOnOutsideClick is false, the mask renders (visible if bottom-most) but doesn't fire the close callback.
  • maskContainer style converted to function: Now accepts a visible boolean — renders rgba(0, 0, 0, 0.45) when visible, transparent otherwise. Added data-mask-visible attribute for test assertions.
  • Extracted DRAWER_ROOT_ID constant from inline string to constants.ts.
  • 102-line hook test suite (useDrawerStackPosition.test.ts): Covers single drawer, nested drawers, 3-deep nesting, closing top/bottom drawer promotion, and unmount while open.
  • 84 lines of new integration tests in Drawer.test.tsx: Covers visible mask with closeOnOutsideClick=false, disabled outside click, single visible mask with multiple drawers, and mask promotion on close.

Fixes

  • Multiple backdrop overlays stacking when opening nested drawers — now only the bottom-most drawer shows the visible mask.
  • Backdrop darkening on repeated back-and-forth navigation — stack cleanup happens synchronously on close/unmount.

@epolikrati epolikrati self-assigned this Jul 10, 2026
Comment thread src/components/Drawer/components/Mask.tsx Outdated

@xanderantoniadis xanderantoniadis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid solution! good job 👏

@epolikrati
epolikrati merged commit 5b27879 into main Jul 15, 2026
1 check passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 6.7.6 🎉

The release is available on:

Your semantic-release bot 📦🚀

@epolikrati
epolikrati deleted the fix/drawer-nested-mask-overlay branch July 15, 2026 13:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants