mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(home): anchor @-mention popup at caret and right-size dropdown widths (#4393)
* improvement(home): anchor @-mention popup at caret and right-size dropdown widths * fix(home): align caret-anchor marker to text-top so mention popup clears glyph * fix(home): enable collision avoidance for @-mention popup so it fits in narrow chats
This commit is contained in:
+5
-4
@@ -245,13 +245,14 @@ export const PlusMenuDropdown = React.memo(
|
||||
align='start'
|
||||
side='top'
|
||||
sideOffset={8}
|
||||
avoidCollisions={!isMention}
|
||||
avoidCollisions
|
||||
collisionPadding={8}
|
||||
className={cn(
|
||||
'flex flex-col overflow-hidden',
|
||||
// Plus-click shows short fixed labels (Workflows, Tables, …) — let it size
|
||||
// to its content via the emcn DropdownMenuContent default max-w.
|
||||
// Mention mode renders resource names directly, so widen for breathing room.
|
||||
isMention && 'w-[300px] max-w-[calc(100vw-32px)]'
|
||||
isMention && 'max-w-[min(300px,calc(100vw-32px))]'
|
||||
)}
|
||||
onCloseAutoFocus={handleCloseAutoFocus}
|
||||
onOpenAutoFocus={handleOpenAutoFocus}
|
||||
@@ -286,7 +287,7 @@ export const PlusMenuDropdown = React.memo(
|
||||
/>
|
||||
<span>Workflows</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className='w-[300px] max-w-[calc(100vw-32px)]'>
|
||||
<DropdownMenuSubContent className='max-w-[min(300px,calc(100vw-32px))]'>
|
||||
<WorkflowFolderTreeItems nodes={workflowTree} onSelect={handleSelect} />
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
@@ -303,7 +304,7 @@ export const PlusMenuDropdown = React.memo(
|
||||
<Icon className='h-[14px] w-[14px]' />
|
||||
<span>{config.label}</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className='w-[300px] max-w-[calc(100vw-32px)]'>
|
||||
<DropdownMenuSubContent className='max-w-[min(300px,calc(100vw-32px))]'>
|
||||
{items.map((item) => (
|
||||
<DropdownMenuItem
|
||||
key={item.id}
|
||||
|
||||
@@ -58,6 +58,51 @@ import type { ChatContext } from '@/stores/panel'
|
||||
|
||||
export type { FileAttachmentForApi } from '@/app/workspace/[workspaceId]/home/types'
|
||||
|
||||
function getCaretAnchor(
|
||||
textarea: HTMLTextAreaElement,
|
||||
caretPos: number
|
||||
): { left: number; top: number } {
|
||||
const textareaRect = textarea.getBoundingClientRect()
|
||||
const style = window.getComputedStyle(textarea)
|
||||
|
||||
const mirror = document.createElement('div')
|
||||
mirror.style.position = 'absolute'
|
||||
mirror.style.top = '0'
|
||||
mirror.style.left = '0'
|
||||
mirror.style.visibility = 'hidden'
|
||||
mirror.style.whiteSpace = 'pre-wrap'
|
||||
mirror.style.overflowWrap = 'break-word'
|
||||
mirror.style.font = style.font
|
||||
mirror.style.padding = style.padding
|
||||
mirror.style.border = style.border
|
||||
mirror.style.width = style.width
|
||||
mirror.style.lineHeight = style.lineHeight
|
||||
mirror.style.boxSizing = style.boxSizing
|
||||
mirror.style.letterSpacing = style.letterSpacing
|
||||
mirror.style.textTransform = style.textTransform
|
||||
mirror.style.textIndent = style.textIndent
|
||||
mirror.style.textAlign = style.textAlign
|
||||
mirror.textContent = textarea.value.substring(0, caretPos)
|
||||
|
||||
const marker = document.createElement('span')
|
||||
marker.style.display = 'inline-block'
|
||||
marker.style.width = '0px'
|
||||
marker.style.padding = '0'
|
||||
marker.style.border = '0'
|
||||
marker.style.verticalAlign = 'text-top'
|
||||
mirror.appendChild(marker)
|
||||
|
||||
document.body.appendChild(mirror)
|
||||
const markerRect = marker.getBoundingClientRect()
|
||||
const mirrorRect = mirror.getBoundingClientRect()
|
||||
document.body.removeChild(mirror)
|
||||
|
||||
return {
|
||||
left: textareaRect.left + (markerRect.left - mirrorRect.left) - textarea.scrollLeft,
|
||||
top: textareaRect.top + (markerRect.top - mirrorRect.top) - textarea.scrollTop,
|
||||
}
|
||||
}
|
||||
|
||||
interface UserInputProps {
|
||||
defaultValue?: string
|
||||
draftScopeKey?: string
|
||||
@@ -304,7 +349,6 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
|
||||
const pendingCursorRef = useRef<number | null>(null)
|
||||
const mentionRangeRef = useRef<{ start: number; end: number } | null>(null)
|
||||
const [mentionQuery, setMentionQuery] = useState<string | null>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
@@ -664,7 +708,7 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
|
||||
getActiveMentionAtRef.current = mentionMenu.getActiveMentionQueryAtPosition
|
||||
|
||||
const syncMentionState = useCallback(
|
||||
(_textarea: HTMLTextAreaElement, text: string, caret: number) => {
|
||||
(textarea: HTMLTextAreaElement, text: string, caret: number) => {
|
||||
const active = getActiveMentionAtRef.current(caret, text)
|
||||
// Treat any whitespace inside the query as a closer — typing a space
|
||||
// after `@foo` should leave the raw `@foo` text and dismiss the menu.
|
||||
@@ -682,10 +726,8 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
|
||||
mentionRangeRef.current = { start: active.start, end: active.end }
|
||||
setMentionQuery(active.query)
|
||||
if (!wasActive) {
|
||||
// Anchor above the whole input box (not at the caret) so the menu can never
|
||||
// overlap the user's typing.
|
||||
const rect = containerRef.current?.getBoundingClientRect()
|
||||
const anchor = rect ? { left: rect.left, top: rect.top } : { left: 0, top: 0 }
|
||||
// Anchor at the caret so the menu floats above the user's cursor.
|
||||
const anchor = getCaretAnchor(textarea, active.start)
|
||||
plusMenuRef.current?.open(anchor, { mention: true })
|
||||
}
|
||||
},
|
||||
@@ -834,7 +876,6 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
onClick={handleContainerClick}
|
||||
className={cn(
|
||||
'relative z-10 mx-auto w-full max-w-[42rem] cursor-text rounded-[20px] border border-[var(--border-1)] bg-[var(--white)] px-2.5 py-2 dark:bg-[var(--surface-4)]',
|
||||
|
||||
Reference in New Issue
Block a user