Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
290 changes: 253 additions & 37 deletions src/common/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,285 @@
import { PropsWithChildren, useEffect, useState } from 'react';
import { createContext, PropsWithChildren, ReactNode, useContext, useState } from 'react';
import noop from 'lodash/noop';
import { cva, VariantProps } from 'class-variance-authority';

import { cn } from 'common/utils/css';
import { BaseComponentProps } from 'common/utils/types';
import { cn } from 'common/utils/css';
import Backdrop from '../Backdrop/Backdrop';
import Divider, { DividerProps } from '../Divider/Divider';
import { default as CommonButton, ButtonProps as CommonButtonProps } from '../Button/Button';

/**
* Defines the properties of the `DialogContext` value.
*/
type DialogContextValue = {
isHidden: boolean;
setIsHidden: (isHidden: boolean) => void;
};

/**
* The `DialogContext` instance.
*/
const DialogContext = createContext<DialogContextValue>({
isHidden: true,
setIsHidden: noop,
});

/**
* Properties for the `Dialog` component.
* @param {boolean} [isOpen] - Indicates if the Dialog should be displayed.
* @param {function} [onClose] - A function called when the Dialog closes.
* @see {@link BaseComponentProps}
* @see {@link PropsWithChildren}
* Defines the properties of the `Dialog` render prop function context object.
* @param close - Closes the dialog.
*/
export interface DialogProps extends BaseComponentProps, PropsWithChildren {
isOpen?: boolean;
onClose?: () => void | Promise<void>;
type DialogRenderFnContext = {
close: () => void;
};

/**
* The `Dialog` render prop function signature.
*/
type DialogRenderFn = (ctx: DialogRenderFnContext) => ReactNode;

export interface DialogProps extends BaseComponentProps {
children?: ReactNode | DialogRenderFn;
}

/**
* A `Dialog` is a modal window that displays on top of the main content,
* typically asking the user to take an action or confirm a decision.
* @param {DialogProps} props - Component properties.
* @returns {JSX.Element} JSX
*/
const Dialog = ({
const Dialog = ({ children, className, testId = 'dialog' }: DialogProps): JSX.Element => {
const [isHidden, setIsHidden] = useState(true);

const close = () => {
setIsHidden(true);
};

return (
<div className={cn(className)} data-testid={testId}>
<DialogContext.Provider value={{ isHidden, setIsHidden }}>
{typeof children === 'function' ? children({ close }) : children}
</DialogContext.Provider>
</div>
);
};

/**
* The `Trigger` renders a clickable element used to open a `Dialog`. There
* should be 1 `Trigger` within a `Dialog`.
*/
const Trigger = ({
children,
className,
isOpen = false,
onClose,
testId = 'dialog',
}: DialogProps): JSX.Element => {
const [isDialogOpen, setIsDialogOpen] = useState(isOpen);

useEffect(() => {
setIsDialogOpen(isOpen);
}, [isOpen]);

const closeDialog = (): void => {
setIsDialogOpen(false);
onClose?.();
};
testId = 'dialog-trigger',
}: BaseComponentProps & PropsWithChildren): JSX.Element => {
const { isHidden, setIsHidden } = useContext(DialogContext);

const handleBackdropClick = (): void => {
closeDialog();
};
return (
<div
role="button"
className={cn('cursor-pointer hover:opacity-80', className)}
onClick={() => setIsHidden(!isHidden)}
data-testid={testId}
>
{children}
</div>
);
};
Dialog.Trigger = Trigger;

const handleDialogClick = (e: React.MouseEvent): void => {
e.stopPropagation();
};
/**
* The `Content` component wraps the contents of a `Dialog` including the
* header, body, and footer. There should be 1 `Content` within a `Dialog`.
*/
const Content = ({
children,
className,
testId = 'dialog-content',
}: BaseComponentProps & PropsWithChildren): JSX.Element => {
const { isHidden, setIsHidden } = useContext(DialogContext);

return (
<div className={cn({ hidden: !isDialogOpen }, className)} data-testid={testId}>
<div className={cn({ hidden: isHidden }, className)} data-testid={testId}>
<Backdrop
className="flex items-center justify-center"
onClick={handleBackdropClick}
onClick={() => setIsHidden(true)}
testId={`${testId}-backdrop`}
>
<div
className="m-4 min-w-72 max-w-[560px] rounded-3xl bg-light-bg p-6 dark:bg-dark-bg"
onClick={handleDialogClick}
className={cn(
'bg-light-bg dark:bg-dark-bg m-4 flex max-w-[560px] min-w-72 flex-col gap-4 rounded-md p-6',
)}
onClick={(e) => e.stopPropagation()}
data-testid={`${testId}-content`}
>
{children}
</div>
</Backdrop>
</div>
);
};
Dialog.Content = Content;

/**
* The `Header` is a block within a dialog `Content` and may contain a `Title`
* and `Subtitle`.
*/
const Header = ({
children,
className,
testId = 'dialog-header',
}: BaseComponentProps & PropsWithChildren): JSX.Element => {
return (
<div className={cn(className)} data-testid={testId}>
{children}
</div>
);
};
Dialog.Header = Header;

/**
* The `Body` is a block which encloses the main content of the `Dialog`.
*/
const Body = ({
children,
className,
testId = 'dialog-body',
}: BaseComponentProps & PropsWithChildren): JSX.Element => {
return (
<div className={cn(className)} data-testid={testId}>
{children}
</div>
);
};
Dialog.Body = Body;

/**
* The `Footer` is a block within a dialog `Content` and contains values such as
* a `ButtonBar`, `Button`, or any components which are located at the bottom of
* the `Dialog`.
*/
const Footer = ({
children,
className,
testId = 'dialog-footer',
}: BaseComponentProps & PropsWithChildren): JSX.Element => {
return (
<div className={cn(className)} data-testid={testId}>
{children}
</div>
);
};
Dialog.Footer = Footer;

/**
* A `Title` for the `Dialog`. The title is optional. When present, the `Title`
* is typically located within the dialog `Header`.
*/
const Title = ({
children,
className,
testId = 'dialog-title',
}: BaseComponentProps & PropsWithChildren): JSX.Element => {
return (
<h5 className={cn('line-clamp-2 text-2xl', className)} data-testid={testId}>
{children}
</h5>
);
};
Dialog.Title = Title;

/**
* A `Subtitle` for the `Dialog`. The subtitle is optional. When present, the
* `Subtitle` is typically located within the dialog `Header`, immediately after
* the `Title`.
*/
const Subtitle = ({
children,
className,
testId = 'dialog-subtitle',
}: BaseComponentProps & PropsWithChildren): JSX.Element => {
return (
<div
className={cn(
'line-clamp-2 leading-tight text-neutral-500 font-stretch-condensed',
className,
)}
data-testid={testId}
>
{children}
</div>
);
};
Dialog.Subtitle = Subtitle;

/**
* A `ButtonBar` organizes one to many dialog `Button` components in a horizontal
* row. The buttons are right justified within the bar.
*/
const ButtonBar = ({
children,
className,
testId = 'dialog-button-bar',
}: BaseComponentProps & PropsWithChildren): JSX.Element => {
return (
<div className={cn('flex items-center justify-end gap-4', className)} data-testid={testId}>
{children}
</div>
);
};
Dialog.ButtonBar = ButtonBar;

/**
* Define the `Button` component base and variant styles.
*/
const buttonVariants = cva('', {
variants: {
variant: {
danger: 'font-bold text-red-600',
primary: 'font-bold text-blue-600 dark:text-blue-400',
secondary: '',
},
},
defaultVariants: { variant: 'secondary' },
});

/**
* The variant attributes of the `Button` component.
*/
type ButtonVariants = VariantProps<typeof buttonVariants>;

/**
* Properties for the `Button` component.
*/
export interface ButtonProps extends Omit<CommonButtonProps, 'variant'>, ButtonVariants {}

/**
* A dialog `Button` is a button which is styles for presentation within a
* `Dialog`.
*/
const Button = ({
className,
variant = 'secondary',
testId = 'dialog-button',
...props
}: ButtonProps): JSX.Element => {
return (
<CommonButton
variant="text"
size="sm"
className={cn(buttonVariants({ variant, className }))}
testId={testId}
{...props}
/>
);
};
Dialog.Button = Button;

/**
* The `Separator` component renders a horizontal divider.
* This is useful to organize and separate content.
*/
const Separator = ({ className, testId = 'dialog-separator' }: DividerProps): JSX.Element => {
return <Divider className={cn('my-1', className)} testId={testId} />;
};
Dialog.Separator = Separator;

export default Dialog;
53 changes: 0 additions & 53 deletions src/common/components/Dialog/DialogButton.tsx

This file was deleted.

Loading