63 lines
1.8 KiB
TypeScript
63 lines
1.8 KiB
TypeScript
import { type CSSProperties, useLayoutEffect, useRef } from "react";
|
|
import { Avatar } from "../ui/Avatar.tsx";
|
|
import type { UserResult } from "../../hooks/useMentionAutocomplete.ts";
|
|
|
|
interface MentionDropdownProps {
|
|
/** For the textarea's `aria-controls` / `aria-activedescendant`. */
|
|
id: string;
|
|
results: UserResult[];
|
|
selectedIndex: number;
|
|
onSelect: (username: string) => void;
|
|
/** Fixed-position placement from `useAnchoredFloat`; see TextEditor. */
|
|
style?: CSSProperties;
|
|
}
|
|
|
|
export function MentionDropdown(
|
|
{ id, results, selectedIndex, onSelect, style }: MentionDropdownProps,
|
|
) {
|
|
const listRef = useRef<HTMLUListElement>(null);
|
|
|
|
// Arrowing past the visible rows keeps the selection in view.
|
|
useLayoutEffect(() => {
|
|
listRef.current
|
|
?.querySelector<HTMLElement>("[aria-selected='true']")
|
|
?.scrollIntoView({ block: "nearest" });
|
|
}, [selectedIndex, results]);
|
|
|
|
if (results.length === 0) return null;
|
|
return (
|
|
<ul
|
|
ref={listRef}
|
|
id={id}
|
|
role="listbox"
|
|
className="mention-dropdown"
|
|
style={style}
|
|
>
|
|
{results.map((user, i) => (
|
|
<li
|
|
key={user.id}
|
|
// TextEditor points aria-activedescendant at `${id}-${index}`.
|
|
id={`${id}-${i}`}
|
|
role="option"
|
|
aria-selected={i === selectedIndex}
|
|
className={`mention-dropdown-item${
|
|
i === selectedIndex ? " mention-dropdown-item--selected" : ""
|
|
}`}
|
|
onMouseDown={(e) => {
|
|
e.preventDefault(); // keep textarea focused
|
|
onSelect(user.username);
|
|
}}
|
|
>
|
|
<Avatar
|
|
userId={user.id}
|
|
username={user.username}
|
|
hasAvatar={!!user.avatarMime}
|
|
size={20}
|
|
/>
|
|
<span className="mention-dropdown-username">@{user.username}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
}
|