diff --git a/docs/infrastructure/2026-10-20-confirmation-keyboard-scroll.md b/docs/infrastructure/2026-10-20-confirmation-keyboard-scroll.md new file mode 100644 index 0000000..faeb001 --- /dev/null +++ b/docs/infrastructure/2026-10-20-confirmation-keyboard-scroll.md @@ -0,0 +1,9 @@ +# Confirmation keyboard scrolling — 20 October 2026 + +The Linux compose journey found a serious `scrollable-region-focusable` violation in the shared confirmation dialog at a short landscape viewport. Its message body can overflow independently while containing no focusable content. The shared owner is `ConfirmProvider`, also used by attachment deletion, application workspace changes, CV template changes and Career evidence deletion. + +The body now exposes a keyboard-focusable region named by the existing localized dialog title. Cancel retains initial focus; confirmation, dismissal, focus restoration and Promise resolution are unchanged. No new strings, dialog framework or saving semantics are introduced. + +The strengthened existing unit case failed ordinarily before the implementation (one failure, one pass). Afterward all eight cases in the confirmation, confirmation-flow, dialog-flow and modal-state suites passed. The full ten-case native compose suite passed without retries/skips/flaky outcomes at 07:28:17–07:29:43 UTC against a fresh production export. This includes the eight-width bilingual theme/keyboard journey and its unchanged all-enabled axe assertion. Build passed. No test inventory change, accessibility waiver or production authorization change is made. + +Human screen-reader and physical-device acceptance remain pending. This narrow correction does not accept the remaining Phase 7 migration or establish live deployment success. diff --git a/job-tracker-ui/src/confirm.test.tsx b/job-tracker-ui/src/confirm.test.tsx index 6a3d27d..571415c 100644 --- a/job-tracker-ui/src/confirm.test.tsx +++ b/job-tracker-ui/src/confirm.test.tsx @@ -31,6 +31,10 @@ test('renders app-owned confirmation dialog', async () => { expect(screen.getByRole('button', { name: /delete/i })).toBeInTheDocument(); expect(screen.getByRole('dialog', { name: 'Delete job' })).toHaveAccessibleDescription('Are you sure?'); expect(screen.getByRole('button', { name: 'Cancel' })).toHaveFocus(); + const messageRegion = screen.getByRole('region', { name: 'Delete job' }); + expect(messageRegion).toHaveAttribute('tabindex', '0'); + messageRegion.focus(); + expect(messageRegion).toHaveFocus(); }); test('uses the selected language for shared dialog defaults', async () => { diff --git a/job-tracker-ui/src/confirm.tsx b/job-tracker-ui/src/confirm.tsx index 9a8dd12..229dd7e 100644 --- a/job-tracker-ui/src/confirm.tsx +++ b/job-tracker-ui/src/confirm.tsx @@ -23,6 +23,7 @@ const ConfirmContext = createContext(null); export function ConfirmProvider({ children }: { children: React.ReactNode }) { const i18n = useOptionalI18n(); const messageId = useId(); + const titleId = useId(); const resolverRef = useRef<((value: boolean) => void) | null>(null); const cancelButtonRef = useRef(null); const [state, setState] = useState({ @@ -60,13 +61,13 @@ export function ConfirmProvider({ children }: { children: React.ReactNode }) { return ( {children} - closeWith(false)} aria-describedby={messageId} fullWidth maxWidth="xs" + closeWith(false)} aria-labelledby={titleId} aria-describedby={messageId} fullWidth maxWidth="xs" slotProps={{ paper: { sx: { borderRadius: 3, border: 1, borderColor: "divider", backgroundImage: "none" } }, transition: { onEntered: () => cancelButtonRef.current?.focus() }, }}> - {state.title} - + {state.title} + {state.destructive ? i18n?.t("confirmDestructiveHint") ?? "This action may be hard to undo."