Skip to content

Commit 1fc6793

Browse files
feat: Add initial changes to support filter
1 parent e65e76d commit 1fc6793

File tree

5 files changed

+306
-28
lines changed

5 files changed

+306
-28
lines changed

example/src/App.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,13 @@ function App() {
390390
setSelectedRows(selectedRows);
391391
};
392392

393+
// Define filter options for the status column
394+
const statusFilterOptions = [
395+
{ label: 'Active', value: 'Active' },
396+
{ label: 'Inactive', value: 'Inactive' },
397+
{ label: 'Pending', value: 'Pending' },
398+
];
399+
393400
return (
394401
<div className="app">
395402
<h1>Multi-Level Table Example</h1>
@@ -403,6 +410,8 @@ function App() {
403410
onSelectionChange={handleSelectionChange}
404411
pageSize={5}
405412
sortable={true}
413+
filterColumn="status"
414+
filterOptions={statusFilterOptions}
406415
/>
407416
</div>
408417
</div>

src/components/FilterDropdown.tsx

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import React, { useEffect, useRef } from 'react';
2+
3+
import type { FilterOption } from '../types/types';
4+
import '../styles/FilterDropdown.css';
5+
6+
interface FilterDropdownProps {
7+
options: FilterOption[];
8+
selectedValues: Set<string | number>;
9+
onFilterChange: (values: Set<string | number>) => void;
10+
onClose: () => void;
11+
theme?: {
12+
colors?: {
13+
background?: string;
14+
textColor?: string;
15+
};
16+
table?: {
17+
filter?: {
18+
background?: string;
19+
textColor?: string;
20+
borderColor?: string;
21+
};
22+
};
23+
};
24+
}
25+
26+
export const FilterDropdown: React.FC<FilterDropdownProps> = ({
27+
options,
28+
selectedValues,
29+
onFilterChange,
30+
onClose,
31+
theme,
32+
}) => {
33+
const dropdownRef = useRef<HTMLDivElement>(null);
34+
35+
useEffect(() => {
36+
const handleClickOutside = (event: MouseEvent) => {
37+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node))
38+
onClose();
39+
40+
};
41+
42+
document.addEventListener('mousedown', handleClickOutside);
43+
44+
return () => document.removeEventListener('mousedown', handleClickOutside);
45+
}, [onClose]);
46+
47+
const handleOptionChange = (value: string | number) => {
48+
const newValues = new Set(selectedValues);
49+
50+
if (newValues.has(value))
51+
newValues.delete(value);
52+
else
53+
newValues.add(value);
54+
55+
onFilterChange(newValues);
56+
};
57+
58+
return (
59+
<div
60+
ref={dropdownRef}
61+
className="filter-dropdown"
62+
style={{
63+
backgroundColor: theme?.colors?.background || '#ffffff',
64+
color: theme?.colors?.textColor || '#000000',
65+
borderColor: theme?.table?.filter?.borderColor || '#d9d9d9',
66+
}}
67+
>
68+
<div className="filter-dropdown-header">
69+
<span>Filter Options</span>
70+
<button
71+
className="filter-dropdown-close"
72+
onClick={onClose}
73+
style={{
74+
color: theme?.colors?.textColor || '#000000',
75+
}}
76+
>
77+
×
78+
</button>
79+
</div>
80+
<div className="filter-dropdown-content">
81+
{options.map((option) => (
82+
<label
83+
key={option.value.toString()}
84+
className="filter-dropdown-option"
85+
style={{
86+
color: theme?.colors?.textColor || '#000000',
87+
}}
88+
>
89+
<input
90+
type="checkbox"
91+
checked={selectedValues.has(option.value)}
92+
onChange={() => handleOptionChange(option.value)}
93+
style={{
94+
borderColor: theme?.table?.filter?.borderColor || '#d9d9d9',
95+
}}
96+
/>
97+
<span>{option.label}</span>
98+
</label>
99+
))}
100+
</div>
101+
</div>
102+
);
103+
};

src/components/MultiLevelTable.tsx

Lines changed: 88 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
useTable,
1010
} from "react-table";
1111

12+
import { FilterDropdown } from './FilterDropdown';
1213
import { Pagination } from "./Pagination";
1314
import type { PaginationProps } from "./Pagination";
1415
import { TableHeader } from "./TableHeader";
@@ -20,6 +21,7 @@ import type { ThemeProps } from "../types/theme";
2021
import type {
2122
Column,
2223
DataItem,
24+
FilterOption,
2325
SelectionState,
2426
TableInstanceWithHooks,
2527
TableStateWithPagination,
@@ -36,6 +38,8 @@ import "../styles/MultiLevelTable.css";
3638
* @property {(row: DataItem) => void} [onRowClick] - Optional callback function when a parent row is clicked
3739
* @property {string[]} [searchableColumns] - Array of column keys to search in
3840
* @property {boolean} [showSearchBar=true] - Whether to show the search bar
41+
* @property {string} [filterColumn] - The column to filter by
42+
* @property {FilterOption[]} [filterOptions] - Array of filter options
3943
*/
4044
export interface MultiLevelTableProps {
4145
data: DataItem[];
@@ -52,6 +56,8 @@ export interface MultiLevelTableProps {
5256
onRowClick?: (row: DataItem) => void;
5357
searchableColumns?: string[];
5458
showSearchBar?: boolean;
59+
filterColumn?: string;
60+
filterOptions?: FilterOption[];
5561
}
5662

5763
/**
@@ -75,6 +81,8 @@ export const MultiLevelTable: React.FC<MultiLevelTableProps> = ({
7581
onRowClick,
7682
searchableColumns,
7783
showSearchBar = true,
84+
filterColumn,
85+
filterOptions = [],
7886
}) => {
7987
const mergedTheme = mergeThemeProps(defaultTheme, theme);
8088
const [selectionState, setSelectionState] = useState<SelectionState>({
@@ -85,30 +93,47 @@ export const MultiLevelTable: React.FC<MultiLevelTableProps> = ({
8593
// Search state
8694
const [searchTerm, setSearchTerm] = useState("");
8795

96+
const [showFilterDropdown, setShowFilterDropdown] = useState(false);
97+
98+
const [selectedFilterValues, setSelectedFilterValues] = useState<Set<string | number>>(new Set());
99+
88100
// Use provided searchableColumns or all columns
89101
const searchCols = useMemo(() => {
90-
if (searchableColumns && searchableColumns.length > 0) return searchableColumns;
102+
if (searchableColumns && searchableColumns.length > 0)
103+
return searchableColumns;
91104

92105
return columns.map(col => col.key);
93106
}, [searchableColumns, columns]);
94107

95-
// Filtered data based on search
108+
// Filtered data based on search and filter
96109
const filteredData = useMemo(() => {
97-
if (!searchTerm)
110+
let filtered = data;
98111

99-
return data;
112+
// Apply search filter
113+
if (searchTerm) {
114+
const lowerSearch = searchTerm.toLowerCase();
100115

101-
const lowerSearch = searchTerm.toLowerCase();
116+
filtered = filtered.filter(row =>
117+
searchCols.some(colKey => {
118+
const value = row[colKey as keyof DataItem];
102119

103-
return data.filter(row =>
104-
searchCols.some(colKey => {
105-
const value = row[colKey as keyof DataItem];
120+
return value && value.toString().toLowerCase().includes(lowerSearch);
121+
})
122+
);
123+
}
106124

107-
return value && value.toString().toLowerCase().includes(lowerSearch);
108-
})
109-
);
125+
// Apply column filter
126+
if (filterColumn && selectedFilterValues.size > 0)
127+
filtered = filtered.filter(row => {
128+
const value = row[filterColumn as keyof DataItem];
110129

111-
}, [data, searchTerm, searchCols]);
130+
return typeof value === 'string' || typeof value === 'number'
131+
? selectedFilterValues.has(value)
132+
: false;
133+
});
134+
135+
return filtered;
136+
}, [data, searchTerm, searchCols, filterColumn, selectedFilterValues]);
112137

113138
// Get all parent row IDs (level 0)
114139
const parentRowIds = useMemo(() => data.map(item => item.id), [data]);
@@ -338,6 +363,56 @@ export const MultiLevelTable: React.FC<MultiLevelTableProps> = ({
338363
);
339364
};
340365

366+
const handleFilterChange = (values: Set<string | number>) => {
367+
setSelectedFilterValues(values);
368+
};
369+
370+
const renderFilterButton = () => {
371+
if (!filterColumn || !filterOptions.length) return null;
372+
373+
return (
374+
<div style={{ position: 'relative' }}>
375+
<button
376+
style={{
377+
padding: "0.5rem 1rem",
378+
borderRadius: "4px",
379+
border: `1px solid ${mergedTheme.table?.cell?.borderColor || "#e2e8f0"}`,
380+
backgroundColor: mergedTheme.colors?.background || "#ffffff",
381+
color: mergedTheme.colors?.textColor || "#000000",
382+
cursor: "pointer",
383+
fontSize: "0.875rem",
384+
display: "flex",
385+
alignItems: "center",
386+
gap: "0.5rem",
387+
}}
388+
onClick={() => setShowFilterDropdown(!showFilterDropdown)}
389+
>
390+
<span>Filter</span>
391+
{selectedFilterValues.size > 0 && (
392+
<span style={{
393+
backgroundColor: '#5d5fef',
394+
color: '#ffffff',
395+
borderRadius: '12px',
396+
padding: '2px 8px',
397+
fontSize: '0.75rem',
398+
}}>
399+
{selectedFilterValues.size}
400+
</span>
401+
)}
402+
</button>
403+
{showFilterDropdown && (
404+
<FilterDropdown
405+
options={filterOptions}
406+
selectedValues={selectedFilterValues}
407+
onFilterChange={handleFilterChange}
408+
onClose={() => setShowFilterDropdown(false)}
409+
theme={mergedTheme}
410+
/>
411+
)}
412+
</div>
413+
);
414+
};
415+
341416
return (
342417
<div style={{ backgroundColor: mergedTheme.colors?.background }}>
343418
<div className="table-wrapper">
@@ -412,22 +487,7 @@ export const MultiLevelTable: React.FC<MultiLevelTableProps> = ({
412487
>
413488
<span>Export</span>
414489
</button>
415-
<button
416-
style={{
417-
padding: "0.5rem 1rem",
418-
borderRadius: "4px",
419-
border: `1px solid ${mergedTheme.table?.cell?.borderColor || "#e2e8f0"}`,
420-
backgroundColor: mergedTheme.colors?.background || "#ffffff",
421-
color: mergedTheme.colors?.textColor || "#000000",
422-
cursor: "pointer",
423-
fontSize: "0.875rem",
424-
display: "flex",
425-
alignItems: "center",
426-
gap: "0.5rem",
427-
}}
428-
>
429-
<span>Filter</span>
430-
</button>
490+
{renderFilterButton()}
431491
</div>
432492
</div>
433493
)}

0 commit comments

Comments
 (0)