-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: NO-JIRA Corner Dialog component (#135)
* 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
1 parent
c81b1a6
commit d916220
Showing
10 changed files
with
544 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'), | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
Oops, something went wrong.