chore(invoice,budget): dedupe split helper, extract OverflowMenu, surface revert errors (#1413)#1414
Conversation
…face revert errors Closes the architect's medium-severity recommendations from the #1407 and #1412 reviews. Four scopes: 1. Extract splitByDeposits() helper in depositAggregateUtils.ts and reuse across the 4 call sites that inlined the proportional-split + dedup pattern (computeDepositAwareAggregates, computeStatusContribution, aggregateInvoiceStatusBreakdown, and computeDiscretionaryInvoiceAmount in budgetSourceService.ts). Behaviour-preserving — existing tests pass unmodified. 2. Extract a shared OverflowMenu component (client/src/components/ OverflowMenu/). DepositRow and DepositCard both consume it instead of duplicating ~330 lines of menu code. Full WAI-ARIA Menu Button keyboard nav, mobile 44px touch targets, design-token-only CSS, dark mode handled by the token cascade. Same aria-haspopup/role attributes as the inline implementation — existing E2E locators still work. 3. Replace inline style={{}} on the <tr> opacity transition with a CSS module class (.tableRowMutating), matching the prior fd73bca fix. 4. Surface API errors in handleRevertToPending, handleRevertToPaid, and handleStateConfirm. Menu-driven reverts show a section-level FormError banner (auto-dismiss 6s). State-confirm modal shows FormError inside the dialog. Two new i18n keys for network-error fallbacks; existing translateApiError() covers coded server errors. Fixes #1413 Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <noreply@anthropic.com> Co-Authored-By: Claude backend-developer (Haiku 4.5) <noreply@anthropic.com> Co-Authored-By: Claude frontend-developer (Haiku 4.5) <noreply@anthropic.com> Co-Authored-By: Claude translator (Sonnet 4.6) <noreply@anthropic.com> Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <noreply@anthropic.com> Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.5) <noreply@anthropic.com>
… nav UX-designer review on PR #1414 found two non-blocking nits in the new OverflowMenu shared component: - Default item focus ring switched from inset 2px var(--color-primary) to inset 3px var(--color-focus-ring) — the canonical menu-item ring used elsewhere in the codebase. - Added missing .itemDanger:focus-visible rule with var(--color-focus-ring-danger) so destructive items have a distinguishable keyboard focus indicator. - Arrow-key / Home / End keyboard handlers and the initial-focus query now use [role="menuitem"]:not(:disabled), so the cursor skips disabled items. Refs #1413 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude frontend-developer (Haiku 4.5) <noreply@anthropic.com>
|
[product-architect] APPROVED Reviewed against the four recommendations I raised in #1407 and #1412. All addressed cleanly. What I verified
Focus-ring tokenYou asked whether item focus should use Architecture / contract compliance
Minor observations (non-blocking)
Ship it. |
steilerDev
left a comment
There was a problem hiding this comment.
[ux-designer]
Design review against the visual spec posted on #1413.
Overall verdict: APPROVED (with one non-blocking note)
All critical token mappings, ARIA structure, keyboard navigation, dark mode, and mobile touch targets match the spec. One medium finding and one informational note are called out below; neither is blocking for merge.
Checklist pass
| Check | Result |
|---|---|
--shadow-focus on trigger :focus-visible |
PASS — box-shadow: var(--shadow-focus) |
--z-dropdown on menu surface |
PASS |
| Spacing tokens throughout (no hardcoded px for padding) | PASS |
| Mobile 44 px touch target on trigger | PASS — min-width/height: 44px |
Mobile 44 px touch target on items (@media max-width: 767px) |
PASS — padding: var(--spacing-3) var(--spacing-4) |
aria-haspopup="true" on trigger |
PASS |
aria-expanded on trigger |
PASS |
role="menu" on dropdown |
PASS |
role="menuitem" on items |
PASS |
| ArrowDown / ArrowUp / Home / End / Escape keyboard nav | PASS |
| ArrowDown on trigger opens menu | PASS |
| Outside-click closes menu | PASS — mousedown listener on document |
| Escape returns focus to trigger | PASS |
No [data-theme="dark"] overrides needed / present |
PASS — all tokens cascade |
| No hardcoded color values | PASS |
prefers-reduced-motion guards on trigger and items |
PASS |
tableRowMutating uses var(--transition-fast) with motion guard |
PASS |
No inline style={{}} remaining in call site |
PASS |
Old per-page menu classes removed from InvoiceDepositsSection.module.css |
PASS |
Finding 1 — Non-blocking: menu item focus ring uses --color-primary instead of --shadow-focus (inset form)
File: client/src/components/OverflowMenu/OverflowMenu.module.css, .item:focus-visible
The spec required the inset focus ring to mirror the project's canonical focus token, expressed as the inset equivalent:
/* Spec */
.item:focus-visible {
box-shadow: inset 0 0 0 3px var(--color-focus-ring);
}The implementation uses:
/* Implemented */
.item:focus-visible {
box-shadow: inset 0 0 0 2px var(--color-primary);
}Two differences:
- Ring width: 2 px instead of 3 px (mismatches trigger's
--shadow-focuswhich is 3 px). - Color:
var(--color-primary)is a fully-opaque blue (#2563eb/#3b82f6dark), whereas--color-focus-ringis the semi-transparent blue (rgba(59,130,246,0.3)/rgba(96,165,250,0.4)dark) used consistently by every other focusable element in the codebase. The opaque solid blue can appear harsh against some item backgrounds in dark mode.
This is a consistency issue, not a contrast failure or an accessibility blocker. var(--color-primary) is a valid token and the focus ring is visible. Fix before merge or in refinement.
Additionally: .itemDanger:focus-visible has no rule at all. The spec required:
.itemDanger:focus-visible {
box-shadow: inset 0 0 0 3px var(--color-focus-ring-danger);
}Without this rule, destructive items receive the same blue focus ring as default items — a minor but noticeable inconsistency when keyboard-navigating to a red item. Fix alongside the default item ring change.
Finding 2 — Informational: disabled items are not skipped by arrow-key navigation
File: client/src/components/OverflowMenu/OverflowMenu.tsx, handleMenuKeyDown
The spec note said: "Disabled items must be skipped by the arrow-key handler (querySelectorAll('[role="menuitem"]:not(:disabled)'))"
The implementation queries all [role="menuitem"] including disabled ones. A keyboard user pressing ArrowDown will land on a disabled button — which is technically reachable by focus (browsers do focus disabled buttons in some configurations) but cannot be activated. This is a low/informational finding for the current use cases (disabled items are rare in the deposits section), but worth addressing before OverflowMenu is adopted more widely.
Open follow-up (unchanged from prior reviews)
The bottom-sheet modal adaptation for mobile (issue #1404) is not part of this PR's scope and remains an open follow-up item.
Approved. The implementation is a clean extraction of the existing inline menu into a well-structured shared component with full token compliance, correct ARIA, and keyboard navigation. The two findings above should be addressed before or shortly after merge.
|
🎉 This PR is included in version 2.6.0-beta.5 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Summary
splitByDeposits()extracted intodepositAggregateUtils.tsand reused across 4 previously-duplicated call sites inbudgetSourceService.ts— behaviour-preserving refactor.client/src/components/OverflowMenu/replaces ~330 lines of duplicated menu code inDepositRow/DepositCard. Full WAI-ARIA Menu Button keyboard nav, mobile 44px touch targets, design-token-only CSS, dark mode via token cascade.<tr>opacity transition moved fromstyle={{}}to.tableRowMutatingCSS module class (matches fd73bca pattern).handleRevertToPending,handleRevertToPaid, andhandleStateConfirmnow surface viaFormErrorbanners (section-level auto-dismiss 6s; modal-level for state-confirm). Two new i18n keys for network-error fallbacks; coded server errors use existingtranslateApiError().Fixes #1413
Test plan
splitByDepositstests, 37OverflowMenutests, 5 error-surfacing testsCo-Authored-By: Claude dev-team-lead (Sonnet 4.6) noreply@anthropic.com