@parca/profile 0.19.124 → 0.19.126
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/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [0.19.126](https://github.com/parca-dev/parca/compare/@parca/profile@0.19.125...@parca/profile@0.19.126) (2026-02-23)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @parca/profile
|
|
9
|
+
|
|
10
|
+
## [0.19.125](https://github.com/parca-dev/parca/compare/@parca/profile@0.19.124...@parca/profile@0.19.125) (2026-02-18)
|
|
11
|
+
|
|
12
|
+
**Note:** Version bump only for package @parca/profile
|
|
13
|
+
|
|
6
14
|
## [0.19.124](https://github.com/parca-dev/parca/compare/@parca/profile@0.19.123...@parca/profile@0.19.124) (2026-02-18)
|
|
7
15
|
|
|
8
16
|
**Note:** Version bump only for package @parca/profile
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Select.d.ts","sourceRoot":"","sources":["../../src/SimpleMatchers/Select.tsx"],"names":[],"mappings":"AAaA,OAAO,
|
|
1
|
+
{"version":3,"file":"Select.d.ts","sourceRoot":"","sources":["../../src/SimpleMatchers/Select.tsx"],"names":[],"mappings":"AAaA,OAAO,KAA0D,MAAM,OAAO,CAAC;AAU/E,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,GAAG,CAAC,OAAO,CAAC;IACpB,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,aAAa,CAAC;CACxB;AAED,MAAM,WAAW,eAAgB,SAAQ,UAAU;IACjD,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAED,UAAU,iBAAiB;IACzB,KAAK,EAAE,iBAAiB,EAAE,GAAG,UAAU,EAAE,CAAC;IAC1C,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,QAAA,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAwXnE,CAAC;AA2CF,eAAe,YAAY,CAAC"}
|
|
@@ -11,8 +11,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
11
11
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
// See the License for the specific language governing permissions and
|
|
13
13
|
// limitations under the License.
|
|
14
|
-
import {
|
|
14
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
15
15
|
import { Icon } from '@iconify/react';
|
|
16
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
16
17
|
import cx from 'classnames';
|
|
17
18
|
import levenshtein from 'fast-levenshtein';
|
|
18
19
|
import { Button, DividerWithLabel, RefreshButton, useParcaContext } from '@parca/components';
|
|
@@ -22,11 +23,11 @@ const CustomSelect = ({ items: itemsProp, selectedKey, onSelection, placeholder
|
|
|
22
23
|
const [isOpen, setIsOpen] = useState(false);
|
|
23
24
|
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
24
25
|
const [searchTerm, setSearchTerm] = useState('');
|
|
26
|
+
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
|
|
25
27
|
const [isRefetching, setIsRefetching] = useState(false);
|
|
26
28
|
const containerRef = useRef(null);
|
|
27
29
|
const optionsRef = useRef(null);
|
|
28
30
|
const searchInputRef = useRef(null);
|
|
29
|
-
const optionRefs = useRef([]);
|
|
30
31
|
const handleRefetch = useCallback(async () => {
|
|
31
32
|
if (refetchValues == null || isRefetching)
|
|
32
33
|
return;
|
|
@@ -38,26 +39,26 @@ const CustomSelect = ({ items: itemsProp, selectedKey, onSelection, placeholder
|
|
|
38
39
|
setIsRefetching(false);
|
|
39
40
|
}
|
|
40
41
|
}, [refetchValues, isRefetching]);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const timer = setTimeout(() => setDebouncedSearchTerm(searchTerm), 150);
|
|
44
|
+
return () => clearTimeout(timer);
|
|
45
|
+
}, [searchTerm]);
|
|
46
|
+
const items = useMemo(() => {
|
|
47
|
+
if (itemsProp[0] != null && 'type' in itemsProp[0]) {
|
|
48
|
+
return itemsProp.flatMap(item => item.values.map(v => ({ ...v, type: item.type })));
|
|
49
|
+
}
|
|
50
|
+
return itemsProp.map(item => ({ ...item, type: '' }));
|
|
51
|
+
}, [itemsProp]);
|
|
52
|
+
const filteredItems = useMemo(() => {
|
|
53
|
+
if (!searchable)
|
|
54
|
+
return items;
|
|
55
|
+
const lowerSearch = debouncedSearchTerm.toLowerCase();
|
|
56
|
+
const filtered = items.filter(item => item.element.active.props.children.toString().toLowerCase().includes(lowerSearch));
|
|
57
|
+
if (debouncedSearchTerm === '') {
|
|
58
|
+
return filtered.sort((a, b) => a.key.localeCompare(b.key));
|
|
59
|
+
}
|
|
60
|
+
return filtered.sort((a, b) => levenshtein.get(a.key, debouncedSearchTerm) - levenshtein.get(b.key, debouncedSearchTerm));
|
|
61
|
+
}, [items, debouncedSearchTerm, searchable]);
|
|
61
62
|
const selection = editable ? selectedKey : items.find(v => v.key === selectedKey);
|
|
62
63
|
useEffect(() => {
|
|
63
64
|
const handleClickOutside = (event) => {
|
|
@@ -75,24 +76,6 @@ const CustomSelect = ({ items: itemsProp, selectedKey, onSelection, placeholder
|
|
|
75
76
|
searchInputRef.current?.focus();
|
|
76
77
|
}
|
|
77
78
|
}, [isOpen, searchable]);
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
if (focusedIndex !== -1 &&
|
|
80
|
-
optionsRef.current !== null &&
|
|
81
|
-
optionRefs.current[focusedIndex] !== null) {
|
|
82
|
-
const optionElement = optionRefs.current[focusedIndex];
|
|
83
|
-
const optionsContainer = optionsRef.current;
|
|
84
|
-
if (optionElement !== null && optionsContainer !== null) {
|
|
85
|
-
const optionRect = optionElement.getBoundingClientRect();
|
|
86
|
-
const containerRect = optionsContainer.getBoundingClientRect();
|
|
87
|
-
if (optionRect.bottom > containerRect.bottom) {
|
|
88
|
-
optionsContainer.scrollTop += optionRect.bottom - containerRect.bottom;
|
|
89
|
-
}
|
|
90
|
-
else if (optionRect.top < containerRect.top) {
|
|
91
|
-
optionsContainer.scrollTop -= containerRect.top - optionRect.top;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}, [focusedIndex]);
|
|
96
79
|
const handleKeyDown = (e) => {
|
|
97
80
|
if (e.key === 'Enter') {
|
|
98
81
|
if (!isOpen) {
|
|
@@ -162,31 +145,69 @@ const CustomSelect = ({ items: itemsProp, selectedKey, onSelection, placeholder
|
|
|
162
145
|
e.target.value = '';
|
|
163
146
|
e.target.value = value;
|
|
164
147
|
};
|
|
165
|
-
const groupedFilteredItems =
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
group
|
|
148
|
+
const groupedFilteredItems = useMemo(() => {
|
|
149
|
+
return filteredItems
|
|
150
|
+
.reduce((acc, item) => {
|
|
151
|
+
const group = acc.find(g => g.type === item.type);
|
|
152
|
+
if (group != null) {
|
|
153
|
+
group.values.push(item);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
acc.push({ type: item.type, values: [item] });
|
|
157
|
+
}
|
|
158
|
+
return acc;
|
|
159
|
+
}, [])
|
|
160
|
+
.sort((a, b) => a.values.length - b.values.length);
|
|
161
|
+
}, [filteredItems]);
|
|
162
|
+
const showHeaders = useMemo(() => groupedFilteredItems.length > 1 && groupedFilteredItems.every(g => g.type !== ''), [groupedFilteredItems]);
|
|
163
|
+
const flatList = useMemo(() => {
|
|
164
|
+
const list = [];
|
|
165
|
+
let optionIndex = 0;
|
|
166
|
+
for (const group of groupedFilteredItems) {
|
|
167
|
+
if (showHeaders && group.type !== '') {
|
|
168
|
+
list.push({ type: 'header', label: group.type });
|
|
169
|
+
}
|
|
170
|
+
for (const item of group.values) {
|
|
171
|
+
list.push({ type: 'option', item: item, flatIndex: optionIndex });
|
|
172
|
+
optionIndex++;
|
|
173
|
+
}
|
|
170
174
|
}
|
|
171
|
-
|
|
172
|
-
|
|
175
|
+
return list;
|
|
176
|
+
}, [groupedFilteredItems, showHeaders]);
|
|
177
|
+
const longestKey = useMemo(() => filteredItems.reduce((a, b) => (a.key.length > b.key.length ? a : b), filteredItems[0])
|
|
178
|
+
?.key ?? '', [filteredItems]);
|
|
179
|
+
const rowVirtualizer = useVirtualizer({
|
|
180
|
+
count: flatList.length,
|
|
181
|
+
getScrollElement: () => optionsRef.current,
|
|
182
|
+
estimateSize: () => 36,
|
|
183
|
+
overscan: 500,
|
|
184
|
+
});
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
if (focusedIndex !== -1) {
|
|
187
|
+
const flatIdx = flatList.findIndex(entry => entry.type === 'option' && entry.flatIndex === focusedIndex);
|
|
188
|
+
if (flatIdx !== -1) {
|
|
189
|
+
rowVirtualizer.scrollToIndex(flatIdx, { align: 'auto' });
|
|
190
|
+
}
|
|
173
191
|
}
|
|
174
|
-
|
|
175
|
-
}, [])
|
|
176
|
-
.sort((a, b) => a.values.length - b.values.length);
|
|
192
|
+
}, [focusedIndex, flatList, rowVirtualizer]);
|
|
177
193
|
return (_jsxs("div", { ref: containerRef, className: "relative", onKeyDown: handleKeyDown, onClick: onButtonClick, children: [_jsxs("div", { id: id, onClick: () => !disabled && setIsOpen(!isOpen), className: cx(styles, width !== undefined ? `w-${width}` : 'w-full', disabled ? 'cursor-not-allowed opacity-50 pointer-events-none' : '', primary ? primaryStyles : defaultStyles, { [className]: className.length > 0 }), tabIndex: 0, role: "button", "aria-haspopup": "listbox", "aria-expanded": isOpen, ...restProps, children: [_jsx("div", { className: cx(icon != null ? '' : 'block overflow-x-hidden text-ellipsis whitespace-nowrap'), children: renderSelection(selection) }), _jsx("div", { className: cx(icon != null ? '' : 'pointer-events-none text-gray-400'), children: icon ?? _jsx(Icon, { icon: "heroicons:chevron-up-down-20-solid", "aria-hidden": "true" }) })] }), isOpen && (_jsx("div", { ref: optionsRef, className: cx('absolute z-50 mt-1 max-h-[50vh] w-max overflow-auto rounded-md bg-gray-50 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-gray-600 dark:bg-gray-900 dark:ring-white dark:ring-opacity-20 sm:text-sm', { [optionsClassname]: optionsClassname.length > 0 }), role: "listbox", children: _jsxs("div", { className: "relative flex flex-col", children: [searchable && (_jsx("div", { className: "sticky z-10 top-[-5px] w-auto max-w-full", children: _jsx("div", { className: "flex flex-col", children: editable ? (_jsxs(_Fragment, { children: [_jsx("textarea", { ref: searchInputRef, className: "w-full px-4 py-2 text-sm border-b border-gray-200 rounded-none ring-0 outline-none bg-gray-50 dark:bg-gray-800 dark:text-white min-h-[50px]", placeholder: "Type a RegEx to add", value: searchTerm, onChange: e => setSearchTerm(e.target.value), onFocus: e => moveCaretToEnd(e) }), editable && searchTerm.length > 0 && (_jsx("div", { className: "p-2 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800", children: _jsx(Button, { variant: "primary", className: "w-full h-[30px]", onClick: () => {
|
|
178
194
|
onSelection(searchTerm);
|
|
179
195
|
setIsOpen(false);
|
|
180
|
-
}, children: "Add" }) }))] })) : (_jsx("input", { ref: searchInputRef, type: "text", className: "w-full px-4 h-[45px] text-sm border-none rounded-none ring-0 outline-none bg-gray-50 dark:bg-gray-800 dark:text-white", placeholder: "Search...", value: searchTerm, onChange: e => setSearchTerm(e.target.value) })) }) })), _jsx("div", { className: "flex-1 min-h-0", children: loading === true ? (_jsx("div", { className: "w-[270px]", children: loader })) : groupedFilteredItems.length === 0 ? (_jsx("div", { className: "px-4 py-3 text-sm text-gray-500 dark:text-gray-400 text-center", ...testId(TEST_IDS.LABEL_VALUE_NO_RESULTS), children: "No values found" })) : (
|
|
181
|
-
|
|
182
|
-
|
|
196
|
+
}, children: "Add" }) }))] })) : (_jsx("input", { ref: searchInputRef, type: "text", className: "w-full px-4 h-[45px] text-sm border-none rounded-none ring-0 outline-none bg-gray-50 dark:bg-gray-800 dark:text-white", placeholder: "Search...", value: searchTerm, onChange: e => setSearchTerm(e.target.value) })) }) })), _jsx("div", { className: "flex-1 min-h-0", children: loading === true ? (_jsx("div", { className: "w-[270px]", children: loader })) : groupedFilteredItems.length === 0 ? (_jsx("div", { className: "px-4 py-3 text-sm text-gray-500 dark:text-gray-400 text-center", ...testId(TEST_IDS.LABEL_VALUE_NO_RESULTS), children: "No values found" })) : ((() => {
|
|
197
|
+
const virtualItems = rowVirtualizer.getVirtualItems();
|
|
198
|
+
const paddingTop = virtualItems.length > 0 ? virtualItems[0]?.start ?? 0 : 0;
|
|
199
|
+
const paddingBottom = virtualItems.length > 0
|
|
200
|
+
? rowVirtualizer.getTotalSize() -
|
|
201
|
+
(virtualItems[virtualItems.length - 1]?.end ?? 0)
|
|
202
|
+
: 0;
|
|
203
|
+
return (_jsxs("div", { children: [_jsx("div", { "aria-hidden": true, className: "pl-3 pr-9 whitespace-nowrap overflow-hidden", style: { height: 0, visibility: 'hidden' }, children: longestKey }), paddingTop > 0 && _jsx("div", { style: { height: paddingTop } }), virtualItems.map(virtualItem => {
|
|
204
|
+
const entry = flatList[virtualItem.index];
|
|
205
|
+
return entry.type === 'header' ? (_jsx("div", { className: "pl-2", children: _jsx(DividerWithLabel, { label: entry.label }) }, virtualItem.key)) : (_jsx(OptionItem, { item: entry.item, index: entry.flatIndex, focusedIndex: focusedIndex, selectedKey: selectedKey, handleSelection: handleSelection }, virtualItem.key));
|
|
206
|
+
}), paddingBottom > 0 && _jsx("div", { style: { height: paddingBottom } })] }));
|
|
207
|
+
})()) }), refetchValues !== undefined && loading !== true && (_jsx(RefreshButton, { onClick: () => void handleRefetch(), disabled: isRefetching, title: "Refresh results", testId: TEST_IDS.LABEL_VALUE_REFRESH_BUTTON, sticky: true, loading: isRefetching }))] }) }))] }));
|
|
183
208
|
};
|
|
184
|
-
const OptionItem = ({ item,
|
|
185
|
-
return (_jsxs("div", {
|
|
186
|
-
if (el !== null) {
|
|
187
|
-
optionRefs.current[index] = el;
|
|
188
|
-
}
|
|
189
|
-
}, className: cx('relative cursor-default select-none py-2 pl-3 pr-9', index === focusedIndex && 'bg-indigo-600 text-white', item.key === selectedKey && 'bg-indigo-100 dark:bg-indigo-700', item.disabled !== null && item.disabled === true && 'opacity-50 cursor-not-allowed', 'focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 hover:bg-indigo-600 hover:text-white'), role: "option", "aria-selected": item.key === selectedKey, tabIndex: -1, onClick: () => {
|
|
209
|
+
const OptionItem = ({ item, index, focusedIndex, selectedKey, handleSelection, }) => {
|
|
210
|
+
return (_jsxs("div", { className: cx('relative cursor-default select-none py-2 pl-3 pr-9', index === focusedIndex && 'bg-indigo-600 text-white', item.key === selectedKey && 'bg-indigo-100 dark:bg-indigo-700', item.disabled !== null && item.disabled === true && 'opacity-50 cursor-not-allowed', 'focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 hover:bg-indigo-600 hover:text-white'), role: "option", "aria-selected": item.key === selectedKey, tabIndex: -1, onClick: () => {
|
|
190
211
|
if (!(item.disabled ?? false)) {
|
|
191
212
|
handleSelection(item.key);
|
|
192
213
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parca/profile",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.126",
|
|
4
4
|
"description": "Profile viewing libraries",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@floating-ui/react": "^0.27.12",
|
|
7
7
|
"@headlessui/react": "^1.7.19",
|
|
8
8
|
"@iconify/react": "^4.0.0",
|
|
9
9
|
"@parca/client": "0.17.17",
|
|
10
|
-
"@parca/components": "0.16.
|
|
10
|
+
"@parca/components": "0.16.403",
|
|
11
11
|
"@parca/dynamicsize": "0.16.72",
|
|
12
12
|
"@parca/hooks": "0.0.118",
|
|
13
13
|
"@parca/icons": "0.16.79",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"@storybook/preview-api": "^8.4.3",
|
|
21
21
|
"@tanstack/react-query": "^4.0.5",
|
|
22
22
|
"@tanstack/react-table": "^8.17.3",
|
|
23
|
+
"@tanstack/react-virtual": "^3.5.0",
|
|
23
24
|
"@tanstack/table-core": "^8.16.0",
|
|
24
25
|
"@types/d3": "^7.4.3",
|
|
25
26
|
"@types/d3-scale": "^4.0.8",
|
|
@@ -87,5 +88,5 @@
|
|
|
87
88
|
"access": "public",
|
|
88
89
|
"registry": "https://registry.npmjs.org/"
|
|
89
90
|
},
|
|
90
|
-
"gitHead": "
|
|
91
|
+
"gitHead": "0cbcaff4a19806bfa5e3e1c7b2c3cb4a0e8eb97c"
|
|
91
92
|
}
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
|
-
import React, {
|
|
14
|
+
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
|
15
15
|
|
|
16
16
|
import {Icon} from '@iconify/react';
|
|
17
|
+
import {useVirtualizer} from '@tanstack/react-virtual';
|
|
17
18
|
import cx from 'classnames';
|
|
18
19
|
import levenshtein from 'fast-levenshtein';
|
|
19
20
|
|
|
@@ -84,11 +85,11 @@ const CustomSelect: React.FC<CustomSelectProps & Record<string, any>> = ({
|
|
|
84
85
|
const [isOpen, setIsOpen] = useState(false);
|
|
85
86
|
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
86
87
|
const [searchTerm, setSearchTerm] = useState('');
|
|
88
|
+
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
|
|
87
89
|
const [isRefetching, setIsRefetching] = useState(false);
|
|
88
90
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
89
91
|
const optionsRef = useRef<HTMLDivElement>(null);
|
|
90
92
|
const searchInputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null);
|
|
91
|
-
const optionRefs = useRef<Array<HTMLElement | null>>([]);
|
|
92
93
|
|
|
93
94
|
const handleRefetch = useCallback(async () => {
|
|
94
95
|
if (refetchValues == null || isRefetching) return;
|
|
@@ -101,30 +102,34 @@ const CustomSelect: React.FC<CustomSelectProps & Record<string, any>> = ({
|
|
|
101
102
|
}
|
|
102
103
|
}, [refetchValues, isRefetching]);
|
|
103
104
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
const timer = setTimeout(() => setDebouncedSearchTerm(searchTerm), 150);
|
|
107
|
+
return () => clearTimeout(timer);
|
|
108
|
+
}, [searchTerm]);
|
|
109
|
+
|
|
110
|
+
const items = useMemo<TypedSelectItem[]>(() => {
|
|
111
|
+
if (itemsProp[0] != null && 'type' in itemsProp[0]) {
|
|
112
|
+
return (itemsProp as GroupedSelectItem[]).flatMap(item =>
|
|
113
|
+
item.values.map(v => ({...v, type: item.type}))
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return (itemsProp as SelectItem[]).map(item => ({...item, type: ''}));
|
|
117
|
+
}, [itemsProp]);
|
|
118
|
+
|
|
119
|
+
const filteredItems = useMemo(() => {
|
|
120
|
+
if (!searchable) return items;
|
|
121
|
+
const lowerSearch = debouncedSearchTerm.toLowerCase();
|
|
122
|
+
const filtered = items.filter(item =>
|
|
123
|
+
item.element.active.props.children.toString().toLowerCase().includes(lowerSearch)
|
|
124
|
+
);
|
|
125
|
+
if (debouncedSearchTerm === '') {
|
|
126
|
+
return filtered.sort((a, b) => a.key.localeCompare(b.key));
|
|
127
|
+
}
|
|
128
|
+
return filtered.sort(
|
|
129
|
+
(a, b) =>
|
|
130
|
+
levenshtein.get(a.key, debouncedSearchTerm) - levenshtein.get(b.key, debouncedSearchTerm)
|
|
108
131
|
);
|
|
109
|
-
}
|
|
110
|
-
items = (itemsProp as SelectItem[]).map(item => ({...item, type: ''}));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const filteredItems = searchable
|
|
114
|
-
? items
|
|
115
|
-
.filter(item =>
|
|
116
|
-
item.element.active.props.children
|
|
117
|
-
.toString()
|
|
118
|
-
.toLowerCase()
|
|
119
|
-
.includes(searchTerm.toLowerCase())
|
|
120
|
-
)
|
|
121
|
-
.sort((a, b) => {
|
|
122
|
-
if (searchTerm === '') {
|
|
123
|
-
return a.key.localeCompare(b.key);
|
|
124
|
-
}
|
|
125
|
-
return levenshtein.get(a.key, searchTerm) - levenshtein.get(b.key, searchTerm);
|
|
126
|
-
})
|
|
127
|
-
: items;
|
|
132
|
+
}, [items, debouncedSearchTerm, searchable]);
|
|
128
133
|
|
|
129
134
|
const selection = editable ? selectedKey : items.find(v => v.key === selectedKey);
|
|
130
135
|
|
|
@@ -147,28 +152,6 @@ const CustomSelect: React.FC<CustomSelectProps & Record<string, any>> = ({
|
|
|
147
152
|
}
|
|
148
153
|
}, [isOpen, searchable]);
|
|
149
154
|
|
|
150
|
-
useEffect(() => {
|
|
151
|
-
if (
|
|
152
|
-
focusedIndex !== -1 &&
|
|
153
|
-
optionsRef.current !== null &&
|
|
154
|
-
optionRefs.current[focusedIndex] !== null
|
|
155
|
-
) {
|
|
156
|
-
const optionElement = optionRefs.current[focusedIndex];
|
|
157
|
-
const optionsContainer = optionsRef.current;
|
|
158
|
-
|
|
159
|
-
if (optionElement !== null && optionsContainer !== null) {
|
|
160
|
-
const optionRect = optionElement.getBoundingClientRect();
|
|
161
|
-
const containerRect = optionsContainer.getBoundingClientRect();
|
|
162
|
-
|
|
163
|
-
if (optionRect.bottom > containerRect.bottom) {
|
|
164
|
-
optionsContainer.scrollTop += optionRect.bottom - containerRect.bottom;
|
|
165
|
-
} else if (optionRect.top < containerRect.top) {
|
|
166
|
-
optionsContainer.scrollTop -= containerRect.top - optionRect.top;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}, [focusedIndex]);
|
|
171
|
-
|
|
172
155
|
const handleKeyDown = (e: React.KeyboardEvent): void => {
|
|
173
156
|
if (e.key === 'Enter') {
|
|
174
157
|
if (!isOpen) {
|
|
@@ -245,17 +228,66 @@ const CustomSelect: React.FC<CustomSelectProps & Record<string, any>> = ({
|
|
|
245
228
|
e.target.value = value;
|
|
246
229
|
};
|
|
247
230
|
|
|
248
|
-
const groupedFilteredItems =
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
group
|
|
253
|
-
|
|
254
|
-
|
|
231
|
+
const groupedFilteredItems = useMemo(() => {
|
|
232
|
+
return filteredItems
|
|
233
|
+
.reduce((acc: GroupedSelectItem[], item) => {
|
|
234
|
+
const group = acc.find(g => g.type === item.type);
|
|
235
|
+
if (group != null) {
|
|
236
|
+
group.values.push(item);
|
|
237
|
+
} else {
|
|
238
|
+
acc.push({type: item.type, values: [item]});
|
|
239
|
+
}
|
|
240
|
+
return acc;
|
|
241
|
+
}, [])
|
|
242
|
+
.sort((a, b) => a.values.length - b.values.length);
|
|
243
|
+
}, [filteredItems]);
|
|
244
|
+
|
|
245
|
+
const showHeaders = useMemo(
|
|
246
|
+
() => groupedFilteredItems.length > 1 && groupedFilteredItems.every(g => g.type !== ''),
|
|
247
|
+
[groupedFilteredItems]
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const flatList = useMemo(() => {
|
|
251
|
+
const list: Array<
|
|
252
|
+
{type: 'header'; label: string} | {type: 'option'; item: TypedSelectItem; flatIndex: number}
|
|
253
|
+
> = [];
|
|
254
|
+
let optionIndex = 0;
|
|
255
|
+
for (const group of groupedFilteredItems) {
|
|
256
|
+
if (showHeaders && group.type !== '') {
|
|
257
|
+
list.push({type: 'header', label: group.type});
|
|
255
258
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
+
for (const item of group.values) {
|
|
260
|
+
list.push({type: 'option', item: item as TypedSelectItem, flatIndex: optionIndex});
|
|
261
|
+
optionIndex++;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return list;
|
|
265
|
+
}, [groupedFilteredItems, showHeaders]);
|
|
266
|
+
|
|
267
|
+
const longestKey = useMemo(
|
|
268
|
+
() =>
|
|
269
|
+
filteredItems.reduce((a, b) => (a.key.length > b.key.length ? a : b), filteredItems[0])
|
|
270
|
+
?.key ?? '',
|
|
271
|
+
[filteredItems]
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const rowVirtualizer = useVirtualizer({
|
|
275
|
+
count: flatList.length,
|
|
276
|
+
getScrollElement: () => optionsRef.current,
|
|
277
|
+
estimateSize: () => 36,
|
|
278
|
+
overscan: 500,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
if (focusedIndex !== -1) {
|
|
283
|
+
const flatIdx = flatList.findIndex(
|
|
284
|
+
entry => entry.type === 'option' && entry.flatIndex === focusedIndex
|
|
285
|
+
);
|
|
286
|
+
if (flatIdx !== -1) {
|
|
287
|
+
rowVirtualizer.scrollToIndex(flatIdx, {align: 'auto'});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}, [focusedIndex, flatList, rowVirtualizer]);
|
|
259
291
|
|
|
260
292
|
return (
|
|
261
293
|
<div ref={containerRef} className="relative" onKeyDown={handleKeyDown} onClick={onButtonClick}>
|
|
@@ -349,28 +381,45 @@ const CustomSelect: React.FC<CustomSelectProps & Record<string, any>> = ({
|
|
|
349
381
|
No values found
|
|
350
382
|
</div>
|
|
351
383
|
) : (
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
384
|
+
(() => {
|
|
385
|
+
const virtualItems = rowVirtualizer.getVirtualItems();
|
|
386
|
+
const paddingTop = virtualItems.length > 0 ? virtualItems[0]?.start ?? 0 : 0;
|
|
387
|
+
const paddingBottom =
|
|
388
|
+
virtualItems.length > 0
|
|
389
|
+
? rowVirtualizer.getTotalSize() -
|
|
390
|
+
(virtualItems[virtualItems.length - 1]?.end ?? 0)
|
|
391
|
+
: 0;
|
|
392
|
+
return (
|
|
393
|
+
<div>
|
|
394
|
+
<div
|
|
395
|
+
aria-hidden
|
|
396
|
+
className="pl-3 pr-9 whitespace-nowrap overflow-hidden"
|
|
397
|
+
style={{height: 0, visibility: 'hidden'}}
|
|
398
|
+
>
|
|
399
|
+
{longestKey}
|
|
359
400
|
</div>
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
401
|
+
{paddingTop > 0 && <div style={{height: paddingTop}} />}
|
|
402
|
+
{virtualItems.map(virtualItem => {
|
|
403
|
+
const entry = flatList[virtualItem.index];
|
|
404
|
+
return entry.type === 'header' ? (
|
|
405
|
+
<div key={virtualItem.key} className="pl-2">
|
|
406
|
+
<DividerWithLabel label={entry.label} />
|
|
407
|
+
</div>
|
|
408
|
+
) : (
|
|
409
|
+
<OptionItem
|
|
410
|
+
key={virtualItem.key}
|
|
411
|
+
item={entry.item}
|
|
412
|
+
index={entry.flatIndex}
|
|
413
|
+
focusedIndex={focusedIndex}
|
|
414
|
+
selectedKey={selectedKey}
|
|
415
|
+
handleSelection={handleSelection}
|
|
416
|
+
/>
|
|
417
|
+
);
|
|
418
|
+
})}
|
|
419
|
+
{paddingBottom > 0 && <div style={{height: paddingBottom}} />}
|
|
420
|
+
</div>
|
|
421
|
+
);
|
|
422
|
+
})()
|
|
374
423
|
)}
|
|
375
424
|
</div>
|
|
376
425
|
{refetchValues !== undefined && loading !== true && (
|
|
@@ -392,14 +441,12 @@ const CustomSelect: React.FC<CustomSelectProps & Record<string, any>> = ({
|
|
|
392
441
|
|
|
393
442
|
const OptionItem = ({
|
|
394
443
|
item,
|
|
395
|
-
optionRefs,
|
|
396
444
|
index,
|
|
397
445
|
focusedIndex,
|
|
398
446
|
selectedKey,
|
|
399
447
|
handleSelection,
|
|
400
448
|
}: {
|
|
401
449
|
item: SelectItem;
|
|
402
|
-
optionRefs: React.MutableRefObject<Array<HTMLElement | null>>;
|
|
403
450
|
index: number;
|
|
404
451
|
focusedIndex: number;
|
|
405
452
|
selectedKey: string | undefined;
|
|
@@ -407,11 +454,6 @@ const OptionItem = ({
|
|
|
407
454
|
}): JSX.Element => {
|
|
408
455
|
return (
|
|
409
456
|
<div
|
|
410
|
-
ref={el => {
|
|
411
|
-
if (el !== null) {
|
|
412
|
-
optionRefs.current[index] = el;
|
|
413
|
-
}
|
|
414
|
-
}}
|
|
415
457
|
className={cx(
|
|
416
458
|
'relative cursor-default select-none py-2 pl-3 pr-9',
|
|
417
459
|
index === focusedIndex && 'bg-indigo-600 text-white',
|