Skip to content

Commit 3958f3c

Browse files
Elena Rashkovanlena.rashkovan
andauthored
feat(calendar): support multi-month and range selection (#545)
Co-authored-by: lena.rashkovan <[email protected]>
1 parent 0dcd87f commit 3958f3c

File tree

4 files changed

+214
-118
lines changed

4 files changed

+214
-118
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import styled from 'styled-components';
2+
import {
3+
Button as BaseButton,
4+
CalendarCell,
5+
CalendarGrid as BaseCalendarGrid,
6+
CalendarHeaderCell,
7+
Heading as BaseHeading
8+
} from 'react-aria-components';
9+
import { get } from '../../../utils/experimental/themeGet';
10+
import { getSemanticValue } from '../../../essentials/experimental';
11+
12+
export const Header = styled.header`
13+
display: flex;
14+
align-items: center;
15+
justify-content: space-between;
16+
padding-bottom: ${get('space.3')};
17+
`;
18+
19+
export const Button = styled(BaseButton)`
20+
appearance: none;
21+
background: none;
22+
border: none;
23+
display: flex;
24+
cursor: pointer;
25+
margin: 0;
26+
padding: 0;
27+
color: ${getSemanticValue('on-surface')};
28+
outline: 0;
29+
30+
&[data-focused] {
31+
outline: ${getSemanticValue('interactive')} solid 0.125rem;
32+
border-radius: ${get('radii.2')};
33+
}
34+
35+
&[data-disabled] {
36+
opacity: 0;
37+
}
38+
`;
39+
40+
export const Heading = styled(BaseHeading)`
41+
margin: 0;
42+
color: ${getSemanticValue('on-surface')};
43+
font-size: var(--wave-exp-typescale-title-2-size);
44+
font-weight: var(--wave-exp-typescale-title-2-weight);
45+
line-height: var(--wave-exp-typescale-title-2-line-height);
46+
`;
47+
48+
export const CalendarGrid = styled(BaseCalendarGrid)`
49+
border-collapse: separate;
50+
border-spacing: 0 0.125rem;
51+
52+
td {
53+
padding: 0;
54+
}
55+
56+
th {
57+
padding: 0 0 ${get('space.1')};
58+
}
59+
`;
60+
61+
export const WeekDay = styled(CalendarHeaderCell)`
62+
color: ${getSemanticValue('on-surface')};
63+
font-size: var(--wave-exp-typescale-label-2-size);
64+
font-weight: var(--wave-exp-typescale-label-2-weight);
65+
line-height: var(--wave-exp-typescale-label-2-line-height);
66+
`;
67+
68+
export const MonthGrid = styled.div`
69+
display: flex;
70+
gap: 1.5rem;
71+
`;
72+
73+
export const Day = styled(CalendarCell)`
74+
position: relative;
75+
display: flex;
76+
align-items: center;
77+
justify-content: center;
78+
color: ${getSemanticValue('on-surface')};
79+
width: 2.5rem;
80+
height: 2.5rem;
81+
border-radius: 50%;
82+
outline: 0;
83+
font-size: var(--wave-exp-typescale-label-2-size);
84+
font-weight: var(--wave-exp-typescale-label-2-weight);
85+
line-height: var(--wave-exp-typescale-label-2-line-height);
86+
transition: background ease 200ms;
87+
88+
&::after {
89+
content: '';
90+
position: absolute;
91+
inset: 0;
92+
border-radius: 50%;
93+
}
94+
95+
&[data-focused]::after {
96+
z-index: 1;
97+
outline: ${getSemanticValue('interactive')} solid 0.125rem;
98+
}
99+
100+
&[data-hovered] {
101+
cursor: pointer;
102+
background: ${getSemanticValue('surface-variant')};
103+
}
104+
105+
&[data-selected] {
106+
background: ${getSemanticValue('interactive-container')};
107+
color: ${getSemanticValue('on-interactive-container')};
108+
}
109+
110+
&[data-disabled] {
111+
opacity: 0.38;
112+
}
113+
114+
&[data-outside-month] {
115+
opacity: 0;
116+
}
117+
118+
[data-selection-type='range'] &[data-selected] {
119+
border-radius: 0;
120+
}
121+
122+
&[data-selection-start][data-selected] {
123+
border-start-start-radius: 50%;
124+
border-end-start-radius: 50%;
125+
}
126+
127+
&[data-selection-end][data-selected] {
128+
border-start-end-radius: 50%;
129+
border-end-end-radius: 50%;
130+
}
131+
`;

src/components/experimental/Calendar/Calendar.tsx

Lines changed: 66 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -2,133 +2,83 @@ import React, { ReactElement } from 'react';
22
import {
33
Calendar as BaseCalendar,
44
CalendarProps as BaseCalendarProps,
5-
CalendarCell,
6-
CalendarGrid as BaseCalendarGrid,
5+
RangeCalendarProps,
76
CalendarGridHeader,
87
CalendarGridBody,
9-
CalendarHeaderCell,
10-
Heading as BaseHeading,
118
DateValue,
12-
Button as BaseButton
9+
RangeCalendar
1310
} from 'react-aria-components';
14-
import styled from 'styled-components';
1511
import ChevronLeftIcon from '../../../icons/arrows/ChevronLeftIcon';
1612
import ChevronRightIcon from '../../../icons/arrows/ChevronRightIcon';
17-
import { getSemanticValue } from '../../../essentials/experimental';
18-
import { textStyles } from '../Text/Text';
19-
import { get } from '../../../utils/experimental/themeGet';
2013

21-
const Header = styled.header`
22-
display: flex;
23-
align-items: center;
24-
justify-content: space-between;
25-
padding-bottom: ${get('space.3')};
26-
`;
27-
28-
const Button = styled(BaseButton)`
29-
appearance: none;
30-
background: none;
31-
border: none;
32-
display: flex;
33-
cursor: pointer;
34-
margin: 0;
35-
padding: 0;
36-
color: ${getSemanticValue('on-surface')};
37-
outline: 0;
38-
39-
&[data-focused] {
40-
outline: ${getSemanticValue('interactive')} solid 0.125rem;
41-
border-radius: ${get('radii.2')};
42-
}
43-
44-
&[data-disabled] {
45-
opacity: 0;
46-
}
47-
`;
48-
49-
const Heading = styled(BaseHeading)`
50-
margin: 0;
51-
color: ${getSemanticValue('on-surface')};
52-
${textStyles.variants.title2}
53-
`;
54-
55-
const CalendarGrid = styled(BaseCalendarGrid)`
56-
border-collapse: collapse;
57-
border-spacing: 0;
58-
59-
td {
60-
padding: 0;
61-
}
62-
63-
th {
64-
padding: 0 0 ${get('space.1')};
65-
}
66-
`;
67-
68-
const WeekDay = styled(CalendarHeaderCell)`
69-
color: ${getSemanticValue('on-surface')};
70-
${textStyles.variants.label2}
71-
`;
72-
73-
const Day = styled(CalendarCell)`
74-
display: flex;
75-
align-items: center;
76-
justify-content: center;
77-
color: ${getSemanticValue('on-surface')};
78-
width: 2.5rem;
79-
height: 2.5rem;
80-
border-radius: 50%;
81-
${textStyles.variants.label2}
82-
transition: background ease 200ms;
83-
84-
&[data-focused] {
85-
outline: ${getSemanticValue('interactive')} solid 0.125rem;
86-
}
87-
88-
&[data-hovered] {
89-
cursor: pointer;
90-
background: ${getSemanticValue('surface-variant')};
91-
}
92-
93-
&[data-selected] {
94-
background: ${getSemanticValue('interactive-container')};
95-
color: ${getSemanticValue('on-interactive-container')};
96-
}
97-
98-
&[data-disabled] {
99-
opacity: 0.38;
100-
}
14+
import * as Styled from './Calendar.styled';
15+
16+
type CalendarProps = { visibleMonths?: 1 | 2 | 3 } & (
17+
| ({ selectionType?: 'single' } & Omit<BaseCalendarProps<DateValue>, 'visibleDuration'>)
18+
| ({ selectionType: 'range' } & Omit<RangeCalendarProps<DateValue>, 'visibleDuration'>)
19+
);
20+
21+
function Calendar({
22+
value,
23+
minValue,
24+
defaultValue,
25+
maxValue,
26+
onChange,
27+
selectionType = 'single',
28+
visibleMonths = 1,
29+
...props
30+
}: CalendarProps): ReactElement {
31+
const calendarInner = (
32+
<>
33+
<Styled.Header>
34+
<Styled.Button slot="previous">
35+
<ChevronLeftIcon size={24} />
36+
</Styled.Button>
37+
<Styled.Heading />
38+
<Styled.Button slot="next">
39+
<ChevronRightIcon size={24} />
40+
</Styled.Button>
41+
</Styled.Header>
42+
<Styled.MonthGrid>
43+
{Array.from({ length: visibleMonths }).map((_, index) => (
44+
// eslint-disable-next-line react/no-array-index-key
45+
<Styled.CalendarGrid weekdayStyle="short" key={`month_${index}`} offset={{ months: index }}>
46+
<CalendarGridHeader>{weekDay => <Styled.WeekDay>{weekDay}</Styled.WeekDay>}</CalendarGridHeader>
47+
<CalendarGridBody>
48+
{date => (
49+
<Styled.Day date={date}>
50+
{({ formattedDate }) =>
51+
formattedDate.length > 1 ? formattedDate : `0${formattedDate}`
52+
}
53+
</Styled.Day>
54+
)}
55+
</CalendarGridBody>
56+
</Styled.CalendarGrid>
57+
))}
58+
</Styled.MonthGrid>
59+
</>
60+
);
10161

102-
&[data-outside-month] {
103-
opacity: 0;
62+
if (selectionType === 'single') {
63+
return (
64+
<BaseCalendar
65+
{...(props as BaseCalendarProps<DateValue>)}
66+
visibleDuration={{ months: visibleMonths }}
67+
data-selection-type="single"
68+
>
69+
{calendarInner}
70+
</BaseCalendar>
71+
);
10472
}
105-
`;
10673

107-
type CalendarProps = BaseCalendarProps<DateValue>;
108-
109-
function Calendar({ value, minValue, defaultValue, maxValue, onChange, ...props }: CalendarProps): ReactElement {
11074
return (
111-
<BaseCalendar {...props}>
112-
<Header>
113-
<Button slot="previous">
114-
<ChevronLeftIcon size={24} />
115-
</Button>
116-
<Heading />
117-
<Button slot="next">
118-
<ChevronRightIcon size={24} />
119-
</Button>
120-
</Header>
121-
<CalendarGrid weekdayStyle="short">
122-
<CalendarGridHeader>{weekDay => <WeekDay>{weekDay}</WeekDay>}</CalendarGridHeader>
123-
<CalendarGridBody>
124-
{date => (
125-
<Day date={date}>
126-
{({ formattedDate }) => (formattedDate.length > 1 ? formattedDate : `0${formattedDate}`)}
127-
</Day>
128-
)}
129-
</CalendarGridBody>
130-
</CalendarGrid>
131-
</BaseCalendar>
75+
<RangeCalendar
76+
{...(props as RangeCalendarProps<DateValue>)}
77+
visibleDuration={{ months: visibleMonths }}
78+
data-selection-type="range"
79+
>
80+
{calendarInner}
81+
</RangeCalendar>
13282
);
13383
}
13484

src/components/experimental/Calendar/docs/Calendar.stories.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,15 @@ export const WithMinValue: Story = {
2727
minValue: TODAY
2828
}
2929
};
30+
31+
export const MultiMonth: Story = {
32+
args: {
33+
visibleMonths: 2
34+
}
35+
};
36+
37+
export const RangeSelection: Story = {
38+
args: {
39+
selectionType: 'range'
40+
}
41+
};

src/essentials/experimental/globalStyles.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,14 @@ export const createThemeGlobalStyle = (
3737
${semanticCssVariablesForLightTheme}
3838
${generateCssVariables(
3939
{
40+
'title-2-size': '1rem',
41+
'title-2-weight': 500,
42+
'title-2-line-height': '1.5rem',
4043
'body-1-size': '1rem',
41-
'body-1-weight': 'normal',
44+
'body-1-weight': 400,
4245
'body-1-line-height': '1.5rem',
4346
'label-2-size': '0.875rem',
44-
'label-2-weight': 'normal',
47+
'label-2-weight': 400,
4548
'label-2-line-height': '1.25rem'
4649
},
4750
'typescale'

0 commit comments

Comments
 (0)