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"