@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.
Files changed (30) hide show
  1. package/dist/components/ProfileMenu/ProfileMenu.d.ts +1 -1
  2. package/dist/components/data-display/Markdown.md +832 -0
  3. package/dist/components/feedback/Dialog.md +605 -3
  4. package/dist/components/feedback/Modal.md +656 -24
  5. package/dist/components/feedback/llms.txt +1 -1
  6. package/dist/components/inputs/Autocomplete.md +734 -2
  7. package/dist/components/inputs/Calendar.md +655 -1
  8. package/dist/components/inputs/DatePicker.md +699 -3
  9. package/dist/components/inputs/DateRangePicker.md +815 -1
  10. package/dist/components/inputs/MonthPicker.md +626 -4
  11. package/dist/components/inputs/MonthRangePicker.md +682 -4
  12. package/dist/components/inputs/Select.md +600 -0
  13. package/dist/components/layout/Container.md +507 -0
  14. package/dist/components/navigation/Breadcrumbs.md +582 -0
  15. package/dist/components/navigation/IconMenuButton.md +693 -0
  16. package/dist/components/navigation/InsetDrawer.md +1150 -3
  17. package/dist/components/navigation/Link.md +526 -0
  18. package/dist/components/navigation/MenuButton.md +632 -0
  19. package/dist/components/navigation/NavigationGroup.md +401 -1
  20. package/dist/components/navigation/NavigationItem.md +311 -0
  21. package/dist/components/navigation/Navigator.md +373 -0
  22. package/dist/components/navigation/Pagination.md +521 -0
  23. package/dist/components/navigation/ProfileMenu.md +605 -0
  24. package/dist/components/navigation/Tabs.md +609 -7
  25. package/dist/components/surfaces/Accordions.md +947 -3
  26. package/dist/index.cjs +3 -1
  27. package/dist/index.js +3 -1
  28. package/dist/llms.txt +1 -1
  29. package/framer/index.js +1 -1
  30. package/package.json +3 -2
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## Introduction
4
4
 
5
+ DateRangePicker is a form input component that allows users to select a date range (start date and end date) from a calendar popup or by typing directly into the input field. It provides flexible date formatting options, date range restrictions, and supports both controlled and uncontrolled modes. DateRangePicker is ideal for booking systems, reporting filters, scheduling, and any scenario requiring a start-to-end date selection.
6
+
5
7
  ```tsx
6
8
  <DateRangePicker onChange={onChange} />
7
9
  ```
@@ -26,8 +28,47 @@
26
28
  | hideClearButton | — | — |
27
29
  | size | — | — |
28
30
 
31
+ > ⚠️ **Usage Warning** ⚠️
32
+ >
33
+ > DateRangePicker involves complex date range handling:
34
+ >
35
+ > - **Value format**: Values use the format `"startDate - endDate"` (e.g., `"2024/04/01 - 2024/04/15"`)
36
+ > - **Format consistency**: Both dates must match the `format` prop
37
+ > - **Start/End validation**: Ensure start date is before end date
38
+ > - **Range span**: Consider maximum range limits for performance and UX
39
+
40
+ ## Usage
41
+
42
+ ```tsx
43
+ import { DateRangePicker } from '@ceed/ads';
44
+
45
+ function DateRangeForm() {
46
+ const [dateRange, setDateRange] = useState('');
47
+
48
+ return (
49
+ <DateRangePicker
50
+ label="Select Date Range"
51
+ value={dateRange}
52
+ onChange={(e) => setDateRange(e.target.value)}
53
+ />
54
+ );
55
+ }
56
+ ```
57
+
58
+ ## Examples
59
+
60
+ ### Playground
61
+
62
+ Interactive example with all controls.
63
+
64
+ ```tsx
65
+ <DateRangePicker onChange={onChange} />
66
+ ```
67
+
29
68
  ### Sizes
30
69
 
70
+ DateRangePicker supports three sizes for different layouts.
71
+
31
72
  ```tsx
32
73
  <Stack gap={2}>
33
74
  <DateRangePicker size="sm" />
@@ -38,6 +79,8 @@
38
79
 
39
80
  ### Disabled
40
81
 
82
+ Prevent user interaction when disabled.
83
+
41
84
  ```tsx
42
85
  <DateRangePicker
43
86
  onChange={onChange}
@@ -45,7 +88,9 @@
45
88
  />
46
89
  ```
47
90
 
48
- ### WithLabel
91
+ ### With Label
92
+
93
+ Add a label above the date range picker.
49
94
 
50
95
  ```tsx
51
96
  <DateRangePicker
@@ -53,3 +98,772 @@
53
98
  label="Date Range"
54
99
  />
55
100
  ```
101
+
102
+ ### With Helper Text
103
+
104
+ Provide additional guidance below the input.
105
+
106
+ ```tsx
107
+ <DateRangePicker
108
+ onChange={onChange}
109
+ label="Date"
110
+ helperText="Please select a date"
111
+ />
112
+ ```
113
+
114
+ ### Error State
115
+
116
+ Show validation errors with error styling.
117
+
118
+ ```tsx
119
+ <DateRangePicker
120
+ onChange={onChange}
121
+ label="Date"
122
+ helperText="Please select a date"
123
+ error
124
+ />
125
+ ```
126
+
127
+ ### Required Field
128
+
129
+ Mark the field as required in forms.
130
+
131
+ ```tsx
132
+ <DateRangePicker
133
+ onChange={onChange}
134
+ label="Label"
135
+ helperText="I'm helper text"
136
+ required
137
+ />
138
+ ```
139
+
140
+ ### Minimum Date
141
+
142
+ Restrict selection to dates on or after a minimum date.
143
+
144
+ ```tsx
145
+ <DateRangePicker
146
+ onChange={onChange}
147
+ minDate="2024-04-10"
148
+ />
149
+ ```
150
+
151
+ ### Maximum Date
152
+
153
+ Restrict selection to dates on or before a maximum date.
154
+
155
+ ```tsx
156
+ <DateRangePicker
157
+ onChange={onChange}
158
+ maxDate="2024-04-10"
159
+ />
160
+ ```
161
+
162
+ ### Disable Future Dates
163
+
164
+ Prevent selection of dates in the future.
165
+
166
+ ```tsx
167
+ <DateRangePicker
168
+ onChange={onChange}
169
+ disableFuture
170
+ />
171
+ ```
172
+
173
+ ### Disable Past Dates
174
+
175
+ Prevent selection of dates in the past.
176
+
177
+ ```tsx
178
+ <DateRangePicker
179
+ onChange={onChange}
180
+ disablePast
181
+ />
182
+ ```
183
+
184
+ ### Controlled
185
+
186
+ Parent component manages the date range state.
187
+
188
+ ```tsx
189
+ <Stack gap={2}>
190
+ <DateRangePicker {...args} onChange={e => {
191
+ args.onChange?.(e);
192
+ setValue(e.target.value);
193
+ }} value={value} />
194
+ <Button onClick={() => {
195
+ const [start, end] = value.split(' - ');
196
+ function shiftMonth(dateString: string) {
197
+ const currentValue = new Date(dateString);
198
+ currentValue.setMonth(currentValue.getMonth() + 1);
199
+ const year = currentValue.getFullYear();
200
+ const month = String(currentValue.getMonth() + 1).padStart(2, '0');
201
+ const day = String(currentValue.getDate()).padStart(2, '0');
202
+ return `${year}/${month}/${day}`;
203
+ }
204
+ setValue(`${shiftMonth(start)} - ${shiftMonth(end)}`);
205
+ }}>
206
+ Shift Month
207
+ </Button>
208
+ </Stack>
209
+ ```
210
+
211
+ ### Uncontrolled
212
+
213
+ Component manages its own state internally.
214
+
215
+ ```tsx
216
+ <DateRangePicker
217
+ onChange={onChange}
218
+ label="Uncontrolled DateRangePicker"
219
+ helperText="Please select a date"
220
+ defaultValue="2024/04/01 - 2025/04/01"
221
+ />
222
+ ```
223
+
224
+ ### With Formats
225
+
226
+ Different value formats for the `onChange` event.
227
+
228
+ ```tsx
229
+ <Stack gap={2}>
230
+ <DateRangePicker {...args} value={value1} label="YYYY.MM.DD" name="YYYY.MM.DD" format="YYYY.MM.DD" onChange={e => {
231
+ setValue1(e.target.value);
232
+ args.onChange?.(e);
233
+ }} />
234
+ <DateRangePicker {...args} value={value2} label="YYYY/MM/DD" name="YYYY/MM/DD" format="YYYY/MM/DD" onChange={e => {
235
+ setValue2(e.target.value);
236
+ args.onChange?.(e);
237
+ }} />
238
+ <DateRangePicker {...args} value={value3} label="MM/DD/YYYY" name="MM/DD/YYYY" format="MM/DD/YYYY" onChange={e => {
239
+ setValue3(e.target.value);
240
+ args.onChange?.(e);
241
+ }} />
242
+ <DateRangePicker {...args} value={value4} label="YYYY-MM-DD" name="YYYY-MM-DD" format="YYYY-MM-DD" onChange={e => {
243
+ setValue4(e.target.value);
244
+ args.onChange?.(e);
245
+ }} />
246
+ <DateRangePicker {...args} value={value5} label="DD/MM/YYYY" name="DD/MM/YYYY" format="DD/MM/YYYY" onChange={e => {
247
+ setValue5(e.target.value);
248
+ args.onChange?.(e);
249
+ }} />
250
+ <DateRangePicker {...args} value={value6} label="DD.MM.YYYY" name="DD.MM.YYYY" format="DD.MM.YYYY" onChange={e => {
251
+ setValue6(e.target.value);
252
+ args.onChange?.(e);
253
+ }} />
254
+ </Stack>
255
+ ```
256
+
257
+ ### With Display Formats
258
+
259
+ Different display formats shown in the input field.
260
+
261
+ ```tsx
262
+ <Stack gap={2}>
263
+ <DateRangePicker {...args} value={value1} label="YYYY.MM.DD" name="YYYY.MM.DD" displayFormat="YYYY.MM.DD" onChange={e => {
264
+ setValue1(e.target.value);
265
+ args.onChange?.(e);
266
+ }} />
267
+ <DateRangePicker {...args} value={value2} label="YYYY/MM/DD" name="YYYY/MM/DD" displayFormat="YYYY/MM/DD" onChange={e => {
268
+ setValue2(e.target.value);
269
+ args.onChange?.(e);
270
+ }} />
271
+ <DateRangePicker {...args} value={value3} label="MM/DD/YYYY" name="MM/DD/YYYY" displayFormat="MM/DD/YYYY" onChange={e => {
272
+ setValue3(e.target.value);
273
+ args.onChange?.(e);
274
+ }} />
275
+ <DateRangePicker {...args} value={value4} label="YYYY-MM-DD" name="YYYY-MM-DD" displayFormat="YYYY-MM-DD" onChange={e => {
276
+ setValue4(e.target.value);
277
+ args.onChange?.(e);
278
+ }} />
279
+ <DateRangePicker {...args} value={value5} label="DD/MM/YYYY" name="DD/MM/YYYY" displayFormat="DD/MM/YYYY" onChange={e => {
280
+ setValue5(e.target.value);
281
+ args.onChange?.(e);
282
+ }} />
283
+ <DateRangePicker {...args} value={value6} label="DD.MM.YYYY" name="DD.MM.YYYY" displayFormat="DD.MM.YYYY" onChange={e => {
284
+ setValue6(e.target.value);
285
+ args.onChange?.(e);
286
+ }} />
287
+ </Stack>
288
+ ```
289
+
290
+ ### Input Read Only
291
+
292
+ Allow calendar selection only, prevent typing.
293
+
294
+ ```tsx
295
+ <DateRangePicker
296
+ onChange={onChange}
297
+ defaultValue="2024/05/03 - 2024/06/03"
298
+ inputReadOnly
299
+ />
300
+ ```
301
+
302
+ ### Read Only
303
+
304
+ Fully read-only state with no interaction.
305
+
306
+ ```tsx
307
+ <DateRangePicker
308
+ onChange={onChange}
309
+ value="2024/05/03 - 2024/06/03"
310
+ readOnly
311
+ />
312
+ ```
313
+
314
+ ### Hide Clear Button
315
+
316
+ Remove the clear button from the calendar popup.
317
+
318
+ ```tsx
319
+ <DateRangePicker
320
+ onChange={onChange}
321
+ defaultValue="2024/05/03 - 2024/06/03"
322
+ hideClearButton
323
+ />
324
+ ```
325
+
326
+ ### With Reset Button
327
+
328
+ Example with an external reset button.
329
+
330
+ ```tsx
331
+ <div style={{
332
+ display: 'flex',
333
+ gap: '10px'
334
+ }}>
335
+ <DateRangePicker {...props} value={value} onChange={event => {
336
+ setValue(event.target.value);
337
+ }} />
338
+ <Button onClick={() => setValue('')}>Reset</Button>
339
+ </div>
340
+ ```
341
+
342
+ ## When to Use
343
+
344
+ ### ✅ Good Use Cases
345
+
346
+ - **Booking systems**: Hotel check-in/out, car rental periods
347
+ - **Report filtering**: Date range filters for analytics and reports
348
+ - **Event scheduling**: Conference dates, project timelines
349
+ - **Leave requests**: Vacation start and end dates
350
+ - **Subscription periods**: Billing cycles, membership durations
351
+ - **Data exports**: Selecting date ranges for exporting data
352
+
353
+ ### ❌ When Not to Use
354
+
355
+ - **Single date selection**: Use DatePicker instead
356
+ - **Month/Year ranges**: Use MonthRangePicker for month-level granularity
357
+ - **Predefined periods**: For "Last 7 days", "This month", use dropdown selection
358
+ - **Time ranges**: For time-based ranges, use dedicated time components
359
+ - **Recurring dates**: For repeating schedules, consider a custom solution
360
+
361
+ ## Common Use Cases
362
+
363
+ ### Booking Form
364
+
365
+ ```tsx
366
+ function BookingForm() {
367
+ const [dates, setDates] = useState('');
368
+ const [error, setError] = useState('');
369
+
370
+ const validateRange = (value) => {
371
+ if (!value) return 'Please select dates';
372
+ const [start, end] = value.split(' - ');
373
+ const startDate = new Date(start);
374
+ const endDate = new Date(end);
375
+ const days = (endDate - startDate) / (1000 * 60 * 60 * 24);
376
+ if (days > 30) return 'Maximum stay is 30 days';
377
+ if (days < 1) return 'Minimum stay is 1 night';
378
+ return '';
379
+ };
380
+
381
+ const handleChange = (e) => {
382
+ const value = e.target.value;
383
+ setDates(value);
384
+ setError(validateRange(value));
385
+ };
386
+
387
+ return (
388
+ <form>
389
+ <DateRangePicker
390
+ label="Check-in / Check-out"
391
+ value={dates}
392
+ onChange={handleChange}
393
+ error={!!error}
394
+ helperText={error || 'Select your stay dates'}
395
+ disablePast
396
+ required
397
+ />
398
+ <Button type="submit" disabled={!!error || !dates}>
399
+ Search Availability
400
+ </Button>
401
+ </form>
402
+ );
403
+ }
404
+ ```
405
+
406
+ ### Report Date Filter
407
+
408
+ ```tsx
409
+ function ReportFilters({ onFilter }) {
410
+ const [dateRange, setDateRange] = useState('');
411
+
412
+ const handleApply = () => {
413
+ if (!dateRange) return;
414
+ const [startDate, endDate] = dateRange.split(' - ');
415
+ onFilter({ startDate, endDate });
416
+ };
417
+
418
+ return (
419
+ <Stack direction="row" gap={2} alignItems="flex-end">
420
+ <DateRangePicker
421
+ label="Report Period"
422
+ value={dateRange}
423
+ onChange={(e) => setDateRange(e.target.value)}
424
+ disableFuture
425
+ helperText="Select date range for the report"
426
+ />
427
+ <Button onClick={handleApply}>Apply Filter</Button>
428
+ </Stack>
429
+ );
430
+ }
431
+ ```
432
+
433
+ ### Leave Request Form
434
+
435
+ ```tsx
436
+ function LeaveRequestForm({ minDate, maxDate }) {
437
+ const [leaveDates, setLeaveDates] = useState('');
438
+ const [leaveType, setLeaveType] = useState('annual');
439
+
440
+ // Calculate business days
441
+ const getBusinessDays = (dateRange) => {
442
+ if (!dateRange) return 0;
443
+ const [start, end] = dateRange.split(' - ').map(d => new Date(d));
444
+ let count = 0;
445
+ const current = new Date(start);
446
+ while (current <= end) {
447
+ const day = current.getDay();
448
+ if (day !== 0 && day !== 6) count++;
449
+ current.setDate(current.getDate() + 1);
450
+ }
451
+ return count;
452
+ };
453
+
454
+ const businessDays = getBusinessDays(leaveDates);
455
+
456
+ return (
457
+ <form>
458
+ <Select
459
+ label="Leave Type"
460
+ value={leaveType}
461
+ onChange={(e) => setLeaveType(e.target.value)}
462
+ >
463
+ <Option value="annual">Annual Leave</Option>
464
+ <Option value="sick">Sick Leave</Option>
465
+ <Option value="personal">Personal Leave</Option>
466
+ </Select>
467
+
468
+ <DateRangePicker
469
+ label="Leave Period"
470
+ value={leaveDates}
471
+ onChange={(e) => setLeaveDates(e.target.value)}
472
+ minDate={minDate}
473
+ maxDate={maxDate}
474
+ disablePast
475
+ helperText={businessDays > 0 ? `${businessDays} business days` : 'Select dates'}
476
+ required
477
+ />
478
+
479
+ <Button type="submit">Submit Request</Button>
480
+ </form>
481
+ );
482
+ }
483
+ ```
484
+
485
+ ### Date Range with Presets
486
+
487
+ ```tsx
488
+ function DateRangeWithPresets() {
489
+ const [dateRange, setDateRange] = useState('');
490
+
491
+ const formatDate = (date) => {
492
+ const year = date.getFullYear();
493
+ const month = String(date.getMonth() + 1).padStart(2, '0');
494
+ const day = String(date.getDate()).padStart(2, '0');
495
+ return `${year}/${month}/${day}`;
496
+ };
497
+
498
+ const applyPreset = (preset) => {
499
+ const today = new Date();
500
+ let start, end;
501
+
502
+ switch (preset) {
503
+ case 'last7':
504
+ end = today;
505
+ start = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
506
+ break;
507
+ case 'last30':
508
+ end = today;
509
+ start = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000);
510
+ break;
511
+ case 'thisMonth':
512
+ start = new Date(today.getFullYear(), today.getMonth(), 1);
513
+ end = new Date(today.getFullYear(), today.getMonth() + 1, 0);
514
+ break;
515
+ case 'lastMonth':
516
+ start = new Date(today.getFullYear(), today.getMonth() - 1, 1);
517
+ end = new Date(today.getFullYear(), today.getMonth(), 0);
518
+ break;
519
+ default:
520
+ return;
521
+ }
522
+
523
+ setDateRange(`${formatDate(start)} - ${formatDate(end)}`);
524
+ };
525
+
526
+ return (
527
+ <Stack gap={2}>
528
+ <Stack direction="row" gap={1}>
529
+ <Button variant="plain" onClick={() => applyPreset('last7')}>
530
+ Last 7 Days
531
+ </Button>
532
+ <Button variant="plain" onClick={() => applyPreset('last30')}>
533
+ Last 30 Days
534
+ </Button>
535
+ <Button variant="plain" onClick={() => applyPreset('thisMonth')}>
536
+ This Month
537
+ </Button>
538
+ <Button variant="plain" onClick={() => applyPreset('lastMonth')}>
539
+ Last Month
540
+ </Button>
541
+ </Stack>
542
+ <DateRangePicker
543
+ label="Custom Range"
544
+ value={dateRange}
545
+ onChange={(e) => setDateRange(e.target.value)}
546
+ disableFuture
547
+ />
548
+ </Stack>
549
+ );
550
+ }
551
+ ```
552
+
553
+ ### API Integration
554
+
555
+ ```tsx
556
+ function DataExport() {
557
+ const [dateRange, setDateRange] = useState('');
558
+ const [isExporting, setIsExporting] = useState(false);
559
+
560
+ const handleExport = async () => {
561
+ if (!dateRange) return;
562
+
563
+ setIsExporting(true);
564
+ const [startDate, endDate] = dateRange.split(' - ');
565
+
566
+ try {
567
+ const response = await api.exportData({
568
+ startDate: startDate.replace(/\//g, '-'), // Convert to API format
569
+ endDate: endDate.replace(/\//g, '-'),
570
+ });
571
+ downloadFile(response.data);
572
+ } finally {
573
+ setIsExporting(false);
574
+ }
575
+ };
576
+
577
+ return (
578
+ <Stack gap={2}>
579
+ <DateRangePicker
580
+ label="Export Period"
581
+ value={dateRange}
582
+ onChange={(e) => setDateRange(e.target.value)}
583
+ format="YYYY/MM/DD"
584
+ disableFuture
585
+ required
586
+ />
587
+ <Button
588
+ onClick={handleExport}
589
+ loading={isExporting}
590
+ disabled={!dateRange}
591
+ >
592
+ Export Data
593
+ </Button>
594
+ </Stack>
595
+ );
596
+ }
597
+ ```
598
+
599
+ ## Props and Customization
600
+
601
+ ### Key Props
602
+
603
+ | Prop | Type | Default | Description |
604
+ | ----------------- | ----------------------------------------------------------- | ---------------- | ------------------------------------------ |
605
+ | `value` | `string` | - | Controlled value (`"startDate - endDate"`) |
606
+ | `defaultValue` | `string` | - | Default value for uncontrolled mode |
607
+ | `onChange` | `(e: { target: { name?: string; value: string } }) => void` | - | Change handler |
608
+ | `format` | `string` | `'YYYY/MM/DD'` | Format for `value` and `onChange` |
609
+ | `displayFormat` | `string` | Same as `format` | Format displayed in the input |
610
+ | `label` | `string` | - | Label text |
611
+ | `helperText` | `string` | - | Helper text below input |
612
+ | `error` | `boolean` | `false` | Error state |
613
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Component size |
614
+ | `disabled` | `boolean` | `false` | Disabled state |
615
+ | `required` | `boolean` | `false` | Required field indicator |
616
+ | `minDate` | `string` | - | Minimum selectable date |
617
+ | `maxDate` | `string` | - | Maximum selectable date |
618
+ | `disableFuture` | `boolean` | `false` | Disable all future dates |
619
+ | `disablePast` | `boolean` | `false` | Disable all past dates |
620
+ | `inputReadOnly` | `boolean` | `false` | Prevent typing, calendar only |
621
+ | `readOnly` | `boolean` | `false` | Fully read-only |
622
+ | `hideClearButton` | `boolean` | `false` | Hide clear button in calendar |
623
+
624
+ ### Value Format
625
+
626
+ The value is always a string with start and end dates separated by `-`:
627
+
628
+ ```tsx
629
+ // Value format: "startDate - endDate"
630
+ const value = "2024/04/01 - 2024/04/15";
631
+
632
+ // Parsing the value
633
+ const [startDate, endDate] = value.split(' - ');
634
+ console.log(startDate); // "2024/04/01"
635
+ console.log(endDate); // "2024/04/15"
636
+ ```
637
+
638
+ ### Format vs DisplayFormat
639
+
640
+ ```tsx
641
+ // format: Affects the value in onChange
642
+ // displayFormat: Affects what users see in the input
643
+
644
+ <DateRangePicker
645
+ format="YYYY-MM-DD" // onChange returns "2024-04-01 - 2024-04-15"
646
+ displayFormat="MM/DD/YYYY" // Input shows "04/01/2024 - 04/15/2024"
647
+ onChange={(e) => {
648
+ console.log(e.target.value); // "2024-04-01 - 2024-04-15"
649
+ }}
650
+ />
651
+ ```
652
+
653
+ ### Supported Format Tokens
654
+
655
+ | Token | Description | Example |
656
+ | ------ | ------------- | ------- |
657
+ | `YYYY` | 4-digit year | 2024 |
658
+ | `MM` | 2-digit month | 04 |
659
+ | `DD` | 2-digit day | 15 |
660
+
661
+ ### Controlled vs Uncontrolled
662
+
663
+ ```tsx
664
+ // Uncontrolled - component manages state
665
+ <DateRangePicker
666
+ defaultValue="2024/04/01 - 2024/04/15"
667
+ onChange={(e) => console.log(e.target.value)}
668
+ />
669
+
670
+ // Controlled - you manage state
671
+ const [dateRange, setDateRange] = useState('2024/04/01 - 2024/04/15');
672
+ <DateRangePicker
673
+ value={dateRange}
674
+ onChange={(e) => setDateRange(e.target.value)}
675
+ />
676
+ ```
677
+
678
+ ## Accessibility
679
+
680
+ DateRangePicker includes built-in accessibility features:
681
+
682
+ ### ARIA Attributes
683
+
684
+ - Input has proper `role="textbox"`
685
+ - Calendar button has `aria-label="Toggle Calendar"`
686
+ - Calendar popup uses `role="tooltip"` with proper labeling
687
+ - Date buttons announce the full date to screen readers
688
+
689
+ ### Keyboard Navigation
690
+
691
+ - **Tab**: Move focus between input and calendar button
692
+ - **Enter/Space**: Open calendar when focused on button
693
+ - **Arrow Keys**: Navigate within calendar
694
+ - **Escape**: Close calendar popup
695
+ - **Enter**: Select focused date
696
+
697
+ ### Screen Reader Support
698
+
699
+ ```tsx
700
+ // Range selection is announced
701
+ // First click: "Start date: April 1, 2024"
702
+ // Second click: "End date: April 15, 2024"
703
+
704
+ // Navigation buttons are descriptive
705
+ <button aria-label="Previous Month">←</button>
706
+ <button aria-label="Next Month">→</button>
707
+ ```
708
+
709
+ ### Focus Management
710
+
711
+ - Focus moves to calendar when opened
712
+ - Focus returns to input when calendar closes
713
+ - Clear visual focus indicators on all interactive elements
714
+ - Range selection provides visual feedback between start and end dates
715
+
716
+ ## Best Practices
717
+
718
+ ### ✅ Do
719
+
720
+ 1. **Validate date ranges**: Ensure start date is before end date
721
+
722
+ ```tsx
723
+ // ✅ Good: Validate the range
724
+ const validateRange = (value) => {
725
+ const [start, end] = value.split(' - ');
726
+ if (new Date(start) > new Date(end)) {
727
+ return 'Start date must be before end date';
728
+ }
729
+ return '';
730
+ };
731
+ ```
732
+
733
+ 2. **Show range duration**: Help users understand the selected period
734
+
735
+ ```tsx
736
+ // ✅ Good: Display duration
737
+ const getDuration = (value) => {
738
+ if (!value) return '';
739
+ const [start, end] = value.split(' - ').map(d => new Date(d));
740
+ const days = Math.ceil((end - start) / (1000 * 60 * 60 * 24));
741
+ return `${days} day${days !== 1 ? 's' : ''}`;
742
+ };
743
+
744
+ <DateRangePicker helperText={getDuration(dateRange) || 'Select dates'} />
745
+ ```
746
+
747
+ 3. **Set reasonable limits**: Prevent excessively long ranges
748
+
749
+ ```tsx
750
+ // ✅ Good: Limit range span
751
+ const MAX_DAYS = 90;
752
+ const validateMaxRange = (value) => {
753
+ const [start, end] = value.split(' - ').map(d => new Date(d));
754
+ const days = (end - start) / (1000 * 60 * 60 * 24);
755
+ return days <= MAX_DAYS;
756
+ };
757
+ ```
758
+
759
+ 4. **Use consistent formats**: Match your application's locale
760
+
761
+ ```tsx
762
+ // ✅ Good: Consistent with app locale
763
+ <DateRangePicker
764
+ format="YYYY-MM-DD" // API format
765
+ displayFormat="MM/DD/YYYY" // US locale display
766
+ />
767
+ ```
768
+
769
+ ### ❌ Don't
770
+
771
+ 1. **Don't allow invalid ranges**: Always validate start \< end
772
+
773
+ ```tsx
774
+ // ❌ Bad: No validation
775
+ <DateRangePicker onChange={(e) => setDateRange(e.target.value)} />
776
+
777
+ // ✅ Good: Validate range
778
+ <DateRangePicker
779
+ onChange={(e) => {
780
+ if (isValidRange(e.target.value)) {
781
+ setDateRange(e.target.value);
782
+ }
783
+ }}
784
+ />
785
+ ```
786
+
787
+ 2. **Don't use inconsistent value formats**: Match value to format prop
788
+
789
+ ```tsx
790
+ // ❌ Bad: Mismatched formats
791
+ <DateRangePicker
792
+ format="YYYY/MM/DD"
793
+ value="04/01/2024 - 04/15/2024" // Wrong!
794
+ />
795
+
796
+ // ✅ Good: Matching formats
797
+ <DateRangePicker
798
+ format="YYYY/MM/DD"
799
+ value="2024/04/01 - 2024/04/15"
800
+ />
801
+ ```
802
+
803
+ 3. **Don't forget mobile users**: Use `inputReadOnly` for better mobile experience
804
+
805
+ 4. **Don't hide important context**: Show duration or business days when relevant
806
+
807
+ ## Performance Considerations
808
+
809
+ ### Memoize Handlers
810
+
811
+ When using DateRangePicker in complex forms:
812
+
813
+ ```tsx
814
+ const handleChange = useCallback((e) => {
815
+ setDateRange(e.target.value);
816
+ }, []);
817
+
818
+ <DateRangePicker value={dateRange} onChange={handleChange} />
819
+ ```
820
+
821
+ ### Parse Values Efficiently
822
+
823
+ When processing date range values:
824
+
825
+ ```tsx
826
+ // Memoize parsed values
827
+ const { startDate, endDate, duration } = useMemo(() => {
828
+ if (!dateRange) return { startDate: null, endDate: null, duration: 0 };
829
+
830
+ const [start, end] = dateRange.split(' - ');
831
+ const startDate = new Date(start);
832
+ const endDate = new Date(end);
833
+ const duration = Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24));
834
+
835
+ return { startDate, endDate, duration };
836
+ }, [dateRange]);
837
+ ```
838
+
839
+ ### Format Conversion for APIs
840
+
841
+ When working with APIs that expect different formats:
842
+
843
+ ```tsx
844
+ function DateRangeField({ value, onChange, apiFormat = 'YYYY-MM-DD' }) {
845
+ const displayFormat = 'YYYY/MM/DD';
846
+
847
+ const convertToDisplay = (apiValue) => {
848
+ if (!apiValue) return '';
849
+ const [start, end] = apiValue.split(' - ');
850
+ return `${convertFormat(start, apiFormat, displayFormat)} - ${convertFormat(end, apiFormat, displayFormat)}`;
851
+ };
852
+
853
+ const convertToApi = (displayValue) => {
854
+ if (!displayValue) return '';
855
+ const [start, end] = displayValue.split(' - ');
856
+ return `${convertFormat(start, displayFormat, apiFormat)} - ${convertFormat(end, displayFormat, apiFormat)}`;
857
+ };
858
+
859
+ return (
860
+ <DateRangePicker
861
+ value={convertToDisplay(value)}
862
+ onChange={(e) => onChange(convertToApi(e.target.value))}
863
+ format={displayFormat}
864
+ />
865
+ );
866
+ }
867
+ ```
868
+
869
+ DateRangePicker is essential for scenarios requiring start and end date selection. Pay attention to date format consistency, validate ranges properly, and provide clear feedback to users about the selected period.