-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[@kadena/react-ui] Split the dialog/ modal and add tests
- Loading branch information
1 parent
ec1a431
commit 4b076bc
Showing
19 changed files
with
660 additions
and
250 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,5 @@ | ||
--- | ||
'@kadena/react-ui': minor | ||
--- | ||
|
||
Imporve `Dialog` and `Modal` components |
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
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
53 changes: 53 additions & 0 deletions
53
packages/libs/react-ui/src/components/Dialog/Dialog.css.ts
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,53 @@ | ||
import { sprinkles } from '@theme/sprinkles.css'; | ||
import { responsiveStyle } from '@theme/themeUtils'; | ||
import { vars } from '@theme/vars.css'; | ||
import { style } from '@vanilla-extract/css'; | ||
|
||
export const openModal = style([ | ||
{ | ||
height: '100vh', | ||
overflowY: 'hidden', | ||
}, | ||
]); | ||
|
||
export const overlayClass = style([ | ||
sprinkles({ | ||
position: 'relative', | ||
pointerEvents: 'initial', | ||
overflow: 'auto', | ||
width: '100%', | ||
}), | ||
responsiveStyle({ | ||
xs: { | ||
maxHeight: '100svh', | ||
maxWidth: '100vw', | ||
inset: 0, | ||
}, | ||
md: { | ||
maxWidth: vars.sizes.$maxContent, | ||
maxHeight: '75vh', | ||
}, | ||
}), | ||
]); | ||
|
||
export const closeButtonClass = style([ | ||
sprinkles({ | ||
position: 'absolute', | ||
top: '$md', | ||
right: '$md', | ||
display: 'flex', | ||
alignItems: 'center', | ||
background: 'none', | ||
border: 'none', | ||
padding: '$sm', | ||
cursor: 'pointer', | ||
color: 'inherit', | ||
}), | ||
]); | ||
|
||
export const titleWrapperClass = style([ | ||
sprinkles({ | ||
marginBottom: '$4', | ||
marginRight: '$20', | ||
}), | ||
]); |
65 changes: 65 additions & 0 deletions
65
packages/libs/react-ui/src/components/Dialog/Dialog.stories.tsx
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,65 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import React, { useState } from 'react'; | ||
import { Button } from '../Button'; | ||
import type { IModalProps } from '../Modal'; | ||
import { Text } from '../Typography'; | ||
import { Dialog } from './Dialog'; | ||
|
||
const meta: Meta<{ title?: string } & IModalProps> = { | ||
title: 'Overlays/Dialog', | ||
parameters: { | ||
docs: { | ||
description: { | ||
component: ` | ||
A Dialog is a type of modal that is used to display information or prompt the user for input. It is a blocking modal, which means it will trap focus within itself and will not allow the user to interact with the rest of the page until it is closed. It is also dismissable, which means it can be closed by clicking on the close button or pressing the escape key. Dialogs are used for important information that requires the user to take action before continuing. | ||
`, | ||
}, | ||
}, | ||
}, | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<IModalProps>; | ||
|
||
export const DialogStory: Story = { | ||
name: 'Dialog', | ||
render: () => { | ||
const [isOpen, setIsOpen] = useState(false); | ||
return ( | ||
<> | ||
<Button onClick={() => setIsOpen(true)}>Modal Trigger</Button> | ||
<Dialog | ||
isOpen={isOpen} | ||
onOpenChange={(isOpen) => setIsOpen(isOpen)} | ||
title={ | ||
<> | ||
<h2>Dialog Title</h2> | ||
<p>Dialog description</p> | ||
</> | ||
} | ||
> | ||
{(state) => ( | ||
<> | ||
<Text> | ||
Dessert gummies pie biscuit chocolate bar cheesecake. Toffee | ||
chocolate bar ice cream cake jujubes pudding fruitcake marzipan. | ||
Donut sweet oat cake dragée candy cupcake biscuit. Carrot cake | ||
sesame snaps marzipan gummies marshmallow topping cake apple pie | ||
pudding. Toffee sweet halvah cake liquorice chupa chups sugar | ||
plum. Tootsie roll marshmallow gummi bears apple pie cake | ||
jujubes pudding. Halvah apple pie tiramisu bear claw caramels | ||
cookie dessert cotton candy. Jelly-o sweet sugar plum topping | ||
topping jujubes powder shortbread lemon drops. Chupa chups | ||
muffin oat cake chupa chups cookie liquorice oat cake tootsie | ||
roll. Gingerbread dessert donut pastry muffin powder sugar plum. | ||
Chupa chups bonbon topping jelly beans pastry. Soufflé chupa | ||
chups wafer fruitcake lollipop apple pie bonbon tart bonbon. | ||
</Text> | ||
<Button onClick={state.close}>Close Button</Button> | ||
</> | ||
)} | ||
</Dialog> | ||
</> | ||
); | ||
}, | ||
}; |
60 changes: 60 additions & 0 deletions
60
packages/libs/react-ui/src/components/Dialog/Dialog.test.tsx
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,60 @@ | ||
import { render, screen } from '@testing-library/react'; | ||
import userEvent from '@testing-library/user-event'; | ||
import React from 'react'; | ||
import { describe, expect, it } from 'vitest'; | ||
|
||
import { Dialog } from './Dialog'; | ||
|
||
describe('Modal', () => { | ||
it('should render the provided children', () => { | ||
render( | ||
<Dialog isOpen> | ||
<div>Hello, world!</div> | ||
</Dialog>, | ||
); | ||
|
||
expect(screen.getByText('Hello, world!')).toBeInTheDocument(); | ||
}); | ||
|
||
it('should render the provided title', () => { | ||
render( | ||
<Dialog isOpen title="Title"> | ||
<div>Hello, world!</div> | ||
</Dialog>, | ||
); | ||
|
||
expect(screen.getByText('Hello, world!')).toBeInTheDocument(); | ||
expect(screen.getByLabelText('Title')).toHaveAttribute('role', 'dialog'); | ||
}); | ||
|
||
it('should use custom aria-label correctly', () => { | ||
render( | ||
<Dialog isOpen aria-label="my own label" title="only visual title"> | ||
<div>Hello, world!</div> | ||
</Dialog>, | ||
); | ||
expect(screen.getByLabelText('my own label')).toHaveAttribute( | ||
'role', | ||
'dialog', | ||
); | ||
}); | ||
|
||
it('should render the dialog when defaultOpen is true', () => { | ||
render( | ||
<Dialog defaultOpen> | ||
<div>Hello, world!</div> | ||
</Dialog>, | ||
); | ||
expect(screen.getByText('Hello, world!')).toBeInTheDocument(); | ||
}); | ||
it('should dismiss the dialog when the escape key is pressed', async () => { | ||
render( | ||
<Dialog defaultOpen> | ||
<div>Hello, world!</div> | ||
</Dialog>, | ||
); | ||
|
||
await userEvent.type(document.body, '{esc}'); | ||
expect(screen.queryByText('Hello, world!')).not.toBeInTheDocument(); | ||
}); | ||
}); |
118 changes: 118 additions & 0 deletions
118
packages/libs/react-ui/src/components/Dialog/Dialog.tsx
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,118 @@ | ||
import { useObjectRef } from '@react-aria/utils'; | ||
import classNames from 'classnames'; | ||
import type { FC, ReactNode } from 'react'; | ||
import React from 'react'; | ||
import type { AriaDialogProps } from 'react-aria'; | ||
import { mergeProps, useDialog } from 'react-aria'; | ||
import type { OverlayTriggerState } from 'react-stately'; | ||
import { useOverlayTriggerState } from 'react-stately'; | ||
import { containerClass as cardContainerClass } from '../Card/Card.css'; | ||
import { SystemIcon } from '../Icon'; | ||
import type { IModalProps } from '../Modal/Modal'; | ||
import { Modal } from '../Modal/Modal'; | ||
import { Heading } from '../Typography'; | ||
import { | ||
closeButtonClass, | ||
overlayClass, | ||
titleWrapperClass, | ||
} from './Dialog.css'; | ||
|
||
interface IBaseDialogProps | ||
extends Omit<IModalProps, 'children'>, | ||
AriaDialogProps { | ||
className?: string; | ||
title?: ReactNode; | ||
children?: ((state: OverlayTriggerState) => ReactNode) | ReactNode; | ||
} | ||
|
||
const BaseDialog = React.forwardRef<HTMLDivElement, IBaseDialogProps>( | ||
(props, ref) => { | ||
const { | ||
title, | ||
className, | ||
children, | ||
isDismissable = true, | ||
state, | ||
...rest | ||
} = props; | ||
const dialogRef = useObjectRef<HTMLDivElement | null>(ref); | ||
const { dialogProps, titleProps } = useDialog( | ||
{ | ||
role: props.role ?? 'dialog', | ||
...rest, | ||
}, | ||
dialogRef, | ||
); | ||
|
||
return ( | ||
<div | ||
ref={dialogRef} | ||
className={classNames(cardContainerClass, overlayClass, className)} | ||
{...mergeProps(rest, dialogProps)} | ||
> | ||
{isDismissable && ( | ||
<button | ||
className={closeButtonClass} | ||
onClick={state.close} | ||
aria-label="Close Modal" | ||
title="Close Modal" | ||
> | ||
<SystemIcon.Close /> | ||
</button> | ||
)} | ||
|
||
{title && ( | ||
<div className={titleWrapperClass} {...titleProps}> | ||
{typeof title === 'string' ? ( | ||
<Heading as="h3">{title}</Heading> | ||
) : ( | ||
title | ||
)} | ||
</div> | ||
)} | ||
{typeof children === 'function' ? children(state) : children} | ||
</div> | ||
); | ||
}, | ||
); | ||
|
||
BaseDialog.displayName = 'BaseDialog'; | ||
|
||
export interface IDialogProps extends Omit<IBaseDialogProps, 'state'> { | ||
children?: ((state: OverlayTriggerState) => ReactNode) | ReactNode; | ||
isOpen?: boolean; | ||
defaultOpen?: boolean; | ||
onOpenChange?: (isOpen: boolean) => void; | ||
} | ||
|
||
export const Dialog: FC<IDialogProps> = ({ | ||
title, | ||
className, | ||
children, | ||
isDismissable = true, | ||
isKeyboardDismissDisabled, | ||
isOpen, | ||
defaultOpen, | ||
onOpenChange, | ||
...props | ||
}) => { | ||
const state = useOverlayTriggerState({ | ||
isOpen, | ||
defaultOpen, | ||
onOpenChange, | ||
}); | ||
|
||
return ( | ||
<Modal isKeyboardDismissDisabled={isKeyboardDismissDisabled} state={state}> | ||
<BaseDialog | ||
state={state} | ||
title={title} | ||
className={className} | ||
isDismissable={isDismissable} | ||
{...props} | ||
> | ||
{children} | ||
</BaseDialog> | ||
</Modal> | ||
); | ||
}; |
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 @@ | ||
export { Dialog, type IDialogProps } from './Dialog'; |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.