@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.
- package/dist/chunks/rehype-accent-FZRUD7VI.js +39 -0
- package/dist/components/CurrencyInput/CurrencyInput.d.ts +1 -1
- package/dist/components/CurrencyInput/hooks/use-currency-setting.d.ts +2 -2
- package/dist/components/DataTable/components.d.ts +2 -1
- package/dist/components/DataTable/styled.d.ts +3 -1
- package/dist/components/DataTable/types.d.ts +1 -0
- package/dist/components/RadioTileGroup/RadioTileGroup.d.ts +56 -0
- package/dist/components/RadioTileGroup/index.d.ts +3 -0
- package/dist/components/data-display/DataTable.md +77 -1
- package/dist/components/data-display/InfoSign.md +74 -91
- package/dist/components/data-display/Typography.md +411 -94
- package/dist/components/feedback/CircularProgress.md +257 -0
- package/dist/components/feedback/Dialog.md +76 -62
- package/dist/components/feedback/Modal.md +430 -138
- package/dist/components/feedback/Skeleton.md +280 -0
- package/dist/components/feedback/llms.txt +2 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/inputs/Autocomplete.md +356 -107
- package/dist/components/inputs/ButtonGroup.md +115 -104
- package/dist/components/inputs/CurrencyInput.md +183 -5
- package/dist/components/inputs/DatePicker.md +108 -431
- package/dist/components/inputs/DateRangePicker.md +131 -492
- package/dist/components/inputs/FilterableCheckboxGroup.md +145 -19
- package/dist/components/inputs/FormControl.md +361 -0
- package/dist/components/inputs/IconButton.md +137 -88
- package/dist/components/inputs/Input.md +204 -73
- package/dist/components/inputs/MonthPicker.md +95 -422
- package/dist/components/inputs/MonthRangePicker.md +89 -466
- package/dist/components/inputs/PercentageInput.md +185 -16
- package/dist/components/inputs/RadioButton.md +163 -35
- package/dist/components/inputs/RadioList.md +241 -0
- package/dist/components/inputs/RadioTileGroup.md +507 -0
- package/dist/components/inputs/Select.md +222 -326
- package/dist/components/inputs/Slider.md +334 -0
- package/dist/components/inputs/Switch.md +143 -376
- package/dist/components/inputs/Textarea.md +213 -10
- package/dist/components/inputs/Uploader/Uploader.md +145 -66
- package/dist/components/inputs/llms.txt +4 -0
- package/dist/components/navigation/Breadcrumbs.md +57 -308
- package/dist/components/navigation/Drawer.md +180 -0
- package/dist/components/navigation/Dropdown.md +98 -215
- package/dist/components/navigation/IconMenuButton.md +40 -502
- package/dist/components/navigation/InsetDrawer.md +281 -650
- package/dist/components/navigation/Link.md +31 -348
- package/dist/components/navigation/Menu.md +92 -285
- package/dist/components/navigation/MenuButton.md +55 -448
- package/dist/components/navigation/Pagination.md +47 -338
- package/dist/components/navigation/Stepper.md +160 -28
- package/dist/components/navigation/Tabs.md +57 -316
- package/dist/components/surfaces/Accordions.md +49 -804
- package/dist/components/surfaces/Card.md +97 -157
- package/dist/components/surfaces/Divider.md +83 -234
- package/dist/components/surfaces/Sheet.md +153 -328
- package/dist/guides/ThemeProvider.md +89 -0
- package/dist/guides/llms.txt +9 -0
- package/dist/index.browser.js +224 -0
- package/dist/index.browser.js.map +7 -0
- package/dist/index.cjs +648 -390
- package/dist/index.d.ts +1 -1
- package/dist/index.js +563 -361
- package/dist/llms.txt +9 -0
- package/framer/index.js +1 -163
- 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
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
86
|
+
### Required Field
|
|
97
87
|
|
|
98
|
-
|
|
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="
|
|
103
|
-
helperText="
|
|
104
|
-
|
|
92
|
+
label="Label"
|
|
93
|
+
helperText="I'm helper text"
|
|
94
|
+
required
|
|
105
95
|
/>
|
|
106
96
|
```
|
|
107
97
|
|
|
108
|
-
###
|
|
98
|
+
### Error State
|
|
109
99
|
|
|
110
|
-
|
|
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="
|
|
115
|
-
helperText="
|
|
116
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const [
|
|
364
|
-
|
|
365
|
-
|
|
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={
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
643
|
-
// ✅ Good: Consistent with app format
|
|
325
|
+
// ❌ Bad: Mismatched format
|
|
644
326
|
<MonthRangePicker
|
|
645
|
-
format="YYYY
|
|
327
|
+
format="YYYY/MM"
|
|
328
|
+
value="04/2024 - 09/2024"
|
|
646
329
|
/>
|
|
647
330
|
```
|
|
648
331
|
|
|
649
|
-
|
|
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
|
-
//
|
|
655
|
-
<MonthRangePicker onChange={(e) => setRange(e.target.value)} />
|
|
656
|
-
|
|
657
|
-
// ✅ Good: Validate range
|
|
335
|
+
// ✅ Good: Dynamic helper text
|
|
658
336
|
<MonthRangePicker
|
|
659
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
|
|
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
|
-
|
|
707
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|