* feat(design-system): add DS::SearchInput + migrate 2 broken-focus callsites
Resolves#1715 §3.
Two standalone search-field callsites — `/settings/preferences`
currency filter and `/settings/providers` filter row — had a hand-
rolled markup that ended in `focus:ring-gray-500`. That utility has
no backing token in the design system (`ring-gray-500` isn't in
Tailwind's default + Sure doesn't register a gray ring color), so
the input rendered with zero focus indicator on a bordered
bg-container surface. Keyboard users couldn't tell when the field
was focused.
Introduce `DS::SearchInput` — icon-on-left, bordered, token-backed
focus ring matching the DS::Button pattern landing in #1840
(`outline-2 outline-offset-2 outline-gray-900` with the dark-mode
override). API:
DS::SearchInput.new(
name: "...",
placeholder: "...",
value: ...,
aria_label: "...", # defaults to placeholder
class: "...", # passed to the wrapper
**opts # spread onto the <input>, e.g. data-*
)
Migrate the two broken callsites. Three other "search" patterns
stay as-is (out of scope for this PR):
- `form.search_field :search` inside `styled_form_with` blocks
(accounts/show/_activity.html.erb, UI::Account::ActivityFeed) —
already routes through StyledFormBuilder's form-field CSS.
- Embedded-dropdown search input inside DS::Select, DS::Menu, and
the splits/category-select panels — uses a different shape
(no border, no ring) because the parent panel provides the chrome.
- Category dropdown's combobox search input
(app/views/category/dropdowns/show.html.erb) — has a custom
`role=combobox` flow and stays intentionally distinct.
* feat(design-system): add embedded variant to DS::SearchInput, migrate 2 more callsites
Adds `variant: :embedded` to `DS::SearchInput` for search inputs that
live *inside* another DS panel (DS::Select dropdown, splits category
filter, future DS::Popover-hosted filters). No own border / no own
focus ring — the parent panel provides the chrome, so adding ring
+ outline competes with its `focus-within` state.
API:
DS::SearchInput.new(variant: :embedded, placeholder: "...", data: {...})
The `:standalone` default (from the previous commit) stays unchanged
and remains the right choice for top-of-list filter inputs.
Migrated:
- `app/components/DS/select.html.erb` — the in-dropdown search input
for `DS::Select.new(searchable: true)`. Was the only remaining
internal raw <input type="search"> markup in the component.
- `app/views/splits/_category_select.html.erb` — split-transaction
category picker filter. Same shape as DS::Select's search but
hand-rolled because the picker isn't a vanilla DS::Select.
Three other search patterns stay out of scope (intentionally, per
the previous commit):
- `form.search_field :search` inside `styled_form_with` — uses
form-field CSS, different visual contract.
- `app/views/category/dropdowns/show.html.erb` — bespoke
`role="combobox"` flow with `aria-expanded` / `aria-autocomplete`
semantics that don't belong in this primitive.
* fix(review): mobile font + embedded variant focus-within ring
- DS::SearchInput: switch text-sm -> text-base sm:text-sm on both
variants so the input keeps its 16px base size on mobile. iOS
Safari zooms the viewport when a focused input is below 16px,
which the unconditional text-sm was triggering on the Settings
Preferences currency search and Settings Bank Sync provider
search.
- DS::Select (searchable variant) + splits/_category_select:
add focus-within:ring-4 focus-within:ring-alpha-black-200
(with theme-dark variant) on the wrapper around the embedded
search input. The embedded variant intentionally has no own
focus ring so it inherits chrome from its parent panel — but
the two current parent panels were not providing one, so
keyboard focus on the dropdown search box rendered with no
visible indicator. Ring matches the .form-field token used
across the design system.
* fix(merge): repair DS::Select search input merge resolution
The previous merge of main left invalid Ruby inside the DS::SearchInput
`data:` hash:
aria-label="<%= t("helpers.select.search_placeholder") %>"
This is an ERB string assignment masquerading as a hash entry — it does
not parse and would have raised SyntaxError at render. Two follow-ups:
- Drop the `aria-label` entry entirely. `DS::SearchInput` already
defaults `aria_label` to `placeholder`, and `placeholder` is set
on the call, so the resulting <input> already carries
`aria-label="<%= t(...) %>"`.
- Restore the `input->select#syncTabindex` action that main #1848
added on the embedded search input. It keeps the roving tabindex
on the listbox in sync as filtered results change. Original PR
branch had only `list-filter#filter`; reintegrate both with
explicit `input->` event prefixes for parity with main.
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
* fix(design-system): DS::Select a11y — fix aria-expanded, listbox keyboard nav, label binding
Closes#1744. Several concrete bugs from the savings-goals audit:
1. **`aria-expanded` wired to the wrong state.** The template had
`aria-expanded="<%= @selected_value.present? ? "true" : "false" %>"`,
which is "has a value been chosen", not "is the menu open".
AT users heard a misleading signal on every page load.
Init to `"false"`; the Stimulus controller's openMenu/close
already correctly maintains the attribute after that.
2. **`aria-labelledby` referenced a nonexistent id.** The trigger
pointed at `"#{method}_label"`, but the rendered `<label>` had
no id at all — the binding silently failed. Add
`id: "#{method}_label"` to `form.label` so the reference
actually resolves to the label text. Only emit
`aria-labelledby` when there *is* a visible label.
3. **`tabindex="0"` on every option.** Listbox options should use
roving tabindex (only the selected option is in tab order; the
rest are reachable via ArrowUp/Down). Set
`tabindex="0"` on the selected option only; `"-1"` on the rest.
The select controller's `select()` handler keeps the roving
invariant on user interaction.
4. **No keyboard navigation between options.** Add ArrowDown/Up
(cycle), Home (first), End (last). The existing Enter/Escape
handlers stay. ArrowUp/Down inside the search input is left
alone so the input's caret behavior isn't hijacked.
5. **Search input had no accessible name.** Add an explicit
`aria-label` matching the placeholder copy so AT users hear
"search" when focus enters the field.
API unchanged. Builder-level routing fix in
`StyledFormBuilder#select` (calling DS::Select for `f.select(...)`
the same way `f.collection_select` already does) is intentionally
out of scope — it's a separate translation pass for the choices
format. Documented as a follow-up.
* fix(review): bridge search input to visible options in DS::Select
ArrowDown/Up from the search input now focus the first/last visible
option, and keyboard navigation operates on visible options only. After
typing a search query, the controller promotes the first visible option
to tabindex="0" so Tab can land on it even when the previously
tab-eligible option is filtered out.
Addresses Codex review on PR #1848 (issue #1744).
* fix(review): include trigger in DS::Select aria-labelledby
Codex P2 follow-up on #1848: \`aria-labelledby=\"#{method}_label\"\`
makes the trigger button's accessible name come solely from the
external form label — that overrides the button's own text node
(\`selected_item[:label]\` / placeholder). Screen readers therefore
announce only "Currency" without ever hearing the selected "USD"
unless the user opens the listbox.
Give the trigger \`id=\"#{method}_trigger\"\` and reference both ids:
\`aria-labelledby=\"#{method}_label #{method}_trigger\"\`. The
accessible-name algorithm concatenates the two, so AT users now
hear \"<Label> <selected value>\" while \`aria-expanded\` /
\`aria-haspopup\` continue to convey the dropdown state.
* feat(select): improve merchant dropdown behavior and placement controls
- add configurable menu_placement strategy to DS::Select (auto/down/up) with safe normalization
forward menu_placement through StyledFormBuilder#collection_select
- force Merchant dropdown to open downward in transaction create and editor forms
- fix select option/search text contrast by applying text-primary in DS select menu
- prevent form jump on open by scrolling only inside dropdown content instead of using scrollIntoView
- clamp internal dropdown scroll to valid bounds for stability
- refactor select controller placement logic for readability (placementMode, clamp) without changing behavior
* set menu_placement=auto for metchant selector
* feat: add new UI component to display dropdown select with filter
* feat: use new dropdown componet for category selection in transactions
* feat: improve dropdown controller
* feat: Add checkbox indicator to highlight selected element in list
* feat: add possibility to define dropdown without search
* feat: initial implementation of variants
* feat: Add default color for dropdown menu
* feat: add "icon" variant for dropdown
* refactor: component + controller refactoring
* refactor: view + component
* fix: adjust min width in selection for mobile
* feat: refactor collection_select method to use new filter dropdown component
* fix: compute fixed position for dropdown
* feat: controller improvements
* lint issues
* feat: add dot color if no icon is available
* refactor: controller refactor + update naming for variant from icon to logo
* fix: set width to 100% for select dropdown
* feat: add variant to collection_select in new transaction form
* fix: typo in placeholder value
* fix: add back include_blank property
* refactor: rename component from FilterDropdown to Select
* fix: translate placeholder and keep value_method and text_method
* fix: remove duplicate variable assignment
* fix: translate placeholder
* fix: verify color format
* fix: use right autocomplete value
* fix: selection issue + controller adjustments
* fix: move calls to startAutoUpdate and stopAutoUpdate
* Update app/javascript/controllers/select_controller.js
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Alessio Cappa <104093777+alessiocappa@users.noreply.github.com>
* fix: add aria-labels
* fix: pass html_options to DS::Select
* fix: unnecessary closing tag
* fix: use offsetvalue for position checks
* fix: use right classes for dropdown transitions
* include options[:prompt] in placeholder init
* fix: remove unused locale key
* fix: Emit a native change event after updating the input value.
* fix: Guard against negative maxHeight in constrained layouts.
* fix: Update test
* fix: lint issues
* Update test/system/transfers_test.rb
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Alessio Cappa <104093777+alessiocappa@users.noreply.github.com>
* Update test/system/transfers_test.rb
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Alessio Cappa <104093777+alessiocappa@users.noreply.github.com>
* refactor: move CSS class for button select form in maybe-design-system.css
---------
Signed-off-by: Alessio Cappa <104093777+alessiocappa@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>