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 2f596df
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: packages/react-components/react-headless-components-preview/library/docs/popover-spec.md
+13-10Lines changed: 13 additions & 10 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -150,18 +150,19 @@ The role on the surface depends on `trapFocus`:
150
150
151
151
### Focus management
152
152
153
-
Focus management is delegated to the platform; the component adds no React-side focus trap, focus finder, or restoration logic.
153
+
The surface's top-layer lifecycle and focus handling come from the shared `useTopLayer` hook.
154
154
155
155
-**`trapFocus={false}` (default).** No autofocus on open unless a child element has the HTML `autofocus` attribute (in which case the popover-side focusing steps focus it per the popover spec). No focus trap — Tab follows the document's normal tab order. Light dismiss is browser-managed.
156
-
-**`trapFocus={true}`.** The browser opens the surface via `dialog.showModal()` and runs the modal `<dialog>` focusing steps: focus moves to the first sequentially-focusable descendant (or to a child carrying `autofocus`), the rest of the page is **inert**, and Tab cycles inside the surface. On close, `dialog.close()` restores focus to whatever element was focused when `showModal()` ran — typically the trigger.
156
+
-**`trapFocus={true}`.** The browser opens the surface via `dialog.showModal()` and runs the modal `<dialog>` focusing steps: focus moves to the first sequentially-focusable descendant (or to a child carrying `autofocus`) and the rest of the page is **inert**. Tab and Shift+Tab wrap inside the surface instead of leaving through the browser chrome.
157
+
-**Restore.** The browser restores focus on close. When focus was inside the surface as it started closing and is now lost to `<body>` or still inside it, focus moves to the trigger. Focus the user moved elsewhere is left alone.
157
158
158
159
#### Specifying initial focus
159
160
160
161
Consumers control which child receives initial focus via the standard React `autoFocus` attribute on the desired child JSX element. There is no `initialFocusRef` prop — the HTML attribute already covers it for both show modes.
161
162
162
163
#### Cancel handling (modal Esc)
163
164
164
-
When `trapFocus={true}`, Escape fires the native `cancel` event on the dialog. `usePopover` listens for `cancel`, calls `event.preventDefault()` (so the browser doesn't close the dialog before React re-renders), and routes through `setOpen(false)`. The subsequent close — driven by `open` flipping to `false` — runs `dialog.close()`, which performs the spec-mandated focus restoration to the trigger. This keeps controlled consumers authoritative without losing the platform's restore behavior.
165
+
When `trapFocus={true}`, Escape fires the native `cancel` event and the browser closes the dialog, restoring focus to the trigger. The surface reports the native `close` event, so `onOpenChange` fires with `type: 'close'` and `open: false`.
165
166
166
167
### Labeling
167
168
@@ -232,13 +233,15 @@ This implementation defers opening until `pointerup`, which avoids opening durin
| Escape | Browser dismisses the topmost open popover per the HTML Popover light-dismiss algorithm. |
238
-
| Click outside | Browser dismisses any popover whose ancestor chain doesn't contain the click target. |
239
-
| Open an unrelated `popover="auto"` peer | Browser dismisses the existing root chain — only one auto-popover root can be open at a time. |
240
-
| Hover-leave (`openOnHover`) | React-side: `setOpen(false)` after `mouseLeaveDelay` ms with the pointer outside both trigger and surface. `setOpen(false)` unmounts the surface in JSX. |
241
-
| Programmatic | Consumer sets `open={false}`. React unmounts the surface; the browser implicitly closes the popover when the element leaves the DOM. |
| Escape | Browser dismisses the topmost open popover per the HTML Popover light-dismiss algorithm. |
239
+
| Click outside | Browser dismisses any popover whose ancestor chain doesn't contain the click target. |
240
+
| Open an unrelated `popover="auto"` peer | Browser dismisses the existing root chain — only one auto-popover root can be open at a time. |
241
+
| Hover-leave (`openOnHover`) | React-side: `setOpen(false)` after `mouseLeaveDelay` ms with the pointer outside both trigger and surface. The surface is hidden, then unmounted after its exit animations. |
242
+
| Programmatic | Consumer sets `open={false}`. The surface is hidden with `hidePopover()` / `close()`, then unmounted after its exit animations. |
243
+
244
+
While the surface exits it keeps `data-phase="exiting"`, loses `data-open`, and is `inert` and `aria-hidden`. Exit styles must keep it displayed and in the top layer (`transition: display … allow-discrete, overlay … allow-discrete`, or `display` keyframes).
242
245
243
246
`usePopover` attaches a `toggle` listener on the surface (re-attached every time the surface mounts) that mirrors browser-driven dismissal:
Copy file name to clipboardExpand all lines: packages/react-components/react-headless-components-preview/library/src/components/Dialog/DialogSurface/DialogSurface.types.ts
Copy file name to clipboardExpand all lines: packages/react-components/react-headless-components-preview/library/src/components/Dialog/DialogSurface/useDialogSurface.ts
Copy file name to clipboardExpand all lines: packages/react-components/react-headless-components-preview/library/src/components/Dropdown/useListboxSlot.ts
0 commit comments