mirror of
https://github.com/we-promise/sure.git
synced 2026-07-12 21:05:20 +00:00
2d3d9744664b8dffa1e95d5ac69eba2ee3316fca
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |