-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* feat(ui): #1807: add Toast v2 UI component * feat(ui): use sonner for UI v2 toasts * chore: changeset
- Loading branch information
Showing
7 changed files
with
887 additions
and
155 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 @@ | ||
--- | ||
'@penumbra-zone/ui': minor | ||
--- | ||
|
||
Add `ToastProvider` and `openToast` function to the v2 UI 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
|
||
import { openToast, ToastProvider, ToastType } from '.'; | ||
import { Button } from '../Button'; | ||
import { Tooltip } from '../Tooltip'; | ||
import { Text } from '../Text'; | ||
import { styled } from 'styled-components'; | ||
|
||
const meta: Meta<typeof ToastProvider> = { | ||
component: ToastProvider, | ||
tags: ['autodocs', '!dev'], | ||
argTypes: {}, | ||
}; | ||
export default meta; | ||
|
||
type Story = StoryObj<typeof ToastProvider>; | ||
|
||
const Row = styled.div` | ||
display: flex; | ||
flex-direction: row; | ||
gap: ${props => props.theme.spacing(2)}; | ||
`; | ||
|
||
export const Basic: Story = { | ||
render: function Render() { | ||
const toast = (type: ToastType) => { | ||
openToast({ | ||
type, | ||
message: 'Hello, world!', | ||
description: 'Additional text can possibly be long enough lorem ipsum dolor sit amet.', | ||
}); | ||
}; | ||
|
||
const upload = () => { | ||
const t = openToast({ | ||
type: 'loading', | ||
message: 'Hello, world!', | ||
}); | ||
|
||
setTimeout(() => { | ||
t.update({ | ||
type: 'error', | ||
message: 'Failed!', | ||
description: 'Unknown error', | ||
}); | ||
}, 2000); | ||
}; | ||
|
||
const action = () => { | ||
openToast({ | ||
type: 'warning', | ||
message: 'Do you confirm?', | ||
dismissible: false, | ||
persistent: true, | ||
action: { | ||
label: 'Yes!', | ||
onClick: () => { | ||
openToast({ | ||
type: 'success', | ||
message: 'Confirmed!', | ||
dismissible: false, | ||
}); | ||
}, | ||
}, | ||
}); | ||
}; | ||
|
||
return ( | ||
<> | ||
<ToastProvider /> | ||
|
||
<Text h4>All style types of toasts</Text> | ||
|
||
<Row> | ||
<Button onClick={() => toast('info')}>Info</Button> | ||
<Button onClick={() => toast('success')}>Success</Button> | ||
<Button onClick={() => toast('warning')}>Warning</Button> | ||
<Button onClick={() => toast('error')}>Error</Button> | ||
<Tooltip message='Cannot be closed by user until status is updated'> | ||
<Button onClick={() => toast('loading')}>Loading</Button> | ||
</Tooltip> | ||
</Row> | ||
|
||
<Text h4>Updating toast</Text> | ||
|
||
<Row> | ||
<Tooltip message='Starts as a loading toast, after 2 seconds updated to the error type'> | ||
<Button onClick={upload}>Open</Button> | ||
</Tooltip> | ||
</Row> | ||
|
||
<Text h4>Action toast</Text> | ||
|
||
<Row> | ||
<Button onClick={action}>Open</Button> | ||
</Row> | ||
</> | ||
); | ||
}, | ||
}; |
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,3 @@ | ||
export { ToastProvider } from './provider'; | ||
export { openToast } from './open'; | ||
export type { Toast, ToastProps, ToastType } from './open'; |
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,107 @@ | ||
import { toast, ExternalToast } from 'sonner'; | ||
import { ReactNode } from 'react'; | ||
|
||
export type ToastType = 'success' | 'info' | 'warning' | 'error' | 'loading'; | ||
type ToastFn = (message: ReactNode, options?: ExternalToast) => string | number; | ||
type ToastId = string | number; | ||
|
||
const toastFnMap: Record<ToastType, ToastFn> = { | ||
success: toast.success, | ||
info: toast.info, | ||
warning: toast.warning, | ||
error: toast.error, | ||
loading: toast.loading, | ||
}; | ||
|
||
export interface ToastProps { | ||
type: ToastType; | ||
message: string; | ||
description?: string; | ||
persistent?: boolean; | ||
dismissible?: boolean; | ||
action?: ExternalToast['action']; | ||
} | ||
|
||
export interface Toast { | ||
update: (newProps: Partial<ToastProps>) => void; | ||
dismiss: VoidFunction; | ||
} | ||
|
||
/** | ||
* If `<ToastProvider />` exists in the document, opens a toast with provided type and options. | ||
* By default, the toast is dismissible and has a duration of 4000 milliseconds. It can | ||
* be programmatically updated to another type and content without re-opening the toast. | ||
* | ||
* Example: | ||
* | ||
* ```tsx | ||
* import { ToastProvider, openToast } from '@penumbra-zone/ui/Toast'; | ||
* import { ToastProvider, openToast } from '@penumbra-zone/ui/Button'; | ||
* | ||
* const Component = () => { | ||
* const open = () => { | ||
* const toast = openToast({ | ||
* type: 'loading', | ||
* message: 'Loading...', | ||
* }); | ||
* | ||
* setTimeout(() => { | ||
* toast.update({ | ||
* type: 'error', | ||
* message: 'Failed!', | ||
* description: 'Unknown error' | ||
* }); | ||
* }, 2000); | ||
* }; | ||
* | ||
* return ( | ||
* <> | ||
* <ToastProvider /> | ||
* <Button onClick={open}>Open</Button> | ||
* </> | ||
* ); | ||
* }; | ||
* ``` | ||
*/ | ||
export const openToast = (props: ToastProps): Toast => { | ||
let options = props; | ||
let id: ToastId | undefined = undefined; | ||
|
||
const open = () => { | ||
const fn = toastFnMap[options.type]; | ||
|
||
id = fn(options.message, { | ||
id, | ||
description: options.description, | ||
closeButton: options.dismissible ?? true, | ||
dismissible: options.dismissible ?? true, | ||
duration: options.persistent ? Infinity : 4000, | ||
action: options.action, | ||
}); | ||
}; | ||
|
||
const dismiss: Toast['dismiss'] = () => { | ||
if (typeof id === 'undefined') { | ||
return; | ||
} | ||
|
||
toast.dismiss(id); | ||
id = undefined; | ||
}; | ||
|
||
const update: Toast['update'] = newProps => { | ||
if (typeof id === 'undefined') { | ||
return; | ||
} | ||
|
||
options = { ...options, ...newProps }; | ||
open(); | ||
}; | ||
|
||
open(); | ||
|
||
return { | ||
dismiss, | ||
update, | ||
}; | ||
}; |
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,41 @@ | ||
import { Toaster } from 'sonner'; | ||
|
||
/** | ||
* If `<ToastProvider />` exists in the document, you can call `openToast` function to open a toast with provided type and options. | ||
* By default, the toast is dismissible and has a duration of 4000 milliseconds. It can | ||
* be programmatically updated to another type and content without re-opening the toast. | ||
* | ||
* Example: | ||
* | ||
* ```tsx | ||
* import { ToastProvider, openToast } from '@penumbra-zone/ui/Toast'; | ||
* import { ToastProvider, openToast } from '@penumbra-zone/ui/Button'; | ||
* | ||
* const Component = () => { | ||
* const open = () => { | ||
* const toast = openToast({ | ||
* type: 'loading', | ||
* message: 'Loading...', | ||
* }); | ||
* | ||
* setTimeout(() => { | ||
* toast.update({ | ||
* type: 'error', | ||
* message: 'Failed!', | ||
* description: 'Unknown error' | ||
* }); | ||
* }, 2000); | ||
* }; | ||
* | ||
* return ( | ||
* <> | ||
* <ToastProvider /> | ||
* <Button onClick={open}>Open</Button> | ||
* </> | ||
* ); | ||
* }; | ||
* ``` | ||
*/ | ||
export const ToastProvider: typeof Toaster = ({ ...props }) => { | ||
return <Toaster theme='dark' richColors expand {...props} />; | ||
}; |
Oops, something went wrong.