Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mean-roses-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Fix modal issues by inlining scroll locking mechanism instead of using `<FloatingOverlay/>` which caused issues in Chromium based browsers
92 changes: 49 additions & 43 deletions packages/clerk-js/src/ui/elements/Modal.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<HTMLDivElement>(null);
const { floating, isOpen, context, nodeId, toggle } = usePopover({
Expand All @@ -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 (
<Popover
nodeId={nodeId}
context={context}
isOpen={isOpen}
>
<FloatingOverlay lockScroll>
<ModalContext.Provider value={modalCtx}>
<ModalContext.Provider value={modalCtx}>
<Flex
id={id}
ref={overlayRef}
elementDescriptor={descriptors.modalBackdrop}
style={style}
sx={[
t => ({
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,
]}
>
<Flex
id={id}
ref={overlayRef}
elementDescriptor={descriptors.modalBackdrop}
style={style}
elementDescriptor={descriptors.modalContent}
ref={floating}
aria-modal='true'
role='dialog'
sx={[
t => ({
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,
]}
>
<Flex
elementDescriptor={descriptors.modalContent}
ref={floating}
aria-modal='true'
role='dialog'
sx={[
t => ({
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}
</Flex>
{props.children}
</Flex>
</ModalContext.Provider>
</FloatingOverlay>
</Flex>
</ModalContext.Provider>
</Popover>
);
});
85 changes: 85 additions & 0 deletions packages/clerk-js/src/ui/hooks/useScrollLock.ts
Original file line number Diff line number Diff line change
@@ -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 <body> 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();
}
},
};
}
Loading