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:
parent
f197c58296
commit
38ef3b0934
@ -1,13 +1,11 @@
|
||||
// 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 { Dynamic } from "solid-js/web";
|
||||
import { useInternalDialogContext } from "./dialog-context";
|
||||
import type { DialogCloseProps } from "./dialog.props";
|
||||
|
||||
export interface DialogCloseProps extends JSX.HTMLAttributes<HTMLButtonElement> {
|
||||
as?: string | Component;
|
||||
children?: JSX.Element;
|
||||
}
|
||||
export type { DialogCloseProps };
|
||||
|
||||
/** Closes the dialog when clicked. Supports polymorphic rendering via `as`. */
|
||||
export function DialogClose(props: DialogCloseProps): JSX.Element {
|
||||
|
||||
@ -5,20 +5,9 @@ import { createDismiss } from "../../utilities/dismiss/create-dismiss";
|
||||
import { createFocusTrap } from "../../utilities/focus-trap/create-focus-trap";
|
||||
import { createScrollLock } from "../../utilities/scroll-lock/create-scroll-lock";
|
||||
import { useInternalDialogContext } from "./dialog-context";
|
||||
import type { DialogContentProps } from "./dialog.props";
|
||||
|
||||
export interface DialogContentProps extends JSX.DialogHtmlAttributes<HTMLDialogElement> {
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
export type { DialogContentProps };
|
||||
|
||||
/**
|
||||
* Dialog content panel. Manages focus trap, scroll lock, and dismiss.
|
||||
|
||||
@ -2,10 +2,9 @@
|
||||
import type { JSX } from "solid-js";
|
||||
import { createUniqueId, onCleanup, onMount, splitProps } from "solid-js";
|
||||
import { useInternalDialogContext } from "./dialog-context";
|
||||
import type { DialogDescriptionProps } from "./dialog.props";
|
||||
|
||||
export interface DialogDescriptionProps extends JSX.HTMLAttributes<HTMLParagraphElement> {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
export type { DialogDescriptionProps };
|
||||
|
||||
/** Renders as p and registers its ID for aria-describedby on Dialog.Content. */
|
||||
export function DialogDescription(props: DialogDescriptionProps): JSX.Element {
|
||||
|
||||
@ -2,10 +2,9 @@
|
||||
import type { JSX } from "solid-js";
|
||||
import { Show, splitProps } from "solid-js";
|
||||
import { useInternalDialogContext } from "./dialog-context";
|
||||
import type { DialogOverlayProps } from "./dialog.props";
|
||||
|
||||
export interface DialogOverlayProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
||||
forceMount?: boolean;
|
||||
}
|
||||
export type { DialogOverlayProps };
|
||||
|
||||
/** Semi-transparent overlay rendered behind dialog content. */
|
||||
export function DialogOverlay(props: DialogOverlayProps): JSX.Element {
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
// packages/core/src/components/dialog/dialog-portal.tsx
|
||||
import type { JSX } from "solid-js";
|
||||
import { Portal } from "../../utilities/portal/portal";
|
||||
import type { DialogPortalProps } from "./dialog.props";
|
||||
|
||||
export interface DialogPortalProps {
|
||||
/** Override the portal target container. */
|
||||
target?: Element | null;
|
||||
children: JSX.Element;
|
||||
}
|
||||
export type { DialogPortalProps };
|
||||
|
||||
/** Renders children into a portal (defaults to document.body). */
|
||||
export function DialogPortal(props: DialogPortalProps): JSX.Element {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// packages/core/src/components/dialog/dialog-root.tsx
|
||||
import type { JSX } from "solid-js";
|
||||
import { createUniqueId } from "solid-js";
|
||||
import type { JSX } from "solid-js";
|
||||
import {
|
||||
type CreateDisclosureStateOptions,
|
||||
createDisclosureState,
|
||||
@ -11,21 +11,9 @@ import {
|
||||
InternalDialogContextProvider,
|
||||
type InternalDialogContextValue,
|
||||
} from "./dialog-context";
|
||||
import type { DialogRootProps } from "./dialog.props";
|
||||
|
||||
export interface 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;
|
||||
}
|
||||
export type { DialogRootProps };
|
||||
|
||||
/**
|
||||
* Root component. Manages open state, provides context to all Dialog parts.
|
||||
|
||||
@ -2,10 +2,9 @@
|
||||
import type { JSX } from "solid-js";
|
||||
import { createUniqueId, onCleanup, onMount, splitProps } from "solid-js";
|
||||
import { useInternalDialogContext } from "./dialog-context";
|
||||
import type { DialogTitleProps } from "./dialog.props";
|
||||
|
||||
export interface DialogTitleProps extends JSX.HTMLAttributes<HTMLHeadingElement> {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
export type { DialogTitleProps };
|
||||
|
||||
/** Renders as h2 and registers its ID for aria-labelledby on Dialog.Content. */
|
||||
export function DialogTitle(props: DialogTitleProps): JSX.Element {
|
||||
|
||||
@ -1,15 +1,11 @@
|
||||
// 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 { Dynamic } from "solid-js/web";
|
||||
import { useInternalDialogContext } from "./dialog-context";
|
||||
import type { DialogTriggerProps } from "./dialog.props";
|
||||
|
||||
export interface 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);
|
||||
}
|
||||
export type { DialogTriggerProps };
|
||||
|
||||
/** Opens the dialog when clicked. Supports polymorphic rendering via `as` and children-as-function. */
|
||||
export function DialogTrigger(props: DialogTriggerProps): JSX.Element {
|
||||
|
||||
62
packages/core/src/components/dialog/dialog.props.ts
Normal file
62
packages/core/src/components/dialog/dialog.props.ts
Normal 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;
|
||||
@ -1,4 +1,3 @@
|
||||
// packages/core/src/components/dialog/index.ts
|
||||
import { DialogClose } from "./dialog-close";
|
||||
import { DialogContent } from "./dialog-content";
|
||||
import { useDialogContext } from "./dialog-context";
|
||||
@ -8,23 +7,6 @@ import { DialogPortal } from "./dialog-portal";
|
||||
import { DialogRoot } from "./dialog-root";
|
||||
import { DialogTitle } from "./dialog-title";
|
||||
import { DialogTrigger } from "./dialog-trigger";
|
||||
|
||||
export const Dialog = Object.assign(DialogRoot, {
|
||||
Content: DialogContent,
|
||||
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";
|
||||
export { DialogRootPropsSchema, DialogContentPropsSchema, DialogMeta } from "./dialog.props";
|
||||
export type { DialogRootProps, DialogContentProps, DialogTitleProps, DialogDescriptionProps, DialogTriggerProps, DialogCloseProps, DialogPortalProps, DialogOverlayProps } from "./dialog.props";
|
||||
export const Dialog = Object.assign(DialogRoot, { Content: DialogContent, Title: DialogTitle, Description: DialogDescription, Trigger: DialogTrigger, Close: DialogClose, Portal: DialogPortal, Overlay: DialogOverlay, useContext: useDialogContext });
|
||||
|
||||
44
packages/core/tests/components/dialog/dialog-props.test.ts
Normal file
44
packages/core/tests/components/dialog/dialog-props.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user