Skip to content

Commit

Permalink
feat(APP-3912): Update Dialog and DialogAlert implementations (#396)
Browse files Browse the repository at this point in the history
  • Loading branch information
cgero-eth authored Jan 30, 2025
1 parent d4e1a68 commit 979f645
Show file tree
Hide file tree
Showing 40 changed files with 898 additions and 608 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

- Bump `actions/setup-node` to 4.2.0
- Update minor and patch NPM dependencies
- **BREAKING**: Update implementation of `Dialog` component:
- Remove `showBackButton` property from `Header` component
- Move `description` property from `Header` to `Content` component
- Rename `onCloseClick` to `onClose`, hide close button when property is not specified
- Remove `alert` property from `Footer` component
- Update `Footer` component to support new `variant` and `hasError` properties
- Update `Content` component to support new `noInset` property
- **BREAKING**: Update implementation of `DialogAlert` component:
- Remove `description` property from `Header` in favour of new `hiddenDescription` property on `Root` component
- Update `Content` component to support new `noInset` property

## [1.0.64] - 2025-01-23

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"@storybook/blocks": "^8.5.2",
"@storybook/react": "^8.5.2",
"@storybook/react-webpack5": "^8.5.2",
"@storybook/test": "^8.5.2",
"@svgr/rollup": "^8.1.0",
"@svgr/webpack": "^8.1.0",
"@tailwindcss/typography": "^0.5.16",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react';
import { useState } from 'react';
import { Dialog } from '..';
import { Button } from '../../../button';
import { type IDialogContentProps } from './dialogContent';
import { DialogStoryComponent } from '../dialogStoryComponent';

const meta: Meta<typeof Dialog.Content> = {
title: 'Core/Components/Dialogs/Dialog/Dialog.Content',
Expand All @@ -17,39 +15,15 @@ const meta: Meta<typeof Dialog.Content> = {

type Story = StoryObj<typeof Dialog.Content>;

const ControlledComponent = (props: IDialogContentProps) => {
const [open, setOpen] = useState(false);

return (
<>
<Button variant="primary" onClick={() => setOpen(true)}>
Show Dialog
</Button>
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Header title="Dialog title" description="Optional dialog description" />
<Dialog.Content {...props} />
<Dialog.Footer
primaryAction={{ label: 'Primary action' }}
secondaryAction={{ label: 'Secondary action' }}
alert={{ message: 'Very informative alert message' }}
/>
</Dialog.Root>
</>
);
};

/**
* Default usage of the `Dialog.Content` component
*/
export const Default: Story = {
args: {
children: <p className="py-2 text-neutral-800">Very important content here!</p>,
},
render: (props) => <ControlledComponent {...props} />,
render: DialogStoryComponent('content'),
};

/**
* Usage example of `Dialog.Content` component with overflowing content
* Usage example of the `Dialog.Content` component with overflowing content
*/
export const ScrollableContent: Story = {
args: {
Expand All @@ -59,7 +33,26 @@ export const ScrollableContent: Story = {
</div>
),
},
render: (props) => <ControlledComponent {...props} />,
render: DialogStoryComponent('content'),
};

/**
* Usage example of the `Dialog.Content` component with a multiline description
*/
export const MultilineDescription: Story = {
args: {
description:
'A long description for the dialog which does not get truncated, a long description for the dialog which does not get truncated',
},
render: DialogStoryComponent('content'),
};

/**
* Use the noInset property to remove the default padding and implement a custom dialog layout.
*/
export const NoInset: Story = {
args: { noInset: true, description: undefined },
render: DialogStoryComponent('content'),
};

export default meta;
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import { render, screen } from '@testing-library/react';
import { DialogRoot } from '../dialogRoot';
import { DialogContent, type IDialogContentProps } from './dialogContent';

describe('<Dialog.Content/> component', () => {
const createTestComponent = (props?: Partial<IDialogContentProps>) => {
const completeProps: IDialogContentProps = { ...props };

return <DialogContent {...completeProps} />;
const hiddenDescription = props?.description ? undefined : 'description';

return (
<DialogRoot hiddenTitle="title" hiddenDescription={hiddenDescription} open={true}>
<DialogContent {...completeProps} />
</DialogRoot>
);
};

it('renders the given content', () => {
const content = 'Test content';

render(createTestComponent({ children: content }));

expect(screen.getByText(content)).toBeInTheDocument();
});

it('renders the dialog description when specified', () => {
const description = 'test-description';
render(createTestComponent({ description }));
expect(screen.getByText(description)).toBeInTheDocument();
expect(screen.getByRole('dialog')).toHaveAccessibleDescription(description);
});
});
31 changes: 25 additions & 6 deletions src/core/components/dialogs/dialog/dialogContent/dialogContent.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
import { Description } from '@radix-ui/react-dialog';
import classNames from 'classnames';
import type React from 'react';
import { type ComponentPropsWithoutRef } from 'react';

export interface IDialogContentProps extends ComponentPropsWithoutRef<'div'> {}
export interface IDialogContentProps extends ComponentPropsWithoutRef<'div'> {
/**
* Optional description of the dialog.
*/
description?: string;
/**
* Removes the default paddings when set to true.
* @default false
*/
noInset?: boolean;
}

/**
* `Dialog.Content` component.
*/
export const DialogContent: React.FC<IDialogContentProps> = ({ className, ...otherProps }) => {
return <div className={classNames('overflow-auto px-4 md:px-6', className)} {...otherProps} />;
export const DialogContent: React.FC<IDialogContentProps> = (props) => {
const { description, noInset = false, className, children, ...otherProps } = props;

return (
<div className={classNames('overflow-auto', { 'px-4 md:px-6': !noInset }, className)} {...otherProps}>
{description && (
<Description className="pb-3 text-sm leading-normal text-neutral-500 md:pb-4">
{description}
</Description>
)}
{children}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react';
import { useState } from 'react';
import { Dialog, type IDialogFooterProps } from '..';
import { Button } from '../../../button';
import { IconType } from '../../../icon';
import { Dialog } from '..';
import { DialogStoryComponent } from '../dialogStoryComponent';

const meta: Meta<typeof Dialog.Footer> = {
title: 'Core/Components/Dialogs/Dialog/Dialog.Footer',
Expand All @@ -17,43 +15,35 @@ const meta: Meta<typeof Dialog.Footer> = {

type Story = StoryObj<typeof Dialog.Footer>;

const ControlledComponent = (props: IDialogFooterProps) => {
const [open, setOpen] = useState(false);

return (
<>
<Button variant="primary" onClick={() => setOpen(true)}>
Show Dialog
</Button>
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Header title="Dialog title" description="Optional dialog description" />
<Dialog.Content>
<p className="py-2 text-neutral-800">Very important content here!</p>
</Dialog.Content>
<Dialog.Footer {...props} />
</Dialog.Root>
</>
);
};

/**
* Default usage of the `Dialog.Footer` component
*/
export const Default: Story = {
args: {
primaryAction: { label: 'Action', iconRight: IconType.SUCCESS },
secondaryAction: { label: 'Cancel' },
alert: { message: 'Very informative alert message' },
},
render: (props) => <ControlledComponent {...props} />,
render: DialogStoryComponent('footer'),
};

/**
* `Dialog.Footer` component with no actions
* The `Dialog.Footer` can be rendered with no actions add bottom spacing and display the dialog shadow
*/
export const Actionless: Story = {
args: {},
render: (props) => <ControlledComponent {...props} />,
args: { primaryAction: undefined, secondaryAction: undefined },
render: DialogStoryComponent('footer'),
};

/**
* Use the wizard variant of the `Dialog.Footer` component to render wizards on dialogs.
*/
export const WizardVariant: Story = {
args: { variant: 'wizard' },
render: DialogStoryComponent('footer'),
};

/**
* Set the `hasError` property to true to display an error feedback on the `Dialog.Footer` component.
*/
export const WithError: Story = {
args: { hasError: true, variant: 'wizard' },
render: DialogStoryComponent('footer'),
};

export default meta;
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,21 @@ import { DialogFooter, type IDialogFooterProps } from './dialogFooter';
describe('<Dialog.Footer/> component', () => {
const createTestComponent = (props?: Partial<IDialogFooterProps>) => {
const completeProps: IDialogFooterProps = {
primaryAction: { label: 'primary' },
secondaryAction: { label: 'secondary' },
...props,
};

return <DialogFooter {...completeProps} />;
};

it('renders an alert message', () => {
const alert = { message: 'test alert' };
render(createTestComponent({ alert }));

const alertElement = screen.getByRole('alert');
expect(alertElement).toBeInTheDocument();
expect(alertElement).toHaveTextContent(alert.message);
it('renders the primary action when set', () => {
const primaryAction = { label: 'primary action' };
render(createTestComponent({ primaryAction }));
expect(screen.getByRole('button', { name: primaryAction.label })).toBeInTheDocument();
});

it('renders the primary and secondary action buttons', () => {
const primaryAction = { label: 'test primary action' };
const secondaryAction = { label: 'test secondary action' };

render(createTestComponent({ primaryAction, secondaryAction }));

expect(screen.getByRole('button', { name: primaryAction.label })).toBeInTheDocument();
it('renders the secondary action when set', () => {
const secondaryAction = { label: 'secondary action' };
render(createTestComponent({ secondaryAction }));
expect(screen.getByRole('button', { name: secondaryAction.label })).toBeInTheDocument();
});
});
87 changes: 40 additions & 47 deletions src/core/components/dialogs/dialog/dialogFooter/dialogFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,66 +1,59 @@
import { type AnchorHTMLAttributes, type ButtonHTMLAttributes, type ComponentPropsWithoutRef } from 'react';
import { AlertInline, type IAlertInlineProps } from '../../../alerts';
import { Button, type IButtonBaseProps } from '../../../button';
import classNames from 'classnames';
import type { ComponentPropsWithoutRef } from 'react';
import { Button, type IButtonProps } from '../../../button';

export type IDialogFooterAction = (
| Pick<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'>
| Pick<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick' | 'type'>
) &
Pick<IButtonBaseProps, 'iconRight' | 'iconLeft' | 'disabled' | 'isLoading'> & {
/**
* Button label
*/
label: string;
};

export interface IDialogFooterProps extends ComponentPropsWithoutRef<'div'> {
export type IDialogFooterAction = Exclude<IButtonProps, 'children' | 'variant'> & {
/**
* Optional AlertInline
* Label of the action button.
*/
alert?: IAlertInlineProps;
label: string;
};

export interface IDialogFooterProps extends ComponentPropsWithoutRef<'div'> {
/**
* Dialog primary action button
* Primary action of the dialog.
*/
primaryAction?: IDialogFooterAction;
/**
* Dialog secondary action button
* Secondary action of the dialog.
*/
secondaryAction?: IDialogFooterAction;
/**
* Variant of the dialog footer.
* @default default
*/
variant?: 'default' | 'wizard';
/**
* Displays the primary actions with error variant when set to true.
*/
hasError?: boolean;
}

/**
* `Dialog.Footer` component
*/
export const DialogFooter: React.FC<IDialogFooterProps> = (props) => {
const { alert, primaryAction, secondaryAction, ...otherProps } = props;
const { label: primaryLabel, ...primaryBtnProps } = primaryAction ?? { label: '' };
const { label: secondaryLabel, ...secondaryButtonProps } = secondaryAction ?? { label: '' };
const { primaryAction, secondaryAction, variant = 'default', hasError, className, ...otherProps } = props;

const renderButtonGroup = !!primaryAction || !!secondaryAction;
const { label: primaryLabel, ...primaryButtonProps } = primaryAction ?? {};

const { label: secondaryLabel, ...secondaryButtonProps } = secondaryAction ?? {};

const footerClassNames = classNames(
'flex gap-3 rounded-b-xl bg-modal-footer px-4 pb-4 pt-3 backdrop-blur-md md:gap-4 md:px-6 md:pb-6',
{ 'flex-col md:flex-row': variant === 'default' },
{ 'flex-row-reverse justify-between': variant === 'wizard' },
className,
);

return (
<div
className="flex flex-col gap-4 rounded-b-xl bg-modal-footer px-4 pb-4 pt-2 backdrop-blur-md md:px-6 md:pb-6"
{...otherProps}
>
{renderButtonGroup && (
<div className="flex flex-col gap-3 md:flex-row">
{primaryAction && (
<Button className="w-full md:w-auto" {...primaryBtnProps} size="lg" variant="primary">
{primaryLabel}
</Button>
)}
{secondaryAction && (
<Button className="w-full md:w-auto" {...secondaryButtonProps} variant="tertiary" size="lg">
{secondaryLabel}
</Button>
)}
</div>
<div className={footerClassNames} {...otherProps}>
{primaryAction && (
<Button size="md" {...primaryButtonProps} variant={hasError ? 'critical' : 'primary'}>
{primaryLabel}
</Button>
)}
{alert && (
<div className="flex w-full justify-center md:justify-start">
<AlertInline variant="info" {...alert} />
</div>
{secondaryAction && (
<Button size="md" {...secondaryButtonProps} variant="tertiary">
{secondaryLabel}
</Button>
)}
</div>
);
Expand Down
Loading

0 comments on commit 979f645

Please sign in to comment.