Skip to content

Commit

Permalink
fix(react-dom): add useFadeIn & convert as render prop
Browse files Browse the repository at this point in the history
  • Loading branch information
manudeli committed Nov 21, 2024
1 parent 0ae4863 commit 7b55958
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 95 deletions.
40 changes: 22 additions & 18 deletions examples/visualization/src/app/react-dom/FadeIn/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,33 @@ export default function Page() {
clientOnly
fallback={
<FadeIn delay={200} duration={1000}>
{skeleton}
{(fadeIn) => (
<div {...fadeIn}>
<div role="status" className="mb-6 animate-pulse space-y-2">
<div className="h-4 w-[42px] rounded-sm bg-gray-300 dark:bg-gray-600" />
<div className="h-2 w-[34px] rounded-sm bg-gray-300 dark:bg-gray-600" />
<div className="h-2 w-[344px] rounded-sm bg-gray-300 dark:bg-gray-600" />
<div className="h-2 w-[344px] rounded-sm bg-gray-300 dark:bg-gray-600" />
<div className="h-4 w-[42px] rounded-sm bg-gray-300 dark:bg-gray-600" />
<div className="h-4 w-[34px] rounded-sm bg-gray-300 dark:bg-gray-600" />
</div>
</div>
)}
</FadeIn>
}
>
<SuspenseQuery {...query.user(userId)}>
{({ data: user }) => (
<FadeIn duration={200} className="max-w-[344px]" triggerOnce>
<h1 className="text-lg font-bold">{user.username}</h1>
<p className="text-xs">{user.userAgent}</p>
<p>{user.age}</p>
<p>{user.maidenName}</p>
<div className="mb-6" />
<FadeIn duration={200} triggerOnce>
{(fadeIn) => (
<div {...fadeIn} className="max-w-[344px]">
<h1 className="text-lg font-bold">{user.username}</h1>
<p className="text-xs">{user.userAgent}</p>
<p>{user.age}</p>
<p>{user.maidenName}</p>
<div className="mb-6" />
</div>
)}
</FadeIn>
)}
</SuspenseQuery>
Expand All @@ -62,14 +77,3 @@ export default function Page() {
</div>
)
}

const skeleton = (
<div role="status" className="mb-6 animate-pulse space-y-2">
<div className="h-4 w-[42px] rounded-sm bg-gray-300 dark:bg-gray-600" />
<div className="h-2 w-[34px] rounded-sm bg-gray-300 dark:bg-gray-600" />
<div className="h-2 w-[344px] rounded-sm bg-gray-300 dark:bg-gray-600" />
<div className="h-2 w-[344px] rounded-sm bg-gray-300 dark:bg-gray-600" />
<div className="h-4 w-[42px] rounded-sm bg-gray-300 dark:bg-gray-600" />
<div className="h-4 w-[34px] rounded-sm bg-gray-300 dark:bg-gray-600" />
</div>
)
24 changes: 24 additions & 0 deletions packages/react-dom/src/FadeIn.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { render, screen } from '@testing-library/react'
import { FadeIn } from './FadeIn'
import { mockAllIsIntersecting } from './test-utils'

describe('<FadeIn/>', () => {
it('renders children with correct styles when in view', () => {
mockAllIsIntersecting(false)
const mockChild = vi.fn()
render(
<FadeIn duration={300} timingFunction="ease-in">
{(fadeIn) => (
<div {...fadeIn} data-testid="fade-in-child">
{mockChild(fadeIn.style)}
</div>
)}
</FadeIn>
)
expect(mockChild).toHaveBeenCalledWith({ opacity: 0, willChange: 'opacity', transition: 'opacity 300ms ease-in' })
mockAllIsIntersecting(true)
const child = screen.getByTestId('fade-in-child')
expect(child).toHaveStyle({ opacity: '1', willChange: 'opacity', transition: 'opacity 300ms ease-in' })
expect(mockChild).toHaveBeenCalledWith({ opacity: 1, willChange: 'opacity', transition: 'opacity 300ms ease-in' })
})
})
23 changes: 4 additions & 19 deletions packages/react-dom/src/FadeIn.test-d.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,10 @@
import type { CSSProperties, ComponentProps } from 'react'
import { FadeIn } from './FadeIn'
import type { OmitKeyof } from './utility-types'

const Example1 = ({}: { x: string }) => <></>
const Example2 = () => <></>
import type { ComponentProps } from 'react'
import type { FadeIn } from './FadeIn'

describe('<FadeIn/>', () => {
it('type check', () => {
// @ts-expect-error ts(2322)
;(() => <FadeIn as="div" href="https://example.com" />)()
;(() => <FadeIn as="a" href="https://example.com" />)()
;(() => <FadeIn />)()
;(() => <FadeIn as={Example1} x="string" />)()
// @ts-expect-error ts(2322)
;(() => <FadeIn as={Example2} x="string" />)()

expectTypeOf<keyof ComponentProps<typeof FadeIn<typeof Example2>>>().toEqualTypeOf<
'root' | 'rootMargin' | 'threshold' | 'triggerOnce' | 'delay' | 'style' | 'as' | 'duration' | 'timingFunction'
>()
expectTypeOf<ComponentProps<typeof FadeIn<typeof Example2>>['style']>().toEqualTypeOf<
OmitKeyof<CSSProperties, 'opacity' | 'willChange' | 'transition'> | undefined
expectTypeOf<keyof ComponentProps<typeof FadeIn>>().toEqualTypeOf<
'root' | 'rootMargin' | 'threshold' | 'triggerOnce' | 'delay' | 'children' | 'duration' | 'timingFunction'
>()
})
})
67 changes: 11 additions & 56 deletions packages/react-dom/src/FadeIn.tsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,22 @@
import { type CSSProperties, type ComponentPropsWithoutRef, type ElementType } from 'react'
import { InView } from './InView'
import { type InViewOptions } from './useInView'
import { type OmitKeyof, type Override } from './utility-types'

type FadeInProps<TAs extends ElementType> = Override<
Override<
ComponentPropsWithoutRef<TAs>,
OmitKeyof<InViewOptions, 'fallbackInView' | 'initialInView' | 'skip' | 'onChange' | 'trackVisibility'>
>,
{
/**
* The element type to render.
* @default 'div'
*/
as?: TAs
/**
* The style of the element.
*/
style?: OmitKeyof<CSSProperties, 'opacity' | 'willChange' | 'transition'>
/**
* The duration in milliseconds of the animation.
* @default 200
*/
duration?: number
/**
* The timing function of the animation.
* @default 'linear'
*/
timingFunction?: CSSProperties['animationTimingFunction']
}
>
import { type ReactNode } from 'react'
import { type FadeInOptions, useFadeIn } from './useFadeIn'

type FadeInProps = FadeInOptions & {
children: (fadeInResult: ReturnType<typeof useFadeIn>) => ReactNode
}
/**
* A component that fades in when it comes into view.
*/
export function FadeIn<TAs extends ElementType = 'div'>({
as,
style,
// fadeIn Options
export function FadeIn({
duration = 200,
timingFunction = 'linear',
// inView options
delay,
root,
rootMargin,
threshold,
triggerOnce,
...restProps
}: FadeInProps<TAs>) {
const Component = as ?? 'div'
return (
<InView rootMargin={rootMargin} delay={delay} threshold={threshold} triggerOnce={triggerOnce}>
{({ inView, ref }) => (
<Component
{...restProps}
ref={ref}
style={{
...style,
opacity: inView ? 1 : 0,
willChange: 'opacity',
transition: `opacity ${duration}ms ${timingFunction}`,
}}
/>
)}
</InView>
)
children,
}: FadeInProps) {
const result = useFadeIn({ delay, duration, root, rootMargin, threshold, timingFunction, triggerOnce })
return <>{children(result)}</>
}
1 change: 1 addition & 0 deletions packages/react-dom/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { InView } from './InView'
export { useInView } from './useInView'
export { FadeIn } from './FadeIn'
export { useFadeIn } from './useFadeIn'
48 changes: 48 additions & 0 deletions packages/react-dom/src/useFadeIn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { type CSSProperties, useMemo } from 'react'
import { type InViewOptions, useInView } from './useInView'
import type { OmitKeyof } from './utility-types'

export type FadeInOptions = OmitKeyof<
InViewOptions,
'fallbackInView' | 'initialInView' | 'skip' | 'onChange' | 'trackVisibility'
> & {
/**
* The duration in milliseconds of the animation.
* @default 200
*/
duration?: number
/**
* The timing function of the animation.
* @default 'linear'
*/
timingFunction?: CSSProperties['transitionTimingFunction']
}
type FadeInResult = Pick<ReturnType<typeof useInView>, 'ref'> & {
style: {
opacity: 0 | 1
willChange: 'opacity'
transition: `opacity ${number}ms ${Required<CSSProperties>['transitionTimingFunction']}`
}
}
export function useFadeIn({
duration = 200,
timingFunction = 'linear',
delay,
root,
rootMargin,
threshold,
triggerOnce,
}: FadeInOptions): FadeInResult {
const { inView, ref } = useInView({ delay, root, rootMargin, threshold, triggerOnce })
return useMemo<FadeInResult>(
() => ({
ref,
style: {
opacity: inView ? 1 : 0,
willChange: 'opacity',
transition: `opacity ${duration}ms ${timingFunction}` as const,
},
}),
[inView, duration, timingFunction]
)
}
1 change: 0 additions & 1 deletion packages/react-dom/src/utility-types/Override.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/react-dom/src/utility-types/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export type { OmitKeyof } from './OmitKeyof'
export type { Override } from './Override'

0 comments on commit 7b55958

Please sign in to comment.