Skip to content

feat(fields): let popover fields anchor to an outer element - #1422

Merged
tenphi merged 2 commits into
mainfrom
andrew/cub-4672-select-picker-target-ref
Sep 22, 2026
Merged

tenphi merged 2 commits into
mainfrom
andrew/cub-4672-select-picker-target-ref

Conversation

@tenphi

@tenphi tenphi commented Sep 22, 2026

Copy link
Copy Markdown
Member

Closes CUB-4672.

The gap

DialogTrigger has always taken targetRef"the ref of the element the Dialog should visually attach itself to" — but none of the ten field components that build a popover on the same machinery passed it through. A consumer had no way to redirect the anchor.

That is what forced the scope carve-out in cubedevinc/cubejs-enterprise#14818: dashboard filter popovers anchor to the whole filter row so adjacent filters line up. The five DialogTrigger-based controls can do that; the three built on ui-kit fields could not, so on one board a boolean filter's list opened offset from the date filter's popover beside it.

The change

targetRef on Select, Picker, FilterPicker, ColorPicker, ColorInput, DatePicker, DateRangePicker, DateRangeSeparatedPicker and PeriodPicker; placement on all of those except FilterPicker, which already had it. MonthPicker, QuarterPicker, YearPicker and WeekPicker inherit both from PeriodPicker (verified in the emitted .d.ts). Every default is the current behaviour, so existing callers are untouched.

Two deliberate deviations from the ticket

placement on Select, which the ticket asks for on Picker only. targetRef alone is half a fix there: Select had only direction, which resolves to `${direction} end`, so anchored to a wide row it right-aligns while the DialogTrigger controls beside it start-align — the popovers still would not line up, which is the ticket's whole goal.

Nine components rather than two. No field component exposed targetRef, so fixing only the two named would have left seven siblings with an identical gap.

The two mechanisms are not identical, and the docs say so

  • Select positions through useOverlayPosition, so targetRef moves the position onlytriggerRef stays the focus/outside-click/dismiss anchor. Anchoring to a wide container does not make clicks inside it count as trigger presses.
  • The DialogTrigger components forward the ref, so the anchor also becomes what outside-click dismissal treats as the trigger (DialogTrigger.tsx:385).

minWidth stays tied to the trigger everywhere, so a narrow control anchored to a wide row keeps its own popover width — matching how the DialogTrigger-based controls already behave.

Note ListBoxPopover already solved this internally as positionTargetRef and is not exported, so it is left alone.

Verification

Gate Result
pnpm test 2822 passed
pnpm test:browser 24 files, 231 passed
pnpm build (emits .d.ts — the broad typecheck) exit 0
pnpm test:types / test:types:consumer exit 0
pnpm audit-docs back to the pre-change baseline exactly
pnpm size no measurable change vs main (both ~590.5 kB against a 592 kB budget)
pnpm chromatic:check budget passed

The new browser test was falsified, not just run. Positioning can only be asserted with real layout — in jsdom every getBoundingClientRect() is 0 × 0, so a build ignoring targetRef entirely would pass a jsdom spec that looked identical. With the wiring deliberately removed, both anchoring cases fail and the default-behaviour case correctly still passes; restored, all four pass.

It also covers the non-obvious risk: DialogTrigger stops attaching its own ref to the trigger child once targetRef is set, and Picker puts its own ref there to hand focus back on close. That path is asserted directly rather than reasoned about.

No stories added — targetRef is a positioning prop that photographs as "a popover, somewhere", so a story would cost a recurring Chromatic snapshot without showing anything the docs do not.

Follow-up

The Cloud call site can drop its carve-out and pass targetRef once console-ui bumps its pin.

🤖 Generated with Claude Code


Correction: the size row originally read 526.92 kB. That was measured against a stale dist/pnpm size bundles from dist/, so it must follow a fresh pnpm build. Re-measured with a fresh build, main is 590.48 kB and this branch is the same to the reported precision. The budget still passes, but headroom is ~1.5 kB, not ~65 kB.

`DialogTrigger` has always taken `targetRef` — "the ref of the element
the Dialog should visually attach itself to" — but none of the ten field
components that build a popover on the same machinery passed it through,
so a consumer could not redirect the anchor.

That is what forced the scope carve-out in cubedevinc/cubejs-enterprise#14818:
dashboard filter popovers anchor to the whole filter row so adjacent
filters line up, which the five `DialogTrigger`-based controls can do and
the three built on ui-kit fields cannot.

Adds `targetRef` to `Select`, `Picker`, `FilterPicker`, `ColorPicker`,
`ColorInput`, `DatePicker`, `DateRangePicker`, `DateRangeSeparatedPicker`
and `PeriodPicker`, and `placement` to all of those but `FilterPicker`,
which already had it. `MonthPicker`, `QuarterPicker`, `YearPicker` and
`WeekPicker` inherit both from `PeriodPicker`. Every default is the
current behaviour, so existing callers are untouched.

`placement` is not in the ticket for `Select`, but `targetRef` is half a
fix without it: `Select` only had `direction`, which resolves to
`${direction} end`, so anchored to a wide row it would right-align while
the `DialogTrigger` controls beside it start-align — the popovers still
would not line up.

The two mechanisms differ, and the per-prop docs say so: on `Select` the
ref moves the POSITION only, since it goes to `useOverlayPosition` while
`triggerRef` stays the focus/dismiss anchor; on the `DialogTrigger`
components the anchor also becomes what outside-click dismissal treats as
the trigger. `minWidth` stays tied to the trigger everywhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Sep 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 87c650f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cube-dev/ui-kit Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Sep 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
cube-ui-kit Ready Ready Preview Sep 22, 2026 4:21pm UTC

Request Review

@github-actions

github-actions Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

📦 NPM canary release

Deployed canary version 0.0.0-canary-48c41d1.

@github-actions

github-actions Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

🏋️ Size limit report

Name Size Passed?
All 577.03 KB (+0.08% 🔺) Yes 🎉
Tree shaking (just a Button) 128.29 KB (+0.02% 🔺) Yes 🎉

Compared against main at 095f53crun 35745770032, 2026-09-22T15:13:05Z.

To see which modules changed, download the size-limit-statoscope-report artifact from this run and open report.html.

@github-actions

github-actions Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

🧪 Storybook is successfully deployed!

@tenphi
tenphi merged commit 565dc93 into main Sep 22, 2026
17 checks passed
@tenphi
tenphi deleted the andrew/cub-4672-select-picker-target-ref branch September 22, 2026 17:58
@tenphi tenphi mentioned this pull request Sep 22, 2026

This branch was successfully deployed

2 active deployments
Preview 87c650f1 Deployed Sep 22, 2026 by vercel[bot]
Chromatic staging 87c650f1 Deployed Sep 22, 2026 by tenphi via Prepare Storybook for review & tests #3853
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant