add dialog semantics, focus trap, and ARIA labels to card detail modal
This commit is contained in:
@@ -28,6 +28,22 @@ export function CardDetailModal({ cardId, onClose }: CardDetailModalProps) {
|
|||||||
const prefersReducedMotion = useReducedMotion();
|
const prefersReducedMotion = useReducedMotion();
|
||||||
const instant = { duration: 0 };
|
const instant = { duration: 0 };
|
||||||
|
|
||||||
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
|
const triggerRef = useRef<Element | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
triggerRef.current = document.activeElement;
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
modalRef.current?.focus();
|
||||||
|
}, 50);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
} else if (triggerRef.current instanceof HTMLElement) {
|
||||||
|
triggerRef.current.focus();
|
||||||
|
triggerRef.current = null;
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{open && card && cardId && (
|
{open && card && cardId && (
|
||||||
@@ -48,6 +64,11 @@ export function CardDetailModal({ cardId, onClose }: CardDetailModalProps) {
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
|
ref={modalRef}
|
||||||
|
tabIndex={-1}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="card-detail-title"
|
||||||
layoutId={prefersReducedMotion ? undefined : `card-${cardId}`}
|
layoutId={prefersReducedMotion ? undefined : `card-${cardId}`}
|
||||||
className="relative w-full max-w-4xl overflow-hidden rounded-xl bg-pylon-surface shadow-2xl"
|
className="relative w-full max-w-4xl overflow-hidden rounded-xl bg-pylon-surface shadow-2xl"
|
||||||
initial={prefersReducedMotion ? { opacity: 0 } : undefined}
|
initial={prefersReducedMotion ? { opacity: 0 } : undefined}
|
||||||
@@ -259,6 +280,7 @@ function InlineTitle({ cardId, title, updateCard, hasColor }: InlineTitleProps)
|
|||||||
onChange={(e) => setDraft(e.target.value)}
|
onChange={(e) => setDraft(e.target.value)}
|
||||||
onBlur={handleSave}
|
onBlur={handleSave}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
|
aria-label="Card title"
|
||||||
className={`flex-1 font-heading text-xl bg-transparent outline-none border-b pb-0.5 ${
|
className={`flex-1 font-heading text-xl bg-transparent outline-none border-b pb-0.5 ${
|
||||||
hasColor ? "text-white border-white/50" : "text-pylon-text border-pylon-accent"
|
hasColor ? "text-white border-white/50" : "text-pylon-text border-pylon-accent"
|
||||||
}`}
|
}`}
|
||||||
@@ -268,6 +290,7 @@ function InlineTitle({ cardId, title, updateCard, hasColor }: InlineTitleProps)
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<h2
|
<h2
|
||||||
|
id="card-detail-title"
|
||||||
onClick={() => setEditing(true)}
|
onClick={() => setEditing(true)}
|
||||||
className={`flex-1 cursor-pointer font-heading text-xl transition-colors ${textColor} ${hoverColor}`}
|
className={`flex-1 cursor-pointer font-heading text-xl transition-colors ${textColor} ${hoverColor}`}
|
||||||
>
|
>
|
||||||
@@ -309,6 +332,7 @@ function CoverColorPicker({
|
|||||||
onClick={() => updateCard(cardId, { coverColor: null })}
|
onClick={() => updateCard(cardId, { coverColor: null })}
|
||||||
className="flex size-6 items-center justify-center rounded-full border border-border text-xs text-pylon-text-secondary hover:bg-pylon-column"
|
className="flex size-6 items-center justify-center rounded-full border border-border text-xs text-pylon-text-secondary hover:bg-pylon-column"
|
||||||
title="None"
|
title="None"
|
||||||
|
aria-label="No cover color"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
@@ -324,6 +348,7 @@ function CoverColorPicker({
|
|||||||
outlineOffset: "1px",
|
outlineOffset: "1px",
|
||||||
}}
|
}}
|
||||||
title={label}
|
title={label}
|
||||||
|
aria-label={label}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user