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

[web]Feat 36 standardize animations #108

Merged
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
71 changes: 43 additions & 28 deletions apps/web/components/base/alert-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
import * as React from 'react'

import { buttonVariants } from '~/components/base/button'
import useReducedMotion from '~/hooks/use-reduced-motion'
import { animations } from '~/lib/animation'
import { cn } from '~/lib/utils'

const AlertDialog = AlertDialogPrimitive.Root
Expand All @@ -15,39 +17,52 @@ const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className,
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName

const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
aria-live="assertive"
aria-atomic="true"
role="alertdialog"
aria-modal="true"
aria-describedby={props['aria-describedby'] || 'alert-dialog-desc'}
>(({ className, ...props }, ref) => {
const reducedMotion = useReducedMotion()
return (
<AlertDialogPrimitive.Overlay
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
'fixed inset-0 z-50',
reducedMotion
? 'bg-black/80'
: animations.fadeAndAnimateAndOverlay.inOut,
className,
)}
{...props}
ref={ref}
/>
</AlertDialogPortal>
))
)
})
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName

const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => {
const reducedMotion = useReducedMotion()

return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
aria-live="assertive"
aria-atomic="true"
role="alertdialog"
aria-modal="true"
aria-describedby={props['aria-describedby'] || 'alert-dialog-desc'}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-default',
reducedMotion ? '' : animations.fadeAndZoomAndAnimate.inOut,
animations.contentSlide.inOut,
'sm:rounded-lg',
className,
)}
{...props}
/>
</AlertDialogPortal>
)
})
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName

const AlertDialogHeader = ({
Expand Down
81 changes: 47 additions & 34 deletions apps/web/components/base/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { X } from 'lucide-react'
import * as React from 'react'

import useReducedMotion from '~/hooks/use-reduced-motion'
import { animations } from '~/lib/animation'
import { cn } from '~/lib/utils'

const Dialog = DialogPrimitive.Root
Expand All @@ -17,45 +18,57 @@ const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className,
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName

const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
>(({ className, ...props }, ref) => {
const reducedMotion = useReducedMotion()
return (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
'fixed inset-0 z-50',
reducedMotion
? 'bg-black/80'
: animations.fadeAndAnimateAndOverlay.inOut,
className,
)}
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
{...props}
>
{children}
<DialogPrimitive.Close
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
aria-label="Close dialog button. Click to close the dialog"
/>
)
})
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName

const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => {
const reducedMotion = useReducedMotion()

return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-default',
reducedMotion ? '' : animations.fadeAndAnimate.inOut,
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className,
)}
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
{...props}
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
{children}
<DialogPrimitive.Close
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
aria-label="Close dialog button. Click to close the dialog"
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
)
})
DialogContent.displayName = DialogPrimitive.Content.displayName

const DialogHeader = ({
Expand Down
67 changes: 45 additions & 22 deletions apps/web/components/base/dropdown-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { Check, ChevronRight, Circle } from 'lucide-react'
import * as React from 'react'
import useReducedMotion from '~/hooks/use-reduced-motion'
import { animations } from '~/lib/animation'

import { cn } from '~/lib/utils'

Expand All @@ -18,6 +20,18 @@ const DropdownMenuSub = DropdownMenuPrimitive.Sub

const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup

// * Returns animation classes for dropdown components
// * @param reducedMotion - Whether to disable animations
// * @returns Combined animation classes string
const getDropdownAnimationClasses = (reducedMotion: boolean): string => {
if (reducedMotion) return ''
return [
animations.fadeAndZoomAndAnimate.inOut,
// Slide animations
animations.slideAndAnimate.slideIn,
].join(' ')
}

const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
Expand All @@ -43,35 +57,44 @@ DropdownMenuSubTrigger.displayName =
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
>(({ className, ...props }, ref) => {
const reducedMotion = useReducedMotion()

const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
return (
<DropdownMenuPrimitive.SubContent
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
getDropdownAnimationClasses(reducedMotion),
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
)
})
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName

const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => {
const reducedMotion = useReducedMotion()
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
getDropdownAnimationClasses(reducedMotion),
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
})
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName

const DropdownMenuItem = React.forwardRef<
Expand Down
29 changes: 18 additions & 11 deletions apps/web/components/base/sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import * as SheetPrimitive from '@radix-ui/react-dialog'
import { type VariantProps, cva } from 'class-variance-authority'
import { X } from 'lucide-react'
import * as React from 'react'

import useReducedMotion from '~/hooks/use-reduced-motion'
import { animations } from '~/lib/animation'
import { cn } from '~/lib/utils'

const Sheet = SheetPrimitive.Root
Expand All @@ -18,16 +19,22 @@ const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className,
)}
{...props}
ref={ref}
/>
))
>(({ className, ...props }, ref) => {
const reducedMotion = useReducedMotion()
return (
<SheetPrimitive.Overlay
className={cn(
'fixed inset-0 z-50',
reducedMotion
? 'bg-black/80'
: animations.fadeAndAnimateAndOverlay.inOut,
className,
)}
{...props}
ref={ref}
/>
)
})
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName

const sheetVariants = cva(
Expand Down
38 changes: 38 additions & 0 deletions apps/web/hooks/use-reduced-motion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useEffect, useState } from 'react'

/**
* Tracks user's reduced motion preference using CSS media queries
* @returns boolean indicating if reduced motion is preferred
*/
const useReducedMotion = () => {
// State to store reduced motion preference (default: animations enabled)
const [reducedMotion, setReducedMotion] = useState(false)

useEffect(() => {
// Guard clause for SSR/Non-browser environments
if (typeof window === 'undefined') return

// Media query list for reduced motion preference
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)')

// Set initial state from current media query match
setReducedMotion(mediaQuery.matches)

// Handler for media query changes
const handleChange = (e: MediaQueryListEvent | MediaQueryListEvent) => {
setReducedMotion(e.matches)
}
if (mediaQuery.addEventListener) {
// Subscribe to media query changes
mediaQuery.addEventListener('change', handleChange)
return () => mediaQuery.removeEventListener('change', handleChange)
}
//fallback for older browsers
mediaQuery.addListener(handleChange)
return () => mediaQuery.removeListener(handleChange)
}, []) // Empty dependency array = runs only on mount

return reducedMotion
}

export default useReducedMotion
Loading
Loading