Skip to content

Commit

Permalink
Make modal effect animated based on drag
Browse files Browse the repository at this point in the history
  • Loading branch information
Temzasse committed Dec 26, 2024
1 parent 2e5d366 commit 5ef080b
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 57 deletions.
97 changes: 83 additions & 14 deletions src/hooks.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,107 @@
import {
type MutableRefObject,
type RefObject,
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';

import { isBrowser, type BoundingBox } from 'framer-motion';
import { type MotionValue, type BoundingBox, transform } from 'framer-motion';

import { IS_SSR } from './constants';
import { type SheetEvents } from './types';
import { applyRootStyles, cleanupRootStyles } from './utils';

export const useIsomorphicLayoutEffect = IS_SSR ? useEffect : useLayoutEffect;

export function useModalEffect(isOpen: boolean, rootId?: string) {
const prevOpen = usePrevious(isOpen);
export function useModalEffect({
y,
rootId,
sheetRef,
}: {
y: MotionValue<number>;
rootId?: string;
sheetRef: RefObject<HTMLDivElement>;
}) {
const heightRef = useRef(window.innerHeight);

function setup() {
const root = document.querySelector(`#${rootId}`) as HTMLDivElement;
const body = document.querySelector('body') as HTMLBodyElement;
if (!root) return;

body.style.backgroundColor = '#000';
root.style.overflow = 'hidden';
root.style.transitionTimingFunction = 'cubic-bezier(0.32, 0.72, 0, 1)';
root.style.transitionProperty = 'transform, border-radius';
root.style.transitionDuration = '0.5s';
root.style.transformOrigin = 'center top';
}

function cleanup() {
const root = document.querySelector(`#${rootId}`) as HTMLDivElement;
const body = document.querySelector('body') as HTMLBodyElement;
if (!root) return;

setTimeout(() => {
body.style.removeProperty('background-color');
root.style.removeProperty('overflow');
root.style.removeProperty('transition-timing-function');
root.style.removeProperty('transition-property');
root.style.removeProperty('transition-duration');
root.style.removeProperty('transform-origin');
root.style.removeProperty('transform');
root.style.removeProperty('border-top-right-radius');
root.style.removeProperty('border-top-left-radius');
}, 100);
}

useIsomorphicLayoutEffect(() => {
return () => {
if (rootId) cleanup();
};
}, []); // eslint-disable-line

// Automatically apply the iOS modal effect to the body when sheet opens/closes
useEffect(() => {
if (rootId && !prevOpen && isOpen) {
applyRootStyles(rootId);
} else if (rootId && !isOpen && prevOpen) {
cleanupRootStyles(rootId);
const root = document.querySelector(`#${rootId}`) as HTMLDivElement;
if (!root) return;

function onCompleted() {
if (y.get() - 10 >= heightRef.current) cleanup();
}
}, [isOpen, prevOpen]); // eslint-disable-line

// Make sure to cleanup modal styles on unmount
useEffect(() => {
const removeStartListener = y.on('animationStart', () => {
heightRef.current = sheetRef.current?.offsetHeight || window.innerHeight;
setup();
});

const removeChangeListener = y.on('change', (value) => {
if (root) {
const progress = Math.max(0, 1 - value / heightRef.current);
const pageWidth = window.innerWidth;
const scale = (pageWidth - 16) / pageWidth;
const ty = transform(progress, [0, 1], [0, 24]);
const s = transform(progress, [0, 1], [1, scale]);
const borderRadius = transform(progress, [0, 1], [0, 10]);
const inset = 'env(safe-area-inset-top)';

root.style.transform = `scale(${s}) translate3d(0, calc(${inset} + ${ty}px), 0)`; // prettier-ignore
root.style.borderTopRightRadius = `${borderRadius}px`;
root.style.borderTopLeftRadius = `${borderRadius}px`;
}
});

const removeCompleteListener = y.on('animationComplete', onCompleted);
const removeCancelListener = y.on('animationCancel', onCompleted);

return () => {
if (rootId && isOpen) cleanupRootStyles(rootId);
removeStartListener();
removeChangeListener();
removeCompleteListener();
removeCancelListener();
};
}, [isOpen]); // eslint-disable-line
}, [y, rootId]); // eslint-disable-line
}

export function useEventCallbacks(
Expand Down
6 changes: 5 additions & 1 deletion src/sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,11 @@ const Sheet = forwardRef<any, SheetProps>(
},
}));

useModalEffect(isOpen, rootId);
useModalEffect({
y,
rootId,
sheetRef,
});

// Framer Motion should handle body scroll locking but it's not working
// properly on iOS. Scroll locking from React Aria seems to work much better.
Expand Down
42 changes: 0 additions & 42 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,48 +15,6 @@ export function getClosest(nums: number[], goal: number) {
return closest;
}

export function applyRootStyles(rootId: string) {
const body = document.querySelector('body') as HTMLBodyElement;
const root = document.querySelector(`#${rootId}`) as HTMLDivElement;

if (root) {
const p = 24;
const h = window.innerHeight;
const s = (h - p) / h;
body.style.backgroundColor = '#000';
root.style.overflow = 'hidden';
root.style.willChange = 'transform';
root.style.transition =
'transform 200ms ease-in-out, border-radius 200ms linear';
root.style.transform = `translateY(calc(env(safe-area-inset-top) + ${p / 2}px)) scale(${s})`; // prettier-ignore
root.style.borderTopRightRadius = '10px';
root.style.borderTopLeftRadius = '10px';
}
}

export function cleanupRootStyles(rootId: string) {
const body = document.querySelector('body') as HTMLBodyElement;
const root = document.getElementById(rootId) as HTMLDivElement;

function onTransitionEnd() {
root.style.removeProperty('overflow');
root.style.removeProperty('will-change');
root.style.removeProperty('transition');
body.style.removeProperty('background-color');
root.removeEventListener('transitionend', onTransitionEnd);
}

if (root) {
// Start animating back
root.style.removeProperty('border-top-right-radius');
root.style.removeProperty('border-top-left-radius');
root.style.removeProperty('transform');

// Remove temp properties after animation is finished
root.addEventListener('transitionend', onTransitionEnd);
}
}

export function inDescendingOrder(arr: number[]) {
for (let i = 0; i < arr.length; i++) {
if (arr[i + 1] > arr[i]) return false;
Expand Down

0 comments on commit 5ef080b

Please sign in to comment.