diff --git a/components/selection-toolbar/compose-post.tsx b/components/selection-toolbar/compose-post.tsx
index d6084c43..55c4d03d 100644
--- a/components/selection-toolbar/compose-post.tsx
+++ b/components/selection-toolbar/compose-post.tsx
@@ -318,7 +318,7 @@ export function ComposePost({
Share to feed
-
+
Share to feed
@@ -344,7 +344,7 @@ export function ComposePost({
-
{body}
+
{body}
>,
document.body,
diff --git a/components/social/mention-autocomplete.tsx b/components/social/mention-autocomplete.tsx
index 5e42d451..641852ff 100644
--- a/components/social/mention-autocomplete.tsx
+++ b/components/social/mention-autocomplete.tsx
@@ -1,16 +1,24 @@
"use client";
/**
- * MentionAutocomplete — a search input with a suggestions popover for tagging
- * users. Suggestions come from the backend prioritised (following → followers
- * → others), 20 per page, and the popover infinite-scrolls (≈5 rows visible).
- * Already-selected users are filtered out. Calls onSelect with the chosen
- * user; the caller manages the selected set.
+ * MentionAutocomplete — a search field for tagging users. Results render
+ * INLINE (in normal flow) directly below the input, not as a floating
+ * popover. This is deliberate: a floating popover inside a drawer/modal
+ * overflows the sheet and its outside-click listener fights the drawer's own
+ * dismiss. Inline results:
+ * • never resize/overflow the drawer (the container scrolls them),
+ * • close on blur (no document listener → outside clicks don't close the
+ * drawer),
+ * • only appear once the user types (no suggestions on bare focus).
+ *
+ * Suggestions come from the backend prioritised (following → followers →
+ * others), 20 per page, infinite-scrolling. Already-selected users are
+ * filtered out. Selecting a user adds it (via onSelect) and closes the list.
*/
import { useEffect, useRef, useState } from "react";
import Image from "next/image";
-import { Loader2, Search } from "lucide-react";
+import { Loader2 } from "lucide-react";
import { posts, type UserSummary } from "@/lib/social";
const PAGE = 20;
@@ -30,22 +38,25 @@ export function MentionAutocomplete({
const [open, setOpen] = useState(false);
const [items, setItems] = useState
([]);
const [loading, setLoading] = useState(false);
- const [hasMore, setHasMore] = useState(true);
+ const [hasMore, setHasMore] = useState(false);
const reqRef = useRef(0);
const busyRef = useRef(false);
- const rootRef = useRef(null);
+ const blurTimer = useRef(null);
- // (Re)load page 0 on open / query change (debounced — timer callback, so
- // setState isn't synchronous-in-effect).
+ const query = q.trim();
+ const showResults = open && query !== "";
+
+ // (Re)load page 0 when the query changes — debounced, only when non-empty.
+ // No fetch on bare focus, so suggestions appear "while typing" only.
useEffect(() => {
- if (!open) return;
+ if (query === "") return;
const t = window.setTimeout(() => {
const token = ++reqRef.current;
busyRef.current = true;
setLoading(true);
posts
- .mentionSuggest(q, { limit: PAGE, offset: 0 })
+ .mentionSuggest(query, { limit: PAGE, offset: 0 })
.then((res) => {
if (reqRef.current !== token) return;
setItems(res);
@@ -65,7 +76,7 @@ export function MentionAutocomplete({
});
}, 200);
return () => window.clearTimeout(t);
- }, [q, open]);
+ }, [query]);
function loadMore() {
if (busyRef.current || !hasMore) return;
@@ -73,7 +84,7 @@ export function MentionAutocomplete({
busyRef.current = true;
setLoading(true);
posts
- .mentionSuggest(q, { limit: PAGE, offset: items.length })
+ .mentionSuggest(query, { limit: PAGE, offset: items.length })
.then((res) => {
if (reqRef.current !== token) return;
setItems((prev) => [...prev, ...res]);
@@ -88,50 +99,33 @@ export function MentionAutocomplete({
});
}
- // Close on outside click / Escape.
- useEffect(() => {
- if (!open) return;
- function onDown(e: PointerEvent) {
- if (rootRef.current && !rootRef.current.contains(e.target as Node)) {
- setOpen(false);
- }
- }
- function onKey(e: KeyboardEvent) {
- if (e.key === "Escape") setOpen(false);
- }
- document.addEventListener("pointerdown", onDown);
- document.addEventListener("keydown", onKey);
- return () => {
- document.removeEventListener("pointerdown", onDown);
- document.removeEventListener("keydown", onKey);
- };
- }, [open]);
-
const excluded = new Set(excludeIds);
const visible = items.filter((u) => !u.id || !excluded.has(u.id));
return (
-
-
-
- setOpen(true)}
- onChange={(e) => {
- setQ(e.target.value);
- setOpen(true);
- }}
- placeholder={placeholder}
- autoComplete="off"
- // text-base (16px) on mobile prevents iOS Safari from auto-zooming
- // on focus (it zooms any input < 16px); compact 14px on desktop.
- className="w-full rounded-lg border border-border bg-background py-2 pl-9 pr-3 text-base sm:text-sm text-foreground outline-none placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring"
- />
-
+
+
setOpen(true)}
+ onChange={(e) => {
+ setQ(e.target.value);
+ setOpen(true);
+ }}
+ onBlur={() => {
+ // Defer so a result tap (which fires onMouseDown→onClick) isn't
+ // pre-empted by the list closing. Plain blur (tapping elsewhere)
+ // just closes the inline list — the drawer is unaffected.
+ blurTimer.current = window.setTimeout(() => setOpen(false), 120);
+ }}
+ placeholder={placeholder}
+ autoComplete="off"
+ // 16px on mobile so iOS Safari doesn't auto-zoom on focus.
+ className="w-full rounded-lg border border-border bg-background px-3 py-2 text-base sm:text-sm text-foreground outline-none placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring"
+ />
- {open ? (
+ {showResults ? (
{
const el = e.currentTarget;
@@ -139,21 +133,27 @@ export function MentionAutocomplete({
loadMore();
}
}}
- className="absolute z-50 mt-1 max-h-60 w-full overflow-y-auto overscroll-contain rounded-lg border border-border bg-card shadow-lg"
+ className="mt-2 max-h-56 overflow-y-auto overscroll-contain rounded-lg border border-border bg-card"
>
{visible.length === 0 && !loading ? (
- {q.trim() ? "No people found." : "No suggestions."}
+ No people found.
) : (
visible.map((u) => (