@bitrise/bitkit 12.76.0 → 12.78.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bitrise/bitkit",
3
3
  "description": "Bitrise React component library",
4
- "version": "12.76.0",
4
+ "version": "12.78.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+ssh://git@github.com/bitrise-io/bitkit.git"
@@ -11,6 +11,7 @@
11
11
  "scripts": {
12
12
  "build-storybook": "storybook build",
13
13
  "lint": "eslint src --ext ts,tsx",
14
+ "lint:fix": "eslint src --ext ts,tsx --fix",
14
15
  "release": "release-it minor --ci",
15
16
  "release-alpha": "release-it --preRelease=alpha --ci",
16
17
  "start": "npm run storybook",
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useState } from 'react';
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { DateTime } from 'luxon';
3
3
  import FocusLock from 'react-focus-lock';
4
4
  import { PopoverAnchor } from '@chakra-ui/react';
@@ -7,6 +7,7 @@ import Popover from '../Popover/Popover';
7
7
  import PopoverContent from '../Popover/PopoverContent';
8
8
  import Box from '../Box/Box';
9
9
  import useResponsive from '../../hooks/useResponsive';
10
+ import Button from '../Button/Button';
10
11
  import DatePickerMonth from './DatePickerMonth';
11
12
  import { DatePickerContext } from './DatePicker.context';
12
13
  import DatePickerMonthSelector from './DatePickerMonthSelector';
@@ -16,38 +17,55 @@ import DatePickerFooter from './DatePickerFooter';
16
17
 
17
18
  export { useDateRange, DateRange };
18
19
 
19
- export interface DatePickerProps {
20
+ export type DatePickerProps = {
20
21
  children: React.ReactNode;
21
22
  selectable?: DateRange;
22
- selected?: DateRange;
23
- onApply?: (range: DateRange) => void;
24
23
  onClose: () => void;
25
24
  onClear?: () => void;
26
25
  visible: boolean;
27
- }
26
+ variant?: 'default' | 'filter';
27
+ } & (
28
+ | {
29
+ selected?: DateRange;
30
+ onApply?: (range: DateRange) => void;
31
+ mode?: 'range';
32
+ }
33
+ | {
34
+ selected?: DateTime;
35
+ onApply?: (day?: DateTime) => void;
36
+ mode: 'day';
37
+ }
38
+ );
28
39
 
29
40
  /**
30
41
  * A simple date selection component, that supports a dual month view and
31
42
  * range selection.
32
43
  */
33
44
  const DatePicker = (props: DatePickerProps) => {
34
- const { children, onApply, onClose, onClear, visible, selectable, selected } = props;
45
+ const { children, onApply, onClose, onClear, visible, selectable, selected, mode, variant = 'default' } = props;
46
+
47
+ const isSelectionHappened = useRef(false);
35
48
 
36
49
  const { isMobile } = useResponsive();
37
50
  const today = DateTime.now().startOf('day');
38
51
 
39
- const [dateFrom, setDateFrom] = useState(selected?.from);
52
+ const initialRange = useMemo(
53
+ () => (mode === 'day' ? selected && new DateRange(selected) : selected),
54
+ [selected, mode],
55
+ );
56
+
57
+ const [dateFrom, setDateFrom] = useState(initialRange?.from);
40
58
  useEffect(() => {
41
- if (!selected?.from || !dateFrom?.equals(selected.from)) {
42
- setDateFrom(selected?.from);
59
+ if (!initialRange?.from || !dateFrom?.equals(initialRange.from)) {
60
+ setDateFrom(initialRange?.from);
43
61
  }
44
- }, [selected]);
45
- const [dateTo, setDateTo] = useState(selected?.to);
62
+ }, [initialRange]);
63
+ const [dateTo, setDateTo] = useState(initialRange?.to);
46
64
  useEffect(() => {
47
- if (!selected?.to || !dateTo?.equals(selected.to)) {
48
- setDateTo(selected?.to);
65
+ if (!initialRange?.to || !dateTo?.equals(initialRange.to)) {
66
+ setDateTo(initialRange?.to);
49
67
  }
50
- }, [selected]);
68
+ }, [initialRange]);
51
69
 
52
70
  const handleClose = () => {
53
71
  onClose();
@@ -57,7 +75,11 @@ const DatePicker = (props: DatePickerProps) => {
57
75
 
58
76
  const handleApply = () => {
59
77
  if (onApply) {
60
- onApply(new DateRange(dateFrom, dateTo));
78
+ if (mode === 'day') {
79
+ onApply(dateFrom);
80
+ } else {
81
+ onApply(new DateRange(dateFrom, dateTo));
82
+ }
61
83
  }
62
84
 
63
85
  handleClose();
@@ -67,8 +89,11 @@ const DatePicker = (props: DatePickerProps) => {
67
89
  initalView: dateFrom || selectable?.to,
68
90
  });
69
91
 
92
+ const isSingleMonthView = mode === 'day' || isMobile;
93
+
70
94
  const [preview, setPreview] = useState<'from' | 'to' | undefined>(undefined);
71
95
  const [isMonthSelector, setIsMonthSelector] = useState<'left' | 'right' | undefined>(undefined);
96
+
72
97
  const handlePreview = useCallback(
73
98
  (date: DateTime) => {
74
99
  if (!preview) {
@@ -85,10 +110,15 @@ const DatePicker = (props: DatePickerProps) => {
85
110
  },
86
111
  [preview, dateFrom, dateTo],
87
112
  );
113
+
88
114
  const handleSelect = useCallback(
89
115
  (date: DateTime) => {
116
+ isSelectionHappened.current = true;
117
+
90
118
  setPreview(undefined);
91
- if (dateFrom && dateTo) {
119
+ if (mode === 'day') {
120
+ setDateFrom(date);
121
+ } else if (dateFrom && dateTo) {
92
122
  if (!preview) {
93
123
  setPreview('from');
94
124
  setDateFrom(date);
@@ -109,16 +139,32 @@ const DatePicker = (props: DatePickerProps) => {
109
139
  },
110
140
  [dateFrom, dateTo, preview],
111
141
  );
142
+
143
+ useEffect(() => {
144
+ if (variant === 'default') {
145
+ if (!dateFrom || !isSelectionHappened.current || preview) {
146
+ return;
147
+ }
148
+
149
+ if (mode !== 'day' && !dateTo) {
150
+ return;
151
+ }
152
+
153
+ handleApply();
154
+ }
155
+ }, [dateFrom, dateTo, preview, isSelectionHappened.current]);
156
+
112
157
  const currentSelected = useDateRange([dateFrom, dateTo]);
113
158
  const ctx = useObjectMemo({
114
159
  selectable,
115
160
  selected: currentSelected,
116
161
  preview,
117
- showOutsideMonths: isMobile,
162
+ showOutsideMonths: isSingleMonthView,
118
163
  today,
119
164
  onPreview: handlePreview,
120
165
  onSelect: handleSelect,
121
166
  });
167
+
122
168
  const onMonthClickLeft = useCallback(() => setIsMonthSelector('left'), []);
123
169
  const onMonthClickRight = useCallback(() => setIsMonthSelector('right'), []);
124
170
  const onMonthSelected = useCallback(
@@ -152,11 +198,11 @@ const DatePicker = (props: DatePickerProps) => {
152
198
  <DatePickerMonth
153
199
  onMonthClick={onMonthClickLeft}
154
200
  onViewDateChange={updateLeftViewDate}
155
- controls={isMobile ? 'both' : 'left'}
201
+ controls={isSingleMonthView ? 'both' : 'left'}
156
202
  viewDate={leftViewDate}
157
203
  />
158
204
 
159
- {!isMobile && (
205
+ {!isSingleMonthView && (
160
206
  <DatePickerMonth
161
207
  onMonthClick={onMonthClickRight}
162
208
  onViewDateChange={updateRightViewDate}
@@ -165,7 +211,22 @@ const DatePicker = (props: DatePickerProps) => {
165
211
  />
166
212
  )}
167
213
  </Box>
168
- <DatePickerFooter onApply={handleApply} onClose={handleClose} onClear={onClear} selected={selected} />
214
+ {variant === 'filter' && (
215
+ <DatePickerFooter
216
+ onApply={handleApply}
217
+ onClose={handleClose}
218
+ onClear={onClear}
219
+ selected={currentSelected}
220
+ mode={mode || 'range'}
221
+ />
222
+ )}
223
+ {variant === 'default' && mode === 'day' && (
224
+ <Box display="flex" justifyContent="space-around">
225
+ <Button onClick={() => handleSelect(DateTime.now())} variant="tertiary">
226
+ Today
227
+ </Button>
228
+ </Box>
229
+ )}
169
230
  </>
170
231
  )}
171
232
  </Box>
@@ -11,95 +11,134 @@ function selectionBorder({ selection }: DatePickerDayViewProps) {
11
11
  }
12
12
  return { borderRadius: borderRadii[selection] };
13
13
  }
14
- function previewStyles({ selection, preview, currentMonth }: DatePickerDayViewProps) {
15
- const beingMoved = selection === preview;
16
- const endpoint = selection === 'from' || selection === 'to';
17
- if (selection === 'incomplete') {
18
- return {
19
- backgroundColor: 'purple.50',
20
- color: 'neutral.100',
21
- };
14
+
15
+ function textStyle({ today, selection, currentMonth, selectable }: DatePickerDayViewProps) {
16
+ const endpoint = selection === 'from' || selection === 'to' || selection === 'incomplete';
17
+
18
+ if (!selectable) {
19
+ return undefined;
22
20
  }
23
- if (selection === 'between') {
24
- return {
25
- bg: 'purple.95',
26
- color: currentMonth ? 'purple.10' : 'neutral.70',
27
- };
21
+
22
+ let backgroundColor: string | undefined;
23
+
24
+ if (!selection) {
25
+ if (currentMonth) {
26
+ backgroundColor = 'sys.interactive.moderate';
27
+ } else {
28
+ backgroundColor = 'sys.interactive.subtle';
29
+ }
28
30
  }
29
31
 
30
32
  if (endpoint) {
31
- if (beingMoved) {
32
- return {
33
- bg: currentMonth ? 'purple.90' : 'purple.93',
34
- color: currentMonth ? 'purple.10' : 'neutral.70',
35
- };
36
- }
37
33
  return {
38
- bg: currentMonth ? 'purple.50' : 'purple.70',
39
- color: currentMonth ? 'neutral.100' : 'purple.95',
34
+ borderColor: today && 'sys.interactive.moderate',
35
+ '&:hover, button:focus-visible > &': {
36
+ backgroundColor,
37
+ },
40
38
  };
41
39
  }
42
- return {};
40
+
41
+ return {
42
+ borderColor: today && 'sys.interactive.base',
43
+ '&:hover, button:focus-visible > &': {
44
+ backgroundColor,
45
+ },
46
+ };
43
47
  }
44
- function selectionStyles({ selection, currentMonth }: DatePickerDayViewProps) {
45
- if (!selection) {
46
- return {};
47
- }
48
- if (selection === 'from' || selection === 'to' || selection === 'incomplete') {
49
- return {
50
- backgroundColor: currentMonth ? 'purple.50' : 'purple.70',
51
- color: currentMonth ? 'neutral.100' : 'neutral.90',
52
- '&:hover, &:focus-visible': {
53
- outline: 'none',
54
- boxShadow: 'none',
55
- color: 'purple.90',
56
- backgroundColor: 'purple.50',
48
+
49
+ const buttonStyles = ({ today, selection, preview, selectable, currentMonth }: DatePickerDayViewProps) => {
50
+ const beingMoved = selection === preview;
51
+
52
+ const baseStyles = {
53
+ active: {
54
+ style: {
55
+ color: today ? 'sys.interactive.base' : 'sys.fg.primary',
57
56
  },
58
- };
59
- }
60
- if (selection === 'between') {
61
- return {
62
- color: currentMonth ? 'purple.10' : 'neutral.70',
63
- backgroundColor: 'purple.90',
64
- '&:hover, &:focus-visible': {
65
- outline: 'none',
66
- boxShadow: 'none',
67
- backgroundColor: 'purple.80',
57
+ rangeEnd: {
58
+ style: {
59
+ backgroundColor: beingMoved ? 'sys.interactive.muted' : 'sys.interactive.base',
60
+ color: beingMoved ? undefined : 'sys.fg.on-color',
61
+ '&:hover, &:focus-visible': {
62
+ backgroundColor: beingMoved ? undefined : 'sys.interactive.base',
63
+ color: beingMoved ? undefined : 'sys.interactive.moderate',
64
+ },
65
+ },
66
+ },
67
+ rangeMid: {
68
+ style: {
69
+ backgroundColor: 'sys.interactive.moderate',
70
+ '&:hover, &:focus-visible': {
71
+ backgroundColor: 'sys.interactive.muted',
72
+ },
73
+ },
74
+ },
75
+ },
76
+ 'n/a': {
77
+ style: {
78
+ color: 'sys.fg.tertiary',
79
+ },
80
+ rangeEnd: {
81
+ style: {
82
+ backgroundColor: beingMoved ? 'sys.interactive.muted' : 'sys.interactive.highlight',
83
+ color: beingMoved ? 'sys.fg.tertiary' : 'sys.interactive.subtle',
84
+ '&:hover, &:focus-visible': {
85
+ color: 'sys.interactive.minimal',
86
+ },
87
+ },
68
88
  },
89
+ rangeMid: {
90
+ style: {
91
+ backgroundColor: 'sys.interactive.moderate',
92
+ color: 'sys.interactive.bold',
93
+ '&:hover, &:focus-visible': {
94
+ backgroundColor: 'sys.interactive.muted',
95
+ color: 'sys.interactive.minimal',
96
+ },
97
+ },
98
+ },
99
+ },
100
+ };
101
+
102
+ if (!selectable) {
103
+ return {
104
+ backgroundColor: 'sys.neutral.subtle',
105
+ color: 'sys.fg.disabled',
69
106
  };
70
107
  }
71
- return { backgroundColor: 'purple.90' };
72
- }
108
+
109
+ const endpoint = selection === 'from' || selection === 'to' || selection === 'incomplete';
110
+ const range = selection && (endpoint ? 'rangeEnd' : 'rangeMid');
111
+ const dayStyles = baseStyles[currentMonth ? 'active' : 'n/a'];
112
+
113
+ return {
114
+ ...dayStyles.style,
115
+ ...(range ? dayStyles?.[range]?.style : {}),
116
+ };
117
+ };
118
+
73
119
  const DatePickerDay = {
74
120
  parts: ['day', 'selection'],
75
121
  baseStyle(props: DatePickerDayViewProps) {
76
- const { today, selectable, currentMonth, showOutsideDays, preview, selection } = props;
122
+ const { today, selectable } = props;
123
+
77
124
  return {
78
125
  text: {
79
126
  border: today ? '1.5px solid' : undefined,
80
- borderColor: today ? 'purple.50' : undefined,
81
127
  width: '40',
82
128
  height: '32',
83
129
  borderRadius: '8',
84
130
  display: 'flex',
85
131
  alignItems: 'center',
86
132
  justifyContent: 'center',
87
- '&:hover, button:focus-visible > &': selectable
88
- ? {
89
- outline: 'none',
90
- boxShadow: 'none',
91
- backgroundColor: selection ? undefined : 'purple.90',
92
- }
93
- : undefined,
133
+ ...textStyle(props),
94
134
  },
95
135
  day: {
96
- color: !selectable || (!currentMonth && showOutsideDays) ? 'neutral.70' : 'purple.10',
97
136
  _focusVisible: {
98
137
  outline: 'none',
99
138
  boxShadow: 'none',
100
139
  },
101
140
  cursor: !selectable ? 'default' : undefined,
102
- ...(preview ? previewStyles(props) : selectionStyles(props)),
141
+ ...buttonStyles(props),
103
142
  ...selectionBorder(props),
104
143
  },
105
144
  };
@@ -51,12 +51,24 @@ const DatePickerDayView = ({
51
51
  currentMonth,
52
52
  });
53
53
  const ariaProps: BoxProps = {};
54
+
54
55
  if (currentMonth) {
55
- ariaProps.as = 'button';
56
- ariaProps.role = 'option';
56
+ ariaProps['aria-selected'] =
57
+ selection === 'from' || selection === 'to' || selection === 'incomplete' || selection === 'between';
58
+ } else {
59
+ ariaProps.tabIndex = -1;
57
60
  }
58
61
  return (
59
- <Box {...ariaProps} sx={day} onMouseEnter={onMouseEnter} onFocus={onMouseEnter} onClick={onClick}>
62
+ <Box
63
+ {...ariaProps}
64
+ as="button"
65
+ role="option"
66
+ disabled={!selectable}
67
+ sx={day}
68
+ onMouseEnter={onMouseEnter}
69
+ onFocus={onMouseEnter}
70
+ onClick={onClick}
71
+ >
60
72
  <Text sx={text}>{children}</Text>
61
73
  </Box>
62
74
  );
@@ -64,7 +76,7 @@ const DatePickerDayView = ({
64
76
  const [DatePickerDayContext, useDatePickerDayContext] = createContext<Context>();
65
77
  export { DatePickerDayContext };
66
78
 
67
- const DatePickerDay = ({ n }: { n: number }): JSX.Element => {
79
+ const DatePickerDay = ({ n }: { n: number }): JSX.Element | null => {
68
80
  const {
69
81
  preview,
70
82
  selectable,
@@ -122,7 +134,10 @@ const DatePickerDay = ({ n }: { n: number }): JSX.Element => {
122
134
  }
123
135
  }, [onSelect, isSelectable, date]);
124
136
 
125
- if (!isCurrentMonth && !showOutsideDays) {
137
+ if (isNextMonth && !showOutsideDays) {
138
+ return null;
139
+ }
140
+ if (isPreviousMonth && !showOutsideDays) {
126
141
  return <div />;
127
142
  }
128
143
  return (
@@ -3,36 +3,71 @@ import Text from '../Text/Text';
3
3
  import Button from '../Button/Button';
4
4
  import ButtonGroup from '../ButtonGroup/ButtonGroup';
5
5
  import { DateRange } from './useDateRange';
6
+ import { useDatePickerContext } from './DatePicker.context';
6
7
 
7
8
  const DatePickerFooter = ({
8
9
  selected,
9
10
  onClose,
10
11
  onApply,
11
12
  onClear,
13
+ mode,
12
14
  }: {
13
15
  selected?: DateRange;
14
16
  onClose: () => void;
15
17
  onApply: () => void;
16
18
  onClear?: () => void;
19
+ mode: 'day' | 'range';
17
20
  }) => {
21
+ const { preview } = useDatePickerContext();
22
+
23
+ const styleGrid = (mobile: string, desktop: string) => (mode === 'day' ? mobile : [mobile, desktop]);
24
+
25
+ const displayDate = selected?.from || selected?.to;
26
+
18
27
  return (
19
28
  <Box
20
29
  display="grid"
21
- gridTemplateColumns={['unset', '1fr auto 1fr']}
22
- gridTemplateRows={['1.25rem 2rem', 'unset']}
23
- gap="24"
30
+ gridTemplateColumns="1fr auto 1fr"
31
+ gridTemplateRows={styleGrid(displayDate ? '1.25rem 2rem' : '0 2rem', 'unset')}
32
+ gap={displayDate ? 24 : 0}
33
+ data-testid="footer"
24
34
  >
25
35
  {!!onClear && (
26
- <Button size="small" variant="tertiary" color="purple.10" width="fit-content" onClick={() => onClear()}>
36
+ <Button
37
+ gridRow={styleGrid('2', '1')}
38
+ gridColumn="1"
39
+ size="small"
40
+ variant="tertiary"
41
+ color="purple.10"
42
+ width="fit-content"
43
+ onClick={() => onClear()}
44
+ >
27
45
  Clear
28
46
  </Button>
29
47
  )}
30
- <Text alignSelf="center" justifySelf="center" size="2" color="text.secondary" gridColumn={['1', '2']}>
31
- {selected?.from?.toFormat('DD', { locale: 'en-US' })}
32
- {selected?.from || selected?.to ? ' - ' : undefined}
33
- {selected?.to?.toFormat('DD', { locale: 'en-US' })}
48
+ <Text
49
+ gridRow="1"
50
+ gridColumn={styleGrid('1 / 4', '2')}
51
+ alignSelf="center"
52
+ justifySelf="center"
53
+ size="2"
54
+ color="text.secondary"
55
+ >
56
+ {mode === 'day' ? (
57
+ selected?.from?.toFormat('DD', { locale: 'en-US' })
58
+ ) : (
59
+ <>
60
+ {(!preview || preview === 'to') && selected?.from?.toFormat('DD', { locale: 'en-US' })}
61
+ {selected?.to && (
62
+ <>
63
+ {selected?.from || selected?.to ? ' - ' : undefined}
64
+ {(!preview || preview === 'from') && selected?.to?.toFormat('DD', { locale: 'en-US' })}
65
+ </>
66
+ )}
67
+ </>
68
+ )}
34
69
  </Text>
35
- <ButtonGroup gridColumn={['1', '3']} justifyContent="end">
70
+ <ButtonGroup gridRow={styleGrid('2', '1')} gridColumn={styleGrid('2 / 4', '3')} justifyContent="end">
36
71
  <Button variant="secondary" onClick={onClose} size="small">
37
72
  Cancel
38
73
  </Button>
@@ -46,7 +46,7 @@ const DatePickerMonth = ({ controls, onViewDateChange, viewDate, onMonthClick }:
46
46
 
47
47
  const monthLabelId = useId();
48
48
  return (
49
- <Box>
49
+ <Box data-testid={`controls-${controls}`}>
50
50
  <DatePickerHeader onPrevious={onPreviousMonth} onNext={onNextMonth} controls={controls}>
51
51
  <DatePickerHeaderPrevious label="previous month" />
52
52
  <DatePickerHeaderContent id={monthLabelId}>