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
65 changes: 65 additions & 0 deletions packages/shell/src/renderer/ui/text-field.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// @vitest-environment jsdom
/**
* `<TextField>` numeric bounds — `min`/`max`/`step` forward to the underlying
* `<input>` so a constrained numeric field ("keep the last N days") can ride the
* shared face instead of hand-rolling `<input type=number>` just to get bounds.
* They're omitted entirely when unset (no `min=""` polluting the DOM).
*/

import { act } from "react";
import { type Root, createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { TextField } from "./text-field";

(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;

describe("TextField numeric bounds", () => {
let host: HTMLDivElement;
let root: Root;

beforeEach(() => {
host = document.createElement("div");
document.body.appendChild(host);
root = createRoot(host);
});

afterEach(() => {
act(() => root.unmount());
host.remove();
});

const input = () => {
const el = host.querySelector("input");
if (!el) throw new Error("no input");
return el;
};

it("forwards min/max/step to the input", () => {
act(() =>
root.render(
<TextField
type="number"
value="30"
onChange={() => {}}
min={1}
max={3650}
step={1}
aria-label="days"
/>,
),
);
const el = input();
expect(el.getAttribute("min")).toBe("1");
expect(el.getAttribute("max")).toBe("3650");
expect(el.getAttribute("step")).toBe("1");
expect(el.type).toBe("number");
});

it("omits the bounds attributes entirely when unset", () => {
act(() => root.render(<TextField value="hi" onChange={() => {}} aria-label="name" />));
const el = input();
expect(el.hasAttribute("min")).toBe(false);
expect(el.hasAttribute("max")).toBe(false);
expect(el.hasAttribute("step")).toBe(false);
});
});
13 changes: 13 additions & 0 deletions packages/shell/src/renderer/ui/text-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ export type TextFieldProps = CommonProps & {
readonly value: string;
readonly onChange: (next: string) => void;
readonly type?: "text" | "email" | "search" | "url" | "tel" | "password" | "number";
/** Numeric bounds/step, forwarded to the underlying `<input>`. Only
* meaningful with `type="number"` — they let a constrained numeric field
* (e.g. "keep the last N days") ride the shared face instead of dropping to
* a hand-rolled `<input type=number>` just to get `min`/`max`. */
readonly min?: number;
readonly max?: number;
readonly step?: number;
readonly placeholder?: string;
readonly maxLength?: number;
readonly autoFocus?: boolean;
Expand Down Expand Up @@ -88,6 +95,9 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(function T
value,
onChange,
type = "text",
min,
max,
step,
placeholder,
maxLength,
autoFocus,
Expand Down Expand Up @@ -141,6 +151,9 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(function T
{...(onKeyDown ? { onKeyDown } : {})}
{...(placeholder !== undefined ? { placeholder } : {})}
{...(maxLength !== undefined ? { maxLength } : {})}
{...(min !== undefined ? { min } : {})}
{...(max !== undefined ? { max } : {})}
{...(step !== undefined ? { step } : {})}
{...(autoComplete !== undefined ? { autoComplete } : {})}
{...(inputMode !== undefined ? { inputMode } : {})}
{...(name !== undefined ? { name } : {})}
Expand Down
Loading