diff --git a/package.json b/package.json index 1f6dc3b..24e740d 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,9 @@ "react-router-dom": "^6.2.1", "react-virtualized": "^9.22.3", "@headlessui/react": "^1.6.6", - "react-device-detect": "^2.2.2" + "react-device-detect": "^2.2.2", + "@use-gesture/react": "^10.2.19", + "@react-spring/web": "^9.5.2" }, "loki": { "configurations": { diff --git a/src/features/notificationButton/ui/NotificationButton/NotificationButton.tsx b/src/features/notificationButton/ui/NotificationButton/NotificationButton.tsx index 69858fb..4d4f791 100644 --- a/src/features/notificationButton/ui/NotificationButton/NotificationButton.tsx +++ b/src/features/notificationButton/ui/NotificationButton/NotificationButton.tsx @@ -5,6 +5,7 @@ import NotificationIcon from 'shared/assets/icons/notification-20-20.svg'; import { Popover } from 'shared/ui/Popups'; import { Drawer } from 'shared/ui/Drawer/Drawer'; import { BrowserView, MobileView } from 'react-device-detect'; +import { AnimationProvider } from 'shared/lib/components/AnimationProvider'; import { NotificationList } from '../../../../entities/Notification'; import cls from './NotificationButton.module.scss'; @@ -37,9 +38,11 @@ export const NotificationButton = memo(() => { {trigger} - - - + + + + + ); diff --git a/src/shared/lib/components/AnimationProvider/AnimationProvider.tsx b/src/shared/lib/components/AnimationProvider/AnimationProvider.tsx new file mode 100644 index 0000000..48ea405 --- /dev/null +++ b/src/shared/lib/components/AnimationProvider/AnimationProvider.tsx @@ -0,0 +1,50 @@ +import { + createContext, ReactNode, useContext, useEffect, useMemo, useRef, useState, +} from 'react'; + +type SpringType = typeof import('@react-spring/web'); +type GestureType = typeof import('@use-gesture/react'); + +interface AnimationContextPayload { + Gesture?: GestureType; + Spring?: SpringType; + isLoaded?: boolean; +} + +const AnimationContext = createContext({}); + +// Both libs depend on each other +const getAsyncAnimationModules = async () => Promise.all([ + import('@react-spring/web'), + import('@use-gesture/react'), +]); + +export const useAnimationLibs = () => useContext(AnimationContext) as Required; + +export const AnimationProvider = ({ children }: {children: ReactNode}) => { + const SpringRef = useRef(); + const GestureRef = useRef(); + const [isLoaded, setIsLoaded] = useState(false); + + useEffect(() => { + getAsyncAnimationModules().then(([Spring, Gesture]) => { + SpringRef.current = Spring; + GestureRef.current = Gesture; + setIsLoaded(true); + }); + }, []); + + const value = useMemo(() => ({ + Gesture: GestureRef.current, + Spring: SpringRef.current, + isLoaded, + }), [isLoaded]); + + return ( + + {children} + + ); +}; diff --git a/src/shared/lib/components/AnimationProvider/index.ts b/src/shared/lib/components/AnimationProvider/index.ts new file mode 100644 index 0000000..9a5eea7 --- /dev/null +++ b/src/shared/lib/components/AnimationProvider/index.ts @@ -0,0 +1 @@ +export { AnimationProvider, useAnimationLibs } from './AnimationProvider'; diff --git a/src/shared/ui/Drawer/Drawer.module.scss b/src/shared/ui/Drawer/Drawer.module.scss index 5197604..330dafb 100644 --- a/src/shared/ui/Drawer/Drawer.module.scss +++ b/src/shared/ui/Drawer/Drawer.module.scss @@ -4,42 +4,11 @@ bottom: 0; right: 0; left: 0; - opacity: 0; - pointer-events: none; - z-index: -1; + z-index: 100; display: flex; align-items: flex-end; } -.content { - height: 70%; - background: var(--bg-color); - bottom: 0; - border-top-left-radius: 12px; - border-top-right-radius: 12px; - position: relative; - width: 100%; - min-height: 100px; - padding: 20px; - transition: 0.3s transform; - transform: translateY(100%); - overflow-y: auto; - overflow-x: hidden; - z-index: 10000; -} - -.content::before { - content: ""; - position: relative; - display: block; - width: 100px; - height: 10px; - background: var(--bg-color); - margin: auto; - bottom: 40px; - border-radius: 12px; -} - .opened { pointer-events: auto; opacity: 1; @@ -50,8 +19,14 @@ } } -.isClosing { - .content { - transform: translateY(100%); - } +.sheet { + z-index: var(--modal-z-index); + position: fixed; + left: 2vw; + height: calc(100vh + 100px); + width: 96vw; + border-radius: 12px 12px 0; + background: var(--bg-color); + touch-action: none; + padding: 15px; } diff --git a/src/shared/ui/Drawer/Drawer.tsx b/src/shared/ui/Drawer/Drawer.tsx index 69b19a7..f35d750 100644 --- a/src/shared/ui/Drawer/Drawer.tsx +++ b/src/shared/ui/Drawer/Drawer.tsx @@ -1,57 +1,107 @@ -import { classNames, Mods } from 'shared/lib/classNames/classNames'; -import React, { memo } from 'react'; +import { classNames } from 'shared/lib/classNames/classNames'; +import React, { + memo, ReactNode, useCallback, useEffect, +} from 'react'; import { useTheme } from 'app/providers/ThemeProvider'; -import { useModal } from 'shared/lib/hook/useModal/useModal'; -import { Portal } from '../Portal/Portal'; +import { useAnimationLibs } from 'shared/lib/components/AnimationProvider'; import { Overlay } from '../Overlay/Overlay'; import cls from './Drawer.module.scss'; +import { Portal } from '../Portal/Portal'; interface DrawerProps { className?: string; - children?: React.ReactNode; + children: ReactNode; isOpen?: boolean; onClose?: () => void; - lazy?: boolean; + lazy?: boolean; } -export const Drawer = memo((props: DrawerProps) => { +const height = window.innerHeight - 100; + +export const DrawerContent = memo((props: DrawerProps) => { + const { Spring, Gesture } = useAnimationLibs(); + const [{ y }, api] = Spring.useSpring(() => ({ y: height })); + const { theme } = useTheme(); const { className, children, - isOpen, onClose, + isOpen, lazy, } = props; - const { theme } = useTheme(); + const openDrawer = useCallback(() => { + api.start({ y: 0, immediate: false }); + }, [api]); - const { - isClosing, - close, - isMounted, - } = useModal({ - onClose, - animationDelay: 300, - isOpen, - }); + useEffect(() => { + if (isOpen) { + openDrawer(); + } + }, [api, isOpen, openDrawer]); - const mods: Mods = { - [cls.opened]: isOpen, - [cls.isClosing]: isClosing, + const close = (velocity = 0) => { + api.start({ + y: height, + immediate: false, + config: { ...Spring.config.stiff, velocity }, + onResolve: onClose, + }); }; - if (lazy && !isMounted) { + const bind = Gesture.useDrag( + ({ + last, + velocity: [, vy], + direction: [, dy], + movement: [, my], + cancel, + }) => { + if (my < -70) cancel(); + + if (last) { + if (my > height * 0.5 || (vy > 0.5 && dy > 0)) { + close(); + } else { + openDrawer(); + } + } else { + api.start({ y: my, immediate: true }); + } + }, + { + from: () => [0, y.get()], filterTaps: true, bounds: { top: 0 }, rubberband: true, + }, + ); + + if (!isOpen) { return null; } + const display = y.to((py) => (py < height ? 'block' : 'none')); + return ( -
+
-
+ {children} -
+
); }); + +export const Drawer = memo((props: DrawerProps) => { + const { isLoaded } = useAnimationLibs(); + + if (!isLoaded) { + return null; + } + + return ; +});