@bitrise/bitkit 12.76.0 → 12.77.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.77.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+ssh://git@github.com/bitrise-io/bitkit.git"
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useState } from 'react';
1
+ import { useCallback, useEffect, useMemo, 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';
@@ -16,38 +16,52 @@ import DatePickerFooter from './DatePickerFooter';
16
16
 
17
17
  export { useDateRange, DateRange };
18
18
 
19
- export interface DatePickerProps {
19
+ export type DatePickerProps = {
20
20
  children: React.ReactNode;
21
21
  selectable?: DateRange;
22
- selected?: DateRange;
23
- onApply?: (range: DateRange) => void;
24
22
  onClose: () => void;
25
23
  onClear?: () => void;
26
24
  visible: boolean;
27
- }
25
+ } & (
26
+ | {
27
+ selected?: DateRange;
28
+ onApply?: (range: DateRange) => void;
29
+ mode?: 'range';
30
+ }
31
+ | {
32
+ selected?: DateTime;
33
+ onApply?: (day?: DateTime) => void;
34
+ mode: 'day';
35
+ }
36
+ );
28
37
 
29
38
  /**
30
39
  * A simple date selection component, that supports a dual month view and
31
40
  * range selection.
32
41
  */
33
42
  const DatePicker = (props: DatePickerProps) => {
34
- const { children, onApply, onClose, onClear, visible, selectable, selected } = props;
43
+ const { children, onApply, onClose, onClear, visible, selectable, selected, mode } = props;
35
44
 
36
45
  const { isMobile } = useResponsive();
37
46
  const today = DateTime.now().startOf('day');
38
47
 
39
- const [dateFrom, setDateFrom] = useState(selected?.from);
48
+ const initialRange = useMemo(
49
+ () => (mode === 'day' ? selected && new DateRange(selected) : selected),
50
+ [selected, mode],
51
+ );
52
+
53
+ const [dateFrom, setDateFrom] = useState(initialRange?.from);
40
54
  useEffect(() => {
41
- if (!selected?.from || !dateFrom?.equals(selected.from)) {
42
- setDateFrom(selected?.from);
55
+ if (!initialRange?.from || !dateFrom?.equals(initialRange.from)) {
56
+ setDateFrom(initialRange?.from);
43
57
  }
44
- }, [selected]);
45
- const [dateTo, setDateTo] = useState(selected?.to);
58
+ }, [initialRange]);
59
+ const [dateTo, setDateTo] = useState(initialRange?.to);
46
60
  useEffect(() => {
47
- if (!selected?.to || !dateTo?.equals(selected.to)) {
48
- setDateTo(selected?.to);
61
+ if (!initialRange?.to || !dateTo?.equals(initialRange.to)) {
62
+ setDateTo(initialRange?.to);
49
63
  }
50
- }, [selected]);
64
+ }, [initialRange]);
51
65
 
52
66
  const handleClose = () => {
53
67
  onClose();
@@ -57,7 +71,11 @@ const DatePicker = (props: DatePickerProps) => {
57
71
 
58
72
  const handleApply = () => {
59
73
  if (onApply) {
60
- onApply(new DateRange(dateFrom, dateTo));
74
+ if (mode === 'day') {
75
+ onApply(dateFrom);
76
+ } else {
77
+ onApply(new DateRange(dateFrom, dateTo));
78
+ }
61
79
  }
62
80
 
63
81
  handleClose();
@@ -67,6 +85,8 @@ const DatePicker = (props: DatePickerProps) => {
67
85
  initalView: dateFrom || selectable?.to,
68
86
  });
69
87
 
88
+ const isSingleMonthView = mode === 'day' || isMobile;
89
+
70
90
  const [preview, setPreview] = useState<'from' | 'to' | undefined>(undefined);
71
91
  const [isMonthSelector, setIsMonthSelector] = useState<'left' | 'right' | undefined>(undefined);
72
92
  const handlePreview = useCallback(
@@ -88,7 +108,9 @@ const DatePicker = (props: DatePickerProps) => {
88
108
  const handleSelect = useCallback(
89
109
  (date: DateTime) => {
90
110
  setPreview(undefined);
91
- if (dateFrom && dateTo) {
111
+ if (mode === 'day') {
112
+ setDateFrom(date);
113
+ } else if (dateFrom && dateTo) {
92
114
  if (!preview) {
93
115
  setPreview('from');
94
116
  setDateFrom(date);
@@ -114,11 +136,12 @@ const DatePicker = (props: DatePickerProps) => {
114
136
  selectable,
115
137
  selected: currentSelected,
116
138
  preview,
117
- showOutsideMonths: isMobile,
139
+ showOutsideMonths: isSingleMonthView,
118
140
  today,
119
141
  onPreview: handlePreview,
120
142
  onSelect: handleSelect,
121
143
  });
144
+
122
145
  const onMonthClickLeft = useCallback(() => setIsMonthSelector('left'), []);
123
146
  const onMonthClickRight = useCallback(() => setIsMonthSelector('right'), []);
124
147
  const onMonthSelected = useCallback(
@@ -152,11 +175,11 @@ const DatePicker = (props: DatePickerProps) => {
152
175
  <DatePickerMonth
153
176
  onMonthClick={onMonthClickLeft}
154
177
  onViewDateChange={updateLeftViewDate}
155
- controls={isMobile ? 'both' : 'left'}
178
+ controls={isSingleMonthView ? 'both' : 'left'}
156
179
  viewDate={leftViewDate}
157
180
  />
158
181
 
159
- {!isMobile && (
182
+ {!isSingleMonthView && (
160
183
  <DatePickerMonth
161
184
  onMonthClick={onMonthClickRight}
162
185
  onViewDateChange={updateRightViewDate}
@@ -165,7 +188,13 @@ const DatePicker = (props: DatePickerProps) => {
165
188
  />
166
189
  )}
167
190
  </Box>
168
- <DatePickerFooter onApply={handleApply} onClose={handleClose} onClear={onClear} selected={selected} />
191
+ <DatePickerFooter
192
+ onApply={handleApply}
193
+ onClose={handleClose}
194
+ onClear={onClear}
195
+ selected={currentSelected}
196
+ mode={mode || 'range'}
197
+ />
169
198
  </>
170
199
  )}
171
200
  </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,69 @@ 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
+
18
25
  return (
19
26
  <Box
20
27
  display="grid"
21
- gridTemplateColumns={['unset', '1fr auto 1fr']}
22
- gridTemplateRows={['1.25rem 2rem', 'unset']}
23
- gap="24"
28
+ gridTemplateColumns="1fr auto 1fr"
29
+ gridTemplateRows={styleGrid(selected ? '1.25rem 2rem' : '0 2rem', 'unset')}
30
+ gap={selected ? 24 : 0}
31
+ data-testid="footer"
24
32
  >
25
33
  {!!onClear && (
26
- <Button size="small" variant="tertiary" color="purple.10" width="fit-content" onClick={() => onClear()}>
34
+ <Button
35
+ gridRow={styleGrid('2', '1')}
36
+ gridColumn="1"
37
+ size="small"
38
+ variant="tertiary"
39
+ color="purple.10"
40
+ width="fit-content"
41
+ onClick={() => onClear()}
42
+ >
27
43
  Clear
28
44
  </Button>
29
45
  )}
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' })}
46
+ <Text
47
+ gridRow="1"
48
+ gridColumn={styleGrid('1 / 4', '2')}
49
+ alignSelf="center"
50
+ justifySelf="center"
51
+ size="2"
52
+ color="text.secondary"
53
+ >
54
+ {mode === 'day' ? (
55
+ selected?.from?.toFormat('DD', { locale: 'en-US' })
56
+ ) : (
57
+ <>
58
+ {(!preview || preview === 'to') && selected?.from?.toFormat('DD', { locale: 'en-US' })}
59
+ {selected?.to && (
60
+ <>
61
+ {selected?.from || selected?.to ? ' - ' : undefined}
62
+ {(!preview || preview === 'from') && selected?.to?.toFormat('DD', { locale: 'en-US' })}
63
+ </>
64
+ )}
65
+ </>
66
+ )}
34
67
  </Text>
35
- <ButtonGroup gridColumn={['1', '3']} justifyContent="end">
68
+ <ButtonGroup gridRow={styleGrid('2', '1')} gridColumn={styleGrid('2 / 4', '3')} justifyContent="end">
36
69
  <Button variant="secondary" onClick={onClose} size="small">
37
70
  Cancel
38
71
  </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}>