Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/app/[locale]/app/commitments/CommitmentsClient.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -101,6 +101,23 @@ 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 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: reduceMotion ? 'auto' : '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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,14 @@ describe('<CommitmentsClient />', () => {
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'] } });
Expand Down
2 changes: 1 addition & 1 deletion src/components/commitments/InstallmentStepper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 − / +.
Expand Down
Loading