Type: bug · Parent: Epic 6 (where the bug was DISCOVERED) · Surfaces: triage promote flow (app/(authed)/triage/_components/PromotePopover.tsx, Subtask 6.11.5 — the four-target picker after clicking the row-level Promote action) · shared components components/ui/Combobox.tsx, components/ui/Popover.tsx · Status: open · Reported by: Yue.
On the triage queue, opening a row's Promote popover and clicking Active sprint flips the popover to step 2 — a searchable Combobox over the project's open sprints (label: t('promote.pickSprint'), searchable, searchPlaceholder: t('promote.pickSearch')). When the combobox opens, the listbox is clipped by the popover's overflow-hidden boundary: the screenshot shows the Combobox trigger ("Select…"), the search input ("Search"), and the FIRST option row ("Sprint 4 · Sp… — Sprint entity, backlog rank…") visible, with every subsequent sprint cut off below the popover edge. The popover itself shows no scroll affordance because the listbox is positioned absolutely inside the trigger's container and lives OUTSIDE the popover-body scroll flow — same shape as the Modal-clipped case below.
Repro. Sign in as zhuyue@motir.co / !QAZ1qaz, open the moooon / motir project → Triage. Make sure the project has 2+ open sprints (the seed populates several). On any triaged row, click the row-level Promote button to open the promote popover, then click Active sprint. Observe: the step-2 panel renders the sprint Combobox; click its trigger ("Select…"). The opened listbox extends below the popover and is clipped — only the search input + the first sprint row are reachable; arrowing further down is invisible. The popover itself does not scroll.
Root cause (high confidence — identical to bug-combobox-menu-clipped-inside-modal, but the parent container is a Popover instead of a Modal). components/ui/Combobox.tsx (lines ~125-134, ~294-298, ~386-413) branches its menu rendering by parent context:
document.body with viewport-anchored position: fixed, so it escapes every overflow ancestor.triggerRef.current?.closest("[role=\"dialog\"]")) → the menu is rendered INLINE as absolute left-0 top-full ... inside the trigger's relative container, because a portal would land outside the dialog's focus scope and a centered dialog's CSS transform breaks fixed-positioned children's viewport coordinates.Radix PopoverContent renders with role="dialog" by default — it shares the focus-trap / outside-click contract with DialogContent. So when the Combobox is opened inside Popover.Content, the closest("[role=\"dialog\"]") check matches the popover as its containing dialog and the menu falls through the INLINE branch. The popover panel itself declares overflow-hidden on its content (components/ui/Popover.tsx:63, matching the Modal's clipping boundary by design — the popover MUST stay capped so it doesn't bleed past its anchored rect). The inline menu's max-h-64 (16rem) listbox is taller than the popover's remaining vertical space (the promote popover is width={320} and step 2 stacks the back link, the label, the trigger, and a confirm button above the menu), so the moment the listbox's bottom edge crosses the popover's bottom edge, it is clipped. The menu has nowhere to scroll to: an absolute child does not contribute to the popover-body scroll height.
This is the SECOND surface of the same defect that bug-combobox-menu-clipped-inside-modal documents (Combobox menu clipped by Popover/Modal overflow-hidden; the inline branch loses reachability for the focus-trap and positioning correctness it gains). The bugs are siblings, not duplicates: the fix site is the same (Combobox.tsx's inline branch), but the bounding ancestor whose getBoundingClientRect the menu must clamp against is the popover panel here, not the modal panel. The two share role="dialog" so the existing closest("[role=\"dialog\"]") ancestry lookup already finds the right element; the fix's rect-measurement just needs to work against whichever dialog ancestor is nearest. Same bug-inline-edit-clipped-when-table-short portal branch — uninvolved — stays UNTOUCHED.
Fix shapes (mirror the Modal sibling — decide at fix time).
inDialog is true, compute listMaxHeight from the containing [role="dialog"] ancestor's bottom edge minus the trigger's bottom edge (with a small gap), and set the listbox's maxHeight to that — mirroring the portaled branch's updatePosition logic (Combobox.tsx:166-189) against triggerRef.current.closest("[role=\"dialog\"]").getBoundingClientRect() instead of window.innerHeight. The menu then SCROLLS INTERNALLY, the popover/modal doesn't, and clipping is gone for BOTH surfaces.Net: a single fix in the Combobox inline branch should resolve both this bug AND bug-combobox-menu-clipped-inside-modal — the dialog-ancestor rect lookup is parent-type-agnostic. Combining the two siblings into one fix Subtask is the natural shape; reference both ids in the fix's Context refs.
Wider impact — full system scan (2026-06-15). The reported repro (PromotePopover.tsx:193) is ONE of MANY affected sites. A full scan of Combobox-rendered-inside-Popover AND Combobox-rendered-inside-Modal across the codebase enumerates every surface that hits the same inline-branch clipping; ALL of them are fixed by the single fix shape above. The fix Subtask MUST verify each one renders correctly post-fix (the clipping is load-bearing for keyboard reachability of every clipped option).
Triage page (the surface in the report):
app/(authed)/triage/_components/PromotePopover.tsx:193 — sprint Combobox (THE REPORTED ONE, step 2 of the Promote popover after picking Active sprint).app/(authed)/triage/_components/PromotePopover.tsx:217 — epic / story parent Combobox (step 2 of Promote after picking Under an epic or Under a story); same Popover, same clipping.app/(authed)/triage/_components/PromotePopover.tsx:173 — Top / Bottom placement Combobox (step 1 of Promote, lives in the FOOTER of the same popover above the four target rows). Two options only, so it's borderline visually — but it's the same inline-clipped branch and would clip if a third placement option were ever added.app/(authed)/triage/_components/MergePicker.tsx:78 — query-driven work-item Combobox (the Mark duplicate / merge triage action) rendered inside a Popover (width={360}). The search-driven option list is taller than the popover when 2+ results land; same clipping shape.Other Combobox-in-Modal sites across the system (each is its own occurrence of the SAME defect; the original bug-combobox-menu-clipped-inside-modal only named the Widget-config / Statistic-type case):
app/(authed)/_components/CreateIssueModal.tsx — HIGHEST density — 5 Combobox-backed pickers + 1 DatePicker stacked in one modal: <TypePicker> (line 193), <ParentPicker> (line 211), <WorkItemTypePicker> (line 251), <ExecutorPicker> (line 264), <PriorityPicker> (line 331), <DatePicker> (line 339). Each picker in components/issues/*Picker.tsx wraps Combobox. The lower-half pickers (PriorityPicker, WorkItemTypePicker, ExecutorPicker) will be MOST severely clipped because they sit near the bottom of a tall modal — every option past the first 1–2 is unreachable. components/issues/WorkItemTypePicker.tsx:16 even carries a source comment acknowledging "Because the create modal is a role=\"dialog\", the Combobox renders [inline]" — the engineer who wrote the picker knew about the inline branch but did not realise it clips below the modal floor.app/(authed)/settings/project/fields/_components/FieldsSettingsEditor.tsx:623 — <TypePicker> inside the Create / Edit custom-field modal (the same TypePicker → Combobox wrap; renders the custom-field type list).app/(authed)/filters/_components/ChangeOwnerDialog.tsx:96 — owner Combobox (workspace member list — can be long; the picker is the ONLY field in this modal so clipping below the modal floor is the dominant failure mode).app/(authed)/filters/_components/SubscribeDialog.tsx:200, 213 — two stacked Comboboxes (subscription frequency + delivery channel) inside the subscribe modal.app/(authed)/settings/organization/members/_components/OrgMembersClient.tsx:479 — role Combobox inside the <InviteModal> (org-roles list — short, but clips when the trigger sits low in the modal). NOTE: the SAME file has another <Combobox> at line 361, but that one is on the page-level members table row (not inside a modal) — unaffected.app/(authed)/settings/project/components/_components/ComponentsSettingsEditor.tsx:821 — move-target Combobox inside <DeleteComponentModal> (lists every OTHER component in the project as a re-parent target; clips when a project has many components). NOTE: the SAME file has a <Combobox> at line 413 on the page-level filter bar (outside the modals) — unaffected.app/(authed)/backlog/_components/CompleteSprintDialog.tsx:321 — Combobox inside the complete-sprint modal (likely the carry-over target sprint picker; the open-sprint list).app/(authed)/dashboard/_components/WidgetConfigModal.tsx:228 — Statistic-type Combobox. This is the ORIGINAL repro surface from bug-combobox-menu-clipped-inside-modal — listed here for completeness so the fix Subtask covers ALL known occurrences in one sweep.Verified safe (no Combobox inside the Modal): AddWidgetModal.tsx, CreateProjectModal.tsx, WorkspaceSwitcher.tsx, BoardSwitcher.tsx, EditFilterDialog.tsx, SaveFilterDialog.tsx, DeleteFilterDialog.tsx, StartSprintDialog.tsx (uses <DatePicker> only, which goes through its OWN Popover — not the Combobox inline branch), ReleaseKeyModal.tsx, ArchiveProjectModal.tsx, ChangeKeyModal.tsx, BoardConfigEditor.tsx, WorkflowEditor.tsx, AddWidgetModal.tsx, CreateDashboardModal.tsx, ShortcutsCheatsheet.tsx, OrgControl.tsx, AttachmentPreview.tsx, IssueQuickView.tsx. AutomationRuleEditor.tsx has 9 Comboboxes but is a full-page editor, NOT a modal — also unaffected.
Out-of-scope (different primitive, different clipping shape, NOT covered by this fix): <DatePicker> inside a Modal opens its OWN <Popover> (not a Combobox), so the inline-vs-portal branch above does NOT apply — Radix's nested-Popover-inside-Dialog has its own focus-trap and clipping interactions that are out of scope here. If a follow-up report surfaces DatePicker clipping, log it as a separate bug — same family, different fix site.
Total: 12 affected Combobox-in-{Popover,Modal} sites across 9 files, all fixed by the single inline-branch clamp. The fix Subtask should have a regression assertion per AFFECTED FILE (not per Combobox), opening each modal/popover at a viewport where the menu would have clipped pre-fix and asserting the listbox's bottom-edge fits inside the dialog ancestor's bottom-edge via getBoundingClientRect.
Test gap that let it ship. Same as the Modal sibling — Combobox tests cover the inline branch's opens/closes/keyboard-nav semantics but not its rendered geometry inside a constrained parent. The fix MUST add a render or Playwright assertion: open the menu inside a Popover.Content whose remaining space is < the menu's natural height, then assert the listbox's bottom-edge fits inside the popover's bottom-edge (getBoundingClientRect). Pair it with the existing Modal-surface regression so the same listMaxHeight fix is covered on both parents.
app/(authed)/triage/_components/PromotePopover.tsx:191-202 — the concrete repro surface (the <Combobox options={sprintOptions} searchable …> rendered inside Popover.Content after goToSprint flips step to sprint)components/ui/Popover.tsx:56-74 — RadixPopover.Content portal + overflow-hidden rounded-(--radius-card) panel; the role="dialog" source (Radix default) that triggers Combobox's inline branchcomponents/ui/Combobox.tsx:125-134, 166-189, 288-310, 386-413 — the inline-vs-portal branch + the portaled branch's viewport clamping (the shape to mirror) + the inline branch's absolute left-0 top-full rendering (the fix site)bug-combobox-menu-clipped-inside-modal (sibling) — the FIRST surface of the same defect (Combobox clipped by Modal overflow-hidden); the fix should subsume both and reference this bug's idbug-inline-edit-clipped-when-table-short (cousin) — the original table-overflow case that justified the portaled branch; the contract this fix must NOT regressmotir-core/CLAUDE.md — colour via --el-*, shape via element-shape tokens (applies to whatever wrapper the fix introduces)Refactor signal (rule of three). This is the THIRD documented occurrence of "menu/popover clipped by an ancestor's overflow" — first was bug-inline-edit-clipped-when-table-short (table → body-portal branch added), second was bug-combobox-menu-clipped-inside-modal (Modal → inline branch fails the same way in reverse), now this one (Popover, same shape as the Modal). The Combobox already encodes both branches inside one component with a closest("[role=\"dialog\"]") switch; this third occurrence does NOT add a new branch (Popover already matches role="dialog"), but it confirms the per-context-branch shape is at its ceiling. If a fourth occurrence surfaces in a NEW container type (e.g. a sticky-header data-grid or a drawer panel without role="dialog"), unify behind a single overflow-escape primitive instead of a fourth branch.