From b9faa27a671db626c1bb6baae6b145d0a693e791 Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:04:52 +0800 Subject: [PATCH] fix(editor): open mention/slash popup upward and clamp height to viewport (#5376) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `@`-mention and `/`-command suggestion popups defaulted to `placement: "bottom-start"`, so they stayed below the caret whenever any space existed below it — even when far more room was above. In bottom-anchored composers (chat input, issue comment/reply) that meant the list opened down over the send controls and, near the viewport bottom, was squashed or clipped off-screen. Two compounding causes: - The preferred side was the cramped one. Composers' roomy side is above the caret, so default to `top-start`; `flip` still sends it down when the caret is near the viewport top. - The floating-ui `size` middleware wrote `maxHeight` on the outer wrapper, which does not clip — the inner list is the scroll container and carried its own fixed `max-h-[300px]/[420px]`. That viewport-unaware cap was the real height authority and could overflow. Publish the size middleware's `availableHeight` as a CSS var and have the list clamp to `min(designMax, availableHeight)`, so there is a single, viewport-aware height authority. Drop the old `Math.max(120, ...)` floor that forced overflow in tight bands. Co-authored-by: Claude Opus 4.8 (1M context) --- .../editor/extensions/mention-suggestion.tsx | 10 +++++-- .../extensions/slash-command-suggestion.tsx | 6 ++++- .../editor/extensions/suggestion-popup.tsx | 27 +++++++++++++++++-- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/packages/views/editor/extensions/mention-suggestion.tsx b/packages/views/editor/extensions/mention-suggestion.tsx index ea0485724b..0edcfa45fa 100644 --- a/packages/views/editor/extensions/mention-suggestion.tsx +++ b/packages/views/editor/extensions/mention-suggestion.tsx @@ -352,9 +352,15 @@ export const MentionList = forwardRef(
{groups.map((group) => ( diff --git a/packages/views/editor/extensions/slash-command-suggestion.tsx b/packages/views/editor/extensions/slash-command-suggestion.tsx index ff8aa958f9..6d13ed993d 100644 --- a/packages/views/editor/extensions/slash-command-suggestion.tsx +++ b/packages/views/editor/extensions/slash-command-suggestion.tsx @@ -127,7 +127,11 @@ export const SlashCommandList = forwardRef< : item.description; return ( -
+ // Height budget clamps to min(design max, viewport-aware + // `--suggestion-available-height` from suggestion-popup.tsx's size + // middleware), falling back to the design max when rendered standalone. + // Single height authority — mirrors MentionList. +
{items.map((item, index) => { const description = describe(item); return ( diff --git a/packages/views/editor/extensions/suggestion-popup.tsx b/packages/views/editor/extensions/suggestion-popup.tsx index 834af16be4..0559e037eb 100644 --- a/packages/views/editor/extensions/suggestion-popup.tsx +++ b/packages/views/editor/extensions/suggestion-popup.tsx @@ -126,7 +126,17 @@ export function createSuggestionPopupRender< getBoundingClientRect: () => clientRect() ?? new DOMRect(), }; computePosition(virtualEl, el, { - placement: "bottom-start", + // Open upward by default. The dominant hosts are bottom-anchored + // composers (chat input, issue comment/reply) whose roomy side is above + // the caret; preferring that side means `size` clamps to a large budget + // with no visible compression, and `flip` only sends it down in the rare + // case the caret sits near the viewport top. A `bottom-start` default + // instead stayed down whenever *any* space existed below — even when far + // more room was above — which read as "mostly opens down and gets + // squashed". Document-body editors (issue/project description, agent + // instructions) also host this popup; there the caret usually has room + // both ways, so `flip` keeps them on-screen regardless of the default. + placement: "top-start", strategy: "fixed", middleware: [ offset(6), @@ -135,7 +145,20 @@ export function createSuggestionPopupRender< size({ padding: 8, apply({ availableHeight }) { - el.style.maxHeight = `${Math.max(120, availableHeight)}px`; + // Publish the viewport-aware height budget as a CSS variable so + // the list component (the real scroll container) can clamp its own + // max-height to `min(designMax, availableHeight)`. Writing + // maxHeight on this wrapper is inert — the wrapper does not clip, + // the inner list scrolls — so it would let a fixed-height list + // overflow past the viewport edge. No floor here: when space is + // tight a short scrollable list beats one clipped off-screen. + el.style.setProperty( + "--suggestion-available-height", + // Guard against a negative budget (caret scrolled just out of + // the boundary): a negative max-height is invalid CSS and would + // drop the clamp entirely, letting the list overflow. + `${Math.max(0, Math.round(availableHeight))}px`, + ); }, }), ],