Skip to content

perf(Layer): an anchored layer's own hide runs only its layout - #10346

Open
vjeux wants to merge 5 commits into
meta:mainfrom
vjeux:uikit/c08-layer-close-layout
Open

vjeux wants to merge 5 commits into
meta:mainfrom
vjeux:uikit/c08-layer-close-layout

Conversation

@vjeux

@vjeux vjeux commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

User impact

Since #9963, closing a Popover or DropdownMenu with Escape, or a DropdownMenu with a pick or a press on its trigger, runs 2 layouts in Chromium instead of 1. Closes that move no focus (a Tooltip, a Popover's trigger press) run 1. The nightly overlay-churn scene, 50 Popover and 50 DropdownMenu closes, rose from a 752 median to 924 (#10247). CI's bisect put 754 → 856 on #9963.

Problem and solution fit

#9963 takes a closed trigger's anchor-name away. hide() did so after hidePopover(), whose focus restore had already forced a layout. The write made layout dirty again, so the next lifecycle pass laid out once more.

Our hide fires the layer's beforetoggle to closed before that layout. Dropping the name there lets the hide's own layout cover the change.

Expected behavior and authority

architecture:layer-runtime INV18 is unchanged: "A trigger carries its CSS anchor name only while its layer is shown", and "A layer that animates out counts as shown until its animations end." The overlay-churn trend (#10247) tracks the layout count.

Smallest restoration

  • In our own hide (a trigger press, Escape, a pick), beforetoggle to closed drops the trigger's name unless the layer's closed state keeps it shown. That is the flag perf(Layer): a closing layer reads its animations only when its closed state declares an exit #10368 reads at the show, before the show's writes, so no layout. A layer that fades out, such as TopNavMegaMenu's panel, keeps the name until its animations end.
  • A close the browser runs itself, such as a light dismiss, releases from its toggle event as on main. It keeps its second layout.
  • removeAnchorName writes only when the name is there.

It builds on #10235 and #10368 (merged), reusing #10368's closed-state read.

Evidence

  • Reproduction before the change: the new LayerCloseLayout.interaction.browser.spec.ts counts 2 layouts in each of 5 closes, for each of the four closes above, on main with perf(Layer): a closing layer reads its animations only when its closed state declares an exit #10368 (Chromium).
  • Result after the change: 1 in each, the hide's own. The element that held the anchor name while open (a Popover's is the element around its button) reports none after the close.
  • The per-close layout count is the deterministic proof; the overlay-churn scene numbers under Testing support it.
  • Representative unchanged path: the fix(Layer): a closed layer's trigger is no CSS anchor #9963 proofs pass, including TopNavMegaMenu's close fade.
  • Regression tests: the new spec, plus six useLayer.test.tsx and anchorName.test.ts cases that fail on main, and guards for the browser's own close and a closed-state fade.

Scope

  • One defect is restored.
  • Separable contradictory or unsettled tagalongs were removed or split.
  • No new public API, visual representation, default, or interaction model is hidden under the bug-fix label.
  • Public text and artifacts contain no internal Meta context.

Testing

  • The branch merges main twice. The first merge (84a2254, with perf(Layer): a closing layer reads its animations only when its closed state declares an exit #10368) drops the branch's own transition check and its name put-back: our hide reads perf(Layer): a closing layer reads its animations only when its closed state declares an exit #10368's flag instead. The second (1257950) takes main's interaction-harness fixes.
  • Red-first, LayerCloseLayout.interaction.browser.spec.ts (Chromium, a desktop page as the perf scene uses) against a Storybook built from main 84a2254: all four cases (Escape on a Popover and on a DropdownMenu, a pick, a press on the menu's trigger) fail with {"layouts":[2,2,2,2,2]}. At this head all pass ([1,1,1,1,1]), 3/3 runs each. WebKit skips it: the rendering trace is Chromium-only.
  • The same trace per close, main against this head, with the pointer resting on its target first: a Popover's trigger press 1 and 1, a Tooltip's Escape 1 and 1, its trigger press 1 and 1. A Tooltip's pointer leave lays out 5–7 times on both, from the pointer's own hover changes.
  • useLayer.test.tsx and anchorName.test.ts: 141/141. On main's useLayer.tsx and anchorName.ts, six cases fail: the drop inside our hide's beforetoggle with no animation read after it; the closed-state rule in our hide (display with normal behavior, zero duration, opacity only, or overlay only drops the name); and no write for an absent name. Guards that pass on both: a close the browser runs itself keeps the name through its beforetoggle and drops it at toggle, or keeps it until a close fade ends; a fade only the closed state declares keeps the name through our hide.
  • The Layer, Popover, DropdownMenu and Tooltip interaction specs (11 files, fix(Layer): a closed layer's trigger is no CSS anchor #9963's TooltipClosedTriggerAnchor among them): Chromium 67 passed; WebKit on the CI profile (--cpus=4, LP_NUM_THREADS=4) 52 passed, 15 skipped.
  • LightboxZoom's proof that the double-tap zoom advances while the main thread is blocked, red in CI before the second merge: 5/5 runs, and its whole file 26/26 (Chromium).
  • Core UI suite (pnpm test:ui): 908 files, 22,223 tests pass. Core typecheck: 0 errors. pnpm check:knowledge passes. ESLint and Prettier are clean on the changed files.
  • The overlay-churn medians in the earlier description were measured before main took perf(Layer): a closing layer reads its animations only when its closed state declares an exit #10368; at this head the per-close count above is the deterministic proof.

Review notes

  • Merged with main rather than rebased, so the reviewed commits stay.

A closed trigger stops being a CSS anchor. The close took the trigger's
anchor name away after the hide had run its own layout, so the browser
laid out once more per close: 2 layouts where there used to be 1.

The layer's beforetoggle to closed fires inside every hide, ours and the
browser's own, before that layout. It now drops the name there when the
layer leaves view with the hide, decided from the layer's own computed
transition: only a display or overlay transition run as a discrete step
keeps a hidden popover in view. That read is style only, no layout. A
layer that fades out keeps today's wait for its running animations.
removeAnchorName writes only when the name is there, and the post-hide
release reads no animations when the trigger holds no name.
@vercel

vercel Bot commented Nov 6, 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 7, 2026 8:29am UTC

Request Review

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

astracat-bot Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 7, 2026, 8:31 AM UTC

@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · 6cad969

Storybook · Sandbox · built from this PR's head

Modified components

Component Size LoC Visual · A11y · RTL
LayerProvider 4.1 kB (0) · 20 B CSS (0) — ✓ · ✓ · ✗
Tokenizer 87.4 kB (+61 B) · 18.3 kB CSS (0) — — · — · —
ChatLayout 60.6 kB (+59 B) · 10.4 kB CSS (0) — — · — · —
112 more components
Component Size LoC Visual · A11y · RTL
ListItem 66.9 kB (+56 B) · 11.3 kB CSS (0) — — · — · —
BreadcrumbItem 81.3 kB (+55 B) · 12.0 kB CSS (0) — — · — · —
RadioList 53.2 kB (+55 B) · 7.2 kB CSS (0) — — · — · —
MultiSelector 91.1 kB (+54 B) · 16.6 kB CSS (0) — — · — · —
TextInput 64.7 kB (+54 B) · 11.0 kB CSS (0) — — · — · —
Button 57.3 kB (+53 B) · 9.9 kB CSS (0) — — · — · —
Calendar 68.3 kB (+53 B) · 10.5 kB CSS (0) — — · — · —
Markdown 176.6 kB (+53 B) · 20.0 kB CSS (0) — — · — · —
Typeahead 76.0 kB (+53 B) · 12.3 kB CSS (0) — — · — · —
ChatLayoutScrollButton 57.8 kB (+52 B) · 10.1 kB CSS (0) — — · — · —
SideNavCollapseButton 58.1 kB (+52 B) · 9.9 kB CSS (0) — — · — · —
TopNavHeading 71.6 kB (+52 B) · 11.0 kB CSS (0) — — · — · —
ChatDictationButton 57.9 kB (+51 B) · 10.0 kB CSS (0) — — · — · —
ComplexSelector 67.7 kB (+51 B) · 11.7 kB CSS (0) — — · — · —
DialogHeader 60.1 kB (+51 B) · 11.4 kB CSS (0) — — · — · —
Pagination 105.8 kB (+51 B) · 17.5 kB CSS (0) — — · — · —
Selector 95.8 kB (+51 B) · 17.1 kB CSS (0) — — · — · —
AvatarGroup 43.0 kB (+50 B) · 5.6 kB CSS (0) — — · — · —
ChatComposer 73.8 kB (+50 B) · 11.7 kB CSS (0) — — · — · —
DrawerHeader 60.3 kB (+50 B) · 12.8 kB CSS (0) — — · — · —
TopNav 68.8 kB (+50 B) · 10.8 kB CSS (0) — — · — · —
TopNavMenu 71.3 kB (+50 B) · 11.1 kB CSS (0) — — · — · —
IconButton 57.3 kB (+49 B) · 9.9 kB CSS (0) — — · — · —
MoreMenu 93.0 kB (+49 B) · 17.1 kB CSS (0) — — · — · —
SideNavItem 67.2 kB (+49 B) · 11.0 kB CSS (0) — — · — · —
CodeBlock 72.0 kB (+48 B) · 11.2 kB CSS (0) — — · — · —
Banner 59.3 kB (+47 B) · 10.4 kB CSS (0) — — · — · —
BaseTypeahead 67.5 kB (+47 B) · 10.8 kB CSS (0) — — · — · —
CheckboxListItem 67.9 kB (+47 B) · 11.3 kB CSS (0) — — · — · —
InputGroup 53.5 kB (+47 B) · 7.8 kB CSS (0) — — · — · —
MobileNavToggle 57.6 kB (+47 B) · 9.9 kB CSS (0) — — · — · —
TableHeaderCell 105.4 kB (+47 B) · 17.2 kB CSS (0) — — · — · —
TopNavMegaMenu 66.4 kB (+47 B) · 11.7 kB CSS (0) — — · — · —
Carousel 66.1 kB (+46 B) · 10.8 kB CSS (0) — — · — · —
ChatComposerTokenElement 71.8 kB (+46 B) · 11.2 kB CSS (0) — — · — · —
ChatSendButton 57.7 kB (+46 B) · 9.9 kB CSS (0) — — · — · —
DropdownMenu 92.7 kB (+46 B) · 17.1 kB CSS (0) — — · — · —
Field 51.8 kB (+46 B) · 7.2 kB CSS (0) — — · — · —
SideNavHeading 72.2 kB (+46 B) · 11.2 kB CSS (0) — — · — · —
StatusDot 28.4 kB (+46 B) · 5.3 kB CSS (0) — — · — · —
TableSelectionToolbar 66.8 kB (+46 B) · 13.0 kB CSS (0) — — · — · —
ToggleButton 58.2 kB (+46 B) · 10.0 kB CSS (0) — — · — · —
Tooltip 26.6 kB (+46 B) · 4.3 kB CSS (0) — — · — · —
ChatComposerInput 71.9 kB (+45 B) · 11.2 kB CSS (0) — — · — · —
HoverCard 25.0 kB (+45 B) · 4.3 kB CSS (0) — — · — · —
InfoTip 44.1 kB (+45 B) · 5.3 kB CSS (0) — — · — · —
Lightbox 108.5 kB (+45 B) · 10.3 kB CSS (0) — — · — · —
RadioListItem 64.3 kB (+45 B) · 10.1 kB CSS (0) — — · — · —
Switch 98.0 kB (+45 B) · 9.9 kB CSS (0) — — · — · —
TableCell 105.8 kB (+45 B) · 17.4 kB CSS (0) — — · — · —
ContextMenu 104.6 kB (+44 B) · 17.0 kB CSS (0) — — · — · —
DropdownMenuSubMenu 67.2 kB (+44 B) · 9.7 kB CSS (0) — — · — · —
AppShell 87.3 kB (+43 B) · 13.4 kB CSS (0) — — · — · —
CheckboxInput 61.3 kB (+43 B) · 9.1 kB CSS (0) — — · — · —
SideNav 86.7 kB (+43 B) · 11.3 kB CSS (0) — — · — · —
AlertDialog 101.4 kB (+42 B) · 14.2 kB CSS (0) — — · — · —
DateInput 178.8 kB (+42 B) · 16.7 kB CSS (0) — — · — · —
Toast 94.8 kB (+42 B) · 10.2 kB CSS (0) — — · — · —
DateTimeInput 201.3 kB (+41 B) · 18.4 kB CSS (0) — — · — · —
NumberInput 68.8 kB (+41 B) · 11.3 kB CSS (0) — — · — · —
PowerSearch 278.0 kB (+41 B) · 23.5 kB CSS (0) — — · — · —
Table 114.0 kB (+41 B) · 17.7 kB CSS (0) — — · — · —
TabMenu 68.4 kB (+41 B) · 11.0 kB CSS (0) — — · — · —
AvatarGroupOverflow 45.5 kB (+40 B) · 5.5 kB CSS (0) — — · — · —
DateRangeInput 88.7 kB (+40 B) · 12.3 kB CSS (0) — — · — · —
FileInput 66.2 kB (+40 B) · 10.8 kB CSS (0) — — · — · —
SegmentedControl 90.6 kB (+40 B) · 6.8 kB CSS (0) — — · — · —
Toolbar 40.0 kB (+40 B) · 7.9 kB CSS (0) — — · — · —
Avatar 40.3 kB (+39 B) · 5.5 kB CSS (0) — — · — · —
CheckboxList 66.6 kB (+39 B) · 8.1 kB CSS (0) — — · — · —
FieldLabel 48.0 kB (+39 B) · 5.6 kB CSS (0) — — · — · —
Popover 85.1 kB (+39 B) · 13.9 kB CSS (0) — — · — · —
Stepper 58.9 kB (+39 B) · 10.0 kB CSS (0) — — · — · —
TextArea 59.2 kB (+39 B) · 9.2 kB CSS (0) — — · — · —
MobileNav 67.1 kB (+36 B) · 10.5 kB CSS (0) — — · — · —
Thumbnail 61.7 kB (+36 B) · 11.5 kB CSS (0) — — · — · —
Slider 103.4 kB (+35 B) · 8.7 kB CSS (0) — — · — · —
TabList 40.7 kB (+35 B) · 6.4 kB CSS (0) — — · — · —
Link 50.2 kB (+31 B) · 7.0 kB CSS (0) — — · — · —
TimeInput 155.7 kB (+30 B) · 15.7 kB CSS (0) — — · — · —
CommandPaletteInput 30.1 kB (+1 B) · 7.5 kB CSS (0) — — · — · —
Text 10.2 kB (+1 B) · 5.4 kB CSS (0) — — · — · —
Timer 12.3 kB (+1 B) · 5.9 kB CSS (0) — — · — · —
CommandPalette 69.4 kB (0) · 12.4 kB CSS (0) — — · — · —
ChatMessageList 26.5 kB (−1 B) · 7.0 kB CSS (0) — — · — · —
ChatToolCalls 30.4 kB (−1 B) · 8.3 kB CSS (0) — — · — · —
Heading 10.8 kB (−1 B) · 5.4 kB CSS (0) — — · — · —
ProgressBar 11.5 kB (−1 B) · 6.2 kB CSS (0) — — · — · —
Spinner 23.1 kB (−1 B) · 6.1 kB CSS (0) — — · — · —
Timestamp 31.0 kB (−1 B) · 10.2 kB CSS (0) — — · — · —
charts/Chart 194.2 kB (new) · 494 B CSS (new) — — · — · —
charts/ChartLegend 46.6 kB (new) · 224 B CSS (new) — — · — · —
charts/ChartTooltip 171.6 kB (new) · 423 B CSS (new) — — · — · —
layer — — — · ✓ · —
Layer — — ✓ · ✓ · —
lab/ChatEmojiPicker 99.5 kB (new) · 773 B CSS (new) — — · — · —
lab/ChatReactionBar 100.7 kB (new) · 1.1 kB CSS (new) — — · — · —
lab/CommentComposer 141.1 kB (new) · 1.9 kB CSS (new) — — · — · —
lab/CommentPin 53.6 kB (new) · 1.2 kB CSS (new) — — · — · —
lab/Comments 320.1 kB (new) · 2.5 kB CSS (new) — — · — · —
lab/CommentThread 315.2 kB (new) · 2.4 kB CSS (new) — — · — · —
lab/DiffViewer 83.7 kB (new) · 2.1 kB CSS (new) — — · — · —
lab/EmojiPicker 83.2 kB (new) · 751 B CSS (new) — — · — · —
lab/ListInput 98.1 kB (new) · 1.6 kB CSS (new) — — · — · —
lab/Schedule 101.7 kB (new) · 3.9 kB CSS (new) — — · — · —
lab/SelectableCardGroup 61.2 kB (new) · 162 B CSS (new) — — · — · —
lab/TourStep 91.4 kB (new) · 590 B CSS (new) — — · — · —
lab/TransferList 104.6 kB (new) · 1.7 kB CSS (new) — — · — · —
lab/TransferListSelector 111.2 kB (new) · 1.9 kB CSS (new) — — · — · —
richtext/RichTextEditor 293.3 kB (new) · 2.6 kB CSS (new) — — · — · —
richtext/RichTextEditorToolbar 247.2 kB (new) · 454 B CSS (new) — — · — · —
richtext/RichTextView 273.0 kB (new) · 2.3 kB CSS (new) — — · — · —

Package size: measured; main has no measurement of the merge base to compare yet.

API changes: cli +14 · ~5 · −0 (against the merge base's committed report)

Committed API report behind this build: cli 19 rows, core 24 rows; run pnpm api:report.

19 API changes (advisory)
Package Where Signature Usage
+ cli ./api · Types · type SearchResultEntry deprecated?: ComponentDeprecation | undefined import type {SearchResultEntry} from '@astryxdesign/cli/api'; const x: SearchResultEntry = {deprecated: …}
+ cli ./authoring · Types · interface ComponentDeprecation interface ComponentDeprecation import type {ComponentDeprecation} from '@astryxdesign/cli/authoring'
+ cli ./authoring · Types · interface ComponentIconSlotDoc interface ComponentIconSlotDoc import type {ComponentIconSlotDoc} from '@astryxdesign/cli/authoring'
+ cli ./authoring · Types · interface ComponentIconSlotStateDoc interface ComponentIconSlotStateDoc import type {ComponentIconSlotStateDoc} from '@astryxdesign/cli/authoring'
+ cli ./authoring · Types · interface MultiComponentDoc deprecated?: ComponentDeprecation import type {MultiComponentDoc} from '@astryxdesign/cli/authoring'; const x: MultiComponentDoc = {deprecated: …}
+ cli ./authoring · Types · interface SingleComponentDoc deprecated?: ComponentDeprecation import type {SingleComponentDoc} from '@astryxdesign/cli/authoring'; const x: SingleComponentDoc = {deprecated: …}
+ cli ./doc · Types · interface ComponentBaseDoc deprecated?: ComponentDeprecation import type {ComponentBaseDoc} from '@astryxdesign/cli/doc'; const x: ComponentBaseDoc = {deprecated: …}
+ cli ./doc · Types · interface SubComponentDoc deprecated?: ComponentDeprecation import type {SubComponentDoc} from '@astryxdesign/cli/doc'; const x: SubComponentDoc = {deprecated: …}
+ cli ./doc · Also exported from an earlier entry point · ComponentDeprecation ComponentDeprecation (./authoring) import {ComponentDeprecation} from '@astryxdesign/cli/doc'
+ cli ./doc · Also exported from an earlier entry point · ComponentIconSlotDoc ComponentIconSlotDoc (./authoring) import {ComponentIconSlotDoc} from '@astryxdesign/cli/doc'
+ cli ./doc · Also exported from an earlier entry point · ComponentIconSlotStateDoc ComponentIconSlotStateDoc (./authoring) import {ComponentIconSlotStateDoc} from '@astryxdesign/cli/doc'
+ cli ./template · Also exported from an earlier entry point · ComponentDeprecation ComponentDeprecation (./authoring) import {ComponentDeprecation} from '@astryxdesign/cli/template'
+ cli ./template · Also exported from an earlier entry point · ComponentIconSlotDoc ComponentIconSlotDoc (./authoring) import {ComponentIconSlotDoc} from '@astryxdesign/cli/template'
+ cli ./template · Also exported from an earlier entry point · ComponentIconSlotStateDoc ComponentIconSlotStateDoc (./authoring) import {ComponentIconSlotStateDoc} from '@astryxdesign/cli/template'
~ cli ./api · Functions · function search function search: (query: string, options?: { cwd?: string | undefined; type?: "template" | "component" | "hook" | "theme" | "doc" | undefined; limit?: number | undefined }) => Promise<SearchResponse> → function search: (query: string, options?: { cwd?: string | undefined; type?: "template" | "component" | "doc" | "theme" | "hook" | undefined; limit?: number | undefined }) => Promise<SearchResponse> import {search} from '@astryxdesign/cli/api'; search(…)
~ cli ./authoring · Types · interface MultiComponentDoc theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[] } → theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[]; componentIcons?: ComponentIconSlotDoc[] } import type {MultiComponentDoc} from '@astryxdesign/cli/authoring'; const x: MultiComponentDoc = {theming: …}
~ cli ./authoring · Types · interface SingleComponentDoc theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[] } → theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[]; componentIcons?: ComponentIconSlotDoc[] } import type {SingleComponentDoc} from '@astryxdesign/cli/authoring'; const x: SingleComponentDoc = {theming: …}
~ cli ./doc · Types · interface ComponentBaseDoc theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[] } → theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[]; componentIcons?: ComponentIconSlotDoc[] } import type {ComponentBaseDoc} from '@astryxdesign/cli/doc'; const x: ComponentBaseDoc = {theming: …}
~ cli ./doc · Types · interface SubComponentDoc theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[] } → theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[]; componentIcons?: ComponentIconSlotDoc[] } import type {SubComponentDoc} from '@astryxdesign/cli/doc'; const x: SubComponentDoc = {theming: …}

Touched components checked; add ci:run-visual, ci:run-a11y or ci:run-rtl to run the full suites.


Generated by PR Enrichment workflow | View full report

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

Changes requested: the one-layout close works for layers that leave view with the hide (Popover, DropdownMenu, Tooltip and HoverCard each drop from 2 layouts to 1 per close in Chromium), but it also drops the anchor of some layers that are still fading out.

  • handleBeforeToggle (useLayer.tsx:1675-1682) removes the trigger's anchor name whenever the open style has no display/overlay transition, so a useLayer layer whose fade is declared on its closed style, or is a keyframe animation of display, jumps to the viewport corner for its whole fade, against layer-runtime INV18 ("A layer that animates out counts as shown until its animations end").
  • The changeset's "A layer that fades out keeps the name until its fade ends, as before" (.changeset/layer-close-one-layout.md:11) and the staysInViewAfterHide JSDoc (useLayer.tsx:533-535) are not true for those two exit styles.

One fix covers both: after the hide, read exitAnimations(layer) again and put the name back while an animation runs, which flushes style but forces no layout. A unit case with a closed-style transition would lock it in.

[Automated review]

Comment thread packages/core/src/Layer/useLayer.tsx Resolved
Comment thread packages/core/src/Layer/useLayer.tsx Resolved
@cixzhang cixzhang added ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) ci:run-perf Run the perf scenes (perf-nightly.yml) on this pull request labels Nov 6, 2026
@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec e1515b5

Our hide's beforetoggle drops the trigger's anchor name when the open
style declares no exit transition. A layer whose closed style declares
its fade, or that runs a keyframe animation of display, still paints
after the hide. The release right after our hide reads the layer's
running animations, a style read with no layout, and puts the name back
until they end. A close the browser runs itself keeps the name through
its beforetoggle and releases it from its toggle event, the first point
where the release can see such a fade.
@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 00d58a4

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

Changes requested: the one-layout close works where the layer's own hide runs. Escape on Popover and DropdownMenu, a DropdownMenu pick or trigger press, and a Tooltip pointer leave each drop from 2 layouts to 1 per close in Chromium, and layers that fade out keep their anchor as on main; one wording fix remains.

  • The changeset (.changeset/layer-close-one-layout.md:5) and the description list "a trigger press" among the closes that now run one layout and name Popover, but a press on an open Popover's trigger is a close the browser runs itself, so it still runs 2 layouts per close, as on main; narrow the wording (for example "such as Escape, a pick, or a press on a DropdownMenu trigger") and list a Popover trigger press beside light dismiss.

Not blocking: the new spec's anchor-name: none check reads the Popover's button, which never holds the name (the element wrapping it does), and the staysInViewAfterHide JSDoc follow-up you noted still applies.

[Automated review]

@vjeux vjeux changed the title perf(Layer): an anchored layer's close runs only the hide's layout perf(Layer): an anchored layer's own hide runs only its layout Nov 6, 2026
Main added the closed-state read for a closing layer. Our hide now
drops the anchor name only when that read says the layer stops
painting, so the duplicate transition check and the name put-back go.
@cixzhang

cixzhang commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 98a05f7

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

Changes requested: the one-layout close works and stays within the layer runtime's anchor rules, but the release note claims more than this change does.

  • Regression: since the merge with main, .changeset/layer-close-one-layout.md credits this change with Tooltip closes and a trigger press dropping from two layouts to one, but on the base a Tooltip close (pointer leave, Escape or trigger press) and a Popover trigger press already run one layout and only Escape on Popover and DropdownMenu and a DropdownMenu pick or trigger press go from two to one, so narrow the sentence to those closes and correct the description's User impact list the same way.

Also worth fixing, not blocking: the new browser spec reads anchor-name on the Popover's button, but Popover anchors the element that wraps it, so that assertion cannot fail for Popover; read the element that held the name while the layer was open.

[Automated review]

Comment thread .changeset/layer-close-one-layout.md Resolved
The release note and the proof now name the closes that go from two
layouts to one: Escape on a Popover or a DropdownMenu, and a pick or a
press on the trigger in a DropdownMenu. The closes that move no focus
already ran one layout. The proof reads the anchor name on the element
that held it while open, which for a Popover wraps its button.
@cixzhang

cixzhang commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 3b4a48e

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

Approved pending CI: CI has not run on this head yet because the pull request is still a draft; it runs once the pull request is marked ready for review.

The release note now names exactly the four closes that go from two layouts to one, and the browser spec covers each of them and checks the element that actually holds the anchor name, so both earlier points are resolved. The change stays within the layer runtime's rule that only a shown layer's trigger is an anchor.

[Automated review]

@vjeux
vjeux marked this pull request as ready for review November 7, 2026 04:41
Picks up main's interaction-harness fixes, which the Lightbox zoom
proof needs on a loaded runner.
@cixzhang

cixzhang commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 6cad969

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

Approved pending CI: the required checks are still running on this head.

Merging main leaves this change's own files untouched, so the earlier assessment stands: the four closes the release note names go from two layouts to one, and a layer whose closed state fades out still keeps its trigger's anchor until the fade ends.

[Automated review]

This branch was successfully deployed

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

Labels

ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) ci:run-perf Run the perf scenes (perf-nightly.yml) on this pull request 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.

2 participants