@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,131 @@
1
+ import { FormEvent, useEffect, useMemo, useState } from 'react';
2
+ import { isEqual, useDebounce } from '../../../utils/utils';
3
+ import { useFilterContext } from '../Filter.context';
4
+ import { FilterFormProps, FilterOptions, FilterOptionsMap, FilterValue } from '../Filter.types';
5
+
6
+ export const MAX_ITEMS = 100;
7
+
8
+ const useFilterForm = (props: Omit<FilterFormProps, 'onCancel'>) => {
9
+ const { category, onChange } = props;
10
+
11
+ const { data, state } = useFilterContext();
12
+
13
+ const categoryData = data[category] || {};
14
+ const { isPatternEnabled, preserveOptionOrder, isInitialLoading, onAsyncSearch, options, optionsMap } = categoryData;
15
+
16
+ const value = useMemo(() => state[category] || [], [category, state]);
17
+
18
+ const [selected, setSelected] = useState<FilterValue>(value);
19
+
20
+ useEffect(() => {
21
+ setSelected(value);
22
+ }, [value]);
23
+
24
+ const [mode, setMode] = useState<'manually' | 'pattern'>(
25
+ isPatternEnabled && value?.length && value[0].includes('*') ? 'pattern' : 'manually',
26
+ );
27
+
28
+ const [searchValue, setSearchValue] = useState<string>('');
29
+ const debouncedSearchValue = useDebounce<string>(searchValue, 1000);
30
+
31
+ const [isLoading, setLoading] = useState(Boolean(isInitialLoading));
32
+ const [foundOptions, setFoundOptions] = useState<FilterOptions>([]);
33
+ const [foundOptionsMap, setFoundOptionsMap] = useState<FilterOptionsMap>();
34
+
35
+ const isAsync = !!onAsyncSearch;
36
+ const withSearch = (options && options.length > 5) || isAsync;
37
+
38
+ const isSubmitDisabled =
39
+ isEqual(selected, value) || (mode === 'pattern' && !!selected[0] && !selected[0].includes('*'));
40
+ const filteredOptions = useMemo(() => {
41
+ if (options?.length) {
42
+ return options.filter((opt) => {
43
+ const optLabel = (optionsMap && optionsMap[opt]) || opt;
44
+ return optLabel.toLowerCase().includes(searchValue.toLowerCase());
45
+ });
46
+ }
47
+ return [];
48
+ }, [searchValue, options]);
49
+
50
+ const onSearchChange = (q: string) => {
51
+ setSearchValue(q);
52
+ if (isAsync) {
53
+ if (q.length > 0) {
54
+ setLoading(true);
55
+ } else {
56
+ setFoundOptions([]);
57
+ }
58
+ }
59
+ };
60
+
61
+ const onSubmit = (e?: FormEvent<HTMLDivElement>) => {
62
+ e?.preventDefault();
63
+ const newSelected = selected[0] === '' ? [] : selected;
64
+ onChange(category, newSelected, value);
65
+ };
66
+
67
+ const onClearClick = () => {
68
+ setSelected([]);
69
+ onChange(category, [], value);
70
+ };
71
+
72
+ const getEmptyText = () => {
73
+ if (searchValue.length) {
74
+ return 'No result. Refine your search term.';
75
+ }
76
+ return '';
77
+ };
78
+
79
+ const getAsyncList = async () => {
80
+ if (onAsyncSearch) {
81
+ const response = await onAsyncSearch(category, searchValue);
82
+ setLoading(false);
83
+ setFoundOptions(response.options);
84
+ setFoundOptionsMap(response.optionsMap || optionsMap);
85
+ }
86
+ };
87
+
88
+ useEffect(() => {
89
+ setLoading(Boolean(isInitialLoading));
90
+ }, [isInitialLoading]);
91
+
92
+ useEffect(() => {
93
+ if (debouncedSearchValue.length > 0) {
94
+ getAsyncList();
95
+ } else {
96
+ setLoading(Boolean(isInitialLoading));
97
+ }
98
+ }, [debouncedSearchValue]);
99
+
100
+ const isEditMode = value.length !== 0;
101
+
102
+ const isAsyncSearch = isAsync && !!searchValue;
103
+
104
+ const items = isAsyncSearch
105
+ ? Array.from(new Set(preserveOptionOrder ? [...foundOptions] : [...value, ...foundOptions]))
106
+ : Array.from(new Set(preserveOptionOrder ? [...filteredOptions] : [...value, ...filteredOptions]));
107
+
108
+ const currentOptionMap = isAsyncSearch ? foundOptionsMap : optionsMap;
109
+
110
+ return {
111
+ currentOptionMap,
112
+ getEmptyText,
113
+ isAsync,
114
+ isLoading,
115
+ isSubmitDisabled,
116
+ mode,
117
+ onClearClick,
118
+ onSearchChange,
119
+ onSubmit,
120
+ isEditMode,
121
+ items,
122
+ selected,
123
+ searchValue,
124
+ setMode,
125
+ setSelected,
126
+ value,
127
+ withSearch,
128
+ };
129
+ };
130
+
131
+ export default useFilterForm;
@@ -0,0 +1,35 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ type UseIsScrollableProps = {
4
+ items: string[];
5
+ hasNotFilteredOption: boolean;
6
+ ref: React.RefObject<HTMLDivElement>;
7
+ };
8
+
9
+ const useIsScrollable = ({ items, hasNotFilteredOption, ref }: UseIsScrollableProps) => {
10
+ const [isScrollable, setIsScrollable] = useState(false);
11
+
12
+ useEffect(() => {
13
+ const checkScrollable = () => {
14
+ if (ref.current) {
15
+ const { scrollHeight, clientHeight } = ref.current;
16
+ setIsScrollable(scrollHeight > clientHeight);
17
+ }
18
+ };
19
+
20
+ checkScrollable();
21
+
22
+ const timer = setTimeout(checkScrollable, 100);
23
+
24
+ window.addEventListener('resize', checkScrollable);
25
+
26
+ return () => {
27
+ clearTimeout(timer);
28
+ window.removeEventListener('resize', checkScrollable);
29
+ };
30
+ }, [items, hasNotFilteredOption]);
31
+
32
+ return { isScrollable };
33
+ };
34
+
35
+ export default useIsScrollable;
@@ -0,0 +1,66 @@
1
+ import { useState, useRef } from 'react';
2
+ import { MAX_ITEMS } from './useFilterForm';
3
+
4
+ type UseListBoxProps = {
5
+ items: string[];
6
+ hasNotFilteredOption: boolean;
7
+ onChange: (option: string) => void;
8
+ };
9
+
10
+ const useListBox = ({ items, hasNotFilteredOption, onChange }: UseListBoxProps) => {
11
+ const [activeIndex, setActiveIndex] = useState(-1);
12
+ const allOptions = hasNotFilteredOption ? ['', ...items.slice(0, MAX_ITEMS)] : items.slice(0, MAX_ITEMS);
13
+ const mouseInteractionRef = useRef(false);
14
+
15
+ const handleKeyDown = (e: React.KeyboardEvent) => {
16
+ mouseInteractionRef.current = false;
17
+ switch (e.key) {
18
+ case 'ArrowDown':
19
+ e.preventDefault();
20
+ setActiveIndex((prev) => (prev < allOptions.length - 1 ? prev + 1 : 0));
21
+ break;
22
+ case 'ArrowUp':
23
+ e.preventDefault();
24
+ setActiveIndex((prev) => (prev > 0 ? prev - 1 : allOptions.length - 1));
25
+ break;
26
+ case 'Enter':
27
+ case ' ':
28
+ e.preventDefault();
29
+ if (activeIndex >= 0) {
30
+ onChange(allOptions[activeIndex]);
31
+ }
32
+ break;
33
+ }
34
+ };
35
+
36
+ const handleFocus = () => {
37
+ if (activeIndex === -1 && !mouseInteractionRef.current) {
38
+ setActiveIndex(0);
39
+ }
40
+ };
41
+
42
+ const handleBlur = () => {
43
+ setActiveIndex(-1);
44
+ };
45
+
46
+ const handleMouseDown = () => {
47
+ mouseInteractionRef.current = true;
48
+ };
49
+
50
+ const handleMouseMove = () => {
51
+ if (activeIndex !== -1) {
52
+ setActiveIndex(-1);
53
+ }
54
+ };
55
+
56
+ return {
57
+ activeIndex,
58
+ handleKeyDown,
59
+ handleFocus,
60
+ handleBlur,
61
+ handleMouseDown,
62
+ handleMouseMove,
63
+ };
64
+ };
65
+
66
+ export default useListBox;
@@ -14,12 +14,6 @@ const InputTheme = defineStyle({
14
14
  background: 'background.disabled',
15
15
  cursor: 'not-allowed',
16
16
  },
17
- _focus: {
18
- boxShadow: 'formFocus',
19
- },
20
- _focusVisible: {
21
- boxShadow: 'formFocus',
22
- },
23
17
  _hover: {
24
18
  borderColor: 'border.hover',
25
19
  },
@@ -41,12 +35,9 @@ const InputTheme = defineStyle({
41
35
  },
42
36
  appearance: 'none',
43
37
  background: 'background.primary',
44
- border: '1px solid',
45
- borderColor: 'border.regular',
38
+
46
39
  borderRadius: '4',
47
- boxShadow: 'inner',
48
- outline: 0,
49
- transition: '200ms',
40
+
50
41
  width: '100%',
51
42
  },
52
43
  icon: {
@@ -75,6 +66,28 @@ const InputTheme = defineStyle({
75
66
  position: 'relative',
76
67
  },
77
68
  },
69
+ variants: {
70
+ desktop: {
71
+ field: {
72
+ border: '1px solid',
73
+ borderColor: 'border.regular',
74
+ boxShadow: 'inner',
75
+ outline: 0,
76
+ transition: '200ms',
77
+ _focus: {
78
+ boxShadow: 'formFocus',
79
+ },
80
+ _focusVisible: {
81
+ boxShadow: 'formFocus',
82
+ },
83
+ },
84
+ },
85
+ mobile: {
86
+ field: {
87
+ boxShadow: 'none',
88
+ },
89
+ },
90
+ },
78
91
  sizes: {
79
92
  lg: {
80
93
  field: {
@@ -89,6 +102,9 @@ const InputTheme = defineStyle({
89
102
  },
90
103
  },
91
104
  },
105
+ defaultProps: {
106
+ variant: 'desktop',
107
+ },
92
108
  });
93
109
 
94
110
  export default InputTheme;
@@ -64,6 +64,7 @@ export interface InputProps extends UsedFormControlProps, UsedChakraInputProps {
64
64
  rightIconName?: TypeIconName;
65
65
  rightIconColor?: IconProps['color'];
66
66
  size?: 'lg' | 'md';
67
+ variant?: 'desktop' | 'mobile';
67
68
  warningText?: ReactNode;
68
69
  }
69
70
 
@@ -111,6 +112,7 @@ const Input = forwardRef<InputProps, 'div'>((props, ref) => {
111
112
  step,
112
113
  type,
113
114
  value,
115
+ variant,
114
116
  warningText,
115
117
  ...rest
116
118
  } = props;
@@ -167,7 +169,7 @@ const Input = forwardRef<InputProps, 'div'>((props, ref) => {
167
169
  };
168
170
 
169
171
  const iconSize = size === 'lg' ? '24' : '16';
170
- const style = useMultiStyleConfig('Input');
172
+ const style = useMultiStyleConfig('Input', { variant });
171
173
 
172
174
  const [leftIconPos, setLeftIconPos] = useState(rem(12));
173
175
 
@@ -220,6 +222,7 @@ const Input = forwardRef<InputProps, 'div'>((props, ref) => {
220
222
  {...inputProps}
221
223
  {...dataAttributes}
222
224
  onChange={onInputChange}
225
+ variant={variant}
223
226
  />
224
227
  {rightAddon && (
225
228
  <RightContentWrapper ref={rightAddonRef} bottom="0">
@@ -7,7 +7,7 @@ export interface SearchInputProps extends Omit<InputProps, 'onChange'> {
7
7
  }
8
8
 
9
9
  const SearchInput = (props: SearchInputProps) => {
10
- const { onChange, value, ...rest } = props;
10
+ const { onChange, value, variant, ...rest } = props;
11
11
 
12
12
  const onInputChange = (event: ChangeEvent<HTMLInputElement>) => {
13
13
  const q = event.target.value;
@@ -29,6 +29,7 @@ const SearchInput = (props: SearchInputProps) => {
29
29
  />
30
30
  );
31
31
  const inputProps: InputProps = {
32
+ boxShadow: variant === 'mobile' ? 'none' : undefined,
32
33
  leftIconColor: 'neutral.60',
33
34
  leftIconName: 'Magnifier',
34
35
  onChange: onInputChange,
@@ -38,7 +39,7 @@ const SearchInput = (props: SearchInputProps) => {
38
39
  value,
39
40
  ...rest,
40
41
  };
41
- return <Input {...inputProps} />;
42
+ return <Input {...inputProps} variant={variant} />;
42
43
  };
43
44
 
44
45
  export default SearchInput;
@@ -47,7 +47,7 @@ import ExpandableCard from './ExpandableCard/ExpandableCard.theme';
47
47
  import FileInput from './Form/FileInput/FileInput.theme';
48
48
  import Filter from './Filter/Filter.theme';
49
49
  import Toggletip from './Toggletip/Toggletip.theme';
50
- import FilterSwitch from './Filter/FilterSwitch/FilterSwitch.theme';
50
+ import FilterSwitch from './Filter/Desktop/FilterSwitch/FilterSwitch.theme';
51
51
  import TagsInput from './Form/TagsInput/TagsInput.theme';
52
52
  import DraggableCard from './DraggableCard/DraggableCard.theme';
53
53
  import SelectableTag from './SelectableTag/SelectableTag.theme';
package/src/index.ts CHANGED
@@ -323,10 +323,10 @@ export { default as FileInput } from './Components/Form/FileInput/FileInput';
323
323
  export type { ToggletipProps } from './Components/Toggletip/Toggletip';
324
324
  export { default as Toggletip } from './Components/Toggletip/Toggletip';
325
325
 
326
- export type { FilterSwitchProps } from './Components/Filter/FilterSwitch/FilterSwitch';
327
- export { default as FilterSwitch } from './Components/Filter/FilterSwitch/FilterSwitch';
326
+ export type { FilterSwitchProps } from './Components/Filter/Desktop/FilterSwitch/FilterSwitch';
327
+ export { default as FilterSwitch } from './Components/Filter/Desktop/FilterSwitch/FilterSwitch';
328
328
 
329
- export { default as FilterSwitchGroup } from './Components/Filter/FilterSwitch/FilterSwitchGroup';
329
+ export { default as FilterSwitchGroup } from './Components/Filter/Desktop/FilterSwitch/FilterSwitchGroup';
330
330
 
331
331
  export type { TablePaginationProps } from './Components/Table/TablePagination';
332
332
  export { default as TablePagination } from './Components/Table/TablePagination';
@@ -337,7 +337,6 @@ export { default as Pagination } from './Components/Pagination/Pagination';
337
337
  export type { ProgressIndicatorProps } from './Components/ProgressIndicator/ProgressIndicator';
338
338
  export { default as ProgressIndicator } from './Components/ProgressIndicator/ProgressIndicator';
339
339
 
340
- export type { FilterProps } from './Components/Filter/Filter';
341
340
  export { default as Filter } from './Components/Filter/Filter';
342
341
  export * from './Components/Filter/Filter.types';
343
342
 
@@ -1,111 +0,0 @@
1
- import { useState } from 'react';
2
- import { Menu, MenuButton, MenuList, Portal, useDisclosure } from 'chakra-ui-2--react';
3
- import Button from '../../Button/Button';
4
- import MenuItem from '../../Menu/MenuItem';
5
- import Tooltip from '../../Tooltip/Tooltip';
6
- import { useFilterContext } from '../Filter.context';
7
- import FilterForm from '../FilterForm/FilterForm';
8
- import { FilterValue } from '../Filter.types';
9
- import { getMissingDependencies, hasAllDependencies } from '../Filter.utils';
10
-
11
- export interface FilterAddProps {
12
- onChange: (category: string, selected: FilterValue) => void;
13
- }
14
-
15
- const FilterAdd = (props: FilterAddProps) => {
16
- const { onChange } = props;
17
- const [selectedCategory, setSelectedCategory] = useState<string | undefined>();
18
-
19
- const { isOpen, onClose: closeMenu, onOpen: openMenu } = useDisclosure();
20
-
21
- const { data, filtersDependOn, isLoading, setPopoverOpen, state } = useFilterContext();
22
-
23
- const onCategorySelect = (category: string) => {
24
- setSelectedCategory(category);
25
- };
26
-
27
- const onOpen = () => {
28
- openMenu();
29
- setPopoverOpen(true);
30
- setSelectedCategory(undefined);
31
- };
32
-
33
- const onClose = () => {
34
- setPopoverOpen(false);
35
- closeMenu();
36
- setSelectedCategory(undefined);
37
- };
38
-
39
- const onFilterChange = (category: string, value: FilterValue) => {
40
- onClose();
41
- onChange(category, value);
42
- };
43
-
44
- const stateKeys = Object.keys(state);
45
-
46
- const categoryList = Object.keys(data).filter((category) => {
47
- return !stateKeys.includes(category) && data[category].type === 'tag';
48
- });
49
-
50
- const isDisabled = !hasAllDependencies(stateKeys, filtersDependOn) || categoryList.length === 0;
51
-
52
- return (
53
- <Menu closeOnSelect={false} isOpen={isOpen} onClose={onClose} onOpen={onOpen}>
54
- <MenuButton
55
- as={Button}
56
- isDisabled={isDisabled}
57
- isLoading={isLoading}
58
- leftIconName="Plus"
59
- position={isOpen ? 'relative' : undefined}
60
- size="sm"
61
- variant="tertiary"
62
- zIndex={isOpen ? 'dialog' : undefined}
63
- >
64
- Add filter {isOpen}
65
- </MenuButton>
66
- <Portal>
67
- <MenuList
68
- paddingY={selectedCategory ? 0 : '12'}
69
- position={isOpen ? 'relative' : undefined}
70
- zIndex={isOpen ? 'dialog' : undefined}
71
- >
72
- {selectedCategory ? (
73
- <FilterForm
74
- category={selectedCategory}
75
- categoryName={data[selectedCategory].categoryName}
76
- categoryNamePlural={data[selectedCategory].categoryNamePlural}
77
- loadingText={data[selectedCategory].loadingText}
78
- onCancel={onClose}
79
- onChange={onFilterChange}
80
- />
81
- ) : (
82
- categoryList.map((category) => {
83
- const { categoryName, dependsOn } = data[category];
84
- const missingDependencies = getMissingDependencies(stateKeys, Object.keys(dependsOn || []));
85
- const isCategoryDisabled = missingDependencies.length > 0 || !data[category]?.options?.length;
86
- return (
87
- <Tooltip
88
- key={category}
89
- isDisabled={!isCategoryDisabled}
90
- label={dependsOn?.[missingDependencies[0]] || 'There is no options for this category.'}
91
- placement="right"
92
- >
93
- <MenuItem
94
- isDisabled={isCategoryDisabled}
95
- onClick={() => onCategorySelect(category)}
96
- pointerEvents="all"
97
- rightIconName="ChevronRight"
98
- >
99
- {categoryName || category}
100
- </MenuItem>
101
- </Tooltip>
102
- );
103
- })
104
- )}
105
- </MenuList>
106
- </Portal>
107
- </Menu>
108
- );
109
- };
110
-
111
- export default FilterAdd;