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
2 changes: 1 addition & 1 deletion components/form/dist/components/Field.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
import type { FieldProps } from "../types";
export declare function Field({ label, required, hint, error, htmlFor, disabled, layout, className, children, }: FieldProps): import("react").JSX.Element;
export declare function Field(props: FieldProps): import("react").JSX.Element;
2 changes: 1 addition & 1 deletion components/form/dist/components/Form.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
import type { FormProps } from "../types";
export declare function Form({ onSubmit, busy, disabled, error, success, ariaLabel, variant, className, children, ...props }: FormProps): import("react").JSX.Element;
export declare function Form(props: FormProps): import("react").JSX.Element;
9 changes: 2 additions & 7 deletions components/form/dist/components/SubmitButton.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,2 @@
export declare const SubmitButton: import("react").ForwardRefExoticComponent<Omit<import("react").ButtonHTMLAttributes<HTMLButtonElement>, "color" | "type"> & {
children: import("react").ReactNode;
busyLabel?: import("react").ReactNode;
variant?: "primary" | "outline" | "secondary";
size?: "sm" | "md";
ignoreBusy?: boolean;
} & import("react").RefAttributes<HTMLButtonElement>>;
import type { SubmitButtonProps } from "../types";
export declare const SubmitButton: import("react").ForwardRefExoticComponent<SubmitButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
162 changes: 112 additions & 50 deletions components/form/dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion components/form/dist/index.js.map

Large diffs are not rendered by default.

36 changes: 18 additions & 18 deletions components/form/dist/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import type { ButtonHTMLAttributes, FormHTMLAttributes, HTMLAttributes, ReactNode } from "react";
export type FormContextValue = {
import type { ButtonHTMLAttributes, FormEventHandler, FormHTMLAttributes, HTMLAttributes, ReactNode } from "react";
export interface FormContextValue {
/** True while a submit is in flight — disables every Field/SubmitButton in the form. */
busy: boolean;
/** Form-level disabled state (e.g. the record is read-only). */
disabled: boolean;
/** Current FormSection nesting depth (1 = top-level, 2 = nested, max 2). */
sectionDepth: number;
};
export type FormProps = Omit<FormHTMLAttributes<HTMLFormElement>, "onSubmit"> & {
}
export interface FormProps extends Omit<FormHTMLAttributes<HTMLFormElement>, "onSubmit"> {
/**
* Submission handler. Receives the native form submit event; the package
* never reads or owns field values — hosts own state and validation.
*/
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
onSubmit: FormEventHandler<HTMLFormElement>;
/**
* True while submitting. Disables all Fields, the Section group, and the
* SubmitButton (spinner) via context until the host flips it back.
Expand All @@ -33,8 +33,8 @@ export type FormProps = Omit<FormHTMLAttributes<HTMLFormElement>, "onSubmit"> &
variant?: "card" | "plain";
className?: string;
children?: ReactNode;
};
export type FormHeaderProps = {
}
export interface FormHeaderProps {
/** Optional leading icon next to the title (only shown with a title). */
icon?: ReactNode;
/** Header title. When omitted, the header section is not rendered. */
Expand All @@ -44,17 +44,17 @@ export type FormHeaderProps = {
/** Optional right-side slot — anything (badges, links, header-level controls). */
actions?: ReactNode;
className?: string;
};
}
export type FormContentProps = HTMLAttributes<HTMLDivElement>;
export type FormFooterProps = {
export interface FormFooterProps {
/** Optional left-side slot — anything (FormStatus, hint text, counters). */
info?: ReactNode;
/** Optional right-side slot — action buttons (SubmitButton, Button, ActionMenu). */
actions?: ReactNode;
/** When neither slot is provided, the footer (and its separator) is skipped. */
className?: string;
};
export type FieldProps = {
}
export interface FieldProps {
/** Accessible name for the control. */
label?: ReactNode;
/** Marks the label with a required asterisk and `aria-required`. */
Expand All @@ -80,8 +80,8 @@ export type FieldProps = {
className?: string;
/** The control. Must be a single element to receive id/disabled wiring. */
children: ReactNode;
};
export type FormSectionProps = {
}
export interface FormSectionProps {
/** Section legend (always rendered). */
title: ReactNode;
/** Optional supporting line under the legend. */
Expand All @@ -90,8 +90,8 @@ export type FormSectionProps = {
disabled?: boolean;
className?: string;
children?: ReactNode;
};
export type SubmitButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "type" | "color"> & {
}
export interface SubmitButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "type" | "color"> {
/** Label shown while idle. */
children: ReactNode;
/**
Expand All @@ -107,11 +107,11 @@ export type SubmitButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "t
* cancel button in the same action row).
*/
ignoreBusy?: boolean;
};
export type FormStatusProps = {
}
export interface FormStatusProps {
/** Message content. */
children: ReactNode;
/** Success (default) or error tone. */
tone?: "success" | "error";
className?: string;
};
}
40 changes: 40 additions & 0 deletions components/form/src/Form.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,46 @@ describe("Field", () => {
expect(screen.getByLabelText("Name")).not.toBeDisabled();
});

it("preserves a child's own disabled state when the form is idle", () => {
render(
<Field label="Name">
<TestInput disabled />
</Field>,
);
expect(screen.getByLabelText("Name")).toBeDisabled();
});

it("sets aria-required on the control when required", () => {
render(
<Field label="Email" required>
<TestInput />
</Field>,
);
expect(screen.getByLabelText(/Email/)).toHaveAttribute("aria-required", "true");
});

it("labels composite controls via aria-labelledby", () => {
render(
<Field label="Plan tier">
<div role="group" />
</Field>,
);
const group = screen.getByRole("group");
const labelId = group.getAttribute("aria-labelledby");
expect(labelId).toBeTruthy();
expect(screen.getByText("Plan tier")).toHaveAttribute("id", labelId);
});

it("does not override an existing aria-label on the control", () => {
render(
<Field label="Plan tier">
<div role="group" aria-label="My own label" />
</Field>,
);
expect(screen.getByRole("group")).toHaveAttribute("aria-label", "My own label");
expect(screen.getByRole("group")).not.toHaveAttribute("aria-labelledby");
});

it("disables a react-ui RadioGroup child when the form is busy", () => {
render(
<Form onSubmit={vi.fn()} busy>
Expand Down
Loading