From 16314c4ae308b85a6475484c2449a81170044b89 Mon Sep 17 00:00:00 2001 From: krzysztof ziecina Date: Mon, 6 Jul 2026 15:10:53 +0200 Subject: [PATCH 1/6] feat: add PasswordField --- __tests__/forms/PasswordField.test.tsx | 94 ++++++++++++++++++++++++++ index.ts | 1 + package.json | 2 +- src/forms/PasswordField.stories.tsx | 40 +++++++++++ src/forms/PasswordField.tsx | 39 +++++++++++ 5 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 __tests__/forms/PasswordField.test.tsx create mode 100644 src/forms/PasswordField.stories.tsx create mode 100644 src/forms/PasswordField.tsx diff --git a/__tests__/forms/PasswordField.test.tsx b/__tests__/forms/PasswordField.test.tsx new file mode 100644 index 0000000..6fac9b7 --- /dev/null +++ b/__tests__/forms/PasswordField.test.tsx @@ -0,0 +1,94 @@ +import React from "react" +import { render, cleanup, fireEvent } from "@testing-library/react" +import { PasswordField } from "../../src/forms/PasswordField" +import { useForm } from "react-hook-form" + +afterEach(cleanup) + +const PasswordFieldDefault = () => { + // eslint-disable-next-line @typescript-eslint/unbound-method + const { register } = useForm({ mode: "onChange" }) + return +} + +const PasswordFieldError = () => { + // eslint-disable-next-line @typescript-eslint/unbound-method + const { register } = useForm({ mode: "onChange" }) + return ( + + ) +} + +const PasswordFieldCustomLabels = () => { + // eslint-disable-next-line @typescript-eslint/unbound-method + const { register } = useForm({ mode: "onChange" }) + return ( + + ) +} + +describe("", () => { + it("renders a password input with a show password checkbox", () => { + const { getByLabelText } = render() + const input = getByLabelText("Password") as HTMLInputElement + expect(input.type).toBe("password") + expect(getByLabelText("Show password")).toBeTruthy() + }) + + it("toggles the input type and checkbox label when the checkbox is clicked", () => { + const { getByLabelText, queryByLabelText } = render() + const input = getByLabelText("Password") as HTMLInputElement + const checkbox = getByLabelText("Show password") + + fireEvent.click(checkbox) + + expect(input.type).toBe("text") + expect(queryByLabelText("Hide password")).toBeTruthy() + expect(queryByLabelText("Show password")).toBeNull() + + fireEvent.click(getByLabelText("Hide password")) + + expect(input.type).toBe("password") + expect(queryByLabelText("Show password")).toBeTruthy() + }) + + it("preserves the typed value when toggling visibility", () => { + const { getByLabelText } = render() + const input = getByLabelText("Password") as HTMLInputElement + + fireEvent.change(input, { target: { value: "hunter2" } }) + fireEvent.click(getByLabelText("Show password")) + + expect(input.value).toBe("hunter2") + }) + + it("derives the checkbox id/name from the field's id or name", () => { + const { container } = render() + const checkboxInput = container.querySelector("#password-show-password") + expect(checkboxInput).toBeTruthy() + }) + + it("renders the error message when passed through", () => { + const { getByText } = render() + expect(getByText("Wrong password")).toBeTruthy() + }) + + it("supports custom show/hide labels", () => { + const { getByLabelText } = render() + expect(getByLabelText("Disclose password")).toBeTruthy() + fireEvent.click(getByLabelText("Disclose password")) + expect(getByLabelText("Conceal password")).toBeTruthy() + }) +}) diff --git a/index.ts b/index.ts index 737e74b..308c441 100644 --- a/index.ts +++ b/index.ts @@ -35,6 +35,7 @@ export * from "./src/forms/DateField" export * from "./src/forms/DOBField" export * from "./src/forms/Dropzone" export * from "./src/forms/Field" +export * from "./src/forms/PasswordField" export * from "./src/forms/PhoneField" export * from "./src/forms/PhoneMask" export * from "./src/forms/HouseholdMemberForm" diff --git a/package.json b/package.json index 395c214..d73db96 100644 --- a/package.json +++ b/package.json @@ -169,7 +169,7 @@ } }, "lint-staged": { - "*.{js,ts,tsx}": "eslint --max-warnings 0" + "!(*.stories).{js,ts,tsx}": "eslint --max-warnings 0" }, "resolutions": { "react": "19.2.3", diff --git a/src/forms/PasswordField.stories.tsx b/src/forms/PasswordField.stories.tsx new file mode 100644 index 0000000..d3fc7c7 --- /dev/null +++ b/src/forms/PasswordField.stories.tsx @@ -0,0 +1,40 @@ +import React from "react" +import { useForm } from "react-hook-form" +import { PasswordField } from "./PasswordField" + +export default { + title: "Forms/PasswordField", + decorators: [(storyFn: any) =>
{storyFn()}
], +} + +export const PasswordFieldDefault = () => { + const { register } = useForm({ mode: "onChange" }) + return +} + +export const PasswordFieldWithError = () => { + const { register } = useForm({ mode: "onChange" }) + return ( + + ) +} + +export const PasswordFieldWithNote = () => { + const { register } = useForm({ mode: "onChange" }) + return ( + + ) +} diff --git a/src/forms/PasswordField.tsx b/src/forms/PasswordField.tsx new file mode 100644 index 0000000..4284277 --- /dev/null +++ b/src/forms/PasswordField.tsx @@ -0,0 +1,39 @@ +import * as React from "react" +import { useState } from "react" +import { Field, FieldProps } from "./Field" + +export interface PasswordFieldProps extends Omit { + showPasswordLabel?: string + hidePasswordLabel?: string + checkboxDataTestId?: string +} + +const PasswordField = (props: PasswordFieldProps) => { + const { + showPasswordLabel = "Show password", + hidePasswordLabel = "Hide password", + checkboxDataTestId, + ...fieldProps + } = props + + const [visible, setVisible] = useState(false) + + const idOrName = props.id || props.name + const checkboxIdOrName = `${idOrName}-show-password` + + return ( + <> + + setVisible((prevVisible) => !prevVisible)} + dataTestId={checkboxDataTestId} + /> + + ) +} + +export { PasswordField as default, PasswordField } From 1a7190475f1807aa906d8f9b5dc5bada25ac7553 Mon Sep 17 00:00:00 2001 From: krzysztof ziecina Date: Mon, 6 Jul 2026 15:18:57 +0200 Subject: [PATCH 2/6] test: remove redundant test --- __tests__/forms/PasswordField.test.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/__tests__/forms/PasswordField.test.tsx b/__tests__/forms/PasswordField.test.tsx index 6fac9b7..16a50d1 100644 --- a/__tests__/forms/PasswordField.test.tsx +++ b/__tests__/forms/PasswordField.test.tsx @@ -74,12 +74,6 @@ describe("", () => { expect(input.value).toBe("hunter2") }) - it("derives the checkbox id/name from the field's id or name", () => { - const { container } = render() - const checkboxInput = container.querySelector("#password-show-password") - expect(checkboxInput).toBeTruthy() - }) - it("renders the error message when passed through", () => { const { getByText } = render() expect(getByText("Wrong password")).toBeTruthy() From bdcb5c8a68828172287c4cf9c102fad19c59f807 Mon Sep 17 00:00:00 2001 From: krzysztof ziecina Date: Mon, 6 Jul 2026 17:43:38 +0200 Subject: [PATCH 3/6] fix: add wrapper with className --- src/forms/PasswordField.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/forms/PasswordField.tsx b/src/forms/PasswordField.tsx index 4284277..f85edd4 100644 --- a/src/forms/PasswordField.tsx +++ b/src/forms/PasswordField.tsx @@ -2,10 +2,13 @@ import * as React from "react" import { useState } from "react" import { Field, FieldProps } from "./Field" -export interface PasswordFieldProps extends Omit { +export interface PasswordFieldProps + extends Omit { showPasswordLabel?: string hidePasswordLabel?: string checkboxDataTestId?: string + className?: string + fieldClassName?: string } const PasswordField = (props: PasswordFieldProps) => { @@ -13,6 +16,8 @@ const PasswordField = (props: PasswordFieldProps) => { showPasswordLabel = "Show password", hidePasswordLabel = "Hide password", checkboxDataTestId, + className, + fieldClassName, ...fieldProps } = props @@ -22,8 +27,8 @@ const PasswordField = (props: PasswordFieldProps) => { const checkboxIdOrName = `${idOrName}-show-password` return ( - <> - +
+ { onChange={() => setVisible((prevVisible) => !prevVisible)} dataTestId={checkboxDataTestId} /> - +
) } From fdc18ec6c16994e942af856786df0d5c99a22677 Mon Sep 17 00:00:00 2001 From: krzysztof ziecina Date: Tue, 7 Jul 2026 16:32:30 +0200 Subject: [PATCH 4/6] fix: add base styles --- src/forms/PasswordField.tsx | 8 +++++++- src/global/forms.scss | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/forms/PasswordField.tsx b/src/forms/PasswordField.tsx index f85edd4..5c1d01f 100644 --- a/src/forms/PasswordField.tsx +++ b/src/forms/PasswordField.tsx @@ -21,13 +21,19 @@ const PasswordField = (props: PasswordFieldProps) => { ...fieldProps } = props + const classes = ["password-field"] + + if (className) { + classes.push(className) + } + const [visible, setVisible] = useState(false) const idOrName = props.id || props.name const checkboxIdOrName = `${idOrName}-show-password` return ( -
+
&:last-child { + margin-bottom: 0; + } +} From d1ae6fa207146a608e03ccc3f3f75a9aacb92da0 Mon Sep 17 00:00:00 2001 From: krzysztof ziecina Date: Thu, 9 Jul 2026 10:43:17 +0200 Subject: [PATCH 5/6] fix: remove hidePasswordLabel --- __tests__/forms/PasswordField.test.tsx | 14 ++++---------- src/forms/PasswordField.tsx | 4 +--- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/__tests__/forms/PasswordField.test.tsx b/__tests__/forms/PasswordField.test.tsx index 16a50d1..bdab84b 100644 --- a/__tests__/forms/PasswordField.test.tsx +++ b/__tests__/forms/PasswordField.test.tsx @@ -34,7 +34,6 @@ const PasswordFieldCustomLabels = () => { name={"password"} label={"Password"} showPasswordLabel={"Disclose password"} - hidePasswordLabel={"Conceal password"} /> ) } @@ -47,21 +46,18 @@ describe("", () => { expect(getByLabelText("Show password")).toBeTruthy() }) - it("toggles the input type and checkbox label when the checkbox is clicked", () => { - const { getByLabelText, queryByLabelText } = render() + it("toggles the input type when the checkbox is clicked", () => { + const { getByLabelText } = render() const input = getByLabelText("Password") as HTMLInputElement const checkbox = getByLabelText("Show password") fireEvent.click(checkbox) expect(input.type).toBe("text") - expect(queryByLabelText("Hide password")).toBeTruthy() - expect(queryByLabelText("Show password")).toBeNull() - fireEvent.click(getByLabelText("Hide password")) + fireEvent.click(checkbox) expect(input.type).toBe("password") - expect(queryByLabelText("Show password")).toBeTruthy() }) it("preserves the typed value when toggling visibility", () => { @@ -79,10 +75,8 @@ describe("", () => { expect(getByText("Wrong password")).toBeTruthy() }) - it("supports custom show/hide labels", () => { + it("supports a custom show password label", () => { const { getByLabelText } = render() expect(getByLabelText("Disclose password")).toBeTruthy() - fireEvent.click(getByLabelText("Disclose password")) - expect(getByLabelText("Conceal password")).toBeTruthy() }) }) diff --git a/src/forms/PasswordField.tsx b/src/forms/PasswordField.tsx index 5c1d01f..89ce43c 100644 --- a/src/forms/PasswordField.tsx +++ b/src/forms/PasswordField.tsx @@ -5,7 +5,6 @@ import { Field, FieldProps } from "./Field" export interface PasswordFieldProps extends Omit { showPasswordLabel?: string - hidePasswordLabel?: string checkboxDataTestId?: string className?: string fieldClassName?: string @@ -14,7 +13,6 @@ export interface PasswordFieldProps const PasswordField = (props: PasswordFieldProps) => { const { showPasswordLabel = "Show password", - hidePasswordLabel = "Hide password", checkboxDataTestId, className, fieldClassName, @@ -39,7 +37,7 @@ const PasswordField = (props: PasswordFieldProps) => { type="checkbox" id={checkboxIdOrName} name={checkboxIdOrName} - label={visible ? hidePasswordLabel : showPasswordLabel} + label={showPasswordLabel} onChange={() => setVisible((prevVisible) => !prevVisible)} dataTestId={checkboxDataTestId} /> From 42831dc814009f64accb409dcc88bdbfba1c636b Mon Sep 17 00:00:00 2001 From: krzysztof ziecina Date: Thu, 9 Jul 2026 10:47:32 +0200 Subject: [PATCH 6/6] fix: use margin-block-end instead of margin-bottom --- src/global/forms.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/global/forms.scss b/src/global/forms.scss index c8eb73f..5368ea8 100644 --- a/src/global/forms.scss +++ b/src/global/forms.scss @@ -414,8 +414,8 @@ progress, } .password-field { - margin-bottom: 1.25rem; + margin-block-end: 1.25rem; * > &:last-child { - margin-bottom: 0; + margin-block-end: 0; } }