Skip to content
Open
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
103 changes: 103 additions & 0 deletions frontend/__tests__/components/ui/form/InputField.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,107 @@ describe("InputField", () => {
fireEvent.input(input, { target: { value: "6" } });
expect(field.handleChange).toHaveBeenCalledWith(6);
});

it("renders password toggle button when type is password", () => {
const field = makeField("password");
const { container } = render(() => (
<InputField field={() => field} type="password" />
));

const toggleButton = container.querySelector(
'button[type="button"]',
) as HTMLButtonElement;
expect(toggleButton).toBeInTheDocument();
});

it("password field starts with type=password (masked)", () => {
const field = makeField("password");
const { container } = render(() => (
<InputField field={() => field} type="password" />
));

expect(container.querySelector("input")).toHaveAttribute(
"type",
"password",
);
});

it("toggling password button switches input type to text", async () => {
const field = makeField("password");
const { container } = render(() => (
<InputField field={() => field} type="password" />
));

const toggleButton = container.querySelector(
'button[type="button"]',
) as HTMLButtonElement;
expect(container.querySelector("input")).toHaveAttribute(
"type",
"password",
);

fireEvent.click(toggleButton);
expect(container.querySelector("input")).toHaveAttribute("type", "text");
});

it("toggling password button a second time switches back to password", async () => {
const field = makeField("password");
const { container } = render(() => (
<InputField field={() => field} type="password" />
));

const toggleButton = container.querySelector(
'button[type="button"]',
) as HTMLButtonElement;

// First toggle: show
fireEvent.click(toggleButton);
expect(container.querySelector("input")).toHaveAttribute("type", "text");

// Second toggle: hide
fireEvent.click(toggleButton);
expect(container.querySelector("input")).toHaveAttribute(
"type",
"password",
);
});

it("does not render password toggle when type is not password", () => {
const field = makeField("name");
const { container } = render(() => (
<InputField field={() => field} type="text" />
));

const toggleButton = container.querySelector('button[type="button"]');
expect(toggleButton).not.toBeInTheDocument();
});

it("shows eye icon when password is hidden (masked)", () => {
const field = makeField("password");
const { container } = render(() => (
<InputField field={() => field} type="password" />
));

expect(
container.querySelector(".fa-eye")?.classList.contains("fa-eye"),
).toBe(true);
});

it("shows eye-slash icon when password is visible", async () => {
const field = makeField("password");
const { container } = render(() => (
<InputField field={() => field} type="password" />
));

const toggleButton = container.querySelector(
'button[type="button"]',
) as HTMLButtonElement;
fireEvent.click(toggleButton);

expect(
container
.querySelector(".fa-eye-slash")
?.classList.contains("fa-eye-slash"),
).toBe(true);
});
});
37 changes: 35 additions & 2 deletions frontend/src/ts/components/ui/form/InputField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ZodDate, ZodFirstPartyTypeKind, ZodNumber, ZodTypeAny } from "zod";

import { cn } from "../../../utils/cn";
import { getZodType, unwrapSchema } from "../../../utils/zod";
import { Button } from "../../common/Button";
import { FieldIndicator } from "./FieldIndicator";

export function InputField(props: {
Expand All @@ -30,6 +31,9 @@ export function InputField(props: {
alwaysShowFieldIndicator?: boolean;
}): JSXElement {
const [shake, setShake] = createSignal(false);
const [showPassword, setShowPassword] = createSignal(false);
const isPasswordType = () => props.type === "password";
const hasValidators = () => !!props.field().options.validators;

const shakeItIfYouWantIt = () => {
if (
Expand Down Expand Up @@ -62,10 +66,18 @@ export function InputField(props: {
"rounded border-none bg-sub-alt p-[0.5em] text-em-base leading-[1.25em] caret-main outline-none",
"focus-visible:shadow-[0_0_0_0.1rem_var(--bg-color),0_0_0_0.2rem_var(--text-color)]",
"autofill-fix",
props.field().options.validators ? "pr-[1.85em]" : "",
isPasswordType() || props.field().options.validators
? "pr-[1.85em]"
: "",
props.class,
)}
type={props.type ?? "text"}
type={
isPasswordType()
? showPassword()
? "text"
: "password"
: (props.type ?? "text")
}
placeholder={props.placeholder ?? ""}
autocomplete={props.autocomplete}
name={props.field().name as string}
Expand Down Expand Up @@ -110,6 +122,27 @@ export function InputField(props: {
max={props.max}
step={props.step?.toString()}
/>
<Show when={isPasswordType()}>
<Button
tabIndex={-1}
variant="text"
balloon={{ text: showPassword() ? "hide password" : "show password" }}
fa={{
icon: showPassword() ? "fa-eye-slash" : "fa-eye",
fixedWidth: true,
}}
class={cn(
"col-start-1 row-start-1 cursor-pointer self-center justify-self-end text-em-base hover:text-main focus-visible:shadow-[0_0_0_0.1rem_var(--bg-color),0_0_0_0.2rem_var(--text-color)]",
hasValidators() ? "pr-[1.8em]" : "pr-[0.4em]",
)}
onClick={() => {
setShowPassword((prev) => {
const next = !prev;
return next;
});
}}
/>
</Show>
<Show when={props.field().options.validators}>
<FieldIndicator
field={props.field()}
Expand Down
Loading