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

Improve dialog and server handoff #477

Merged
merged 4 commits into from
May 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Introduce Open/Closed state, to simplify component communication ([#466](https://github.com/tailwindlabs/headlessui/pull/466))

### Fixes

- Improve SSR for `Dialog` ([#477](https://github.com/tailwindlabs/headlessui/pull/477))
- Delay focus trap initialization ([#477](https://github.com/tailwindlabs/headlessui/pull/477))

## [Unreleased - Vue]

### Added
Expand Down
4 changes: 3 additions & 1 deletion packages/@headlessui-react/src/components/dialog/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { contains } from '../../internal/dom-containers'
import { Description, useDescriptions } from '../description/description'
import { useWindowEvent } from '../../hooks/use-window-event'
import { useOpenClosed, State } from '../../internal/open-closed'
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'

enum DialogStates {
Open,
Expand Down Expand Up @@ -235,7 +236,8 @@ let DialogRoot = forwardRefWithAs(function Dialog<
return () => observer.disconnect()
}, [dialogState, internalDialogRef, close])

let enabled = dialogState === DialogStates.Open
let ready = useServerHandoffComplete()
let enabled = ready && dialogState === DialogStates.Open

useFocusTrap(containers, enabled, { initialFocus })
useInertOthers(internalDialogRef, enabled)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { Props } from '../../types'
import { render } from '../../utils/render'
import { useFocusTrap } from '../../hooks/use-focus-trap'
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'

let DEFAULT_FOCUS_TRAP_TAG = 'div' as const

Expand All @@ -18,7 +19,8 @@ export function FocusTrap<TTag extends ElementType = typeof DEFAULT_FOCUS_TRAP_T
let containers = useRef<Set<HTMLElement>>(new Set())
let { initialFocus, ...passthroughProps } = props

useFocusTrap(containers, true, { initialFocus })
let ready = useServerHandoffComplete()
useFocusTrap(containers, ready, { initialFocus })

let propsWeControl = {
ref(element: HTMLElement | null) {
Expand Down
11 changes: 6 additions & 5 deletions packages/@headlessui-react/src/components/portal/portal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { render } from '../../utils/render'
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
import { useElementStack, StackProvider } from '../../internal/stack-context'
import { usePortalRoot } from '../../internal/portal-force-root'
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'

function usePortalTarget(): HTMLElement | null {
let forceInRoot = usePortalRoot()
Expand Down Expand Up @@ -57,6 +58,8 @@ export function Portal<TTag extends ElementType = typeof DEFAULT_PORTAL_TAG>(
typeof window === 'undefined' ? null : document.createElement('div')
)

let ready = useServerHandoffComplete()

useElementStack(element)

useIsoMorphicEffect(() => {
Expand All @@ -77,16 +80,14 @@ export function Portal<TTag extends ElementType = typeof DEFAULT_PORTAL_TAG>(
}
}, [target, element])

if (!ready) return null

return (
<StackProvider>
{!target || !element
? null
: createPortal(
render({
props: passthroughProps,
defaultTag: DEFAULT_PORTAL_TAG,
name: 'Portal',
}),
render({ props: passthroughProps, defaultTag: DEFAULT_PORTAL_TAG, name: 'Portal' }),
element
)}
</StackProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
import { Features, PropsForFeatures, render, RenderStrategy } from '../../utils/render'
import { Reason, transition } from './utils/transition'
import { OpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed'
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'

type ID = ReturnType<typeof useId>

Expand Down Expand Up @@ -260,11 +261,13 @@ function TransitionChild<TTag extends ElementType = typeof DEFAULT_TRANSITION_CH

let events = useEvents({ beforeEnter, afterEnter, beforeLeave, afterLeave })

let ready = useServerHandoffComplete()

useEffect(() => {
if (state === TreeStates.Visible && container.current === null) {
if (ready && state === TreeStates.Visible && container.current === null) {
throw new Error('Did you forget to passthrough the `ref` to the actual DOM node?')
}
}, [container, state])
}, [container, state, ready])

// Skipping initial transition
let skip = initial && !appear
Expand Down
4 changes: 2 additions & 2 deletions packages/@headlessui-react/src/hooks/use-focus-trap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import {
useRef,
// Types
MutableRefObject,
useEffect,
} from 'react'

import { Keys } from '../components/keyboard'
import { useIsoMorphicEffect } from './use-iso-morphic-effect'
import { focusElement, focusIn, Focus, FocusResult } from '../utils/focus-management'
import { contains } from '../internal/dom-containers'
import { useWindowEvent } from './use-window-event'
Expand All @@ -22,7 +22,7 @@ export function useFocusTrap(
let mounted = useRef(false)

// Handle initial focus
useIsoMorphicEffect(() => {
useEffect(() => {
if (!enabled) return
if (containers.current.size !== 1) return

Expand Down
11 changes: 4 additions & 7 deletions packages/@headlessui-react/src/hooks/use-id.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,25 @@
import { useState, useEffect } from 'react'
import { useState } from 'react'
import { useIsoMorphicEffect } from './use-iso-morphic-effect'
import { useServerHandoffComplete } from './use-server-handoff-complete'

// We used a "simple" approach first which worked for SSR and rehydration on the client. However we
// didn't take care of the Suspense case. To fix this we used the approach the @reach-ui/auto-id
// uses.
//
// Credits: https://github.com/reach/reach-ui/blob/develop/packages/auto-id/src/index.tsx

let state = { serverHandoffComplete: false }
let id = 0
function generateId() {
return ++id
}

export function useId() {
let [id, setId] = useState(state.serverHandoffComplete ? generateId : null)
let ready = useServerHandoffComplete()
let [id, setId] = useState(ready ? generateId : null)

useIsoMorphicEffect(() => {
if (id === null) setId(generateId())
}, [id])

useEffect(() => {
if (state.serverHandoffComplete === false) state.serverHandoffComplete = true
}, [])

return id != null ? '' + id : undefined
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useState, useEffect } from 'react'

let state = { serverHandoffComplete: false }

export function useServerHandoffComplete() {
let [serverHandoffComplete, setServerHandoffComplete] = useState(state.serverHandoffComplete)

useEffect(() => {
if (serverHandoffComplete === true) return

setServerHandoffComplete(true)
}, [serverHandoffComplete])

useEffect(() => {
if (state.serverHandoffComplete === false) state.serverHandoffComplete = true
}, [])

return serverHandoffComplete
}