generated from gravity-ui/package-example
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(usePromiseDialog): add usePromiseDialog hook and Confirm component
- Loading branch information
kseniyakuzina
committed
Mar 14, 2024
1 parent
8027223
commit 43666f4
Showing
20 changed files
with
562 additions
and
0 deletions.
There are no files selected for viewing
Validating CODEOWNERS rules …
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
@use '@gravity-ui/uikit/styles/mixins'; | ||
@use '../../components/variables'; | ||
|
||
$block: '.#{variables.$ns}confirm'; | ||
|
||
#{$block} { | ||
&__message { | ||
margin: 0; | ||
@include mixins.text-body-2; | ||
} | ||
} |
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,58 @@ | ||
import React from 'react'; | ||
|
||
import type {ButtonProps, DialogProps} from '@gravity-ui/uikit'; | ||
import {Dialog} from '@gravity-ui/uikit'; | ||
|
||
import {block} from '../utils/cn'; | ||
|
||
import './Confirm.scss'; | ||
|
||
const b = block('confirm'); | ||
|
||
export type ConfirmProps = { | ||
title?: string; | ||
message?: string; | ||
content?: React.ReactNode; | ||
confirmButtonText?: string; | ||
cancelButtonText?: string; | ||
onConfirm: () => void; | ||
onCancel: () => void; | ||
confirmButtonProps?: ButtonProps; | ||
cancelButtonProps?: ButtonProps; | ||
onClose?: DialogProps['onClose']; | ||
} & Omit<DialogProps, 'children' | 'onClose'>; | ||
|
||
export const Confirm = ({ | ||
title, | ||
message = '', | ||
content, | ||
confirmButtonText, | ||
cancelButtonText, | ||
onConfirm, | ||
onCancel, | ||
confirmButtonProps, | ||
cancelButtonProps, | ||
onClose, | ||
...dialogProps | ||
}: ConfirmProps) => { | ||
return ( | ||
<Dialog {...dialogProps} onClose={onClose ?? onCancel}> | ||
<Dialog.Header caption={title} /> | ||
<Dialog.Body> | ||
{content} | ||
{message && !content && <p className={b('message')}>{message}</p>} | ||
</Dialog.Body> | ||
<Dialog.Footer | ||
preset="default" | ||
showError={false} | ||
listenKeyEnter | ||
textButtonApply={confirmButtonText} | ||
textButtonCancel={cancelButtonText} | ||
onClickButtonApply={onConfirm} | ||
onClickButtonCancel={onCancel} | ||
propsButtonCancel={cancelButtonProps} | ||
propsButtonApply={confirmButtonProps} | ||
/> | ||
</Dialog> | ||
); | ||
}; |
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,54 @@ | ||
<!--GITHUB_BLOCK--> | ||
|
||
# Confirm | ||
|
||
<!--/GITHUB_BLOCK--> | ||
|
||
```tsx | ||
import {Confirm} from '@gravity-ui/components'; | ||
``` | ||
|
||
`Confirm` is a utility component, which renders confirmation dialogs | ||
|
||
## Properties | ||
|
||
| Name | Description | Type | Required | | ||
| :----------------- | :------------------------------------------------------------- | :-----------: | :------: | | ||
| title | The confirm dialog title | `string` | Yes | | ||
| cancelButtonText | The cancel button text | `string` | Yes | | ||
| confirmButtonText | The ok button text | `string` | Yes | | ||
| message | The confirmation message (used if the content is not provided) | `string` | | | ||
| content | The confirmation custom content | `ReactNode` | | | ||
| confirmButtonProps | The ok button props | `ButtonProps` | | | ||
| cancelButtonProps | The cancel buttonProps | `ButtonProps` | | | ||
|
||
And other Dialog props | ||
|
||
## Usage | ||
|
||
```tsx | ||
import {Confirm} from '@gravity-ui/components'; | ||
|
||
const [open, setOpen] = React.useState(false); | ||
|
||
return ( | ||
<React.Fragment> | ||
<Button view="normal" onClick={() => setOpen(true)}> | ||
Show confirm | ||
</Button> | ||
<Confirm | ||
{...args} | ||
title="Do you want to confirm?" | ||
onConfirm={() => { | ||
alert('Confirmed'); | ||
setOpen(false); | ||
}} | ||
onCancel={() => setOpen(false)} | ||
cancelButtonText="No" | ||
confirmButtonText="Yes" | ||
open={open} | ||
aria-labelledby="app-confirmation-dialog-title" | ||
/> | ||
</React.Fragment> | ||
); | ||
``` |
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,38 @@ | ||
import React from 'react'; | ||
|
||
import {Button} from '@gravity-ui/uikit'; | ||
import type {Meta, StoryFn} from '@storybook/react'; | ||
|
||
import {Confirm} from '../Confirm'; | ||
import type {ConfirmProps} from '../Confirm'; | ||
|
||
export default { | ||
title: 'Components/Confirm', | ||
component: Confirm, | ||
} as Meta<ConfirmProps>; | ||
|
||
const DefaultTemplate: StoryFn<ConfirmProps> = (args) => { | ||
const [open, setOpen] = React.useState(false); | ||
|
||
return ( | ||
<React.Fragment> | ||
<Button view="normal" onClick={() => setOpen(true)}> | ||
Show confirm | ||
</Button> | ||
<Confirm | ||
{...args} | ||
title="Do you want to confirm?" | ||
onConfirm={() => { | ||
alert('Confirmed'); | ||
setOpen(false); | ||
}} | ||
onCancel={() => setOpen(false)} | ||
cancelButtonText="No" | ||
confirmButtonText="Yes" | ||
open={open} | ||
aria-labelledby="app-confirmation-dialog-title" | ||
/> | ||
</React.Fragment> | ||
); | ||
}; | ||
export const Default = DefaultTemplate.bind({}); |
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 * from './Confirm'; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './useDialog'; |
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,7 @@ | ||
import React from 'react'; | ||
|
||
import type {DialogContextType} from './types'; | ||
|
||
export const DialogContext = React.createContext<DialogContextType>({ | ||
openDialog: () => Promise.resolve({success: false}), | ||
}); |
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 React from 'react'; | ||
|
||
import {getUniqId} from '@gravity-ui/uikit'; | ||
import omit from 'lodash/omit'; | ||
|
||
import {DialogContext} from './DialogContext'; | ||
import type {DialogRendererProps, DialogResult} from './types'; | ||
|
||
type DialogProviderProps = { | ||
children: React.ReactNode | React.ReactNode[]; | ||
}; | ||
|
||
export const DialogProvider = ({children}: DialogProviderProps) => { | ||
const [dialogs, setDialogs] = React.useState<Record<number, React.ReactNode>>({}); | ||
const dialogsRef: React.MutableRefObject<Record<number, React.ReactNode>> = | ||
React.useRef(dialogs); | ||
|
||
if (dialogsRef.current !== dialogs) { | ||
dialogsRef.current = dialogs; | ||
} | ||
|
||
const contextValue = React.useMemo( | ||
() => ({ | ||
openDialog: <ResultType extends unknown>( | ||
renderDialog: ({ | ||
onSuccess, | ||
asyncOnSuccess, | ||
onCancel, | ||
}: DialogRendererProps<ResultType>) => React.ReactNode, | ||
) => | ||
new Promise<{success: boolean; value?: ResultType}>((resolve) => { | ||
const key = getUniqId(); | ||
|
||
const handleClose = (result: DialogResult<ResultType>) => { | ||
resolve(result); | ||
|
||
setDialogs(omit(dialogsRef.current, key)); | ||
}; | ||
|
||
const handleSuccess = (value: ResultType) => { | ||
handleClose({success: true, value}); | ||
}; | ||
|
||
const handleSuccessPromise = ( | ||
getValue: Promise<ResultType>, | ||
onError: (error: unknown) => void, | ||
) => { | ||
getValue | ||
.then((value) => { | ||
handleClose({success: true, value}); | ||
}) | ||
.catch(onError); | ||
}; | ||
|
||
const handleCancel = () => { | ||
handleClose({success: false}); | ||
}; | ||
|
||
const dialog = renderDialog({ | ||
onSuccess: handleSuccess, | ||
asyncOnSuccess: handleSuccessPromise, | ||
onCancel: handleCancel, | ||
}); | ||
|
||
requestAnimationFrame(() => { | ||
setDialogs({ | ||
...dialogsRef.current, | ||
[key]: dialog, | ||
}); | ||
}); | ||
}), | ||
}), | ||
[], | ||
); | ||
|
||
return ( | ||
<DialogContext.Provider value={contextValue}> | ||
{children} | ||
{Object.entries(dialogs).map(([key, dialog]) => ( | ||
<React.Fragment key={key}>{dialog}</React.Fragment> | ||
))} | ||
</DialogContext.Provider> | ||
); | ||
}; |
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,109 @@ | ||
<!--GITHUB_BLOCK--> | ||
|
||
# useDialog | ||
|
||
<!--/GITHUB_BLOCK--> | ||
|
||
```tsx | ||
import {useDialog} from '@gravity-ui/components'; | ||
``` | ||
|
||
### Provider | ||
|
||
Before usage, you should wrap your components with `DialogProvider` | ||
|
||
```tsx | ||
import {DialogProvider} from '@gravity-ui/components'; | ||
|
||
<DialogProvider>{children}</DialogProvider>; | ||
``` | ||
|
||
`useDialog` is a utility hook, which allows you to open a dialog without adding a Dialog component to your code and controlling it's state | ||
|
||
## Returns | ||
|
||
Returns an object with `openDialog` property. `openDialog` is a generic function, which accepts a dialog content renderer and the dialog props. The content renderer provides an object | ||
`{ onSuccess, asyncOnSuccess, onCancel }`. | ||
|
||
- You should call `onSuccess` with the result of calling the dialog (in this case the promise result will be `{ success: true, value }`); | ||
- `asyncOnSuccess` with the promise which resolves the result (the promise dialog result will be `{ success: true, value }`, but the dialog won't resolve and close in case of an error occurred in the promise, passed with the arguments) | ||
- `onCancel` if you want to cancel the action (in this case the promise result will be `{ success: false }`). | ||
|
||
```ts | ||
function openDialog<ResultRype extends unknown>( | ||
renderContent: ({ | ||
onSuccess, | ||
asyncOnSuccess, | ||
onCancel, | ||
}: { | ||
onSuccess: (value: ResultType) => void; | ||
asyncOnSuccess: (getValue: Promise<ResultRype>, onError: (error: unknown) => void) => void; | ||
onCancel: () => void; | ||
}) => React.ReactNode | React.ReactNode[], | ||
dialogProps?: Partial<DialogProps>, | ||
): Promise<{ | ||
success: boolean; | ||
value?: ResultType; | ||
}>; | ||
``` | ||
|
||
## Examples | ||
|
||
#### Base | ||
|
||
```tsx | ||
import {Dialog} from '@gravity-ui/uikit'; | ||
import {useDialog} from '@gravity-ui/components'; | ||
|
||
const {openDialog} = useDialog(); | ||
|
||
const handleOpenNoteEditor = useCallback(async () => { | ||
const result = await openDialog<string>(({onSuccess, onCancel}) => ( | ||
<Dialog open onClose={onCancel} size="s"> | ||
<NoteEditor onSave={onSuccess} onCancel={onCancel} /> | ||
</Dialog> | ||
)); | ||
|
||
if (result.success) { | ||
const note = result.value; | ||
alert(`Your note is ${note}`); | ||
} else { | ||
alert('You cancelled creating the note'); | ||
} | ||
}, [openDialog]); | ||
``` | ||
|
||
#### Don't close dialog on error | ||
|
||
```tsx | ||
import {Dialog} from '@gravity-ui/uikit'; | ||
import {useDialog} from '@gravity-ui/components'; | ||
|
||
const {openDialog} = useDialog(); | ||
|
||
const handleOpenNoteEditor = useCallback(async () => { | ||
const handleSave = (note: string) => { | ||
return new Promise<string>((resolve, reject) => { | ||
if (note) { | ||
resolve(note); | ||
} else { | ||
alert('Enter the note'); | ||
reject(); | ||
} | ||
}); | ||
}; | ||
|
||
const result = await openDialog<string>( | ||
({asyncOnSuccess, onCancel}) => <Dialog open onClose={onCancel} size="s"> | ||
<NoteEditor onSave{(note: string) => asyncOnSuccess(handleSave(note), console.error)} onCancel={onCancel} /> | ||
</Dialog> | ||
); | ||
|
||
if (result.success) { | ||
const note = result.value; | ||
alert(`Your note is ${note}`); | ||
} else { | ||
alert('You cancelled creating the note'); | ||
} | ||
}, [openDialog]); | ||
``` |
Oops, something went wrong.