Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add usePositionFixed hook #436

Merged
merged 8 commits into from
Sep 22, 2024
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
41 changes: 29 additions & 12 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import { DrawerDirection } from './types';
import { useControllableState } from './use-controllable-state';
import { useScaleBackground } from './use-scale-background';
import { usePositionFixed } from './use-position-fixed';

export interface WithFadeFromProps {
snapPoints: (number | string)[];
Expand Down Expand Up @@ -58,6 +59,7 @@ export type DialogProps = {
snapToSequentialPoint?: boolean;
container?: HTMLElement | null;
onAnimationEnd?: (open: boolean) => void;
preventScrollRestoration?: boolean;
} & (WithFadeFromProps | WithoutFadeFromProps);

export function Root({
Expand All @@ -79,11 +81,13 @@ export function Root({
fixed,
modal = true,
onClose,
nested,
noBodyStyles,
direction = 'bottom',
defaultOpen = false,
disablePreventScroll = true,
snapToSequentialPoint = false,
preventScrollRestoration = false,
repositionInputs = true,
onAnimationEnd,
container,
Expand All @@ -94,6 +98,10 @@ export function Root({
onChange: (o: boolean) => {
onOpenChange?.(o);

if (!o) {
restorePositionSetting();
}

setTimeout(() => {
onAnimationEnd?.(o);
}, TRANSITIONS.DURATION * 1000);
Expand Down Expand Up @@ -154,6 +162,15 @@ export function Root({
!isOpen || isDragging || !modal || justReleased || !hasBeenOpened || !repositionInputs || !disablePreventScroll,
});

const { restorePositionSetting } = usePositionFixed({
isOpen,
modal,
nested,
hasBeenOpened,
preventScrollRestoration,
noBodyStyles,
});

function getScale() {
return (window.innerWidth - WINDOW_TOP_OFFSET) / window.innerWidth;
}
Expand All @@ -173,7 +190,7 @@ export function Root({
// Ensure we maintain correct pointer capture even when going outside of the drawer
(event.target as HTMLElement).setPointerCapture(event.pointerId);

pointerStart.current = isVertical(direction) ? event.clientY : event.clientX;
pointerStart.current = isVertical(direction) ? event.pageY : event.pageX;
}

function shouldDrag(el: EventTarget, isDraggingInDirection: boolean) {
Expand Down Expand Up @@ -212,12 +229,12 @@ export function Root({
return false;
}

if (isDraggingInDirection) {
lastTimeDragPrevented.current = date;
// if (isDraggingInDirection) {
// lastTimeDragPrevented.current = date;

// We are dragging down so we should allow scrolling
return false;
}
// // We are dragging down so we should allow scrolling
// return false;
// }

// Keep climbing up the DOM tree as long as there's a parent
while (element) {
Expand Down Expand Up @@ -252,7 +269,7 @@ export function Root({
if (isDragging) {
const directionMultiplier = direction === 'bottom' || direction === 'right' ? 1 : -1;
const draggedDistance =
(pointerStart.current - (isVertical(direction) ? event.clientY : event.clientX)) * directionMultiplier;
(pointerStart.current - (isVertical(direction) ? event.pageY : event.pageX)) * directionMultiplier;
const isDraggingInDirection = draggedDistance > 0;

// Pre condition for disallowing dragging in the close direction.
Expand Down Expand Up @@ -489,7 +506,7 @@ export function Root({
if (dragStartTime.current === null) return;

const timeTaken = dragEndTime.current.getTime() - dragStartTime.current.getTime();
const distMoved = pointerStart.current - (isVertical(direction) ? event.clientY : event.clientX);
const distMoved = pointerStart.current - (isVertical(direction) ? event.pageY : event.pageX);
const velocity = Math.abs(distMoved) / timeTaken;

if (velocity > 0.05) {
Expand Down Expand Up @@ -765,7 +782,7 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(function (
onPointerDown={(event) => {
if (handleOnly) return;
rest.onPointerDown?.(event);
pointerStartRef.current = { x: event.clientX, y: event.clientY };
pointerStartRef.current = { x: event.pageX, y: event.pageY };
onPress(event);
}}
onPointerDownOutside={(e) => {
Expand All @@ -790,8 +807,8 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(function (
if (handleOnly) return;
rest.onPointerMove?.(event);
if (!pointerStartRef.current) return;
const yPosition = event.clientY - pointerStartRef.current.y;
const xPosition = event.clientX - pointerStartRef.current.x;
const yPosition = event.pageY - pointerStartRef.current.y;
const xPosition = event.pageX - pointerStartRef.current.x;

const swipeStartThreshold = event.pointerType === 'touch' ? 10 : 2;
const delta = { x: xPosition, y: yPosition };
Expand Down Expand Up @@ -867,7 +884,7 @@ export const Handle = React.forwardRef<HTMLDivElement, HandleProps>(function (
}

const isLastSnapPoint = activeSnapPoint === snapPoints[snapPoints.length - 1];

if (isLastSnapPoint && dismissible) {
closeDrawer();
return;
Expand Down
131 changes: 131 additions & 0 deletions src/use-position-fixed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import React from 'react';
import { isSafari } from './use-prevent-scroll';

let previousBodyPosition: Record<string, string> | null = null;

/**
* This hook is necessary to prevent buggy behavior on iOS devices (need to test on Android).
* I won't get into too much detail about what bugs it solves, but so far I've found that setting the body to `position: fixed` is the most reliable way to prevent those bugs.
* Issues that this hook solves:
* https://github.com/emilkowalski/vaul/issues/435
* https://github.com/emilkowalski/vaul/issues/433
* And more that I discovered, but were just not reported.
*/

export function usePositionFixed({
isOpen,
modal,
nested,
hasBeenOpened,
preventScrollRestoration,
noBodyStyles,
}: {
isOpen: boolean;
modal: boolean;
nested: boolean;
hasBeenOpened: boolean;
preventScrollRestoration: boolean;
noBodyStyles: boolean;
}) {
const [activeUrl, setActiveUrl] = React.useState(() => (typeof window !== 'undefined' ? window.location.href : ''));
const scrollPos = React.useRef(0);

const setPositionFixed = React.useCallback(() => {
// All browsers on iOS will return true here.
if (!isSafari()) return;

// If previousBodyPosition is already set, don't set it again.
if (previousBodyPosition === null && isOpen && !noBodyStyles) {
previousBodyPosition = {
position: document.body.style.position,
top: document.body.style.top,
left: document.body.style.left,
height: document.body.style.height,
right: 'unset',
};

// Update the dom inside an animation frame
const { scrollX, innerHeight } = window;

document.body.style.setProperty('position', 'fixed', 'important');
Object.assign(document.body.style, {
top: `${-scrollPos.current}px`,
left: `${-scrollX}px`,
right: '0px',
height: 'auto',
});

window.setTimeout(
() =>
window.requestAnimationFrame(() => {
// Attempt to check if the bottom bar appeared due to the position change
const bottomBarHeight = innerHeight - window.innerHeight;
if (bottomBarHeight && scrollPos.current >= innerHeight) {
// Move the content further up so that the bottom bar doesn't hide it
document.body.style.top = `${-(scrollPos.current + bottomBarHeight)}px`;
}
}),
300,
);
}
}, [isOpen]);

const restorePositionSetting = React.useCallback(() => {
// All browsers on iOS will return true here.
if (!isSafari()) return;

if (previousBodyPosition !== null && !noBodyStyles) {
// Convert the position from "px" to Int
const y = -parseInt(document.body.style.top, 10);
const x = -parseInt(document.body.style.left, 10);

// Restore styles
Object.assign(document.body.style, previousBodyPosition);

window.requestAnimationFrame(() => {
if (preventScrollRestoration && activeUrl !== window.location.href) {
setActiveUrl(window.location.href);
return;
}

window.scrollTo(x, y);
});

previousBodyPosition = null;
}
}, [activeUrl]);

React.useEffect(() => {
function onScroll() {
scrollPos.current = window.scrollY;
}

onScroll();

window.addEventListener('scroll', onScroll);

return () => {
window.removeEventListener('scroll', onScroll);
};
}, []);

React.useEffect(() => {
if (nested || !hasBeenOpened) return;
// This is needed to force Safari toolbar to show **before** the drawer starts animating to prevent a gnarly shift from happening
if (isOpen) {
// avoid for standalone mode (PWA)
const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
!isStandalone && setPositionFixed();

if (!modal) {
window.setTimeout(() => {
restorePositionSetting();
}, 500);
}
} else {
restorePositionSetting();
}
}, [isOpen, hasBeenOpened, activeUrl, modal, nested, setPositionFixed, restorePositionSetting]);

return { restorePositionSetting };
}
4 changes: 1 addition & 3 deletions src/use-scale-background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function useScaleBackground() {

if (!wrapper) return;

const bodyAndWrapperCleanup = chain(
chain(
setBackgroundColorOnScale && !noBodyStyles ? assignStyle(document.body, { background: 'black' }) : noop,
assignStyle(wrapper, {
transformOrigin: isVertical(direction) ? 'top' : 'left',
Expand All @@ -48,8 +48,6 @@ export function useScaleBackground() {
return () => {
wrapperStylesCleanup();
timeoutIdRef.current = window.setTimeout(() => {
bodyAndWrapperCleanup();

if (initialBackgroundColor) {
document.body.style.background = initialBackgroundColor;
} else {
Expand Down
2 changes: 1 addition & 1 deletion test/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

body,
main {
/* min-height: 500vh; */
min-height: 500vh;
}

html {
Expand Down
Loading
Loading