You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 345c66c
Browse filesBrowse the repository at this point in the historyBrowse files
fix(react-headless-components-preview): move overlays onto the reworked useTopLayer options
- Popover passes the hook's onKeyDown to the surface; returnFocus targets the trigger
- DialogSurface reports Escape through the hook as escapeKeyDown and other browser opens and closes as surfaceToggle; non-modal uses returnFocus: 'always'
- Menu focuses its first item with initialFocus and re-applies focusgroup in onShow
- Tooltip, Listbox and Toaster move to onOpenChange and returnFocus
- spec: add the Consumers and Changes from master sections
Copy file name to clipboardExpand all lines: packages/react-components/react-headless-components-preview/library/docs/top-layer-spec.md
+26Lines changed: 26 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -145,6 +145,15 @@ This covers what the browser doesn't restore: `manual`, `hint`, a `<dialog>` sho
145
145
- Showing a modal closes open `auto` popovers. Content that must sit above a modal, like toasts, has to be shown after it.
146
146
- Popovers opened without user activation in between share a close watcher, so one Escape closes them all. Tests need trusted input (`cypress-real-events`).
147
147
148
+
## Consumers
149
+
150
+
-**Popover / TeachingPopover / InfoLabel.**`usePopover` drives `contentRef` with `mode: trapFocus ? 'modal' : mode` and `returnFocus: triggerRef`, routes `onOpenChange` into its open state, and passes the hook's `onKeyDown` to the surface, so a `manual` surface closes on Escape. A custom surface without `contentRef` is mounted only while open.
151
+
-**Dialog / Drawer.**`DialogSurface` uses `'modal'`, or `'manual'` when non-modal, with `preventEscapeClose` because `onOpenChange` can veto. Escape through the hook's `onKeyDown` is reported as `escapeKeyDown`; Escape inside a nested layer, or while an `auto` or `hint` popover such as a Tooltip is open inside, is left to that layer, and Escape from content portaled out of the surface still closes it. Every other browser open or close (`popovertarget`, `commandfor`, `<form method="dialog">`, `dialog.close()`, a close request that never reached the surface) is reported as `surfaceToggle`. A non-modal surface uses `returnFocus: 'always'`, as before. Backdrop clicks stay in a React handler.
152
+
-**Tooltip.**`mode: 'hint'`, which acts as `manual` where unsupported, so it never closes an open `auto` popover. Stays mounted so `aria-labelledby` / `aria-describedby` resolve.
153
+
-**Menu.**`MenuPopover` stays mounted with `mode: 'auto'`, focuses the first enabled item through `initialFocus`, re-applies `focusgroup` in `onShow`, and leaves focus return to `useMenu`. Browser closes are reported as `clickOutside`.
154
+
-**Combobox / Dropdown / TagPicker listbox.**`mode: 'auto'`, rendered only while open. Focus stays on the trigger. Browser opens and closes go through `setOpen`.
155
+
-**Toaster.** Each toast position is a `manual` popover, shown while it has toasts.
156
+
148
157
## Decisions
149
158
150
159
**React owns `open`.** The hook reports every open and close the browser makes, and none made through `open`. A browser close is final: the element stays closed even if the consumer keeps `open`. Escape on a modal follows the platform: the browser closes it and `onOpenChange` reports it. `preventEscapeClose` keeps it open for consumers that can veto.
@@ -154,3 +163,20 @@ This covers what the browser doesn't restore: `manual`, `hint`, a `<dialog>` sho
154
163
**Internal only.** A public hook would let consumers build overlays without roles, keyboard handling or focus behaviour. Custom overlays compose the components instead; where a component lacks a mode (e.g. `manual` or `hint` on Popover), the component gains it. The hook can become public later if a case comes up that composition cannot cover. Option shapes can change freely until then.
155
164
156
165
**Focus follows the platform.** The hook doesn't move focus on open unless asked, doesn't wrap Tab, and returns focus only where the browser doesn't, including after light dismiss, so keyboard and screen reader users don't lose their place.
166
+
167
+
## Changes from master
168
+
169
+
Visible differences in the components moved onto `useTopLayer`. Everything else is unchanged.
| Popover exit | unmounted once the browser reported the close | also waits for exit animations, like `DialogSurface`|
174
+
| Exiting Popover and Dialog | — |`inert` and `aria-hidden="true"` while exiting |
175
+
| Escape in a layer nested in a `DialogSurface`, or with a Tooltip open inside | the outer dialog closed too, unless the inner handler stopped the event | only the inner one closes |
176
+
|`popover="hint"` on Tooltip |`hint`; browsers without it treat it as `manual`| unchanged |
177
+
| Custom Popover surface | mounted only while open | unchanged |
178
+
| Popover kept `open` after a browser dismiss | shown again | stays closed, with a dev warning; use `mode="manual"` to prevent dismissal |
179
+
| Modal Dialog native close (`<form method="dialog">`) | closes, not reported, so React state goes out of sync | closes, reported as `surfaceToggle`|
180
+
| Modal Dialog close request without a keydown on it (focus on `<body>`) | ignored | reported as `surfaceToggle`|
Copy file name to clipboardExpand all lines: packages/react-components/react-headless-components-preview/library/src/components/Dialog/DialogSurface/useDialogSurface.ts
+20-38Lines changed: 20 additions & 38 deletions
Original file line number
Diff line number
Diff line change
@@ -13,7 +13,6 @@ import { useDialogContext } from '../dialogContext';
Copy file name to clipboardExpand all lines: packages/react-components/react-headless-components-preview/library/src/components/Dropdown/useListboxSlot.ts
+2-3Lines changed: 2 additions & 3 deletions
Original file line number
Diff line number
Diff line change
@@ -106,9 +106,8 @@ export function useListboxSlot(
Copy file name to clipboardExpand all lines: packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuPopover/useMenuPopover.ts
Copy file name to clipboardExpand all lines: packages/react-components/react-headless-components-preview/library/src/components/Popover/Popover.test.tsx
+4-1Lines changed: 4 additions & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -311,7 +311,10 @@ describe('Popover', () => {
311
311
constonRender=jest.fn();
312
312
constMeasuredPopover=()=>{
313
313
conststate=usePopover({children: <div/>});
314
-
onRender();
314
+
// Count commits: `useTopLayer` adjusts state during render, which re-runs the body but doesn't commit twice.
Copy file name to clipboardExpand all lines: packages/react-components/react-headless-components-preview/library/src/components/Popover/Popover.types.ts
Copy file name to clipboardExpand all lines: packages/react-components/react-headless-components-preview/library/src/components/Popover/PopoverSurface/usePopoverSurface.ts
0 commit comments