Skip to content

Commit 1b76a34

Browse files
committed
refactor: migrate calendar from react-aria to react-day-picker
1 parent a8e6941 commit 1b76a34

File tree

3 files changed

+235
-150
lines changed

3 files changed

+235
-150
lines changed
Lines changed: 168 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,131 +1,223 @@
11
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';
92
import { get } from '../../../utils/experimental/themeGet';
103
import { getSemanticValue } from '../../../essentials/experimental';
114

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-
`;
5+
// Root container that scopes all DayPicker styles
6+
export const Container = styled.div`
7+
/* Define react-day-picker CSS custom properties */
8+
--rdp-accent-color: ${getSemanticValue('interactive')};
9+
--rdp-accent-background-color: ${getSemanticValue('interactive-container')};
10+
--rdp-animation_duration: 0.2s;
11+
--rdp-animation_timing: ease;
12+
--rdp-day-height: 2.5rem;
13+
--rdp-day-width: 2.5rem;
14+
--rdp-day_button-border-radius: 50%;
15+
--rdp-day_button-border: none;
16+
--rdp-day_button-height: 2.5rem;
17+
--rdp-day_button-width: 2.5rem;
18+
--rdp-selected-border: none;
19+
--rdp-disabled-opacity: 0.38;
20+
--rdp-outside-opacity: 0;
21+
--rdp-today-color: ${getSemanticValue('on-surface')};
22+
--rdp-months-gap: 1.5rem;
23+
--rdp-nav_button-disabled-opacity: 0;
24+
--rdp-nav_button-height: 2.5rem;
25+
--rdp-nav_button-width: 2.5rem;
26+
--rdp-nav-height: 2.5rem;
27+
--rdp-range_middle-background-color: ${getSemanticValue('interactive-container')};
28+
--rdp-range_middle-color: ${getSemanticValue('on-interactive-container')};
29+
--rdp-range_start-color: ${getSemanticValue('on-interactive-container')};
30+
--rdp-range_start-background: ${getSemanticValue('interactive-container')};
31+
--rdp-range_end-background: ${getSemanticValue('interactive-container')};
32+
--rdp-range_end-color: ${getSemanticValue('on-interactive-container')};
33+
--rdp-weekday-opacity: 1;
34+
--rdp-weekday-padding: 0 0 ${get('space.1')};
35+
--rdp-weekday-text-align: center;
1836
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;
2737
color: ${getSemanticValue('on-surface')};
28-
outline: 0;
2938
30-
&[data-focused] {
31-
outline: ${getSemanticValue('interactive')} solid 0.125rem;
32-
border-radius: ${get('radii.2')};
39+
.rdp {
40+
width: fit-content;
3341
}
3442
35-
&[data-disabled] {
36-
opacity: 0;
43+
/* Layout for multiple months */
44+
.rdp-months {
45+
display: flex;
46+
flex-direction: row;
47+
gap: var(--rdp-months-gap);
48+
position: relative;
3749
}
38-
`;
3950
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-
`;
51+
.rdp-month {
52+
display: flex;
53+
flex-direction: column;
54+
gap: ${get('space.3')};
55+
}
4756
48-
export const CalendarGrid = styled(BaseCalendarGrid)`
49-
border-collapse: separate;
50-
border-spacing: 0 0.125rem;
57+
/* Navigation */
58+
.rdp-nav {
59+
position: absolute;
60+
inset-inline: 0;
61+
top: 0;
62+
display: flex;
63+
align-items: center;
64+
justify-content: space-between;
65+
gap: ${get('space.1')};
66+
pointer-events: none; /* allow buttons only */
67+
height: var(--rdp-nav-height);
68+
}
5169
52-
td {
70+
.rdp-button_previous,
71+
.rdp-button_next {
72+
appearance: none;
73+
background: none;
74+
border: 0;
75+
display: inline-flex;
76+
align-items: center;
77+
justify-content: center;
78+
width: var(--rdp-nav_button-width);
79+
height: var(--rdp-nav_button-height);
5380
padding: 0;
81+
color: ${getSemanticValue('on-surface')};
82+
border-radius: ${get('radii.2')};
83+
pointer-events: auto;
84+
cursor: pointer;
5485
}
5586
56-
th {
57-
padding: 0 0 ${get('space.1')};
87+
.rdp-button_previous:focus-visible,
88+
.rdp-button_next:focus-visible {
89+
outline: ${getSemanticValue('interactive')} solid 0.125rem;
5890
}
59-
`;
6091
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-
`;
92+
.rdp-button_previous:disabled,
93+
.rdp-button_next:disabled {
94+
opacity: var(--rdp-nav_button-disabled-opacity);
95+
}
6796
68-
export const MonthGrid = styled.div`
69-
display: flex;
70-
gap: 1.5rem;
97+
.rdp-caption_label {
98+
margin: 0;
99+
color: ${getSemanticValue('on-surface')};
100+
font-size: var(--wave-exp-typescale-title-2-size);
101+
font-weight: var(--wave-exp-typescale-title-2-weight);
102+
line-height: var(--wave-exp-typescale-title-2-line-height);
103+
display: flex;
104+
align-items: center;
105+
justify-content: center;
106+
inline-size: 100%;
107+
block-size: var(--rdp-nav-height);
108+
}
109+
110+
.rdp-weekdays {
111+
/* Use a fixed 7-column grid so headers align regardless of outside days */
112+
display: grid;
113+
grid-template-columns: repeat(7, var(--rdp-day-width));
114+
}
115+
116+
.rdp-weekday {
117+
color: ${getSemanticValue('on-surface')};
118+
font-size: var(--wave-exp-typescale-label-2-size);
119+
font-weight: var(--wave-exp-typescale-label-2-weight);
120+
line-height: var(--wave-exp-typescale-label-2-line-height);
121+
text-align: var(--rdp-weekday-text-align);
122+
opacity: var(--rdp-weekday-opacity);
123+
padding: var(--rdp-weekday-padding);
124+
flex: 1;
125+
border-radius: ${get('radii.2')};
126+
}
127+
128+
.rdp-week {
129+
margin-top: 0.125rem; /* match original row spacing */
130+
/* Fixed 7-column grid to keep days aligned when outside days are hidden */
131+
display: grid;
132+
grid-template-columns: repeat(7, var(--rdp-day-width));
133+
inline-size: 100%;
134+
}
71135
`;
72136

73-
export const Day = styled(CalendarCell)`
137+
// Custom Day button used via components.DayButton
138+
export const DayButton = styled.button`
74139
position: relative;
75140
display: flex;
76141
align-items: center;
77142
justify-content: center;
143+
width: var(--rdp-day_button-width);
144+
height: var(--rdp-day_button-height);
145+
min-width: var(--rdp-day_button-width);
146+
aspect-ratio: 1 / 1;
147+
padding: 0;
148+
margin: 0;
149+
border: var(--rdp-day_button-border);
150+
background: transparent;
78151
color: ${getSemanticValue('on-surface')};
79-
width: 2.5rem;
80-
height: 2.5rem;
81-
border-radius: 50%;
152+
border-radius: var(--rdp-day_button-border-radius);
82153
outline: 0;
83154
font-size: var(--wave-exp-typescale-label-2-size);
84155
font-weight: var(--wave-exp-typescale-label-2-weight);
85156
line-height: var(--wave-exp-typescale-label-2-line-height);
86-
transition: background ease 200ms;
157+
transition: background var(--rdp-animation_duration) var(--rdp-animation_timing);
87158
88159
&::after {
89160
content: '';
90161
position: absolute;
91162
inset: 0;
92-
border-radius: 50%;
163+
border-radius: inherit;
164+
pointer-events: none;
93165
}
94166
95-
&[data-focused]::after {
96-
z-index: 1;
97-
outline: ${getSemanticValue('interactive')} solid 0.125rem;
167+
/* When DayPicker marks outside days as hidden, keep layout space to avoid grid shift */
168+
&[hidden] {
169+
display: inline-flex; /* override UA stylesheet that sets display: none */
170+
visibility: hidden; /* hide content while preserving size */
98171
}
99172
100-
&[data-hovered] {
173+
&:hover {
101174
cursor: pointer;
102175
background: ${getSemanticValue('surface-variant')};
103176
}
104177
105-
&[data-selected] {
106-
background: ${getSemanticValue('interactive-container')};
107-
color: ${getSemanticValue('on-interactive-container')};
178+
&:focus-visible::after {
179+
outline: ${getSemanticValue('interactive')} solid 0.125rem;
108180
}
109181
110-
&[data-disabled] {
111-
opacity: 0.38;
182+
/* Today's date */
183+
&.rdp-day_today {
184+
color: var(--rdp-today-color);
112185
}
113186
114-
&[data-outside-month] {
115-
opacity: 0;
187+
/* Selected day */
188+
&.rdp-day_selected {
189+
background: var(--rdp-accent-background-color);
190+
color: var(--rdp-range_start-color);
191+
border: var(--rdp-selected-border);
116192
}
117193
118-
[data-selection-type='range'] &[data-selected] {
119-
border-radius: 0;
194+
/* Disabled and outside */
195+
&.rdp-day_disabled {
196+
opacity: var(--rdp-disabled-opacity);
120197
}
121198
122-
&[data-selection-start][data-selected] {
123-
border-start-start-radius: 50%;
124-
border-end-start-radius: 50%;
199+
&.rdp-day_outside {
200+
opacity: var(--rdp-outside-opacity);
201+
}
202+
203+
/* Range selection rounding */
204+
&.rdp-day_range_start.rdp-day_selected {
205+
background: var(--rdp-range_start-background);
206+
color: var(--rdp-range_start-color);
207+
border-start-start-radius: var(--rdp-day_button-border-radius);
208+
border-end-start-radius: var(--rdp-day_button-border-radius);
209+
}
210+
211+
&.rdp-day_range_middle {
212+
border-radius: 0;
213+
background: var(--rdp-range_middle-background-color);
214+
color: var(--rdp-range_middle-color);
125215
}
126216
127-
&[data-selection-end][data-selected] {
128-
border-start-end-radius: 50%;
129-
border-end-end-radius: 50%;
217+
&.rdp-day_range_end.rdp-day_selected {
218+
background: var(--rdp-range_end-background);
219+
color: var(--rdp-range_end-color);
220+
border-start-end-radius: var(--rdp-day_button-border-radius);
221+
border-end-end-radius: var(--rdp-day_button-border-radius);
130222
}
131223
`;

0 commit comments

Comments
 (0)