@ceed/ads 1.23.3 → 1.23.5

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