Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: NO-JIRA Corner Dialog component #135

Merged
merged 18 commits into from
May 7, 2024
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
18 changes: 18 additions & 0 deletions src/components/CornerDialog/CornerDialog.props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { ReactNode, MouseEvent } from 'react';

import { CornerDialogConfig } from './CornerDialog.styles';
import { DefaultButtonProps } from '../Button/Button.props';

import { DistributiveOmit } from '@/utility-types/DistributiveOmit';

export type CornerDialogProps = {
custom?: CornerDialogConfig;
intent?: 'none' | 'warning' | 'negative';
title: string;
content: ReactNode;
actions?: DistributiveOmit<
mjamrozekvl marked this conversation as resolved.
Show resolved Hide resolved
DefaultButtonProps,
'size' | 'hasDropdownIndicator'
>[];
onCloseClick?: (e?: MouseEvent) => void;
};
170 changes: 170 additions & 0 deletions src/components/CornerDialog/CornerDialog.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { action } from '@storybook/addon-actions';
import type { Meta, StoryObj } from '@storybook/react';

import { CornerDialog } from './CornerDialog';

import { CornerDialogDocs } from '@/docs-components/CornerDialogDocs';
import { TetDocs } from '@/docs-components/TetDocs';
import { tet } from '@/tetrisly';

const meta = {
title: 'CornerDialog',
component: CornerDialog,
tags: ['autodocs'],
argTypes: {},
args: {
intent: 'none',
title: 'Corner Dialog',
content: 'Description',
actions: undefined,
onCloseClick: action('onCloseClick'),
},
parameters: {
docs: {
description: {
component:
'A small, non-intrusive window that appears in the corner of the screen to convey contextual information or prompt user interaction. Often used for hints, tips, or non-essential notifications.',
},
page: () => (
<TetDocs docs="https://docs.tetrisly.com/components/in-progress/cornerdialog">
<CornerDialogDocs />
</TetDocs>
),
},
},
} satisfies Meta<typeof CornerDialog>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
intent: 'none',
title: 'Corner Dialog',
content: 'Description',
actions: [
{ label: 'Action', onClick: action('onClick') },
{
label: 'Primary Action',
onClick: action('onClick'),
appearance: 'primary',
},
],
onCloseClick: action('onCloseClick'),
},
};

export const Decision: Story = {
args: {
intent: 'none',
title: 'Title',
content: 'Description',
actions: [
{ label: 'Cancel', onClick: action('onCancelClick') },
{
label: 'Accept',
onClick: action('onAcceptClick'),
appearance: 'primary',
},
],
onCloseClick: action('onCloseClick'),
},
};

export const Confirmation: Story = {
args: {
intent: 'none',
title: 'Title',
content: 'Description',
actions: [
{
label: 'Accept',
onClick: action('onAcceptClick'),
appearance: 'primary',
},
],
onCloseClick: undefined,
},
};

export const NegativeWithDestructiveButton: Story = {
args: {
intent: 'negative',
title: 'Title',
content: 'Description',
actions: [
{ label: 'Cancel', onClick: action('onCancelClick') },
{
label: 'Remove',
onClick: action('onRemoveClick'),
appearance: `primary`,
intent: 'destructive',
},
],
onCloseClick: action('onCloseClick'),
},
};

export const WarningAndAdditionalAction: Story = {
args: {
intent: 'warning',
title: 'Title',
content: 'Description',
actions: [
{
label: 'Find out more',
onClick: action('onFindOutMoreClick'),
custom: {
default: {
position: 'absolute',
left: 0,
},
},
},
{ label: 'Cancel', onClick: action('onCancelClick') },
{
label: 'Accept',
onClick: action('onAcceptClick'),
appearance: 'primary',
},
],
onCloseClick: action('onCloseClick'),
custom: {
innerElements: {
footer: {
position: 'relative',
},
},
},
},
};

export const CustomContent: Story = {
args: {
intent: 'none',
title: 'Corner Dialog with custom content',
content: (
<tet.div>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.{' '}
<tet.span color="$color-blue-0" fontWeight="$font-weight-600">
Morbi pellentesque elit ut sem accumsan, eget maximus erat eleifend.
</tet.span>
Vestibulum ac tortor nunc.{' '}
<tet.span textDecoration="underline">
Nam tincidunt nibh eget nulla aliquet, et auctor dui rhoncus. Donec
bibendum rhoncus lacus vel scelerisque.
</tet.span>
Suspendisse feugiat ligula quis eros interdum varius. Ut nec ex est.
</tet.div>
),
actions: [
{ label: 'Action', onClick: action('onClick') },
{
label: 'Primary Action',
onClick: action('onClick'),
appearance: 'primary',
},
],
onCloseClick: action('onCloseClick'),
},
};
84 changes: 84 additions & 0 deletions src/components/CornerDialog/CornerDialog.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type { BaseProps } from '@/types/BaseProps';

export type CornerDialogFooterConfig = {
actions?: BaseProps;
} & BaseProps;

export type CornerDialogConfig = BaseProps & {
innerElements?: {
intentIndicator?: BaseProps;
intentWarning?: BaseProps;
intentNegative?: BaseProps;
body?: BaseProps;
header?: BaseProps;
headerTitle?: BaseProps;
closeButton?: BaseProps;
content?: BaseProps;
footer?: CornerDialogFooterConfig;
};
};

export const defaultConfig = {
display: 'flex',
w: 'fit-content',
minWidth: '420px',
p: '$space-component-padding-2xLarge',
flexDirection: 'row',
alignItems: 'flex-start',
gap: '$space-component-padding-large',
borderRadius: '$border-radius-xLarge',
bg: '$color-interaction-background-modeless',
boxShadow: '$elevation-bottom-300',
borderWidth: '$border-width-small',
borderStyle: '$border-style-solid',
borderColor: '$color-border-defaultA',
overflow: 'hidden',
innerElements: {
intentIndicator: {
h: '$size-xSmall',
display: 'flex',
alignItems: 'flex-end',
},
intentWarning: {
color: '$color-content-warning-secondary',
},
intentNegative: {
color: '$color-content-negative-secondary',
},
body: {
display: 'flex',
flexGrow: 1,
flexDirection: 'column',
justifyContent: 'space-between',
gap: '$space-component-padding-large',
},
header: {
display: 'flex',
alignSelf: 'stretch',
alignItems: 'center',
},
headerTitle: {
display: 'flex',
flexGrow: 1,
alignItems: 'center',
justifyContent: 'space-between',
color: '$color-content-primary',
text: '$typo-body-strong-large',
},
closeButton: {},
content: {
text: '$typo-body-medium',
color: '$color-content-secondary',
},
footer: {
display: 'flex',
alignSelf: 'stretch',
justifyContent: 'flex-end',
gap: '$space-component-padding-small',
},
},
} as const satisfies CornerDialogConfig;

export const cornerDialogStyles = {
defaultConfig,
};
96 changes: 96 additions & 0 deletions src/components/CornerDialog/CornerDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { vi } from 'vitest';

import { CornerDialog } from './CornerDialog';
import { render, screen, fireEvent } from '../../tests/render';

describe('CornerDialog', () => {
it('should render empty corner dialog', () => {
render(<CornerDialog intent="none" title="Title" content="Content" />);
const cornerDialog = screen.getByTestId('corner-dialog');
expect(cornerDialog).toBeInTheDocument();

const header = screen.getByTestId('corner-dialog-header');
expect(header).toBeInTheDocument();

const headerTitle = screen.getByTestId('corner-dialog-header-title');
expect(headerTitle).toBeInTheDocument();
expect(headerTitle).toHaveTextContent('Title');

const content = screen.getByTestId('corner-dialog-content');
expect(content).toBeInTheDocument();
expect(content).toHaveTextContent('Content');
});

it('should render warning corner dialog', () => {
render(<CornerDialog intent="warning" title="Title" content="Content" />);
const warningIcon = screen.getByTestId('warning-icon');
expect(warningIcon).toBeInTheDocument();
});

it('should render negative corner dialog', () => {
render(<CornerDialog intent="negative" title="Title" content="Content" />);
const negativeIcon = screen.getByTestId('negative-icon');
expect(negativeIcon).toBeInTheDocument();
});

it('should render close icon when onCloseClick handler is provided', () => {
render(
<CornerDialog
intent="none"
title="Title"
content="Content"
onCloseClick={() => {}}
/>,
);
const closeIcon = screen.getByTestId('close-icon');
expect(closeIcon).toBeInTheDocument();
});

it('should render footer if at least one action is provided', () => {
render(
<CornerDialog
intent="none"
title="Title"
content="Content"
actions={[{ label: 'Action' }]}
/>,
);
const footer = screen.getByTestId('corner-dialog-footer');
expect(footer).toBeInTheDocument();
});

it('should render footer with 2 actions', async () => {
render(
<CornerDialog
intent="none"
title="Title"
content="Content"
actions={[{ label: 'First action' }, { label: 'Second action' }]}
/>,
);

const firstActionButton = await screen.findByText('First action');
expect(firstActionButton).toBeInTheDocument();

const secondActionButton = await screen.findByText('Second action');
expect(secondActionButton).toBeInTheDocument();
});

it('should call onCloseClick after click to close icon', () => {
const onCloseClickMock = vi.fn();

render(
<CornerDialog
intent="none"
title="Title"
content="Content"
onCloseClick={onCloseClickMock}
/>,
);

const closeIcon = screen.getByTestId('close-icon');
expect(closeIcon).toBeInTheDocument();
fireEvent.click(closeIcon);
expect(onCloseClickMock).toHaveBeenCalled();
});
});
Loading
Loading