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

Fix enter transitions for the Transition component #3074

Merged
merged 27 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f27367d
improve `transition` logic
RobinMalfait Mar 30, 2024
123fd32
simplify check
RobinMalfait Mar 30, 2024
3513a70
updating playgrounds to improve transitions
RobinMalfait Mar 30, 2024
d0e4787
update changelog
RobinMalfait Apr 2, 2024
025fd7d
track in-flight transitions
RobinMalfait Apr 2, 2024
5c2d990
document `disposables()` and `useDisposables()` functions
RobinMalfait Apr 3, 2024
fb59c52
ensure we alway return a cleanup function
RobinMalfait Apr 3, 2024
3204ea4
move comment
RobinMalfait Apr 3, 2024
f08b995
apply `enterTo` or `leaveTo` classes once we are done transitioning
RobinMalfait Apr 3, 2024
bf11592
cleanup & simplify logic
RobinMalfait Apr 3, 2024
5fcd736
update comment
RobinMalfait Apr 3, 2024
5b86274
fix another bug + update tests
RobinMalfait Apr 3, 2024
b28c2a5
add failing test as playground
RobinMalfait Apr 3, 2024
7b2fb84
simplify event callbacks
RobinMalfait Apr 3, 2024
ce083ca
use existing `useOnDisappear` hook
RobinMalfait Apr 3, 2024
893563d
remove repro
RobinMalfait Apr 3, 2024
b905bf8
only unmount if we are not transitioning ourselves
RobinMalfait Apr 3, 2024
8682bac
`show` is already guaranteed to be a boolean
RobinMalfait Apr 3, 2024
6aa5789
cleanup temporary test changes
RobinMalfait Apr 3, 2024
9acfe6c
Update packages/@headlessui-react/src/components/transition/utils/tra…
RobinMalfait Apr 5, 2024
5b796d9
Update packages/@headlessui-react/src/components/transition/utils/tra…
RobinMalfait Apr 5, 2024
b3cabac
Update packages/@headlessui-react/src/components/transition/utils/tra…
RobinMalfait Apr 5, 2024
53b8471
Update packages/@headlessui-react/src/components/transition/utils/tra…
RobinMalfait Apr 5, 2024
53d8afb
Update packages/@headlessui-react/src/utils/disposables.ts
RobinMalfait Apr 5, 2024
1c60486
Update packages/@headlessui-react/src/components/transition/transitio…
RobinMalfait Apr 5, 2024
ae8c51d
Update packages/@headlessui-react/src/components/transition/transitio…
RobinMalfait Apr 5, 2024
3b310f1
run prettier
RobinMalfait Apr 5, 2024
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
1 change: 1 addition & 0 deletions packages/@headlessui-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Expose missing `data-disabled` and `data-focus` attributes on the `TabsPanel`, `MenuButton`, `PopoverButton` and `DisclosureButton` components ([#3061](https://github.com/tailwindlabs/headlessui/pull/3061))
- Fix cursor position when re-focusing the `ComboboxInput` component ([#3065](https://github.com/tailwindlabs/headlessui/pull/3065))
- Keep focus inside of the `<ComboboxInput />` component ([#3073](https://github.com/tailwindlabs/headlessui/pull/3073))
- Fix enter transitions for the `Transition` component ([#3074](https://github.com/tailwindlabs/headlessui/pull/3074))

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ exports[`Setup API transition classes should be possible to passthrough the tran
exports[`Setup API transition classes should be possible to passthrough the transition classes and immediately apply the enter transitions when appear is set to true 1`] = `
<div
class="enter enter-from"
style=""
>
Children
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ describe('Setup API', () => {
<div
class="foo1
foo2 foo1 foo2 leave"
style=""
reinink marked this conversation as resolved.
Show resolved Hide resolved
>
Children
</div>
Expand Down
84 changes: 37 additions & 47 deletions packages/@headlessui-react/src/components/transition/transition.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import React, {
Fragment,
createContext,
useContext,
useEffect,
useMemo,
useRef,
useState,
Expand All @@ -18,6 +17,7 @@ import { useFlags } from '../../hooks/use-flags'
import { useIsMounted } from '../../hooks/use-is-mounted'
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
import { useLatestValue } from '../../hooks/use-latest-value'
import { useOnDisappear } from '../../hooks/use-on-disappear'
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'
import { useSyncRefs } from '../../hooks/use-sync-refs'
import { useTransition } from '../../hooks/use-transition'
Expand Down Expand Up @@ -259,26 +259,6 @@ function useNesting(done?: () => void, parent?: NestingContextValues) {
)
}

function noop() {}
let eventNames = ['beforeEnter', 'afterEnter', 'beforeLeave', 'afterLeave'] as const
function ensureEventHooksExist(events: TransitionEvents) {
let result = {} as Record<keyof typeof events, () => void>
for (let name of eventNames) {
result[name] = events[name] ?? noop
}
return result
}

function useEvents(events: TransitionEvents) {
let eventsRef = useRef(ensureEventHooksExist(events))

useEffect(() => {
eventsRef.current = ensureEventHooksExist(events)
}, [events])

return eventsRef
}

// ---

let DEFAULT_TRANSITION_CHILD_TAG = 'div' as const
Expand Down Expand Up @@ -349,7 +329,7 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
leaveTo: splitClasses(leaveTo),
})

let events = useEvents({
let events = useLatestValue({
beforeEnter,
afterEnter,
beforeLeave,
Expand All @@ -369,6 +349,7 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
let immediate = appear && show && initial

let transitionDirection = (() => {
if (immediate) return 'enter'
if (!ready) return 'idle'
if (skip) return 'idle'
return show ? 'enter' : 'leave'
Expand All @@ -380,11 +361,11 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
return match(direction, {
enter: () => {
transitionStateFlags.addFlag(State.Opening)
events.current.beforeEnter()
events.current.beforeEnter?.()
},
leave: () => {
transitionStateFlags.addFlag(State.Closing)
events.current.beforeLeave()
events.current.beforeLeave?.()
},
idle: () => {},
})
Expand All @@ -394,26 +375,29 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
return match(direction, {
enter: () => {
transitionStateFlags.removeFlag(State.Opening)
events.current.afterEnter()
events.current.afterEnter?.()
},
leave: () => {
transitionStateFlags.removeFlag(State.Closing)
events.current.afterLeave()
events.current.afterLeave?.()
},
idle: () => {},
})
})

let isTransitioning = useRef(false)

let nesting = useNesting(() => {
// When all children have been unmounted we can only hide ourselves if and only if we are not
// transitioning ourselves. Otherwise we would unmount before the transitions are finished.
// When all children have been unmounted we can only hide ourselves if and
// only if we are not transitioning ourselves. Otherwise we would unmount
// before the transitions are finished.
if (isTransitioning.current) return

setState(TreeStates.Hidden)
unregister(container)
}, parentNesting)

let isTransitioning = useRef(false)
useTransition({
immediate,
container,
classes,
direction: transitionDirection,
Expand All @@ -426,8 +410,8 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
nesting.onStop(container, direction, afterEvent)

if (direction === 'leave' && !hasChildren(nesting)) {
// When we don't have children anymore we can safely unregister from the parent and hide
// ourselves.
// When we don't have children anymore we can safely unregister from the
// parent and hide ourselves.
setState(TreeStates.Hidden)
unregister(container)
}
Expand All @@ -437,10 +421,10 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
let theirProps = rest
let ourProps = { ref: transitionRef }

// Already apply the `enter` and `enterFrom` on the server if required
if (immediate) {
theirProps = {
...theirProps,
// Already apply the `enter` and `enterFrom` on the server if required
className: classNames(rest.className, ...classes.current.enter, ...classes.current.enterFrom),
}
}
Expand All @@ -457,6 +441,21 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
if (theirProps.className === '') delete theirProps.className
}

// If we were never transitioning, or we're not transitioning anymore, then
// apply the `enterTo` and `leaveTo` classes as the final state.
else {
theirProps.className = classNames(
rest.className,
container.current?.className,
...match(transitionDirection, {
enter: [...classes.current.enterTo, ...classes.current.entered],
leave: classes.current.leaveTo,
idle: [],
})
)
if (theirProps.className === '') delete theirProps.className
}

return (
<NestingContext.Provider value={nesting}>
<OpenClosedProvider
Expand Down Expand Up @@ -504,7 +503,7 @@ function TransitionRootFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_C
show = (usesOpenClosedState & State.Open) === State.Open
}

if (![true, false].includes(show as unknown as boolean)) {
if (show === undefined) {
throw new Error('A <Transition /> is used but it is missing a `show={true | false}` prop.')
}

Expand Down Expand Up @@ -532,27 +531,18 @@ function TransitionRootFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_C
}, [changes, show])

let transitionBag = useMemo<TransitionContextValues>(
() => ({ show: show as boolean, appear, initial }),
() => ({ show, appear, initial }),
[show, appear, initial]
)

// Ensure we change the tree state to hidden once the transition becomes hidden
useOnDisappear(internalTransitionRef, () => setState(TreeStates.Hidden))

useIsoMorphicEffect(() => {
if (show) {
setState(TreeStates.Visible)
} else if (!hasChildren(nestingBag)) {
setState(TreeStates.Hidden)
} else if (
process.env.NODE_ENV !==
'test' /* TODO: Remove this once we have real tests! JSDOM doesn't "render", therefore getBoundingClientRect() will always result in `0`. */
) {
let node = internalTransitionRef.current
if (!node) return
let rect = node.getBoundingClientRect()

if (rect.x === 0 && rect.y === 0 && rect.width === 0 && rect.height === 0) {
// The node is completely hidden, let's hide it
setState(TreeStates.Hidden)
}
}
}, [show, nestingBag])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ it('should be possible to transition', async () => {
)

await new Promise<void>((resolve) => {
transition(
element,
{
transition(element, {
direction: 'enter', // Show
classes: {
base: [],
enter: ['enter'],
enterFrom: ['enterFrom'],
Expand All @@ -38,9 +38,8 @@ it('should be possible to transition', async () => {
leaveTo: [],
entered: ['entered'],
},
true, // Show
resolve
)
done: resolve,
})
})

await new Promise((resolve) => d.nextFrame(resolve))
Expand All @@ -49,13 +48,13 @@ it('should be possible to transition', async () => {
expect(snapshots[0].content).toEqual('<div></div>')

// Start of transition
expect(snapshots[1].content).toEqual('<div class="enter enterFrom"></div>')
expect(snapshots[1].content).toEqual('<div style="" class="enter enterFrom"></div>')

// NOTE: There is no `enter enterTo`, because we didn't define a duration. Therefore it is not
// necessary to put the classes on the element and immediately remove them.

// Cleanup phase
expect(snapshots[2].content).toEqual('<div class="enterTo entered"></div>')
expect(snapshots[2].content).toEqual('<div style="" class="entered enterTo enter"></div>')

d.dispose()
})
Expand Down Expand Up @@ -84,9 +83,9 @@ it('should wait the correct amount of time to finish a transition', async () =>
)

await new Promise<void>((resolve) => {
transition(
element,
{
transition(element, {
direction: 'enter', // Show
classes: {
base: [],
enter: ['enter'],
enterFrom: ['enterFrom'],
Expand All @@ -96,9 +95,8 @@ it('should wait the correct amount of time to finish a transition', async () =>
leaveTo: [],
entered: ['entered'],
},
true, // Show
resolve
)
done: resolve,
})
})

await new Promise((resolve) => d.nextFrame(resolve))
Expand Down Expand Up @@ -154,9 +152,9 @@ it('should keep the delay time into account', async () => {
)

await new Promise<void>((resolve) => {
transition(
element,
{
transition(element, {
direction: 'enter', // Show
classes: {
base: [],
enter: ['enter'],
enterFrom: ['enterFrom'],
Expand All @@ -166,9 +164,8 @@ it('should keep the delay time into account', async () => {
leaveTo: [],
entered: ['entered'],
},
true, // Show
resolve
)
done: resolve,
})
})

await new Promise((resolve) => d.nextFrame(resolve))
Expand Down
Loading
Loading