/** * @fileoverview Select collection modal component * * Modal dialog for selecting a collection when creating new content * without a pre-selected collection. Triggered by Alt+N shortcut. * * @module @writenex/astro/client/components/SelectCollectionModal */ import { ChevronRight, Folder, X } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import type { Collection } from "../../hooks/useApi"; import { useFocusTrap } from "../../hooks/useFocusTrap"; import "./SelectCollectionModal.css"; /** * Props for SelectCollectionModal component */ interface SelectCollectionModalProps { /** Whether the modal is open */ isOpen: boolean; /** Callback to close the modal */ onClose: () => void; /** Callback when a collection is selected */ onSelect: (collectionName: string) => void; /** Available collections */ collections: Collection[]; /** Whether collections are loading */ isLoading?: boolean; } /** * Modal dialog for selecting a collection * * @component */ export function SelectCollectionModal({ isOpen, onClose, onSelect, collections, isLoading = false, }: SelectCollectionModalProps): React.ReactElement | null { const [focusedIndex, setFocusedIndex] = useState(0); const listRef = useRef(null); const triggerRef = useRef(null); // Store the trigger element when modal opens useEffect(() => { if (isOpen) { triggerRef.current = document.activeElement as HTMLElement; setFocusedIndex(0); } }, [isOpen]); // Focus trap for accessibility const { containerRef } = useFocusTrap({ enabled: isOpen, onEscape: onClose, returnFocusTo: triggerRef.current, }); // Keyboard navigation useEffect(() => { if (!isOpen || collections.length === 0) return; const handleKeyDown = (e: KeyboardEvent) => { switch (e.key) { case "ArrowDown": e.preventDefault(); setFocusedIndex((prev) => prev < collections.length - 1 ? prev + 1 : 0 ); break; case "ArrowUp": e.preventDefault(); setFocusedIndex((prev) => prev > 0 ? prev - 1 : collections.length - 1 ); break; case "Enter": e.preventDefault(); if (collections[focusedIndex]) { onSelect(collections[focusedIndex].name); } break; } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [isOpen, collections, focusedIndex, onSelect]); // Scroll focused item into view useEffect(() => { if (listRef.current && collections.length > 0) { const focusedItem = listRef.current.children[focusedIndex] as HTMLElement; focusedItem?.scrollIntoView({ block: "nearest" }); } }, [focusedIndex, collections.length]); const handleOverlayClick = useCallback( (e: React.MouseEvent) => { if (e.target === e.currentTarget) { onClose(); } }, [onClose] ); const handleItemClick = useCallback( (collectionName: string) => { onSelect(collectionName); }, [onSelect] ); if (!isOpen) return null; return (
{/* Header */}

Select Collection

{/* Body */}

Choose a collection to create new content in:

{isLoading ? (
Loading collections...
) : collections.length === 0 ? (
No collections found
) : (
    {collections.map((collection, index) => (
  • ))}
)}
{/* Footer hint */}
Navigate Enter Select Esc Cancel
); }