All files / src/components/public FilterSheet.tsx

90.9% Statements 10/11
100% Branches 3/3
85.71% Functions 6/7
90.9% Lines 10/11

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96                                                                85x 85x   85x       8x                               5x         1x                                     1x 1x     2x 2x                      
/**
 * FilterSheet Component (Story 4.2 - Task 2b)
 *
 * Mobile filter sheet (modal) for archive browsing
 * Wraps FilterSidebar in a sheet/modal for mobile devices
 */
 
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FilterSidebar } from './FilterSidebar';
import type { ArchiveFilters } from '@/types/event.types';
import type { Topic } from '@/types/topic.types';
 
interface FilterSheetProps {
  filters: ArchiveFilters;
  topics: Topic[];
  onFilterChange: (filters: ArchiveFilters) => void;
  onClearFilters: () => void;
  onSortChange: (sort: string) => void;
  currentSort: string;
  loading?: boolean;
}
 
export function FilterSheet({
  filters,
  topics,
  onFilterChange,
  onClearFilters,
  onSortChange,
  currentSort,
  loading = false,
}: FilterSheetProps) {
  const { t } = useTranslation();
  const [isOpen, setIsOpen] = useState(false);
 
  return (
    <div data-testid="filter-sheet">
      {/* Trigger Button */}
      <button
        onClick={() => setIsOpen(true)}
        className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
        data-testid="filter-sheet-trigger"
      >
        {t('archive.filters.title')}
      </button>
 
      {/* Modal Overlay */}
      {isOpen && (
        <div
          className="fixed inset-0 z-50 flex items-end justify-center bg-black bg-opacity-50"
          onClick={() => setIsOpen(false)}
        >
          {/* Sheet */}
          <div
            className="bg-background text-foreground w-full max-h-[80vh] rounded-t-lg overflow-y-auto p-6"
            onClick={(e) => e.stopPropagation()}
          >
            <div className="flex justify-between items-center mb-4">
              <h2 className="text-lg font-semibold">{t('archive.filters.title')}</h2>
              <button
                onClick={() => setIsOpen(false)}
                className="text-muted-foreground hover:text-foreground"
              >
                ✕
              </button>
            </div>
 
            {/* Filter Content.
                onFilterChange must NOT auto-close the sheet: FilterSidebar fires it on
                mount (300 ms-debounced search effect) and on every topic toggle, which
                used to close the sheet the moment the user opened it. The sheet now
                stays open until the user explicitly closes via the ✕ button or the
                backdrop, so multiple topics can be selected in one go. Sort and Clear
                remain one-shot actions that close the sheet after applying. */}
            <FilterSidebar
              filters={filters}
              topics={topics}
              onFilterChange={onFilterChange}
              onClearFilters={() => {
                onClearFilters();
                setIsOpen(false);
              }}
              onSortChange={(s) => {
                onSortChange(s);
                setIsOpen(false);
              }}
              currentSort={currentSort}
              loading={loading}
            />
          </div>
        </div>
      )}
    </div>
  );
}