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

Implement sibling <Dialog /> components #3242

Merged
merged 30 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ccc5b3f
add `DefaultMap` implementation
RobinMalfait May 25, 2024
73de62b
add `useHierarchy` hook
RobinMalfait May 25, 2024
77dc39f
start `FocusTrapFeatures.None` with `0` instead of `1`
RobinMalfait May 25, 2024
78773f1
simplify `Dialog`'s implementation
RobinMalfait May 25, 2024
cb75ac7
delete `StackContext` and `StackProvider` components
RobinMalfait May 25, 2024
c92bd14
use `useHierarchy` in `useOutsideClick` hook
RobinMalfait May 25, 2024
93e65e7
use `useHierarchy` in `useInertOthers` hook
RobinMalfait May 25, 2024
64d1b25
add new `useEscape` hook
RobinMalfait May 25, 2024
82a0ee8
use new `useEscape` hook
RobinMalfait May 25, 2024
268dbf6
use `useHierarchy` in `useEscape` hook
RobinMalfait May 25, 2024
b187790
use `useHierarchy` in `useScrollLock` hook
RobinMalfait May 25, 2024
4c8d6dd
pass features instead of `enabled` boolean
RobinMalfait May 26, 2024
650f02f
simplify demo mode feature flags
RobinMalfait May 26, 2024
73bf733
use similar signature for hooks with `enabled` parameter
RobinMalfait May 26, 2024
ee5daf5
move `focusTrapFeatures` parameter to the front
RobinMalfait May 26, 2024
f650c95
drop `FocusTrapFeatures.All`, list them explicitly
RobinMalfait May 26, 2024
26d45db
always enable `FocusTrapFeatures.RestoreFocus` when enabled
RobinMalfait May 26, 2024
51d836e
use `useHierarchy` in `<FocusTrap>` component
RobinMalfait May 26, 2024
076b6df
drop `useHierarchy` from `<Dialog>` component
RobinMalfait May 26, 2024
344f8bd
simplify focusTrapFeatures setup
RobinMalfait May 26, 2024
a752f8d
simplify `useHierarchy`
RobinMalfait May 26, 2024
e10d68e
move `enabled`-like argument to front
RobinMalfait May 26, 2024
f86efe0
polyfill `toSpliced` for older Node versions
RobinMalfait May 26, 2024
0ed8675
add sibling dialogs playground
RobinMalfait May 26, 2024
bf72f1d
rename `useHierarchy` to `useIsTopLayer`
RobinMalfait May 27, 2024
7a505eb
inline variable
RobinMalfait May 27, 2024
600da20
remove `unstable_batchedUpdates`
RobinMalfait May 27, 2024
38cbf2d
add tiny bit of information to dialog
RobinMalfait May 27, 2024
4810d39
update changelog
RobinMalfait May 27, 2024
a911e1f
re-add internal `PortalGroup`
RobinMalfait May 27, 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
147 changes: 44 additions & 103 deletions packages/@headlessui-react/src/components/dialog/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,15 @@ import React, {
useMemo,
useReducer,
useRef,
useState,
type ContextType,
type ElementType,
type MutableRefObject,
type MouseEvent as ReactMouseEvent,
type Ref,
type RefObject,
} from 'react'
import { useEscape } from '../../hooks/use-escape'
import { useEvent } from '../../hooks/use-event'
import { useEventListener } from '../../hooks/use-event-listener'
import { useId } from '../../hooks/use-id'
import { useInertOthers } from '../../hooks/use-inert-others'
import { useIsTouchDevice } from '../../hooks/use-is-touch-device'
Expand All @@ -32,8 +31,6 @@ import { useSyncRefs } from '../../hooks/use-sync-refs'
import { CloseProvider } from '../../internal/close-provider'
import { HoistFormFields } from '../../internal/form-fields'
import { State, useOpenClosed } from '../../internal/open-closed'
import { ForcePortalRoot } from '../../internal/portal-force-root'
import { StackMessage, StackProvider } from '../../internal/stack-context'
import type { Props } from '../../types'
import { match } from '../../utils/match'
import {
Expand All @@ -50,7 +47,6 @@ import {
type _internal_ComponentDescription,
} from '../description/description'
import { FocusTrap, FocusTrapFeatures } from '../focus-trap/focus-trap'
import { Keys } from '../keyboard'
import { Portal, useNestedPortals } from '../portal/portal'

enum DialogStates {
Expand Down Expand Up @@ -147,7 +143,6 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
__demoMode = false,
...theirProps
} = props
let [nestedDialogCount, setNestedDialogCount] = useState(0)

let didWarnOnRole = useRef(false)

Expand Down Expand Up @@ -224,8 +219,6 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(

let ready = useServerHandoffComplete()
let enabled = ready ? dialogState === DialogStates.Open : false
let hasNestedDialogs = nestedDialogCount > 1 // 1 is the current dialog
let hasParentDialog = useContext(DialogContext) !== null
let [portals, PortalWrapper] = useNestedPortals()

// We use this because reading these values during initial render(s)
Expand All @@ -247,10 +240,6 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
defaultContainers: [defaultContainer],
})

// If there are multiple dialogs, then you can be the root, the leaf or one
// in between. We only care about whether you are the top most one or not.
let position = !hasNestedDialogs ? 'leaf' : 'parent'

// When the `Dialog` is wrapped in a `Transition` (or another Headless UI component that exposes
// the OpenClosed state) then we get some information via context about its state. When the
// `Transition` is about to close, then the `State.Closing` state will be exposed. This allows us
Expand All @@ -260,13 +249,7 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
usesOpenClosedState !== null ? (usesOpenClosedState & State.Closing) === State.Closing : false

// Ensure other elements can't be interacted with
let inertOthersEnabled = (() => {
if (__demoMode) return false
// Only the top-most dialog should be allowed, all others should be inert
if (hasNestedDialogs) return false
if (isClosing) return false
return enabled
})()
let inertOthersEnabled = __demoMode ? false : isClosing ? false : enabled
useInertOthers(inertOthersEnabled, {
allowed: useEvent(() => [
// Allow the headlessui-portal of the Dialog to be interactive. This
Expand All @@ -281,26 +264,14 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
})

// Close Dialog on outside click
let outsideClickEnabled = (() => {
if (!enabled) return false
if (hasNestedDialogs) return false
return true
})()
let outsideClickEnabled = enabled
useOutsideClick(outsideClickEnabled, resolveRootContainers, (event) => {
event.preventDefault()
close()
})

// Handle `Escape` to close
let escapeToCloseEnabled = (() => {
if (hasNestedDialogs) return false
if (dialogState !== DialogStates.Open) return false
return true
})()
useEventListener(ownerDocument?.defaultView, 'keydown', (event) => {
if (!escapeToCloseEnabled) return
if (event.defaultPrevented) return
if (event.key !== Keys.Escape) return
useEscape(enabled, ownerDocument?.defaultView, (event) => {
event.preventDefault()
event.stopPropagation()

Expand All @@ -322,12 +293,7 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
})

// Scroll lock
let scrollLockEnabled = (() => {
if (isClosing) return false
if (dialogState !== DialogStates.Open) return false
if (hasParentDialog) return false
return true
})()
let scrollLockEnabled = __demoMode ? false : isClosing ? false : enabled
useScrollLock(scrollLockEnabled, ownerDocument, resolveRootContainers)

// Ensure we close the dialog as soon as the dialog itself becomes hidden
Expand Down Expand Up @@ -355,79 +321,54 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
'aria-describedby': describedby,
}

let shouldAutoFocus = !useIsTouchDevice()
let shouldMoveFocusInside = !useIsTouchDevice()
let focusTrapFeatures = FocusTrapFeatures.None

let focusTrapFeatures = enabled
? match(position, {
parent: FocusTrapFeatures.RestoreFocus,
leaf: FocusTrapFeatures.All & ~FocusTrapFeatures.FocusLock,
})
: FocusTrapFeatures.None
if (enabled && !__demoMode) {
focusTrapFeatures |= FocusTrapFeatures.RestoreFocus
focusTrapFeatures |= FocusTrapFeatures.TabLock

// Enable AutoFocus feature
if (autoFocus) {
focusTrapFeatures |= FocusTrapFeatures.AutoFocus
}

// Remove initialFocus when we should not auto focus at all
if (!shouldAutoFocus) {
focusTrapFeatures &= ~FocusTrapFeatures.InitialFocus
}
if (autoFocus) {
focusTrapFeatures |= FocusTrapFeatures.AutoFocus
}

if (__demoMode) {
focusTrapFeatures = FocusTrapFeatures.None
if (shouldMoveFocusInside) {
focusTrapFeatures |= FocusTrapFeatures.InitialFocus
}
}

return (
<StackProvider
type="Dialog"
enabled={dialogState === DialogStates.Open}
element={internalDialogRef}
onUpdate={useEvent((message, type) => {
if (type !== 'Dialog') return

match(message, {
[StackMessage.Add]: () => setNestedDialogCount((count) => count + 1),
[StackMessage.Remove]: () => setNestedDialogCount((count) => count - 1),
})
})}
>
<ForcePortalRoot force={true}>
<Portal>
<DialogContext.Provider value={contextBag}>
<Portal.Group target={internalDialogRef}>
<ForcePortalRoot force={false}>
<DescriptionProvider slot={slot} name="Dialog.Description">
<PortalWrapper>
<FocusTrap
initialFocus={initialFocus}
initialFocusFallback={__demoMode ? undefined : internalDialogRef}
containers={resolveRootContainers}
features={focusTrapFeatures}
>
<CloseProvider value={close}>
{render({
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_DIALOG_TAG,
features: DialogRenderFeatures,
visible: dialogState === DialogStates.Open,
name: 'Dialog',
})}
</CloseProvider>
</FocusTrap>
</PortalWrapper>
</DescriptionProvider>
</ForcePortalRoot>
</Portal.Group>
</DialogContext.Provider>
</Portal>
</ForcePortalRoot>
<>
<Portal>
<DialogContext.Provider value={contextBag}>
<DescriptionProvider slot={slot}>
<PortalWrapper>
<FocusTrap
initialFocus={initialFocus}
initialFocusFallback={internalDialogRef}
containers={resolveRootContainers}
features={focusTrapFeatures}
>
<CloseProvider value={close}>
{render({
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_DIALOG_TAG,
features: DialogRenderFeatures,
visible: dialogState === DialogStates.Open,
name: 'Dialog',
})}
</CloseProvider>
</FocusTrap>
</PortalWrapper>
</DescriptionProvider>
</DialogContext.Provider>
</Portal>
<HoistFormFields>
<MainTreeNode />
</HoistFormFields>
</StackProvider>
</>
)
}

Expand Down
Loading
Loading