diff --git a/packages/ui/components/CommentPopover.tsx b/packages/ui/components/CommentPopover.tsx index 3fd84ce6..bfbf2180 100644 --- a/packages/ui/components/CommentPopover.tsx +++ b/packages/ui/components/CommentPopover.tsx @@ -529,6 +529,7 @@ export const CommentPopover: React.FC = ({ hasUnsavedContent || (allowEmptySubmit && initialText.trim().length > 0); const canAskAI = !!onAskAI && !askAIDisabled && text.trim().length > 0; + const showsSkillMenu = skillAc.menu !== null; // Shared by both footers. Disabled once anything is typed or attached so a // click can never discard a draft; with content present, Save is the path. @@ -570,7 +571,9 @@ export const CommentPopover: React.FC = ({ aria-modal="true" aria-label={isGlobal ? 'Global comment' : 'Comment'} tabIndex={-1} - className="relative w-full max-w-xl max-h-full min-h-0 bg-popover border border-border rounded-xl shadow-2xl flex flex-col overflow-hidden" + className={`relative w-full max-w-xl max-h-full min-h-0 bg-popover border border-border rounded-xl shadow-2xl flex flex-col ${ + showsSkillMenu ? 'overflow-visible' : 'overflow-hidden' + }`} style={{ animation: 'comment-dialog-in 0.15s ease-out', }} @@ -617,7 +620,9 @@ export const CommentPopover: React.FC = ({ {chipsRow} {/* Textarea */} -
+
{skillAc.menu && ( = ({ left: dragPosition.left, width: position.width, maxHeight: visibleBounds.height, - overflowY: 'auto', + overflowY: showsSkillMenu ? 'visible' : 'auto', } : { top: position.top, left: position.left, width: position.width, maxHeight: position.maxHeight, - overflowY: 'auto', + overflowY: showsSkillMenu ? 'visible' : 'auto', ...(position.flipAbove ? { transform: 'translateY(-100%)' } : {}), animation: position.flipAbove ? 'comment-popover-in-above 0.15s ease-out' diff --git a/packages/ui/components/SkillReferenceMenu.placement.test.tsx b/packages/ui/components/SkillReferenceMenu.placement.test.tsx index b206e035..631fac2d 100644 --- a/packages/ui/components/SkillReferenceMenu.placement.test.tsx +++ b/packages/ui/components/SkillReferenceMenu.placement.test.tsx @@ -139,6 +139,18 @@ function list(): HTMLElement { return el; } +function clippingAncestor(el: HTMLElement): HTMLElement | null { + let ancestor = el.parentElement; + while (ancestor && ancestor !== document.body) { + const style = window.getComputedStyle(ancestor); + const clipsOverflow = [style.overflow, style.overflowX, style.overflowY] + .some((value) => /^(auto|clip|hidden|scroll)$/.test(value)); + if (clipsOverflow) return ancestor; + ancestor = ancestor.parentElement; + } + return null; +} + function makeRect(top: number, bottom: number): DOMRect { return { x: 100, @@ -208,6 +220,15 @@ function assertMenuInsideViewport(): { direction: string; maxListHeight: number } describe('SkillReferenceMenu adaptive placement', () => { + test.skipIf(!hasDom)( + 'the menu escapes the comment card overflow instead of being clipped at its edge', + async () => { + await mountPopover(); + await type(textarea(), '$'); + expect(clippingAncestor(menu()) === null).toBe(true); + }, + ); + test.skipIf(!hasDom)( 'THE bug: composer near the top of the viewport opens the menu BELOW, on screen', async () => {