import { useVirtualizer } from '@tanstack/react-virtual'; import { useRef, useState, useMemo, useCallback } from 'react'; /** * Option shape for checkbox list items */ export interface CheckboxOption { label: string; value: string; } /** * Props for VirtualizedCheckboxList component */ export interface VirtualizedCheckboxListProps { /** Array of checkbox options to render */ options: CheckboxOption[]; /** Currently selected values */ selectedValues: string[]; /** Callback when selection changes */ onSelectionChange: (selectedValues: string[]) => void; /** Height of each item in pixels (default: 40) */ itemHeight?: number; /** Total height of the scrollable container in pixels (default: 400) */ containerHeight?: number; /** Placeholder text for search input (default: "Search...") */ searchPlaceholder?: string; /** Label for "Select All" button (default: "Select All") */ selectAllLabel?: string; /** Label for "Clear All" button (default: "Clear All") */ clearAllLabel?: string; /** Class name for the container element */ className?: string; /** Whether to show the selected count (default: true) */ showSelectedCount?: boolean; } /** * VirtualizedCheckboxList - Windowed checkbox list for large datasets (200+ items) * * Features: * - Virtualized rendering using @tanstack/react-virtual for optimal performance * - Built-in search/filter functionality * - Select All / Clear All actions * - Selected count display * - Accessible keyboard navigation * - Tailwind CSS styling support * * @example * ```tsx * const [selectedValues, setSelectedValues] = useState([]); * * * ``` */ export function VirtualizedCheckboxList({ options, selectedValues, onSelectionChange, itemHeight = 40, containerHeight = 400, searchPlaceholder = 'Search...', selectAllLabel = 'Select All', clearAllLabel = 'Clear All', className = '', showSelectedCount = true, }: VirtualizedCheckboxListProps) { const [searchTerm, setSearchTerm] = useState(''); const parentRef = useRef(null); // Filter options based on search term const filteredOptions = useMemo(() => { if (!searchTerm.trim()) { return options; } const lowerSearch = searchTerm.toLowerCase(); return options.filter((option) => option.label.toLowerCase().includes(lowerSearch) ); }, [options, searchTerm]); // Set up virtualizer for windowed rendering const virtualizer = useVirtualizer({ count: filteredOptions.length, getScrollElement: () => parentRef.current, estimateSize: () => itemHeight, overscan: 5, // Render 5 extra items above/below viewport }); // Toggle individual checkbox const handleToggle = useCallback( (value: string) => { const isSelected = selectedValues.includes(value); if (isSelected) { onSelectionChange(selectedValues.filter((v) => v !== value)); } else { onSelectionChange([...selectedValues, value]); } }, [selectedValues, onSelectionChange] ); // Select all filtered options const handleSelectAll = useCallback(() => { const allFilteredValues = filteredOptions.map((opt) => opt.value); const uniqueValues = Array.from( new Set([...selectedValues, ...allFilteredValues]) ); onSelectionChange(uniqueValues); }, [filteredOptions, selectedValues, onSelectionChange]); // Clear all selections const handleClearAll = useCallback(() => { onSelectionChange([]); }, [onSelectionChange]); // Clear search const handleClearSearch = useCallback(() => { setSearchTerm(''); }, []); const selectedCount = selectedValues.length; const totalCount = options.length; const isAllSelected = filteredOptions.length > 0 && filteredOptions.every((opt) => selectedValues.includes(opt.value) ); return (
{/* Header: Search + Actions */}
{/* Search input */}
setSearchTerm(e.target.value)} placeholder={searchPlaceholder} className="w-full px-3 py-2 pr-8 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" aria-label="Filter options" /> {searchTerm && ( )}
{/* Action buttons and count */}
{showSelectedCount && ( {selectedCount} / {totalCount} selected )}
{/* Virtualized checkbox list */}
{filteredOptions.length === 0 ? (
No options found
) : (
{virtualizer.getVirtualItems().map((virtualItem) => { const option = filteredOptions[virtualItem.index]; const isChecked = selectedValues.includes(option.value); return ( ); })}
)}
); }