Release composer: replace the tag datalist with a proper recent-tags dropdown #254

Closed
opened 2026-09-09 20:28:00 +00:00 by crueber · 3 comments
Owner

What's requested

In the "new release" interface (/:owner/:name/releases/new), the tag field should offer a real dropdown of recent tags rather than relying on the browser's datalist behavior.

Current state (code evidence)

  • web/src/pages/ReleaseNew.jsx:100-118: the tag input uses a native <datalist id="release-tags"> fed by tagNames() (from ctx.repoClient.tags({n:100}), line 16). The data is already there and sorted by the tags stream.
  • Native datalists are inconsistent: Chrome shows a dropdown only after typing/click in some cases, Safari/Firefox render differently, styling can't be controlled, and on many browsers the suggestions only appear on focus — users don't discover the list without typing first. The repo already has a consistent popover pattern built for exactly this (RefPicker in web/src/pages/Repo.jsx:216+ — button trigger, streamed list, keyboard/Esc handling, outside-click close).

Proposed design

  • Replace the datalist with a small combobox: the existing input#release-tag stays the source of truth (keep typing to filter), plus a dropdown trigger/button showing recent tags (most recent first — the tags stream order) as a styled popover.
  • Reuse the existing popover conventions rather than inventing: RefPicker's trigger/list/keyboard pattern, or the lighter <details> approach the clone menu uses. Filter options as the user types (client-side on tagNames() — no new fetches; useData cache key tags:{full} already single-flights).
  • Keep the existing help text semantics ("N tags available", "No tags yet — push one with git first…") — the empty state should still render (and will change meaning once #253's server-side tag creation lands; leave the text connected to reality at implementation time).
  • Accessibility: combobox with aria-expanded, listbox semantics, arrow-key navigation, Enter selects, Esc closes — mirror RefPicker's keyboard handling.

Acceptance criteria

  • The tag field on /releases/new shows recent tags in a styled dropdown (recent first), consistent with the app's popover styling in light/dark themes.
  • Typing filters the list; selecting fills the input; keyboard navigation (arrows/Enter/Esc) works.
  • Behavior is consistent across browsers (no reliance on native datalist rendering).
  • Empty repo / no tags still shows the current empty-state guidance.
  • No new API calls: rides the existing tags:{full} cache entry.
## What's requested In the "new release" interface (`/:owner/:name/releases/new`), the tag field should offer a real dropdown of recent tags rather than relying on the browser's datalist behavior. ## Current state (code evidence) - `web/src/pages/ReleaseNew.jsx:100-118`: the tag input uses a native `<datalist id="release-tags">` fed by `tagNames()` (from `ctx.repoClient.tags({n:100})`, line 16). The data is already there and sorted by the tags stream. - Native datalists are inconsistent: Chrome shows a dropdown only after typing/click in some cases, Safari/Firefox render differently, styling can't be controlled, and on many browsers the suggestions only appear on focus — users don't discover the list without typing first. The repo already has a consistent popover pattern built for exactly this (`RefPicker` in `web/src/pages/Repo.jsx:216+` — button trigger, streamed list, keyboard/Esc handling, outside-click close). ## Proposed design - Replace the datalist with a small combobox: the existing `input#release-tag` stays the source of truth (keep typing to filter), plus a dropdown trigger/button showing recent tags (most recent first — the tags stream order) as a styled popover. - Reuse the existing popover conventions rather than inventing: `RefPicker`'s trigger/list/keyboard pattern, or the lighter `<details>` approach the clone menu uses. Filter options as the user types (client-side on `tagNames()` — no new fetches; `useData` cache key `tags:{full}` already single-flights). - Keep the existing help text semantics ("N tags available", "No tags yet — push one with git first…") — the empty state should still render (and will change meaning once #253's server-side tag creation lands; leave the text connected to reality at implementation time). - Accessibility: combobox with `aria-expanded`, listbox semantics, arrow-key navigation, Enter selects, Esc closes — mirror `RefPicker`'s keyboard handling. ## Acceptance criteria - [ ] The tag field on `/releases/new` shows recent tags in a styled dropdown (recent first), consistent with the app's popover styling in light/dark themes. - [ ] Typing filters the list; selecting fills the input; keyboard navigation (arrows/Enter/Esc) works. - [ ] Behavior is consistent across browsers (no reliance on native datalist rendering). - [ ] Empty repo / no tags still shows the current empty-state guidance. - [ ] No new API calls: rides the existing `tags:{full}` cache entry.
Author
Owner

Fixed by PR #264 (branch fix/issue-254): tag datalist → styled combobox. #264

Fixed by PR #264 (branch fix/issue-254): tag datalist → styled combobox. https://git.packden.us/crueber/walhub/pulls/264
Author
Owner

Review of PR #264 (fix/issue-254, tag dropdown combobox) — verified in scratch worktree at b4aa966; no browser per instructions (node tests + reasoning only).

INPUT SOURCE OF TRUTH — PASS. web/src/pages/ReleaseNew.jsx:176 value={getTag()} stays bound; :178 trims as before; chooseTag (:92-96) sets input + closes + refocuses. Free-typed non-listed tag submittable as before: submit enabled on any non-empty tag (:331) and create (:49-54) has no list-membership check; title placeholder still defaults to typed tag (:270).

ORDER — PASS. Dropdown renders filteredTags() (:217) over tagNames() stream order (:73-74, tags({n:100}) call :17 unchanged); filterTagNames (web/src/lib/releases.js:29-34) is filter-only, never re-sorts. Tests assert passthrough order (releases-panel.test.js:50-57).

FILTER — PASS. Case-insensitive substring, blank/whitespace/undefined→all, non-array→[] (:29-34); covered by tests :59-71.

KEYBOARD — PASS. :112-135 arrows preventDefault, open-if-closed at index 0, wrap via modulo; Enter selects+refocuses when active valid, else just closes (input already focused — correct, lets native submit proceed); Esc via document handler :100-105 closes + refocuses input (works from input, trigger, or option).

OUTSIDE-CLICK — PASS. :97-99 contains-check + :106-111 listeners with onCleanup removal (LabelPicker/MilestonePicker idiom as commented :81-82). Trigger lives inside comboRoot so toggle isn't swallowed.

ARIA — PASS. Input role=combobox :168 with aria-expanded/controls=release-tag-list/autocomplete=activedescendant (:169-174, omitted when inactive); list role=listbox :212 with matching id + label; rows role=option :227 with indexed ids :228 matching activedescendant and aria-selected :229; trigger aria-haspopup=listbox :193.

EMPTY STATE — PASS, truthful post-#253. Filtered-empty :219-221 and no-tags help :251 both say 'create it from a commit' — releases still can't create tags, commit view can. No-tags case keeps popover shut (:209) with trigger disabled (:198), so guidance line is the single channel.

OPAQUE PANEL + SCROLL — PASS. .tag-drop added to the shared solid-panel rule (web/src/ui.css:133, ordered after .card); popover carries scroll-slim + max-h-72 overflow-y-auto (:214).

FETCHES/DEPS — PASS. Same tags:{full} cache key (:17), no new useData; no package.json/lock change (verified empty diff).

DOCS — PASS. docs/go/12_web_ui.md: #49 entry notes supersede by #254, new #254 entry matches implementation (combobox, tags:{full}, keyboard, truthful empty state, no backend/SDK/deps, headless cover, browser-proof-open note). AGENTS laws 1/7/8/12 hold (no new deps, listeners cleaned, no seam changes, doc updated in same change).

TESTS (scratch worktree, node_modules symlinked read-only from main checkout — no main files touched): releases-panel.test.js 8/8 pass; all other unit files 518/518 pass (192+154+172 across three batches); smoke.test.js 3/3 assertions pass with the known file-level event-loop hang — file untouched by this PR so pre-existing as the PR description claims. vite build green (139 modules, 1.87s), esbuild SDK bundle green.

No fixes pushed — nothing to fix. MERGE RECOMMENDATION: ready to merge.

Review of PR #264 (fix/issue-254, tag dropdown combobox) — verified in scratch worktree at b4aa966; no browser per instructions (node tests + reasoning only). INPUT SOURCE OF TRUTH — PASS. web/src/pages/ReleaseNew.jsx:176 value={getTag()} stays bound; :178 trims as before; chooseTag (:92-96) sets input + closes + refocuses. Free-typed non-listed tag submittable as before: submit enabled on any non-empty tag (:331) and create (:49-54) has no list-membership check; title placeholder still defaults to typed tag (:270). ORDER — PASS. Dropdown renders filteredTags() (:217) over tagNames() stream order (:73-74, tags({n:100}) call :17 unchanged); filterTagNames (web/src/lib/releases.js:29-34) is filter-only, never re-sorts. Tests assert passthrough order (releases-panel.test.js:50-57). FILTER — PASS. Case-insensitive substring, blank/whitespace/undefined→all, non-array→[] (:29-34); covered by tests :59-71. KEYBOARD — PASS. :112-135 arrows preventDefault, open-if-closed at index 0, wrap via modulo; Enter selects+refocuses when active valid, else just closes (input already focused — correct, lets native submit proceed); Esc via document handler :100-105 closes + refocuses input (works from input, trigger, or option). OUTSIDE-CLICK — PASS. :97-99 contains-check + :106-111 listeners with onCleanup removal (LabelPicker/MilestonePicker idiom as commented :81-82). Trigger lives inside comboRoot so toggle isn't swallowed. ARIA — PASS. Input role=combobox :168 with aria-expanded/controls=release-tag-list/autocomplete=activedescendant (:169-174, omitted when inactive); list role=listbox :212 with matching id + label; rows role=option :227 with indexed ids :228 matching activedescendant and aria-selected :229; trigger aria-haspopup=listbox :193. EMPTY STATE — PASS, truthful post-#253. Filtered-empty :219-221 and no-tags help :251 both say 'create it from a commit' — releases still can't create tags, commit view can. No-tags case keeps popover shut (:209) with trigger disabled (:198), so guidance line is the single channel. OPAQUE PANEL + SCROLL — PASS. .tag-drop added to the shared solid-panel rule (web/src/ui.css:133, ordered after .card); popover carries scroll-slim + max-h-72 overflow-y-auto (:214). FETCHES/DEPS — PASS. Same tags:{full} cache key (:17), no new useData; no package.json/lock change (verified empty diff). DOCS — PASS. docs/go/12_web_ui.md: #49 entry notes supersede by #254, new #254 entry matches implementation (combobox, tags:{full}, keyboard, truthful empty state, no backend/SDK/deps, headless cover, browser-proof-open note). AGENTS laws 1/7/8/12 hold (no new deps, listeners cleaned, no seam changes, doc updated in same change). TESTS (scratch worktree, node_modules symlinked read-only from main checkout — no main files touched): releases-panel.test.js 8/8 pass; all other unit files 518/518 pass (192+154+172 across three batches); smoke.test.js 3/3 assertions pass with the known file-level event-loop hang — file untouched by this PR so pre-existing as the PR description claims. vite build green (139 modules, 1.87s), esbuild SDK bundle green. No fixes pushed — nothing to fix. MERGE RECOMMENDATION: ready to merge.
Author
Owner

Fixed by PR #264 (review clean; 518/518 node tests), merged. Closing.

Fixed by PR #264 (review clean; 518/518 node tests), merged. Closing.
crueber added this to the v1 milestone 2026-09-10 22:20:52 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
crueber/walhub#254
No description provided.