Skip to content

Commit

Permalink
[useScrollLock] New implementation (#1159)
Browse files Browse the repository at this point in the history
Co-authored-by: Vlad Moroz <[email protected]>
  • Loading branch information
atomiks and vladmoroz authored Dec 18, 2024
1 parent 5b4adc9 commit f4a7932
Show file tree
Hide file tree
Showing 10 changed files with 365 additions and 207 deletions.
18 changes: 16 additions & 2 deletions docs/src/app/(private)/experiments/scroll-lock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,28 @@ export default function ScrollLock() {
<div>
<h1>useScrollLock</h1>
<p>On macOS, enable `Show scroll bars: Always` in `Appearance` Settings.</p>
<div
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100%',
background: 'lightgray',
textAlign: 'right',
padding: 5,
}}
>
Fixed content should not shift
</div>
<div
style={{
position: 'fixed',
top: 15,
display: 'flex',
gap: 10,
background: 'white',
padding: 20,
background: 'rgba(0,0,0,0.1)',
backdropFilter: 'blur(20px)',
padding: 10,
}}
>
<div>
Expand Down
10 changes: 0 additions & 10 deletions docs/src/components/CodeBlock.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,6 @@
display: none;
}

/* iOS Safari appears to flicker the scroll containers during tasking animations */
[data-mobile-nav-open] & {
overflow: hidden;
}

/* Scroll containers may be focusable */
&:focus-visible {
position: relative;
Expand Down Expand Up @@ -65,11 +60,6 @@
/* Prevent Chrome/Safari page navigation gestures when scrolling horizontally */
overscroll-behavior-x: contain;

/* iOS Safari appears to flicker the scroll containers during tasking animations */
[data-mobile-nav-open] & {
overflow: hidden;
}

& code {
/* Different fonts may introduce vertical align issues */
display: block;
Expand Down
15 changes: 0 additions & 15 deletions docs/src/components/Demo/Demo.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,6 @@
overscroll-behavior-x: contain;
scrollbar-width: thin;

/* iOS Safari appears to flicker the scroll containers during tasking animations */
[data-mobile-nav-open] & {
overflow: hidden;
}

/* Scroll containers may be focusable */
&:focus-visible {
position: relative;
Expand Down Expand Up @@ -73,11 +68,6 @@
display: none;
}

/* iOS Safari appears to flicker the scroll containers during tasking animations */
[data-mobile-nav-open] & {
overflow: hidden;
}

/* Scroll containers may be focusable */
&:focus-visible {
position: relative;
Expand Down Expand Up @@ -180,11 +170,6 @@
/* Prevent Chrome/Safari page navigation gestures when scrolling horizontally */
overscroll-behavior-x: contain;

/* iOS Safari appears to flicker the scroll containers during tasking animations */
[data-mobile-nav-open] & {
overflow: hidden;
}

/* Scroll containers may be focusable */
&:focus-visible {
position: relative;
Expand Down
17 changes: 0 additions & 17 deletions docs/src/components/MobileNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,6 @@ function PopupImpl({ children }: React.PropsWithChildren) {
rem.current = parseFloat(getComputedStyle(document.documentElement).fontSize);
}, []);

// iOS Safari appears to flicker the scroll containers during tasking animations
// As a workaround, we set an attribute on body to know when the nav is mounted
// and disable scroll on the problematic scroll containers in the meantime
const timeout = React.useRef(0);
React.useLayoutEffect(() => {
window.clearTimeout(timeout.current);
document.body.setAttribute('data-mobile-nav-open', '');
return () => {
window.clearTimeout(timeout.current);
timeout.current = window.setTimeout(() => {
document.body.removeAttribute('data-mobile-nav-open');
// Safari seems to need some arbitrary time to be done with whatever causes the flicker
// Using setTimeout 0, RAFs, or even double RAFs doesn't work reliably
}, 100);
};
}, []);

return (
<React.Fragment>
<div className="MobileNavBottomOverscroll" />
Expand Down
27 changes: 12 additions & 15 deletions docs/src/components/QuickNav/QuickNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function onMounted(ref: React.RefObject<HTMLDivElement | null>) {

let top: number;
let bottom: number;
let prevScrollY = getScrollY();
let prevScrollY = window.scrollY;
let resizeObserver: ResizeObserver | undefined;
let state: 'Scrollable' | 'StickyTop' | 'StickyBottom' = 'StickyTop';
let raf = 0;
Expand Down Expand Up @@ -68,17 +68,17 @@ function onMounted(ref: React.RefObject<HTMLDivElement | null>) {
// if it was `position: static` and `position: absolute`
// relative to the start of the document
ref.current.style.position = 'static';
const staticTop = getScrollY() + ref.current.getBoundingClientRect().y;
const staticTop = window.scrollY + ref.current.getBoundingClientRect().y;
ref.current.style.position = 'absolute';
const absoluteTop = getScrollY() + ref.current.getBoundingClientRect().y;
const absoluteTop = window.scrollY + ref.current.getBoundingClientRect().y;

// Get the nav bottom Y coordinate when it's at its maximum possible bottom position
// relative to the start of the document
ref.current.style.position = 'absolute';
ref.current.style.top = 'auto';
ref.current.style.bottom = '0';
const rect = ref.current.getBoundingClientRect();
const absoluteBottom = getScrollY() + rect.bottom;
const absoluteBottom = window.scrollY + rect.bottom;

ref.current.style.position = '';
ref.current.style.top = initialStyles.top;
Expand Down Expand Up @@ -128,8 +128,8 @@ function onMounted(ref: React.RefObject<HTMLDivElement | null>) {
if (ref.current) {
state = 'Scrollable';
const { absoluteTop, absoluteBottom, staticTop } = getCachedPositions();
const marginTop = Math.max(staticTop - absoluteTop, getScrollY() + newTop - absoluteTop);
const marginBottom = Math.max(0, absoluteBottom - getScrollY() - newBottom);
const marginTop = Math.max(staticTop - absoluteTop, window.scrollY + newTop - absoluteTop);
const marginBottom = Math.max(0, absoluteBottom - window.scrollY - newBottom);

// Choose the smaller margin because at document edges,
// the larger one may push the nav out of the container edges
Expand All @@ -150,12 +150,14 @@ function onMounted(ref: React.RefObject<HTMLDivElement | null>) {
function handleUpdate() {
cancelAnimationFrame(raf);
raf = requestAnimationFrame(() => {
if (!ref.current) {
const isScrollLocked = document.documentElement.hasAttribute('data-base-ui-scroll-locked');

if (!ref.current || isScrollLocked) {
return;
}

const delta = getScrollY() - prevScrollY;
prevScrollY = getScrollY();
const delta = window.scrollY - prevScrollY;
prevScrollY = window.scrollY;

// We may get into <0.1px rounding issues in Safari and Firefox
const rect = ref.current.getBoundingClientRect();
Expand Down Expand Up @@ -228,7 +230,7 @@ function onMounted(ref: React.RefObject<HTMLDivElement | null>) {
unstick(Math.min(cssTop, top), Math.max(window.innerHeight, bottom));
}

prevScrollY = getScrollY();
prevScrollY = window.scrollY;
window.addEventListener('scroll', handleUpdate);
});
}
Expand All @@ -250,11 +252,6 @@ function onMounted(ref: React.RefObject<HTMLDivElement | null>) {
};
}

/** Get window.scrollY accounting for a potential scroll lock offset top style */
function getScrollY() {
return window.scrollY - parseFloat(document.documentElement.style.top || '0');
}

export function Title({ className, ...props }: React.ComponentProps<'h2'>) {
return <div aria-hidden className={clsx('QuickNavTitle', className)} {...props} />;
}
Expand Down
5 changes: 0 additions & 5 deletions docs/src/components/Table.css
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,5 @@
&::-webkit-scrollbar {
display: none;
}

/* iOS Safari appears to flicker the scroll containers during tasking animations */
[data-mobile-nav-open] & {
overflow: hidden;
}
}
}
1 change: 1 addition & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"@babel/runtime": "^7.26.0",
"@floating-ui/react": "^0.27.2",
"@floating-ui/utils": "^0.2.8",
"@react-aria/overlays": "^3.24.0",
"prop-types": "^15.8.1",
"use-sync-external-store": "^1.4.0"
},
Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/utils/detectBrowser.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { getUserAgent } from '@floating-ui/react/utils';

interface NavigatorUAData {
brands: Array<{ brand: string; version: string }>;
mobile: boolean;
Expand Down Expand Up @@ -29,3 +31,7 @@ export function isWebKit() {
export function isIOS() {
return /iP(hone|ad|od)|iOS/.test(getPlatform());
}

export function isFirefox() {
return /firefox/i.test(getUserAgent());
}
Loading

0 comments on commit f4a7932

Please sign in to comment.