@react-fabric/data 1.0.0-beta.46 → 1.0.0-beta.48

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.
@@ -30,7 +30,7 @@ var AddColumn = function AddColumn(_ref) {
30
30
  var columns = _ref.columns,
31
31
  onClick = _ref.onClick;
32
32
  return /*#__PURE__*/jsxRuntime.jsx("div", {
33
- className: "group font-medium w-6 flex flex-nowrap text-start items-end",
33
+ className: "group font-medium w-6 flex flex-nowrap text-start items-center",
34
34
  children: /*#__PURE__*/jsxRuntime.jsxs(core.Dropdown, {
35
35
  placement: "bottom-end",
36
36
  children: [/*#__PURE__*/jsxRuntime.jsx(core.Icon, {
@@ -0,0 +1,97 @@
1
+ 'use strict';
2
+
3
+ var _slicedToArray = require('@babel/runtime/helpers/slicedToArray');
4
+ var core = require('@react-fabric/core');
5
+ var form = require('@react-fabric/form');
6
+ var utilities = require('@react-fabric/utilities');
7
+ var react = require('react');
8
+ var useFilteredList = require('../hooks/useFilteredList.js');
9
+ var jsxRuntime = require('react/jsx-runtime');
10
+
11
+ var ListFilter = function ListFilter(_ref) {
12
+ var _ref$list = _ref.list,
13
+ list = _ref$list === void 0 ? utilities.EMPTY_ARRAY : _ref$list,
14
+ filter = _ref.filter,
15
+ onFilter = _ref.onFilter;
16
+ var _useState = react.useState(new Set(filter)),
17
+ _useState2 = _slicedToArray(_useState, 2),
18
+ selected = _useState2[0],
19
+ setSelected = _useState2[1];
20
+ var _useFilteredList = useFilteredList.useFilteredList(list, function (item, query) {
21
+ var _item$id;
22
+ return utilities.matchString((_item$id = item.id) !== null && _item$id !== void 0 ? _item$id : item, query);
23
+ }),
24
+ filteredList = _useFilteredList.filteredList,
25
+ query = _useFilteredList.query,
26
+ onSearch = _useFilteredList.onSearch;
27
+ return /*#__PURE__*/jsxRuntime.jsxs("div", {
28
+ className: "flex flex-col",
29
+ children: [/*#__PURE__*/jsxRuntime.jsx("div", {
30
+ children: /*#__PURE__*/jsxRuntime.jsx(form.Search, {
31
+ autoFocus: true,
32
+ value: query,
33
+ onSearch: onSearch,
34
+ searchOnChange: true
35
+ })
36
+ }), /*#__PURE__*/jsxRuntime.jsxs("div", {
37
+ className: "flex-1 overflow-auto scroll-thin",
38
+ children: [filteredList.map(function (v, idx) {
39
+ var _v$id, _v$label;
40
+ return /*#__PURE__*/jsxRuntime.jsx("div", {
41
+ className: "flex gap-x-1 text-xs items-center",
42
+ children: /*#__PURE__*/jsxRuntime.jsx(form.Checkbox, {
43
+ checked: selected.has((_v$id = v.id) !== null && _v$id !== void 0 ? _v$id : v),
44
+ onChange: function onChange(b) {
45
+ var _v$id2, _v$id3;
46
+ var set = new Set(Array.from(selected.values()));
47
+ b ? set.add((_v$id2 = v.id) !== null && _v$id2 !== void 0 ? _v$id2 : v) : set["delete"]((_v$id3 = v.id) !== null && _v$id3 !== void 0 ? _v$id3 : v);
48
+ setSelected(set);
49
+ },
50
+ label: /*#__PURE__*/jsxRuntime.jsx("span", {
51
+ className: "truncate",
52
+ children: (_v$label = v.label) !== null && _v$label !== void 0 ? _v$label : v
53
+ })
54
+ })
55
+ }, idx);
56
+ }), filteredList.length === 0 && /*#__PURE__*/jsxRuntime.jsx("span", {
57
+ className: "text-muted text-xs",
58
+ children: "No items found"
59
+ })]
60
+ }), /*#__PURE__*/jsxRuntime.jsx("div", {
61
+ className: "flex justify-end",
62
+ children: /*#__PURE__*/jsxRuntime.jsx(core.DropdownDismiss, {
63
+ children: /*#__PURE__*/jsxRuntime.jsx(core.Button, {
64
+ size: "xs",
65
+ onClick: function onClick() {
66
+ return onFilter === null || onFilter === void 0 ? void 0 : onFilter(Array.from(selected));
67
+ },
68
+ children: "Apply"
69
+ })
70
+ })
71
+ })]
72
+ });
73
+ };
74
+ var ColumnFilters = function ColumnFilters(_ref2) {
75
+ var type = _ref2.type,
76
+ list = _ref2.list,
77
+ filter = _ref2.filter,
78
+ onFilter = _ref2.onFilter;
79
+ return /*#__PURE__*/jsxRuntime.jsxs("div", {
80
+ className: "flex flex-col min-w-[16rem]",
81
+ children: [list && /*#__PURE__*/jsxRuntime.jsx(ListFilter, {
82
+ list: list,
83
+ filter: filter,
84
+ onFilter: onFilter
85
+ }), !list && type === "string" && /*#__PURE__*/jsxRuntime.jsx(form.Input, {
86
+ autoFocus: true,
87
+ allowClear: true,
88
+ value: filter,
89
+ onEnterPressed: function onEnterPressed(e) {
90
+ var _e$target$value;
91
+ return onFilter((_e$target$value = e.target.value) !== null && _e$target$value !== void 0 ? _e$target$value : "");
92
+ }
93
+ })]
94
+ });
95
+ };
96
+
97
+ exports.ColumnFilters = ColumnFilters;
@@ -2,13 +2,16 @@
2
2
 
3
3
  var _toConsumableArray = require('@babel/runtime/helpers/toConsumableArray');
4
4
  var core = require('@react-fabric/core');
5
+ var utilities = require('@react-fabric/utilities');
5
6
  var react = require('react');
7
+ var ColumnFilters = require('./ColumnFilters.js');
6
8
  var Context = require('./Context.js');
7
9
  var types = require('./types.js');
8
10
  var jsxRuntime = require('react/jsx-runtime');
9
11
 
10
12
  var HeaderCell = function HeaderCell(_ref) {
11
13
  var id = _ref.id,
14
+ dataType = _ref.dataType,
12
15
  label = _ref.label,
13
16
  icon = _ref.icon,
14
17
  iconBg = _ref.iconBg,
@@ -16,14 +19,18 @@ var HeaderCell = function HeaderCell(_ref) {
16
19
  rtlFlip = _ref.rtlFlip,
17
20
  actions = _ref.actions;
18
21
  _ref.locked;
19
- var tooltip = _ref.tooltip,
22
+ var filter = _ref.filter,
23
+ filterable = _ref.filterable,
24
+ filterOptions = _ref.filterOptions,
25
+ tooltip = _ref.tooltip,
20
26
  sortable = _ref.sortable,
21
27
  resizable = _ref.resizable,
22
28
  _width = _ref.width,
23
29
  maxWidth = _ref.maxWidth,
24
30
  minWidth = _ref.minWidth,
25
31
  sort = _ref.sort,
26
- onSort = _ref.onSort;
32
+ onSort = _ref.onSort,
33
+ _onFilter = _ref.onFilter;
27
34
  var _useTableContext = Context.useTableContext(),
28
35
  startResize = _useTableContext.startResize,
29
36
  widths = _useTableContext.widths;
@@ -39,6 +46,21 @@ var HeaderCell = function HeaderCell(_ref) {
39
46
  }, [onSort, sort, sortable]);
40
47
  var menus = react.useMemo(function () {
41
48
  var ret = [];
49
+ if (filterable && (dataType !== null && dataType !== void 0 ? dataType : filterOptions)) {
50
+ ret.push( /*#__PURE__*/jsxRuntime.jsx(core.Menu, {
51
+ label: "Filter",
52
+ icon: filter ? core.CoreIcons.tick : "",
53
+ trigger: "click",
54
+ children: /*#__PURE__*/jsxRuntime.jsx(ColumnFilters.ColumnFilters, {
55
+ type: dataType,
56
+ list: filterOptions,
57
+ filter: filter,
58
+ onFilter: function onFilter(value) {
59
+ return _onFilter === null || _onFilter === void 0 ? void 0 : _onFilter(id, value);
60
+ }
61
+ })
62
+ }, "filters"));
63
+ }
42
64
  if (sortable) {
43
65
  ret.push( /*#__PURE__*/jsxRuntime.jsx(core.MenuItem, {
44
66
  label: "Sort ascending",
@@ -54,14 +76,23 @@ var HeaderCell = function HeaderCell(_ref) {
54
76
  }
55
77
  }, "sort-desc"));
56
78
  }
57
- if (sortable && actions) {
58
- ret.push( /*#__PURE__*/jsxRuntime.jsx(core.Divider, {}));
59
- }
60
79
  if (actions) {
61
- ret.push.apply(ret, _toConsumableArray(actions));
80
+ var _ret;
81
+ ret.push( /*#__PURE__*/jsxRuntime.jsx(core.Divider, {}, "div"));
82
+ (_ret = ret).push.apply(_ret, _toConsumableArray(actions));
83
+ }
84
+ if (ret.length === 1) {
85
+ ret = [/*#__PURE__*/jsxRuntime.jsx(ColumnFilters.ColumnFilters, {
86
+ type: dataType,
87
+ list: filterOptions,
88
+ filter: filter,
89
+ onFilter: function onFilter(value) {
90
+ return _onFilter === null || _onFilter === void 0 ? void 0 : _onFilter(id, value);
91
+ }
92
+ }, "filters")];
62
93
  }
63
94
  return ret;
64
- }, [sortable, actions]);
95
+ }, [sortable, actions, _onFilter, handleSort]);
65
96
  return /*#__PURE__*/jsxRuntime.jsxs("div", {
66
97
  role: "none",
67
98
  "data-id": id,
@@ -104,6 +135,15 @@ var HeaderCell = function HeaderCell(_ref) {
104
135
  className: "text-sm",
105
136
  children: menus
106
137
  })
138
+ }), filterable && !utilities.isEmpty(filter) && /*#__PURE__*/jsxRuntime.jsx(core.Button, {
139
+ size: "sm",
140
+ variant: "link",
141
+ "aria-label": "clear filter",
142
+ stopPropagation: true,
143
+ icon: core.CoreIcons.funnelRemove,
144
+ onClick: function onClick() {
145
+ return _onFilter === null || _onFilter === void 0 ? void 0 : _onFilter(id);
146
+ }
107
147
  }), resizable && /*#__PURE__*/jsxRuntime.jsx("div", {
108
148
  role: "none",
109
149
  className: "cursor-col-resize border-e bg-transparent border-tint-200 w-[5px] self-stretch",
@@ -37,6 +37,7 @@ var Table = function Table(_ref) {
37
37
  canExpand = _ref.canExpand,
38
38
  children = _ref.children,
39
39
  onSort = _ref.onSort,
40
+ onFilter = _ref.onFilter,
40
41
  onScroll = _ref.onScroll,
41
42
  onRowClick = _ref.onRowClick,
42
43
  onCheckedChanged = _ref.onCheckedChanged;
@@ -136,20 +137,23 @@ var Table = function Table(_ref) {
136
137
  }), (_state$start = state.start) === null || _state$start === void 0 ? void 0 : _state$start.map(function (col, idx) {
137
138
  return /*#__PURE__*/jsxRuntime.jsx(HeaderCell.HeaderCell, _objectSpread(_objectSpread({}, col), {}, {
138
139
  sort: sort,
139
- onSort: onSort
140
+ onSort: onSort,
141
+ onFilter: onFilter
140
142
  }), idx);
141
143
  })]
142
144
  }), (_state$cols = state.cols) === null || _state$cols === void 0 ? void 0 : _state$cols.map(function (col, idx) {
143
145
  return /*#__PURE__*/jsxRuntime.jsx(HeaderCell.HeaderCell, _objectSpread(_objectSpread({}, col), {}, {
144
146
  sort: sort,
145
- onSort: onSort
147
+ onSort: onSort,
148
+ onFilter: onFilter
146
149
  }), idx);
147
150
  }), /*#__PURE__*/jsxRuntime.jsxs("div", {
148
151
  className: wrapperEnd,
149
152
  children: [(_state$end = state.end) === null || _state$end === void 0 ? void 0 : _state$end.map(function (col, idx) {
150
153
  return /*#__PURE__*/jsxRuntime.jsx(HeaderCell.HeaderCell, _objectSpread(_objectSpread({}, col), {}, {
151
154
  sort: sort,
152
- onSort: onSort
155
+ onSort: onSort,
156
+ onFilter: onFilter
153
157
  }), idx);
154
158
  }), hideableColumns && /*#__PURE__*/jsxRuntime.jsx(AddColumn.AddColumn, {
155
159
  onClick: onHide,
@@ -194,7 +198,7 @@ var Table = function Table(_ref) {
194
198
  })]
195
199
  })]
196
200
  }, key) : /*#__PURE__*/jsxRuntime.jsx(BodyRow.BodyRow, {
197
- rowKey: key,
201
+ rowKey: key.toString(),
198
202
  index: index,
199
203
  checkableRows: checkableRows,
200
204
  checked: keyProperty && checkState.checked.includes(data[keyProperty]) ? 1 : 0,
@@ -19,7 +19,7 @@ var TreeNode = function TreeNode(_ref) {
19
19
  defaultLeafIcon = _ref.defaultLeafIcon,
20
20
  defaultNodeIcon = _ref.defaultNodeIcon,
21
21
  onToggle = _ref.onToggle,
22
- _onClick = _ref.onClick,
22
+ onClick = _ref.onClick,
23
23
  onSelect = _ref.onSelect,
24
24
  onChecked = _ref.onChecked,
25
25
  children = _ref.children,
@@ -63,7 +63,7 @@ var TreeNode = function TreeNode(_ref) {
63
63
  return /*#__PURE__*/jsxRuntime.jsxs("div", {
64
64
  role: "treeitem",
65
65
  "aria-selected": node.selected,
66
- className: classNames("flex flex-nowrap items-center", node.leaf ? leafClassName : nodeClassName),
66
+ className: classNames("flex flex-nowrap items-center", node.className, node.leaf ? leafClassName : nodeClassName),
67
67
  "data-testid": node["data-testid"],
68
68
  "data-test-value": node["data-test-value"],
69
69
  children: [filler, node.loading && /*#__PURE__*/jsxRuntime.jsx("div", {
@@ -93,10 +93,10 @@ var TreeNode = function TreeNode(_ref) {
93
93
  })
94
94
  }), !node.empty && /*#__PURE__*/jsxRuntime.jsxs("div", {
95
95
  role: "none",
96
- className: classNames("group/tool data-[selected]:bg-primary-100 flex flex-nowrap flex-1 overflow-hidden select-none", node.childSelected && "font-medium", (canSelect || _onClick) && "hover:bg-primary-50 cursor-pointer"),
96
+ className: classNames("group/tool data-[selected]:bg-primary-100 flex flex-nowrap flex-1 overflow-hidden select-none", node.childSelected && "font-medium", node.disabled && "text-muted opacity-65", !node.disabled && (canSelect || onClick) && "hover:bg-primary-50 cursor-pointer"),
97
97
  "data-selected": node.selected,
98
- onClick: function onClick() {
99
- _onClick === null || _onClick === void 0 || _onClick(node.id, node.data);
98
+ onClick: node.disabled ? undefined : function () {
99
+ onClick === null || onClick === void 0 || onClick(node.id, node.data);
100
100
  canSelect ? onSelect(node.id) : canCheck ? onChecked(node.id) : onToggle(node.id);
101
101
  },
102
102
  children: [nodeIcon && /*#__PURE__*/jsxRuntime.jsxs("div", {
@@ -85,7 +85,8 @@ var _VirtualGallery = function _VirtualGallery(_ref) {
85
85
  },
86
86
  estimateSize: function estimateSize() {
87
87
  return height;
88
- }
88
+ },
89
+ overscan: 2
89
90
  });
90
91
  var virtualItems = virtualizer.getVirtualItems();
91
92
  var currentIndex = react.useRef(0);
@@ -127,14 +128,15 @@ var _VirtualGallery = function _VirtualGallery(_ref) {
127
128
  var _virtualItems$2, _virtualItems$slice$p;
128
129
  var firstItem = (_virtualItems$2 = virtualItems[0]) === null || _virtualItems$2 === void 0 ? void 0 : _virtualItems$2.index;
129
130
  var lastItem = virtualItems === null || virtualItems === void 0 || (_virtualItems$slice$p = virtualItems.slice(-1).pop()) === null || _virtualItems$slice$p === void 0 ? void 0 : _virtualItems$slice$p.index;
131
+ var firstIndex = firstItem * columnCount;
130
132
  var lastIndex = (lastItem !== null && lastItem !== void 0 ? lastItem : 0) * columnCount + columnCount;
131
- var canLoadPrevPage = firstItem !== undefined && items[firstItem * columnCount] === null;
133
+ var canLoadPrevPage = firstItem !== undefined && items[firstIndex] === null;
132
134
  var canLoadNextPage = lastItem !== undefined && lastIndex >= count - 1 && count < total;
133
135
  var db = utilities.debounce(onLoadMore);
134
136
  if (canLoadPrevPage) {
135
- db(firstItem);
137
+ db(firstIndex);
136
138
  } else if (!loading && canLoadNextPage) {
137
- db(lastItem + 1);
139
+ db(lastIndex + 1);
138
140
  }
139
141
  return function () {
140
142
  return db.cancel();
@@ -175,7 +177,7 @@ var _VirtualGallery = function _VirtualGallery(_ref) {
175
177
  return /*#__PURE__*/jsxRuntime.jsx("div", _objectSpread(_objectSpread({
176
178
  ref: scrollerRef,
177
179
  onScroll: handleScroll,
178
- className: classNames("area-content bg-base overflow-auto contain-strict")
180
+ className: classNames("area-content bg-base overflow-auto")
179
181
  }, aria), {}, {
180
182
  children: /*#__PURE__*/jsxRuntime.jsxs("div", {
181
183
  className: classNames("inline-flex flex-nowrap gap-2 justify-center min-h-full min-w-full"),
@@ -195,12 +197,11 @@ var _VirtualGallery = function _VirtualGallery(_ref) {
195
197
  }
196
198
  }), virtualItems.map(function (_ref3) {
197
199
  var key = _ref3.key,
198
- index = _ref3.index,
199
- measureElement = _ref3.measureElement;
200
+ index = _ref3.index;
200
201
  return /*#__PURE__*/jsxRuntime.jsx("div", {
201
202
  className: "flex justify-start",
202
203
  "data-index": index,
203
- ref: measureElement,
204
+ ref: virtualizer.measureElement,
204
205
  children: columnMap.map(function (col) {
205
206
  return /*#__PURE__*/jsxRuntime.jsx("div", {
206
207
  "data-index": index * columnCount + col,
@@ -56,6 +56,7 @@ var _VirtualList = function _VirtualList(_ref) {
56
56
  var scrollerRef = react.useRef(null);
57
57
  var virtualizer = reactVirtual.useVirtualizer({
58
58
  count: count,
59
+ overscan: 2,
59
60
  horizontal: orientation === "horizontal",
60
61
  getScrollElement: function getScrollElement() {
61
62
  return scrollerRef.current;
@@ -164,7 +165,7 @@ var _VirtualList = function _VirtualList(_ref) {
164
165
  return /*#__PURE__*/jsxRuntime.jsx("div", _objectSpread(_objectSpread({
165
166
  ref: scrollerRef,
166
167
  onScroll: handleScroll,
167
- className: classNames("area-content bg-base overflow-auto contain-strict")
168
+ className: classNames("area-content bg-base overflow-auto")
168
169
  }, aria), {}, {
169
170
  children: /*#__PURE__*/jsxRuntime.jsxs("div", {
170
171
  className: classNames("inline-flex flex-nowrap gap-2 justify-center min-h-full min-w-full", orientation === "horizontal" && "flex-col"),
@@ -30,7 +30,7 @@ var AddColumn = function AddColumn(_ref) {
30
30
  var columns = _ref.columns,
31
31
  onClick = _ref.onClick;
32
32
  return /*#__PURE__*/jsx("div", {
33
- className: "group font-medium w-6 flex flex-nowrap text-start items-end",
33
+ className: "group font-medium w-6 flex flex-nowrap text-start items-center",
34
34
  children: /*#__PURE__*/jsxs(Dropdown, {
35
35
  placement: "bottom-end",
36
36
  children: [/*#__PURE__*/jsx(Icon, {
@@ -0,0 +1,97 @@
1
+ 'use client';
2
+
3
+ import _slicedToArray from '@babel/runtime/helpers/slicedToArray';
4
+ import { DropdownDismiss, Button } from '@react-fabric/core';
5
+ import { Input, Search, Checkbox } from '@react-fabric/form';
6
+ import { matchString, EMPTY_ARRAY } from '@react-fabric/utilities';
7
+ import { useState } from 'react';
8
+ import { useFilteredList } from '../hooks/useFilteredList.js';
9
+ import { jsxs, jsx } from 'react/jsx-runtime';
10
+
11
+ var ListFilter = function ListFilter(_ref) {
12
+ var _ref$list = _ref.list,
13
+ list = _ref$list === void 0 ? EMPTY_ARRAY : _ref$list,
14
+ filter = _ref.filter,
15
+ onFilter = _ref.onFilter;
16
+ var _useState = useState(new Set(filter)),
17
+ _useState2 = _slicedToArray(_useState, 2),
18
+ selected = _useState2[0],
19
+ setSelected = _useState2[1];
20
+ var _useFilteredList = useFilteredList(list, function (item, query) {
21
+ var _item$id;
22
+ return matchString((_item$id = item.id) !== null && _item$id !== void 0 ? _item$id : item, query);
23
+ }),
24
+ filteredList = _useFilteredList.filteredList,
25
+ query = _useFilteredList.query,
26
+ onSearch = _useFilteredList.onSearch;
27
+ return /*#__PURE__*/jsxs("div", {
28
+ className: "flex flex-col",
29
+ children: [/*#__PURE__*/jsx("div", {
30
+ children: /*#__PURE__*/jsx(Search, {
31
+ autoFocus: true,
32
+ value: query,
33
+ onSearch: onSearch,
34
+ searchOnChange: true
35
+ })
36
+ }), /*#__PURE__*/jsxs("div", {
37
+ className: "flex-1 overflow-auto scroll-thin",
38
+ children: [filteredList.map(function (v, idx) {
39
+ var _v$id, _v$label;
40
+ return /*#__PURE__*/jsx("div", {
41
+ className: "flex gap-x-1 text-xs items-center",
42
+ children: /*#__PURE__*/jsx(Checkbox, {
43
+ checked: selected.has((_v$id = v.id) !== null && _v$id !== void 0 ? _v$id : v),
44
+ onChange: function onChange(b) {
45
+ var _v$id2, _v$id3;
46
+ var set = new Set(Array.from(selected.values()));
47
+ b ? set.add((_v$id2 = v.id) !== null && _v$id2 !== void 0 ? _v$id2 : v) : set["delete"]((_v$id3 = v.id) !== null && _v$id3 !== void 0 ? _v$id3 : v);
48
+ setSelected(set);
49
+ },
50
+ label: /*#__PURE__*/jsx("span", {
51
+ className: "truncate",
52
+ children: (_v$label = v.label) !== null && _v$label !== void 0 ? _v$label : v
53
+ })
54
+ })
55
+ }, idx);
56
+ }), filteredList.length === 0 && /*#__PURE__*/jsx("span", {
57
+ className: "text-muted text-xs",
58
+ children: "No items found"
59
+ })]
60
+ }), /*#__PURE__*/jsx("div", {
61
+ className: "flex justify-end",
62
+ children: /*#__PURE__*/jsx(DropdownDismiss, {
63
+ children: /*#__PURE__*/jsx(Button, {
64
+ size: "xs",
65
+ onClick: function onClick() {
66
+ return onFilter === null || onFilter === void 0 ? void 0 : onFilter(Array.from(selected));
67
+ },
68
+ children: "Apply"
69
+ })
70
+ })
71
+ })]
72
+ });
73
+ };
74
+ var ColumnFilters = function ColumnFilters(_ref2) {
75
+ var type = _ref2.type,
76
+ list = _ref2.list,
77
+ filter = _ref2.filter,
78
+ onFilter = _ref2.onFilter;
79
+ return /*#__PURE__*/jsxs("div", {
80
+ className: "flex flex-col min-w-[16rem]",
81
+ children: [list && /*#__PURE__*/jsx(ListFilter, {
82
+ list: list,
83
+ filter: filter,
84
+ onFilter: onFilter
85
+ }), !list && type === "string" && /*#__PURE__*/jsx(Input, {
86
+ autoFocus: true,
87
+ allowClear: true,
88
+ value: filter,
89
+ onEnterPressed: function onEnterPressed(e) {
90
+ var _e$target$value;
91
+ return onFilter((_e$target$value = e.target.value) !== null && _e$target$value !== void 0 ? _e$target$value : "");
92
+ }
93
+ })]
94
+ });
95
+ };
96
+
97
+ export { ColumnFilters };
@@ -1,14 +1,17 @@
1
1
  'use client';
2
2
 
3
3
  import _toConsumableArray from '@babel/runtime/helpers/toConsumableArray';
4
- import { MenuItem, CoreIcons, Divider, Tooltip, Icon, DropdownTool, Menu } from '@react-fabric/core';
4
+ import { Menu, CoreIcons, MenuItem, Divider, Tooltip, Icon, DropdownTool, Button } from '@react-fabric/core';
5
+ import { isEmpty } from '@react-fabric/utilities';
5
6
  import { useMemo, useCallback } from 'react';
7
+ import { ColumnFilters } from './ColumnFilters.js';
6
8
  import { useTableContext } from './Context.js';
7
9
  import { COL_DEFAULT_WIDTH, COL_MIN_WIDTH, COL_MAX_WIDTH } from './types.js';
8
10
  import { jsx, jsxs } from 'react/jsx-runtime';
9
11
 
10
12
  var HeaderCell = function HeaderCell(_ref) {
11
13
  var id = _ref.id,
14
+ dataType = _ref.dataType,
12
15
  label = _ref.label,
13
16
  icon = _ref.icon,
14
17
  iconBg = _ref.iconBg,
@@ -16,14 +19,18 @@ var HeaderCell = function HeaderCell(_ref) {
16
19
  rtlFlip = _ref.rtlFlip,
17
20
  actions = _ref.actions;
18
21
  _ref.locked;
19
- var tooltip = _ref.tooltip,
22
+ var filter = _ref.filter,
23
+ filterable = _ref.filterable,
24
+ filterOptions = _ref.filterOptions,
25
+ tooltip = _ref.tooltip,
20
26
  sortable = _ref.sortable,
21
27
  resizable = _ref.resizable,
22
28
  _width = _ref.width,
23
29
  maxWidth = _ref.maxWidth,
24
30
  minWidth = _ref.minWidth,
25
31
  sort = _ref.sort,
26
- onSort = _ref.onSort;
32
+ onSort = _ref.onSort,
33
+ _onFilter = _ref.onFilter;
27
34
  var _useTableContext = useTableContext(),
28
35
  startResize = _useTableContext.startResize,
29
36
  widths = _useTableContext.widths;
@@ -39,6 +46,21 @@ var HeaderCell = function HeaderCell(_ref) {
39
46
  }, [onSort, sort, sortable]);
40
47
  var menus = useMemo(function () {
41
48
  var ret = [];
49
+ if (filterable && (dataType !== null && dataType !== void 0 ? dataType : filterOptions)) {
50
+ ret.push( /*#__PURE__*/jsx(Menu, {
51
+ label: "Filter",
52
+ icon: filter ? CoreIcons.tick : "",
53
+ trigger: "click",
54
+ children: /*#__PURE__*/jsx(ColumnFilters, {
55
+ type: dataType,
56
+ list: filterOptions,
57
+ filter: filter,
58
+ onFilter: function onFilter(value) {
59
+ return _onFilter === null || _onFilter === void 0 ? void 0 : _onFilter(id, value);
60
+ }
61
+ })
62
+ }, "filters"));
63
+ }
42
64
  if (sortable) {
43
65
  ret.push( /*#__PURE__*/jsx(MenuItem, {
44
66
  label: "Sort ascending",
@@ -54,14 +76,23 @@ var HeaderCell = function HeaderCell(_ref) {
54
76
  }
55
77
  }, "sort-desc"));
56
78
  }
57
- if (sortable && actions) {
58
- ret.push( /*#__PURE__*/jsx(Divider, {}));
59
- }
60
79
  if (actions) {
61
- ret.push.apply(ret, _toConsumableArray(actions));
80
+ var _ret;
81
+ ret.push( /*#__PURE__*/jsx(Divider, {}, "div"));
82
+ (_ret = ret).push.apply(_ret, _toConsumableArray(actions));
83
+ }
84
+ if (ret.length === 1) {
85
+ ret = [/*#__PURE__*/jsx(ColumnFilters, {
86
+ type: dataType,
87
+ list: filterOptions,
88
+ filter: filter,
89
+ onFilter: function onFilter(value) {
90
+ return _onFilter === null || _onFilter === void 0 ? void 0 : _onFilter(id, value);
91
+ }
92
+ }, "filters")];
62
93
  }
63
94
  return ret;
64
- }, [sortable, actions]);
95
+ }, [sortable, actions, _onFilter, handleSort]);
65
96
  return /*#__PURE__*/jsxs("div", {
66
97
  role: "none",
67
98
  "data-id": id,
@@ -104,6 +135,15 @@ var HeaderCell = function HeaderCell(_ref) {
104
135
  className: "text-sm",
105
136
  children: menus
106
137
  })
138
+ }), filterable && !isEmpty(filter) && /*#__PURE__*/jsx(Button, {
139
+ size: "sm",
140
+ variant: "link",
141
+ "aria-label": "clear filter",
142
+ stopPropagation: true,
143
+ icon: CoreIcons.funnelRemove,
144
+ onClick: function onClick() {
145
+ return _onFilter === null || _onFilter === void 0 ? void 0 : _onFilter(id);
146
+ }
107
147
  }), resizable && /*#__PURE__*/jsx("div", {
108
148
  role: "none",
109
149
  className: "cursor-col-resize border-e bg-transparent border-tint-200 w-[5px] self-stretch",
@@ -37,6 +37,7 @@ var Table = function Table(_ref) {
37
37
  canExpand = _ref.canExpand,
38
38
  children = _ref.children,
39
39
  onSort = _ref.onSort,
40
+ onFilter = _ref.onFilter,
40
41
  onScroll = _ref.onScroll,
41
42
  onRowClick = _ref.onRowClick,
42
43
  onCheckedChanged = _ref.onCheckedChanged;
@@ -136,20 +137,23 @@ var Table = function Table(_ref) {
136
137
  }), (_state$start = state.start) === null || _state$start === void 0 ? void 0 : _state$start.map(function (col, idx) {
137
138
  return /*#__PURE__*/jsx(HeaderCell, _objectSpread(_objectSpread({}, col), {}, {
138
139
  sort: sort,
139
- onSort: onSort
140
+ onSort: onSort,
141
+ onFilter: onFilter
140
142
  }), idx);
141
143
  })]
142
144
  }), (_state$cols = state.cols) === null || _state$cols === void 0 ? void 0 : _state$cols.map(function (col, idx) {
143
145
  return /*#__PURE__*/jsx(HeaderCell, _objectSpread(_objectSpread({}, col), {}, {
144
146
  sort: sort,
145
- onSort: onSort
147
+ onSort: onSort,
148
+ onFilter: onFilter
146
149
  }), idx);
147
150
  }), /*#__PURE__*/jsxs("div", {
148
151
  className: wrapperEnd,
149
152
  children: [(_state$end = state.end) === null || _state$end === void 0 ? void 0 : _state$end.map(function (col, idx) {
150
153
  return /*#__PURE__*/jsx(HeaderCell, _objectSpread(_objectSpread({}, col), {}, {
151
154
  sort: sort,
152
- onSort: onSort
155
+ onSort: onSort,
156
+ onFilter: onFilter
153
157
  }), idx);
154
158
  }), hideableColumns && /*#__PURE__*/jsx(AddColumn, {
155
159
  onClick: onHide,
@@ -194,7 +198,7 @@ var Table = function Table(_ref) {
194
198
  })]
195
199
  })]
196
200
  }, key) : /*#__PURE__*/jsx(BodyRow, {
197
- rowKey: key,
201
+ rowKey: key.toString(),
198
202
  index: index,
199
203
  checkableRows: checkableRows,
200
204
  checked: keyProperty && checkState.checked.includes(data[keyProperty]) ? 1 : 0,
@@ -19,7 +19,7 @@ var TreeNode = function TreeNode(_ref) {
19
19
  defaultLeafIcon = _ref.defaultLeafIcon,
20
20
  defaultNodeIcon = _ref.defaultNodeIcon,
21
21
  onToggle = _ref.onToggle,
22
- _onClick = _ref.onClick,
22
+ onClick = _ref.onClick,
23
23
  onSelect = _ref.onSelect,
24
24
  onChecked = _ref.onChecked,
25
25
  children = _ref.children,
@@ -63,7 +63,7 @@ var TreeNode = function TreeNode(_ref) {
63
63
  return /*#__PURE__*/jsxs("div", {
64
64
  role: "treeitem",
65
65
  "aria-selected": node.selected,
66
- className: classNames("flex flex-nowrap items-center", node.leaf ? leafClassName : nodeClassName),
66
+ className: classNames("flex flex-nowrap items-center", node.className, node.leaf ? leafClassName : nodeClassName),
67
67
  "data-testid": node["data-testid"],
68
68
  "data-test-value": node["data-test-value"],
69
69
  children: [filler, node.loading && /*#__PURE__*/jsx("div", {
@@ -93,10 +93,10 @@ var TreeNode = function TreeNode(_ref) {
93
93
  })
94
94
  }), !node.empty && /*#__PURE__*/jsxs("div", {
95
95
  role: "none",
96
- className: classNames("group/tool data-[selected]:bg-primary-100 flex flex-nowrap flex-1 overflow-hidden select-none", node.childSelected && "font-medium", (canSelect || _onClick) && "hover:bg-primary-50 cursor-pointer"),
96
+ className: classNames("group/tool data-[selected]:bg-primary-100 flex flex-nowrap flex-1 overflow-hidden select-none", node.childSelected && "font-medium", node.disabled && "text-muted opacity-65", !node.disabled && (canSelect || onClick) && "hover:bg-primary-50 cursor-pointer"),
97
97
  "data-selected": node.selected,
98
- onClick: function onClick() {
99
- _onClick === null || _onClick === void 0 || _onClick(node.id, node.data);
98
+ onClick: node.disabled ? undefined : function () {
99
+ onClick === null || onClick === void 0 || onClick(node.id, node.data);
100
100
  canSelect ? onSelect(node.id) : canCheck ? onChecked(node.id) : onToggle(node.id);
101
101
  },
102
102
  children: [nodeIcon && /*#__PURE__*/jsxs("div", {
@@ -85,7 +85,8 @@ var _VirtualGallery = function _VirtualGallery(_ref) {
85
85
  },
86
86
  estimateSize: function estimateSize() {
87
87
  return height;
88
- }
88
+ },
89
+ overscan: 2
89
90
  });
90
91
  var virtualItems = virtualizer.getVirtualItems();
91
92
  var currentIndex = useRef(0);
@@ -127,14 +128,15 @@ var _VirtualGallery = function _VirtualGallery(_ref) {
127
128
  var _virtualItems$2, _virtualItems$slice$p;
128
129
  var firstItem = (_virtualItems$2 = virtualItems[0]) === null || _virtualItems$2 === void 0 ? void 0 : _virtualItems$2.index;
129
130
  var lastItem = virtualItems === null || virtualItems === void 0 || (_virtualItems$slice$p = virtualItems.slice(-1).pop()) === null || _virtualItems$slice$p === void 0 ? void 0 : _virtualItems$slice$p.index;
131
+ var firstIndex = firstItem * columnCount;
130
132
  var lastIndex = (lastItem !== null && lastItem !== void 0 ? lastItem : 0) * columnCount + columnCount;
131
- var canLoadPrevPage = firstItem !== undefined && items[firstItem * columnCount] === null;
133
+ var canLoadPrevPage = firstItem !== undefined && items[firstIndex] === null;
132
134
  var canLoadNextPage = lastItem !== undefined && lastIndex >= count - 1 && count < total;
133
135
  var db = debounce(onLoadMore);
134
136
  if (canLoadPrevPage) {
135
- db(firstItem);
137
+ db(firstIndex);
136
138
  } else if (!loading && canLoadNextPage) {
137
- db(lastItem + 1);
139
+ db(lastIndex + 1);
138
140
  }
139
141
  return function () {
140
142
  return db.cancel();
@@ -175,7 +177,7 @@ var _VirtualGallery = function _VirtualGallery(_ref) {
175
177
  return /*#__PURE__*/jsx("div", _objectSpread(_objectSpread({
176
178
  ref: scrollerRef,
177
179
  onScroll: handleScroll,
178
- className: classNames("area-content bg-base overflow-auto contain-strict")
180
+ className: classNames("area-content bg-base overflow-auto")
179
181
  }, aria), {}, {
180
182
  children: /*#__PURE__*/jsxs("div", {
181
183
  className: classNames("inline-flex flex-nowrap gap-2 justify-center min-h-full min-w-full"),
@@ -195,12 +197,11 @@ var _VirtualGallery = function _VirtualGallery(_ref) {
195
197
  }
196
198
  }), virtualItems.map(function (_ref3) {
197
199
  var key = _ref3.key,
198
- index = _ref3.index,
199
- measureElement = _ref3.measureElement;
200
+ index = _ref3.index;
200
201
  return /*#__PURE__*/jsx("div", {
201
202
  className: "flex justify-start",
202
203
  "data-index": index,
203
- ref: measureElement,
204
+ ref: virtualizer.measureElement,
204
205
  children: columnMap.map(function (col) {
205
206
  return /*#__PURE__*/jsx("div", {
206
207
  "data-index": index * columnCount + col,
@@ -56,6 +56,7 @@ var _VirtualList = function _VirtualList(_ref) {
56
56
  var scrollerRef = useRef(null);
57
57
  var virtualizer = useVirtualizer({
58
58
  count: count,
59
+ overscan: 2,
59
60
  horizontal: orientation === "horizontal",
60
61
  getScrollElement: function getScrollElement() {
61
62
  return scrollerRef.current;
@@ -164,7 +165,7 @@ var _VirtualList = function _VirtualList(_ref) {
164
165
  return /*#__PURE__*/jsx("div", _objectSpread(_objectSpread({
165
166
  ref: scrollerRef,
166
167
  onScroll: handleScroll,
167
- className: classNames("area-content bg-base overflow-auto contain-strict")
168
+ className: classNames("area-content bg-base overflow-auto")
168
169
  }, aria), {}, {
169
170
  children: /*#__PURE__*/jsxs("div", {
170
171
  className: classNames("inline-flex flex-nowrap gap-2 justify-center min-h-full min-w-full", orientation === "horizontal" && "flex-col"),
@@ -0,0 +1,9 @@
1
+ import { type TableColumn } from "./types";
2
+ interface FilterProps {
3
+ type: TableColumn["dataType"];
4
+ onFilter: (value: AnyObject) => void;
5
+ list?: AnyObject[];
6
+ filter: AnyObject;
7
+ }
8
+ export declare const ColumnFilters: ({ type, list, filter, onFilter, }: FilterProps) => import("react/jsx-runtime").JSX.Element;
9
+ export {};
@@ -1,2 +1,2 @@
1
1
  import { type TableColumn, type TableProps } from "./types";
2
- export declare const HeaderCell: ({ id, label, icon, iconBg, iconColor, rtlFlip, actions, locked, tooltip, sortable, resizable, width: _width, maxWidth, minWidth, sort, onSort, }: TableColumn & Pick<TableProps, "sort" | "onSort">) => import("react/jsx-runtime").JSX.Element;
2
+ export declare const HeaderCell: ({ id, dataType, label, icon, iconBg, iconColor, rtlFlip, actions, locked, filter, filterable, filterOptions, tooltip, sortable, resizable, width: _width, maxWidth, minWidth, sort, onSort, onFilter, }: TableColumn & Pick<TableProps, "sort" | "onSort" | "onFilter">) => import("react/jsx-runtime").JSX.Element;
@@ -1,2 +1,2 @@
1
1
  import { type TableProps } from "./types";
2
- export declare const Table: <T extends KeyValue<any> = KeyValue<any>>({ ref, data, columns, keyProperty, groupProperty, checkableRows, initialScroll, hideableColumns, sort, loading, checked, emptyDisplay, groupRenderer, canExpand, children, onSort, onScroll, onRowClick, onCheckedChanged, }: TableProps<T>) => import("react/jsx-runtime").JSX.Element;
2
+ export declare const Table: <T extends KeyValue<any> = KeyValue<any>>({ ref, data, columns, keyProperty, groupProperty, checkableRows, initialScroll, hideableColumns, sort, loading, checked, emptyDisplay, groupRenderer, canExpand, children, onSort, onFilter, onScroll, onRowClick, onCheckedChanged, }: TableProps<T>) => import("react/jsx-runtime").JSX.Element;
@@ -16,7 +16,8 @@ export interface TableColumn<T = KeyValue> extends CssProp, IconProps, TestProps
16
16
  resizable?: boolean;
17
17
  sortable?: boolean;
18
18
  filterable?: boolean;
19
- filterList?: AnyObject[];
19
+ filter?: AnyObject;
20
+ filterOptions?: AnyObject[];
20
21
  width?: string | number;
21
22
  minWidth?: string | number;
22
23
  maxWidth?: string | number;
@@ -61,4 +62,5 @@ export interface TableProps<T = KeyValue> extends RefProp<TableRef> {
61
62
  onScroll?: (firstItem: number) => void;
62
63
  canExpand?: (record: T) => boolean;
63
64
  children?: (record: T) => Elements<AnyObject>;
65
+ onFilter?: (id: keyof T | string, value?: AnyObject) => void;
64
66
  }
@@ -15,7 +15,7 @@ export declare const useVirtualTable: <T extends KeyValue<any> = KeyValue<any>>(
15
15
  onCheckChanged?: (checked: AnyObject[]) => void;
16
16
  }) => {
17
17
  scrollerRef: import("react").RefObject<HTMLDivElement>;
18
- items: import("@tanstack/virtual-core").VirtualItem<Element>[];
18
+ items: import("@tanstack/virtual-core").VirtualItem[];
19
19
  checkState: DataState<T>;
20
20
  getData: (index: number) => T;
21
21
  virtualizer: import("@tanstack/virtual-core").Virtualizer<HTMLDivElement, Element>;
@@ -51,13 +51,13 @@ export interface VirtualGalleryProps<T> extends TestProps {
51
51
  /**
52
52
  * load more callback
53
53
  */
54
- onLoadMore?: (page?: number) => void;
54
+ onLoadMore?: (lastIndex?: number) => void;
55
55
  /**
56
56
  * scroll handler
57
57
  */
58
58
  onScroll?: (top: number) => void;
59
59
  }
60
60
  export declare const VirtualGallery: {
61
- <T extends KeyValue<any>>({ items, children, columns, fullWidth, maxWidth, minWidth, total, padding, defaultHeight: height, defaultWidth: width, hideScroller, initialScroll, loading, onLoadMore, onScroll, galleryRef, ...aria }: VirtualGalleryProps<T>): import("react/jsx-runtime").JSX.Element;
61
+ <T extends unknown>({ items, children, columns, fullWidth, maxWidth, minWidth, total, padding, defaultHeight: height, defaultWidth: width, hideScroller, initialScroll, loading, onLoadMore, onScroll, galleryRef, ...aria }: VirtualGalleryProps<T>): import("react/jsx-runtime").JSX.Element;
62
62
  displayName: string;
63
63
  };
@@ -51,13 +51,13 @@ export interface VirtualListProps<T> extends TestProps {
51
51
  /**
52
52
  * load more callback
53
53
  */
54
- onLoadMore?: (page?: number) => void;
54
+ onLoadMore?: (lastIndex?: number) => void;
55
55
  /**
56
56
  * scroll handler
57
57
  */
58
58
  onScroll?: (top: number) => void;
59
59
  }
60
60
  export declare const VirtualList: {
61
- <T extends KeyValue<any>>({ items, total, children, fullWidth, maxWidth, minWidth, padding, orientation, defaultHeight: height, defaultWidth: width, hideScroller, initialScroll, loading, onLoadMore, onScroll, listRef, ...aria }: VirtualListProps<T>): import("react/jsx-runtime").JSX.Element;
61
+ <T extends unknown>({ items, total, children, fullWidth, maxWidth, minWidth, padding, orientation, defaultHeight: height, defaultWidth: width, hideScroller, initialScroll, loading, onLoadMore, onScroll, listRef, ...aria }: VirtualListProps<T>): import("react/jsx-runtime").JSX.Element;
62
62
  displayName: string;
63
63
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-fabric/data",
3
- "version": "1.0.0-beta.46",
3
+ "version": "1.0.0-beta.48",
4
4
  "description": "RUF: Data package",
5
5
  "author": "Adarsh Pastakia <adarshpastakia@outlook.com>",
6
6
  "license": "MIT",
@@ -35,11 +35,11 @@
35
35
  "watch": "npm-run-all -p watch:rollup watch:dts"
36
36
  },
37
37
  "dependencies": {
38
- "@react-fabric/core": "^1.0.0-beta.46",
39
- "@react-fabric/date": "^1.0.0-beta.46",
40
- "@react-fabric/form": "^1.0.0-beta.46",
41
- "@react-fabric/utilities": "^1.0.0-beta.46",
42
- "@tanstack/react-virtual": "^3.8.6",
38
+ "@react-fabric/core": "^1.0.0-beta.48",
39
+ "@react-fabric/date": "^1.0.0-beta.48",
40
+ "@react-fabric/form": "^1.0.0-beta.48",
41
+ "@react-fabric/utilities": "^1.0.0-beta.48",
42
+ "@tanstack/react-virtual": "^3.13.9",
43
43
  "classnames": "^2.5.1",
44
44
  "color": "^4.2.3",
45
45
  "memoize-one": "^6.0.0"
@@ -54,5 +54,5 @@
54
54
  "node": ">=18.12.0",
55
55
  "npm": ">=8.19.0"
56
56
  },
57
- "gitHead": "0dfc20b9c84712bb1cb5205c22464e8a1eb2341c"
57
+ "gitHead": "0b1001b4ae1ee67f8e1817d6395d1f3945321897"
58
58
  }
@@ -32,7 +32,7 @@ export const AddColumn = ({
32
32
  onClick: (id: string) => void;
33
33
  }) => {
34
34
  return (
35
- <div className="group font-medium w-6 flex flex-nowrap text-start items-end">
35
+ <div className="group font-medium w-6 flex flex-nowrap text-start items-center">
36
36
  <Dropdown placement="bottom-end">
37
37
  <Icon
38
38
  icon={CoreIcons.insert}
@@ -0,0 +1,105 @@
1
+ /*
2
+ * React Fabric
3
+ * @version: 1.0.0
4
+ *
5
+ *
6
+ * The MIT License (MIT)
7
+ * Copyright (c) 2025 Adarsh Pastakia
8
+ *
9
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
10
+ * and associated documentation files (the "Software"), to deal in the Software without restriction,
11
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
12
+ * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
13
+ * subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all copies or substantial
16
+ * portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19
+ * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
20
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ */
23
+
24
+ import { Button, DropdownDismiss } from "@react-fabric/core";
25
+ import { Checkbox, Input, Search } from "@react-fabric/form";
26
+ import { EMPTY_ARRAY, matchString } from "@react-fabric/utilities";
27
+ import { useState } from "react";
28
+ import { useFilteredList } from "../hooks/useFilteredList";
29
+ import { type TableColumn } from "./types";
30
+
31
+ interface FilterProps {
32
+ type: TableColumn["dataType"];
33
+ onFilter: (value: AnyObject) => void;
34
+ list?: AnyObject[];
35
+ filter: AnyObject;
36
+ }
37
+
38
+ const ListFilter = ({
39
+ list = EMPTY_ARRAY,
40
+ filter,
41
+ onFilter,
42
+ }: Partial<FilterProps>) => {
43
+ const [selected, setSelected] = useState<Set<string>>(new Set(filter));
44
+
45
+ const { filteredList, query, onSearch } = useFilteredList(
46
+ list,
47
+ (item, query) => {
48
+ return matchString(item.id ?? item, query);
49
+ },
50
+ );
51
+
52
+ return (
53
+ <div className="flex flex-col">
54
+ <div>
55
+ <Search autoFocus value={query} onSearch={onSearch} searchOnChange />
56
+ </div>
57
+ <div className="flex-1 overflow-auto scroll-thin">
58
+ {filteredList.map((v, idx) => (
59
+ <div key={idx} className="flex gap-x-1 text-xs items-center">
60
+ <Checkbox
61
+ checked={selected.has(v.id ?? v)}
62
+ onChange={(b) => {
63
+ const set = new Set(Array.from(selected.values()));
64
+ b ? set.add(v.id ?? v) : set.delete(v.id ?? v);
65
+ setSelected(set);
66
+ }}
67
+ label={<span className="truncate">{v.label ?? v}</span>}
68
+ />
69
+ </div>
70
+ ))}
71
+ {filteredList.length === 0 && (
72
+ <span className="text-muted text-xs">No items found</span>
73
+ )}
74
+ </div>
75
+ <div className="flex justify-end">
76
+ <DropdownDismiss>
77
+ <Button size="xs" onClick={() => onFilter?.(Array.from(selected))}>
78
+ Apply
79
+ </Button>
80
+ </DropdownDismiss>
81
+ </div>
82
+ </div>
83
+ );
84
+ };
85
+
86
+ export const ColumnFilters = ({
87
+ type,
88
+ list,
89
+ filter,
90
+ onFilter,
91
+ }: FilterProps) => {
92
+ return (
93
+ <div className="flex flex-col min-w-[16rem]">
94
+ {list && <ListFilter list={list} filter={filter} onFilter={onFilter} />}
95
+ {!list && type === "string" && (
96
+ <Input
97
+ autoFocus
98
+ allowClear
99
+ value={filter}
100
+ onEnterPressed={(e: AnyObject) => onFilter(e.target.value ?? "")}
101
+ />
102
+ )}
103
+ </div>
104
+ );
105
+ };
@@ -22,6 +22,7 @@
22
22
  */
23
23
 
24
24
  import {
25
+ Button,
25
26
  CoreIcons,
26
27
  Divider,
27
28
  DropdownTool,
@@ -30,7 +31,9 @@ import {
30
31
  MenuItem,
31
32
  Tooltip,
32
33
  } from "@react-fabric/core";
34
+ import { isEmpty } from "@react-fabric/utilities";
33
35
  import { useCallback, useMemo } from "react";
36
+ import { ColumnFilters } from "./ColumnFilters";
34
37
  import { useTableContext } from "./Context";
35
38
  import {
36
39
  COL_DEFAULT_WIDTH,
@@ -42,6 +45,7 @@ import {
42
45
 
43
46
  export const HeaderCell = ({
44
47
  id,
48
+ dataType,
45
49
  label,
46
50
  icon,
47
51
  iconBg,
@@ -49,6 +53,9 @@ export const HeaderCell = ({
49
53
  rtlFlip,
50
54
  actions,
51
55
  locked,
56
+ filter,
57
+ filterable,
58
+ filterOptions,
52
59
  tooltip,
53
60
  sortable,
54
61
  resizable,
@@ -57,7 +64,8 @@ export const HeaderCell = ({
57
64
  minWidth,
58
65
  sort,
59
66
  onSort,
60
- }: TableColumn & Pick<TableProps, "sort" | "onSort">) => {
67
+ onFilter,
68
+ }: TableColumn & Pick<TableProps, "sort" | "onSort" | "onFilter">) => {
61
69
  const { startResize, widths } = useTableContext();
62
70
 
63
71
  const width = useMemo(
@@ -78,7 +86,24 @@ export const HeaderCell = ({
78
86
  );
79
87
 
80
88
  const menus = useMemo(() => {
81
- const ret = [];
89
+ let ret = [];
90
+ if (filterable && (dataType ?? filterOptions)) {
91
+ ret.push(
92
+ <Menu
93
+ key="filters"
94
+ label="Filter"
95
+ icon={filter ? CoreIcons.tick : ""}
96
+ trigger="click"
97
+ >
98
+ <ColumnFilters
99
+ type={dataType}
100
+ list={filterOptions}
101
+ filter={filter}
102
+ onFilter={(value) => onFilter?.(id, value)}
103
+ />
104
+ </Menu>,
105
+ );
106
+ }
82
107
  if (sortable) {
83
108
  ret.push(
84
109
  <MenuItem
@@ -95,14 +120,23 @@ export const HeaderCell = ({
95
120
  />,
96
121
  );
97
122
  }
98
- if (sortable && actions) {
99
- ret.push(<Divider />);
100
- }
101
123
  if (actions) {
124
+ ret.push(<Divider key="div" />);
102
125
  ret.push(...actions);
103
126
  }
127
+ if (ret.length === 1) {
128
+ ret = [
129
+ <ColumnFilters
130
+ key="filters"
131
+ type={dataType}
132
+ list={filterOptions}
133
+ filter={filter}
134
+ onFilter={(value) => onFilter?.(id, value)}
135
+ />,
136
+ ];
137
+ }
104
138
  return ret;
105
- }, [sortable, actions]);
139
+ }, [sortable, actions, onFilter, handleSort]);
106
140
 
107
141
  return (
108
142
  <div
@@ -136,6 +170,16 @@ export const HeaderCell = ({
136
170
  <Menu className="text-sm">{menus}</Menu>
137
171
  </DropdownTool>
138
172
  )}
173
+ {filterable && !isEmpty(filter) && (
174
+ <Button
175
+ size="sm"
176
+ variant="link"
177
+ aria-label="clear filter"
178
+ stopPropagation
179
+ icon={CoreIcons.funnelRemove}
180
+ onClick={() => onFilter?.(id)}
181
+ />
182
+ )}
139
183
  {resizable && (
140
184
  <div
141
185
  role="none"
@@ -65,6 +65,7 @@ export const Table = <T extends KeyValue = KeyValue>({
65
65
  canExpand,
66
66
  children,
67
67
  onSort,
68
+ onFilter,
68
69
  onScroll,
69
70
  onRowClick,
70
71
  onCheckedChanged,
@@ -176,15 +177,33 @@ export const Table = <T extends KeyValue = KeyValue>({
176
177
  />
177
178
  )}
178
179
  {state.start?.map((col, idx) => (
179
- <HeaderCell key={idx} {...col} sort={sort} onSort={onSort} />
180
+ <HeaderCell
181
+ key={idx}
182
+ {...col}
183
+ sort={sort}
184
+ onSort={onSort}
185
+ onFilter={onFilter}
186
+ />
180
187
  ))}
181
188
  </div>
182
189
  {state.cols?.map((col, idx) => (
183
- <HeaderCell key={idx} {...col} sort={sort} onSort={onSort} />
190
+ <HeaderCell
191
+ key={idx}
192
+ {...col}
193
+ sort={sort}
194
+ onSort={onSort}
195
+ onFilter={onFilter}
196
+ />
184
197
  ))}
185
198
  <div className={wrapperEnd}>
186
199
  {state.end?.map((col, idx) => (
187
- <HeaderCell key={idx} {...col} sort={sort} onSort={onSort} />
200
+ <HeaderCell
201
+ key={idx}
202
+ {...col}
203
+ sort={sort}
204
+ onSort={onSort}
205
+ onFilter={onFilter}
206
+ />
188
207
  ))}
189
208
 
190
209
  {hideableColumns && (
@@ -236,7 +255,7 @@ export const Table = <T extends KeyValue = KeyValue>({
236
255
  </div>
237
256
  ) : (
238
257
  <BodyRow
239
- rowKey={key}
258
+ rowKey={key.toString()}
240
259
  index={index}
241
260
  checkableRows={checkableRows}
242
261
  checked={
@@ -50,7 +50,8 @@ export interface TableColumn<T = KeyValue>
50
50
  resizable?: boolean;
51
51
  sortable?: boolean;
52
52
  filterable?: boolean;
53
- filterList?: AnyObject[];
53
+ filter?: AnyObject;
54
+ filterOptions?: AnyObject[];
54
55
  width?: string | number;
55
56
  minWidth?: string | number;
56
57
  maxWidth?: string | number;
@@ -103,4 +104,6 @@ export interface TableProps<T = KeyValue> extends RefProp<TableRef> {
103
104
 
104
105
  canExpand?: (record: T) => boolean;
105
106
  children?: (record: T) => Elements<AnyObject>;
107
+
108
+ onFilter?: (id: keyof T | string, value?: AnyObject) => void;
106
109
  }
@@ -99,6 +99,7 @@ export const TreeNode = ({
99
99
  aria-selected={node.selected}
100
100
  className={classNames(
101
101
  "flex flex-nowrap items-center",
102
+ node.className,
102
103
  node.leaf ? leafClassName : nodeClassName,
103
104
  )}
104
105
  data-testid={node["data-testid"]}
@@ -145,17 +146,24 @@ export const TreeNode = ({
145
146
  className={classNames(
146
147
  "group/tool data-[selected]:bg-primary-100 flex flex-nowrap flex-1 overflow-hidden select-none",
147
148
  node.childSelected && "font-medium",
148
- (canSelect || onClick) && "hover:bg-primary-50 cursor-pointer",
149
+ node.disabled && "text-muted opacity-65",
150
+ !node.disabled &&
151
+ (canSelect || onClick) &&
152
+ "hover:bg-primary-50 cursor-pointer",
149
153
  )}
150
154
  data-selected={node.selected}
151
- onClick={() => {
152
- onClick?.(node.id, node.data);
153
- canSelect
154
- ? onSelect(node.id)
155
- : canCheck
156
- ? onChecked(node.id)
157
- : onToggle(node.id);
158
- }}
155
+ onClick={
156
+ node.disabled
157
+ ? undefined
158
+ : () => {
159
+ onClick?.(node.id, node.data);
160
+ canSelect
161
+ ? onSelect(node.id)
162
+ : canCheck
163
+ ? onChecked(node.id)
164
+ : onToggle(node.id);
165
+ }
166
+ }
159
167
  >
160
168
  {nodeIcon && (
161
169
  <div className="flex-content w-6 self-center leading-none">
@@ -94,7 +94,7 @@ export interface VirtualGalleryProps<T> extends TestProps {
94
94
  /**
95
95
  * load more callback
96
96
  */
97
- onLoadMore?: (page?: number) => void;
97
+ onLoadMore?: (lastIndex?: number) => void;
98
98
  /**
99
99
  * scroll handler
100
100
  */
@@ -103,7 +103,7 @@ export interface VirtualGalleryProps<T> extends TestProps {
103
103
 
104
104
  const createItemList = memoize((items) => items);
105
105
 
106
- const _VirtualGallery = <T extends KeyValue>({
106
+ const _VirtualGallery = <T extends AnyObject>({
107
107
  items,
108
108
  children,
109
109
  columns,
@@ -172,6 +172,7 @@ const _VirtualGallery = <T extends KeyValue>({
172
172
  count: Math.ceil(count / columnCount),
173
173
  getScrollElement: () => scrollerRef.current,
174
174
  estimateSize: () => height,
175
+ overscan: 2,
175
176
  });
176
177
 
177
178
  const virtualItems = virtualizer.getVirtualItems();
@@ -219,16 +220,17 @@ const _VirtualGallery = <T extends KeyValue>({
219
220
  if (onLoadMore) {
220
221
  const firstItem = virtualItems[0]?.index;
221
222
  const lastItem = virtualItems?.slice(-1).pop()?.index;
223
+ const firstIndex = firstItem * columnCount;
222
224
  const lastIndex = (lastItem ?? 0) * columnCount + columnCount;
223
225
  const canLoadPrevPage =
224
- firstItem !== undefined && items[firstItem * columnCount] === null;
226
+ firstItem !== undefined && items[firstIndex] === null;
225
227
  const canLoadNextPage =
226
228
  lastItem !== undefined && lastIndex >= count - 1 && count < total;
227
229
  const db = debounce(onLoadMore);
228
230
  if (canLoadPrevPage) {
229
- db(firstItem);
231
+ db(firstIndex);
230
232
  } else if (!loading && canLoadNextPage) {
231
- db(lastItem + 1);
233
+ db(lastIndex + 1);
232
234
  }
233
235
  return () => db.cancel();
234
236
  }
@@ -277,9 +279,7 @@ const _VirtualGallery = <T extends KeyValue>({
277
279
  <div
278
280
  ref={scrollerRef}
279
281
  onScroll={handleScroll}
280
- className={classNames(
281
- "area-content bg-base overflow-auto contain-strict",
282
- )}
282
+ className={classNames("area-content bg-base overflow-auto")}
283
283
  {...aria}
284
284
  >
285
285
  <div
@@ -306,12 +306,12 @@ const _VirtualGallery = <T extends KeyValue>({
306
306
  }}
307
307
  >
308
308
  <div style={{ height: virtualItems[0]?.start }} />
309
- {virtualItems.map(({ key, index, measureElement }) => (
309
+ {virtualItems.map(({ key, index }) => (
310
310
  <div
311
311
  className="flex justify-start"
312
312
  key={key}
313
313
  data-index={index}
314
- ref={measureElement}
314
+ ref={virtualizer.measureElement}
315
315
  >
316
316
  {columnMap.map((col) => (
317
317
  <div
@@ -101,7 +101,7 @@ export interface VirtualListProps<T> extends TestProps {
101
101
  /**
102
102
  * load more callback
103
103
  */
104
- onLoadMore?: (page?: number) => void;
104
+ onLoadMore?: (lastIndex?: number) => void;
105
105
  /**
106
106
  * scroll handler
107
107
  */
@@ -110,7 +110,7 @@ export interface VirtualListProps<T> extends TestProps {
110
110
 
111
111
  const createItemList = memoizeOne((items) => items);
112
112
 
113
- const _VirtualList = <T extends KeyValue>({
113
+ const _VirtualList = <T extends AnyObject>({
114
114
  items,
115
115
  total = 0,
116
116
  children,
@@ -149,6 +149,7 @@ const _VirtualList = <T extends KeyValue>({
149
149
  const scrollerRef = useRef<HTMLDivElement>(null);
150
150
  const virtualizer = useVirtualizer({
151
151
  count,
152
+ overscan: 2,
152
153
  horizontal: orientation === "horizontal",
153
154
  getScrollElement: () => scrollerRef.current,
154
155
  estimateSize: () => (orientation === "horizontal" ? width : height),
@@ -263,9 +264,7 @@ const _VirtualList = <T extends KeyValue>({
263
264
  <div
264
265
  ref={scrollerRef}
265
266
  onScroll={handleScroll}
266
- className={classNames(
267
- "area-content bg-base overflow-auto contain-strict",
268
- )}
267
+ className={classNames("area-content bg-base overflow-auto")}
269
268
  {...aria}
270
269
  >
271
270
  <div