diff --git a/packages/core/package.json b/packages/core/package.json index 3665fd5a79..d56f75571d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -114,7 +114,6 @@ "react-focus-lock": "^2.13.2", "react-inlinesvg": "^4.1.3", "react-is": "^16.9.0", - "react-remove-scroll": "^2.6.0", "react-transition-group": "^4.4.5", "react-virtualized-auto-sizer": "^1.0.7", "react-window": "^1.8.7", diff --git a/packages/core/src/components/Modal/Modal/Modal.tsx b/packages/core/src/components/Modal/Modal/Modal.tsx index 5b6b4fa653..6ff53cf4e8 100644 --- a/packages/core/src/components/Modal/Modal/Modal.tsx +++ b/packages/core/src/components/Modal/Modal/Modal.tsx @@ -1,6 +1,5 @@ import React, { forwardRef, useCallback, useMemo, useRef, useState } from "react"; import cx from "classnames"; -import { RemoveScroll } from "react-remove-scroll"; import FocusLock from "react-focus-lock"; import { CSSTransition } from "react-transition-group"; import { getTestId } from "../../../tests/test-ids-utils"; @@ -16,6 +15,7 @@ import { keyCodes } from "../../../constants"; import { createPortal } from "react-dom"; import usePortalTarget from "../hooks/usePortalTarget/usePortalTarget"; import useFocusEscapeTargets from "../hooks/useFocusEscapeTargets/useFocusEscapeTargets"; +import useBodyScrollLock from "../hooks/useBodyScrollLock/useBodyScrollLock"; import { LayerProvider } from "@vibe/layer"; // @ts-expect-error This is a precaution to support all possible module systems (ESM/CJS) @@ -111,6 +111,8 @@ const Modal = forwardRef( const shouldAllowFocusEscape = useFocusEscapeTargets(allowFocusEscapeTo); + useBodyScrollLock(show); + /** * Returning true means that the focus-lock is allowed to manage the element. * Returning false means that the focus-lock would surrender control to the element. @@ -158,34 +160,33 @@ const Modal = forwardRef( onClick={onBackdropClick} aria-hidden /> - - - + , portalTargetElement diff --git a/packages/core/src/components/Modal/hooks/useBodyScrollLock/useBodyScrollLock.ts b/packages/core/src/components/Modal/hooks/useBodyScrollLock/useBodyScrollLock.ts new file mode 100644 index 0000000000..891e06cbb1 --- /dev/null +++ b/packages/core/src/components/Modal/hooks/useBodyScrollLock/useBodyScrollLock.ts @@ -0,0 +1,38 @@ +import { useEffect } from "react"; + +// Reference-counted body scroll lock so multiple stacked modals don't +// fight over `document.body` styles. The first lock captures and applies +// the styles; the last unlock restores them. +let activeLockCount = 0; +let savedBodyOverflow = ""; +let savedBodyPaddingRight = ""; + +const useBodyScrollLock = (locked: boolean) => { + useEffect(() => { + if (!locked || typeof document === "undefined") return undefined; + + if (activeLockCount === 0) { + const { body, documentElement } = document; + savedBodyOverflow = body.style.overflow; + savedBodyPaddingRight = body.style.paddingRight; + + const scrollbarWidth = window.innerWidth - documentElement.clientWidth; + body.style.overflow = "hidden"; + if (scrollbarWidth > 0) { + const currentPaddingRight = parseFloat(getComputedStyle(body).paddingRight) || 0; + body.style.paddingRight = `${currentPaddingRight + scrollbarWidth}px`; + } + } + + activeLockCount += 1; + return () => { + activeLockCount -= 1; + if (activeLockCount === 0) { + document.body.style.overflow = savedBodyOverflow; + document.body.style.paddingRight = savedBodyPaddingRight; + } + }; + }, [locked]); +}; + +export default useBodyScrollLock;