@boxcustodia/library 2.0.0-alpha.19 → 2.0.0-alpha.20
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/button/button.cjs.js +1 -1
- package/dist/components/button/button.es.js +19 -18
- package/dist/components/button/components/base-button.cjs.js +1 -1
- package/dist/components/button/components/base-button.es.js +20 -20
- package/dist/components/calendar/calendar.cjs.js +1 -1
- package/dist/components/calendar/calendar.es.js +1 -0
- package/dist/components/date-picker/date-input.cjs.js +1 -1
- package/dist/components/date-picker/date-input.es.js +92 -75
- package/dist/components/date-picker/date-picker.cjs.js +1 -1
- package/dist/components/date-picker/date-picker.es.js +104 -95
- package/dist/components/date-picker/date-picker.utils.cjs.js +1 -1
- package/dist/components/date-picker/date-picker.utils.es.js +51 -43
- package/dist/components/date-picker/use-hidden-field-value.cjs.js +1 -0
- package/dist/components/date-picker/use-hidden-field-value.es.js +11 -0
- package/dist/components/menu/menu.es.js +1 -9
- package/dist/components/otp/otp.cjs.js +2 -0
- package/dist/components/otp/otp.es.js +93 -0
- package/dist/components/password/password.cjs.js +1 -1
- package/dist/components/password/password.es.js +2 -2
- package/dist/components/select/select.cjs.js +1 -1
- package/dist/components/select/select.es.js +68 -60
- package/dist/hooks/internal/is-apple-device.cjs.js +1 -0
- package/dist/hooks/internal/is-apple-device.es.js +9 -0
- package/dist/hooks/internal/use-latest-ref.cjs.js +1 -0
- package/dist/hooks/internal/use-latest-ref.es.js +11 -0
- package/dist/hooks/use-array/use-array.cjs.js +1 -1
- package/dist/hooks/use-array/use-array.es.js +54 -42
- package/dist/hooks/use-async/use-async.cjs.js +1 -1
- package/dist/hooks/use-async/use-async.es.js +53 -20
- package/dist/hooks/use-boolean/use-boolean.cjs.js +1 -0
- package/dist/hooks/use-boolean/use-boolean.es.js +25 -0
- package/dist/hooks/use-click-outside/use-click-outside.cjs.js +1 -1
- package/dist/hooks/use-click-outside/use-click-outside.es.js +26 -12
- package/dist/hooks/use-debounce-callback/use-debounced-callback.cjs.js +1 -1
- package/dist/hooks/use-debounce-callback/use-debounced-callback.es.js +27 -10
- package/dist/hooks/use-debounce-value/use-debounced-value.cjs.js +1 -1
- package/dist/hooks/use-debounce-value/use-debounced-value.es.js +7 -9
- package/dist/hooks/use-disclosure/use-disclosure.cjs.js +1 -1
- package/dist/hooks/use-disclosure/use-disclosure.es.js +21 -11
- package/dist/hooks/use-document-title/use-document-title.cjs.js +1 -1
- package/dist/hooks/use-document-title/use-document-title.es.js +14 -12
- package/dist/hooks/use-event-listener/use-event-listener.cjs.js +1 -1
- package/dist/hooks/use-event-listener/use-event-listener.es.js +17 -9
- package/dist/hooks/use-hotkey/use-hotkey.cjs.js +1 -1
- package/dist/hooks/use-hotkey/use-hotkey.es.js +30 -14
- package/dist/hooks/use-hotkey/utils/is-input-field.cjs.js +1 -1
- package/dist/hooks/use-hotkey/utils/is-input-field.es.js +4 -2
- package/dist/hooks/use-hotkey/utils/match-and-run.cjs.js +1 -0
- package/dist/hooks/use-hotkey/utils/match-and-run.es.js +12 -0
- package/dist/hooks/use-hotkey/utils/match-key-modifiers.cjs.js +1 -1
- package/dist/hooks/use-hotkey/utils/match-key-modifiers.es.js +13 -12
- package/dist/hooks/use-hover/use-hover.cjs.js +1 -1
- package/dist/hooks/use-hover/use-hover.es.js +32 -17
- package/dist/hooks/use-is-visible/use-is-visible.cjs.js +1 -1
- package/dist/hooks/use-is-visible/use-is-visible.es.js +31 -27
- package/dist/hooks/use-local-storage/use-local-storage.cjs.js +1 -1
- package/dist/hooks/use-local-storage/use-local-storage.es.js +52 -20
- package/dist/hooks/use-media-query/use-media-query.cjs.js +1 -1
- package/dist/hooks/use-media-query/use-media-query.es.js +21 -11
- package/dist/hooks/use-mutation/use-mutation.cjs.js +1 -1
- package/dist/hooks/use-mutation/use-mutation.es.js +36 -22
- package/dist/hooks/use-object/use-object.cjs.js +1 -1
- package/dist/hooks/use-object/use-object.es.js +26 -22
- package/dist/hooks/use-prevent-page-close/use-prevent-page-close.cjs.js +1 -0
- package/dist/hooks/use-prevent-page-close/use-prevent-page-close.es.js +14 -0
- package/dist/hooks/use-step/use-step.cjs.js +1 -1
- package/dist/hooks/use-step/use-step.es.js +25 -24
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +308 -300
- package/dist/src/components/date-picker/date-picker.utils.d.ts +17 -0
- package/dist/src/components/date-picker/use-hidden-field-value.d.ts +12 -0
- package/dist/src/components/index.d.ts +1 -0
- package/dist/src/hooks/index.d.ts +2 -2
- package/dist/src/hooks/internal/index.d.ts +2 -0
- package/dist/src/hooks/internal/is-apple-device.d.ts +12 -0
- package/dist/src/hooks/internal/use-latest-ref.d.ts +12 -0
- package/dist/src/hooks/use-array/use-array.d.ts +24 -11
- package/dist/src/hooks/use-async/use-async.d.ts +16 -13
- package/dist/src/hooks/use-boolean/index.d.ts +1 -0
- package/dist/src/hooks/use-boolean/use-boolean.d.ts +15 -0
- package/dist/src/hooks/use-boolean/use-boolean.test.d.ts +1 -0
- package/dist/src/hooks/use-click-outside/use-click-outside.d.ts +23 -1
- package/dist/src/hooks/use-debounce-callback/use-debounced-callback.d.ts +19 -1
- package/dist/src/hooks/use-debounce-value/use-debounced-value.d.ts +10 -1
- package/dist/src/hooks/use-disclosure/use-disclosure.d.ts +17 -8
- package/dist/src/hooks/use-document-title/use-document-title.d.ts +11 -0
- package/dist/src/hooks/use-event-listener/use-event-listener.d.ts +18 -1
- package/dist/src/hooks/use-hotkey/index.d.ts +2 -1
- package/dist/src/hooks/use-hotkey/use-hotkey.d.ts +62 -5
- package/dist/src/hooks/use-hotkey/utils/index.d.ts +4 -3
- package/dist/src/hooks/use-hotkey/utils/is-input-field.d.ts +12 -2
- package/dist/src/hooks/use-hotkey/utils/is-input-field.test.d.ts +1 -0
- package/dist/src/hooks/use-hotkey/utils/match-and-run.d.ts +36 -0
- package/dist/src/hooks/use-hotkey/utils/match-and-run.test.d.ts +1 -0
- package/dist/src/hooks/use-hotkey/utils/match-key-modifiers.d.ts +20 -6
- package/dist/src/hooks/use-hotkey/utils/match-key-modifiers.test.d.ts +1 -0
- package/dist/src/hooks/use-hover/use-hover.d.ts +8 -4
- package/dist/src/hooks/use-is-visible/use-is-visible.d.ts +28 -4
- package/dist/src/hooks/use-local-storage/use-local-storage.d.ts +13 -2
- package/dist/src/hooks/use-media-query/use-media-query.d.ts +10 -1
- package/dist/src/hooks/use-media-query/use-media-query.test.d.ts +1 -0
- package/dist/src/hooks/use-mutation/use-mutation.d.ts +18 -11
- package/dist/src/hooks/use-object/use-object.d.ts +15 -6
- package/dist/src/hooks/use-prevent-page-close/index.d.ts +1 -0
- package/dist/src/hooks/use-prevent-page-close/use-prevent-page-close.d.ts +10 -0
- package/dist/src/hooks/use-prevent-page-close/use-prevent-page-close.test.d.ts +1 -0
- package/dist/src/hooks/use-step/use-step.d.ts +18 -11
- package/dist/src/utils/form.d.ts +10 -0
- package/package.json +1 -1
- package/src/components/alert-dialog/alert-dialog.test.tsx +13 -9
- package/src/components/auto-complete/auto-complete.test.tsx +4 -14
- package/src/components/avatar/avatar.test.tsx +7 -12
- package/src/components/button/button.test.tsx +10 -15
- package/src/components/button/button.tsx +14 -9
- package/src/components/button/components/base-button.tsx +2 -4
- package/src/components/calendar/calendar.test.tsx +12 -19
- package/src/components/calendar/calendar.tsx +4 -0
- package/src/components/card/card.test.tsx +4 -6
- package/src/components/checkbox/checkbox.test.tsx +12 -8
- package/src/components/checkbox-group/checkbox-group.test.tsx +7 -8
- package/src/components/combobox/combobox.test.tsx +24 -21
- package/src/components/date-picker/date-input-form.test.tsx +77 -0
- package/src/components/date-picker/date-input.stories.tsx +30 -18
- package/src/components/date-picker/date-input.tsx +77 -44
- package/src/components/date-picker/date-picker.stories.tsx +31 -1
- package/src/components/date-picker/date-picker.test.tsx +3 -13
- package/src/components/date-picker/date-picker.tsx +35 -16
- package/src/components/date-picker/date-picker.utils.test.ts +32 -14
- package/src/components/date-picker/date-picker.utils.ts +33 -0
- package/src/components/date-picker/use-date-input-popover.test.ts +3 -1
- package/src/components/date-picker/use-hidden-field-value.ts +23 -0
- package/src/components/dialog/dialog.test.tsx +10 -8
- package/src/components/dropzone/dropzone.test.tsx +11 -13
- package/src/components/empty/empty.test.tsx +4 -3
- package/src/components/field/field.test.tsx +12 -13
- package/src/components/form/form.stories.tsx +16 -1
- package/src/components/index.ts +1 -0
- package/src/components/label/label.test.tsx +3 -3
- package/src/components/menu/menu.tsx +1 -5
- package/src/components/number-input/number-input.test.tsx +6 -2
- package/src/components/password/password.test.tsx +20 -6
- package/src/components/password/password.tsx +2 -2
- package/src/components/popover/popover.test.tsx +4 -4
- package/src/components/progress/progress.test.tsx +7 -8
- package/src/components/radio-group/radio-group.test.tsx +17 -11
- package/src/components/select/select.test.tsx +10 -10
- package/src/components/select/select.tsx +9 -1
- package/src/components/stepper/stepper.stories.tsx +11 -15
- package/src/components/stepper/stepper.test.tsx +6 -4
- package/src/components/switch/switch.test.tsx +3 -3
- package/src/components/table/table.test.tsx +9 -3
- package/src/components/tabs/tabs.test.tsx +6 -2
- package/src/components/tag/tag.test.tsx +1 -3
- package/src/components/textarea/textarea.test.tsx +4 -1
- package/src/components/timeline/timeline.test.tsx +10 -5
- package/src/components/toast/toast.test.tsx +11 -14
- package/src/components/tooltip/tooltip.test.tsx +1 -5
- package/src/components/tree/tree.test.tsx +3 -1
- package/src/hooks/index.ts +2 -2
- package/src/hooks/internal/index.ts +2 -0
- package/src/hooks/internal/is-apple-device.test.ts +41 -0
- package/src/hooks/internal/is-apple-device.ts +33 -0
- package/src/hooks/internal/use-isomorphic-layout-effect.ts +3 -1
- package/src/hooks/internal/use-latest-ref.ts +21 -0
- package/src/hooks/use-array/use-array.stories.tsx +435 -64
- package/src/hooks/use-array/use-array.test.tsx +398 -15
- package/src/hooks/use-array/use-array.ts +105 -66
- package/src/hooks/use-async/use-async.stories.tsx +255 -131
- package/src/hooks/use-async/use-async.test.ts +397 -0
- package/src/hooks/use-async/use-async.ts +117 -39
- package/src/hooks/use-boolean/index.ts +1 -0
- package/src/hooks/use-boolean/use-boolean.stories.tsx +377 -0
- package/src/hooks/use-boolean/use-boolean.test.tsx +177 -0
- package/src/hooks/use-boolean/use-boolean.ts +50 -0
- package/src/hooks/use-click-outside/use-click-outside.stories.tsx +188 -18
- package/src/hooks/use-click-outside/use-click-outside.test.tsx +89 -10
- package/src/hooks/use-click-outside/use-click-outside.ts +62 -16
- package/src/hooks/use-debounce-callback/use-debounced-callback.stories.tsx +141 -41
- package/src/hooks/use-debounce-callback/use-debounced-callback.test.ts +217 -9
- package/src/hooks/use-debounce-callback/use-debounced-callback.ts +71 -11
- package/src/hooks/use-debounce-value/use-debounced-value.stories.tsx +247 -47
- package/src/hooks/use-debounce-value/use-debounced-value.test.ts +105 -10
- package/src/hooks/use-debounce-value/use-debounced-value.ts +19 -10
- package/src/hooks/use-disclosure/use-disclosure.stories.tsx +305 -14
- package/src/hooks/use-disclosure/use-disclosure.test.ts +198 -50
- package/src/hooks/use-disclosure/use-disclosure.ts +49 -29
- package/src/hooks/use-document-title/use-document-title.stories.tsx +54 -0
- package/src/hooks/use-document-title/use-document-title.test.tsx +26 -0
- package/src/hooks/use-document-title/{use-document-title.tsx → use-document-title.ts} +17 -3
- package/src/hooks/use-event-listener/use-event-listener.stories.tsx +105 -9
- package/src/hooks/use-event-listener/use-event-listener.test.tsx +77 -10
- package/src/hooks/use-event-listener/use-event-listener.ts +71 -11
- package/src/hooks/use-focus-trap/use-focus-trap.test.ts +31 -6
- package/src/hooks/use-focus-trap/use-focus-trap.ts +3 -2
- package/src/hooks/use-hotkey/index.ts +9 -1
- package/src/hooks/use-hotkey/use-hotkey.stories.tsx +279 -74
- package/src/hooks/use-hotkey/use-hotkey.test.tsx +286 -34
- package/src/hooks/use-hotkey/use-hotkey.ts +141 -17
- package/src/hooks/use-hotkey/utils/index.ts +8 -3
- package/src/hooks/use-hotkey/utils/is-input-field.test.ts +78 -0
- package/src/hooks/use-hotkey/utils/is-input-field.ts +31 -10
- package/src/hooks/use-hotkey/utils/match-and-run.test.ts +203 -0
- package/src/hooks/use-hotkey/utils/match-and-run.ts +62 -0
- package/src/hooks/use-hotkey/utils/match-key-modifiers.test.ts +65 -0
- package/src/hooks/use-hotkey/utils/match-key-modifiers.ts +39 -12
- package/src/hooks/use-hover/use-hover.stories.tsx +258 -80
- package/src/hooks/use-hover/use-hover.test.tsx +266 -26
- package/src/hooks/use-hover/use-hover.tsx +93 -28
- package/src/hooks/use-is-visible/use-is-visible.stories.tsx +193 -46
- package/src/hooks/use-is-visible/use-is-visible.test.tsx +235 -7
- package/src/hooks/use-is-visible/use-is-visible.ts +114 -0
- package/src/hooks/use-local-storage/use-local-storage.stories.tsx +129 -29
- package/src/hooks/use-local-storage/use-local-storage.test.ts +106 -41
- package/src/hooks/use-local-storage/use-local-storage.ts +100 -31
- package/src/hooks/use-media-query/use-media-query.stories.tsx +86 -26
- package/src/hooks/use-media-query/use-media-query.test.ts +132 -0
- package/src/hooks/use-media-query/use-media-query.ts +39 -14
- package/src/hooks/use-memoized-fn/use-memoized-fn.ts +0 -1
- package/src/hooks/use-mutation/use-mutation.stories.tsx +260 -94
- package/src/hooks/use-mutation/use-mutation.test.ts +359 -0
- package/src/hooks/use-mutation/use-mutation.ts +97 -0
- package/src/hooks/use-object/use-object.stories.tsx +310 -79
- package/src/hooks/use-object/use-object.test.tsx +235 -56
- package/src/hooks/use-object/use-object.ts +59 -0
- package/src/hooks/use-pagination/use-pagination.tsx +0 -1
- package/src/hooks/use-prevent-page-close/index.ts +1 -0
- package/src/hooks/use-prevent-page-close/use-prevent-page-close.stories.tsx +39 -0
- package/src/hooks/use-prevent-page-close/use-prevent-page-close.test.ts +89 -0
- package/src/hooks/use-prevent-page-close/use-prevent-page-close.ts +27 -0
- package/src/hooks/use-range-pagination/use-range-pagination.test.tsx +1 -1
- package/src/hooks/use-range-pagination/use-range-pagination.tsx +1 -1
- package/src/hooks/use-selection/use-selection.ts +0 -1
- package/src/hooks/use-step/use-step.stories.tsx +178 -65
- package/src/hooks/use-step/use-step.test.ts +178 -53
- package/src/hooks/use-step/use-step.ts +57 -49
- package/src/utils/form.test.tsx +13 -8
- package/src/utils/form.tsx +10 -0
- package/src/utils/functions/getFormData.test.ts +1 -1
- package/dist/hooks/use-hotkey/utils/create-hotkey-listener.cjs.js +0 -1
- package/dist/hooks/use-hotkey/utils/create-hotkey-listener.es.js +0 -10
- package/dist/hooks/use-prevent-close-window/use-prevent-close-window.cjs.js +0 -1
- package/dist/hooks/use-prevent-close-window/use-prevent-close-window.es.js +0 -15
- package/dist/hooks/use-toggle/use-toggle.cjs.js +0 -1
- package/dist/hooks/use-toggle/use-toggle.es.js +0 -10
- package/dist/src/hooks/use-hotkey/utils/create-hotkey-listener.d.ts +0 -1
- package/dist/src/hooks/use-prevent-close-window/index.d.ts +0 -1
- package/dist/src/hooks/use-prevent-close-window/use-prevent-close-window.d.ts +0 -13
- package/dist/src/hooks/use-toggle/index.d.ts +0 -1
- package/dist/src/hooks/use-toggle/use-toggle.d.ts +0 -3
- package/src/hooks/use-async/use-async.test.tsx +0 -68
- package/src/hooks/use-hotkey/utils/create-hotkey-listener.ts +0 -25
- package/src/hooks/use-is-visible/use-is-visible.tsx +0 -49
- package/src/hooks/use-mutation/use-mutation.test.tsx +0 -83
- package/src/hooks/use-mutation/use-mutation.tsx +0 -59
- package/src/hooks/use-object/use-object.tsx +0 -46
- package/src/hooks/use-prevent-close-window/index.ts +0 -1
- package/src/hooks/use-prevent-close-window/use-prevent-close-window.stories.tsx +0 -32
- package/src/hooks/use-prevent-close-window/use-prevent-close-window.test.ts +0 -79
- package/src/hooks/use-prevent-close-window/use-prevent-close-window.ts +0 -33
- package/src/hooks/use-toggle/index.ts +0 -1
- package/src/hooks/use-toggle/use-toggle.stories.tsx +0 -25
- package/src/hooks/use-toggle/use-toggle.test.tsx +0 -64
- package/src/hooks/use-toggle/use-toggle.ts +0 -14
- /package/dist/src/{hooks/use-prevent-close-window/use-prevent-close-window.test.d.ts → components/date-picker/date-input-form.test.d.ts} +0 -0
- /package/dist/src/hooks/{use-toggle/use-toggle.test.d.ts → internal/is-apple-device.test.d.ts} +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { render, screen, within } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { Button } from "../button/button";
|
|
5
|
+
import { Field } from "../field/field";
|
|
6
|
+
import { Form } from "../form/form";
|
|
7
|
+
import { DateInput } from "./date-input";
|
|
8
|
+
|
|
9
|
+
const setup = () => {
|
|
10
|
+
const onFormSubmit = vi.fn();
|
|
11
|
+
const user = userEvent.setup();
|
|
12
|
+
render(
|
|
13
|
+
<Form onFormSubmit={onFormSubmit}>
|
|
14
|
+
<Field
|
|
15
|
+
name="startDate"
|
|
16
|
+
label="Start date"
|
|
17
|
+
error={{ message: "Start date is required.", match: "valueMissing" }}
|
|
18
|
+
>
|
|
19
|
+
<DateInput required />
|
|
20
|
+
</Field>
|
|
21
|
+
<Button type="submit">Submit</Button>
|
|
22
|
+
</Form>,
|
|
23
|
+
);
|
|
24
|
+
return { onFormSubmit, user };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe("DateInput + Form across flows", () => {
|
|
28
|
+
it("A: digits typed → submits iso", async () => {
|
|
29
|
+
const { onFormSubmit, user } = setup();
|
|
30
|
+
const input = screen.getByRole("textbox");
|
|
31
|
+
await user.click(input);
|
|
32
|
+
await user.type(input, "05062026");
|
|
33
|
+
await user.tab();
|
|
34
|
+
await user.click(screen.getByRole("button", { name: /submit/i }));
|
|
35
|
+
expect(onFormSubmit).toHaveBeenCalledTimes(1);
|
|
36
|
+
expect(onFormSubmit.mock.calls[0][0]).toMatchObject({
|
|
37
|
+
startDate: "2026-06-05",
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("B: open+close popover then digits → submits iso", async () => {
|
|
42
|
+
const { onFormSubmit, user } = setup();
|
|
43
|
+
await user.click(screen.getByRole("button", { name: "Seleccionar fecha" }));
|
|
44
|
+
await screen.findByRole("grid");
|
|
45
|
+
await user.keyboard("{Escape}");
|
|
46
|
+
const input = screen.getByRole("textbox");
|
|
47
|
+
await user.click(input);
|
|
48
|
+
await user.type(input, "05062026");
|
|
49
|
+
await user.tab();
|
|
50
|
+
await user.click(screen.getByRole("button", { name: /submit/i }));
|
|
51
|
+
expect(onFormSubmit).toHaveBeenCalledTimes(1);
|
|
52
|
+
expect(onFormSubmit.mock.calls[0][0]).toMatchObject({
|
|
53
|
+
startDate: "2026-06-05",
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("C: select from calendar → submits iso", async () => {
|
|
58
|
+
const { onFormSubmit, user } = setup();
|
|
59
|
+
await user.click(screen.getByRole("button", { name: "Seleccionar fecha" }));
|
|
60
|
+
const grid = await screen.findByRole("grid");
|
|
61
|
+
await user.click(
|
|
62
|
+
within(grid).getByRole("button", { name: /15 de junio de 2026/ }),
|
|
63
|
+
);
|
|
64
|
+
await user.click(screen.getByRole("button", { name: /submit/i }));
|
|
65
|
+
expect(onFormSubmit).toHaveBeenCalledTimes(1);
|
|
66
|
+
expect(onFormSubmit.mock.calls[0][0]).toMatchObject({
|
|
67
|
+
startDate: "2026-06-15",
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("D: empty → blocks submit with required error", async () => {
|
|
72
|
+
const { onFormSubmit, user } = setup();
|
|
73
|
+
await user.click(screen.getByRole("button", { name: /submit/i }));
|
|
74
|
+
expect(onFormSubmit).not.toHaveBeenCalled();
|
|
75
|
+
expect(screen.getByText("Start date is required.")).toBeInTheDocument();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
|
2
2
|
import { lastDayOfMonth, nextSaturday } from "date-fns";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { action } from "storybook/actions";
|
|
5
|
-
import { Button } from "../../components";
|
|
5
|
+
import { Button, Field, Form } from "../../components";
|
|
6
6
|
import { DateInput } from "./date-input";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -145,22 +145,34 @@ export const Controlled: Story = {
|
|
|
145
145
|
};
|
|
146
146
|
|
|
147
147
|
/**
|
|
148
|
-
* `
|
|
149
|
-
* The value
|
|
150
|
-
*
|
|
148
|
+
* `Field` wires `required` validation and routes the error message automatically.
|
|
149
|
+
* The value submitted to the form is an ISO date string (`yyyy-MM-dd`);
|
|
150
|
+
* an empty field submits an empty string, which triggers `valueMissing`.
|
|
151
151
|
*/
|
|
152
|
-
export const
|
|
153
|
-
render: () =>
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
152
|
+
export const WithForm: Story = {
|
|
153
|
+
render: () => {
|
|
154
|
+
const [submitted, setSubmitted] = useState<string | null>(null);
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<Form
|
|
158
|
+
className="flex flex-col gap-3"
|
|
159
|
+
onFormSubmit={(data) => setSubmitted(data.startDate as string)}
|
|
160
|
+
>
|
|
161
|
+
<Field
|
|
162
|
+
name="startDate"
|
|
163
|
+
label="Start date"
|
|
164
|
+
error={{ message: "Start date is required.", match: "valueMissing" }}
|
|
165
|
+
>
|
|
166
|
+
<DateInput required />
|
|
167
|
+
</Field>
|
|
168
|
+
<Button type="submit">Submit</Button>
|
|
169
|
+
{submitted !== null && (
|
|
170
|
+
<p className="text-sm text-muted-foreground">
|
|
171
|
+
Submitted:{" "}
|
|
172
|
+
<span className="font-mono">{submitted || "(empty)"}</span>
|
|
173
|
+
</p>
|
|
174
|
+
)}
|
|
175
|
+
</Form>
|
|
176
|
+
);
|
|
177
|
+
},
|
|
166
178
|
};
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
|
4
|
-
import { isDate } from "date-fns";
|
|
4
|
+
import { format, isDate } from "date-fns";
|
|
5
5
|
import { CalendarIcon } from "lucide-react";
|
|
6
6
|
import { KeyboardEvent } from "react";
|
|
7
7
|
import {
|
|
8
8
|
Calendar,
|
|
9
9
|
FieldControl,
|
|
10
|
+
FieldRoot,
|
|
10
11
|
inputBaseClasses,
|
|
11
12
|
PopoverPopup,
|
|
12
13
|
PopoverRoot,
|
|
@@ -16,6 +17,7 @@ import { cn } from "../../lib";
|
|
|
16
17
|
import type { DateInputProps } from "./date-picker.model";
|
|
17
18
|
import { useDateInput } from "./use-date-input";
|
|
18
19
|
import { useDateInputPopover } from "./use-date-input-popover";
|
|
20
|
+
import { useHiddenFieldValue } from "./use-hidden-field-value";
|
|
19
21
|
|
|
20
22
|
export const DateInput = (props: DateInputProps) => {
|
|
21
23
|
const {
|
|
@@ -29,6 +31,7 @@ export const DateInput = (props: DateInputProps) => {
|
|
|
29
31
|
renderFooter,
|
|
30
32
|
disabled,
|
|
31
33
|
onBlur: onBlurProp,
|
|
34
|
+
required,
|
|
32
35
|
...rest
|
|
33
36
|
} = props;
|
|
34
37
|
|
|
@@ -38,6 +41,12 @@ export const DateInput = (props: DateInputProps) => {
|
|
|
38
41
|
defaultProp: defaultValueProp ?? null,
|
|
39
42
|
});
|
|
40
43
|
|
|
44
|
+
// Value handed to the form, derived from selectedDate. Empty string when no
|
|
45
|
+
// date so `required` triggers `valueMissing`.
|
|
46
|
+
const formValue = selectedDate ? format(selectedDate, "yyyy-MM-dd") : "";
|
|
47
|
+
|
|
48
|
+
const hiddenInputRef = useHiddenFieldValue(formValue);
|
|
49
|
+
|
|
41
50
|
const {
|
|
42
51
|
inputValue,
|
|
43
52
|
handleChange,
|
|
@@ -90,10 +99,12 @@ export const DateInput = (props: DateInputProps) => {
|
|
|
90
99
|
<div
|
|
91
100
|
className={cn(
|
|
92
101
|
inputBaseClasses,
|
|
93
|
-
"flex items-center gap-2 h-8",
|
|
102
|
+
"relative flex items-center gap-2 h-8",
|
|
94
103
|
"has-[:focus-visible]:border-ring",
|
|
95
104
|
"aria-invalid:border-error has-[:focus-visible]:aria-invalid:ring-error/20",
|
|
96
|
-
|
|
105
|
+
// Re-assert the error border on focus: `has-[:focus-visible]:border-ring`
|
|
106
|
+
// ties on specificity and would otherwise wash the red out while typing.
|
|
107
|
+
"has-aria-invalid:border-error has-[:focus-visible]:has-aria-invalid:border-error has-[:focus-visible]:has-aria-invalid:ring-error/20",
|
|
97
108
|
"has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50",
|
|
98
109
|
!selectedDate && "text-muted-foreground",
|
|
99
110
|
className,
|
|
@@ -103,6 +114,43 @@ export const DateInput = (props: DateInputProps) => {
|
|
|
103
114
|
if (e.target === e.currentTarget) inputRef.current?.focus();
|
|
104
115
|
}}
|
|
105
116
|
>
|
|
117
|
+
{/* Hidden form control — owns the Field's name + validity. Left
|
|
118
|
+
uncontrolled and driven imperatively via `emitNativeInputChange`: a
|
|
119
|
+
controlled `value` keeps React's value tracker in sync, which would
|
|
120
|
+
suppress the synthetic `input` event Base UI needs to re-validate.
|
|
121
|
+
`onFocus` redirects to the visible input so that when the Form focuses
|
|
122
|
+
the invalid control on submit, focus does not trap on this sr-only node. */}
|
|
123
|
+
<FieldControl
|
|
124
|
+
render={
|
|
125
|
+
<input
|
|
126
|
+
ref={hiddenInputRef}
|
|
127
|
+
type="text"
|
|
128
|
+
className="sr-only"
|
|
129
|
+
tabIndex={-1}
|
|
130
|
+
aria-hidden="true"
|
|
131
|
+
required={required}
|
|
132
|
+
disabled={disabled}
|
|
133
|
+
onFocus={() => inputRef.current?.focus()}
|
|
134
|
+
/>
|
|
135
|
+
}
|
|
136
|
+
/>
|
|
137
|
+
<input
|
|
138
|
+
type="text"
|
|
139
|
+
value={inputValue}
|
|
140
|
+
onChange={handleChange}
|
|
141
|
+
onBlur={(e) => {
|
|
142
|
+
handleBlur();
|
|
143
|
+
onBlurProp?.(e);
|
|
144
|
+
}}
|
|
145
|
+
onKeyDown={handleKeyDown}
|
|
146
|
+
placeholder={placeholder ?? "dd/mm/aaaa"}
|
|
147
|
+
autoComplete={autoComplete ?? "off"}
|
|
148
|
+
disabled={disabled}
|
|
149
|
+
className="flex-1 min-w-0 bg-transparent outline-none text-start truncate pt-px"
|
|
150
|
+
aria-label={placeholder}
|
|
151
|
+
ref={inputRef}
|
|
152
|
+
{...rest}
|
|
153
|
+
/>
|
|
106
154
|
<PopoverTrigger
|
|
107
155
|
type="button"
|
|
108
156
|
disabled={disabled}
|
|
@@ -112,53 +160,38 @@ export const DateInput = (props: DateInputProps) => {
|
|
|
112
160
|
<CalendarIcon data-slot="date-input-icon" />
|
|
113
161
|
<span className="sr-only">Seleccionar fecha</span>
|
|
114
162
|
</PopoverTrigger>
|
|
115
|
-
<FieldControl
|
|
116
|
-
render={
|
|
117
|
-
<input
|
|
118
|
-
type="text"
|
|
119
|
-
value={inputValue}
|
|
120
|
-
onChange={handleChange}
|
|
121
|
-
onBlur={(e) => {
|
|
122
|
-
handleBlur();
|
|
123
|
-
onBlurProp?.(e);
|
|
124
|
-
}}
|
|
125
|
-
onKeyDown={handleKeyDown}
|
|
126
|
-
placeholder={placeholder ?? "dd/mm/aaaa"}
|
|
127
|
-
autoComplete={autoComplete ?? "off"}
|
|
128
|
-
disabled={disabled}
|
|
129
|
-
className="flex-1 min-w-0 bg-transparent outline-none text-start truncate pt-px"
|
|
130
|
-
aria-label={placeholder}
|
|
131
|
-
ref={inputRef}
|
|
132
|
-
{...rest}
|
|
133
|
-
/>
|
|
134
|
-
}
|
|
135
|
-
/>
|
|
136
163
|
</div>
|
|
137
164
|
<PopoverPopup
|
|
138
165
|
className="w-auto p-0"
|
|
139
166
|
align="start"
|
|
140
167
|
data-slot="date-input-content"
|
|
141
168
|
>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
169
|
+
{/* Isolate the popover content in its own FieldRoot. The Calendar's
|
|
170
|
+
month/year `Select`s are Base UI form controls; without this they
|
|
171
|
+
connect to the OUTER Field and hijack its shared validation ref,
|
|
172
|
+
nulling it on close and breaking submit validation. */}
|
|
173
|
+
<FieldRoot className="contents">
|
|
174
|
+
<Calendar
|
|
175
|
+
className="border-none"
|
|
176
|
+
mode="single"
|
|
177
|
+
selected={selectedDate ?? undefined}
|
|
178
|
+
onSelect={(next) => {
|
|
179
|
+
if (!next || !isDate(next)) {
|
|
180
|
+
setFromExternalDate(null);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (disabledDate?.(next)) return;
|
|
184
|
+
setFromExternalDate(next);
|
|
185
|
+
setOpen(false);
|
|
186
|
+
}}
|
|
187
|
+
disabled={disabledDate}
|
|
188
|
+
/>
|
|
189
|
+
{footerContent && (
|
|
190
|
+
<div className="p-2" data-slot="date-input-footer">
|
|
191
|
+
{footerContent}
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
</FieldRoot>
|
|
162
195
|
</PopoverPopup>
|
|
163
196
|
</PopoverRoot>
|
|
164
197
|
);
|
|
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
|
2
2
|
import { lastDayOfMonth, nextSaturday } from "date-fns";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { action } from "storybook/actions";
|
|
5
|
-
import { Button } from "../../components";
|
|
5
|
+
import { Button, Field, Form } from "../../components";
|
|
6
6
|
import { DatePicker } from "./date-picker";
|
|
7
7
|
import type { DatePickerFooterProps, DateRange } from "./date-picker.model";
|
|
8
8
|
|
|
@@ -224,3 +224,33 @@ export const WithFooter: Story = {
|
|
|
224
224
|
return <DatePicker mode="single" renderFooter={Footer} />;
|
|
225
225
|
},
|
|
226
226
|
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Native form integration with the plain `Form` component — no schema library.
|
|
230
|
+
*
|
|
231
|
+
* `DatePicker` keeps a hidden `FieldControl` whose value is the ISO date
|
|
232
|
+
* (`yyyy-MM-dd` in single mode, `""` when empty). That hidden control owns the
|
|
233
|
+
* `name`, carries `required`, and is what lands in the submitted values.
|
|
234
|
+
*
|
|
235
|
+
* Validation is the browser's: `required` + an empty string make the field
|
|
236
|
+
* `valueMissing`, so the browser blocks submit and `Field`'s `error.match`
|
|
237
|
+
* surfaces the message. `onFormSubmit` only fires once the value is valid —
|
|
238
|
+
* the `console.log` shows the submitted values (`startDate` as `yyyy-MM-dd`).
|
|
239
|
+
*/
|
|
240
|
+
export const WithForm: Story = {
|
|
241
|
+
render: () => (
|
|
242
|
+
<Form
|
|
243
|
+
className="flex flex-col gap-3"
|
|
244
|
+
onFormSubmit={(values, e) => console.log("onSubmit", values, e)}
|
|
245
|
+
>
|
|
246
|
+
<Field
|
|
247
|
+
name="startDate"
|
|
248
|
+
label="Start date"
|
|
249
|
+
error={{ message: "Start date is required.", match: "valueMissing" }}
|
|
250
|
+
>
|
|
251
|
+
<DatePicker mode="single" required />
|
|
252
|
+
</Field>
|
|
253
|
+
<Button type="submit">Submit</Button>
|
|
254
|
+
</Form>
|
|
255
|
+
),
|
|
256
|
+
};
|
|
@@ -170,9 +170,7 @@ describe("DatePicker component", () => {
|
|
|
170
170
|
|
|
171
171
|
describe("multiple mode", () => {
|
|
172
172
|
it("shows formatted date when single date selected", () => {
|
|
173
|
-
render(
|
|
174
|
-
<DatePicker mode="multiple" value={[new Date(2024, 0, 15)]} />,
|
|
175
|
-
);
|
|
173
|
+
render(<DatePicker mode="multiple" value={[new Date(2024, 0, 15)]} />);
|
|
176
174
|
expect(screen.getByRole("button")).toHaveTextContent("15/01/2024");
|
|
177
175
|
});
|
|
178
176
|
|
|
@@ -288,10 +286,7 @@ describe("DatePicker component", () => {
|
|
|
288
286
|
|
|
289
287
|
it("passes data-slot date-picker-footer to footer wrapper", async () => {
|
|
290
288
|
render(
|
|
291
|
-
<DatePicker
|
|
292
|
-
mode="single"
|
|
293
|
-
renderFooter={() => <div>Footer</div>}
|
|
294
|
-
/>,
|
|
289
|
+
<DatePicker mode="single" renderFooter={() => <div>Footer</div>} />,
|
|
295
290
|
);
|
|
296
291
|
await userEvent.click(screen.getByRole("button"));
|
|
297
292
|
await waitFor(() => {
|
|
@@ -481,12 +476,7 @@ describe("DateInput component", () => {
|
|
|
481
476
|
});
|
|
482
477
|
|
|
483
478
|
it("footer data-slot date-input-footer present when open", async () => {
|
|
484
|
-
render(
|
|
485
|
-
<DateInput
|
|
486
|
-
name="date"
|
|
487
|
-
renderFooter={() => <div>Footer</div>}
|
|
488
|
-
/>,
|
|
489
|
-
);
|
|
479
|
+
render(<DateInput name="date" renderFooter={() => <div>Footer</div>} />);
|
|
490
480
|
await userEvent.click(
|
|
491
481
|
screen.getByRole("button", { name: "Seleccionar fecha" }),
|
|
492
482
|
);
|
|
@@ -6,6 +6,7 @@ import { type ReactElement, ReactNode, useRef, useState } from "react";
|
|
|
6
6
|
import {
|
|
7
7
|
Calendar,
|
|
8
8
|
FieldControl,
|
|
9
|
+
FieldRoot,
|
|
9
10
|
inputBaseClasses,
|
|
10
11
|
PopoverPopup,
|
|
11
12
|
PopoverRoot,
|
|
@@ -21,10 +22,12 @@ import type {
|
|
|
21
22
|
SingleDatePickerProps,
|
|
22
23
|
} from "./date-picker.model";
|
|
23
24
|
import {
|
|
25
|
+
formatISODate,
|
|
24
26
|
formatMultipleDates,
|
|
25
27
|
formatRangeDate,
|
|
26
28
|
formatSingleDate,
|
|
27
29
|
} from "./date-picker.utils";
|
|
30
|
+
import { useHiddenFieldValue } from "./use-hidden-field-value";
|
|
28
31
|
|
|
29
32
|
export function DatePicker(props: SingleDatePickerProps): ReactElement;
|
|
30
33
|
export function DatePicker(props: RangeDatePickerProps): ReactElement;
|
|
@@ -67,6 +70,7 @@ const PickerShell = ({
|
|
|
67
70
|
required,
|
|
68
71
|
}: PickerShellProps) => {
|
|
69
72
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
73
|
+
const hiddenInputRef = useHiddenFieldValue(formValue);
|
|
70
74
|
|
|
71
75
|
return (
|
|
72
76
|
<PopoverRoot data-slot={slot} open={open} onOpenChange={onOpenChange}>
|
|
@@ -80,7 +84,11 @@ const PickerShell = ({
|
|
|
80
84
|
"inline-flex items-center gap-2 h-8 cursor-pointer text-left",
|
|
81
85
|
"focus-visible:border-ring",
|
|
82
86
|
"aria-invalid:border-error focus-visible:aria-invalid:ring-error/20",
|
|
83
|
-
|
|
87
|
+
// Attribute selector (specificity 0,2,0) beats `border-input`;
|
|
88
|
+
// `group-data-[invalid]` ties and loses. Matches the OTP convention.
|
|
89
|
+
// The `focus-visible` variant re-asserts the error border so it is
|
|
90
|
+
// not washed out by `focus-visible:border-ring` while focused.
|
|
91
|
+
"in-[[data-slot=field][data-invalid]]:border-error focus-visible:in-[[data-slot=field][data-invalid]]:border-error focus-visible:in-[[data-slot=field][data-invalid]]:ring-error/20",
|
|
84
92
|
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
85
93
|
!hasValue && "text-muted-foreground",
|
|
86
94
|
className,
|
|
@@ -88,19 +96,24 @@ const PickerShell = ({
|
|
|
88
96
|
/>
|
|
89
97
|
}
|
|
90
98
|
>
|
|
91
|
-
<
|
|
92
|
-
<
|
|
99
|
+
<span className="flex-1 truncate">{displayText}</span>
|
|
100
|
+
<CalendarIcon
|
|
101
|
+
data-slot="date-picker-icon"
|
|
102
|
+
className="shrink-0 text-muted-foreground"
|
|
103
|
+
/>
|
|
93
104
|
</PopoverTrigger>
|
|
94
|
-
{/* Hidden input registered with Field for validity tracking and FormData.
|
|
105
|
+
{/* Hidden input registered with Field for validity tracking and FormData.
|
|
106
|
+
Left uncontrolled and driven imperatively via `emitNativeInputChange`:
|
|
107
|
+
a controlled `value` keeps React's value tracker in sync, which would
|
|
108
|
+
suppress the synthetic `input` event Base UI needs to re-validate. */}
|
|
95
109
|
<FieldControl
|
|
96
110
|
render={
|
|
97
111
|
<input
|
|
112
|
+
ref={hiddenInputRef}
|
|
98
113
|
type="text"
|
|
99
114
|
tabIndex={-1}
|
|
100
115
|
aria-hidden="true"
|
|
101
116
|
className="sr-only"
|
|
102
|
-
value={formValue}
|
|
103
|
-
onChange={() => {}}
|
|
104
117
|
required={required}
|
|
105
118
|
onFocus={() => triggerRef.current?.focus()}
|
|
106
119
|
/>
|
|
@@ -111,15 +124,21 @@ const PickerShell = ({
|
|
|
111
124
|
align="start"
|
|
112
125
|
data-slot={`${slot}-content`}
|
|
113
126
|
>
|
|
114
|
-
{
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
127
|
+
{/* Isolate popover content in its own FieldRoot. The Calendar's
|
|
128
|
+
month/year `Select`s are Base UI form controls; without this they
|
|
129
|
+
register on the OUTER Field, evict the hidden control's registration
|
|
130
|
+
on close, and the field submits empty. */}
|
|
131
|
+
<FieldRoot className="contents">
|
|
132
|
+
{children}
|
|
133
|
+
{footer && (
|
|
134
|
+
<div
|
|
135
|
+
className={cn("p-2", classNames?.footer)}
|
|
136
|
+
data-slot={`${slot}-footer`}
|
|
137
|
+
>
|
|
138
|
+
{footer}
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
</FieldRoot>
|
|
123
142
|
</PopoverPopup>
|
|
124
143
|
</PopoverRoot>
|
|
125
144
|
);
|
|
@@ -170,7 +189,7 @@ const SinglePicker = ({
|
|
|
170
189
|
className={className}
|
|
171
190
|
classNames={classNames}
|
|
172
191
|
footer={renderFooter?.(footerProps)}
|
|
173
|
-
formValue={
|
|
192
|
+
formValue={formatISODate(value)}
|
|
174
193
|
required={required}
|
|
175
194
|
>
|
|
176
195
|
<Calendar
|
|
@@ -24,7 +24,9 @@ describe("formatSingleDate", () => {
|
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
it("formats a valid date as dd/MM/yyyy", () => {
|
|
27
|
-
expect(formatSingleDate(new Date(2025, 0, 15), "Select")).toBe(
|
|
27
|
+
expect(formatSingleDate(new Date(2025, 0, 15), "Select")).toBe(
|
|
28
|
+
"15/01/2025",
|
|
29
|
+
);
|
|
28
30
|
});
|
|
29
31
|
});
|
|
30
32
|
|
|
@@ -36,24 +38,29 @@ describe("formatRangeDate", () => {
|
|
|
36
38
|
});
|
|
37
39
|
|
|
38
40
|
it("returns placeholder when both start and end are null", () => {
|
|
39
|
-
expect(formatRangeDate({ start: null, end: null }, "Pick range")).toBe(
|
|
41
|
+
expect(formatRangeDate({ start: null, end: null }, "Pick range")).toBe(
|
|
42
|
+
"Pick range",
|
|
43
|
+
);
|
|
40
44
|
});
|
|
41
45
|
|
|
42
46
|
it("formats range with only start as '... → start'", () => {
|
|
43
|
-
expect(
|
|
44
|
-
|
|
45
|
-
);
|
|
47
|
+
expect(
|
|
48
|
+
formatRangeDate({ start: null, end: new Date(2025, 11, 31) }, "Pick"),
|
|
49
|
+
).toBe("... → 31/12/2025");
|
|
46
50
|
});
|
|
47
51
|
|
|
48
52
|
it("formats range with only end as 'start → ...'", () => {
|
|
49
|
-
expect(
|
|
50
|
-
|
|
51
|
-
);
|
|
53
|
+
expect(
|
|
54
|
+
formatRangeDate({ start: new Date(2025, 0, 1), end: null }, "Pick"),
|
|
55
|
+
).toBe("01/01/2025 → ...");
|
|
52
56
|
});
|
|
53
57
|
|
|
54
58
|
it("formats range with both dates", () => {
|
|
55
59
|
expect(
|
|
56
|
-
formatRangeDate(
|
|
60
|
+
formatRangeDate(
|
|
61
|
+
{ start: new Date(2025, 0, 1), end: new Date(2025, 11, 31) },
|
|
62
|
+
"Pick",
|
|
63
|
+
),
|
|
57
64
|
).toBe("01/01/2025 → 31/12/2025");
|
|
58
65
|
});
|
|
59
66
|
});
|
|
@@ -70,12 +77,17 @@ describe("formatMultipleDates", () => {
|
|
|
70
77
|
});
|
|
71
78
|
|
|
72
79
|
it("formats single date as dd/MM/yyyy", () => {
|
|
73
|
-
expect(formatMultipleDates([new Date(2025, 0, 15)], "Select")).toBe(
|
|
80
|
+
expect(formatMultipleDates([new Date(2025, 0, 15)], "Select")).toBe(
|
|
81
|
+
"15/01/2025",
|
|
82
|
+
);
|
|
74
83
|
});
|
|
75
84
|
|
|
76
85
|
it("shows count for multiple dates", () => {
|
|
77
86
|
expect(
|
|
78
|
-
formatMultipleDates(
|
|
87
|
+
formatMultipleDates(
|
|
88
|
+
[new Date(2025, 0, 1), new Date(2025, 0, 2)],
|
|
89
|
+
"Select",
|
|
90
|
+
),
|
|
79
91
|
).toBe("2 fechas seleccionadas");
|
|
80
92
|
});
|
|
81
93
|
});
|
|
@@ -226,17 +238,23 @@ describe("maskDateInput — full date (5-8 digits)", () => {
|
|
|
226
238
|
});
|
|
227
239
|
|
|
228
240
|
it("truncates year to 4 digits max", () => {
|
|
229
|
-
expect(maskDateInput("120620259", ctx("9", "12/06/2025"))).toBe(
|
|
241
|
+
expect(maskDateInput("120620259", ctx("9", "12/06/2025"))).toBe(
|
|
242
|
+
"12/06/2025",
|
|
243
|
+
);
|
|
230
244
|
});
|
|
231
245
|
});
|
|
232
246
|
|
|
233
247
|
describe("maskDateInput — segment-based (2+ slashes present)", () => {
|
|
234
248
|
it("uses segment parsing when raw already has two slashes", () => {
|
|
235
|
-
expect(maskDateInput("12/06/2025", ctx(null, "12/06/2025"))).toBe(
|
|
249
|
+
expect(maskDateInput("12/06/2025", ctx(null, "12/06/2025"))).toBe(
|
|
250
|
+
"12/06/2025",
|
|
251
|
+
);
|
|
236
252
|
});
|
|
237
253
|
|
|
238
254
|
it("handles delete within a segment (two slashes)", () => {
|
|
239
|
-
expect(maskDateInput("12/0/2025", ctx("Backspace", "12/06/2025"))).toBe(
|
|
255
|
+
expect(maskDateInput("12/0/2025", ctx("Backspace", "12/06/2025"))).toBe(
|
|
256
|
+
"12/0/2025",
|
|
257
|
+
);
|
|
240
258
|
});
|
|
241
259
|
|
|
242
260
|
it("returns empty string when all segment parts are empty", () => {
|
|
@@ -11,6 +11,16 @@ export const formatSingleDate = (
|
|
|
11
11
|
return format(date, DISPLAY_DATE_FORMAT);
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* ISO 8601 calendar date (`yyyy-MM-dd`) used as the form value — locale-agnostic,
|
|
16
|
+
* sortable, and parseable by `new Date()` / backends without a custom format.
|
|
17
|
+
* The display string keeps `DISPLAY_DATE_FORMAT`; this is the value layer.
|
|
18
|
+
*/
|
|
19
|
+
export const formatISODate = (date: Date | null | undefined): string => {
|
|
20
|
+
if (!date || !isDate(date)) return "";
|
|
21
|
+
return format(date, "yyyy-MM-dd");
|
|
22
|
+
};
|
|
23
|
+
|
|
14
24
|
export const formatRangeDate = (
|
|
15
25
|
range: DateRange | undefined,
|
|
16
26
|
placeholder: string,
|
|
@@ -38,6 +48,29 @@ export const formatDate = (date: Date | null): string => {
|
|
|
38
48
|
return format(date, DISPLAY_DATE_FORMAT);
|
|
39
49
|
};
|
|
40
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Emits a native `input` event on a hidden form control so Base UI's Field
|
|
53
|
+
* re-runs validation. Base UI clears the error and re-validates only inside the
|
|
54
|
+
* control's `onChange`; assigning `value` via a React prop never fires a native
|
|
55
|
+
* event, so date changes driven from outside (calendar selection, typing into
|
|
56
|
+
* the visible input) would leave the field stuck in its invalid state.
|
|
57
|
+
*
|
|
58
|
+
* Sets the value through the prototype setter so React's value tracker detects
|
|
59
|
+
* the diff, then dispatches a bubbling `input` event React maps to `onChange`.
|
|
60
|
+
*/
|
|
61
|
+
export const emitNativeInputChange = (
|
|
62
|
+
input: HTMLInputElement | null,
|
|
63
|
+
value: string,
|
|
64
|
+
): void => {
|
|
65
|
+
if (!input) return;
|
|
66
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
67
|
+
HTMLInputElement.prototype,
|
|
68
|
+
"value",
|
|
69
|
+
)?.set;
|
|
70
|
+
setter?.call(input, value);
|
|
71
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
72
|
+
};
|
|
73
|
+
|
|
41
74
|
export const parseInputToDate = (raw: string): Date | null => {
|
|
42
75
|
const trimmed = raw.trim();
|
|
43
76
|
if (!trimmed) return null;
|
|
@@ -41,7 +41,9 @@ describe("useDateInputPopover", () => {
|
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
it("disabled=true prevents keydown from opening popover", () => {
|
|
44
|
-
const { result } = renderHook(() =>
|
|
44
|
+
const { result } = renderHook(() =>
|
|
45
|
+
useDateInputPopover({ disabled: true }),
|
|
46
|
+
);
|
|
45
47
|
const event = { key: " ", preventDefault: vi.fn() } as any;
|
|
46
48
|
act(() => result.current.handleInputKeyDown(event));
|
|
47
49
|
expect(result.current.open).toBe(false);
|