@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
@@ -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;