Skip to content

chore(core): remove react-remove-scroll dependency#3373

Open
orrgottlieb wants to merge 1 commit into
masterfrom
chore/remove-react-remove-scroll
Open

chore(core): remove react-remove-scroll dependency#3373
orrgottlieb wants to merge 1 commit into
masterfrom
chore/remove-react-remove-scroll

Conversation

@orrgottlieb

@orrgottlieb orrgottlieb commented May 22, 2026

Copy link
Copy Markdown
Contributor

User description

Summary

  • Modal was the only consumer of react-remove-scroll. Replaced it with a small internal hook (useBodyScrollLock) that toggles document.body's overflow and pads for the scrollbar to prevent layout jump.
  • Multiple stacked modals share a single lock via a module-level counter, so the body styles are only touched on the first lock and restored on the last unlock.

Why

@vibe/core is shipped into every monday MFE that uses Vibe via Trident's shared-dependencies system, but react-remove-scroll is not in Trident's shared lists (BASE_LIST / EXPANDED_LIST). Every MFE bundle re-includes it. Removing this dependency reduces the per-MFE bundle size and trims one source of dependency-permutation risk.

Test plan

  • All 72 existing Modal tests pass locally
  • Verify in Storybook that the body doesn't scroll while a modal is open
  • Verify there's no layout shift when opening the modal on a page with a vertical scrollbar
  • Verify stacked modals still lock scroll correctly

🤖 Generated with Claude Code


PR Type

Enhancement


Description

  • Replace react-remove-scroll with internal useBodyScrollLock hook

  • Implement reference-counted scroll lock for stacked modals

  • Prevent layout shift by padding body for scrollbar width

  • Remove third-party dependency from package.json


Diagram Walkthrough

flowchart LR
  A["Modal Component"] -->|"uses"| B["useBodyScrollLock Hook"]
  B -->|"manages"| C["document.body styles"]
  C -->|"toggles"| D["overflow hidden"]
  C -->|"adjusts"| E["padding-right"]
  F["Reference Counter"] -->|"tracks"| B
  F -->|"prevents conflicts"| G["Stacked Modals"]
Loading

File Walkthrough

Relevant files
Enhancement
useBodyScrollLock.ts
New useBodyScrollLock hook implementation                               

packages/core/src/components/Modal/hooks/useBodyScrollLock/useBodyScrollLock.ts

  • New hook that manages body scroll lock with reference counting
  • Saves and restores original overflow and paddingRight styles
  • Calculates scrollbar width to prevent layout shift
  • Supports multiple stacked modals via counter mechanism
+38/-0   
Modal.tsx
Replace RemoveScroll with useBodyScrollLock hook                 

packages/core/src/components/Modal/Modal/Modal.tsx

  • Remove RemoveScroll component import and wrapper
  • Add useBodyScrollLock hook import and call
  • Unwrap modal content from RemoveScroll component
  • Simplify JSX structure by removing wrapper element
+30/-29 
Dependencies
package.json
Remove react-remove-scroll dependency                                       

packages/core/package.json

  • Remove react-remove-scroll dependency from dependencies list
  • Reduces bundle size for all consuming MFEs
+0/-1     

Modal was the only consumer of react-remove-scroll. Replaced it with
useBodyScrollLock — a small ref-counted hook that toggles
document.body's overflow and pads for the scrollbar to avoid layout
jump. Multiple stacked modals share a single lock via the counter.

Drops one third-party dep that every MFE consuming @vibe/core
re-bundles, since Trident's shared-deps list does not externalize
react-remove-scroll.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@orrgottlieb orrgottlieb requested a review from a team as a code owner May 22, 2026 09:31
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (1)

Grey Divider


Action required

1. Unlocks during exit 🐞 Bug ≡ Correctness
Description
Modal calls useBodyScrollLock(show), so when show becomes false the hook cleanup restores
document.body styles immediately even though the modal stays mounted for the exit animation. This
makes the background scrollable while the modal/overlay is still visible during CSSTransition
exit.
Code

packages/core/src/components/Modal/Modal/Modal.tsx[R114-115]

Evidence
The modal is configured to animate out before unmounting, while the scroll lock hook is keyed only
on show and restores styles in its effect cleanup, so it unlocks during the exit period rather
than after unmount.

packages/core/src/components/Modal/Modal/Modal.tsx[140-196]
packages/core/src/components/Modal/Modal/Modal.module.scss[157-174]
packages/core/src/components/Modal/hooks/useBodyScrollLock/useBodyScrollLock.ts[10-35]
packages/core/src/components/Modal/Modal/tests/Modal.test.tsx[9-22]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`useBodyScrollLock(show)` unlocks scroll immediately when `show` flips to `false`, but the modal remains mounted during the exit animation (`CSSTransition` + `.containerExitActive`). This creates a window where the modal is still visible while body scroll is already re-enabled.

### Issue Context
The modal uses `CSSTransition` with `timeout.exit` and `unmountOnExit`, and exit animations are defined in SCSS. Scroll lock should remain active until the modal is actually unmounted / exit completes.

### Fix Focus Areas
- packages/core/src/components/Modal/Modal/Modal.tsx[112-196]
- packages/core/src/components/Modal/hooks/useBodyScrollLock/useBodyScrollLock.ts[10-35]

### Implementation sketch
- Introduce a local state like `isScrollLocked`.
- Set `isScrollLocked` to `true` on enter (or when mounting).
- Set `isScrollLocked` to `false` in `CSSTransition`’s `onExited` callback (after exit animation completes).
- Call `useBodyScrollLock(isScrollLocked)` instead of `useBodyScrollLock(show)`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. useBodyScrollLock untested behavior 📘 Rule violation ☼ Reliability
Description
Modal now mutates document.body styles via useBodyScrollLock(show) but there are no tests
validating scroll-lock behavior (overflow/padding restoration), increasing regression risk
(especially for stacked modals). This does not meet the requirement that tests validate real DOM
behavior.
Code

packages/core/src/components/Modal/Modal/Modal.tsx[R114-115]

Evidence
PR Compliance ID 8 requires tests to validate real DOM behavior. The PR introduces body style
mutations for scroll locking (useBodyScrollLock(show) and its document.body.style changes), but
the existing Modal tests focus on roles/ARIA and do not validate body overflow/padding behavior or
restoration.

CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes
packages/core/src/components/Modal/Modal/Modal.tsx[112-115]
packages/core/src/components/Modal/hooks/useBodyScrollLock/useBodyScrollLock.ts[10-35]
packages/core/src/components/Modal/Modal/tests/Modal.test.tsx[24-52]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Modal` now uses `useBodyScrollLock(show)` which changes `document.body.style.overflow` and `document.body.style.paddingRight`, but the test suite does not assert this real DOM behavior (including restoration and stacked-modal reference counting).

## Issue Context
This PR replaces `react-remove-scroll` with a custom scroll-lock implementation; without direct assertions on `document.body` style changes, regressions can slip in (e.g., body remains locked after close, padding not restored, stacked modals unlocking too early).

## Fix Focus Areas
- packages/core/src/components/Modal/Modal/Modal.tsx[112-115]
- packages/core/src/components/Modal/hooks/useBodyScrollLock/useBodyScrollLock.ts[10-35]
- packages/core/src/components/Modal/Modal/__tests__/Modal.test.tsx[24-433]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Docs unlock hook broken 🐞 Bug ≡ Correctness
Description
Storybook’s useRemoveModalScrollLock only disables scroll lock when document.body has
data-scroll-locked, but the new useBodyScrollLock never sets that attribute (it only edits
overflow/paddingRight). As a result, Modal docs previews will remain scroll-locked, breaking the
documented docs-only behavior.
Code

packages/core/src/components/Modal/hooks/useBodyScrollLock/useBodyScrollLock.ts[R14-24]

Evidence
The docs helper’s condition depends on data-scroll-locked, and repo-wide search shows it’s only
referenced there; the new scroll lock implementation only changes inline styles and never sets that
attribute, so the docs override won’t trigger.

packages/docs/src/pages/components/Modal/Modal.stories.helpers.tsx[55-113]
packages/core/src/components/Modal/hooks/useBodyScrollLock/useBodyScrollLock.ts[14-33]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The Storybook docs helper that previously neutralized `react-remove-scroll` relies on `document.body[data-scroll-locked]`, which is no longer present after switching to `useBodyScrollLock` (styles-based lock). This breaks the intended ability to scroll the docs page while a preview modal is open.

### Issue Context
- Docs helper checks/removes `data-scroll-locked` when `show && isDocsView && !isFullView`.
- New core hook uses `document.body.style.overflow = "hidden"` + padding compensation and does not set any attribute.

### Fix Focus Areas
- packages/docs/src/pages/components/Modal/Modal.stories.helpers.tsx[55-113]
- packages/core/src/components/Modal/hooks/useBodyScrollLock/useBodyScrollLock.ts[14-33]

### Implementation options
- **Docs-only fix (preferred):** update `useRemoveModalScrollLock` to undo the new lock (e.g., in `requestAnimationFrame`, restore `document.body.style.overflow` and `paddingRight` to pre-lock values stored in a ref), instead of checking `data-scroll-locked`.
- **Core API fix:** add an internal/optional prop to `Modal` to disable body scroll lock for Storybook docs previews, and pass it from `withOpenedModalPreview`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +114 to +115
useBodyScrollLock(show);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. Unlocks during exit 🐞 Bug ≡ Correctness

Modal calls useBodyScrollLock(show), so when show becomes false the hook cleanup restores
document.body styles immediately even though the modal stays mounted for the exit animation. This
makes the background scrollable while the modal/overlay is still visible during CSSTransition
exit.
Agent Prompt
### Issue description
`useBodyScrollLock(show)` unlocks scroll immediately when `show` flips to `false`, but the modal remains mounted during the exit animation (`CSSTransition` + `.containerExitActive`). This creates a window where the modal is still visible while body scroll is already re-enabled.

### Issue Context
The modal uses `CSSTransition` with `timeout.exit` and `unmountOnExit`, and exit animations are defined in SCSS. Scroll lock should remain active until the modal is actually unmounted / exit completes.

### Fix Focus Areas
- packages/core/src/components/Modal/Modal/Modal.tsx[112-196]
- packages/core/src/components/Modal/hooks/useBodyScrollLock/useBodyScrollLock.ts[10-35]

### Implementation sketch
- Introduce a local state like `isScrollLocked`.
- Set `isScrollLocked` to `true` on enter (or when mounting).
- Set `isScrollLocked` to `false` in `CSSTransition`’s `onExited` callback (after exit animation completes).
- Call `useBodyScrollLock(isScrollLocked)` instead of `useBodyScrollLock(show)`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@github-actions

Copy link
Copy Markdown
Contributor

📦 Bundle Size Analysis

Changed Components

Component Base PR Diff
Modal 79.14KB 76.56KB -2.58KB 🟢
Unchanged Components
Component Base PR Diff
@vibe/button 17.3KB 17.29KB -9B 🟢
@vibe/clickable 5.95KB 5.96KB +3B 🔺
@vibe/dialog 52.14KB 52.16KB +14B 🔺
@vibe/icon-button 66.09KB 66.1KB +13B 🔺
@vibe/icon 12.92KB 12.89KB -32B 🟢
@vibe/layer 2.96KB 2.96KB 0B ➖
@vibe/layout 9.82KB 9.83KB +11B 🔺
@vibe/loader 5.64KB 5.65KB +10B 🔺
@vibe/tooltip 61.33KB 61.32KB -7B 🟢
@vibe/typography 63.47KB 63.43KB -47B 🟢
Accordion 6.31KB 6.29KB -14B 🟢
AccordionItem 66.43KB 66.39KB -47B 🟢
AlertBanner 70.83KB 70.9KB +71B 🔺
AlertBannerButton 18.76KB 18.76KB -2B 🟢
AlertBannerLink 15.26KB 15.26KB +4B 🔺
AlertBannerText 63.95KB 63.91KB -38B 🟢
AttentionBox 74.35KB 74.29KB -61B 🟢
Avatar 66.84KB 66.72KB -119B 🟢
AvatarGroup 93.29KB 93.25KB -40B 🟢
Badge 43.19KB 43.17KB -24B 🟢
BreadcrumbItem 64.7KB 64.64KB -61B 🟢
BreadcrumbMenu 68.57KB 68.58KB +12B 🔺
BreadcrumbMenuItem 77.07KB 77KB -69B 🟢
BreadcrumbsBar 5.68KB 5.68KB -1B 🟢
ButtonGroup 68.32KB 68.3KB -17B 🟢
Checkbox 66.83KB 66.92KB +99B 🔺
Chips 75.05KB 75.01KB -36B 🟢
ColorPicker 74.47KB 74.45KB -26B 🟢
ColorPickerContent 73.73KB 73.7KB -28B 🟢
Combobox 84.08KB 84.03KB -50B 🟢
Counter 42.21KB 42.28KB +65B 🔺
DatePicker 112.41KB 112.49KB +82B 🔺
Divider 5.42KB 5.46KB +44B 🔺
Dropdown 95.35KB 95.26KB -97B 🟢
EditableHeading 66.63KB 66.53KB -104B 🟢
EditableText 66.46KB 66.42KB -38B 🟢
EmptyState 70.48KB 70.39KB -97B 🟢
ExpandCollapse 66.22KB 66.19KB -32B 🟢
FormattedNumber 5.86KB 5.84KB -13B 🟢
GridKeyboardNavigationContext 4.65KB 4.65KB -4B 🟢
HiddenText 5.4KB 5.39KB -15B 🟢
Info 72.06KB 72.06KB +3B 🔺
Label 68.65KB 68.67KB +21B 🔺
Link 14.91KB 14.88KB -30B 🟢
List 72.88KB 72.84KB -43B 🟢
ListItem 65.54KB 65.49KB -59B 🟢
ListItemAvatar 66.88KB 66.92KB +40B 🔺
ListItemIcon 13.97KB 13.97KB +5B 🔺
ListTitle 65.02KB 64.95KB -69B 🟢
Menu 8.65KB 8.64KB -19B 🟢
MenuDivider 5.56KB 5.57KB +7B 🔺
MenuGridItem 7.16KB 7.19KB +36B 🔺
MenuItem 76.95KB 77.06KB +105B 🔺
MenuItemButton 70.11KB 70.06KB -55B 🟢
MenuTitle 65.35KB 65.34KB -11B 🟢
MenuButton 66.08KB 66.14KB +57B 🔺
ModalContent 4.72KB 4.71KB -1B 🟢
ModalHeader 65.79KB 65.77KB -19B 🟢
ModalMedia 7.51KB 7.5KB -3B 🟢
ModalFooter 67.72KB 67.68KB -35B 🟢
ModalFooterWizard 68.6KB 68.56KB -40B 🟢
ModalBasicLayout 8.96KB 8.9KB -57B 🟢
ModalMediaLayout 8.08KB 8.06KB -19B 🟢
ModalSideBySideLayout 6.3KB 6.29KB -4B 🟢
MultiStepIndicator 52.96KB 52.95KB -11B 🟢
NumberField 72.87KB 72.87KB -6B 🟢
ProgressBar 7.34KB 7.35KB +7B 🔺
RadioButton 65.9KB 65.9KB +3B 🔺
Search 70.65KB 70.61KB -46B 🟢
Skeleton 6KB 6.01KB +4B 🔺
Slider 73.86KB 73.84KB -19B 🟢
SplitButton 66.48KB 66.52KB +46B 🔺
SplitButtonMenu 8.8KB 8.76KB -34B 🟢
Steps 71.31KB 71.36KB +57B 🔺
Table 7.26KB 7.25KB -13B 🟢
TableBody 66.68KB 66.69KB +11B 🔺
TableCell 65.22KB 65.26KB +36B 🔺
TableContainer 5.31KB 5.32KB +16B 🔺
TableHeader 5.64KB 5.64KB +1B 🔺
TableHeaderCell 72.2KB 72.15KB -43B 🟢
TableRow 5.56KB 5.55KB -8B 🟢
TableRowMenu 68.87KB 68.85KB -27B 🟢
TableVirtualizedBody 71.42KB 71.38KB -39B 🟢
Tab 64KB 63.93KB -73B 🟢
TabList 8.89KB 8.86KB -30B 🟢
TabPanel 5.3KB 5.29KB -14B 🟢
TabPanels 5.86KB 5.86KB -2B 🟢
TabsContext 5.48KB 5.51KB +29B 🔺
TextArea 66.26KB 66.25KB -3B 🟢
TextField 69.43KB 69.43KB -2B 🟢
TextWithHighlight 64.35KB 64.3KB -48B 🟢
ThemeProvider 4.36KB 4.36KB -1B 🟢
Tipseen 71.17KB 71.15KB -21B 🟢
TipseenContent 71.6KB 71.6KB -2B 🟢
TipseenMedia 71.27KB 71.3KB +26B 🔺
TipseenWizard 73.93KB 73.8KB -137B 🟢
Toast 74.1KB 73.94KB -168B 🟢
ToastButton 18.59KB 18.62KB +33B 🔺
ToastLink 15.05KB 15.08KB +31B 🔺
Toggle 66.62KB 66.59KB -27B 🟢
TransitionView 5.42KB 5.45KB +30B 🔺
VirtualizedGrid 12.54KB 12.54KB +2B 🔺
VirtualizedList 12.28KB 12.26KB -12B 🟢
List (Next) 8.17KB 8.16KB -15B 🟢
ListItem (Next) 69.88KB 69.82KB -62B 🟢
ListTitle (Next) 65.31KB 65.29KB -21B 🟢

📊 Summary:

  • Total Base Size: 4.75MB
  • Total PR Size: 4.75MB
  • Total Difference: 3.99KB

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant