mirror of
https://github.com/we-promise/sure.git
synced 2026-07-25 11:12:13 +00:00
b660fcfaf25a2ccbabc2bac950bb7fca89327194
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5f16a36b06 |
Fix keyboard navigation in DS::Select dropdowns (#2689)
* Fix keyboard navigation in DS::Select dropdowns. Open the listbox on Tab focus and arrow/space/enter from the trigger so account and category selectors in the transaction form are usable without a mouse, and reset option tabindex when the menu closes. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix Tab skipping DS::Select triggers in transaction form. Keep focus on the trigger when Tab opens the menu, keep listbox options at tabindex -1 with inert on the closed menu, and move focus into options only via arrow keys so Tab no longer jumps to amount or date. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix search input losing focus during DS::Select filtering. Only repoint focus in syncTabindex when a listbox option was focused and became hidden; skip while the user is typing in the search field or when clearSearch runs on menu open. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix Escape closing the parent dialog from an open DS::Select. Stop Escape propagation so only the listbox closes, and suppress handleButtonFocus briefly after close+refocus so keyboard modality does not reopen the menu via :focus-visible. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix Tab leaving DS::Select after suppressReopenOnFocus change. Focus the trigger before closing on Tab and advance to the next dialog field manually so inert on the menu no longer drops focus to body. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
77dda53ffb |
feat(ds): one canonical focus ring across primitives (#2140)
* feat(ds): one canonical focus ring across primitives (#2136) Replaces the grab-bag of per-primitive focus indicators (neutral ring-alpha-black/white, outline-gray-900/white, faint form-field ring-4) with a single recipe — the #1737 accessibility follow-up. - New --color-focus-ring token: blue-600 (light) / blue-500 (dark), >=4:1 against both surfaces. - Canonical .focus-ring / .focus-ring-within in components.css: a 2px outline + 2px offset on :focus-visible only. Outline (not a box-shadow ring) so the offset gap is transparent on any surface with no layout shift; :focus-visible so it never shows for mouse/touch. - Applied to every focusable DS primitive: Button (had none), Link, Disclosure summary, Tabs nav, MenuItem (replaces the browser-default box), SearchInput, Tooltip trigger, Popover trigger, Select panel (focus-within), Toggle (peer-driven outline-focus-ring). .form-field adopts it via :focus-within, replacing the ~1:1 ring-4. - Dialog close button is a DS::Button icon variant, so it inherits the focus-visible-only ring and keeps no resting border (fixes "stuck ring"). Verified in-browser, light+dark: focus-visible ring on button, input, and full-width menu row — consistent blue 2px+offset, legible on both surfaces. Remaining follow-up: >=44px touch targets (disclosure trigger, composer send); bespoke notification / account-new close buttons that still carry a permanent border. * fix(ds): #2136 interactive-state follow-ups — touch target + close-button chrome - Disclosure default trigger: add min-h-11 (44px) so the standalone disclosure summary clears the touch-target minimum (was px-3 py-2 ~36px). Composer send + the coming-soon icons are already DS::Button icon/md (w-11 h-11). - Notification close buttons (sync_toast, notice): drop the resting border-alpha-black-50 box ("frame shouts, glyph muted"); keep a bg-container + shadow-xs chip so the corner control stays visible over the page, and brighten the muted glyph on hover (text-subdued -> hover:text-primary). * refactor(ds): focus ring -> neutral hugging box-shadow (was blue outline) Per design feedback: the blue 2px outline + 2px offset read as a loud, detached frame on the otherwise-neutral UI. Switch the canonical .focus-ring to a soft box-shadow ring that hugs the control (follows border-radius, no gap), in the theme-aware neutral focus-ring token (alpha-black/white-400). Transparent outline kept as a forced-colors fallback; toggle peer-driver switched from outline-* to ring-* to match. Still one token, :focus-visible only. Strength is tunable (currently subtle ~1.5:1). * fix(ds): focus ring vanished on shadowed controls — outline, not box-shadow The neutral box-shadow ring lived in the components layer, so any utility-layer shadow-* (or .form-field's focus-within:shadow-none) on the same element overrode it and the ring silently disappeared on shadowed buttons/inputs. Draw the same subtle neutral ring with a hugging `outline` (outline-offset: 0) instead — a separate property with no box-shadow conflict, and it doubles as the forced-colors indicator. Toggle peer-driver switched ring-* -> outline-* to match. Look is unchanged (neutral, hugging, subtle); it just no longer vanishes. * fix(a11y): enlarge sync-toast close-button touch target (p-0.5 -> p-1.5) The hover-revealed close button had ~2px padding around a 20px icon (~24px total), at the WCAG 2.5.8 AAA boundary. p-1.5 brings the interactive area to ~32px. Addresses CodeRabbit review on #2140. * fix(ds): keep form-field's resting halo; stop the outline color flash Two testing findings: - .form-field reverts to its original always-on soft ring (focus-within ring-4 at low alpha, theme-aware) instead of adopting the keyboard-only outline. It's a resting decoration, not a focus indicator, and the lower-opacity halo was the better look. The canonical block's comment documents the deliberate opt-out. - .focus-ring/.focus-ring-within now carry a base transparent 2px outline so consumers with transition-all (form-field had it) animate transparent -> token on focus instead of passing through currentColor, which flashed as a black border appearing and then fading out. * feat(ds): focus-ring token clears WCAG 3:1 non-text contrast alpha-black-400 (20%) measured ~1.6:1 against white — visible but below the AA bar for focus indicators. Bump to the 700 stop (50%): ~3.95:1 on light containers, ~4.6:1 on dark. Recipe unchanged; one token edit via tokens:build. * fix(ds): ring the hand-rolled privacy toggle too The header pair showed two different focus treatments: panel-right (a DS::Button) got the new token ring while the hand-rolled privacy toggle next to it fell back to the browser-default ring — the sweep covered DS primitives but not bespoke buttons. Both privacy toggles (mobile + desktop) now carry .focus-ring. Also documents the transition interplay on the focused-state rule: consumers with transition-colors fade the ring in over 150ms because Tailwind v4's color transition list includes outline-color. Verified settled value at the intended 50% alpha via Playwright. * fix(ds): ring the sidebar and settings nav links The reshoot caught both nav species falling back to the browser's blue default ring — main sidebar items and settings nav items are bespoke link_to markup the primitive sweep missed, and they're the primary keyboard path in the app. Both adopt .focus-ring (main nav adds rounded-lg so the outline follows a shape). * fix(ds): retire the legacy base-layer button ring for the canonical outline The @layer base button rule still painted a ring-2 ring-offset-2 box-shadow on :focus-visible. Box-shadow and outline are independent properties, so .focus-ring (an outline) could never clear it and every button-tag primitive double-painted both indicators on keyboard focus. Apply the canonical recipe to the base button rule itself: every <button> now gets the transparent resting outline + focus-ring token on :focus-visible by default. Two bespoke buttons suppressed the outline with focus:outline-none and relied on the base ring for their keyboard indicator (category dropdown rows, the sign-up password toggle). Drop the suppression so they pick up the canonical outline — :focus-visible keeps it keyboard-only, which is what the suppression was protecting against anyway. * fix(ds): segmented control adopts the canonical focus recipe The segment rule inlined its own focus-visible outline (offset 2, alpha-400 colors) with a comment noting it was temporary until the canonical token landed — this branch is that token. Drop the inlined utilities: button segments get the outline from the base button rule, and link segments now carry .focus-ring. |
||
|
|
7a0665a8f6 |
fix(ds): one height rail for icon and text buttons (#2202)
* fix(ds): put icon buttons on the text-button height rail Icon-only DS::Button containers were 32/44/48px squares while text buttons of the same nominal size render ~28/36/48px tall, so every mixed header row (icon menu trigger next to text buttons — the transactions index, account pages, the app header) sat misaligned. - buttonish SIZES: icon containers now share the text rail (sm w-7, md w-9, lg w-12 unchanged). - DS::Popover's hand-rolled w-11 trigger joins the rail at w-9. - The layout's hand-rolled privacy toggle (mobile + desktop) matches the md icon-button chrome: w-9, rounded-lg, container-inset hover — it sat at w-8 with a different hover next to a DS icon button. - DS::Select's panel adopts shadow-border-lg, the elevation Menu and Popover already use, replacing the weaker shadow-lg+border-xs combo. Measured on the transactions header after the change: 36/38/36px. * fix(ds): keep the 44px touch target on coarse pointers The height rail trades icon-button size for row alignment, which is a pointer-precision tradeoff: WCAG 2.5.5's 44x44 minimum is about fingers, not mice. sm/md icon containers (and the two off-rail consumers: the popover trigger and the layout privacy toggles) gain pointer-coarse:w-11/h-11, so touch devices keep the full target while fine-pointer layouts get the aligned 36px row. Measured via Playwright: desktop 36x36, iPhone emulation 44x44 on both the menu trigger and the privacy toggle. |
||
|
|
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> |
||
|
|
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. |
||
|
|
f699660479 |
Add exchange rate feature with multi-currency transactions and transfers support (#1099)
Co-authored-by: Pedro J. Aramburu <pedro@joakin.dev> |
||
|
|
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 |
||
|
|
71c0735824 | Linter | ||
|
|
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> |