@alaarab/ogrid-fluent 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,8 +13,8 @@ npm install @alaarab/ogrid-fluent
13
13
  ```
14
14
  @fluentui/react-components ^9.0.0
15
15
  @fluentui/react-icons ^2.0.0
16
- react ^17.0.0 || ^18.0.0
17
- react-dom ^17.0.0 || ^18.0.0
16
+ react ^17.0.0 || ^18.0.0 || ^19.0.0
17
+ react-dom ^17.0.0 || ^18.0.0 || ^19.0.0
18
18
  ```
19
19
 
20
20
  ## Quick Start
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useCallback, useRef, useEffect } from 'react';
2
+ import { useRef, useEffect } from 'react';
3
3
  import { Checkbox, makeStyles, tokens, mergeClasses, } from '@fluentui/react-components';
4
4
  import { TableSettingsRegular, ChevronDownRegular, ChevronUpRegular } from '@fluentui/react-icons';
5
+ import { useColumnChooserState } from '@alaarab/ogrid-core';
5
6
  const useStyles = makeStyles({
6
7
  container: {
7
8
  position: 'relative',
@@ -116,9 +117,9 @@ const useStyles = makeStyles({
116
117
  export const ColumnChooser = (props) => {
117
118
  const { columns, visibleColumns, onVisibilityChange, className } = props;
118
119
  const classes = useStyles();
119
- const [isOpen, setIsOpen] = useState(false);
120
120
  const buttonRef = useRef(null);
121
121
  const dropdownRef = useRef(null);
122
+ const { open: isOpen, handleToggle, handleClose, handleCheckboxChange: setColumnVisible, handleSelectAll, handleClearAll, visibleCount, totalCount, } = useColumnChooserState({ columns, visibleColumns, onVisibilityChange });
122
123
  useEffect(() => {
123
124
  if (!isOpen)
124
125
  return;
@@ -127,7 +128,7 @@ export const ColumnChooser = (props) => {
127
128
  const isOutsideDropdown = dropdownRef.current && !dropdownRef.current.contains(target);
128
129
  const isOutsideButton = buttonRef.current && !buttonRef.current.contains(target);
129
130
  if (isOutsideDropdown && isOutsideButton) {
130
- setIsOpen(false);
131
+ handleClose();
131
132
  }
132
133
  };
133
134
  const timeoutId = setTimeout(() => {
@@ -137,46 +138,15 @@ export const ColumnChooser = (props) => {
137
138
  clearTimeout(timeoutId);
138
139
  document.removeEventListener('mousedown', handleClickOutside);
139
140
  };
140
- }, [isOpen]);
141
- useEffect(() => {
142
- if (!isOpen)
143
- return;
144
- const handleKeyDown = (event) => {
145
- if (event.key === 'Escape') {
146
- event.preventDefault();
147
- setIsOpen(false);
148
- }
149
- };
150
- document.addEventListener('keydown', handleKeyDown, true);
151
- return () => document.removeEventListener('keydown', handleKeyDown, true);
152
- }, [isOpen]);
153
- const handleToggle = useCallback(() => {
154
- setIsOpen(prev => !prev);
155
- }, []);
156
- const handleCheckboxChange = useCallback((columnKey) => {
141
+ }, [isOpen, handleClose]);
142
+ const handleCheckboxChange = (columnKey) => {
157
143
  return (ev, data) => {
158
144
  ev.stopPropagation();
159
- onVisibilityChange(columnKey, data.checked === true);
145
+ setColumnVisible(columnKey)(data.checked === true);
160
146
  };
161
- }, [onVisibilityChange]);
162
- const handleDropdownClick = useCallback((e) => {
147
+ };
148
+ const handleDropdownClick = (e) => {
163
149
  e.stopPropagation();
164
- }, []);
165
- const handleSelectAll = useCallback(() => {
166
- columns.forEach(col => {
167
- if (!visibleColumns.has(col.columnId)) {
168
- onVisibilityChange(col.columnId, true);
169
- }
170
- });
171
- }, [columns, visibleColumns, onVisibilityChange]);
172
- const handleClearAll = useCallback(() => {
173
- columns.forEach(col => {
174
- if (!col.required && visibleColumns.has(col.columnId)) {
175
- onVisibilityChange(col.columnId, false);
176
- }
177
- });
178
- }, [columns, visibleColumns, onVisibilityChange]);
179
- const visibleCount = visibleColumns.size;
180
- const totalCount = columns.length;
150
+ };
181
151
  return (_jsxs("div", { className: `${classes.container} ${className || ''}`, children: [_jsxs("button", { type: "button", ref: buttonRef, className: mergeClasses(classes.triggerButton, isOpen && classes.triggerButtonOpen), onClick: handleToggle, "aria-expanded": isOpen, "aria-haspopup": "listbox", children: [_jsx(TableSettingsRegular, { className: classes.buttonIcon }), _jsxs("span", { children: ["Column Visibility (", visibleCount, " of ", totalCount, ")"] }), isOpen ? _jsx(ChevronUpRegular, { className: classes.chevron }) : _jsx(ChevronDownRegular, { className: classes.chevron })] }), isOpen && (_jsxs("div", { className: classes.dropdown, ref: dropdownRef, onClick: handleDropdownClick, children: [_jsxs("div", { className: classes.header, children: ["Select Columns (", visibleCount, " of ", totalCount, ")"] }), _jsx("div", { className: classes.optionsList, children: columns.map(column => (_jsx("div", { className: classes.optionItem, children: _jsx(Checkbox, { label: column.name, checked: visibleColumns.has(column.columnId), onChange: handleCheckboxChange(column.columnId) }) }, column.columnId))) }), _jsxs("div", { className: classes.actions, children: [_jsx("button", { type: "button", className: classes.clearButton, onClick: handleClearAll, children: "Clear All" }), _jsx("button", { type: "button", className: classes.selectAllButton, onClick: handleSelectAll, children: "Select All" })] })] }))] }));
182
152
  };
@@ -1,252 +1,44 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from 'react';
3
- import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
4
- import { Input, Checkbox, Spinner, Avatar, Tooltip } from '@fluentui/react-components';
5
- import { SearchRegular, ArrowUpRegular, ArrowDownRegular, ArrowSortRegular, FilterRegular } from '@fluentui/react-icons';
3
+ import { Tooltip } from '@fluentui/react-components';
4
+ import { ArrowUpRegular, ArrowDownRegular, ArrowSortRegular, FilterRegular } from '@fluentui/react-icons';
5
+ import { useColumnHeaderFilterState } from '@alaarab/ogrid-core';
6
+ import { TextFilterPopover } from './TextFilterPopover';
7
+ import { MultiSelectFilterPopover } from './MultiSelectFilterPopover';
8
+ import { PeopleFilterPopover } from './PeopleFilterPopover';
6
9
  import styles from './ColumnHeaderFilter.module.css';
7
- const SEARCH_DEBOUNCE_MS = 150;
8
- const EMPTY_ARRAY = [];
9
10
  export const ColumnHeaderFilter = React.memo((props) => {
10
- const { columnName, filterType, isSorted = false, isSortedDescending = false, onSort, selectedValues, onFilterChange, options, isLoadingOptions = false, textValue = '', onTextChange, selectedUser, onUserChange, peopleSearch } = props;
11
- const safeSelectedValues = selectedValues ?? EMPTY_ARRAY;
12
- const safeOptions = options ?? EMPTY_ARRAY;
13
- const [isFilterOpen, setIsFilterOpen] = useState(false);
14
- const [tempSelected, setTempSelected] = useState(new Set(safeSelectedValues));
15
- const [tempTextValue, setTempTextValue] = useState(textValue);
16
- const [searchText, setSearchText] = useState('');
17
- const [debouncedSearchText, setDebouncedSearchText] = useState('');
18
- const [peopleSuggestions, setPeopleSuggestions] = useState([]);
19
- const [isPeopleLoading, setIsPeopleLoading] = useState(false);
20
- const [peopleSearchText, setPeopleSearchText] = useState('');
21
- const [popoverPosition, setPopoverPosition] = useState(null);
22
- const headerRef = useRef(null);
23
- const popoverRef = useRef(null);
24
- const searchDebounceRef = useRef();
25
- const peopleSearchTimeoutRef = useRef(undefined);
26
- const peopleInputRef = useRef(null);
27
- useEffect(() => {
28
- if (isFilterOpen) {
29
- setTempSelected(new Set(safeSelectedValues));
30
- setTempTextValue(textValue);
31
- setSearchText('');
32
- setDebouncedSearchText('');
33
- setPeopleSearchText('');
34
- setPeopleSuggestions([]);
35
- if (headerRef.current) {
36
- const rect = headerRef.current.getBoundingClientRect();
37
- setPopoverPosition({
38
- top: rect.bottom + 4,
39
- left: rect.left
40
- });
41
- }
42
- if (filterType === 'people') {
43
- setTimeout(() => {
44
- peopleInputRef.current?.focus();
45
- }, 50);
46
- }
47
- }
48
- else {
49
- setPopoverPosition(null);
50
- }
51
- // eslint-disable-next-line react-hooks/exhaustive-deps
52
- }, [isFilterOpen]);
53
- useEffect(() => {
54
- if (searchDebounceRef.current) {
55
- clearTimeout(searchDebounceRef.current);
56
- }
57
- searchDebounceRef.current = setTimeout(() => {
58
- setDebouncedSearchText(searchText);
59
- }, SEARCH_DEBOUNCE_MS);
60
- return () => {
61
- if (searchDebounceRef.current) {
62
- clearTimeout(searchDebounceRef.current);
63
- }
64
- };
65
- }, [searchText]);
66
- useEffect(() => {
67
- if (!isFilterOpen)
68
- return;
69
- const handleClickOutside = (event) => {
70
- const target = event.target;
71
- const isOutsidePopover = popoverRef.current && !popoverRef.current.contains(target);
72
- const isOutsideHeader = headerRef.current && !headerRef.current.contains(target);
73
- if (isOutsidePopover && isOutsideHeader) {
74
- setIsFilterOpen(false);
75
- }
76
- };
77
- const timeoutId = setTimeout(() => {
78
- document.addEventListener('mousedown', handleClickOutside);
79
- }, 0);
80
- return () => {
81
- clearTimeout(timeoutId);
82
- document.removeEventListener('mousedown', handleClickOutside);
83
- };
84
- }, [isFilterOpen]);
85
- useEffect(() => {
86
- if (!isFilterOpen)
87
- return;
88
- const handleKeyDown = (event) => {
89
- if (event.key === 'Escape' || event.key === 'Esc') {
90
- event.preventDefault();
91
- event.stopPropagation();
92
- setIsFilterOpen(false);
93
- }
94
- };
95
- document.addEventListener('keydown', handleKeyDown, true);
96
- return () => document.removeEventListener('keydown', handleKeyDown, true);
97
- }, [isFilterOpen]);
98
- const filteredOptions = useMemo(() => {
99
- if (!debouncedSearchText.trim()) {
100
- return safeOptions;
101
- }
102
- const searchLower = debouncedSearchText.toLowerCase().trim();
103
- return safeOptions.filter(opt => opt.toLowerCase().includes(searchLower));
104
- }, [safeOptions, debouncedSearchText]);
105
- useEffect(() => {
106
- if (!peopleSearch || !isFilterOpen || filterType !== 'people')
107
- return;
108
- if (peopleSearchTimeoutRef.current) {
109
- window.clearTimeout(peopleSearchTimeoutRef.current);
110
- }
111
- if (!peopleSearchText.trim()) {
112
- setPeopleSuggestions([]);
113
- return;
114
- }
115
- setIsPeopleLoading(true);
116
- peopleSearchTimeoutRef.current = window.setTimeout(async () => {
117
- try {
118
- const results = await peopleSearch(peopleSearchText);
119
- setPeopleSuggestions(results.slice(0, 10));
120
- }
121
- catch (error) {
122
- console.error('Error searching people:', error);
123
- setPeopleSuggestions([]);
124
- }
125
- finally {
126
- setIsPeopleLoading(false);
127
- }
128
- }, 300);
129
- return () => {
130
- if (peopleSearchTimeoutRef.current) {
131
- window.clearTimeout(peopleSearchTimeoutRef.current);
132
- }
133
- };
134
- }, [peopleSearchText, peopleSearch, isFilterOpen, filterType]);
135
- const handleFilterIconClick = useCallback((e) => {
136
- e.stopPropagation();
137
- e.preventDefault();
138
- setIsFilterOpen(prev => !prev);
139
- }, []);
140
- const handlePopoverClick = useCallback((e) => {
141
- e.stopPropagation();
142
- }, []);
143
- const handleInputFocus = useCallback((e) => {
144
- e.stopPropagation();
145
- }, []);
146
- const handleInputMouseDown = useCallback((e) => {
147
- e.stopPropagation();
148
- }, []);
149
- const handleInputClick = useCallback((e) => {
150
- e.stopPropagation();
151
- }, []);
152
- const handleInputKeyDown = useCallback((e) => {
153
- if (e.key !== 'Escape' && e.key !== 'Esc') {
154
- e.stopPropagation();
155
- }
156
- }, []);
157
- const handleSortClick = useCallback((e) => {
158
- e.stopPropagation();
159
- if (onSort) {
160
- onSort();
161
- }
162
- }, [onSort]);
163
- const handleCheckboxChange = useCallback((option, checked) => {
164
- setTempSelected(prev => {
165
- const newSet = new Set(prev);
166
- if (checked) {
167
- newSet.add(option);
168
- }
169
- else {
170
- newSet.delete(option);
171
- }
172
- return newSet;
173
- });
174
- }, []);
175
- const handleSelectAll = useCallback(() => {
176
- setTempSelected(new Set(filteredOptions));
177
- }, [filteredOptions]);
178
- const handleClearSelection = useCallback(() => {
179
- setTempSelected(new Set());
180
- }, []);
181
- const handleApplyMultiSelect = useCallback(() => {
182
- if (onFilterChange) {
183
- onFilterChange(Array.from(tempSelected));
184
- }
185
- setIsFilterOpen(false);
186
- }, [onFilterChange, tempSelected]);
187
- const handleTextApply = useCallback(() => {
188
- if (onTextChange) {
189
- onTextChange(tempTextValue.trim());
190
- }
191
- setIsFilterOpen(false);
192
- }, [onTextChange, tempTextValue]);
193
- const handleTextClear = useCallback(() => {
194
- setTempTextValue('');
195
- }, []);
196
- const handleTextKeyDown = useCallback((event) => {
197
- if (event.key === 'Enter') {
198
- event.preventDefault();
199
- handleTextApply();
200
- }
201
- }, [handleTextApply]);
202
- const handleUserSelect = useCallback((user) => {
203
- if (onUserChange) {
204
- onUserChange(user);
205
- }
206
- setIsFilterOpen(false);
207
- }, [onUserChange]);
208
- const handleClearUser = useCallback(() => {
209
- if (onUserChange) {
210
- onUserChange(undefined);
211
- }
212
- setIsFilterOpen(false);
213
- }, [onUserChange]);
214
- const hasActiveFilter = useMemo(() => {
215
- if (filterType === 'multiSelect') {
216
- return safeSelectedValues.length > 0;
217
- }
218
- if (filterType === 'text') {
219
- return !!textValue.trim();
220
- }
221
- if (filterType === 'people') {
222
- return !!selectedUser;
223
- }
224
- return false;
225
- }, [filterType, safeSelectedValues, textValue, selectedUser]);
11
+ const { columnName, filterType, isSorted = false, isSortedDescending = false, onSort, selectedValues, onFilterChange, options, isLoadingOptions = false, textValue = '', onTextChange, selectedUser, onUserChange, peopleSearch, } = props;
12
+ const state = useColumnHeaderFilterState({
13
+ filterType,
14
+ isSorted,
15
+ isSortedDescending,
16
+ onSort,
17
+ selectedValues,
18
+ onFilterChange,
19
+ options,
20
+ isLoadingOptions,
21
+ textValue,
22
+ onTextChange,
23
+ selectedUser,
24
+ onUserChange,
25
+ peopleSearch,
26
+ });
27
+ const { headerRef, popoverRef, peopleInputRef, isFilterOpen, tempSelected, setTempTextValue, searchText, setSearchText, filteredOptions, peopleSuggestions, isPeopleLoading, peopleSearchText, setPeopleSearchText, hasActiveFilter, popoverPosition, handlers, } = state;
226
28
  const renderPopoverContent = () => {
227
29
  if (filterType === 'multiSelect') {
228
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.popoverSearch, onClick: handlePopoverClick, children: [_jsx(Input, { placeholder: "Search...", value: searchText, onChange: (e, data) => setSearchText(data.value || ''), onFocus: handleInputFocus, onMouseDown: handleInputMouseDown, onClick: handleInputClick, onKeyDown: handleInputKeyDown, autoComplete: "off", className: styles.searchInput, contentBefore: _jsx(SearchRegular, {}) }), _jsxs("div", { className: styles.resultCount, children: [filteredOptions.length, " of ", safeOptions.length, " options"] })] }), _jsxs("div", { className: styles.selectAllRow, onClick: handlePopoverClick, children: [_jsxs("button", { type: "button", className: styles.selectAllButton, onClick: handleSelectAll, children: ["Select All (", filteredOptions.length, ")"] }), _jsx("button", { type: "button", className: styles.selectAllButton, onClick: handleClearSelection, children: "Clear" })] }), _jsx("div", { className: styles.popoverOptions, onClick: handlePopoverClick, children: isLoadingOptions ? (_jsx("div", { className: styles.loadingContainer, children: _jsx(Spinner, { size: "small", label: "Loading..." }) })) : filteredOptions.length === 0 ? (_jsx("div", { className: styles.noResults, children: "No options found" })) : (filteredOptions.map(option => (_jsx("div", { className: styles.popoverOption, children: _jsx(Checkbox, { label: option, checked: tempSelected.has(option), onChange: (ev, data) => {
229
- ev.stopPropagation();
230
- handleCheckboxChange(option, data.checked === true);
231
- } }) }, option)))) }), _jsxs("div", { className: styles.popoverActions, onClick: handlePopoverClick, children: [_jsx("button", { type: "button", className: styles.clearButton, onClick: handleClearSelection, children: "Clear" }), _jsx("button", { type: "button", className: styles.applyButton, onClick: handleApplyMultiSelect, children: "Apply" })] })] }));
30
+ return (_jsx(MultiSelectFilterPopover, { searchText: searchText, onSearchChange: setSearchText, options: options ?? [], filteredOptions: filteredOptions, selected: tempSelected, onOptionToggle: handlers.handleCheckboxChange, onSelectAll: handlers.handleSelectAll, onClearSelection: handlers.handleClearSelection, onApply: handlers.handleApplyMultiSelect, isLoading: isLoadingOptions, onPopoverClick: handlers.handlePopoverClick, onInputFocus: handlers.handleInputFocus, onInputMouseDown: handlers.handleInputMouseDown, onInputClick: handlers.handleInputClick, onInputKeyDown: handlers.handleInputKeyDown }));
232
31
  }
233
32
  if (filterType === 'text') {
234
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.popoverSearch, onClick: handlePopoverClick, children: _jsx(Input, { placeholder: "Enter search term...", value: tempTextValue, onChange: (e, data) => setTempTextValue(data.value || ''), onKeyDown: (e) => {
235
- handleInputKeyDown(e);
236
- handleTextKeyDown(e);
237
- }, onFocus: handleInputFocus, onMouseDown: handleInputMouseDown, onClick: handleInputClick, autoComplete: "off", className: styles.searchInput, contentBefore: _jsx(SearchRegular, {}) }) }), _jsxs("div", { className: styles.popoverActions, onClick: handlePopoverClick, children: [_jsx("button", { type: "button", className: styles.clearButton, onClick: handleTextClear, disabled: !tempTextValue, children: "Clear" }), _jsx("button", { type: "button", className: styles.applyButton, onClick: handleTextApply, children: "Apply" })] })] }));
33
+ return (_jsx(TextFilterPopover, { value: state.tempTextValue, onValueChange: setTempTextValue, onApply: handlers.handleTextApply, onClear: handlers.handleTextClear, onPopoverClick: handlers.handlePopoverClick, onInputFocus: handlers.handleInputFocus, onInputMouseDown: handlers.handleInputMouseDown, onInputClick: handlers.handleInputClick, onInputKeyDown: handlers.handleInputKeyDown }));
238
34
  }
239
35
  if (filterType === 'people') {
240
- return (_jsxs(_Fragment, { children: [selectedUser && (_jsxs("div", { className: styles.selectedUserSection, onClick: handlePopoverClick, children: [_jsx("div", { className: styles.selectedUserLabel, children: "Currently filtered by:" }), _jsxs("div", { className: styles.selectedUser, children: [_jsxs("div", { className: styles.userInfo, children: [_jsx(Avatar, { name: selectedUser.displayName, image: { src: selectedUser.photo }, size: 32 }), _jsxs("div", { className: styles.userText, children: [_jsx("div", { children: selectedUser.displayName }), _jsx("div", { className: styles.userSecondary, children: selectedUser.email })] })] }), _jsx("button", { type: "button", className: styles.removeUserButton, onClick: handleClearUser, "aria-label": "Remove filter", children: _jsx(FilterRegular, {}) })] })] })), _jsx("div", { className: styles.popoverSearch, onClick: handlePopoverClick, children: _jsxs("div", { className: styles.nativeInputWrapper, children: [_jsx(SearchRegular, { className: styles.nativeInputIcon }), _jsx("input", { ref: peopleInputRef, type: "text", placeholder: "Search for a person...", value: peopleSearchText, onChange: (e) => setPeopleSearchText(e.target.value), onFocus: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), autoComplete: "off", className: styles.nativeInput })] }) }), _jsx("div", { className: styles.popoverOptions, onClick: handlePopoverClick, children: isPeopleLoading && peopleSearchText.trim() ? (_jsx("div", { className: styles.loadingContainer, children: _jsx(Spinner, { size: "small", label: "Searching..." }) })) : peopleSuggestions.length === 0 && peopleSearchText.trim() ? (_jsx("div", { className: styles.noResults, children: "No results found" })) : peopleSearchText.trim() ? (peopleSuggestions.map(user => (_jsx("div", { className: styles.personOption, onClick: (e) => {
241
- e.stopPropagation();
242
- handleUserSelect(user);
243
- }, children: _jsxs("div", { className: styles.userInfo, children: [_jsx(Avatar, { name: user.displayName, image: { src: user.photo }, size: 32 }), _jsxs("div", { className: styles.userText, children: [_jsx("div", { children: user.displayName }), _jsx("div", { className: styles.userSecondary, children: user.email })] })] }) }, user.id || user.email || user.displayName)))) : (_jsx("div", { className: styles.noResults, children: "Type to search..." })) }), selectedUser && (_jsx("div", { className: styles.popoverActions, onClick: handlePopoverClick, children: _jsx("button", { type: "button", className: styles.clearButton, onClick: handleClearUser, children: "Clear Filter" }) }))] }));
36
+ return (_jsx(PeopleFilterPopover, { selectedUser: selectedUser, searchText: peopleSearchText, onSearchChange: setPeopleSearchText, suggestions: peopleSuggestions, isLoading: isPeopleLoading, onUserSelect: handlers.handleUserSelect, onClearUser: handlers.handleClearUser, onPopoverClick: handlers.handlePopoverClick, inputRef: peopleInputRef }));
244
37
  }
245
38
  return null;
246
39
  };
247
- return (_jsxs("div", { className: styles.columnHeader, ref: headerRef, children: [_jsx("div", { className: styles.headerContent, children: _jsx(Tooltip, { content: columnName, relationship: "label", withArrow: true, children: _jsx("span", { className: styles.columnNameTooltipTrigger, children: _jsx("span", { className: styles.columnName, "data-header-label": true, children: columnName }) }) }) }), _jsxs("div", { className: styles.headerActions, children: [onSort && (_jsx("button", { type: "button", className: `${styles.sortIcon} ${isSorted ? styles.sortActive : ''}`, onClick: handleSortClick, "aria-label": `Sort by ${columnName}`, title: isSorted ? (isSortedDescending ? 'Sorted descending' : 'Sorted ascending') : 'Sort', children: isSorted ? (isSortedDescending ? _jsx(ArrowDownRegular, {}) : _jsx(ArrowUpRegular, {})) : (_jsx(ArrowSortRegular, {})) })), filterType !== 'none' && (_jsxs("button", { type: "button", className: `${styles.filterIcon} ${hasActiveFilter ? styles.filterActive : ''} ${isFilterOpen ? styles.filterOpen : ''}`, onClick: handleFilterIconClick, "aria-label": `Filter ${columnName}`, title: `Filter ${columnName}`, children: [_jsx(FilterRegular, {}), hasActiveFilter && _jsx("span", { className: styles.filterBadge })] }))] }), isFilterOpen && filterType !== 'none' && popoverPosition && (_jsxs("div", { className: styles.filterPopover, ref: popoverRef, onClick: handlePopoverClick, style: {
248
- top: `${popoverPosition.top}px`,
249
- left: `${popoverPosition.left}px`
250
- }, children: [_jsxs("div", { className: styles.popoverHeader, children: ["Filter: ", columnName] }), renderPopoverContent()] }))] }));
40
+ return (_jsxs("div", { className: styles.columnHeader, ref: headerRef, children: [_jsx("div", { className: styles.headerContent, children: _jsx(Tooltip, { content: columnName, relationship: "label", withArrow: true, children: _jsx("span", { className: styles.columnNameTooltipTrigger, children: _jsx("span", { className: styles.columnName, "data-header-label": true, children: columnName }) }) }) }), _jsxs("div", { className: styles.headerActions, children: [onSort && (_jsx("button", { type: "button", className: `${styles.sortIcon} ${isSorted ? styles.sortActive : ''}`, onClick: handlers.handleSortClick, "aria-label": `Sort by ${columnName}`, title: isSorted ? (isSortedDescending ? 'Sorted descending' : 'Sorted ascending') : 'Sort', children: isSorted ? (isSortedDescending ? _jsx(ArrowDownRegular, {}) : _jsx(ArrowUpRegular, {})) : (_jsx(ArrowSortRegular, {})) })), filterType !== 'none' && (_jsxs("button", { type: "button", className: `${styles.filterIcon} ${hasActiveFilter ? styles.filterActive : ''} ${isFilterOpen ? styles.filterOpen : ''}`, onClick: handlers.handleFilterIconClick, "aria-label": `Filter ${columnName}`, title: `Filter ${columnName}`, children: [_jsx(FilterRegular, {}), hasActiveFilter && _jsx("span", { className: styles.filterBadge })] }))] }), isFilterOpen && filterType !== 'none' && (_jsxs("div", { className: styles.filterPopover, ref: popoverRef, onClick: handlers.handlePopoverClick, style: popoverPosition
41
+ ? { top: `${popoverPosition.top}px`, left: `${popoverPosition.left}px` }
42
+ : { top: 0, left: 0 }, children: [_jsxs("div", { className: styles.popoverHeader, children: ["Filter: ", columnName] }), renderPopoverContent()] }))] }));
251
43
  });
252
44
  ColumnHeaderFilter.displayName = 'ColumnHeaderFilter';
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Input, Checkbox, Spinner } from '@fluentui/react-components';
3
+ import { SearchRegular } from '@fluentui/react-icons';
4
+ import styles from './ColumnHeaderFilter.module.css';
5
+ export const MultiSelectFilterPopover = ({ searchText, onSearchChange, options, filteredOptions, selected, onOptionToggle, onSelectAll, onClearSelection, onApply, isLoading, onPopoverClick, onInputFocus, onInputMouseDown, onInputClick, onInputKeyDown, }) => (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.popoverSearch, onClick: onPopoverClick, children: [_jsx(Input, { placeholder: "Search...", value: searchText, onChange: (e, data) => onSearchChange(data.value ?? ''), onFocus: onInputFocus, onMouseDown: onInputMouseDown, onClick: onInputClick, onKeyDown: onInputKeyDown, autoComplete: "off", className: styles.searchInput, contentBefore: _jsx(SearchRegular, {}) }), _jsxs("div", { className: styles.resultCount, children: [filteredOptions.length, " of ", options.length, " options"] })] }), _jsxs("div", { className: styles.selectAllRow, onClick: onPopoverClick, children: [_jsxs("button", { type: "button", className: styles.selectAllButton, onClick: onSelectAll, children: ["Select All (", filteredOptions.length, ")"] }), _jsx("button", { type: "button", className: styles.selectAllButton, onClick: onClearSelection, children: "Clear" })] }), _jsx("div", { className: styles.popoverOptions, onClick: onPopoverClick, children: isLoading ? (_jsx("div", { className: styles.loadingContainer, children: _jsx(Spinner, { size: "small", label: "Loading..." }) })) : filteredOptions.length === 0 ? (_jsx("div", { className: styles.noResults, children: "No options found" })) : (filteredOptions.map((option) => (_jsx("div", { className: styles.popoverOption, children: _jsx(Checkbox, { label: option, checked: selected.has(option), onChange: (ev, data) => {
6
+ ev.stopPropagation();
7
+ onOptionToggle(option, data.checked === true);
8
+ } }) }, option)))) }), _jsxs("div", { className: styles.popoverActions, onClick: onPopoverClick, children: [_jsx("button", { type: "button", className: styles.clearButton, onClick: onClearSelection, children: "Clear" }), _jsx("button", { type: "button", className: styles.applyButton, onClick: onApply, children: "Apply" })] })] }));
9
+ MultiSelectFilterPopover.displayName = 'MultiSelectFilterPopover';
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Spinner, Avatar } from '@fluentui/react-components';
3
+ import { SearchRegular, FilterRegular } from '@fluentui/react-icons';
4
+ import styles from './ColumnHeaderFilter.module.css';
5
+ export const PeopleFilterPopover = ({ selectedUser, searchText, onSearchChange, suggestions, isLoading, onUserSelect, onClearUser, onPopoverClick, inputRef, }) => (_jsxs(_Fragment, { children: [selectedUser && (_jsxs("div", { className: styles.selectedUserSection, onClick: onPopoverClick, children: [_jsx("div", { className: styles.selectedUserLabel, children: "Currently filtered by:" }), _jsxs("div", { className: styles.selectedUser, children: [_jsxs("div", { className: styles.userInfo, children: [_jsx(Avatar, { name: selectedUser.displayName, image: { src: selectedUser.photo }, size: 32 }), _jsxs("div", { className: styles.userText, children: [_jsx("div", { children: selectedUser.displayName }), _jsx("div", { className: styles.userSecondary, children: selectedUser.email })] })] }), _jsx("button", { type: "button", className: styles.removeUserButton, onClick: onClearUser, "aria-label": "Remove filter", children: _jsx(FilterRegular, {}) })] })] })), _jsx("div", { className: styles.popoverSearch, onClick: onPopoverClick, children: _jsxs("div", { className: styles.nativeInputWrapper, children: [_jsx(SearchRegular, { className: styles.nativeInputIcon }), _jsx("input", { ref: inputRef, type: "text", placeholder: "Search for a person...", value: searchText, onChange: (e) => onSearchChange(e.target.value), onFocus: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), autoComplete: "off", className: styles.nativeInput })] }) }), _jsx("div", { className: styles.popoverOptions, onClick: onPopoverClick, children: isLoading && searchText.trim() ? (_jsx("div", { className: styles.loadingContainer, children: _jsx(Spinner, { size: "small", label: "Searching..." }) })) : suggestions.length === 0 && searchText.trim() ? (_jsx("div", { className: styles.noResults, children: "No results found" })) : searchText.trim() ? (suggestions.map((user) => (_jsx("div", { className: styles.personOption, onClick: (e) => {
6
+ e.stopPropagation();
7
+ onUserSelect(user);
8
+ }, children: _jsxs("div", { className: styles.userInfo, children: [_jsx(Avatar, { name: user.displayName, image: { src: user.photo }, size: 32 }), _jsxs("div", { className: styles.userText, children: [_jsx("div", { children: user.displayName }), _jsx("div", { className: styles.userSecondary, children: user.email })] })] }) }, user.id ?? user.email ?? user.displayName)))) : (_jsx("div", { className: styles.noResults, children: "Type to search..." })) }), selectedUser && (_jsx("div", { className: styles.popoverActions, onClick: onPopoverClick, children: _jsx("button", { type: "button", className: styles.clearButton, onClick: onClearUser, children: "Clear Filter" }) }))] }));
9
+ PeopleFilterPopover.displayName = 'PeopleFilterPopover';
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Input } from '@fluentui/react-components';
3
+ import { SearchRegular } from '@fluentui/react-icons';
4
+ import styles from './ColumnHeaderFilter.module.css';
5
+ export const TextFilterPopover = ({ value, onValueChange, onApply, onClear, onPopoverClick, onInputFocus, onInputMouseDown, onInputClick, onInputKeyDown, }) => (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.popoverSearch, onClick: onPopoverClick, children: _jsx(Input, { placeholder: "Enter search term...", value: value, onChange: (e, data) => onValueChange(data.value ?? ''), onKeyDown: (e) => {
6
+ onInputKeyDown(e);
7
+ if (e.key === 'Enter') {
8
+ e.preventDefault();
9
+ onApply();
10
+ }
11
+ }, onFocus: onInputFocus, onMouseDown: onInputMouseDown, onClick: onInputClick, autoComplete: "off", className: styles.searchInput, contentBefore: _jsx(SearchRegular, {}) }) }), _jsxs("div", { className: styles.popoverActions, onClick: onPopoverClick, children: [_jsx("button", { type: "button", className: styles.clearButton, onClick: onClear, disabled: !value, children: "Clear" }), _jsx("button", { type: "button", className: styles.applyButton, onClick: onApply, children: "Apply" })] })] }));
12
+ TextFilterPopover.displayName = 'TextFilterPopover';