diff --git a/frontend/src/components/delete-confirm-dialog.tsx b/frontend/src/components/delete-confirm-dialog.tsx index 7ea7aaf..ab03b60 100644 --- a/frontend/src/components/delete-confirm-dialog.tsx +++ b/frontend/src/components/delete-confirm-dialog.tsx @@ -1,55 +1,56 @@ -import { useEffect, useId, useRef } from 'react' +import { useEffect, useId, useRef } from "react"; +import { useFocusTrap } from "../hooks/use-focus-trap"; type DeleteConfirmDialogProps = { - open: boolean - title: string - message: string - confirmLabel?: string - cancelLabel?: string - onConfirm: () => void - onClose: () => void -} + open: boolean; + title: string; + message: string; + confirmLabel?: string; + cancelLabel?: string; + onConfirm: () => void; + onClose: () => void; +}; export default function DeleteConfirmDialog({ open, title, message, - confirmLabel = 'Move to trash', - cancelLabel = 'Cancel', + confirmLabel = "Move to trash", + cancelLabel = "Cancel", onConfirm, onClose, }: DeleteConfirmDialogProps) { - const titleId = useId() - const descId = useId() - const panelRef = useRef(null) - const confirmRef = useRef(null) + const titleId = useId(); + const descId = useId(); + const panelRef = useFocusTrap(open); + const confirmRef = useRef(null); useEffect(() => { - if (!open) return - const t = window.setTimeout(() => confirmRef.current?.focus(), 0) - return () => window.clearTimeout(t) - }, [open]) + if (!open) return; + const t = window.setTimeout(() => confirmRef.current?.focus(), 0); + return () => window.clearTimeout(t); + }, [open]); useEffect(() => { - if (!open) return + if (!open) return; const onKey = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - e.preventDefault() - onClose() + if (e.key === "Escape") { + e.preventDefault(); + onClose(); } - } - window.addEventListener('keydown', onKey) - return () => window.removeEventListener('keydown', onKey) - }, [open, onClose]) + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [open, onClose]); - if (!open) return null + if (!open) return null; return (
{ - if (e.target === e.currentTarget) onClose() + onMouseDown={(e) => { + if (e.target === e.currentTarget) onClose(); }} >
@@ -90,5 +91,5 @@ export default function DeleteConfirmDialog({
- ) + ); } diff --git a/frontend/src/components/editor-shortcuts-modal.tsx b/frontend/src/components/editor-shortcuts-modal.tsx index 8847cb3..aa87db6 100644 --- a/frontend/src/components/editor-shortcuts-modal.tsx +++ b/frontend/src/components/editor-shortcuts-modal.tsx @@ -1,48 +1,71 @@ -import { Cancel01Icon } from '@hugeicons/core-free-icons' -import { HugeiconsIcon } from '@hugeicons/react' +import { useEffect } from "react"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { Cancel01Icon } from "@hugeicons/core-free-icons"; +import { useFocusTrap } from "../hooks/use-focus-trap"; -type ShortcutRow = { keys: string; action: string } +type ShortcutRow = { keys: string; action: string }; const ROWS: ShortcutRow[] = [ - { keys: 'Cmd/Ctrl + Z', action: 'Undo' }, - { keys: 'Cmd/Ctrl + Shift + Z', action: 'Redo' }, - { keys: 'Cmd/Ctrl + G', action: 'Group selection' }, - { keys: 'Cmd/Ctrl + Shift + G', action: 'Ungroup' }, - { keys: 'Cmd/Ctrl + D', action: 'Duplicate selection' }, - { keys: 'Cmd/Ctrl + C / V', action: 'Copy / paste (Avnac clipboard)' }, - { keys: 'Arrow keys', action: 'Nudge selection 1px' }, - { keys: 'Shift + Arrow keys', action: 'Nudge selection 10px' }, - { keys: 'Delete / Backspace', action: 'Delete selection' }, - { keys: 'Option/Alt + drag', action: 'Duplicate while dragging (canvas)' }, + { keys: "Cmd/Ctrl + Z", action: "Undo" }, + { keys: "Cmd/Ctrl + Shift + Z", action: "Redo" }, + { keys: "Cmd/Ctrl + G", action: "Group selection" }, + { keys: "Cmd/Ctrl + Shift + G", action: "Ungroup" }, + { keys: "Cmd/Ctrl + D", action: "Duplicate selection" }, + { keys: "Cmd/Ctrl + C / V", action: "Copy / paste (Avnac clipboard)" }, + { keys: "Arrow keys", action: "Nudge selection 1px" }, + { keys: "Shift + Arrow keys", action: "Nudge selection 10px" }, + { keys: "Delete / Backspace", action: "Delete selection" }, + { keys: "Option/Alt + drag", action: "Duplicate while dragging (canvas)" }, { - keys: 'Vector board — tools', - action: 'V = Move, P = Pen, Shift+P = Pencil, R = Rectangle, O = Ellipse', + keys: "Vector board — tools", + action: "V = Move, P = Pen, Shift+P = Pencil, R = Rectangle, O = Ellipse", }, { - keys: 'Vector board — selection', + keys: "Vector board — selection", action: - 'Shift+click multi-selects; drag empty area marquees; Shift+drag marquee = additive; Delete/Backspace removes; Cmd/Ctrl+C/V copies/pastes; Alt+drag duplicates; Arrow nudges 1px (Shift = 10px)', + "Shift+click multi-selects; drag empty area marquees; Shift+drag marquee = additive; Delete/Backspace removes; Cmd/Ctrl+C/V copies/pastes; Alt+drag duplicates; Arrow nudges 1px (Shift = 10px)", }, { - keys: 'Vector board — transform', + keys: "Vector board — transform", action: - 'Drag selection handles to resize (Shift = proportional, Alt = from center); double-click a pen shape to edit its anchors, then Alt+click to remove an anchor, Esc to exit', + "Drag selection handles to resize (Shift = proportional, Alt = from center); double-click a pen shape to edit its anchors, then Alt+click to remove an anchor, Esc to exit", }, { - keys: 'Vector board — view / z-order', + keys: "Vector board — view / z-order", action: - 'Space+drag or middle-click drag to pan; Cmd/Ctrl+wheel zooms; Cmd/Ctrl+0 resets, Cmd/Ctrl+1 fits, Cmd/Ctrl+=/- zooms; Cmd/Ctrl+] / [ moves selection forward/backward (add Shift for front/back)', + "Space+drag or middle-click drag to pan; Cmd/Ctrl+wheel zooms; Cmd/Ctrl+0 resets, Cmd/Ctrl+1 fits, Cmd/Ctrl+=/- zooms; Cmd/Ctrl+] / [ moves selection forward/backward (add Shift for front/back)", }, - { keys: '?', action: 'Show shortcuts' }, -] + { keys: "?", action: "Show shortcuts" }, +]; type Props = { - open: boolean - onClose: () => void -} + open: boolean; + onClose: () => void; +}; export default function EditorShortcutsModal({ open, onClose }: Props) { - if (!open) return null + const panelRef = useFocusTrap(open); + useEffect(() => { + if (!open) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault(); + onClose(); + } + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [open, onClose]); + + useEffect(() => { + if (!open) return; + const t = window.setTimeout(() => { + panelRef.current?.querySelector("button")?.focus(); + }, 0); + return () => window.clearTimeout(t); + }, [open]); + + if (!open) return null; return (
{ - if (e.target === e.currentTarget) onClose() + onMouseDown={(e) => { + if (e.target === e.currentTarget) onClose(); }} >
@@ -85,5 +109,5 @@ export default function EditorShortcutsModal({ open, onClose }: Props) {
- ) + ); } diff --git a/frontend/src/components/image-crop-modal.tsx b/frontend/src/components/image-crop-modal.tsx index afac13d..54103ae 100644 --- a/frontend/src/components/image-crop-modal.tsx +++ b/frontend/src/components/image-crop-modal.tsx @@ -3,6 +3,7 @@ import { HugeiconsIcon } from '@hugeicons/react' import type { CSSProperties } from 'react' import { useCallback, useEffect, useLayoutEffect, useReducer, useRef, useState } from 'react' import { createPortal } from 'react-dom' +import { useFocusTrap } from '../hooks/use-focus-trap' const MIN_SIDE = 12 const HANDLE_PX = 9 @@ -35,7 +36,14 @@ function clampCrop(r: CropRect, nw: number, nh: number): CropRect { return { x, y, w, h } } -export default function ImageCropModal({ open, imageSrc, initialCrop, onCancel, onApply }: Props) { +export default function ImageCropModal({ + open, + imageSrc, + initialCrop, + onCancel, + onApply, +}: Props) { + const dialogRef = useFocusTrap(open) const wrapRef = useRef(null) const imgRef = useRef(null) const initialCropRef = useRef(initialCrop) @@ -230,7 +238,7 @@ export default function ImageCropModal({ open, imageSrc, initialCrop, onCancel, if (e.target === e.currentTarget) onCancel() }} > -
+

Crop image