Skip to content

Commit 158cf94

Browse files
committed
feat(mobile): add Calendar component and replace native date picker
New Features: - Add standalone Calendar component for date selection - Support min/max date constraints with automatic navigation - Support disabled dates and highlighted date ranges - Comprehensive accessibility with screen reader support Enhancements: - Integrate Calendar into DatePicker with confirmation flow - Add triggerDisabled prop to Tooltip for better a11y around "disabled" triggers - Add calendarStyle, highlightedDates, and seedDate props to DatePicker - Add custom a11y labels for Calendar navigation Dependencies: - Remove react-native-date-picker (no longer needed) Tests: - Add Storybook for Calendar, including DatePicker, standalone, and custom triggers - Add unit tests for Calendar, DatePicker, Tooltip Documentation: - Added mobile docs for Calendar - Updated mobile docs for DatePicker and Tooltip - Removed references to react-native-date-picker in mobile docs for DatePicker
1 parent ce54c04 commit 158cf94

File tree

31 files changed

+2975
-260
lines changed

31 files changed

+2975
-260
lines changed
Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
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+
### Accessibility
211+
212+
Always provide accessibility labels for the navigation controls and error messages for disabled dates.
213+
214+
```jsx
215+
function Example() {
216+
const [selectedDate, setSelectedDate] = useState(new Date());
217+
218+
const today = new Date(new Date().setHours(0, 0, 0, 0));
219+
const nextMonth = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate());
220+
221+
return (
222+
<Calendar
223+
selectedDate={selectedDate}
224+
onPressDate={setSelectedDate}
225+
maxDate={nextMonth}
226+
disabledDateError="Date is not available for selection"
227+
nextArrowAccessibilityLabel="Go to next month"
228+
previousArrowAccessibilityLabel="Go to previous month"
229+
/>
230+
);
231+
}
232+
```
233+
234+
### Date selection with Chip trigger
235+
236+
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.
237+
238+
```jsx
239+
function Example() {
240+
const { locale } = useLocale();
241+
const [date, setDate] = useState(null);
242+
const [showPicker, setShowPicker] = useState(false);
243+
const [calendarSelectedDate, setCalendarSelectedDate] = useState(null);
244+
const calendarRef = useRef(null);
245+
246+
const formatDateLabel = useCallback(
247+
(date, locale) =>
248+
date
249+
? date.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' })
250+
: 'Select date',
251+
[],
252+
);
253+
254+
const handleOpenPicker = useCallback(() => {
255+
setCalendarSelectedDate(date);
256+
setShowPicker(true);
257+
}, [date]);
258+
259+
const handleClosePicker = useCallback(() => setShowPicker(false), []);
260+
261+
const handleCancelPicker = useCallback(() => {
262+
setCalendarSelectedDate(null);
263+
handleClosePicker();
264+
}, [handleClosePicker]);
265+
266+
const handleCalendarDatePress = useCallback((selectedDate) => {
267+
setCalendarSelectedDate(selectedDate);
268+
}, []);
269+
270+
const handleModalShow = useCallback(() => {
271+
calendarRef.current?.focusInitialDate();
272+
}, []);
273+
274+
const handleConfirmCalendar = useCallback(() => {
275+
if (calendarSelectedDate) {
276+
setDate(calendarSelectedDate);
277+
handleClosePicker();
278+
}
279+
}, [calendarSelectedDate, handleClosePicker]);
280+
281+
const formattedLabel = formatDateLabel(date, locale);
282+
283+
const trayFooter = useMemo(
284+
() => (
285+
<Box paddingTop={3} paddingX={3}>
286+
<Button
287+
block
288+
compact
289+
accessibilityHint={!calendarSelectedDate ? 'Select a date first' : undefined}
290+
accessibilityLabel="Confirm date selection"
291+
disabled={!calendarSelectedDate}
292+
onPress={handleConfirmCalendar}
293+
>
294+
Confirm
295+
</Button>
296+
</Box>
297+
),
298+
[calendarSelectedDate, handleConfirmCalendar],
299+
);
300+
301+
return (
302+
<>
303+
<Box alignSelf="flex-start">
304+
<Chip
305+
compact
306+
accessibilityLabel={formattedLabel}
307+
end={<AnimatedCaret active color="fg" rotate={showPicker ? 0 : 180} size="xs" />}
308+
onPress={handleOpenPicker}
309+
>
310+
{formattedLabel}
311+
</Chip>
312+
</Box>
313+
{showPicker && (
314+
<Tray
315+
accessibilityRole="none"
316+
footer={trayFooter}
317+
handleBarAccessibilityLabel="Close calendar"
318+
handleBarVariant="inside"
319+
onCloseComplete={handleCancelPicker}
320+
onOpenComplete={handleModalShow}
321+
>
322+
<Calendar
323+
ref={calendarRef}
324+
onPressDate={handleCalendarDatePress}
325+
paddingBottom={2}
326+
paddingX={2}
327+
selectedDate={calendarSelectedDate}
328+
/>
329+
</Tray>
330+
)}
331+
</>
332+
);
333+
}
334+
```
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+
/>
Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
id: calendar
33
title: Calendar
4-
platform_switcher_options: { web: true, mobile: false }
4+
platform_switcher_options: { web: true, mobile: true }
55
hide_title: true
66
---
77

@@ -10,16 +10,26 @@ import { ComponentHeader } from '@site/src/components/page/ComponentHeader';
1010
import { ComponentTabsContainer } from '@site/src/components/page/ComponentTabsContainer';
1111

1212
import webPropsToc from ':docgen/web/dates/Calendar/toc-props';
13+
import mobilePropsToc from ':docgen/mobile/dates/Calendar/toc-props';
14+
1315
import WebPropsTable from './_webPropsTable.mdx';
16+
import MobilePropsTable from './_mobilePropsTable.mdx';
17+
import MobileExamples, { toc as mobileExamplesToc } from './_mobileExamples.mdx';
1418
import WebExamples, { toc as webExamplesToc } from './_webExamples.mdx';
19+
1520
import webMetadata from './webMetadata.json';
21+
import mobileMetadata from './mobileMetadata.json';
1622

1723
<VStack gap={5}>
18-
<ComponentHeader title="Calendar" webMetadata={webMetadata} />
24+
<ComponentHeader title="Calendar" webMetadata={webMetadata} mobileMetadata={mobileMetadata} />
1925
<ComponentTabsContainer
2026
webPropsTable={<WebPropsTable />}
2127
webExamples={<WebExamples />}
28+
mobilePropsTable={<MobilePropsTable />}
29+
mobileExamples={<MobileExamples />}
2230
webExamplesToc={webExamplesToc}
31+
mobileExamplesToc={mobileExamplesToc}
2332
webPropsToc={webPropsToc}
33+
mobilePropsToc={mobilePropsToc}
2434
/>
2535
</VStack>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"import": "import { Calendar } from '@coinbase/cds-mobile/dates/Calendar'",
3+
"source": "https://github.com/coinbase/cds/blob/master/packages/mobile/src/dates/Calendar.tsx",
4+
"description": "Calendar is a flexible, accessible date grid component for selecting dates, supporting keyboard navigation, disabled/highlighted dates, and custom rendering.",
5+
"relatedComponents": [
6+
{
7+
"label": "DatePicker",
8+
"url": "/components/other/DatePicker/"
9+
}
10+
]
11+
}

0 commit comments

Comments
 (0)