Milestone v1.0: FamilySync MVP #1

Merged
luckberg merged 376 commits from gsd/v1.0-milestone into main 2026-06-10 17:39:19 -04:00
Showing only changes of commit 4244e8cd29 - Show all commits
@@ -616,4 +616,58 @@ describe('EventForm — Plan 03-12 gap closures', () => {
// Should return a YYYY-MM-DD string
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/)
})
// ── WR-07: Tab/Shift+Tab focus trap ──────────────────────────────────────
//
// The dialog must trap Tab focus: pressing Tab from the last focusable element
// wraps to the first, and Shift+Tab from the first wraps to the last.
// Today EventForm only calls .focus() once on open — Tab escapes the modal.
it('WR-07: Tab from last focusable element wraps focus to first inside dialog', () => {
renderForm({ mode: 'create' })
const dialog = screen.getByRole('dialog')
const focusable = Array.from(
dialog.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
),
)
expect(focusable.length).toBeGreaterThan(1)
const lastElement = focusable[focusable.length - 1]
const firstElement = focusable[0]
// Focus the last element, then dispatch Tab
lastElement.focus()
expect(document.activeElement).toBe(lastElement)
fireEvent.keyDown(dialog, { key: 'Tab', shiftKey: false })
// WR-07 fix: focus should have wrapped to first element inside dialog
expect(document.activeElement).toBe(firstElement)
})
it('WR-07: Shift+Tab from first focusable element wraps focus to last inside dialog', () => {
renderForm({ mode: 'create' })
const dialog = screen.getByRole('dialog')
const focusable = Array.from(
dialog.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
),
)
expect(focusable.length).toBeGreaterThan(1)
const firstElement = focusable[0]
const lastElement = focusable[focusable.length - 1]
// Focus the first element, then dispatch Shift+Tab
firstElement.focus()
expect(document.activeElement).toBe(firstElement)
fireEvent.keyDown(dialog, { key: 'Tab', shiftKey: true })
// WR-07 fix: focus should have wrapped to last element inside dialog
expect(document.activeElement).toBe(lastElement)
})
})