Accessibility: Focus Management in Dialogs
Accessibility: Focus Management in Dialogs
As of v1.0.441, all modal dialogs in the Making Tax Digital platform implement a full WCAG 2.1.2-compliant focus trap. This ensures keyboard-only users and assistive technology users cannot accidentally interact with background content while a dialog is open.
Affected Components
| Component | Location | Dialog Type |
|---|---|---|
ConfirmDialog | src/components/confirm-dialog.tsx | General-purpose confirmation (e.g. archive property) |
DeleteConfirmDialog | src/app/dashboard/transactions/delete-confirm-dialog.tsx | Destructive delete for manual transactions |
MfaVerifyModal | src/components/mfa-verify-modal.tsx | MFA code entry before HMRC submission |
The useFocusTrap Hook
All three dialogs share a single useFocusTrap hook (src/hooks/use-focus-trap.ts). The hook accepts three parameters:
useFocusTrap({
open: boolean, // whether the dialog is currently open
onClose: () => void, // callback to close the dialog
containerRef: RefObject<HTMLElement>, // ref to the dialog container
});
Behaviour
- Tab cycling — pressing Tab from the last focusable element in the container wraps focus back to the first. Shift+Tab from the first element wraps to the last.
- Escape to close — pressing
EscapecallsonClose, closing the dialog and returning focus. - Focus save/restore — when the dialog opens, the currently focused element (
document.activeElement) is saved. When the dialog closes (including via Escape), focus is returned to that element. - Body scroll lock —
document.body.style.overflowis set tohiddenwhile the dialog is open, preventing background content from scrolling. aria-hiddenfiltering — elements markedaria-hidden="true"are excluded from the focusable element query, matching the same pattern used in the platform's basemodal.tsx.
WCAG Compliance
| WCAG Criterion | Requirement | Implementation |
|---|---|---|
| 2.1.1 Keyboard | All functionality must be operable via keyboard | All dialog actions (confirm, cancel, close) are reachable by Tab alone |
| 2.1.2 No Keyboard Trap | If focus can be moved into a component, it must be movable out again | Escape key always closes the dialog; focus cycles within the container but never escapes to background content |
| 2.4.3 Focus Order | Focus order must preserve meaning and operability | Focus moves into the dialog on open and returns to the trigger element on close |
Dialog-Specific Notes
ConfirmDialog
Used for confirmations such as archiving a property. Tab cycles through the close button (×), Cancel, and Confirm. Previously relied on three separate useEffect hooks; these have been replaced by a single useFocusTrap call.
DeleteConfirmDialog
Used when permanently deleting a manual transaction from the transaction ledger. The component was previously defined inline inside transaction-ledger.tsx with only a partial Escape-key handler and no Tab cycling. It has been extracted to its own file and upgraded with the full focus trap.
Tab cycles between Cancel and Delete.
MfaVerifyModal
Shown before an HMRC submission when MFA is enabled. The modal is always open when rendered — the parent component controls mounting. Tab focus is contained within the panel div. All action buttons carry visible focus-ring styles (focus:ring-2 focus:ring-ring).
Testing Keyboard Focus
To manually verify focus trap behaviour:
-
ConfirmDialog — trigger an archive action on a property. Tab should cycle through ×, Cancel, and Confirm. Pressing Tab from Confirm should wrap back to ×. Pressing Escape should close the dialog and return focus to the button that opened it.
-
DeleteConfirmDialog — initiate deletion of a manual transaction. Tab should cycle between Cancel and Delete. Escape should cancel and restore focus.
-
MfaVerifyModal — begin an HMRC submission with MFA enabled. Tab should cycle through all interactive elements within the modal panel. Escape should cancel the modal.