Skip to content

Commit b96d862

Browse files
authored
feat(calendars): add MonthCalendar and MultiMonthCalendar components (#129)
* initial * reduce api surface * reoptimize * multimonth calendar * optimize * refactor * add issue link * fix * add back example * remove fast memoize
1 parent 8ccaf7b commit b96d862

19 files changed

Lines changed: 45723 additions & 18 deletions

jest.config.js

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,8 @@
1-
const { defaults } = require('jest-config');
2-
31
module.exports = {
4-
preset: './tests/presets.js',
5-
transform: {
6-
'^.+\\.jsx?$': 'babel-jest',
7-
'^.+\\.mdx?$': '<rootDir>/tests/mdxTransformer.js',
8-
},
9-
moduleNameMapper: {
10-
'^docz$': '<rootDir>/tests/doczMock.js',
11-
},
12-
moduleFileExtensions: [
13-
...defaults.moduleFileExtensions,
14-
'web.ts',
15-
'ts',
16-
'web.tsx',
17-
'tsx',
2+
preset: 'ts-jest',
3+
testPathIgnorePatterns: [
4+
'/node_modules/',
5+
'<rootDir>/tests',
6+
'<rootDir>/dist',
187
],
19-
setupFiles: ['jest-canvas-mock'],
208
};

jest.config.snapshot.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const { defaults } = require('jest-config');
2+
3+
module.exports = {
4+
preset: './tests/presets.js',
5+
transform: {
6+
'^.+\\.jsx?$': 'babel-jest',
7+
'^.+\\.mdx?$': '<rootDir>/tests/mdxTransformer.js',
8+
},
9+
moduleNameMapper: {
10+
'^docz$': '<rootDir>/tests/doczMock.js',
11+
},
12+
moduleFileExtensions: [
13+
...defaults.moduleFileExtensions,
14+
'web.ts',
15+
'ts',
16+
'web.tsx',
17+
'tsx',
18+
],
19+
testRegex: './tests/snapshot.test.js',
20+
setupFiles: ['jest-canvas-mock'],
21+
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"fix:lint": "tslint --project ./tsconfig.json \"./src/**/*.{ts,tsx}\"",
2828
"fix:prettier": "prettier \"./src/**/*.{ts,tsx}\" --write",
2929
"semantic-release": "semantic-release",
30-
"test": "jest",
30+
"test": "jest -c jest.config.snapshot.js",
3131
"travis-deploy-once": "travis-deploy-once"
3232
},
3333
"contributors": [
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { format } from 'date-fns';
2+
import * as React from 'react';
3+
4+
import { ThemeContext } from '../../../theme';
5+
import { Box } from '../../Layout';
6+
import { Month } from '../types';
7+
import { DATE_FORMAT } from './constants';
8+
import MonthDay, { MonthDayBaseProps } from './MonthDay';
9+
10+
export interface MonthBodyProps extends MonthDayBaseProps {
11+
month: Month;
12+
}
13+
14+
const MonthBody = (props: MonthBodyProps) => {
15+
const { onSelect, month } = props;
16+
17+
const theme = React.useContext(ThemeContext);
18+
19+
return (
20+
<Box>
21+
{month.weeks.map(week => (
22+
<Box flexDirection="row" key={week.index}>
23+
{week.days.map(day => {
24+
const {
25+
date,
26+
isCurrentMonth,
27+
isSelected,
28+
isSelectedStart,
29+
isSelectedEnd,
30+
} = day;
31+
32+
if (!isCurrentMonth) {
33+
return (
34+
<Box
35+
flex={1}
36+
justifyContent="center"
37+
alignItems="flex-start"
38+
key={date.toISOString()}
39+
paddingVertical={4}
40+
zIndex={-1}
41+
>
42+
<Box
43+
backgroundColor={
44+
isSelected
45+
? theme.colors.background.primary.focus
46+
: 'transparent'
47+
}
48+
flex={1}
49+
height={40}
50+
width="100%"
51+
/>
52+
</Box>
53+
);
54+
}
55+
56+
return (
57+
<Box
58+
flex={1}
59+
justifyContent="center"
60+
alignItems="flex-start"
61+
key={format(date, DATE_FORMAT)}
62+
>
63+
<MonthDay
64+
onSelect={onSelect}
65+
date={date}
66+
isSelected={isSelected}
67+
isSelectionStart={isSelectedStart}
68+
isSelectionEnd={isSelectedEnd}
69+
/>
70+
</Box>
71+
);
72+
})}
73+
</Box>
74+
))}
75+
</Box>
76+
);
77+
};
78+
79+
const propsAreEqual = (
80+
prevProps: MonthBodyProps,
81+
nextProps: MonthBodyProps,
82+
) => {
83+
return (
84+
prevProps.month.selectedRange === nextProps.month.selectedRange &&
85+
prevProps.onSelect === nextProps.onSelect
86+
);
87+
};
88+
89+
export default React.memo(MonthBody, propsAreEqual);
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
---
2+
name: Calendars
3+
menu: Components
4+
---
5+
6+
import {
7+
format,
8+
addDays,
9+
subDays,
10+
addMonths,
11+
subMonths,
12+
isSameDay,
13+
isBefore,
14+
isAfter,
15+
} from 'date-fns';
16+
import { Playground, PropsTable } from 'docz';
17+
import { State } from 'react-powerplug';
18+
19+
import { Box, Spacing } from '../../Layout';
20+
import { Heading } from '../../Typography';
21+
import MonthCalendar from './MonthCalendar';
22+
import MultiMonthCalendar from './MultiMonthCalendar';
23+
import WeekDays from './WeekDays';
24+
25+
## MonthCalendar
26+
27+
<Playground>
28+
<Spacing paddingBottom={4}>
29+
<Heading size="xxxlarge">{format(new Date(), 'MMMM YYYY')}</Heading>
30+
</Spacing>
31+
<WeekDays />
32+
<State
33+
initial={{
34+
selectedStartDate: null,
35+
selectedEndDate: null,
36+
}}
37+
>
38+
{({ state: { selectedStartDate, selectedEndDate }, setState }) => (
39+
<MonthCalendar
40+
selectedStartDate={selectedStartDate}
41+
selectedEndDate={selectedEndDate}
42+
// MUST SEE https://github.com/facebook/react/issues/14972
43+
onSelect={date => {
44+
if (!selectedStartDate && !selectedEndDate) {
45+
setState({
46+
selectedStartDate: date,
47+
selectedEndDate: null,
48+
});
49+
} else if (selectedStartDate && !selectedEndDate) {
50+
if (isBefore(date, selectedStartDate)) {
51+
setState({
52+
selectedStartDate: date,
53+
selectedEndDate: null,
54+
});
55+
} else if (!isSameDay(selectedStartDate, date)) {
56+
setState({
57+
selectedEndDate: date,
58+
});
59+
}
60+
} else {
61+
setState({
62+
selectedStartDate: date,
63+
selectedEndDate: null,
64+
});
65+
}
66+
}}
67+
/>
68+
)}
69+
</State>
70+
</Playground>
71+
72+
## MultiMonthCalendar
73+
74+
Check out this issue for idiomatic use of MultiMonthCalendar event handler https://github.com/facebook/react/issues/14972
75+
Also applies to `MonthCalendar`
76+
77+
<Playground>
78+
<State
79+
initial={{
80+
selectedStartDate: null,
81+
selectedEndDate: null,
82+
}}
83+
>
84+
{({ state: { selectedStartDate, selectedEndDate }, setState }) => (
85+
<MultiMonthCalendar
86+
selectedStartDate={selectedStartDate}
87+
selectedEndDate={selectedEndDate}
88+
startMonthDate={subMonths(new Date(), 2)}
89+
endMonthDate={addMonths(new Date(), 2)}
90+
// MUST SEE https://github.com/facebook/react/issues/14972
91+
onSelect={date => {
92+
if (!selectedStartDate && !selectedEndDate) {
93+
setState({
94+
selectedStartDate: date,
95+
selectedEndDate: null,
96+
});
97+
} else if (selectedStartDate && !selectedEndDate) {
98+
if (isBefore(date, selectedStartDate)) {
99+
setState({
100+
selectedStartDate: date,
101+
selectedEndDate: null,
102+
});
103+
} else if (isAfter(date, selectedStartDate)) {
104+
setState({
105+
selectedEndDate: date,
106+
});
107+
}
108+
} else {
109+
setState({
110+
selectedStartDate: date,
111+
selectedEndDate: null,
112+
});
113+
}
114+
}}
115+
/>
116+
)}
117+
</State>
118+
</Playground>
119+
120+
### Props
121+
122+
<PropsTable of={MonthCalendar} />
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as React from 'react';
2+
3+
import { Box } from '../../Layout';
4+
import { DEFAULT_FIRST_DAY_OF_WEEK } from '../constants';
5+
import { marshalTimeRange } from '../utils';
6+
import MonthBody from './MonthBody';
7+
import { MonthDayBaseProps } from './MonthDay';
8+
import { getWeeksInMonth } from './utils/getWeeksInMonth';
9+
10+
export interface MonthCalendarProps extends MonthDayBaseProps {
11+
/** Date to which display its month for. @default Date */
12+
date?: Date;
13+
/** Highlights the date or start date on the calendar */
14+
selectedStartDate?: Date | null;
15+
/** Highlights the end date on the calendar. Will created a selected range */
16+
selectedEndDate?: Date | null;
17+
firstDayOfWeekIndex?: number;
18+
}
19+
20+
const MonthCalendar = (props: MonthCalendarProps) => {
21+
const {
22+
date = new Date(),
23+
firstDayOfWeekIndex = DEFAULT_FIRST_DAY_OF_WEEK,
24+
selectedStartDate: propSelectedStartDate = null,
25+
selectedEndDate: propSelectedEndDate = null,
26+
onSelect,
27+
} = props;
28+
29+
const [selectedStartDate, selectedEndDate] = marshalTimeRange(
30+
propSelectedStartDate,
31+
propSelectedEndDate,
32+
);
33+
34+
const month = getWeeksInMonth(
35+
date,
36+
selectedStartDate,
37+
selectedEndDate,
38+
firstDayOfWeekIndex,
39+
);
40+
41+
return (
42+
<Box flex={1} width="100%">
43+
<MonthBody onSelect={onSelect} month={month} />
44+
</Box>
45+
);
46+
};
47+
48+
export default MonthCalendar;

0 commit comments

Comments
 (0)