Skip to content

Commit

Permalink
Updated – Switching from @react-spring/web to motion (#2646)
Browse files Browse the repository at this point in the history
* Switch from `react-spring` to `motion`

* Handle reduced motion automatically

* Improved close icon alignment and hover state

* actually lazy load the animation code. 18kb gz savings

---------

Co-authored-by: David Crespo <[email protected]>
  • Loading branch information
benjaminleonard and david-crespo authored Jan 10, 2025
1 parent b55e161 commit b57531d
Show file tree
Hide file tree
Showing 16 changed files with 329 additions and 348 deletions.
3 changes: 1 addition & 2 deletions app/components/RoundedSector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
*
* Copyright Oxide Computer Company
*/
import { useReducedMotion } from 'motion/react'
import { useEffect, useMemo, useState } from 'react'

import { useReducedMotion } from '~/hooks/use-reduce-motion'

export function RoundedSector({
angle,
size,
Expand Down
48 changes: 21 additions & 27 deletions app/components/ToastStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,39 @@
*
* Copyright Oxide Computer Company
*/
import { animated, useTransition } from '@react-spring/web'
import { AnimatePresence } from 'motion/react'
import * as m from 'motion/react-m'

import { removeToast, useToastStore } from '~/stores/toast'
import { Toast } from '~/ui/lib/Toast'

export function ToastStack() {
const toasts = useToastStore((state) => state.toasts)

const transition = useTransition(toasts, {
keys: (toast) => toast.id,
from: { opacity: 0, y: 10, scale: 95 },
enter: { opacity: 1, y: 0, scale: 100 },
leave: { opacity: 0, y: 10, scale: 95 },
config: { duration: 100 },
})

return (
<div
className="pointer-events-auto fixed bottom-4 left-4 z-toast flex flex-col items-end space-y-2"
data-testid="Toasts"
>
{transition((style, item) => (
<animated.div
style={{
opacity: style.opacity,
y: style.y,
transform: style.scale.to((val) => `scale(${val}%, ${val}%)`),
}}
>
<Toast
key={item.id}
{...item.options}
onClose={() => {
removeToast(item.id)
item.options.onClose?.()
}}
/>
</animated.div>
))}
<AnimatePresence>
{toasts.map((toast) => (
<m.div
key={toast.id}
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.95 }}
transition={{ type: 'spring', duration: 0.2, bounce: 0 }}
>
<Toast
{...toast.options}
onClose={() => {
removeToast(toast.id)
toast.options.onClose?.()
}}
/>
</m.div>
))}
</AnimatePresence>
</div>
)
}
38 changes: 0 additions & 38 deletions app/hooks/use-reduce-motion.tsx

This file was deleted.

19 changes: 12 additions & 7 deletions app/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Copyright Oxide Computer Company
*/
import { QueryClientProvider } from '@tanstack/react-query'
import { LazyMotion, MotionConfig } from 'motion/react'
// import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
Expand All @@ -16,7 +17,6 @@ import { queryClient } from '@oxide/api'

import { ConfirmActionModal } from './components/ConfirmActionModal'
import { ErrorBoundary } from './components/ErrorBoundary'
import { ReduceMotion } from './hooks/use-reduce-motion'
// stripped out by rollup in production
import { startMockAPI } from './msw-mock-api'
import { routes } from './routes'
Expand All @@ -33,6 +33,8 @@ if (process.env.SHA) {
)
}

const loadFeatures = () => import('./util/motion-features').then((res) => res.domAnimation)

const root = createRoot(document.getElementById('root')!)

function render() {
Expand All @@ -46,12 +48,15 @@ function render() {
root.render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<ErrorBoundary>
<ConfirmActionModal />
<SkipLink id="skip-nav" />
<ReduceMotion />
<RouterProvider router={router} />
</ErrorBoundary>
<LazyMotion strict features={loadFeatures}>
<MotionConfig reducedMotion="user">
<ErrorBoundary>
<ConfirmActionModal />
<SkipLink id="skip-nav" />
<RouterProvider router={router} />
</ErrorBoundary>
</MotionConfig>
</LazyMotion>
{/* <ReactQueryDevtools initialIsOpen={false} /> */}
</QueryClientProvider>
</StrictMode>
Expand Down
34 changes: 28 additions & 6 deletions app/ui/lib/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Copyright Oxide Computer Company
*/
import cn from 'classnames'
import * as m from 'motion/react-m'
import { forwardRef, type MouseEventHandler, type ReactNode } from 'react'

import { Spinner } from '~/ui/lib/Spinner'
Expand Down Expand Up @@ -90,9 +91,14 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
with={<Tooltip content={disabledReason} ref={ref} placement="bottom" />}
>
<button
className={cn(buttonStyle({ size, variant }), className, {
'visually-disabled': isDisabled,
})}
className={cn(
buttonStyle({ size, variant }),
className,
{
'visually-disabled': isDisabled,
},
'overflow-hidden'
)}
ref={ref}
/* eslint-disable-next-line react/button-has-type */
type={type}
Expand All @@ -101,10 +107,26 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
aria-disabled={isDisabled}
{...rest}
>
{loading && <Spinner className="absolute" variant={variant} />}
<span className={cn('flex items-center', innerClassName, { invisible: loading })}>
{loading && (
<m.span
animate={{ opacity: 1, y: '-50%', x: '-50%' }}
initial={{ opacity: 0, y: 'calc(-50% - 25px)', x: '-50%' }}
transition={{ type: 'spring', duration: 0.3, bounce: 0 }}
className="absolute left-1/2 top-1/2"
>
<Spinner variant={variant} />
</m.span>
)}
<m.span
className={cn('flex items-center', innerClassName)}
animate={{
opacity: loading ? 0 : 1,
y: loading ? 25 : 0,
}}
transition={{ type: 'spring', duration: 0.3, bounce: 0 }}
>
{children}
</span>
</m.span>
</button>
</Wrap>
)
Expand Down
43 changes: 26 additions & 17 deletions app/ui/lib/CopyToClipboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
* Copyright Oxide Computer Company
*/

import { animated, config, useTransition } from '@react-spring/web'
import cn from 'classnames'
import { AnimatePresence } from 'motion/react'
import * as m from 'motion/react-m'
import { useState } from 'react'

import { Copy12Icon, Success12Icon } from '@oxide/design-system/icons/react'
Expand All @@ -20,6 +21,11 @@ type Props = {
className?: string
}

const variants = {
hidden: { opacity: 0, scale: 0.75 },
visible: { opacity: 1, scale: 1 },
}

export const CopyToClipboard = ({
ariaLabel = 'Click to copy',
text,
Expand All @@ -35,14 +41,14 @@ export const CopyToClipboard = ({
})
}

const transitions = useTransition(hasCopied, {
from: { opacity: 0, transform: 'scale(0.8)' },
enter: { opacity: 1, transform: 'scale(1)' },
leave: { opacity: 0, transform: 'scale(0.8)' },
config: config.stiff,
trail: 100,
initial: null,
})
const animateProps = {
className: 'absolute inset-0 flex items-center justify-center',
variants,
initial: 'hidden',
animate: 'visible',
exit: 'hidden',
transition: { type: 'spring', duration: 0.2, bounce: 0 },
}

return (
<button
Expand All @@ -58,14 +64,17 @@ export const CopyToClipboard = ({
type="button"
aria-label={hasCopied ? 'Copied' : ariaLabel}
>
{transitions((styles, item) => (
<animated.div
style={styles}
className="absolute inset-0 flex items-center justify-center"
>
{item ? <Success12Icon /> : <Copy12Icon />}
</animated.div>
))}
<AnimatePresence mode="wait" initial={false}>
{hasCopied ? (
<m.span key="checkmark" {...animateProps}>
<Success12Icon />
</m.span>
) : (
<m.span key="copy" {...animateProps}>
<Copy12Icon />
</m.span>
)}
</AnimatePresence>
</button>
)
}
7 changes: 6 additions & 1 deletion app/ui/lib/DialogOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@
* Copyright Oxide Computer Company
*/

import * as m from 'motion/react-m'
import { forwardRef } from 'react'

export const DialogOverlay = forwardRef<HTMLDivElement>((_, ref) => (
<div
<m.div
ref={ref}
aria-hidden
className="fixed inset-0 z-modalOverlay overflow-auto bg-scrim"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15, ease: 'easeOut' }}
/>
))
Loading

0 comments on commit b57531d

Please sign in to comment.