All files / src/components/features/shop/ShopSidebar CategoryDropdown.tsx

100% Statements 192/192
90.9% Branches 30/33
75% Functions 6/8
100% Lines 192/192

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 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 1931x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 1x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 66x 66x 66x 66x 66x 22x 22x 22x 22x 22x 22x 22x 22x 22x 44x 44x 44x 44x 44x 44x 44x 44x 44x 44x 44x 44x 44x 44x 44x 44x 44x 44x 44x 44x 22x 22x 66x 66x 66x 66x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 66x 66x 66x 66x 66x 66x 66x 66x 23x 23x 23x 23x 23x 1x 1x  
"use client";
 
import { useState, useId } from "react";
import { Icon } from "@/components/ui/icons";
import { Button } from "@/components/ui";
 
type ChildCategory = {
  id: number;
  name: string;
  products: number;
  isRefined?: boolean;
};
 
type Category = {
  id: number;
  name: string;
  products: number;
  children?: ChildCategory[];
};
 
interface CategoryItemProps {
  category: Category;
  onSelect: (categoryId: number) => void;
  isExpanded: boolean;
  onToggleExpand: (categoryId: number) => void;
  selectedCategoryId: number | null;
}
 
const CategoryItem = ({
  category,
  onSelect,
  isExpanded,
  onToggleExpand,
  selectedCategoryId}: CategoryItemProps) => {
  const hasChildren = category.children && category.children.length > 0;
  const isSelected = selectedCategoryId === category.id;
  const childrenId = useId();
 
  return (
    <div role="group" aria-labelledby={`category-${category.id}`}>
      <div className="flex items-center justify-between">
        <Button
          variant="ghost"
          size="sm"
          className={`${
            isSelected && "text-blue"
          } group justify-between flex-1 min-w-0`}
          onClick={() => {
            onSelect(category.id);
          }}
          aria-pressed={isSelected}
          aria-label={`${category.name}, ${category.products} products${isSelected ? ', selected' : ''}`}
        >
          <div className="flex items-center gap-2 min-w-0 flex-1">
            <div
              className={`flex-shrink-0 flex items-center justify-center rounded w-4 h-4 border ${
                isSelected ? "border-blue bg-blue" : "bg-white dark:bg-gray-700 border-gray-3 dark:border-gray-600"
              }`}
              aria-hidden="true"
            >
              {isSelected && <Icon name="check" size={10} className="text-white" />}
            </div>
 
            <span id={`category-${category.id}`} className="dark:text-gray-300 whitespace-nowrap">{category.name}</span>
          </div>
 
          <span
            className={`${
              isSelected ? "text-white bg-blue" : "bg-gray-2 dark:bg-gray-700 dark:text-gray-300"
            } flex-shrink-0 inline-flex rounded-[30px] text-custom-xs px-2 ease-out duration-200 group-hover:text-white group-hover:bg-blue ml-2`}
            aria-hidden="true"
          >
            {category.products}
          </span>
        </Button>
 
        {hasChildren && (
          <Button
            variant="ghost"
            size="sm"
            onClick={() => onToggleExpand(category.id)}
            className="text-gray-4 dark:text-gray-500 p-1 ml-1 min-h-0 hover:text-dark dark:hover:text-gray-300"
            aria-expanded={isExpanded}
            aria-controls={childrenId}
            aria-label={`${isExpanded ? 'Collapse' : 'Expand'} ${category.name} subcategories`}
          >
            <Icon name="chevron-down" size={16} rotate={isExpanded ? 180 : 0} aria-hidden="true" />
          </Button>
        )}
      </div>
 
      {/* Child categories */}
      {hasChildren && (
        <div
          id={childrenId}
          className={`ml-6 mt-2 flex-col gap-3 border-l border-gray-2 dark:border-gray-700 pl-3 ${
            isExpanded ? "flex" : "hidden"
          }`}
          role="group"
          aria-label={`${category.name} subcategories`}
        >
          {category?.children?.map((child) => {
            const isChildSelected = selectedCategoryId === child.id;
            return (
              <Button
                key={child.id}
                onClick={() => onSelect(child.id)}
                variant="ghost"
                size="sm"
                className="group justify-between text-sm"
                aria-pressed={isChildSelected}
                aria-label={`${child.name}, ${child.products} products${isChildSelected ? ', selected' : ''}`}
              >
                <span className="dark:text-gray-300">{child.name}</span>
                <span
                  className="bg-gray-2 dark:bg-gray-700 dark:text-gray-300 inline-flex rounded-[30px] text-custom-xs px-2 ease-out duration-200 group-hover:text-white group-hover:bg-blue"
                  aria-hidden="true"
                >
                  {child.products}
                </span>
              </Button>
            );
          })}
        </div>
      )}
    </div>
  );
};
 
interface CategoryDropdownProps {
  categories: Category[];
  onCategorySelect?: (categoryId: number) => void;
  expandedCategories?: Set<number>;
  onToggleExpand?: (categoryId: number) => void;
  selectedCategoryId?: number | null;
}
 
const CategoryDropdown = ({
  categories,
  onCategorySelect = () => {},
  expandedCategories = new Set(),
  onToggleExpand = () => {},
  selectedCategoryId = null}: CategoryDropdownProps) => {
  const [toggleDropdown, setToggleDropdown] = useState(true);
  const contentId = useId();
 
  return (
    <div className="bg-white dark:bg-gray-800 shadow-1 rounded-lg">
      <Button
        variant="ghost"
        onClick={() => setToggleDropdown(!toggleDropdown)}
        aria-expanded={toggleDropdown}
        aria-controls={contentId}
        className={`w-full flex items-center justify-between py-3 pl-6 pr-5.5 rounded-none min-h-0 ${
          toggleDropdown && "shadow-filter dark:shadow-none dark:border-b dark:border-gray-700"
        }`}
      >
        <span className="text-dark dark:text-gray-100 font-medium">Category</span>
        <Icon
          name="chevron-down"
          size={24}
          rotate={toggleDropdown ? 180 : 0}
          className="text-dark dark:text-gray-300"
          aria-hidden="true"
        />
      </Button>
 
      {/* Category filter list */}
      <div
        id={contentId}
        role="group"
        aria-label="Category filters"
        className={`flex-col gap-3 py-6 pl-6 pr-5.5 ${
          toggleDropdown ? "flex" : "hidden"
        }`}
      >
        {categories.map((category) => (
          <CategoryItem
            key={category.id}
            category={category}
            onSelect={onCategorySelect}
            isExpanded={expandedCategories.has(category.id)}
            onToggleExpand={onToggleExpand}
            selectedCategoryId={selectedCategoryId}
          />
        ))}
      </div>
    </div>
  );
};
 
export default CategoryDropdown;