A full-screen drawer that lets users manage their TalentTrust preferences. Opened by SettingsTrigger. Fully accessible (WCAG 2.1 AA): implements dialog semantics, focus trap, Escape key handling, and focus restoration.
| Prop | Type | Required | Description |
|---|---|---|---|
isOpen |
boolean |
Yes | Controls whether the panel is rendered and visible |
onClose |
() => void |
Yes | Callback invoked when the panel should close (Escape, backdrop click, Close button, Done button) |
import { SettingsTrigger } from '@/components/settings/SettingsTrigger';
// SettingsTrigger manages isOpen state and renders SettingsPanel internally.
// Place it once at the app/layout level.
export default function Layout({ children }) {
return (
<>
{children}
<SettingsTrigger />
</>
);
}To use SettingsPanel standalone:
import { useState } from 'react';
import { SettingsPanel } from '@/components/settings/SettingsPanel';
export function MyComponent() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Open settings</button>
<SettingsPanel isOpen={open} onClose={() => setOpen(false)} />
</>
);
}- Initial Focus: When the dialog opens, focus is automatically set to the close button (
aria-label="Close settings") - Focus Trap: Keyboard focus is trapped within the dialog and cannot leave to the page behind it
- Focus Restoration: Managed by parent component (SettingsTrigger) to restore focus to the element that opened the dialog
| Key | Action | Test Coverage |
|---|---|---|
Tab |
Move focus to the next interactive element inside the panel. Wraps from the last element back to the first. | ✅ Comprehensive |
Shift + Tab |
Move focus to the previous interactive element. Wraps from the first element to the last. | ✅ Comprehensive |
Escape |
Close the panel (only when dialog is open). | ✅ Edge cases tested |
Enter / Space |
Activate the focused button, radio button, or toggle switch. | ✅ Through click interactions |
Arrow Keys |
Navigate between radio buttons within radiogroups (browser default). | ✅ ARIA roles validated |
- Forward Tab Wrapping: Pressing Tab on the last focusable element moves focus to the first element
- Reverse Tab Wrapping: Pressing Shift+Tab on the first focusable element moves focus to the last element
- Focusable Elements: Includes all buttons (close, theme, currency, toast density, done), toggle switches, and any interactive controls
- No Escape When Closed: Escape key does not trigger onClose when dialog is not rendered
| Attribute | Value | Purpose |
|---|---|---|
role |
"dialog" |
Identifies the drawer as a modal dialog to assistive technologies |
aria-modal |
"true" |
Tells screen readers that content behind the dialog is inert |
aria-labelledby |
"settings-panel-title" |
Associates the dialog with its visible "Settings" heading |
- The close button (
aria-label="Close settings") receives focus immediately, so keyboard users know where they are.
TabandShift+Tabare intercepted via akeydownlistener ondocument. Focus cycles through all non-disabled focusable elements inside the dialog and never reaches the page behind it.
- Managed by
SettingsTrigger: auseRefholds a reference to the gear-icon trigger button. After the panel unmounts,requestAnimationFramerestores focus to that button, satisfying WCAG 2.4.3 Focus Order.
| Preference | Options |
|---|---|
| Theme | light / dark / system |
| Currency Display | usd / ngn / compact |
| Toast Density | relaxed / compact |
| Quiet Mode | on / off (toggle) |
Preferences are persisted to localStorage under the key talenttrust-user-preferences via the usePreferences hook (@/lib/preferences).
Tests live in src/components/settings/__tests__/SettingsPanel.test.tsx and provide comprehensive coverage of:
- Initial focus on close button when dialog opens
- Escape-to-close behavior while dialog is open
- Forward Tab wrapping from last to first focusable element
- Reverse Shift+Tab wrapping from first to last focusable element
- Focus trap implementation with multiple focusable elements
- Edge cases: no focus wrapping when no focusable elements, no escape handling when closed
- Theme preference updates (light, dark, system)
- Currency display preference updates (usd, ngn, compact)
- Toast density preference updates (relaxed, compact)
- Quiet mode toggle updates
- LocalStorage persistence for all preference changes
- Restoration of preferences from localStorage on remount
- WCAG 2.1 AA compliance validation using jest-axe
- Dialog semantics:
role="dialog",aria-modal="true" - Proper ARIA labeling:
aria-labelledbypointing to Settings heading - All interactive controls have
focus-visiblering classes - Proper ARIA roles and labels for all preference controls (radiogroups, switches)
- Closed state: renders no dialog content
- Open state: renders all settings sections and controls
- Backdrop click to close
- Done button click to close
- Close button click to close
- All preference controls are properly labeled with ARIA attributes
- Focus management works correctly with the actual DOM focusable elements
- Accessibility audits pass with no violations
Run:
npm test -- --testPathPattern=SettingsPanel