@cloud-ru/uikit-product-mobile-chips 0.8.36
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 +1178 -0
- package/LICENSE +201 -0
- package/README.md +8 -0
- package/package.json +60 -0
- package/src/components/AdaptiveChips/AdaptiveChips.tsx +21 -0
- package/src/components/AdaptiveChips/index.ts +1 -0
- package/src/components/MobileChipChoice/components/ChipChoiceBase/ChipChoiceBase.tsx +148 -0
- package/src/components/MobileChipChoice/components/ChipChoiceBase/index.ts +1 -0
- package/src/components/MobileChipChoice/components/ChipChoiceBase/styles.module.scss +89 -0
- package/src/components/MobileChipChoice/components/MobileChipChoiceCustom.tsx +81 -0
- package/src/components/MobileChipChoice/components/MobileChipChoiceDate.tsx +135 -0
- package/src/components/MobileChipChoice/components/MobileChipChoiceDateRange.tsx +101 -0
- package/src/components/MobileChipChoice/components/MobileChipChoiceMultiple.tsx +218 -0
- package/src/components/MobileChipChoice/components/MobileChipChoiceSingle.tsx +176 -0
- package/src/components/MobileChipChoice/components/MobileChipChoiceTime.tsx +122 -0
- package/src/components/MobileChipChoice/components/index.ts +6 -0
- package/src/components/MobileChipChoice/components/styles.module.scss +36 -0
- package/src/components/MobileChipChoice/constants.ts +20 -0
- package/src/components/MobileChipChoice/hooks.tsx +127 -0
- package/src/components/MobileChipChoice/index.ts +38 -0
- package/src/components/MobileChipChoice/styles.module.scss +103 -0
- package/src/components/MobileChipChoice/types.ts +139 -0
- package/src/components/MobileChipChoice/utils/index.ts +3 -0
- package/src/components/MobileChipChoice/utils/options.tsx +61 -0
- package/src/components/MobileChipChoice/utils/typeGuards.ts +35 -0
- package/src/components/MobileChipChoice/utils/utils.ts +62 -0
- package/src/components/MobileChipChoiceRow/MobileChipChoiceRow.tsx +275 -0
- package/src/components/MobileChipChoiceRow/components/ForwardedChipChoice.tsx +10 -0
- package/src/components/MobileChipChoiceRow/components/constants.ts +12 -0
- package/src/components/MobileChipChoiceRow/components/index.ts +1 -0
- package/src/components/MobileChipChoiceRow/constants.ts +21 -0
- package/src/components/MobileChipChoiceRow/index.ts +2 -0
- package/src/components/MobileChipChoiceRow/styles.module.scss +32 -0
- package/src/components/MobileChipChoiceRow/types.ts +60 -0
- package/src/components/index.ts +3 -0
- package/src/constants.ts +50 -0
- package/src/helperComponents/ButtonClearValue/ButtonClearValue.tsx +40 -0
- package/src/helperComponents/ButtonClearValue/index.ts +1 -0
- package/src/helperComponents/ButtonClearValue/styles.module.scss +50 -0
- package/src/helperComponents/ItemContent/ItemContent.tsx +37 -0
- package/src/helperComponents/ItemContent/index.ts +1 -0
- package/src/helperComponents/ItemContent/styles.module.scss +80 -0
- package/src/helperComponents/index.ts +2 -0
- package/src/index.ts +1 -0
- package/src/styles.module.scss +113 -0
- package/src/types.ts +26 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { ReactNode, useCallback, useMemo, useRef } from 'react';
|
|
2
|
+
import { useUncontrolledProp } from 'uncontrollable';
|
|
3
|
+
|
|
4
|
+
import { useLocale } from '@cloud-ru/uikit-product-locale';
|
|
5
|
+
import { MobileDropdown } from '@cloud-ru/uikit-product-mobile-dropdown';
|
|
6
|
+
import { TimePicker, TimePickerProps } from '@snack-uikit/calendar';
|
|
7
|
+
import { useValueControl } from '@snack-uikit/utils';
|
|
8
|
+
|
|
9
|
+
import { CHIP_CHOICE_TEST_IDS, SIZE } from '../../../constants';
|
|
10
|
+
import { DEFAULT_LOCALE } from '../constants';
|
|
11
|
+
import { useHandleOnKeyDown } from '../hooks';
|
|
12
|
+
import { ChipChoiceCommonProps } from '../types';
|
|
13
|
+
import { ChipChoiceBase } from './ChipChoiceBase';
|
|
14
|
+
import styles from './styles.module.scss';
|
|
15
|
+
|
|
16
|
+
const getStringTimeValue = (
|
|
17
|
+
time: TimePickerProps['value'],
|
|
18
|
+
{ showSeconds, locale }: Pick<TimePickerProps, 'showSeconds'> & { locale: Intl.Locale },
|
|
19
|
+
) => {
|
|
20
|
+
if (!time) {
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const date = new Date();
|
|
25
|
+
date.setHours(time.hours ?? 0);
|
|
26
|
+
date.setMinutes(time.minutes ?? 0);
|
|
27
|
+
date.setSeconds(time.seconds ?? 0);
|
|
28
|
+
|
|
29
|
+
return date.toLocaleTimeString(locale, {
|
|
30
|
+
hour: 'numeric',
|
|
31
|
+
minute: 'numeric',
|
|
32
|
+
second: showSeconds ? 'numeric' : undefined,
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type TimeValue = TimePickerProps['value'];
|
|
37
|
+
|
|
38
|
+
export type MobileChipChoiceTimeProps = Omit<ChipChoiceCommonProps, 'widthStrategy'> &
|
|
39
|
+
Pick<TimePickerProps, 'value' | 'defaultValue' | 'showSeconds'> & {
|
|
40
|
+
/** Колбек смены значения */
|
|
41
|
+
onChange?(value: TimeValue): void;
|
|
42
|
+
/** Колбек формирующий строковое представление выбранного значения. Принимает выбранное значение */
|
|
43
|
+
valueRender?(value?: TimeValue): ReactNode;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function MobileChipChoiceTime({
|
|
47
|
+
size = SIZE.S,
|
|
48
|
+
value,
|
|
49
|
+
defaultValue,
|
|
50
|
+
onChange,
|
|
51
|
+
valueRender,
|
|
52
|
+
showSeconds = true,
|
|
53
|
+
onClearButtonClick,
|
|
54
|
+
open: openProp,
|
|
55
|
+
onOpenChange,
|
|
56
|
+
...rest
|
|
57
|
+
}: MobileChipChoiceTimeProps) {
|
|
58
|
+
const [selectedValue, setSelectedValue] = useValueControl<TimeValue>({ value, defaultValue, onChange });
|
|
59
|
+
|
|
60
|
+
const localRef = useRef<HTMLDivElement>(null);
|
|
61
|
+
|
|
62
|
+
const [open, setOpen] = useUncontrolledProp(openProp, false, onOpenChange);
|
|
63
|
+
const handleOnKeyDown = useHandleOnKeyDown({ setOpen });
|
|
64
|
+
|
|
65
|
+
const closeDroplist = useCallback(() => {
|
|
66
|
+
setOpen(false);
|
|
67
|
+
setTimeout(() => localRef.current?.focus(), 0);
|
|
68
|
+
}, [setOpen]);
|
|
69
|
+
|
|
70
|
+
const { t } = useLocale('Chips');
|
|
71
|
+
|
|
72
|
+
const valueToRender = useMemo(() => {
|
|
73
|
+
if (valueRender) {
|
|
74
|
+
return valueRender(selectedValue);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!selectedValue) return t('allLabel');
|
|
78
|
+
|
|
79
|
+
return getStringTimeValue(selectedValue, { showSeconds, locale: DEFAULT_LOCALE });
|
|
80
|
+
}, [selectedValue, showSeconds, t, valueRender]);
|
|
81
|
+
|
|
82
|
+
const handleChangeValue = useCallback(
|
|
83
|
+
(value: TimeValue) => {
|
|
84
|
+
setSelectedValue(value);
|
|
85
|
+
closeDroplist();
|
|
86
|
+
},
|
|
87
|
+
[closeDroplist, setSelectedValue],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const navigationStartRef = useRef<HTMLButtonElement>(null);
|
|
91
|
+
const focusNavigationStartItem = () => setTimeout(() => navigationStartRef.current?.focus(), 0);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<MobileDropdown
|
|
95
|
+
content={
|
|
96
|
+
<TimePicker
|
|
97
|
+
className={styles.timeWrapper}
|
|
98
|
+
value={selectedValue}
|
|
99
|
+
size='l'
|
|
100
|
+
fitToContainer={false}
|
|
101
|
+
onChangeValue={handleChangeValue}
|
|
102
|
+
navigationStartRef={navigationStartRef}
|
|
103
|
+
onFocusLeave={closeDroplist}
|
|
104
|
+
showSeconds={showSeconds}
|
|
105
|
+
/>
|
|
106
|
+
}
|
|
107
|
+
open={open}
|
|
108
|
+
onOpenChange={setOpen}
|
|
109
|
+
data-test-id={CHIP_CHOICE_TEST_IDS.droplist}
|
|
110
|
+
>
|
|
111
|
+
<ChipChoiceBase
|
|
112
|
+
{...rest}
|
|
113
|
+
ref={localRef}
|
|
114
|
+
onClearButtonClick={onClearButtonClick}
|
|
115
|
+
value={selectedValue}
|
|
116
|
+
valueToRender={valueToRender}
|
|
117
|
+
size={size}
|
|
118
|
+
onKeyDown={handleOnKeyDown(focusNavigationStartItem)}
|
|
119
|
+
/>
|
|
120
|
+
</MobileDropdown>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
@use '@sbercloud/figma-tokens-cloud-platform/build/scss/components/styles-tokens-fields';
|
|
2
|
+
|
|
3
|
+
.footer {
|
|
4
|
+
width: 100%;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.footerTopLine {
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: space-between;
|
|
11
|
+
gap: 4px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.counter {
|
|
15
|
+
color: styles-tokens-fields.$sys-neutral-text-light;
|
|
16
|
+
|
|
17
|
+
@include styles-tokens-fields.composite-var(styles-tokens-fields.$sans-body-m);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.bodyNoPadding {
|
|
21
|
+
/* stylelint-disable-next-line declaration-no-important */
|
|
22
|
+
padding: 0 !important;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.dateWrapper {
|
|
26
|
+
height: 384px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.dateTimeWrapper {
|
|
30
|
+
height: 458px;
|
|
31
|
+
padding-bottom: 8px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.timeWrapper {
|
|
35
|
+
min-width: 100%;
|
|
36
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BUTTON_SIZE, SIZE } from '../../constants';
|
|
2
|
+
|
|
3
|
+
export const BUTTON_CLEAR_VALUE_SIZE_MAP = {
|
|
4
|
+
[SIZE.Xs]: BUTTON_SIZE.Xxs,
|
|
5
|
+
[SIZE.S]: BUTTON_SIZE.Xs,
|
|
6
|
+
[SIZE.M]: BUTTON_SIZE.Xs,
|
|
7
|
+
[SIZE.L]: BUTTON_SIZE.Xs,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const CHIP_CHOICE_TYPE = {
|
|
11
|
+
Multiple: 'multiple',
|
|
12
|
+
Date: 'date',
|
|
13
|
+
DateTime: 'date-time',
|
|
14
|
+
DateRange: 'date-range',
|
|
15
|
+
Single: 'single',
|
|
16
|
+
Custom: 'custom',
|
|
17
|
+
Time: 'time',
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_LOCALE = new Intl.Locale('ru-RU');
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import FuzzySearch from 'fuzzy-search';
|
|
2
|
+
import { KeyboardEvent, KeyboardEventHandler, useCallback, useMemo } from 'react';
|
|
3
|
+
|
|
4
|
+
import { useLocale } from '@cloud-ru/uikit-product-locale';
|
|
5
|
+
import { ItemId, MobileDroplistProps } from '@cloud-ru/uikit-product-mobile-dropdown';
|
|
6
|
+
import { ButtonFilled, ButtonFunction } from '@snack-uikit/button';
|
|
7
|
+
|
|
8
|
+
import { CHIP_CHOICE_TEST_IDS } from '../../constants';
|
|
9
|
+
import styles from './styles.module.scss';
|
|
10
|
+
import { AccordionOption, BaseOption, ContentRenderProps, FilterOption, NestListOption } from './types';
|
|
11
|
+
|
|
12
|
+
type UseHandleOnKeyDownProps = {
|
|
13
|
+
setOpen(open: boolean): void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function useHandleOnKeyDown({ setOpen }: UseHandleOnKeyDownProps) {
|
|
17
|
+
return useCallback(
|
|
18
|
+
(onKeyDown?: KeyboardEventHandler<HTMLElement>) => (e: KeyboardEvent<HTMLDivElement>) => {
|
|
19
|
+
if (e.code === 'Space') {
|
|
20
|
+
e.stopPropagation();
|
|
21
|
+
} else {
|
|
22
|
+
onKeyDown?.(e);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (['ArrowDown'].includes(e.key)) {
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
setOpen(true);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (['ArrowUp'].includes(e.key)) {
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
setOpen(false);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (e.key === 'Tab') {
|
|
36
|
+
setOpen(false);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
[setOpen],
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const DEFAULT_MIN_SEARCH_INPUT_LENGTH = 2;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Нечеткий поиск среди айтемов по полям 'content.option', 'content.caption', 'content.description', 'label'
|
|
47
|
+
*/
|
|
48
|
+
export function useOptionSearch<T extends ContentRenderProps = ContentRenderProps>({
|
|
49
|
+
options,
|
|
50
|
+
flatMapOptions,
|
|
51
|
+
minSearchInputLength,
|
|
52
|
+
disableFuzzySearch,
|
|
53
|
+
}: {
|
|
54
|
+
options: FilterOption<T>[];
|
|
55
|
+
flatMapOptions: (BaseOption<T> | AccordionOption<T> | NestListOption<T>)[];
|
|
56
|
+
minSearchInputLength?: number;
|
|
57
|
+
disableFuzzySearch?: boolean;
|
|
58
|
+
}) {
|
|
59
|
+
return useCallback(
|
|
60
|
+
(search: string) => {
|
|
61
|
+
if (search.length < (minSearchInputLength ?? DEFAULT_MIN_SEARCH_INPUT_LENGTH)) return options;
|
|
62
|
+
|
|
63
|
+
if (disableFuzzySearch) {
|
|
64
|
+
return options.filter(option => {
|
|
65
|
+
const fieldsForSearch = [option.label];
|
|
66
|
+
|
|
67
|
+
if ('contentRenderProps' in option) {
|
|
68
|
+
fieldsForSearch.push(option?.contentRenderProps?.description);
|
|
69
|
+
fieldsForSearch.push(option?.contentRenderProps?.caption);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return fieldsForSearch
|
|
73
|
+
.filter((v): v is ItemId => Boolean(v))
|
|
74
|
+
.some(value => value.toString().includes(search));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return new FuzzySearch(
|
|
79
|
+
flatMapOptions,
|
|
80
|
+
['label', 'contentRenderProps.description', 'contentRenderProps.caption'],
|
|
81
|
+
{},
|
|
82
|
+
).search(search);
|
|
83
|
+
},
|
|
84
|
+
[disableFuzzySearch, flatMapOptions, minSearchInputLength, options],
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type UseAutoApplyProps = {
|
|
89
|
+
autoApply: boolean;
|
|
90
|
+
onApprove(): void;
|
|
91
|
+
onCancel(): void;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export function useAutoApplyFooter({
|
|
95
|
+
autoApply,
|
|
96
|
+
onApprove,
|
|
97
|
+
onCancel,
|
|
98
|
+
}: UseAutoApplyProps): MobileDroplistProps['footer'] {
|
|
99
|
+
const { t } = useLocale('Chips');
|
|
100
|
+
|
|
101
|
+
return useMemo(() => {
|
|
102
|
+
if (autoApply) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className={styles.autoApplyFooter} data-test-id={CHIP_CHOICE_TEST_IDS.footer}>
|
|
108
|
+
<ButtonFunction
|
|
109
|
+
fullWidth
|
|
110
|
+
size='l'
|
|
111
|
+
appearance='neutral'
|
|
112
|
+
label={t('cancel')}
|
|
113
|
+
onClick={onCancel}
|
|
114
|
+
data-test-id={CHIP_CHOICE_TEST_IDS.cancelButton}
|
|
115
|
+
/>
|
|
116
|
+
<ButtonFilled
|
|
117
|
+
size='l'
|
|
118
|
+
fullWidth
|
|
119
|
+
appearance='primary'
|
|
120
|
+
label={t('apply')}
|
|
121
|
+
onClick={onApprove}
|
|
122
|
+
data-test-id={CHIP_CHOICE_TEST_IDS.approveButton}
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}, [t, autoApply, onApprove, onCancel]);
|
|
127
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CustomContentRenderProps,
|
|
3
|
+
MobileChipChoiceCustom,
|
|
4
|
+
MobileChipChoiceCustomProps,
|
|
5
|
+
MobileChipChoiceDate,
|
|
6
|
+
MobileChipChoiceDateProps,
|
|
7
|
+
MobileChipChoiceDateRange,
|
|
8
|
+
MobileChipChoiceDateRangeProps,
|
|
9
|
+
MobileChipChoiceMultiple,
|
|
10
|
+
MobileChipChoiceSingle,
|
|
11
|
+
MobileChipChoiceTime,
|
|
12
|
+
MobileChipChoiceTimeProps,
|
|
13
|
+
} from './components';
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
FilterOption,
|
|
17
|
+
MobileChipChoiceMultipleProps,
|
|
18
|
+
MobileChipChoiceSingleProps,
|
|
19
|
+
ContentRenderProps,
|
|
20
|
+
} from './types';
|
|
21
|
+
export type {
|
|
22
|
+
MobileChipChoiceCustomProps,
|
|
23
|
+
MobileChipChoiceDateProps,
|
|
24
|
+
MobileChipChoiceDateRangeProps,
|
|
25
|
+
MobileChipChoiceTimeProps,
|
|
26
|
+
CustomContentRenderProps,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export namespace MobileChipChoice {
|
|
30
|
+
export const Custom = MobileChipChoiceCustom;
|
|
31
|
+
export const Single = MobileChipChoiceSingle;
|
|
32
|
+
export const Multiple = MobileChipChoiceMultiple;
|
|
33
|
+
export const Date = MobileChipChoiceDate;
|
|
34
|
+
export const DateRange = MobileChipChoiceDateRange;
|
|
35
|
+
export const Time = MobileChipChoiceTime;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { isAccordionOption, isBaseOption, isGroupOption, isGroupSelectOption, isNextListOption } from './utils';
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
@use '@sbercloud/figma-tokens-cloud-platform/build/scss/components/styles-tokens-chips-chipChoice';
|
|
2
|
+
@use '@sbercloud/figma-tokens-cloud-platform/build/scss/components/styles-tokens-element';
|
|
3
|
+
@use '../../styles.module';
|
|
4
|
+
|
|
5
|
+
$sizes: 'xs', 's', 'm', 'l';
|
|
6
|
+
|
|
7
|
+
$labelTypography: (
|
|
8
|
+
'xs': styles-tokens-element.$light-label-s,
|
|
9
|
+
's': styles-tokens-element.$light-label-m,
|
|
10
|
+
'm': styles-tokens-element.$sans-label-l,
|
|
11
|
+
'l': styles-tokens-element.$light-label-l,
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
$valueTypography: (
|
|
15
|
+
'xs': styles-tokens-element.$sans-label-s,
|
|
16
|
+
's': styles-tokens-element.$sans-label-m,
|
|
17
|
+
'm': styles-tokens-element.$sans-label-l,
|
|
18
|
+
'l': styles-tokens-element.$sans-label-l,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
.label,
|
|
22
|
+
.value {
|
|
23
|
+
display: inline-flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.choiceChip {
|
|
28
|
+
background-color: styles-tokens-element.$sys-neutral-background1-level;
|
|
29
|
+
border-color: styles-tokens-element.$sys-neutral-decor-default;
|
|
30
|
+
|
|
31
|
+
@include styles.chip-defaults;
|
|
32
|
+
@include styles.chip-anatomy-styles(styles-tokens-chips-chipChoice.$chip-choice, $sizes, $labelTypography);
|
|
33
|
+
|
|
34
|
+
.label,
|
|
35
|
+
.value {
|
|
36
|
+
color: styles-tokens-element.$sys-neutral-text-support;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.icon {
|
|
40
|
+
color: styles-tokens-element.$sys-neutral-text-light;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@each $size in $sizes {
|
|
44
|
+
&[data-size='#{$size}'] {
|
|
45
|
+
.value {
|
|
46
|
+
@include styles-tokens-element.composite-var($valueTypography, $size);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
&:hover,
|
|
52
|
+
&:active,
|
|
53
|
+
&:focus-visible {
|
|
54
|
+
background-color: styles-tokens-element.$sys-neutral-background2-level;
|
|
55
|
+
border-color: styles-tokens-element.$sys-neutral-decor-hovered;
|
|
56
|
+
|
|
57
|
+
.label {
|
|
58
|
+
color: styles-tokens-element.$sys-neutral-text-support;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.value {
|
|
62
|
+
color: styles-tokens-element.$sys-neutral-text-main;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
&:focus-visible {
|
|
67
|
+
@include styles.chip-outline;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
&[data-disabled] {
|
|
71
|
+
cursor: not-allowed;
|
|
72
|
+
background-color: styles-tokens-element.$sys-neutral-background;
|
|
73
|
+
border-color: styles-tokens-element.$sys-neutral-decor-disabled;
|
|
74
|
+
|
|
75
|
+
.label,
|
|
76
|
+
.value {
|
|
77
|
+
color: styles-tokens-element.$sys-neutral-text-light;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
&[data-loading] {
|
|
82
|
+
background-color: styles-tokens-element.$sys-neutral-background;
|
|
83
|
+
border-color: styles-tokens-element.$sys-neutral-decor-activated;
|
|
84
|
+
|
|
85
|
+
@include styles.chip-loading-state(styles-tokens-chips-chipChoice.$chip-choice, true, null, null, true);
|
|
86
|
+
|
|
87
|
+
.label,
|
|
88
|
+
.value {
|
|
89
|
+
color: styles-tokens-element.$sys-neutral-text-support;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.triggerClassName {
|
|
95
|
+
--offset: #{styles-tokens-element.$space-drop-list-drop-offset};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.autoApplyFooter {
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
justify-content: space-between;
|
|
102
|
+
gap: 8px;
|
|
103
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { MouseEventHandler, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BaseItemProps,
|
|
5
|
+
DroplistProps,
|
|
6
|
+
GroupItemProps,
|
|
7
|
+
GroupSelectItemProps,
|
|
8
|
+
ItemContentProps,
|
|
9
|
+
ItemId,
|
|
10
|
+
NextListItemProps,
|
|
11
|
+
SelectionMultipleState,
|
|
12
|
+
SelectionSingleState,
|
|
13
|
+
} from '@cloud-ru/uikit-product-mobile-dropdown';
|
|
14
|
+
import { TruncateStringProps } from '@snack-uikit/truncate-string';
|
|
15
|
+
import { WithSupportProps } from '@snack-uikit/utils';
|
|
16
|
+
|
|
17
|
+
import { BaseChipProps, Size } from '../../types';
|
|
18
|
+
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
export type AnyType = any;
|
|
21
|
+
|
|
22
|
+
export type ContentRenderProps = Omit<ItemContentProps, 'option' | 'disabled'>;
|
|
23
|
+
|
|
24
|
+
export type FilterOption<T extends ContentRenderProps = ContentRenderProps> =
|
|
25
|
+
// eslint-disable-next-line no-use-before-define
|
|
26
|
+
| BaseOption<T>
|
|
27
|
+
// eslint-disable-next-line no-use-before-define
|
|
28
|
+
| AccordionOption<T>
|
|
29
|
+
// eslint-disable-next-line no-use-before-define
|
|
30
|
+
| GroupOption<T>
|
|
31
|
+
// eslint-disable-next-line no-use-before-define
|
|
32
|
+
| GroupSelectOption<T>
|
|
33
|
+
// eslint-disable-next-line no-use-before-define
|
|
34
|
+
| NestListOption<T>;
|
|
35
|
+
|
|
36
|
+
export type BaseOption<T extends ContentRenderProps = ContentRenderProps> = Omit<BaseItemProps, 'content' | 'id'> & {
|
|
37
|
+
value: ItemId;
|
|
38
|
+
label: ItemId;
|
|
39
|
+
contentRenderProps?: T;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type AccordionOption<T extends ContentRenderProps = ContentRenderProps> = Omit<
|
|
43
|
+
BaseOption<T>,
|
|
44
|
+
'switch' | 'inactive' | 'value'
|
|
45
|
+
> & {
|
|
46
|
+
id?: ItemId;
|
|
47
|
+
type: 'collapse';
|
|
48
|
+
options: FilterOption<T>[];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type GroupOption<T extends ContentRenderProps = ContentRenderProps> = Omit<GroupItemProps, 'items'> & {
|
|
52
|
+
options: FilterOption<T>[];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type GroupSelectOption<T extends ContentRenderProps = ContentRenderProps> = Omit<
|
|
56
|
+
GroupSelectItemProps,
|
|
57
|
+
'items'
|
|
58
|
+
> & {
|
|
59
|
+
options: FilterOption<T>[];
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type NestListOption<T extends ContentRenderProps = ContentRenderProps> = Omit<
|
|
63
|
+
NextListItemProps,
|
|
64
|
+
'items' | 'content'
|
|
65
|
+
> & {
|
|
66
|
+
label: ItemId;
|
|
67
|
+
contentRenderProps?: T;
|
|
68
|
+
options: FilterOption<T>[];
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type ChipChoiceCommonProps = WithSupportProps<
|
|
72
|
+
Partial<BaseChipProps> & {
|
|
73
|
+
/** Размер */
|
|
74
|
+
size?: Size;
|
|
75
|
+
/** Колбек обработки клика */
|
|
76
|
+
onClick?: MouseEventHandler<HTMLButtonElement | HTMLDivElement>;
|
|
77
|
+
/** Колбек для клика по кнопке очистки */
|
|
78
|
+
onClearButtonClick?: MouseEventHandler<HTMLButtonElement>;
|
|
79
|
+
/** Управляет состоянием показан/не показан. */
|
|
80
|
+
open?: boolean;
|
|
81
|
+
/** Колбек отображения компонента. Срабатывает при изменении состояния open. */
|
|
82
|
+
onOpenChange?(isOpen: boolean): void;
|
|
83
|
+
/** Вариант обрезания значения */
|
|
84
|
+
truncateVariant?: TruncateStringProps['variant'];
|
|
85
|
+
}
|
|
86
|
+
>;
|
|
87
|
+
|
|
88
|
+
export type ChipChoiceSelectCommonProps<T extends ContentRenderProps = ContentRenderProps> = ChipChoiceCommonProps & {
|
|
89
|
+
options: FilterOption<T>[];
|
|
90
|
+
|
|
91
|
+
contentRender?(option: { label: ItemId; value?: ItemId; contentRenderProps?: T }): ReactNode;
|
|
92
|
+
filterFn?(option: { label: ItemId; value?: ItemId; contentRenderProps?: T }): boolean;
|
|
93
|
+
|
|
94
|
+
searchable?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Отключает Fuzzy Search. Иногда в дроплисте могут быть различные айдишники - нам важно искать их без Fuzzy Search
|
|
97
|
+
* @default false
|
|
98
|
+
*/
|
|
99
|
+
disableFuzzySearch?: boolean;
|
|
100
|
+
|
|
101
|
+
/** Флаг, отвечающий за применение выбранного значения по умолчанию */
|
|
102
|
+
autoApply?: boolean;
|
|
103
|
+
/** Колбек основной кнопки */
|
|
104
|
+
onApprove?(): void;
|
|
105
|
+
/** Колбек кнопки отмены */
|
|
106
|
+
onCancel?(): void;
|
|
107
|
+
} & Pick<
|
|
108
|
+
DroplistProps,
|
|
109
|
+
| 'selection'
|
|
110
|
+
| 'scrollRef'
|
|
111
|
+
| 'scrollContainerRef'
|
|
112
|
+
| 'noDataState'
|
|
113
|
+
| 'footer'
|
|
114
|
+
| 'footerActiveElementsRefs'
|
|
115
|
+
| 'dataError'
|
|
116
|
+
| 'errorDataState'
|
|
117
|
+
| 'dataFiltered'
|
|
118
|
+
| 'noResultsState'
|
|
119
|
+
| 'loading'
|
|
120
|
+
| 'virtualized'
|
|
121
|
+
>;
|
|
122
|
+
|
|
123
|
+
export type MobileChipChoiceSingleProps<T extends ContentRenderProps = ContentRenderProps> =
|
|
124
|
+
ChipChoiceSelectCommonProps<T> &
|
|
125
|
+
Omit<SelectionSingleState, 'mode'> & {
|
|
126
|
+
/** Массив опций */
|
|
127
|
+
options: FilterOption<T>[];
|
|
128
|
+
/** Колбек формирующий отображение выбранного значения. Принимает выбранное значение. По умолчанию для отображения используется FilterOption.label */
|
|
129
|
+
valueRender?(option?: BaseOption<T>): ReactNode;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export type MobileChipChoiceMultipleProps<T extends ContentRenderProps = ContentRenderProps> =
|
|
133
|
+
ChipChoiceSelectCommonProps<T> &
|
|
134
|
+
Omit<SelectionMultipleState, 'mode'> & {
|
|
135
|
+
/** Массив опций */
|
|
136
|
+
options: FilterOption<T>[];
|
|
137
|
+
/** Колбек формирующий отображение выбранного значения. Принимает выбранное значение. По умолчанию для отображения используется FilterOption.label */
|
|
138
|
+
valueRender?(option?: BaseOption<T>[]): ReactNode;
|
|
139
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { ItemId, MobileDroplistProps } from '@cloud-ru/uikit-product-mobile-dropdown';
|
|
4
|
+
|
|
5
|
+
import { ItemContent } from '../../../helperComponents';
|
|
6
|
+
import { BaseOption, ContentRenderProps, FilterOption } from '../types';
|
|
7
|
+
import { isAccordionOption, isGroupOption, isGroupSelectOption, isNextListOption } from './typeGuards';
|
|
8
|
+
|
|
9
|
+
export function transformOptionsToItems<T extends ContentRenderProps = ContentRenderProps>(
|
|
10
|
+
options: FilterOption<T>[],
|
|
11
|
+
contentRender?: (option: { label: ItemId; value?: ItemId; contentRenderProps?: T; disabled?: boolean }) => ReactNode,
|
|
12
|
+
): MobileDroplistProps['items'] {
|
|
13
|
+
return options.map(option => {
|
|
14
|
+
if (isAccordionOption<T>(option) || isNextListOption<T>(option)) {
|
|
15
|
+
const { label, options, id, contentRenderProps, disabled, ...rest } = option;
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
...rest,
|
|
19
|
+
disabled,
|
|
20
|
+
id,
|
|
21
|
+
content: contentRender ? (
|
|
22
|
+
contentRender({ label, contentRenderProps, disabled })
|
|
23
|
+
) : (
|
|
24
|
+
<ItemContent option={label} {...contentRenderProps} disabled={disabled} />
|
|
25
|
+
),
|
|
26
|
+
items: transformOptionsToItems<T>(options),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (isGroupSelectOption<T>(option)) {
|
|
31
|
+
const { options, ...rest } = option;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
...rest,
|
|
35
|
+
items: transformOptionsToItems<T>(options),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (isGroupOption(option)) {
|
|
40
|
+
const { options, ...rest } = option;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
...rest,
|
|
44
|
+
items: transformOptionsToItems(options),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { label, value, contentRenderProps, disabled, ...rest } = option as BaseOption<T>;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
...rest,
|
|
52
|
+
disabled,
|
|
53
|
+
id: value,
|
|
54
|
+
content: contentRender ? (
|
|
55
|
+
contentRender({ label, contentRenderProps, disabled, value })
|
|
56
|
+
) : (
|
|
57
|
+
<ItemContent option={label} {...contentRenderProps} disabled={disabled} />
|
|
58
|
+
),
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AccordionOption,
|
|
3
|
+
AnyType,
|
|
4
|
+
BaseOption,
|
|
5
|
+
ContentRenderProps,
|
|
6
|
+
GroupOption,
|
|
7
|
+
GroupSelectOption,
|
|
8
|
+
NestListOption,
|
|
9
|
+
} from '../types';
|
|
10
|
+
|
|
11
|
+
export function isBaseOption<T extends ContentRenderProps = ContentRenderProps>(
|
|
12
|
+
option: AnyType,
|
|
13
|
+
): option is BaseOption<T> {
|
|
14
|
+
return !('options' in option);
|
|
15
|
+
}
|
|
16
|
+
export function isAccordionOption<T extends ContentRenderProps = ContentRenderProps>(
|
|
17
|
+
option: AnyType,
|
|
18
|
+
): option is AccordionOption<T> {
|
|
19
|
+
return option && 'options' in option && option['type'] === 'collapse';
|
|
20
|
+
}
|
|
21
|
+
export function isNextListOption<T extends ContentRenderProps = ContentRenderProps>(
|
|
22
|
+
option: AnyType,
|
|
23
|
+
): option is NestListOption<T> {
|
|
24
|
+
return option && 'options' in option && option['type'] === 'next-list';
|
|
25
|
+
}
|
|
26
|
+
export function isGroupOption<T extends ContentRenderProps = ContentRenderProps>(
|
|
27
|
+
option: AnyType,
|
|
28
|
+
): option is GroupOption<T> {
|
|
29
|
+
return option && 'options' in option && option['type'] === 'group';
|
|
30
|
+
}
|
|
31
|
+
export function isGroupSelectOption<T extends ContentRenderProps = ContentRenderProps>(
|
|
32
|
+
option: AnyType,
|
|
33
|
+
): option is GroupSelectOption<T> {
|
|
34
|
+
return option && 'options' in option && option['type'] === 'group-select';
|
|
35
|
+
}
|