Skip to content

[select] Add data-popup-side to trigger#4671

Merged
mj12albert merged 3 commits into
mui:masterfrom
mj12albert:select-trigger-data-popup-side
Apr 28, 2026
Merged

[select] Add data-popup-side to trigger#4671
mj12albert merged 3 commits into
mui:masterfrom
mj12albert:select-trigger-data-popup-side

Conversation

@mj12albert
Copy link
Copy Markdown
Member

@mj12albert mj12albert commented Apr 23, 2026

Add data-popup-side to trigger matching Combobox

Closes #4667

@mj12albert mj12albert added the component: select Changes related to the select component. label Apr 23, 2026
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 23, 2026

commit: 91c8500

@code-infra-dashboard
Copy link
Copy Markdown

code-infra-dashboard Bot commented Apr 23, 2026

Bundle size

Bundle Parsed size Gzip size
@base-ui/react 🔺+214B(+0.05%) 🔺+70B(+0.05%)

Details of bundle changes

Performance

Total duration: 1,502.04 ms 🔺+216.34 ms(+16.8%) | Renders: 53 (+0) | Paint: 2,328.30 ms 🔺+343.77 ms(+17.3%)

Test Duration Renders
Tabs mount (200 instances) 289.93 ms 🔺+55.53 ms(+23.7%) 4 (+0)
Slider mount (300 instances) 174.99 ms 🔺+38.59 ms(+28.3%) 3 (+0)
Select mount (200 instances) 176.71 ms 🔺+30.82 ms(+21.1%) 3 (+0)
Mixed surface mount (app-like density) 120.63 ms 🔺+25.39 ms(+26.7%) 5 (+0)
Popover mount (300 instances) 117.90 ms 🔺+25.36 ms(+27.4%) 2 (+0)

...and 7 more. View full report

Details of benchmark changes


Check out the code infra dashboard for more information about this PR.

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 23, 2026

Deploy Preview for base-ui ready!

Name Link
🔨 Latest commit 91c8500
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/69f06cbf01e8cd0008ff35c5
😎 Deploy Preview https://deploy-preview-4671--base-ui.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@mj12albert mj12albert marked this pull request as ready for review April 23, 2026 17:58
@mj12albert mj12albert added the type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. label Apr 23, 2026
Copy link
Copy Markdown
Member

@LukasTy LukasTy left a comment

Choose a reason for hiding this comment

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

LGTM. 👍

Copy link
Copy Markdown
Member

@flaviendelangle flaviendelangle left a comment

Choose a reason for hiding this comment

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

PR Review Summary — #4671

[select] Add data-popup-side to trigger by @mj12albert · 8 files, +70 / −16 · Closes #4667

Three agents reviewed the diff against the upstream mui/base-ui repo (code-reviewer, pr-test-analyzer, silent-failure-hunter). Bottom line: ship it, with one design question worth confirming and a small follow-up test.

Critical Issues

None.

Important Issues

1. positioning.side vs renderedSide divergence — design question, not a bug

packages/react/src/select/positioner/SelectPositioner.tsx:139-142

Select's positioner derives renderedSide = alignItemWithTriggerActive ? 'none' : positioning.side and uses it for SelectPositionerState.side. The new effect pushes the raw positioning.side to the store. So under the default alignItemWithTrigger={true}, the trigger will expose data-popup-side="bottom" (or whichever side floating-ui resolved) while the positioner itself reports data-side="none".

  • The code-reviewer flagged this as an inconsistency.
  • The silent-failure-hunter argues this is intentionally correct: data-popup-side should reflect actual placement, not the rendered-as-none override.

Both views are defensible. Worth a one-line note in the PR confirming the intent — and an explicit test asserting the contract under default alignItemWithTrigger.

2. Test gaps

packages/react/src/select/trigger/SelectTrigger.test.tsx:178-203

Only side="right" with alignItemWithTrigger={false} is covered. Worth adding (and ideally for Combobox too, for parity):

  • Default alignItemWithTrigger mode — the common case, currently untested. Settles the question above.
  • Collision-flip — requested side="bottom" but viewport forces flip to top. Most failure-prone path; protects against a refactor that ever read the requested prop instead of the resolved side.

defaultOpen initial-render gating is a nice-to-have but lower value.

Suggested collision-flip test:

it.skipIf(isJSDOM)('reflects the resolved side after collision flip', async () => {
  const { user } = await render(
    <div style={{ position: 'fixed', top: 0, left: 0, height: '100vh', width: '100vw' }}>
      <div style={{ position: 'absolute', bottom: 4, left: 100 }}>
        <Select.Root>
          <Select.Trigger data-testid="trigger">Trigger</Select.Trigger>
          <Select.Portal>
            <Select.Positioner side="bottom" alignItemWithTrigger={false}>
              <Select.Popup style={{ height: 200 }}>
                <Select.Item value="a">a</Select.Item>
              </Select.Popup>
            </Select.Positioner>
          </Select.Portal>
        </Select.Root>
      </div>
    </div>,
  );
  const trigger = screen.getByTestId('trigger');
  await user.click(trigger);
  await waitFor(() => expect(screen.queryByRole('listbox')).not.toBe(null));
  expect(trigger).toHaveAttribute('data-popup-side', 'top');
});

Suggestions

  • Cleanup the store on positioner unmount (packages/react/src/select/positioner/SelectPositioner.tsx:142-145). The useIsoLayoutEffect has no cleanup, so store.popupSide lingers across open/close cycles. It's currently safe because the trigger gates on mounted && positionerElement — but positionerElement and listElement are nulled on unmount, so leaving popupSide stale is a foot-gun for future readers. A one-liner return () => store.set('popupSide', null); makes the lifecycle pattern self-consistent.
  • Shared mapping for parity with Combobox. Combobox extracts triggerStateAttributesMapping into combobox/utils/stateAttributesMapping.ts; Select inlines it in SelectTrigger.tsx:32-37. Extracting a shared helper would reduce future drift. Non-blocking.

Strengths

  • Pattern faithfully mirrors Combobox: identical store field + selector, identical useIsoLayoutEffect push, identical mounted && positionerElement gating, identical state-attribute mapping, identical Side | null typing from the canonical useAnchorPositioning module.
  • No stale-attribute window on close. Render-time computation popupSide = mounted && positionerElement ? popupSideValue : null (SelectTrigger.tsx:94) is synchronous with the positioner's ref-callback setPositionerElement(null), so the attribute disappears in the same render.
  • Conventions respected: useIsoLayoutEffect, no as any, no shadow-DOM-unsafe globals, no flushMicrotasks in the test, it.skipIf(isJSDOM) correctly applied.
  • Docs kept in sync: page.mdx, select/types.md, JSDoc on SelectTriggerState.popupSide, and SelectTriggerDataAttributes.ts all match — looks like pnpm docs:api was run.
  • No silent failures. The null fallback is a correct expression of "popup is closed", not error-hiding. getStateAttributesProps properly omits the attribute when the mapping returns null.
  • Initial state is null in SelectRoot.tsx:154 — attribute correctly absent on first render.

Recommended Action

  1. Confirm with @mj12albert whether exposing data-popup-side="bottom" while data-side="none" (under default alignItemWithTrigger) is intentional.
  2. If yes, add a default-mode test plus a collision-flip test (consider mirroring on Combobox for parity).
  3. Optional: add the unmount cleanup one-liner.

No re-review needed after these — they're all small.

@mj12albert mj12albert enabled auto-merge (squash) April 28, 2026 08:15
@mj12albert mj12albert merged commit 4597333 into mui:master Apr 28, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component: select Changes related to the select component. type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[select] Expose resolved popup side on Trigger

3 participants