Skip to content

feat(react-tag-picker): add portal-free base render - #36512

Draft
Hotell wants to merge 1 commit into
microsoft:masterfrom
Hotell:demo/tag-picker-portal-free-render
Draft

feat(react-tag-picker): add portal-free base render#36512
Hotell wants to merge 1 commit into
microsoft:masterfrom
Hotell:demo/tag-picker-portal-free-render

Conversation

@Hotell

@Hotell Hotell commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Previous Behavior

renderTagPicker_unstable statically imports Portal:

import { Portal } from '@fluentui/react-portal';
{state.popover && (state.inline ? state.popover : <Portal mountNode={state.mountNode}>{state.popover}</Portal>)}

Because the import is static, @fluentui/react-portal — and through its mount-node styles, Griffel — is bundled by every consumer of this render, regardless of the inline branch taken at runtime.

The headless TagPicker is affected even though it can never portal: useTagPicker sets inline: true, and inline is deliberately omitted from TagPickerBaseProps. So headless ships portal code it cannot execute.

New Behavior

The shared JSX moves into renderTagPickerBase, which keeps the popover in DOM order and imports no portal. The styled render keeps portaling and delegates:

export const renderTagPicker_unstable = (state, contexts) =>
  renderTagPickerContent(
    state,
    contexts,
    state.popover && (state.inline ? state.popover : <Portal mountNode={state.mountNode}>{state.popover}</Portal>),
  );

Headless re-exports the base render, so nothing about its rendered output changes:

export { renderTagPickerBase_unstable as renderTagPicker } from '@fluentui/react-tag-picker';

Keeping the base in its own module is what removes the static import — sharing a file would re-introduce it. This mirrors the base-hook split used by #36503 and #36504.

Bundle size

Measured with nx run react-headless-components-preview:bundle-size --skip-nx-cache.

Standalone, on this branch:

Fixture Minified Gzipped
headless: entire library 231.539 → 227.809 kB (−3.730 kB) 66.903 → 65.667 kB (−1.236 kB, −1.8%)

The full win needs the icon PRs. Stacked on #36503 + #36504, where react-icons no longer drags Griffel in through a second path:

Fixture Minified Gzipped
headless /tag-picker 62.147 → 53.498 kB (−8.649 kB) 20.628 → 17.538 kB (−3.090 kB, −15.0%)
headless: entire library 228.973 → 220.343 kB (−8.630 kB) 65.797 → 62.627 kB (−3.170 kB, −4.8%)

With all three applied, the headless bundle contains no react-portal, @griffel/* or @fluentui/react-icons modules — verified by grepping the emitted bundle and by the check in #36511.

Notes for reviewers

  • Behavior-neutral for headless: it already rendered in DOM order via inline: true; only the unreachable import is removed.
  • The styled TagPicker is unchanged — same JSX, same portal branch.
  • renderTagPickerContent is module-internal and not exported from the package.
  • Unit tests pass for react-tag-picker and react-headless-components-preview.

Related Issue(s)

renderTagPicker_unstable statically imports Portal, so @fluentui/react-portal is bundled by any consumer of the render - including the headless TagPicker, which sets inline: true and never portals at runtime.

Moves the shared JSX into renderTagPickerBase, which keeps the popover in DOM order, and leaves portaling in the styled render. Headless re-exports the base render.

Keeping the base in its own module is what removes the static import; sharing a file would re-introduce it.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Pull request demo site: URL

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

📊 Bundle size report

Package & Exports Baseline (minified/GZIP) PR Change
react-components
react-components: entire library
1.295 MB
326.55 kB
1.295 MB
326.55 kB
22 B
react-headless-components-preview
react-headless-components-preview: entire library
231.539 kB
66.883 kB
227.809 kB
65.645 kB
-3.73 kB
-1.238 kB
react-tag-picker
@fluentui/react-tag-picker - package
174.152 kB
54.355 kB
174.244 kB
54.404 kB
92 B
49 B
Unchanged fixtures
Package & Exports Size (minified/GZIP)
react-components
react-components: Button, FluentProvider & webLightTheme
66.297 kB
19.006 kB
react-components
react-components: Accordion, Button, FluentProvider, Image, Menu, Popover
226.106 kB
68.067 kB
react-components
react-components: FluentProvider & webLightTheme
39.52 kB
13.116 kB
react-portal-compat
PortalCompatProvider
5.341 kB
2.146 kB
react-timepicker-compat
TimePicker
140.991 kB
46.069 kB
🤖 This report was generated against 7ed216e116af3d202b028d568dc6c4c975b74ad0

@@ -0,0 +1,7 @@
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🕵🏾‍♀️ visual changes to review in the Visual Change Report

vr-tests-react-components/Avatar Converged 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Avatar Converged.badgeMask.normal.chromium.png 5 Changed
vr-tests-react-components/Menu 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Menu.Nested Submenus Small Viewport Flipped.nested menu.chromium.png 699 Changed
vr-tests-react-components/Menu Converged - submenuIndicator slotted content 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Menu Converged - submenuIndicator slotted content.default - RTL.submenus open.chromium.png 599 Changed
vr-tests-react-components/Menu Converged - submenuIndicator slotted content.default.submenus open.chromium.png 413 Changed
vr-tests-react-components/Positioning 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Positioning.Positioning end.chromium.png 503 Changed
vr-tests-react-components/Positioning.Positioning end.updated 2 times.chromium.png 864 Changed
vr-tests-react-components/ProgressBar converged 3 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness - Dark Mode.default.chromium.png 42 Changed
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness - High Contrast.default.chromium.png 45 Changed
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness.default.chromium.png 39 Changed
vr-tests-react-components/TagPicker 3 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/TagPicker.disabled - Dark Mode.disabled input hover.chromium.png 658 Changed
vr-tests-react-components/TagPicker.disabled - High Contrast.chromium.png 1319 Changed
vr-tests-react-components/TagPicker.disabled - RTL.disabled input hover.chromium.png 635 Changed

There were 3 duplicate changes discarded. Check the build logs for more information.

Hotell added a commit to Hotell/fluentui that referenced this pull request Aug 5, 2026
…n TagPicker fixture

microsoft#36503 merged, adding TagPicker.fixture.js. It retains @griffel/core and
@griffel/react through the portal that ./tag-picker mounts, which the check
correctly reports as a regression because the fixture is new and nothing
allowlisted it.

Records it as tracked debt so the branch is green against current master. microsoft#36512
removes the leak, and the entry has to be deleted in the same change - the
allowlist is shrink-only, so a fixed leak keeps failing until its entry goes.

The same merge also shrank the AllComponents icon leak: react-icons now survives
only through ./teaching-popover, since the tag picker no longer ships a default
icon.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant