Search field, combobox (single and multi) and file input components #625

Closed
opened 2026-10-27 15:07:36 +01:00 by simon · 0 comments
Owner

Motivation

The member search (magnifier, clear button, live filter, URL sync) and the group's “Add members” picker (chips, no icon, no clear, list vanishes without results) share nothing; Escape behaves differently in each. There are three combobox implementations (two copies), with options wrongly in the Tab order, no “No results”, four placeholder wordings, and in the user form a page-wide key listener with a database read per change (fixed minimally in U1). The global-settings “Add field” list is semantically a menu button. The CSV upload has no proper component. DESIGN_GUIDELINES §19 and §18.3 define the target.

Visible improvement

Searching and picking feel the same everywhere: one look, Escape and “×” clear, a clear “No results” message, full keyboard support.

Scope (ordered steps, just ci-dev green after each)

  1. <.search_input>: magnifier, one clear button (native search cancel suppressed), Escape clears, focus stays, 300 ms debounce, URL sync, translated accessible name. Migrate the member overview's search bar (incl. its inline SVG).
  2. <.combobox> single and multi, APG combobox with a listbox popup: focus in input, aria-activedescendant, options not tabbable, keyboard navigation in a colocated hook on the client (no roundtrip per key), active option reset when the options change, Enter never selects from a stale result while a debounced search is pending (decide: wait for the result or ignore Enter), “No results for “‹query›””, result count announced, chips with xs remove buttons (≥ 24 px target), Backspace removes the last chip, “Add N” commit for multi; server-side filtering with debounce; no page-wide key listener. The server accepts a selection only if its id is among the options it rendered (loaded with the actor), and the commit handler re-checks the relation permission with the actor before saving (as GroupLive.Show does today: lookup in available_members, then can?(actor, :update, group); DESIGN_GUIDELINES §19.2). Use Doggo's combobox as the reference implementation (MIT, attribution in the moduledoc).
  3. Migrate: group “Add members” (multi), user ↔ member link in the user form (single, saved with the form), filter-builder field picker.
  4. “Add field” in global settings becomes a menu button with the full APG Menu Button keyboard model (it picks an action from a short list, not an entity; DESIGN_GUIDELINES §19.3). U8 later moves it onto the shared <.menu_button> mechanism.
  5. <.file_input>: label bound to the real input, help text (types, max size), file name, upload errors with aria-invalid, hover state; migrate the CSV import.
  6. Showcase sections: search, combobox (empty, results, no results, multi with chips), file input with error.

Out of scope

  • List toolbars on other index pages (U10).
  • Menus and popovers in general (U8).

Acceptance criteria

  • DESIGN_GUIDELINES §19.1, §19.2, §19.3, §18.3 are met; no phx-window-keydown in pickers.
  • Tests: debounce and URL sync of the search; ArrowDown + Enter within the debounce window never selects from the previous result (ui-tagged browser test); combobox selection, chip removal, commit, “No results”; a forged select event with an id outside the rendered options selects nothing; a forged commit from an actor without update permission saves nothing; no database read on keys that do not change the query.
  • Before/after screenshots: member search, group “Add members”, user form member link, CSV upload with an error.
  • Keyboard and screen reader walkthrough per APG combobox recorded in the PR.
  • Showcase sections added.
  • just ci-dev green after every step.

Dependencies

  • Consider #188 (is search-while-typing accessible?): the debounce and the result announcement of <.search_input> answer it; close #188 with a reference to this issue.
  • U2 (daisyUI version); U3 (button API, tokens).
  • #554 (colocated hooks convention) first, so new hooks are created colocated.
  • U10 uses <.search_input> on more index pages.

Anchors (verified against main @ d9093a1f, re-check before filing)

  • Search bar: lib/mv_web/components/search_bar_component.ex
  • User form picker: lib/mv_web/live/user_live/form.ex:232,816
  • Group “Add members”: lib/mv_web/live/group_live/show.ex
  • Filter builder picker: lib/mv_web/components/add_filter_builder_component.ex
  • “Add field” list: lib/mv_web/live/global_settings_live.ex:226
  • CSV upload: lib/mv_web/live/import_live/components.ex:79,90

Known constraint: debounce vs. keyboard (from #593)

Phoenix LiveView routes every phx-* event of an element through the element's single phx-debounce timer, focus included (only blur/focusout are exempt). An input therefore cannot have a debounced phx-change (search) and undelayed phx-keydown/phx-focus (arrow keys, Enter, Escape, open-on-focus) at the same time. Today the two pickers make opposite trade-offs: the user form member picker (#member-search-input) has no debounce, so every keystroke runs one :available_for_linking read; group_live/show.ex puts the keys behind a 300 ms debounce (delayed or dropped arrow keys). #593 tried a hook variant (the ComboBox hook pushes navigation keys while phx-change stays debounced) and reverted it: a debounced search arriving after an immediate Enter/Escape reopened the dropdown and reset the highlight. The <.combobox> in step 2 must order events so that a late search never reopens a list already closed by Enter/Escape and never resets the highlight (e.g. send the current input value with each key and apply a pending search first, or ignore searches for an already-applied query). Additional acceptance:

  • Typing N characters quickly yields one search read.
  • ArrowDown/ArrowUp/Enter/Escape react immediately; Enter right after typing selects the highlighted entry and the list stays closed.
  • Clicking into the input opens the list and it stays open.
  • Browser-verified (Playwright), not only LiveViewTest.

Caution

  • If the own combobox proves too costly, the fallback is Doggo's combobox as a dependency (adoption plan, section (c)); decide in /solutions, not mid-run.
  • Client-side state (active option) is not visible to has_element?; cover keyboard behavior in a ui-tagged browser test.

The filter builder's date values use the #548 stopgap: a native <input type="date"> in a daisyUI .input plus relative presets and type=month (add_filter_builder_component.ex:493-496). If U7 or U10 builds a shared date-range control, the requirements are: from–to range across months and years, quick year and decade jump, typed input (year + localized month name), month-only view for birthdays, WCAG 2.2 AA, dd.MM.yyyy with Monday start, a LiveView hook or web component with phx-update="ignore", vendorable offline (single-file ESM, no npm). Candidates: native-first (today) vs. vanilla-calendar-pro vs. a custom year+month select; research in .pipeline/research/mitgliederverwaltung-548-datepicker-options.md. Optional for U7; decide in its /clarify whether it belongs here or in U10 (list toolbars).

## Motivation The member search (magnifier, clear button, live filter, URL sync) and the group's “Add members” picker (chips, no icon, no clear, list vanishes without results) share nothing; Escape behaves differently in each. There are three combobox implementations (two copies), with options wrongly in the Tab order, no “No results”, four placeholder wordings, and in the user form a page-wide key listener with a database read per change (fixed minimally in U1). The global-settings “Add field” list is semantically a menu button. The CSV upload has no proper component. DESIGN_GUIDELINES §19 and §18.3 define the target. ## Visible improvement Searching and picking feel the same everywhere: one look, Escape and “×” clear, a clear “No results” message, full keyboard support. ## Scope (ordered steps, `just ci-dev` green after each) 1. `<.search_input>`: magnifier, one clear button (native search cancel suppressed), Escape clears, focus stays, 300 ms debounce, URL sync, translated accessible name. Migrate the member overview's search bar (incl. its inline SVG). 2. `<.combobox>` single and multi, APG combobox with a listbox popup: focus in input, `aria-activedescendant`, options not tabbable, keyboard navigation in a colocated hook on the client (no roundtrip per key), active option reset when the options change, Enter never selects from a stale result while a debounced search is pending (decide: wait for the result or ignore Enter), “No results for “‹query›””, result count announced, chips with `xs` remove buttons (≥ 24 px target), Backspace removes the last chip, “Add N” commit for multi; server-side filtering with debounce; no page-wide key listener. The server accepts a selection only if its id is among the options it rendered (loaded with the actor), and the commit handler re-checks the relation permission with the actor before saving (as `GroupLive.Show` does today: lookup in `available_members`, then `can?(actor, :update, group)`; DESIGN_GUIDELINES §19.2). Use Doggo's combobox as the reference implementation (MIT, attribution in the moduledoc). 3. Migrate: group “Add members” (multi), user ↔ member link in the user form (single, saved with the form), filter-builder field picker. 4. “Add field” in global settings becomes a menu button with the full APG Menu Button keyboard model (it picks an action from a short list, not an entity; DESIGN_GUIDELINES §19.3). U8 later moves it onto the shared `<.menu_button>` mechanism. 5. `<.file_input>`: label bound to the real input, help text (types, max size), file name, upload errors with `aria-invalid`, hover state; migrate the CSV import. 6. Showcase sections: search, combobox (empty, results, no results, multi with chips), file input with error. ## Out of scope - List toolbars on other index pages (U10). - Menus and popovers in general (U8). ## Acceptance criteria - DESIGN_GUIDELINES §19.1, §19.2, §19.3, §18.3 are met; no `phx-window-keydown` in pickers. - Tests: debounce and URL sync of the search; ArrowDown + Enter within the debounce window never selects from the previous result (`ui`-tagged browser test); combobox selection, chip removal, commit, “No results”; a forged select event with an id outside the rendered options selects nothing; a forged commit from an actor without update permission saves nothing; no database read on keys that do not change the query. - Before/after screenshots: member search, group “Add members”, user form member link, CSV upload with an error. - Keyboard and screen reader walkthrough per APG combobox recorded in the PR. - Showcase sections added. - `just ci-dev` green after every step. ## Dependencies - Consider #188 (is search-while-typing accessible?): the debounce and the result announcement of `<.search_input>` answer it; close #188 with a reference to this issue. - U2 (daisyUI version); U3 (button API, tokens). - #554 (colocated hooks convention) first, so new hooks are created colocated. - U10 uses `<.search_input>` on more index pages. ## Anchors (verified against `main` @ `d9093a1f`, re-check before filing) - Search bar: `lib/mv_web/components/search_bar_component.ex` - User form picker: `lib/mv_web/live/user_live/form.ex:232,816` - Group “Add members”: `lib/mv_web/live/group_live/show.ex` - Filter builder picker: `lib/mv_web/components/add_filter_builder_component.ex` - “Add field” list: `lib/mv_web/live/global_settings_live.ex:226` - CSV upload: `lib/mv_web/live/import_live/components.ex:79,90` ## Known constraint: debounce vs. keyboard (from #593) Phoenix LiveView routes every `phx-*` event of an element through the element's single `phx-debounce` timer, focus included (only blur/focusout are exempt). An input therefore cannot have a debounced `phx-change` (search) and undelayed `phx-keydown`/`phx-focus` (arrow keys, Enter, Escape, open-on-focus) at the same time. Today the two pickers make opposite trade-offs: the user form member picker (`#member-search-input`) has no debounce, so every keystroke runs one `:available_for_linking` read; `group_live/show.ex` puts the keys behind a 300 ms debounce (delayed or dropped arrow keys). #593 tried a hook variant (the `ComboBox` hook pushes navigation keys while `phx-change` stays debounced) and reverted it: a debounced search arriving after an immediate Enter/Escape reopened the dropdown and reset the highlight. The `<.combobox>` in step 2 must order events so that a late search never reopens a list already closed by Enter/Escape and never resets the highlight (e.g. send the current input value with each key and apply a pending search first, or ignore searches for an already-applied query). Additional acceptance: - Typing N characters quickly yields one search read. - ArrowDown/ArrowUp/Enter/Escape react immediately; Enter right after typing selects the highlighted entry and the list stays closed. - Clicking into the input opens the list and it stays open. - Browser-verified (Playwright), not only LiveViewTest. ## Caution - If the own combobox proves too costly, the fallback is Doggo's combobox as a dependency (adoption plan, section (c)); decide in `/solutions`, not mid-run. - Client-side state (active option) is not visible to `has_element?`; cover keyboard behavior in a `ui`-tagged browser test. ## Related: date range input (from #548 FU-1, regrouped 2026-10-25) The filter builder's date values use the #548 stopgap: a native `<input type="date">` in a daisyUI `.input` plus relative presets and `type=month` (`add_filter_builder_component.ex:493-496`). If U7 or U10 builds a shared date-range control, the requirements are: from–to range across months and years, quick year and decade jump, typed input (year + localized month name), month-only view for birthdays, WCAG 2.2 AA, dd.MM.yyyy with Monday start, a LiveView hook or web component with `phx-update="ignore"`, vendorable offline (single-file ESM, no npm). Candidates: native-first (today) vs. vanilla-calendar-pro vs. a custom year+month select; research in `.pipeline/research/mitgliederverwaltung-548-datepicker-options.md`. Optional for U7; decide in its /clarify whether it belongs here or in U10 (list toolbars).
simon closed this issue 2026-10-28 09:31:50 +01:00
Sign in to join this conversation.
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
local-it/mitgliederverwaltung#625
No description provided.