Skip to content

Commit

Permalink
Ensure appear works using the Transition component (even when use…
Browse files Browse the repository at this point in the history
…d with SSR) (#2646)

* ensure `appear` works in combination with SSR

* add appear transition example

* update changelog

* add scale to appear example

* trigger immediate transition once the DOM is ready

* ensure React doesn't change the `className` underneath us

* handle all base classes

We are bypassing React when handling classes in the Transition
component. Let's ensure the base classes from the prop are also added
correctly.

* add missing `base` to tests

* simplify `useTransition` hook

* add react-hot-toast example

* make TS happy

* ensure the `classNames` are unique

* remove classNames if it results in an empty string

This will ensure that we don't end up with `class=""` in the DOM

* ensure `unmount` is defaulting to `true`

* do not read from `prevShow` in render

After fixing the other bugs, this part only caused bugs right now. Even
when re-rendering the Transition component while transitioning. Dropping
this fixes that behaviour.

* extend `appear` demo with appear, show, unmount booleans

+ a `lazily` one to mimic a conditional render on the client instead of
  a fresh page refresh.
  • Loading branch information
RobinMalfait committed Aug 7, 2023
1 parent 88a0138 commit 842890d
Show file tree
Hide file tree
Showing 10 changed files with 395 additions and 22 deletions.
1 change: 1 addition & 0 deletions packages/@headlessui-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Disable smooth scrolling when opening/closing `Dialog` components on iOS ([#2635](https://github.com/tailwindlabs/headlessui/pull/2635))
- Don't assume `<Tab />` components are available when setting the next index ([#2642](https://github.com/tailwindlabs/headlessui/pull/2642))
- Fix incorrectly focused `Combobox.Input` component on page load ([#2654](https://github.com/tailwindlabs/headlessui/pull/2654))
- Ensure `appear` works using the `Transition` component (even when used with SSR) ([#2646](https://github.com/tailwindlabs/headlessui/pull/2646))

## [1.7.16] - 2023-07-27

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,15 +302,14 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
} = props as typeof props
let container = useRef<HTMLElement | null>(null)
let transitionRef = useSyncRefs(container, ref)
let strategy = rest.unmount ? RenderStrategy.Unmount : RenderStrategy.Hidden
let strategy = rest.unmount ?? true ? RenderStrategy.Unmount : RenderStrategy.Hidden

let { show, appear, initial } = useTransitionContext()

let [state, setState] = useState(show ? TreeStates.Visible : TreeStates.Hidden)

let parentNesting = useParentNesting()
let { register, unregister } = parentNesting
let prevShow = useRef<boolean | null>(null)

useEffect(() => register(container), [register, container])

Expand All @@ -332,6 +331,7 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
}, [state, container, register, unregister, show, strategy])

let classes = useLatestValue({
base: splitClasses(rest.className),
enter: splitClasses(enter),
enterFrom: splitClasses(enterFrom),
enterTo: splitClasses(enterTo),
Expand All @@ -358,11 +358,11 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_

// Skipping initial transition
let skip = initial && !appear
let immediate = appear && show && initial

let transitionDirection = (() => {
if (!ready) return 'idle'
if (skip) return 'idle'
if (prevShow.current === show) return 'idle'
return show ? 'enter' : 'leave'
})() as TransitionDirection

Expand Down Expand Up @@ -404,6 +404,7 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
}, parentNesting)

useTransition({
immediate,
container,
classes,
direction: transitionDirection,
Expand All @@ -422,25 +423,23 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
}),
})

useEffect(() => {
if (!skip) return

if (strategy === RenderStrategy.Hidden) {
prevShow.current = null
} else {
prevShow.current = show
}
}, [show, skip, state])

let theirProps = rest
let ourProps = { ref: transitionRef }

if (appear && show && initial) {
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),
}
} else {
// When we re-render while we are in the middle of the transition, then we should take the
// incoming className and the current classes that are applied.
//
// This is a bit dirty, but we need to make sure React is not applying changes to the class
// attribute while we are transitioning.
theirProps.className = classNames(rest.className, container.current?.className)
if (theirProps.className === '') delete theirProps.className
}

return (
Expand Down Expand Up @@ -476,7 +475,7 @@ function TransitionRootFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_C
ref: Ref<HTMLElement>
) {
// @ts-expect-error
let { show, appear = false, unmount, ...theirProps } = props as typeof props
let { show, appear = false, unmount = true, ...theirProps } = props as typeof props
let internalTransitionRef = useRef<HTMLElement | null>(null)
let transitionRef = useSyncRefs(internalTransitionRef, ref)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ it('should be possible to transition', async () => {
transition(
element,
{
base: [],
enter: ['enter'],
enterFrom: ['enterFrom'],
enterTo: ['enterTo'],
Expand Down Expand Up @@ -87,6 +88,7 @@ it('should wait the correct amount of time to finish a transition', async () =>
transition(
element,
{
base: [],
enter: ['enter'],
enterFrom: ['enterFrom'],
enterTo: ['enterTo'],
Expand Down Expand Up @@ -156,6 +158,7 @@ it('should keep the delay time into account', async () => {
transition(
element,
{
base: [],
enter: ['enter'],
enterFrom: ['enterFrom'],
enterTo: ['enterTo'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ function waitForTransition(node: HTMLElement, done: () => void) {
export function transition(
node: HTMLElement,
classes: {
base: string[]
enter: string[]
enterFrom: string[]
enterTo: string[]
Expand Down Expand Up @@ -116,6 +117,7 @@ export function transition(

removeClasses(
node,
...classes.base,
...classes.enter,
...classes.enterTo,
...classes.enterFrom,
Expand All @@ -124,15 +126,15 @@ export function transition(
...classes.leaveTo,
...classes.entered
)
addClasses(node, ...base, ...from)
addClasses(node, ...classes.base, ...base, ...from)

d.nextFrame(() => {
removeClasses(node, ...from)
addClasses(node, ...to)
removeClasses(node, ...classes.base, ...base, ...from)
addClasses(node, ...classes.base, ...base, ...to)

waitForTransition(node, () => {
removeClasses(node, ...base)
addClasses(node, ...classes.entered)
removeClasses(node, ...classes.base, ...base)
addClasses(node, ...classes.base, ...classes.entered)

return _done()
})
Expand Down
18 changes: 17 additions & 1 deletion packages/@headlessui-react/src/hooks/use-transition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import { useIsoMorphicEffect } from './use-iso-morphic-effect'
import { useLatestValue } from './use-latest-value'

interface TransitionArgs {
immediate: boolean
container: MutableRefObject<HTMLElement | null>
classes: MutableRefObject<{
base: string[]

enter: string[]
enterFrom: string[]
enterTo: string[]
Expand All @@ -26,12 +29,25 @@ interface TransitionArgs {
onStop: MutableRefObject<(direction: TransitionArgs['direction']) => void>
}

export function useTransition({ container, direction, classes, onStart, onStop }: TransitionArgs) {
export function useTransition({
immediate,
container,
direction,
classes,
onStart,
onStop,
}: TransitionArgs) {
let mounted = useIsMounted()
let d = useDisposables()

let latestDirection = useLatestValue(direction)

useIsoMorphicEffect(() => {
if (!immediate) return

latestDirection.current = 'enter'
}, [immediate])

useIsoMorphicEffect(() => {
let dd = disposables()
d.add(dd.dispose)
Expand Down
14 changes: 13 additions & 1 deletion packages/@headlessui-react/src/utils/class-names.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
export function classNames(...classes: (false | null | undefined | string)[]): string {
return classes.filter(Boolean).join(' ')
return Array.from(
new Set(
classes.flatMap((value) => {
if (typeof value === 'string') {
return value.split(' ')
}

return []
})
)
)
.filter(Boolean)
.join(' ')
}
1 change: 1 addition & 0 deletions packages/playground-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-flatpickr": "^3.10.9",
"react-hot-toast": "2.3.0",
"tailwindcss": "^3.2.7"
},
"devDependencies": {
Expand Down
Loading

2 comments on commit 842890d

@vercel
Copy link

@vercel vercel bot commented on 842890d Aug 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

headlessui-react – ./packages/playground-react

headlessui-react-tailwindlabs.vercel.app
headlessui-react-git-main-tailwindlabs.vercel.app
headlessui-react.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 842890d Aug 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

headlessui-vue – ./packages/playground-vue

headlessui-vue-git-main-tailwindlabs.vercel.app
headlessui-vue-tailwindlabs.vercel.app
headlessui-vue.vercel.app

Please sign in to comment.