@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,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
|
-
###
|
|
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.
|