Skip to content

fix(copilot): correct composer popover positioning and stacking in the widget shadow DOM - #3025

Open
RoyBA wants to merge 5 commits into
Chainlit:mainfrom
RoyBA:fix/copilot-popover-positioning
Open

fix(copilot): correct composer popover positioning and stacking in the widget shadow DOM#3025
RoyBA wants to merge 5 commits into
Chainlit:mainfrom
RoyBA:fix/copilot-popover-positioning

Conversation

@RoyBA

@RoyBA RoyBA commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Summary

In the Copilot widget, the message-composer popovers — tools picker (CommandPopoverButton), ModePicker, and FavoriteButton — could render far off-target (and, once portaled, behind the chat) when the widget is embedded in a host page that scales it. This fixes both the positioning and the stacking, scoped to the widget's shadow DOM. The standalone Chainlit app is unaffected.

recording2

How to reproduce

  1. Embed the Copilot widget in a host page whose root shrinks text, e.g. html { font-size: 10px } (common in some design systems). The widget's own text now renders tiny and hard to read.
  2. To fix the text size, scale the widget up via customCssUrl:
    #chainlit-copilot-chat { zoom: 1.45; }
  3. Open the commands/tools popover (or the mode picker / favorites) in the composer.
  4. Bug: the popover is detached from its trigger, and at some zoom levels renders outside the visible page area entirely.

A transformed host root reproduces the same thing without zoom — e.g. body { transform: translateZ(0) } (or filter / perspective / will-change: transform), which is common on real hosts.

Root causes

Two compounding, shadow-DOM-specific issues:

  1. Mispositioning under host zoom / transform / root font-size. The popovers rendered their PopoverContent inline (no Portal). In floating mode the whole chat panel is itself a Radix popper whose wrapper carries a transform, so it becomes the containing block for the popovers' position: fixed content. floating-ui can't compensate for the combined host scale + panel transform, so the computed offset drifts — and the drift grows with the zoom factor, pushing the popover off-screen.
  2. Stacking (once portaled). Radix copies the content's computed z-index onto [data-radix-popper-content-wrapper] as an inline style. These pickers set no z-index, so the wrapper inlines auto and loses to the widget's z-50 chat surfaces.

Fix

  • Portal each popover into the widget's shadow root via <PopoverPortal container={window.cl_shadowRootElement}> — the convention the shared frontend/src/components/ui/* primitives already use. This moves the content out of the transformed/zoomed panel, so position is computed against the shadow root, not a scaled containing block. In the standalone app that global is undefined, so Radix falls back to document.body (unchanged behavior).
  • Set an explicit z-[51] on each picker's own content, so Radix copies it onto the popper wrapper (matching the z-50-on-content convention of the other poppers) and it stays above the z-50 chat. Scoped per-component — no global rule, no !important, no effect on other poppers.

Files changed

  • CommandPopoverButton.tsx
  • ModePicker.tsx
  • FavoriteButton.tsx
  • frontend/tests/{CommandPopoverButton,ModePicker,FavoriteButton}.spec.tsx (tests)

Testing

  • Added unit tests for all three pickers: each verifies the popover is portaled into cl_shadowRootElement and carries the z-[51] stacking class. pnpm test → 38/38.
  • pnpm lint, pnpm format-check, pnpm type-check pass.
  • Manual: verified each picker in floating and sidebar modes, under host zoom and non-default root font-size, in light and dark themes.

@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. frontend Pertains to the frontend. labels Aug 26, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 4 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread libs/copilot/src/index.css Outdated
The command, favorite, and mode picker popovers rendered their content inline. In the widget's floating mode the chat panel is itself a transformed Radix popper, which becomes the containing block for the fixed-positioned popovers and breaks floating-ui's scale compensation under host zoom/font-size, throwing them off-screen.

Portal each popover into cl_shadowRootElement (the convention the shared ui/* primitives already use) and set an explicit z-index on each picker's content so Radix copies it onto the popper wrapper, keeping them above the z-50 chat. This matches how ui/popover, dropdown, select set z-50 on their own content and avoids a global wrapper override.

Co-Authored-By: GitHub Copilot <noreply@github.com>
@RoyBA
RoyBA force-pushed the fix/copilot-popover-positioning branch from d20929c to 3209114 Compare August 26, 2026 11:49
@dokterbob

Copy link
Copy Markdown
Collaborator

@RoyBA Any chance for tests on this one?

Add tests for the three composer pickers verifying each popover is portaled into cl_shadowRootElement and carries the z-[51] stacking class that keeps it above the chat surfaces.

Co-Authored-By: GitHub Copilot <noreply@github.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 3 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread frontend/tests/ModePicker.spec.tsx
Comment thread frontend/tests/FavoriteButton.spec.tsx Outdated
Comment thread frontend/tests/FavoriteButton.spec.tsx Outdated
Comment thread frontend/tests/CommandPopoverButton.spec.tsx Outdated
Address review: use attachShadow instead of a plain div and assert the popover lands in the encapsulated shadow tree (not the light DOM), add a standalone document.body fallback test, centralize the ResizeObserver/scrollIntoView shims in setup-tests, and move shadow-host cleanup into afterEach.

Co-Authored-By: GitHub Copilot <noreply@github.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 5 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread frontend/tests/testUtils.ts
Address review: move cleanupShadowHosts into setup-tests' global afterEach so mountShadowHost consumers can't leak the DOM host or the cl_shadowRootElement global, removing the per-spec afterEach boilerplate.

Co-Authored-By: GitHub Copilot <noreply@github.com>
@RoyBA

RoyBA commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

@RoyBA Any chance for tests on this one?

@dokterbob Added unit tests for all three composer pickers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

frontend Pertains to the frontend. size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants