Skip to content

perf(core): an overlay's eager import no longer carries the lazy BottomSheet's padding and focus code - #8550

Draft
vjeux wants to merge 1 commit into
meta:mainfrom
vjeux:uikit/overlay-eager-padding
Draft

vjeux wants to merge 1 commit into
meta:mainfrom
vjeux:uikit/overlay-eager-padding

Conversation

@vjeux

@vjeux vjeux commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Review only this PR's change: vjeux/astryx@c4a9ae0...f665bef

User impact

Importing Popover, a component that imports it (Carousel, Table and others) or DropdownMenu loads about 3.5 KB minified of BottomSheet's padding styles and focus helper, though BottomSheet loads only on a phone.

Problem and solution fit

Expected behavior and authority

architecture:component-style-authoring INV6: a coarse-pointer feature loads lazily, so the component's import does not carry it.

Smallest restoration

  • paddingStyles and overlayPaddingReset move to their own modules, which padding.stylex.ts re-exports (its public entry is unchanged) and useLayer and usePopover import.
  • isFocusDetached moves to its own module, still re-exported by utils.
  • Two tests follow the move (a source read, and StyleX's development class name).

Evidence

The INV6 size tool's bundle, main c4a9ae0 to this head:

Component Minified Gzip
Carousel −3,824 −973
ContextMenu −3,548 −1,028
Markdown −3,545 −1,033
Table −3,540 −987
Popover −2,880 −728
DateRangeInput −2,875 −620
DropdownMenu −2,566 −628
MoreMenu −2,559 −647

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

  • Core unit tests, touched components: 123 files, 3,205 passed.
  • Root build (no generated changes), typecheck, check:changesets, eslint, prettier: clean.
  • Open pull requests touching these files: 7 of 10 merge cleanly; 3 conflict with main identically without this change.

…omSheet's padding and focus code

Since Popover loads a BottomSheet with import() for its phone presentation (meta#8229), esbuild puts a module that both the eager code and the BottomSheet chunk import into one shared chunk, which the entry imports statically and which keeps every export either side uses. Layout/padding.stylex.ts was such a module: usePopover uses paddingStyles and useLayer uses overlayPaddingReset, while BottomSheetPanel uses the container padding variables, so every component that imports Popover eagerly carried 3,975 B of it instead of 1,413. utils/focusReturn.ts was another, reached eagerly through the utils barrel's isFocusDetached re-export while only BottomSheet uses its focusForLastInput (264 B). paddingStyles and overlayPaddingReset now live in their own modules, which padding.stylex.ts re-exports for its public entry and which useLayer and usePopover import directly; isFocusDetached moves to its own module, which the barrel re-exports.
@vercel

vercel Bot commented Oct 26, 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 26, 2026 4:23pm UTC

Request Review

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 26, 2026
@astracat-bot

astracat-bot Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 30, 2026, 12:04 AM UTC

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 26, 2026
@github-actions

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 977 -
Complexity Very High Very High (145) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/AlertDialog 54.6KB 54.1KB -491B 12.4KB 12.4KB 0B
core/AppShell 55.7KB 55.5KB -187B 11.6KB 11.6KB 0B
core/AspectRatio 3.7KB 3.7KB 0B 3.5KB 3.5KB 0B
core/Avatar 37.5KB 37.5KB +11B 6.3KB 5.2KB -1.1KB
core/AvatarGroup 39.8KB 39.8KB +4B 6.4KB 5.3KB -1.1KB
core/AvatarGroupOverflow 39.9KB 39.9KB +10B 6.4KB 5.3KB -1.1KB
core/AvatarStatusDot 7.1KB 7.1KB 0B 3.4KB 3.4KB 0B
core/Badge 3.9KB 3.9KB 0B 3.8KB 3.8KB 0B
core/Banner 47.8KB 47.8KB -3B 9.9KB 8.8KB -1.1KB
core/BaseTypeahead 53.8KB 53.8KB +25B 9.9KB 9.2KB -685B
core/Blockquote 3.5KB 3.5KB 0B 3.4KB 3.4KB 0B
core/BottomSheet 26.0KB 25.9KB -31B 7.7KB 7.7KB 0B
core/BottomSheetSwitcher 9.2KB 9.2KB 0B 3.3KB 3.3KB 0B
core/BreadcrumbItem 65.9KB 65.9KB +12B 11.0KB 10.3KB -695B
core/Breadcrumbs 22.4KB 22.4KB 0B 3.3KB 3.3KB 0B
core/Button 43.3KB 43.3KB -4B 9.2KB 8.1KB -1.1KB
core/ButtonGroup 8.4KB 8.4KB 0B 3.3KB 3.3KB 0B
core/Calendar 54.9KB 54.9KB 0B 10.0KB 8.9KB -1.1KB
core/Card 6.4KB 6.4KB -1B 6.3KB 6.2KB -90B
core/Carousel 50.2KB 49.2KB -975B 10.2KB 9.1KB -1.1KB
core/Center 4.2KB 4.2KB -1B 4.6KB 4.5KB -112B
core/ChatComposer 60.4KB 60.4KB +11B 10.8KB 10.1KB -708B
core/ChatComposerDrawer 23.9KB 23.9KB 0B 4.5KB 4.5KB 0B
core/ChatComposerInput 55.5KB 55.5KB +9B 10.1KB 9.4KB -700B
core/ChatComposerTokenElement 55.5KB 55.5KB +9B 10.1KB 9.4KB -700B
core/ChatDictationButton 46.5KB 46.5KB -11B 9.5KB 8.4KB -1.1KB
core/ChatLayout 48.8KB 48.8KB 0B 9.9KB 8.9KB -1.1KB
core/ChatLayoutScrollButton 46.4KB 46.4KB +4B 9.6KB 8.5KB -1.1KB
core/ChatMessage 23.1KB 23.1KB 0B 3.5KB 3.5KB 0B
core/ChatMessageBubble 7.0KB 7.0KB 0B 3.6KB 3.6KB 0B
core/ChatMessageList 28.4KB 28.4KB -1B 7.8KB 6.6KB -1.1KB
core/ChatMessageMetadata 25.8KB 25.8KB 0B 3.9KB 3.9KB 0B
core/ChatSendButton 45.8KB 45.8KB -2B 9.2KB 8.1KB -1.1KB
core/ChatSystemMessage 6.7KB 6.7KB +5B 3.7KB 3.7KB 0B
core/ChatTokenizedText 6.9KB 6.9KB 0B 3.9KB 3.9KB 0B
core/ChatToolCalls 31.9KB 31.9KB 0B 8.9KB 7.8KB -1.1KB
core/CheckIndicator 9.0KB 9.0KB 0B 3.6KB 3.6KB 0B
core/CheckboxIndicator 4.1KB 4.1KB 0B 3.8KB 3.8KB 0B
core/CheckboxInput 49.4KB 49.4KB +13B 9.3KB 8.2KB -1.1KB
core/CheckboxList 46.0KB 46.0KB +10B 8.1KB 7.0KB -1.1KB
core/CheckboxListItem 53.6KB 53.6KB +7B 10.5KB 9.4KB -1.1KB
core/Citation 25.9KB 25.9KB -4B 4.1KB 4.1KB 0B
core/ClickableCard 12.8KB 12.8KB +1B 6.9KB 6.8KB -99B
core/Code 3.5KB 3.5KB 0B 3.4KB 3.4KB 0B
core/CodeBlock 60.5KB 60.5KB 0B 10.7KB 9.5KB -1.1KB
core/Collapsible 11.7KB 11.7KB 0B 4.2KB 4.2KB 0B
core/CollapsibleGroup 6.4KB 6.4KB 0B 3.2KB 3.2KB 0B
core/CommandPalette 49.2KB 49.1KB -68B 11.8KB 11.8KB 0B
core/CommandPaletteEmpty 6.1KB 6.1KB 0B 3.3KB 3.3KB 0B
core/CommandPaletteFooter 23.3KB 23.3KB +7B 3.5KB 3.5KB 0B
core/CommandPaletteGroup 3.5KB 3.5KB 0B 3.3KB 3.3KB 0B
core/CommandPaletteInput 31.9KB 31.9KB +2B 8.1KB 7.0KB -1.1KB
core/CommandPaletteItem 8.4KB 8.4KB 0B 3.5KB 3.5KB 0B
core/CommandPaletteList 22.4KB 22.4KB -3B 3.3KB 3.3KB 0B
core/ComplexSelector 55.3KB 55.3KB +34B 10.6KB 9.9KB -706B
core/ContextMenu 77.3KB 76.3KB -1.0KB 14.5KB 14.5KB 0B
core/ContextMenuItem 16.6KB 16.6KB -11B 5.2KB 5.2KB 0B
core/DateInput 92.5KB 92.1KB -341B 14.6KB 14.6KB 0B
core/DateRangeInput 69.0KB 68.3KB -620B 11.2KB 10.5KB -701B
core/DateTimeInput 102.6KB 102.3KB -336B 15.1KB 15.1KB 0B
core/Dialog 13.7KB 13.7KB +3B 7.1KB 7.1KB 0B
core/DialogHeader 48.8KB 48.7KB -123B 9.8KB 9.8KB 0B
core/Divider 6.5KB 6.5KB 0B 3.6KB 3.6KB 0B
core/Drawer 29.3KB 29.2KB -85B 8.0KB 8.0KB 0B
core/DrawerHeader 48.9KB 48.9KB -76B 11.2KB 11.2KB 0B
core/DropdownMenu 72.2KB 71.6KB -626B 14.6KB 14.6KB 0B
core/DropdownMenuCheckboxItem 16.1KB 16.1KB -12B 5.6KB 5.6KB 0B
core/DropdownMenuDivider 6.6KB 6.6KB 0B 3.6KB 3.6KB 0B
core/DropdownMenuGroup 6.1KB 6.1KB 0B 3.3KB 3.3KB 0B
core/DropdownMenuItem 15.4KB 15.4KB -5B 5.2KB 5.2KB 0B
core/DropdownMenuRadioGroup 6.0KB 6.0KB 0B 3.2KB 3.2KB 0B
core/DropdownMenuRadioItem 16.2KB 16.2KB -8B 5.6KB 5.6KB 0B
core/DropdownMenuSubMenu 57.5KB 57.5KB +9B 9.5KB 8.4KB -1.1KB
core/EmptyState 7.6KB 7.6KB 0B 3.5KB 3.5KB 0B
core/Field 44.2KB 44.2KB +4B 8.0KB 6.9KB -1.1KB
core/FieldLabel 39.8KB 39.8KB +34B 6.5KB 5.4KB -1.1KB
core/FieldStatus 10.3KB 10.3KB 0B 4.2KB 4.2KB 0B
core/FileInput 53.7KB 53.7KB -6B 10.3KB 9.2KB -1.1KB
core/FormLayout 6.3KB 6.3KB 0B 3.4KB 3.4KB 0B
core/Grid 4.3KB 4.3KB 0B 3.7KB 3.7KB 0B
core/GridSpan 3.4KB 3.4KB 0B 3.2KB 3.2KB 0B
core/HStack 7.3KB 7.3KB -2B 4.9KB 4.8KB -109B
core/Heading 9.1KB 9.1KB 0B 6.8KB 5.7KB -1.1KB
core/HoverCard 17.2KB 17.2KB -1B 4.9KB 3.8KB -1.1KB
core/Icon 8.8KB 8.8KB 0B 3.6KB 3.6KB 0B
core/IconButton 43.3KB 43.3KB +2B 9.2KB 8.1KB -1.1KB
core/InputGroup 44.8KB 44.8KB +7B 8.0KB 6.9KB -1.1KB
core/InputGroupText 6.3KB 6.3KB 0B 3.7KB 3.7KB 0B
core/Item 11.4KB 11.4KB 0B 4.7KB 4.7KB 0B
core/Kbd 6.7KB 6.7KB 0B 3.5KB 3.5KB 0B
core/LayerProvider 63.2KB 63.2KB +1B 10.1KB 9.0KB -1.1KB
core/Layout 8.1KB 8.1KB 0B 5.5KB 5.5KB -47B
core/LayoutContent 7.4KB 7.4KB +1B 4.9KB 4.8KB -76B
core/LayoutFooter 7.3KB 7.3KB +5B 4.8KB 4.7KB -78B
core/LayoutHeader 7.4KB 7.4KB 0B 4.8KB 4.8KB -78B
core/LayoutPanel 7.6KB 7.6KB +3B 4.8KB 4.8KB -68B
core/Lightbox 49.5KB 49.5KB +5B 9.8KB 9.8KB 0B
core/Link 43.5KB 43.5KB 0B 7.6KB 6.5KB -1.1KB
core/List 6.7KB 6.7KB 0B 3.4KB 3.4KB 0B
core/ListItem 53.0KB 53.0KB -1B 10.5KB 9.4KB -1.1KB
core/Markdown 143.9KB 142.9KB -1.0KB 17.2KB 17.2KB 0B
core/MetadataList 23.1KB 23.1KB 0B 3.6KB 3.6KB 0B
core/MetadataListItem 6.5KB 6.5KB 0B 3.4KB 3.4KB 0B
core/MobileNav 48.8KB 48.8KB +2B 10.1KB 10.1KB 0B
core/MobileNavToggle 46.2KB 46.2KB -9B 9.4KB 8.3KB -1.1KB
core/MoreMenu 72.5KB 71.8KB -649B 14.6KB 14.6KB 0B
core/MultiSelector 75.7KB 75.4KB -318B 14.5KB 14.5KB 0B
core/NavHeadingMenu 8.9KB 8.9KB 0B 3.2KB 3.2KB 0B
core/NavIcon 3.4KB 3.4KB 0B 3.3KB 3.3KB 0B
core/NumberInput 55.6KB 55.6KB +9B 10.7KB 9.6KB -1.1KB
core/Outline 28.6KB 28.6KB 0B 4.0KB 4.0KB 0B
core/OverflowList 8.5KB 8.5KB 0B 3.5KB 3.5KB 0B
core/Overlay 19.5KB 19.5KB -1B 3.8KB 3.8KB 0B
core/Pagination 87.3KB 86.9KB -441B 15.2KB 15.2KB 0B
core/Popover 49.8KB 49.1KB -737B 12.0KB 12.0KB 0B
core/PowerSearch 147.7KB 147.4KB -327B 18.5KB 18.5KB 0B
core/ProgressBar 7.7KB 7.7KB +1B 6.6KB 5.5KB -1.1KB
core/RadioIndicator 3.8KB 3.8KB 0B 3.7KB 3.7KB 0B
core/RadioList 45.5KB 45.5KB -3B 8.0KB 6.9KB -1.1KB
core/RadioListItem 52.1KB 52.1KB +17B 9.7KB 8.6KB -1.1KB
core/ResizeHandle 25.1KB 25.1KB 0B 3.9KB 3.9KB 0B
core/ScrollableArea 10.4KB 10.4KB -3B 4.3KB 4.2KB -110B
core/Section 7.5KB 7.5KB +7B 6.2KB 6.1KB -52B
core/SegmentedControl 38.8KB 38.8KB +17B 6.3KB 5.2KB -1.1KB
core/SegmentedControlItem 8.6KB 8.6KB 0B 3.9KB 3.9KB 0B
core/SelectableCard 12.8KB 12.8KB -1B 7.1KB 7.0KB -90B
core/Selector 78.7KB 78.5KB -282B 14.8KB 14.8KB 0B
core/SelectorOption 14.5KB 14.5KB 0B 5.1KB 5.1KB 0B
core/SideNav 55.8KB 55.8KB -2B 10.6KB 10.6KB 0B
core/SideNavCollapseButton 46.6KB 46.6KB -2B 9.5KB 8.4KB -1.1KB
core/SideNavHeading 54.7KB 54.7KB -7B 10.1KB 9.5KB -706B
core/SideNavItem 54.4KB 54.4KB +12B 10.1KB 9.4KB -701B
core/SideNavSection 6.5KB 6.5KB 0B 3.6KB 3.6KB 0B
core/Skeleton 3.8KB 3.8KB 0B 3.6KB 3.6KB 0B
core/Slider 48.4KB 48.4KB -1B 8.5KB 7.4KB -1.1KB
core/Spinner 26.3KB 26.3KB 0B 7.5KB 6.4KB -1.1KB
core/Stack 7.1KB 7.1KB 0B 4.9KB 4.8KB -109B
core/StackItem 6.0KB 6.0KB 0B 3.3KB 3.3KB 0B
core/StatusDot 19.0KB 19.0KB +5B 6.2KB 5.1KB -1.1KB
core/Step 32.4KB 32.4KB 0B 5.3KB 5.3KB 0B
core/Stepper 47.4KB 47.4KB -2B 9.6KB 8.5KB -1.1KB
core/Switch 48.4KB 48.4KB +2B 9.3KB 8.2KB -1.1KB
core/Tab 9.7KB 9.7KB 0B 4.1KB 4.1KB 0B
core/TabList 40.2KB 40.2KB -4B 7.2KB 6.1KB -1.1KB
core/TabMenu 51.6KB 51.6KB -8B 10.0KB 9.3KB -705B
core/Table 83.5KB 82.5KB -987B 15.0KB 15.0KB 0B
core/TableBody 3.3KB 3.3KB 0B 3.2KB 3.2KB 0B
core/TableCell 78.4KB 77.4KB -979B 14.6KB 14.6KB 0B
core/TableFooter 3.3KB 3.3KB 0B 3.2KB 3.2KB 0B
core/TableHeader 3.3KB 3.3KB 0B 3.2KB 3.2KB 0B
core/TableHeaderCell 78.1KB 77.2KB -1001B 14.6KB 14.6KB 0B
core/TableRow 6.2KB 6.2KB 0B 3.5KB 3.5KB 0B
core/TableSelectionToolbar 52.4KB 52.1KB -308B 11.2KB 11.2KB 0B
core/Text 8.6KB 8.6KB 0B 6.8KB 5.7KB -1.1KB
core/TextArea 48.9KB 48.9KB +4B 9.4KB 8.3KB -1.1KB
core/TextInput 53.1KB 53.1KB -8B 10.5KB 9.3KB -1.1KB
core/Thumbnail 50.1KB 50.1KB -7B 11.0KB 9.8KB -1.1KB
core/TimeInput 79.3KB 78.9KB -384B 13.6KB 13.6KB 0B
core/Timer 9.2KB 9.2KB 0B 6.8KB 5.7KB -1.1KB
core/Timestamp 27.5KB 27.5KB +1B 9.5KB 8.5KB -1.1KB
core/Toast 59.8KB 59.8KB -5B 9.7KB 8.6KB -1.1KB
core/ToggleButton 44.1KB 44.1KB -8B 9.3KB 8.2KB -1.1KB
core/ToggleButtonGroup 6.3KB 6.3KB 0B 3.2KB 3.2KB 0B
core/Token 29.6KB 29.6KB 0B 4.5KB 4.5KB 0B
core/Tokenizer 64.3KB 64.3KB -4B 11.7KB 11.0KB -698B
core/Toolbar 39.8KB 39.8KB +6B 7.7KB 7.7KB 0B
core/Tooltip 16.8KB 16.8KB +3B 4.9KB 3.8KB -1.1KB
core/TopNav 49.7KB 49.7KB -2B 10.4KB 10.4KB 0B
core/TopNavHeading 54.2KB 54.1KB -39B 9.9KB 9.3KB -697B
core/TopNavItem 9.0KB 9.0KB 0B 3.8KB 3.8KB 0B
core/TopNavMegaMenu 54.2KB 54.1KB -29B 10.7KB 10.1KB -696B
core/TopNavMegaMenuFeaturedCard 6.7KB 6.7KB 0B 3.4KB 3.4KB 0B
core/TopNavMegaMenuItem 9.1KB 9.1KB 0B 4.1KB 4.1KB 0B
core/TopNavMenu 53.6KB 53.6KB +18B 10.1KB 9.4KB -695B
core/TreeList 32.5KB 32.5KB 0B 4.7KB 4.7KB 0B
core/Typeahead 61.0KB 61.0KB -6B 11.3KB 10.6KB -687B
core/TypeaheadItem 6.2KB 6.2KB 0B 3.4KB 3.4KB 0B
core/VStack 7.3KB 7.3KB -3B 4.9KB 4.8KB -109B

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 0B

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

@vjeux
vjeux marked this pull request as draft October 29, 2026 22:37

@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 looks ready to merge: moving paddingStyles, overlayPaddingReset and isFocusDetached into their own modules keeps BottomSheet-only code out of the eager bundles of Popover, DropdownMenu and the components built on them, while the public entry points, the built stylesheet and the rendered stories stay the same. The visual report compares against an older baseline: the Selector sheet, Collapsible and AppShell stories it flags render identically before and after this change. One optional fix: the changeset's "about 3.5 KB minified" fits Carousel, ContextMenu, Markdown and Table, but Popover saves about 2.9 KB, DropdownMenu about 2.6 KB and DateInput only 66 B, so "up to about 3.8 KB minified (about 1 KB gzip)" would be accurate.

[Automated review]

'@astryxdesign/core': patch
---

[perf] Components that import Popover no longer load BottomSheet's container padding styles and focus helper up front. Popover loads a BottomSheet on demand for its phone presentation, and a bundler that splits code put the modules both of them use into one chunk that loads with the component, keeping every export either side uses: about 3.5 KB minified in Popover, Carousel, DateRangeInput, DropdownMenu, Table and the other components that import Popover or DropdownMenu. `paddingStyles` and `overlayPaddingReset` now have their own modules, still exported from `@astryxdesign/core/Layout/padding.stylex`, and `isFocusDetached` has its own module, still exported from `@astryxdesign/core/utils`. Rendering is unchanged.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: "about 3.5 KB minified" fits Carousel, ContextMenu, Markdown and Table, but Popover saves about 2.9 KB, DateRangeInput about 2.9 KB, DropdownMenu about 2.6 KB and DateInput only 66 B. "Up to about 3.8 KB minified (about 1 KB gzip)" would be accurate.

cixzhang pushed a commit that referenced this pull request Oct 31, 2026
…rs (#9130)

## User impact

A `DropdownMenu` whose trigger also has a tooltip (a custom trigger in `Tooltip`, or the default button's `tooltip`) rewrites that button's inline style on every render around it. The menu's anchor comes off and goes back, so `anchor-name` is rewritten and its two names swap order. A page that renders often never settles. We found it moving one app onto Astryx.

## Problem and solution fit

- `DropdownMenu`'s trigger ref was a new function each render, so React called it again on every commit (`null`, then the element), moving the anchor each time.
- Stabilizing those refs alone was not enough. They wrap `usePopover`'s `triggerRef`, keyed on the whole layer object, which changes whenever `onShow` or `onHide` does; `DropdownMenu`'s `onShow` changes with an inline `renderTrigger`, `onClick` or `onOpenChange`. That version still made 2 and 4 writes per render in the tests below.
- So the fix is in the hook:

```ts
const layerRef = layer.ref;
const triggerRef = useCallback(
  (el: HTMLElement | null) => {
    triggerElementRef.current = el;
    layerRef(el);
  },
  [layerRef],
);
```

The callback reads nothing else from the layer, and `layer.ref` (`useLayer`'s anchor callback, keyed on the anchor name and anchor-clearance option) reads only refs, so nothing goes stale.

## Expected behavior and authority

Tooltip (#5951) and `useHoverCard` (#6606) got the same fix: a trigger ref keeps its identity when nothing about the trigger changes. No API or record changes.

## Smallest restoration

- `usePopover`: `triggerRef` is keyed on `layer.ref` instead of `layer`.
- `DropdownMenu`: stable refs on its three trigger paths, a custom trigger, the default button (still forwarding the caller's `ref`) and the bottom-sheet trigger.
- Triggers that take `usePopover`'s ref directly get it stable too: ComplexSelector's built-in trigger, DateTimeInput's date field and Breadcrumbs.

Not chosen: stabilizing each `DropdownMenu` callback instead, leaving every other `usePopover` caller churning.

Also edited by open #7677 and #8550, both of which merge cleanly with this change.

## Evidence

- Before (main eae5d92): one parent render writes the trigger's style 2 times with a custom trigger in `Tooltip`, and 4 times with the default button's `tooltip` and an inline `onOpenChange`. The two anchor names swap order, and `usePopover`'s `triggerRef` changes whenever `onShow` or `onHide` does.
- After: 0 writes, the same order, the same ref. A caller's `ref` on the default button still gets the element.

## Scope

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

## Testing

How to check:
1. Render `<DropdownMenu button={{label: 'Actions', tooltip: 'More'}} onOpenChange={() => {}}>` inside a component that renders every second.
2. Watch the button's `style` attribute in the browser's Elements panel. Before: it flashes every second. After: it stays still.

The new cases in `DropdownMenu.test.tsx` and `Popover.test.tsx` count the trigger's style writes with a MutationObserver. They fail on main and pass with the fix.

Local gates at 08a5dc5:
- The DropdownMenu, Popover, Tooltip and Layer suites, plus every other component that uses `usePopover`'s trigger ref (Breadcrumbs, Chat, ComplexSelector, DateInput, DateRangeInput, DateTimeInput, MultiSelector): 2,171 tests pass. The 6 new cases fail on main.
- The DropdownMenu, Tooltip and Popover Chromium specs: 217 passed.
- Core typecheck, ESLint with no warnings, Prettier, `pnpm check:repo` and the Storybook build.
- `pnpm test:ui` and `pnpm test:node`: the same failures as on main, none in these components.

This branch was successfully deployed

1 active deployment
Preview — f665befd Deployed Oct 26, 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant