Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions __tests__/forms/PasswordField.test.tsx
Original file line number Diff line number Diff line change
@@ -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 <PasswordField register={register} name={"password"} label={"Password"} />
}

const PasswordFieldError = () => {
// eslint-disable-next-line @typescript-eslint/unbound-method
const { register } = useForm({ mode: "onChange" })
return (
<PasswordField
register={register}
name={"password"}
label={"Password"}
error={true}
errorMessage={"Wrong password"}
/>
)
}

const PasswordFieldCustomLabels = () => {
// eslint-disable-next-line @typescript-eslint/unbound-method
const { register } = useForm({ mode: "onChange" })
return (
<PasswordField
register={register}
name={"password"}
label={"Password"}
showPasswordLabel={"Disclose password"}
/>
)
}

describe("<PasswordField>", () => {
it("renders a password input with a show password checkbox", () => {
const { getByLabelText } = render(<PasswordFieldDefault />)
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(<PasswordFieldDefault />)
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(<PasswordFieldDefault />)
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(<PasswordFieldError />)
expect(getByText("Wrong password")).toBeTruthy()
})

it("supports a custom show password label", () => {
const { getByLabelText } = render(<PasswordFieldCustomLabels />)
expect(getByLabelText("Disclose password")).toBeTruthy()
})
})
1 change: 1 addition & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
40 changes: 40 additions & 0 deletions src/forms/PasswordField.stories.tsx
Original file line number Diff line number Diff line change
@@ -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) => <div style={{ padding: "1rem" }}>{storyFn()}</div>],
}

export const PasswordFieldDefault = () => {
const { register } = useForm({ mode: "onChange" })
return <PasswordField id="password-1" register={register} name={"password"} label={"Password"} />
}

export const PasswordFieldWithError = () => {
const { register } = useForm({ mode: "onChange" })
return (
<PasswordField
id="password2"
register={register}
name={"password"}
label={"Password"}
error={true}
errorMessage={"Password must be at least 8 characters"}
/>
)
}

export const PasswordFieldWithNote = () => {
const { register } = useForm({ mode: "onChange" })
return (
<PasswordField
id="password-3"
register={register}
name={"password"}
label={"Password"}
note={"Must be at least 8 characters and include a number."}
/>
)
}
48 changes: 48 additions & 0 deletions src/forms/PasswordField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as React from "react"
import { useState } from "react"
import { Field, FieldProps } from "./Field"

export interface PasswordFieldProps
extends Omit<FieldProps, "type" | "postInputContent" | "className"> {
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 (
<div className={classes.join(" ")}>
<Field {...fieldProps} className={fieldClassName} type={visible ? "text" : "password"} />
<Field
type="checkbox"
id={checkboxIdOrName}
name={checkboxIdOrName}
label={showPasswordLabel}
onChange={() => setVisible((prevVisible) => !prevVisible)}
dataTestId={checkboxDataTestId}
/>
</div>
)
}

export { PasswordField as default, PasswordField }
7 changes: 7 additions & 0 deletions src/global/forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}