@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
|
+
MonthRangePicker is a form input component that allows users to select a range of months (start month and end month) from a calendar popup. Unlike DateRangePicker which provides day-level granularity, MonthRangePicker focuses on month-level selection. It is ideal for scenarios like fiscal period reporting, quarterly comparisons, subscription duration, or any use case requiring a span of months without specific day selection.
|
|
6
|
+
|
|
5
7
|
```tsx
|
|
6
8
|
<MonthRangePicker />
|
|
7
9
|
```
|
|
@@ -23,8 +25,47 @@
|
|
|
23
25
|
| format | — | — |
|
|
24
26
|
| onChange | — | — |
|
|
25
27
|
|
|
28
|
+
> ⚠️ **Usage Warning** ⚠️
|
|
29
|
+
>
|
|
30
|
+
> MonthRangePicker has unique formatting behaviors:
|
|
31
|
+
>
|
|
32
|
+
> - **Value format**: Values use `"YYYY/MM - YYYY/MM"` format (without day component)
|
|
33
|
+
> - **Range validation**: Ensure start month is before or equal to end month
|
|
34
|
+
> - **Format consistency**: Both months in the range must match the `format` prop
|
|
35
|
+
> - **Month boundaries**: Consider inclusive vs exclusive range interpretations
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { MonthRangePicker } from '@ceed/ads';
|
|
41
|
+
|
|
42
|
+
function MonthRangeForm() {
|
|
43
|
+
const [monthRange, setMonthRange] = useState('');
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<MonthRangePicker
|
|
47
|
+
label="Select Month Range"
|
|
48
|
+
value={monthRange}
|
|
49
|
+
onChange={(e) => setMonthRange(e.target.value)}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Examples
|
|
56
|
+
|
|
57
|
+
### Playground
|
|
58
|
+
|
|
59
|
+
Interactive example with all controls.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<MonthRangePicker />
|
|
63
|
+
```
|
|
64
|
+
|
|
26
65
|
### Sizes
|
|
27
66
|
|
|
67
|
+
MonthRangePicker supports three sizes for different layouts.
|
|
68
|
+
|
|
28
69
|
```tsx
|
|
29
70
|
<Stack gap={2}>
|
|
30
71
|
<MonthRangePicker size="sm" />
|
|
@@ -33,13 +74,17 @@
|
|
|
33
74
|
</Stack>
|
|
34
75
|
```
|
|
35
76
|
|
|
36
|
-
###
|
|
77
|
+
### With Label
|
|
78
|
+
|
|
79
|
+
Add a label above the month range picker.
|
|
37
80
|
|
|
38
81
|
```tsx
|
|
39
82
|
<MonthRangePicker label="Month Range" />
|
|
40
83
|
```
|
|
41
84
|
|
|
42
|
-
###
|
|
85
|
+
### With Helper Text
|
|
86
|
+
|
|
87
|
+
Provide additional guidance below the input.
|
|
43
88
|
|
|
44
89
|
```tsx
|
|
45
90
|
<MonthRangePicker
|
|
@@ -48,7 +93,9 @@
|
|
|
48
93
|
/>
|
|
49
94
|
```
|
|
50
95
|
|
|
51
|
-
### Error
|
|
96
|
+
### Error State
|
|
97
|
+
|
|
98
|
+
Show validation errors with error styling.
|
|
52
99
|
|
|
53
100
|
```tsx
|
|
54
101
|
<MonthRangePicker
|
|
@@ -58,7 +105,96 @@
|
|
|
58
105
|
/>
|
|
59
106
|
```
|
|
60
107
|
|
|
61
|
-
###
|
|
108
|
+
### Required Field
|
|
109
|
+
|
|
110
|
+
Mark the field as required in forms.
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
<MonthRangePicker
|
|
114
|
+
label="Label"
|
|
115
|
+
helperText="I'm helper text"
|
|
116
|
+
required
|
|
117
|
+
/>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Disabled
|
|
121
|
+
|
|
122
|
+
Prevent user interaction when disabled.
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
<MonthRangePicker disabled />
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Minimum Date
|
|
129
|
+
|
|
130
|
+
Restrict selection to months on or after a minimum date.
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
<MonthRangePicker minDate="2024-04-10" />
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Maximum Date
|
|
137
|
+
|
|
138
|
+
Restrict selection to months on or before a maximum date.
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
<MonthRangePicker maxDate="2024-04-10" />
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Disable Future
|
|
145
|
+
|
|
146
|
+
Prevent selection of months in the future.
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
<MonthRangePicker disableFuture />
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Disable Past
|
|
153
|
+
|
|
154
|
+
Prevent selection of months in the past.
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
<MonthRangePicker disablePast />
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Controlled
|
|
161
|
+
|
|
162
|
+
Parent component manages the month range state.
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
<Stack gap={2}>
|
|
166
|
+
<MonthRangePicker {...args} value={value} onChange={e => setValue(e.target.value)} />
|
|
167
|
+
<Button onClick={() => {
|
|
168
|
+
const [start, end] = value.split(' - ');
|
|
169
|
+
function shiftMonth(dateString: string) {
|
|
170
|
+
const currentValue = new Date(dateString);
|
|
171
|
+
currentValue.setFullYear(currentValue.getFullYear() + 1);
|
|
172
|
+
const year = currentValue.getFullYear();
|
|
173
|
+
const month = String(currentValue.getMonth() + 1).padStart(2, '0');
|
|
174
|
+
return `${year}/${month}`;
|
|
175
|
+
}
|
|
176
|
+
setValue(`${shiftMonth(start)} - ${shiftMonth(end)}`);
|
|
177
|
+
}}>
|
|
178
|
+
Shift Year
|
|
179
|
+
</Button>
|
|
180
|
+
</Stack>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Uncontrolled
|
|
184
|
+
|
|
185
|
+
Component manages its own state internally.
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
<MonthRangePicker
|
|
189
|
+
label="Uncontrolled MonthPicker"
|
|
190
|
+
helperText="Please select a date"
|
|
191
|
+
defaultValue="2024/04"
|
|
192
|
+
/>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### With Formats
|
|
196
|
+
|
|
197
|
+
Different value formats for regional preferences.
|
|
62
198
|
|
|
63
199
|
```tsx
|
|
64
200
|
<Stack gap={2}>
|
|
@@ -68,3 +204,545 @@
|
|
|
68
204
|
<MonthRangePicker {...args} value={value['YYYY-MM']} label="YYYY-MM" name="YYYY-MM" format="YYYY-MM" onChange={handleChange} />
|
|
69
205
|
</Stack>
|
|
70
206
|
```
|
|
207
|
+
|
|
208
|
+
### With Reset Button
|
|
209
|
+
|
|
210
|
+
Example with an external reset button.
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
<div style={{
|
|
214
|
+
display: 'flex',
|
|
215
|
+
gap: '10px'
|
|
216
|
+
}}>
|
|
217
|
+
<MonthRangePicker {...props} value={value} onChange={event => {
|
|
218
|
+
setValue(event.target.value);
|
|
219
|
+
}} />
|
|
220
|
+
<Button onClick={() => setValue('')}>Reset</Button>
|
|
221
|
+
</div>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## When to Use
|
|
225
|
+
|
|
226
|
+
### ✅ Good Use Cases
|
|
227
|
+
|
|
228
|
+
- **Fiscal period reports**: Q1-Q4 reports, fiscal year ranges
|
|
229
|
+
- **Subscription periods**: Multi-month subscription durations
|
|
230
|
+
- **Budget planning**: Budget allocation across multiple months
|
|
231
|
+
- **Historical comparisons**: Compare data across month spans
|
|
232
|
+
- **Seasonal analysis**: Analyzing seasonal trends
|
|
233
|
+
- **Project timelines**: Multi-month project duration
|
|
234
|
+
|
|
235
|
+
### ❌ When Not to Use
|
|
236
|
+
|
|
237
|
+
- **Single month selection**: Use MonthPicker instead
|
|
238
|
+
- **Specific date ranges**: Use DateRangePicker when days matter
|
|
239
|
+
- **Year selection**: Use a year picker or dropdown
|
|
240
|
+
- **Quarter selection**: Consider custom quarter selector
|
|
241
|
+
- **Indefinite periods**: Use separate start/end fields with "ongoing" option
|
|
242
|
+
|
|
243
|
+
## Common Use Cases
|
|
244
|
+
|
|
245
|
+
### Fiscal Period Report
|
|
246
|
+
|
|
247
|
+
```tsx
|
|
248
|
+
function FiscalReportSelector() {
|
|
249
|
+
const [period, setPeriod] = useState('');
|
|
250
|
+
|
|
251
|
+
const handleGenerate = () => {
|
|
252
|
+
if (!period) return;
|
|
253
|
+
const [startMonth, endMonth] = period.split(' - ');
|
|
254
|
+
generateReport({ startMonth, endMonth });
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<Stack gap={2}>
|
|
259
|
+
<MonthRangePicker
|
|
260
|
+
label="Fiscal Period"
|
|
261
|
+
value={period}
|
|
262
|
+
onChange={(e) => setPeriod(e.target.value)}
|
|
263
|
+
disableFuture
|
|
264
|
+
helperText="Select the fiscal period for the report"
|
|
265
|
+
/>
|
|
266
|
+
<Button onClick={handleGenerate} disabled={!period}>
|
|
267
|
+
Generate Report
|
|
268
|
+
</Button>
|
|
269
|
+
</Stack>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Subscription Duration
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
function SubscriptionForm() {
|
|
278
|
+
const [duration, setDuration] = useState('');
|
|
279
|
+
|
|
280
|
+
// Calculate months count
|
|
281
|
+
const getMonthsCount = (value) => {
|
|
282
|
+
if (!value) return 0;
|
|
283
|
+
const [start, end] = value.split(' - ');
|
|
284
|
+
const [startYear, startMonth] = start.split('/').map(Number);
|
|
285
|
+
const [endYear, endMonth] = end.split('/').map(Number);
|
|
286
|
+
return (endYear - startYear) * 12 + (endMonth - startMonth) + 1;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const monthsCount = getMonthsCount(duration);
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<form>
|
|
293
|
+
<MonthRangePicker
|
|
294
|
+
label="Subscription Period"
|
|
295
|
+
value={duration}
|
|
296
|
+
onChange={(e) => setDuration(e.target.value)}
|
|
297
|
+
disablePast
|
|
298
|
+
helperText={monthsCount > 0 ? `${monthsCount} month subscription` : 'Select period'}
|
|
299
|
+
required
|
|
300
|
+
/>
|
|
301
|
+
<Typography level="body-sm">
|
|
302
|
+
Total: ${monthsCount * 9.99}/period
|
|
303
|
+
</Typography>
|
|
304
|
+
<Button type="submit">Subscribe</Button>
|
|
305
|
+
</form>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Budget Planning
|
|
311
|
+
|
|
312
|
+
```tsx
|
|
313
|
+
function BudgetPlanningForm({ departments }) {
|
|
314
|
+
const [planningPeriod, setPlanningPeriod] = useState('');
|
|
315
|
+
const [allocations, setAllocations] = useState({});
|
|
316
|
+
|
|
317
|
+
// Default to next fiscal year
|
|
318
|
+
useEffect(() => {
|
|
319
|
+
const now = new Date();
|
|
320
|
+
const nextYear = now.getFullYear() + 1;
|
|
321
|
+
setPlanningPeriod(`${nextYear}/01 - ${nextYear}/12`);
|
|
322
|
+
}, []);
|
|
323
|
+
|
|
324
|
+
return (
|
|
325
|
+
<form>
|
|
326
|
+
<MonthRangePicker
|
|
327
|
+
label="Budget Planning Period"
|
|
328
|
+
value={planningPeriod}
|
|
329
|
+
onChange={(e) => setPlanningPeriod(e.target.value)}
|
|
330
|
+
format="YYYY/MM"
|
|
331
|
+
helperText="Select the months for budget allocation"
|
|
332
|
+
/>
|
|
333
|
+
|
|
334
|
+
{planningPeriod && departments.map((dept) => (
|
|
335
|
+
<CurrencyInput
|
|
336
|
+
key={dept.id}
|
|
337
|
+
label={dept.name}
|
|
338
|
+
value={allocations[dept.id] || ''}
|
|
339
|
+
onChange={(value) => setAllocations(prev => ({
|
|
340
|
+
...prev,
|
|
341
|
+
[dept.id]: value
|
|
342
|
+
}))}
|
|
343
|
+
/>
|
|
344
|
+
))}
|
|
345
|
+
|
|
346
|
+
<Button type="submit">Submit Budget Plan</Button>
|
|
347
|
+
</form>
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Year-over-Year Comparison
|
|
353
|
+
|
|
354
|
+
```tsx
|
|
355
|
+
function YearOverYearComparison() {
|
|
356
|
+
const [currentPeriod, setCurrentPeriod] = useState('');
|
|
357
|
+
const [previousPeriod, setPreviousPeriod] = useState('');
|
|
358
|
+
|
|
359
|
+
// Auto-calculate previous period
|
|
360
|
+
useEffect(() => {
|
|
361
|
+
if (!currentPeriod) return;
|
|
362
|
+
|
|
363
|
+
const [start, end] = currentPeriod.split(' - ');
|
|
364
|
+
const [startYear, startMonth] = start.split('/').map(Number);
|
|
365
|
+
const [endYear, endMonth] = end.split('/').map(Number);
|
|
366
|
+
|
|
367
|
+
const prevStart = `${startYear - 1}/${String(startMonth).padStart(2, '0')}`;
|
|
368
|
+
const prevEnd = `${endYear - 1}/${String(endMonth).padStart(2, '0')}`;
|
|
369
|
+
setPreviousPeriod(`${prevStart} - ${prevEnd}`);
|
|
370
|
+
}, [currentPeriod]);
|
|
371
|
+
|
|
372
|
+
return (
|
|
373
|
+
<Stack gap={2}>
|
|
374
|
+
<MonthRangePicker
|
|
375
|
+
label="Current Period"
|
|
376
|
+
value={currentPeriod}
|
|
377
|
+
onChange={(e) => setCurrentPeriod(e.target.value)}
|
|
378
|
+
disableFuture
|
|
379
|
+
/>
|
|
380
|
+
<MonthRangePicker
|
|
381
|
+
label="Previous Period (Auto-calculated)"
|
|
382
|
+
value={previousPeriod}
|
|
383
|
+
disabled
|
|
384
|
+
/>
|
|
385
|
+
<Button disabled={!currentPeriod}>
|
|
386
|
+
Compare Periods
|
|
387
|
+
</Button>
|
|
388
|
+
</Stack>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Seasonal Analysis
|
|
394
|
+
|
|
395
|
+
```tsx
|
|
396
|
+
function SeasonalAnalysis() {
|
|
397
|
+
const [period, setPeriod] = useState('');
|
|
398
|
+
|
|
399
|
+
const getSeason = (monthRange) => {
|
|
400
|
+
if (!monthRange) return '';
|
|
401
|
+
const [start, end] = monthRange.split(' - ');
|
|
402
|
+
const startMonth = parseInt(start.split('/')[1]);
|
|
403
|
+
const endMonth = parseInt(end.split('/')[1]);
|
|
404
|
+
|
|
405
|
+
// Simple season detection based on months
|
|
406
|
+
if (startMonth >= 3 && endMonth <= 5) return 'Spring';
|
|
407
|
+
if (startMonth >= 6 && endMonth <= 8) return 'Summer';
|
|
408
|
+
if (startMonth >= 9 && endMonth <= 11) return 'Fall';
|
|
409
|
+
if ((startMonth >= 12 || startMonth <= 2)) return 'Winter';
|
|
410
|
+
return 'Custom Period';
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
return (
|
|
414
|
+
<Stack gap={2}>
|
|
415
|
+
<MonthRangePicker
|
|
416
|
+
label="Analysis Period"
|
|
417
|
+
value={period}
|
|
418
|
+
onChange={(e) => setPeriod(e.target.value)}
|
|
419
|
+
disableFuture
|
|
420
|
+
/>
|
|
421
|
+
{period && (
|
|
422
|
+
<Typography level="body-sm">
|
|
423
|
+
Season: {getSeason(period)}
|
|
424
|
+
</Typography>
|
|
425
|
+
)}
|
|
426
|
+
</Stack>
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### Data Export with Range
|
|
432
|
+
|
|
433
|
+
```tsx
|
|
434
|
+
function DataExportForm() {
|
|
435
|
+
const [exportRange, setExportRange] = useState('');
|
|
436
|
+
const [isExporting, setIsExporting] = useState(false);
|
|
437
|
+
|
|
438
|
+
const handleExport = async () => {
|
|
439
|
+
if (!exportRange) return;
|
|
440
|
+
|
|
441
|
+
setIsExporting(true);
|
|
442
|
+
const [startMonth, endMonth] = exportRange.split(' - ');
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
const response = await api.exportData({
|
|
446
|
+
startMonth: startMonth.replace('/', '-'),
|
|
447
|
+
endMonth: endMonth.replace('/', '-'),
|
|
448
|
+
});
|
|
449
|
+
downloadFile(response.data);
|
|
450
|
+
} finally {
|
|
451
|
+
setIsExporting(false);
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
return (
|
|
456
|
+
<Stack gap={2}>
|
|
457
|
+
<MonthRangePicker
|
|
458
|
+
label="Export Period"
|
|
459
|
+
value={exportRange}
|
|
460
|
+
onChange={(e) => setExportRange(e.target.value)}
|
|
461
|
+
format="YYYY/MM"
|
|
462
|
+
disableFuture
|
|
463
|
+
required
|
|
464
|
+
/>
|
|
465
|
+
<Button
|
|
466
|
+
onClick={handleExport}
|
|
467
|
+
loading={isExporting}
|
|
468
|
+
disabled={!exportRange}
|
|
469
|
+
>
|
|
470
|
+
Export Data
|
|
471
|
+
</Button>
|
|
472
|
+
</Stack>
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
## Props and Customization
|
|
478
|
+
|
|
479
|
+
### Key Props
|
|
480
|
+
|
|
481
|
+
| Prop | Type | Default | Description |
|
|
482
|
+
| --------------- | ----------------------------------------------------------- | ----------- | ---------------------------------------- |
|
|
483
|
+
| `value` | `string` | - | Controlled value (`"YYYY/MM - YYYY/MM"`) |
|
|
484
|
+
| `defaultValue` | `string` | - | Default value for uncontrolled mode |
|
|
485
|
+
| `onChange` | `(e: { target: { name?: string; value: string } }) => void` | - | Change handler |
|
|
486
|
+
| `format` | `string` | `'YYYY/MM'` | Format for value and display |
|
|
487
|
+
| `label` | `string` | - | Label text |
|
|
488
|
+
| `helperText` | `string` | - | Helper text below input |
|
|
489
|
+
| `error` | `boolean` | `false` | Error state |
|
|
490
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Component size |
|
|
491
|
+
| `disabled` | `boolean` | `false` | Disabled state |
|
|
492
|
+
| `required` | `boolean` | `false` | Required field indicator |
|
|
493
|
+
| `minDate` | `string` | - | Minimum selectable month |
|
|
494
|
+
| `maxDate` | `string` | - | Maximum selectable month |
|
|
495
|
+
| `disableFuture` | `boolean` | `false` | Disable all future months |
|
|
496
|
+
| `disablePast` | `boolean` | `false` | Disable all past months |
|
|
497
|
+
|
|
498
|
+
### Value Format
|
|
499
|
+
|
|
500
|
+
The value is a string with start and end months separated by `-`:
|
|
501
|
+
|
|
502
|
+
```tsx
|
|
503
|
+
// Value format: "YYYY/MM - YYYY/MM"
|
|
504
|
+
const value = "2024/04 - 2024/09";
|
|
505
|
+
|
|
506
|
+
// Parsing the value
|
|
507
|
+
const [startMonth, endMonth] = value.split(' - ');
|
|
508
|
+
console.log(startMonth); // "2024/04"
|
|
509
|
+
console.log(endMonth); // "2024/09"
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### Supported Format Tokens
|
|
513
|
+
|
|
514
|
+
| Token | Description | Example |
|
|
515
|
+
| ------ | ------------- | ------- |
|
|
516
|
+
| `YYYY` | 4-digit year | 2024 |
|
|
517
|
+
| `MM` | 2-digit month | 04 |
|
|
518
|
+
|
|
519
|
+
Common format patterns:
|
|
520
|
+
|
|
521
|
+
- `YYYY/MM` - Default (2024/04)
|
|
522
|
+
- `YYYY-MM` - ISO-like (2024-04)
|
|
523
|
+
- `MM/YYYY` - European (04/2024)
|
|
524
|
+
- `YYYY.MM` - Period separator (2024.04)
|
|
525
|
+
|
|
526
|
+
### Format Affects Both Display and Value
|
|
527
|
+
|
|
528
|
+
Unlike DatePicker components, MonthRangePicker's `format` prop affects both the display and the value:
|
|
529
|
+
|
|
530
|
+
```tsx
|
|
531
|
+
<MonthRangePicker
|
|
532
|
+
format="YYYY-MM" // Both display AND onChange value use this format
|
|
533
|
+
onChange={(e) => {
|
|
534
|
+
console.log(e.target.value); // "2024-04 - 2024-09"
|
|
535
|
+
}}
|
|
536
|
+
/>
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Controlled vs Uncontrolled
|
|
540
|
+
|
|
541
|
+
```tsx
|
|
542
|
+
// Uncontrolled - component manages state
|
|
543
|
+
<MonthRangePicker
|
|
544
|
+
defaultValue="2024/04 - 2024/09"
|
|
545
|
+
onChange={(e) => console.log(e.target.value)}
|
|
546
|
+
/>
|
|
547
|
+
|
|
548
|
+
// Controlled - you manage state
|
|
549
|
+
const [monthRange, setMonthRange] = useState('2024/04 - 2024/09');
|
|
550
|
+
<MonthRangePicker
|
|
551
|
+
value={monthRange}
|
|
552
|
+
onChange={(e) => setMonthRange(e.target.value)}
|
|
553
|
+
/>
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
## Accessibility
|
|
557
|
+
|
|
558
|
+
MonthRangePicker includes built-in accessibility features:
|
|
559
|
+
|
|
560
|
+
### ARIA Attributes
|
|
561
|
+
|
|
562
|
+
- Input has proper `role="textbox"`
|
|
563
|
+
- Calendar button has `aria-label="Toggle Calendar"`
|
|
564
|
+
- Month grid uses proper navigation roles
|
|
565
|
+
- Selected months marked with `aria-selected`
|
|
566
|
+
|
|
567
|
+
### Keyboard Navigation
|
|
568
|
+
|
|
569
|
+
- **Tab**: Move focus between input and calendar button
|
|
570
|
+
- **Enter/Space**: Open calendar when focused on button
|
|
571
|
+
- **Arrow Keys**: Navigate between months in calendar
|
|
572
|
+
- **Escape**: Close calendar popup
|
|
573
|
+
- **Enter**: Select focused month
|
|
574
|
+
|
|
575
|
+
### Screen Reader Support
|
|
576
|
+
|
|
577
|
+
```tsx
|
|
578
|
+
// Range selection is announced
|
|
579
|
+
// First click: "Start month: April 2024"
|
|
580
|
+
// Second click: "End month: September 2024"
|
|
581
|
+
|
|
582
|
+
// Navigation buttons are descriptive
|
|
583
|
+
<button aria-label="Previous Year">←</button>
|
|
584
|
+
<button aria-label="Next Year">→</button>
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Focus Management
|
|
588
|
+
|
|
589
|
+
- Focus moves to current year's month grid when opened
|
|
590
|
+
- Focus returns to input when calendar closes
|
|
591
|
+
- Clear visual focus indicators on all interactive elements
|
|
592
|
+
- Range selection provides visual feedback between start and end months
|
|
593
|
+
|
|
594
|
+
## Best Practices
|
|
595
|
+
|
|
596
|
+
### ✅ Do
|
|
597
|
+
|
|
598
|
+
1. **Validate month ranges**: Ensure start month is before end month
|
|
599
|
+
|
|
600
|
+
```tsx
|
|
601
|
+
// ✅ Good: Validate the range
|
|
602
|
+
const validateRange = (value) => {
|
|
603
|
+
const [start, end] = value.split(' - ');
|
|
604
|
+
const [startYear, startMonth] = start.split('/').map(Number);
|
|
605
|
+
const [endYear, endMonth] = end.split('/').map(Number);
|
|
606
|
+
|
|
607
|
+
if (startYear > endYear) return false;
|
|
608
|
+
if (startYear === endYear && startMonth > endMonth) return false;
|
|
609
|
+
return true;
|
|
610
|
+
};
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
2. **Show range duration**: Help users understand the period length
|
|
614
|
+
|
|
615
|
+
```tsx
|
|
616
|
+
// ✅ Good: Display duration
|
|
617
|
+
const getMonthsCount = (value) => {
|
|
618
|
+
if (!value) return 0;
|
|
619
|
+
const [start, end] = value.split(' - ');
|
|
620
|
+
const [startYear, startMonth] = start.split('/').map(Number);
|
|
621
|
+
const [endYear, endMonth] = end.split('/').map(Number);
|
|
622
|
+
return (endYear - startYear) * 12 + (endMonth - startMonth) + 1;
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
<MonthRangePicker
|
|
626
|
+
helperText={`${getMonthsCount(range)} months selected`}
|
|
627
|
+
/>
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
3. **Set reasonable limits**: Prevent excessively long ranges
|
|
631
|
+
|
|
632
|
+
```tsx
|
|
633
|
+
// ✅ Good: Limit range span
|
|
634
|
+
const MAX_MONTHS = 24;
|
|
635
|
+
const validateMaxRange = (value) => {
|
|
636
|
+
return getMonthsCount(value) <= MAX_MONTHS;
|
|
637
|
+
};
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
4. **Use consistent formats**: Match your application's conventions
|
|
641
|
+
|
|
642
|
+
```tsx
|
|
643
|
+
// ✅ Good: Consistent with app format
|
|
644
|
+
<MonthRangePicker
|
|
645
|
+
format="YYYY-MM" // ISO format for API compatibility
|
|
646
|
+
/>
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
### ❌ Don't
|
|
650
|
+
|
|
651
|
+
1. **Don't allow invalid ranges**: Always validate start ≤ end
|
|
652
|
+
|
|
653
|
+
```tsx
|
|
654
|
+
// ❌ Bad: No validation
|
|
655
|
+
<MonthRangePicker onChange={(e) => setRange(e.target.value)} />
|
|
656
|
+
|
|
657
|
+
// ✅ Good: Validate range
|
|
658
|
+
<MonthRangePicker
|
|
659
|
+
onChange={(e) => {
|
|
660
|
+
if (isValidRange(e.target.value)) {
|
|
661
|
+
setRange(e.target.value);
|
|
662
|
+
}
|
|
663
|
+
}}
|
|
664
|
+
/>
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
2. **Don't use inconsistent value formats**: Match value to format prop
|
|
668
|
+
|
|
669
|
+
```tsx
|
|
670
|
+
// ❌ Bad: Mismatched formats
|
|
671
|
+
<MonthRangePicker
|
|
672
|
+
format="YYYY/MM"
|
|
673
|
+
value="04/2024 - 09/2024" // Wrong! Should use YYYY/MM
|
|
674
|
+
/>
|
|
675
|
+
|
|
676
|
+
// ✅ Good: Matching formats
|
|
677
|
+
<MonthRangePicker
|
|
678
|
+
format="YYYY/MM"
|
|
679
|
+
value="2024/04 - 2024/09"
|
|
680
|
+
/>
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
3. **Don't forget to handle empty states**: Validate before processing
|
|
684
|
+
|
|
685
|
+
4. **Don't use for day-level selection**: Use DateRangePicker when days matter
|
|
686
|
+
|
|
687
|
+
## Performance Considerations
|
|
688
|
+
|
|
689
|
+
### Memoize Handlers
|
|
690
|
+
|
|
691
|
+
When using MonthRangePicker in complex forms:
|
|
692
|
+
|
|
693
|
+
```tsx
|
|
694
|
+
const handleChange = useCallback((e) => {
|
|
695
|
+
setMonthRange(e.target.value);
|
|
696
|
+
}, []);
|
|
697
|
+
|
|
698
|
+
<MonthRangePicker value={monthRange} onChange={handleChange} />
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
### Parse Values Efficiently
|
|
702
|
+
|
|
703
|
+
When processing month range values:
|
|
704
|
+
|
|
705
|
+
```tsx
|
|
706
|
+
const { startYear, startMonth, endYear, endMonth, monthsCount } = useMemo(() => {
|
|
707
|
+
if (!monthRange) return { startYear: null, startMonth: null, endYear: null, endMonth: null, monthsCount: 0 };
|
|
708
|
+
|
|
709
|
+
const [start, end] = monthRange.split(' - ');
|
|
710
|
+
const [startYear, startMonth] = start.split('/').map(Number);
|
|
711
|
+
const [endYear, endMonth] = end.split('/').map(Number);
|
|
712
|
+
const monthsCount = (endYear - startYear) * 12 + (endMonth - startMonth) + 1;
|
|
713
|
+
|
|
714
|
+
return { startYear, startMonth, endYear, endMonth, monthsCount };
|
|
715
|
+
}, [monthRange]);
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
### Format Conversion for APIs
|
|
719
|
+
|
|
720
|
+
When working with APIs that expect different formats:
|
|
721
|
+
|
|
722
|
+
```tsx
|
|
723
|
+
function MonthRangeField({ value, onChange, apiFormat = 'YYYY-MM' }) {
|
|
724
|
+
const displayFormat = 'YYYY/MM';
|
|
725
|
+
|
|
726
|
+
const convertToDisplay = (apiValue) => {
|
|
727
|
+
if (!apiValue) return '';
|
|
728
|
+
const [start, end] = apiValue.split(' - ');
|
|
729
|
+
return `${start.replace('-', '/')} - ${end.replace('-', '/')}`;
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
const convertToApi = (displayValue) => {
|
|
733
|
+
if (!displayValue) return '';
|
|
734
|
+
const [start, end] = displayValue.split(' - ');
|
|
735
|
+
return `${start.replace('/', '-')} - ${end.replace('/', '-')}`;
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
return (
|
|
739
|
+
<MonthRangePicker
|
|
740
|
+
value={convertToDisplay(value)}
|
|
741
|
+
onChange={(e) => onChange(convertToApi(e.target.value))}
|
|
742
|
+
format={displayFormat}
|
|
743
|
+
/>
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
MonthRangePicker provides an efficient way to select multi-month periods for fiscal reporting, budget planning, and data analysis. Remember that the `format` prop affects both display and value, and always validate that the start month precedes the end month.
|