@homecode/ui 5.6.1 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +192 -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,226 @@ 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 pointerPressTimer = useRef(0);
34
+ const blurTimer = useRef(0);
38
35
  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
- }
36
+ const [isSearching, setIsSearching] = useState(false);
37
+ const [innerSearchVal, setInnerSearchVal] = useState('');
38
+ const searchVal = props.searchValue ?? innerSearchVal;
39
+ const searchQuery = isSearching ? searchVal : '';
40
+ const { selectedIds, setSelectedIds } = useSelectedIds(value);
41
+ const { allOptions, visibleOptions, navigableOptions, optionsById } = useSelectOptions({
42
+ options,
43
+ additionalOptions,
44
+ searchQuery,
45
+ });
46
+ const handleOpen = useCallback(() => {
110
47
  popupProps?.onOpen?.();
111
48
  onOpen?.();
112
- };
113
- const onPopupClose = () => {
114
- setIsOpen(false);
115
- setSearchVal('');
116
- setIsSearchActive(false);
117
- setItemFocus(0);
49
+ }, [popupProps?.onOpen, onOpen]);
50
+ const handleClose = useCallback(() => {
118
51
  popupProps?.onClose?.();
119
52
  onClose?.();
53
+ }, [popupProps?.onClose, onClose]);
54
+ const { isOpen, open, close } = useSelectOpenState({
55
+ isOpen: props.isOpen,
56
+ onOpen: handleOpen,
57
+ onClose: handleClose,
58
+ });
59
+ const isErrorVisible = !isOpen && !!error;
60
+ const hasChips = isMultiple$1 && selectedIds.length > 0;
61
+ const isSelected = (id) => selectedIds.includes(id);
62
+ const getLabel = (id) => optionsById.items[id]?.label || '';
63
+ const isInsideSelect = (elem) => elem instanceof Element &&
64
+ Boolean(elem.closest(`[data-select-id="${instanceId.current}"]`));
65
+ const openIfEnabled = () => {
66
+ if (disabled)
67
+ return;
68
+ open();
120
69
  };
121
- const onFocus = () => {
122
- setIsFocused(true);
70
+ const resetSearch = () => {
71
+ setIsSearching(false);
72
+ setInnerSearchVal('');
73
+ if (searchVal)
74
+ onSearchChange?.('');
123
75
  };
124
- const onBlur = () => {
125
- setIsFocused(false);
76
+ const applySelection = (ids, nextValue) => {
77
+ setSelectedIds(ids);
78
+ onChange?.(nextValue);
126
79
  };
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
80
  const onItemToggle = (id) => {
81
+ if (!isSelectable(optionsById.items[id]))
82
+ return;
135
83
  if (isMultiple$1) {
136
- const newValue = isSelected(id)
137
- ? selected.filter(i => i !== id)
138
- : [...selected, id];
139
- setSelected(newValue);
140
- onChange?.(newValue);
84
+ const ids = isSelected(id)
85
+ ? selectedIds.filter(i => i !== id)
86
+ : [...selectedIds, id];
87
+ applySelection(ids, ids);
141
88
  }
142
89
  else {
143
- // mono select
144
- const newValue = isSelected(id) ? null : id;
145
- const newSelected = isSelected(id) ? [] : [id];
146
- setSelected(newSelected);
147
- onChange?.(newValue);
90
+ const nextValue = isSelected(id) ? null : id;
91
+ applySelection(nextValue === null ? [] : [id], nextValue);
148
92
  }
149
- setSearchVal('');
93
+ resetSearch();
150
94
  if (closeOnSelect)
151
- setIsOpen(false);
95
+ close();
96
+ };
97
+ const selectAll = () => {
98
+ const ids = allOptions.filter(isSelectable).map(({ id }) => id);
99
+ applySelection(ids, isMultiple$1 ? ids : (ids[0] ?? null));
100
+ };
101
+ const dropSelected = () => applySelection([], isMultiple$1 ? [] : null);
102
+ const applyPreset = (ids) => applySelection(ids, isMultiple$1 ? ids : (ids[0] ?? null));
103
+ const { focusedId, setFocusedId } = useKeyboardNav({
104
+ isOpen,
105
+ navigableOptions,
106
+ selectedIds,
107
+ onSelect: onItemToggle,
108
+ });
109
+ const onOptionHover = useThrottle((id) => {
110
+ if (getInteractionMode() !== INTERACTION_MODE.POINTER)
111
+ return;
112
+ setFocusedId(id);
113
+ }, 100, { trailing: true });
114
+ const onScrollInnerRef = useCallback((elem) => {
115
+ scrollInnerRef.current = elem;
116
+ }, []);
117
+ const onFocusedElemRef = (elem) => {
118
+ const content = contentRef.current;
119
+ const list = scrollInnerRef.current;
120
+ if (!elem || !content || !list)
121
+ return;
122
+ const { top, bottom } = elem.getBoundingClientRect();
123
+ const rect = content.getBoundingClientRect();
124
+ if (top < rect.top) {
125
+ list.scrollTop -= rect.top - top;
126
+ }
127
+ else if (bottom > rect.bottom) {
128
+ list.scrollTop += bottom - rect.bottom;
129
+ }
130
+ };
131
+ const handleSearchChange = (e, val) => {
132
+ setIsSearching(true);
133
+ setInnerSearchVal(val);
134
+ onSearchChange?.(val);
135
+ openIfEnabled();
136
+ };
137
+ const handleFocus = e => {
138
+ window.clearTimeout(blurTimer.current);
139
+ onFocus?.(e);
152
140
  };
153
- const getLabel = (id) => ids.items[id]?.label || '';
141
+ /**
142
+ * Popup skips its own blur-close while it is controlled, so closing on blur is
143
+ * up to Select. A press inside the popup blurs the trigger too — that one must
144
+ * not close the list.
145
+ */
146
+ const handleBlur = e => {
147
+ onBlur?.(e);
148
+ window.clearTimeout(blurTimer.current);
149
+ blurTimer.current = window.setTimeout(() => {
150
+ if (pointerPressedInside.current)
151
+ return;
152
+ close();
153
+ }, BLUR_CLOSE_DELAY);
154
+ };
155
+ const onTriggerPointerDown = () => {
156
+ if (disabled)
157
+ return;
158
+ // A searchable trigger is a text field: pressing it must never close.
159
+ if (isOpen && !isSearchable)
160
+ close();
161
+ else
162
+ open();
163
+ };
164
+ useEvent({
165
+ event: 'pointerdown',
166
+ isCapture: true,
167
+ callback: e => {
168
+ window.clearTimeout(pointerPressTimer.current);
169
+ pointerPressedInside.current = isInsideSelect(e.target);
170
+ pointerPressTimer.current = window.setTimeout(() => (pointerPressedInside.current = false), POINTER_PRESS_TTL);
171
+ },
172
+ });
173
+ useEvent({
174
+ event: 'click',
175
+ isActive: isOpen,
176
+ callback: e => {
177
+ if (!isInsideSelect(e.target))
178
+ close();
179
+ },
180
+ });
181
+ useEffect(() => {
182
+ if (!isOpen)
183
+ resetSearch();
184
+ }, [isOpen]);
185
+ useEffect(() => {
186
+ if (props.searchValue === undefined)
187
+ return;
188
+ setIsSearching(props.searchValue !== '');
189
+ }, [props.searchValue]);
190
+ useEffect(() => () => {
191
+ window.clearTimeout(blurTimer.current);
192
+ window.clearTimeout(pointerPressTimer.current);
193
+ }, []);
194
+ const triggerProps = useMemo(() => ({
195
+ label,
196
+ size,
197
+ round,
198
+ variant,
199
+ ...props.triggerProps,
200
+ }), [props.triggerProps, label, size, round, variant]);
154
201
  const getFieldLabel = (label) => {
155
202
  if (disableLabel)
156
203
  return null;
157
- // @ts-ignore
158
- const length = value?.length;
159
- if (isMultiple$1 && length && showSelectedCount)
160
- return `${label} (${length})`;
204
+ if (isMultiple$1 && selectedIds.length && showSelectedCount)
205
+ return `${label} (${selectedIds.length})`;
161
206
  return label;
162
207
  };
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
- };
208
+ const selectedLabel = useMemo(() => selectedIds
209
+ .map(getLabel)
210
+ .filter(Boolean)
211
+ .join(', '), [selectedIds, optionsById]);
189
212
  const triggerArrow = useMemo(() => {
190
213
  if (disableTriggerArrow || (inputProps?.hasClear && searchVal))
191
214
  return null;
192
215
  return (jsx(Icon, { type: "chevronDown", className: cn(S.triggerArrow, isOpen && S.isOpen), size: size }));
193
- }, [isOpen, searchVal]);
216
+ }, [disableTriggerArrow, inputProps?.hasClear, searchVal, isOpen, size]);
217
+ const renderSelectedChips = () => selectedIds.map(id => {
218
+ const label = getLabel(id);
219
+ if (!label)
220
+ return null;
221
+ 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));
222
+ });
194
223
  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;
224
+ const inputValue = isSearching ? searchVal : isMultiple$1 ? '' : selectedLabel;
203
225
  return (jsx(Input, { ...triggerProps, ...inputProps,
204
226
  // TODO: autoComplete
205
227
  addonRight: triggerArrow, error: isErrorVisible, value: inputValue, onChange: handleSearchChange, label: getFieldLabel(label), placeholder: hasChips && !inputValue ? '' : inputProps?.placeholder }));
206
228
  };
207
229
  const renderTriggerButton = () => {
208
230
  const { label, className, ...rest } = triggerProps;
209
- const props = omit(rest, ['name', 'inputProps']);
210
- isMultiple$1 && value && value.length > 0;
231
+ const buttonProps = omit(rest, ['name', 'inputProps']);
211
232
  const fullSelectedLabel = [selectedLabel, additionalLabel].filter(Boolean);
212
233
  const hasSelected = fullSelectedLabel.length > 0;
213
234
  const displayLabel = hasSelected ? fullSelectedLabel : label;
214
235
  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) }))] }));
236
+ const classes = cn(S.triggerButton, isErrorVisible && S.isError, triggerArrow && S.hasTriggerArrow, className);
237
+ 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
238
  };
219
239
  const renderTrigger = () => {
220
240
  if (trigger)
@@ -222,13 +242,12 @@ function Select2(props) {
222
242
  const triggerElem = isSearchable
223
243
  ? renderTriggerInput()
224
244
  : 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 })] }));
245
+ 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
246
  };
228
247
  const renderPresets = () => {
229
248
  const items = presets.map(({ label, ids }) => ({
230
249
  children: label,
231
- onClick: () => setSelected(ids),
250
+ onClick: () => applyPreset(ids),
232
251
  key: label,
233
252
  }));
234
253
  if (selectAllButton) {
@@ -247,87 +266,25 @@ function Select2(props) {
247
266
  }
248
267
  if (items.length === 0)
249
268
  return null;
250
- return (jsx("div", { className: S.presetPanel, children: items.map(props => (jsx(Button, { className: S.presetButton, variant: "clear", ...props }))) }, "preset-panel"));
269
+ return (jsx("div", { className: S.presetPanel, children: items.map(({ key, ...rest }) => (jsx(Button, { className: S.presetButton, variant: "clear", ...rest }, key))) }, "preset-panel"));
251
270
  };
252
271
  const renderOption = (item, level = 0) => {
253
272
  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
273
  const optionProps = {
259
- className,
274
+ className: cn(S.option, isGroupHeader && S.isGroup, id === focusedId && S.isFocused, isSelected(id) && S.isSelected, S[`level-${level}`], optionClassName),
260
275
  onPointerUp: () => onItemToggle(id),
261
276
  onPointerEnter: () => onOptionHover(id),
262
277
  };
263
- // @ts-ignore
264
- if (isFocused)
278
+ if (id === focusedId)
265
279
  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();
280
+ return (jsx("div", { ...optionProps, children: renderLabel(item) }, id));
279
281
  };
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]);
282
+ 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
283
  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,
284
+ 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: {
285
+ 'data-select-id': instanceId.current,
286
+ onFocus: handleFocus,
287
+ onBlur: handleBlur,
331
288
  }, content: optionsList }), isErrorVisible && (jsx(AssistiveText, { variant: "danger", size: size, children: error }))] }));
332
289
  }
333
290
 
@@ -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);