fix: render mention autocomplete via portal to prevent overflow clipping
The mention suggestion dropdown was getting clipped when typing at the end of a long description inside modals/dialogs because parent containers had overflow-y-auto. Render it via createPortal to document.body with fixed positioning and z-index 9999 so it always appears above all UI. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
c6364149b1
commit
0fd75aa579
1 changed files with 48 additions and 40 deletions
|
|
@ -8,6 +8,7 @@ import {
|
||||||
useState,
|
useState,
|
||||||
type DragEvent,
|
type DragEvent,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
import {
|
import {
|
||||||
CodeMirrorEditor,
|
CodeMirrorEditor,
|
||||||
MDXEditor,
|
MDXEditor,
|
||||||
|
|
@ -82,6 +83,9 @@ interface MentionState {
|
||||||
query: string;
|
query: string;
|
||||||
top: number;
|
top: number;
|
||||||
left: number;
|
left: number;
|
||||||
|
/** Viewport-relative coords for portal positioning */
|
||||||
|
viewportTop: number;
|
||||||
|
viewportLeft: number;
|
||||||
textNode: Text;
|
textNode: Text;
|
||||||
atPos: number;
|
atPos: number;
|
||||||
endPos: number;
|
endPos: number;
|
||||||
|
|
@ -155,6 +159,8 @@ function detectMention(container: HTMLElement): MentionState | null {
|
||||||
query,
|
query,
|
||||||
top: rect.bottom - containerRect.top,
|
top: rect.bottom - containerRect.top,
|
||||||
left: rect.left - containerRect.left,
|
left: rect.left - containerRect.left,
|
||||||
|
viewportTop: rect.bottom,
|
||||||
|
viewportLeft: rect.left,
|
||||||
textNode: textNode as Text,
|
textNode: textNode as Text,
|
||||||
atPos,
|
atPos,
|
||||||
endPos: offset,
|
endPos: offset,
|
||||||
|
|
@ -554,46 +560,48 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
||||||
plugins={plugins}
|
plugins={plugins}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Mention dropdown */}
|
{/* Mention dropdown — rendered via portal so it isn't clipped by overflow containers */}
|
||||||
{mentionActive && filteredMentions.length > 0 && (
|
{mentionActive && filteredMentions.length > 0 &&
|
||||||
<div
|
createPortal(
|
||||||
className="absolute z-50 min-w-[180px] max-h-[200px] overflow-y-auto rounded-md border border-border bg-popover shadow-md"
|
<div
|
||||||
style={{ top: mentionState.top + 4, left: mentionState.left }}
|
className="fixed z-[9999] min-w-[180px] max-h-[200px] overflow-y-auto rounded-md border border-border bg-popover shadow-md"
|
||||||
>
|
style={{ top: mentionState.viewportTop + 4, left: mentionState.viewportLeft }}
|
||||||
{filteredMentions.map((option, i) => (
|
>
|
||||||
<button
|
{filteredMentions.map((option, i) => (
|
||||||
key={option.id}
|
<button
|
||||||
className={cn(
|
key={option.id}
|
||||||
"flex items-center gap-2 w-full px-3 py-1.5 text-sm text-left hover:bg-accent/50 transition-colors",
|
className={cn(
|
||||||
i === mentionIndex && "bg-accent",
|
"flex items-center gap-2 w-full px-3 py-1.5 text-sm text-left hover:bg-accent/50 transition-colors",
|
||||||
)}
|
i === mentionIndex && "bg-accent",
|
||||||
onMouseDown={(e) => {
|
)}
|
||||||
e.preventDefault(); // prevent blur
|
onMouseDown={(e) => {
|
||||||
selectMention(option);
|
e.preventDefault(); // prevent blur
|
||||||
}}
|
selectMention(option);
|
||||||
onMouseEnter={() => setMentionIndex(i)}
|
}}
|
||||||
>
|
onMouseEnter={() => setMentionIndex(i)}
|
||||||
{option.kind === "project" && option.projectId ? (
|
>
|
||||||
<span
|
{option.kind === "project" && option.projectId ? (
|
||||||
className="inline-flex h-2 w-2 rounded-full border border-border/50"
|
<span
|
||||||
style={{ backgroundColor: option.projectColor ?? "#64748b" }}
|
className="inline-flex h-2 w-2 rounded-full border border-border/50"
|
||||||
/>
|
style={{ backgroundColor: option.projectColor ?? "#64748b" }}
|
||||||
) : (
|
/>
|
||||||
<AgentIcon
|
) : (
|
||||||
icon={option.agentIcon}
|
<AgentIcon
|
||||||
className="h-3.5 w-3.5 shrink-0 text-muted-foreground"
|
icon={option.agentIcon}
|
||||||
/>
|
className="h-3.5 w-3.5 shrink-0 text-muted-foreground"
|
||||||
)}
|
/>
|
||||||
<span>{option.name}</span>
|
)}
|
||||||
{option.kind === "project" && option.projectId && (
|
<span>{option.name}</span>
|
||||||
<span className="ml-auto text-[10px] uppercase tracking-wide text-muted-foreground">
|
{option.kind === "project" && option.projectId && (
|
||||||
Project
|
<span className="ml-auto text-[10px] uppercase tracking-wide text-muted-foreground">
|
||||||
</span>
|
Project
|
||||||
)}
|
</span>
|
||||||
</button>
|
)}
|
||||||
))}
|
</button>
|
||||||
</div>
|
))}
|
||||||
)}
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
|
||||||
{isDragOver && canDropImage && (
|
{isDragOver && canDropImage && (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue