Skip to content

docs(spec): AST-113 Popover anchoring, dismissal veto, modality and layer focus return (draft) - #11112

Draft
nynexman4464 wants to merge 2 commits into
mainfrom
nynexman4464/docs/ast-113-popover-anchoring
Draft

nynexman4464 wants to merge 2 commits into
mainfrom
nynexman4464/docs/ast-113-popover-anchoring

Conversation

@nynexman4464

@nynexman4464 nynexman4464 commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Exact decision needed

Whether Popover and usePopover gain three caller-owned inputs, whether isModal becomes the modality switch, and whether every layer shares one focus-return rule:

  1. an anchor separate from the trigger (an element ref, a function that returns an element, or a rectangle inside a scrolling region);
  2. anchorOutOfView: 'hold' | 'hide';
  3. a cancellable onOutsidePress;
  4. isModal (boolean, default true) sets Tab containment and aria-modal together. A non-modal popover lets Tab follow the browser's order: anchor, then popover, then what follows the anchor. This supersedes component:Popover DEC-9 for isModal: false.
  5. One focus-return rule for Popover, Dialog and DropdownMenu, with no off switch:
    • it acts only on stranded focus;
    • a child layer inherits the parent's chain;
    • the fallback chain is opener → re-resolved opener → anchor → the opener's former place → nearest ancestor → parent layer → main, never the body.

Open for the owner: OQ1 (names) and OQ2 (keep or drop the rectangle anchor).

Existing authority searched

  • component:Popover (current):
    • Trigger composition requires a button anchor.
    • AR3/DEC-9 make every Popover a focus region whatever isModal says; DEC-5 supersedes that for isModal: false.
    • AR4 owns focus return today.
  • component:Dialog FR4/AR2 and component:DropdownMenu FR6 own their focus return. FR17–FR20 become the shared rule they point to.
  • architecture:layer-runtime INV7, INV12, INV18, INV21:
    • INV21 (script never moves a layer to follow its anchor) shapes FR4 and DEC-2.
    • FR7 builds on the anchor-visibility observation (spec:AST-059 FR5).
    • The layer stack gains the opener relationship (FR19).
  • 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.
  • Code:
    • findTriggerButton and its warning in Popover.tsx.
    • Containment gated only on open in usePopover.tsx.
    • Native popover="auto" light dismiss, which cannot be cancelled.
    • Focus return in three places: useFocusTrap.ts, Dialog.tsx's close branch and removal path, and DropdownMenu's handleLayerHide. utils/focusReturn.ts already holds the temporary-tab-stop helper.
  • Open PRs searched for anchor, Popover, focus trap, outside press, focus return, Tour: none decide these claims.

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: false callers in the repo:

  • PowerSearch's token editor, a dialog popover: Tab will leave the editor in page order. That matches its own description as a non-modal dialog.
  • Popover's own tests.

No story, template block or lab component sets it.

Rejected alternatives

  • Widening anchorRef to non-buttons: it changes what a released input does to the element it names.
  • Re-reading a rectangle on scroll and moving the layer: forbidden by INV21.
  • A boolean predicate for the veto: it names no event and cannot grow later.
  • A separate focus-region input with a popover-and-anchor cycle: a second name for modality, and a trap the platform does not have.
  • A returnFocus input 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 usePopover for anchoring, the veto and modality; Popover, Dialog and DropdownMenu for focus return. Out of scope: useLayer options, 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):

  • about 123 apps anchor a popover to something other than its trigger, 27 of them through a virtual anchor;
  • more than 70 veto outside presses;
  • more than 87 control focus on close.

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 returnFocus input) is superseded by DEC-6 and is rewritten as the unified focus-return PR.

Writing rubric results

  • R1 narration: none; Intent and decisions are present tense. DEC-5 states what it supersedes as a fact about authority, not as history.
  • R2 research in the record: none; survey counts and prior-art notes live in this description only.
  • R3 evidence naming a change: none; verification cells name tests and states.
  • R4 rejected alternatives over one line or outside a decision: five, each one line inside DEC-1, DEC-2, DEC-4, DEC-5 and DEC-6.
  • R5 pull request or issue references remaining: none.
  • R6 ruled open questions still listing options: none. The old focus-region question is ruled (DEC-5) and removed; OQ2 is unanswered and keeps its two options.
  • R7 project status: six FRn gap: lines state today's observable mismatch. The isModal: false caller list is current-state impact, not work order.

Scope

Components: Popover, usePopover, Dialog, DropdownMenu, useFocusTrap (internal), Layer (internal), utils/focusReturn (internal)

  • This PR contains specification records only.
  • It records observable behavior and preserves equivalent internal implementations.
  • Any named internal mechanism is intentionally public, names its dependent caller/system, and explains why an equivalent implementation would not satisfy the contract.
  • Verification states the evidence layer and failure signal without prescribing CI job/workflow topology.
  • It records one intentional durable decision, not a rescue for a separable tagalong.
  • The canonical knowledge owner, affected current claims, and overlapping open work are named; any ownership collision is resolved or called out for human decision.
  • Adjacent API, visual, accessibility, composition, and implementation facts remain outside scope unless this exact decision depends on them.
  • Public text and artifacts contain no internal Meta context.

Validation

  • pnpm check:knowledge: passes ("Knowledge templates and records are aligned")
  • Prettier
  • git diff --check

@vercel

vercel Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 11, 2026 12:02am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Nov 10, 2026
@astracat-bot

astracat-bot Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Nov 11, 2026, 12:18 AM UTC

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 opener falls 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, and component:Popover AR4 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 that component:Popover records. 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 when returnFocus="anchor" has no focusable target, which FR2 and FR4 both allow. State both outcomes, for example falling back to opener.
  • Non-goals cite spec:AST-114, which does not exist on main. Drop the id until that record lands.

[Automated review]

Comment on lines +105 to +110
- **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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@nynexman4464 nynexman4464 changed the title docs(spec): AST-113 Popover anchoring, dismissal veto, focus region and focus return (draft) docs(spec): AST-113 Popover anchoring, dismissal veto, modality and layer focus return (draft) Nov 10, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: false is the only released behavior that changes, but FR14–FR16 and FR19–FR21 change Escape, the sheet, role-none popups such as PowerSearch's adding popup, and focus return, and spec:AST-017 requires each one to be named and the isModal: false change 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:BottomSheet keeps finalFocusRef, so DEC-6 needs a narrower scope and the record needs to say which rule a Popover shown as a sheet follows.

Automated review (maintainer link)

Comment on lines +198 to +202
- **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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +221 to +224
- `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".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Comment on lines +78 to +83
- **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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +142 to +145
- `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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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).

Comment on lines +333 to +339
### 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

This branch was successfully deployed

1 active deployment
Preview — e6927de5 Deployed Nov 11, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant