Skip to content

Commit 100b03a

Browse files
committed
feat: add Calendar component and replace mobile native date picker
New Features: * [Mobile] Standalone Calendar component cloned from cds-web and updated for cds-mobile Enhancements: * [Mobile] Comprehensive accessibility with screen reader support for Calendar component * [Mobile] Replaced react-native-date-picker with Calendar component for DatePicker * [Mobile + Web] Added/replaced calendar style props to match the new "classnames/styles" APIs * [Mobile] Added custom a11y labels for Calendar navigation Dependencies: * [Mobile] Remove react-native-date-picker (no longer needed) Tests: * [Mobile] Add Storybook for Calendar, including DatePicker, standalone, and custom triggers * [Mobile] Add unit tests for Calendar and DatePicker * [Web] Add slot styling examples for Calendar storybook Documentation: * [Mobile] Added mobile docs for Calendar * [Mobile + Web] Updated Calendar and DatePicker documentation to reflect the updated style APIs
1 parent 9ec435a commit 100b03a

35 files changed

+3482
-384
lines changed
Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
### Basic usage
2+
3+
A basic Calendar with date selection functionality. The Calendar component is used within the DatePicker and can also be used independently.
4+
5+
```jsx
6+
function Example() {
7+
const [selectedDate, setSelectedDate] = useState(new Date());
8+
9+
return <Calendar selectedDate={selectedDate} onPressDate={setSelectedDate} />;
10+
}
11+
```
12+
13+
### No selection
14+
15+
A Calendar without an initially selected date.
16+
17+
```jsx
18+
function Example() {
19+
const [selectedDate, setSelectedDate] = useState(null);
20+
21+
return <Calendar selectedDate={selectedDate} onPressDate={setSelectedDate} />;
22+
}
23+
```
24+
25+
### Seeding the calendar
26+
27+
The `seedDate` prop controls which month the Calendar opens to when there is no selected date value. Defaults to today when undefined.
28+
29+
```jsx
30+
function Example() {
31+
const [selectedDate, setSelectedDate] = useState(null);
32+
33+
const today = new Date(new Date().setHours(0, 0, 0, 0));
34+
const seedDate = new Date(today.getFullYear(), today.getMonth() + 1, 15);
35+
36+
return <Calendar selectedDate={selectedDate} onPressDate={setSelectedDate} seedDate={seedDate} />;
37+
}
38+
```
39+
40+
### Minimum and maximum dates
41+
42+
Use `minDate` and `maxDate` to restrict the selectable date range. Navigation to dates before the `minDate` and after the `maxDate` is disabled. Make sure to provide the `disabledDateError` prop.
43+
44+
```jsx
45+
function Example() {
46+
const [selectedDate, setSelectedDate] = useState(new Date());
47+
48+
const today = new Date(new Date().setHours(0, 0, 0, 0));
49+
const lastMonth15th = new Date(today.getFullYear(), today.getMonth() - 1, 15);
50+
const nextMonth15th = new Date(today.getFullYear(), today.getMonth() + 1, 15);
51+
52+
return (
53+
<Calendar
54+
selectedDate={selectedDate}
55+
onPressDate={setSelectedDate}
56+
minDate={lastMonth15th}
57+
maxDate={nextMonth15th}
58+
disabledDateError="Date is outside allowed range"
59+
/>
60+
);
61+
}
62+
```
63+
64+
### Future dates only
65+
66+
Restrict selection to future dates by setting `minDate` to today.
67+
68+
```jsx
69+
function Example() {
70+
const [selectedDate, setSelectedDate] = useState(null);
71+
72+
const today = new Date(new Date().setHours(0, 0, 0, 0));
73+
74+
return (
75+
<Calendar
76+
selectedDate={selectedDate}
77+
onPressDate={setSelectedDate}
78+
minDate={today}
79+
disabledDateError="Past dates are not available"
80+
/>
81+
);
82+
}
83+
```
84+
85+
### Highlighted dates
86+
87+
Use `highlightedDates` to visually emphasize specific dates or date ranges. A number is created for every individual date within a tuple range, so do not abuse this with massive ranges.
88+
89+
```jsx
90+
function Example() {
91+
const [selectedDate, setSelectedDate] = useState(new Date());
92+
93+
const today = new Date(new Date().setHours(0, 0, 0, 0));
94+
const yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1);
95+
const nextWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7);
96+
97+
return (
98+
<Calendar
99+
selectedDate={selectedDate}
100+
onPressDate={setSelectedDate}
101+
highlightedDates={[yesterday, today, nextWeek]}
102+
/>
103+
);
104+
}
105+
```
106+
107+
### Disabled dates
108+
109+
Use `disabledDates` to prevent selection of specific dates or date ranges. Make sure to provide the `disabledDateError` prop.
110+
111+
```jsx
112+
function Example() {
113+
const [selectedDate, setSelectedDate] = useState(null);
114+
115+
const today = new Date(new Date().setHours(0, 0, 0, 0));
116+
117+
// Disable weekends for demonstration
118+
const getNextWeekendDates = (centerDate) => {
119+
const weekends = [];
120+
const currentDate = new Date(centerDate);
121+
122+
// Find next 4 weekends
123+
for (let i = 0; i < 4; i++) {
124+
// Find next Saturday
125+
const daysUntilSaturday = (6 - currentDate.getDay() + 7) % 7 || 7;
126+
currentDate.setDate(currentDate.getDate() + daysUntilSaturday);
127+
128+
const saturday = new Date(currentDate);
129+
const sunday = new Date(currentDate);
130+
sunday.setDate(sunday.getDate() + 1);
131+
132+
weekends.push([saturday, sunday]);
133+
134+
// Move to next week
135+
currentDate.setDate(currentDate.getDate() + 7);
136+
}
137+
138+
return weekends;
139+
};
140+
141+
return (
142+
<Calendar
143+
selectedDate={selectedDate}
144+
onPressDate={setSelectedDate}
145+
disabledDates={getNextWeekendDates(today)}
146+
disabledDateError="Weekends are not available"
147+
/>
148+
);
149+
}
150+
```
151+
152+
### Date ranges
153+
154+
Highlight a date range using a tuple `[startDate, endDate]`.
155+
156+
```jsx
157+
function Example() {
158+
const [selectedDate, setSelectedDate] = useState(new Date());
159+
160+
const today = new Date(new Date().setHours(0, 0, 0, 0));
161+
const yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1);
162+
const nextWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7);
163+
164+
return (
165+
<Calendar
166+
selectedDate={selectedDate}
167+
onPressDate={setSelectedDate}
168+
highlightedDates={[[yesterday, nextWeek]]}
169+
/>
170+
);
171+
}
172+
```
173+
174+
### Hidden controls
175+
176+
Hide the navigation arrows with `hideControls`. This is typically used when `minDate` and `maxDate` are set to the first and last days of the same month.
177+
178+
```jsx
179+
function Example() {
180+
const [selectedDate, setSelectedDate] = useState(new Date());
181+
182+
const today = new Date(new Date().setHours(0, 0, 0, 0));
183+
const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
184+
const lastDayOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0);
185+
186+
return (
187+
<Calendar
188+
selectedDate={selectedDate}
189+
onPressDate={setSelectedDate}
190+
minDate={firstDayOfMonth}
191+
maxDate={lastDayOfMonth}
192+
hideControls
193+
/>
194+
);
195+
}
196+
```
197+
198+
### Disabled
199+
200+
Disable the entire Calendar with the `disabled` prop.
201+
202+
```jsx
203+
function Example() {
204+
const selectedDate = new Date();
205+
206+
return <Calendar selectedDate={selectedDate} disabled />;
207+
}
208+
```
209+
210+
### Slot styling
211+
212+
Use the `styles` prop to target specific elements: `root`, `header`, `monthLabel`, `navArrows` (container), `navArrow` (each button), `dayHeader`, `content` (day header + date grid), `calendarGrid` (date cells container), and `dayCell` (use `dayCell.button` for the cell container and `dayCell.text` for the date number; both use `base`, and button also uses `pressed`/`disabled`, text uses `selected`/`disabled`/`highlighted`).
213+
214+
```jsx
215+
function Example() {
216+
const [selectedDate, setSelectedDate] = useState(new Date());
217+
218+
return (
219+
<Calendar
220+
selectedDate={selectedDate}
221+
onPressDate={setSelectedDate}
222+
styles={{
223+
root: {
224+
backgroundColor: 'lightgray',
225+
borderColor: '#ccc',
226+
borderRadius: 16,
227+
borderWidth: 1,
228+
padding: 12,
229+
},
230+
header: {
231+
backgroundColor: 'rgba(0, 120, 0, 0.1)',
232+
borderRadius: 16,
233+
paddingBottom: 0,
234+
},
235+
monthLabel: { opacity: 0.9 },
236+
navArrows: {
237+
borderColor: '#888',
238+
borderRadius: 8,
239+
borderStyle: 'dashed',
240+
borderWidth: 1,
241+
padding: 4,
242+
},
243+
navArrow: { backgroundColor: 'transparent' },
244+
calendarGrid: { paddingVertical: 8 },
245+
dayCell: { button: { base: { borderRadius: 8 } } },
246+
}}
247+
/>
248+
);
249+
}
250+
```
251+
252+
On web you can also use `classNames` and CSS to target elements like icon or day-header text; on mobile only the `styles` prop is supported.
253+
254+
### Accessibility
255+
256+
Always provide accessibility labels for the navigation controls and error messages for disabled dates.
257+
258+
```jsx
259+
function Example() {
260+
const [selectedDate, setSelectedDate] = useState(new Date());
261+
262+
const today = new Date(new Date().setHours(0, 0, 0, 0));
263+
const nextMonth = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate());
264+
265+
return (
266+
<Calendar
267+
selectedDate={selectedDate}
268+
onPressDate={setSelectedDate}
269+
maxDate={nextMonth}
270+
disabledDateError="Date is not available for selection"
271+
nextArrowAccessibilityLabel="Go to next month"
272+
previousArrowAccessibilityLabel="Go to previous month"
273+
/>
274+
);
275+
}
276+
```
277+
278+
### Date selection with Chip trigger
279+
280+
When you need a compact trigger instead of the full [DateInput](/components/other/DateInput/) used in [DatePicker](/components/other/DatePicker/), you can use a [Chip](/components/inputs/Chip) (or similar control) to open a Tray with a Calendar and a confirm button.
281+
282+
```jsx
283+
function Example() {
284+
const { locale } = useLocale();
285+
const [date, setDate] = useState(null);
286+
const [showPicker, setShowPicker] = useState(false);
287+
const [calendarSelectedDate, setCalendarSelectedDate] = useState(null);
288+
const calendarRef = useRef(null);
289+
290+
const formatDateLabel = useCallback(
291+
(date, locale) =>
292+
date
293+
? date.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' })
294+
: 'Select date',
295+
[],
296+
);
297+
298+
const handleOpenPicker = useCallback(() => {
299+
setCalendarSelectedDate(date);
300+
setShowPicker(true);
301+
}, [date]);
302+
303+
const handleClosePicker = useCallback(() => setShowPicker(false), []);
304+
305+
const handleCancelPicker = useCallback(() => {
306+
setCalendarSelectedDate(null);
307+
handleClosePicker();
308+
}, [handleClosePicker]);
309+
310+
const handleCalendarDatePress = useCallback((selectedDate) => {
311+
setCalendarSelectedDate(selectedDate);
312+
}, []);
313+
314+
const handleModalShow = useCallback(() => {
315+
calendarRef.current?.focusInitialDate();
316+
}, []);
317+
318+
const handleConfirmCalendar = useCallback(() => {
319+
if (calendarSelectedDate) {
320+
setDate(calendarSelectedDate);
321+
handleClosePicker();
322+
}
323+
}, [calendarSelectedDate, handleClosePicker]);
324+
325+
const formattedLabel = formatDateLabel(date, locale);
326+
327+
const trayFooter = useMemo(
328+
() => (
329+
<Box paddingTop={3} paddingX={3}>
330+
<Button
331+
block
332+
compact
333+
accessibilityHint={!calendarSelectedDate ? 'Select a date first' : undefined}
334+
accessibilityLabel="Confirm date selection"
335+
disabled={!calendarSelectedDate}
336+
onPress={handleConfirmCalendar}
337+
>
338+
Confirm
339+
</Button>
340+
</Box>
341+
),
342+
[calendarSelectedDate, handleConfirmCalendar],
343+
);
344+
345+
return (
346+
<>
347+
<Box alignSelf="flex-start">
348+
<Chip
349+
compact
350+
accessibilityLabel={formattedLabel}
351+
end={<AnimatedCaret active color="fg" rotate={showPicker ? 0 : 180} size="xs" />}
352+
onPress={handleOpenPicker}
353+
>
354+
{formattedLabel}
355+
</Chip>
356+
</Box>
357+
{showPicker && (
358+
<Tray
359+
accessibilityRole="none"
360+
footer={trayFooter}
361+
handleBarAccessibilityLabel="Close calendar"
362+
handleBarVariant="inside"
363+
onCloseComplete={handleCancelPicker}
364+
onOpenComplete={handleModalShow}
365+
>
366+
<Calendar
367+
ref={calendarRef}
368+
onPressDate={handleCalendarDatePress}
369+
paddingBottom={2}
370+
paddingX={2}
371+
selectedDate={calendarSelectedDate}
372+
/>
373+
</Tray>
374+
)}
375+
</>
376+
);
377+
}
378+
```
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import ComponentPropsTable from '@site/src/components/page/ComponentPropsTable';
2+
3+
import mobilePropsData from ':docgen/mobile/dates/Calendar/data';
4+
import { sharedParentTypes } from ':docgen/_types/sharedParentTypes';
5+
import { sharedTypeAliases } from ':docgen/_types/sharedTypeAliases';
6+
7+
<ComponentPropsTable
8+
props={mobilePropsData}
9+
sharedTypeAliases={sharedTypeAliases}
10+
sharedParentTypes={sharedParentTypes}
11+
/>

0 commit comments

Comments
 (0)