@ceed/ads 1.20.0 → 1.20.1-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ProfileMenu/ProfileMenu.d.ts +1 -1
- package/dist/components/data-display/Markdown.md +832 -0
- package/dist/components/feedback/Dialog.md +605 -3
- package/dist/components/feedback/Modal.md +656 -24
- package/dist/components/feedback/llms.txt +1 -1
- package/dist/components/inputs/Autocomplete.md +734 -2
- package/dist/components/inputs/Calendar.md +655 -1
- package/dist/components/inputs/DatePicker.md +699 -3
- package/dist/components/inputs/DateRangePicker.md +815 -1
- package/dist/components/inputs/MonthPicker.md +626 -4
- package/dist/components/inputs/MonthRangePicker.md +682 -4
- package/dist/components/inputs/Select.md +600 -0
- package/dist/components/layout/Container.md +507 -0
- package/dist/components/navigation/Breadcrumbs.md +582 -0
- package/dist/components/navigation/IconMenuButton.md +693 -0
- package/dist/components/navigation/InsetDrawer.md +1150 -3
- package/dist/components/navigation/Link.md +526 -0
- package/dist/components/navigation/MenuButton.md +632 -0
- package/dist/components/navigation/NavigationGroup.md +401 -1
- package/dist/components/navigation/NavigationItem.md +311 -0
- package/dist/components/navigation/Navigator.md +373 -0
- package/dist/components/navigation/Pagination.md +521 -0
- package/dist/components/navigation/ProfileMenu.md +605 -0
- package/dist/components/navigation/Tabs.md +609 -7
- package/dist/components/surfaces/Accordions.md +947 -3
- package/dist/index.cjs +3 -1
- package/dist/index.js +3 -1
- package/dist/llms.txt +1 -1
- package/framer/index.js +1 -1
- package/package.json +3 -2
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Introduction
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Calendar is a low-level UI component that renders an interactive calendar grid for date selection. It provides the visual foundation for higher-level picker components like DatePicker, DateRangePicker, and MonthPicker. While you'll rarely use Calendar directly in forms, it's useful for building custom date selection interfaces or embedded calendar views where you need full control over the calendar behavior.
|
|
6
6
|
|
|
7
7
|
```tsx
|
|
8
8
|
<Calendar />
|
|
@@ -12,8 +12,662 @@
|
|
|
12
12
|
| ------ | ----------- | ------- |
|
|
13
13
|
| locale | — | — |
|
|
14
14
|
|
|
15
|
+
> ⚠️ **Usage Warning** ⚠️
|
|
16
|
+
>
|
|
17
|
+
> Calendar is a **building block component** intended for custom implementations:
|
|
18
|
+
>
|
|
19
|
+
> - **For standard date input**: Use `DatePicker` instead
|
|
20
|
+
> - **For date ranges**: Use `DateRangePicker` instead
|
|
21
|
+
> - **For month selection**: Use `MonthPicker` or `MonthRangePicker`
|
|
22
|
+
> - **Value type**: Calendar uses `Date` objects, not strings like picker components
|
|
23
|
+
> - **No input field**: Calendar is just the grid, with no text input
|
|
24
|
+
|
|
15
25
|
## Usage
|
|
16
26
|
|
|
17
27
|
```tsx
|
|
18
28
|
import { Calendar } from '@ceed/ads';
|
|
29
|
+
|
|
30
|
+
function DateViewer() {
|
|
31
|
+
const [date, setDate] = useState<Date | null>(null);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Calendar
|
|
35
|
+
value={date ? [date, undefined] : undefined}
|
|
36
|
+
onChange={(dates) => setDate(dates[0] || null)}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Examples
|
|
43
|
+
|
|
44
|
+
### Controlled
|
|
45
|
+
|
|
46
|
+
Parent component manages the calendar state.
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
<Calendar />
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Uncontrolled
|
|
53
|
+
|
|
54
|
+
Calendar manages its own state internally.
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
<Calendar />
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### With Locale
|
|
61
|
+
|
|
62
|
+
Display calendar in different languages.
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
<Calendar locale="de" />
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Range Selection
|
|
69
|
+
|
|
70
|
+
Enable selection of date ranges (start and end dates).
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
<Calendar rangeSelection />
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Default Value with Range Selection
|
|
77
|
+
|
|
78
|
+
Pre-populate a date range when the calendar loads.
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
<Calendar rangeSelection defaultValue={[new Date(2024, 2, 1), new Date(2024, 2, 20)]} />
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Only Month View
|
|
85
|
+
|
|
86
|
+
Show only the month grid without day selection.
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
<Calendar
|
|
90
|
+
views={['month']}
|
|
91
|
+
defaultValue={[new Date(2024, 2, 1), undefined]}
|
|
92
|
+
/>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### All Views
|
|
96
|
+
|
|
97
|
+
Show both month and day views with navigation between them.
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
<Calendar views={['month', 'day']} rangeSelection defaultValue={[new Date(2024, 2, 1), new Date(2024, 2, 20)]} />
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Month Selection
|
|
104
|
+
|
|
105
|
+
Use the calendar for month-level selection only.
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
<Calendar view="month" value={[value, undefined]} onMonthChange={setValue} />
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Minimum Date
|
|
112
|
+
|
|
113
|
+
Restrict selection to dates on or after a minimum date.
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
<Calendar minDate={new Date('2024-02-15')} rangeSelection />
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Maximum Date
|
|
120
|
+
|
|
121
|
+
Restrict selection to dates on or before a maximum date.
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
<Calendar maxDate={new Date('2024-02-15')} rangeSelection />
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Disable Future Dates
|
|
128
|
+
|
|
129
|
+
Prevent selection of any future dates.
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
<Calendar disableFuture rangeSelection />
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Disable Past Dates
|
|
136
|
+
|
|
137
|
+
Prevent selection of any past dates.
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
<Calendar disablePast rangeSelection />
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Custom Date Disabling
|
|
144
|
+
|
|
145
|
+
Use `shouldDisableDate` to disable specific dates like weekends.
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
<Calendar shouldDisableDate={date => [0, 6].includes(date.getDay())} />
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Weekends Disabled with Past Dates
|
|
152
|
+
|
|
153
|
+
Combine multiple restrictions: disable weekends and past dates.
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
<Calendar shouldDisableDate={date => [0, 6].includes(date.getDay())} disablePast />
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Complex Date Restrictions
|
|
160
|
+
|
|
161
|
+
Disable weekends, past dates, and limit to one week ahead.
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
<Calendar shouldDisableDate={date => [0, 6].includes(date.getDay()) || date.getTime() >= new Date().getTime() + 7 * 24 * 60 * 60 * 1000} disablePast />
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## When to Use
|
|
168
|
+
|
|
169
|
+
### ✅ Good Use Cases
|
|
170
|
+
|
|
171
|
+
- **Custom date picker UI**: Building a specialized date selection interface
|
|
172
|
+
- **Embedded calendar**: Showing a calendar inline within a page layout
|
|
173
|
+
- **Dashboard widget**: Calendar as a date navigation widget
|
|
174
|
+
- **Event calendar**: Base for event/appointment calendars
|
|
175
|
+
- **Custom range pickers**: Building specialized range selection UI
|
|
176
|
+
- **Full control needed**: When you need direct access to calendar behavior
|
|
177
|
+
|
|
178
|
+
### ❌ When Not to Use
|
|
179
|
+
|
|
180
|
+
- **Form date input**: Use `DatePicker` for standard form fields
|
|
181
|
+
- **Date range forms**: Use `DateRangePicker` for start/end date pairs
|
|
182
|
+
- **Month selection forms**: Use `MonthPicker` or `MonthRangePicker`
|
|
183
|
+
- **Simple date display**: Use a text display or formatted date
|
|
184
|
+
- **Date with time**: Calendar doesn't include time selection
|
|
185
|
+
|
|
186
|
+
## Common Use Cases
|
|
187
|
+
|
|
188
|
+
### Embedded Calendar Widget
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
function CalendarWidget() {
|
|
192
|
+
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<Box sx={{ p: 2, border: '1px solid', borderColor: 'divider', borderRadius: 'md' }}>
|
|
196
|
+
<Typography level="title-md" mb={2}>
|
|
197
|
+
Select a Date
|
|
198
|
+
</Typography>
|
|
199
|
+
<Calendar
|
|
200
|
+
value={[selectedDate, undefined]}
|
|
201
|
+
onChange={(dates) => dates[0] && setSelectedDate(dates[0])}
|
|
202
|
+
/>
|
|
203
|
+
<Typography level="body-sm" mt={2}>
|
|
204
|
+
Selected: {selectedDate.toLocaleDateString()}
|
|
205
|
+
</Typography>
|
|
206
|
+
</Box>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Event Calendar with Highlighted Dates
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
function EventCalendar({ events }) {
|
|
215
|
+
const [viewDate, setViewDate] = useState<Date>(new Date());
|
|
216
|
+
|
|
217
|
+
// Get dates with events
|
|
218
|
+
const eventDates = useMemo(() => {
|
|
219
|
+
return events.map((e) => e.date.toDateString());
|
|
220
|
+
}, [events]);
|
|
221
|
+
|
|
222
|
+
const hasEvent = (date: Date) => eventDates.includes(date.toDateString());
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<Stack gap={2}>
|
|
226
|
+
<Calendar
|
|
227
|
+
value={[viewDate, undefined]}
|
|
228
|
+
onChange={(dates) => dates[0] && setViewDate(dates[0])}
|
|
229
|
+
/>
|
|
230
|
+
{hasEvent(viewDate) && (
|
|
231
|
+
<Box>
|
|
232
|
+
<Typography level="title-sm">Events on {viewDate.toLocaleDateString()}</Typography>
|
|
233
|
+
{events
|
|
234
|
+
.filter((e) => e.date.toDateString() === viewDate.toDateString())
|
|
235
|
+
.map((event) => (
|
|
236
|
+
<Typography key={event.id} level="body-sm">
|
|
237
|
+
{event.title}
|
|
238
|
+
</Typography>
|
|
239
|
+
))}
|
|
240
|
+
</Box>
|
|
241
|
+
)}
|
|
242
|
+
</Stack>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Appointment Booking Calendar
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
function BookingCalendar({ availableSlots, onBook }) {
|
|
251
|
+
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
|
|
252
|
+
|
|
253
|
+
// Get available dates
|
|
254
|
+
const availableDates = useMemo(() => {
|
|
255
|
+
return new Set(availableSlots.map((slot) => slot.date.toDateString()));
|
|
256
|
+
}, [availableSlots]);
|
|
257
|
+
|
|
258
|
+
// Disable dates without available slots
|
|
259
|
+
const shouldDisableDate = (date: Date) => {
|
|
260
|
+
const isPast = date < new Date(new Date().setHours(0, 0, 0, 0));
|
|
261
|
+
const hasNoSlots = !availableDates.has(date.toDateString());
|
|
262
|
+
return isPast || hasNoSlots;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const slotsForSelectedDate = useMemo(() => {
|
|
266
|
+
if (!selectedDate) return [];
|
|
267
|
+
return availableSlots.filter(
|
|
268
|
+
(slot) => slot.date.toDateString() === selectedDate.toDateString()
|
|
269
|
+
);
|
|
270
|
+
}, [selectedDate, availableSlots]);
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<Stack direction="row" gap={3}>
|
|
274
|
+
<Calendar
|
|
275
|
+
value={selectedDate ? [selectedDate, undefined] : undefined}
|
|
276
|
+
onChange={(dates) => setSelectedDate(dates[0] || null)}
|
|
277
|
+
shouldDisableDate={shouldDisableDate}
|
|
278
|
+
disablePast
|
|
279
|
+
/>
|
|
280
|
+
{selectedDate && (
|
|
281
|
+
<Box sx={{ minWidth: 200 }}>
|
|
282
|
+
<Typography level="title-sm" mb={1}>
|
|
283
|
+
Available Times on {selectedDate.toLocaleDateString()}
|
|
284
|
+
</Typography>
|
|
285
|
+
<Stack gap={1}>
|
|
286
|
+
{slotsForSelectedDate.map((slot) => (
|
|
287
|
+
<Button
|
|
288
|
+
key={slot.id}
|
|
289
|
+
variant="outlined"
|
|
290
|
+
size="sm"
|
|
291
|
+
onClick={() => onBook(slot)}
|
|
292
|
+
>
|
|
293
|
+
{slot.time}
|
|
294
|
+
</Button>
|
|
295
|
+
))}
|
|
296
|
+
</Stack>
|
|
297
|
+
</Box>
|
|
298
|
+
)}
|
|
299
|
+
</Stack>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Date Range Selection with Preview
|
|
305
|
+
|
|
306
|
+
```tsx
|
|
307
|
+
function DateRangeSelector({ onRangeSelect }) {
|
|
308
|
+
const [range, setRange] = useState<[Date | undefined, Date | undefined]>([undefined, undefined]);
|
|
309
|
+
|
|
310
|
+
const handleChange = (dates: [Date | undefined, Date | undefined]) => {
|
|
311
|
+
setRange(dates);
|
|
312
|
+
if (dates[0] && dates[1]) {
|
|
313
|
+
onRangeSelect({ start: dates[0], end: dates[1] });
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const dayCount = useMemo(() => {
|
|
318
|
+
if (!range[0] || !range[1]) return 0;
|
|
319
|
+
const diff = range[1].getTime() - range[0].getTime();
|
|
320
|
+
return Math.ceil(diff / (1000 * 60 * 60 * 24)) + 1;
|
|
321
|
+
}, [range]);
|
|
322
|
+
|
|
323
|
+
return (
|
|
324
|
+
<Stack gap={2}>
|
|
325
|
+
<Calendar
|
|
326
|
+
rangeSelection
|
|
327
|
+
value={range}
|
|
328
|
+
onChange={handleChange}
|
|
329
|
+
disablePast
|
|
330
|
+
/>
|
|
331
|
+
<Box>
|
|
332
|
+
{range[0] && (
|
|
333
|
+
<Typography level="body-sm">
|
|
334
|
+
Start: {range[0].toLocaleDateString()}
|
|
335
|
+
</Typography>
|
|
336
|
+
)}
|
|
337
|
+
{range[1] && (
|
|
338
|
+
<Typography level="body-sm">
|
|
339
|
+
End: {range[1].toLocaleDateString()} ({dayCount} days)
|
|
340
|
+
</Typography>
|
|
341
|
+
)}
|
|
342
|
+
{!range[0] && (
|
|
343
|
+
<Typography level="body-sm" color="neutral">
|
|
344
|
+
Select start date
|
|
345
|
+
</Typography>
|
|
346
|
+
)}
|
|
347
|
+
</Box>
|
|
348
|
+
</Stack>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Multi-Language Calendar
|
|
354
|
+
|
|
355
|
+
```tsx
|
|
356
|
+
function LocalizedCalendar({ userLocale }) {
|
|
357
|
+
const [date, setDate] = useState<Date>(new Date());
|
|
358
|
+
|
|
359
|
+
// Map common locales
|
|
360
|
+
const getCalendarLocale = (locale: string) => {
|
|
361
|
+
const localeMap: Record<string, string> = {
|
|
362
|
+
'en-US': 'en',
|
|
363
|
+
'ko-KR': 'ko',
|
|
364
|
+
'ja-JP': 'ja',
|
|
365
|
+
'de-DE': 'de',
|
|
366
|
+
'fr-FR': 'fr',
|
|
367
|
+
'zh-CN': 'zh',
|
|
368
|
+
};
|
|
369
|
+
return localeMap[locale] || 'en';
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
return (
|
|
373
|
+
<Calendar
|
|
374
|
+
value={[date, undefined]}
|
|
375
|
+
onChange={(dates) => dates[0] && setDate(dates[0])}
|
|
376
|
+
locale={getCalendarLocale(userLocale)}
|
|
377
|
+
/>
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Month Navigation Widget
|
|
383
|
+
|
|
384
|
+
```tsx
|
|
385
|
+
function MonthNavigator({ onMonthChange }) {
|
|
386
|
+
const [currentMonth, setCurrentMonth] = useState<Date>(new Date());
|
|
387
|
+
|
|
388
|
+
const handleMonthChange = (date: Date) => {
|
|
389
|
+
setCurrentMonth(date);
|
|
390
|
+
onMonthChange(date);
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
return (
|
|
394
|
+
<Box>
|
|
395
|
+
<Typography level="title-md" mb={2}>
|
|
396
|
+
{currentMonth.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}
|
|
397
|
+
</Typography>
|
|
398
|
+
<Calendar
|
|
399
|
+
view="month"
|
|
400
|
+
views={['month']}
|
|
401
|
+
value={[currentMonth, undefined]}
|
|
402
|
+
onMonthChange={handleMonthChange}
|
|
403
|
+
/>
|
|
404
|
+
</Box>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Props and Customization
|
|
410
|
+
|
|
411
|
+
### Key Props
|
|
412
|
+
|
|
413
|
+
| Prop | Type | Default | Description |
|
|
414
|
+
| ------------------- | --------------------------------------------------------- | --------- | ------------------------------------------------ |
|
|
415
|
+
| `value` | `[Date \| undefined, Date \| undefined]` | - | Controlled value as array `[startDate, endDate]` |
|
|
416
|
+
| `defaultValue` | `[Date \| undefined, Date \| undefined]` | - | Default value for uncontrolled mode |
|
|
417
|
+
| `onChange` | `(dates: [Date \| undefined, Date \| undefined]) => void` | - | Change handler |
|
|
418
|
+
| `rangeSelection` | `boolean` | `false` | Enable date range selection mode |
|
|
419
|
+
| `view` | `'day' \| 'month'` | `'day'` | Current view mode |
|
|
420
|
+
| `views` | `('day' \| 'month')[]` | `['day']` | Available views for navigation |
|
|
421
|
+
| `locale` | `string` | `'en'` | Locale for month/day names |
|
|
422
|
+
| `minDate` | `Date` | - | Minimum selectable date |
|
|
423
|
+
| `maxDate` | `Date` | - | Maximum selectable date |
|
|
424
|
+
| `disableFuture` | `boolean` | `false` | Disable all future dates |
|
|
425
|
+
| `disablePast` | `boolean` | `false` | Disable all past dates |
|
|
426
|
+
| `shouldDisableDate` | `(date: Date) => boolean` | - | Custom function to disable specific dates |
|
|
427
|
+
| `onMonthChange` | `(date: Date) => void` | - | Callback when month changes |
|
|
428
|
+
|
|
429
|
+
### Value Format
|
|
430
|
+
|
|
431
|
+
Calendar uses `Date` objects instead of strings:
|
|
432
|
+
|
|
433
|
+
```tsx
|
|
434
|
+
// Single date selection (use undefined for second element)
|
|
435
|
+
<Calendar
|
|
436
|
+
value={[new Date('2024-04-15'), undefined]}
|
|
437
|
+
onChange={(dates) => console.log(dates[0])} // Date object
|
|
438
|
+
/>
|
|
439
|
+
|
|
440
|
+
// Range selection
|
|
441
|
+
<Calendar
|
|
442
|
+
rangeSelection
|
|
443
|
+
value={[new Date('2024-04-01'), new Date('2024-04-15')]}
|
|
444
|
+
onChange={(dates) => {
|
|
445
|
+
const [start, end] = dates;
|
|
446
|
+
console.log('Start:', start); // Date object
|
|
447
|
+
console.log('End:', end); // Date object
|
|
448
|
+
}}
|
|
449
|
+
/>
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### View Modes
|
|
453
|
+
|
|
454
|
+
```tsx
|
|
455
|
+
// Day view only (default)
|
|
456
|
+
<Calendar views={['day']} />
|
|
457
|
+
|
|
458
|
+
// Month view only
|
|
459
|
+
<Calendar views={['month']} view="month" />
|
|
460
|
+
|
|
461
|
+
// Both views with navigation
|
|
462
|
+
<Calendar views={['month', 'day']} />
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Date Restrictions
|
|
466
|
+
|
|
467
|
+
```tsx
|
|
468
|
+
// Restrict to specific range
|
|
469
|
+
<Calendar
|
|
470
|
+
minDate={new Date('2024-01-01')}
|
|
471
|
+
maxDate={new Date('2024-12-31')}
|
|
472
|
+
/>
|
|
473
|
+
|
|
474
|
+
// Disable weekends
|
|
475
|
+
<Calendar
|
|
476
|
+
shouldDisableDate={(date) => [0, 6].includes(date.getDay())}
|
|
477
|
+
/>
|
|
478
|
+
|
|
479
|
+
// Combine multiple restrictions
|
|
480
|
+
<Calendar
|
|
481
|
+
disablePast
|
|
482
|
+
disableFuture={false}
|
|
483
|
+
maxDate={new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)} // 30 days ahead
|
|
484
|
+
shouldDisableDate={(date) => [0, 6].includes(date.getDay())}
|
|
485
|
+
/>
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
## Accessibility
|
|
489
|
+
|
|
490
|
+
Calendar includes built-in accessibility features:
|
|
491
|
+
|
|
492
|
+
### ARIA Attributes
|
|
493
|
+
|
|
494
|
+
- Calendar uses proper grid roles for navigation
|
|
495
|
+
- Days are announced with full date context
|
|
496
|
+
- Disabled dates are marked as `aria-disabled`
|
|
497
|
+
- Selected dates use `aria-selected`
|
|
498
|
+
|
|
499
|
+
### Keyboard Navigation
|
|
500
|
+
|
|
501
|
+
- **Arrow Keys**: Navigate between days
|
|
502
|
+
- **Page Up/Down**: Navigate between months
|
|
503
|
+
- **Home/End**: Jump to start/end of week
|
|
504
|
+
- **Enter/Space**: Select focused date
|
|
505
|
+
- **Tab**: Move focus to calendar navigation buttons
|
|
506
|
+
|
|
507
|
+
### Screen Reader Support
|
|
508
|
+
|
|
509
|
+
```tsx
|
|
510
|
+
// Days are announced with full context
|
|
511
|
+
<button aria-label="April 15, 2024">15</button>
|
|
512
|
+
|
|
513
|
+
// Month navigation buttons are descriptive
|
|
514
|
+
<button aria-label="Previous Month">←</button>
|
|
515
|
+
<button aria-label="Next Month">→</button>
|
|
516
|
+
|
|
517
|
+
// Disabled dates are announced
|
|
518
|
+
<button aria-label="April 20, 2024 (unavailable)" aria-disabled="true">20</button>
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### Focus Management
|
|
522
|
+
|
|
523
|
+
- Focus remains within calendar during navigation
|
|
524
|
+
- Visual focus indicator on active day
|
|
525
|
+
- Disabled dates are not focusable
|
|
526
|
+
|
|
527
|
+
## Best Practices
|
|
528
|
+
|
|
529
|
+
### ✅ Do
|
|
530
|
+
|
|
531
|
+
1. **Use appropriate picker components for forms**: Default to DatePicker/DateRangePicker
|
|
532
|
+
|
|
533
|
+
```tsx
|
|
534
|
+
// ✅ Good: Use DatePicker for form input
|
|
535
|
+
<DatePicker
|
|
536
|
+
label="Birth Date"
|
|
537
|
+
value={date}
|
|
538
|
+
onChange={(e) => setDate(e.target.value)}
|
|
539
|
+
/>
|
|
19
540
|
```
|
|
541
|
+
|
|
542
|
+
2. **Provide clear context when using Calendar directly**: Add surrounding UI to explain the calendar's purpose
|
|
543
|
+
|
|
544
|
+
```tsx
|
|
545
|
+
// ✅ Good: Clear context around calendar
|
|
546
|
+
<Box>
|
|
547
|
+
<Typography level="title-md">Select Appointment Date</Typography>
|
|
548
|
+
<Calendar value={[date, undefined]} onChange={handleChange} />
|
|
549
|
+
<Typography level="body-sm">Selected: {date?.toLocaleDateString()}</Typography>
|
|
550
|
+
</Box>
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
3. **Use appropriate date restrictions**: Prevent invalid selections
|
|
554
|
+
|
|
555
|
+
```tsx
|
|
556
|
+
// ✅ Good: Reasonable constraints for booking
|
|
557
|
+
<Calendar
|
|
558
|
+
disablePast
|
|
559
|
+
maxDate={new Date(Date.now() + 90 * 24 * 60 * 60 * 1000)} // 90 days ahead
|
|
560
|
+
shouldDisableDate={(date) => [0, 6].includes(date.getDay())} // No weekends
|
|
561
|
+
/>
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
4. **Handle the array value format correctly**: Calendar always uses `[start, end]` format
|
|
565
|
+
|
|
566
|
+
```tsx
|
|
567
|
+
// ✅ Good: Proper value handling
|
|
568
|
+
const [date, setDate] = useState<Date | null>(null);
|
|
569
|
+
<Calendar
|
|
570
|
+
value={date ? [date, undefined] : undefined}
|
|
571
|
+
onChange={(dates) => setDate(dates[0] || null)}
|
|
572
|
+
/>
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### ❌ Don't
|
|
576
|
+
|
|
577
|
+
1. **Don't use Calendar when DatePicker suffices**: For standard form inputs, use the picker components
|
|
578
|
+
|
|
579
|
+
```tsx
|
|
580
|
+
// ❌ Bad: Using Calendar for simple date input
|
|
581
|
+
<form>
|
|
582
|
+
<Calendar onChange={handleChange} /> {/* Missing label, not form-friendly */}
|
|
583
|
+
</form>
|
|
584
|
+
|
|
585
|
+
// ✅ Good: Use DatePicker
|
|
586
|
+
<DatePicker label="Date" value={value} onChange={handleChange} />
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
2. **Don't forget to handle both elements of the array**: Even for single selection
|
|
590
|
+
|
|
591
|
+
```tsx
|
|
592
|
+
// ❌ Bad: Ignoring array format
|
|
593
|
+
<Calendar onChange={(date) => setDate(date)} />
|
|
594
|
+
|
|
595
|
+
// ✅ Good: Proper array handling
|
|
596
|
+
<Calendar onChange={(dates) => setDate(dates[0])} />
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
3. **Don't mix Date objects with string values**: Calendar uses Date objects
|
|
600
|
+
|
|
601
|
+
```tsx
|
|
602
|
+
// ❌ Bad: Using string values
|
|
603
|
+
<Calendar value={["2024-04-15", undefined]} />
|
|
604
|
+
|
|
605
|
+
// ✅ Good: Using Date objects
|
|
606
|
+
<Calendar value={[new Date("2024-04-15"), undefined]} />
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
4. **Don't forget locale for international users**: Set appropriate locale
|
|
610
|
+
|
|
611
|
+
## Performance Considerations
|
|
612
|
+
|
|
613
|
+
### Memoize shouldDisableDate
|
|
614
|
+
|
|
615
|
+
For complex date validation, memoize the function:
|
|
616
|
+
|
|
617
|
+
```tsx
|
|
618
|
+
const shouldDisableDate = useCallback((date: Date) => {
|
|
619
|
+
// Check if weekend
|
|
620
|
+
if ([0, 6].includes(date.getDay())) return true;
|
|
621
|
+
|
|
622
|
+
// Check against holiday list
|
|
623
|
+
const dateString = date.toISOString().split('T')[0];
|
|
624
|
+
return holidays.includes(dateString);
|
|
625
|
+
}, [holidays]); // Only recreate when holidays change
|
|
626
|
+
|
|
627
|
+
<Calendar shouldDisableDate={shouldDisableDate} />
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
### Avoid Inline Object Creation
|
|
631
|
+
|
|
632
|
+
When using controlled mode, avoid creating new arrays on every render:
|
|
633
|
+
|
|
634
|
+
```tsx
|
|
635
|
+
// ❌ Bad: New array on every render
|
|
636
|
+
<Calendar value={[date, undefined]} />
|
|
637
|
+
|
|
638
|
+
// ✅ Good: Memoize the value
|
|
639
|
+
const calendarValue = useMemo(
|
|
640
|
+
() => (date ? [date, undefined] : undefined),
|
|
641
|
+
[date]
|
|
642
|
+
);
|
|
643
|
+
<Calendar value={calendarValue} />
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### Optimize Event Handlers
|
|
647
|
+
|
|
648
|
+
```tsx
|
|
649
|
+
const handleChange = useCallback((dates: [Date | undefined, Date | undefined]) => {
|
|
650
|
+
setSelectedDate(dates[0] || null);
|
|
651
|
+
}, []);
|
|
652
|
+
|
|
653
|
+
<Calendar onChange={handleChange} />
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### Lazy Date Calculations
|
|
657
|
+
|
|
658
|
+
For complex availability calculations:
|
|
659
|
+
|
|
660
|
+
```tsx
|
|
661
|
+
const disabledDates = useMemo(() => {
|
|
662
|
+
// Pre-calculate disabled dates once
|
|
663
|
+
return new Set(
|
|
664
|
+
blockedDates.map((d) => d.toDateString())
|
|
665
|
+
);
|
|
666
|
+
}, [blockedDates]);
|
|
667
|
+
|
|
668
|
+
const shouldDisableDate = useCallback((date: Date) => {
|
|
669
|
+
return disabledDates.has(date.toDateString());
|
|
670
|
+
}, [disabledDates]);
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
Calendar is a foundational component for building custom date selection interfaces. For most use cases, prefer the higher-level picker components (DatePicker, DateRangePicker, MonthPicker) which provide built-in form integration, labels, and text input. Use Calendar directly when you need full control over the calendar UI or are building specialized date-related features.
|