Migrate Dialog to Zod props

Centralises all Dialog prop interfaces and schemas in dialog.props.ts.
Adds DialogRootPropsSchema, DialogContentPropsSchema, and DialogMeta for
AI/MCP discovery. Sub-components now import types from the shared file.
This commit is contained in:
Mats Bosson 2026-03-29 20:36:18 +07:00
parent f197c58296
commit 38ef3b0934
11 changed files with 128 additions and 75 deletions

View File

@ -1,13 +1,11 @@
// packages/core/src/components/dialog/dialog-close.tsx // packages/core/src/components/dialog/dialog-close.tsx
import type { Component, JSX } from "solid-js"; import type { JSX } from "solid-js";
import { mergeProps, splitProps } from "solid-js"; import { mergeProps, splitProps } from "solid-js";
import { Dynamic } from "solid-js/web"; import { Dynamic } from "solid-js/web";
import { useInternalDialogContext } from "./dialog-context"; import { useInternalDialogContext } from "./dialog-context";
import type { DialogCloseProps } from "./dialog.props";
export interface DialogCloseProps extends JSX.HTMLAttributes<HTMLButtonElement> { export type { DialogCloseProps };
as?: string | Component;
children?: JSX.Element;
}
/** Closes the dialog when clicked. Supports polymorphic rendering via `as`. */ /** Closes the dialog when clicked. Supports polymorphic rendering via `as`. */
export function DialogClose(props: DialogCloseProps): JSX.Element { export function DialogClose(props: DialogCloseProps): JSX.Element {

View File

@ -5,20 +5,9 @@ import { createDismiss } from "../../utilities/dismiss/create-dismiss";
import { createFocusTrap } from "../../utilities/focus-trap/create-focus-trap"; import { createFocusTrap } from "../../utilities/focus-trap/create-focus-trap";
import { createScrollLock } from "../../utilities/scroll-lock/create-scroll-lock"; import { createScrollLock } from "../../utilities/scroll-lock/create-scroll-lock";
import { useInternalDialogContext } from "./dialog-context"; import { useInternalDialogContext } from "./dialog-context";
import type { DialogContentProps } from "./dialog.props";
export interface DialogContentProps extends JSX.DialogHtmlAttributes<HTMLDialogElement> { export type { DialogContentProps };
/**
* Called when auto-focus fires on open. Call event.preventDefault() to prevent default.
*/
onOpenAutoFocus?: (event: Event) => void;
/**
* Called when auto-focus fires on close. Call event.preventDefault() to prevent default.
*/
onCloseAutoFocus?: (event: Event) => void;
/** Keep mounted even when closed (for animation control). */
forceMount?: boolean;
children?: JSX.Element;
}
/** /**
* Dialog content panel. Manages focus trap, scroll lock, and dismiss. * Dialog content panel. Manages focus trap, scroll lock, and dismiss.

View File

@ -2,10 +2,9 @@
import type { JSX } from "solid-js"; import type { JSX } from "solid-js";
import { createUniqueId, onCleanup, onMount, splitProps } from "solid-js"; import { createUniqueId, onCleanup, onMount, splitProps } from "solid-js";
import { useInternalDialogContext } from "./dialog-context"; import { useInternalDialogContext } from "./dialog-context";
import type { DialogDescriptionProps } from "./dialog.props";
export interface DialogDescriptionProps extends JSX.HTMLAttributes<HTMLParagraphElement> { export type { DialogDescriptionProps };
children?: JSX.Element;
}
/** Renders as p and registers its ID for aria-describedby on Dialog.Content. */ /** Renders as p and registers its ID for aria-describedby on Dialog.Content. */
export function DialogDescription(props: DialogDescriptionProps): JSX.Element { export function DialogDescription(props: DialogDescriptionProps): JSX.Element {

View File

@ -2,10 +2,9 @@
import type { JSX } from "solid-js"; import type { JSX } from "solid-js";
import { Show, splitProps } from "solid-js"; import { Show, splitProps } from "solid-js";
import { useInternalDialogContext } from "./dialog-context"; import { useInternalDialogContext } from "./dialog-context";
import type { DialogOverlayProps } from "./dialog.props";
export interface DialogOverlayProps extends JSX.HTMLAttributes<HTMLDivElement> { export type { DialogOverlayProps };
forceMount?: boolean;
}
/** Semi-transparent overlay rendered behind dialog content. */ /** Semi-transparent overlay rendered behind dialog content. */
export function DialogOverlay(props: DialogOverlayProps): JSX.Element { export function DialogOverlay(props: DialogOverlayProps): JSX.Element {

View File

@ -1,12 +1,9 @@
// packages/core/src/components/dialog/dialog-portal.tsx // packages/core/src/components/dialog/dialog-portal.tsx
import type { JSX } from "solid-js"; import type { JSX } from "solid-js";
import { Portal } from "../../utilities/portal/portal"; import { Portal } from "../../utilities/portal/portal";
import type { DialogPortalProps } from "./dialog.props";
export interface DialogPortalProps { export type { DialogPortalProps };
/** Override the portal target container. */
target?: Element | null;
children: JSX.Element;
}
/** Renders children into a portal (defaults to document.body). */ /** Renders children into a portal (defaults to document.body). */
export function DialogPortal(props: DialogPortalProps): JSX.Element { export function DialogPortal(props: DialogPortalProps): JSX.Element {

View File

@ -1,6 +1,6 @@
// packages/core/src/components/dialog/dialog-root.tsx // packages/core/src/components/dialog/dialog-root.tsx
import type { JSX } from "solid-js";
import { createUniqueId } from "solid-js"; import { createUniqueId } from "solid-js";
import type { JSX } from "solid-js";
import { import {
type CreateDisclosureStateOptions, type CreateDisclosureStateOptions,
createDisclosureState, createDisclosureState,
@ -11,21 +11,9 @@ import {
InternalDialogContextProvider, InternalDialogContextProvider,
type InternalDialogContextValue, type InternalDialogContextValue,
} from "./dialog-context"; } from "./dialog-context";
import type { DialogRootProps } from "./dialog.props";
export interface DialogRootProps { export type { DialogRootProps };
/** Controlled open state. */
open?: boolean;
/** Default open state (uncontrolled). */
defaultOpen?: boolean;
/** Called when open state should change. */
onOpenChange?: (open: boolean) => void;
/**
* Whether the dialog blocks outside interaction and traps focus.
* @default true
*/
modal?: boolean;
children: JSX.Element;
}
/** /**
* Root component. Manages open state, provides context to all Dialog parts. * Root component. Manages open state, provides context to all Dialog parts.

View File

@ -2,10 +2,9 @@
import type { JSX } from "solid-js"; import type { JSX } from "solid-js";
import { createUniqueId, onCleanup, onMount, splitProps } from "solid-js"; import { createUniqueId, onCleanup, onMount, splitProps } from "solid-js";
import { useInternalDialogContext } from "./dialog-context"; import { useInternalDialogContext } from "./dialog-context";
import type { DialogTitleProps } from "./dialog.props";
export interface DialogTitleProps extends JSX.HTMLAttributes<HTMLHeadingElement> { export type { DialogTitleProps };
children?: JSX.Element;
}
/** Renders as h2 and registers its ID for aria-labelledby on Dialog.Content. */ /** Renders as h2 and registers its ID for aria-labelledby on Dialog.Content. */
export function DialogTitle(props: DialogTitleProps): JSX.Element { export function DialogTitle(props: DialogTitleProps): JSX.Element {

View File

@ -1,15 +1,11 @@
// packages/core/src/components/dialog/dialog-trigger.tsx // packages/core/src/components/dialog/dialog-trigger.tsx
import type { Component, JSX } from "solid-js"; import type { JSX } from "solid-js";
import { mergeProps, splitProps } from "solid-js"; import { mergeProps, splitProps } from "solid-js";
import { Dynamic } from "solid-js/web"; import { Dynamic } from "solid-js/web";
import { useInternalDialogContext } from "./dialog-context"; import { useInternalDialogContext } from "./dialog-context";
import type { DialogTriggerProps } from "./dialog.props";
export interface DialogTriggerProps export type { DialogTriggerProps };
extends Omit<JSX.HTMLAttributes<HTMLButtonElement>, "children"> {
/** Render as a different element or component. */
as?: string | Component;
children?: JSX.Element | ((props: JSX.HTMLAttributes<HTMLElement>) => JSX.Element);
}
/** Opens the dialog when clicked. Supports polymorphic rendering via `as` and children-as-function. */ /** Opens the dialog when clicked. Supports polymorphic rendering via `as` and children-as-function. */
export function DialogTrigger(props: DialogTriggerProps): JSX.Element { export function DialogTrigger(props: DialogTriggerProps): JSX.Element {

View File

@ -0,0 +1,62 @@
// packages/core/src/components/dialog/dialog.props.ts
import { z } from "zod/v4";
import type { Component, JSX } from "solid-js";
import type { ComponentMeta } from "../../meta";
export const DialogRootPropsSchema = z.object({
open: z.boolean().optional().describe("Controlled open state"),
defaultOpen: z.boolean().optional().describe("Initial open state (uncontrolled)"),
modal: z.boolean().optional().describe("Whether to trap focus and add backdrop. Defaults to true"),
});
export interface DialogRootProps extends z.infer<typeof DialogRootPropsSchema> {
onOpenChange?: (open: boolean) => void;
children: JSX.Element;
}
export const DialogContentPropsSchema = z.object({
forceMount: z.boolean().optional().describe("Force mount content even when closed, useful for animations"),
});
export interface DialogContentProps
extends z.infer<typeof DialogContentPropsSchema>,
Omit<JSX.DialogHtmlAttributes<HTMLDialogElement>, keyof z.infer<typeof DialogContentPropsSchema>> {
onOpenAutoFocus?: (event: Event) => void;
onCloseAutoFocus?: (event: Event) => void;
children?: JSX.Element;
}
export interface DialogTriggerProps extends Omit<JSX.HTMLAttributes<HTMLButtonElement>, "children"> {
as?: string | Component;
children?: JSX.Element | ((props: JSX.HTMLAttributes<HTMLElement>) => JSX.Element);
}
export interface DialogTitleProps extends JSX.HTMLAttributes<HTMLHeadingElement> {
children?: JSX.Element;
}
export interface DialogDescriptionProps extends JSX.HTMLAttributes<HTMLParagraphElement> {
children?: JSX.Element;
}
export interface DialogCloseProps extends JSX.HTMLAttributes<HTMLButtonElement> {
as?: string | Component;
children?: JSX.Element;
}
export interface DialogOverlayProps extends JSX.HTMLAttributes<HTMLDivElement> {
forceMount?: boolean;
}
export interface DialogPortalProps {
/** Override the portal target container. */
target?: Element | null;
children: JSX.Element;
}
export const DialogMeta: ComponentMeta = {
name: "Dialog",
description: "Modal overlay that interrupts the user with important content requiring acknowledgment",
parts: ["Root", "Trigger", "Portal", "Overlay", "Content", "Title", "Description", "Close"] as const,
requiredParts: ["Root", "Content", "Title"] as const,
} as const;

View File

@ -1,4 +1,3 @@
// packages/core/src/components/dialog/index.ts
import { DialogClose } from "./dialog-close"; import { DialogClose } from "./dialog-close";
import { DialogContent } from "./dialog-content"; import { DialogContent } from "./dialog-content";
import { useDialogContext } from "./dialog-context"; import { useDialogContext } from "./dialog-context";
@ -8,23 +7,6 @@ import { DialogPortal } from "./dialog-portal";
import { DialogRoot } from "./dialog-root"; import { DialogRoot } from "./dialog-root";
import { DialogTitle } from "./dialog-title"; import { DialogTitle } from "./dialog-title";
import { DialogTrigger } from "./dialog-trigger"; import { DialogTrigger } from "./dialog-trigger";
export { DialogRootPropsSchema, DialogContentPropsSchema, DialogMeta } from "./dialog.props";
export const Dialog = Object.assign(DialogRoot, { export type { DialogRootProps, DialogContentProps, DialogTitleProps, DialogDescriptionProps, DialogTriggerProps, DialogCloseProps, DialogPortalProps, DialogOverlayProps } from "./dialog.props";
Content: DialogContent, export const Dialog = Object.assign(DialogRoot, { Content: DialogContent, Title: DialogTitle, Description: DialogDescription, Trigger: DialogTrigger, Close: DialogClose, Portal: DialogPortal, Overlay: DialogOverlay, useContext: useDialogContext });
Title: DialogTitle,
Description: DialogDescription,
Trigger: DialogTrigger,
Close: DialogClose,
Portal: DialogPortal,
Overlay: DialogOverlay,
useContext: useDialogContext,
});
export type { DialogRootProps } from "./dialog-root";
export type { DialogContentProps } from "./dialog-content";
export type { DialogTitleProps } from "./dialog-title";
export type { DialogDescriptionProps } from "./dialog-description";
export type { DialogTriggerProps } from "./dialog-trigger";
export type { DialogCloseProps } from "./dialog-close";
export type { DialogPortalProps } from "./dialog-portal";
export type { DialogOverlayProps } from "./dialog-overlay";

View File

@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import {
DialogRootPropsSchema,
DialogContentPropsSchema,
DialogMeta,
} from "../../../src/components/dialog/dialog.props";
describe("Dialog Zod schemas", () => {
it("validates correct root props", () => {
const result = DialogRootPropsSchema.safeParse({ open: true, modal: false });
expect(result.success).toBe(true);
});
it("validates empty root props (all optional)", () => {
const result = DialogRootPropsSchema.safeParse({});
expect(result.success).toBe(true);
});
it("rejects invalid root props", () => {
const result = DialogRootPropsSchema.safeParse({ open: "yes" });
expect(result.success).toBe(false);
});
it("validates content props", () => {
const result = DialogContentPropsSchema.safeParse({ forceMount: true });
expect(result.success).toBe(true);
});
it("exposes meta with required parts", () => {
expect(DialogMeta.name).toBe("Dialog");
expect(DialogMeta.parts).toContain("Root");
expect(DialogMeta.parts).toContain("Content");
expect(DialogMeta.parts).toContain("Title");
expect(DialogMeta.requiredParts).toContain("Root");
expect(DialogMeta.requiredParts).toContain("Content");
expect(DialogMeta.requiredParts).toContain("Title");
});
it("schema has descriptions on all fields", () => {
const shape = DialogRootPropsSchema.shape;
expect(shape.open.description).toBeDefined();
expect(shape.modal.description).toBeDefined();
});
});