@ceed/cds 1.24.1-next.3 → 1.25.0

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 (63) hide show
  1. package/dist/chunks/rehype-accent-FZRUD7VI.js +39 -0
  2. package/dist/components/CurrencyInput/CurrencyInput.d.ts +1 -1
  3. package/dist/components/CurrencyInput/hooks/use-currency-setting.d.ts +2 -2
  4. package/dist/components/DataTable/components.d.ts +2 -1
  5. package/dist/components/DataTable/styled.d.ts +3 -1
  6. package/dist/components/DataTable/types.d.ts +1 -0
  7. package/dist/components/RadioTileGroup/RadioTileGroup.d.ts +56 -0
  8. package/dist/components/RadioTileGroup/index.d.ts +3 -0
  9. package/dist/components/data-display/DataTable.md +77 -1
  10. package/dist/components/data-display/InfoSign.md +74 -91
  11. package/dist/components/data-display/Typography.md +411 -94
  12. package/dist/components/feedback/CircularProgress.md +257 -0
  13. package/dist/components/feedback/Dialog.md +76 -62
  14. package/dist/components/feedback/Modal.md +430 -138
  15. package/dist/components/feedback/Skeleton.md +280 -0
  16. package/dist/components/feedback/llms.txt +2 -0
  17. package/dist/components/index.d.ts +1 -0
  18. package/dist/components/inputs/Autocomplete.md +356 -107
  19. package/dist/components/inputs/ButtonGroup.md +115 -104
  20. package/dist/components/inputs/CurrencyInput.md +183 -5
  21. package/dist/components/inputs/DatePicker.md +108 -431
  22. package/dist/components/inputs/DateRangePicker.md +131 -492
  23. package/dist/components/inputs/FilterableCheckboxGroup.md +145 -19
  24. package/dist/components/inputs/FormControl.md +361 -0
  25. package/dist/components/inputs/IconButton.md +137 -88
  26. package/dist/components/inputs/Input.md +204 -73
  27. package/dist/components/inputs/MonthPicker.md +95 -422
  28. package/dist/components/inputs/MonthRangePicker.md +89 -466
  29. package/dist/components/inputs/PercentageInput.md +185 -16
  30. package/dist/components/inputs/RadioButton.md +163 -35
  31. package/dist/components/inputs/RadioList.md +241 -0
  32. package/dist/components/inputs/RadioTileGroup.md +507 -0
  33. package/dist/components/inputs/Select.md +222 -326
  34. package/dist/components/inputs/Slider.md +334 -0
  35. package/dist/components/inputs/Switch.md +143 -376
  36. package/dist/components/inputs/Textarea.md +213 -10
  37. package/dist/components/inputs/Uploader/Uploader.md +145 -66
  38. package/dist/components/inputs/llms.txt +4 -0
  39. package/dist/components/navigation/Breadcrumbs.md +57 -308
  40. package/dist/components/navigation/Drawer.md +180 -0
  41. package/dist/components/navigation/Dropdown.md +98 -215
  42. package/dist/components/navigation/IconMenuButton.md +40 -502
  43. package/dist/components/navigation/InsetDrawer.md +281 -650
  44. package/dist/components/navigation/Link.md +31 -348
  45. package/dist/components/navigation/Menu.md +92 -285
  46. package/dist/components/navigation/MenuButton.md +55 -448
  47. package/dist/components/navigation/Pagination.md +47 -338
  48. package/dist/components/navigation/Stepper.md +160 -28
  49. package/dist/components/navigation/Tabs.md +57 -316
  50. package/dist/components/surfaces/Accordions.md +49 -804
  51. package/dist/components/surfaces/Card.md +97 -157
  52. package/dist/components/surfaces/Divider.md +83 -234
  53. package/dist/components/surfaces/Sheet.md +153 -328
  54. package/dist/guides/ThemeProvider.md +89 -0
  55. package/dist/guides/llms.txt +9 -0
  56. package/dist/index.browser.js +224 -0
  57. package/dist/index.browser.js.map +7 -0
  58. package/dist/index.cjs +648 -390
  59. package/dist/index.d.ts +1 -1
  60. package/dist/index.js +563 -361
  61. package/dist/llms.txt +9 -0
  62. package/framer/index.js +1 -163
  63. package/package.json +22 -17
@@ -2,7 +2,9 @@
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.
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 -- users click once to set the start month and again to set the end month.
6
+
7
+ It is ideal for scenarios like fiscal period reporting, quarterly comparisons, subscription durations, or any use case requiring a span of months without specific day selection. The component supports multiple value formats, controlled and uncontrolled modes, and date range constraints.
6
8
 
7
9
  ```tsx
8
10
  <MonthRangePicker />
@@ -25,15 +27,6 @@ MonthRangePicker is a form input component that allows users to select a range o
25
27
  | format | — | — |
26
28
  | onChange | — | — |
27
29
 
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
30
  ## Usage
38
31
 
39
32
  ```tsx
@@ -52,19 +45,14 @@ function MonthRangeForm() {
52
45
  }
53
46
  ```
54
47
 
55
- ## Examples
56
-
57
- ### Playground
58
-
59
- Interactive example with all controls.
60
-
61
- ```tsx
62
- <MonthRangePicker />
63
- ```
48
+ > 💡 **Use built-in form props**
49
+ >
50
+ > This component natively supports form elements such as `label` and `helperText` props.
51
+ > When building forms, use these built-in props instead of manually composing labels and helper text with Typography.
64
52
 
65
- ### Sizes
53
+ ## Sizes
66
54
 
67
- MonthRangePicker supports three sizes for different layouts.
55
+ MonthRangePicker supports three sizes (`sm`, `md`, `lg`) for different layouts and contexts.
68
56
 
69
57
  ```tsx
70
58
  <Stack gap={2}>
@@ -74,9 +62,11 @@ MonthRangePicker supports three sizes for different layouts.
74
62
  </Stack>
75
63
  ```
76
64
 
65
+ ## Form Field Features
66
+
77
67
  ### With Label
78
68
 
79
- Add a label above the month range picker.
69
+ Add a label above the month range picker to indicate the field purpose.
80
70
 
81
71
  ```tsx
82
72
  <MonthRangePicker label="Month Range" />
@@ -84,7 +74,7 @@ Add a label above the month range picker.
84
74
 
85
75
  ### With Helper Text
86
76
 
87
- Provide additional guidance below the input.
77
+ Provide additional guidance below the input to help users understand the expected selection.
88
78
 
89
79
  ```tsx
90
80
  <MonthRangePicker
@@ -93,41 +83,43 @@ Provide additional guidance below the input.
93
83
  />
94
84
  ```
95
85
 
96
- ### Error State
86
+ ### Required Field
97
87
 
98
- Show validation errors with error styling.
88
+ Mark the field as required in forms. An asterisk indicator is displayed alongside the label.
99
89
 
100
90
  ```tsx
101
91
  <MonthRangePicker
102
- label="Month"
103
- helperText="Please select a month"
104
- error
92
+ label="Label"
93
+ helperText="I'm helper text"
94
+ required
105
95
  />
106
96
  ```
107
97
 
108
- ### Required Field
98
+ ### Error State
109
99
 
110
- Mark the field as required in forms.
100
+ Display validation errors with error styling applied to the input, label, and helper text.
111
101
 
112
102
  ```tsx
113
103
  <MonthRangePicker
114
- label="Label"
115
- helperText="I'm helper text"
116
- required
104
+ label="Month"
105
+ helperText="Please select a month"
106
+ error
117
107
  />
118
108
  ```
119
109
 
120
110
  ### Disabled
121
111
 
122
- Prevent user interaction when disabled.
112
+ Prevent user interaction when the picker is disabled.
123
113
 
124
114
  ```tsx
125
115
  <MonthRangePicker disabled />
126
116
  ```
127
117
 
118
+ ## Date Constraints
119
+
128
120
  ### Minimum Date
129
121
 
130
- Restrict selection to months on or after a minimum date.
122
+ Restrict selection to months on or after a minimum date. Months before the limit are visually disabled.
131
123
 
132
124
  ```tsx
133
125
  <MonthRangePicker minDate="2024-04-10" />
@@ -135,7 +127,7 @@ Restrict selection to months on or after a minimum date.
135
127
 
136
128
  ### Maximum Date
137
129
 
138
- Restrict selection to months on or before a maximum date.
130
+ Restrict selection to months on or before a maximum date. Months after the limit are visually disabled.
139
131
 
140
132
  ```tsx
141
133
  <MonthRangePicker maxDate="2024-04-10" />
@@ -143,7 +135,7 @@ Restrict selection to months on or before a maximum date.
143
135
 
144
136
  ### Disable Future
145
137
 
146
- Prevent selection of months in the future.
138
+ Prevent selection of any month in the future, relative to the current date.
147
139
 
148
140
  ```tsx
149
141
  <MonthRangePicker disableFuture />
@@ -151,15 +143,17 @@ Prevent selection of months in the future.
151
143
 
152
144
  ### Disable Past
153
145
 
154
- Prevent selection of months in the past.
146
+ Prevent selection of any month in the past, relative to the current date.
155
147
 
156
148
  ```tsx
157
149
  <MonthRangePicker disablePast />
158
150
  ```
159
151
 
152
+ ## Controlled vs Uncontrolled
153
+
160
154
  ### Controlled
161
155
 
162
- Parent component manages the month range state.
156
+ The parent component manages the month range state via the `value` and `onChange` props. External buttons or logic can programmatically update the selected range.
163
157
 
164
158
  ```tsx
165
159
  <Stack gap={2}>
@@ -182,7 +176,7 @@ Parent component manages the month range state.
182
176
 
183
177
  ### Uncontrolled
184
178
 
185
- Component manages its own state internally.
179
+ The component manages its own state internally using `defaultValue`. The parent can still listen for changes via `onChange`.
186
180
 
187
181
  ```tsx
188
182
  <MonthRangePicker
@@ -192,9 +186,11 @@ Component manages its own state internally.
192
186
  />
193
187
  ```
194
188
 
195
- ### With Formats
189
+ ## Formats
190
+
191
+ The `format` prop determines both the display format and the value emitted by `onChange`. Unlike MonthPicker, MonthRangePicker does not have a separate `displayFormat` prop -- the `format` prop controls both.
196
192
 
197
- Different value formats for regional preferences.
193
+ The value is a string with start and end months separated by `-` (e.g., `"2024/04 - 2024/09"`).
198
194
 
199
195
  ```tsx
200
196
  <Stack gap={2}>
@@ -207,7 +203,7 @@ Different value formats for regional preferences.
207
203
 
208
204
  ### With Reset Button
209
205
 
210
- Example with an external reset button.
206
+ A controlled example with an external reset button to clear the selected range.
211
207
 
212
208
  ```tsx
213
209
  <div style={{
@@ -221,25 +217,6 @@ Example with an external reset button.
221
217
  </div>
222
218
  ```
223
219
 
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
220
  ## Common Use Cases
244
221
 
245
222
  ### Fiscal Period Report
@@ -271,14 +248,13 @@ function FiscalReportSelector() {
271
248
  }
272
249
  ```
273
250
 
274
- ### Subscription Duration
251
+ ### Subscription Duration with Month Count
275
252
 
276
253
  ```tsx
277
254
  function SubscriptionForm() {
278
255
  const [duration, setDuration] = useState('');
279
256
 
280
- // Calculate months count
281
- const getMonthsCount = (value) => {
257
+ const getMonthsCount = (value: string) => {
282
258
  if (!value) return 0;
283
259
  const [start, end] = value.split(' - ');
284
260
  const [startYear, startMonth] = start.split('/').map(Number);
@@ -286,65 +262,19 @@ function SubscriptionForm() {
286
262
  return (endYear - startYear) * 12 + (endMonth - startMonth) + 1;
287
263
  };
288
264
 
289
- const monthsCount = getMonthsCount(duration);
290
-
291
265
  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>
266
+ <MonthRangePicker
267
+ label="Subscription Period"
268
+ value={duration}
269
+ onChange={(e) => setDuration(e.target.value)}
270
+ disablePast
271
+ helperText={
272
+ getMonthsCount(duration) > 0
273
+ ? `${getMonthsCount(duration)} month subscription`
274
+ : 'Select start and end months'
275
+ }
276
+ required
277
+ />
348
278
  );
349
279
  }
350
280
  ```
@@ -354,20 +284,14 @@ function BudgetPlanningForm({ departments }) {
354
284
  ```tsx
355
285
  function YearOverYearComparison() {
356
286
  const [currentPeriod, setCurrentPeriod] = useState('');
357
- const [previousPeriod, setPreviousPeriod] = useState('');
358
287
 
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]);
288
+ const getPreviousPeriod = (value: string) => {
289
+ if (!value) return '';
290
+ const [start, end] = value.split(' - ');
291
+ const [sYear, sMonth] = start.split('/').map(Number);
292
+ const [eYear, eMonth] = end.split('/').map(Number);
293
+ return `${sYear - 1}/${String(sMonth).padStart(2, '0')} - ${eYear - 1}/${String(eMonth).padStart(2, '0')}`;
294
+ };
371
295
 
372
296
  return (
373
297
  <Stack gap={2}>
@@ -379,370 +303,69 @@ function YearOverYearComparison() {
379
303
  />
380
304
  <MonthRangePicker
381
305
  label="Previous Period (Auto-calculated)"
382
- value={previousPeriod}
306
+ value={getPreviousPeriod(currentPeriod)}
383
307
  disabled
384
308
  />
385
- <Button disabled={!currentPeriod}>
386
- Compare Periods
387
- </Button>
388
309
  </Stack>
389
310
  );
390
311
  }
391
312
  ```
392
313
 
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
314
  ## Best Practices
595
315
 
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
316
+ 1. **Match value format to `format` prop**: The `value` prop must follow the same pattern as `format`, with start and end separated by `-`.
614
317
 
615
318
  ```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
-
319
+ // ✅ Good: Matching formats
625
320
  <MonthRangePicker
626
- helperText={`${getMonthsCount(range)} months selected`}
321
+ format="YYYY/MM"
322
+ value="2024/04 - 2024/09"
627
323
  />
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
324
 
642
- ```tsx
643
- // ✅ Good: Consistent with app format
325
+ // ❌ Bad: Mismatched format
644
326
  <MonthRangePicker
645
- format="YYYY-MM" // ISO format for API compatibility
327
+ format="YYYY/MM"
328
+ value="04/2024 - 09/2024"
646
329
  />
647
330
  ```
648
331
 
649
- ### Don't
650
-
651
- 1. **Don't allow invalid ranges**: Always validate start ≤ end
332
+ 2. **Show range duration in helper text**: Help users understand the length of the period they selected.
652
333
 
653
334
  ```tsx
654
- // Bad: No validation
655
- <MonthRangePicker onChange={(e) => setRange(e.target.value)} />
656
-
657
- // ✅ Good: Validate range
335
+ // Good: Dynamic helper text
658
336
  <MonthRangePicker
659
- onChange={(e) => {
660
- if (isValidRange(e.target.value)) {
661
- setRange(e.target.value);
662
- }
663
- }}
337
+ helperText={`${getMonthsCount(range)} months selected`}
664
338
  />
665
339
  ```
666
340
 
667
- 2. **Don't use inconsistent value formats**: Match value to format prop
341
+ 3. **Set reasonable date constraints**: Use `minDate`, `maxDate`, `disableFuture`, or `disablePast` to prevent invalid selections at the UI level.
668
342
 
669
343
  ```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
- />
344
+ // Good: Logical constraints for historical reporting
345
+ <MonthRangePicker disableFuture minDate="2020/01" />
681
346
  ```
682
347
 
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:
348
+ 4. **Validate before processing**: Always check that the value is non-empty and properly formed before parsing.
692
349
 
693
350
  ```tsx
694
- const handleChange = useCallback((e) => {
695
- setMonthRange(e.target.value);
696
- }, []);
697
-
698
- <MonthRangePicker value={monthRange} onChange={handleChange} />
351
+ // Good: Guard against empty values
352
+ const handleSubmit = () => {
353
+ if (!range || !range.includes(' - ')) return;
354
+ const [start, end] = range.split(' - ');
355
+ // process start and end
356
+ };
699
357
  ```
700
358
 
701
- ### Parse Values Efficiently
702
-
703
- When processing month range values:
359
+ 5. **Use consistent `format` across your application**: Choose one format (e.g., `YYYY-MM` for API compatibility) and stick with it.
704
360
 
705
361
  ```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]);
362
+ // Good: ISO format for API compatibility
363
+ <MonthRangePicker format="YYYY-MM" />
716
364
  ```
717
365
 
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
- ```
366
+ ## Accessibility
747
367
 
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.
368
+ - The input has `role="textbox"` and the calendar toggle button has `aria-label="Toggle Calendar"` for screen reader identification.
369
+ - **Keyboard navigation**: Use **Tab** to move focus between the input and calendar button, **Enter/Space** to open the calendar, **Arrow Keys** to navigate months, and **Escape** to close the popup.
370
+ - Range selection is conveyed through visual highlighting between the start and end months in the calendar grid. Each month button has a descriptive `aria-label` (e.g., "April 2024").
371
+ - Focus management moves focus into the calendar grid when opened and returns focus to the input when the calendar is closed.