@cloud-ru/ds-fields 1.0.2-preview-cd0cd6fb.0 → 1.0.2

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,12 @@
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
+ ## 1.0.2 (2026-08-20)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **FF-8879:** keep selected item label when items change in FieldSelect ([f42c100](https://github.com/cloud-ru-tech/snack-v2/commit/f42c100707a25c9bee46906f5a78b8624d1b3ecf))
11
+
6
12
  ## 1.0.1 (2026-08-18)
7
13
 
8
14
  **Note:** Version bump only for package @ds/fields
@@ -61,13 +61,23 @@ exports.FieldSelect = (0, react_1.forwardRef)(function FieldSelect(props, ref) {
61
61
  }, [multiple, props.value, multipleLocal]);
62
62
  const effectiveValidationState = (0, react_1.useMemo)(() => (error ? field_decorator_1.VALIDATION_STATE.Error : validationState), [error, validationState]);
63
63
  const allItems = (0, react_1.useMemo)(() => [...(pinTop ?? []), ...items, ...(pinBottom ?? [])], [items, pinTop, pinBottom]);
64
+ // Выбранное значение переживает смену `items` (серверный поиск, ленивая подгрузка).
65
+ const seenItems = (0, react_1.useRef)(new Map());
66
+ const resolveItem = (0, react_1.useMemo)(() => {
67
+ for (const item of (0, utils_2.flatten)(allItems)) {
68
+ if (item.id !== undefined) {
69
+ seenItems.current.set(item.id, item);
70
+ }
71
+ }
72
+ return (id) => (0, utils_2.findItem)(allItems, id) ?? seenItems.current.get(id);
73
+ }, [allItems]);
64
74
  const selectedPairs = (0, react_1.useMemo)(() => {
65
75
  if (!multiple || multipleValue.length === 0) {
66
76
  return [];
67
77
  }
68
78
  // Неизвестное значение (ещё не загружены опции) показываем по его id, не теряем (паритет с легаси).
69
79
  return multipleValue.map(id => {
70
- const item = (0, utils_2.findItem)(allItems, id);
80
+ const item = resolveItem(id);
71
81
  return {
72
82
  id,
73
83
  label: item ? (0, utils_2.extractLabel)(item) : String(id),
@@ -75,14 +85,14 @@ exports.FieldSelect = (0, react_1.forwardRef)(function FieldSelect(props, ref) {
75
85
  appearance: (0, utils_2.extractAppearance)(item),
76
86
  };
77
87
  });
78
- }, [allItems, multiple, multipleValue]);
88
+ }, [multiple, multipleValue, resolveItem]);
79
89
  const formatPair = (0, react_1.useCallback)((pair) => (selectedOptionFormatter ? selectedOptionFormatter(pair) : pair.label), [selectedOptionFormatter]);
80
90
  const selectedLabel = (0, react_1.useMemo)(() => {
81
91
  if (!multiple) {
82
92
  if (singleValue === undefined) {
83
93
  return '';
84
94
  }
85
- const item = (0, utils_2.findItem)(allItems, singleValue);
95
+ const item = resolveItem(singleValue);
86
96
  const label = item ? (0, utils_2.extractLabel)(item) : String(singleValue);
87
97
  return formatPair({ id: singleValue, label });
88
98
  }
@@ -93,7 +103,7 @@ exports.FieldSelect = (0, react_1.forwardRef)(function FieldSelect(props, ref) {
93
103
  return props.formatSelected(selectedPairs);
94
104
  }
95
105
  return selectedPairs.map(formatPair).join(', ');
96
- }, [allItems, multiple, singleValue, selectedPairs, chips, props, formatPair]);
106
+ }, [multiple, singleValue, selectedPairs, chips, props, formatPair, resolveItem]);
97
107
  // Строка поиска: controlled через `search.value`, иначе локальный state. Auto-sync к выбранному
98
108
  // значению делаем через СТАБИЛЬНЫЙ setLocalInput, а не через setInputValue в deps эффекта —
99
109
  // иначе нестабильная ссылка сеттера зацикливает эффект (сбрасывала бы значение постоянно).
@@ -24,11 +24,6 @@ exports.FieldTextArea = (0, react_1.forwardRef)(function FieldTextArea({ label =
24
24
  const copyButtonRef = (0, react_1.useRef)(null);
25
25
  const [focusVisible, setFocusVisible] = (0, react_1.useState)(false);
26
26
  const [hover, setHover] = (0, react_1.useState)(false);
27
- // autosize меряет высоту до того, как ThemeScope навесит классы темы — пересчитываем после mount.
28
- const [, remeasure] = (0, react_1.useReducer)((tick) => tick + 1, 0);
29
- (0, react_1.useEffect)(() => {
30
- remeasure();
31
- }, []);
32
27
  const resolvedAutoFocus = (0, hooks_1.useAdaptiveAutoFocus)(autoFocus, layoutPresets);
33
28
  // controlled/uncontrolled: useValueControl держит внутреннее состояние при отсутствии `value`,
34
29
  // событие в `onChange` пробрасываем сами (useValueControl передаёт только значение).
@@ -16,7 +16,7 @@ import { useAdaptiveAutoFocus } from '../../hooks/index.js';
16
16
  import { copyTextToClipboard, getAcrylicProps, preventSlotMouseDown, stopSlotClickPropagation, toInputSize, useCopyButton, } from '../shared/index.js';
17
17
  import fieldStyles from '../shared/styles.module.css';
18
18
  import styles from './styles.module.css';
19
- import { extractAppearance, extractLabel, filterItems, findItem, isMultiple, TAG_SIZE_MAP } from './utils.js';
19
+ import { extractAppearance, extractLabel, filterItems, findItem, flatten, isMultiple, TAG_SIZE_MAP, } from './utils.js';
20
20
  export const FieldSelect = forwardRef(function FieldSelect(props, ref) {
21
21
  const { label = '', labelTooltip, caption, hint, error, validationState = VALIDATION_STATE.Default, showHintIcon, length, required, size = SIZE.M, className, fieldClassName, items, pinTop, pinBottom, placeholder = '', iconBefore, prefix, postfix, disabled, readonly: readOnly, background = true, open: openProp, onOpenChange, placement, widthStrategy = 'eq', searchable = true, search, autocomplete = false, addOptionByEnter = false, resetSearchOnOptionSelection = true, showClearButton = true, showCopyButton = true, onCopyButtonClick, id, name, autoFocus, layoutPresets, onFocus: onFocusProp, onBlur: onBlurProp, enableFuzzySearch = true, selectedOptionFormatter, footer, footerActiveElementsRefs, loading, noDataState, noResultsState, errorDataState, dataError, dataFiltered, virtualized, scrollToSelectedItem, limitedScrollHeight, untouchableScrollbars, closeOnPopstate, onKeyDown: onKeyDownProp, 'data-test-id': dataTestId = TEST_IDS.fieldSelect, } = props;
22
22
  const multiple = isMultiple(props);
@@ -55,13 +55,23 @@ export const FieldSelect = forwardRef(function FieldSelect(props, ref) {
55
55
  }, [multiple, props.value, multipleLocal]);
56
56
  const effectiveValidationState = useMemo(() => (error ? VALIDATION_STATE.Error : validationState), [error, validationState]);
57
57
  const allItems = useMemo(() => [...(pinTop ?? []), ...items, ...(pinBottom ?? [])], [items, pinTop, pinBottom]);
58
+ // Выбранное значение переживает смену `items` (серверный поиск, ленивая подгрузка).
59
+ const seenItems = useRef(new Map());
60
+ const resolveItem = useMemo(() => {
61
+ for (const item of flatten(allItems)) {
62
+ if (item.id !== undefined) {
63
+ seenItems.current.set(item.id, item);
64
+ }
65
+ }
66
+ return (id) => findItem(allItems, id) ?? seenItems.current.get(id);
67
+ }, [allItems]);
58
68
  const selectedPairs = useMemo(() => {
59
69
  if (!multiple || multipleValue.length === 0) {
60
70
  return [];
61
71
  }
62
72
  // Неизвестное значение (ещё не загружены опции) показываем по его id, не теряем (паритет с легаси).
63
73
  return multipleValue.map(id => {
64
- const item = findItem(allItems, id);
74
+ const item = resolveItem(id);
65
75
  return {
66
76
  id,
67
77
  label: item ? extractLabel(item) : String(id),
@@ -69,14 +79,14 @@ export const FieldSelect = forwardRef(function FieldSelect(props, ref) {
69
79
  appearance: extractAppearance(item),
70
80
  };
71
81
  });
72
- }, [allItems, multiple, multipleValue]);
82
+ }, [multiple, multipleValue, resolveItem]);
73
83
  const formatPair = useCallback((pair) => (selectedOptionFormatter ? selectedOptionFormatter(pair) : pair.label), [selectedOptionFormatter]);
74
84
  const selectedLabel = useMemo(() => {
75
85
  if (!multiple) {
76
86
  if (singleValue === undefined) {
77
87
  return '';
78
88
  }
79
- const item = findItem(allItems, singleValue);
89
+ const item = resolveItem(singleValue);
80
90
  const label = item ? extractLabel(item) : String(singleValue);
81
91
  return formatPair({ id: singleValue, label });
82
92
  }
@@ -87,7 +97,7 @@ export const FieldSelect = forwardRef(function FieldSelect(props, ref) {
87
97
  return props.formatSelected(selectedPairs);
88
98
  }
89
99
  return selectedPairs.map(formatPair).join(', ');
90
- }, [allItems, multiple, singleValue, selectedPairs, chips, props, formatPair]);
100
+ }, [multiple, singleValue, selectedPairs, chips, props, formatPair, resolveItem]);
91
101
  // Строка поиска: controlled через `search.value`, иначе локальный state. Auto-sync к выбранному
92
102
  // значению делаем через СТАБИЛЬНЫЙ setLocalInput, а не через setInputValue в deps эффекта —
93
103
  // иначе нестабильная ссылка сеттера зацикливает эффект (сбрасывала бы значение постоянно).
@@ -5,7 +5,7 @@ import { BAR_HIDE_STRATEGY, RESIZE, Scroll, SIZE as SCROLL_SIZE } from '@cloud-r
5
5
  import { extractSupportProps, useValueControl } from '@cloud-ru/ds-utils';
6
6
  import cn from 'classnames';
7
7
  import mergeRefs from 'merge-refs';
8
- import { forwardRef, useCallback, useEffect, useMemo, useReducer, useRef, useState, } from 'react';
8
+ import { forwardRef, useCallback, useMemo, useRef, useState, } from 'react';
9
9
  import TextareaAutosize from 'react-textarea-autosize';
10
10
  import { TEST_IDS } from '../../constants.js';
11
11
  import { useAdaptiveAutoFocus } from '../../hooks/index.js';
@@ -18,11 +18,6 @@ export const FieldTextArea = forwardRef(function FieldTextArea({ label = '', lab
18
18
  const copyButtonRef = useRef(null);
19
19
  const [focusVisible, setFocusVisible] = useState(false);
20
20
  const [hover, setHover] = useState(false);
21
- // autosize меряет высоту до того, как ThemeScope навесит классы темы — пересчитываем после mount.
22
- const [, remeasure] = useReducer((tick) => tick + 1, 0);
23
- useEffect(() => {
24
- remeasure();
25
- }, []);
26
21
  const resolvedAutoFocus = useAdaptiveAutoFocus(autoFocus, layoutPresets);
27
22
  // controlled/uncontrolled: useValueControl держит внутреннее состояние при отсутствии `value`,
28
23
  // событие в `onChange` пробрасываем сами (useValueControl передаёт только значение).