@boxcustodia/library 2.0.0-alpha.11 → 2.0.0-alpha.12
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/index.css +1 -1
- package/package.json +4 -3
- package/src/__doc__/Changelog.mdx +6 -0
- package/src/__doc__/Components.mdx +73 -0
- package/src/__doc__/Examples.tsx +69 -0
- package/src/__doc__/Icons.mdx +41 -0
- package/src/__doc__/Intro.mdx +138 -0
- package/src/__doc__/MCP.mdx +71 -0
- package/src/__doc__/Migration.mdx +475 -0
- package/src/__doc__/Theme.mdx +132 -0
- package/src/__doc__/Types.mdx +252 -0
- package/src/components/alert/alert.stories.tsx +142 -0
- package/src/components/alert/alert.tsx +109 -0
- package/src/components/alert/index.ts +7 -0
- package/src/components/alert-dialog/alert-dialog.stories.tsx +173 -0
- package/src/components/alert-dialog/alert-dialog.test.tsx +49 -0
- package/src/components/alert-dialog/alert-dialog.tsx +265 -0
- package/src/components/alert-dialog/index.ts +1 -0
- package/src/components/auto-complete/auto-complete-primitives.tsx +155 -0
- package/src/components/auto-complete/auto-complete.stories.tsx +241 -0
- package/src/components/auto-complete/auto-complete.tsx +82 -0
- package/src/components/auto-complete/index.ts +2 -0
- package/src/components/avatar/avatar.stories.tsx +84 -0
- package/src/components/avatar/avatar.test.tsx +61 -0
- package/src/components/avatar/avatar.tsx +104 -0
- package/src/components/avatar/index.ts +1 -0
- package/src/components/background-image/background-image.stories.tsx +21 -0
- package/src/components/background-image/background-image.test.tsx +29 -0
- package/src/components/background-image/background-image.tsx +23 -0
- package/src/components/background-image/index.ts +1 -0
- package/src/components/button/button.stories.tsx +396 -0
- package/src/components/button/button.test.tsx +58 -0
- package/src/components/button/button.tsx +31 -0
- package/src/components/button/button.variants.ts +44 -0
- package/src/components/button/components/base-button.tsx +86 -0
- package/src/components/button/components/loader-overlay.tsx +21 -0
- package/src/components/button/components/loading-icon.tsx +47 -0
- package/src/components/button/index.ts +3 -0
- package/src/components/calendar/calendar.model.ts +86 -0
- package/src/components/calendar/calendar.stories.tsx +155 -0
- package/src/components/calendar/calendar.test.tsx +12 -0
- package/src/components/calendar/calendar.tsx +185 -0
- package/src/components/calendar/components/calendar-navigation.tsx +141 -0
- package/src/components/calendar/components/day.tsx +61 -0
- package/src/components/calendar/components/decade-view.tsx +45 -0
- package/src/components/calendar/components/index.ts +6 -0
- package/src/components/calendar/components/month-view.tsx +58 -0
- package/src/components/calendar/components/week-days.tsx +27 -0
- package/src/components/calendar/components/year-view.tsx +29 -0
- package/src/components/calendar/hooks/index.ts +4 -0
- package/src/components/calendar/hooks/use-calendar-navigation.ts +79 -0
- package/src/components/calendar/hooks/use-calendar.ts +90 -0
- package/src/components/calendar/hooks/use-multiple-calendar.ts +34 -0
- package/src/components/calendar/hooks/use-range-calendar.ts +91 -0
- package/src/components/calendar/hooks/use-single-calendar.ts +18 -0
- package/src/components/calendar/index.ts +1 -0
- package/src/components/calendar/utils/typeguards.ts +7 -0
- package/src/components/card/card.stories.tsx +116 -0
- package/src/components/card/card.tsx +74 -0
- package/src/components/card/index.ts +1 -0
- package/src/components/center/center.stories.tsx +81 -0
- package/src/components/center/center.tsx +24 -0
- package/src/components/center/index.ts +1 -0
- package/src/components/checkbox/checkbox.stories.tsx +307 -0
- package/src/components/checkbox/checkbox.tsx +273 -0
- package/src/components/checkbox/index.ts +1 -0
- package/src/components/checkbox-group/checkbox-group.stories.tsx +104 -0
- package/src/components/checkbox-group/checkbox-group.tsx +16 -0
- package/src/components/checkbox-group/index.ts +1 -0
- package/src/components/combobox/combobox.stories.tsx +339 -0
- package/src/components/combobox/combobox.tsx +892 -0
- package/src/components/combobox/index.ts +1 -0
- package/src/components/date-picker/date-input.stories.tsx +158 -0
- package/src/components/date-picker/date-input.tsx +163 -0
- package/src/components/date-picker/date-picker.model.ts +90 -0
- package/src/components/date-picker/date-picker.stories.tsx +200 -0
- package/src/components/date-picker/date-picker.test.tsx +23 -0
- package/src/components/date-picker/date-picker.tsx +298 -0
- package/src/components/date-picker/date-picker.utils.ts +260 -0
- package/src/components/date-picker/index.ts +3 -0
- package/src/components/date-picker/use-date-input-popover.ts +48 -0
- package/src/components/date-picker/use-date-input.ts +125 -0
- package/src/components/dialog/dialog.stories.tsx +171 -0
- package/src/components/dialog/dialog.test.tsx +68 -0
- package/src/components/dialog/dialog.tsx +277 -0
- package/src/components/dialog/index.ts +1 -0
- package/src/components/divider/divider.stories.tsx +70 -0
- package/src/components/divider/divider.test.tsx +22 -0
- package/src/components/divider/divider.tsx +23 -0
- package/src/components/divider/index.ts +1 -0
- package/src/components/dropzone/dropzone.stories.tsx +210 -0
- package/src/components/dropzone/dropzone.tsx +154 -0
- package/src/components/dropzone/file-types.ts +64 -0
- package/src/components/dropzone/index.ts +3 -0
- package/src/components/dropzone/upload-primitives.tsx +310 -0
- package/src/components/dropzone/use-dropzone.ts +122 -0
- package/src/components/empty-state/empty-state.stories.tsx +56 -0
- package/src/components/empty-state/empty-state.tsx +39 -0
- package/src/components/empty-state/index.ts +1 -0
- package/src/components/field/field.stories.tsx +223 -0
- package/src/components/field/field.tsx +229 -0
- package/src/components/field/index.ts +1 -0
- package/src/components/form/form.stories.tsx +594 -0
- package/src/components/form/form.tsx +30 -0
- package/src/components/form/index.ts +1 -0
- package/src/components/heading/heading.stories.tsx +74 -0
- package/src/components/heading/heading.tsx +28 -0
- package/src/components/heading/heading.variants.ts +27 -0
- package/src/components/heading/index.ts +1 -0
- package/src/components/index.ts +46 -0
- package/src/components/input/index.ts +1 -0
- package/src/components/input/input.stories.tsx +104 -0
- package/src/components/input/input.tsx +75 -0
- package/src/components/kbd/index.ts +1 -0
- package/src/components/kbd/kbd.stories.tsx +40 -0
- package/src/components/kbd/kbd.tsx +31 -0
- package/src/components/kbd/kbd.variants.ts +26 -0
- package/src/components/label/index.ts +1 -0
- package/src/components/label/label.stories.tsx +68 -0
- package/src/components/label/label.test.tsx +61 -0
- package/src/components/label/label.tsx +62 -0
- package/src/components/loader/index.ts +1 -0
- package/src/components/loader/loader.stories.tsx +60 -0
- package/src/components/loader/loader.test.tsx +26 -0
- package/src/components/loader/loader.tsx +60 -0
- package/src/components/menu/index.ts +2 -0
- package/src/components/menu/menu-primitives.tsx +248 -0
- package/src/components/menu/menu.stories.tsx +203 -0
- package/src/components/menu/menu.tsx +100 -0
- package/src/components/menu/util/render-menu-item.tsx +54 -0
- package/src/components/multi-select/hooks/use-multi-select.ts +66 -0
- package/src/components/multi-select/index.ts +1 -0
- package/src/components/multi-select/multi-select.stories.tsx +294 -0
- package/src/components/multi-select/multi-select.tsx +300 -0
- package/src/components/multi-select/multi-select.variants.ts +22 -0
- package/src/components/number-input/index.ts +1 -0
- package/src/components/number-input/number-input.stories.tsx +209 -0
- package/src/components/number-input/number-input.test.tsx +87 -0
- package/src/components/number-input/number-input.tsx +230 -0
- package/src/components/pagination/components/pagination-option.tsx +27 -0
- package/src/components/pagination/index.ts +1 -0
- package/src/components/pagination/pagination.stories.tsx +80 -0
- package/src/components/pagination/pagination.test.tsx +76 -0
- package/src/components/pagination/pagination.tsx +102 -0
- package/src/components/password/index.ts +1 -0
- package/src/components/password/password.stories.tsx +104 -0
- package/src/components/password/password.tsx +71 -0
- package/src/components/popover/index.ts +1 -0
- package/src/components/popover/popover.stories.tsx +213 -0
- package/src/components/popover/popover.tsx +203 -0
- package/src/components/progress/index.ts +1 -0
- package/src/components/progress/progress.stories.tsx +124 -0
- package/src/components/progress/progress.test.tsx +25 -0
- package/src/components/progress/progress.tsx +124 -0
- package/src/components/scroll-area/index.ts +1 -0
- package/src/components/scroll-area/scroll-area.stories.tsx +166 -0
- package/src/components/scroll-area/scroll-area.tsx +64 -0
- package/src/components/select/index.ts +1 -0
- package/src/components/select/select.stories.tsx +253 -0
- package/src/components/select/select.tsx +430 -0
- package/src/components/show/index.ts +1 -0
- package/src/components/show/show.stories.tsx +197 -0
- package/src/components/show/show.test.tsx +41 -0
- package/src/components/show/show.tsx +16 -0
- package/src/components/skeleton/index.ts +1 -0
- package/src/components/skeleton/skeleton.stories.tsx +36 -0
- package/src/components/skeleton/skeleton.test.tsx +14 -0
- package/src/components/skeleton/skeleton.tsx +15 -0
- package/src/components/stack/index.ts +1 -0
- package/src/components/stack/stack.stories.tsx +194 -0
- package/src/components/stack/stack.tsx +52 -0
- package/src/components/stepper/Stepper.tsx +190 -0
- package/src/components/stepper/context/stepper-context.tsx +11 -0
- package/src/components/stepper/index.ts +1 -0
- package/src/components/stepper/stepper.stories.tsx +130 -0
- package/src/components/stepper/stepper.test.tsx +91 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/switch/switch.stories.tsx +122 -0
- package/src/components/switch/switch.test.tsx +30 -0
- package/src/components/switch/switch.tsx +86 -0
- package/src/components/table/index.ts +3 -0
- package/src/components/table/table-primitives.tsx +122 -0
- package/src/components/table/table.model.ts +20 -0
- package/src/components/table/table.stories.tsx +169 -0
- package/src/components/table/table.test.tsx +91 -0
- package/src/components/table/table.tsx +109 -0
- package/src/components/table-pagination/index.ts +2 -0
- package/src/components/table-pagination/table-pagination.model.ts +2 -0
- package/src/components/table-pagination/table-pagination.stories.tsx +23 -0
- package/src/components/table-pagination/table-pagination.test.tsx +32 -0
- package/src/components/table-pagination/table-pagination.tsx +108 -0
- package/src/components/tabs/context/tabs-context.tsx +14 -0
- package/src/components/tabs/index.ts +1 -0
- package/src/components/tabs/tabs.stories.tsx +182 -0
- package/src/components/tabs/tabs.test.tsx +61 -0
- package/src/components/tabs/tabs.tsx +175 -0
- package/src/components/tag/index.ts +2 -0
- package/src/components/tag/tag.stories.tsx +170 -0
- package/src/components/tag/tag.test.tsx +18 -0
- package/src/components/tag/tag.tsx +99 -0
- package/src/components/tag/tag.variants.ts +31 -0
- package/src/components/textarea/index.ts +1 -0
- package/src/components/textarea/textarea.stories.tsx +73 -0
- package/src/components/textarea/textarea.tsx +105 -0
- package/src/components/timeline/index.ts +1 -0
- package/src/components/timeline/timeline-status.ts +5 -0
- package/src/components/timeline/timeline.stories.tsx +84 -0
- package/src/components/timeline/timeline.tsx +147 -0
- package/src/components/toast/index.ts +1 -0
- package/src/components/toast/toast.stories.tsx +392 -0
- package/src/components/toast/toast.test.tsx +50 -0
- package/src/components/toast/toast.tsx +411 -0
- package/src/components/tooltip/index.ts +1 -0
- package/src/components/tooltip/tooltip.stories.tsx +226 -0
- package/src/components/tooltip/tooltip.test.tsx +46 -0
- package/src/components/tooltip/tooltip.tsx +171 -0
- package/src/components/tree/hooks/use-controllable-tree-state.ts +80 -0
- package/src/components/tree/index.ts +2 -0
- package/src/components/tree/tree-primitives.tsx +126 -0
- package/src/components/tree/tree.stories.tsx +468 -0
- package/src/components/tree/tree.tsx +42 -0
- package/src/hooks/index.ts +26 -0
- package/src/hooks/useArray/__doc__/useArray.stories.tsx +100 -0
- package/src/hooks/useArray/__test__/useArray.test.tsx +88 -0
- package/src/hooks/useArray/index.ts +1 -0
- package/src/hooks/useArray/useArray.ts +76 -0
- package/src/hooks/useAsync/__doc__/useAsync.stories.tsx +149 -0
- package/src/hooks/useAsync/__test__/useAsync.test.tsx +68 -0
- package/src/hooks/useAsync/index.ts +1 -0
- package/src/hooks/useAsync/useAsync.ts +58 -0
- package/src/hooks/useClickOutside/__doc__/useClickOutside.stories.tsx +40 -0
- package/src/hooks/useClickOutside/__test__/useClickOutside.test.tsx +33 -0
- package/src/hooks/useClickOutside/index.ts +1 -0
- package/src/hooks/useClickOutside/useClickOutside.ts +26 -0
- package/src/hooks/useClipboard/__doc__/useClipboard.stories.tsx +45 -0
- package/src/hooks/useClipboard/__test__/useClipboard.test.tsx +19 -0
- package/src/hooks/useClipboard/index.ts +1 -0
- package/src/hooks/useClipboard/useClipboard.tsx +28 -0
- package/src/hooks/useDebounceCallback/__doc__/useDebouncedCallback.stories.tsx +84 -0
- package/src/hooks/useDebounceCallback/index.ts +1 -0
- package/src/hooks/useDebounceCallback/useDebouncedCallback.ts +23 -0
- package/src/hooks/useDebounceValue/__doc__/useDebouncedValue.stories.tsx +75 -0
- package/src/hooks/useDebounceValue/index.ts +1 -0
- package/src/hooks/useDebounceValue/useDebouncedValue.ts +17 -0
- package/src/hooks/useDisclosure/__doc__/useDisclosure.stories.tsx +39 -0
- package/src/hooks/useDisclosure/__test__/useDisclosure.test.ts +43 -0
- package/src/hooks/useDisclosure/index.ts +1 -0
- package/src/hooks/useDisclosure/useDisclosure.ts +37 -0
- package/src/hooks/useDocumentTitle/__doc__/useDocumentTitle.stories.tsx +26 -0
- package/src/hooks/useDocumentTitle/index.ts +1 -0
- package/src/hooks/useDocumentTitle/useDocumentTitle.tsx +11 -0
- package/src/hooks/useEventListener/__doc__/useEventListener.stories.tsx +28 -0
- package/src/hooks/useEventListener/__test__/useEventListener.test.tsx +26 -0
- package/src/hooks/useEventListener/index.ts +1 -0
- package/src/hooks/useEventListener/useEventListener.ts +25 -0
- package/src/hooks/useFocusTrap/__doc__/useFocusTrap.stories.tsx +37 -0
- package/src/hooks/useFocusTrap/index.ts +1 -0
- package/src/hooks/useFocusTrap/scopeTab.ts +38 -0
- package/src/hooks/useFocusTrap/tabbable.ts +70 -0
- package/src/hooks/useFocusTrap/useFocusTrap.ts +78 -0
- package/src/hooks/useHotkey/__docs__/useHotkey.stories.tsx +116 -0
- package/src/hooks/useHotkey/__test__/useHotkey.test.tsx +105 -0
- package/src/hooks/useHotkey/__utils__/create-hotkey-listener.ts +25 -0
- package/src/hooks/useHotkey/__utils__/index.ts +3 -0
- package/src/hooks/useHotkey/__utils__/is-input-field.ts +14 -0
- package/src/hooks/useHotkey/__utils__/match-key-modifiers.ts +25 -0
- package/src/hooks/useHotkey/index.ts +1 -0
- package/src/hooks/useHotkey/useHotkey.ts +34 -0
- package/src/hooks/useHover/__doc__/useHover.stories.tsx +41 -0
- package/src/hooks/useHover/__test__/useHover.test.tsx +45 -0
- package/src/hooks/useHover/index.ts +1 -0
- package/src/hooks/useHover/useHover.tsx +40 -0
- package/src/hooks/useIsVisible/__doc__/useIsVisible.stories.tsx +60 -0
- package/src/hooks/useIsVisible/index.ts +1 -0
- package/src/hooks/useIsVisible/useIsVisible.tsx +50 -0
- package/src/hooks/useLocalStorage/__doc__/useLocalStorage.stories.tsx +86 -0
- package/src/hooks/useLocalStorage/__test__/useLocalStorage.test.ts +85 -0
- package/src/hooks/useLocalStorage/index.ts +1 -0
- package/src/hooks/useLocalStorage/useLocalStorage.ts +57 -0
- package/src/hooks/useMediaQuery/__doc__/useMediaQuery.stories.tsx +39 -0
- package/src/hooks/useMediaQuery/index.ts +1 -0
- package/src/hooks/useMediaQuery/useMediaQuery.ts +22 -0
- package/src/hooks/useMemoizedFn/index.ts +1 -0
- package/src/hooks/useMemoizedFn/useMemoizedFn.ts +32 -0
- package/src/hooks/useMutation/__doc__/useMutation.stories.tsx +111 -0
- package/src/hooks/useMutation/__test__/useMutation.test.tsx +83 -0
- package/src/hooks/useMutation/index.ts +1 -0
- package/src/hooks/useMutation/useMutation.tsx +60 -0
- package/src/hooks/useObject/__doc__/useObject.stories.tsx +119 -0
- package/src/hooks/useObject/__test__/useObject.test.tsx +87 -0
- package/src/hooks/useObject/index.ts +1 -0
- package/src/hooks/useObject/useObject.tsx +48 -0
- package/src/hooks/usePagination/__doc__/usePagination.stories.tsx +72 -0
- package/src/hooks/usePagination/__test__/usePagination.test.tsx +98 -0
- package/src/hooks/usePagination/index.ts +2 -0
- package/src/hooks/usePagination/usePagination.tsx +74 -0
- package/src/hooks/usePortal/__doc__/usePortal.stories.tsx +19 -0
- package/src/hooks/usePortal/__test__/usePortal.test.tsx +20 -0
- package/src/hooks/usePortal/index.ts +1 -0
- package/src/hooks/usePortal/usePortal.ts +40 -0
- package/src/hooks/usePreventCloseWindow/__doc__/usePreventCloseWindow.stories.tsx +32 -0
- package/src/hooks/usePreventCloseWindow/index.ts +1 -0
- package/src/hooks/usePreventCloseWindow/usePreventCloseWindow.ts +33 -0
- package/src/hooks/useRangePagination/__test__/useRangePagination.test.tsx +63 -0
- package/src/hooks/useRangePagination/index.ts +2 -0
- package/src/hooks/useRangePagination/useRangePagination.tsx +72 -0
- package/src/hooks/useSelection/__doc__/useSelection.stories.tsx +140 -0
- package/src/hooks/useSelection/__test__/useSelection.test.tsx +57 -0
- package/src/hooks/useSelection/index.ts +1 -0
- package/src/hooks/useSelection/useSelection.ts +121 -0
- package/src/hooks/useStep/__doc__/useStep.stories.tsx +98 -0
- package/src/hooks/useStep/__test__/useStep.test.ts +51 -0
- package/src/hooks/useStep/index.ts +1 -0
- package/src/hooks/useStep/useStep.ts +57 -0
- package/src/hooks/useToggle/__doc__/useToggle.stories.tsx +25 -0
- package/src/hooks/useToggle/__test__/useToggle.test.tsx +43 -0
- package/src/hooks/useToggle/index.ts +1 -0
- package/src/hooks/useToggle/useToggle.ts +16 -0
- package/src/index.ts +6 -0
- package/src/lib/cn.ts +8 -0
- package/src/lib/index.ts +1 -0
- package/src/models/Generic.model.ts +67 -0
- package/src/models/index.ts +1 -0
- package/src/providers/index.ts +2 -0
- package/src/providers/library-provider.tsx +44 -0
- package/src/providers/theme/ThemeProvider.tsx +25 -0
- package/src/providers/theme/index.ts +3 -0
- package/src/providers/theme/types.ts +11 -0
- package/src/providers/theme/useThemeProps.ts +25 -0
- package/src/stores/theme.store.ts +31 -0
- package/src/styles/components.css +4 -0
- package/src/styles/index.css +2 -0
- package/src/styles/library.css +2 -0
- package/src/styles/theme.css +232 -0
- package/src/utils/dates/parseDateRange.utility.ts +39 -0
- package/src/utils/form.tsx +91 -0
- package/src/utils/functions/createSafeContext.ts +17 -0
- package/src/utils/functions/ensureReactElement.tsx +30 -0
- package/src/utils/functions/getFormData.ts +19 -0
- package/src/utils/functions/index.ts +4 -0
- package/src/utils/functions/mergeRefs.ts +18 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/strings/extractInitials.utility.ts +10 -0
- package/src/utils/strings/index.ts +1 -0
- package/src/utils/tests/click.ts +3 -0
- package/src/utils/tests/index.ts +2 -0
- package/src/utils/tests/keyboard.ts +21 -0
- package/src/utils/tests/type.ts +6 -0
- package/dist/components.css +0 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./combobox";
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { lastDayOfMonth, nextSaturday } from "date-fns";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { action } from "storybook/actions";
|
|
5
|
+
import { Button } from "../../components";
|
|
6
|
+
import { DateInput } from "./date-input";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Typeable date field with `DD/MM/YYYY` auto-masking and a calendar popover.
|
|
10
|
+
* Built as a composite of an `<input>` and a `Popover`.
|
|
11
|
+
*
|
|
12
|
+
* Key behaviors:
|
|
13
|
+
* - The calendar icon button and **Spacebar** (while focused on the input) open the popover.
|
|
14
|
+
* **Escape** closes it and reverts the text to the last committed value.
|
|
15
|
+
* - **Enter** or **blur** commits the typed text — invalid or empty input clears the field.
|
|
16
|
+
* - Short-year entry: `25` is normalised to `2025` on commit.
|
|
17
|
+
* - `disabledDate` enforces restrictions in both the calendar and the keyboard path:
|
|
18
|
+
* if the user types a disabled date it is rejected and the last valid value is restored.
|
|
19
|
+
* - `aria-invalid` activates the error ring on the outer wrapper (not the inner `<input>`),
|
|
20
|
+
* consistent with `Input`'s invalid state. Typically set by a wrapping `Field`.
|
|
21
|
+
* - Extends all native `<input>` HTML attributes: `name`, `required`, `autoComplete`, etc.
|
|
22
|
+
*/
|
|
23
|
+
const meta: Meta<typeof DateInput> = {
|
|
24
|
+
title: "Components/DateInput",
|
|
25
|
+
component: DateInput,
|
|
26
|
+
parameters: { layout: "centered" },
|
|
27
|
+
args: {
|
|
28
|
+
onChange: action("onChange"),
|
|
29
|
+
},
|
|
30
|
+
argTypes: {
|
|
31
|
+
onChange: { control: false },
|
|
32
|
+
onBlur: { control: false },
|
|
33
|
+
disabledDate: { control: false },
|
|
34
|
+
renderFooter: { control: false },
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default meta;
|
|
39
|
+
type Story = StoryObj<typeof DateInput>;
|
|
40
|
+
|
|
41
|
+
export const Default: Story = {};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Pass `defaultValue` for one-shot initialisation without managing state.
|
|
45
|
+
* Use `value` + `onChange` for fully controlled usage.
|
|
46
|
+
*/
|
|
47
|
+
export const WithDefaultValue: Story = {
|
|
48
|
+
args: {
|
|
49
|
+
defaultValue: new Date(2025, 0, 15),
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const Disabled: Story = {
|
|
54
|
+
args: { disabled: true },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* `aria-invalid` applies an error ring to the entire field wrapper.
|
|
59
|
+
* Typically injected by a parent `Field` rather than set directly.
|
|
60
|
+
*/
|
|
61
|
+
export const Invalid: Story = {
|
|
62
|
+
args: { "aria-invalid": true },
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* `disabledDate` blocks dates in both the calendar and the text input.
|
|
67
|
+
* If the user types a disabled date, the previous valid value is restored on blur or Enter.
|
|
68
|
+
*/
|
|
69
|
+
export const DisabledDates: Story = {
|
|
70
|
+
args: {
|
|
71
|
+
disabledDate: (date: Date) => date.getDay() === 0 || date.getDay() === 6,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* `renderFooter` receives `{ value, clear, selectToday, setValue }`.
|
|
77
|
+
* The popover stays open while the user interacts with footer buttons.
|
|
78
|
+
* Selecting a date via `setValue` also updates the text input.
|
|
79
|
+
*/
|
|
80
|
+
export const WithFooter: Story = {
|
|
81
|
+
render: () => (
|
|
82
|
+
<DateInput
|
|
83
|
+
onChange={action("onChange")}
|
|
84
|
+
renderFooter={({ value, clear, selectToday, setValue }) => (
|
|
85
|
+
<div className="flex flex-col gap-2">
|
|
86
|
+
<Button variant="outline" size="sm" onClick={selectToday}>
|
|
87
|
+
Today
|
|
88
|
+
</Button>
|
|
89
|
+
<Button
|
|
90
|
+
variant="outline"
|
|
91
|
+
size="sm"
|
|
92
|
+
onClick={() => setValue(nextSaturday(new Date()))}
|
|
93
|
+
>
|
|
94
|
+
Next Saturday
|
|
95
|
+
</Button>
|
|
96
|
+
<Button
|
|
97
|
+
variant="outline"
|
|
98
|
+
size="sm"
|
|
99
|
+
onClick={() => setValue(lastDayOfMonth(new Date()))}
|
|
100
|
+
>
|
|
101
|
+
Last day of month
|
|
102
|
+
</Button>
|
|
103
|
+
<Button variant="error" size="sm" onClick={clear}>
|
|
104
|
+
Clear
|
|
105
|
+
</Button>
|
|
106
|
+
{value && (
|
|
107
|
+
<span className="text-xs text-muted-foreground">
|
|
108
|
+
{value.toLocaleDateString()}
|
|
109
|
+
</span>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
/>
|
|
114
|
+
),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* `value` + `onChange` make the field fully controlled. Updating `value`
|
|
119
|
+
* externally always syncs the text input and the calendar selection.
|
|
120
|
+
*/
|
|
121
|
+
export const Controlled: Story = {
|
|
122
|
+
render: () => {
|
|
123
|
+
const [date, setDate] = useState<Date | null>(null);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div className="flex flex-col gap-4">
|
|
127
|
+
<DateInput value={date} onChange={setDate} />
|
|
128
|
+
<p className="text-sm text-muted-foreground">
|
|
129
|
+
{date ? date.toLocaleDateString() : "No selection"}
|
|
130
|
+
</p>
|
|
131
|
+
<Button variant="outline" size="sm" onClick={() => setDate(null)}>
|
|
132
|
+
Clear externally
|
|
133
|
+
</Button>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* `name` and `required` enable native form participation.
|
|
141
|
+
* The value included in `FormData` is the date string in `DD/MM/YYYY` format.
|
|
142
|
+
* An empty field submits an empty string for the key.
|
|
143
|
+
*/
|
|
144
|
+
export const NativeForm: Story = {
|
|
145
|
+
render: () => (
|
|
146
|
+
<form
|
|
147
|
+
className="flex flex-col gap-2"
|
|
148
|
+
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
|
|
149
|
+
e.preventDefault();
|
|
150
|
+
const data = new FormData(e.currentTarget);
|
|
151
|
+
alert(`date: ${data.get("date")}`);
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
<DateInput name="date" required />
|
|
155
|
+
<Button type="submit">Submit</Button>
|
|
156
|
+
</form>
|
|
157
|
+
),
|
|
158
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
|
2
|
+
import { isDate } from "date-fns";
|
|
3
|
+
import { CalendarIcon } from "lucide-react";
|
|
4
|
+
import { KeyboardEvent } from "react";
|
|
5
|
+
import {
|
|
6
|
+
Calendar,
|
|
7
|
+
FieldControl,
|
|
8
|
+
inputBaseClasses,
|
|
9
|
+
PopoverPopup,
|
|
10
|
+
PopoverRoot,
|
|
11
|
+
PopoverTrigger,
|
|
12
|
+
} from "../../components";
|
|
13
|
+
import { cn } from "../../lib";
|
|
14
|
+
import type { DateInputProps } from "./date-picker.model";
|
|
15
|
+
import { useDateInput } from "./use-date-input";
|
|
16
|
+
import { useDateInputPopover } from "./use-date-input-popover";
|
|
17
|
+
|
|
18
|
+
export const DateInput = (props: DateInputProps) => {
|
|
19
|
+
const {
|
|
20
|
+
placeholder,
|
|
21
|
+
value: valueProp,
|
|
22
|
+
onChange,
|
|
23
|
+
defaultValue: defaultValueProp,
|
|
24
|
+
autoComplete,
|
|
25
|
+
className,
|
|
26
|
+
disabledDate,
|
|
27
|
+
renderFooter,
|
|
28
|
+
disabled,
|
|
29
|
+
onBlur: onBlurProp,
|
|
30
|
+
...rest
|
|
31
|
+
} = props;
|
|
32
|
+
|
|
33
|
+
const [selectedDate, setSelectedDate] = useControllableState<Date | null>({
|
|
34
|
+
prop: valueProp,
|
|
35
|
+
onChange: (nextValue) => onChange?.(nextValue ?? null),
|
|
36
|
+
defaultProp: defaultValueProp ?? null,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const {
|
|
40
|
+
inputValue,
|
|
41
|
+
handleChange,
|
|
42
|
+
handleBlur,
|
|
43
|
+
handleKeyDown: handleInputKeyDown,
|
|
44
|
+
clear: clearInput,
|
|
45
|
+
setFromExternalDate,
|
|
46
|
+
} = useDateInput({
|
|
47
|
+
date: selectedDate ?? null,
|
|
48
|
+
onDateChange: (next) => setSelectedDate(next),
|
|
49
|
+
disabledDate,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const {
|
|
53
|
+
open,
|
|
54
|
+
setOpen,
|
|
55
|
+
handleInputKeyDown: handlePopoverKeyDown,
|
|
56
|
+
inputRef,
|
|
57
|
+
} = useDateInputPopover({ disabled });
|
|
58
|
+
|
|
59
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
60
|
+
handleInputKeyDown(event);
|
|
61
|
+
handlePopoverKeyDown(event);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const onClear = () => {
|
|
65
|
+
setFromExternalDate(null);
|
|
66
|
+
clearInput();
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const onSelectToday = () => {
|
|
70
|
+
const today = new Date();
|
|
71
|
+
if (disabledDate?.(today)) return;
|
|
72
|
+
setFromExternalDate(today);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const footerContent = renderFooter?.({
|
|
76
|
+
value: selectedDate ?? null,
|
|
77
|
+
clear: onClear,
|
|
78
|
+
selectToday: onSelectToday,
|
|
79
|
+
setValue: (nextValue) => {
|
|
80
|
+
const next = nextValue && isDate(nextValue) ? nextValue : null;
|
|
81
|
+
setSelectedDate(next);
|
|
82
|
+
setFromExternalDate(next);
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<PopoverRoot data-slot="date-input" open={open} onOpenChange={setOpen}>
|
|
88
|
+
<div
|
|
89
|
+
className={cn(
|
|
90
|
+
inputBaseClasses,
|
|
91
|
+
"flex items-center gap-2 h-10",
|
|
92
|
+
"has-[:focus-visible]:border-ring",
|
|
93
|
+
"aria-invalid:border-error has-[:focus-visible]:aria-invalid:ring-error/20",
|
|
94
|
+
"has-aria-invalid:border-error has-[:focus-visible]:has-aria-invalid:ring-error/20",
|
|
95
|
+
"has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50",
|
|
96
|
+
!selectedDate && "text-muted-foreground",
|
|
97
|
+
className,
|
|
98
|
+
"field-error",
|
|
99
|
+
)}
|
|
100
|
+
onClick={(e) => {
|
|
101
|
+
if (e.target === e.currentTarget) inputRef.current?.focus();
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
<PopoverTrigger
|
|
105
|
+
type="button"
|
|
106
|
+
disabled={disabled}
|
|
107
|
+
data-slot="date-input-trigger"
|
|
108
|
+
className="flex shrink-0 items-center justify-center text-muted-foreground hover:text-foreground"
|
|
109
|
+
>
|
|
110
|
+
<CalendarIcon data-slot="date-input-icon" />
|
|
111
|
+
<span className="sr-only">Seleccionar fecha</span>
|
|
112
|
+
</PopoverTrigger>
|
|
113
|
+
<FieldControl
|
|
114
|
+
render={
|
|
115
|
+
<input
|
|
116
|
+
type="text"
|
|
117
|
+
value={inputValue}
|
|
118
|
+
onChange={handleChange}
|
|
119
|
+
onBlur={(e) => {
|
|
120
|
+
handleBlur();
|
|
121
|
+
onBlurProp?.(e);
|
|
122
|
+
}}
|
|
123
|
+
onKeyDown={handleKeyDown}
|
|
124
|
+
placeholder={placeholder ?? "dd/mm/aaaa"}
|
|
125
|
+
autoComplete={autoComplete ?? "off"}
|
|
126
|
+
disabled={disabled}
|
|
127
|
+
className="flex-1 min-w-0 bg-transparent outline-none text-start truncate pt-px"
|
|
128
|
+
aria-label={placeholder}
|
|
129
|
+
ref={inputRef}
|
|
130
|
+
{...rest}
|
|
131
|
+
/>
|
|
132
|
+
}
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
<PopoverPopup
|
|
136
|
+
className="w-auto p-0"
|
|
137
|
+
align="start"
|
|
138
|
+
data-slot="date-input-content"
|
|
139
|
+
>
|
|
140
|
+
<Calendar
|
|
141
|
+
className="border-none"
|
|
142
|
+
mode="single"
|
|
143
|
+
selected={selectedDate ?? undefined}
|
|
144
|
+
onSelect={(next) => {
|
|
145
|
+
if (!next || !isDate(next)) {
|
|
146
|
+
setFromExternalDate(null);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (disabledDate?.(next)) return;
|
|
150
|
+
setFromExternalDate(next);
|
|
151
|
+
setOpen(false);
|
|
152
|
+
}}
|
|
153
|
+
disabled={disabledDate}
|
|
154
|
+
/>
|
|
155
|
+
{footerContent && (
|
|
156
|
+
<div className="p-2" data-slot="date-input-footer">
|
|
157
|
+
{footerContent}
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
</PopoverPopup>
|
|
161
|
+
</PopoverRoot>
|
|
162
|
+
);
|
|
163
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export type DateRange = {
|
|
4
|
+
start: Date | null;
|
|
5
|
+
end: Date | null;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type DatePickerMode = "single" | "range" | "multiple";
|
|
9
|
+
|
|
10
|
+
type SingleFooterProps = {
|
|
11
|
+
mode: "single";
|
|
12
|
+
value: Date | null;
|
|
13
|
+
clear: () => void;
|
|
14
|
+
selectToday: () => void;
|
|
15
|
+
setValue: (value: Date | null) => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type RangeFooterProps = {
|
|
19
|
+
mode: "range";
|
|
20
|
+
value: DateRange;
|
|
21
|
+
clear: () => void;
|
|
22
|
+
setValue: (value: DateRange) => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type MultipleFooterProps = {
|
|
26
|
+
mode: "multiple";
|
|
27
|
+
value: Date[];
|
|
28
|
+
clear: () => void;
|
|
29
|
+
setValue: (value: Date[]) => void;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type DatePickerFooterProps =
|
|
33
|
+
| SingleFooterProps
|
|
34
|
+
| RangeFooterProps
|
|
35
|
+
| MultipleFooterProps;
|
|
36
|
+
|
|
37
|
+
type DatePickerBaseProps = {
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
disabledDate?: (date: Date) => boolean;
|
|
40
|
+
renderFooter?: (props: DatePickerFooterProps) => ReactNode;
|
|
41
|
+
className?: string;
|
|
42
|
+
required?: boolean;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type SingleDatePickerProps = DatePickerBaseProps & {
|
|
46
|
+
mode: "single";
|
|
47
|
+
value?: Date | null;
|
|
48
|
+
defaultValue?: Date | null;
|
|
49
|
+
onChange?: (value: Date | null) => void;
|
|
50
|
+
placeholder?: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type RangeDatePickerProps = DatePickerBaseProps & {
|
|
54
|
+
mode: "range";
|
|
55
|
+
value?: DateRange;
|
|
56
|
+
defaultValue?: DateRange;
|
|
57
|
+
onChange?: (value: DateRange) => void;
|
|
58
|
+
placeholder?: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type MultipleDatePickerProps = DatePickerBaseProps & {
|
|
62
|
+
mode: "multiple";
|
|
63
|
+
value?: Date[];
|
|
64
|
+
defaultValue?: Date[];
|
|
65
|
+
onChange?: (value: Date[]) => void;
|
|
66
|
+
placeholder?: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type DatePickerProps =
|
|
70
|
+
| SingleDatePickerProps
|
|
71
|
+
| RangeDatePickerProps
|
|
72
|
+
| MultipleDatePickerProps;
|
|
73
|
+
|
|
74
|
+
export type DateInputFooterProps = {
|
|
75
|
+
value: Date | null;
|
|
76
|
+
clear: () => void;
|
|
77
|
+
selectToday: () => void;
|
|
78
|
+
setValue: (value: Date | null) => void;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type DateInputProps = Omit<
|
|
82
|
+
ComponentProps<"input">,
|
|
83
|
+
"value" | "onChange" | "defaultValue"
|
|
84
|
+
> & {
|
|
85
|
+
value?: Date | null;
|
|
86
|
+
defaultValue?: Date | null;
|
|
87
|
+
onChange?: (value: Date | null) => void;
|
|
88
|
+
disabledDate?: (date: Date) => boolean;
|
|
89
|
+
renderFooter?: (props: DateInputFooterProps) => ReactNode;
|
|
90
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { lastDayOfMonth, nextSaturday } from "date-fns";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { action } from "storybook/actions";
|
|
5
|
+
import { Button } from "../../components";
|
|
6
|
+
import { DatePicker } from "./date-picker";
|
|
7
|
+
import type { DatePickerFooterProps, DateRange } from "./date-picker.model";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Trigger button that opens a calendar in a `Popover`. Supports three modes
|
|
11
|
+
* via a discriminated union on `mode`:
|
|
12
|
+
*
|
|
13
|
+
* - `"single"` — selects one `Date | null`
|
|
14
|
+
* - `"range"` — selects `DateRange = { start: Date | null; end: Date | null }`
|
|
15
|
+
* - `"multiple"` — selects `Date[]`
|
|
16
|
+
*
|
|
17
|
+
* The types of `value`, `defaultValue`, and `onChange` are **inferred from `mode`** —
|
|
18
|
+
* TypeScript will error if `mode="range"` is paired with a `Date | null` value.
|
|
19
|
+
*
|
|
20
|
+
* `renderFooter` receives a mode-discriminated prop bag. Always narrow `mode` before
|
|
21
|
+
* accessing `selectToday` or single-mode `setValue`, which only exist in `"single"`.
|
|
22
|
+
*/
|
|
23
|
+
const meta: Meta<typeof DatePicker> = {
|
|
24
|
+
title: "Components/DatePicker",
|
|
25
|
+
component: DatePicker,
|
|
26
|
+
parameters: { layout: "centered" },
|
|
27
|
+
args: {
|
|
28
|
+
mode: "single",
|
|
29
|
+
onChange: action("onChange"),
|
|
30
|
+
},
|
|
31
|
+
argTypes: {
|
|
32
|
+
onChange: { control: false },
|
|
33
|
+
disabledDate: { control: false },
|
|
34
|
+
renderFooter: { control: false },
|
|
35
|
+
value: { control: false },
|
|
36
|
+
defaultValue: { control: false },
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default meta;
|
|
41
|
+
type Story = StoryObj<typeof DatePicker>;
|
|
42
|
+
|
|
43
|
+
export const Default: Story = {};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Pass `defaultValue` for one-shot initialisation without managing state.
|
|
47
|
+
* Use `value` + `onChange` for fully controlled usage.
|
|
48
|
+
*/
|
|
49
|
+
export const WithDefaultValue: Story = {
|
|
50
|
+
args: {
|
|
51
|
+
defaultValue: new Date(2025, 0, 15),
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const Disabled: Story = {
|
|
56
|
+
args: { disabled: true },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Dates matching the predicate are visually dimmed and unselectable.
|
|
61
|
+
* Clicking a disabled day has no effect on the current value.
|
|
62
|
+
*/
|
|
63
|
+
export const DisabledDates: Story = {
|
|
64
|
+
args: {
|
|
65
|
+
disabledDate: (date: Date) => date.getDay() === 0 || date.getDay() === 6,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Selects a contiguous date range. The first click sets `start`, the second
|
|
71
|
+
* sets `end`. A third click resets `start` and begins a new range.
|
|
72
|
+
* `value` and `onChange` use `DateRange = { start: Date | null; end: Date | null }`.
|
|
73
|
+
*/
|
|
74
|
+
export const Range: Story = {
|
|
75
|
+
args: { mode: "range", onChange: action("onChange") },
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Selects multiple non-contiguous dates. Each click toggles the day on or off.
|
|
80
|
+
* `value` and `onChange` use `Date[]`.
|
|
81
|
+
*/
|
|
82
|
+
export const Multiple: Story = {
|
|
83
|
+
args: { mode: "multiple", onChange: action("onChange") },
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* `value` + `onChange` make the picker fully controlled. Setting `value`
|
|
88
|
+
* externally always updates the trigger label.
|
|
89
|
+
*/
|
|
90
|
+
export const Controlled: Story = {
|
|
91
|
+
render: () => {
|
|
92
|
+
const [date, setDate] = useState<Date | null>(null);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className="flex flex-col gap-4">
|
|
96
|
+
<DatePicker mode="single" value={date} onChange={setDate} />
|
|
97
|
+
<p className="text-sm text-muted-foreground">
|
|
98
|
+
{date ? date.toLocaleDateString() : "No selection"}
|
|
99
|
+
</p>
|
|
100
|
+
<Button variant="outline" size="sm" onClick={() => setDate(null)}>
|
|
101
|
+
Clear externally
|
|
102
|
+
</Button>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Range controlled state requires `DateRange`.
|
|
110
|
+
* The first click sets `start`, the second sets `end`.
|
|
111
|
+
* A third click starts a new range from the clicked date.
|
|
112
|
+
*/
|
|
113
|
+
export const ControlledRange: Story = {
|
|
114
|
+
render: () => {
|
|
115
|
+
const [range, setRange] = useState<DateRange>({ start: null, end: null });
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div className="flex flex-col gap-4">
|
|
119
|
+
<DatePicker mode="range" value={range} onChange={setRange} />
|
|
120
|
+
<p className="text-sm text-muted-foreground">
|
|
121
|
+
{range.start?.toLocaleDateString() ?? "—"} →{" "}
|
|
122
|
+
{range.end?.toLocaleDateString() ?? "—"}
|
|
123
|
+
</p>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Multiple controlled state. The array grows or shrinks on every click.
|
|
131
|
+
*/
|
|
132
|
+
export const ControlledMultiple: Story = {
|
|
133
|
+
render: () => {
|
|
134
|
+
const [dates, setDates] = useState<Date[]>([]);
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div className="flex flex-col gap-4">
|
|
138
|
+
<DatePicker mode="multiple" value={dates} onChange={setDates} />
|
|
139
|
+
<p className="text-sm text-muted-foreground">
|
|
140
|
+
{dates.length > 0
|
|
141
|
+
? dates.map((d) => d.toLocaleDateString()).join(", ")
|
|
142
|
+
: "No selection"}
|
|
143
|
+
</p>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* `renderFooter` receives a mode-discriminated object — always narrow `mode` first.
|
|
151
|
+
* `selectToday` and `setValue(Date | null)` are only available in `"single"` mode.
|
|
152
|
+
* Range and multiple expose only `clear` and `setValue` typed to their own value shape.
|
|
153
|
+
*
|
|
154
|
+
* ```tsx
|
|
155
|
+
* renderFooter={(props) => {
|
|
156
|
+
* if (props.mode !== "single") return null;
|
|
157
|
+
* return <Button onClick={props.selectToday}>Today</Button>;
|
|
158
|
+
* }}
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export const WithFooter: Story = {
|
|
162
|
+
render: () => {
|
|
163
|
+
const Footer = (props: DatePickerFooterProps) => {
|
|
164
|
+
if (props.mode !== "single") return null;
|
|
165
|
+
const { value, clear, selectToday, setValue } = props;
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div className="flex flex-col gap-2">
|
|
169
|
+
<Button variant="outline" size="sm" onClick={selectToday}>
|
|
170
|
+
Today
|
|
171
|
+
</Button>
|
|
172
|
+
<Button
|
|
173
|
+
variant="outline"
|
|
174
|
+
size="sm"
|
|
175
|
+
onClick={() => setValue(nextSaturday(new Date()))}
|
|
176
|
+
>
|
|
177
|
+
Next Saturday
|
|
178
|
+
</Button>
|
|
179
|
+
<Button
|
|
180
|
+
variant="outline"
|
|
181
|
+
size="sm"
|
|
182
|
+
onClick={() => setValue(lastDayOfMonth(new Date()))}
|
|
183
|
+
>
|
|
184
|
+
Last day of month
|
|
185
|
+
</Button>
|
|
186
|
+
<Button variant="error" size="sm" onClick={clear}>
|
|
187
|
+
Clear
|
|
188
|
+
</Button>
|
|
189
|
+
{value && (
|
|
190
|
+
<span className="text-xs text-muted-foreground">
|
|
191
|
+
{value.toLocaleDateString()}
|
|
192
|
+
</span>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
return <DatePicker mode="single" renderFooter={Footer} />;
|
|
199
|
+
},
|
|
200
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { render } from "@testing-library/react";
|
|
2
|
+
import { describe, it } from "vitest";
|
|
3
|
+
import { DateInput, DatePicker } from "../../components";
|
|
4
|
+
|
|
5
|
+
describe("DatePicker component", () => {
|
|
6
|
+
it("should render single mode", () => {
|
|
7
|
+
render(<DatePicker mode="single" />);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("should render range mode", () => {
|
|
11
|
+
render(<DatePicker mode="range" />);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should render multiple mode", () => {
|
|
15
|
+
render(<DatePicker mode="multiple" />);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("DateInput component", () => {
|
|
20
|
+
it("should render correctly", () => {
|
|
21
|
+
render(<DateInput name="date" />);
|
|
22
|
+
});
|
|
23
|
+
});
|