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 e971e16cc6 - Show all commits
+40
View File
@@ -21,6 +21,7 @@
* Accessibility:
* - role="dialog", aria-modal="true", aria-label="New Event"/"Edit Event"
* - Focus moves to Title input on open
* - Focus trap: Tab/Shift+Tab cycle focus within dialog; never reaches background (WR-07)
* - Escape / backdrop click closes form
* - All-day toggle: role="switch", aria-checked
* - Recurrence: <select> with labeled options
@@ -111,6 +112,7 @@ export function EventForm() {
const setLastSyncedUid = useCalendarStore((s) => s.setLastSyncedUid)
const queryClient = useQueryClient()
const titleRef = useRef<HTMLInputElement>(null)
const dialogRef = useRef<HTMLDivElement>(null)
// ── Resolve event for edit mode ─────────────────────────────────────────────
@@ -285,6 +287,42 @@ export function EventForm() {
return () => document.removeEventListener('keydown', onKeyDown)
}, [eventFormOpen]) // eslint-disable-line react-hooks/exhaustive-deps
// ── Focus trap: Tab / Shift+Tab cycles within dialog (WR-07) ───────────────
//
// Queries all standard focusable elements inside the dialog on each keydown.
// If Tab is pressed on the last focusable element, wraps to the first.
// If Shift+Tab is pressed on the first focusable element, wraps to the last.
// No external library — implemented inline to avoid adding a dependency.
const handleDialogKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key !== 'Tab' || !dialogRef.current) return
const focusable = Array.from(
dialogRef.current.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
),
).filter((el) => !el.hasAttribute('disabled') && el.getAttribute('tabindex') !== '-1')
if (focusable.length === 0) return
const first = focusable[0]
const last = focusable[focusable.length - 1]
if (e.shiftKey) {
// Shift+Tab: if on first element, wrap to last
if (document.activeElement === first) {
e.preventDefault()
last.focus()
}
} else {
// Tab: if on last element, wrap to first
if (document.activeElement === last) {
e.preventDefault()
first.focus()
}
}
}
// ── Focus: move to title input on open ─────────────────────────────────────
useEffect(() => {
@@ -392,10 +430,12 @@ export function EventForm() {
{/* Dialog */}
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-label={label}
tabIndex={-1}
onKeyDown={handleDialogKeyDown}
style={dialogStyle}
>
{/* Header */}