mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-28 05:46:58 +02:00
fix(editor): open mention/slash popup upward and clamp height to viewport (#5376)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -352,9 +352,15 @@ export const MentionList = forwardRef<MentionListRef, MentionListProps>(
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col overflow-y-auto overscroll-contain border bg-popover py-1",
|
||||
// Height budget: clamp to whichever is smaller — the design max or the
|
||||
// viewport-aware `--suggestion-available-height` published by the
|
||||
// floating-ui `size` middleware (suggestion-popup.tsx). The var falls
|
||||
// back to the design max when the popup renders outside that
|
||||
// controller. This is the single height authority; do not add a second
|
||||
// fixed max-height above it or the list can overflow the viewport.
|
||||
contextLayout
|
||||
? "max-h-[420px] w-96 rounded-lg shadow-xl"
|
||||
: "max-h-[300px] w-72 rounded-md shadow-md",
|
||||
? "max-h-[min(420px,var(--suggestion-available-height,420px))] w-96 rounded-lg shadow-xl"
|
||||
: "max-h-[min(300px,var(--suggestion-available-height,300px))] w-72 rounded-md shadow-md",
|
||||
)}
|
||||
>
|
||||
{groups.map((group) => (
|
||||
|
||||
@@ -127,7 +127,11 @@ export const SlashCommandList = forwardRef<
|
||||
: item.description;
|
||||
|
||||
return (
|
||||
<div className="rounded-md border bg-popover py-1 shadow-md w-72 max-h-[300px] overflow-y-auto">
|
||||
// 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.
|
||||
<div className="rounded-md border bg-popover py-1 shadow-md w-72 max-h-[min(300px,var(--suggestion-available-height,300px))] overflow-y-auto">
|
||||
{items.map((item, index) => {
|
||||
const description = describe(item);
|
||||
return (
|
||||
|
||||
@@ -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`,
|
||||
);
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user