diff --git a/.changeset/mean-roses-dress.md b/.changeset/mean-roses-dress.md new file mode 100644 index 00000000000..bbd7598c0df --- /dev/null +++ b/.changeset/mean-roses-dress.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Fix modal issues by inlining scroll locking mechanism instead of using `` which caused issues in Chromium based browsers diff --git a/packages/clerk-js/src/ui/elements/Modal.tsx b/packages/clerk-js/src/ui/elements/Modal.tsx index f0524ab6740..ee4e035aae4 100644 --- a/packages/clerk-js/src/ui/elements/Modal.tsx +++ b/packages/clerk-js/src/ui/elements/Modal.tsx @@ -1,9 +1,9 @@ -import { createContextAndHook } from '@clerk/shared/react'; -import { FloatingOverlay } from '@floating-ui/react'; +import { createContextAndHook, useSafeLayoutEffect } from '@clerk/shared/react'; import React, { useRef } from 'react'; import { descriptors, Flex } from '../customizables'; import { usePopover } from '../hooks'; +import { useScrollLock } from '../hooks/useScrollLock'; import type { ThemableCssProp } from '../styledSystem'; import { animations, mqu } from '../styledSystem'; import { withFloatingTree } from './contexts'; @@ -22,6 +22,7 @@ type ModalProps = React.PropsWithChildren<{ }>; export const Modal = withFloatingTree((props: ModalProps) => { + const { disableScrollLock, enableScrollLock } = useScrollLock(); const { handleClose, handleOpen, contentSx, containerSx, canCloseModal, id, style } = props; const overlayRef = useRef(null); const { floating, isOpen, context, nodeId, toggle } = usePopover({ @@ -38,62 +39,67 @@ export const Modal = withFloatingTree((props: ModalProps) => { handleOpen?.(); } }, [isOpen]); - const modalCtx = React.useMemo(() => ({ value: canCloseModal === false ? {} : { toggle } }), [toggle, canCloseModal]); + useSafeLayoutEffect(() => { + enableScrollLock(); + + return () => { + disableScrollLock(); + }; + }, []); + return ( - - + + ({ + animation: `${animations.fadeIn} 150ms ${t.transitionTiming.$common}`, + zIndex: t.zIndices.$modal, + backgroundColor: t.colors.$modalBackdrop, + alignItems: 'flex-start', + justifyContent: 'center', + overflow: 'auto', + width: '100vw', + height: ['100vh', '-webkit-fill-available'], + position: 'fixed', + left: 0, + top: 0, + }), + containerSx, + ]} + > ({ - animation: `${animations.fadeIn} 150ms ${t.transitionTiming.$common}`, - zIndex: t.zIndices.$modal, - backgroundColor: t.colors.$modalBackdrop, - alignItems: 'flex-start', - justifyContent: 'center', - overflow: 'auto', - width: '100vw', - height: ['100vh', '-webkit-fill-available'], - position: 'fixed', - left: 0, - top: 0, + position: 'relative', + outline: 0, + animation: `${animations.modalSlideAndFade} 180ms ${t.transitionTiming.$easeOut}`, + margin: `${t.space.$16} 0`, + [mqu.sm]: { + margin: `${t.space.$10} 0`, + }, }), - containerSx, + contentSx, ]} > - ({ - position: 'relative', - outline: 0, - animation: `${animations.modalSlideAndFade} 180ms ${t.transitionTiming.$easeOut}`, - margin: `${t.space.$16} 0`, - [mqu.sm]: { - margin: `${t.space.$10} 0`, - }, - }), - contentSx, - ]} - > - {props.children} - + {props.children} - - + + ); }); diff --git a/packages/clerk-js/src/ui/hooks/useScrollLock.ts b/packages/clerk-js/src/ui/hooks/useScrollLock.ts new file mode 100644 index 00000000000..3b3549380fc --- /dev/null +++ b/packages/clerk-js/src/ui/hooks/useScrollLock.ts @@ -0,0 +1,85 @@ +// The following code is adapted from Floating UI +// Source: https://github.com/floating-ui/floating-ui/blob/c09c59d6e594c3527888a52ed0f3e8a2978663c2/packages/react/src/components/FloatingOverlay.tsx +// Copyright (c) Floating UI contributors +// SPDX-License-Identifier: MIT +// Avoid Chrome DevTools blue warning. +export function getPlatform(): string { + const uaData = (navigator as any).userAgentData as { platform: string } | undefined; + + if (uaData?.platform) { + return uaData.platform; + } + + return navigator.platform; +} + +let lockCount = 0; +function enableScrollLock() { + const isIOS = /iP(hone|ad|od)|iOS/.test(getPlatform()); + const bodyStyle = document.body.style; + // RTL scrollbar + const scrollbarX = + Math.round(document.documentElement.getBoundingClientRect().left) + document.documentElement.scrollLeft; + const paddingProp = scrollbarX ? 'paddingLeft' : 'paddingRight'; + const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; + const scrollX = bodyStyle.left ? parseFloat(bodyStyle.left) : window.scrollX; + const scrollY = bodyStyle.top ? parseFloat(bodyStyle.top) : window.scrollY; + + bodyStyle.overflow = 'hidden'; + + if (scrollbarWidth) { + bodyStyle[paddingProp] = `${scrollbarWidth}px`; + } + + // Only iOS doesn't respect `overflow: hidden` on document.body, and this + // technique has fewer side effects. + if (isIOS) { + // iOS 12 does not support `visualViewport`. + const offsetLeft = window.visualViewport?.offsetLeft || 0; + const offsetTop = window.visualViewport?.offsetTop || 0; + + Object.assign(bodyStyle, { + position: 'fixed', + top: `${-(scrollY - Math.floor(offsetTop))}px`, + left: `${-(scrollX - Math.floor(offsetLeft))}px`, + right: '0', + }); + } + + return () => { + Object.assign(bodyStyle, { + overflow: '', + [paddingProp]: '', + }); + + if (isIOS) { + Object.assign(bodyStyle, { + position: '', + top: '', + left: '', + right: '', + }); + window.scrollTo(scrollX, scrollY); + } + }; +} + +let cleanup = () => {}; + +export function useScrollLock() { + return { + enableScrollLock: () => { + lockCount++; + + if (lockCount === 1) { + cleanup = enableScrollLock(); + } + }, + disableScrollLock: () => { + lockCount--; + if (lockCount === 0) { + cleanup(); + } + }, + }; +}