Repository navigation
docs(spec): AST-113 Popover anchoring, dismissal veto, modality and layer focus return (draft) - #11112
docs(spec): AST-113 Popover anchoring, dismissal veto, modality and layer focus return (draft)#11112nynexman4464 wants to merge 2 commits into
Conversation
…nd focus return (draft)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
There was a problem hiding this comment.
This draft spec needs an engineering owner's decision before it can be approved. Four record fixes are needed whatever that decision is.
Owner decision needed: spec:AST-113 adds five Popover and usePopover inputs that no current record covers, and its FR12 none region reverses component:Popover DEC-9. An engineering owner records approval by commenting /approve-spec 8da8cb49394d08382d4e3984ebdd46158a3c91c8 (the author may do this as an engineering owner) or says what to change. The decision covers OQ1–OQ3 and whether a pointer press on a popover-and-anchor anchor should dismiss the popover.
Required fixes:
- FR17 says
openerfalls back to the trigger and calls that the released behavior. Popover today returns focus only to the element focused at open, with no trigger fallback, andcomponent:PopoverAR4 says "to the trigger". Either state the released rule, or list the change and the AR4 amendment in Current-state impact. - FR9 (a cancellable outside press) and FR15 (Escape ignored while focus is outside the region) cannot run on the native
popover="auto"dismissal thatcomponent:Popoverrecords. List those rows in Current-state impact, and say what such a popover does about auto-popover exclusivity and native close requests. - Focus has no defined outcome when
anchorOutOfView="hide"hides a surface that holds focus, or whenreturnFocus="anchor"has no focusable target, which FR2 and FR4 both allow. State both outcomes, for example falling back toopener. - Non-goals cite
spec:AST-114, which does not exist onmain. Drop the id until that record lands.
[Automated review]
| - **FR7 — The caller chooses hold or hide.** `anchorOutOfView` is `hold` | ||
| (default) or `hide`. `hold` is the layer runtime's released behavior | ||
| (`spec:AST-059` FR5). With `hide`, while the anchor is out of view — clipped | ||
| by the viewport or by any scrolling ancestor, or no longer in the document — | ||
| the surface is hidden and takes no pointer input. It stays open: it does not | ||
| report a dismissal, and it shows again in place when the anchor returns. |
There was a problem hiding this comment.
What happens to focus that is inside the surface when hide hides it, for example a person typing in an inline editor whose row scrolls away? FR16 and FR17 cover only a close, and the same gap applies to FR6. Please state where focus goes on hide and when the surface shows again.
…e for every layer
There was a problem hiding this comment.
This draft spec needs an engineering owner's decision before it can be approved. Six record fixes are needed whatever that decision is.
Owner decision needed: spec:AST-113 adds anchor, anchorOutOfView and onOutsidePress to Popover and usePopover. It also makes isModal: false drop Tab containment, which reverses component:Popover DEC-9, and gives Popover, Dialog and DropdownMenu one focus-return rule. No current record covers these. An engineering owner records approval by commenting /approve-spec e6927de5062bcaf9e383da8c4a05ed390c5c83e8 (the author may do this as an engineering owner), or says what to change. The decision includes rulings on OQ1 (the names) and OQ2 (the rectangle anchor).
Required fixes:
- FR22 says that dropping Tab containment for
isModal: falseis the only released behavior that changes, but FR14–FR16 and FR19–FR21 change Escape, the sheet, role-nonepopups such as PowerSearch's adding popup, and focus return, andspec:AST-017requires each one to be named and theisModal: falsechange to be classified. - Current-state impact does not list the
component:Popover"Dismissal" and "Light and Escape dismissal" rows that FR9 and FR15 change, or DEC-8, which FR16 changes, and it names a "Modality row" that Popover does not have. - Miss: Current-state impact also leaves out
component:Popover's "Trigger binding" output and its first side-effects bullet, which both say the trigger is the anchor, a claim that FR1 changes. - FR12's non-modal Tab order starts "from the anchor", but FR2 and FR4 allow an anchor that cannot take focus and FR5 allows a popover with no trigger, so the record needs to state the order from the trigger and for a trigger-less popover.
- FR7's
hide(and FR6) can hide a surface that holds focus, and no requirement says where that focus goes or whether it comes back when the anchor returns. - DEC-6 claims one focus-return rule "for every layer", but FR17–FR21 cover three components and
component:BottomSheetkeepsfinalFocusRef, so DEC-6 needs a narrower scope and the record needs to say which rule a Popover shown as a sheet follows.
| - **FR22 — Defaults are the released behavior, except FR12's `false`.** A | ||
| Popover or `usePopover` that sets none of `anchor`, `anchorOutOfView` and | ||
| `onOutsidePress` anchors, holds and dismisses as before. `isModal: false` | ||
| stops containing Tab (FR12); this is the one released behavior that changes, | ||
| and its release note says so. |
There was a problem hiding this comment.
This is not the only released behavior that changes. FR15 takes Escape away from a non-modal popover while focus is elsewhere; today native popover="auto" closes on Escape. FR16 replaces DEC-8's modal, always light-dismissing sheet. FR14 removes containment from role-none popovers that set isModal: false, including PowerSearch's adding popup. FR19–FR21 change where focus goes when it would fall to the body. spec:AST-017 FR3 requires the release note to name each changed default and the input that restores it. No input gives containment without aria-modal, so please also classify this change under FR1 or FR53.
| - `component:Popover`: Public concepts gains Anchor, Anchor out of view and | ||
| Outside press rows, and its Modality row says `isModal` sets containment; | ||
| AR3 and DEC-9 name FR12's modal default; AR4 points to FR17–FR20; Current- | ||
| state impact loses "Popover has no viewport-rectangle anchor". |
There was a problem hiding this comment.
Please also list these component:Popover claims:
- the "Dismissal" row and the "Light and Escape dismissal" input row, which record native
popover="auto"dismissal (FR9 and FR15 cannot run on it); - DEC-8, whose modal, always light-dismissing sheet FR16 changes for
isModal: false.
There is also no "Modality" row. The rows are "Popup semantics" and "Popup role, label, and modality".
| - **FR1 — The anchor is separate from the trigger.** Popover and `usePopover` | ||
| take an optional `anchor`. When it is set, the layer anchors to the anchor | ||
| and the trigger keeps only its trigger role: its toggle, `aria-haspopup`, | ||
| `aria-expanded` and `aria-controls`. The anchor receives no handler and no | ||
| ARIA attribute. When it is unset, the trigger is the anchor, which is the | ||
| released behavior. |
There was a problem hiding this comment.
Miss: component:Popover's "Trigger binding" output says "the attached trigger is its anchor", and its first side-effects bullet says the trigger carries the CSS anchor name. With anchor set, neither holds. Please add both to Current-state impact.
| - `false`: the popover contains nothing and carries no `aria-modal`. Tab and | ||
| Shift+Tab move in the browser's sequential focus order: from the anchor | ||
| (else the trigger) into the popover, and from the popover's last control | ||
| to whatever follows the anchor. |
There was a problem hiding this comment.
"From the anchor" has no start point when the anchor cannot take focus. FR2 allows that, FR4's box is never focusable, and FR1 gives the anchor no handler. Platform support ties this order to invoker-based focus order, which follows whatever element invokes the popover, normally the trigger. Please state the order from the trigger, and say where Tab reaches a trigger-less popover (FR5).
| ### DEC-6 — One focus return rule for every layer, with no off switch | ||
|
|
||
| **Reference:** `spec:AST-113/DEC-6` | ||
| **Decider:** `nynexman4464` (direction, 2026-11-10), pending owner review | ||
|
|
||
| Losing focus strands keyboard and screen reader users, so every layer returns | ||
| stranded focus through the same chain, and no caller can opt out. The chain |
There was a problem hiding this comment.
FR17–FR21 cover only Popover, Dialog and DropdownMenu. component:BottomSheet FR11 still focuses an explicit finalFocusRef "wherever focus is", which FR17 forbids. Please narrow "every layer" here and in the Intent to those three components, and say which rule a Popover presented as a sheet (FR16) follows.
Exact decision needed
Whether Popover and
usePopovergain three caller-owned inputs, whetherisModalbecomes the modality switch, and whether every layer shares one focus-return rule:anchorseparate from the trigger (an element ref, a function that returns an element, or a rectangle inside a scrolling region);anchorOutOfView: 'hold' | 'hide';onOutsidePress;isModal(boolean, defaulttrue) sets Tab containment andaria-modaltogether. A non-modal popover lets Tab follow the browser's order: anchor, then popover, then what follows the anchor. This supersedescomponent:PopoverDEC-9 forisModal: false.main, never the body.Open for the owner: OQ1 (names) and OQ2 (keep or drop the rectangle anchor).
Existing authority searched
component:Popover(current):isModalsays; DEC-5 supersedes that forisModal: false.component:DialogFR4/AR2 andcomponent:DropdownMenuFR6 own their focus return. FR17–FR20 become the shared rule they point to.architecture:layer-runtimeINV7, INV12, INV18, INV21:spec:AST-059FR5).family:overlay-dismissal: Escape ordering, scoped by FR15.component:BottomSheet: its non-modal (hasScrim={false}) sheet is what FR16 presents for a non-modal Popover on a compact touch screen.findTriggerButtonand its warning inPopover.tsx.usePopover.tsx.popover="auto"light dismiss, which cannot be cancelled.useFocusTrap.ts,Dialog.tsx's close branch and removal path, and DropdownMenu'shandleLayerHide.utils/focusReturn.tsalready holds the temporary-tab-stop helper.Decision
See the record. Every default is the released behavior except
isModal: false, which stops containing Tab. The changeset for that implementation carries a release note.Audit of existing
isModal: falsecallers in the repo:No story, template block or lab component sets it.
Rejected alternatives
anchorRefto non-buttons: it changes what a released input does to the element it names.returnFocusinput with a value that skips the return. A caller that wants a specific element focuses it in its hide notification, and FR17 leaves it there.Claim scope and non-goals
Popover and
usePopoverfor anchoring, the veto and modality; Popover, Dialog and DropdownMenu for focus return. Out of scope:useLayeroptions, ContextMenu point opening, the shared Escape/outside stack (spec:AST-003), focus entry, and the Tour component.Evidence and implementation relationship
Demand, from a survey of one large internal codebase (aggregate counts only):
The fallback chain follows established focus-recovery practice in prior design-system work and current libraries: opener first, then re-resolution, then the opener's former position among its container's tab stops.
Draft implementation PRs, stacked on this record and blocked on its approval:
#11121 (a
returnFocusinput) is superseded by DEC-6 and is rewritten as the unified focus-return PR.Writing rubric results
FRn gap:lines state today's observable mismatch. TheisModal: falsecaller list is current-state impact, not work order.Scope
Components: Popover, usePopover, Dialog, DropdownMenu, useFocusTrap (internal), Layer (internal), utils/focusReturn (internal)
Validation
pnpm check:knowledge: passes ("Knowledge templates and records are aligned")git diff --check