@bitrise/bitkit 13.317.0 → 13.319.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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/Components/DatePicker/DatePicker.tsx +59 -54
  3. package/src/Components/Drawer/Drawer.tsx +19 -5
  4. package/src/Components/Dropdown/DropdownOption.tsx +1 -1
  5. package/src/Components/Filter/Desktop/Filter.tsx +92 -0
  6. package/src/Components/Filter/Desktop/FilterAdd/FilterAdd.tsx +89 -0
  7. package/src/Components/Filter/{FilterDate → Desktop/FilterDate}/FilterDate.tsx +21 -10
  8. package/src/Components/Filter/{FilterForm → Desktop}/FilterForm.tsx +24 -115
  9. package/src/Components/Filter/{FilterItem → Desktop}/FilterItem.tsx +1 -1
  10. package/src/Components/Filter/{FilterSwitch → Desktop/FilterSwitch}/FilterSwitch.theme.ts +1 -1
  11. package/src/Components/Filter/{FilterSwitch → Desktop/FilterSwitch}/FilterSwitch.tsx +4 -4
  12. package/src/Components/Filter/{FilterSwitchAdapter → Desktop/FilterSwitch}/FilterSwitchAdapter.tsx +5 -5
  13. package/src/Components/Filter/Filter.storyData.ts +14 -1
  14. package/src/Components/Filter/Filter.tsx +16 -106
  15. package/src/Components/Filter/Filter.types.ts +34 -1
  16. package/src/Components/Filter/Filter.utils.ts +13 -0
  17. package/src/Components/Filter/Mobile/DateSelectOption.tsx +53 -0
  18. package/src/Components/Filter/Mobile/Filter.tsx +57 -0
  19. package/src/Components/Filter/Mobile/FilterAdd.tsx +97 -0
  20. package/src/Components/Filter/Mobile/FilterDrawer.tsx +96 -0
  21. package/src/Components/Filter/Mobile/FilterForm.tsx +236 -0
  22. package/src/Components/Filter/Mobile/FilterItem.tsx +95 -0
  23. package/src/Components/Filter/Mobile/MultiSelectOptions.tsx +69 -0
  24. package/src/Components/Filter/Mobile/SingleSelectOptions.tsx +136 -0
  25. package/src/Components/Filter/hooks/useFilterAdd.ts +68 -0
  26. package/src/Components/Filter/hooks/useFilterForm.ts +131 -0
  27. package/src/Components/Filter/hooks/useIsScrollable.ts +35 -0
  28. package/src/Components/Filter/hooks/useListBox.ts +66 -0
  29. package/src/Components/Form/Input/Input.theme.ts +27 -11
  30. package/src/Components/Form/Input/Input.tsx +4 -1
  31. package/src/Components/SearchInput/SearchInput.tsx +3 -2
  32. package/src/Components/components.theme.ts +1 -1
  33. package/src/index.ts +3 -4
  34. package/src/Components/Filter/FilterAdd/FilterAdd.tsx +0 -111
  35. /package/src/Components/Filter/{FilterSearch → Desktop}/FilterSearch.tsx +0 -0
  36. /package/src/Components/Filter/{FilterSwitch → Desktop/FilterSwitch}/FilterSwitchGroup.tsx +0 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bitrise/bitkit",
3
3
  "description": "Bitrise React component library",
4
- "version": "13.317.0",
4
+ "version": "13.319.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+ssh://git@github.com/bitrise-io/bitkit.git"
@@ -19,13 +19,13 @@ import { DatePickerDayViewProps } from './DatePickerDay';
19
19
  export { useDateRange, DateRange };
20
20
 
21
21
  export type DatePickerProps = {
22
- children: React.ReactNode;
22
+ children?: React.ReactNode;
23
+ onClose?: () => void;
23
24
  dayTooltip?: DatePickerDayViewProps['tooltip'];
24
25
  selectable?: DateRange;
25
- onClose: () => void;
26
26
  onClear?: () => void;
27
27
  visible: boolean;
28
- variant?: 'default' | 'filter';
28
+ variant?: 'default' | 'filter' | 'inline';
29
29
  } & (
30
30
  | {
31
31
  selected?: DateRange;
@@ -88,7 +88,7 @@ const DatePicker = (props: DatePickerProps) => {
88
88
  }, [initialRange]);
89
89
 
90
90
  const handleClose = () => {
91
- onClose();
91
+ onClose?.();
92
92
  setDateTo(undefined);
93
93
  setDateFrom(undefined);
94
94
  };
@@ -158,7 +158,7 @@ const DatePicker = (props: DatePickerProps) => {
158
158
  setDateFrom(dateFromNew);
159
159
  setDateTo(dateToNew);
160
160
 
161
- if (variant === 'default') {
161
+ if (variant === 'default' || variant === 'inline') {
162
162
  if (!dateFromNew || previewNew) {
163
163
  return;
164
164
  }
@@ -200,58 +200,63 @@ const DatePicker = (props: DatePickerProps) => {
200
200
  [isMonthSelector],
201
201
  );
202
202
 
203
- return (
204
- <Popover isLazy isOpen={visible} lazyBehavior="unmount" modifiers={[]} onClose={onClose}>
205
- <PopoverAnchor>{children}</PopoverAnchor>
206
- <PopoverContent aria-label="select date range">
207
- <FocusLock returnFocus>
208
- <DatePickerContext value={ctx}>
209
- <Box padding="24">
210
- {isMonthSelector ? (
211
- <DatePickerMonthSelector
212
- onMonthSelected={onMonthSelected}
213
- viewDate={isMonthSelector === 'left' ? leftViewDate : rightViewDate}
214
- />
215
- ) : (
216
- <>
217
- <Box display="flex" gap="32" marginBottom="24">
218
- <DatePickerMonth
219
- controls={isSingleMonthView ? 'both' : 'left'}
220
- onMonthClick={onMonthClickLeft}
221
- onViewDateChange={updateLeftViewDate}
222
- viewDate={leftViewDate}
223
- />
203
+ const datePicker = (
204
+ <DatePickerContext value={ctx}>
205
+ <Box padding={variant === 'inline' ? '0' : '24'}>
206
+ {isMonthSelector ? (
207
+ <DatePickerMonthSelector
208
+ onMonthSelected={onMonthSelected}
209
+ viewDate={isMonthSelector === 'left' ? leftViewDate : rightViewDate}
210
+ />
211
+ ) : (
212
+ <>
213
+ <Box display="flex" gap="32" marginBottom="24">
214
+ <DatePickerMonth
215
+ controls={isSingleMonthView ? 'both' : 'left'}
216
+ onMonthClick={onMonthClickLeft}
217
+ onViewDateChange={updateLeftViewDate}
218
+ viewDate={leftViewDate}
219
+ />
224
220
 
225
- {!isSingleMonthView && (
226
- <DatePickerMonth
227
- controls="right"
228
- onMonthClick={onMonthClickRight}
229
- onViewDateChange={updateRightViewDate}
230
- viewDate={rightViewDate}
231
- />
232
- )}
233
- </Box>
234
- {variant === 'filter' && (
235
- <DatePickerFooter
236
- mode={mode || 'range'}
237
- onApply={() => handleApply(dateFrom, dateTo)}
238
- onClear={onClear}
239
- onClose={handleClose}
240
- selected={currentSelected}
241
- />
242
- )}
243
- {variant === 'default' && mode === 'day' && (
244
- <Box display="flex" justifyContent="space-around">
245
- <Button onClick={() => handleSelect(DateTime.now())} variant="tertiary">
246
- Today
247
- </Button>
248
- </Box>
249
- )}
250
- </>
221
+ {!isSingleMonthView && (
222
+ <DatePickerMonth
223
+ controls="right"
224
+ onMonthClick={onMonthClickRight}
225
+ onViewDateChange={updateRightViewDate}
226
+ viewDate={rightViewDate}
227
+ />
251
228
  )}
252
229
  </Box>
253
- </DatePickerContext>
254
- </FocusLock>
230
+ {variant === 'filter' && (
231
+ <DatePickerFooter
232
+ mode={mode || 'range'}
233
+ onApply={() => handleApply(dateFrom, dateTo)}
234
+ onClear={onClear}
235
+ onClose={handleClose}
236
+ selected={currentSelected}
237
+ />
238
+ )}
239
+ {variant === 'default' && mode === 'day' && (
240
+ <Box display="flex" justifyContent="space-around">
241
+ <Button onClick={() => handleSelect(DateTime.now())} variant="tertiary">
242
+ Today
243
+ </Button>
244
+ </Box>
245
+ )}
246
+ </>
247
+ )}
248
+ </Box>
249
+ </DatePickerContext>
250
+ );
251
+
252
+ return variant === 'inline' ? (
253
+ datePicker
254
+ ) : (
255
+ <Popover isLazy isOpen={visible} lazyBehavior="unmount" modifiers={[]} onClose={onClose}>
256
+ <PopoverAnchor>{children}</PopoverAnchor>
257
+ <PopoverContent aria-label="select date range">
258
+ <FocusLock returnFocus />
259
+ {datePicker}
255
260
  </PopoverContent>
256
261
  </Popover>
257
262
  );
@@ -19,6 +19,8 @@ import Icon from '../Icon/Icon';
19
19
 
20
20
  export interface DrawerProps
21
21
  extends Pick<ChakraDrawerProps, 'finalFocusRef' | 'initialFocusRef' | 'isOpen' | 'onClose' | 'onCloseComplete'> {
22
+ blockScrollOnMount?: boolean;
23
+ bodyRef?: React.RefObject<HTMLDivElement>;
22
24
  children: DrawerContentProps['children'];
23
25
  footer?: ReactNode;
24
26
  maxWidth?: BoxProps['maxWidth'];
@@ -26,6 +28,7 @@ export interface DrawerProps
26
28
  padding?: BoxProps['padding'];
27
29
  margin?: BoxProps['margin'];
28
30
  headerPadding?: BoxProps['padding'];
31
+ hideCloseButton?: boolean;
29
32
  bodyPadding?: BoxProps['padding'];
30
33
  overlayProps?: ModalOverlayProps;
31
34
  contentProps?: DrawerContentProps;
@@ -34,12 +37,21 @@ export interface DrawerProps
34
37
  footerProps?: ModalFooterProps;
35
38
  }
36
39
 
37
- const Drawer = ({ overlayProps, contentProps, headerProps, bodyProps, footerProps, ...props }: DrawerProps) => {
40
+ const Drawer = ({
41
+ bodyRef,
42
+ overlayProps,
43
+ contentProps,
44
+ headerProps,
45
+ bodyProps,
46
+ footerProps,
47
+ ...props
48
+ }: DrawerProps) => {
38
49
  const {
39
50
  bodyPadding,
40
51
  children,
41
52
  footer,
42
53
  headerPadding,
54
+ hideCloseButton,
43
55
  maxWidth = '20rem',
44
56
  padding,
45
57
  margin,
@@ -51,13 +63,15 @@ const Drawer = ({ overlayProps, contentProps, headerProps, bodyProps, footerProp
51
63
  <ChakraDrawer {...drawerProps}>
52
64
  <DrawerOverlay {...overlayProps} />
53
65
  <DrawerContent maxWidth={maxWidth} padding={padding} margin={margin} {...contentProps}>
54
- <DrawerCloseButton size="md">
55
- <Icon name="Cross" />
56
- </DrawerCloseButton>
66
+ {!hideCloseButton && (
67
+ <DrawerCloseButton size="md">
68
+ <Icon name="Cross" />
69
+ </DrawerCloseButton>
70
+ )}
57
71
  <DrawerHeader as="h3" padding={headerPadding} {...headerProps}>
58
72
  {title}
59
73
  </DrawerHeader>
60
- <DrawerBody padding={bodyPadding} {...bodyProps}>
74
+ <DrawerBody padding={bodyPadding} {...bodyProps} ref={bodyRef}>
61
75
  {children}
62
76
  </DrawerBody>
63
77
  {footer && <DrawerFooter {...footerProps}>{footer}</DrawerFooter>}
@@ -135,7 +135,7 @@ const DropdownDetailedOption = <T = string,>({
135
135
  <chakra.div opacity={isDisabled ? '0.5' : '1'}>{icon}</chakra.div>
136
136
  <chakra.div>
137
137
  {title}
138
- <Text color={isDisabled ? 'text/disabled' : 'input/text/helper'} size="2">
138
+ <Text color={isDisabled ? 'text/disabled' : 'input/text/helper'} size="2" whiteSpace="pre-line">
139
139
  {subtitle}
140
140
  </Text>
141
141
  </chakra.div>
@@ -0,0 +1,92 @@
1
+ import { Modal, ModalOverlay, useMultiStyleConfig } from 'chakra-ui-2--react';
2
+ import Box from '../../Box/Box';
3
+ import Icon from '../../Icon/Icon';
4
+ import Button from '../../Button/Button';
5
+ import Divider from '../../Divider/Divider';
6
+ import { FilterProps } from '../Filter.types';
7
+ import { useFilterContext } from '../Filter.context';
8
+ import FilterAdd from './FilterAdd/FilterAdd';
9
+ import FilterDate from './FilterDate/FilterDate';
10
+ import FilterItem from './FilterItem';
11
+ import FilterSearch from './FilterSearch';
12
+ import FilterSwitchAdapter from './FilterSwitch/FilterSwitchAdapter';
13
+
14
+ const Filter = (props: FilterProps) => {
15
+ const {
16
+ data,
17
+ defaultState,
18
+ filtersDependOn,
19
+ isLoading,
20
+ isMobile,
21
+ onChange,
22
+ searchTooltip,
23
+ showAdd = true,
24
+ showFilterIcon = true,
25
+ showClearFilters = true,
26
+ showSearch,
27
+ ...rest
28
+ } = props;
29
+
30
+ const filterStyle = useMultiStyleConfig('Filter');
31
+
32
+ const { filters, isPopoverOpen, onClearFilters, onFilterChange, showClearButton, state } = useFilterContext();
33
+
34
+ return (
35
+ <Box sx={filterStyle.container} {...rest}>
36
+ <Modal isOpen={isPopoverOpen} onClose={() => {}}>
37
+ {isPopoverOpen && <ModalOverlay />}
38
+ </Modal>
39
+ <Box sx={filterStyle.content}>
40
+ {showFilterIcon && <Icon name="Filter" sx={filterStyle.icon} />}
41
+
42
+ {Object.keys(filters.switch).map((category) => (
43
+ <FilterSwitchAdapter key={category} category={category} />
44
+ ))}
45
+
46
+ {Object.keys(filters.dateRange).map((category) => (
47
+ <FilterDate category={category} isMobile={Boolean(isMobile)} key={category} />
48
+ ))}
49
+
50
+ {Object.keys(filters.select).map((category) => (
51
+ <FilterItem key={category} category={category} />
52
+ ))}
53
+ {Object.keys(filters.tag).map((category) => {
54
+ if (!state[category]) {
55
+ return;
56
+ }
57
+ return <FilterItem key={category} category={category} />;
58
+ })}
59
+
60
+ {showAdd && <FilterAdd />}
61
+ </Box>
62
+ {(showClearButton || showSearch) && (
63
+ <Box sx={filterStyle.rightContent}>
64
+ {showClearButton && (
65
+ <Button
66
+ isDisabled={isLoading}
67
+ leftIconName="Cross"
68
+ minWidth="7.5rem"
69
+ onClick={onClearFilters}
70
+ size="sm"
71
+ variant="tertiary"
72
+ >
73
+ Clear filters
74
+ </Button>
75
+ )}
76
+ {showSearch && (
77
+ <>
78
+ <Divider flexShrink="0" orientation="vertical" size="1" variant="solid" />
79
+ <FilterSearch
80
+ onChange={onFilterChange}
81
+ value={state.search?.length ? state.search[0] : ''}
82
+ searchTooltip={searchTooltip}
83
+ />
84
+ </>
85
+ )}
86
+ </Box>
87
+ )}
88
+ </Box>
89
+ );
90
+ };
91
+
92
+ export default Filter;
@@ -0,0 +1,89 @@
1
+ import { Menu, MenuButton, MenuList, Portal } from 'chakra-ui-2--react';
2
+ import Button from '../../../Button/Button';
3
+ import MenuItem from '../../../Menu/MenuItem';
4
+ import Tooltip from '../../../Tooltip/Tooltip';
5
+ import { useFilterContext } from '../../Filter.context';
6
+ import FilterForm from '../FilterForm';
7
+ import { FilterValue } from '../../Filter.types';
8
+ import { getMissingDependencies } from '../../Filter.utils';
9
+ import useFilterAdd from '../../hooks/useFilterAdd';
10
+
11
+ export interface FilterAddProps {
12
+ onChange: (category: string, selected: FilterValue) => void;
13
+ }
14
+
15
+ const FilterAdd = () => {
16
+ const { data, isLoading } = useFilterContext();
17
+
18
+ const {
19
+ categoryList,
20
+ isDisabled,
21
+ isMenuOpen,
22
+ onCategorySelect,
23
+ onChange,
24
+ onClose,
25
+ onOpen,
26
+ selectedCategory,
27
+ stateKeys,
28
+ } = useFilterAdd({ isMobile: false });
29
+
30
+ return (
31
+ <Menu closeOnSelect={false} isOpen={isMenuOpen} onClose={onClose} onOpen={onOpen}>
32
+ <MenuButton
33
+ as={Button}
34
+ isDisabled={isDisabled}
35
+ isLoading={isLoading}
36
+ leftIconName="Plus"
37
+ position={isMenuOpen ? 'relative' : undefined}
38
+ size="sm"
39
+ variant="tertiary"
40
+ zIndex={isMenuOpen ? 'dialog' : undefined}
41
+ >
42
+ Add filter {isMenuOpen}
43
+ </MenuButton>
44
+ <Portal>
45
+ <MenuList
46
+ paddingY={selectedCategory ? 0 : '12'}
47
+ position={isMenuOpen ? 'relative' : undefined}
48
+ zIndex={isMenuOpen ? 'dialog' : undefined}
49
+ >
50
+ {selectedCategory ? (
51
+ <FilterForm
52
+ category={selectedCategory}
53
+ categoryName={data[selectedCategory].categoryName}
54
+ categoryNamePlural={data[selectedCategory].categoryNamePlural}
55
+ loadingText={data[selectedCategory].loadingText}
56
+ onCancel={onClose}
57
+ onChange={onChange}
58
+ />
59
+ ) : (
60
+ categoryList.map((category) => {
61
+ const { categoryName, dependsOn } = data[category];
62
+ const missingDependencies = getMissingDependencies(stateKeys, Object.keys(dependsOn || []));
63
+ const isCategoryDisabled = missingDependencies.length > 0 || !data[category]?.options?.length;
64
+ return (
65
+ <Tooltip
66
+ key={category}
67
+ isDisabled={!isCategoryDisabled}
68
+ label={dependsOn?.[missingDependencies[0]] || 'There is no options for this category.'}
69
+ placement="right"
70
+ >
71
+ <MenuItem
72
+ isDisabled={isCategoryDisabled}
73
+ onClick={() => onCategorySelect(category)}
74
+ pointerEvents="all"
75
+ rightIconName="ChevronRight"
76
+ >
77
+ {categoryName || category}
78
+ </MenuItem>
79
+ </Tooltip>
80
+ );
81
+ })
82
+ )}
83
+ </MenuList>
84
+ </Portal>
85
+ </Menu>
86
+ );
87
+ };
88
+
89
+ export default FilterAdd;
@@ -1,21 +1,21 @@
1
- // import { useEffect } from 'react';
2
1
  import { useEffect } from 'react';
3
2
  import { useDisclosure, useMultiStyleConfig } from 'chakra-ui-2--react';
4
3
  import { DateTime } from 'luxon';
5
- import Box from '../../Box/Box';
6
- import DatePicker, { DatePickerProps, DateRange, useDateRange } from '../../DatePicker/DatePicker';
7
- import Icon from '../../Icon/Icon';
8
- import Text from '../../Text/Text';
9
- import { useFilterContext } from '../Filter.context';
10
- import { FilterStyle } from '../Filter.theme';
11
- import { FilterCategoryProps } from '../Filter.types';
4
+ import DatePicker, { DatePickerProps, DateRange, useDateRange } from '../../../DatePicker/DatePicker';
5
+ import { useFilterContext } from '../../Filter.context';
6
+ import { FilterStyle } from '../../Filter.theme';
7
+ import { FilterCategoryProps } from '../../Filter.types';
8
+ import Box from '../../../Box/Box';
9
+ import Text from '../../../Text/Text';
10
+ import Icon from '../../../Icon/Icon';
12
11
 
13
12
  export type FilterDateProps = {
14
13
  category: string;
14
+ isMobile: boolean;
15
15
  };
16
16
 
17
17
  const FilterDate = (props: FilterDateProps) => {
18
- const { category } = props;
18
+ const { category, isMobile } = props;
19
19
  const filterStyle = useMultiStyleConfig('Filter') as FilterStyle;
20
20
 
21
21
  const { isLoading, onFilterChange, onFilterClear, setPopoverOpen, state, data } = useFilterContext();
@@ -58,7 +58,18 @@ const FilterDate = (props: FilterDateProps) => {
58
58
  ? `${selectedRange.from?.toFormat('LLL dd')} - ${selectedRange.to?.toFormat('LLL dd')}`
59
59
  : 'All dates';
60
60
 
61
- return (
61
+ return isMobile ? (
62
+ <DatePicker
63
+ selectable={dateRangeData.selectable || selectable}
64
+ onApply={onDateRangeApply}
65
+ onClear={value?.length ? onClearClick : undefined}
66
+ dayTooltip={dateRangeData.dayTooltip}
67
+ selected={selectedRange}
68
+ visible={isOpen}
69
+ variant="inline"
70
+ mode="range"
71
+ />
72
+ ) : (
62
73
  <DatePicker
63
74
  selectable={dateRangeData.selectable || selectable}
64
75
  onApply={onDateRangeApply}
@@ -1,4 +1,3 @@
1
- import { FormEvent, useEffect, useMemo, useState } from 'react';
2
1
  import { useMultiStyleConfig } from 'chakra-ui-2--react';
3
2
  import FocusLock from 'react-focus-lock';
4
3
  import Tooltip from '../../Tooltip/Tooltip';
@@ -18,12 +17,10 @@ import RadioGroup from '../../Form/Radio/RadioGroup';
18
17
  import SearchInput from '../../SearchInput/SearchInput';
19
18
  import Text from '../../Text/Text';
20
19
  import { FilterStyle } from '../Filter.theme';
21
- import { FilterOptions, FilterOptionsMap, FilterValue } from '../Filter.types';
22
- import { isEqual, useDebounce } from '../../../utils/utils';
20
+ import { FilterFormProps, FilterOptions, FilterValue } from '../Filter.types';
23
21
  import { getOptionLabel } from '../Filter.utils';
24
22
  import { useFilterContext } from '../Filter.context';
25
-
26
- const MAX_ITEMS = 100;
23
+ import useFilterForm, { MAX_ITEMS } from '../hooks/useFilterForm';
27
24
 
28
25
  /**
29
26
  * https://gist.github.com/donmccurdy/6d073ce2c6f3951312dfa45da14a420f
@@ -88,128 +85,40 @@ const MatchingResults = (props: MatchingResultsProps) => {
88
85
  );
89
86
  };
90
87
 
91
- export type FilterFormProps = {
92
- category: string;
93
- categoryName?: string;
94
- categoryNamePlural?: string;
95
- loadingText?: string;
96
- onChange: (category: string, selected: FilterValue, previousValue: FilterValue) => void;
97
- onCancel: () => void;
98
- };
99
-
100
88
  const FilterForm = (props: FilterFormProps) => {
101
- const { category, categoryName, categoryNamePlural, loadingText, onCancel, onChange } = props;
89
+ const { category, categoryName, categoryNamePlural, loadingText, onCancel } = props;
102
90
 
103
91
  const filterStyle = useMultiStyleConfig('Filter') as FilterStyle;
104
92
 
105
- const { data, state } = useFilterContext();
106
-
107
- const {
108
- isPatternEnabled,
109
- iconsMap,
110
- hasNotFilteredOption = true,
111
- preserveOptionOrder,
112
- isInitialLoading,
113
- isMultiple,
114
- onAsyncSearch,
115
- options,
116
- optionsMap,
117
- } = data[category];
118
- const value = state[category] || [];
119
-
120
- const [selected, setSelected] = useState<FilterValue>(value);
121
-
122
- const [mode, setMode] = useState<'manually' | 'pattern'>(
123
- isPatternEnabled && value?.length && value[0].includes('*') ? 'pattern' : 'manually',
124
- );
125
-
126
- const [searchValue, setSearchValue] = useState<string>('');
127
- const debouncedSearchValue = useDebounce<string>(searchValue, 1000);
128
-
129
- const [isLoading, setLoading] = useState(Boolean(isInitialLoading));
130
- const [foundOptions, setFoundOptions] = useState<FilterOptions>([]);
131
- const [foundOptionsMap, setFoundOptionsMap] = useState<FilterOptionsMap>();
132
-
133
- const isAsync = !!onAsyncSearch;
134
- const withSearch = (options && options.length > 5) || isAsync;
135
-
136
- const isSubmitDisabled =
137
- isEqual(selected, value) || (mode === 'pattern' && !!selected[0] && !selected[0].includes('*'));
138
- const filteredOptions = useMemo(() => {
139
- if (options?.length) {
140
- return options.filter((opt) => {
141
- const optLabel = (optionsMap && optionsMap[opt]) || opt;
142
- return optLabel.toLowerCase().includes(searchValue.toLowerCase());
143
- });
144
- }
145
- return [];
146
- }, [searchValue, options]);
147
-
148
- const onSearchChange = (q: string) => {
149
- setSearchValue(q);
150
- if (isAsync) {
151
- if (q.length > 0) {
152
- setLoading(true);
153
- } else {
154
- setFoundOptions([]);
155
- }
156
- }
157
- };
93
+ const { data } = useFilterContext();
158
94
 
159
- const onSubmit = (e: FormEvent<HTMLDivElement>) => {
160
- e.preventDefault();
161
- const newSelected = selected[0] === '' ? [] : selected;
162
- onChange(category, newSelected, value);
163
- };
95
+ const { isPatternEnabled, iconsMap, hasNotFilteredOption = true, isInitialLoading, isMultiple } = data[category];
164
96
 
165
- const onClearClick = () => {
166
- setSelected([]);
167
- onChange(category, [], value);
168
- };
97
+ const {
98
+ currentOptionMap,
99
+ getEmptyText,
100
+ isAsync,
101
+ isLoading,
102
+ isSubmitDisabled,
103
+ mode,
104
+ onClearClick,
105
+ onSearchChange,
106
+ onSubmit,
107
+ isEditMode,
108
+ items,
109
+ searchValue,
110
+ selected,
111
+ setMode,
112
+ setSelected,
113
+ value,
114
+ withSearch,
115
+ } = useFilterForm(props);
169
116
 
170
117
  const onCancelClick = () => {
171
118
  setSelected(value);
172
119
  onCancel();
173
120
  };
174
121
 
175
- const getEmptyText = () => {
176
- if (searchValue.length) {
177
- return 'No result. Refine your search term.';
178
- }
179
- return '';
180
- };
181
-
182
- const getAsyncList = async () => {
183
- if (onAsyncSearch) {
184
- const response = await onAsyncSearch(category, searchValue);
185
- setLoading(false);
186
- setFoundOptions(response.options);
187
- setFoundOptionsMap(response.optionsMap || optionsMap);
188
- }
189
- };
190
-
191
- useEffect(() => {
192
- setLoading(Boolean(isInitialLoading));
193
- }, [isInitialLoading]);
194
-
195
- useEffect(() => {
196
- if (debouncedSearchValue.length > 0) {
197
- getAsyncList();
198
- } else {
199
- setLoading(Boolean(isInitialLoading));
200
- }
201
- }, [debouncedSearchValue]);
202
-
203
- const isEditMode = value.length !== 0;
204
-
205
- const isAsyncSearch = isAsync && !!searchValue;
206
-
207
- const items = isAsyncSearch
208
- ? Array.from(new Set(preserveOptionOrder ? [...foundOptions] : [...value, ...foundOptions]))
209
- : Array.from(new Set(preserveOptionOrder ? [...filteredOptions] : [...value, ...filteredOptions]));
210
-
211
- const currentOptionMap = isAsyncSearch ? foundOptionsMap : optionsMap;
212
-
213
122
  return (
214
123
  <FocusLock>
215
124
  <Box as="form" data-testid={`${category}-form`} onSubmit={onSubmit} sx={filterStyle.form}>
@@ -10,9 +10,9 @@ import Text from '../../Text/Text';
10
10
  import Tooltip from '../../Tooltip/Tooltip';
11
11
  import { FilterStyle } from '../Filter.theme';
12
12
  import { FilterValue } from '../Filter.types';
13
- import FilterForm from '../FilterForm/FilterForm';
14
13
  import { useFilterContext } from '../Filter.context';
15
14
  import { getOptionLabel } from '../Filter.utils';
15
+ import FilterForm from './FilterForm';
16
16
 
17
17
  export type FilterItemProps = {
18
18
  category: string;
@@ -1,4 +1,4 @@
1
- import { rem } from '../../../utils/utils';
1
+ import { rem } from '../../../../utils/utils';
2
2
 
3
3
  const FilterSwitch = {
4
4
  baseStyle: ({ isChecked, iconColor }: { isChecked: boolean; iconColor?: string }) => {
@@ -9,10 +9,10 @@ import {
9
9
  } from 'chakra-ui-2--react';
10
10
  import { omitThemingProps } from 'chakra-ui-2--styled-system';
11
11
  import { createContext } from 'chakra-ui-2--react-utils';
12
- import Divider from '../../Divider/Divider';
13
- import Text from '../../Text/Text';
14
- import Box from '../../Box/Box';
15
- import Icon, { TypeIconName } from '../../Icon/Icon';
12
+ import Divider from '../../../Divider/Divider';
13
+ import Text from '../../../Text/Text';
14
+ import Box from '../../../Box/Box';
15
+ import Icon, { TypeIconName } from '../../../Icon/Icon';
16
16
 
17
17
  type RadioInputProps = ChakraRadioProps['inputProps'] & {
18
18
  'data-testid'?: string;