@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.
- package/components/Autocomplete/Autocomplete.d.ts +1 -1
- package/components/Autocomplete/Autocomplete.js +53 -15
- package/components/Autocomplete/constants.d.ts +6 -0
- package/components/Autocomplete/constants.js +7 -0
- package/components/Autocomplete/styles.js +15 -1
- package/components/Autocomplete/useLogic/useLogic.d.ts +2 -1
- package/components/Autocomplete/useLogic/useLogic.js +11 -2
- package/components/TagsList/Tag/Tag.d.ts +1 -1
- package/components/TagsList/Tag/constants.d.ts +1 -0
- package/components/TagsList/Tag/constants.js +1 -0
- package/components/TagsList/Tag/index.d.ts +1 -0
- package/components/TagsList/Tag/index.js +1 -0
- package/components/TagsList/Tag/styles.js +2 -0
- package/components/TagsList/TagsList.js +6 -6
- package/components/TagsList/constants.d.ts +2 -0
- package/components/TagsList/constants.js +2 -0
- package/components/TagsList/styles.js +5 -1
- package/components/TagsList/types.d.ts +14 -5
- package/components/TagsList/useLogic/useLogic.d.ts +5 -5
- package/components/TagsList/useLogic/useLogic.js +70 -110
- package/components/TagsList/utils/calculateVisibleTagsCount/calculateVisibleTagsCount.d.ts +6 -0
- package/components/TagsList/utils/calculateVisibleTagsCount/calculateVisibleTagsCount.js +35 -0
- package/components/TagsList/utils/calculateVisibleTagsCount/index.d.ts +1 -0
- package/components/TagsList/utils/calculateVisibleTagsCount/index.js +1 -0
- package/components/TagsList/utils/getAvailableWidth/getAvailableWidth.d.ts +1 -0
- package/components/TagsList/utils/getAvailableWidth/getAvailableWidth.js +7 -0
- package/components/TagsList/utils/getAvailableWidth/index.d.ts +1 -0
- package/components/TagsList/utils/getAvailableWidth/index.js +1 -0
- package/components/TagsList/utils/getKey/getKey.d.ts +1 -1
- package/components/TagsList/utils/getKey/getKey.js +6 -3
- package/components/TagsList/utils/getTagWidth/getTagWidth.d.ts +2 -0
- package/components/TagsList/utils/getTagWidth/getTagWidth.js +21 -0
- package/components/TagsList/utils/getTagWidth/index.d.ts +1 -0
- package/components/TagsList/utils/getTagWidth/index.js +1 -0
- package/components/TagsList/utils/index.d.ts +3 -0
- package/components/TagsList/utils/index.js +3 -0
- package/components/TextField/TextField.js +6 -5
- package/components/TextField/constants.d.ts +1 -0
- package/components/TextField/constants.js +1 -0
- package/components/TextField/styles.js +4 -0
- package/components/TreeAsyncAutocomplete/OptionsModal/styles.js +1 -1
- package/components/TreeAutocomplete/OptionsModal/OptionsModal.js +1 -1
- package/components/TreeAutocomplete/OptionsModal/styles.d.ts +4 -0
- package/components/TreeAutocomplete/OptionsModal/styles.js +10 -3
- package/components/TreeAutocomplete/OptionsModal/useLogic/useLogic.d.ts +5 -3
- package/components/TreeAutocomplete/OptionsModal/useLogic/useLogic.js +2 -1
- package/components/TreeLikeAsyncAutocomplete/Input/Input.js +1 -1
- package/components/TreeLikeAsyncAutocomplete/OptionsModal/styles.js +1 -1
- package/components/TreeLikeAutocomplete/OptionsModal/OptionsModal.js +1 -1
- package/components/TreeLikeAutocomplete/OptionsModal/styles.d.ts +4 -0
- package/components/TreeLikeAutocomplete/OptionsModal/styles.js +10 -3
- package/components/TreeLikeAutocomplete/OptionsModal/useLogic/useLogic.d.ts +5 -3
- package/components/TreeLikeAutocomplete/OptionsModal/useLogic/useLogic.js +2 -1
- package/node/components/Autocomplete/Autocomplete.d.ts +1 -1
- package/node/components/Autocomplete/Autocomplete.js +51 -13
- package/node/components/Autocomplete/constants.d.ts +6 -0
- package/node/components/Autocomplete/constants.js +8 -1
- package/node/components/Autocomplete/styles.js +15 -1
- package/node/components/Autocomplete/useLogic/useLogic.d.ts +2 -1
- package/node/components/Autocomplete/useLogic/useLogic.js +10 -1
- package/node/components/TagsList/Tag/Tag.d.ts +1 -1
- package/node/components/TagsList/Tag/constants.d.ts +1 -0
- package/node/components/TagsList/Tag/constants.js +4 -0
- package/node/components/TagsList/Tag/index.d.ts +1 -0
- package/node/components/TagsList/Tag/index.js +1 -0
- package/node/components/TagsList/Tag/styles.js +2 -0
- package/node/components/TagsList/TagsList.js +6 -6
- package/node/components/TagsList/constants.d.ts +2 -0
- package/node/components/TagsList/constants.js +5 -0
- package/node/components/TagsList/styles.js +5 -1
- package/node/components/TagsList/types.d.ts +14 -5
- package/node/components/TagsList/useLogic/useLogic.d.ts +5 -5
- package/node/components/TagsList/useLogic/useLogic.js +70 -110
- package/node/components/TagsList/utils/calculateVisibleTagsCount/calculateVisibleTagsCount.d.ts +6 -0
- package/node/components/TagsList/utils/calculateVisibleTagsCount/calculateVisibleTagsCount.js +39 -0
- package/node/components/TagsList/utils/calculateVisibleTagsCount/index.d.ts +1 -0
- package/node/components/TagsList/utils/calculateVisibleTagsCount/index.js +5 -0
- package/node/components/TagsList/utils/getAvailableWidth/getAvailableWidth.d.ts +1 -0
- package/node/components/TagsList/utils/getAvailableWidth/getAvailableWidth.js +11 -0
- package/node/components/TagsList/utils/getAvailableWidth/index.d.ts +1 -0
- package/node/components/TagsList/utils/getAvailableWidth/index.js +5 -0
- package/node/components/TagsList/utils/getKey/getKey.d.ts +1 -1
- package/node/components/TagsList/utils/getKey/getKey.js +6 -3
- package/node/components/TagsList/utils/getTagWidth/getTagWidth.d.ts +2 -0
- package/node/components/TagsList/utils/getTagWidth/getTagWidth.js +25 -0
- package/node/components/TagsList/utils/getTagWidth/index.d.ts +1 -0
- package/node/components/TagsList/{public.js → utils/getTagWidth/index.js} +1 -2
- package/node/components/TagsList/utils/index.d.ts +3 -0
- package/node/components/TagsList/utils/index.js +7 -1
- package/node/components/TextField/TextField.js +6 -5
- package/node/components/TextField/constants.d.ts +1 -0
- package/node/components/TextField/constants.js +1 -0
- package/node/components/TextField/styles.js +4 -0
- package/node/components/TreeAsyncAutocomplete/OptionsModal/styles.js +1 -1
- package/node/components/TreeAutocomplete/OptionsModal/OptionsModal.js +1 -1
- package/node/components/TreeAutocomplete/OptionsModal/styles.d.ts +4 -0
- package/node/components/TreeAutocomplete/OptionsModal/styles.js +10 -3
- package/node/components/TreeAutocomplete/OptionsModal/useLogic/useLogic.d.ts +5 -3
- package/node/components/TreeAutocomplete/OptionsModal/useLogic/useLogic.js +2 -1
- package/node/components/TreeLikeAsyncAutocomplete/Input/Input.js +1 -1
- package/node/components/TreeLikeAsyncAutocomplete/OptionsModal/styles.js +1 -1
- package/node/components/TreeLikeAutocomplete/OptionsModal/OptionsModal.js +1 -1
- package/node/components/TreeLikeAutocomplete/OptionsModal/styles.d.ts +4 -0
- package/node/components/TreeLikeAutocomplete/OptionsModal/styles.js +10 -3
- package/node/components/TreeLikeAutocomplete/OptionsModal/useLogic/useLogic.d.ts +5 -3
- package/node/components/TreeLikeAutocomplete/OptionsModal/useLogic/useLogic.js +2 -1
- package/package.json +1 -1
- package/components/TagsList/public.d.ts +0 -2
- package/components/TagsList/public.js +0 -2
- 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
|
|
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 {
|
|
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
|
|
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,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const MAX_TAG_WIDTH = "246px";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MAX_TAG_WIDTH = '246px';
|
|
@@ -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 {
|
|
12
|
-
const { className,
|
|
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)));
|
|
16
|
-
}),
|
|
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
|
};
|
|
@@ -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: ${
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
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
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
21
|
+
if (data.length === 0) {
|
|
22
|
+
setVisibleOptions([]);
|
|
23
|
+
setHiddenCount(0);
|
|
62
24
|
return;
|
|
63
25
|
}
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
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
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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 (
|
|
64
|
+
if (!data.length) {
|
|
65
|
+
setVisibleOptions([]);
|
|
66
|
+
setHiddenCount(0);
|
|
109
67
|
return;
|
|
110
68
|
}
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
|
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
|
|
85
|
+
const newValue = data.filter((value) => {
|
|
130
86
|
if (typeof value === 'string') {
|
|
131
87
|
return value !== option;
|
|
132
88
|
}
|
|
133
|
-
|
|
89
|
+
if (keyId) {
|
|
90
|
+
return value[keyId] !== option[keyId];
|
|
91
|
+
}
|
|
92
|
+
return getOptionLabel(value) !== getOptionLabel(option);
|
|
134
93
|
});
|
|
135
|
-
onChange(newValue
|
|
94
|
+
onChange(newValue.length ? newValue : []);
|
|
136
95
|
};
|
|
137
|
-
return { label, shrinks, onDelete };
|
|
96
|
+
return { label, shrinks: false, onDelete };
|
|
138
97
|
};
|
|
139
|
-
const
|
|
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';
|