Files
sure/app/components/UI/account/chart.html.erb
Guillem Arias Fauste 96079188a2 feat(dashboard): unify per-widget period selectors into one picker (#2162)
* feat(ds): elevate dropdown overlays and stabilize selection check gutter

Menus and popovers floated at the same elevation as inline cards
(shadow-border-xs), so dropdowns blended into the content beneath them.
Bump DS::Menu and DS::Popover panels to shadow-border-lg.

DS::MenuItem rendered its leading icon only when present, so a selection
check shifted the row's text out of alignment with the unselected rows.
Add a `selected:` param that reserves a fixed-width check gutter (check
when selected, empty otherwise) so row text stays aligned. Apply the same
reserved gutter to the bespoke category dropdown row, and add a
`selectable` menu preview.

* feat(dashboard): unify per-widget period selectors into one picker

The dashboard rendered three identical period <select>s (cashflow,
outflows, net worth), each writing the same global User#default_period
and full-reloading the page via turbo_frame "_top" — so changing one
changed all. Replace them with a single shared UI::PeriodPicker
(DS::Menu of period links) in a toolbar, and wrap the sections grid in a
"dashboard_sections" Turbo frame so a period change swaps only the
dashboard (no full-page reload). Reuse the same picker on the account
chart, removing its duplicate select.

* refactor(dashboard): use DS::MenuItem selected: gutter in period picker

Now that DS::MenuItem reserves a check gutter, the period picker passes
selected: instead of a conditional leading check icon, so the current
period's row stays aligned with the rest.

* fix(ds): expose menu selection via menuitemradio + aria-checked

Selectable DS::MenuItem rows conveyed selection only visually. Render them
as role="menuitemradio" with aria-checked so assistive tech gets the
selection state of single-select lists, merging the menu ARIA contract with
any caller-supplied aria. Addresses CodeRabbit review feedback.

* refactor(dashboard): drop redundant aria-current from period picker

DS::MenuItem now exposes selection via menuitemradio + aria-checked, so the
period picker no longer needs its own aria-current. Update the component
test to assert the new ARIA.

* fix(ds): include selectable roles in menu roving-focus query

DS::MenuItem selectable rows render as role=menuitemradio, but the menu
controller built its roving-focus list from [role=menuitem] only, leaving
single-select menus with no keyboard focus/arrow handling. Query the
menuitemradio/menuitemcheckbox roles too. Addresses Codex review feedback.

* fix(dashboard): keep non-picker links out of frame + fix custom-month picker

- turbo_frame_tag "dashboard_sections" now targets _top so ordinary links
  inside the sections (e.g. Balance Sheet account links) navigate the page
  instead of failing to resolve inside the frame.
- Period.current_month_for / last_month_for carry their semantic key for
  custom-month families, so the picker shows the right label and checks the
  right option instead of falling back to 30D.

Addresses Codex review feedback.

* fix(dashboard): announce selected period in picker trigger's accessible name

The static "Select time period" aria-label overrode the visible selected
label as the trigger's accessible name, so assistive tech kept announcing
the same name regardless of selection. Interpolate the selected short
label into the aria-label and pin it with a component test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 23:11:43 +02:00

59 lines
2.4 KiB
Plaintext

<div id="<%= dom_id(account, :chart) %>" class="bg-container shadow-border-xs rounded-xl space-y-2">
<div class="flex justify-between flex-col-reverse lg:flex-row gap-2 px-4 pt-4 mb-2">
<div class="space-y-2 w-full">
<div class="flex items-center gap-1">
<%= tag.p title, class: "text-sm font-medium text-secondary" %>
<% if account.supports_trades? %>
<%= render "investments/value_tooltip", balance: account.balance_money, holdings: holdings_value_money, cash: account.cash_balance_money %>
<% end %>
</div>
<div class="flex flex-row gap-2 items-baseline">
<%= tag.p view_balance_money.format, class: "text-primary text-3xl font-medium truncate privacy-sensitive" %>
<% if converted_balance_money %>
<%= tag.p converted_balance_money.format, class: "text-sm font-medium text-secondary privacy-sensitive" %>
<% end %>
</div>
</div>
<div class="flex items-center gap-2">
<% if account.supports_trades? %>
<%= form_with url: account_path(account), method: :get, data: { controller: "auto-submit-form" } do |form| %>
<%= form.select :chart_view,
[[t(".views.total_value"), "balance"], [t(".views.holdings"), "holdings_balance"], [t(".views.cash"), "cash_balance"]],
{ selected: view },
class: "bg-container border border-secondary rounded-lg text-sm pr-7 cursor-pointer text-primary focus:outline-hidden focus:ring-0",
data: { "auto-submit-form-target": "auto" } %>
<% end %>
<% end %>
<%= render UI::PeriodPicker.new(
selected: period,
url: account_path(account),
extra_params: account.supports_trades? ? { chart_view: view } : {}
) %>
</div>
</div>
<%= turbo_frame_tag dom_id(@account, :chart_details) do %>
<div class="px-4">
<%= render partial: "shared/trend_change", locals: { trend: trend, comparison_label: comparison_label } %>
</div>
<div class="h-64 pb-4">
<% if series.any? %>
<div
id="lineChart"
class="w-full h-full privacy-sensitive"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= series.to_json %>"></div>
<% else %>
<div class="w-full h-full flex items-center justify-center">
<p class="text-secondary text-sm"><%= t(".no_data_available") %></p>
</div>
<% end %>
</div>
<% end %>
</div>