Skip to content

feat(Popover): anchorRect anchors a popover its caller opens on a viewport rectangle - #8182

Closed
vjeux wants to merge 3 commits into
meta:mainfrom
vjeux:popover-anchor-rect
Closed

vjeux wants to merge 3 commits into
meta:mainfrom
vjeux:popover-anchor-rect

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Review only this PR's change: the branch sits on main, so this diff is already its own.

User need

What led to it: this comes from moving every menu and picker in our app onto Astryx, which began with a press, slide and lift bug in its phone menus. In our app, a list row's menu opens a "Group with…" picker beside the row, where there is no button to anchor on.

Why: Popover anchors only on a trigger, so an app draws an invisible role="button" box in the right place, which announces a fake button and breaks if that box is hidden.

Problem and solution fit

Popover takes anchorRect, a getBoundingClientRect() rectangle, and sits beside it as beside a trigger, following a new rectangle when the caller passes one. No trigger ARIA is added anywhere, and focus returns where it was. It warns beside children or anchorRef, or without isOpen. Following scroll is out of scope.

Current specification

component:Popover (current) anchors only on a trigger (AR1). This PR proposes a concept row, FR10 and DEC-5, pending owner review; under the sheet presentation the sheet holds the content.

Public delta and caller burden

  • New optional anchorRect and type PopoverAnchorRect; without it nothing changes. The caller opens it with isOpen and passes a new rectangle when its target moves.
<Popover
  anchorRect={rowRect}
  isOpen={rowRect != null}
  onOpenChange={isOpen => !isOpen && setRowRect(null)}
  placement="end"
  label="Group sessions"
  content={<GroupForm />}
/>

Design and alternatives

  • anchorRef on any element. Simple, but often there is no element: a text selection, a point in a canvas.
  • A function that returns the rectangle. Lazy, but CSS anchor positioning needs a box in the page.
  • A rectangle that Popover turns into an invisible anchor box (chosen). Any caller can measure one, and the box is never painted, hit or announced. The cost: the caller updates the rectangle when its target moves.

Evidence

Four cases in Popover.test.tsx fail on main: the hidden anchor, no trigger ARIA, a new rectangle moving the popover, and the warnings. A Chromium test checks placement beside the row, that the row still takes clicks, focus return on Escape, and a transformed ancestor. On this head every CI check passes except pr-rtl, which runs the native touch-target audit main added in #7848 after this branch last took main; it clears when the branch next merges main.

Writing rubric results

R1 narration: none; present tense.
R2 research: none.
R3 evidence: FR10 cites DEC-5 and the tests.
R4 rejected alternatives: two Rejected: lines in DEC-5.
R5 change references: none.
R6 open questions: none.

Scope

  • One capability is added.
  • Unrelated fixes, cleanup, and policy changes were removed or split.
  • Consumer docs, focused tests, and migration evidence ship with the feature.
  • Public text and artifacts contain no internal Meta context.

Testing

In Storybook, open Core/Popover → Anchor Rect.

  1. Click "Group with…": a popover opens beside "Session row", tops level.
  2. The row is still a normal link: a screen reader finds no extra button.
  3. Press Escape: the popover closes and focus returns to "Group with…".

@vercel

vercel Bot commented Oct 24, 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 Oct 27, 2026 1:03am UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 30, 2026, 1:43 AM UTC

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 24, 2026
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 24, 2026
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 24, 2026
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

Modified Components

Popover (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 412B 412B 0B
Lines of Code 977 1130 -
Complexity Very High Very High (160) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/DateInput 94.0KB 94.0KB 0B 14.6KB 14.6KB 0B
core/DateRangeInput 69.4KB 69.4KB 0B 11.2KB 11.2KB 0B
core/DateTimeInput 104.2KB 104.2KB 0B 15.1KB 15.1KB 0B
core/Popover 49.8KB 50.4KB +522B 12.0KB 12.0KB +42B
core/PowerSearch 149.1KB 149.1KB 0B 18.5KB 18.5KB 0B

Sizes are gzip. Import counts the JavaScript the import loads eagerly; CSS counts the package stylesheet rules referenced by the modules it can load from its own package, lazily loaded ones included.

Bundle Size Summary

Gzip figures measure each package's entry file, not the code an import pulls in.

Package Entry (ESM) Entry gzip before Entry gzip after Delta
@astryxdesign/core 4.9KB 1.2KB 1.2KB 0B
Package stylesheet Gzip before Gzip after Delta
@astryxdesign/core 34.3KB 34.3KB +22B

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

55 of 502 shot(s) changed.

A change here is a question, not a failure: check whether the after is the
picture you intended. Record that review on the PR. Baseline maintenance is an
explicit dispatch of CI; this report never rewrites the baseline or adds a release gate.

component story theme mode pixels
Selector Indicator Space Bottom Sheet Narrow End RTL neutral light 753,933
Selector Indicator Space Bottom Sheet Wide Start neutral light 740,072
RichTextEditor Markdown parity: side by side probe dark 558,840
RichTextEditor Markdown parity: side by side probe light 540,756
Chat Full AI Chat probe light 230,823
Chat Full AI Chat probe dark 229,805
AppShell With Mobile Nav probe light 217,010
AppShell With Mobile Nav probe dark 216,960
Collapsible Dividers probe dark 66,445
Collapsible Dividers probe light 66,445
AppShell Playground neutral dark 65,198
AppShell Playground neutral light 63,788
Chat Default neutral dark 36,304
Chat Default neutral light 35,767
Chat Empty State probe light 23,531
Chat Empty State probe dark 22,622
ChatAutoScroll Scroll Behavior Comparison neutral dark 15,378
ChatAutoScroll Scroll Behavior Comparison neutral light 14,107
Chat With Attachments probe light 13,406
Chat With Attachments probe dark 11,232

and 35 more.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This pull request is waiting on a spec decision: anchorRect adds a way to anchor Popover that the current component:Popover contract does not cover, and DEC-5, FR9 and the AR1/AR4 clauses are still pending owner review. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. The decision should also settle whether attaching usePopover's triggerRef to a caller's own element already covers this need. Whatever the decision, three fixes are needed:

  • The documented example in Popover.doc.mjs passes nullable state that anchorRect's type rejects, and with undefined a closed Popover falls back to trigger mode and warns about children; show the conditional mount the story uses, or accept null.
  • FR6 still says the anatomy contains a caller trigger; amend it together with DEC-5, and define where focus goes in AR4 when the element focused before opening is gone.
  • The transform correction only undoes translation, so inside a scale() ancestor the popover overlaps the row; place the anchor box outside such ancestors, or limit the FR9 and DEC-5 wording to translated ancestors.

[Automated review]

{
label: 'Opened by the app beside a list row',
code: `<Popover
anchorRect={rowRect}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This example sets the state to null (setRowRect(null)), which anchorRect?: PopoverAnchorRect rejects (TS2322: Type 'PopoverAnchorRect | null' is not assignable to type 'PopoverAnchorRect | undefined'). Written as rowRect ?? undefined instead, the closed Popover falls back to wrapped-trigger mode: it renders an empty anchor wrapper and warns that children must contain a button. Showing the story's conditional mount here ({rect != null && <Popover anchorRect={rect} isOpen … />}), or accepting null as the closed state of rectangle mode, fixes both problems.

{
name: 'Trigger element',
required: true,
required: false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

FR6 in Popover.spec.md still says the anatomy "contains the caller trigger and content". Making the trigger optional here needs FR6 amended together with DEC-5.

const dx = rectLeft - placed.left;
const dy = rectTop - placed.top;
if (Math.abs(dx) >= 0.5 || Math.abs(dy) >= 0.5) {
box.style.setProperty('translate', `${dx}px ${dy}px`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This applies a viewport-space difference as a local translate, which is exact only for translation. In a column under transform: scale(0.75), the box renders at 75% of the row's size and the popover starts 23 px inside the row's end. Rendering the box outside any ancestor that can become its containing block (for example, a portal to document.body) would remove the need for the correction. Otherwise, FR9 and DEC-5 should say translated ancestors only.

- **AR4 — Focus return.** Dismissal returns focus to the trigger when focus had
entered the closing Popover and would otherwise be lost.
entered the closing Popover and would otherwise be lost; on an anchor
rectangle, to the element focused before it opened.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This clause doesn't say where focus goes if the element focused before opening has unmounted, for example a menu item whose menu closed. useFocusTrap skips an element that is no longer connected, so focus falls to the page body.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This pull request is waiting on a spec decision: anchorRect adds an anchor source that the current component:Popover contract does not cover, and DEC-5, FR9 and the AR1/AR4 clauses are still pending owner review. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner), and the decision should also settle whether attaching usePopover's triggerRef to a caller's own element already covers this need. Whatever the decision, three fixes are needed:

  • Under a scale() ancestor the popover covers the end of the row, because the offset in Popover.tsx only undoes translation; render the anchor box outside transformed ancestors, or limit FR9 and DEC-5 to translated ones.
  • The documented example in Popover.doc.mjs passes nullable state that anchorRect rejects (TS2322), and undefined falls back to trigger mode with a children warning; show the conditional mount the story uses, or accept null.
  • FR6 still says the anatomy contains a caller trigger; amend it together with DEC-5.

[Automated review]

vjeux added 2 commits October 26, 2026 09:18
…wport rectangle

A popover the app opens beside a list row, a link that keeps its own semantics, a text selection or a point had no anchor: anchorRef must reference a button and wires trigger state and handlers onto it, and an anchorRef without one is not anchored at all. anchorRect takes a rectangle in viewport coordinates, the shape getBoundingClientRect() returns; Popover places an invisible box there (never painted or hit, hidden by opacity because a visibility-hidden anchor hides its layer, offset after layout so a transformed ancestor does not move it) and anchors the layer on it. It moves when a new rectangle is passed, no element gets trigger ARIA, and focus returns on close to the element that had it. Beside children or anchorRef the rectangle wins with a development warning, and an uncontrolled one warns that nothing can open it. Proposed FR9 and DEC-5 of component:Popover record it, with a concept row, AR1 and AR4 notes and a verification row. An AnchorRect story and a Chromium spec check the placement, the box taking no pointer, focus return on Escape and a transformed ancestor; jsdom cases check the box, the absence of trigger state, a moved rectangle and both warnings.
…ntent once, in the sheet

Main's Popover now presents as a BottomSheet when presentation resolves to bottom-sheet (DEC-6), rendering the anchored layer and the sheet as two surfaces. On the rebase, rectangle mode renders its invisible anchor box and then those same two surfaces instead of its own popover.render call, so a sheet presentation opens the sheet and holds the content there once, and the rectangle anchors nothing. FR10 (renumbered from FR9, which main gave to the finger growth rule) says so. A new PopoverPresentation case opens an anchorRect popover with presentation="bottom-sheet": the sheet is open and holds the one Apply button, and no anchored layer is open; it fails with rectangle mode's previous direct popover.render.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This pull request is waiting on a spec decision: anchorRect adds an anchor source that the current component:Popover contract does not cover, and DEC-5, FR10, the Anchor rectangle concept row and the AR1/AR4 clauses are still pending owner review. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner), and the decision should also settle whether attaching usePopover's triggerRef to a caller's own element already covers this need. Whatever the decision, three fixes are needed:

  • Under a scale() ancestor the popover covers the end of the row, because the offset in Popover.tsx only undoes translation; render the anchor box outside transformed ancestors, or limit FR10 and DEC-5 to translated ones.
  • The documented example in Popover.doc.mjs passes nullable state that anchorRect rejects (TS2322), and undefined falls back to trigger mode with a children warning; show the conditional mount the story uses, or accept null.
  • FR6 still says the anatomy contains a caller trigger; amend it together with DEC-5.

[Automated review]

Main's meta#8608 (DEC-9, a Popover is a focus region) added two Tab rows to the focus-entry matrix beside this branch's anchor-rectangle row; both stay.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This pull request is waiting on a spec decision: anchorRect adds an anchor source that the current component:Popover contract does not cover, and DEC-5, FR10, the Anchor rectangle concept row and the AR1/AR4 clauses are still pending owner review. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner), and the decision should also settle whether attaching usePopover's triggerRef to a caller's own element already covers this need. Whatever the decision, three fixes are needed:

  • Under a scale() ancestor the popover covers the end of the row, because the offset in Popover.tsx only undoes translation; render the anchor box outside transformed ancestors, or limit FR10 and DEC-5 to translated ones.
  • The documented example in Popover.doc.mjs passes nullable state that anchorRect rejects (TS2322), and undefined falls back to trigger mode with a children warning; show the conditional mount the story uses, or accept null.
  • FR6 still says the anatomy contains a caller trigger; amend it together with DEC-5.

[Automated review]

@cixzhang cixzhang closed this Oct 28, 2026
@cixzhang cixzhang reopened this Oct 28, 2026
@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf: closed and reopened only to restart this PR's CI on the current workflow after tonight's queue-wide cancel; nothing in the PR changed.

@vjeux

vjeux commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

I'm pausing this work for now to reduce contention on the repo. Thank you for the reviews and the spec guidance.

@vjeux vjeux closed this Oct 30, 2026

This branch was successfully deployed

1 active deployment
Preview — 19b41643 Deployed Oct 27, 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. needs:design-review Affects visuals — Design should review needs:spec-owner-review Current knowledge records await owner approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants