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

Use CSS Animations for entry and exit animations #354

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ export const TRANSITIONS = {
};

export const VELOCITY_THRESHOLD = 0.4;

export const BORDER_RADIUS = 8;

export const WINDOW_TOP_OFFSET = 26;
8 changes: 6 additions & 2 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { DrawerDirection } from './types';
interface DrawerContextValue {
drawerRef: React.RefObject<HTMLDivElement>;
overlayRef: React.RefObject<HTMLDivElement>;
scaleBackground: (open: boolean) => void;
onPress: (event: React.PointerEvent<HTMLDivElement>) => void;
onRelease: (event: React.PointerEvent<HTMLDivElement>) => void;
onDrag: (event: React.PointerEvent<HTMLDivElement>) => void;
Expand All @@ -28,12 +27,14 @@ interface DrawerContextValue {
openProp?: boolean;
onOpenChange?: (o: boolean) => void;
direction?: DrawerDirection;
shouldScaleBackground: boolean;
setBackgroundColorOnScale: boolean;
noBodyStyles: boolean;
}

export const DrawerContext = React.createContext<DrawerContextValue>({
drawerRef: { current: null },
overlayRef: { current: null },
scaleBackground: () => {},
onPress: () => {},
onRelease: () => {},
onDrag: () => {},
Expand All @@ -57,6 +58,9 @@ export const DrawerContext = React.createContext<DrawerContextValue>({
closeDrawer: () => {},
setVisible: () => {},
direction: 'bottom',
shouldScaleBackground: false,
setBackgroundColorOnScale: true,
noBodyStyles: false,
});

export const useDrawerContext = () => {
Expand Down
27 changes: 26 additions & 1 deletion src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DrawerDirection } from './types';
import { AnyFunction, DrawerDirection } from './types';

interface Style {
[key: string]: string;
Expand Down Expand Up @@ -90,3 +90,28 @@ export function getTranslate(element: HTMLElement, direction: DrawerDirection):
export function dampenValue(v: number) {
return 8 * (Math.log(v + 1) - 2);
}

export function assignStyle(element: HTMLElement | null | undefined, style: Partial<CSSStyleDeclaration>) {
if (!element) return () => {};

const prevStyle = element.style.cssText;
Object.assign(element.style, style);

return () => {
element.style.cssText = prevStyle;
};
}

/**
* Receives functions as arguments and returns a new function that calls all.
*/
export function chain<T>(...fns: T[]) {
return (...args: T extends AnyFunction ? Parameters<T> : never) => {
for (const fn of fns) {
if (typeof fn === 'function') {
// @ts-ignore
fn(...args);
}
}
};
}
146 changes: 37 additions & 109 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
'use client';

import * as DialogPrimitive from '@radix-ui/react-dialog';
import React, { useRef, useState } from 'react';
import React from 'react';
import { DrawerContext, useDrawerContext } from './context';
import './style.css';
import { usePreventScroll, isInput, isIOS, useIsomorphicLayoutEffect } from './use-prevent-scroll';
import { useComposedRefs } from './use-composed-refs';
import { usePositionFixed } from './use-position-fixed';
import { useSnapPoints } from './use-snap-points';
import { set, reset, getTranslate, dampenValue, isVertical } from './helpers';
import { TRANSITIONS, VELOCITY_THRESHOLD } from './constants';
import { set, getTranslate, dampenValue, isVertical } from './helpers';
import { BORDER_RADIUS, TRANSITIONS, VELOCITY_THRESHOLD } from './constants';
import { DrawerDirection } from './types';
import { useControllableState } from './use-controllable-state';
import { useScaleBackground } from './use-scale-background';

const CLOSE_THRESHOLD = 0.25;

const SCROLL_LOCK_TIMEOUT = 100;

const BORDER_RADIUS = 8;

const NESTED_DISPLACEMENT = 16;

const WINDOW_TOP_OFFSET = 26;
Expand All @@ -38,6 +38,7 @@
activeSnapPoint?: number | string | null;
setActiveSnapPoint?: (snapPoint: number | string | null) => void;
children?: React.ReactNode;
defaultOpen?: boolean;
open?: boolean;
closeThreshold?: number;
noBodyStyles?: boolean;
Expand All @@ -59,10 +60,11 @@
} & (WithFadeFromProps | WithoutFadeFromProps);

function Root({
defaultOpen,
open: openProp,
onOpenChange,
children,
shouldScaleBackground,
shouldScaleBackground = false,
onDrag: onDragProp,
onRelease: onReleaseProp,
snapPoints,
Expand All @@ -83,11 +85,14 @@
preventScrollRestoration = true,
disablePreventScroll = false,
}: DialogProps) {
const [isOpen = false, setIsOpen] = React.useState<boolean>(false);
const [isOpen = false, setIsOpen] = useControllableState({
defaultProp: defaultOpen,
prop: openProp,
onChange: onOpenChange,
});
const [hasBeenOpened, setHasBeenOpened] = React.useState<boolean>(false);
// Not visible = translateY(100%)
const [visible, setVisible] = React.useState<boolean>(false);
const [mounted, setMounted] = React.useState<boolean>(false);
const [isDragging, setIsDragging] = React.useState<boolean>(false);
const [justReleased, setJustReleased] = React.useState<boolean>(false);
const overlayRef = React.useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -346,7 +351,6 @@

React.useEffect(() => {
return () => {
scaleBackground(false);
restorePositionSetting();
};
}, []);
Expand Down Expand Up @@ -409,29 +413,10 @@
}, [activeSnapPointIndex, snapPoints, snapPointsOffset]);

function closeDrawer() {
if (!drawerRef.current) return;

cancelDrag();

onClose?.();
set(drawerRef.current, {
transform: isVertical(direction)
? `translate3d(0, ${direction === 'bottom' ? '100%' : '-100%'}, 0)`
: `translate3d(${direction === 'right' ? '100%' : '-100%'}, 0, 0)`,
transition: `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`,
});

set(overlayRef.current, {
opacity: '0',
transition: `opacity ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`,
});

scaleBackground(false);

setTimeout(() => {
setVisible(false);
setIsOpen(false);
}, 300);
setIsOpen(false);
setVisible(false);

setTimeout(() => {
// reset(document.documentElement, 'scrollBehavior');
Expand All @@ -441,11 +426,12 @@
}, TRANSITIONS.DURATION * 1000); // seconds to ms
}


React.useEffect(() => {
if (!isOpen && shouldScaleBackground) {
// Can't use `onAnimationEnd` as the component will be invisible by then
const id = setTimeout(() => {
reset(document.body);

Check failure on line 434 in src/index.tsx

View workflow job for this annotation

GitHub Actions / test

Cannot find name 'reset'. Did you mean 'set'?
}, 200);

return () => clearTimeout(id);
Expand All @@ -464,15 +450,16 @@

// This can be done much better
React.useEffect(() => {
if (mounted) {

Check failure on line 453 in src/index.tsx

View workflow job for this annotation

GitHub Actions / test

Cannot find name 'mounted'.
onOpenChange?.(isOpen);
}
}, [isOpen]);

React.useEffect(() => {
setMounted(true);

Check failure on line 459 in src/index.tsx

View workflow job for this annotation

GitHub Actions / test

Cannot find name 'setMounted'.
}, []);


function resetDrawer() {
if (!drawerRef.current) return;
const wrapper = document.querySelector('[vaul-drawer-wrapper]');
Expand Down Expand Up @@ -593,7 +580,6 @@
});

openTime.current = new Date();
scaleBackground(true);
}
}, [isOpen]);

Expand All @@ -610,58 +596,6 @@
}
}, [visible]);

function scaleBackground(open: boolean) {
const wrapper = document.querySelector('[vaul-drawer-wrapper]');

if (!wrapper || !shouldScaleBackground) return;

if (open) {
if (setBackgroundColorOnScale) {
if (!noBodyStyles) {
// setting original styles initially
set(document.body, {
background: document.body.style.backgroundColor || document.body.style.background,
});
// setting body styles, with cache ignored, so that we can get correct original styles in reset
set(
document.body,
{
background: 'black',
},
true,
);
}
}

set(wrapper, {
borderRadius: `${BORDER_RADIUS}px`,
overflow: 'hidden',
...(isVertical(direction)
? {
transform: `scale(${getScale()}) translate3d(0, calc(env(safe-area-inset-top) + 14px), 0)`,
transformOrigin: 'top',
}
: {
transform: `scale(${getScale()}) translate3d(calc(env(safe-area-inset-top) + 14px), 0, 0)`,
transformOrigin: 'left',
}),
transitionProperty: 'transform, border-radius',
transitionDuration: `${TRANSITIONS.DURATION}s`,
transitionTimingFunction: `cubic-bezier(${TRANSITIONS.EASE.join(',')})`,
});
} else {
// Exit
reset(wrapper, 'overflow');
reset(wrapper, 'transform');
reset(wrapper, 'borderRadius');
set(wrapper, {
transitionProperty: 'transform, border-radius',
transitionDuration: `${TRANSITIONS.DURATION}s`,
transitionTimingFunction: `cubic-bezier(${TRANSITIONS.EASE.join(',')})`,
});
}
}

function onNestedOpenChange(o: boolean) {
const scale = o ? (window.innerWidth - NESTED_DISPLACEMENT) / window.innerWidth : 1;
const y = o ? -NESTED_DISPLACEMENT : 0;
Expand All @@ -688,7 +622,7 @@
}
}

function onNestedDrag(event: React.PointerEvent<HTMLDivElement>, percentageDragged: number) {
function onNestedDrag(_event: React.PointerEvent<HTMLDivElement>, percentageDragged: number) {
if (percentageDragged < 0) return;

const initialDim = isVertical(direction) ? window.innerHeight : window.innerWidth;
Expand All @@ -704,7 +638,7 @@
});
}

function onNestedRelease(event: React.PointerEvent<HTMLDivElement>, o: boolean) {
function onNestedRelease(_event: React.PointerEvent<HTMLDivElement>, o: boolean) {
const dim = isVertical(direction) ? window.innerHeight : window.innerWidth;
const scale = o ? (dim - NESTED_DISPLACEMENT) / dim : 1;
const translate = o ? -NESTED_DISPLACEMENT : 0;
Expand All @@ -722,18 +656,10 @@
return (
<DialogPrimitive.Root
modal={modal}
onOpenChange={(o: boolean) => {
if (openProp !== undefined) {
onOpenChange?.(o);
return;
}

if (!o) {
closeDrawer();
} else {
setHasBeenOpened(true);
setIsOpen(o);
}
defaultOpen={defaultOpen}
onOpenChange={(open) => {
if (open) setHasBeenOpened(true);
setIsOpen(open);
}}
open={isOpen}
>
Expand All @@ -745,7 +671,6 @@
setActiveSnapPoint,
drawerRef,
overlayRef,
scaleBackground,
onOpenChange,
onPress,
setVisible,
Expand All @@ -765,6 +690,9 @@
modal,
snapPointsOffset,
direction,
shouldScaleBackground,
setBackgroundColorOnScale,
noBodyStyles,
}}
>
{children}
Expand Down Expand Up @@ -916,20 +844,20 @@
onPress,
onRelease,
onDrag,
dismissible,
keyboardIsOpen,
snapPointsOffset,
visible,
closeDrawer,
modal,
openProp,
onOpenChange,
setVisible,
handleOnly,
direction,
isOpen,
snapPoints,
closeDrawer,
} = useDrawerContext();
const composedRef = useComposedRefs(ref, drawerRef);
const pointerStartRef = React.useRef<{ x: number; y: number } | null>(null);
const hasSnapPoints = snapPoints && snapPoints.length > 0;
const wasBeyondThePointRef = React.useRef(false);

const isDeltaInDirection = (delta: { x: number; y: number }, direction: DrawerDirection, threshold = 0) => {
Expand Down Expand Up @@ -961,11 +889,18 @@
setVisible(true);
}, []);

React.useEffect(() => {
if (!isOpen) closeDrawer();
}, [closeDrawer, isOpen]);

useScaleBackground();

return (
<DialogPrimitive.Content
vaul-drawer=""
vaul-drawer-direction={direction}
vaul-drawer-visible={visible ? 'true' : 'false'}
vaul-snap-points={isOpen && hasSnapPoints ? 'true' : 'false'}
{...rest}
ref={composedRef}
style={
Expand Down Expand Up @@ -999,13 +934,6 @@
if (keyboardIsOpen.current) {
keyboardIsOpen.current = false;
}
e.preventDefault();
onOpenChange?.(false);
if (!dismissible || openProp !== undefined) {
return;
}

closeDrawer();
}}
onFocusOutside={(e) => {
if (!modal) {
Expand Down
Loading
Loading