Skip to content

Commit

Permalink
feat: NO-JIRA Corner Dialog component (#135)
Browse files Browse the repository at this point in the history
* feat: NO-JIRA basic CornerDialog scaffold

* refactor: NO-JIRA styles builder and corner dialog docs

* refactor: NO-JIRA remove unnecessary stylesBuilder directory

* test: NO-JIRA add tests to CornerDialog

* docs: NO-JIRA add missing figma scenarios

* docs: NO-JIRA add custom content story example

* style: NO-JIRA sizes according to figma

* refactor: NO-JIRA export styles

* style: NO-JIRA set react event object to optional

* refactor: NO-JIRA exclude size and hasDropdownIndicator from action button props

* refactor: NO-JIRA replace color with appropriate variable

* refactor: NO-JIRA replace rest of wrongly defined variables

* fix: NO-JIRA introduce DistributiveOmit typescript utility type

reason described here: #135 (comment)

* style: NO-JIRA disable @typescript-eslint/no-explicit-any rule in DistributiveOmit util

* refactor: NO-JIRA remove unnecessary React. type prefix

* fix: NO-JIRA export CornerDialog and Status components in src/index.ts file

* fix: NO-JIRA use -500 instead of 40px unit
  • Loading branch information
mjamrozekvl authored May 7, 2024
1 parent c81b1a6 commit d916220
Show file tree
Hide file tree
Showing 10 changed files with 544 additions and 0 deletions.
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<
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

0 comments on commit d916220

Please sign in to comment.