@ceed/ads 1.29.0 → 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.
- 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/hooks.d.ts +2 -1
- package/dist/components/DataTable/utils.d.ts +1 -0
- package/dist/components/ProfileMenu/ProfileMenu.d.ts +1 -1
- package/dist/components/SearchBar/SearchBar.d.ts +21 -0
- package/dist/components/SearchBar/index.d.ts +3 -0
- package/dist/components/data-display/Badge.md +39 -71
- package/dist/components/data-display/DataTable.md +1 -1
- package/dist/components/data-display/InfoSign.md +98 -74
- package/dist/components/data-display/Typography.md +97 -363
- package/dist/components/feedback/Dialog.md +62 -76
- package/dist/components/feedback/Modal.md +44 -259
- package/dist/components/feedback/llms.txt +0 -2
- package/dist/components/index.d.ts +2 -0
- package/dist/components/inputs/Autocomplete.md +107 -356
- package/dist/components/inputs/ButtonGroup.md +106 -115
- package/dist/components/inputs/Calendar.md +459 -98
- package/dist/components/inputs/CurrencyInput.md +5 -183
- package/dist/components/inputs/DatePicker.md +431 -108
- package/dist/components/inputs/DateRangePicker.md +492 -131
- package/dist/components/inputs/FilterMenu.md +19 -169
- package/dist/components/inputs/FilterableCheckboxGroup.md +23 -123
- package/dist/components/inputs/IconButton.md +88 -137
- package/dist/components/inputs/Input.md +0 -5
- package/dist/components/inputs/MonthPicker.md +422 -95
- package/dist/components/inputs/MonthRangePicker.md +466 -89
- package/dist/components/inputs/PercentageInput.md +16 -185
- package/dist/components/inputs/RadioButton.md +35 -163
- package/dist/components/inputs/RadioTileGroup.md +61 -150
- package/dist/components/inputs/SearchBar.md +44 -0
- package/dist/components/inputs/Select.md +326 -222
- package/dist/components/inputs/Switch.md +376 -136
- package/dist/components/inputs/Textarea.md +10 -213
- package/dist/components/inputs/Uploader/Uploader.md +66 -145
- package/dist/components/inputs/llms.txt +1 -3
- package/dist/components/navigation/Breadcrumbs.md +322 -80
- package/dist/components/navigation/Dropdown.md +221 -92
- package/dist/components/navigation/IconMenuButton.md +502 -40
- package/dist/components/navigation/InsetDrawer.md +738 -68
- package/dist/components/navigation/Link.md +298 -39
- package/dist/components/navigation/Menu.md +285 -92
- package/dist/components/navigation/MenuButton.md +448 -55
- package/dist/components/navigation/Pagination.md +338 -47
- package/dist/components/navigation/ProfileMenu.md +268 -45
- package/dist/components/navigation/Stepper.md +28 -160
- package/dist/components/navigation/Tabs.md +316 -57
- package/dist/components/surfaces/Sheet.md +334 -151
- package/dist/index.browser.js +15 -13
- package/dist/index.browser.js.map +4 -4
- package/dist/index.cjs +313 -291
- package/dist/index.d.ts +1 -1
- package/dist/index.js +450 -372
- package/dist/llms.txt +1 -8
- package/framer/index.js +1 -1
- package/package.json +16 -15
- package/dist/chunks/rehype-accent-FZRUD7VI.js +0 -39
- package/dist/components/feedback/CircularProgress.md +0 -257
- package/dist/components/feedback/Skeleton.md +0 -280
- package/dist/components/inputs/FormControl.md +0 -361
- package/dist/components/inputs/RadioList.md +0 -241
- package/dist/components/inputs/Slider.md +0 -334
- package/dist/guides/ThemeProvider.md +0 -116
- 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
|
|
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
|
-
|
|
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
|
-
|
|
57
|
+
### Playground
|
|
54
58
|
|
|
55
|
-
|
|
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
|
|
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
|
|
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
|
-
###
|
|
96
|
+
### Error State
|
|
87
97
|
|
|
88
|
-
|
|
98
|
+
Show validation errors with error styling.
|
|
89
99
|
|
|
90
100
|
```tsx
|
|
91
101
|
<MonthRangePicker
|
|
92
|
-
label="
|
|
93
|
-
helperText="
|
|
94
|
-
|
|
102
|
+
label="Month"
|
|
103
|
+
helperText="Please select a month"
|
|
104
|
+
error
|
|
95
105
|
/>
|
|
96
106
|
```
|
|
97
107
|
|
|
98
|
-
###
|
|
108
|
+
### Required Field
|
|
99
109
|
|
|
100
|
-
|
|
110
|
+
Mark the field as required in forms.
|
|
101
111
|
|
|
102
112
|
```tsx
|
|
103
113
|
<MonthRangePicker
|
|
104
|
-
label="
|
|
105
|
-
helperText="
|
|
106
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
274
|
+
### Subscription Duration
|
|
252
275
|
|
|
253
276
|
```tsx
|
|
254
277
|
function SubscriptionForm() {
|
|
255
278
|
const [duration, setDuration] = useState('');
|
|
256
279
|
|
|
257
|
-
|
|
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
|
-
<
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const [
|
|
293
|
-
|
|
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={
|
|
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
|
-
|
|
393
|
+
### Seasonal Analysis
|
|
315
394
|
|
|
316
|
-
|
|
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
|
|
322
|
-
|
|
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
|
-
|
|
541
|
+
```tsx
|
|
542
|
+
// Uncontrolled - component manages state
|
|
326
543
|
<MonthRangePicker
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
630
|
+
3. **Set reasonable limits**: Prevent excessively long ranges
|
|
342
631
|
|
|
343
632
|
```tsx
|
|
344
|
-
// ✅ Good:
|
|
345
|
-
|
|
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. **
|
|
640
|
+
4. **Use consistent formats**: Match your application's conventions
|
|
349
641
|
|
|
350
642
|
```tsx
|
|
351
|
-
// ✅ Good:
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
649
|
+
### ❌ Don't
|
|
650
|
+
|
|
651
|
+
1. **Don't allow invalid ranges**: Always validate start ≤ end
|
|
360
652
|
|
|
361
653
|
```tsx
|
|
362
|
-
//
|
|
363
|
-
<MonthRangePicker
|
|
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
|
-
|
|
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
|
-
-
|
|
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.
|