diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index bc95d87..e15e5ba 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -28,13 +28,13 @@ jobs:
- name: 💾 Cache node_modules
id: cache-node-modules
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: 🏗️ Install
- uses: borales/actions-yarn@v4
+ uses: borales/actions-yarn@v5
with:
cmd: install
@@ -48,7 +48,7 @@ jobs:
run: yarn build
- name: 🚢 Release
- uses: borales/actions-yarn@v4
+ uses: borales/actions-yarn@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/package.json b/package.json
index 6c94fcd..e8a9313 100644
--- a/package.json
+++ b/package.json
@@ -45,7 +45,7 @@
},
"devDependencies": {
"@emotion/react": "^11.11.1",
- "@form-atoms/field": "^5.1.0",
+ "@form-atoms/field": "^5.1.1",
"@form-atoms/list-atom": "^1.0.11",
"@mdx-js/react": "^2.3.0",
"@semantic-release/changelog": "^6.0.3",
diff --git a/src/components/datepicker-field/DatepickerField.stories.tsx b/src/components/datepicker-field/DatepickerField.stories.tsx
new file mode 100644
index 0000000..a9ee5a1
--- /dev/null
+++ b/src/components/datepicker-field/DatepickerField.stories.tsx
@@ -0,0 +1,54 @@
+import { dateField } from "@form-atoms/field";
+
+import { DatepickerField } from "./DatepickerField";
+import { FormStory, meta, optionalField } from "../../stories/story-form";
+
+export default {
+ title: "DatepickerField",
+ ...meta,
+};
+
+const dueDate = dateField({
+ schema: (s) => {
+ return s.min(new Date());
+ },
+});
+
+export const Required: FormStory = {
+ args: {
+ fields: { dueDate },
+ children: ({ required }) => (
+
+ ),
+ },
+};
+
+const optional = dateField().optional();
+
+export const Optional: FormStory = {
+ ...optionalField,
+ args: {
+ fields: { optional },
+ children: () => ,
+ },
+};
+
+const initialized = dateField();
+
+export const Initialized: FormStory = {
+ args: {
+ fields: { initialized },
+ children: () => (
+
+ ),
+ },
+};
diff --git a/src/components/datepicker-field/DatepickerField.test.tsx b/src/components/datepicker-field/DatepickerField.test.tsx
new file mode 100644
index 0000000..3c3b291
--- /dev/null
+++ b/src/components/datepicker-field/DatepickerField.test.tsx
@@ -0,0 +1,113 @@
+import { dateField } from "@form-atoms/field";
+import { act, render, renderHook, screen } from "@testing-library/react";
+import { userEvent } from "@testing-library/user-event";
+import { formAtom, useFieldActions, useFormSubmit } from "form-atoms";
+import { describe, expect, it } from "vitest";
+
+import { DatepickerField } from "./DatepickerField";
+
+describe("", () => {
+ it("focuses input when clicked on label", async () => {
+ const field = dateField();
+
+ render();
+
+ await act(() =>
+ userEvent.click(screen.getByLabelText("label", { exact: false })),
+ );
+
+ expect(screen.getByRole("dialog")).toHaveFocus();
+ });
+
+ describe("with required field", () => {
+ it("renders error message when submitting empty", async () => {
+ const field = dateField();
+
+ const form = formAtom({
+ field,
+ });
+ const { result } = renderHook(() => useFormSubmit(form));
+
+ const onSubmit = vi.fn();
+ await act(async () => {
+ result.current(onSubmit)();
+ });
+
+ render();
+
+ expect(screen.getByRole("dialog")).toBeInvalid();
+ expect(screen.getByText("This field is required")).toBeInTheDocument();
+ expect(onSubmit).not.toBeCalled();
+ });
+
+ it("submits without error when valid", async () => {
+ const value = new Date();
+ const field = dateField();
+ const form = formAtom({ field });
+ const { result } = renderHook(() => useFormSubmit(form));
+
+ render(
+ ,
+ );
+
+ const input = screen.getByRole("dialog");
+
+ expect(input).toBeValid();
+
+ const onSubmit = vi.fn();
+ await act(async () => {
+ result.current(onSubmit)();
+ });
+
+ expect(onSubmit).toHaveBeenCalledWith({ field: value });
+ });
+ });
+
+ describe("with optional field", () => {
+ it("submits with undefined", async () => {
+ const field = dateField().optional();
+ const form = formAtom({ field });
+ const { result } = renderHook(() => useFormSubmit(form));
+
+ render();
+
+ const dateInput = screen.getByRole("dialog");
+
+ expect(dateInput).toBeValid();
+
+ const onSubmit = vi.fn();
+ await act(async () => {
+ result.current(onSubmit)();
+ });
+
+ expect(onSubmit).toHaveBeenCalledWith({ field: undefined });
+ });
+ });
+
+ describe("placeholder", () => {
+ it("renders", () => {
+ const field = dateField();
+
+ render();
+
+ expect(screen.getByPlaceholderText("Pick a date")).toBeInTheDocument();
+ });
+
+ it("appears when the field is cleared", async () => {
+ const field = dateField({ value: new Date() });
+ const { result: fieldActions } = renderHook(() => useFieldActions(field));
+
+ render();
+
+ expect(
+ screen.queryByPlaceholderText("Pick a date"),
+ ).not.toBeInTheDocument();
+
+ await act(async () => {
+ fieldActions.current.setValue(undefined);
+ });
+
+ expect(screen.queryByPlaceholderText("Pick a date")).toBeInTheDocument();
+ });
+ });
+});
diff --git a/src/components/datepicker-field/DatepickerField.tsx b/src/components/datepicker-field/DatepickerField.tsx
new file mode 100644
index 0000000..6804eb9
--- /dev/null
+++ b/src/components/datepicker-field/DatepickerField.tsx
@@ -0,0 +1,60 @@
+import { DateFieldProps, useDateFieldProps } from "@form-atoms/field";
+import { Datepicker, DatepickerProps } from "flowbite-react";
+
+import { FlowbiteField } from "../field";
+
+type DatepickerFIeldProps = DateFieldProps &
+ Omit;
+
+export const DatepickerField = ({
+ field,
+ label,
+ helperText,
+ required,
+ initialValue,
+ placeholder = "Please select a date",
+ ...uiProps
+}: DatepickerFIeldProps) => {
+ const {
+ // TODO(flowbite-react/Datepicker): support forwardRef
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ ref,
+ value,
+ onChange,
+ ...dateFieldProps
+ } = useDateFieldProps(field, {
+ initialValue,
+ });
+
+ const emptyProps = !value
+ ? {
+ value: "",
+ placeholder,
+ }
+ : {};
+
+ return (
+
+ {(fieldProps) => (
+ {
+ onChange({
+ // @ts-expect-error fake event
+ currentTarget: { valueAsDate },
+ });
+ }}
+ />
+ )}
+
+ );
+};
diff --git a/yarn.lock b/yarn.lock
index 82f73aa..0cacfe0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2726,9 +2726,9 @@ __metadata:
languageName: node
linkType: hard
-"@form-atoms/field@npm:^5.1.0":
- version: 5.1.0
- resolution: "@form-atoms/field@npm:5.1.0"
+"@form-atoms/field@npm:^5.1.1":
+ version: 5.1.1
+ resolution: "@form-atoms/field@npm:5.1.1"
dependencies:
react-render-prop-type: "npm:0.1.0"
peerDependencies:
@@ -2738,7 +2738,7 @@ __metadata:
jotai-effect: ^0
react: ">=16.8"
zod: ^3
- checksum: 556d1366b731cb8a995bd37b9057f9877ebcf0406dbed9c0e1a175d4d0edd3d439528b3f4b27c758f2364d938c35c1fbccd397664aad2c28232605038aad2b9a
+ checksum: af504b7abfc3352919d5882d9e40ebe4c504a94dcced380c9bbd139b55074518fbd7a4b7256dbdabc1c650094865142496890f647cb52cd145471422097d53f6
languageName: node
linkType: hard
@@ -2747,7 +2747,7 @@ __metadata:
resolution: "@form-atoms/flowbite@workspace:."
dependencies:
"@emotion/react": "npm:^11.11.1"
- "@form-atoms/field": "npm:^5.1.0"
+ "@form-atoms/field": "npm:^5.1.1"
"@form-atoms/list-atom": "npm:^1.0.11"
"@mdx-js/react": "npm:^2.3.0"
"@semantic-release/changelog": "npm:^6.0.3"