Commit Graph

6 Commits

Author SHA1 Message Date
Guillem Arias Fauste
8e444ff98b feat(design-system): add DS::SearchInput primitive (closes #1715 §3) (#1853)
* 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>
2026-05-21 12:54:54 +02:00
Guillem Arias Fauste
25bb394378 fix(design-system): DS::Select a11y — fix aria-expanded, listbox keyboard nav, label binding (#1848)
* 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.
2026-05-20 18:19:58 +02:00
Pedro J. Aramburu
f699660479 Add exchange rate feature with multi-currency transactions and transfers support (#1099)
Co-authored-by: Pedro J. Aramburu <pedro@joakin.dev>
2026-04-08 21:05:58 +02:00
Tao Chen
2658c36b05 feat(select): improve merchant dropdown behavior and placement controls (#1364)
* 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
2026-04-07 20:52:14 +02:00
sokiee
71c0735824 Linter 2026-03-22 10:48:54 +01:00
Alessio Cappa
0f78f54f90 New select component (#1071)
* 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>
2026-03-06 10:16:14 +01:00