Migrate overlay components to Zod props
This commit is contained in:
parent
c4547473a4
commit
6dd06986cc
@ -12,18 +12,8 @@ import {
|
||||
type InternalDrawerContextValue,
|
||||
} from "./drawer-context";
|
||||
|
||||
/** Props for the Drawer root component. */
|
||||
export interface DrawerRootProps {
|
||||
/** Controls open state externally. */
|
||||
open?: boolean;
|
||||
/** Initial open state when uncontrolled. */
|
||||
defaultOpen?: boolean;
|
||||
/** Called when open state changes. */
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
/** Which edge the drawer slides from. @default "right" */
|
||||
side?: DrawerSide;
|
||||
children: JSX.Element;
|
||||
}
|
||||
import type { DrawerRootProps } from "./drawer.props";
|
||||
export type { DrawerRootProps } from "./drawer.props";
|
||||
|
||||
/**
|
||||
* Root component for Drawer. Manages open state and provides context.
|
||||
|
||||
16
packages/core/src/components/drawer/drawer.props.ts
Normal file
16
packages/core/src/components/drawer/drawer.props.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { z } from "zod/v4";
|
||||
import type { JSX } from "solid-js";
|
||||
import type { ComponentMeta } from "../../meta";
|
||||
export const DrawerRootPropsSchema = z.object({
|
||||
open: z.boolean().optional(),
|
||||
defaultOpen: z.boolean().optional(),
|
||||
modal: z.boolean().optional(),
|
||||
side: z.enum(["left", "right", "top", "bottom"]).optional(),
|
||||
});
|
||||
export interface DrawerRootProps extends z.infer<typeof DrawerRootPropsSchema> { onOpenChange?: (open: boolean) => void; children: JSX.Element; }
|
||||
export const DrawerMeta: ComponentMeta = {
|
||||
name: "Drawer",
|
||||
description: "Panel that slides in from the edge of the screen, used for navigation or secondary content",
|
||||
parts: ["Root", "Trigger", "Portal", "Overlay", "Content", "Title", "Description", "Close"] as const,
|
||||
requiredParts: ["Root", "Content", "Title"] as const,
|
||||
} as const;
|
||||
@ -7,25 +7,7 @@ import { DrawerPortal } from "./drawer-portal";
|
||||
import { DrawerRoot } from "./drawer-root";
|
||||
import { DrawerTitle } from "./drawer-title";
|
||||
import { DrawerTrigger } from "./drawer-trigger";
|
||||
|
||||
/** Compound Drawer component with sub-components attached as properties. */
|
||||
export const Drawer = Object.assign(DrawerRoot, {
|
||||
Content: DrawerContent,
|
||||
Title: DrawerTitle,
|
||||
Description: DrawerDescription,
|
||||
Trigger: DrawerTrigger,
|
||||
Close: DrawerClose,
|
||||
Portal: DrawerPortal,
|
||||
Overlay: DrawerOverlay,
|
||||
useContext: useDrawerContext,
|
||||
});
|
||||
|
||||
export type { DrawerRootProps } from "./drawer-root";
|
||||
export type { DrawerContentProps } from "./drawer-content";
|
||||
export type { DrawerTitleProps } from "./drawer-title";
|
||||
export type { DrawerDescriptionProps } from "./drawer-description";
|
||||
export type { DrawerTriggerProps } from "./drawer-trigger";
|
||||
export type { DrawerCloseProps } from "./drawer-close";
|
||||
export type { DrawerPortalProps } from "./drawer-portal";
|
||||
export type { DrawerOverlayProps } from "./drawer-overlay";
|
||||
export const Drawer = Object.assign(DrawerRoot, { Content: DrawerContent, Title: DrawerTitle, Description: DrawerDescription, Trigger: DrawerTrigger, Close: DrawerClose, Portal: DrawerPortal, Overlay: DrawerOverlay, useContext: useDrawerContext });
|
||||
export { DrawerRootPropsSchema, DrawerMeta } from "./drawer.props";
|
||||
export type { DrawerRootProps } from "./drawer.props";
|
||||
export type { DrawerContextValue, DrawerSide } from "./drawer-context";
|
||||
|
||||
@ -13,19 +13,8 @@ import {
|
||||
import type { InternalHoverCardContextValue } from "./hover-card-context";
|
||||
import { HoverCardTrigger } from "./hover-card-trigger";
|
||||
|
||||
export interface HoverCardRootProps {
|
||||
/** Controlled open state. */
|
||||
open?: boolean | undefined;
|
||||
/** Initial open state (uncontrolled). */
|
||||
defaultOpen?: boolean | undefined;
|
||||
/** Called when open state changes. */
|
||||
onOpenChange?: ((open: boolean) => void) | undefined;
|
||||
/** Delay in ms before opening. @default 700 */
|
||||
openDelay?: number | undefined;
|
||||
/** Delay in ms before closing. @default 300 */
|
||||
closeDelay?: number | undefined;
|
||||
children: JSX.Element;
|
||||
}
|
||||
import type { HoverCardRootProps } from "./hover-card.props";
|
||||
export type { HoverCardRootProps } from "./hover-card.props";
|
||||
|
||||
/**
|
||||
* Root component for HoverCard. Manages open state, floating
|
||||
|
||||
16
packages/core/src/components/hover-card/hover-card.props.ts
Normal file
16
packages/core/src/components/hover-card/hover-card.props.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { z } from "zod/v4";
|
||||
import type { JSX } from "solid-js";
|
||||
import type { ComponentMeta } from "../../meta";
|
||||
export const HoverCardRootPropsSchema = z.object({
|
||||
open: z.boolean().optional(),
|
||||
defaultOpen: z.boolean().optional(),
|
||||
openDelay: z.number().optional(),
|
||||
closeDelay: z.number().optional(),
|
||||
});
|
||||
export interface HoverCardRootProps extends z.infer<typeof HoverCardRootPropsSchema> { onOpenChange?: (open: boolean) => void; children: JSX.Element; }
|
||||
export const HoverCardMeta: ComponentMeta = {
|
||||
name: "HoverCard",
|
||||
description: "Card that appears on hover to preview linked content without navigating",
|
||||
parts: ["Root", "Trigger", "Portal", "Content", "Arrow"] as const,
|
||||
requiredParts: ["Root", "Trigger", "Content"] as const,
|
||||
} as const;
|
||||
@ -7,3 +7,4 @@ export type { HoverCardRootProps } from "./hover-card-root";
|
||||
export type { HoverCardTriggerProps } from "./hover-card-trigger";
|
||||
export type { HoverCardContentProps } from "./hover-card-content";
|
||||
export type { HoverCardContextValue } from "./hover-card-context";
|
||||
export { HoverCardRootPropsSchema, HoverCardMeta } from "./hover-card.props";
|
||||
|
||||
@ -10,3 +10,5 @@ export type { PopoverTriggerProps } from "./popover-trigger";
|
||||
export type { PopoverCloseProps } from "./popover-close";
|
||||
export type { PopoverPortalProps } from "./popover-portal";
|
||||
export const Popover = Object.assign(PopoverRoot, { Content: PopoverContent, Trigger: PopoverTrigger, Close: PopoverClose, Portal: PopoverPortal, useContext: usePopoverContext });
|
||||
export { PopoverRootPropsSchema, PopoverMeta } from "./popover.props";
|
||||
export type { PopoverRootProps } from "./popover.props";
|
||||
|
||||
@ -14,25 +14,8 @@ import {
|
||||
type InternalPopoverContextValue,
|
||||
} from "./popover-context";
|
||||
|
||||
export interface PopoverRootProps {
|
||||
/** Controlled open state. */
|
||||
open?: boolean | undefined;
|
||||
/** Default open state (uncontrolled). */
|
||||
defaultOpen?: boolean | undefined;
|
||||
/** Called when open state should change. */
|
||||
onOpenChange?: ((open: boolean) => void) | undefined;
|
||||
/**
|
||||
* Whether the popover blocks outside interaction and traps focus.
|
||||
* @default false
|
||||
*/
|
||||
modal?: boolean | undefined;
|
||||
/**
|
||||
* Floating UI placement.
|
||||
* @default "bottom"
|
||||
*/
|
||||
placement?: Placement | undefined;
|
||||
children: JSX.Element;
|
||||
}
|
||||
import type { PopoverRootProps } from "./popover.props";
|
||||
export type { PopoverRootProps } from "./popover.props";
|
||||
|
||||
/**
|
||||
* Root component for Popover. Manages open state, floating positioning,
|
||||
|
||||
15
packages/core/src/components/popover/popover.props.ts
Normal file
15
packages/core/src/components/popover/popover.props.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { z } from "zod/v4";
|
||||
import type { JSX } from "solid-js";
|
||||
import type { ComponentMeta } from "../../meta";
|
||||
export const PopoverRootPropsSchema = z.object({
|
||||
open: z.boolean().optional(),
|
||||
defaultOpen: z.boolean().optional(),
|
||||
modal: z.boolean().optional(),
|
||||
});
|
||||
export interface PopoverRootProps extends z.infer<typeof PopoverRootPropsSchema> { onOpenChange?: (open: boolean) => void; children: JSX.Element; }
|
||||
export const PopoverMeta: ComponentMeta = {
|
||||
name: "Popover",
|
||||
description: "Floating content panel anchored to a trigger element, for interactive content",
|
||||
parts: ["Root", "Trigger", "Portal", "Content", "Arrow", "Close", "Title", "Description"] as const,
|
||||
requiredParts: ["Root", "Trigger", "Content"] as const,
|
||||
} as const;
|
||||
@ -10,3 +10,4 @@ export const Toast = {
|
||||
|
||||
export type { ToastRegionProps } from "./toast-region";
|
||||
export type { ToastData, ToastType } from "./toast-store";
|
||||
export { ToastRegionPropsSchema, ToastMeta } from "./toast.props";
|
||||
|
||||
@ -2,13 +2,8 @@ import type { JSX } from "solid-js";
|
||||
import { For, splitProps } from "solid-js";
|
||||
import { useToastStore } from "./toast-store";
|
||||
|
||||
export interface ToastRegionProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
||||
/** Maximum visible toasts. @default 5 */
|
||||
limit?: number | undefined;
|
||||
/** ARIA label for the region. @default "Notifications" */
|
||||
label?: string | undefined;
|
||||
children?: JSX.Element | undefined;
|
||||
}
|
||||
import type { ToastRegionProps } from "./toast.props";
|
||||
export type { ToastRegionProps } from "./toast.props";
|
||||
|
||||
/** Region where toasts are rendered. Place once in your app root. */
|
||||
export function ToastRegion(props: ToastRegionProps): JSX.Element {
|
||||
|
||||
16
packages/core/src/components/toast/toast.props.ts
Normal file
16
packages/core/src/components/toast/toast.props.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { z } from "zod/v4";
|
||||
import type { JSX } from "solid-js";
|
||||
import type { ComponentMeta } from "../../meta";
|
||||
export const ToastRegionPropsSchema = z.object({
|
||||
placement: z.enum(["top-start", "top-center", "top-end", "bottom-start", "bottom-center", "bottom-end"]).optional(),
|
||||
duration: z.number().optional(),
|
||||
limit: z.number().optional(),
|
||||
swipeDirection: z.enum(["left", "right", "up", "down"]).optional(),
|
||||
});
|
||||
export interface ToastRegionProps extends z.infer<typeof ToastRegionPropsSchema>, JSX.HTMLAttributes<HTMLDivElement> { label?: string; children?: JSX.Element; }
|
||||
export const ToastMeta: ComponentMeta = {
|
||||
name: "Toast",
|
||||
description: "Temporary notification that auto-dismisses, with imperative toast.add() API for programmatic creation",
|
||||
parts: ["Region", "List", "Root", "Title", "Description", "Close", "Action"] as const,
|
||||
requiredParts: ["Region", "List"] as const,
|
||||
} as const;
|
||||
@ -2,3 +2,5 @@ export { Tooltip, TooltipRoot } from "./tooltip-root";
|
||||
export { TooltipTrigger } from "./tooltip-trigger";
|
||||
export { TooltipContent } from "./tooltip-content";
|
||||
export { useTooltipContext } from "./tooltip-context";
|
||||
export { TooltipRootPropsSchema, TooltipMeta } from "./tooltip.props";
|
||||
export type { TooltipRootProps } from "./tooltip.props";
|
||||
|
||||
@ -12,20 +12,8 @@ import {
|
||||
} from "./tooltip-context";
|
||||
import type { InternalTooltipContextValue } from "./tooltip-context";
|
||||
import { TooltipTrigger } from "./tooltip-trigger";
|
||||
|
||||
export interface TooltipRootProps {
|
||||
/** Controlled open state. */
|
||||
open?: boolean | undefined;
|
||||
/** Initial open state (uncontrolled). */
|
||||
defaultOpen?: boolean | undefined;
|
||||
/** Called when open state changes. */
|
||||
onOpenChange?: ((open: boolean) => void) | undefined;
|
||||
/** Delay in ms before the tooltip opens on hover. @default 700 */
|
||||
openDelay?: number | undefined;
|
||||
/** Delay in ms before the tooltip closes after leaving. @default 300 */
|
||||
closeDelay?: number | undefined;
|
||||
children: JSX.Element;
|
||||
}
|
||||
import type { TooltipRootProps } from "./tooltip.props";
|
||||
export type { TooltipRootProps } from "./tooltip.props";
|
||||
|
||||
/**
|
||||
* Root component for Tooltip. Manages open state, delay timers,
|
||||
|
||||
17
packages/core/src/components/tooltip/tooltip.props.ts
Normal file
17
packages/core/src/components/tooltip/tooltip.props.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { z } from "zod/v4";
|
||||
import type { JSX } from "solid-js";
|
||||
import type { ComponentMeta } from "../../meta";
|
||||
export const TooltipRootPropsSchema = z.object({
|
||||
open: z.boolean().optional(),
|
||||
defaultOpen: z.boolean().optional(),
|
||||
disabled: z.boolean().optional(),
|
||||
openDelay: z.number().optional(),
|
||||
closeDelay: z.number().optional(),
|
||||
});
|
||||
export interface TooltipRootProps extends z.infer<typeof TooltipRootPropsSchema> { onOpenChange?: (open: boolean) => void; children: JSX.Element; }
|
||||
export const TooltipMeta: ComponentMeta = {
|
||||
name: "Tooltip",
|
||||
description: "Floating label that appears on hover/focus to describe an element",
|
||||
parts: ["Root", "Trigger", "Portal", "Content", "Arrow"] as const,
|
||||
requiredParts: ["Root", "Trigger", "Content"] as const,
|
||||
} as const;
|
||||
@ -5,27 +5,42 @@ import { DropdownMenuRootPropsSchema, DropdownMenuMeta } from "../../src/compone
|
||||
import { ListboxRootPropsSchema, ListboxMeta } from "../../src/components/listbox/listbox.props";
|
||||
import { SeparatorPropsSchema, SeparatorMeta } from "../../src/components/separator/separator.props";
|
||||
import { PaginationRootPropsSchema, PaginationMeta } from "../../src/components/pagination/pagination.props";
|
||||
|
||||
describe("Collection component schemas", () => {
|
||||
it("Select validates", () => {
|
||||
expect(SelectRootPropsSchema.safeParse({ value: "opt1", placeholder: "Choose..." }).success).toBe(true);
|
||||
const result = SelectRootPropsSchema.safeParse({ value: "opt1", placeholder: "Choose..." });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("Combobox validates", () => {
|
||||
expect(ComboboxRootPropsSchema.safeParse({ inputValue: "search", allowCustomValue: true }).success).toBe(true);
|
||||
const result = ComboboxRootPropsSchema.safeParse({ inputValue: "search", allowCustomValue: true });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("DropdownMenu validates", () => {
|
||||
expect(DropdownMenuRootPropsSchema.safeParse({ open: true }).success).toBe(true);
|
||||
const result = DropdownMenuRootPropsSchema.safeParse({ open: true });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
it("Listbox validates", () => {
|
||||
expect(ListboxRootPropsSchema.safeParse({ value: "a" }).success).toBe(true);
|
||||
expect(ListboxRootPropsSchema.safeParse({ value: ["a", "b"], multiple: true }).success).toBe(true);
|
||||
|
||||
it("Listbox validates single and multiple", () => {
|
||||
const single = ListboxRootPropsSchema.safeParse({ value: "a" });
|
||||
const multi = ListboxRootPropsSchema.safeParse({ value: ["a", "b"], multiple: true });
|
||||
expect(single.success).toBe(true);
|
||||
expect(multi.success).toBe(true);
|
||||
});
|
||||
it("Separator validates", () => {
|
||||
expect(SeparatorPropsSchema.safeParse({ orientation: "vertical" }).success).toBe(true);
|
||||
expect(SeparatorPropsSchema.safeParse({ orientation: "angled" }).success).toBe(false);
|
||||
|
||||
it("Separator validates orientation enum", () => {
|
||||
const valid = SeparatorPropsSchema.safeParse({ orientation: "vertical" });
|
||||
const invalid = SeparatorPropsSchema.safeParse({ orientation: "angled" });
|
||||
expect(valid.success).toBe(true);
|
||||
expect(invalid.success).toBe(false);
|
||||
});
|
||||
|
||||
it("Pagination validates", () => {
|
||||
expect(PaginationRootPropsSchema.safeParse({ page: 3, count: 10, siblingCount: 2 }).success).toBe(true);
|
||||
const result = PaginationRootPropsSchema.safeParse({ page: 3, count: 10, siblingCount: 2 });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("all Meta objects valid", () => {
|
||||
const metas = [SelectMeta, ComboboxMeta, DropdownMenuMeta, ListboxMeta, SeparatorMeta, PaginationMeta];
|
||||
for (const meta of metas) {
|
||||
|
||||
28
packages/core/tests/schemas/overlay-components.test.ts
Normal file
28
packages/core/tests/schemas/overlay-components.test.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { TooltipRootPropsSchema, TooltipMeta } from "../../src/components/tooltip/tooltip.props";
|
||||
import { PopoverRootPropsSchema, PopoverMeta } from "../../src/components/popover/popover.props";
|
||||
import { HoverCardRootPropsSchema, HoverCardMeta } from "../../src/components/hover-card/hover-card.props";
|
||||
import { DrawerRootPropsSchema, DrawerMeta } from "../../src/components/drawer/drawer.props";
|
||||
import { ToastRegionPropsSchema, ToastMeta } from "../../src/components/toast/toast.props";
|
||||
const ok = (schema: { safeParse: (v: unknown) => { success: boolean } }, v: unknown) => schema.safeParse(v).success;
|
||||
describe("Overlay component schemas", () => {
|
||||
it("Tooltip validates delays", () => { expect(ok(TooltipRootPropsSchema, { openDelay: 500, closeDelay: 200 })).toBe(true); });
|
||||
it("Popover validates modal", () => { expect(ok(PopoverRootPropsSchema, { open: true, modal: true })).toBe(true); });
|
||||
it("HoverCard validates delays", () => { expect(ok(HoverCardRootPropsSchema, { openDelay: 300 })).toBe(true); });
|
||||
it("Drawer validates side enum", () => {
|
||||
expect(ok(DrawerRootPropsSchema, { side: "left" })).toBe(true);
|
||||
expect(ok(DrawerRootPropsSchema, { side: "center" })).toBe(false);
|
||||
});
|
||||
it("Toast validates placement", () => {
|
||||
expect(ok(ToastRegionPropsSchema, { placement: "top-center", duration: 3000, limit: 5 })).toBe(true);
|
||||
expect(ok(ToastRegionPropsSchema, { placement: "middle" })).toBe(false);
|
||||
});
|
||||
it("all Meta objects valid", () => {
|
||||
for (const meta of [TooltipMeta, PopoverMeta, HoverCardMeta, DrawerMeta, ToastMeta]) {
|
||||
expect(meta.name).toBeTruthy();
|
||||
expect(meta.description).toBeTruthy();
|
||||
expect(meta.parts.length).toBeGreaterThan(0);
|
||||
expect(meta.requiredParts.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user