@ceed/ads 1.29.1 → 1.30.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/components/CurrencyInput/CurrencyInput.d.ts +1 -1
  2. package/dist/components/CurrencyInput/hooks/use-currency-setting.d.ts +2 -2
  3. package/dist/components/ProfileMenu/ProfileMenu.d.ts +1 -1
  4. package/dist/components/SearchBar/SearchBar.d.ts +21 -0
  5. package/dist/components/SearchBar/index.d.ts +3 -0
  6. package/dist/components/data-display/Badge.md +39 -71
  7. package/dist/components/data-display/DataTable.md +1 -1
  8. package/dist/components/data-display/InfoSign.md +98 -74
  9. package/dist/components/data-display/Typography.md +97 -363
  10. package/dist/components/feedback/Dialog.md +62 -76
  11. package/dist/components/feedback/Modal.md +44 -259
  12. package/dist/components/feedback/llms.txt +0 -2
  13. package/dist/components/index.d.ts +2 -0
  14. package/dist/components/inputs/Autocomplete.md +107 -356
  15. package/dist/components/inputs/ButtonGroup.md +106 -115
  16. package/dist/components/inputs/Calendar.md +459 -98
  17. package/dist/components/inputs/CurrencyInput.md +5 -183
  18. package/dist/components/inputs/DatePicker.md +431 -108
  19. package/dist/components/inputs/DateRangePicker.md +492 -131
  20. package/dist/components/inputs/FilterMenu.md +19 -169
  21. package/dist/components/inputs/FilterableCheckboxGroup.md +23 -123
  22. package/dist/components/inputs/IconButton.md +88 -137
  23. package/dist/components/inputs/Input.md +0 -5
  24. package/dist/components/inputs/MonthPicker.md +422 -95
  25. package/dist/components/inputs/MonthRangePicker.md +466 -89
  26. package/dist/components/inputs/PercentageInput.md +16 -185
  27. package/dist/components/inputs/RadioButton.md +35 -163
  28. package/dist/components/inputs/RadioTileGroup.md +61 -150
  29. package/dist/components/inputs/SearchBar.md +44 -0
  30. package/dist/components/inputs/Select.md +326 -222
  31. package/dist/components/inputs/Switch.md +376 -136
  32. package/dist/components/inputs/Textarea.md +10 -213
  33. package/dist/components/inputs/Uploader/Uploader.md +66 -145
  34. package/dist/components/inputs/llms.txt +1 -3
  35. package/dist/components/navigation/Breadcrumbs.md +322 -80
  36. package/dist/components/navigation/Dropdown.md +221 -92
  37. package/dist/components/navigation/IconMenuButton.md +502 -40
  38. package/dist/components/navigation/InsetDrawer.md +738 -68
  39. package/dist/components/navigation/Link.md +298 -39
  40. package/dist/components/navigation/Menu.md +285 -92
  41. package/dist/components/navigation/MenuButton.md +448 -55
  42. package/dist/components/navigation/Pagination.md +338 -47
  43. package/dist/components/navigation/ProfileMenu.md +268 -45
  44. package/dist/components/navigation/Stepper.md +28 -160
  45. package/dist/components/navigation/Tabs.md +316 -57
  46. package/dist/components/surfaces/Sheet.md +334 -151
  47. package/dist/index.browser.js +15 -13
  48. package/dist/index.browser.js.map +4 -4
  49. package/dist/index.cjs +289 -288
  50. package/dist/index.d.ts +1 -1
  51. package/dist/index.js +426 -369
  52. package/dist/llms.txt +1 -8
  53. package/framer/index.js +1 -1
  54. package/package.json +16 -15
  55. package/dist/chunks/rehype-accent-FZRUD7VI.js +0 -39
  56. package/dist/components/feedback/CircularProgress.md +0 -257
  57. package/dist/components/feedback/Skeleton.md +0 -280
  58. package/dist/components/inputs/FormControl.md +0 -361
  59. package/dist/components/inputs/RadioList.md +0 -241
  60. package/dist/components/inputs/Slider.md +0 -334
  61. package/dist/guides/ThemeProvider.md +0 -116
  62. package/dist/guides/llms.txt +0 -9
@@ -2,9 +2,7 @@
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 -- 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.
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.
8
6
 
9
7
  ```tsx
10
8
  <MonthRangePicker />
@@ -27,6 +25,15 @@ It is ideal for scenarios like fiscal period reporting, quarterly comparisons, s
27
25
  | format | — | — |
28
26
  | onChange | — | — |
29
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
+
30
37
  ## Usage
31
38
 
32
39
  ```tsx
@@ -45,14 +52,19 @@ function MonthRangeForm() {
45
52
  }
46
53
  ```
47
54
 
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.
55
+ ## Examples
52
56
 
53
- ## Sizes
57
+ ### Playground
54
58
 
55
- MonthRangePicker supports three sizes (`sm`, `md`, `lg`) for different layouts and contexts.
59
+ Interactive example with all controls.
60
+
61
+ ```tsx
62
+ <MonthRangePicker />
63
+ ```
64
+
65
+ ### Sizes
66
+
67
+ MonthRangePicker supports three sizes for different layouts.
56
68
 
57
69
  ```tsx
58
70
  <Stack gap={2}>
@@ -62,11 +74,9 @@ MonthRangePicker supports three sizes (`sm`, `md`, `lg`) for different layouts a
62
74
  </Stack>
63
75
  ```
64
76
 
65
- ## Form Field Features
66
-
67
77
  ### With Label
68
78
 
69
- Add a label above the month range picker to indicate the field purpose.
79
+ Add a label above the month range picker.
70
80
 
71
81
  ```tsx
72
82
  <MonthRangePicker label="Month Range" />
@@ -74,7 +84,7 @@ Add a label above the month range picker to indicate the field purpose.
74
84
 
75
85
  ### With Helper Text
76
86
 
77
- Provide additional guidance below the input to help users understand the expected selection.
87
+ Provide additional guidance below the input.
78
88
 
79
89
  ```tsx
80
90
  <MonthRangePicker
@@ -83,43 +93,41 @@ Provide additional guidance below the input to help users understand the expecte
83
93
  />
84
94
  ```
85
95
 
86
- ### Required Field
96
+ ### Error State
87
97
 
88
- Mark the field as required in forms. An asterisk indicator is displayed alongside the label.
98
+ Show validation errors with error styling.
89
99
 
90
100
  ```tsx
91
101
  <MonthRangePicker
92
- label="Label"
93
- helperText="I'm helper text"
94
- required
102
+ label="Month"
103
+ helperText="Please select a month"
104
+ error
95
105
  />
96
106
  ```
97
107
 
98
- ### Error State
108
+ ### Required Field
99
109
 
100
- Display validation errors with error styling applied to the input, label, and helper text.
110
+ Mark the field as required in forms.
101
111
 
102
112
  ```tsx
103
113
  <MonthRangePicker
104
- label="Month"
105
- helperText="Please select a month"
106
- error
114
+ label="Label"
115
+ helperText="I'm helper text"
116
+ required
107
117
  />
108
118
  ```
109
119
 
110
120
  ### Disabled
111
121
 
112
- Prevent user interaction when the picker is disabled.
122
+ Prevent user interaction when disabled.
113
123
 
114
124
  ```tsx
115
125
  <MonthRangePicker disabled />
116
126
  ```
117
127
 
118
- ## Date Constraints
119
-
120
128
  ### Minimum Date
121
129
 
122
- Restrict selection to months on or after a minimum date. Months before the limit are visually disabled.
130
+ Restrict selection to months on or after a minimum date.
123
131
 
124
132
  ```tsx
125
133
  <MonthRangePicker minDate="2024-04-10" />
@@ -127,7 +135,7 @@ Restrict selection to months on or after a minimum date. Months before the limit
127
135
 
128
136
  ### Maximum Date
129
137
 
130
- Restrict selection to months on or before a maximum date. Months after the limit are visually disabled.
138
+ Restrict selection to months on or before a maximum date.
131
139
 
132
140
  ```tsx
133
141
  <MonthRangePicker maxDate="2024-04-10" />
@@ -135,7 +143,7 @@ Restrict selection to months on or before a maximum date. Months after the limit
135
143
 
136
144
  ### Disable Future
137
145
 
138
- Prevent selection of any month in the future, relative to the current date.
146
+ Prevent selection of months in the future.
139
147
 
140
148
  ```tsx
141
149
  <MonthRangePicker disableFuture />
@@ -143,17 +151,15 @@ Prevent selection of any month in the future, relative to the current date.
143
151
 
144
152
  ### Disable Past
145
153
 
146
- Prevent selection of any month in the past, relative to the current date.
154
+ Prevent selection of months in the past.
147
155
 
148
156
  ```tsx
149
157
  <MonthRangePicker disablePast />
150
158
  ```
151
159
 
152
- ## Controlled vs Uncontrolled
153
-
154
160
  ### Controlled
155
161
 
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.
162
+ Parent component manages the month range state.
157
163
 
158
164
  ```tsx
159
165
  <Stack gap={2}>
@@ -176,7 +182,7 @@ The parent component manages the month range state via the `value` and `onChange
176
182
 
177
183
  ### Uncontrolled
178
184
 
179
- The component manages its own state internally using `defaultValue`. The parent can still listen for changes via `onChange`.
185
+ Component manages its own state internally.
180
186
 
181
187
  ```tsx
182
188
  <MonthRangePicker
@@ -186,11 +192,9 @@ The component manages its own state internally using `defaultValue`. The parent
186
192
  />
187
193
  ```
188
194
 
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.
195
+ ### With Formats
192
196
 
193
- The value is a string with start and end months separated by `-` (e.g., `"2024/04 - 2024/09"`).
197
+ Different value formats for regional preferences.
194
198
 
195
199
  ```tsx
196
200
  <Stack gap={2}>
@@ -203,7 +207,7 @@ The value is a string with start and end months separated by `-` (e.g., `"2024/0
203
207
 
204
208
  ### With Reset Button
205
209
 
206
- A controlled example with an external reset button to clear the selected range.
210
+ Example with an external reset button.
207
211
 
208
212
  ```tsx
209
213
  <div style={{
@@ -217,6 +221,25 @@ A controlled example with an external reset button to clear the selected range.
217
221
  </div>
218
222
  ```
219
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
+
220
243
  ## Common Use Cases
221
244
 
222
245
  ### Fiscal Period Report
@@ -248,13 +271,14 @@ function FiscalReportSelector() {
248
271
  }
249
272
  ```
250
273
 
251
- ### Subscription Duration with Month Count
274
+ ### Subscription Duration
252
275
 
253
276
  ```tsx
254
277
  function SubscriptionForm() {
255
278
  const [duration, setDuration] = useState('');
256
279
 
257
- const getMonthsCount = (value: string) => {
280
+ // Calculate months count
281
+ const getMonthsCount = (value) => {
258
282
  if (!value) return 0;
259
283
  const [start, end] = value.split(' - ');
260
284
  const [startYear, startMonth] = start.split('/').map(Number);
@@ -262,19 +286,65 @@ function SubscriptionForm() {
262
286
  return (endYear - startYear) * 12 + (endMonth - startMonth) + 1;
263
287
  };
264
288
 
289
+ const monthsCount = getMonthsCount(duration);
290
+
265
291
  return (
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
- />
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>
278
348
  );
279
349
  }
280
350
  ```
@@ -284,14 +354,20 @@ function SubscriptionForm() {
284
354
  ```tsx
285
355
  function YearOverYearComparison() {
286
356
  const [currentPeriod, setCurrentPeriod] = useState('');
357
+ const [previousPeriod, setPreviousPeriod] = useState('');
287
358
 
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
- };
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]);
295
371
 
296
372
  return (
297
373
  <Stack gap={2}>
@@ -303,69 +379,370 @@ function YearOverYearComparison() {
303
379
  />
304
380
  <MonthRangePicker
305
381
  label="Previous Period (Auto-calculated)"
306
- value={getPreviousPeriod(currentPeriod)}
382
+ value={previousPeriod}
307
383
  disabled
308
384
  />
385
+ <Button disabled={!currentPeriod}>
386
+ Compare Periods
387
+ </Button>
309
388
  </Stack>
310
389
  );
311
390
  }
312
391
  ```
313
392
 
314
- ## Best Practices
393
+ ### Seasonal Analysis
315
394
 
316
- 1. **Match value format to `format` prop**: The `value` prop must follow the same pattern as `format`, with start and end separated by `-`.
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:
317
529
 
318
530
  ```tsx
319
- // ✅ Good: Matching formats
320
531
  <MonthRangePicker
321
- format="YYYY/MM"
322
- value="2024/04 - 2024/09"
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
+ }}
323
536
  />
537
+ ```
538
+
539
+ ### Controlled vs Uncontrolled
324
540
 
325
- // ❌ Bad: Mismatched format
541
+ ```tsx
542
+ // Uncontrolled - component manages state
326
543
  <MonthRangePicker
327
- format="YYYY/MM"
328
- value="04/2024 - 09/2024"
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)}
329
553
  />
330
554
  ```
331
555
 
332
- 2. **Show range duration in helper text**: Help users understand the length of the period they selected.
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
333
599
 
334
600
  ```tsx
335
- // ✅ Good: Dynamic helper text
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
+
336
625
  <MonthRangePicker
337
626
  helperText={`${getMonthsCount(range)} months selected`}
338
627
  />
339
628
  ```
340
629
 
341
- 3. **Set reasonable date constraints**: Use `minDate`, `maxDate`, `disableFuture`, or `disablePast` to prevent invalid selections at the UI level.
630
+ 3. **Set reasonable limits**: Prevent excessively long ranges
342
631
 
343
632
  ```tsx
344
- // ✅ Good: Logical constraints for historical reporting
345
- <MonthRangePicker disableFuture minDate="2020/01" />
633
+ // ✅ Good: Limit range span
634
+ const MAX_MONTHS = 24;
635
+ const validateMaxRange = (value) => {
636
+ return getMonthsCount(value) <= MAX_MONTHS;
637
+ };
346
638
  ```
347
639
 
348
- 4. **Validate before processing**: Always check that the value is non-empty and properly formed before parsing.
640
+ 4. **Use consistent formats**: Match your application's conventions
349
641
 
350
642
  ```tsx
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
- };
643
+ // ✅ Good: Consistent with app format
644
+ <MonthRangePicker
645
+ format="YYYY-MM" // ISO format for API compatibility
646
+ />
357
647
  ```
358
648
 
359
- 5. **Use consistent `format` across your application**: Choose one format (e.g., `YYYY-MM` for API compatibility) and stick with it.
649
+ ### Don't
650
+
651
+ 1. **Don't allow invalid ranges**: Always validate start ≤ end
360
652
 
361
653
  ```tsx
362
- // Good: ISO format for API compatibility
363
- <MonthRangePicker format="YYYY-MM" />
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
+ />
364
665
  ```
365
666
 
366
- ## Accessibility
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
+ ```
367
747
 
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.
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.