diff --git a/examples/visualization/src/app/react-dom/FadeIn/page.tsx b/examples/visualization/src/app/react-dom/FadeIn/page.tsx index 461aa47c8..833169b75 100644 --- a/examples/visualization/src/app/react-dom/FadeIn/page.tsx +++ b/examples/visualization/src/app/react-dom/FadeIn/page.tsx @@ -40,18 +40,33 @@ export default function Page() { clientOnly fallback={ - {skeleton} + {(fadeIn) => ( +
+
+
+
+
+
+
+
+
+
+ )} } > {({ data: user }) => ( - -

{user.username}

-

{user.userAgent}

-

{user.age}

-

{user.maidenName}

-
+ + {(fadeIn) => ( +
+

{user.username}

+

{user.userAgent}

+

{user.age}

+

{user.maidenName}

+
+
+ )} )} @@ -62,14 +77,3 @@ export default function Page() {
) } - -const skeleton = ( -
-
-
-
-
-
-
-
-) diff --git a/packages/react-dom/src/FadeIn.spec.tsx b/packages/react-dom/src/FadeIn.spec.tsx new file mode 100644 index 000000000..0ed9ce558 --- /dev/null +++ b/packages/react-dom/src/FadeIn.spec.tsx @@ -0,0 +1,24 @@ +import { render, screen } from '@testing-library/react' +import { FadeIn } from './FadeIn' +import { mockAllIsIntersecting } from './test-utils' + +describe('', () => { + it('renders children with correct styles when in view', () => { + mockAllIsIntersecting(false) + const mockChild = vi.fn() + render( + + {(fadeIn) => ( +
+ {mockChild(fadeIn.style)} +
+ )} +
+ ) + 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' }) + }) +}) diff --git a/packages/react-dom/src/FadeIn.test-d.tsx b/packages/react-dom/src/FadeIn.test-d.tsx index 15af2837b..fc30a2927 100644 --- a/packages/react-dom/src/FadeIn.test-d.tsx +++ b/packages/react-dom/src/FadeIn.test-d.tsx @@ -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('', () => { it('type check', () => { - // @ts-expect-error ts(2322) - ;(() => )() - ;(() => )() - ;(() => )() - ;(() => )() - // @ts-expect-error ts(2322) - ;(() => )() - - expectTypeOf>>().toEqualTypeOf< - 'root' | 'rootMargin' | 'threshold' | 'triggerOnce' | 'delay' | 'style' | 'as' | 'duration' | 'timingFunction' - >() - expectTypeOf>['style']>().toEqualTypeOf< - OmitKeyof | undefined + expectTypeOf>().toEqualTypeOf< + 'root' | 'rootMargin' | 'threshold' | 'triggerOnce' | 'delay' | 'children' | 'duration' | 'timingFunction' >() }) }) diff --git a/packages/react-dom/src/FadeIn.tsx b/packages/react-dom/src/FadeIn.tsx index a7d8b3d9c..8a2ddb6dd 100644 --- a/packages/react-dom/src/FadeIn.tsx +++ b/packages/react-dom/src/FadeIn.tsx @@ -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 = Override< - Override< - ComponentPropsWithoutRef, - OmitKeyof - >, - { - /** - * The element type to render. - * @default 'div' - */ - as?: TAs - /** - * The style of the element. - */ - style?: OmitKeyof - /** - * 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) => ReactNode +} /** * A component that fades in when it comes into view. */ -export function FadeIn({ - as, - style, - // fadeIn Options +export function FadeIn({ duration = 200, timingFunction = 'linear', - // inView options delay, + root, rootMargin, threshold, triggerOnce, - ...restProps -}: FadeInProps) { - const Component = as ?? 'div' - return ( - - {({ inView, ref }) => ( - - )} - - ) + children, +}: FadeInProps) { + const result = useFadeIn({ delay, duration, root, rootMargin, threshold, timingFunction, triggerOnce }) + return <>{children(result)} } diff --git a/packages/react-dom/src/index.ts b/packages/react-dom/src/index.ts index 0e0c494a3..e6b6f0a6f 100644 --- a/packages/react-dom/src/index.ts +++ b/packages/react-dom/src/index.ts @@ -1,3 +1,4 @@ export { InView } from './InView' export { useInView } from './useInView' export { FadeIn } from './FadeIn' +export { useFadeIn } from './useFadeIn' diff --git a/packages/react-dom/src/useFadeIn.ts b/packages/react-dom/src/useFadeIn.ts new file mode 100644 index 000000000..f817e4f7c --- /dev/null +++ b/packages/react-dom/src/useFadeIn.ts @@ -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, 'ref'> & { + style: { + opacity: 0 | 1 + willChange: 'opacity' + transition: `opacity ${number}ms ${Required['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( + () => ({ + ref, + style: { + opacity: inView ? 1 : 0, + willChange: 'opacity', + transition: `opacity ${duration}ms ${timingFunction}` as const, + }, + }), + [inView, duration, timingFunction] + ) +} diff --git a/packages/react-dom/src/utility-types/Override.ts b/packages/react-dom/src/utility-types/Override.ts deleted file mode 100644 index edfa44ee6..000000000 --- a/packages/react-dom/src/utility-types/Override.ts +++ /dev/null @@ -1 +0,0 @@ -export type Override = Omit & TObject diff --git a/packages/react-dom/src/utility-types/index.ts b/packages/react-dom/src/utility-types/index.ts index 6765a418a..e439d3d4c 100644 --- a/packages/react-dom/src/utility-types/index.ts +++ b/packages/react-dom/src/utility-types/index.ts @@ -1,2 +1 @@ export type { OmitKeyof } from './OmitKeyof' -export type { Override } from './Override'