diff --git a/packages/core/src/components/alert-dialog/alert-dialog-action.tsx b/packages/core/src/components/alert-dialog/alert-dialog-action.tsx new file mode 100644 index 0000000..9553b41 --- /dev/null +++ b/packages/core/src/components/alert-dialog/alert-dialog-action.tsx @@ -0,0 +1,25 @@ +import type { JSX } from "solid-js"; +import { splitProps } from "solid-js"; +import { useInternalAlertDialogContext } from "./alert-dialog-context"; + +/** Props for AlertDialog.Action. */ +export interface AlertDialogActionProps extends JSX.ButtonHTMLAttributes { + children?: JSX.Element; +} + +/** Confirms the action and closes the AlertDialog. */ +export function AlertDialogAction(props: AlertDialogActionProps): JSX.Element { + const [local, rest] = splitProps(props, ["children", "onClick"]); + const ctx = useInternalAlertDialogContext(); + + const handleClick: JSX.EventHandler = (e) => { + if (typeof local.onClick === "function") local.onClick(e); + ctx.setOpen(false); + }; + + return ( + + ); +} diff --git a/packages/core/src/components/alert-dialog/alert-dialog-cancel.tsx b/packages/core/src/components/alert-dialog/alert-dialog-cancel.tsx new file mode 100644 index 0000000..db50c08 --- /dev/null +++ b/packages/core/src/components/alert-dialog/alert-dialog-cancel.tsx @@ -0,0 +1,25 @@ +import type { JSX } from "solid-js"; +import { splitProps } from "solid-js"; +import { useInternalAlertDialogContext } from "./alert-dialog-context"; + +/** Props for AlertDialog.Cancel. */ +export interface AlertDialogCancelProps extends JSX.ButtonHTMLAttributes { + children?: JSX.Element; +} + +/** Cancels the action and closes the AlertDialog. */ +export function AlertDialogCancel(props: AlertDialogCancelProps): JSX.Element { + const [local, rest] = splitProps(props, ["children", "onClick"]); + const ctx = useInternalAlertDialogContext(); + + const handleClick: JSX.EventHandler = (e) => { + if (typeof local.onClick === "function") local.onClick(e); + ctx.setOpen(false); + }; + + return ( + + ); +} diff --git a/packages/core/src/components/alert-dialog/alert-dialog-content.tsx b/packages/core/src/components/alert-dialog/alert-dialog-content.tsx new file mode 100644 index 0000000..a63aafa --- /dev/null +++ b/packages/core/src/components/alert-dialog/alert-dialog-content.tsx @@ -0,0 +1,55 @@ +import type { JSX } from "solid-js"; +import { Show, createEffect, onCleanup, splitProps } from "solid-js"; +import { createFocusTrap } from "../../utilities/focus-trap/create-focus-trap"; +import { createScrollLock } from "../../utilities/scroll-lock/create-scroll-lock"; +import { useInternalAlertDialogContext } from "./alert-dialog-context"; + +/** Props for AlertDialog.Content. */ +export interface AlertDialogContentProps extends JSX.HTMLAttributes { + forceMount?: boolean; + children?: JSX.Element; +} + +/** + * AlertDialog content panel. Uses focus trap and scroll lock. + * Does NOT dismiss on Escape or outside pointer click (unlike Dialog). + */ +export function AlertDialogContent(props: AlertDialogContentProps): JSX.Element { + const [local, rest] = splitProps(props, ["children", "forceMount"]); + const ctx = useInternalAlertDialogContext(); + let contentRef: HTMLDivElement | undefined; + + const focusTrap = createFocusTrap(() => contentRef ?? null); + const scrollLock = createScrollLock(); + + createEffect(() => { + if (ctx.isOpen()) { + focusTrap.activate(); + scrollLock.lock(); + } else { + focusTrap.deactivate(); + scrollLock.unlock(); + } + onCleanup(() => { + focusTrap.deactivate(); + scrollLock.unlock(); + }); + }); + + return ( + +
+ {local.children} +
+
+ ); +} diff --git a/packages/core/src/components/alert-dialog/alert-dialog-context.ts b/packages/core/src/components/alert-dialog/alert-dialog-context.ts new file mode 100644 index 0000000..c57e0bf --- /dev/null +++ b/packages/core/src/components/alert-dialog/alert-dialog-context.ts @@ -0,0 +1,52 @@ +import type { Accessor } from "solid-js"; +import { createContext, useContext } from "solid-js"; + +/** Internal context shared between all AlertDialog parts. */ +export interface InternalAlertDialogContextValue { + isOpen: Accessor; + setOpen: (open: boolean) => void; + contentId: Accessor; + titleId: Accessor; + setTitleId: (id: string | undefined) => void; + descriptionId: Accessor; + setDescriptionId: (id: string | undefined) => void; +} + +const InternalAlertDialogContext = createContext(); + +/** + * Returns the internal AlertDialog context. Throws if used outside . + */ +export function useInternalAlertDialogContext(): InternalAlertDialogContextValue { + const ctx = useContext(InternalAlertDialogContext); + if (!ctx) { + throw new Error( + "[PettyUI] AlertDialog parts must be used inside .\n" + + " Fix: Wrap AlertDialog.Content, AlertDialog.Trigger, etc. inside .", + ); + } + return ctx; +} + +export const InternalAlertDialogContextProvider = InternalAlertDialogContext.Provider; + +/** Public context exposed to consumers via AlertDialog.useContext(). */ +export interface AlertDialogContextValue { + /** Whether the alert dialog is currently open. */ + open: Accessor; +} + +const AlertDialogContext = createContext(); + +/** + * Returns the public AlertDialog context. Throws if used outside . + */ +export function useAlertDialogContext(): AlertDialogContextValue { + const ctx = useContext(AlertDialogContext); + if (!ctx) { + throw new Error("[PettyUI] AlertDialog.useContext() called outside of ."); + } + return ctx; +} + +export const AlertDialogContextProvider = AlertDialogContext.Provider; diff --git a/packages/core/src/components/alert-dialog/alert-dialog-description.tsx b/packages/core/src/components/alert-dialog/alert-dialog-description.tsx new file mode 100644 index 0000000..2a9b571 --- /dev/null +++ b/packages/core/src/components/alert-dialog/alert-dialog-description.tsx @@ -0,0 +1,22 @@ +import type { JSX } from "solid-js"; +import { createUniqueId, onCleanup, onMount, splitProps } from "solid-js"; +import { useInternalAlertDialogContext } from "./alert-dialog-context"; + +/** Props for AlertDialog.Description. */ +export interface AlertDialogDescriptionProps extends JSX.HTMLAttributes { + children?: JSX.Element; +} + +/** Description for the AlertDialog. Registers its ID for aria-describedby. */ +export function AlertDialogDescription(props: AlertDialogDescriptionProps): JSX.Element { + const [local, rest] = splitProps(props, ["children"]); + const ctx = useInternalAlertDialogContext(); + const id = createUniqueId(); + onMount(() => ctx.setDescriptionId(id)); + onCleanup(() => ctx.setDescriptionId(undefined)); + return ( +

+ {local.children} +

+ ); +} diff --git a/packages/core/src/components/alert-dialog/alert-dialog-overlay.tsx b/packages/core/src/components/alert-dialog/alert-dialog-overlay.tsx new file mode 100644 index 0000000..a23c163 --- /dev/null +++ b/packages/core/src/components/alert-dialog/alert-dialog-overlay.tsx @@ -0,0 +1,19 @@ +import type { JSX } from "solid-js"; +import { Show, splitProps } from "solid-js"; +import { useInternalAlertDialogContext } from "./alert-dialog-context"; + +/** Props for AlertDialog.Overlay. */ +export interface AlertDialogOverlayProps extends JSX.HTMLAttributes { + forceMount?: boolean; +} + +/** Backdrop overlay behind AlertDialog content. */ +export function AlertDialogOverlay(props: AlertDialogOverlayProps): JSX.Element { + const [local, rest] = splitProps(props, ["forceMount"]); + const ctx = useInternalAlertDialogContext(); + return ( + +