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
5 changes: 4 additions & 1 deletion .github/workflows/deploy-storybook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,7 @@ jobs:
run: yarn build

- name: Deploy Storybook to GitHub Pages
run: npx gh-pages -d storybook-static -b storybook-ui
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npx gh-pages -d storybook-static -b storybook-ui -r https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git
116 changes: 116 additions & 0 deletions src/components/baseDialog/BaseDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { IconWeight, X } from '@phosphor-icons/react';

export interface BaseDialogProps {
isOpen: boolean;
title?: string;
hideCloseButton?: boolean;
subTitle?: string;
dialogRounded?: boolean;
children: JSX.Element | JSX.Element[];
classes?: string;
titleClasses?: string;
panelClasses?: string;
closeClass?: string;
weightIcon?: IconWeight;
bgColor?: string;
onClose: () => void;
dataTest?: string;
}

/**
* BaseDialog component
*
* @property {boolean} isOpen
* - Controls whether the dialog is open or closed. If true, the dialog is visible.
*
* @property {string} [title]
* - The title of the dialog, displayed at the top of the dialog box.
*
* @property {boolean} [hideCloseButton]
* - If true, hides the close button (X icon) in the top right corner of the dialog.
*
* @property {string} [subTitle]
* - A subtitle for the dialog, displayed below the title.
*
* @property {boolean} [dialogRounded]
* - If true, applies a more rounded corner style to the dialog.
*
* @property {JSX.Element | JSX.Element[]} children
* - The content to be displayed inside the dialog. Can be a single JSX element or an array of elements.
*
* @property {string} [classes]
* - Custom classes for the outermost container of the dialog. Allows additional styling like margins or padding.
*
* @property {string} [titleClasses]
* - Custom classes for styling the title element. Can modify font size, weight, etc.
*
* @property {string} [panelClasses]
* - Custom classes for the main dialog panel, where the content is displayed.
*
* @property {string} [closeClass]
* - Custom classes for the close button, allowing for customization of the button's appearance.
*
* @property {IconWeight} [weightIcon]
* - Controls the thickness of the close button icon (X). Options range from "thin" to "bold".
*
* @property {string} [bgColor]
* - Custom background color for the dialog. Defaults to a light surface color if not provided.
*
* @property {() => void} onClose
* - Callback function triggered when the close button or overlay is clicked, used to close the dialog.
*/

const BaseDialog = ({
isOpen,
title,
subTitle,
dialogRounded,
children,
onClose,
classes,
panelClasses,
titleClasses,
closeClass,
weightIcon,
bgColor,
dataTest,
hideCloseButton,
}: BaseDialogProps): JSX.Element => {
return (
<div
data-test={dataTest}
className={`${isOpen ? 'flex' : 'hidden'} ${
classes || ''
} absolute bottom-0 left-0 right-0 top-0 z-50 bg-black/40`}
>
<div
className={`${panelClasses || ''} absolute left-1/2 top-1/2 flex w-104 -translate-x-1/2
-translate-y-1/2 flex-col overflow-hidden ${dialogRounded ? 'rounded-2xl' : 'rounded-lg pt-8'} text-gray-100 ${
bgColor || 'bg-surface'
}`}
>
<div className={`${subTitle ? 'justify-between bg-gray-1 p-5' : ''} flex flex-row items-start`}>
{title ? (
<div className="relative flex max-w-full flex-1 flex-col truncate">
<span className={`${titleClasses || ''} truncate text-xl`} title={title}>
{title}
</span>
<span className="max-w-fit flex-1 truncate text-base font-normal text-gray-50">{subTitle}</span>
</div>
) : null}
{hideCloseButton ? null : (
<div
className={`relative ml-auto cursor-pointer bg-surface
transition duration-200 ease-in-out ${closeClass || 'text-primary hover:text-primary-dark'} `}
>
<X role="button" onClick={onClose} size={28} weight={weightIcon} />
</div>
)}
</div>
{children}
</div>
</div>
);
};

export default BaseDialog;
94 changes: 94 additions & 0 deletions src/components/baseDialog/__test__/BaseDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import BaseDialog from '../BaseDialog';

describe('BaseDialog', () => {
const onCloseMock = vi.fn();
const defaultProps = {
title: 'Dialog Title',
subTitle: 'This is a subtitle',
isOpen: true,
onClose: onCloseMock,
children: <div>Dialog content</div>,
};
const renderBaseDialog = (props = {}) => render(<BaseDialog {...defaultProps} {...props} />);

afterEach(() => {
vi.clearAllMocks();
});

it('should match snapshot', () => {
const baseDialog = renderBaseDialog();
expect(baseDialog).toMatchSnapshot();
});

it('should render the dialog when isOpen is true', () => {
const { container } = renderBaseDialog();
expect(container.firstChild).toHaveClass('flex');
});

it('should not render the dialog when isOpen is false', () => {
const { container } = renderBaseDialog({ isOpen: false });
expect(container.firstChild).toHaveClass('hidden');
});

it('should trigger onClose when the close button is clicked', () => {
const { getByRole } = renderBaseDialog();
const closeButton = getByRole('button');

fireEvent.click(closeButton);
expect(onCloseMock).toHaveBeenCalledTimes(1);
});

it('should apply custom class names correctly', () => {
const { container } = renderBaseDialog({ classes: 'custom-class' });
expect(container.firstChild).toHaveClass('custom-class');
});

it('should render the title and subtitle when provided', () => {
const { getByText } = renderBaseDialog();

expect(getByText('Dialog Title')).toBeInTheDocument();
expect(getByText('This is a subtitle')).toBeInTheDocument();
});

it('should not render the title div when title is not provided', () => {
const { container } = renderBaseDialog({ title: undefined });
const titleDiv = container.querySelector('[title]');
expect(titleDiv).not.toBeInTheDocument();
});

it('should not render the subtitle when subTitle is not provided', () => {
const { container } = renderBaseDialog({ subTitle: undefined });
const subTitleElement = container.querySelector('.justify-between.bg-gray-1.p-5');
expect(subTitleElement).not.toBeInTheDocument();
});

it('should render children content inside the dialog', () => {
const { getByText } = renderBaseDialog();
expect(getByText('Dialog content')).toBeInTheDocument();
});

it('should hide close button when hideCloseButton is true', () => {
const { queryByRole } = renderBaseDialog({ hideCloseButton: true });
expect(queryByRole('button')).toBeNull();
});

it('should apply the bgColor prop if provided', () => {
const { container } = renderBaseDialog({ bgColor: 'bg-red' });
expect(container.firstChild?.firstChild).toHaveClass('bg-red');
});

it('should render with rounded-2xl class when dialogRounded is true', () => {
const { container } = renderBaseDialog({ dialogRounded: true });
const dialogPanel = container.querySelector('.rounded-2xl');
expect(dialogPanel).toBeInTheDocument();
});

it('should render with rounded-lg and pt-8 classes when dialogRounded is false', () => {
const { container } = renderBaseDialog({ dialogRounded: false });
const dialogPanel = container.querySelector('.rounded-lg.pt-8');
expect(dialogPanel).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`BaseDialog > should match snapshot 1`] = `
{
"asFragment": [Function],
"baseElement": <body>
<div>
<div
class="flex absolute bottom-0 left-0 right-0 top-0 z-50 bg-black/40"
>
<div
class=" absolute left-1/2 top-1/2 flex w-104 -translate-x-1/2
-translate-y-1/2 flex-col overflow-hidden rounded-lg pt-8 text-gray-100 bg-surface"
>
<div
class="justify-between bg-gray-1 p-5 flex flex-row items-start"
>
<div
class="relative flex max-w-full flex-1 flex-col truncate"
>
<span
class=" truncate text-xl"
title="Dialog Title"
>
Dialog Title
</span>
<span
class="max-w-fit flex-1 truncate text-base font-normal text-gray-50"
>
This is a subtitle
</span>
</div>
<div
class="relative ml-auto cursor-pointer bg-surface
transition duration-200 ease-in-out text-primary hover:text-primary-dark "
>
<svg
fill="currentColor"
height="28"
role="button"
viewBox="0 0 256 256"
width="28"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"
/>
</svg>
</div>
</div>
<div>
Dialog content
</div>
</div>
</div>
</div>
</body>,
"container": <div>
<div
class="flex absolute bottom-0 left-0 right-0 top-0 z-50 bg-black/40"
>
<div
class=" absolute left-1/2 top-1/2 flex w-104 -translate-x-1/2
-translate-y-1/2 flex-col overflow-hidden rounded-lg pt-8 text-gray-100 bg-surface"
>
<div
class="justify-between bg-gray-1 p-5 flex flex-row items-start"
>
<div
class="relative flex max-w-full flex-1 flex-col truncate"
>
<span
class=" truncate text-xl"
title="Dialog Title"
>
Dialog Title
</span>
<span
class="max-w-fit flex-1 truncate text-base font-normal text-gray-50"
>
This is a subtitle
</span>
</div>
<div
class="relative ml-auto cursor-pointer bg-surface
transition duration-200 ease-in-out text-primary hover:text-primary-dark "
>
<svg
fill="currentColor"
height="28"
role="button"
viewBox="0 0 256 256"
width="28"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"
/>
</svg>
</div>
</div>
<div>
Dialog content
</div>
</div>
</div>
</div>,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`;
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export * from './list/List';
export * from './dropdown/Dropdown';
export * from './menu/Menu';
export * from './breadcrumbs/Breadcrumbs';
export * from './baseDialog/BaseDialog';
Loading