diff --git a/apps/www/src/app/examples/page.tsx b/apps/www/src/app/examples/page.tsx index ba7352813..07a636ddb 100644 --- a/apps/www/src/app/examples/page.tsx +++ b/apps/www/src/app/examples/page.tsx @@ -1439,7 +1439,6 @@ const Page = () => { Dialog Title - This is the dialog content. diff --git a/apps/www/src/components/demo/demo-playground.tsx b/apps/www/src/components/demo/demo-playground.tsx index 62ea31e85..0fefbe869 100644 --- a/apps/www/src/components/demo/demo-playground.tsx +++ b/apps/www/src/components/demo/demo-playground.tsx @@ -100,7 +100,11 @@ export default function DemoPlayground({ <> - + diff --git a/apps/www/src/components/docs/search.tsx b/apps/www/src/components/docs/search.tsx index 3c35448fc..890b848c6 100644 --- a/apps/www/src/components/docs/search.tsx +++ b/apps/www/src/components/docs/search.tsx @@ -1,5 +1,4 @@ 'use client'; -import { getFileFromUrl, getFolderFromUrl } from '@/lib/utils'; import { Cross1Icon, ExclamationTriangleIcon, @@ -15,13 +14,14 @@ import { } from '@raystack/apsara'; import { cx } from 'class-variance-authority'; import { + flattenTree, type Item as PageItem, - Root, - flattenTree + Root } from 'fumadocs-core/page-tree'; import { useDocsSearch } from 'fumadocs-core/search/client'; import { useRouter } from 'next/navigation'; import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; +import { getFileFromUrl, getFolderFromUrl } from '@/lib/utils'; import styles from './search.module.css'; type Item = Omit & { @@ -134,10 +134,8 @@ export default function DocsSearch({ pageTree }: { pageTree: Root }) { return ( - - - - + }> + 1}> diff --git a/apps/www/src/components/playground/code-block-examples.tsx b/apps/www/src/components/playground/code-block-examples.tsx index 5e84c6004..823af5482 100644 --- a/apps/www/src/components/playground/code-block-examples.tsx +++ b/apps/www/src/components/playground/code-block-examples.tsx @@ -25,8 +25,8 @@ export function CodeBlockExamples() { {` - - + }> + Basic Dialog A simple dialog example - @@ -44,9 +43,7 @@ export function CodeBlockExamples() { - - - + Cancel} /> `} diff --git a/apps/www/src/components/playground/dialog-examples.tsx b/apps/www/src/components/playground/dialog-examples.tsx index 83e1ddc12..2cc1074fb 100644 --- a/apps/www/src/components/playground/dialog-examples.tsx +++ b/apps/www/src/components/playground/dialog-examples.tsx @@ -8,14 +8,14 @@ export function DialogExamples() { - - - + }>Dialog Custom Styled Dialog @@ -25,10 +25,10 @@ export function DialogExamples() { - - + }> + Open Dialog - + No Close Button This dialog doesn't show the close button. diff --git a/apps/www/src/content/docs/components/code-block/demo.ts b/apps/www/src/content/docs/components/code-block/demo.ts index bee5adfa7..04e4ef8de 100644 --- a/apps/www/src/content/docs/components/code-block/demo.ts +++ b/apps/www/src/content/docs/components/code-block/demo.ts @@ -11,8 +11,8 @@ const tsxCode = `{\`function add(a: number, b: number): number { }\`}`; const longCode = `{\` - - + }> + Basic Dialog > A simple dialog example - @@ -30,9 +29,7 @@ const longCode = `{\` - - - + Cancel} /> \`}`; diff --git a/apps/www/src/content/docs/components/dialog/demo.ts b/apps/www/src/content/docs/components/dialog/demo.ts index a343b2928..6757548a2 100644 --- a/apps/www/src/content/docs/components/dialog/demo.ts +++ b/apps/www/src/content/docs/components/dialog/demo.ts @@ -1,11 +1,11 @@ 'use client'; -export const getCode = (props: any) => { - const { title, description, ...rest } = props; +export const getCode = (props: { title?: string; description?: string }) => { + const { title, description } = props; return ` - - + }> + Basic Dialog { > ${title} - @@ -22,7 +21,7 @@ export const getCode = (props: any) => { - + Cancel} /> @@ -49,8 +48,8 @@ export const controlledDemo = { return ( - - + }> + Controlled Dialog @@ -69,14 +68,12 @@ export const customDemo = { type: 'code', code: ` - - + }> + Styled Dialog Custom Styled Dialog @@ -92,18 +89,15 @@ export const onlyHeaderDemo = { type: 'code', code: ` - - + }> + Only Header and Body Title - @@ -118,14 +112,12 @@ export const onlyFooterDemo = { type: 'code', code: ` - - + }> + Only Footer and Body Title @@ -134,9 +126,60 @@ export const onlyFooterDemo = { - + Close} /> ` }; + +export const nestedDemo = { + type: 'code', + code: ` + function NestedDialogExample() { + const [parentOpen, setParentOpen] = React.useState(false); + const [nestedOpen, setNestedOpen] = React.useState(false); + + return ( + <> + + }> + Open Parent Dialog + + + + Parent Dialog + + + + This is the parent dialog. Click the button below to open a nested dialog. + + + }> + Open Nested Dialog + + + + Nested Dialog + + + + This is a nested dialog. Notice how the parent dialog scales down + and becomes slightly transparent when this dialog is open. + + + + Close} /> + + + + + + Close Parent} /> + + + + + ); + }` +}; diff --git a/apps/www/src/content/docs/components/dialog/index.mdx b/apps/www/src/content/docs/components/dialog/index.mdx index ddb87622a..78463f578 100644 --- a/apps/www/src/content/docs/components/dialog/index.mdx +++ b/apps/www/src/content/docs/components/dialog/index.mdx @@ -4,7 +4,7 @@ description: A window overlaid on either the primary window or another dialog wi source: packages/raystack/components/dialog --- -import { playground, closeDemo, customDemo, controlledDemo, onlyHeaderDemo, onlyFooterDemo } from "./demo.ts"; +import { playground, closeDemo, customDemo, controlledDemo, onlyHeaderDemo, onlyFooterDemo, nestedDemo } from "./demo.ts"; @@ -79,6 +79,11 @@ Example with header and body. +### Nested Dialogs + +You can nest dialogs within one another. When a nested dialog opens, the parent dialog automatically scales down and becomes slightly transparent. The nested dialog's backdrop won't be rendered, allowing you to see the parent dialog behind it. + + ### Accessibility diff --git a/apps/www/src/content/docs/components/dialog/props.ts b/apps/www/src/content/docs/components/dialog/props.ts index 67a9ee7f2..2e517e8af 100644 --- a/apps/www/src/content/docs/components/dialog/props.ts +++ b/apps/www/src/content/docs/components/dialog/props.ts @@ -16,26 +16,28 @@ export interface DialogContentProps { /** Controls dialog width */ width?: string | number; - /** Enables backdrop blur effect */ - overlayBlur?: boolean; + /** + * Controls whether to show the close button + * @default true + */ + showCloseButton?: boolean; - /** Custom class for overlay styling */ - overlayClassName?: string; + /** + * Toggle nested dialog animation (scaling and translation) + * @default true + */ + showNestedAnimation?: boolean; - /** Custom styles for overlay */ - overlayStyle?: React.CSSProperties; + /** Overlay configuration including blur, className, and style */ + overlay?: { + blur?: boolean; + className?: string; + style?: React.CSSProperties; + forceRender?: boolean; + } & React.ComponentPropsWithoutRef<'div'>; /** Additional CSS class names */ className?: string; - - /** Position of the dialog */ - side?: 'top' | 'right' | 'bottom' | 'left'; - - /** Accessible label for the dialog */ - ariaLabel?: string; - - /** Detailed description for screen readers */ - ariaDescription?: string; } export interface DialogHeaderProps { diff --git a/apps/www/src/styles.css b/apps/www/src/styles.css index 68914c329..c3d2b6c94 100644 --- a/apps/www/src/styles.css +++ b/apps/www/src/styles.css @@ -1,5 +1,4 @@ * { - overscroll-behavior: none; box-sizing: border-box; } :root { diff --git a/packages/raystack/components/dialog/__tests__/dialog.test.tsx b/packages/raystack/components/dialog/__tests__/dialog.test.tsx index fbc396b0e..893707000 100644 --- a/packages/raystack/components/dialog/__tests__/dialog.test.tsx +++ b/packages/raystack/components/dialog/__tests__/dialog.test.tsx @@ -24,7 +24,6 @@ const BasicDialog = ({ {DIALOG_TITLE} - {DIALOG_DESCRIPTION} diff --git a/packages/raystack/components/dialog/dialog-content.tsx b/packages/raystack/components/dialog/dialog-content.tsx new file mode 100644 index 000000000..98c75eae2 --- /dev/null +++ b/packages/raystack/components/dialog/dialog-content.tsx @@ -0,0 +1,67 @@ +'use client'; + +import { Dialog as DialogPrimitive } from '@base-ui/react'; +import { cx } from 'class-variance-authority'; +import { type ElementRef, forwardRef } from 'react'; +import styles from './dialog.module.css'; +import { CloseButton } from './dialog-misc'; + +export interface DialogContentProps extends DialogPrimitive.Popup.Props { + showCloseButton?: boolean; + overlay?: DialogPrimitive.Backdrop.Props & { blur?: boolean }; + width?: string | number; + /** + * Toggles nested dialog animation (scaling and translation) + * `@default` true + */ + showNestedAnimation?: boolean; +} + +export const DialogContent = forwardRef< + ElementRef, + DialogContentProps +>( + ( + { + className, + children, + showCloseButton = true, + overlay, + width, + style, + showNestedAnimation = true, + ...props + }, + ref + ) => { + return ( + + + + + {children} + {showCloseButton && } + + + + ); + } +); + +DialogContent.displayName = 'Dialog.Content'; diff --git a/packages/raystack/components/dialog/dialog-misc.tsx b/packages/raystack/components/dialog/dialog-misc.tsx new file mode 100644 index 000000000..fd07ce9b7 --- /dev/null +++ b/packages/raystack/components/dialog/dialog-misc.tsx @@ -0,0 +1,101 @@ +'use client'; + +import { Dialog as DialogPrimitive } from '@base-ui/react'; +import { Cross1Icon } from '@radix-ui/react-icons'; +import { cx } from 'class-variance-authority'; +import { ComponentPropsWithoutRef, type ElementRef, forwardRef } from 'react'; +import { Flex } from '../flex'; +import styles from './dialog.module.css'; + +export const DialogHeader = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +DialogHeader.displayName = 'Dialog.Header'; + +export const DialogFooter = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +DialogFooter.displayName = 'Dialog.Footer'; + +export const DialogBody = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +DialogBody.displayName = 'Dialog.Body'; + +export const CloseButton = forwardRef< + ElementRef, + DialogPrimitive.Close.Props +>(({ className, ...props }, ref) => { + return ( + + + ); +}); + +CloseButton.displayName = 'Dialog.CloseButton'; + +export const DialogTitle = forwardRef< + ElementRef, + DialogPrimitive.Title.Props +>(({ className, ...props }, ref) => { + return ( + + ); +}); + +DialogTitle.displayName = 'Dialog.Title'; + +export const DialogDescription = forwardRef< + ElementRef, + DialogPrimitive.Description.Props +>(({ className, ...props }, ref) => { + return ( + + ); +}); + +DialogDescription.displayName = 'Dialog.Description'; diff --git a/packages/raystack/components/dialog/dialog.module.css b/packages/raystack/components/dialog/dialog.module.css index f2f4b0076..7cf020e82 100644 --- a/packages/raystack/components/dialog/dialog.module.css +++ b/packages/raystack/components/dialog/dialog.module.css @@ -2,28 +2,53 @@ background-color: var(--rs-color-overlay-base-primary); position: fixed; inset: 0; + transition: opacity 150ms cubic-bezier(0.45, 1.005, 0, 1.005); z-index: var(--rs-z-index-portal); } +.dialogOverlay[data-starting-style], +.dialogOverlay[data-ending-style] { + opacity: 0; +} + .dialogContent { background-color: var(--rs-color-background-base-primary); border-radius: var(--rs-radius-2); box-shadow: var(--rs-shadow-floating); - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 90vw; + max-width: 90vw; min-width: 200px; min-height: 100px; max-height: 85vh; padding: 0; - z-index: var(--rs-z-index-portal); + position: fixed; + top: 50%; + left: 50%; + transition: all 150ms; + transform: translate(-50%, -50%); +} + +.dialogContent.showNestedAnimation { + transform: translate(-50%, -50%) + scale(calc(1 - 0.1 * var(--nested-dialogs, 0))); + translate: 0 calc(0px + 1.25rem * var(--nested-dialogs, 0)); } .dialogContent:focus { outline: none; } +.dialogContent[data-nested-dialog-open]::after { + content: ""; + inset: 0; + position: absolute; + border-radius: inherit; + background-color: rgb(0 0 0 / 0.05); +} + +.dialogContent.showNestedAnimation[data-starting-style], +.dialogContent.showNestedAnimation[data-ending-style] { + opacity: 0; + transform: translate(-50%, -50%) scale(0.9); +} .close { all: unset; @@ -35,6 +60,12 @@ cursor: pointer; } +.closeButton { + position: absolute; + top: var(--rs-space-5); + right: var(--rs-space-7); +} + .close:hover { background-color: var(--rs-color-background-base-primary-hover); color: var(--rs-color-foreground-base-primary); @@ -44,15 +75,18 @@ backdrop-filter: var(--rs-blur-lg); } -.dialogOverlay[data-state="open"] { - animation: fadeIn 150ms cubic-bezier(0.22, 1, 0.36, 1); -} -.dialogOverlay[data-state="close"] { - animation: fadeOut 150ms cubic-bezier(0.22, 1, 0.36, 1); +.viewport { + display: flex; + align-items: center; + justify-content: center; + position: fixed; + inset: 0; + z-index: var(--rs-z-index-portal); } @media (prefers-reduced-motion: reduce) { - .overlay { + .dialogOverlay, + .dialogContent { animation: none; transition: none; } @@ -65,7 +99,6 @@ .title { color: var(--rs-color-foreground-base-primary); - /* Body/Large Plus */ font-size: var(--rs-font-size-large); font-style: normal; font-weight: var(--rs-font-weight-medium); diff --git a/packages/raystack/components/dialog/dialog.tsx b/packages/raystack/components/dialog/dialog.tsx index 08b1ee094..f8da8188e 100644 --- a/packages/raystack/components/dialog/dialog.tsx +++ b/packages/raystack/components/dialog/dialog.tsx @@ -1,194 +1,20 @@ -import { Cross1Icon } from '@radix-ui/react-icons'; -import { cva, cx, VariantProps } from 'class-variance-authority'; -import { Dialog as DialogPrimitive } from 'radix-ui'; +import { Dialog as DialogPrimitive } from '@base-ui/react'; +import { DialogContent } from './dialog-content'; import { - ComponentProps, - ComponentPropsWithoutRef, - ElementRef, - forwardRef -} from 'react'; -import { Flex } from '../flex'; -import styles from './dialog.module.css'; - -const dialogContent = cva(styles.dialogContent); - -export interface DialogContentProps - extends ComponentPropsWithoutRef, - VariantProps { - ariaLabel?: string; - ariaDescription?: string; - overlayBlur?: boolean; - overlayClassName?: string; - overlayStyle?: React.CSSProperties; - width?: string | number; - scrollableOverlay?: boolean; -} - -const DialogContent = forwardRef< - ElementRef, - DialogContentProps ->( - ( - { - className, - children, - ariaLabel, - ariaDescription, - overlayBlur = false, - overlayClassName, - overlayStyle, - width, - scrollableOverlay = false, - ...props - }, - ref - ) => { - const overlayProps: DialogPrimitive.DialogOverlayProps = { - className: cx( - styles.dialogOverlay, - overlayClassName, - overlayBlur && styles.overlayBlur - ), - style: overlayStyle, - 'aria-hidden': 'true', - role: 'presentation' - }; - - const content = ( - - {children} - - ); - return ( - - {scrollableOverlay ? ( - - {content} - - ) : ( - <> - - {content} - - )} - - ); - } -); - -DialogContent.displayName = DialogPrimitive.Content.displayName; - -const DialogHeader = ({ - children, - className -}: { - children: React.ReactNode; - className?: string; -}) => ( - - {children} - -); - -const DialogFooter = ({ - children, - className -}: { - children: React.ReactNode; - className?: string; -}) => ( - - {children} - -); - -const DialogBody = ({ - children, - className -}: { - children: React.ReactNode; - className?: string; -}) => ( - - {children} - -); - -type CloseButtonProps = ComponentProps; -export function CloseButton({ className, ...props }: CloseButtonProps) { - return ( - - - - ); -} - -interface DialogTitleProps - extends ComponentProps { - children: React.ReactNode; -} - -export function DialogTitle({ - children, - className, - ...props -}: DialogTitleProps) { - return ( - - {children} - - ); -} - -interface DialogDescriptionProps - extends ComponentProps { - children: React.ReactNode; - className?: string; -} - -export function DialogDescription({ - children, - className, - ...props -}: DialogDescriptionProps) { - return ( - - {children} - - ); -} + CloseButton, + DialogBody, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle +} from './dialog-misc'; export const Dialog = Object.assign(DialogPrimitive.Root, { - Trigger: DialogPrimitive.Trigger, - Content: DialogContent, Header: DialogHeader, Footer: DialogFooter, Body: DialogBody, + Trigger: DialogPrimitive.Trigger, + Content: DialogContent, Close: DialogPrimitive.Close, CloseButton: CloseButton, Title: DialogTitle, diff --git a/packages/raystack/components/sheet/__tests__/sheet.test.tsx b/packages/raystack/components/sheet/__tests__/sheet.test.tsx index a9b3e81df..f72c23035 100644 --- a/packages/raystack/components/sheet/__tests__/sheet.test.tsx +++ b/packages/raystack/components/sheet/__tests__/sheet.test.tsx @@ -278,16 +278,6 @@ describe('Sheet', () => { }); describe('Accessibility', () => { - // it('has correct ARIA roles', async () => { - // await renderAndOpenSheet(); - - // await waitFor(() => { - // const dialog = screen.getByRole('dialog'); - // expect(dialog).toBeInTheDocument(); - // expect(dialog).toHaveAttribute('tabIndex', '-1'); - // }); - // }); - it('has proper ARIA attributes', async () => { await renderAndOpenSheet(); diff --git a/packages/raystack/components/sheet/sheet.tsx b/packages/raystack/components/sheet/sheet.tsx index 06f0d5c25..6a0e19893 100644 --- a/packages/raystack/components/sheet/sheet.tsx +++ b/packages/raystack/components/sheet/sheet.tsx @@ -9,7 +9,6 @@ import { } from './sheet-misc'; export type { SheetContentProps } from './sheet-content'; -export type { SheetRootProps } from './sheet-root'; export const Sheet = Object.assign(DialogPrimitive.Root, { Trigger: DialogPrimitive.Trigger,