503 feat: creates autocomplete for tagging feature#537
Conversation
|
I tested with BE running against local Docker. @ triggers the autocomplete dropdown at caret position. Selecting a user closes the dropdown and inserts text.
-Search filter not working: Typing after @ sends the correct query param, but the BE returns all users unfiltered.
|
|
@DarrellRoberts did you see this comment of ivan? |
|
some more inputs
|
ivannissimrch
left a comment
There was a problem hiding this comment.
I tested with BE running against local Docker.
@ triggers the autocomplete dropdown at caret position. Selecting a user closes the dropdown and inserts text.
Missing name fields in BE response: /user returns user account data, but no name fields (firstName, fullName).
tsx renders user.fullName, which is undefined, so the dropdown shows avatars only, and selecting a user inserts @undefined into the comment.
-Search filter not working: Typing after @ sends the correct query param, but the BE returns all users unfiltered.
nadavosa
left a comment
There was a problem hiding this comment.
Thanks for building this — the overlay technique for syntax highlighting is a solid approach. The author flagged it wasn't tested against a running BE, so I'll treat this as a pre-merge review rather than a final one. Several issues need to be addressed before this can ship.
🔴 Blockers
1. Hard-coded user IDs in production code — CommentDisplay.tsx
const { renderHighlightedText } = useCommentTag(content, () => null, null, [4, 6, 67]);[4, 6, 67] are clearly test/debug IDs that must not reach production. This also reveals a deeper design issue: to render tagged users as links in existing comments, the IDs need to come from the comment data itself, not hard-coded. The comment content "@Alice" has no embedded ID — the tag-to-ID mapping needs to be stored separately (e.g. in a taggedUserIds field on the TimedText type or derived from a lookup). This is a feature gap that needs a plan before merging.
2. Wrong logic in useLayoutEffect — Autocomplete.tsx
const el = textAreaRef?.current;
if (!el && !debouncedUserFilter) return; // ← AND should be OR
const textBeforeCaret = debouncedUserFilter?.substring(0, el?.selectionStart); // ← wrong input
const lastAtIndex = textBeforeCaret?.lastIndexOf("@");Two bugs here:
a) !el && !debouncedUserFilter only returns early when both are falsy. If el is null but debouncedUserFilter has a value, execution continues and crashes on el.selectionStart. Should be !el || !debouncedUserFilter.
b) debouncedUserFilter.substring(0, el.selectionStart) takes a substring of the search term (e.g. "john") using the cursor index of the full textarea. These are completely different strings. The caret position calculation is using the wrong input — it should derive position from the full textarea value and the index of the @ character, not from the debounced search term.
3. CSS typo — styles.ts
user-selects: none; // ← invalid CSS propertyShould be user-select: none; (no trailing s). As written, the overlay is mouse-interactive despite visually blocking the textarea, which may cause click-through issues.
🟡 Non-blocking issues
4. Nested AutocompleteRow inside AutocompleteRow — Autocomplete.tsx
<AutocompleteRow key={user.id} onClick={...}>
<AvatarImg ... />
<AutocompleteRow>{user.fullName}</AutocompleteRow> {/* wrong */}
</AutocompleteRow>The inner AutocompleteRow should be a span or a plain div. Nesting the styled row component inside itself produces unexpected layout (two sets of flex + hover + padding).
5. Trailing slash on apiPathUser — constants.ts
export const apiPathUser = `/${apiPrefix}/user/`;All other path constants have no trailing slash (apiPathVolunteer, apiPathOpportunity, etc.). This will produce …/user//me if anyone concatenates it with "me". Remove the trailing slash.
6. API always fires when no @ is typed — Autocomplete.tsx
When newCommentText has no @, userFilter returns "". The query guard is enabled: debouncedUserFilter !== null, so "" passes through and fires an API call for all coordinators on every keystroke before any @ is typed.
Change enabled to:
enabled: debouncedUserFilter !== null && debouncedUserFilter.length > 0,or, even simpler, only render <Autocomplete> when showAutocomplete is true (which is already gated) and don't mount the component at all when hidden, so the query never runs.
7. handleUserSelect is an unnecessary passthrough — Autocomplete.tsx
const handleUserSelect = (userId: number, firstName: string) => {
handleTagAdd(userId, firstName);
};This just calls handleTagAdd directly with no extra logic. Remove it and use onClick={() => handleTagAdd(user.id, user.firstName)}.
8. showAutocomplete effect doesn't track cursor movement — useCommentTag.tsx
const cursorPosition = textAreaRef.current?.selectionStart;selectionStart is read inside a useEffect that only depends on [value]. If the user moves the cursor without changing text (e.g. clicking elsewhere in the textarea), the autocomplete state won't update. Consider also tracking a selectionStart state or listening to onSelect/onClick on the textarea.
9. No keyboard navigation — Autocomplete.tsx
The dropdown is click-only. Standard UX expectation for an autocomplete is to support ArrowDown/ArrowUp to move through results and Enter or Tab to confirm. Consider this a known gap to address in a follow-up if shipping now.
Nits
resolvedAvatarUrlinAutocomplete.tsxis a trivial wrapper — inline it asgetImageUrl(user.avatarUrl || defaultAvatarVolunteerProfile).NewCommentSectionnow has a fixedheight: 200pxwhich may feel rigid. Considermin-heightinstead.AddCommentButtonhasaspect-ratio: 4/1— this makes the button change width based on its height, which can look unexpected at different viewports. The previous approach of fixed padding is more predictable.textarea-caretis independenciesbut@types/textarea-caretis indevDependencies. That's correct as-is.
|
Hey, cool feature! A few things to fix before this can merge: Must fix:
Smaller things:
|
|
thanks @nadavosa and @ivannissimrch for checking! It should work now with the users @nadavosa regarding Keyboard navgiation, I haven't implement that yet as it presents another challenge (the |
|
@nadavosa ignore my comment above. I've now added keyboard navigation but I'll honest I pretty much "vibe-coded" this to save time. From what I tested though, it works fine and I just needed to tweak it a bit. |
nadavosa
left a comment
There was a problem hiding this comment.
Thanks for the updates @DarrellRoberts — several things look much better. Re-checking against the previous blockers:
Fixed ✅
- Hard-coded
[4, 6, 67]user IDs are gone useCommentTagis now a real hook (state, effects, callbacks)- Variable shadowing (
value→match) fixed user-select: noneCSS typo fixed- Keyboard navigation (ArrowUp/Down/Enter/Escape) added
Still needs fixing:
🔴 Blocker: useLayoutEffect caret calculation still wrong — Autocomplete.tsx
const el = textAreaRef?.current;
if (!el && !userFilter) return; // ← still &&, should be ||
const textBeforeCaret = userFilter?.substring(0, el?.selectionStart); // ← still wrong inputTwo issues persist:
a) !el && !userFilter — if el is null but userFilter has a value, this doesn't return early. The second if (!el) return below saves you from a crash, but textBeforeCaret is set to a nonsensical value first. Change to !el || !userFilter.
b) userFilter.substring(0, el.selectionStart) — userFilter is the debounced search term (e.g. "joh"), not the full textarea content. Taking a substring of "joh" up to the cursor position of the full textarea makes no sense. The @ character will never be found in textBeforeCaret, so lastAtIndex is always -1, and the dropdown always uses el.selectionStart as the position index instead of the actual @ position. The dropdown appears in the wrong spot when the @ is not at position 0.
Fix: use the full textarea value instead:
const el = textAreaRef?.current;
if (!el || !userFilter) return;
const textBeforeCaret = el.value.substring(0, el.selectionStart);
const lastAtIndex = textBeforeCaret.lastIndexOf("@");🔴 Blocker: useCommentTag fires a user-list API call for every rendered comment — useCommentTag.tsx
const { data: users } = useGetQuery<ApiUserGet[]>({
queryKey: ["users"],
apiPath: apiPathUser,
params: { sortOrder: SortOrder.NewToOld },
staleTime: cacheTTL,
// no `enabled` guard
});CommentDisplay calls useCommentTag(content) — one call per existing comment on the page. Each call mounts this hook, and since there's no enabled guard, each fires the /user/ query. With 10 comments that's 10 concurrent API calls on page load (deduplicated by React Query's cache, but still runs once per key mount).
Fix: add enabled: false when not in edit mode (i.e. when setNewCommentText is not provided):
const { data: users } = useGetQuery<ApiUserGet[]>({
...
enabled: !!setNewCommentText, // only fetch when in comment-input mode
});🟡 Non-blocking: apiPathUser trailing slash — constants.ts
export const apiPathUser = `/${apiPrefix}/user/`; // ← trailing slash
export const apiPathMe = `/${apiPrefix}/user/me`; // ← no trailing slash on siblingsInconsistent with all other path constants. If apiPathMe is ever derived from apiPathUser + "me", this produces …/user//me. Remove the trailing slash.
🟡 Non-blocking: fixed height: 200px on NewCommentSection — styles.ts
A hard fixed height clips long comments and the shadow scroll approach means users can't tell there's more content. Use min-height: 200px; max-height: 300px; overflow-y: auto and remove the overflow-y: hidden to give it room to breathe.
🟡 Non-blocking: no focus indicator — styles.ts
TextArea has outline: none and border: none, and NewCommentSection has no :focus-within style. Keyboard users get no visual feedback that the field is active. Add:
export const NewCommentSection = styled.div`
...
&:focus-within {
border-color: var(--color-midnight);
}
`;Nits
handleUserSelectinAutocomplete.tsxis still a passthrough — useonClick={() => handleTagAdd(user.id, user.firstName)}directlyresolvedAvatarUrlhelper can be inlined asgetImageUrl(user.avatarUrl || defaultAvatarVolunteerProfile)
nadavosa
left a comment
There was a problem hiding this comment.
Two blockers remain (useLayoutEffect caret calc + API call on every rendered comment). Details in the comment above.
|
thanks @nadavosa !
if (!el || userFilter === null) return;Fair point though on the
So these blockers should now be fixed. |
…org/fe into darrell/feature/auto-complete

Description
Triggers autocomplete menu for when user triggers a user tag
@PLEASE NOTE: my
bewasn't working when I implemented this, so it's quite likely that there are bugs. Please test with a runningbethat can fetch from/userRelated Issues
Closes #503
Changes
textarea-caretlibrary (neccessary for position of autocomplete). Link to libraryuseDebouncehook to/hooks<Autocomplete>component along with logicuseCommenthookScreenshots / Demos
Checklist