diff --git a/__tests__/forms/PasswordField.test.tsx b/__tests__/forms/PasswordField.test.tsx new file mode 100644 index 0000000..bdab84b --- /dev/null +++ b/__tests__/forms/PasswordField.test.tsx @@ -0,0 +1,82 @@ +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 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") + + fireEvent.click(checkbox) + + expect(input.type).toBe("password") + }) + + 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("renders the error message when passed through", () => { + const { getByText } = render() + expect(getByText("Wrong password")).toBeTruthy() + }) + + it("supports a custom show password label", () => { + const { getByLabelText } = render() + expect(getByLabelText("Disclose 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..89ce43c --- /dev/null +++ b/src/forms/PasswordField.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import { useState } from "react" +import { Field, FieldProps } from "./Field" + +export interface PasswordFieldProps + extends Omit { + showPasswordLabel?: string + checkboxDataTestId?: string + className?: string + fieldClassName?: string +} + +const PasswordField = (props: PasswordFieldProps) => { + const { + showPasswordLabel = "Show password", + checkboxDataTestId, + className, + fieldClassName, + ...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 ( +
+ + setVisible((prevVisible) => !prevVisible)} + dataTestId={checkboxDataTestId} + /> +
+ ) +} + +export { PasswordField as default, PasswordField } diff --git a/src/global/forms.scss b/src/global/forms.scss index fbb0b96..5368ea8 100644 --- a/src/global/forms.scss +++ b/src/global/forms.scss @@ -412,3 +412,10 @@ progress, @apply bg-primary; transition: width 0.25s; } + +.password-field { + margin-block-end: 1.25rem; + * > &:last-child { + margin-block-end: 0; + } +}