* Add a user preference under Settings → Appearance that prevents modals
from closing when clicking outside them. Useful to avoid accidentally
losing unsaved form data.
- Add `disable_modal_click_outside?` helper to User model (JSONB prefs)
- DS::Dialog reads the user preference as default when not explicitly set
(existing callers passing disable_click_outside: true/false are unaffected)
- Wire up save in AppearancesController
- Add toggle in the Modals section of the Appearance settings page
- Add i18n strings
* fix(i18n): move modal keys to appearances.show namespace in 7 locales
The modal translation keys (modals_title, modals_subtitle,
disable_modal_click_outside_title, disable_modal_click_outside_description)
were under settings.preferences.show but the view calls t(".modals_title")
from settings/appearances/show.html.erb. Moved them to
settings.appearances.show in de, es, nb, nl, ro, tr, and zh-TW.
* refactor(ds): decouple Dialog from Current.user via defaults_provider
---------
Co-authored-by: neko <neko@nixos>
The dialog close button rendered as a :md icon button (44x44px with a
20px glyph) — noticeably larger than the dialog's own action buttons
(36px tall) and visually heavy next to the title. Pass size: :sm so the
close control is 32x32px with a 16px glyph, matching the action row's
weight. 32px still clears the WCAG 2.5.8 (AA) 24px minimum target.
* fix(design-system): DS::Dialog a11y — role, aria-modal, aria-labelledby, heading_level
Closes#1740. The savings-goals audit captured the dialog rendering
without `role`, `aria-modal`, or `aria-labelledby` — AT users
landing focus inside the dialog hear no title and no modal-mode
hint. Affects every modal/drawer surface in the app (transfer
matches, valuations, trades, imports, settings, etc. — 30+ views).
Fixes:
1. `role="dialog"` + `aria-modal="true"` on the `<dialog>` element.
Native `<dialog>` already maps to these implicitly in modern
browsers, but Safari and pre-2024 mappings benefit from the
explicit role.
2. `aria-labelledby` wired to a stable `dialog-title-<8-char hex>`
id minted in initialize. The header slot's `<h*>` carries the
matching id; AT now announces the title on focus-in. If the
caller passes `custom_header: true` (no title), the
`aria-labelledby` reference resolves to nothing and AT
gracefully falls back to the first focusable.
3. New `heading_level:` kwarg (default `2`). Lets callers nest
dialogs inside surfaces that already have an `<h2>` heading
without breaking outline order. The existing `<h2>` baseline
stays as the default.
API is additive; existing 30+ DS::Dialog callsites work without
modification.
Out of scope (own issues):
- Drawer modal-vs-non-modal split (`<dialog>` is currently always
opened via `showModal()`). Browser behavior is correct for both
variants today; non-modal drawer is a separate UX call.
- Reduced-motion audit — no CSS transitions on `dialog` open/close.
- Explicit focus-on-open (title vs first input) — browser-native
`showModal()` already focuses the first focusable; caller can
override with `autofocus`. Not changing the default here.
- `en.common.close` missing translation — separate bug, filed.
* fix(review): gate aria-labelledby + validate heading_level
Only emit aria-labelledby when the header slot rendered an auto-title
so the id reference never dangles (custom_header: true and body-only
dialogs like the global confirm dialog no longer expose a broken
label). Validate heading_level is an Integer 1..6 in the initializer
to prevent invalid <h0>/<h7> markup. Update stale comment that
referenced tag.public_send instead of content_tag.
* fix(ds-dialog): always emit aria-labelledby (slot lambda is lazy)
The previous fix gated `aria-labelledby` on `@has_auto_title`, set
inside the `renders_one :header` slot lambda. ViewComponent v3
evaluates slot lambdas lazily at slot-render time (after the parent
template's `tag.dialog` opening attributes are computed), so the
flag was always `false` when the `aria-labelledby` attribute was
read.
Verified end-to-end via Playwright on `/design-system/preview/dialog/{modal,drawer}`:
the rendered `<dialog>` is missing `aria-labelledby` even when
`with_header(title: ...)` is set, despite the matching `<h2 id="dialog-title-...">`
being present in the DOM. AT therefore announces "dialog" with no
title — the exact regression the PR set out to fix on slot-driven
callers (which is every dialog in the app).
Always emitting `aria-labelledby="dialog-title-<hex>"` is safe per
the WAI-ARIA spec: a dangling reference (e.g. `custom_header: true`
or body-only dialogs) is silently ignored, and callers can override
via `**opts` (last-wins). This matches the intent stated in the PR
body of #1740.
- Drop now-dead `@has_auto_title` ivar + `has_auto_title?` predicate.
- Update template comment to explain the slot-lambda timing trap.
DS::Dialog#close_button called I18n.t("common.close") but no
`common.close` key exists in any locale file, so every modal rendered
the literal string "Translation missing: en.common.close" as both the
`title` and `aria-label` of the X close button — visible to screen
readers and as a hover tooltip.
Switch to `ds.dialog.close` to mirror the existing `ds.alert.*`
namespace under config/locales/views/components/*.yml, and add the
English string. Other locales fall back to English (fallbacks=true in
config/application.rb) until translated.
Closes#1763.
Co-authored-by: plind-junior <plind-junior@users.noreply.github.com>
* feat: Add responsive dialog behavior for transaction modals
Add responsive option to DS::Dialog component that switches between:
- Mobile (< 1024px): Modal style (centered) with inline close button
- Desktop (≥ 1024px): Drawer style (right side panel) with header close button
Update transaction, transfer, holding, trade, and valuation views to use
responsive behavior, maintaining mobile experience while reverting desktop
to drawer style like budget categories.
Changes:
- app/components/DS/dialog.rb: Add responsive parameter and helper methods
- app/components/DS/dialog.html.erb: Apply responsive styling
- app/views/*/show.html.erb: Add responsive: true and hide close icons on mobile
* fix: Enhance close button accessibility in dialog components
* fix: Refactor dialog component to improve close button handling and accessibility
* feat: implement expandable view for cashflow sankey chart
* refactor: migrate cashflow dialog sizing to tailwind utilities
* refactor: declarative draggable restore on cashflow dialog close
* refactor: localized title and use Tailwind utilities
* refactor: update dialog interaction especially on mobile
* refactor: add global expand text to localization
* fix: restore draggable immediately after dialog close
* Whitespace noise
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
* Introduce SnapTrade integration with models, migrations, views, and activity processing logic.
* Refactor SnapTrade activities processing: improve activity fetching flow, handle pending states, and update UI elements for enhanced user feedback.
* Update Brakeman ignore file to include intentional redirect for SnapTrade OAuth portal.
* Refactor SnapTrade models, views, and processing logic: add currency extraction helper, improve pending state handling, optimize migration checks, and enhance user feedback in UI.
* Remove encryption for SnapTrade `snaptrade_user_id`, as it is an identifier, not a secret.
* Introduce `SnaptradeConnectionCleanupJob` to asynchronously handle SnapTrade connection cleanup and improve i18n for SnapTrade item status messages.
* Update SnapTrade encryption: make `snaptrade_user_secret` non-deterministic to enhance security.
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: luckyPipewrench <luckypipewrench@proton.me>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>