From 27661c73d8fc3a27afcc4d70524175729a7bdbad Mon Sep 17 00:00:00 2001 From: Miroslav Petrik Date: Thu, 14 Mar 2024 11:03:46 +0100 Subject: [PATCH 1/2] =?UTF-8?q?feat(DatepickerField):=20=F0=9F=8E=87Datepi?= =?UTF-8?q?cker=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 6 +- package.json | 2 +- .../DatepickerField.stories.tsx | 57 +++++++++++ .../datepicker-field/DatepickerField.test.tsx | 96 +++++++++++++++++++ .../datepicker-field/DatepickerField.tsx | 51 ++++++++++ yarn.lock | 10 +- 6 files changed, 213 insertions(+), 9 deletions(-) create mode 100644 src/components/datepicker-field/DatepickerField.stories.tsx create mode 100644 src/components/datepicker-field/DatepickerField.test.tsx create mode 100644 src/components/datepicker-field/DatepickerField.tsx 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..5d1b88a --- /dev/null +++ b/src/components/datepicker-field/DatepickerField.stories.tsx @@ -0,0 +1,57 @@ +import { dateField } from "@form-atoms/field"; + +import { DatepickerField } from "./DatepickerField"; +import { FormStory, meta, optionalField } from "../../stories/story-form"; + +export default { + title: "DatepickerField", + ...meta, +}; + +const birthday = dateField({ + schema: (s) => { + const date = new Date(); + date.setFullYear(date.getFullYear() - 15); + + return s.max(date); + }, +}); + +export const Required: FormStory = { + args: { + fields: { birthday }, + 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..bb9b26e --- /dev/null +++ b/src/components/datepicker-field/DatepickerField.test.tsx @@ -0,0 +1,96 @@ +import { dateField } from "@form-atoms/field"; +import { act, render, renderHook, screen } from "@testing-library/react"; +import { userEvent } from "@testing-library/user-event"; +import { formAtom, 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 textarea = screen.getByRole("dialog"); + + expect(textarea).toBeValid(); + + const onSubmit = vi.fn(); + await act(async () => { + result.current(onSubmit)(); + }); + + expect(onSubmit).toHaveBeenCalledWith({ field: undefined }); + }); + }); +}); diff --git a/src/components/datepicker-field/DatepickerField.tsx b/src/components/datepicker-field/DatepickerField.tsx new file mode 100644 index 0000000..f91a3d6 --- /dev/null +++ b/src/components/datepicker-field/DatepickerField.tsx @@ -0,0 +1,51 @@ +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, + ...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, + }); + + 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" From 21a2e6a9988f65c331066caa525fbb1146cf3a9f Mon Sep 17 00:00:00 2001 From: Miroslav Petrik Date: Thu, 14 Mar 2024 11:35:59 +0100 Subject: [PATCH 2/2] feat(DatepickerField): control placeholder --- .../DatepickerField.stories.tsx | 15 +++--- .../datepicker-field/DatepickerField.test.tsx | 53 ++++++++++++------- .../datepicker-field/DatepickerField.tsx | 9 ++++ 3 files changed, 50 insertions(+), 27 deletions(-) diff --git a/src/components/datepicker-field/DatepickerField.stories.tsx b/src/components/datepicker-field/DatepickerField.stories.tsx index 5d1b88a..a9ee5a1 100644 --- a/src/components/datepicker-field/DatepickerField.stories.tsx +++ b/src/components/datepicker-field/DatepickerField.stories.tsx @@ -8,24 +8,21 @@ export default { ...meta, }; -const birthday = dateField({ +const dueDate = dateField({ schema: (s) => { - const date = new Date(); - date.setFullYear(date.getFullYear() - 15); - - return s.max(date); + return s.min(new Date()); }, }); export const Required: FormStory = { args: { - fields: { birthday }, + fields: { dueDate }, children: ({ required }) => ( ), }, diff --git a/src/components/datepicker-field/DatepickerField.test.tsx b/src/components/datepicker-field/DatepickerField.test.tsx index bb9b26e..3c3b291 100644 --- a/src/components/datepicker-field/DatepickerField.test.tsx +++ b/src/components/datepicker-field/DatepickerField.test.tsx @@ -1,7 +1,7 @@ import { dateField } from "@form-atoms/field"; import { act, render, renderHook, screen } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; -import { formAtom, useFormSubmit } from "form-atoms"; +import { formAtom, useFieldActions, useFormSubmit } from "form-atoms"; import { describe, expect, it } from "vitest"; import { DatepickerField } from "./DatepickerField"; @@ -33,7 +33,7 @@ describe("", () => { result.current(onSubmit)(); }); - render(); + render(); expect(screen.getByRole("dialog")).toBeInvalid(); expect(screen.getByText("This field is required")).toBeInTheDocument(); @@ -42,20 +42,12 @@ describe("", () => { it("submits without error when valid", async () => { const value = new Date(); - const field = dateField(); - const form = formAtom({ - field, - }); + const form = formAtom({ field }); const { result } = renderHook(() => useFormSubmit(form)); render( - , + , ); const input = screen.getByRole("dialog"); @@ -74,16 +66,14 @@ describe("", () => { describe("with optional field", () => { it("submits with undefined", async () => { const field = dateField().optional(); - const form = formAtom({ - field, - }); + const form = formAtom({ field }); const { result } = renderHook(() => useFormSubmit(form)); - render(); + render(); - const textarea = screen.getByRole("dialog"); + const dateInput = screen.getByRole("dialog"); - expect(textarea).toBeValid(); + expect(dateInput).toBeValid(); const onSubmit = vi.fn(); await act(async () => { @@ -93,4 +83,31 @@ describe("", () => { 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 index f91a3d6..6804eb9 100644 --- a/src/components/datepicker-field/DatepickerField.tsx +++ b/src/components/datepicker-field/DatepickerField.tsx @@ -12,6 +12,7 @@ export const DatepickerField = ({ helperText, required, initialValue, + placeholder = "Please select a date", ...uiProps }: DatepickerFIeldProps) => { const { @@ -25,6 +26,13 @@ export const DatepickerField = ({ initialValue, }); + const emptyProps = !value + ? { + value: "", + placeholder, + } + : {}; + return ( { onChange({