@homecode/ui 5.6.1 → 5.7.1

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.
Files changed (54) hide show
  1. package/dist/esm/src/components/Autocomplete/Autocomplete.js +6 -3
  2. package/dist/esm/src/components/InputFile/InputFile.js +4 -1
  3. package/dist/esm/src/components/Popup/Popup.helpers.js +8 -1
  4. package/dist/esm/src/components/Popup/Popup.js +12 -6
  5. package/dist/esm/src/components/PromptComposer/PromptComposer.js +5 -1
  6. package/dist/esm/src/components/PromptComposer/promptComposerInsert.js +2 -1
  7. package/dist/esm/src/components/PromptComposer/usePromptComposerEditor.js +99 -21
  8. package/dist/esm/src/components/Select/Select.helpers.js +28 -1
  9. package/dist/esm/src/components/Select/Select.hooks.js +131 -0
  10. package/dist/esm/src/components/Select/Select2.js +213 -235
  11. package/dist/esm/src/components/Tooltip/Tooltip.styl.js +1 -1
  12. package/dist/esm/src/components/Tooltip/TooltipOverTrigger.js +4 -3
  13. package/dist/esm/src/components/Tooltip/tooltipTextStyles.js +17 -2
  14. package/dist/esm/src/tiptap/slash-mention/createSlashMentionExtension.js +51 -31
  15. package/dist/esm/src/tools/dom.js +6 -8
  16. package/dist/esm/src/tools/resizeObserver.js +1 -1
  17. package/dist/esm/types/src/components/Popup/Popup.d.ts +2 -1
  18. package/dist/esm/types/src/components/Popup/Popup.helpers.d.ts +1 -0
  19. package/dist/esm/types/src/components/Popup/Popup.types.d.ts +1 -0
  20. package/dist/esm/types/src/components/PromptComposer/PromptComposer.d.ts +4 -0
  21. package/dist/esm/types/src/components/PromptComposer/PromptComposer.types.d.ts +17 -3
  22. package/dist/esm/types/src/components/PromptComposer/index.d.ts +1 -1
  23. package/dist/esm/types/src/components/PromptComposer/usePromptComposerEditor.d.ts +6 -2
  24. package/dist/esm/types/src/components/Select/Select.helpers.d.ts +7 -1
  25. package/dist/esm/types/src/components/Select/Select.hooks.d.ts +55 -0
  26. package/dist/esm/types/src/components/Select/Select.types.d.ts +1 -1
  27. package/dist/esm/types/src/components/Select/Select2.test.d.ts +1 -0
  28. package/dist/esm/types/src/tiptap/slash-mention/createSlashMentionExtension.d.ts +1 -1
  29. package/dist/esm/types/src/tiptap/slash-mention/index.d.ts +1 -1
  30. package/dist/esm/types/src/tiptap/slash-mention/types.d.ts +12 -1
  31. package/graphify-out/.graphify_labels.json +18 -25
  32. package/graphify-out/2026-07-26/.graphify_labels.json +216 -0
  33. package/graphify-out/2026-07-26/GRAPH_REPORT.md +1036 -0
  34. package/graphify-out/2026-07-26/cost.json +12 -0
  35. package/graphify-out/2026-07-26/graph.json +71426 -0
  36. package/graphify-out/2026-07-26/manifest.json +2372 -0
  37. package/graphify-out/2026-07-27/.graphify_labels.json +214 -0
  38. package/graphify-out/2026-07-27/GRAPH_REPORT.md +1014 -0
  39. package/graphify-out/2026-07-27/cost.json +12 -0
  40. package/graphify-out/2026-07-27/graph.json +71778 -0
  41. package/graphify-out/2026-07-27/manifest.json +2372 -0
  42. package/graphify-out/2026-07-30/.graphify_labels.json +197 -0
  43. package/graphify-out/2026-07-30/GRAPH_REPORT.md +929 -0
  44. package/graphify-out/2026-07-30/cost.json +12 -0
  45. package/graphify-out/2026-07-30/graph.json +71798 -0
  46. package/graphify-out/2026-07-30/manifest.json +2372 -0
  47. package/graphify-out/GRAPH_REPORT.md +167 -249
  48. package/graphify-out/cache/ast/2690b69f91795ec634393c1fb8d9fd21ec09ec6759eaf6711e3b86c2a8edb385.json +1 -0
  49. package/graphify-out/cache/ast/2ae06c5acabf11eea845c93c2a8486317fbb18dccc1843ecd88077848af2ab29.json +1 -0
  50. package/graphify-out/cache/stat-index.json +1 -1
  51. package/graphify-out/graph.html +4 -4
  52. package/graphify-out/graph.json +3354 -2931
  53. package/graphify-out/manifest.json +47 -47
  54. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
- import { useRef, useState, useMemo, useEffect, useCallback, createElement } from 'react';
1
+ import { useRef, useState, useCallback, useEffect, useMemo, createElement } from 'react';
2
2
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
- import { mapById, isMultiple, renderLabel } from './Select.helpers.js';
4
- import { INTERACTION_MODE, getInteractionMode } from '../../tools/dom.js';
3
+ import { isMultiple, renderLabel, isSelectable } from './Select.helpers.js';
4
+ import { getInteractionMode, INTERACTION_MODE } from '../../tools/dom.js';
5
5
  import cn from 'classnames';
6
6
  import { omit } from '../../tools/object.js';
7
7
  import { AssistiveText } from '../AssistiveText/AssistiveText.js';
@@ -15,206 +15,247 @@ import { RequiredStar } from '../RequiredStar/RequiredStar.js';
15
15
  import { Scroll } from '../Scroll/Scroll.js';
16
16
  import { useThrottle } from '../../hooks/useThrottle.js';
17
17
  import useEvent from '../../hooks/useEvent.js';
18
+ import { generateUID } from '../../tools/uid.js';
19
+ import { useSelectedIds, useSelectOptions, useSelectOpenState, useKeyboardNav } from './Select.hooks.js';
18
20
  import S from './Select.styl.js';
19
21
 
22
+ /** Time given to a pointer press inside the popup before a blur closes it. */
23
+ const BLUR_CLOSE_DELAY = 60;
24
+ const POINTER_PRESS_TTL = 100;
20
25
  function Select2(props) {
21
- const { className, value, onChange, onChipClick, onSearchChange, disableTriggerArrow, inputProps, popupProps, scrollProps, size = 'm', round, optionClassName, selectedChipClassName, additionalOptions = [], options, variant, label, additionalLabel, error, blur, disabled, trigger, required, hideRequiredStar, isSearchable, presets = [], selectAllButton, clearButton, showSelectedCount, disableLabel, selectedChipRemoveTooltip, selectedChipIds, onOpen, onClose, } = props;
26
+ const { className, value, onChange, onChipClick, onSearchChange, disableTriggerArrow, inputProps, popupProps, scrollProps, size = 'm', round, optionsClassName, optionClassName, selectedChipClassName, additionalOptions = [], options = [], variant, label, additionalLabel, error, blur, disabled, trigger, required, hideRequiredStar, isSearchable, presets = [], selectAllButton, clearButton, showSelectedCount, disableLabel, selectedChipRemoveTooltip, selectedChipIds, onOpen, onClose, onFocus, onBlur, } = props;
22
27
  const isMultiple$1 = isMultiple(value);
23
28
  const closeOnSelect = props.closeOnSelect ?? !isMultiple$1;
29
+ const instanceId = useRef(generateUID());
24
30
  const contentRef = useRef(null);
25
31
  const scrollInnerRef = useRef(null);
26
- const focusedItemId = useRef(null);
27
- const focusedElem = useRef(null);
28
- const maxIndex = useRef(-1);
29
- const isFirstSelectedMeet = useRef(false);
30
- const searchValLower = useRef('');
31
- const [searchVal, _setSearchVal] = useState('');
32
- const setSearchVal = (val) => {
33
- _setSearchVal(val);
34
- searchValLower.current = val.toLowerCase();
35
- };
36
- const [isOpen, setIsOpen] = useState(false);
37
- const [isFocused, setIsFocused] = useState(false);
32
+ const pointerPressedInside = useRef(false);
33
+ const pressStartedInside = useRef(false);
34
+ const pointerPressTimer = useRef(0);
35
+ const blurTimer = useRef(0);
38
36
  const [labelClipPath, setLabelClipPath] = useState('');
39
- const [focusedItemIndex, setFocusedItemIndex] = useState(-1);
40
- // Add tree-related state from Select.tsx
41
- const [items, setItems] = useState([]);
42
- const ids = useMemo(() => mapById(props.options), [props.options]);
43
- // Add basic selection state
44
- const [selected, setSelected] = useState(isMultiple$1 ? value : [value]);
45
- const [optionsUpdated, setOptionsUpdated] = useState(0);
46
- const isErrorVisible = !isOpen && !!error;
47
- const triggerProps = useMemo(() => ({
48
- label,
49
- size,
50
- round,
51
- variant,
52
- ...props.triggerProps,
53
- }), [props.triggerProps, label, size, round, variant]);
54
- const isSelected = (id) => isMultiple$1
55
- ? selected.includes(id)
56
- : selected[0] === id;
57
- const isClickedInside = (elem) => !!(elem instanceof Element &&
58
- (elem.closest(`.${S.root}`) || elem.closest(`.${S.options}`)));
59
- const setNewItems = (newItems) => {
60
- maxIndex.current = newItems.length - 1;
61
- setItems(newItems);
62
- setOptionsUpdated(optionsUpdated + 1);
63
- if (focusedItemIndex > maxIndex.current && maxIndex.current >= 0) {
64
- setFocusedItemIndex(maxIndex.current);
65
- focusedItemId.current = newItems[maxIndex.current]?.id;
66
- }
67
- };
68
- const setItemFocus = index => {
69
- focusedItemId.current = items[index]?.id;
70
- setFocusedItemIndex(index);
71
- };
72
- const [isSearchActive, setIsSearchActive] = useState(false);
73
- const handleSearchChange = (e, value) => {
74
- setSearchVal(value);
75
- setIsSearchActive(true);
76
- onSearchChange?.(value);
77
- setNewItems(options.filter(filterOption));
78
- };
79
- const selectAll = () => {
80
- const newValue = options.map(({ id }) => id);
81
- setSelected(newValue);
82
- onChange?.(newValue);
83
- };
84
- const dropSelected = () => {
85
- setSelected([]);
86
- onChange?.([]);
87
- };
88
- const onFocusedElemRef = elem => {
89
- focusedElem.current = elem;
90
- if (elem) {
91
- const content = contentRef.current;
92
- if (!content)
93
- return;
94
- const { top, bottom } = elem.getBoundingClientRect();
95
- const rect = contentRef.current.getBoundingClientRect();
96
- const list = scrollInnerRef.current;
97
- if (top < rect.top) {
98
- list.scrollTop -= rect.top - top;
99
- }
100
- else if (bottom > rect.bottom) {
101
- list.scrollTop += bottom - rect.bottom;
102
- }
103
- }
104
- };
105
- const onPopupOpen = () => {
106
- setIsOpen(true);
107
- if (focusedItemIndex === -1) {
108
- setItemFocus(0);
109
- }
37
+ const [isSearching, setIsSearching] = useState(false);
38
+ const [innerSearchVal, setInnerSearchVal] = useState('');
39
+ const searchVal = props.searchValue ?? innerSearchVal;
40
+ const searchQuery = isSearching ? searchVal : '';
41
+ const { selectedIds, setSelectedIds } = useSelectedIds(value);
42
+ const { allOptions, visibleOptions, navigableOptions, optionsById } = useSelectOptions({
43
+ options,
44
+ additionalOptions,
45
+ searchQuery,
46
+ });
47
+ const handleOpen = useCallback(() => {
110
48
  popupProps?.onOpen?.();
111
49
  onOpen?.();
112
- };
113
- const onPopupClose = () => {
114
- setIsOpen(false);
115
- setSearchVal('');
116
- setIsSearchActive(false);
117
- setItemFocus(0);
50
+ }, [popupProps?.onOpen, onOpen]);
51
+ const handleClose = useCallback(() => {
118
52
  popupProps?.onClose?.();
119
53
  onClose?.();
54
+ }, [popupProps?.onClose, onClose]);
55
+ const { isOpen, open, close } = useSelectOpenState({
56
+ isOpen: props.isOpen,
57
+ onOpen: handleOpen,
58
+ onClose: handleClose,
59
+ });
60
+ const isErrorVisible = !isOpen && !!error;
61
+ const hasChips = isMultiple$1 && selectedIds.length > 0;
62
+ const isSelected = (id) => selectedIds.includes(id);
63
+ const getLabel = (id) => optionsById.items[id]?.label || '';
64
+ const isInsideSelect = (elem) => elem instanceof Element &&
65
+ Boolean(elem.closest(`[data-select-id="${instanceId.current}"]`));
66
+ const openIfEnabled = () => {
67
+ if (disabled)
68
+ return;
69
+ open();
120
70
  };
121
- const onFocus = () => {
122
- setIsFocused(true);
71
+ const resetSearch = () => {
72
+ setIsSearching(false);
73
+ setInnerSearchVal('');
74
+ if (searchVal)
75
+ onSearchChange?.('');
123
76
  };
124
- const onBlur = () => {
125
- setIsFocused(false);
77
+ const applySelection = (ids, nextValue) => {
78
+ setSelectedIds(ids);
79
+ onChange?.(nextValue);
126
80
  };
127
- const onOptionHover = useThrottle(id => {
128
- const mode = getInteractionMode();
129
- if (mode !== INTERACTION_MODE.POINTER)
130
- return;
131
- const index = items.findIndex(item => item.id === id);
132
- setItemFocus(index);
133
- }, 100, { trailing: true });
134
81
  const onItemToggle = (id) => {
82
+ if (!isSelectable(optionsById.items[id]))
83
+ return;
135
84
  if (isMultiple$1) {
136
- const newValue = isSelected(id)
137
- ? selected.filter(i => i !== id)
138
- : [...selected, id];
139
- setSelected(newValue);
140
- onChange?.(newValue);
85
+ const ids = isSelected(id)
86
+ ? selectedIds.filter(i => i !== id)
87
+ : [...selectedIds, id];
88
+ applySelection(ids, ids);
141
89
  }
142
90
  else {
143
- // mono select
144
- const newValue = isSelected(id) ? null : id;
145
- const newSelected = isSelected(id) ? [] : [id];
146
- setSelected(newSelected);
147
- onChange?.(newValue);
91
+ const nextValue = isSelected(id) ? null : id;
92
+ applySelection(nextValue === null ? [] : [id], nextValue);
148
93
  }
149
- setSearchVal('');
94
+ resetSearch();
150
95
  if (closeOnSelect)
151
- setIsOpen(false);
96
+ close();
97
+ };
98
+ const selectAll = () => {
99
+ const ids = allOptions.filter(isSelectable).map(({ id }) => id);
100
+ applySelection(ids, isMultiple$1 ? ids : (ids[0] ?? null));
101
+ };
102
+ const dropSelected = () => applySelection([], isMultiple$1 ? [] : null);
103
+ const applyPreset = (ids) => applySelection(ids, isMultiple$1 ? ids : (ids[0] ?? null));
104
+ const { focusedId, setFocusedId } = useKeyboardNav({
105
+ isOpen,
106
+ navigableOptions,
107
+ selectedIds,
108
+ onSelect: onItemToggle,
109
+ });
110
+ const onOptionHover = useThrottle((id) => {
111
+ if (getInteractionMode() !== INTERACTION_MODE.POINTER)
112
+ return;
113
+ setFocusedId(id);
114
+ }, 100, { trailing: true });
115
+ const onScrollInnerRef = useCallback((elem) => {
116
+ scrollInnerRef.current = elem;
117
+ }, []);
118
+ const onFocusedElemRef = (elem) => {
119
+ const content = contentRef.current;
120
+ const list = scrollInnerRef.current;
121
+ if (!elem || !content || !list)
122
+ return;
123
+ const { top, bottom } = elem.getBoundingClientRect();
124
+ const rect = content.getBoundingClientRect();
125
+ if (top < rect.top) {
126
+ list.scrollTop -= rect.top - top;
127
+ }
128
+ else if (bottom > rect.bottom) {
129
+ list.scrollTop += bottom - rect.bottom;
130
+ }
131
+ };
132
+ const handleSearchChange = (e, val) => {
133
+ setIsSearching(true);
134
+ setInnerSearchVal(val);
135
+ onSearchChange?.(val);
136
+ openIfEnabled();
137
+ };
138
+ const handleFocus = e => {
139
+ window.clearTimeout(blurTimer.current);
140
+ onFocus?.(e);
141
+ };
142
+ /**
143
+ * Popup skips its own blur-close while it is controlled, so closing on blur is
144
+ * up to Select. A press inside the popup blurs the trigger too — that one must
145
+ * not close the list.
146
+ */
147
+ const handleBlur = e => {
148
+ onBlur?.(e);
149
+ window.clearTimeout(blurTimer.current);
150
+ blurTimer.current = window.setTimeout(() => {
151
+ if (pointerPressedInside.current)
152
+ return;
153
+ close();
154
+ }, BLUR_CLOSE_DELAY);
152
155
  };
153
- const getLabel = (id) => ids.items[id]?.label || '';
156
+ const onTriggerPointerDown = () => {
157
+ if (disabled)
158
+ return;
159
+ // A searchable trigger is a text field: pressing it must never close.
160
+ if (isOpen && !isSearchable)
161
+ close();
162
+ else
163
+ open();
164
+ };
165
+ const markPressInside = (isInside) => {
166
+ window.clearTimeout(pointerPressTimer.current);
167
+ pointerPressedInside.current = isInside;
168
+ pointerPressTimer.current = window.setTimeout(() => (pointerPressedInside.current = false), POINTER_PRESS_TTL);
169
+ };
170
+ useEvent({
171
+ event: 'pointerdown',
172
+ isCapture: true,
173
+ callback: e => {
174
+ pressStartedInside.current = isInsideSelect(e.target);
175
+ markPressInside(pressStartedInside.current);
176
+ },
177
+ });
178
+ useEvent({
179
+ event: 'pointerup',
180
+ isCapture: true,
181
+ callback: () => {
182
+ // The release can land on the portalled popup that mounted under the
183
+ // cursor mid-gesture, so the up-target is not a reliable inside signal.
184
+ if (pressStartedInside.current)
185
+ markPressInside(true);
186
+ pressStartedInside.current = false;
187
+ },
188
+ });
189
+ useEvent({
190
+ event: 'click',
191
+ isActive: isOpen,
192
+ callback: e => {
193
+ // The click concluding a press that started inside must not count as an
194
+ // outside click: the portalled popup can mount under the cursor, which
195
+ // retargets the click to a common ancestor outside the select.
196
+ if (pointerPressedInside.current)
197
+ return;
198
+ if (!isInsideSelect(e.target))
199
+ close();
200
+ },
201
+ });
202
+ useEffect(() => {
203
+ if (!isOpen)
204
+ resetSearch();
205
+ }, [isOpen]);
206
+ useEffect(() => {
207
+ if (props.searchValue === undefined)
208
+ return;
209
+ setIsSearching(props.searchValue !== '');
210
+ }, [props.searchValue]);
211
+ useEffect(() => () => {
212
+ window.clearTimeout(blurTimer.current);
213
+ window.clearTimeout(pointerPressTimer.current);
214
+ }, []);
215
+ const triggerProps = useMemo(() => ({
216
+ label,
217
+ size,
218
+ round,
219
+ variant,
220
+ ...props.triggerProps,
221
+ }), [props.triggerProps, label, size, round, variant]);
154
222
  const getFieldLabel = (label) => {
155
223
  if (disableLabel)
156
224
  return null;
157
- // @ts-ignore
158
- const length = value?.length;
159
- if (isMultiple$1 && length && showSelectedCount)
160
- return `${label} (${length})`;
225
+ if (isMultiple$1 && selectedIds.length && showSelectedCount)
226
+ return `${label} (${selectedIds.length})`;
161
227
  return label;
162
228
  };
163
- const filterOption = ({ label }) => {
164
- return label.toLowerCase().includes(searchValLower.current);
165
- };
166
- const selectedLabel = useMemo(() => {
167
- if (!isMultiple$1)
168
- return getLabel(value);
169
- if (!value)
170
- return '';
171
- return (value
172
- // @ts-ignore
173
- .reduce((acc, id) => {
174
- const label = getLabel(id);
175
- return label ? [...acc, label] : acc;
176
- }, [])
177
- .join(', '));
178
- }, [isMultiple$1, value, ids]);
179
- const renderSelectedChips = () => {
180
- if (!isMultiple$1 || !value || !value.length)
181
- return null;
182
- return value.map(id => {
183
- const label = getLabel(id);
184
- if (!label)
185
- return null;
186
- return (jsx(Chip, { className: cn(S.chip, selectedChipIds?.includes(id) && selectedChipClassName, ids.items[id]?.chipClassName), size: size, selected: selectedChipIds?.includes(id), onRemove: () => onItemToggle(id), onClick: () => onChipClick?.(id), removeTooltip: selectedChipRemoveTooltip, children: label }, id));
187
- });
188
- };
229
+ const selectedLabel = useMemo(() => selectedIds
230
+ .map(getLabel)
231
+ .filter(Boolean)
232
+ .join(', '), [selectedIds, optionsById]);
189
233
  const triggerArrow = useMemo(() => {
190
234
  if (disableTriggerArrow || (inputProps?.hasClear && searchVal))
191
235
  return null;
192
236
  return (jsx(Icon, { type: "chevronDown", className: cn(S.triggerArrow, isOpen && S.isOpen), size: size }));
193
- }, [isOpen, searchVal]);
237
+ }, [disableTriggerArrow, inputProps?.hasClear, searchVal, isOpen, size]);
238
+ const renderSelectedChips = () => selectedIds.map(id => {
239
+ const label = getLabel(id);
240
+ if (!label)
241
+ return null;
242
+ return (jsx(Chip, { className: cn(S.chip, selectedChipIds?.includes(id) && selectedChipClassName, optionsById.items[id]?.chipClassName), size: size, selected: selectedChipIds?.includes(id), onRemove: () => onItemToggle(id), onClick: () => onChipClick?.(id), removeTooltip: selectedChipRemoveTooltip, children: label }, id));
243
+ });
194
244
  const renderTriggerInput = () => {
195
- const hasChips = isMultiple$1 && value && value.length > 0;
196
- const inputValue = isMultiple$1
197
- ? isFocused && isSearchActive
198
- ? searchVal
199
- : ''
200
- : isFocused && isSearchActive
201
- ? searchVal
202
- : selectedLabel;
245
+ const inputValue = isSearching ? searchVal : isMultiple$1 ? '' : selectedLabel;
203
246
  return (jsx(Input, { ...triggerProps, ...inputProps,
204
247
  // TODO: autoComplete
205
248
  addonRight: triggerArrow, error: isErrorVisible, value: inputValue, onChange: handleSearchChange, label: getFieldLabel(label), placeholder: hasChips && !inputValue ? '' : inputProps?.placeholder }));
206
249
  };
207
250
  const renderTriggerButton = () => {
208
251
  const { label, className, ...rest } = triggerProps;
209
- const props = omit(rest, ['name', 'inputProps']);
210
- isMultiple$1 && value && value.length > 0;
252
+ const buttonProps = omit(rest, ['name', 'inputProps']);
211
253
  const fullSelectedLabel = [selectedLabel, additionalLabel].filter(Boolean);
212
254
  const hasSelected = fullSelectedLabel.length > 0;
213
255
  const displayLabel = hasSelected ? fullSelectedLabel : label;
214
256
  const title = hasSelected && !isMultiple$1 ? fullSelectedLabel : null;
215
- const isError = isErrorVisible;
216
- const classes = cn(S.triggerButton, isError && S.isError, triggerArrow && S.hasTriggerArrow, className);
217
- return (jsxs("div", { children: [jsxs(Button, { className: classes, variant: "default", ...props, style: { clipPath: labelClipPath }, title: title?.join?.(', '), children: [jsx("div", { className: cn(S.triggerButtonLabel, hasSelected && S.hasSelected), children: displayLabel }), triggerArrow] }), !isMultiple$1 && (jsx(Label, { size: size, isOnTop: hasSelected, isError: isError, onClipPathChange: setLabelClipPath, children: getFieldLabel(label) }))] }));
257
+ const classes = cn(S.triggerButton, isErrorVisible && S.isError, triggerArrow && S.hasTriggerArrow, className);
258
+ return (jsxs("div", { children: [jsxs(Button, { className: classes, variant: "default", ...buttonProps, style: { clipPath: labelClipPath }, title: title?.join?.(', '), children: [jsx("div", { className: cn(S.triggerButtonLabel, hasSelected && S.hasSelected), children: displayLabel }), triggerArrow] }), !isMultiple$1 && (jsx(Label, { size: size, isOnTop: hasSelected, isError: isErrorVisible, onClipPathChange: setLabelClipPath, children: getFieldLabel(label) }))] }));
218
259
  };
219
260
  const renderTrigger = () => {
220
261
  if (trigger)
@@ -222,13 +263,12 @@ function Select2(props) {
222
263
  const triggerElem = isSearchable
223
264
  ? renderTriggerInput()
224
265
  : renderTriggerButton();
225
- const hasChips = isMultiple$1 && value && value.length > 0;
226
- return (jsxs("div", { className: S.trigger, children: [hasChips && (jsx(Scroll, { y: true, className: S.chipsContainer, innerClassName: S.chipContainerInner, size: size, fadeSize: size, autoHide: true, children: renderSelectedChips() })), triggerElem, required && !hideRequiredStar && jsx(RequiredStar, { size: size })] }));
266
+ return (jsxs("div", { className: S.trigger, onPointerDown: onTriggerPointerDown, children: [hasChips && (jsx(Scroll, { y: true, className: S.chipsContainer, innerClassName: S.chipContainerInner, size: size, fadeSize: size, autoHide: true, children: renderSelectedChips() })), triggerElem, required && !hideRequiredStar && jsx(RequiredStar, { size: size })] }));
227
267
  };
228
268
  const renderPresets = () => {
229
269
  const items = presets.map(({ label, ids }) => ({
230
270
  children: label,
231
- onClick: () => setSelected(ids),
271
+ onClick: () => applyPreset(ids),
232
272
  key: label,
233
273
  }));
234
274
  if (selectAllButton) {
@@ -247,87 +287,25 @@ function Select2(props) {
247
287
  }
248
288
  if (items.length === 0)
249
289
  return null;
250
- return (jsx("div", { className: S.presetPanel, children: items.map(props => (jsx(Button, { className: S.presetButton, variant: "clear", ...props }))) }, "preset-panel"));
290
+ return (jsx("div", { className: S.presetPanel, children: items.map(({ key, ...rest }) => (jsx(Button, { className: S.presetButton, variant: "clear", ...rest }, key))) }, "preset-panel"));
251
291
  };
252
292
  const renderOption = (item, level = 0) => {
253
293
  const { id, isGroupHeader } = item;
254
- const isFocused = id === focusedItemId.current;
255
- const isSelected = selected.includes(id);
256
- const items = [];
257
- const className = cn(S.option, isGroupHeader && S.isGroup, isFocused && S.isFocused, isSelected && S.isSelected, S[`level-${level}`], optionClassName);
258
294
  const optionProps = {
259
- className,
295
+ className: cn(S.option, isGroupHeader && S.isGroup, id === focusedId && S.isFocused, isSelected(id) && S.isSelected, S[`level-${level}`], optionClassName),
260
296
  onPointerUp: () => onItemToggle(id),
261
297
  onPointerEnter: () => onOptionHover(id),
262
298
  };
263
- // @ts-ignore
264
- if (isFocused)
299
+ if (id === focusedId)
265
300
  optionProps.ref = onFocusedElemRef;
266
- if (isSelected && !isFirstSelectedMeet.current) {
267
- isFirstSelectedMeet.current = true;
268
- }
269
- if (filterOption(item)) {
270
- items.unshift(jsx("div", { ...optionProps, children: renderLabel(item) }, id));
271
- }
272
- return items;
273
- };
274
- const renderOptions = () => {
275
- isFirstSelectedMeet.current = false;
276
- return [...additionalOptions, ...items]
277
- .map(item => renderOption(item))
278
- .flat();
301
+ return (jsx("div", { ...optionProps, children: renderLabel(item) }, id));
279
302
  };
280
- useEffect(() => {
281
- const items = additionalOptions?.length
282
- ? [...additionalOptions, ...options]
283
- : options;
284
- setNewItems(items);
285
- }, [options]);
286
- useEffect(() => {
287
- setIsOpen(props.isOpen);
288
- }, [props.isOpen]);
289
- useEffect(() => {
290
- const searchVal = props.searchValue;
291
- if (searchVal)
292
- setSearchVal(searchVal);
293
- }, [props.searchValue]);
294
- const onKeyDown = useCallback((e) => {
295
- const currIndex = focusedItemIndex;
296
- if (e.key === 'ArrowUp') {
297
- if (currIndex > 0)
298
- setItemFocus(currIndex - 1);
299
- }
300
- if (e.key === 'ArrowDown') {
301
- if (currIndex < maxIndex.current)
302
- setItemFocus(currIndex + 1);
303
- }
304
- if (currIndex === -1 || !isOpen)
305
- return;
306
- if (e.key === 'Enter') {
307
- e.preventDefault();
308
- e.stopPropagation();
309
- onItemToggle(items[currIndex].id);
310
- }
311
- }, [items, focusedItemIndex, isOpen, selected]);
312
- useEvent({
313
- event: 'keydown',
314
- isActive: isOpen,
315
- callback: onKeyDown,
316
- });
317
- useEvent({
318
- event: 'click',
319
- // isActive: isMultiple,
320
- callback: e => {
321
- if (!isClickedInside(e.target)) {
322
- setIsOpen(false);
323
- }
324
- },
325
- });
326
- const optionsList = useMemo(() => (jsxs("div", { ref: contentRef, children: [renderPresets(), createElement(Scroll, { y: true, ...scrollProps, offset: { y: { before: 10, after: 10 } }, className: cn(S.options, S[`size-${size}`], scrollProps?.className), onInnerRef: elem => (scrollInnerRef.current = elem), key: "items-scroll" }, renderOptions())] })), [items, searchVal, focusedItemIndex, selected]);
303
+ const optionsList = (jsxs("div", { ref: contentRef, "data-select-id": instanceId.current, children: [renderPresets(), createElement(Scroll, { y: true, ...scrollProps, offset: { y: { before: 10, after: 10 } }, className: cn(S.options, S[`size-${size}`], optionsClassName, scrollProps?.className), onInnerRef: onScrollInnerRef, key: "items-scroll" }, visibleOptions.map(item => renderOption(item)))] }));
327
304
  const classes = cn(S.root, className, S[`size-${size}`]);
328
- return (jsxs(Fragment, { children: [jsx(Popup, { className: classes, direction: "bottom", size: size, focusControl: true, hoverControl: isFocused, blur: blur, isOpen: isOpen, disabled: disabled, ...popupProps, onOpen: onPopupOpen, onClose: onPopupClose, trigger: renderTrigger(), triggerProps: {
329
- onFocus: onFocus,
330
- onBlur: onBlur,
305
+ return (jsxs(Fragment, { children: [jsx(Popup, { className: classes, direction: "bottom", size: size, focusControl: true, blur: blur, round: round, disabled: disabled, ...popupProps, isOpen: isOpen, onOpen: openIfEnabled, onClose: close, trigger: renderTrigger(), triggerProps: {
306
+ 'data-select-id': instanceId.current,
307
+ onFocus: handleFocus,
308
+ onBlur: handleBlur,
331
309
  }, content: optionsList }), isErrorVisible && (jsx(AssistiveText, { variant: "danger", size: size, children: error }))] }));
332
310
  }
333
311
 
@@ -1,6 +1,6 @@
1
1
  import styleInject from '../../../node_modules/style-inject/dist/style-inject.es.js';
2
2
 
3
- var css_248z = ".Tooltip_trigger__9GSNV{display:inline-block}.Tooltip_tooltip__tzL8q{word-wrap:break-word;background:var(--decent-color-alpha-900);border-radius:4px;box-shadow:0 0 0 1px var(--accent-color-alpha-100);color:var(--accent-color);font-size:14px;line-height:1.4;margin:0;max-width:250px;padding:6px 12px;pointer-events:none;position:fixed;z-index:9999}.Tooltip_tooltip__tzL8q.Tooltip_blur__SQYHW{-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);background:var(--decent-color-alpha-500)}.Tooltip_tooltip__tzL8q:after{content:\"\";position:absolute}.Tooltip_tooltip__tzL8q[data-direction=top]{transform:translateY(0)}.Tooltip_tooltip__tzL8q[data-direction=top]:after{left:50%;top:100%;transform:translateX(-50%)}.Tooltip_tooltip__tzL8q[data-direction=bottom]{transform:translateY(0)}.Tooltip_tooltip__tzL8q[data-direction=bottom]:after{bottom:100%;left:50%;transform:translateX(-50%)}.Tooltip_tooltip__tzL8q[data-direction=left]{transform:translateX(0)}.Tooltip_tooltip__tzL8q[data-direction=left]:after{left:100%;top:50%;transform:translateY(-50%)}.Tooltip_tooltip__tzL8q[data-direction=right]{transform:translateX(0)}.Tooltip_tooltip__tzL8q[data-direction=right]:after{right:100%;top:50%;transform:translateY(-50%)}.Tooltip_tooltip__tzL8q:popover-open{animation:Tooltip_fadeIn__Buhx4 .2s ease-out}.Tooltip_tooltip__tzL8q.Tooltip_tooltipOverTrigger__gRUoT{word-wrap:break-word;box-sizing:border-box;height:-moz-fit-content;height:fit-content;max-width:none;overflow-wrap:break-word;white-space:normal}.Tooltip_tooltip__tzL8q.Tooltip_tooltipOverTrigger__gRUoT:after{display:none}.Tooltip_tooltip__tzL8q.Tooltip_tooltipOverTrigger__gRUoT:popover-open{animation:Tooltip_fadeIn__Buhx4 .15s ease-out}@keyframes Tooltip_fadeIn__Buhx4{0%{opacity:0}to{opacity:1}}";
3
+ var css_248z = ".Tooltip_trigger__9GSNV{display:inline-block}.Tooltip_tooltip__tzL8q{word-wrap:break-word;background:var(--decent-color-alpha-900);border-radius:4px;box-shadow:0 0 0 1px var(--accent-color-alpha-100);color:var(--accent-color);font-size:14px;line-height:1.4;margin:0;max-width:250px;padding:6px 12px;pointer-events:none;position:fixed;z-index:9999}.Tooltip_tooltip__tzL8q.Tooltip_blur__SQYHW{-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);background:var(--decent-color-alpha-500)}.Tooltip_tooltip__tzL8q:after{content:\"\";position:absolute}.Tooltip_tooltip__tzL8q[data-direction=top]{transform:translateY(0)}.Tooltip_tooltip__tzL8q[data-direction=top]:after{left:50%;top:100%;transform:translateX(-50%)}.Tooltip_tooltip__tzL8q[data-direction=bottom]{transform:translateY(0)}.Tooltip_tooltip__tzL8q[data-direction=bottom]:after{bottom:100%;left:50%;transform:translateX(-50%)}.Tooltip_tooltip__tzL8q[data-direction=left]{transform:translateX(0)}.Tooltip_tooltip__tzL8q[data-direction=left]:after{left:100%;top:50%;transform:translateY(-50%)}.Tooltip_tooltip__tzL8q[data-direction=right]{transform:translateX(0)}.Tooltip_tooltip__tzL8q[data-direction=right]:after{right:100%;top:50%;transform:translateY(-50%)}.Tooltip_tooltip__tzL8q:popover-open{animation:Tooltip_fadeIn__Buhx4 .2s ease-out}.Tooltip_tooltip__tzL8q.Tooltip_tooltipOverTrigger__gRUoT{word-wrap:break-word;box-sizing:border-box;font-size:inherit;height:-moz-fit-content;height:fit-content;line-height:inherit;max-width:none;overflow-wrap:break-word;white-space:normal}.Tooltip_tooltip__tzL8q.Tooltip_tooltipOverTrigger__gRUoT:after{display:none}.Tooltip_tooltip__tzL8q.Tooltip_tooltipOverTrigger__gRUoT:popover-open{animation:Tooltip_fadeIn__Buhx4 .15s ease-out}@keyframes Tooltip_fadeIn__Buhx4{0%{opacity:0}to{opacity:1}}";
4
4
  var S = {"trigger":"Tooltip_trigger__9GSNV","tooltip":"Tooltip_tooltip__tzL8q","blur":"Tooltip_blur__SQYHW","fadeIn":"Tooltip_fadeIn__Buhx4","tooltipOverTrigger":"Tooltip_tooltipOverTrigger__gRUoT"};
5
5
  styleInject(css_248z);
6
6
 
@@ -2,13 +2,14 @@ import { jsx } from 'react/jsx-runtime';
2
2
  import { useRef, useCallback, useLayoutEffect, useEffect } from 'react';
3
3
  import S from './Tooltip.styl.js';
4
4
  import { TooltipCore } from './TooltipCore.js';
5
- import { applyTriggerTextStyles, getTextStyleSource, clearTriggerTextStyles } from './tooltipTextStyles.js';
5
+ import { getTextStyleSource, applyTriggerTextStyles, clearTriggerTextStyles } from './tooltipTextStyles.js';
6
6
 
7
7
  function parsePx(value) {
8
8
  return Number.parseFloat(value) || 0;
9
9
  }
10
10
  function applyOverTriggerStyles(contentEl, triggerEl) {
11
- const rect = triggerEl.getBoundingClientRect();
11
+ const textSource = getTextStyleSource(triggerEl);
12
+ const rect = textSource.getBoundingClientRect();
12
13
  const contentComputed = window.getComputedStyle(contentEl);
13
14
  const paddingLeft = parsePx(contentComputed.paddingLeft);
14
15
  const paddingRight = parsePx(contentComputed.paddingRight);
@@ -21,7 +22,7 @@ function applyOverTriggerStyles(contentEl, triggerEl) {
21
22
  contentEl.style.setProperty('transform', 'none');
22
23
  contentEl.style.width = `${rect.width + paddingLeft + paddingRight + borderLeft + borderRight}px`;
23
24
  contentEl.style.boxSizing = 'border-box';
24
- applyTriggerTextStyles(contentEl, getTextStyleSource(triggerEl));
25
+ applyTriggerTextStyles(contentEl, textSource);
25
26
  // Trigger ancestors (e.g. menu items) often use nowrap for ellipsis; allow wrap here.
26
27
  contentEl.style.setProperty('white-space', 'normal');
27
28
  contentEl.style.setProperty('overflow-wrap', 'break-word');
@@ -30,9 +30,24 @@ const TRIGGER_TEXT_STYLE_PROPS = [
30
30
  'overflow-wrap',
31
31
  'hyphens',
32
32
  ];
33
+ function hasDirectText(el) {
34
+ return Array.from(el.childNodes).some(node => node.nodeType === Node.TEXT_NODE &&
35
+ (node.textContent?.trim().length ?? 0) > 0);
36
+ }
33
37
  function getTextStyleSource(triggerEl) {
34
- const firstChild = triggerEl.firstElementChild;
35
- return firstChild instanceof HTMLElement ? firstChild : triggerEl;
38
+ let source = triggerEl;
39
+ const walk = (el) => {
40
+ if (hasDirectText(el)) {
41
+ source = el;
42
+ }
43
+ for (const child of el.children) {
44
+ if (child instanceof HTMLElement) {
45
+ walk(child);
46
+ }
47
+ }
48
+ };
49
+ walk(triggerEl);
50
+ return source;
36
51
  }
37
52
  function applyTriggerTextStyles(contentEl, sourceEl) {
38
53
  const computed = window.getComputedStyle(sourceEl);