Skip to content

Commit

Permalink
use render features in Transition (React)
Browse files Browse the repository at this point in the history
  • Loading branch information
RobinMalfait committed Oct 17, 2020
1 parent 8b49009 commit 2779867
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ describe('Setup API', () => {

it('should be possible to use a render prop', () => {
const { container } = render(
<Transition show={true}>{ref => <span ref={ref}>Children</span>}</Transition>
<Transition show={true} as={React.Fragment}>
{() => <span>Children</span>}
</Transition>
)

expect(container.firstChild).toMatchInlineSnapshot(`
Expand All @@ -131,12 +133,20 @@ describe('Setup API', () => {
})

it(
'should yell at us when we forget to apply the ref when using a render prop',
'should yell at us when we forget to forward the ref when using a render prop',
suppressConsoleLogs(() => {
expect.assertions(1)

function Dummy(props: any) {
return <span {...props}>Children</span>
}

expect(() => {
render(<Transition show={true}>{() => <span>Children</span>}</Transition>)
render(
<Transition show={true} as={React.Fragment}>
{() => <Dummy />}
</Transition>
)
}).toThrowErrorMatchingInlineSnapshot(
`"Did you forget to passthrough the \`ref\` to the actual DOM node?"`
)
Expand Down Expand Up @@ -253,8 +263,10 @@ describe('Setup API', () => {
const { container } = render(
<div className="My Page">
<Transition show={true}>
<Transition.Child>{ref => <aside ref={ref}>Sidebar</aside>}</Transition.Child>
<Transition.Child>{ref => <section ref={ref}>Content</section>}</Transition.Child>
<Transition.Child as={React.Fragment}>{() => <aside>Sidebar</aside>}</Transition.Child>
<Transition.Child as={React.Fragment}>
{() => <section>Content</section>}
</Transition.Child>
</Transition>
</div>
)
Expand All @@ -278,11 +290,15 @@ describe('Setup API', () => {
it('should be possible to use render props on the Transition and Transition.Child components', () => {
const { container } = render(
<div className="My Page">
<Transition show={true}>
{ref => (
<article ref={ref}>
<Transition.Child>{ref => <aside ref={ref}>Sidebar</aside>}</Transition.Child>
<Transition.Child>{ref => <section ref={ref}>Content</section>}</Transition.Child>
<Transition show={true} as={React.Fragment}>
{() => (
<article>
<Transition.Child as={React.Fragment}>
{() => <aside>Sidebar</aside>}
</Transition.Child>
<Transition.Child as={React.Fragment}>
{() => <section>Content</section>}
</Transition.Child>
</article>
)}
</Transition>
Expand All @@ -306,16 +322,24 @@ describe('Setup API', () => {
})

it(
'should yell at us when we forgot to apply the ref on one of the Transition.Child components',
'should yell at us when we forgot to forward the ref on one of the Transition.Child components',
suppressConsoleLogs(() => {
expect.assertions(1)

function Dummy(props: any) {
return <div {...props} />
}

expect(() => {
render(
<div className="My Page">
<Transition show={true}>
<Transition.Child>{ref => <aside ref={ref}>Sidebar</aside>}</Transition.Child>
<Transition.Child>{() => <section>Content</section>}</Transition.Child>
<Transition.Child as={React.Fragment}>
{() => <Dummy>Sidebar</Dummy>}
</Transition.Child>
<Transition.Child as={React.Fragment}>
{() => <Dummy>Content</Dummy>}
</Transition.Child>
</Transition>
</div>
)
Expand All @@ -326,21 +350,23 @@ describe('Setup API', () => {
)

it(
'should yell at us when we forgot to apply a ref on the Transition component',
'should yell at us when we forgot to forward a ref on the Transition component',
suppressConsoleLogs(() => {
expect.assertions(1)

function Dummy(props: any) {
return <div {...props} />
}

expect(() => {
render(
<div className="My Page">
<Transition show={true}>
<Transition show={true} as={React.Fragment}>
{() => (
<article>
<Transition.Child>{ref => <aside ref={ref}>Sidebar</aside>}</Transition.Child>
<Transition.Child>
{ref => <section ref={ref}>Content</section>}
</Transition.Child>
</article>
<Dummy>
<Transition.Child>{() => <aside>Sidebar</aside>}</Transition.Child>
<Transition.Child>{() => <section>Content</section>}</Transition.Child>
</Dummy>
)}
</Transition>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import * as React from 'react'
import { Props } from 'types'

import { useId } from '../../hooks/use-id'
import { useIsInitialRender } from '../../hooks/use-is-initial-render'
import { useIsMounted } from '../../hooks/use-is-mounted'
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'

import { match } from '../../utils/match'
import { Features, PropsForFeatures, render } from '../../utils/render'
import { Reason, transition } from './utils/transition'

type ID = ReturnType<typeof useId>
Expand Down Expand Up @@ -43,24 +44,9 @@ export type TransitionEvents = Partial<{
afterLeave(): void
}>

type HTMLTags = keyof JSX.IntrinsicElements
type HTMLTagProps<TTag extends HTMLTags> = JSX.IntrinsicElements[TTag]

type AsShortcut<TTag extends HTMLTags> = {
children?: React.ReactNode
as?: TTag
} & Omit<HTMLTagProps<TTag>, 'ref'>

type AsRenderPropFunction = {
children: (ref: React.MutableRefObject<any>) => JSX.Element
}

type BaseConfig = Partial<{ appear: boolean }>

type TransitionChildProps<TTag extends HTMLTags> = BaseConfig &
(AsShortcut<TTag> | AsRenderPropFunction) &
TransitionClasses &
TransitionEvents
type TransitionChildProps<TTag> = Props<TTag, TransitionChildRenderPropArg> &
PropsForFeatures<typeof TransitionChildRenderFeatures> &
Partial<{ appear: boolean } & TransitionClasses & TransitionEvents>

function useTransitionContext() {
const context = React.useContext(TransitionContext)
Expand Down Expand Up @@ -156,7 +142,15 @@ function useEvents(events: TransitionEvents) {
return eventsRef
}

function TransitionChild<TTag extends HTMLTags = 'div'>(props: TransitionChildProps<TTag>) {
// ---

const DEFAULT_TRANSITION_CHILD_TAG = 'div'
type TransitionChildRenderPropArg = React.MutableRefObject<HTMLDivElement>
const TransitionChildRenderFeatures = Features.RenderStrategy

function TransitionChild<TTag extends React.ElementType = typeof DEFAULT_TRANSITION_CHILD_TAG>(
props: TransitionChildProps<TTag>
) {
const {
// Event "handlers"
beforeEnter,
Expand All @@ -171,9 +165,6 @@ function TransitionChild<TTag extends HTMLTags = 'div'>(props: TransitionChildPr
leave,
leaveFrom,
leaveTo,

// ..
children,
...rest
} = props
const container = React.useRef<HTMLElement | null>(null)
Expand Down Expand Up @@ -266,32 +257,27 @@ function TransitionChild<TTag extends HTMLTags = 'div'>(props: TransitionChildPr
leaveToClasses,
])

// Unmount the whole tree
if (state === TreeStates.Hidden) return null
const propsBag = {}
const propsWeControl = { ref: container }
const passthroughProps = rest

if (typeof children === 'function') {
return (
<NestingContext.Provider value={nesting}>
{(children as AsRenderPropFunction['children'])(container)}
</NestingContext.Provider>
)
}

const { as: Component = 'div', ...passthroughProps } = rest as AsShortcut<TTag>
return (
<NestingContext.Provider value={nesting}>
{/* @ts-expect-error Expression produces a union type that is too complex to represent. */}
<Component {...passthroughProps} ref={container}>
{children}
</Component>
{render(
{ ...passthroughProps, ...propsWeControl },
propsBag,
DEFAULT_TRANSITION_CHILD_TAG,
TransitionChildRenderFeatures,
state === TreeStates.Visible
)}
</NestingContext.Provider>
)
}

export function Transition<TTag extends HTMLTags = 'div'>(
export function Transition<TTag extends React.ElementType = typeof DEFAULT_TRANSITION_CHILD_TAG>(
props: TransitionChildProps<TTag> & { show: boolean; appear?: boolean }
) {
const { show, appear = false, ...rest } = props
const { show, appear = false, unmount, ...passthroughProps } = props

if (![true, false].includes(show)) {
throw new Error('A <Transition /> is used but it is missing a `show={true | false}` prop.')
Expand All @@ -317,13 +303,22 @@ export function Transition<TTag extends HTMLTags = 'div'>(
}
}, [show, nestingBag])

const sharedProps = { unmount }

return (
<NestingContext.Provider value={nestingBag}>
<TransitionContext.Provider value={transitionBag}>
{match(state, {
[TreeStates.Visible]: () => <TransitionChild {...rest} />,
[TreeStates.Hidden]: null,
})}
{render(
{
...sharedProps,
as: React.Fragment,
children: <TransitionChild unmount={unmount} {...passthroughProps} />,
},
null,
React.Fragment,
TransitionChildRenderFeatures,
state === TreeStates.Visible
)}
</TransitionContext.Provider>
</NestingContext.Provider>
)
Expand Down

0 comments on commit 2779867

Please sign in to comment.