From 65c8c4955faa8728e974b5a54f71721603d2035d Mon Sep 17 00:00:00 2001 From: thierryvm Date: Fri, 24 Jul 2026 17:33:57 +0200 Subject: [PATCH 1/2] fix(commitments): scroll + focus the edit form when it opens (dashboard-ux-auditor P1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking the pencil on a row far down the list opened the edit/create form at the TOP of the page — off-screen, with no change for a sighted user and no cue for keyboard/screen-reader users. An effect keyed on formMode/editingId now focuses the label field (preventScroll) and smooth-scrolls the form into view. Also aligns the stepper's disabled opacity 40 → 50 with the design-system token (P3). --- .../app/commitments/CommitmentsClient.tsx | 16 +++++++++++++++- .../__tests__/CommitmentsClient.test.tsx | 8 ++++++++ .../commitments/InstallmentStepper.tsx | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/app/[locale]/app/commitments/CommitmentsClient.tsx b/src/app/[locale]/app/commitments/CommitmentsClient.tsx index b3b2162..41f1ec9 100644 --- a/src/app/[locale]/app/commitments/CommitmentsClient.tsx +++ b/src/app/[locale]/app/commitments/CommitmentsClient.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useMemo, useOptimistic, useState, useTransition } from 'react'; +import { useEffect, useMemo, useOptimistic, useState, useTransition } from 'react'; import { Check, Pencil, Plus, Trash2 } from 'lucide-react'; import { useTranslations } from 'next-intl'; @@ -101,6 +101,20 @@ export function CommitmentsClient({ setFormMode('closed'); } + // When the form opens (create OR edit), bring it into view and move focus into + // it. Without this, clicking the pencil on a row far down the list opens the + // form at the TOP of the page — off-screen, with no change for a sighted user + // and no cue for keyboard/screen-reader users (dashboard-ux-auditor P1). + useEffect(() => { + if (formMode === 'closed') return; + // `preventScroll` so focus doesn't jump instantly — the smooth scroll below + // does the moving. `scrollIntoView` is guarded (jsdom doesn't implement it). + document.getElementById('commitment-label')?.focus({ preventScroll: true }); + document + .getElementById('commitments-form') + ?.scrollIntoView?.({ behavior: 'smooth', block: 'start' }); + }, [formMode, editingId]); + // Optimistic paid ledger: a Set of `${commitmentId}|${periodKey}`. The reducer // takes an EXPLICIT intent (`{key, paid}`) rather than a blind toggle so a `+` // always adds and a `−` always removes — idempotent even on a stale read, so a diff --git a/src/app/[locale]/app/commitments/__tests__/CommitmentsClient.test.tsx b/src/app/[locale]/app/commitments/__tests__/CommitmentsClient.test.tsx index 805f100..b1ed629 100644 --- a/src/app/[locale]/app/commitments/__tests__/CommitmentsClient.test.tsx +++ b/src/app/[locale]/app/commitments/__tests__/CommitmentsClient.test.tsx @@ -346,6 +346,14 @@ describe('', () => { expect(createMock).not.toHaveBeenCalled(); }); + it('moves focus into the form when the pencil opens edit', async () => { + renderPage([carLoan]); + fireEvent.click(screen.getByTestId('commitment-edit-car')); + // The effect focuses the label field so keyboard/SR users land in the form + // even when it opened off-screen at the top of a long list (ux-auditor P1). + await waitFor(() => expect(screen.getByLabelText('Libellé')).toHaveFocus()); + }); + it('blocks reducing installmentsTotal below the number already ticked', async () => { updateMock.mockResolvedValue({ ok: true }); renderPage([carLoan], { paidKeysByCommitment: { car: ['2026-1', '2026-2', '2026-3'] } }); diff --git a/src/components/commitments/InstallmentStepper.tsx b/src/components/commitments/InstallmentStepper.tsx index ba26335..775813d 100644 --- a/src/components/commitments/InstallmentStepper.tsx +++ b/src/components/commitments/InstallmentStepper.tsx @@ -18,7 +18,7 @@ type Props = { }; const BTN = - 'focus-visible:ring-brand-600 border-border hover:border-brand-600 flex size-11 shrink-0 items-center justify-center rounded-full border-2 transition-colors [-webkit-tap-highlight-color:transparent] focus-visible:ring-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-40 md:size-9'; + 'focus-visible:ring-brand-600 border-border hover:border-brand-600 flex size-11 shrink-0 items-center justify-center rounded-full border-2 transition-colors [-webkit-tap-highlight-color:transparent] focus-visible:ring-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:size-9'; /** * Payment stepper for a finite commitment — « X / N payées » with − / +. From 5a1d366932f4af04c74ffaac0dc8a9dd1d6846d7 Mon Sep 17 00:00:00 2001 From: thierryvm Date: Fri, 24 Jul 2026 17:48:17 +0200 Subject: [PATCH 2/2] fix(commitments): respect prefers-reduced-motion for the edit-form scroll (Codex review) Users with prefers-reduced-motion: reduce get an instant jump instead of a smooth scroll when the form opens (WCAG 2.3.3 Animation from Interactions). --- src/app/[locale]/app/commitments/CommitmentsClient.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/[locale]/app/commitments/CommitmentsClient.tsx b/src/app/[locale]/app/commitments/CommitmentsClient.tsx index 41f1ec9..e11c32c 100644 --- a/src/app/[locale]/app/commitments/CommitmentsClient.tsx +++ b/src/app/[locale]/app/commitments/CommitmentsClient.tsx @@ -107,12 +107,15 @@ export function CommitmentsClient({ // and no cue for keyboard/screen-reader users (dashboard-ux-auditor P1). useEffect(() => { if (formMode === 'closed') return; - // `preventScroll` so focus doesn't jump instantly — the smooth scroll below - // does the moving. `scrollIntoView` is guarded (jsdom doesn't implement it). + // `preventScroll` so focus doesn't jump instantly — the scroll below does + // the moving. `scrollIntoView` is guarded (jsdom doesn't implement it). document.getElementById('commitment-label')?.focus({ preventScroll: true }); + // Respect prefers-reduced-motion (WCAG 2.3.3): no smooth animation for users + // who asked to reduce motion — jump instantly instead (Codex review). + const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches; document .getElementById('commitments-form') - ?.scrollIntoView?.({ behavior: 'smooth', block: 'start' }); + ?.scrollIntoView?.({ behavior: reduceMotion ? 'auto' : 'smooth', block: 'start' }); }, [formMode, editingId]); // Optimistic paid ledger: a Set of `${commitmentId}|${periodKey}`. The reducer