@bitrise/bitkit 10.34.0 → 10.35.0-alpha-datepicker-rewrite.2

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 (29) hide show
  1. package/package.json +9 -20
  2. package/src/Components/DatePicker/DatePicker.context.ts +14 -0
  3. package/src/Components/DatePicker/DatePicker.stories.tsx +16 -0
  4. package/src/Components/DatePicker/DatePicker.test.tsx +94 -0
  5. package/src/Components/DatePicker/DatePicker.tsx +178 -0
  6. package/src/Components/DatePicker/DatePickerDay.theme.ts +108 -0
  7. package/src/Components/DatePicker/DatePickerDay.tsx +137 -0
  8. package/src/Components/DatePicker/DatePickerFooter.tsx +41 -0
  9. package/src/Components/DatePicker/DatePickerGrid.tsx +10 -0
  10. package/src/Components/DatePicker/DatePickerHeader.tsx +72 -0
  11. package/src/Components/DatePicker/DatePickerMonth.tsx +94 -0
  12. package/src/Components/DatePicker/DatePickerMonthSelector.tsx +132 -0
  13. package/src/Components/DatePicker/useDateRange.ts +48 -0
  14. package/src/Components/DatePicker/useViewDate.ts +35 -0
  15. package/src/Components/NumberInput/NumberInput.theme.ts +36 -0
  16. package/src/Foundations/Shadows/Shadows.ts +1 -0
  17. package/src/Foundations/Sizes/Sizes.ts +1 -0
  18. package/src/Old/hooks/index.ts +0 -1
  19. package/src/index.ts +3 -0
  20. package/src/old.ts +0 -3
  21. package/src/theme.ts +4 -0
  22. package/src/tsconfig.tsbuildinfo +1 -1
  23. package/src/utils/utils.ts +8 -0
  24. package/src/Old/DatePicker/DatePicker.css +0 -74
  25. package/src/Old/DatePicker/DatePicker.tsx +0 -194
  26. package/src/Old/DatePicker/DatePickerDay.tsx +0 -72
  27. package/src/Old/DatePicker/DatePickerGrid.tsx +0 -12
  28. package/src/Old/DatePicker/DatePickerMonth.tsx +0 -87
  29. package/src/Old/hooks/useMediaQuery.ts +0 -91
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@bitrise/bitkit",
3
3
  "description": "Bitrise React component library",
4
- "version": "10.34.0",
4
+ "version": "10.35.0-alpha-datepicker-rewrite.2",
5
5
  "repository": "git@github.com:bitrise-io/bitkit.git",
6
6
  "main": "src/index.ts",
7
7
  "license": "UNLICENSED",
8
8
  "scripts": {
9
9
  "build:icons": "ts-node ./scripts/build-icons.ts",
10
10
  "commitlint": "commitlint",
11
- "lint": "eslint src/**/*.tsx",
11
+ "lint": "eslint src --ext ts,tsx",
12
12
  "semantic-release": "semantic-release",
13
13
  "start": "yarn storybook",
14
- "test": "jest ./src",
14
+ "test": "jest",
15
15
  "storybook": "start-storybook -p 6006",
16
16
  "build-storybook": "build-storybook"
17
17
  },
@@ -30,9 +30,10 @@
30
30
  "@popperjs/core": "^2.11.5",
31
31
  "classnames": "^2.3.1",
32
32
  "framer-motion": "^6.5.1",
33
- "luxon": "^2.5.0",
33
+ "luxon": "^3.0.3",
34
34
  "react": "^18.2.0",
35
35
  "react-dom": "^18.2.0",
36
+ "react-focus-lock": "^2.9.1",
36
37
  "react-popper": "^2.3.0"
37
38
  },
38
39
  "peerDependencies": {
@@ -64,10 +65,10 @@
64
65
  "@testing-library/dom": "^8.16.0",
65
66
  "@testing-library/jest-dom": "^5.16.4",
66
67
  "@testing-library/react": "^13.3.0",
67
- "@testing-library/user-event": "^14.3.0",
68
+ "@testing-library/user-event": "^14.4.3",
68
69
  "@types/cheerio": "^0.22.31",
69
70
  "@types/enzyme": "^3.10.12",
70
- "@types/jest": "^28.1.6",
71
+ "@types/jest": "^29.0.3",
71
72
  "@types/luxon": "^2.4.0",
72
73
  "@types/react": "18.0.18",
73
74
  "@types/react-dom": "^18.0.6",
@@ -93,14 +94,14 @@
93
94
  "glob": "^8.0.3",
94
95
  "husky": "^7.0.4",
95
96
  "identity-obj-proxy": "^3.0.0",
96
- "jest": "^28.1.3",
97
+ "jest": "^29.0.3",
97
98
  "jest-environment-jsdom": "^28.1.3",
98
99
  "jsdom": "^19.0.0",
99
100
  "prettier": "^2.7.1",
100
101
  "react-hook-form": "^7.33.1",
101
102
  "recast": "^0.21.1",
102
103
  "semantic-release": "^19.0.3",
103
- "ts-jest": "^28.0.7",
104
+ "ts-jest": "^29.0.1",
104
105
  "ts-node": "^10.9.1",
105
106
  "tsconfig-paths-webpack-plugin": "^3.5.2",
106
107
  "typescript": "^4.8.2",
@@ -113,18 +114,6 @@
113
114
  "publishConfig": {
114
115
  "access": "public"
115
116
  },
116
- "jest": {
117
- "moduleNameMapper": {
118
- "^.+\\.css$": "identity-obj-proxy"
119
- },
120
- "setupFilesAfterEnv": [
121
- "./spec/jest.setup.ts"
122
- ],
123
- "transform": {
124
- "\\.tsx?$": "ts-jest"
125
- },
126
- "testEnvironment": "./spec/test-env.js"
127
- },
128
117
  "resolutions": {
129
118
  "**/ast-types": "npm:@gkz/ast-types",
130
119
  "ts-node": "^10.9.1",
@@ -0,0 +1,14 @@
1
+ import { createContext } from '@chakra-ui/react-utils';
2
+ import { DateTime } from 'luxon';
3
+ import { DateRange } from './useDateRange';
4
+
5
+ interface Context {
6
+ selectable?: DateRange;
7
+ selected?: DateRange;
8
+ today: DateTime;
9
+ preview: 'from' | 'to' | undefined;
10
+ onSelect: (d: DateTime) => void;
11
+ showOutsideMonths: boolean;
12
+ onPreview: (d: DateTime) => void;
13
+ }
14
+ export const [DatePickerContext, useDatePickerContext] = createContext<Context>();
@@ -0,0 +1,16 @@
1
+ import { ComponentMeta } from '@storybook/react';
2
+ import { DateTime } from 'luxon';
3
+ import DatePicker from './DatePicker';
4
+ import { DateRange } from './useDateRange';
5
+
6
+ export default {
7
+ component: DatePicker,
8
+ args: { visible: true, children: <button type="button">ok</button> },
9
+ } as ComponentMeta<typeof DatePicker>;
10
+
11
+ export const Default = {};
12
+ export const Selectable = {
13
+ args: {
14
+ selectable: new DateRange(DateTime.local(2022, 11, 20), DateTime.local(2022, 11, 30)),
15
+ },
16
+ };
@@ -0,0 +1,94 @@
1
+ import { render, screen, waitFor, within } from '@testing-library/react';
2
+ import { DateTime } from 'luxon';
3
+ import userEventGlobal from '@testing-library/user-event';
4
+ import Provider from '../Provider/Provider';
5
+ import DatePicker, { DatePickerProps } from './DatePicker';
6
+ import { DateRange } from './useDateRange';
7
+
8
+ jest.mock('../../hooks/useResponsive', () => jest.fn(() => ({ isMobile: false })));
9
+
10
+ jest.useFakeTimers();
11
+ jest.setSystemTime(DateTime.local(2022, 1, 2).toJSDate());
12
+ const userEvent = userEventGlobal.setup({ advanceTimers: (t) => jest.advanceTimersByTime(t) });
13
+ const Subject = (props: Partial<DatePickerProps>) => (
14
+ <Provider>
15
+ <DatePicker onClose={() => {}} visible {...props}>
16
+ <div />
17
+ </DatePicker>
18
+ </Provider>
19
+ );
20
+ describe('DatePicker', () => {
21
+ it('works', async () => {
22
+ const handler = jest.fn<void, [DateRange]>();
23
+ render(<Subject onApply={handler} />);
24
+ await userEvent.click(await screen.findByRole('button', { name: 'January' }));
25
+ await userEvent.selectOptions(
26
+ await screen.findByRole('listbox', { name: 'month' }),
27
+ await screen.findByRole('option', { name: 'October' }),
28
+ );
29
+ const month = await screen.findByRole('listbox', { name: 'October 2022' });
30
+ await userEvent.selectOptions(month, await within(month).findByRole('option', { name: '23' }));
31
+ await userEvent.click(await screen.findByRole('button', { name: 'November' }));
32
+ const year = await screen.findByRole('spinbutton', { name: 'year' });
33
+ await userEvent.type(year, '{BackSpace}3');
34
+ await userEvent.selectOptions(
35
+ await screen.findByRole('listbox', { name: 'month' }),
36
+ await screen.findByRole('option', { name: 'February' }),
37
+ );
38
+ const month2 = await screen.findByRole('listbox', { name: 'February 2023' });
39
+ await userEvent.selectOptions(month2, await within(month2).findByRole('option', { name: '10' }));
40
+ await userEvent.click(await screen.findByRole('button', { name: 'Apply' }));
41
+ expect(handler).toHaveBeenCalledWith(expect.any(DateRange));
42
+ const [{ from: begin, to: end }] = handler.mock.lastCall!;
43
+ expect(begin! < end!).toBe(true);
44
+ expect(begin?.toObject()).toMatchObject({ year: 2022, month: 10, day: 23 });
45
+ expect(end?.toObject()).toMatchObject({ year: 2023, month: 2, day: 10 });
46
+ });
47
+ describe('with range pre-selected', () => {
48
+ describe('when range start/end is in the same month', () => {
49
+ it('shows the start month on the left and the next month on the right', async () => {
50
+ render(<Subject selected={new DateRange(DateTime.local(2022, 2, 1), DateTime.local(2022, 2, 7))} />);
51
+ const left = await screen.findByRole('listbox', { name: 'February 2022' });
52
+ expect(left).toBeInTheDocument();
53
+ const right = await screen.findByRole('listbox', { name: 'March 2022' });
54
+ expect(right).toBeInTheDocument();
55
+ });
56
+ });
57
+ describe('when range start/end is in consequitive months', () => {
58
+ it('shows the start month on the left and the next month on the right', async () => {
59
+ render(<Subject selected={new DateRange(DateTime.local(2022, 2, 1), DateTime.local(2022, 3, 7))} />);
60
+ const left = await screen.findByRole('listbox', { name: 'February 2022' });
61
+ expect(left).toBeInTheDocument();
62
+ const right = await screen.findByRole('listbox', { name: 'March 2022' });
63
+ expect(right).toBeInTheDocument();
64
+ });
65
+ });
66
+ describe('when range start/end have more than 1 month between them', () => {
67
+ it('shows the start month on the left and the next month on the right', async () => {
68
+ render(<Subject selected={new DateRange(DateTime.local(2022, 2, 1), DateTime.local(2022, 5, 7))} />);
69
+ const left = await screen.findByRole('listbox', { name: 'February 2022' });
70
+ expect(left).toBeInTheDocument();
71
+ const right = await screen.findByRole('listbox', { name: 'March 2022' });
72
+ expect(right).toBeInTheDocument();
73
+ });
74
+ });
75
+ });
76
+ describe('month selector', () => {
77
+ it('shows current month as selected', async () => {
78
+ render(<Subject />);
79
+ const left = await screen.findByRole('button', { name: 'January' });
80
+ await userEvent.click(left);
81
+ const jan = await screen.findByRole('option', { name: 'January' });
82
+ const feb = await screen.findByRole('option', { name: 'February' });
83
+ expect(jan).toHaveAttribute('aria-selected', 'true');
84
+ expect(feb).not.toHaveAttribute('aria-selected');
85
+ });
86
+ it('focuses the year input when clicked', async () => {
87
+ render(<Subject />);
88
+ const left = await screen.findByRole('button', { name: 'January' });
89
+ await userEvent.click(left);
90
+ const year = await screen.findByRole('spinbutton', { name: 'year' });
91
+ await waitFor(() => expect(year).toHaveFocus());
92
+ });
93
+ });
94
+ });
@@ -0,0 +1,178 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import { DateTime } from 'luxon';
3
+ import FocusLock from 'react-focus-lock';
4
+ import { PopoverAnchor } from '@chakra-ui/react';
5
+ import { useObjectMemo } from '../../utils/utils';
6
+ import Popover from '../Popover/Popover';
7
+ import PopoverContent from '../Popover/PopoverContent';
8
+ import Box from '../Box/Box';
9
+ import useResponsive from '../../hooks/useResponsive';
10
+ import DatePickerMonth from './DatePickerMonth';
11
+ import { DatePickerContext } from './DatePicker.context';
12
+ import DatePickerMonthSelector from './DatePickerMonthSelector';
13
+ import useDateRange, { DateRange } from './useDateRange';
14
+ import useViewDate from './useViewDate';
15
+ import DatePickerFooter from './DatePickerFooter';
16
+
17
+ export { useDateRange, DateRange };
18
+
19
+ export interface DatePickerProps {
20
+ children: React.ReactNode;
21
+ selectable?: DateRange;
22
+ selected?: DateRange;
23
+ onApply?: (range: DateRange) => void;
24
+ onClose: () => void;
25
+ visible: boolean;
26
+ }
27
+
28
+ /**
29
+ * A simple date selection component, that supports a dual month view and
30
+ * range selection.
31
+ */
32
+ const DatePicker = (props: DatePickerProps) => {
33
+ const { children, onApply, onClose, visible, selectable, selected } = props;
34
+
35
+ const { isMobile } = useResponsive();
36
+ const today = DateTime.now().startOf('day');
37
+
38
+ const [dateFrom, setDateFrom] = useState(selected?.from);
39
+ useEffect(() => {
40
+ if (!selected?.from || !dateFrom?.equals(selected.from)) {
41
+ setDateFrom(selected?.from);
42
+ }
43
+ }, [selected]);
44
+ const [dateTo, setDateTo] = useState(selected?.to);
45
+ useEffect(() => {
46
+ if (!selected?.to || !dateTo?.equals(selected.to)) {
47
+ setDateTo(selected?.to);
48
+ }
49
+ }, [selected]);
50
+
51
+ const handleClose = () => {
52
+ onClose();
53
+ setDateTo(undefined);
54
+ setDateFrom(undefined);
55
+ };
56
+
57
+ const handleApply = () => {
58
+ if (onApply) {
59
+ onApply(new DateRange(dateFrom, dateTo));
60
+ }
61
+
62
+ handleClose();
63
+ };
64
+
65
+ const { leftViewDate, rightViewDate, updateLeftViewDate, updateRightViewDate } = useViewDate({
66
+ initalView: dateFrom || selectable?.from,
67
+ });
68
+
69
+ const [preview, setPreview] = useState<'from' | 'to' | undefined>(undefined);
70
+ const [isMonthSelector, setIsMonthSelector] = useState<'left' | 'right' | undefined>(undefined);
71
+ const handlePreview = useCallback(
72
+ (date: DateTime) => {
73
+ if (!preview) {
74
+ return;
75
+ }
76
+ if (dateFrom) {
77
+ if (date > dateFrom) {
78
+ setPreview('to');
79
+ } else {
80
+ setPreview('from');
81
+ }
82
+ }
83
+ setDateTo(date);
84
+ },
85
+ [preview, dateFrom, dateTo],
86
+ );
87
+ const handleSelect = useCallback(
88
+ (date: DateTime) => {
89
+ setPreview(undefined);
90
+ if (dateFrom && dateTo) {
91
+ if (!preview) {
92
+ setPreview('from');
93
+ setDateFrom(date);
94
+ setDateTo(undefined);
95
+ }
96
+ } else if (dateTo && date > dateTo) {
97
+ setDateFrom(dateTo);
98
+ setDateTo(date);
99
+ } else if (dateFrom && date < dateFrom) {
100
+ setDateFrom(date);
101
+ setDateTo(dateFrom);
102
+ } else if (dateFrom) {
103
+ setDateTo(date);
104
+ } else {
105
+ setPreview('from');
106
+ setDateFrom(date);
107
+ }
108
+ },
109
+ [dateFrom, dateTo, preview],
110
+ );
111
+ const currentSelected = useDateRange([dateFrom, dateTo]);
112
+ const ctx = useObjectMemo({
113
+ selectable,
114
+ selected: currentSelected,
115
+ preview,
116
+ showOutsideMonths: isMobile,
117
+ today,
118
+ onPreview: handlePreview,
119
+ onSelect: handleSelect,
120
+ });
121
+ const onMonthClickLeft = useCallback(() => setIsMonthSelector('left'), []);
122
+ const onMonthClickRight = useCallback(() => setIsMonthSelector('right'), []);
123
+ const onMonthSelected = useCallback(
124
+ (month: number, year: number) => {
125
+ if (isMonthSelector === 'left') {
126
+ updateLeftViewDate({ month, year });
127
+ }
128
+ if (isMonthSelector === 'right') {
129
+ updateRightViewDate({ month, year });
130
+ }
131
+ setIsMonthSelector(undefined);
132
+ },
133
+ [isMonthSelector],
134
+ );
135
+
136
+ return (
137
+ <Popover modifiers={[]} isOpen={visible} onClose={onClose} isLazy lazyBehavior="unmount">
138
+ <PopoverAnchor>{children}</PopoverAnchor>
139
+ <PopoverContent aria-label="select date range">
140
+ <FocusLock returnFocus>
141
+ <DatePickerContext value={ctx}>
142
+ <Box padding="24">
143
+ {isMonthSelector ? (
144
+ <DatePickerMonthSelector
145
+ onMonthSelected={onMonthSelected}
146
+ viewDate={isMonthSelector === 'left' ? leftViewDate : rightViewDate}
147
+ />
148
+ ) : (
149
+ <>
150
+ <Box display="flex" gap="32" marginBottom="24">
151
+ <DatePickerMonth
152
+ onMonthClick={onMonthClickLeft}
153
+ onViewDateChange={updateLeftViewDate}
154
+ controls={isMobile ? 'both' : 'left'}
155
+ viewDate={leftViewDate}
156
+ />
157
+
158
+ {!isMobile && (
159
+ <DatePickerMonth
160
+ onMonthClick={onMonthClickRight}
161
+ onViewDateChange={updateRightViewDate}
162
+ controls="right"
163
+ viewDate={rightViewDate}
164
+ />
165
+ )}
166
+ </Box>
167
+ <DatePickerFooter onApply={handleApply} onClose={handleClose} selected={selected} />
168
+ </>
169
+ )}
170
+ </Box>
171
+ </DatePickerContext>
172
+ </FocusLock>
173
+ </PopoverContent>
174
+ </Popover>
175
+ );
176
+ };
177
+
178
+ export default DatePicker;
@@ -0,0 +1,108 @@
1
+ import type { DatePickerDayViewProps } from './DatePickerDay';
2
+
3
+ const borderRadii: Record<string, string> = {
4
+ from: '0.5rem 0 0 0.5rem',
5
+ to: '0 0.5rem 0.5rem 0',
6
+ incomplete: '0.5rem',
7
+ };
8
+ function selectionBorder({ selection }: DatePickerDayViewProps) {
9
+ if (!selection || selection === 'between') {
10
+ return {};
11
+ }
12
+ return { borderRadius: borderRadii[selection] };
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
+ };
22
+ }
23
+ if (selection === 'between') {
24
+ return {
25
+ bg: 'purple.95',
26
+ color: currentMonth ? 'purple.10' : 'neutral.70',
27
+ };
28
+ }
29
+
30
+ if (endpoint) {
31
+ if (beingMoved) {
32
+ return {
33
+ bg: currentMonth ? 'purple.90' : 'purple.93',
34
+ color: currentMonth ? 'purple.10' : 'neutral.70',
35
+ };
36
+ }
37
+ return {
38
+ bg: currentMonth ? 'purple.50' : 'purple.70',
39
+ color: currentMonth ? 'neutral.100' : 'purple.95',
40
+ };
41
+ }
42
+ return {};
43
+ }
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',
57
+ },
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',
68
+ },
69
+ };
70
+ }
71
+ return { backgroundColor: 'purple.90' };
72
+ }
73
+ const DatePickerDay = {
74
+ parts: ['day', 'selection'],
75
+ baseStyle(props: DatePickerDayViewProps) {
76
+ const { today, selectable, currentMonth, showOutsideDays, preview, selection } = props;
77
+ return {
78
+ text: {
79
+ border: today ? '1.5px solid' : undefined,
80
+ borderColor: today ? 'purple.50' : undefined,
81
+ width: '40',
82
+ height: '32',
83
+ borderRadius: '8',
84
+ display: 'flex',
85
+ alignItems: 'center',
86
+ justifyContent: 'center',
87
+ '&:hover, button:focus-visible > &': selectable
88
+ ? {
89
+ outline: 'none',
90
+ boxShadow: 'none',
91
+ backgroundColor: selection ? undefined : 'purple.90',
92
+ }
93
+ : undefined,
94
+ },
95
+ day: {
96
+ color: !selectable || (!currentMonth && showOutsideDays) ? 'neutral.70' : 'purple.10',
97
+ _focusVisible: {
98
+ outline: 'none',
99
+ boxShadow: 'none',
100
+ },
101
+ cursor: !selectable ? 'default' : undefined,
102
+ ...(preview ? previewStyles(props) : selectionStyles(props)),
103
+ ...selectionBorder(props),
104
+ },
105
+ };
106
+ },
107
+ };
108
+ export default DatePickerDay;
@@ -0,0 +1,137 @@
1
+ import { ReactNode, useCallback } from 'react';
2
+ import { DateTime } from 'luxon';
3
+ import { BoxProps, useMultiStyleConfig } from '@chakra-ui/react';
4
+ import { createContext } from '@chakra-ui/react-utils';
5
+ import Box from '../Box/Box';
6
+ import Text from '../Text/Text';
7
+ import { useDatePickerContext } from './DatePicker.context';
8
+
9
+ interface Context {
10
+ onPreviousMonth: () => void;
11
+ onNextMonth: () => void;
12
+ viewDate: DateTime;
13
+ }
14
+ export interface DatePickerDayViewProps {
15
+ onClick: () => void;
16
+ onMouseEnter: () => void;
17
+ selectable: boolean;
18
+ today: boolean;
19
+ currentMonth: boolean;
20
+ showOutsideDays: boolean;
21
+ preview: 'from' | 'to' | undefined;
22
+ selection?: 'from' | 'to' | 'between' | 'incomplete';
23
+ children: ReactNode;
24
+ }
25
+ const DatePickerDayView = ({
26
+ onClick,
27
+ onMouseEnter,
28
+ selectable,
29
+ selection,
30
+ showOutsideDays,
31
+ currentMonth,
32
+ today,
33
+ preview,
34
+ children,
35
+ }: DatePickerDayViewProps) => {
36
+ const { day, text } = useMultiStyleConfig('DatePickerDay', {
37
+ today,
38
+ selectable,
39
+ selection,
40
+ preview,
41
+ showOutsideDays,
42
+ currentMonth,
43
+ });
44
+ const ariaProps: BoxProps = {};
45
+ if (currentMonth) {
46
+ ariaProps.as = 'button';
47
+ ariaProps.role = 'option';
48
+ }
49
+ return (
50
+ <Box {...ariaProps} sx={day} onMouseEnter={onMouseEnter} onFocus={onMouseEnter} onClick={onClick}>
51
+ <Text sx={text}>{children}</Text>
52
+ </Box>
53
+ );
54
+ };
55
+ const [DatePickerDayContext, useDatePickerDayContext] = createContext<Context>();
56
+ export { DatePickerDayContext };
57
+
58
+ const DatePickerDay = ({ n }: { n: number }): JSX.Element => {
59
+ const {
60
+ preview,
61
+ selectable,
62
+ selected,
63
+ onPreview,
64
+ showOutsideMonths: showOutsideDays,
65
+ onSelect,
66
+ today,
67
+ } = useDatePickerContext();
68
+ const { viewDate, onPreviousMonth, onNextMonth } = useDatePickerDayContext();
69
+ const { from: dateSelectableFrom, to: dateSelectableTo } = selectable || {};
70
+ const { from: dateSelectedFrom, to: dateSelectedTo } = selected || {};
71
+
72
+ const date = viewDate.plus({ days: n - viewDate.weekday });
73
+ const daysInPreviousMonth = viewDate.minus({ month: 1 }).daysInMonth;
74
+
75
+ const dayOfWeek = viewDate.weekday;
76
+ const { daysInMonth } = viewDate;
77
+
78
+ const isPreviousMonth = n < dayOfWeek;
79
+ const isNextMonth = n - dayOfWeek >= daysInMonth;
80
+ const isCurrentMonth = !isPreviousMonth && !isNextMonth;
81
+ const isAfterSelectableFromDate = !dateSelectableFrom || date >= dateSelectableFrom;
82
+ const isBeforeSelectableToDate = !dateSelectableTo || date <= dateSelectableTo;
83
+ const isSelectable = isAfterSelectableFromDate && isBeforeSelectableToDate;
84
+
85
+ const hasSelectionRange = dateSelectedFrom && dateSelectedTo && !dateSelectedFrom.equals(dateSelectedTo);
86
+
87
+ let selection: 'from' | 'to' | 'between' | 'incomplete' | undefined;
88
+ const interactive = isCurrentMonth || showOutsideDays;
89
+ if (hasSelectionRange && interactive && date > dateSelectedFrom && date < dateSelectedTo) {
90
+ selection = 'between';
91
+ }
92
+ if (interactive && dateSelectedFrom && date.equals(dateSelectedFrom)) {
93
+ selection = hasSelectionRange ? 'from' : 'incomplete';
94
+ }
95
+ if (interactive && dateSelectedTo && date.equals(dateSelectedTo)) {
96
+ selection = hasSelectionRange ? 'to' : 'incomplete';
97
+ }
98
+
99
+ const onClick = useCallback(() => {
100
+ if (!isSelectable) {
101
+ return;
102
+ }
103
+ onSelect(date);
104
+ if (isPreviousMonth) {
105
+ onPreviousMonth();
106
+ } else if (isNextMonth) {
107
+ onNextMonth();
108
+ }
109
+ }, [isPreviousMonth, isSelectable, onSelect, date, onPreviousMonth, onNextMonth]);
110
+ const onMouseEnter = useCallback(() => {
111
+ if (isSelectable) {
112
+ onPreview(date);
113
+ }
114
+ }, [onSelect, isSelectable, date]);
115
+
116
+ if (!isCurrentMonth && !showOutsideDays) {
117
+ return <div />;
118
+ }
119
+ return (
120
+ <DatePickerDayView
121
+ today={today.equals(viewDate.set({ day: n - dayOfWeek + 1 }).startOf('day'))}
122
+ preview={preview}
123
+ onClick={onClick}
124
+ onMouseEnter={onMouseEnter}
125
+ selectable={isSelectable}
126
+ currentMonth={isCurrentMonth}
127
+ showOutsideDays={showOutsideDays}
128
+ selection={selection}
129
+ >
130
+ {isPreviousMonth && daysInPreviousMonth - (dayOfWeek - n - 1)}
131
+ {isCurrentMonth && n - dayOfWeek + 1}
132
+ {isNextMonth && n - dayOfWeek - daysInMonth + 1}
133
+ </DatePickerDayView>
134
+ );
135
+ };
136
+
137
+ export default DatePickerDay;
@@ -0,0 +1,41 @@
1
+ import Box from '../Box/Box';
2
+ import Text from '../Text/Text';
3
+ import Button from '../Button/Button';
4
+ import ButtonGroup from '../ButtonGroup/ButtonGroup';
5
+ import { DateRange } from './useDateRange';
6
+
7
+ const DatePickerFooter = ({
8
+ selected,
9
+ onClose,
10
+ onApply,
11
+ }: {
12
+ selected?: DateRange;
13
+ onClose: () => void;
14
+ onApply: () => void;
15
+ }) => {
16
+ return (
17
+ <Box
18
+ display="grid"
19
+ gridTemplateColumns={['unset', '1fr auto 1fr']}
20
+ gridTemplateRows={['1.25rem 2rem', 'unset']}
21
+ gap="24"
22
+ >
23
+ <Text alignSelf="center" justifySelf="center" size="2" color="text.secondary" gridColumn={['1', '2']}>
24
+ {selected?.from?.toFormat('DD', { locale: 'en-US' })}
25
+ {selected?.from || selected?.to ? ' - ' : undefined}
26
+ {selected?.to?.toFormat('DD', { locale: 'en-US' })}
27
+ </Text>
28
+ <ButtonGroup gridColumn={['1', '3']} justifyContent="end">
29
+ <Button variant="secondary" onClick={onClose} size="small">
30
+ Cancel
31
+ </Button>
32
+
33
+ <Button onClick={onApply} size="small">
34
+ Apply
35
+ </Button>
36
+ </ButtonGroup>
37
+ </Box>
38
+ );
39
+ };
40
+
41
+ export default DatePickerFooter;
@@ -0,0 +1,10 @@
1
+ import * as React from 'react';
2
+ import Box, { BoxProps } from '../Box/Box';
3
+
4
+ type Props = BoxProps;
5
+
6
+ const DatePickerGrid: React.FunctionComponent<Props> = (props: Props) => {
7
+ return <Box display="grid" {...props} gridTemplateColumns="repeat(7, 2.5rem)" gridAutoRows="2rem" gridRowGap="4" />;
8
+ };
9
+
10
+ export default DatePickerGrid;