diff --git a/libs/react/src/components/dialog/dialog.test.tsx b/libs/react/src/components/dialog/dialog.test.tsx new file mode 100644 index 0000000..b9d64c0 --- /dev/null +++ b/libs/react/src/components/dialog/dialog.test.tsx @@ -0,0 +1,653 @@ +import { act, render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import React from "react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { Dialog } from "./dialog"; + +describe("Dialog Component", () => { + beforeEach(() => { + vi.useFakeTimers({ shouldAdvanceTime: true }); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + describe("Basic Rendering", () => { + it("renders the trigger element", () => { + render( + Open}> +

Content

+
, + ); + + expect(screen.getByRole("button", { name: "Open" })).toBeInTheDocument(); + }); + + it("does not render dialog content when closed", () => { + render( + Open}> +

Content

+
, + ); + + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + + it("renders dialog content when defaultOpen is true", async () => { + render( + Open}> +

Content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + expect(screen.getByRole("dialog")).toBeInTheDocument(); + expect(screen.getByRole("dialog")).toHaveTextContent("Content"); + }); + + it("renders with data-ck='dialog' attribute", async () => { + const { baseElement } = render( + Open}> +

Content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + const dialog = baseElement.querySelector('[data-ck="dialog"]'); + expect(dialog).toBeInTheDocument(); + }); + + it("renders with data-ck='dialog-overlay' attribute", async () => { + const { baseElement } = render( + Open}> +

Content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + const overlay = baseElement.querySelector('[data-ck="dialog-overlay"]'); + expect(overlay).toBeInTheDocument(); + }); + + it("renders with data-variant attribute", async () => { + const { baseElement } = render( + Open} + variantName="danger" + > +

Content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + const dialog = baseElement.querySelector('[data-ck="dialog"]'); + expect(dialog).toHaveAttribute("data-variant", "danger"); + }); + + it("sets data-state='open' when open", async () => { + const { baseElement } = render( + Open}> +

Content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + const dialog = baseElement.querySelector('[data-ck="dialog"]'); + expect(dialog).toHaveAttribute("data-state", "open"); + + const overlay = baseElement.querySelector('[data-ck="dialog-overlay"]'); + expect(overlay).toHaveAttribute("data-state", "open"); + }); + + it("has correct displayName", () => { + expect( + (Dialog as unknown as { displayName?: string }).displayName, + ).toBe("Dialog"); + }); + }); + + describe("Trigger Interaction", () => { + it("opens on trigger click", async () => { + const { baseElement } = render( + Open}> +

Content

+
, + ); + + const trigger = screen.getByRole("button", { name: "Open" }); + + await act(async () => { + await userEvent.click(trigger); + vi.advanceTimersByTime(100); + }); + + await waitFor(() => { + const dialog = baseElement.querySelector('[data-ck="dialog"]'); + expect(dialog).toBeInTheDocument(); + expect(dialog).toHaveAttribute("data-state", "open"); + }); + }); + + it("closes on second trigger click", async () => { + const { baseElement } = render( + Open}> +

Content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + expect(screen.getByRole("dialog")).toBeInTheDocument(); + + // When dialog is open modally, trigger gets aria-hidden from FloatingFocusManager, + // so we query by text instead of role + const trigger = baseElement.querySelector("button") as HTMLElement; + + await act(async () => { + await userEvent.click(trigger); + }); + + await act(async () => { + vi.advanceTimersByTime(300); + }); + + await waitFor(() => { + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + }); + }); + + describe("Controlled Mode", () => { + it("opens when open prop is true", async () => { + render( + +

Content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + expect(screen.getByRole("dialog")).toBeInTheDocument(); + }); + + it("stays closed when open prop is false", () => { + render( + +

Content

+
, + ); + + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + + it("calls onOpenChange when trigger click toggles", async () => { + const onOpenChange = vi.fn(); + + render( + Open} + onOpenChange={onOpenChange} + > +

Content

+
, + ); + + const trigger = screen.getByRole("button", { name: "Open" }); + + await act(async () => { + await userEvent.click(trigger); + vi.advanceTimersByTime(100); + }); + + expect(onOpenChange).toHaveBeenCalledWith(true); + }); + + it("works without a trigger (controlled only)", async () => { + render( + +

Controlled content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + expect(screen.getByRole("dialog")).toHaveTextContent( + "Controlled content", + ); + }); + }); + + describe("Dismiss", () => { + it("closes on Escape key", async () => { + render( + Open}> +

Content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + expect(screen.getByRole("dialog")).toBeInTheDocument(); + + await act(async () => { + await userEvent.keyboard("{Escape}"); + }); + + await act(async () => { + vi.advanceTimersByTime(300); + }); + + await waitFor(() => { + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + }); + + it("does not close on Escape when closeOnEscape is false", async () => { + const { baseElement } = render( + Open} + > +

Content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + expect(screen.getByRole("dialog")).toBeInTheDocument(); + + await act(async () => { + await userEvent.keyboard("{Escape}"); + }); + + await act(async () => { + vi.advanceTimersByTime(300); + }); + + const dialog = baseElement.querySelector('[data-ck="dialog"]'); + expect(dialog).toBeInTheDocument(); + }); + + it("closes on overlay click", async () => { + const { baseElement } = render( + Open}> +

Content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + expect(screen.getByRole("dialog")).toBeInTheDocument(); + + const overlay = baseElement.querySelector( + '[data-ck="dialog-overlay"]', + ) as HTMLElement; + + await act(async () => { + await userEvent.click(overlay); + }); + + await act(async () => { + vi.advanceTimersByTime(300); + }); + + await waitFor(() => { + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + }); + + it("does not close on overlay click when closeOnOverlayClick is false", async () => { + const { baseElement } = render( + Open} + > +

Content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + expect(screen.getByRole("dialog")).toBeInTheDocument(); + + const overlay = baseElement.querySelector( + '[data-ck="dialog-overlay"]', + ) as HTMLElement; + + await act(async () => { + await userEvent.click(overlay); + }); + + await act(async () => { + vi.advanceTimersByTime(300); + }); + + const dialog = baseElement.querySelector('[data-ck="dialog"]'); + expect(dialog).toBeInTheDocument(); + }); + + it("does not close when clicking dialog content (not overlay)", async () => { + const { baseElement } = render( + Open}> +

Content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + const dialog = baseElement.querySelector( + '[data-ck="dialog"]', + ) as HTMLElement; + + await act(async () => { + await userEvent.click(dialog); + }); + + await act(async () => { + vi.advanceTimersByTime(300); + }); + + expect( + baseElement.querySelector('[data-ck="dialog"]'), + ).toBeInTheDocument(); + }); + }); + + describe("Content", () => { + it("renders children content", async () => { + render( + Open}> +

Dialog Title

+

Dialog body text

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + expect(screen.getByText("Dialog Title")).toBeInTheDocument(); + expect(screen.getByText("Dialog body text")).toBeInTheDocument(); + }); + + it("renders complex ReactNode children", async () => { + render( + Open}> +
+ + +
+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + expect(screen.getByTestId("dialog-form")).toBeInTheDocument(); + expect(screen.getByPlaceholderText("Name")).toBeInTheDocument(); + }); + }); + + describe("Accessibility", () => { + it("has role='dialog' by default", async () => { + render( + Open}> +

Content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + expect(screen.getByRole("dialog")).toBeInTheDocument(); + }); + + it("supports role='alertdialog'", async () => { + render( + Open} + > +

Content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + expect(screen.getByRole("alertdialog")).toBeInTheDocument(); + }); + + it("has aria-modal='true'", async () => { + render( + Open}> +

Content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + const dialog = screen.getByRole("dialog"); + expect(dialog).toHaveAttribute("aria-modal", "true"); + }); + + it("trigger has aria-haspopup attribute", () => { + render( + Open}> +

Content

+
, + ); + + const trigger = screen.getByRole("button", { name: "Open" }); + expect(trigger).toHaveAttribute("aria-haspopup", "dialog"); + }); + + it("trigger has aria-expanded when dialog is open", async () => { + const { baseElement } = render( + Open}> +

Content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + // When dialog is open modally, trigger gets aria-hidden from FloatingFocusManager, + // so we query the DOM directly + const trigger = baseElement.querySelector("button") as HTMLElement; + expect(trigger).toHaveAttribute("aria-expanded", "true"); + }); + + it("uses auto-generated id when not provided", async () => { + render( + Open}> +

Content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + const dialog = screen.getByRole("dialog"); + expect(dialog).toHaveAttribute("id"); + expect(dialog.id).toMatch(/^ck-dialog-/); + }); + + it("uses provided id", async () => { + render( + Open} + > +

Content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + const dialog = screen.getByRole("dialog"); + expect(dialog).toHaveAttribute("id", "my-dialog"); + }); + }); + + describe("forceMount", () => { + it("keeps content mounted when closed (forceMount=true)", () => { + const { baseElement } = render( + Open}> +

Content

+
, + ); + + const dialog = baseElement.querySelector('[data-ck="dialog"]'); + expect(dialog).toBeInTheDocument(); + expect(dialog).toHaveAttribute("data-state", "closed"); + }); + + it("applies visibility:hidden when closed and forceMount", () => { + const { baseElement } = render( + Open}> +

Content

+
, + ); + + const overlay = baseElement.querySelector('[data-ck="dialog-overlay"]'); + expect(overlay).toHaveStyle({ visibility: "hidden" }); + }); + }); + + describe("Ref Forwarding", () => { + it("forwards ref to the dialog content element", async () => { + const ref = React.createRef(); + + render( + Open} + ref={ref} + > +

Content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + expect(ref.current).toBeInstanceOf(HTMLDivElement); + expect(ref.current?.getAttribute("data-ck")).toBe("dialog"); + }); + }); + + describe("className", () => { + it("applies className to the dialog content", async () => { + const { baseElement } = render( + Open} + > +

Content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + const dialog = baseElement.querySelector('[data-ck="dialog"]'); + expect(dialog).toHaveClass("custom-dialog"); + }); + + it("applies overlayClassName to the overlay", async () => { + const { baseElement } = render( + Open} + > +

Content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + const overlay = baseElement.querySelector('[data-ck="dialog-overlay"]'); + expect(overlay).toHaveClass("custom-overlay"); + }); + }); + + describe("Portal", () => { + it("renders inline when portal is false", async () => { + const { container } = render( + Open}> +

Content

+
, + ); + + await act(async () => { + vi.advanceTimersByTime(100); + }); + + const dialog = container.querySelector('[data-ck="dialog"]'); + expect(dialog).toBeInTheDocument(); + }); + }); +}); diff --git a/libs/react/src/components/dialog/dialog.tsx b/libs/react/src/components/dialog/dialog.tsx new file mode 100644 index 0000000..c0aa03b --- /dev/null +++ b/libs/react/src/components/dialog/dialog.tsx @@ -0,0 +1,349 @@ +"use client"; + +import { + FloatingFocusManager, + FloatingOverlay, + FloatingPortal, +} from "@floating-ui/react"; +import { + cloneElement, + type CSSProperties, + forwardRef, + isValidElement, + type ReactElement, + type ReactNode, + type RefObject, + useCallback, + useId, + useRef, +} from "react"; + +import type { VariantFor } from "../../types/register"; + +import { useExitTransition } from "../../hooks"; +import { mergeRefs } from "../../utils/merge-refs"; +import { useDialog } from "./use-dialog"; + +// --------------------------------------------------------------------------- +// Dialog +// --------------------------------------------------------------------------- + +/** + * Props for the Dialog component. + */ +export interface DialogProps { + /** + * Dialog body content. + */ + children: ReactNode; + + /** + * Additional className on the dialog content panel. + */ + className?: string; + + /** + * Close on Escape key press. + * @default true + */ + closeOnEscape?: boolean; + + /** + * Close when clicking the overlay backdrop. + * @default true + */ + closeOnOverlayClick?: boolean; + + /** + * Default open state for uncontrolled mode. + * @default false + */ + defaultOpen?: boolean; + + /** + * Exit animation duration in ms (fallback timeout for CSS animations). + * @default 150 + */ + exitDuration?: number; + + /** + * Keep content mounted when closed. Useful for animation libraries + * that manage their own enter/exit animations. + * Content is rendered with `data-state="closed"` and `visibility: hidden`. + * @default false + */ + forceMount?: boolean; + + /** + * ID for the dialog element. Auto-generated if not provided. + */ + id?: string; + + /** + * Initial focus target when the dialog opens. + * - `number`: index of the tabbable element (0 = first, -1 = floating element itself) + * - `RefObject`: ref to the element to focus + * @default 0 + */ + initialFocus?: number | RefObject; + + /** + * Callback when open state changes. Used with `open` for controlled mode. + */ + onOpenChange?: (open: boolean) => void; + + /** + * Controlled open state. Use with `onOpenChange`. + */ + open?: boolean; + + /** + * Additional className on the overlay backdrop. + */ + overlayClassName?: string; + + /** + * Whether to render the dialog in a portal. Prevents overflow clipping. + * @default true + */ + portal?: boolean; + + /** + * Explicit portal root element. Defaults to `document.body`. + */ + portalRoot?: HTMLElement | null; + + /** + * Whether to return focus to the trigger when the dialog closes. + * @default true + */ + returnFocus?: boolean; + + /** + * ARIA role for the dialog. + * - `"dialog"`: standard dialog + * - `"alertdialog"`: requires explicit user action to dismiss + * @default "dialog" + */ + role?: "alertdialog" | "dialog"; + + /** + * Optional trigger element. When provided, clicking it toggles the dialog. + * Must accept a ref. + */ + trigger?: ReactElement; + + /** + * Variant name for the `data-variant` attribute. + */ + variantName?: VariantFor<"dialog">; +} + +/** + * A headless, accessible Dialog (modal) component. + * + * @description + * Provides a full-screen overlay modal for focused interactions, powered by + * `@floating-ui/react` for focus management and accessibility. + * + * ## Features + * - Free-form children — consumer controls all content structure + * - Optional trigger element or fully controlled mode + * - `role="dialog"` or `role="alertdialog"` for confirmations + * - Focus trapping via `FloatingFocusManager` + * - Scroll lock via `FloatingOverlay` + * - Configurable overlay click and Escape key dismiss + * - Portal rendering (on by default) + * - CSS exit animations via `data-state` attribute + * + * ## Keyboard Support + * + * | Key | Action | + * | --- | --- | + * | `Escape` | Close dialog (configurable) | + * | `Tab` | Navigate within dialog (focus trapped) | + * + * ## Accessibility + * + * Follows the WAI-ARIA Dialog (Modal) pattern: + * - `role="dialog"` or `role="alertdialog"` on the dialog element + * - `aria-modal="true"` for screen reader isolation + * - Focus is trapped within the dialog + * - Focus returns to trigger on close + * - `aria-haspopup` and `aria-expanded` on trigger (when provided) + * + * ## Data Attributes + * + * | Attribute | Values | Description | + * | --- | --- | --- | + * | `data-ck` | `"dialog"` | Content panel identifier | + * | `data-ck` | `"dialog-overlay"` | Overlay backdrop identifier | + * | `data-state` | `"open"` \| `"closed"` | Animation state | + * | `data-variant` | string | User-defined variant | + * + * @example + * ```tsx + * // With trigger (uncontrolled) + * Open}> + *

Settings

+ *

Configure your preferences.

+ *
+ * ``` + * + * @example + * ```tsx + * // Controlled (no trigger) + * + *

Confirm

+ * + *
+ * ``` + * + * @example + * ```tsx + * // Alert dialog + * Delete}> + *

Are you sure?

+ * + * + *
+ * ``` + */ +const Dialog = forwardRef( + ( + { + children, + className, + closeOnEscape, + closeOnOverlayClick = true, + defaultOpen, + exitDuration = 150, + forceMount = false, + id: externalId, + initialFocus = 0, + onOpenChange, + open, + overlayClassName, + portal = true, + portalRoot, + returnFocus = true, + role: dialogRole, + trigger, + variantName, + }, + ref, + ) => { + const autoId = useId(); + const dialogId = externalId ?? `ck-dialog-${autoId}`; + const contentNodeRef = useRef(null); + + const dialog = useDialog({ + closeOnEscape, + defaultOpen, + hasTrigger: trigger !== undefined, + onOpenChange, + open, + role: dialogRole, + }); + + const { dataState, isMounted } = useExitTransition({ + animateOnMount: true, + duration: exitDuration, + isOpen: dialog.isOpen, + nodeRef: contentNodeRef, + }); + + const shouldRender = forceMount || isMounted; + + const handleOverlayClick = useCallback( + (e: React.MouseEvent) => { + if (closeOnOverlayClick && e.target === e.currentTarget) { + dialog.setIsOpen(false); + } + }, + [closeOnOverlayClick, dialog], + ); + + // Clone the trigger element and merge interaction props + ref + const triggerElement = + trigger !== undefined && isValidElement(trigger) + ? cloneElement( + trigger as ReactElement>, + dialog.getReferenceProps({ + ref: mergeRefs( + dialog.referenceRef, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (trigger as any).ref, + ), + }), + ) + : null; + + const dialogContent = shouldRender ? ( + +
( + dialog.floatingRef, + contentNodeRef, + ref, + )} + > + {children} +
+
+ ) : null; + + const wrappedContent = + dialogContent !== null && isMounted ? ( + + {dialogContent} + + ) : ( + dialogContent + ); + + return ( + <> + {triggerElement} + {portal ? ( + + {wrappedContent} + + ) : ( + wrappedContent + )} + + ); + }, +); + +Dialog.displayName = "Dialog"; + +export { Dialog }; diff --git a/libs/react/src/components/dialog/index.ts b/libs/react/src/components/dialog/index.ts new file mode 100644 index 0000000..672b593 --- /dev/null +++ b/libs/react/src/components/dialog/index.ts @@ -0,0 +1,2 @@ +export * from "./dialog"; +export * from "./use-dialog"; diff --git a/libs/react/src/components/dialog/use-dialog.ts b/libs/react/src/components/dialog/use-dialog.ts new file mode 100644 index 0000000..a6fea50 --- /dev/null +++ b/libs/react/src/components/dialog/use-dialog.ts @@ -0,0 +1,182 @@ +"use client"; + +import { + useClick, + useDismiss, + useFloating, + useInteractions, + useRole, +} from "@floating-ui/react"; +import { useCallback, useState } from "react"; + +/** + * Configuration options for the Dialog positioning and interaction hook. + * + * @remarks + * This hook wraps `@floating-ui/react` directly (not via the generic `useFloating` wrapper) + * because dialog interaction hooks (`useClick`, `useDismiss`, `useRole`) + * require the full `FloatingContext` with `open`/`onOpenChange` wired in. + * + * Unlike `usePopover` and `useTooltip`, this hook uses no positioning middleware — + * Dialog is centered via CSS, not Floating UI positioning. + */ +export interface UseDialogOptions { + /** + * Close on Escape key press. + * @default true + */ + closeOnEscape?: boolean; + + /** + * Default open state for uncontrolled mode. + * @default false + */ + defaultOpen?: boolean; + + /** + * Whether a trigger element is provided (enables `useClick`). + * @default false + */ + hasTrigger?: boolean; + + /** + * Callback when open state changes. Used for controlled mode. + */ + onOpenChange?: (open: boolean) => void; + + /** + * Controlled open state. + */ + open?: boolean; + + /** + * ARIA role for the dialog. + * - `"dialog"`: standard dialog + * - `"alertdialog"`: requires explicit user action to dismiss + * @default "dialog" + */ + role?: "alertdialog" | "dialog"; +} + +/** + * Return type for the useDialog hook. + */ +export interface UseDialogReturn { + /** + * Floating UI context. Required by `FloatingFocusManager`. + */ + context: ReturnType["context"]; + + /** + * Ref setter for the floating (dialog content) element. + */ + floatingRef: (node: HTMLElement | null) => void; + + /** + * Returns props to spread on the floating (dialog) element. + */ + getFloatingProps: ( + userProps?: React.HTMLProps, + ) => Record; + + /** + * Returns props to spread on the reference (trigger) element. + */ + getReferenceProps: ( + userProps?: React.HTMLProps, + ) => Record; + + /** + * Whether the dialog is currently open. + */ + isOpen: boolean; + + /** + * Ref setter for the reference (trigger) element. + */ + referenceRef: (node: HTMLElement | null) => void; + + /** + * Set the open state programmatically. + */ + setIsOpen: (open: boolean) => void; +} + +/** + * Core hook for Dialog interaction management. + * + * @description + * Wraps `@floating-ui/react` with dialog-specific interactions: + * - Optional click trigger to toggle open/close + * - Dismiss on Escape key + * - Dialog or alertdialog ARIA role + * + * Unlike tooltip/popover hooks, this hook uses no positioning middleware — + * Dialog is centered via CSS, not Floating UI positioning. + * + * @remarks + * Uses `@floating-ui/react`'s `useFloating` directly because the interaction hooks + * require the full `FloatingContext` with `open`/`onOpenChange`. + * + * @see {@link UseDialogOptions} for configuration + */ +export function useDialog(options: UseDialogOptions = {}): UseDialogReturn { + const { + closeOnEscape = true, + defaultOpen = false, + hasTrigger = false, + onOpenChange: controlledOnOpenChange, + open: controlledOpen, + role: dialogRole = "dialog", + } = options; + + const isControlled = controlledOpen !== undefined; + const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen); + const isOpen = isControlled ? controlledOpen : uncontrolledOpen; + + const setIsOpen = useCallback( + (nextOpen: boolean) => { + if (!isControlled) { + setUncontrolledOpen(nextOpen); + } + controlledOnOpenChange?.(nextOpen); + }, + [controlledOnOpenChange, isControlled], + ); + + const floating = useFloating({ + onOpenChange: setIsOpen, + open: isOpen, + }); + + const { context } = floating; + + const clickInteraction = useClick(context, { + enabled: hasTrigger, + }); + + const dismissInteraction = useDismiss(context, { + escapeKey: closeOnEscape, + outsidePress: false, + }); + + const roleInteraction = useRole(context, { + role: dialogRole, + }); + + const { getFloatingProps, getReferenceProps } = useInteractions([ + clickInteraction, + dismissInteraction, + roleInteraction, + ]); + + return { + context, + floatingRef: floating.refs.setFloating, + getFloatingProps, + getReferenceProps, + isOpen, + referenceRef: floating.refs.setReference, + setIsOpen, + }; +} diff --git a/libs/react/src/components/index.ts b/libs/react/src/components/index.ts index e31f8a5..bb1ca98 100644 --- a/libs/react/src/components/index.ts +++ b/libs/react/src/components/index.ts @@ -3,6 +3,7 @@ export * from "./badge"; export * from "./button"; export * from "./checkbox"; export * from "./combobox"; +export * from "./dialog"; export * from "./heading"; export * from "./icon"; export * from "./input";