@astral/ui 4.36.0 → 4.37.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 (110) hide show
  1. package/components/Autocomplete/Autocomplete.d.ts +1 -1
  2. package/components/Autocomplete/Autocomplete.js +53 -15
  3. package/components/Autocomplete/constants.d.ts +6 -0
  4. package/components/Autocomplete/constants.js +7 -0
  5. package/components/Autocomplete/styles.js +15 -1
  6. package/components/Autocomplete/useLogic/useLogic.d.ts +2 -1
  7. package/components/Autocomplete/useLogic/useLogic.js +11 -2
  8. package/components/TagsList/Tag/Tag.d.ts +1 -1
  9. package/components/TagsList/Tag/constants.d.ts +1 -0
  10. package/components/TagsList/Tag/constants.js +1 -0
  11. package/components/TagsList/Tag/index.d.ts +1 -0
  12. package/components/TagsList/Tag/index.js +1 -0
  13. package/components/TagsList/Tag/styles.js +2 -0
  14. package/components/TagsList/TagsList.js +6 -6
  15. package/components/TagsList/constants.d.ts +2 -0
  16. package/components/TagsList/constants.js +2 -0
  17. package/components/TagsList/styles.js +5 -1
  18. package/components/TagsList/types.d.ts +14 -5
  19. package/components/TagsList/useLogic/useLogic.d.ts +5 -5
  20. package/components/TagsList/useLogic/useLogic.js +70 -110
  21. package/components/TagsList/utils/calculateVisibleTagsCount/calculateVisibleTagsCount.d.ts +6 -0
  22. package/components/TagsList/utils/calculateVisibleTagsCount/calculateVisibleTagsCount.js +35 -0
  23. package/components/TagsList/utils/calculateVisibleTagsCount/index.d.ts +1 -0
  24. package/components/TagsList/utils/calculateVisibleTagsCount/index.js +1 -0
  25. package/components/TagsList/utils/getAvailableWidth/getAvailableWidth.d.ts +1 -0
  26. package/components/TagsList/utils/getAvailableWidth/getAvailableWidth.js +7 -0
  27. package/components/TagsList/utils/getAvailableWidth/index.d.ts +1 -0
  28. package/components/TagsList/utils/getAvailableWidth/index.js +1 -0
  29. package/components/TagsList/utils/getKey/getKey.d.ts +1 -1
  30. package/components/TagsList/utils/getKey/getKey.js +6 -3
  31. package/components/TagsList/utils/getTagWidth/getTagWidth.d.ts +2 -0
  32. package/components/TagsList/utils/getTagWidth/getTagWidth.js +21 -0
  33. package/components/TagsList/utils/getTagWidth/index.d.ts +1 -0
  34. package/components/TagsList/utils/getTagWidth/index.js +1 -0
  35. package/components/TagsList/utils/index.d.ts +3 -0
  36. package/components/TagsList/utils/index.js +3 -0
  37. package/components/TextField/TextField.js +6 -5
  38. package/components/TextField/constants.d.ts +1 -0
  39. package/components/TextField/constants.js +1 -0
  40. package/components/TextField/styles.js +4 -0
  41. package/components/TreeAsyncAutocomplete/OptionsModal/styles.js +1 -1
  42. package/components/TreeAutocomplete/OptionsModal/OptionsModal.js +1 -1
  43. package/components/TreeAutocomplete/OptionsModal/styles.d.ts +4 -0
  44. package/components/TreeAutocomplete/OptionsModal/styles.js +10 -3
  45. package/components/TreeAutocomplete/OptionsModal/useLogic/useLogic.d.ts +5 -3
  46. package/components/TreeAutocomplete/OptionsModal/useLogic/useLogic.js +2 -1
  47. package/components/TreeLikeAsyncAutocomplete/Input/Input.js +1 -1
  48. package/components/TreeLikeAsyncAutocomplete/OptionsModal/styles.js +1 -1
  49. package/components/TreeLikeAutocomplete/OptionsModal/OptionsModal.js +1 -1
  50. package/components/TreeLikeAutocomplete/OptionsModal/styles.d.ts +4 -0
  51. package/components/TreeLikeAutocomplete/OptionsModal/styles.js +10 -3
  52. package/components/TreeLikeAutocomplete/OptionsModal/useLogic/useLogic.d.ts +5 -3
  53. package/components/TreeLikeAutocomplete/OptionsModal/useLogic/useLogic.js +2 -1
  54. package/node/components/Autocomplete/Autocomplete.d.ts +1 -1
  55. package/node/components/Autocomplete/Autocomplete.js +51 -13
  56. package/node/components/Autocomplete/constants.d.ts +6 -0
  57. package/node/components/Autocomplete/constants.js +8 -1
  58. package/node/components/Autocomplete/styles.js +15 -1
  59. package/node/components/Autocomplete/useLogic/useLogic.d.ts +2 -1
  60. package/node/components/Autocomplete/useLogic/useLogic.js +10 -1
  61. package/node/components/TagsList/Tag/Tag.d.ts +1 -1
  62. package/node/components/TagsList/Tag/constants.d.ts +1 -0
  63. package/node/components/TagsList/Tag/constants.js +4 -0
  64. package/node/components/TagsList/Tag/index.d.ts +1 -0
  65. package/node/components/TagsList/Tag/index.js +1 -0
  66. package/node/components/TagsList/Tag/styles.js +2 -0
  67. package/node/components/TagsList/TagsList.js +6 -6
  68. package/node/components/TagsList/constants.d.ts +2 -0
  69. package/node/components/TagsList/constants.js +5 -0
  70. package/node/components/TagsList/styles.js +5 -1
  71. package/node/components/TagsList/types.d.ts +14 -5
  72. package/node/components/TagsList/useLogic/useLogic.d.ts +5 -5
  73. package/node/components/TagsList/useLogic/useLogic.js +70 -110
  74. package/node/components/TagsList/utils/calculateVisibleTagsCount/calculateVisibleTagsCount.d.ts +6 -0
  75. package/node/components/TagsList/utils/calculateVisibleTagsCount/calculateVisibleTagsCount.js +39 -0
  76. package/node/components/TagsList/utils/calculateVisibleTagsCount/index.d.ts +1 -0
  77. package/node/components/TagsList/utils/calculateVisibleTagsCount/index.js +5 -0
  78. package/node/components/TagsList/utils/getAvailableWidth/getAvailableWidth.d.ts +1 -0
  79. package/node/components/TagsList/utils/getAvailableWidth/getAvailableWidth.js +11 -0
  80. package/node/components/TagsList/utils/getAvailableWidth/index.d.ts +1 -0
  81. package/node/components/TagsList/utils/getAvailableWidth/index.js +5 -0
  82. package/node/components/TagsList/utils/getKey/getKey.d.ts +1 -1
  83. package/node/components/TagsList/utils/getKey/getKey.js +6 -3
  84. package/node/components/TagsList/utils/getTagWidth/getTagWidth.d.ts +2 -0
  85. package/node/components/TagsList/utils/getTagWidth/getTagWidth.js +25 -0
  86. package/node/components/TagsList/utils/getTagWidth/index.d.ts +1 -0
  87. package/node/components/TagsList/{public.js → utils/getTagWidth/index.js} +1 -2
  88. package/node/components/TagsList/utils/index.d.ts +3 -0
  89. package/node/components/TagsList/utils/index.js +7 -1
  90. package/node/components/TextField/TextField.js +6 -5
  91. package/node/components/TextField/constants.d.ts +1 -0
  92. package/node/components/TextField/constants.js +1 -0
  93. package/node/components/TextField/styles.js +4 -0
  94. package/node/components/TreeAsyncAutocomplete/OptionsModal/styles.js +1 -1
  95. package/node/components/TreeAutocomplete/OptionsModal/OptionsModal.js +1 -1
  96. package/node/components/TreeAutocomplete/OptionsModal/styles.d.ts +4 -0
  97. package/node/components/TreeAutocomplete/OptionsModal/styles.js +10 -3
  98. package/node/components/TreeAutocomplete/OptionsModal/useLogic/useLogic.d.ts +5 -3
  99. package/node/components/TreeAutocomplete/OptionsModal/useLogic/useLogic.js +2 -1
  100. package/node/components/TreeLikeAsyncAutocomplete/Input/Input.js +1 -1
  101. package/node/components/TreeLikeAsyncAutocomplete/OptionsModal/styles.js +1 -1
  102. package/node/components/TreeLikeAutocomplete/OptionsModal/OptionsModal.js +1 -1
  103. package/node/components/TreeLikeAutocomplete/OptionsModal/styles.d.ts +4 -0
  104. package/node/components/TreeLikeAutocomplete/OptionsModal/styles.js +10 -3
  105. package/node/components/TreeLikeAutocomplete/OptionsModal/useLogic/useLogic.d.ts +5 -3
  106. package/node/components/TreeLikeAutocomplete/OptionsModal/useLogic/useLogic.js +2 -1
  107. package/package.json +1 -1
  108. package/components/TagsList/public.d.ts +0 -2
  109. package/components/TagsList/public.js +0 -2
  110. package/node/components/TagsList/public.d.ts +0 -2
@@ -1,6 +1,6 @@
1
1
  /// <reference types="react" />
2
2
  import { type AutocompleteRenderInputParams } from './types';
3
- export declare const Autocomplete: <TAutocompleteValueProps, TMultiple extends boolean, TDisableClearable extends boolean, TFreeSolo extends boolean>(props: Omit<import("..").WithoutEmotionSpecific<import("@mui/material/Autocomplete/Autocomplete").AutocompleteProps<TAutocompleteValueProps, TMultiple, TDisableClearable, TFreeSolo, "div">>, "size" | "clearText" | "closeText" | "noOptionsText" | "openText" | "renderInput"> & Pick<import("../TextField").TextFieldProps, "label" | "success" | "error" | "placeholder" | "required" | "inputRef" | "helperText" | "inputProps"> & {
3
+ export declare const Autocomplete: <TAutocompleteValueProps, TMultiple extends boolean, TDisableClearable extends boolean, TFreeSolo extends boolean>(props: Omit<import("..").WithoutEmotionSpecific<import("@mui/material").AutocompleteProps<TAutocompleteValueProps, TMultiple, TDisableClearable, TFreeSolo, "div">>, "size" | "clearText" | "closeText" | "noOptionsText" | "openText" | "renderInput"> & Pick<import("../TextField").TextFieldProps, "label" | "success" | "error" | "placeholder" | "required" | "inputRef" | "helperText" | "inputProps"> & {
4
4
  renderInput?: ((props: Omit<import("..").WithoutEmotionSpecific<import("@mui/material").TextFieldProps>, "color" | "select" | "margin" | "variant" | "multiline" | "SelectProps"> & {
5
5
  success?: boolean | undefined;
6
6
  startAdornment?: import("react").ReactNode;
@@ -1,29 +1,66 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import ListItemIcon from '@mui/material/ListItemIcon';
3
3
  import MuiPopper from '@mui/material/Popper';
4
- import { useCallback } from 'react';
4
+ import { useCallback, useRef, } from 'react';
5
5
  import { CrossOutlineMd } from '../../icons/CrossOutlineMd';
6
6
  import { DownOutlineMd } from '../../icons/DownOutlineMd';
7
7
  import { Checkbox } from '../Checkbox';
8
8
  import { forwardRefWithGeneric } from '../forwardRefWithGeneric';
9
9
  import { Loader } from '../Loader';
10
10
  import { OverflowTypography } from '../OverflowTypography';
11
- import { Tag } from '../Tag';
11
+ import { TagsList } from '../TagsList';
12
12
  import { TextField } from '../TextField';
13
13
  import { classNames } from '../utils/classNames';
14
- import { DEFAULT_AUTOCOMPLETE_ELEMENT_ROWS_COUNT, DEFAULT_PLACEHOLDER, } from './constants';
14
+ import { DEFAULT_AUTOCOMPLETE_ELEMENT_ROWS_COUNT, DEFAULT_PLACEHOLDER, INPUT_MIN_WIDTH, } from './constants';
15
15
  import { StyledAutocomplete, StyledMenuItem } from './styles';
16
16
  import { useLogic } from './useLogic';
17
17
  const AutocompleteInner = (props, ref) => {
18
18
  const { isValueEmpty, isPopperVisible, autocompleteProps } = useLogic(props);
19
- const { required, success, error, label, getOptionLabel, helperText, inputRef, multiple, size = 'medium', placeholder = DEFAULT_PLACEHOLDER, overflowOption, closeText = 'Закрыть', openText = 'Открыть', clearText = 'Очистить', loadingText = _jsx(Loader, {}), isLoadedDataError, loadedDataError, noOptionsText, autoHighlight, renderInput: externalRenderInput, renderTags: externalRenderTags, renderOption: externalRenderOption, inputProps, hideHelperText = false, loading, ...restProps } = props;
19
+ const inputBaseRef = useRef(null);
20
+ const { required, success, error, label, getOptionLabel, helperText, inputRef, multiple, size = 'medium', placeholder = DEFAULT_PLACEHOLDER, overflowOption, closeText = 'Закрыть', openText = 'Открыть', clearText = 'Очистить', loadingText = _jsx(Loader, {}), isLoadedDataError, loadedDataError, noOptionsText, autoHighlight, renderInput: externalRenderInput, renderTags: externalRenderTags, renderOption: externalRenderOption, inputProps, hideHelperText = false, loading, value, onChange, ...restProps } = props;
20
21
  const renderInput = useCallback((inputParams) => {
22
+ const createStartAdornment = () => {
23
+ if (!multiple || !Array.isArray(value)) {
24
+ return null;
25
+ }
26
+ const tagsListElement = (_jsx(TagsList, { data: value, getOptionLabel: (option) => getOptionLabel?.(option) ??
27
+ option.toString(), inputContainerRef: inputBaseRef, reservedWidth: INPUT_MIN_WIDTH, onChange: (tagValues) => {
28
+ if (!onChange) {
29
+ return;
30
+ }
31
+ const syntheticEvent = {
32
+ type: 'change',
33
+ target: { value: tagValues },
34
+ currentTarget: { value: tagValues },
35
+ };
36
+ const reason = 'removeOption';
37
+ onChange(syntheticEvent, tagValues, reason);
38
+ } }));
39
+ return tagsListElement;
40
+ };
21
41
  const generalInputParams = {
22
42
  ...inputParams,
23
43
  inputProps: {
24
44
  ...inputParams.inputProps,
25
45
  className: classNames(inputParams?.inputProps?.className, inputProps?.className),
26
46
  },
47
+ InputProps: {
48
+ ...inputParams.InputProps,
49
+ ref: (element) => {
50
+ inputBaseRef.current = element;
51
+ const orig = inputParams.InputProps?.ref;
52
+ if (typeof orig === 'function') {
53
+ orig(element);
54
+ }
55
+ else if (orig) {
56
+ orig.current = element;
57
+ }
58
+ },
59
+ // Если есть externalRenderTags, используем renderTags(mui использует inputParams.InputProps?.startAdornment)
60
+ startAdornment: externalRenderTags
61
+ ? inputParams.InputProps?.startAdornment
62
+ : createStartAdornment(),
63
+ },
27
64
  inputRef,
28
65
  required,
29
66
  placeholder: isValueEmpty ? placeholder : '',
@@ -40,7 +77,9 @@ const AutocompleteInner = (props, ref) => {
40
77
  return _jsx(TextField, { ...generalInputParams });
41
78
  }, [
42
79
  isValueEmpty,
80
+ value,
43
81
  externalRenderInput,
82
+ externalRenderTags,
44
83
  inputRef,
45
84
  required,
46
85
  placeholder,
@@ -50,6 +89,9 @@ const AutocompleteInner = (props, ref) => {
50
89
  helperText,
51
90
  size,
52
91
  hideHelperText,
92
+ multiple,
93
+ getOptionLabel,
94
+ onChange,
53
95
  ]);
54
96
  const renderOption = useCallback((optionProps, option, optionState, ownerState) => {
55
97
  if (externalRenderOption) {
@@ -59,26 +101,22 @@ const AutocompleteInner = (props, ref) => {
59
101
  const { key, ...restOptionProps } = optionProps;
60
102
  return (_jsxs(StyledMenuItem, { selected: isSelected, ...restOptionProps, children: [multiple && (_jsx(ListItemIcon, { children: _jsx(Checkbox, { role: "menuitemcheckbox", checked: isSelected }) })), _jsx(OverflowTypography, { rowsCount: DEFAULT_AUTOCOMPLETE_ELEMENT_ROWS_COUNT, ...overflowOption, children: optionProps.key })] }, key));
61
103
  }, [multiple, overflowOption, externalRenderOption]);
62
- const renderTags = useCallback((tags, getTagProps, ownerSate) => {
63
- if (externalRenderTags) {
64
- return externalRenderTags(tags, getTagProps, ownerSate);
65
- }
66
- return tags.map((tag, index) => {
67
- const title = getOptionLabel?.(tag) || '';
68
- const { key, ...tagProps } = getTagProps({ index });
69
- return (_jsx(Tag, { variant: "light", color: "grey", label: title, ...tagProps }, key));
70
- });
71
- }, [getOptionLabel, externalRenderTags]);
72
104
  const renderPopper = useCallback((popperProps) => {
73
105
  if (!isPopperVisible) {
74
106
  return null;
75
107
  }
76
108
  return _jsx(MuiPopper, { ...popperProps });
77
109
  }, [isPopperVisible]);
110
+ const renderTags = useCallback((tags, getTagProps, ownerState) => {
111
+ if (externalRenderTags) {
112
+ return externalRenderTags(tags, getTagProps, ownerState);
113
+ }
114
+ return null;
115
+ }, [externalRenderTags]);
78
116
  return (_jsx(StyledAutocomplete, { ...restProps, ref: ref, size: size, clearText: clearText, closeText: !loading ? closeText : '', openText: !loading ? openText : '', loadingText: loadingText, multiple: multiple, disableCloseOnSelect: multiple, clearIcon: _jsx(CrossOutlineMd, {}), loading: loading, popupIcon: loading ? _jsx(Loader, { color: "primary", size: "small" }) : _jsx(DownOutlineMd, {}), forcePopupIcon: true, slotProps: {
79
117
  popper: {
80
118
  component: renderPopper,
81
119
  },
82
- }, getOptionLabel: getOptionLabel, renderInput: renderInput, renderTags: renderTags, renderOption: renderOption, ...autocompleteProps }));
120
+ }, getOptionLabel: getOptionLabel, renderInput: renderInput, renderTags: renderTags, renderOption: renderOption, value: value, onChange: onChange, ...autocompleteProps }));
83
121
  };
84
122
  export const Autocomplete = forwardRefWithGeneric(AutocompleteInner);
@@ -1,2 +1,8 @@
1
1
  export declare const DEFAULT_AUTOCOMPLETE_ELEMENT_ROWS_COUNT = 2;
2
+ export declare const INPUT_MIN_WIDTH = 110;
2
3
  export declare const DEFAULT_PLACEHOLDER = "\u041D\u0430\u0447\u043D\u0438\u0442\u0435 \u0432\u0432\u043E\u0434\u0438\u0442\u044C \u0434\u043B\u044F \u043F\u043E\u0438\u0441\u043A\u0430";
4
+ export declare const autocompleteClassnames: {
5
+ root: string;
6
+ multiple: string;
7
+ emptyValue: string;
8
+ };
@@ -1,2 +1,9 @@
1
+ import { createUIKitClassname } from '../utils/createUIKitClassname';
1
2
  export const DEFAULT_AUTOCOMPLETE_ELEMENT_ROWS_COUNT = 2;
3
+ export const INPUT_MIN_WIDTH = 110;
2
4
  export const DEFAULT_PLACEHOLDER = 'Начните вводить для поиска';
5
+ export const autocompleteClassnames = {
6
+ root: createUIKitClassname('autocomplete'),
7
+ multiple: createUIKitClassname('autocomplete_multiple'),
8
+ emptyValue: createUIKitClassname('autocomplete_empty-value'),
9
+ };
@@ -5,6 +5,7 @@ import { inputBaseClasses } from '@mui/material/InputBase';
5
5
  import { MenuItem } from '../MenuItem';
6
6
  import { svgIconClassnames } from '../SvgIcon';
7
7
  import { styled } from '../styled';
8
+ import { autocompleteClassnames, INPUT_MIN_WIDTH } from './constants';
8
9
  export const StyledMenuItem = styled(MenuItem) `
9
10
  max-height: ${({ theme }) => theme.spacing(13)};
10
11
 
@@ -23,5 +24,18 @@ export const StyledAutocomplete = styled(MuiAutocomplete) `
23
24
 
24
25
  & .${autocompleteClasses.endAdornment} .${iconButtonClasses.root}:hover {
25
26
  background-color: ${({ loading, theme }) => (loading ? 'transparent' : theme.palette.grey[100])};
26
- }
27
+ }
28
+
29
+ &:not(.${autocompleteClassnames.emptyValue}) .${inputBaseClasses.root} {
30
+ gap: ${({ theme }) => theme.spacing(3)};
31
+ }
32
+
33
+ & .${autocompleteClasses.inputRoot} .${inputBaseClasses.input} {
34
+ min-width: ${INPUT_MIN_WIDTH}px;
35
+ }
36
+
37
+ & .${autocompleteClasses.inputRoot} {
38
+ flex-wrap: nowrap;
39
+ }
40
+
27
41
  `;
@@ -2,10 +2,11 @@ import { type AutocompleteInputChangeReason } from '@mui/material/Autocomplete';
2
2
  import { type SyntheticEvent } from 'react';
3
3
  import { type AutocompleteProps } from '../types';
4
4
  type UseLogicParams<TAutocompleteValueProps, TMultiple extends boolean, TDisableClearable extends boolean, TFreeSolo extends boolean> = AutocompleteProps<TAutocompleteValueProps, TMultiple, TDisableClearable, TFreeSolo>;
5
- export declare const useLogic: <TAutocompleteValueProps, TMultiple extends boolean, TDisableClearable extends boolean, TFreeSolo extends boolean>({ value, isOptionEqualToValue: externalOptionEqualToValue, autoHighlight, freeSolo, options, loading, isLoadedDataError, loadedDataError, noOptionsText, onInputChange, }: UseLogicParams<TAutocompleteValueProps, TMultiple, TDisableClearable, TFreeSolo>) => {
5
+ export declare const useLogic: <TAutocompleteValueProps, TMultiple extends boolean, TDisableClearable extends boolean, TFreeSolo extends boolean>({ value, isOptionEqualToValue: externalOptionEqualToValue, autoHighlight, freeSolo, options, multiple, className, loading, isLoadedDataError, loadedDataError, noOptionsText, onInputChange, }: UseLogicParams<TAutocompleteValueProps, TMultiple, TDisableClearable, TFreeSolo>) => {
6
6
  isValueEmpty: boolean;
7
7
  isPopperVisible: boolean | undefined;
8
8
  autocompleteProps: {
9
+ className: string;
9
10
  isOptionEqualToValue: (option: TAutocompleteValueProps, currentValue: TAutocompleteValueProps) => boolean;
10
11
  autoHighlight: boolean;
11
12
  noOptionsText: string | number | bigint | boolean | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<import("react").ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<import("react").ReactNode> | null | undefined> | null;
@@ -1,6 +1,8 @@
1
- import { useCallback, useState } from 'react';
1
+ import { useCallback, useMemo, useState } from 'react';
2
+ import { classNames } from '../../utils/classNames';
3
+ import { autocompleteClassnames } from '../constants';
2
4
  import { checkIsInputEmpty } from './utils';
3
- export const useLogic = ({ value, isOptionEqualToValue: externalOptionEqualToValue, autoHighlight = true, freeSolo, options, loading, isLoadedDataError, loadedDataError = 'Ошибка загрузки данных', noOptionsText = 'Нет данных', onInputChange, }) => {
5
+ export const useLogic = ({ value, isOptionEqualToValue: externalOptionEqualToValue, autoHighlight = true, freeSolo, options, multiple, className, loading, isLoadedDataError, loadedDataError = 'Ошибка загрузки данных', noOptionsText = 'Нет данных', onInputChange, }) => {
4
6
  const [innerInputValue, setInnerInputValue] = useState('');
5
7
  const isValueEmpty = checkIsInputEmpty(value);
6
8
  const isInnerInputValueNotEmpty = innerInputValue.length >= 1;
@@ -21,10 +23,17 @@ export const useLogic = ({ value, isOptionEqualToValue: externalOptionEqualToVal
21
23
  setInnerInputValue(currentValue);
22
24
  }
23
25
  };
26
+ const classnames = useMemo(() => {
27
+ return classNames(className, autocompleteClassnames.root, {
28
+ [autocompleteClassnames.multiple]: multiple,
29
+ [autocompleteClassnames.emptyValue]: isValueEmpty,
30
+ });
31
+ }, [multiple, isValueEmpty]);
24
32
  return {
25
33
  isValueEmpty,
26
34
  isPopperVisible,
27
35
  autocompleteProps: {
36
+ className: classnames,
28
37
  isOptionEqualToValue,
29
38
  autoHighlight: freeSolo ? false : autoHighlight,
30
39
  noOptionsText: isLoadedDataError ? loadedDataError : noOptionsText,
@@ -20,7 +20,7 @@ export type TagProps = {
20
20
  /**
21
21
  * Хендлер клика по тегу
22
22
  */
23
- onClick: (value: MouseEvent<HTMLDivElement>) => void;
23
+ onClick?: (value: MouseEvent<HTMLDivElement>) => void;
24
24
  /**
25
25
  * Название класса, применяется к корневому компоненту
26
26
  */
@@ -0,0 +1 @@
1
+ export declare const MAX_TAG_WIDTH = "246px";
@@ -0,0 +1 @@
1
+ export const MAX_TAG_WIDTH = '246px';
@@ -1 +1,2 @@
1
1
  export * from './Tag';
2
+ export * from './constants';
@@ -1 +1,2 @@
1
1
  export * from './Tag';
2
+ export * from './constants';
@@ -3,6 +3,8 @@ import { Tag, tagClassnames } from '../../Tag';
3
3
  export const StyledTag = styled(Tag, {
4
4
  shouldForwardProp: (prop) => prop !== '$shrinks',
5
5
  }) `
6
+ overflow: hidden;
7
+
6
8
  /* 4em ширина необходима для отображения двух букв и избежания перекрытия кнопкой сброса всего текста тега */
7
9
  min-width: ${({ $shrinks }) => ($shrinks ? 'calc(2em + 20px)' : 'unset')};
8
10
  max-width: 246px;
@@ -8,10 +8,10 @@ import { getKey } from './utils';
8
8
  * Не предназначен для использования в продуктах, не экспортируется из пакета
9
9
  */
10
10
  export const TagsList = (props) => {
11
- const { maxItems, tagsContainerRef, visibleOptions, getTagProps } = useLogic(props);
12
- const { className, data, keyId, isDisabled, onClick } = props;
13
- return (_jsxs(Wrapper, { className: className, ref: tagsContainerRef, children: [visibleOptions?.map((option, index) => {
14
- const tagProps = getTagProps(option, index);
15
- return (_jsx(Tag, { isDisabled: isDisabled, onClick: onClick, ...tagProps }, getKey(option, keyId)));
16
- }), data && maxItems < data.length && (_jsx(CounterTag, { isDisabled: isDisabled, label: `+${data.length - maxItems}`, onClick: onClick }, "more"))] }));
11
+ const { hiddenCount, tagsContainerRef, visibleOptions, getTagProps, showCounter, } = useLogic(props);
12
+ const { className, keyId, isDisabled, onClick, getOptionLabel } = props;
13
+ return (_jsxs(Wrapper, { className: className, ref: tagsContainerRef, children: [visibleOptions?.map((option) => {
14
+ const tagProps = getTagProps(option);
15
+ return (_jsx(Tag, { isDisabled: isDisabled, onClick: onClick, ...tagProps }, getKey(option, keyId, getOptionLabel)));
16
+ }), showCounter && (_jsx(CounterTag, { isDisabled: isDisabled, label: `+${hiddenCount}`, onClick: onClick }, "more"))] }));
17
17
  };
@@ -0,0 +1,2 @@
1
+ export declare const MIN_AVAILABLE_WIDTH = 50;
2
+ export declare const GAP_BETWEEN_TAGS = 4;
@@ -0,0 +1,2 @@
1
+ export const MIN_AVAILABLE_WIDTH = 50;
2
+ export const GAP_BETWEEN_TAGS = 4; // 4px
@@ -1,11 +1,15 @@
1
1
  import { styled } from '../styled';
2
2
  import { tagClassnames } from '../Tag';
3
+ import { GAP_BETWEEN_TAGS } from './constants';
3
4
  import { Tag } from './Tag';
4
5
  export const Wrapper = styled.div `
6
+ overflow: hidden;
5
7
  display: flex;
6
- column-gap: ${({ theme }) => theme.spacing(1)};
8
+ column-gap: ${GAP_BETWEEN_TAGS}px;
7
9
  `;
8
10
  export const CounterTag = styled(Tag) `
11
+ overflow: unset;
12
+
9
13
  .${tagClassnames.label} {
10
14
  min-width: unset;
11
15
  }
@@ -1,4 +1,4 @@
1
- import { type MouseEvent } from 'react';
1
+ import { type MouseEvent, type RefObject } from 'react';
2
2
  export type TagValue = string | Record<string, unknown>;
3
3
  export type TagsListProps<TData extends TagValue = TagValue> = {
4
4
  /**
@@ -14,16 +14,25 @@ export type TagsListProps<TData extends TagValue = TagValue> = {
14
14
  */
15
15
  isDisabled?: boolean;
16
16
  /**
17
- * Поле, используемое в качестве ключа списка
17
+ * Поле, используемое в качестве ключа списка.
18
+ * Если не передано, для генерации ключа будет использоваться getOptionLabel
18
19
  */
19
- keyId: TData extends string ? never : keyof TData;
20
+ keyId?: TData extends string ? never : keyof TData;
20
21
  /**
21
22
  * Используется для определения строкового значения опции
22
23
  */
23
24
  getOptionLabel: (value: TData) => string | number;
24
25
  onChange: (value: TData[] | undefined) => void;
25
26
  /**
26
- * Функция, вызываемая при клике не тэг
27
+ * Функция, вызываемая при клике на тэг
27
28
  */
28
- onClick: (value: MouseEvent<HTMLDivElement>) => void;
29
+ onClick?: (value: MouseEvent<HTMLDivElement>) => void;
30
+ /**
31
+ * Ref на внешний контейнер, чья ширина используется как база для расчёта доступного пространства..
32
+ */
33
+ inputContainerRef?: RefObject<HTMLElement | null>;
34
+ /**
35
+ * Зарезервированная ширина, которая будет вычтена из доступной ширины при расчете количества отображаемых тегов
36
+ */
37
+ reservedWidth?: number;
29
38
  };
@@ -1,12 +1,12 @@
1
1
  /// <reference types="react" />
2
- import type { TagValue } from '../types';
3
- import { type TagsListProps } from '../types';
2
+ import type { TagsListProps, TagValue } from '../types';
4
3
  type UseLogicParams<TData extends TagValue> = TagsListProps<TData>;
5
- export declare const useLogic: <TData extends TagValue>({ data, keyId, getOptionLabel, onChange, }: UseLogicParams<TData>) => {
6
- maxItems: number;
4
+ export declare const useLogic: <TData extends TagValue>({ data, keyId, getOptionLabel, onChange, inputContainerRef, reservedWidth, }: UseLogicParams<TData>) => {
7
5
  visibleOptions: TData[];
6
+ hiddenCount: number;
7
+ showCounter: boolean;
8
8
  tagsContainerRef: import("react").RefObject<HTMLDivElement | null>;
9
- getTagProps: (option: TData, index: number) => {
9
+ getTagProps: (option: TData) => {
10
10
  label: string | number;
11
11
  shrinks: boolean;
12
12
  onDelete: () => void;
@@ -1,145 +1,105 @@
1
1
  import { useEffect, useLayoutEffect, useRef, useState } from 'react';
2
2
  import { useTheme } from '../../theme/hooks/useTheme';
3
- export const useLogic = ({ data = [], keyId, getOptionLabel, onChange, }) => {
4
- // Сколько тегов можно отобразить в инпуте
5
- const [maxItems, setMaxItems] = useState(1);
6
- const ignoreResizeRef = useRef(false);
3
+ import { MIN_AVAILABLE_WIDTH } from '../constants';
4
+ import { calculateVisibleTagsCount, getAvailableWidth } from '../utils';
5
+ export const useLogic = ({ data = [], keyId, getOptionLabel, onChange, inputContainerRef, reservedWidth = 0, }) => {
6
+ const [visibleOptions, setVisibleOptions] = useState([]);
7
+ const [hiddenCount, setHiddenCount] = useState(0);
7
8
  const tagsContainerRef = useRef(null);
9
+ const availableWidthRef = useRef(0);
8
10
  const theme = useTheme();
9
11
  /**
10
- * Определяет, сколько тегов можно отобразить в строке,
11
- * учитывая ширину контейнера и промежутки (gap) между тегами.
12
- *
13
- * @param container - DOM-элемент контейнера с тегами
14
- * @param gap - расстояние между тегами (например, 4px)
15
- * @param padding - отступ внутри тега
16
- * @returns количество тегов, которые помещаются без переполнения
12
+ * Обновляет доступную ширину для расчета количества отображаемых тегов
17
13
  */
18
- const getTagsCountToAdd = (container, gap, padding) => {
19
- const tags = Array.from(container.children);
20
- // Если нет ни одного тега, сразу возвращаем 0
21
- if (tags.length === 0) {
22
- return 0;
14
+ const updateAvailableWidth = () => {
15
+ const target = inputContainerRef?.current ?? tagsContainerRef.current;
16
+ if (target) {
17
+ availableWidthRef.current = getAvailableWidth(target, reservedWidth);
23
18
  }
24
- // Преобразуем значение из темы в число
25
- const gapValue = parseInt(gap, 10);
26
- const paddingValue = parseInt(padding, 10);
27
- let totalWidth = 0; // Накопленная ширина всех тегов + gap
28
- let lastFittingIndex = tags.length; // Индекс, до которого теги помещаются
29
- for (let i = 0; i < tags.length; i++) {
30
- const tag = tags[i];
31
- const contentEl = tag.firstElementChild;
32
- // Полная ширина элемента (включая margin/padding/border)
33
- const tagWidth = tag.clientWidth;
34
- // gap учитывается, начиная со второго тега
35
- const currentGap = i > 0 ? gapValue : 0;
36
- // Проверяем, переполнен ли текст внутри тега (например, обрезался)
37
- const isContentOverflowing = contentEl?.scrollWidth > contentEl?.clientWidth;
38
- // Если добавление текущего тега превышает ширину контейнера
39
- // или если сам контент внутри тега не помещается
40
- if (totalWidth + currentGap + tagWidth > container.clientWidth ||
41
- isContentOverflowing) {
42
- lastFittingIndex = i;
43
- break;
44
- }
45
- // Увеличиваем суммарную ширину
46
- totalWidth += currentGap + tagWidth + paddingValue;
47
- }
48
- // Если даже первый тег не влезает, всё равно показываем один тег
49
- if (lastFittingIndex === 0 && tags.length > 0) {
50
- return 1;
51
- }
52
- return lastFittingIndex;
53
19
  };
54
- /**
55
- * Пересчитывает максимальное количество тегов (maxItems),
56
- * которые помещаются в строку контейнера без переполнения.
57
- */
58
20
  const recomputeMaxItems = () => {
59
- const containerEl = tagsContainerRef.current;
60
- // Если контейнер или данные не заданы — прекращаем
61
- if (!containerEl || !data?.length) {
21
+ if (data.length === 0) {
22
+ setVisibleOptions([]);
23
+ setHiddenCount(0);
62
24
  return;
63
25
  }
64
- // Берём первый тег как шаблон (для клонирования)
65
- const tagTemplate = containerEl.children[0];
66
- if (!tagTemplate) {
26
+ const hasNoInputContainer = !inputContainerRef?.current;
27
+ const isZeroWidth = availableWidthRef.current === 0;
28
+ const isSmallWidth = availableWidthRef.current < MIN_AVAILABLE_WIDTH;
29
+ // Если нет inputContainerRef и доступная ширина слишком мала или не вычислена - показываем первый тег и счетчик(если есть)
30
+ if (hasNoInputContainer && (isZeroWidth || isSmallWidth)) {
31
+ setVisibleOptions(data.slice(0, 1));
32
+ setHiddenCount(data.length - 1);
67
33
  return;
68
34
  }
69
- // Создаём скрытый клон-контейнер, чтобы измерить ширину вне DOM-окружения
70
- const clone = document.createElement('div');
71
- // Задаем стили для клон-контейнера
72
- clone.style.cssText = `
73
- visibility: hidden;
74
- position: absolute;
75
- width: ${containerEl.clientWidth}px;
76
- white-space: nowrap;
77
- `;
78
- // На основе шаблона создаём копии тегов для всех элементов data
79
- data.forEach((option) => {
80
- const tagClone = tagTemplate.cloneNode(true);
81
- const contentEl = tagClone.firstElementChild;
82
- if (contentEl) {
83
- // Подставляем текст соответствующего option
84
- contentEl.textContent = getOptionLabel(option).toString();
85
- // Включаем горизонтальный скролл, чтобы можно было определить overflow
86
- contentEl.style.overflowX = 'auto';
87
- }
88
- clone.appendChild(tagClone);
89
- });
90
- // Добавляем клон-контейнер в DOM, чтобы получить размеры
91
- document.body.appendChild(clone);
92
- // Вычисляем, сколько тегов влезает с учётом gap и padding
93
- const visibleCount = getTagsCountToAdd(clone, theme.spacing(1), theme.spacing(2));
94
- // Сохраняем результат в стейт
95
- setMaxItems(visibleCount);
96
- // Удаляем клон из DOM
97
- document.body.removeChild(clone);
98
- };
99
- useLayoutEffect(() => {
100
- if (typeof window === 'undefined' || !data || !data.length) {
35
+ // Если нет inputContainerRef и данных всего 1, показываем его даже при малой ширине (fallback)
36
+ if (hasNoInputContainer && data.length === 1) {
37
+ setVisibleOptions(data.slice(0, 1));
38
+ setHiddenCount(0);
39
+ return;
40
+ }
41
+ if (isSmallWidth) {
42
+ setVisibleOptions([]);
43
+ setHiddenCount(data.length);
44
+ }
45
+ // Расчет количества отображаемых тегов
46
+ const labels = data.map((item) => getOptionLabel(item).toString());
47
+ const visibleCount = calculateVisibleTagsCount(labels, availableWidthRef.current, theme);
48
+ const finalCount = Math.min(visibleCount, data.length);
49
+ if (finalCount === 0 && hasNoInputContainer) {
50
+ setVisibleOptions(data.slice(0, 1));
51
+ setHiddenCount(data.length - 1);
101
52
  return;
102
53
  }
54
+ setVisibleOptions(data.slice(0, finalCount));
55
+ setHiddenCount(data.length - finalCount);
56
+ };
57
+ useEffect(() => {
58
+ updateAvailableWidth();
59
+ }, [reservedWidth]);
60
+ useLayoutEffect(() => {
103
61
  recomputeMaxItems();
104
- // Не пересчитываем ширину контейнера, так как сами ее модифицировали
105
- ignoreResizeRef.current = true;
106
- }, [data]);
62
+ }, []);
107
63
  useEffect(() => {
108
- if (typeof window === 'undefined' || !data || !data.length) {
64
+ if (!data.length) {
65
+ setVisibleOptions([]);
66
+ setHiddenCount(0);
109
67
  return;
110
68
  }
111
- if (!tagsContainerRef.current) {
112
- throw new Error('Tags container ref is not set');
113
- }
114
- // Пересчитываем maxItems при ресайзе контейнера
115
- const observer = new ResizeObserver(() => {
116
- if (ignoreResizeRef.current) {
117
- ignoreResizeRef.current = false;
118
- return;
119
- }
69
+ const resizeObserver = new ResizeObserver(() => {
70
+ updateAvailableWidth();
120
71
  recomputeMaxItems();
121
72
  });
122
- observer.observe(tagsContainerRef.current);
123
- return () => observer.disconnect();
73
+ const target = inputContainerRef?.current ?? tagsContainerRef.current;
74
+ if (!target) {
75
+ return;
76
+ }
77
+ resizeObserver.observe(target);
78
+ return () => {
79
+ resizeObserver.disconnect();
80
+ };
124
81
  }, [data]);
125
- const getTagProps = (option, index) => {
82
+ const getTagProps = (option) => {
126
83
  const label = getOptionLabel(option);
127
- const shrinks = index === maxItems - 1 && maxItems <= data.length;
128
84
  const onDelete = () => {
129
- const newValue = data?.filter((value) => {
85
+ const newValue = data.filter((value) => {
130
86
  if (typeof value === 'string') {
131
87
  return value !== option;
132
88
  }
133
- return value[keyId] !== option[keyId];
89
+ if (keyId) {
90
+ return value[keyId] !== option[keyId];
91
+ }
92
+ return getOptionLabel(value) !== getOptionLabel(option);
134
93
  });
135
- onChange(newValue?.length ? newValue : []);
94
+ onChange(newValue.length ? newValue : []);
136
95
  };
137
- return { label, shrinks, onDelete };
96
+ return { label, shrinks: false, onDelete };
138
97
  };
139
- const visibleOptions = data?.slice(0, maxItems);
98
+ const showCounter = hiddenCount > 0;
140
99
  return {
141
- maxItems,
142
100
  visibleOptions,
101
+ hiddenCount,
102
+ showCounter,
143
103
  tagsContainerRef,
144
104
  getTagProps,
145
105
  };
@@ -0,0 +1,6 @@
1
+ import type { Theme } from '../../../theme/types';
2
+ /**
3
+ * Вычисляет количество тегов, которые могут поместиться в доступную ширину
4
+ * с учетом пробелов между тегами и счетчика оставшихся элементов
5
+ */
6
+ export declare const calculateVisibleTagsCount: (tagLabels: string[], availableWidth: number, theme: Theme) => number;
@@ -0,0 +1,35 @@
1
+ import { GAP_BETWEEN_TAGS, MIN_AVAILABLE_WIDTH } from '../../constants';
2
+ import { getTagWidth } from '../getTagWidth';
3
+ /**
4
+ * Вычисляет количество тегов, которые могут поместиться в доступную ширину
5
+ * с учетом пробелов между тегами и счетчика оставшихся элементов
6
+ */
7
+ export const calculateVisibleTagsCount = (tagLabels, availableWidth, theme) => {
8
+ if (!tagLabels.length || availableWidth <= 0) {
9
+ return 0;
10
+ }
11
+ const tagWidths = tagLabels.map((label) => getTagWidth(label, theme));
12
+ const canFitTagAtIndex = (tagIndex, accumulatedWidth) => {
13
+ const isFirstTag = tagIndex === 0;
14
+ const gapWidth = isFirstTag ? 0 : GAP_BETWEEN_TAGS;
15
+ const tagsAfterCurrent = tagLabels.length - (tagIndex + 1);
16
+ const counterTagWidth = tagsAfterCurrent > 0
17
+ ? getTagWidth(`+${tagsAfterCurrent}`, theme, true) + GAP_BETWEEN_TAGS
18
+ : 0;
19
+ const totalWidthWithTag = accumulatedWidth + gapWidth + tagWidths[tagIndex];
20
+ return totalWidthWithTag + counterTagWidth <= availableWidth;
21
+ };
22
+ const fittingTagIndex = tagWidths.findIndex((_, index) => {
23
+ const widthBeforeCurrentTag = tagWidths
24
+ .slice(0, index)
25
+ .reduce((sum, width, i) => sum + width + (i > 0 ? GAP_BETWEEN_TAGS : 0), 0);
26
+ return !canFitTagAtIndex(index, widthBeforeCurrentTag);
27
+ });
28
+ const visibleTagsCount = fittingTagIndex === -1 ? tagLabels.length : fittingTagIndex;
29
+ if (visibleTagsCount === 0) {
30
+ const counterWidthAlone = getTagWidth(`+${tagLabels.length}`, theme);
31
+ const remainingAfterCounter = availableWidth - counterWidthAlone;
32
+ return remainingAfterCounter < MIN_AVAILABLE_WIDTH ? 0 : 1;
33
+ }
34
+ return visibleTagsCount;
35
+ };
@@ -0,0 +1 @@
1
+ export { calculateVisibleTagsCount } from './calculateVisibleTagsCount';
@@ -0,0 +1 @@
1
+ export { calculateVisibleTagsCount } from './calculateVisibleTagsCount';