Repository navigation
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report🔎 Preview ·
|
| 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
There was a problem hiding this comment.
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 nodisplay/overlaytransition, so auseLayerlayer whose fade is declared on its closed style, or is a keyframe animation ofdisplay, 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 thestaysInViewAfterHideJSDoc (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]
|
/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.
|
/approve-spec 00d58a4 |
There was a problem hiding this comment.
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]
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.
|
/approve-spec 98a05f7 |
There was a problem hiding this comment.
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.mdcredits 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]
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.
|
/approve-spec 3b4a48e |
There was a problem hiding this comment.
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]
Picks up main's interaction-harness fixes, which the Lightbox zoom proof needs on a loaded runner.
|
/approve-spec 6cad969 |
There was a problem hiding this comment.
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]
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-nameaway.hide()did so afterhidePopover(), 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
beforetoggleto closed before that layout. Dropping the name there lets the hide's own layout cover the change.Expected behavior and authority
architecture:layer-runtimeINV18 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
beforetoggleto 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.toggleevent as on main. It keeps its second layout.removeAnchorNamewrites only when the name is there.It builds on #10235 and #10368 (merged), reusing #10368's closed-state read.
Evidence
LayerCloseLayout.interaction.browser.spec.tscounts 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).noneafter the close.useLayer.test.tsxandanchorName.test.tscases that fail on main, and guards for the browser's own close and a closed-state fade.Scope
Testing
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.useLayer.test.tsxandanchorName.test.ts: 141/141. On main'suseLayer.tsxandanchorName.ts, six cases fail: the drop inside our hide'sbeforetogglewith no animation read after it; the closed-state rule in our hide (displaywithnormalbehavior, zero duration,opacityonly, oroverlayonly 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 itsbeforetoggleand drops it attoggle, or keeps it until a close fade ends; a fade only the closed state declares keeps the name through our hide.TooltipClosedTriggerAnchoramong 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).pnpm test:ui): 908 files, 22,223 tests pass. Core typecheck: 0 errors.pnpm check:knowledgepasses. ESLint and Prettier are clean on the changed files.Review notes