@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.
- package/package.json +1 -1
- package/src/Components/DatePicker/DatePicker.tsx +59 -54
- package/src/Components/Drawer/Drawer.tsx +19 -5
- package/src/Components/Dropdown/DropdownOption.tsx +1 -1
- package/src/Components/Filter/Desktop/Filter.tsx +92 -0
- package/src/Components/Filter/Desktop/FilterAdd/FilterAdd.tsx +89 -0
- package/src/Components/Filter/{FilterDate → Desktop/FilterDate}/FilterDate.tsx +21 -10
- package/src/Components/Filter/{FilterForm → Desktop}/FilterForm.tsx +24 -115
- package/src/Components/Filter/{FilterItem → Desktop}/FilterItem.tsx +1 -1
- package/src/Components/Filter/{FilterSwitch → Desktop/FilterSwitch}/FilterSwitch.theme.ts +1 -1
- package/src/Components/Filter/{FilterSwitch → Desktop/FilterSwitch}/FilterSwitch.tsx +4 -4
- package/src/Components/Filter/{FilterSwitchAdapter → Desktop/FilterSwitch}/FilterSwitchAdapter.tsx +5 -5
- package/src/Components/Filter/Filter.storyData.ts +14 -1
- package/src/Components/Filter/Filter.tsx +16 -106
- package/src/Components/Filter/Filter.types.ts +34 -1
- package/src/Components/Filter/Filter.utils.ts +13 -0
- package/src/Components/Filter/Mobile/DateSelectOption.tsx +53 -0
- package/src/Components/Filter/Mobile/Filter.tsx +57 -0
- package/src/Components/Filter/Mobile/FilterAdd.tsx +97 -0
- package/src/Components/Filter/Mobile/FilterDrawer.tsx +96 -0
- package/src/Components/Filter/Mobile/FilterForm.tsx +236 -0
- package/src/Components/Filter/Mobile/FilterItem.tsx +95 -0
- package/src/Components/Filter/Mobile/MultiSelectOptions.tsx +69 -0
- package/src/Components/Filter/Mobile/SingleSelectOptions.tsx +136 -0
- package/src/Components/Filter/hooks/useFilterAdd.ts +68 -0
- package/src/Components/Filter/hooks/useFilterForm.ts +131 -0
- package/src/Components/Filter/hooks/useIsScrollable.ts +35 -0
- package/src/Components/Filter/hooks/useListBox.ts +66 -0
- package/src/Components/Form/Input/Input.theme.ts +27 -11
- package/src/Components/Form/Input/Input.tsx +4 -1
- package/src/Components/SearchInput/SearchInput.tsx +3 -2
- package/src/Components/components.theme.ts +1 -1
- package/src/index.ts +3 -4
- package/src/Components/Filter/FilterAdd/FilterAdd.tsx +0 -111
- /package/src/Components/Filter/{FilterSearch → Desktop}/FilterSearch.tsx +0 -0
- /package/src/Components/Filter/{FilterSwitch → Desktop/FilterSwitch}/FilterSwitchGroup.tsx +0 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
import ProgressSpinner from '../../ProgressSpinner/ProgressSpinner';
|
|
3
|
+
import Divider from '../../Divider/Divider';
|
|
4
|
+
import SearchInput from '../../SearchInput/SearchInput';
|
|
5
|
+
import ControlButton from '../../ControlButton/ControlButton';
|
|
6
|
+
import Text from '../../Text/Text';
|
|
7
|
+
import type { DialogProps } from '../../Dialog/DialogProps';
|
|
8
|
+
import Box from '../../Box/Box';
|
|
9
|
+
import Button from '../../Button/Button';
|
|
10
|
+
import Drawer from '../../Drawer/Drawer';
|
|
11
|
+
import { useFilterContext } from '../Filter.context';
|
|
12
|
+
import useFilterForm from '../hooks/useFilterForm';
|
|
13
|
+
import { FilterValue } from '../Filter.types';
|
|
14
|
+
import ButtonGroup from '../../ButtonGroup/ButtonGroup';
|
|
15
|
+
import useIsScrollable from '../hooks/useIsScrollable';
|
|
16
|
+
import DateSelectOption from './DateSelectOption';
|
|
17
|
+
import MultiSelectOptions from './MultiSelectOptions';
|
|
18
|
+
import SingleSelectOptions from './SingleSelectOptions';
|
|
19
|
+
|
|
20
|
+
type FilterFormProps = Pick<DialogProps, 'isOpen' | 'onClose'> & { selectedCategory: string };
|
|
21
|
+
|
|
22
|
+
const FilterForm = ({ isOpen, onClose, selectedCategory }: FilterFormProps) => {
|
|
23
|
+
const { data, onFilterChange } = useFilterContext();
|
|
24
|
+
|
|
25
|
+
const bodyRef = useRef<HTMLDivElement>(null);
|
|
26
|
+
|
|
27
|
+
const categoryData = data[selectedCategory || ''];
|
|
28
|
+
|
|
29
|
+
const categoryName = categoryData?.categoryName;
|
|
30
|
+
const categoryNamePlural = categoryData?.categoryNamePlural;
|
|
31
|
+
const hasNotFilteredOption =
|
|
32
|
+
categoryData?.hasNotFilteredOption === undefined ? true : categoryData?.hasNotFilteredOption;
|
|
33
|
+
const isMultiple = categoryData?.isMultiple;
|
|
34
|
+
const loadingText = categoryData?.loadingText;
|
|
35
|
+
const type = categoryData?.type;
|
|
36
|
+
const name = categoryData?.categoryName;
|
|
37
|
+
|
|
38
|
+
const onChange = (newCategory: string, newValue: FilterValue) => {
|
|
39
|
+
if (isMultiple) {
|
|
40
|
+
onFilterChange(newCategory, newValue);
|
|
41
|
+
} else {
|
|
42
|
+
onFilterChange(newCategory, newValue);
|
|
43
|
+
onClose();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const {
|
|
48
|
+
currentOptionMap,
|
|
49
|
+
getEmptyText,
|
|
50
|
+
isAsync,
|
|
51
|
+
isLoading,
|
|
52
|
+
isSubmitDisabled,
|
|
53
|
+
onClearClick,
|
|
54
|
+
onSearchChange,
|
|
55
|
+
onSubmit,
|
|
56
|
+
items,
|
|
57
|
+
searchValue,
|
|
58
|
+
selected,
|
|
59
|
+
setSelected,
|
|
60
|
+
withSearch,
|
|
61
|
+
} = useFilterForm({
|
|
62
|
+
category: selectedCategory,
|
|
63
|
+
categoryName,
|
|
64
|
+
categoryNamePlural,
|
|
65
|
+
loadingText,
|
|
66
|
+
onChange,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const { isScrollable } = useIsScrollable({
|
|
70
|
+
items,
|
|
71
|
+
hasNotFilteredOption,
|
|
72
|
+
ref: bodyRef,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const onClear = () => {
|
|
76
|
+
onClearClick();
|
|
77
|
+
onClose();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const count = selected.length || 0;
|
|
81
|
+
const showCount = isMultiple && type !== 'dateRange';
|
|
82
|
+
|
|
83
|
+
const clearText = type === 'dateRange' ? 'Reset' : `Clear ${isMultiple ? 'all' : ''}`;
|
|
84
|
+
|
|
85
|
+
const showFooter = selected.length > 0;
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<Drawer
|
|
89
|
+
bodyPadding="0"
|
|
90
|
+
bodyProps={{ overflowY: 'auto' }}
|
|
91
|
+
bodyRef={bodyRef}
|
|
92
|
+
contentProps={{ zIndex: 'dialog', top: '0' }}
|
|
93
|
+
headerPadding={16}
|
|
94
|
+
hideCloseButton
|
|
95
|
+
isOpen={isOpen}
|
|
96
|
+
maxWidth="100%"
|
|
97
|
+
onClose={onClose}
|
|
98
|
+
padding={0}
|
|
99
|
+
title={
|
|
100
|
+
<Box display="flex" alignItems="center" gap="12">
|
|
101
|
+
<ControlButton
|
|
102
|
+
aria-label="Back"
|
|
103
|
+
color="icon/secondary"
|
|
104
|
+
iconName="ArrowLeft"
|
|
105
|
+
isTooltipDisabled
|
|
106
|
+
onClick={onClose}
|
|
107
|
+
size="sm"
|
|
108
|
+
/>
|
|
109
|
+
<Box display="flex" flexDirection="column">
|
|
110
|
+
<Text as="h6" color="text/tertiary" textStyle="heading/h6">
|
|
111
|
+
Filter
|
|
112
|
+
</Text>
|
|
113
|
+
<Text as="h3" color="text/primary" textStyle="heading/mobile/h3">
|
|
114
|
+
{name}
|
|
115
|
+
</Text>
|
|
116
|
+
</Box>
|
|
117
|
+
</Box>
|
|
118
|
+
}
|
|
119
|
+
footer={
|
|
120
|
+
showFooter && (hasNotFilteredOption || isMultiple) ? (
|
|
121
|
+
<Box width="100%">
|
|
122
|
+
<Divider />
|
|
123
|
+
<ButtonGroup
|
|
124
|
+
backgroundColor="background/primary"
|
|
125
|
+
display="flex"
|
|
126
|
+
gap="12"
|
|
127
|
+
justifyContent="space-between"
|
|
128
|
+
paddingBlock="12"
|
|
129
|
+
paddingInline="16"
|
|
130
|
+
width="100%"
|
|
131
|
+
>
|
|
132
|
+
{hasNotFilteredOption && (
|
|
133
|
+
<Button variant="secondary" onClick={onClear} flex="1" maxWidth="calc(50% - 0.5rem)" size="md">
|
|
134
|
+
{clearText}
|
|
135
|
+
</Button>
|
|
136
|
+
)}
|
|
137
|
+
{isMultiple && (
|
|
138
|
+
<Button
|
|
139
|
+
alignSelf="flex-end"
|
|
140
|
+
flex="1"
|
|
141
|
+
isDisabled={isSubmitDisabled}
|
|
142
|
+
onClick={() => {
|
|
143
|
+
onSubmit();
|
|
144
|
+
onClose();
|
|
145
|
+
}}
|
|
146
|
+
size="md"
|
|
147
|
+
type="submit"
|
|
148
|
+
>
|
|
149
|
+
Apply {showCount && count ? `(${count})` : undefined}
|
|
150
|
+
</Button>
|
|
151
|
+
)}
|
|
152
|
+
</ButtonGroup>
|
|
153
|
+
</Box>
|
|
154
|
+
) : null
|
|
155
|
+
}
|
|
156
|
+
>
|
|
157
|
+
<Box display="flex" flexDirection="column" gap="16">
|
|
158
|
+
<Box
|
|
159
|
+
backgroundColor="background/primary"
|
|
160
|
+
display="flex"
|
|
161
|
+
flexDirection="column"
|
|
162
|
+
gap="8"
|
|
163
|
+
marginBottom="8"
|
|
164
|
+
position="sticky"
|
|
165
|
+
top="0"
|
|
166
|
+
>
|
|
167
|
+
{(withSearch || isAsync) && (
|
|
168
|
+
<>
|
|
169
|
+
<Divider />
|
|
170
|
+
<SearchInput
|
|
171
|
+
marginInline="12"
|
|
172
|
+
onChange={onSearchChange}
|
|
173
|
+
placeholder="Search for options"
|
|
174
|
+
value={searchValue}
|
|
175
|
+
variant="mobile"
|
|
176
|
+
/>
|
|
177
|
+
</>
|
|
178
|
+
)}
|
|
179
|
+
<Divider />
|
|
180
|
+
</Box>
|
|
181
|
+
<Box display="flex" flexDirection="column" gap="8">
|
|
182
|
+
{isLoading && (
|
|
183
|
+
<Box display="flex" alignItems="center" padding="24">
|
|
184
|
+
<ProgressSpinner color="sys/primary/base" marginRight="12" size="16" />
|
|
185
|
+
<Text color="text/secondary">{loadingText || 'Loading...'}</Text>
|
|
186
|
+
</Box>
|
|
187
|
+
)}
|
|
188
|
+
{type === 'dateRange' && (
|
|
189
|
+
<DateSelectOption
|
|
190
|
+
key={selectedCategory}
|
|
191
|
+
onChange={setSelected}
|
|
192
|
+
onClear={onClear}
|
|
193
|
+
selectedItems={selected}
|
|
194
|
+
/>
|
|
195
|
+
)}
|
|
196
|
+
{type !== 'dateRange' && isMultiple && (
|
|
197
|
+
<MultiSelectOptions
|
|
198
|
+
currentOptionMap={currentOptionMap}
|
|
199
|
+
emptyText={getEmptyText()}
|
|
200
|
+
isLoading={isLoading}
|
|
201
|
+
items={items}
|
|
202
|
+
onChange={setSelected}
|
|
203
|
+
selectedItems={selected}
|
|
204
|
+
/>
|
|
205
|
+
)}
|
|
206
|
+
{type !== 'dateRange' && !isMultiple && (
|
|
207
|
+
<SingleSelectOptions
|
|
208
|
+
currentOptionMap={currentOptionMap}
|
|
209
|
+
emptyText={getEmptyText()}
|
|
210
|
+
hasNotFilteredOption={hasNotFilteredOption}
|
|
211
|
+
isLoading={isLoading}
|
|
212
|
+
items={items}
|
|
213
|
+
onChange={(option: string) => {
|
|
214
|
+
onChange(selectedCategory, [option]);
|
|
215
|
+
}}
|
|
216
|
+
selectedItems={selected}
|
|
217
|
+
/>
|
|
218
|
+
)}
|
|
219
|
+
{isScrollable && (
|
|
220
|
+
<Box
|
|
221
|
+
background="linear-gradient(0deg, white 0%, rgba(255, 255, 255, 0) 100%)"
|
|
222
|
+
bottom="0"
|
|
223
|
+
height="32"
|
|
224
|
+
left="0"
|
|
225
|
+
pointerEvents="none"
|
|
226
|
+
position="sticky"
|
|
227
|
+
right="0"
|
|
228
|
+
/>
|
|
229
|
+
)}
|
|
230
|
+
</Box>
|
|
231
|
+
</Box>
|
|
232
|
+
</Drawer>
|
|
233
|
+
);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export default FilterForm;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useId } from 'react';
|
|
2
|
+
import { FormControl } from 'chakra-ui-2--react';
|
|
3
|
+
import FormLabel from '../../Form/FormLabel';
|
|
4
|
+
import Button from '../../Button/Button';
|
|
5
|
+
import Box from '../../Box/Box';
|
|
6
|
+
import Icon from '../../Icon/Icon';
|
|
7
|
+
import IconButton from '../../IconButton/IconButton';
|
|
8
|
+
import { useFilterContext } from '../Filter.context';
|
|
9
|
+
import { getDateRangeLabel, getOptionLabel } from '../Filter.utils';
|
|
10
|
+
|
|
11
|
+
export type FilterItemProps = {
|
|
12
|
+
category: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const FilterItem = ({ category }: FilterItemProps) => {
|
|
16
|
+
const { data, onFilterClear, setSelectedCategory, state } = useFilterContext();
|
|
17
|
+
|
|
18
|
+
const value = state[category];
|
|
19
|
+
|
|
20
|
+
const { categoryName, categoryNamePlural, isMultiple, optionsMap, unfilteredLabel, type } = data[category];
|
|
21
|
+
|
|
22
|
+
const pluralCategoryString = (categoryNamePlural || `${category}s`).toLowerCase();
|
|
23
|
+
|
|
24
|
+
const getText = () => {
|
|
25
|
+
if (!value || value.length === 0) {
|
|
26
|
+
return unfilteredLabel || `All ${pluralCategoryString}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (value.length === 2 && type === 'dateRange') {
|
|
30
|
+
return getDateRangeLabel(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (value.length > 1) {
|
|
34
|
+
return `${value.length} ${pluralCategoryString}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return getOptionLabel(value[0], optionsMap);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const buttonId = useId();
|
|
41
|
+
return (
|
|
42
|
+
<FormControl flex="1" isRequired>
|
|
43
|
+
<FormLabel htmlFor={buttonId}>{isMultiple && type !== 'dateRange' ? categoryNamePlural : categoryName}</FormLabel>
|
|
44
|
+
{type === 'tag' && (
|
|
45
|
+
<Box display="flex" alignItems="center">
|
|
46
|
+
<Button
|
|
47
|
+
borderRight="0"
|
|
48
|
+
borderRightRadius="0"
|
|
49
|
+
fontWeight="normal"
|
|
50
|
+
id={buttonId}
|
|
51
|
+
justifyContent="flex-start"
|
|
52
|
+
onClick={() => setSelectedCategory(category)}
|
|
53
|
+
rightIconName={type !== 'tag' ? 'ChevronRight' : undefined}
|
|
54
|
+
size="lg"
|
|
55
|
+
variant="secondary"
|
|
56
|
+
width="100%"
|
|
57
|
+
>
|
|
58
|
+
{getText()}
|
|
59
|
+
</Button>
|
|
60
|
+
<IconButton
|
|
61
|
+
aria-label="Clear"
|
|
62
|
+
borderLeft="0"
|
|
63
|
+
borderLeftRadius="0"
|
|
64
|
+
iconName="Cross"
|
|
65
|
+
onClick={() => onFilterClear(category)}
|
|
66
|
+
variant="secondary"
|
|
67
|
+
/>
|
|
68
|
+
</Box>
|
|
69
|
+
)}
|
|
70
|
+
{type !== 'tag' && (
|
|
71
|
+
<Button
|
|
72
|
+
fontWeight="normal"
|
|
73
|
+
id={buttonId}
|
|
74
|
+
justifyContent="space-between"
|
|
75
|
+
onClick={() => setSelectedCategory(category)}
|
|
76
|
+
rightIconName="ChevronRight"
|
|
77
|
+
size="lg"
|
|
78
|
+
variant="secondary"
|
|
79
|
+
width="100%"
|
|
80
|
+
>
|
|
81
|
+
{type === 'dateRange' ? (
|
|
82
|
+
<Box display="flex" alignItems="center" gap="8">
|
|
83
|
+
<Icon color="icon/tertiary" name="Calendar" />
|
|
84
|
+
{getText()}
|
|
85
|
+
</Box>
|
|
86
|
+
) : (
|
|
87
|
+
getText()
|
|
88
|
+
)}
|
|
89
|
+
</Button>
|
|
90
|
+
)}
|
|
91
|
+
</FormControl>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export default FilterItem;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Checkbox, CheckboxGroup } from 'chakra-ui-2--react';
|
|
2
|
+
import Box from '../../Box/Box';
|
|
3
|
+
import Text from '../../Text/Text';
|
|
4
|
+
import { FilterOptionsMap } from '../Filter.types';
|
|
5
|
+
import { getOptionLabel } from '../Filter.utils';
|
|
6
|
+
import { MAX_ITEMS } from '../hooks/useFilterForm';
|
|
7
|
+
|
|
8
|
+
type MultiSelectOptionsProps = {
|
|
9
|
+
currentOptionMap?: FilterOptionsMap;
|
|
10
|
+
emptyText?: string;
|
|
11
|
+
isLoading: boolean;
|
|
12
|
+
items: string[];
|
|
13
|
+
onChange: (option: string[]) => void;
|
|
14
|
+
selectedItems: string[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const MultiSelectOptions = ({
|
|
18
|
+
currentOptionMap,
|
|
19
|
+
emptyText,
|
|
20
|
+
isLoading,
|
|
21
|
+
items,
|
|
22
|
+
onChange,
|
|
23
|
+
selectedItems,
|
|
24
|
+
}: MultiSelectOptionsProps) => {
|
|
25
|
+
if (isLoading) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<CheckboxGroup onChange={onChange} value={selectedItems}>
|
|
31
|
+
<Box>
|
|
32
|
+
{items.length ? (
|
|
33
|
+
items?.slice(0, MAX_ITEMS).map((option) => {
|
|
34
|
+
const id = `checkbox-${option}`;
|
|
35
|
+
const isSelected = selectedItems.includes(option);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Box
|
|
39
|
+
alignItems="center"
|
|
40
|
+
as="label"
|
|
41
|
+
backgroundColor={isSelected ? 'background/selected' : 'transparent'}
|
|
42
|
+
display="flex"
|
|
43
|
+
gap="16"
|
|
44
|
+
htmlFor={id}
|
|
45
|
+
paddingBlock="12"
|
|
46
|
+
paddingInlineStart="24"
|
|
47
|
+
paddingInlineEnd="24"
|
|
48
|
+
cursor="pointer"
|
|
49
|
+
role="option"
|
|
50
|
+
_focusWithin={{
|
|
51
|
+
backgroundColor: isSelected ? 'background/selected-hover' : 'background/hover',
|
|
52
|
+
}}
|
|
53
|
+
_hover={{ backgroundColor: isSelected ? 'background/selected-hover' : 'background/hover' }}
|
|
54
|
+
_active={{ backgroundColor: 'background/active' }}
|
|
55
|
+
>
|
|
56
|
+
<Checkbox id={id} key={option} value={option} position="static" />
|
|
57
|
+
{getOptionLabel(option, currentOptionMap)}
|
|
58
|
+
</Box>
|
|
59
|
+
);
|
|
60
|
+
})
|
|
61
|
+
) : (
|
|
62
|
+
<Text paddingInline={16}>{emptyText}</Text>
|
|
63
|
+
)}
|
|
64
|
+
</Box>
|
|
65
|
+
</CheckboxGroup>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default MultiSelectOptions;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import Icon from '../../Icon/Icon';
|
|
3
|
+
import Box from '../../Box/Box';
|
|
4
|
+
import Text from '../../Text/Text';
|
|
5
|
+
import { getOptionLabel } from '../Filter.utils';
|
|
6
|
+
import { FilterOptionsMap } from '../Filter.types';
|
|
7
|
+
import { MAX_ITEMS } from '../hooks/useFilterForm';
|
|
8
|
+
import useListBox from '../hooks/useListBox';
|
|
9
|
+
|
|
10
|
+
type SingleSelectOptionsProps = {
|
|
11
|
+
currentOptionMap?: FilterOptionsMap;
|
|
12
|
+
emptyText?: string;
|
|
13
|
+
hasNotFilteredOption: boolean;
|
|
14
|
+
isLoading: boolean;
|
|
15
|
+
items: string[];
|
|
16
|
+
onChange: (option: string) => void;
|
|
17
|
+
selectedItems: string[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const Option = ({
|
|
21
|
+
onChange,
|
|
22
|
+
index,
|
|
23
|
+
isActive,
|
|
24
|
+
isSelected,
|
|
25
|
+
option,
|
|
26
|
+
children,
|
|
27
|
+
}: Pick<SingleSelectOptionsProps, 'onChange'> & {
|
|
28
|
+
index: number;
|
|
29
|
+
isActive?: boolean;
|
|
30
|
+
isSelected: boolean;
|
|
31
|
+
option: string;
|
|
32
|
+
children: ReactNode;
|
|
33
|
+
}) => {
|
|
34
|
+
const getBackgroundColor = () => {
|
|
35
|
+
if (isActive && isSelected) return 'background/selected-hover';
|
|
36
|
+
if (isSelected) return 'background/selected';
|
|
37
|
+
if (isActive) return 'background/hover';
|
|
38
|
+
return undefined;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const getHoverBackgroundColor = () => {
|
|
42
|
+
if (isSelected) return 'background/selected-hover';
|
|
43
|
+
return 'background/hover';
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Box
|
|
48
|
+
display="flex"
|
|
49
|
+
alignItems="center"
|
|
50
|
+
backgroundColor={getBackgroundColor()}
|
|
51
|
+
gap="16"
|
|
52
|
+
id={`option-${index}`}
|
|
53
|
+
onClick={(e) => {
|
|
54
|
+
e.stopPropagation();
|
|
55
|
+
onChange(option);
|
|
56
|
+
}}
|
|
57
|
+
paddingBlock="12"
|
|
58
|
+
paddingInlineStart={isSelected ? '16' : '56'}
|
|
59
|
+
paddingInlineEnd="24"
|
|
60
|
+
cursor="pointer"
|
|
61
|
+
role="option"
|
|
62
|
+
tabIndex={-1}
|
|
63
|
+
sx={{
|
|
64
|
+
backgroundColor: getBackgroundColor(),
|
|
65
|
+
'&:hover': {
|
|
66
|
+
backgroundColor: getHoverBackgroundColor(),
|
|
67
|
+
},
|
|
68
|
+
'&:active': {
|
|
69
|
+
backgroundColor: 'background/active',
|
|
70
|
+
},
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
{children}
|
|
74
|
+
</Box>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const SingleSelectOptions = ({
|
|
79
|
+
currentOptionMap,
|
|
80
|
+
emptyText,
|
|
81
|
+
hasNotFilteredOption,
|
|
82
|
+
isLoading,
|
|
83
|
+
items,
|
|
84
|
+
onChange,
|
|
85
|
+
selectedItems,
|
|
86
|
+
}: SingleSelectOptionsProps) => {
|
|
87
|
+
const { activeIndex, handleKeyDown, handleFocus, handleBlur, handleMouseDown, handleMouseMove } = useListBox({
|
|
88
|
+
items: hasNotFilteredOption ? [''].concat(items) : items,
|
|
89
|
+
hasNotFilteredOption,
|
|
90
|
+
onChange,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (isLoading) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<Box
|
|
99
|
+
aria-label="Filter options"
|
|
100
|
+
onKeyDown={handleKeyDown}
|
|
101
|
+
onFocus={handleFocus}
|
|
102
|
+
onBlur={handleBlur}
|
|
103
|
+
onMouseDown={handleMouseDown}
|
|
104
|
+
onMouseMove={handleMouseMove}
|
|
105
|
+
outline="none"
|
|
106
|
+
role="listbox"
|
|
107
|
+
tabIndex={0}
|
|
108
|
+
_focusVisible={{
|
|
109
|
+
boxShadow: 'none',
|
|
110
|
+
outline: 'none',
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
{items.length ? (
|
|
114
|
+
items?.slice(0, MAX_ITEMS).map((option, index) => {
|
|
115
|
+
const isSelected = selectedItems.includes(option);
|
|
116
|
+
return (
|
|
117
|
+
<Option
|
|
118
|
+
index={index}
|
|
119
|
+
isActive={activeIndex === index}
|
|
120
|
+
isSelected={isSelected}
|
|
121
|
+
onChange={onChange}
|
|
122
|
+
option={option}
|
|
123
|
+
>
|
|
124
|
+
{isSelected && <Icon name="Check" color="icon/interactive" />}
|
|
125
|
+
{getOptionLabel(option, currentOptionMap)}
|
|
126
|
+
</Option>
|
|
127
|
+
);
|
|
128
|
+
})
|
|
129
|
+
) : (
|
|
130
|
+
<Text paddingInline={16}>{emptyText}</Text>
|
|
131
|
+
)}
|
|
132
|
+
</Box>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export default SingleSelectOptions;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useDisclosure } from 'chakra-ui-2--react';
|
|
3
|
+
import { useFilterContext } from '../Filter.context';
|
|
4
|
+
import { hasAllDependencies } from '../Filter.utils';
|
|
5
|
+
import { FilterValue } from '../Filter.types';
|
|
6
|
+
|
|
7
|
+
type UseFilterAddProps = {
|
|
8
|
+
isMobile?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const useFilterAdd = ({ isMobile }: UseFilterAddProps) => {
|
|
12
|
+
const { isOpen: isMenuOpen, onClose: closeMenu, onOpen: openMenu } = useDisclosure();
|
|
13
|
+
|
|
14
|
+
const { data, filtersDependOn, onFilterChange, setPopoverOpen, state } = useFilterContext();
|
|
15
|
+
|
|
16
|
+
const [selectedCategory, setSelectedCategory] = useState<string | undefined>();
|
|
17
|
+
|
|
18
|
+
const onCategorySelect = (category: string) => {
|
|
19
|
+
setSelectedCategory(category);
|
|
20
|
+
if (isMobile) {
|
|
21
|
+
closeMenu();
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const onOpen = () => {
|
|
26
|
+
openMenu();
|
|
27
|
+
|
|
28
|
+
if (!isMobile) {
|
|
29
|
+
setPopoverOpen(true);
|
|
30
|
+
setSelectedCategory(undefined);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const onClose = () => {
|
|
35
|
+
setPopoverOpen(false);
|
|
36
|
+
closeMenu();
|
|
37
|
+
setSelectedCategory(undefined);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const onChange = (category: string, value: FilterValue) => {
|
|
41
|
+
onClose();
|
|
42
|
+
onFilterChange(category, value);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const stateKeys = Object.keys(state);
|
|
46
|
+
|
|
47
|
+
const categoryList = Object.keys(data).filter((category) => {
|
|
48
|
+
return !stateKeys.includes(category) && data[category].type === 'tag';
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const isDisabled = !hasAllDependencies(stateKeys, filtersDependOn) || categoryList.length === 0;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
categoryList,
|
|
55
|
+
closeMenu,
|
|
56
|
+
isDisabled,
|
|
57
|
+
isMenuOpen,
|
|
58
|
+
onCategorySelect,
|
|
59
|
+
onChange,
|
|
60
|
+
onClose,
|
|
61
|
+
onOpen,
|
|
62
|
+
selectedCategory,
|
|
63
|
+
setSelectedCategory,
|
|
64
|
+
stateKeys,
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export default useFilterAdd;
|