gbs-add-block 1.2.9 → 1.2.11
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/README.md +12 -2
- package/index.cjs +85 -32
- package/package.json +1 -1
- package/source/beta-components/datepicker/README.md +141 -0
- package/source/beta-components/datepicker/__tests__/core.test.ts +220 -0
- package/source/beta-components/datepicker/core/calendar.ts +263 -0
- package/source/beta-components/datepicker/core/format.ts +170 -0
- package/source/beta-components/datepicker/core/index.ts +40 -0
- package/source/beta-components/datepicker/core/types.ts +68 -0
- package/source/beta-components/datepicker/index.ts +16 -0
- package/source/beta-components/datepicker/react/Calendar.tsx +160 -0
- package/source/beta-components/datepicker/react/CalendarPanel.tsx +170 -0
- package/source/beta-components/datepicker/react/DatePicker.tsx +249 -0
- package/source/beta-components/datepicker/react/DateRangePicker.tsx +291 -0
- package/source/beta-components/datepicker/react/MonthYearPanels.tsx +123 -0
- package/source/beta-components/datepicker/react/Popover.tsx +87 -0
- package/source/beta-components/datepicker/react/icons.tsx +32 -0
- package/source/beta-components/datepicker/react/locale.ts +23 -0
- package/source/beta-components/datepicker/react/props.ts +76 -0
- package/source/beta-components/datepicker/react/useDatePicker.ts +547 -0
- package/source/beta-components/datepicker/styles.css +446 -0
- /package/source/{components → beta-components}/combobox/README.md +0 -0
- /package/source/{components → beta-components}/combobox/__tests__/core.test.ts +0 -0
- /package/source/{components → beta-components}/combobox/core/filter.ts +0 -0
- /package/source/{components → beta-components}/combobox/core/index.ts +0 -0
- /package/source/{components → beta-components}/combobox/core/types.ts +0 -0
- /package/source/{components → beta-components}/combobox/core/virtual.ts +0 -0
- /package/source/{components → beta-components}/combobox/index.ts +0 -0
- /package/source/{components → beta-components}/combobox/react/Listbox.tsx +0 -0
- /package/source/{components → beta-components}/combobox/react/MultiSelect.tsx +0 -0
- /package/source/{components → beta-components}/combobox/react/Popover.tsx +0 -0
- /package/source/{components → beta-components}/combobox/react/Select.tsx +0 -0
- /package/source/{components → beta-components}/combobox/react/icons.tsx +0 -0
- /package/source/{components → beta-components}/combobox/react/locale.ts +0 -0
- /package/source/{components → beta-components}/combobox/react/props.ts +0 -0
- /package/source/{components → beta-components}/combobox/react/useCombobox.ts +0 -0
- /package/source/{components → beta-components}/combobox/styles.css +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/README.md +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/__tests__/core.test.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/__tests__/export.test.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/core/columnHelper.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/core/columns.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/core/filtering.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/core/grid.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/core/index.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/core/rows.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/core/sorting.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/core/state.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/core/store.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/core/types.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/core/values.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/core/virtual.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/export/csv.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/export/download.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/export/pdf.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/export/table.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/export/xlsx.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/export/zip.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/index.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/react/Cell.tsx +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/react/ColumnMenu.tsx +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/react/DataGrid.tsx +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/react/FilterForm.tsx +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/react/HeaderRow.tsx +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/react/Pagination.tsx +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/react/Popover.tsx +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/react/Row.tsx +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/react/Toolbar.tsx +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/react/Viewport.tsx +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/react/context.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/react/hooks.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/react/icons.tsx +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/react/keyboard.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/react/locale.ts +0 -0
- /package/source/{components/datagridbeta → beta-components/datagrid}/styles.css +0 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# GBS Building Blocks 2.0 (v1.2.
|
|
1
|
+
# GBS Building Blocks 2.0 (v1.2.11)
|
|
2
2
|
|
|
3
3
|
Latest and upgraded version of GBS building blocks with headless UI and removed dependencies.
|
|
4
4
|
|
|
@@ -6,7 +6,17 @@ Latest and upgraded version of GBS building blocks with headless UI and removed
|
|
|
6
6
|
|
|
7
7
|
For detailed documentation on usage and props, Please visit: [Building Block Documentation v2.0](https://gramprokit.vercel.app)
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Beta Components
|
|
10
|
+
|
|
11
|
+
DataGrid and Combobox use the redesigned core API and design system. Install them with the beta flag:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx gbs-add-block -a DataGrid -beta
|
|
15
|
+
npx gbs-add-block -a Combobox -beta
|
|
16
|
+
npx gbs-add-block -a DatePicker -beta
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## What's New 🎉 (Ver 1.2.11)
|
|
10
20
|
|
|
11
21
|
- Update Candidate for next major change 2.0.0
|
|
12
22
|
|
package/index.cjs
CHANGED
|
@@ -34,9 +34,8 @@ const CONFIG = {
|
|
|
34
34
|
"Bargraph",
|
|
35
35
|
"UsePaginatedData",
|
|
36
36
|
"UseUploader",
|
|
37
|
-
"DataGridBeta",
|
|
38
|
-
"Combobox"
|
|
39
37
|
],
|
|
38
|
+
betaComponents: ["DataGrid", "Combobox", "DatePicker"],
|
|
40
39
|
// Define component dependencies
|
|
41
40
|
dependencies: {
|
|
42
41
|
FormRenderer: ["Select", "MultiSelect", "Input", "DatePicker"],
|
|
@@ -45,8 +44,19 @@ const CONFIG = {
|
|
|
45
44
|
};
|
|
46
45
|
|
|
47
46
|
const SOURCE_PATH = path.join(__dirname, "source", "components");
|
|
47
|
+
const BETA_SOURCE_PATH = path.join(__dirname, "source", "beta-components");
|
|
48
48
|
const DEFAULT_DEST_PATH = path.join(process.cwd(), "component-lib");
|
|
49
49
|
|
|
50
|
+
const normalizeComponent = (component, availableComponents) =>
|
|
51
|
+
availableComponents.find(
|
|
52
|
+
(available) => available.toLowerCase() === component.toLowerCase(),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const getAvailableComponents = (beta = false) =>
|
|
56
|
+
beta ? CONFIG.betaComponents : CONFIG.components;
|
|
57
|
+
|
|
58
|
+
const getSourcePath = (beta = false) => (beta ? BETA_SOURCE_PATH : SOURCE_PATH);
|
|
59
|
+
|
|
50
60
|
const copyCommonFiles = async (destPath) => {
|
|
51
61
|
const commonFiles = [
|
|
52
62
|
{ src: ["..", "utils.ts"], dest: "utils.ts" },
|
|
@@ -68,13 +78,18 @@ const checkComponentExists = (component, destPath) => {
|
|
|
68
78
|
return fs.existsSync(componentPath);
|
|
69
79
|
};
|
|
70
80
|
|
|
71
|
-
const copyComponent = async (component, destPath) => {
|
|
81
|
+
const copyComponent = async (component, destPath, beta = false) => {
|
|
72
82
|
try {
|
|
73
|
-
const componentSrc = path.join(
|
|
83
|
+
const componentSrc = path.join(
|
|
84
|
+
getSourcePath(beta),
|
|
85
|
+
component.toLowerCase(),
|
|
86
|
+
);
|
|
74
87
|
const componentDest = path.join(destPath, component.toLowerCase());
|
|
75
88
|
|
|
76
89
|
if (!fs.existsSync(componentSrc)) {
|
|
77
|
-
throw new Error(
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Component ${component} not found in ${beta ? "beta " : ""}source directory.`,
|
|
92
|
+
);
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
await fs.copy(componentSrc, componentDest, { overwrite: true });
|
|
@@ -91,15 +106,21 @@ const copyComponent = async (component, destPath) => {
|
|
|
91
106
|
}
|
|
92
107
|
};
|
|
93
108
|
|
|
94
|
-
const installComponentWithDependencies = async (
|
|
109
|
+
const installComponentWithDependencies = async (
|
|
110
|
+
component,
|
|
111
|
+
destPath,
|
|
112
|
+
beta = false,
|
|
113
|
+
) => {
|
|
95
114
|
// Get dependencies for the component
|
|
96
115
|
const dependencies = CONFIG.dependencies[component] || [];
|
|
97
116
|
const componentsToInstall = new Set([component, ...dependencies]);
|
|
98
117
|
|
|
99
118
|
// Check which components need to be installed
|
|
100
|
-
const pendingInstalls =
|
|
101
|
-
|
|
102
|
-
|
|
119
|
+
const pendingInstalls = beta
|
|
120
|
+
? Array.from(componentsToInstall)
|
|
121
|
+
: Array.from(componentsToInstall).filter(
|
|
122
|
+
(comp) => !checkComponentExists(comp, destPath),
|
|
123
|
+
);
|
|
103
124
|
|
|
104
125
|
if (pendingInstalls.length === 0) {
|
|
105
126
|
console.log(
|
|
@@ -110,7 +131,7 @@ const installComponentWithDependencies = async (component, destPath) => {
|
|
|
110
131
|
|
|
111
132
|
// Install all pending components
|
|
112
133
|
for (const comp of pendingInstalls) {
|
|
113
|
-
await copyComponent(comp, destPath);
|
|
134
|
+
await copyComponent(comp, destPath, beta);
|
|
114
135
|
}
|
|
115
136
|
|
|
116
137
|
if (dependencies.length > 0) {
|
|
@@ -123,7 +144,11 @@ const installComponentWithDependencies = async (component, destPath) => {
|
|
|
123
144
|
console.log(`\nFor documentation visit: ${CONFIG.docs}`);
|
|
124
145
|
};
|
|
125
146
|
|
|
126
|
-
const installMultipleComponents = async (
|
|
147
|
+
const installMultipleComponents = async (
|
|
148
|
+
components,
|
|
149
|
+
destPath,
|
|
150
|
+
beta = false,
|
|
151
|
+
) => {
|
|
127
152
|
const allComponentsToInstall = new Set();
|
|
128
153
|
|
|
129
154
|
// Collect all components and their dependencies
|
|
@@ -134,9 +159,11 @@ const installMultipleComponents = async (components, destPath) => {
|
|
|
134
159
|
});
|
|
135
160
|
|
|
136
161
|
// Filter out already installed components
|
|
137
|
-
const pendingInstalls =
|
|
138
|
-
|
|
139
|
-
|
|
162
|
+
const pendingInstalls = beta
|
|
163
|
+
? Array.from(allComponentsToInstall)
|
|
164
|
+
: Array.from(allComponentsToInstall).filter(
|
|
165
|
+
(comp) => !checkComponentExists(comp, destPath),
|
|
166
|
+
);
|
|
140
167
|
|
|
141
168
|
if (pendingInstalls.length === 0) {
|
|
142
169
|
console.log(
|
|
@@ -149,7 +176,7 @@ const installMultipleComponents = async (components, destPath) => {
|
|
|
149
176
|
|
|
150
177
|
// Install all pending components
|
|
151
178
|
for (const comp of pendingInstalls) {
|
|
152
|
-
await copyComponent(comp, destPath);
|
|
179
|
+
await copyComponent(comp, destPath, beta);
|
|
153
180
|
}
|
|
154
181
|
|
|
155
182
|
// Show dependency information
|
|
@@ -169,7 +196,7 @@ const installMultipleComponents = async (components, destPath) => {
|
|
|
169
196
|
console.log(`\nFor documentation visit: ${CONFIG.docs}`);
|
|
170
197
|
};
|
|
171
198
|
|
|
172
|
-
const interactiveComponentSelector = async () => {
|
|
199
|
+
const interactiveComponentSelector = async (beta = false) => {
|
|
173
200
|
return new Promise((resolve) => {
|
|
174
201
|
const rl = readline.createInterface({
|
|
175
202
|
input: process.stdin,
|
|
@@ -186,7 +213,9 @@ const interactiveComponentSelector = async () => {
|
|
|
186
213
|
"Use ↑/↓ arrow keys to navigate, SPACE to select/deselect, ENTER to install\n",
|
|
187
214
|
);
|
|
188
215
|
|
|
189
|
-
|
|
216
|
+
const availableComponents = getAvailableComponents(beta);
|
|
217
|
+
|
|
218
|
+
availableComponents.forEach((component, index) => {
|
|
190
219
|
const isSelected = selectedComponents.has(component);
|
|
191
220
|
const isCurrentIndex = index === currentIndex;
|
|
192
221
|
const deps = CONFIG.dependencies[component]
|
|
@@ -216,13 +245,13 @@ const interactiveComponentSelector = async () => {
|
|
|
216
245
|
break;
|
|
217
246
|
case "\u001b[B": // Down arrow
|
|
218
247
|
currentIndex = Math.min(
|
|
219
|
-
|
|
248
|
+
getAvailableComponents(beta).length - 1,
|
|
220
249
|
currentIndex + 1,
|
|
221
250
|
);
|
|
222
251
|
renderMenu();
|
|
223
252
|
break;
|
|
224
253
|
case " ": // Space bar
|
|
225
|
-
const component =
|
|
254
|
+
const component = getAvailableComponents(beta)[currentIndex];
|
|
226
255
|
if (selectedComponents.has(component)) {
|
|
227
256
|
selectedComponents.delete(component);
|
|
228
257
|
} else {
|
|
@@ -265,21 +294,29 @@ const parseMultipleComponents = (componentString) => {
|
|
|
265
294
|
.filter((comp) => comp.length > 0);
|
|
266
295
|
};
|
|
267
296
|
|
|
268
|
-
const validateComponents = (components) => {
|
|
297
|
+
const validateComponents = (components, beta = false) => {
|
|
298
|
+
const availableComponents = getAvailableComponents(beta);
|
|
269
299
|
const invalidComponents = components.filter(
|
|
270
|
-
(comp) => !
|
|
300
|
+
(comp) => !availableComponents.includes(comp),
|
|
271
301
|
);
|
|
272
302
|
if (invalidComponents.length > 0) {
|
|
273
303
|
console.error(`Invalid components: ${invalidComponents.join(", ")}`);
|
|
274
|
-
console.log(
|
|
275
|
-
|
|
304
|
+
console.log(`\nAvailable ${beta ? "beta " : ""}components:`);
|
|
305
|
+
availableComponents.forEach((comp) => console.log(`- ${comp}`));
|
|
306
|
+
if (!beta) {
|
|
307
|
+
console.log("\nRedesigned beta components (install with -beta):");
|
|
308
|
+
CONFIG.betaComponents.forEach((comp) => console.log(`- ${comp}`));
|
|
309
|
+
}
|
|
276
310
|
return false;
|
|
277
311
|
}
|
|
278
312
|
return true;
|
|
279
313
|
};
|
|
280
314
|
|
|
281
315
|
const main = async () => {
|
|
282
|
-
const
|
|
316
|
+
const args = hideBin(process.argv).map((arg) =>
|
|
317
|
+
arg === "-beta" ? "--beta" : arg,
|
|
318
|
+
);
|
|
319
|
+
const argv = yargs(args)
|
|
283
320
|
.option("add", {
|
|
284
321
|
alias: "a",
|
|
285
322
|
describe:
|
|
@@ -296,27 +333,39 @@ const main = async () => {
|
|
|
296
333
|
describe: "List available components",
|
|
297
334
|
type: "boolean",
|
|
298
335
|
})
|
|
336
|
+
.option("beta", {
|
|
337
|
+
describe: "Install redesigned beta components",
|
|
338
|
+
type: "boolean",
|
|
339
|
+
default: false,
|
|
340
|
+
})
|
|
299
341
|
.example("$0 -a Button", "Install a single component")
|
|
300
342
|
.example("$0 -a Button,Card,Modal", "Install multiple components")
|
|
343
|
+
.example("$0 -a DataGrid -beta", "Install the redesigned beta DataGrid")
|
|
344
|
+
.example("$0 -a Combobox -beta", "Install the redesigned beta Combobox")
|
|
301
345
|
.example("$0 -i", "Interactive selection mode")
|
|
302
346
|
.help().argv;
|
|
303
347
|
|
|
304
348
|
// List components if requested
|
|
305
349
|
if (argv.list) {
|
|
306
|
-
|
|
307
|
-
|
|
350
|
+
const availableComponents = getAvailableComponents(argv.beta);
|
|
351
|
+
console.log(`\nAvailable ${argv.beta ? "beta " : ""}components:`);
|
|
352
|
+
availableComponents.forEach((comp) => {
|
|
308
353
|
const deps = CONFIG.dependencies[comp]
|
|
309
354
|
? ` (requires: ${CONFIG.dependencies[comp].join(", ")})`
|
|
310
355
|
: "";
|
|
311
356
|
console.log(`- ${comp}${deps}`);
|
|
312
357
|
});
|
|
358
|
+
if (!argv.beta) {
|
|
359
|
+
console.log("\nRedesigned beta components (install with -beta):");
|
|
360
|
+
CONFIG.betaComponents.forEach((comp) => console.log(`- ${comp}`));
|
|
361
|
+
}
|
|
313
362
|
return;
|
|
314
363
|
}
|
|
315
364
|
|
|
316
365
|
// Interactive mode
|
|
317
366
|
if (argv.interactive) {
|
|
318
367
|
console.log("Starting interactive component selector...\n");
|
|
319
|
-
const selectedComponents = await interactiveComponentSelector();
|
|
368
|
+
const selectedComponents = await interactiveComponentSelector(argv.beta);
|
|
320
369
|
|
|
321
370
|
if (selectedComponents.length === 0) {
|
|
322
371
|
console.log("No components selected. Exiting...");
|
|
@@ -333,7 +382,7 @@ const main = async () => {
|
|
|
333
382
|
}
|
|
334
383
|
|
|
335
384
|
// Install selected components
|
|
336
|
-
await installMultipleComponents(selectedComponents, destPath);
|
|
385
|
+
await installMultipleComponents(selectedComponents, destPath, argv.beta);
|
|
337
386
|
return;
|
|
338
387
|
}
|
|
339
388
|
|
|
@@ -346,10 +395,14 @@ const main = async () => {
|
|
|
346
395
|
|
|
347
396
|
// Parse components (single or multiple)
|
|
348
397
|
const componentInput = argv.add;
|
|
349
|
-
const components = parseMultipleComponents(componentInput)
|
|
398
|
+
const components = parseMultipleComponents(componentInput).map(
|
|
399
|
+
(component) =>
|
|
400
|
+
normalizeComponent(component, getAvailableComponents(argv.beta)) ||
|
|
401
|
+
component,
|
|
402
|
+
);
|
|
350
403
|
|
|
351
404
|
// Validate all components
|
|
352
|
-
if (!validateComponents(components)) {
|
|
405
|
+
if (!validateComponents(components, argv.beta)) {
|
|
353
406
|
process.exit(1);
|
|
354
407
|
}
|
|
355
408
|
|
|
@@ -365,10 +418,10 @@ const main = async () => {
|
|
|
365
418
|
// Install components
|
|
366
419
|
if (components.length === 1) {
|
|
367
420
|
// Single component installation (existing behavior)
|
|
368
|
-
await installComponentWithDependencies(components[0], destPath);
|
|
421
|
+
await installComponentWithDependencies(components[0], destPath, argv.beta);
|
|
369
422
|
} else {
|
|
370
423
|
// Multiple components installation
|
|
371
|
-
await installMultipleComponents(components, destPath);
|
|
424
|
+
await installMultipleComponents(components, destPath, argv.beta);
|
|
372
425
|
}
|
|
373
426
|
};
|
|
374
427
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# DatePicker and DateRangePicker
|
|
2
|
+
|
|
3
|
+
Two date fields built on one engine and styled to match the DataGrid and the
|
|
4
|
+
Combobox. No runtime dependencies besides React 19.
|
|
5
|
+
|
|
6
|
+
- **`<DatePicker>`** — one date.
|
|
7
|
+
- **`<DateRangePicker>`** — a start and an end, with a live preview between them.
|
|
8
|
+
|
|
9
|
+
Both support typed entry in the user's own date order, min/max limits, blocked
|
|
10
|
+
days, month and year panels, week numbers, forms, full keyboard control, dark
|
|
11
|
+
mode and right-to-left layouts.
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { DatePicker, DateRangePicker } from "@/components/date-picker";
|
|
17
|
+
import "@/components/date-picker/styles.css";
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The stylesheet reuses the DataGrid's `--dg-*` variables when that stylesheet is
|
|
21
|
+
loaded, so every component shares a theme, and falls back to the same palette
|
|
22
|
+
when used on its own.
|
|
23
|
+
|
|
24
|
+
## Basic usage
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
const [date, setDate] = useState<Date | null>(null);
|
|
28
|
+
|
|
29
|
+
<DatePicker label="Start date" value={date} onChange={setDate} />;
|
|
30
|
+
|
|
31
|
+
const [range, setRange] = useState<DateRange>({ start: null, end: null });
|
|
32
|
+
|
|
33
|
+
<DateRangePicker label="Period" value={range} onChange={setRange} />;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Both are controlled with `value` + `onChange`, or uncontrolled with
|
|
37
|
+
`defaultValue`. `value`, `defaultValue`, `min` and `max` accept a `Date`, an ISO
|
|
38
|
+
`yyyy-mm-dd` string or a timestamp; `onChange` always gives you `Date` objects at
|
|
39
|
+
local midnight. The range picker's `onChange` also receives a `complete` flag,
|
|
40
|
+
which is `false` after the first of the two clicks.
|
|
41
|
+
|
|
42
|
+
## Props
|
|
43
|
+
|
|
44
|
+
Shared by both components:
|
|
45
|
+
|
|
46
|
+
| Prop | Type | Default | Description |
|
|
47
|
+
| --- | --- | --- | --- |
|
|
48
|
+
| `min` / `max` | `Date \| string \| number` | — | Earliest and latest selectable day. |
|
|
49
|
+
| `isDateDisabled` | `(date: Date) => boolean` | — | Called per rendered day; return true to block it. |
|
|
50
|
+
| `locale` | `string` | runtime locale | Decides month names, field order and the first day of the week. |
|
|
51
|
+
| `weekStartsOn` | `0`–`6` | locale's first day | 0 is Sunday. |
|
|
52
|
+
| `format` | `Intl.DateTimeFormatOptions \| (date, locale) => string` | `{ year, month: "short", day }` | How the chosen date is written in the field. |
|
|
53
|
+
| `numberOfMonths` | `number` | `1` (range: `2`) | Months shown side by side. |
|
|
54
|
+
| `showWeekNumbers` | `boolean` | `false` | Adds an ISO week column. |
|
|
55
|
+
| `showToday` | `boolean` | `true` (range: `false`) | The "Today" shortcut in the footer. |
|
|
56
|
+
| `allowInput` | `boolean` | `true` | Let people type a date. When false the field is read-only and opens on click. |
|
|
57
|
+
| `fixedWeeks` | `boolean` | `true` | Always six week rows, so the popover never changes height. |
|
|
58
|
+
| `label`, `description`, `error` | `ReactNode` | — | Field label, hint and error message. `error` also marks the control invalid. |
|
|
59
|
+
| `placeholder` | `string` | the locale's pattern | e.g. `dd/mm/yyyy`. |
|
|
60
|
+
| `required`, `disabled`, `readOnly` | `boolean` | `false` | Field states. |
|
|
61
|
+
| `clearable` | `boolean` | `true` | Show the clear button. |
|
|
62
|
+
| `size` | `"sm"` \| `"md"` \| `"lg"` | `"md"` | Control height and font size. |
|
|
63
|
+
| `name` | `string` | — | Posts `yyyy-mm-dd` in a hidden input. The range picker posts `name-start` and `name-end`. |
|
|
64
|
+
| `className`, `classNames`, `style` | — | — | Styling hooks. Slots: `root`, `label`, `control`, `input`, `popover`, `calendar`, `day`, `footer`, `presets`. |
|
|
65
|
+
| `localeText` | `Partial<DatePickerLocaleText>` | English | Overrides UI text. |
|
|
66
|
+
| `onOpenChange` | `(open: boolean) => void` | — | Fires when the calendar opens or closes. |
|
|
67
|
+
| `ref` | `Ref<DatePickerHandle<T>>` | — | `open()`, `close()`, `toggle()`, `focus()`, `clear()`, `getValue()`. |
|
|
68
|
+
|
|
69
|
+
`DatePicker` adds `value` / `defaultValue` (`Date \| null`) and `closeOnSelect`
|
|
70
|
+
(default `true`). `DateRangePicker` adds `value` / `defaultValue`
|
|
71
|
+
(`{ start, end }`), `presets` and `closeOnSelect` (closes once both ends are set).
|
|
72
|
+
|
|
73
|
+
## Typed entry
|
|
74
|
+
|
|
75
|
+
People can type instead of picking. Input is read in the locale's own field
|
|
76
|
+
order, so `03/04/2026` is 3 April in `en-GB` and 4 March in `en-US`. ISO
|
|
77
|
+
(`2026-03-12`) is accepted in every locale, month names work (`12 Mar 2026`), and
|
|
78
|
+
a missing year or month falls back to the month on screen. The calendar follows
|
|
79
|
+
along as you type. Text that isn't a date, or a date outside the limits, shows a
|
|
80
|
+
message when the field loses focus and leaves the value untouched.
|
|
81
|
+
|
|
82
|
+
## Range presets
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
<DateRangePicker
|
|
86
|
+
value={range}
|
|
87
|
+
onChange={setRange}
|
|
88
|
+
presets={[
|
|
89
|
+
{ label: "Last 7 days", range: { start: addDays(new Date(), -6), end: new Date() } },
|
|
90
|
+
{ label: "This month", range: { start: startOfMonth(new Date()), end: new Date() } },
|
|
91
|
+
]}
|
|
92
|
+
/>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Keyboard
|
|
96
|
+
|
|
97
|
+
| Keys | Action |
|
|
98
|
+
| --- | --- |
|
|
99
|
+
| **Enter**, **↓** | Open the calendar (from the field). |
|
|
100
|
+
| **←** / **→** | Previous / next day (swapped in right-to-left layouts). |
|
|
101
|
+
| **↑** / **↓** | Same weekday, previous / next week. |
|
|
102
|
+
| **Home** / **End** | First / last day of the week. |
|
|
103
|
+
| **Page Up** / **Page Down** | Previous / next month. |
|
|
104
|
+
| **Shift + Page Up/Down** | Previous / next year. |
|
|
105
|
+
| **Enter**, **Space** | Choose the focused day. |
|
|
106
|
+
| **Escape** | Close and return focus to the calendar button. |
|
|
107
|
+
|
|
108
|
+
Each month is a `role="grid"` where only the focused day is tabbable, which is
|
|
109
|
+
the WAI-ARIA pattern for a date picker. The calendar renders in the top layer
|
|
110
|
+
through the native Popover API, so it is never clipped by a scrolling parent
|
|
111
|
+
(including inside a DataGrid cell).
|
|
112
|
+
|
|
113
|
+
## Theming
|
|
114
|
+
|
|
115
|
+
Override `--dp-*` variables (they default to the grid's `--dg-*`):
|
|
116
|
+
|
|
117
|
+
```css
|
|
118
|
+
.dp-root { --dp-accent: #7c3aed; --dp-radius: 12px; --dp-day-size: 38px; }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
State attributes for styling: `data-state="open"`, `data-size`, `data-invalid`,
|
|
122
|
+
`data-disabled` on the root and control; `data-today`, `data-selected`,
|
|
123
|
+
`data-outside`, `data-weekend` on days; `data-in-range`, `data-range-start`,
|
|
124
|
+
`data-range-end` on day cells.
|
|
125
|
+
|
|
126
|
+
## Headless use
|
|
127
|
+
|
|
128
|
+
`useDatePicker()` holds the whole engine (selection, visible months, typed
|
|
129
|
+
input, keyboard movement, popover state) and is exported if you want a different
|
|
130
|
+
UI on top. The framework-free helpers — `buildMonth`, `buildMonths`, `parseDate`,
|
|
131
|
+
`formatDate`, `moveByKey`, `normalizeRange`, `isInRange`, `toISODate` — are
|
|
132
|
+
exported from `@/components/date-picker/core` and also run on a server.
|
|
133
|
+
|
|
134
|
+
## Known limits
|
|
135
|
+
|
|
136
|
+
- Dates only: no time-of-day or time zone selection. Values are local midnight.
|
|
137
|
+
- One range per field; multiple disjoint ranges aren't supported.
|
|
138
|
+
- Only the Gregorian calendar is handled, though month and weekday names,
|
|
139
|
+
field order and the first day of the week all follow the locale.
|
|
140
|
+
- When rendering on a server, pass `locale` explicitly so the server and the
|
|
141
|
+
browser format the field identically.
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
addDays,
|
|
4
|
+
addMonths,
|
|
5
|
+
buildMonth,
|
|
6
|
+
buildMonths,
|
|
7
|
+
clampDate,
|
|
8
|
+
compareDay,
|
|
9
|
+
getISOWeek,
|
|
10
|
+
isInRange,
|
|
11
|
+
isSameDay,
|
|
12
|
+
moveByKey,
|
|
13
|
+
moveInGrid,
|
|
14
|
+
normalizeRange,
|
|
15
|
+
parseISODate,
|
|
16
|
+
toDate,
|
|
17
|
+
toISODate,
|
|
18
|
+
yearPage,
|
|
19
|
+
} from "../core/calendar";
|
|
20
|
+
import {
|
|
21
|
+
fieldOrder,
|
|
22
|
+
inputPlaceholder,
|
|
23
|
+
monthNames,
|
|
24
|
+
parseDate,
|
|
25
|
+
weekdayNames,
|
|
26
|
+
} from "../core/format";
|
|
27
|
+
|
|
28
|
+
const iso = (date: Date | null) => (date ? toISODate(date) : null);
|
|
29
|
+
|
|
30
|
+
describe("date normalization", () => {
|
|
31
|
+
it("reads ISO strings as local days, never shifted by the time zone", () => {
|
|
32
|
+
expect(iso(toDate("2026-03-12"))).toBe("2026-03-12");
|
|
33
|
+
expect(iso(toDate(new Date(2026, 2, 12, 23, 59)))).toBe("2026-03-12");
|
|
34
|
+
expect(toDate(null)).toBeNull();
|
|
35
|
+
expect(toDate("not a date")).toBeNull();
|
|
36
|
+
expect(parseISODate("2026-02-31")).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("compares and matches days regardless of the time", () => {
|
|
40
|
+
expect(compareDay(new Date(2026, 0, 1, 23), new Date(2026, 0, 2, 0))).toBeLessThan(0);
|
|
41
|
+
expect(isSameDay(new Date(2026, 0, 1, 23), new Date(2026, 0, 1))).toBe(true);
|
|
42
|
+
expect(isSameDay(null, new Date(2026, 0, 1))).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("keeps the day of the month when a month is too short", () => {
|
|
46
|
+
expect(iso(addMonths(new Date(2026, 0, 31), 1))).toBe("2026-02-28");
|
|
47
|
+
expect(iso(addMonths(new Date(2024, 0, 31), 1))).toBe("2024-02-29");
|
|
48
|
+
expect(iso(addDays(new Date(2026, 11, 31), 1))).toBe("2027-01-01");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("clamps to the limits", () => {
|
|
52
|
+
const min = new Date(2026, 0, 10);
|
|
53
|
+
const max = new Date(2026, 0, 20);
|
|
54
|
+
expect(iso(clampDate(new Date(2026, 0, 1), min, max))).toBe("2026-01-10");
|
|
55
|
+
expect(iso(clampDate(new Date(2026, 0, 31), min, max))).toBe("2026-01-20");
|
|
56
|
+
expect(iso(clampDate(new Date(2026, 0, 15), min, max))).toBe("2026-01-15");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("month building", () => {
|
|
61
|
+
const today = new Date(2026, 1, 10);
|
|
62
|
+
|
|
63
|
+
it("returns six aligned weeks with the leading days of the previous month", () => {
|
|
64
|
+
const february = buildMonth(2026, 1, { weekStartsOn: 1, today });
|
|
65
|
+
expect(february.weeks).toHaveLength(6);
|
|
66
|
+
expect(february.weeks[0].days).toHaveLength(7);
|
|
67
|
+
// February 2026 starts on a Sunday, so a Monday-first grid opens on 26 January.
|
|
68
|
+
expect(february.weeks[0].days[0].key).toBe("2026-01-26");
|
|
69
|
+
expect(february.weeks[0].days[0].outside).toBe(true);
|
|
70
|
+
expect(february.weeks[0].days[6].key).toBe("2026-02-01");
|
|
71
|
+
expect(february.weeks[0].days[6].outside).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("starts the week where the caller asks", () => {
|
|
75
|
+
const sundayFirst = buildMonth(2026, 1, { weekStartsOn: 0, today });
|
|
76
|
+
expect(sundayFirst.weeks[0].days[0].key).toBe("2026-02-01");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("marks today, weekends and days outside the limits", () => {
|
|
80
|
+
const february = buildMonth(2026, 1, {
|
|
81
|
+
weekStartsOn: 1,
|
|
82
|
+
today,
|
|
83
|
+
min: new Date(2026, 1, 5),
|
|
84
|
+
isDateDisabled: (date) => date.getDate() === 20,
|
|
85
|
+
});
|
|
86
|
+
const days = february.weeks.flatMap((week) => week.days);
|
|
87
|
+
const byKey = (key: string) => days.find((day) => day.key === key);
|
|
88
|
+
|
|
89
|
+
expect(byKey("2026-02-10")?.today).toBe(true);
|
|
90
|
+
expect(byKey("2026-02-09")?.today).toBe(false);
|
|
91
|
+
expect(byKey("2026-02-07")?.weekend).toBe(true);
|
|
92
|
+
expect(byKey("2026-02-04")?.disabled).toBe(true);
|
|
93
|
+
expect(byKey("2026-02-05")?.disabled).toBe(false);
|
|
94
|
+
expect(byKey("2026-02-20")?.disabled).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("builds consecutive months for a range view", () => {
|
|
98
|
+
const months = buildMonths(new Date(2026, 10, 20), 2, { today });
|
|
99
|
+
expect(months.map((month) => `${month.year}-${month.month}`)).toEqual(["2026-10", "2026-11"]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("numbers ISO weeks, including the ones spanning a year end", () => {
|
|
103
|
+
expect(getISOWeek(new Date(2024, 0, 1))).toBe(1);
|
|
104
|
+
expect(getISOWeek(new Date(2026, 0, 1))).toBe(1);
|
|
105
|
+
expect(getISOWeek(new Date(2021, 0, 1))).toBe(53);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("ranges", () => {
|
|
110
|
+
it("puts a backwards range the right way round", () => {
|
|
111
|
+
const start = new Date(2026, 0, 20);
|
|
112
|
+
const end = new Date(2026, 0, 10);
|
|
113
|
+
expect(iso(normalizeRange({ start, end }).start)).toBe("2026-01-10");
|
|
114
|
+
expect(iso(normalizeRange({ start, end }).end)).toBe("2026-01-20");
|
|
115
|
+
expect(normalizeRange({ start, end: null }).end).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("includes both ends", () => {
|
|
119
|
+
const start = new Date(2026, 0, 10);
|
|
120
|
+
const end = new Date(2026, 0, 20);
|
|
121
|
+
expect(isInRange(new Date(2026, 0, 10), start, end)).toBe(true);
|
|
122
|
+
expect(isInRange(new Date(2026, 0, 20), start, end)).toBe(true);
|
|
123
|
+
expect(isInRange(new Date(2026, 0, 21), start, end)).toBe(false);
|
|
124
|
+
expect(isInRange(new Date(2026, 0, 15), start, null)).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("keyboard movement", () => {
|
|
129
|
+
const wednesday = new Date(2026, 1, 11);
|
|
130
|
+
|
|
131
|
+
it("moves by day, week and month", () => {
|
|
132
|
+
expect(iso(moveByKey(wednesday, "ArrowLeft"))).toBe("2026-02-10");
|
|
133
|
+
expect(iso(moveByKey(wednesday, "ArrowRight"))).toBe("2026-02-12");
|
|
134
|
+
expect(iso(moveByKey(wednesday, "ArrowUp"))).toBe("2026-02-04");
|
|
135
|
+
expect(iso(moveByKey(wednesday, "ArrowDown"))).toBe("2026-02-18");
|
|
136
|
+
expect(iso(moveByKey(wednesday, "PageUp"))).toBe("2026-01-11");
|
|
137
|
+
expect(iso(moveByKey(wednesday, "PageDown"))).toBe("2026-03-11");
|
|
138
|
+
expect(iso(moveByKey(wednesday, "PageUp", { shiftKey: true }))).toBe("2025-02-11");
|
|
139
|
+
expect(iso(moveByKey(wednesday, "PageDown", { shiftKey: true }))).toBe("2027-02-11");
|
|
140
|
+
expect(moveByKey(wednesday, "a")).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("takes Home and End to the ends of the displayed week", () => {
|
|
144
|
+
expect(iso(moveByKey(wednesday, "Home", { weekStartsOn: 1 }))).toBe("2026-02-09");
|
|
145
|
+
expect(iso(moveByKey(wednesday, "End", { weekStartsOn: 1 }))).toBe("2026-02-15");
|
|
146
|
+
expect(iso(moveByKey(wednesday, "Home", { weekStartsOn: 0 }))).toBe("2026-02-08");
|
|
147
|
+
expect(iso(moveByKey(wednesday, "End", { weekStartsOn: 0 }))).toBe("2026-02-14");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("swaps the arrows in right-to-left layouts", () => {
|
|
151
|
+
expect(iso(moveByKey(wednesday, "ArrowRight", { rtl: true }))).toBe("2026-02-10");
|
|
152
|
+
expect(iso(moveByKey(wednesday, "ArrowLeft", { rtl: true }))).toBe("2026-02-12");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("moves inside the month and year panels without leaving the grid", () => {
|
|
156
|
+
expect(moveInGrid(4, "ArrowDown", 3, 12)).toBe(7);
|
|
157
|
+
expect(moveInGrid(10, "ArrowDown", 3, 12)).toBe(11);
|
|
158
|
+
expect(moveInGrid(1, "ArrowUp", 3, 12)).toBe(0);
|
|
159
|
+
expect(moveInGrid(5, "Home", 3, 12)).toBe(0);
|
|
160
|
+
expect(moveInGrid(5, "End", 3, 12)).toBe(11);
|
|
161
|
+
expect(moveInGrid(5, "Enter", 3, 12)).toBeNull();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("pages years in steady blocks", () => {
|
|
165
|
+
expect(yearPage(2026, 12)[0]).toBe(2016);
|
|
166
|
+
expect(yearPage(2026, 12)).toHaveLength(12);
|
|
167
|
+
expect(yearPage(2016, 12)[0]).toBe(2016);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("locale formatting", () => {
|
|
172
|
+
it("reports the field order and a matching placeholder", () => {
|
|
173
|
+
expect(fieldOrder("en-GB")).toEqual(["day", "month", "year"]);
|
|
174
|
+
expect(fieldOrder("en-US")).toEqual(["month", "day", "year"]);
|
|
175
|
+
expect(inputPlaceholder("en-GB")).toBe("dd/mm/yyyy");
|
|
176
|
+
expect(inputPlaceholder("en-US")).toBe("mm/dd/yyyy");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("orders weekday names from the first day of the week", () => {
|
|
180
|
+
expect(weekdayNames("en-GB", 1)[0].short).toBe("Mon");
|
|
181
|
+
expect(weekdayNames("en-US", 0)[0].short).toBe("Sun");
|
|
182
|
+
expect(weekdayNames("en-GB", 1)[0].long).toBe("Monday");
|
|
183
|
+
expect(monthNames("en-GB")[0]).toBe("January");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("typed input", () => {
|
|
188
|
+
const reference = new Date(2026, 4, 20);
|
|
189
|
+
|
|
190
|
+
it("accepts ISO whatever the locale", () => {
|
|
191
|
+
expect(iso(parseDate("2026-03-12", "en-US", reference))).toBe("2026-03-12");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("reads numbers in the locale's own order", () => {
|
|
195
|
+
expect(iso(parseDate("3/4/2026", "en-GB", reference))).toBe("2026-04-03");
|
|
196
|
+
expect(iso(parseDate("3/4/2026", "en-US", reference))).toBe("2026-03-04");
|
|
197
|
+
expect(iso(parseDate("15.8.2026", "de-DE", reference))).toBe("2026-08-15");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("fills in what was left out", () => {
|
|
201
|
+
expect(iso(parseDate("15/8", "en-GB", reference))).toBe("2026-08-15");
|
|
202
|
+
expect(iso(parseDate("5", "en-GB", reference))).toBe("2026-05-05");
|
|
203
|
+
expect(iso(parseDate("15/8/26", "en-GB", reference))).toBe("2026-08-15");
|
|
204
|
+
expect(iso(parseDate("15/8/95", "en-GB", reference))).toBe("1995-08-15");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("understands written month names", () => {
|
|
208
|
+
expect(iso(parseDate("12 Mar 2026", "en-GB", reference))).toBe("2026-03-12");
|
|
209
|
+
expect(iso(parseDate("March 12, 2026", "en-US", reference))).toBe("2026-03-12");
|
|
210
|
+
expect(iso(parseDate("7 September", "en-GB", reference))).toBe("2026-09-07");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("rejects anything that isn't a date", () => {
|
|
214
|
+
expect(parseDate("", "en-GB", reference)).toBeNull();
|
|
215
|
+
expect(parseDate("hello", "en-GB", reference)).toBeNull();
|
|
216
|
+
expect(parseDate("31/2/2026", "en-GB", reference)).toBeNull();
|
|
217
|
+
expect(parseDate("2026-02-31", "en-GB", reference)).toBeNull();
|
|
218
|
+
expect(parseDate("45/45/2026", "en-GB", reference)).toBeNull();
|
|
219
|
+
});
|
|
220
|
+
});
|