diff --git a/docs/data/toolpad/core/components/use-dialogs/AlertDialog.js b/docs/data/toolpad/core/components/use-dialogs/AlertDialog.js new file mode 100644 index 00000000000..a82e0bfb66b --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/AlertDialog.js @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { DialogsProvider, useDialogs } from '@toolpad/core/useDialogs'; +import Button from '@mui/material/Button'; + +function DemoContent() { + const dialogs = useDialogs(); + return ( +
+ +
+ ); +} + +export default function AlertDialog() { + return ( + + + + ); +} diff --git a/docs/data/toolpad/core/components/use-dialogs/AlertDialog.tsx b/docs/data/toolpad/core/components/use-dialogs/AlertDialog.tsx new file mode 100644 index 00000000000..a82e0bfb66b --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/AlertDialog.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { DialogsProvider, useDialogs } from '@toolpad/core/useDialogs'; +import Button from '@mui/material/Button'; + +function DemoContent() { + const dialogs = useDialogs(); + return ( +
+ +
+ ); +} + +export default function AlertDialog() { + return ( + + + + ); +} diff --git a/docs/data/toolpad/core/components/use-dialogs/AlertDialog.tsx.preview b/docs/data/toolpad/core/components/use-dialogs/AlertDialog.tsx.preview new file mode 100644 index 00000000000..4f81c7d194c --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/AlertDialog.tsx.preview @@ -0,0 +1 @@ +await dialogs.alert('Hello World'); \ No newline at end of file diff --git a/docs/data/toolpad/core/components/use-dialogs/ConfirmDialog.js b/docs/data/toolpad/core/components/use-dialogs/ConfirmDialog.js new file mode 100644 index 00000000000..d28e8a7ddfa --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/ConfirmDialog.js @@ -0,0 +1,36 @@ +import * as React from 'react'; +import { DialogsProvider, useDialogs } from '@toolpad/core/useDialogs'; +import Button from '@mui/material/Button'; + +function DemoContent() { + const dialogs = useDialogs(); + return ( +
+ +
+ ); +} + +export default function ConfirmDialog() { + return ( + + + + ); +} diff --git a/docs/data/toolpad/core/components/use-dialogs/ConfirmDialog.tsx b/docs/data/toolpad/core/components/use-dialogs/ConfirmDialog.tsx new file mode 100644 index 00000000000..d28e8a7ddfa --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/ConfirmDialog.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import { DialogsProvider, useDialogs } from '@toolpad/core/useDialogs'; +import Button from '@mui/material/Button'; + +function DemoContent() { + const dialogs = useDialogs(); + return ( +
+ +
+ ); +} + +export default function ConfirmDialog() { + return ( + + + + ); +} diff --git a/docs/data/toolpad/core/components/use-dialogs/ConfirmDialog.tsx.preview b/docs/data/toolpad/core/components/use-dialogs/ConfirmDialog.tsx.preview new file mode 100644 index 00000000000..9500e780731 --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/ConfirmDialog.tsx.preview @@ -0,0 +1,9 @@ +const confirmed = await dialogs.confirm('Are you sure?', { + okText: 'Yes', + cancelText: 'No', +}); +if (confirmed) { + await dialogs.alert("Then let's do it!"); +} else { + await dialogs.alert('Ok, forget about it!'); +} \ No newline at end of file diff --git a/docs/data/toolpad/core/components/use-dialogs/CustomDialog.js b/docs/data/toolpad/core/components/use-dialogs/CustomDialog.js new file mode 100644 index 00000000000..cd1b47cd5a8 --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/CustomDialog.js @@ -0,0 +1,61 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { DialogsProvider, useDialogs } from '@toolpad/core/useDialogs'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; + +function MyCustomDialog({ open, onClose }) { + return ( + onClose()}> + Custom dialog + I am a custom dialog + + + + + ); +} + +MyCustomDialog.propTypes = { + /** + * A function to call when the dialog should be closed. If the dialog has a return + * value, it should be passed as an argument to this function. You should use the promise + * that is returned to show a loading state while the dialog is performing async actions + * on close. + * @param result The result to return from the dialog. + * @returns A promise that resolves when the dialog can be fully closed. + */ + onClose: PropTypes.func.isRequired, + /** + * Whether the dialog is open. + */ + open: PropTypes.bool.isRequired, +}; + +function DemoContent() { + const dialogs = useDialogs(); + return ( +
+ +
+ ); +} + +export default function CustomDialog() { + return ( + + + + ); +} diff --git a/docs/data/toolpad/core/components/use-dialogs/CustomDialog.tsx b/docs/data/toolpad/core/components/use-dialogs/CustomDialog.tsx new file mode 100644 index 00000000000..dde8f93c14d --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/CustomDialog.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import { DialogsProvider, useDialogs, DialogProps } from '@toolpad/core/useDialogs'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; + +function MyCustomDialog({ open, onClose }: DialogProps) { + return ( + onClose()}> + Custom dialog + I am a custom dialog + + + + + ); +} + +function DemoContent() { + const dialogs = useDialogs(); + return ( +
+ +
+ ); +} + +export default function CustomDialog() { + return ( + + + + ); +} diff --git a/docs/data/toolpad/core/components/use-dialogs/CustomDialog.tsx.preview b/docs/data/toolpad/core/components/use-dialogs/CustomDialog.tsx.preview new file mode 100644 index 00000000000..3c31a5c7950 --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/CustomDialog.tsx.preview @@ -0,0 +1 @@ +await dialogs.open(MyCustomDialog); \ No newline at end of file diff --git a/docs/data/toolpad/core/components/use-dialogs/CustomDialogWithPayload.js b/docs/data/toolpad/core/components/use-dialogs/CustomDialogWithPayload.js new file mode 100644 index 00000000000..c53ce5083e7 --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/CustomDialogWithPayload.js @@ -0,0 +1,73 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { DialogsProvider, useDialogs } from '@toolpad/core/useDialogs'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; +import TextField from '@mui/material/TextField'; +import Stack from '@mui/material/Stack'; + +function MyCustomDialog({ payload, open, onClose }) { + return ( + onClose()}> + Dialog with payload + The payload is "{payload}" + + + + + ); +} + +MyCustomDialog.propTypes = { + /** + * A function to call when the dialog should be closed. If the dialog has a return + * value, it should be passed as an argument to this function. You should use the promise + * that is returned to show a loading state while the dialog is performing async actions + * on close. + * @param result The result to return from the dialog. + * @returns A promise that resolves when the dialog can be fully closed. + */ + onClose: PropTypes.func.isRequired, + /** + * Whether the dialog is open. + */ + open: PropTypes.bool.isRequired, + /** + * The payload that was passed when the dialog was opened. + */ + payload: PropTypes.string.isRequired, +}; + +function DemoContent() { + const dialogs = useDialogs(); + const [payload, setPayload] = React.useState('Some payload'); + return ( + + setPayload(event.currentTarget.value)} + /> + + + ); +} + +export default function CustomDialogWithPayload() { + return ( + + + + ); +} diff --git a/docs/data/toolpad/core/components/use-dialogs/CustomDialogWithPayload.tsx b/docs/data/toolpad/core/components/use-dialogs/CustomDialogWithPayload.tsx new file mode 100644 index 00000000000..7705547d2ca --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/CustomDialogWithPayload.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import { DialogsProvider, useDialogs, DialogProps } from '@toolpad/core/useDialogs'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; +import TextField from '@mui/material/TextField'; +import Stack from '@mui/material/Stack'; + +function MyCustomDialog({ payload, open, onClose }: DialogProps) { + return ( + onClose()}> + Dialog with payload + The payload is "{payload}" + + + + + ); +} + +function DemoContent() { + const dialogs = useDialogs(); + const [payload, setPayload] = React.useState('Some payload'); + return ( + + setPayload(event.currentTarget.value)} + /> + + + ); +} + +export default function CustomDialogWithPayload() { + return ( + + + + ); +} diff --git a/docs/data/toolpad/core/components/use-dialogs/CustomDialogWithPayload.tsx.preview b/docs/data/toolpad/core/components/use-dialogs/CustomDialogWithPayload.tsx.preview new file mode 100644 index 00000000000..1a23cecf043 --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/CustomDialogWithPayload.tsx.preview @@ -0,0 +1 @@ +await dialogs.open(MyCustomDialog, payload); \ No newline at end of file diff --git a/docs/data/toolpad/core/components/use-dialogs/CustomDialogWithResult.js b/docs/data/toolpad/core/components/use-dialogs/CustomDialogWithResult.js new file mode 100644 index 00000000000..f224de43d21 --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/CustomDialogWithResult.js @@ -0,0 +1,74 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { DialogsProvider, useDialogs } from '@toolpad/core/useDialogs'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; +import TextField from '@mui/material/TextField'; +import Stack from '@mui/material/Stack'; +import DialogContentText from '@mui/material/DialogContentText'; + +function MyCustomDialog({ open, onClose }) { + const [result, setResult] = React.useState('Jon Snow'); + return ( + onClose(null)}> + Dialog with payload + + What is your name? + setResult(event.currentTarget.value)} + /> + + + + + + ); +} + +MyCustomDialog.propTypes = { + /** + * A function to call when the dialog should be closed. If the dialog has a return + * value, it should be passed as an argument to this function. You should use the promise + * that is returned to show a loading state while the dialog is performing async actions + * on close. + * @param result The result to return from the dialog. + * @returns A promise that resolves when the dialog can be fully closed. + */ + onClose: PropTypes.func.isRequired, + /** + * Whether the dialog is open. + */ + open: PropTypes.bool.isRequired, +}; + +function DemoContent() { + const dialogs = useDialogs(); + return ( + + + + ); +} + +export default function CustomDialogWithResult() { + return ( + + + + ); +} diff --git a/docs/data/toolpad/core/components/use-dialogs/CustomDialogWithResult.tsx b/docs/data/toolpad/core/components/use-dialogs/CustomDialogWithResult.tsx new file mode 100644 index 00000000000..518095645d7 --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/CustomDialogWithResult.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import { DialogsProvider, useDialogs, DialogProps } from '@toolpad/core/useDialogs'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; +import TextField from '@mui/material/TextField'; +import Stack from '@mui/material/Stack'; +import DialogContentText from '@mui/material/DialogContentText'; + +function MyCustomDialog({ open, onClose }: DialogProps) { + const [result, setResult] = React.useState('Jon Snow'); + return ( + onClose(null)}> + Dialog with payload + + What is your name? + setResult(event.currentTarget.value)} + /> + + + + + + ); +} + +function DemoContent() { + const dialogs = useDialogs(); + return ( + + + + ); +} + +export default function CustomDialogWithResult() { + return ( + + + + ); +} diff --git a/docs/data/toolpad/core/components/use-dialogs/CustomDialogWithResult.tsx.preview b/docs/data/toolpad/core/components/use-dialogs/CustomDialogWithResult.tsx.preview new file mode 100644 index 00000000000..7a20a35ab3e --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/CustomDialogWithResult.tsx.preview @@ -0,0 +1,2 @@ +const result = await dialogs.open(MyCustomDialog); +await dialogs.alert(`Your name is "${result}"`); \ No newline at end of file diff --git a/docs/data/toolpad/core/components/use-dialogs/PromptDialog.js b/docs/data/toolpad/core/components/use-dialogs/PromptDialog.js new file mode 100644 index 00000000000..36977fbc84b --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/PromptDialog.js @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { DialogsProvider, useDialogs } from '@toolpad/core/useDialogs'; +import Button from '@mui/material/Button'; + +function DemoContent() { + const dialogs = useDialogs(); + return ( +
+ +
+ ); +} + +export default function PromptDialog() { + return ( + + + + ); +} diff --git a/docs/data/toolpad/core/components/use-dialogs/PromptDialog.tsx b/docs/data/toolpad/core/components/use-dialogs/PromptDialog.tsx new file mode 100644 index 00000000000..36977fbc84b --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/PromptDialog.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { DialogsProvider, useDialogs } from '@toolpad/core/useDialogs'; +import Button from '@mui/material/Button'; + +function DemoContent() { + const dialogs = useDialogs(); + return ( +
+ +
+ ); +} + +export default function PromptDialog() { + return ( + + + + ); +} diff --git a/docs/data/toolpad/core/components/use-dialogs/PromptDialog.tsx.preview b/docs/data/toolpad/core/components/use-dialogs/PromptDialog.tsx.preview new file mode 100644 index 00000000000..b20e0553547 --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/PromptDialog.tsx.preview @@ -0,0 +1,4 @@ +const name = await dialogs.prompt("What's your name?"); +if (name) { + await dialogs.alert(`Hi there, ${name}`); +} \ No newline at end of file diff --git a/docs/data/toolpad/core/components/use-dialogs/StackedDialog.js b/docs/data/toolpad/core/components/use-dialogs/StackedDialog.js new file mode 100644 index 00000000000..86aec23eb80 --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/StackedDialog.js @@ -0,0 +1,74 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { DialogsProvider, useDialogs } from '@toolpad/core/useDialogs'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; + +function MyCustomDialog({ payload, open, onClose }) { + const dialogs = useDialogs(); + return ( + onClose()}> + Dialog {payload} + + + + + + + + ); +} + +MyCustomDialog.propTypes = { + /** + * A function to call when the dialog should be closed. If the dialog has a return + * value, it should be passed as an argument to this function. You should use the promise + * that is returned to show a loading state while the dialog is performing async actions + * on close. + * @param result The result to return from the dialog. + * @returns A promise that resolves when the dialog can be fully closed. + */ + onClose: PropTypes.func.isRequired, + /** + * Whether the dialog is open. + */ + open: PropTypes.bool.isRequired, + /** + * The payload that was passed when the dialog was opened. + */ + payload: PropTypes.number.isRequired, +}; + +function DemoContent() { + const dialogs = useDialogs(); + return ( +
+ +
+ ); +} + +export default function StackedDialog() { + return ( + + + + ); +} diff --git a/docs/data/toolpad/core/components/use-dialogs/StackedDialog.tsx b/docs/data/toolpad/core/components/use-dialogs/StackedDialog.tsx new file mode 100644 index 00000000000..74cac321345 --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/StackedDialog.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import { DialogsProvider, useDialogs, DialogProps } from '@toolpad/core/useDialogs'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; + +function MyCustomDialog({ payload, open, onClose }: DialogProps) { + const dialogs = useDialogs(); + return ( + onClose()}> + Dialog {payload} + + + + + + + + ); +} + +function DemoContent() { + const dialogs = useDialogs(); + return ( +
+ +
+ ); +} + +export default function StackedDialog() { + return ( + + + + ); +} diff --git a/docs/data/toolpad/core/components/use-dialogs/StackedDialog.tsx.preview b/docs/data/toolpad/core/components/use-dialogs/StackedDialog.tsx.preview new file mode 100644 index 00000000000..45e7f84f761 --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/StackedDialog.tsx.preview @@ -0,0 +1 @@ +await dialogs.open(MyCustomDialog, payload + 1); \ No newline at end of file diff --git a/docs/data/toolpad/core/components/use-dialogs/use-dialogs-api.md b/docs/data/toolpad/core/components/use-dialogs/use-dialogs-api.md new file mode 100644 index 00000000000..de5b34176ac --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/use-dialogs-api.md @@ -0,0 +1,176 @@ +# useDialogs API + +

API reference for the useDialogs hook.

+ +:::success +For examples and details on the usage of this React hook, visit the demo pages: + +- [useDialogs](/toolpad/core/react-use-dialogs/) + +::: + +## Import + +```js +import useDialogs from '@toolpad/core/useDialogs'; +// or +import { useDialogs } from '@toolpad/core'; +``` + +Learn about the difference by reading this [guide](https://mui.com/material-ui/guides/minimizing-bundle-size/) on minimizing bundle size. + +## Usage + +Get access to the dialogs API by invoking the hook. + +```js +const dialogs = useDialogs(); +``` + +## Reference + +### `dialogs.alert` + +Open an alert dialog, returns a promise that resolves when the user closes the dialog. + +```js +await dialogs.alert('Something went wrong', { + title: 'Attention!', +}); +``` + +**Signature:** + +```js +function alert(message, options?) +``` + +**Parameters:** + +- `message`: `React.ReactNode` The message to display in the alert dialog. +- `options?`: `object` Extra configuration for the alert dialog. + - `onClose?`: `() => Promise` A function that is called before closing the dialog closes. The dialog stays open as long as the returned promise is not resolved. Use this if you want to perform an async action on close and show a loading state. + - `title?`: `React.ReactNode` A title for the dialog. Defaults to `'Alert'`. + - `okText?`: `React.ReactNode` The text to show in the "Ok" button. Defaults to `'Ok'`. + +**Returns:** + +`Promise` A promise that resolves once the user has dismissed the dialog. + +### `dialogs.confirm` + +Open a confirmation dialog. Returns a promise that resolves to true if the user confirms, false if the user cancels. + +```js +const confirmed = await dialogs.confirm('Are you sure?', { + okText: 'Yes', + cancelText: 'No', +}); +``` + +**Signature:** + +```js +function confirm(message, options?) +``` + +**Parameters:** + +- `message`: `React.ReactNode` The message to display in the confirmation dialog. +- `options?`: `object` Extra configuration for the confirmation dialog. + - `onClose?`: `(result: boolean) => Promise` A function that is called before closing the dialog closes. The dialog stays open as long as the returned promise is not resolved. Use this if you want to perform an async action on close and show a loading state. + - `title?`: `React.ReactNode` A title for the dialog. Defaults to `'Confirm'`. + - `okText?`: `React.ReactNode` The text to show in the "Ok" button. Defaults to `'Ok'`. + - `severity?`: `'error' | 'info' | 'success' | 'warning'` Denotes the purpose of the dialog. This will affect the color of the "Ok" button. Defaults to `undefined`. + - `cancelText?`: `React.ReactNode` The text to show in the "Cancel" button. Defaults to `'Cancel'`. + +**Returns:** + +`Promise` A promise that resolves to true if the user confirms, false if the user cancels. + +### `dialogs.prompt` + +Open a prompt dialog to request user input. Returns a promise that resolves to the input if the user confirms, null if the user cancels. + +```js +const input = await dialogs.prompt('What is your name?', { + cancelText: 'Leave me alone', +}); +``` + +**Signature:** + +```js +function prompt(message, options?) +``` + +**Parameters:** + +- `message`: `React.ReactNode` The message to display in the prompt dialog. +- `options?`: `object` Extra configuration for the prompt dialog. + - `onClose?`: `(result: string) => Promise` A function that is called before closing the dialog closes. The dialog stays open as long as the returned promise is not resolved. Use this if you want to perform an async action on close and show a loading state. + - `title?`: `React.ReactNode` A title for the dialog. Defaults to `'Prompt'`. + - `okText?`: `React.ReactNode` The text to show in the "Ok" button. Defaults to `'Ok'`. + - `cancelText?`: `React.ReactNode` The text to show in the "Cancel" button. Defaults to `'Cancel'`. + +**Returns:** + +`Promise` A promise that resolves to the user input if the user confirms, null if the user cancels. + +### `dialogs.open` + +Open a custom dialog. The dialog is a React component that optionally takes a payload and optionally returns a result in its `onClose` property. + +```js +function MyDialog({ open, onClose, payload }) { + // ... +} + +const result = await dialogs.open(MyDialog, 123, { + onClose: async (result) => callApi(result), +}); +``` + +**Signature:** + +```js +function open(component, payload, options?) +``` + +**Parameters:** + +- `component`: `React.ComponentType<{ open: boolean, onClose: (result: R) => Promise, payload: P }>` The dialog component to display. +- `payload?`: `P` The optional payload passed to the dialog. Useful if you want to parametrize the dialog, or use instance specific data. This value will be kept constant during the lifetime of the dialog. +- `options?`: `object` Extra configuration for the dialog. + - `onClose?`: `(result: R) => Promise` A function that is called before closing the dialog closes. The dialog stays open as long as the returned promise is not resolved. Use this if you want to perform an async action on close and show a loading state. + +**Returns:** + +`Promise` A promise that resolves to the user input if the user confirms, null if the user cancels. + +### `dialogs.close` + +Programmatically close a dialog that was previously opened with `dialogs.open`. If the dialog returns a result, `close` must also be called with a result. The original dialog promise will be resolved with this result. This promise is also returned from the `close` function. + +```js +const myDialog = dialogs.open(/* ... */); + +// ... + +const result = await dialogs.close(myDialog, 123); +``` + +**Signature:** + +```js +function close(dialog, result) +``` + +**Parameters:** + +- `dialog`: `Promise` The dialog to close. This should be a promise that was previously returned by `dialogs.open`. +- `result?`: `R` The result to return from the dialog. + +**Returns:** + +`Promise` A promise that resolves with the dialog result when the dialog is fully closed. diff --git a/docs/data/toolpad/core/components/use-dialogs/use-dialogs.md b/docs/data/toolpad/core/components/use-dialogs/use-dialogs.md new file mode 100644 index 00000000000..703134587a2 --- /dev/null +++ b/docs/data/toolpad/core/components/use-dialogs/use-dialogs.md @@ -0,0 +1,104 @@ +--- +productId: toolpad-core +title: useDialogs +components: DialogsProvider +--- + +# Dialogs + +

Imperative APIs to open and interact with dialogs.

+ +Toolpad core offers a set of abstractions that makes interacting with dialogs simpler. It has an imperative API to open and close dialogs, and allows dialogs to be stacked on top of each other. + +First thing you need to do is install the DialogsProvider at the root of your application. + +```tsx +import { DialogsProvider } from '@toolpad/core/useDialogs'; + +function App({ children }) { + return {children}; +} +``` + +To get access to the dialogs API you first have to call the `useDialogs` hook. + +```js +import { useDialogs } from '@toolpad/core/useDialogs'; + +function MyApp() { + const dialogs = useDialogs(); + // ... +} +``` + +:::info +The Toolpad AppProvider automatically comes with dialogs enabled. You won't need to explicitly add the DialogsProvider in Toolpad applications. +::: + +## Basic dialog + +Dialogs are React components that taken `open` and `onClose` properties and return a Dialog component. The `open` property reflects the open state of the dialog and you can call the `onClose` handler to close it. + +```js +function MyCustomDialog({ open, onClose }: DialogProps) { + return ( + onClose()}> + Custom dialog + I am a custom dialog + + + + + ); +} +``` + +Now you can call the `dialogs.open` function and pass the component as a first parameter. + +{{"demo": "CustomDialog.js"}} + +## With dialog payload + +You can pass a `payload` to the dialog with the second parameter. The payload stays constant for the lifetime of the dialog. + +{{"demo": "CustomDialogWithPayload.js"}} + +## With dialog result + +A dialog can return a value with the `onClose` handler. The promise returned by the `open` method is resolved with the value that was passed to `onClose`. + +{{"demo": "CustomDialogWithResult.js"}} + +## Stacked dialogs + +Dialogs can be stacked. A dialog can open other another dialog which comes to the foreground upon opening. Closing the latter reveals the former again. + +{{"demo": "StackedDialog.js"}} + +## System dialogs + +Toolpad comes with a set of system dialogs that improve on the native `window.alert`, `window.confirm`, and `window.prompt` APIs. These APIs are very similar, but they create dialogs that follow your application theme. + +### Alert + +Analog to [`window.alert`](https://developer.mozilla.org/en-US/docs/Web/API/Window/alert) it opens a dialog with a message for the user. The only action to be taken is to acknowledge the message after which the dialog closes. +The dialog title and button text are customizable with the `title` and `okText` properties. + +{{"demo": "AlertDialog.js"}} + +### Confirm + +Analog to [`window.confirm`](https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm) it opens a dialog with a question for the user. The user can either confirm or cancel and the dialog result is a boolean which is `true` when the user confirmed. +The dialog title and button texts are customizable with the `title`, `okText`, and `cancelText` properties. + +{{"demo": "ConfirmDialog.js"}} + +### Prompt + +Analog to [`window.prompt`](https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt) it opens a dialog inquiring the user for some input text. The user can fill the input box and upon confirmation the promise returned from the `prompt` call is resolved with its value. The dialog title and button texts are customizable with the `title`, `okText`, and `cancelText` properties. + +{{"demo": "PromptDialog.js"}} + +## Hook API + +- [`useDialogs()`](/toolpad/core/react-use-dialogs/api/) diff --git a/docs/data/toolpad/core/components/use-notifications/AlertNotification.js b/docs/data/toolpad/core/components/use-notifications/AlertNotification.js new file mode 100644 index 00000000000..06da677b18d --- /dev/null +++ b/docs/data/toolpad/core/components/use-notifications/AlertNotification.js @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { useNotifications } from '@toolpad/core/useNotifications'; +import Button from '@mui/material/Button'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormControl from '@mui/material/FormControl'; +import FormLabel from '@mui/material/FormLabel'; +import RadioGroup from '@mui/material/RadioGroup'; +import Radio from '@mui/material/Radio'; +import Stack from '@mui/material/Stack'; + +export default function AlertNotification() { + const notifications = useNotifications(); + const [severity, setSeverity] = React.useState('info'); + return ( +
+ + + Severity + setSeverity(event.target.value)} + aria-labelledby="alert-notification-severity" + name="severity" + > + } label="Info" /> + } label="Success" /> + } label="Warning" /> + } label="Error" /> + + + + +
+ ); +} diff --git a/docs/data/toolpad/core/components/use-notifications/AlertNotification.tsx b/docs/data/toolpad/core/components/use-notifications/AlertNotification.tsx new file mode 100644 index 00000000000..0a6f2a961a1 --- /dev/null +++ b/docs/data/toolpad/core/components/use-notifications/AlertNotification.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { useNotifications } from '@toolpad/core/useNotifications'; +import Button from '@mui/material/Button'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormControl from '@mui/material/FormControl'; +import FormLabel from '@mui/material/FormLabel'; +import RadioGroup from '@mui/material/RadioGroup'; +import Radio from '@mui/material/Radio'; +import Stack from '@mui/material/Stack'; + +export default function AlertNotification() { + const notifications = useNotifications(); + const [severity, setSeverity] = React.useState< + 'info' | 'success' | 'warning' | 'error' + >('info'); + return ( +
+ + + Severity + setSeverity(event.target.value as any)} + aria-labelledby="alert-notification-severity" + name="severity" + > + } label="Info" /> + } label="Success" /> + } label="Warning" /> + } label="Error" /> + + + + +
+ ); +} diff --git a/docs/data/toolpad/core/components/use-notifications/AlertNotification.tsx.preview b/docs/data/toolpad/core/components/use-notifications/AlertNotification.tsx.preview new file mode 100644 index 00000000000..354c4c2d4e5 --- /dev/null +++ b/docs/data/toolpad/core/components/use-notifications/AlertNotification.tsx.preview @@ -0,0 +1,4 @@ +notifications.show('Consider yourself notified!', { + severity, + autoHideDuration: 3000, +}); \ No newline at end of file diff --git a/docs/data/toolpad/core/components/use-notifications/BasicNotification.js b/docs/data/toolpad/core/components/use-notifications/BasicNotification.js new file mode 100644 index 00000000000..3c7cfc6903f --- /dev/null +++ b/docs/data/toolpad/core/components/use-notifications/BasicNotification.js @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { useNotifications } from '@toolpad/core/useNotifications'; +import Button from '@mui/material/Button'; + +export default function BasicNotification() { + const notifications = useNotifications(); + return ( +
+ +
+ ); +} diff --git a/docs/data/toolpad/core/components/use-notifications/BasicNotification.tsx b/docs/data/toolpad/core/components/use-notifications/BasicNotification.tsx new file mode 100644 index 00000000000..3c7cfc6903f --- /dev/null +++ b/docs/data/toolpad/core/components/use-notifications/BasicNotification.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { useNotifications } from '@toolpad/core/useNotifications'; +import Button from '@mui/material/Button'; + +export default function BasicNotification() { + const notifications = useNotifications(); + return ( +
+ +
+ ); +} diff --git a/docs/data/toolpad/core/components/use-notifications/BasicNotification.tsx.preview b/docs/data/toolpad/core/components/use-notifications/BasicNotification.tsx.preview new file mode 100644 index 00000000000..d782fa22794 --- /dev/null +++ b/docs/data/toolpad/core/components/use-notifications/BasicNotification.tsx.preview @@ -0,0 +1,3 @@ +notifications.show('Consider yourself notified!', { + autoHideDuration: 3000, +}); \ No newline at end of file diff --git a/docs/data/toolpad/core/components/use-notifications/CloseNotification.js b/docs/data/toolpad/core/components/use-notifications/CloseNotification.js new file mode 100644 index 00000000000..a8a0402e77a --- /dev/null +++ b/docs/data/toolpad/core/components/use-notifications/CloseNotification.js @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { useNotifications } from '@toolpad/core/useNotifications'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Switch from '@mui/material/Switch'; + +export default function CloseNotification() { + const notifications = useNotifications(); + const [online, setOnline] = React.useState(true); + const prevOnline = React.useRef(online); + React.useEffect(() => { + if (prevOnline.current === online) { + return () => {}; + } + prevOnline.current = online; + + // preview-start + const key = online + ? notifications.show('You are now online', { + severity: 'success', + autoHideDuration: 3000, + }) + : notifications.show('You are now offline', { + severity: 'error', + }); + + return () => { + notifications.close(key); + }; + // preview-end + }, [notifications, online]); + + return ( +
+ setOnline((prev) => !prev)} /> + } + label="Online" + /> +
+ ); +} diff --git a/docs/data/toolpad/core/components/use-notifications/CloseNotification.tsx b/docs/data/toolpad/core/components/use-notifications/CloseNotification.tsx new file mode 100644 index 00000000000..a8a0402e77a --- /dev/null +++ b/docs/data/toolpad/core/components/use-notifications/CloseNotification.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { useNotifications } from '@toolpad/core/useNotifications'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Switch from '@mui/material/Switch'; + +export default function CloseNotification() { + const notifications = useNotifications(); + const [online, setOnline] = React.useState(true); + const prevOnline = React.useRef(online); + React.useEffect(() => { + if (prevOnline.current === online) { + return () => {}; + } + prevOnline.current = online; + + // preview-start + const key = online + ? notifications.show('You are now online', { + severity: 'success', + autoHideDuration: 3000, + }) + : notifications.show('You are now offline', { + severity: 'error', + }); + + return () => { + notifications.close(key); + }; + // preview-end + }, [notifications, online]); + + return ( +
+ setOnline((prev) => !prev)} /> + } + label="Online" + /> +
+ ); +} diff --git a/docs/data/toolpad/core/components/use-notifications/CloseNotification.tsx.preview b/docs/data/toolpad/core/components/use-notifications/CloseNotification.tsx.preview new file mode 100644 index 00000000000..9d1eb56a00e --- /dev/null +++ b/docs/data/toolpad/core/components/use-notifications/CloseNotification.tsx.preview @@ -0,0 +1,12 @@ +const key = online + ? notifications.show('You are now online', { + severity: 'success', + autoHideDuration: 3000, + }) + : notifications.show('You are now offline', { + severity: 'error', + }); + +return () => { + notifications.close(key); +}; \ No newline at end of file diff --git a/docs/data/toolpad/core/components/use-notifications/DedupeNotification.js b/docs/data/toolpad/core/components/use-notifications/DedupeNotification.js new file mode 100644 index 00000000000..ba06386bf58 --- /dev/null +++ b/docs/data/toolpad/core/components/use-notifications/DedupeNotification.js @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { useNotifications } from '@toolpad/core/useNotifications'; +import Button from '@mui/material/Button'; + +export default function DedupeNotification() { + const notifications = useNotifications(); + return ( + // preview +
+ +
+ ); +} diff --git a/docs/data/toolpad/core/components/use-notifications/DedupeNotification.tsx b/docs/data/toolpad/core/components/use-notifications/DedupeNotification.tsx new file mode 100644 index 00000000000..ba06386bf58 --- /dev/null +++ b/docs/data/toolpad/core/components/use-notifications/DedupeNotification.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { useNotifications } from '@toolpad/core/useNotifications'; +import Button from '@mui/material/Button'; + +export default function DedupeNotification() { + const notifications = useNotifications(); + return ( + // preview +
+ +
+ ); +} diff --git a/docs/data/toolpad/core/components/use-notifications/DedupeNotification.tsx.preview b/docs/data/toolpad/core/components/use-notifications/DedupeNotification.tsx.preview new file mode 100644 index 00000000000..da00866cded --- /dev/null +++ b/docs/data/toolpad/core/components/use-notifications/DedupeNotification.tsx.preview @@ -0,0 +1,4 @@ +notifications.show('Listen carefully, I will say this only once', { + key: 'dedupe-notification', + autoHideDuration: 5000, +}); \ No newline at end of file diff --git a/docs/data/toolpad/core/components/use-notifications/MultipleNotifications.js b/docs/data/toolpad/core/components/use-notifications/MultipleNotifications.js new file mode 100644 index 00000000000..86d7a74e846 --- /dev/null +++ b/docs/data/toolpad/core/components/use-notifications/MultipleNotifications.js @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { useNotifications } from '@toolpad/core/useNotifications'; +import Button from '@mui/material/Button'; + +export default function MultipleNotifications() { + const notifications = useNotifications(); + return ( +
+ +
+ ); +} diff --git a/docs/data/toolpad/core/components/use-notifications/MultipleNotifications.tsx b/docs/data/toolpad/core/components/use-notifications/MultipleNotifications.tsx new file mode 100644 index 00000000000..86d7a74e846 --- /dev/null +++ b/docs/data/toolpad/core/components/use-notifications/MultipleNotifications.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { useNotifications } from '@toolpad/core/useNotifications'; +import Button from '@mui/material/Button'; + +export default function MultipleNotifications() { + const notifications = useNotifications(); + return ( +
+ +
+ ); +} diff --git a/docs/data/toolpad/core/components/use-notifications/MultipleNotifications.tsx.preview b/docs/data/toolpad/core/components/use-notifications/MultipleNotifications.tsx.preview new file mode 100644 index 00000000000..6e82e81e512 --- /dev/null +++ b/docs/data/toolpad/core/components/use-notifications/MultipleNotifications.tsx.preview @@ -0,0 +1,2 @@ +notifications.show('Hello', { autoHideDuration: 1000 }); +notifications.show('Goodbye', { autoHideDuration: 1000 }); \ No newline at end of file diff --git a/docs/data/toolpad/core/components/use-notifications/ScopedNotification.js b/docs/data/toolpad/core/components/use-notifications/ScopedNotification.js new file mode 100644 index 00000000000..0116e791617 --- /dev/null +++ b/docs/data/toolpad/core/components/use-notifications/ScopedNotification.js @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { + NotificationsProvider, + useNotifications, +} from '@toolpad/core/useNotifications'; +import Button from '@mui/material/Button'; +import { Box, Snackbar, styled } from '@mui/material'; + +const notificationsProviderSlots = { + snackbar: styled(Snackbar)({ position: 'absolute' }), +}; + +function ScopedContent() { + const notifications = useNotifications(); + return ( + + + + ); +} + +export default function ScopedNotification() { + return ( + + + + + + ); +} diff --git a/docs/data/toolpad/core/components/use-notifications/ScopedNotification.tsx b/docs/data/toolpad/core/components/use-notifications/ScopedNotification.tsx new file mode 100644 index 00000000000..e10db7591a5 --- /dev/null +++ b/docs/data/toolpad/core/components/use-notifications/ScopedNotification.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { + NotificationsProvider, + useNotifications, + NotificationsProviderSlots, +} from '@toolpad/core/useNotifications'; +import Button from '@mui/material/Button'; +import { Box, Snackbar, styled } from '@mui/material'; + +const notificationsProviderSlots: NotificationsProviderSlots = { + snackbar: styled(Snackbar)({ position: 'absolute' }), +}; + +function ScopedContent() { + const notifications = useNotifications(); + return ( + + + + ); +} + +export default function ScopedNotification() { + return ( + + + + + + ); +} diff --git a/docs/data/toolpad/core/components/use-notifications/ScopedNotification.tsx.preview b/docs/data/toolpad/core/components/use-notifications/ScopedNotification.tsx.preview new file mode 100644 index 00000000000..63b153cf7dd --- /dev/null +++ b/docs/data/toolpad/core/components/use-notifications/ScopedNotification.tsx.preview @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/docs/data/toolpad/core/components/use-notifications/use-notifications-api.md b/docs/data/toolpad/core/components/use-notifications/use-notifications-api.md new file mode 100644 index 00000000000..9c9f4ee64ad --- /dev/null +++ b/docs/data/toolpad/core/components/use-notifications/use-notifications-api.md @@ -0,0 +1,85 @@ +# useNotifications API + +

API reference for the useNotifications hook.

+ +:::success +For examples and details on the usage of this React hook, visit the demo pages: + +- [useNotifications](/toolpad/core/react-use-notifications/) + +::: + +## Import + +```js +import useNotifications from '@toolpad/core/useNotifications'; +// or +import { useNotifications } from '@toolpad/core'; +``` + +Learn about the difference by reading this [guide](https://mui.com/material-ui/guides/minimizing-bundle-size/) on minimizing bundle size. + +## Usage + +Get access to the notifications API by invoking the hook. + +```js +const notifications = useNotifications(); +``` + +## Reference + +### `notifications.show` + +Call the `notifications.show` method to show a snackbar in the application. + +```js +const notification = notifications.show('Something great just happened!', { + severity: 'success', +}); +``` + +**Signature:** + +```js +function show(message, options?) +``` + +**Parameters:** + +- `message`: `React.ReactNode` The message to show in the snackbar. +- `options?`: `object` An options object to configure the notification. -`key?`: `string` The key to use for deduping notifications. If not provided, a unique key will be generated. + - `severity?`: `'info' | 'warning' | 'error' | 'success'` The severity of the notification. When provided, the snackbar will show an alert with the specified severity. Defaults to `undefined`. + - `autoHideDuration?`: `number` The duration in milliseconds after which the notification will automatically close. By default notifications don't hide automatically. + - `actionText?`: `React.ReactNode` The text to display on the action button. + - `onAction?`: `() => void` The callback to call when the action button is clicked. + +**Returns** + +A unique key that can be used to close the notification programmatically. + +### `notifications.close` + +programmaticaly closes a notification. + +```js +const notification = notifications.show(/* ... */); + +// ... + +notifications.close(notification); +``` + +**Signature:** + +```js +function close(notification) +``` + +**Parameters** + +- `key`: `string` + +**Returns** + +`void` diff --git a/docs/data/toolpad/core/components/use-notifications/use-notifications.md b/docs/data/toolpad/core/components/use-notifications/use-notifications.md new file mode 100644 index 00000000000..c500eadfec3 --- /dev/null +++ b/docs/data/toolpad/core/components/use-notifications/use-notifications.md @@ -0,0 +1,80 @@ +--- +productId: toolpad-core +title: useNotifications +components: NotificationsProvider +--- + +# Notifications + +

Imperative APIs to show and interact with application notifications

+ +Toolpad core offers a set of abstractions that make it easier to interact with notifications. Notifications are used to give short updates to the user about things that are happening during the application lifetime. They appear at the bottom of the screen. The Toolpad API allows for opening multiple notifications concurrenlty. + +First thing you need to do to get access to the notifications APIs is install the NotificationsProvider. + +```js +import { NotificationsProvider } from '@toolpad/core/useNotifications'; + +function App({ children }) { + return {children}; +} +``` + +Now you can get acess to the notifications APIs through the `useNotifications` hook. + +```js +import { useNotifications } from '@toolpad/core/useNotifications'; + +function MyApp() { + const notifications = useNotifications(); + // ... +} +``` + +:::info +The Toolpad AppProvider automatically comes with notifications installed. You won't need to explicitly add the NotificationsProvider in Toolpad applications. +::: + +## Basic notification + +You can notify your users with a neutral message by calling `notifications.show`. To have the notification automatically hide, add the `autoHideDuration` option. This expresses the time in milliseconds after which to close the notification. + +{{"demo": "BasicNotification.js"}} + +## Alert notifications + +You can send notifications under the form of alerts with the `severity` property. It takes a value from `"info"`, `"success"`, `"warning"`, or `"error"`. + +{{"demo": "AlertNotification.js"}} + +## Multiple notifications + +Multiple concurrent notifications are stacked and when more than one notification is available, a badge is shown with the amount of open notification. Try it out with the following demo: + +{{"demo": "MultipleNotifications.js"}} + +## Close notifications + +You can programmatically close existing notifications. Each notification has an associated key. You can call the `notifications.close` method with this key to close the opened notification. + +{{"demo": "CloseNotification.js"}} + +## Dedupe notifications + +You can supply your own value for a key to shown notifications to associate them with this key. Notifications with the same key are deduplicated as long as one is already open. If you try to show a notification with the same key, the call is simply ignored. + +{{"demo": "DedupeNotification.js"}} + +## Scoped notifications + +Notification providers can be nested. That way you can scope the notifications to a subset of the page. Use the slots to position the snackbar relative to a specific element on the page. + +{{"demo": "ScopedNotification.js"}} + +## 🚧 Notification center + +When multiple notifications are available, click the badge to open the notification center to show a scrollable list of all available notifications. This feature is not available yet. + +## Hook API + +- [`useNotifications()`](/toolpad/core/react-use-notifications/api/) diff --git a/docs/data/toolpad/core/pages.ts b/docs/data/toolpad/core/pages.ts index 7fae536b320..d2806c38d7a 100644 --- a/docs/data/toolpad/core/pages.ts +++ b/docs/data/toolpad/core/pages.ts @@ -48,7 +48,6 @@ const pages: MuiPage[] = [ }, ], }, - { pathname: '/toolpad/core/components-group', title: 'Components', @@ -88,6 +87,20 @@ const pages: MuiPage[] = [ }, ], }, + { + pathname: '/toolpad/core/components/filter-group', + subheader: 'Utils', + children: [ + { + pathname: '/toolpad/core/react-use-dialogs', + title: 'useDialogs', + }, + { + pathname: '/toolpad/core/react-use-notifications', + title: 'useNotifications', + }, + ], + }, ], }, { diff --git a/docs/data/toolpad/core/pagesApi.js b/docs/data/toolpad/core/pagesApi.js index b9e2c06e032..360899799b6 100644 --- a/docs/data/toolpad/core/pagesApi.js +++ b/docs/data/toolpad/core/pagesApi.js @@ -2,5 +2,7 @@ module.exports = [ { pathname: '/toolpad/core/api/app-provider' }, { pathname: '/toolpad/core/api/dashboard-layout' }, { pathname: '/toolpad/core/api/data-grid' }, + { pathname: '/toolpad/core/api/dialogs-provider' }, { pathname: '/toolpad/core/api/line-chart' }, + { pathname: '/toolpad/core/api/notifications-provider' }, ]; diff --git a/docs/package.json b/docs/package.json index ea01195f87e..d2ba150f5b4 100644 --- a/docs/package.json +++ b/docs/package.json @@ -33,6 +33,7 @@ "@mui/material": "next", "@mui/material-nextjs": "next", "@mui/styles": "next", + "@mui/system": "next", "@mui/utils": "next", "@mui/x-license": "7.7.0", "@toolpad/core": "workspace:*", diff --git a/docs/pages/toolpad/core/api/data-grid.json b/docs/pages/toolpad/core/api/data-grid.json index 54797238575..724b85b8453 100644 --- a/docs/pages/toolpad/core/api/data-grid.json +++ b/docs/pages/toolpad/core/api/data-grid.json @@ -13,7 +13,838 @@ "import { DataGrid } from '@toolpad-core/DataGrid';", "import { DataGrid } from '@toolpad-core';" ], - "classes": [], + "classes": [ + { + "key": "actionsCell", + "className": "", + "description": "Styles applied to the root element of the cell with type=\"actions\".", + "isGlobal": false + }, + { + "key": "aggregationColumnHeader", + "className": "", + "description": "Styles applied to the root element of the column header when aggregated.", + "isGlobal": false + }, + { + "key": "aggregationColumnHeader--alignCenter", + "className": "", + "description": "Styles applied to the root element of the header when aggregation if `headerAlign=\"center\"`.", + "isGlobal": false + }, + { + "key": "aggregationColumnHeader--alignLeft", + "className": "", + "description": "Styles applied to the root element of the header when aggregation if `headerAlign=\"left\"`.", + "isGlobal": false + }, + { + "key": "aggregationColumnHeader--alignRight", + "className": "", + "description": "Styles applied to the root element of the header when aggregation if `headerAlign=\"right\"`.", + "isGlobal": false + }, + { + "key": "aggregationColumnHeaderLabel", + "className": "", + "description": "Styles applied to the aggregation label in the column header when aggregated.", + "isGlobal": false + }, + { + "key": "autoHeight", + "className": "", + "description": "Styles applied to the root element if `autoHeight={true}`.", + "isGlobal": false + }, + { + "key": "autosizing", + "className": "", + "description": "Styles applied to the root element while it is being autosized.", + "isGlobal": false + }, + { + "key": "booleanCell", + "className": "", + "description": "Styles applied to the icon of the boolean cell.", + "isGlobal": false + }, + { + "key": "cell", + "className": "", + "description": "Styles applied to the cell element.", + "isGlobal": false + }, + { + "key": "cell--editable", + "className": "", + "description": "Styles applied to the cell element if the cell is editable.", + "isGlobal": false + }, + { + "key": "cell--editing", + "className": "", + "description": "Styles applied to the cell element if the cell is in edit mode.", + "isGlobal": false + }, + { + "key": "cell--flex", + "className": "", + "description": "Styles applied to the cell element in flex display mode.", + "isGlobal": false + }, + { + "key": "cell--pinnedLeft", + "className": "", + "description": "Styles applied to the cell element if it is pinned to the left.", + "isGlobal": false + }, + { + "key": "cell--pinnedRight", + "className": "", + "description": "Styles applied to the cell element if it is pinned to the right.", + "isGlobal": false + }, + { + "key": "cell--rangeBottom", + "className": "", + "description": "Styles applied to the cell element if it is at the bottom edge of a cell selection range.", + "isGlobal": false + }, + { + "key": "cell--rangeLeft", + "className": "", + "description": "Styles applied to the cell element if it is at the left edge of a cell selection range.", + "isGlobal": false + }, + { + "key": "cell--rangeRight", + "className": "", + "description": "Styles applied to the cell element if it is at the right edge of a cell selection range.", + "isGlobal": false + }, + { + "key": "cell--rangeTop", + "className": "", + "description": "Styles applied to the cell element if it is at the top edge of a cell selection range.", + "isGlobal": false + }, + { + "key": "cell--selectionMode", + "className": "", + "description": "Styles applied to the cell element if it is in a cell selection range.", + "isGlobal": false + }, + { + "key": "cell--textCenter", + "className": "", + "description": "Styles applied to the cell element if `align=\"center\"`.", + "isGlobal": false + }, + { + "key": "cell--textLeft", + "className": "", + "description": "Styles applied to the cell element if `align=\"left\"`.", + "isGlobal": false + }, + { + "key": "cell--textRight", + "className": "", + "description": "Styles applied to the cell element if `align=\"right\"`.", + "isGlobal": false + }, + { + "key": "cell--withLeftBorder", + "className": "", + "description": "Styles applied the cell if `showColumnVerticalBorder={true}`.", + "isGlobal": false + }, + { + "key": "cell--withRightBorder", + "className": "", + "description": "Styles applied the cell if `showColumnVerticalBorder={true}`.", + "isGlobal": false + }, + { + "key": "cellCheckbox", + "className": "", + "description": "Styles applied to the cell checkbox element.", + "isGlobal": false + }, + { + "key": "cellEmpty", + "className": "", + "description": "Styles applied to the empty cell element.", + "isGlobal": false + }, + { + "key": "cellSkeleton", + "className": "", + "description": "Styles applied to the skeleton cell element.", + "isGlobal": false + }, + { + "key": "checkboxInput", + "className": "", + "description": "Styles applied to the selection checkbox element.", + "isGlobal": false + }, + { + "key": "columnHeader", + "className": "", + "description": "Styles applied to the column header element.", + "isGlobal": false + }, + { + "key": "columnHeader--alignCenter", + "className": "", + "description": "Styles applied to the column header if `headerAlign=\"center\"`.", + "isGlobal": false + }, + { + "key": "columnHeader--alignLeft", + "className": "", + "description": "Styles applied to the column header if `headerAlign=\"left\"`.", + "isGlobal": false + }, + { + "key": "columnHeader--alignRight", + "className": "", + "description": "Styles applied to the column header if `headerAlign=\"right\"`.", + "isGlobal": false + }, + { + "key": "columnHeader--dragging", + "className": "", + "description": "Styles applied to the floating column header element when it is dragged.", + "isGlobal": false + }, + { + "key": "columnHeader--emptyGroup", + "className": "", + "description": "Styles applied to the empty column group header cell.", + "isGlobal": false + }, + { + "key": "columnHeader--filledGroup", + "className": "", + "description": "Styles applied to the column group header cell if not empty.", + "isGlobal": false + }, + { + "key": "columnHeader--filtered", + "className": "", + "description": "Styles applied to the column header if the column has a filter applied to it.", + "isGlobal": false + }, + { + "key": "columnHeader--last", + "className": "", + "description": "Styles applied to the last column header element.", + "isGlobal": false + }, + { + "key": "columnHeader--moving", + "className": "", + "description": "Styles applied to the column header if it is being dragged.", + "isGlobal": false + }, + { + "key": "columnHeader--numeric", + "className": "", + "description": "Styles applied to the column header if the type of the column is `number`.", + "isGlobal": false + }, + { "key": "columnHeader--pinnedLeft", "className": "", "description": "", "isGlobal": false }, + { "key": "columnHeader--pinnedRight", "className": "", "description": "", "isGlobal": false }, + { + "key": "columnHeader--sortable", + "className": "", + "description": "Styles applied to the column header if the column is sortable.", + "isGlobal": false + }, + { + "key": "columnHeader--sorted", + "className": "", + "description": "Styles applied to the column header if the column is sorted.", + "isGlobal": false + }, + { + "key": "columnHeader--withLeftBorder", + "className": "", + "description": "", + "isGlobal": false + }, + { + "key": "columnHeader--withRightBorder", + "className": "", + "description": "Styles applied the column header if `showColumnVerticalBorder={true}`.", + "isGlobal": false + }, + { + "key": "columnHeaderCheckbox", + "className": "", + "description": "Styles applied to the header checkbox cell element.", + "isGlobal": false + }, + { + "key": "columnHeaderDraggableContainer", + "className": "", + "description": "Styles applied to the column header's draggable container element.", + "isGlobal": false + }, + { + "key": "columnHeaders", + "className": "", + "description": "Styles applied to the column headers.", + "isGlobal": false + }, + { + "key": "columnHeaderTitle", + "className": "", + "description": "Styles applied to the column header's title element;", + "isGlobal": false + }, + { + "key": "columnHeaderTitleContainer", + "className": "", + "description": "Styles applied to the column header's title container element.", + "isGlobal": false + }, + { + "key": "columnHeaderTitleContainerContent", + "className": "", + "description": "Styles applied to the column header's title excepted buttons.", + "isGlobal": false + }, + { + "key": "columnSeparator", + "className": "", + "description": "Styles applied to the column header separator element.", + "isGlobal": false + }, + { + "key": "columnSeparator--resizable", + "className": "", + "description": "Styles applied to the column header separator if the column is resizable.", + "isGlobal": false + }, + { + "key": "columnSeparator--resizing", + "className": "", + "description": "Styles applied to the column header separator if the column is being resized.", + "isGlobal": false + }, + { + "key": "columnSeparator--sideLeft", + "className": "", + "description": "Styles applied to the column header separator if the side is \"left\".", + "isGlobal": false + }, + { + "key": "columnSeparator--sideRight", + "className": "", + "description": "Styles applied to the column header separator if the side is \"right\".", + "isGlobal": false + }, + { + "key": "columnsManagement", + "className": "", + "description": "Styles applied to the columns management body.", + "isGlobal": false + }, + { + "key": "columnsManagementFooter", + "className": "", + "description": "Styles applied to the columns management footer element.", + "isGlobal": false + }, + { + "key": "columnsManagementHeader", + "className": "", + "description": "Styles applied to the columns management header element.", + "isGlobal": false + }, + { + "key": "columnsManagementRow", + "className": "", + "description": "Styles applied to the columns management row element.", + "isGlobal": false + }, + { + "key": "container--bottom", + "className": "", + "description": "Styles applied to the bottom container.", + "isGlobal": false + }, + { + "key": "container--top", + "className": "", + "description": "Styles applied to the top container.", + "isGlobal": false + }, + { + "key": "detailPanel", + "className": "", + "description": "Styles applied to the detail panel element.", + "isGlobal": false + }, + { + "key": "detailPanels", + "className": "", + "description": "Styles applied to the detail panels wrapper element.", + "isGlobal": false + }, + { + "key": "detailPanelToggleCell", + "className": "", + "description": "Styles applied to the detail panel toggle cell element.", + "isGlobal": false + }, + { + "key": "detailPanelToggleCell--expanded", + "className": "", + "description": "Styles applied to the detail panel toggle cell element if expanded.", + "isGlobal": false + }, + { + "key": "editBooleanCell", + "className": "", + "description": "Styles applied to root of the boolean edit component.", + "isGlobal": false + }, + { + "key": "editInputCell", + "className": "", + "description": "Styles applied to the root of the input component.", + "isGlobal": false + }, + { + "key": "filterForm", + "className": "", + "description": "Styles applied to the root of the filter form component.", + "isGlobal": false + }, + { + "key": "filterFormColumnInput", + "className": "", + "description": "Styles applied to the column input of the filter form component.", + "isGlobal": false + }, + { + "key": "filterFormDeleteIcon", + "className": "", + "description": "Styles applied to the delete icon of the filter form component.", + "isGlobal": false + }, + { + "key": "filterFormLogicOperatorInput", + "className": "", + "description": "Styles applied to the link operator input of the filter form component.", + "isGlobal": false + }, + { + "key": "filterFormOperatorInput", + "className": "", + "description": "Styles applied to the operator input of the filter form component.", + "isGlobal": false + }, + { + "key": "filterFormValueInput", + "className": "", + "description": "Styles applied to the value input of the filter form component.", + "isGlobal": false + }, + { + "key": "filterIcon", + "className": "", + "description": "Styles applied to the filter icon element.", + "isGlobal": false + }, + { + "key": "footerCell", + "className": "", + "description": "Styles applied to the root element of the cell inside a footer row.", + "isGlobal": false + }, + { + "key": "footerContainer", + "className": "", + "description": "Styles applied to the footer container element.", + "isGlobal": false + }, + { + "key": "groupingCriteriaCell", + "className": "", + "description": "Styles applied to the root element of the grouping criteria cell", + "isGlobal": false + }, + { + "key": "groupingCriteriaCellToggle", + "className": "", + "description": "Styles applied to the toggle of the grouping criteria cell", + "isGlobal": false + }, + { + "key": "headerFilterRow", + "className": "", + "description": "Styles applied to the column header filter row.", + "isGlobal": false + }, + { + "key": "iconButtonContainer", + "className": "", + "description": "Styles applied to the column header icon's container.", + "isGlobal": false + }, + { + "key": "iconSeparator", + "className": "", + "description": "Styles applied to the column header separator icon element.", + "isGlobal": false + }, + { + "key": "main", + "className": "", + "description": "Styles applied to the main container element.", + "isGlobal": false + }, + { + "key": "main--hasPinnedRight", + "className": "", + "description": "Styles applied to the main container element when it has right pinned columns.", + "isGlobal": false + }, + { + "key": "menu", + "className": "", + "description": "Styles applied to the menu element.", + "isGlobal": false + }, + { + "key": "menuIcon", + "className": "", + "description": "Styles applied to the menu icon element.", + "isGlobal": false + }, + { + "key": "menuIconButton", + "className": "", + "description": "Styles applied to the menu icon button element.", + "isGlobal": false + }, + { + "key": "menuList", + "className": "", + "description": "Styles applied to the menu list element.", + "isGlobal": false + }, + { + "key": "menuOpen", + "className": "", + "description": "Styles applied to the menu icon element if the menu is open.", + "isGlobal": false + }, + { + "key": "overlay", + "className": "", + "description": "Styles applied to the overlay element.", + "isGlobal": false + }, + { + "key": "overlayWrapper", + "className": "", + "description": "Styles applied to the overlay wrapper element.", + "isGlobal": false + }, + { + "key": "overlayWrapperInner", + "className": "", + "description": "Styles applied to the overlay wrapper inner element.", + "isGlobal": false + }, + { + "key": "panel", + "className": "", + "description": "Styles applied to the panel element.", + "isGlobal": false + }, + { + "key": "panelContent", + "className": "", + "description": "Styles applied to the panel content element.", + "isGlobal": false + }, + { + "key": "panelFooter", + "className": "", + "description": "Styles applied to the panel footer element.", + "isGlobal": false + }, + { + "key": "panelHeader", + "className": "", + "description": "Styles applied to the panel header element.", + "isGlobal": false + }, + { + "key": "panelWrapper", + "className": "", + "description": "Styles applied to the panel wrapper element.", + "isGlobal": false + }, + { + "key": "paper", + "className": "", + "description": "Styles applied to the paper element.", + "isGlobal": false + }, + { + "key": "pinnedColumns", + "className": "", + "description": "Styles applied to the pinned columns.", + "isGlobal": false + }, + { + "key": "pinnedRows", + "className": "", + "description": "Styles applied to the pinned rows container.", + "isGlobal": false + }, + { + "key": "pinnedRows--bottom", + "className": "", + "description": "Styles applied to the bottom pinned rows container.", + "isGlobal": false + }, + { + "key": "pinnedRows--top", + "className": "", + "description": "Styles applied to the top pinned rows container.", + "isGlobal": false + }, + { + "key": "pinnedRowsRenderZone", + "className": "", + "description": "Styles applied to pinned rows render zones.", + "isGlobal": false + }, + { + "key": "root", + "className": "", + "description": "Styles applied to the root element.", + "isGlobal": false + }, + { + "key": "root--densityComfortable", + "className": "", + "description": "Styles applied to the root element if density is \"comfortable\".", + "isGlobal": false + }, + { + "key": "root--densityCompact", + "className": "", + "description": "Styles applied to the root element if density is \"compact\".", + "isGlobal": false + }, + { + "key": "root--densityStandard", + "className": "", + "description": "Styles applied to the root element if density is \"standard\" (default).", + "isGlobal": false + }, + { + "key": "root--disableUserSelection", + "className": "", + "description": "Styles applied to the root element when user selection is disabled.", + "isGlobal": false + }, + { + "key": "row", + "className": "", + "description": "Styles applied to the row element.", + "isGlobal": false + }, + { + "key": "row--detailPanelExpanded", + "className": "", + "description": "Styles applied to the row if its detail panel is open.", + "isGlobal": false + }, + { + "key": "row--dragging", + "className": "", + "description": "Styles applied to the floating special row reorder cell element when it is dragged.", + "isGlobal": false + }, + { + "key": "row--dynamicHeight", + "className": "", + "description": "Styles applied to the row if it has dynamic row height.", + "isGlobal": false + }, + { + "key": "row--editable", + "className": "", + "description": "Styles applied to the row element if the row is editable.", + "isGlobal": false + }, + { + "key": "row--editing", + "className": "", + "description": "Styles applied to the row element if the row is in edit mode.", + "isGlobal": false + }, + { + "key": "row--firstVisible", + "className": "", + "description": "Styles applied to the first visible row element on every page of the grid.", + "isGlobal": false + }, + { + "key": "row--lastVisible", + "className": "", + "description": "Styles applied to the last visible row element on every page of the grid.", + "isGlobal": false + }, + { + "key": "rowCount", + "className": "", + "description": "Styles applied to the footer row count element to show the total number of rows.\nOnly works when pagination is disabled.", + "isGlobal": false + }, + { + "key": "rowReorderCell", + "className": "", + "description": "Styles applied to the root element of the row reorder cell", + "isGlobal": false + }, + { + "key": "rowReorderCell--draggable", + "className": "", + "description": "Styles applied to the root element of the row reorder cell when dragging is allowed", + "isGlobal": false + }, + { + "key": "rowReorderCellContainer", + "className": "", + "description": "Styles applied to the row reorder cell container element.", + "isGlobal": false + }, + { + "key": "rowReorderCellPlaceholder", + "className": "", + "description": "Styles applied to the row's draggable placeholder element inside the special row reorder cell.", + "isGlobal": false + }, + { + "key": "scrollArea", + "className": "", + "description": "Styles applied to both scroll area elements.", + "isGlobal": false + }, + { + "key": "scrollArea--left", + "className": "", + "description": "Styles applied to the left scroll area element.", + "isGlobal": false + }, + { + "key": "scrollArea--right", + "className": "", + "description": "Styles applied to the right scroll area element.", + "isGlobal": false + }, + { + "key": "scrollbar", + "className": "", + "description": "Styles applied to the scrollbars.", + "isGlobal": false + }, + { + "key": "scrollbar--horizontal", + "className": "", + "description": "Styles applied to the horizontal scrollbar.", + "isGlobal": false + }, + { + "key": "scrollbar--vertical", + "className": "", + "description": "Styles applied to the horizontal scrollbar.", + "isGlobal": false + }, + { + "key": "selectedRowCount", + "className": "", + "description": "Styles applied to the footer selected row count element.", + "isGlobal": false + }, + { + "key": "sortIcon", + "className": "", + "description": "Styles applied to the sort icon element.", + "isGlobal": false + }, + { + "key": "toolbarContainer", + "className": "", + "description": "Styles applied to the toolbar container element.", + "isGlobal": false + }, + { + "key": "toolbarFilterList", + "className": "", + "description": "Styles applied to the toolbar filter list element.", + "isGlobal": false + }, + { + "key": "treeDataGroupingCell", + "className": "", + "description": "Styles applied to the root of the grouping column of the tree data.", + "isGlobal": false + }, + { + "key": "treeDataGroupingCellToggle", + "className": "", + "description": "Styles applied to the toggle of the grouping cell of the tree data.", + "isGlobal": false + }, + { + "key": "virtualScroller", + "className": "", + "description": "Styles applied to the virtualization container.", + "isGlobal": false + }, + { + "key": "virtualScrollerContent", + "className": "", + "description": "Styles applied to the virtualization content.", + "isGlobal": false + }, + { + "key": "virtualScrollerContent--overflowed", + "className": "", + "description": "Styles applied to the virtualization content when its height is bigger than the virtualization container.", + "isGlobal": false + }, + { + "key": "virtualScrollerRenderZone", + "className": "", + "description": "Styles applied to the virtualization render zone.", + "isGlobal": false + }, + { + "key": "withBorderColor", + "className": "", + "description": "Styles applied to cells, column header and other elements that have border.\nSets border color only.", + "isGlobal": false + }, + { + "key": "withVerticalBorder", + "className": "", + "description": "Styles applied the grid if `showColumnVerticalBorder={true}`.", + "isGlobal": false + } + ], "spread": true, "themeDefaultProps": false, "muiName": "DataGrid", diff --git a/docs/pages/toolpad/core/api/dialogs-provider.js b/docs/pages/toolpad/core/api/dialogs-provider.js new file mode 100644 index 00000000000..5f44132665d --- /dev/null +++ b/docs/pages/toolpad/core/api/dialogs-provider.js @@ -0,0 +1,23 @@ +import * as React from 'react'; +import ApiPage from 'docs/src/modules/components/ApiPage'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import jsonPageContent from './dialogs-provider.json'; + +export default function Page(props) { + const { descriptions, pageContent } = props; + return ; +} + +Page.getInitialProps = () => { + const req = require.context( + 'docs-toolpad/translations/api-docs/dialogs-provider', + false, + /\.\/dialogs-provider.*.json$/, + ); + const descriptions = mapApiPageTranslations(req); + + return { + descriptions, + pageContent: jsonPageContent, + }; +}; diff --git a/docs/pages/toolpad/core/api/dialogs-provider.json b/docs/pages/toolpad/core/api/dialogs-provider.json new file mode 100644 index 00000000000..2310fc48fe5 --- /dev/null +++ b/docs/pages/toolpad/core/api/dialogs-provider.json @@ -0,0 +1,13 @@ +{ + "props": {}, + "name": "DialogsProvider", + "imports": ["import { DialogsProvider } from '@toolpad-core/useDialogs';"], + "classes": [], + "spread": true, + "themeDefaultProps": false, + "muiName": "DialogsProvider", + "filename": "/packages/toolpad-core/src/useDialogs/DialogsProvider.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/toolpad/core/api/notifications-provider.js b/docs/pages/toolpad/core/api/notifications-provider.js new file mode 100644 index 00000000000..1867f26371a --- /dev/null +++ b/docs/pages/toolpad/core/api/notifications-provider.js @@ -0,0 +1,23 @@ +import * as React from 'react'; +import ApiPage from 'docs/src/modules/components/ApiPage'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import jsonPageContent from './notifications-provider.json'; + +export default function Page(props) { + const { descriptions, pageContent } = props; + return ; +} + +Page.getInitialProps = () => { + const req = require.context( + 'docs-toolpad/translations/api-docs/notifications-provider', + false, + /\.\/notifications-provider.*.json$/, + ); + const descriptions = mapApiPageTranslations(req); + + return { + descriptions, + pageContent: jsonPageContent, + }; +}; diff --git a/docs/pages/toolpad/core/api/notifications-provider.json b/docs/pages/toolpad/core/api/notifications-provider.json new file mode 100644 index 00000000000..29c1fdff962 --- /dev/null +++ b/docs/pages/toolpad/core/api/notifications-provider.json @@ -0,0 +1,21 @@ +{ + "props": {}, + "name": "NotificationsProvider", + "imports": ["import { NotificationsProvider } from '@toolpad-core/useNotifications';"], + "slots": [ + { + "name": "snackbar", + "description": "The component that renders the snackbar.", + "default": "Snackbar", + "class": null + } + ], + "classes": [], + "spread": true, + "themeDefaultProps": false, + "muiName": "NotificationsProvider", + "filename": "/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/toolpad/core/components/dashboard-layout.js b/docs/pages/toolpad/core/react-use-dialogs/api.js similarity index 61% rename from docs/pages/toolpad/core/components/dashboard-layout.js rename to docs/pages/toolpad/core/react-use-dialogs/api.js index 928b679bea5..4b7eaff25b9 100644 --- a/docs/pages/toolpad/core/components/dashboard-layout.js +++ b/docs/pages/toolpad/core/react-use-dialogs/api.js @@ -1,6 +1,6 @@ import * as React from 'react'; import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; -import * as pageProps from '../../../../data/toolpad/core/components/dashboard-layout/dashboard-layout.md?muiMarkdown'; +import * as pageProps from 'docs-toolpad/data/toolpad/core/components/use-dialogs/use-dialogs-api.md?muiMarkdown'; export default function Page() { return ; diff --git a/docs/pages/toolpad/core/react-use-dialogs/index.js b/docs/pages/toolpad/core/react-use-dialogs/index.js new file mode 100644 index 00000000000..3181d8c0420 --- /dev/null +++ b/docs/pages/toolpad/core/react-use-dialogs/index.js @@ -0,0 +1,7 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import * as pageProps from 'docs-toolpad/data/toolpad/core/components/use-dialogs/use-dialogs.md?muiMarkdown'; + +export default function Page() { + return ; +} diff --git a/docs/pages/toolpad/core/react-use-notifications/api.js b/docs/pages/toolpad/core/react-use-notifications/api.js new file mode 100644 index 00000000000..fdf8eea12c5 --- /dev/null +++ b/docs/pages/toolpad/core/react-use-notifications/api.js @@ -0,0 +1,7 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import * as pageProps from 'docs-toolpad/data/toolpad/core/components/use-notifications/use-notifications-api.md?muiMarkdown'; + +export default function Page() { + return ; +} diff --git a/docs/pages/toolpad/core/react-use-notifications/index.js b/docs/pages/toolpad/core/react-use-notifications/index.js new file mode 100644 index 00000000000..62e976cbd79 --- /dev/null +++ b/docs/pages/toolpad/core/react-use-notifications/index.js @@ -0,0 +1,29 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import * as pageProps from 'docs-toolpad/data/toolpad/core/components/use-notifications/use-notifications.md?muiMarkdown'; +import { NotificationsProvider } from '@toolpad/core/useNotifications'; +import { Snackbar, createTheme, useTheme, ThemeProvider } from '@mui/material'; + +function DemoThemedSnackbar(props) { + const outerTheme = useTheme(); + const paletteMode = outerTheme.palette.mode; + const demoTheme = React.useMemo( + () => createTheme({ palette: { mode: paletteMode } }), + [paletteMode], + ); + return ( + + + + ); +} + +const notificationsProviderSlots = { snackbar: DemoThemedSnackbar }; + +export default function Page() { + return ( + + + + ); +} diff --git a/docs/translations/api-docs/data-grid/data-grid.json b/docs/translations/api-docs/data-grid/data-grid.json index 994401fdf15..7f867e41d8a 100644 --- a/docs/translations/api-docs/data-grid/data-grid.json +++ b/docs/translations/api-docs/data-grid/data-grid.json @@ -8,5 +8,573 @@ "description": "The height of the datagrid in pixels. If left undefined, it adopts the height of its parent." } }, - "classDescriptions": {} + "classDescriptions": { + "actionsCell": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the root element of the cell with type="actions"" + }, + "aggregationColumnHeader": { + "description": "Styles applied to {{nodeName}} when {{conditions}}.", + "nodeName": "the root element of the column header", + "conditions": "aggregated" + }, + "aggregationColumnHeader--alignCenter": { + "description": "Styles applied to {{nodeName}} when {{conditions}}.", + "nodeName": "the root element of the header", + "conditions": "aggregation if headerAlign=\"center\"" + }, + "aggregationColumnHeader--alignLeft": { + "description": "Styles applied to {{nodeName}} when {{conditions}}.", + "nodeName": "the root element of the header", + "conditions": "aggregation if headerAlign=\"left\"" + }, + "aggregationColumnHeader--alignRight": { + "description": "Styles applied to {{nodeName}} when {{conditions}}.", + "nodeName": "the root element of the header", + "conditions": "aggregation if headerAlign=\"right\"" + }, + "aggregationColumnHeaderLabel": { + "description": "Styles applied to {{nodeName}} when {{conditions}}.", + "nodeName": "the aggregation label in the column header", + "conditions": "aggregated" + }, + "autoHeight": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the root element", + "conditions": "autoHeight={true}" + }, + "autosizing": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the root element while it is being autosized" + }, + "booleanCell": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the icon of the boolean cell" + }, + "cell": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the cell element" }, + "cell--editable": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the cell element", + "conditions": "the cell is editable" + }, + "cell--editing": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the cell element", + "conditions": "the cell is in edit mode" + }, + "cell--flex": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the cell element in flex display mode" + }, + "cell--pinnedLeft": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the cell element", + "conditions": "it is pinned to the left" + }, + "cell--pinnedRight": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the cell element", + "conditions": "it is pinned to the right" + }, + "cell--rangeBottom": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the cell element", + "conditions": "it is at the bottom edge of a cell selection range" + }, + "cell--rangeLeft": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the cell element", + "conditions": "it is at the left edge of a cell selection range" + }, + "cell--rangeRight": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the cell element", + "conditions": "it is at the right edge of a cell selection range" + }, + "cell--rangeTop": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the cell element", + "conditions": "it is at the top edge of a cell selection range" + }, + "cell--selectionMode": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the cell element", + "conditions": "it is in a cell selection range" + }, + "cell--textCenter": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the cell element", + "conditions": "align=\"center\"" + }, + "cell--textLeft": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the cell element", + "conditions": "align=\"left\"" + }, + "cell--textRight": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the cell element", + "conditions": "align=\"right\"" + }, + "cell--withLeftBorder": { + "description": "Styles applied the cell if showColumnVerticalBorder={true}." + }, + "cell--withRightBorder": { + "description": "Styles applied the cell if showColumnVerticalBorder={true}." + }, + "cellCheckbox": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the cell checkbox element" + }, + "cellEmpty": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the empty cell element" + }, + "cellSkeleton": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the skeleton cell element" + }, + "checkboxInput": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the selection checkbox element" + }, + "columnHeader": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the column header element" + }, + "columnHeader--alignCenter": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the column header", + "conditions": "headerAlign=\"center\"" + }, + "columnHeader--alignLeft": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the column header", + "conditions": "headerAlign=\"left\"" + }, + "columnHeader--alignRight": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the column header", + "conditions": "headerAlign=\"right\"" + }, + "columnHeader--dragging": { + "description": "Styles applied to {{nodeName}} when {{conditions}}.", + "nodeName": "the floating column header element", + "conditions": "it is dragged" + }, + "columnHeader--emptyGroup": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the empty column group header cell" + }, + "columnHeader--filledGroup": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the column group header cell", + "conditions": "not empty" + }, + "columnHeader--filtered": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the column header", + "conditions": "the column has a filter applied to it" + }, + "columnHeader--last": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the last column header element" + }, + "columnHeader--moving": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the column header", + "conditions": "it is being dragged" + }, + "columnHeader--numeric": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the column header", + "conditions": "the type of the column is number" + }, + "columnHeader--pinnedLeft": { "description": "" }, + "columnHeader--pinnedRight": { "description": "" }, + "columnHeader--sortable": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the column header", + "conditions": "the column is sortable" + }, + "columnHeader--sorted": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the column header", + "conditions": "the column is sorted" + }, + "columnHeader--withLeftBorder": { "description": "" }, + "columnHeader--withRightBorder": { + "description": "Styles applied the column header if showColumnVerticalBorder={true}." + }, + "columnHeaderCheckbox": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the header checkbox cell element" + }, + "columnHeaderDraggableContainer": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the column header's draggable container element" + }, + "columnHeaders": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the column headers" + }, + "columnHeaderTitle": { + "description": "Styles applied to the column header's title element;" + }, + "columnHeaderTitleContainer": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the column header's title container element" + }, + "columnHeaderTitleContainerContent": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the column header's title excepted buttons" + }, + "columnSeparator": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the column header separator element" + }, + "columnSeparator--resizable": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the column header separator", + "conditions": "the column is resizable" + }, + "columnSeparator--resizing": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the column header separator", + "conditions": "the column is being resized" + }, + "columnSeparator--sideLeft": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the column header separator", + "conditions": "the side is "left"" + }, + "columnSeparator--sideRight": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the column header separator", + "conditions": "the side is "right"" + }, + "columnsManagement": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the columns management body" + }, + "columnsManagementFooter": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the columns management footer element" + }, + "columnsManagementHeader": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the columns management header element" + }, + "columnsManagementRow": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the columns management row element" + }, + "container--bottom": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the bottom container" + }, + "container--top": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the top container" + }, + "detailPanel": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the detail panel element" + }, + "detailPanels": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the detail panels wrapper element" + }, + "detailPanelToggleCell": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the detail panel toggle cell element" + }, + "detailPanelToggleCell--expanded": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the detail panel toggle cell element", + "conditions": "expanded" + }, + "editBooleanCell": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "root of the boolean edit component" + }, + "editInputCell": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the root of the input component" + }, + "filterForm": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the root of the filter form component" + }, + "filterFormColumnInput": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the column input of the filter form component" + }, + "filterFormDeleteIcon": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the delete icon of the filter form component" + }, + "filterFormLogicOperatorInput": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the link operator input of the filter form component" + }, + "filterFormOperatorInput": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the operator input of the filter form component" + }, + "filterFormValueInput": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the value input of the filter form component" + }, + "filterIcon": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the filter icon element" + }, + "footerCell": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the root element of the cell inside a footer row" + }, + "footerContainer": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the footer container element" + }, + "groupingCriteriaCell": { + "description": "Styles applied to the root element of the grouping criteria cell" + }, + "groupingCriteriaCellToggle": { + "description": "Styles applied to the toggle of the grouping criteria cell" + }, + "headerFilterRow": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the column header filter row" + }, + "iconButtonContainer": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the column header icon's container" + }, + "iconSeparator": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the column header separator icon element" + }, + "main": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the main container element" + }, + "main--hasPinnedRight": { + "description": "Styles applied to {{nodeName}} when {{conditions}}.", + "nodeName": "the main container element", + "conditions": "it has right pinned columns" + }, + "menu": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the menu element" }, + "menuIcon": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the menu icon element" + }, + "menuIconButton": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the menu icon button element" + }, + "menuList": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the menu list element" + }, + "menuOpen": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the menu icon element", + "conditions": "the menu is open" + }, + "overlay": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the overlay element" + }, + "overlayWrapper": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the overlay wrapper element" + }, + "overlayWrapperInner": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the overlay wrapper inner element" + }, + "panel": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the panel element" }, + "panelContent": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the panel content element" + }, + "panelFooter": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the panel footer element" + }, + "panelHeader": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the panel header element" + }, + "panelWrapper": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the panel wrapper element" + }, + "paper": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the paper element" }, + "pinnedColumns": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the pinned columns" + }, + "pinnedRows": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the pinned rows container" + }, + "pinnedRows--bottom": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the bottom pinned rows container" + }, + "pinnedRows--top": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the top pinned rows container" + }, + "pinnedRowsRenderZone": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "pinned rows render zones" + }, + "root": { "description": "Styles applied to the root element." }, + "root--densityComfortable": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the root element", + "conditions": "density is "comfortable"" + }, + "root--densityCompact": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the root element", + "conditions": "density is "compact"" + }, + "root--densityStandard": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the root element", + "conditions": "density is "standard" (default)" + }, + "root--disableUserSelection": { + "description": "Styles applied to {{nodeName}} when {{conditions}}.", + "nodeName": "the root element", + "conditions": "user selection is disabled" + }, + "row": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the row element" }, + "row--detailPanelExpanded": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the row", + "conditions": "its detail panel is open" + }, + "row--dragging": { + "description": "Styles applied to {{nodeName}} when {{conditions}}.", + "nodeName": "the floating special row reorder cell element", + "conditions": "it is dragged" + }, + "row--dynamicHeight": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the row", + "conditions": "it has dynamic row height" + }, + "row--editable": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the row element", + "conditions": "the row is editable" + }, + "row--editing": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the row element", + "conditions": "the row is in edit mode" + }, + "row--firstVisible": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the first visible row element on every page of the grid" + }, + "row--lastVisible": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the last visible row element on every page of the grid" + }, + "rowCount": { + "description": "Styles applied to {{nodeName}}. Only works when pagination is disabled.", + "nodeName": "the footer row count element to show the total number of rows" + }, + "rowReorderCell": { + "description": "Styles applied to the root element of the row reorder cell" + }, + "rowReorderCell--draggable": { + "description": "Styles applied to the root element of the row reorder cell when dragging is allowed" + }, + "rowReorderCellContainer": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the row reorder cell container element" + }, + "rowReorderCellPlaceholder": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the row's draggable placeholder element inside the special row reorder cell" + }, + "scrollArea": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "both scroll area elements" + }, + "scrollArea--left": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the left scroll area element" + }, + "scrollArea--right": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the right scroll area element" + }, + "scrollbar": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the scrollbars" }, + "scrollbar--horizontal": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the horizontal scrollbar" + }, + "scrollbar--vertical": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the horizontal scrollbar" + }, + "selectedRowCount": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the footer selected row count element" + }, + "sortIcon": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the sort icon element" + }, + "toolbarContainer": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the toolbar container element" + }, + "toolbarFilterList": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the toolbar filter list element" + }, + "treeDataGroupingCell": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the root of the grouping column of the tree data" + }, + "treeDataGroupingCellToggle": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the toggle of the grouping cell of the tree data" + }, + "virtualScroller": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the virtualization container" + }, + "virtualScrollerContent": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the virtualization content" + }, + "virtualScrollerContent--overflowed": { + "description": "Styles applied to {{nodeName}} when {{conditions}}.", + "nodeName": "the virtualization content", + "conditions": "its height is bigger than the virtualization container" + }, + "virtualScrollerRenderZone": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the virtualization render zone" + }, + "withBorderColor": { + "description": "Styles applied to {{nodeName}}, {{conditions}}. Sets border color only.", + "nodeName": "cells", + "conditions": "column header and other elements that have border" + }, + "withVerticalBorder": { + "description": "Styles applied the grid if showColumnVerticalBorder={true}." + } + } } diff --git a/docs/translations/api-docs/dialogs-provider/dialogs-provider.json b/docs/translations/api-docs/dialogs-provider/dialogs-provider.json new file mode 100644 index 00000000000..bc8924646fd --- /dev/null +++ b/docs/translations/api-docs/dialogs-provider/dialogs-provider.json @@ -0,0 +1,5 @@ +{ + "componentDescription": "Provider for Dialog stacks. The subtree of this component can use the `useDialogs` hook to\naccess the dialogs API. The dialogs are rendered in the order they are requested.", + "propDescriptions": {}, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/notifications-provider/notifications-provider.json b/docs/translations/api-docs/notifications-provider/notifications-provider.json new file mode 100644 index 00000000000..d0aababd27a --- /dev/null +++ b/docs/translations/api-docs/notifications-provider/notifications-provider.json @@ -0,0 +1,6 @@ +{ + "componentDescription": "Provider for Notifications. The subtree of this component can use the `useNotifications` hook to\naccess the notifications API. The notifications are shown in the same order they are requested.", + "propDescriptions": {}, + "classDescriptions": {}, + "slotDescriptions": { "snackbar": "The component that renders the snackbar." } +} diff --git a/packages/toolpad-core/package.json b/packages/toolpad-core/package.json index c0ea87feac8..dfb1783a81f 100644 --- a/packages/toolpad-core/package.json +++ b/packages/toolpad-core/package.json @@ -44,10 +44,12 @@ "dev": "concurrently \"pnpm build:stable --watch\" \"pnpm build:types --watch --preserveWatchOutput\"", "check-types": "tsc --noEmit", "test": "vitest run", + "test:dev": "vitest", "test:browser": "vitest run --browser.enabled", "test:browser:dev": "vitest --browser.enabled" }, "dependencies": { + "@mui/base": "5.0.0-beta.40", "@mui/lab": "5.0.0-alpha.170", "@mui/utils": "5.15.20", "@mui/x-charts": "7.7.0", diff --git a/packages/toolpad-core/src/AppProvider/AppProvider.tsx b/packages/toolpad-core/src/AppProvider/AppProvider.tsx index 697bc81335e..3a12193605d 100644 --- a/packages/toolpad-core/src/AppProvider/AppProvider.tsx +++ b/packages/toolpad-core/src/AppProvider/AppProvider.tsx @@ -4,6 +4,8 @@ import PropTypes from 'prop-types'; import { ThemeProvider, Theme } from '@mui/material/styles'; import CssBaseline from '@mui/material/CssBaseline'; import { baseTheme } from '../themes'; +import { NotificationsProvider } from '../useNotifications'; +import { DialogsProvider } from '../useDialogs'; export interface NavigateOptions { history?: 'auto' | 'push' | 'replace'; @@ -101,9 +103,13 @@ function AppProvider(props: AppProviderProps) { - - {children} - + + + + {children} + + + ); diff --git a/packages/toolpad-core/src/index.ts b/packages/toolpad-core/src/index.ts index ac93cc55f24..6257db47151 100644 --- a/packages/toolpad-core/src/index.ts +++ b/packages/toolpad-core/src/index.ts @@ -7,3 +7,7 @@ export * from './DataProvider'; export * from './DataGrid'; export * from './LineChart'; + +export * from './useDialogs'; + +export * from './useNotifications'; diff --git a/packages/toolpad-core/src/useDialogs/DialogsContext.tsx b/packages/toolpad-core/src/useDialogs/DialogsContext.tsx new file mode 100644 index 00000000000..eabc60e89ec --- /dev/null +++ b/packages/toolpad-core/src/useDialogs/DialogsContext.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import type { CloseDialog, OpenDialog } from './useDialogs'; + +/** + * @ignore - internal component. + */ + +export const DialogsContext = React.createContext<{ + open: OpenDialog; + close: CloseDialog; +} | null>(null); diff --git a/packages/toolpad-core/src/useDialogs/DialogsProvider.test.tsx b/packages/toolpad-core/src/useDialogs/DialogsProvider.test.tsx new file mode 100644 index 00000000000..14a1e3cb822 --- /dev/null +++ b/packages/toolpad-core/src/useDialogs/DialogsProvider.test.tsx @@ -0,0 +1,16 @@ +/** + * @vitest-environment jsdom + */ + +import * as React from 'react'; +import { describe, test } from 'vitest'; +import describeConformance from '@toolpad/utils/describeConformance'; +import { DialogsProvider } from './DialogsProvider'; + +describe('DialogsProvider', () => { + describeConformance(, () => ({ + skip: ['themeDefaultProps'], + })); + + test('dummy test', () => {}); +}); diff --git a/packages/toolpad-core/src/useDialogs/DialogsProvider.tsx b/packages/toolpad-core/src/useDialogs/DialogsProvider.tsx new file mode 100644 index 00000000000..6d83266848a --- /dev/null +++ b/packages/toolpad-core/src/useDialogs/DialogsProvider.tsx @@ -0,0 +1,118 @@ +import invariant from 'invariant'; +import * as React from 'react'; +import { DialogsContext } from './DialogsContext'; +import type { DialogComponent, OpenDialog, OpenDialogOptions } from './useDialogs'; + +interface DialogStackEntry { + key: string; + open: boolean; + promise: Promise; + Component: DialogComponent; + payload: P; + onClose: (result: R) => Promise; + resolve: (result: R) => void; +} + +export interface DialogProviderProps { + children?: React.ReactNode; + unmountAfter?: number; +} + +/** + * Provider for Dialog stacks. The subtree of this component can use the `useDialogs` hook to + * access the dialogs API. The dialogs are rendered in the order they are requested. + * + * Demos: + * + * - [Dialogs](https://mui.com/toolpad/core/react-use-dialogs/) + * + * API: + * + * - [DialogsProvider API](https://mui.com/toolpad/core/api/dialogs-provider) + */ +function DialogsProvider(props: DialogProviderProps) { + const { children, unmountAfter = 1000 } = props; + const [stack, setStack] = React.useState[]>([]); + const keyPrefix = React.useId(); + const nextId = React.useRef(0); + + const requestDialog = React.useCallback( + function open( + Component: DialogComponent, + payload: P, + options: OpenDialogOptions = {}, + ) { + const { onClose = async () => {} } = options; + let resolve: ((result: R) => void) | undefined; + const promise = new Promise((resolveImpl) => { + resolve = resolveImpl; + }); + invariant(resolve, 'resolve not set'); + + const key = `${keyPrefix}-${nextId.current}`; + nextId.current += 1; + + const newEntry: DialogStackEntry = { + key, + open: true, + promise, + Component, + payload, + onClose, + resolve, + }; + + setStack((prevStack) => [...prevStack, newEntry]); + return promise; + }, + [keyPrefix], + ); + + const closeDialogUi = React.useCallback( + function closeDialogUi(dialog: Promise) { + setStack((prevStack) => + prevStack.map((entry) => (entry.promise === dialog ? { ...entry, open: false } : entry)), + ); + setTimeout(() => { + // wait for closing animation + setStack((prevStack) => prevStack.filter((entry) => entry.promise !== dialog)); + }, unmountAfter); + }, + [unmountAfter], + ); + + const closeDialog = React.useCallback( + async function closeDialog(dialog: Promise, result: R) { + const entryToClose = stack.find((entry) => entry.promise === dialog); + invariant(entryToClose, 'dialog not found'); + await entryToClose.onClose(result); + entryToClose.resolve(result); + closeDialogUi(dialog); + return dialog; + }, + [stack, closeDialogUi], + ); + + const contextValue = React.useMemo( + () => ({ open: requestDialog, close: closeDialog }), + [requestDialog, closeDialog], + ); + + return ( + + {children} + {stack.map(({ key, open, Component, payload, promise }) => ( + { + await closeDialog(promise, result); + }} + /> + ))} + + ); +} + +export { DialogsProvider }; diff --git a/packages/toolpad-core/src/useDialogs/index.ts b/packages/toolpad-core/src/useDialogs/index.ts new file mode 100644 index 00000000000..6fdad438d37 --- /dev/null +++ b/packages/toolpad-core/src/useDialogs/index.ts @@ -0,0 +1,2 @@ +export * from './useDialogs'; +export * from './DialogsProvider'; diff --git a/packages/toolpad-core/src/useDialogs/useDialogs.test.tsx b/packages/toolpad-core/src/useDialogs/useDialogs.test.tsx new file mode 100644 index 00000000000..66f6dc51bdc --- /dev/null +++ b/packages/toolpad-core/src/useDialogs/useDialogs.test.tsx @@ -0,0 +1,199 @@ +/** + * @vitest-environment jsdom + */ + +import * as React from 'react'; +import { describe, test, expect } from 'vitest'; +import { renderHook, within, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; +import { DialogProps, useDialogs } from './useDialogs'; +import { DialogsProvider } from './DialogsProvider'; + +interface TestWrapperProps { + children: React.ReactNode; +} + +function TestWrapper({ children }: TestWrapperProps) { + return {children}; +} + +describe('useDialogs', () => { + describe('alert', () => { + test('can show and hide', async () => { + const { result, rerender } = renderHook(() => useDialogs(), { wrapper: TestWrapper }); + + const dialogResult = result.current.alert('Hello'); + + rerender(); + + const dialog = await screen.findByRole('dialog'); + + expect(within(dialog).getByText('Hello')).toBeTruthy(); + + await userEvent.click(within(dialog).getByRole('button', { name: 'Ok' })); + + rerender(); + + expect(await dialogResult).toBeUndefined(); + + expect(screen.queryByRole('dialog')).toBeFalsy(); + }); + }); + + describe('confirm', () => { + test('can show and confirm', async () => { + const { result, rerender } = renderHook(() => useDialogs(), { wrapper: TestWrapper }); + + const dialogResult = result.current.confirm('Hello'); + + rerender(); + + const dialog = await screen.findByRole('dialog'); + + expect(within(dialog).getByText('Hello')).toBeTruthy(); + + await userEvent.click(within(dialog).getByRole('button', { name: 'Ok' })); + + rerender(); + + expect(await dialogResult).toBe(true); + + expect(screen.queryByRole('dialog')).toBeFalsy(); + }); + + test('can show and cancel', async () => { + const { result, rerender } = renderHook(() => useDialogs(), { wrapper: TestWrapper }); + + const dialogResult = result.current.confirm('Hello'); + + rerender(); + + const dialog = await screen.findByRole('dialog'); + + expect(within(dialog).getByText('Hello')).toBeTruthy(); + + await userEvent.click(within(dialog).getByRole('button', { name: 'Cancel' })); + + rerender(); + + expect(await dialogResult).toBe(false); + + expect(screen.queryByRole('dialog')).toBeFalsy(); + }); + }); + + describe('prompt', () => { + test('can show and take input', async () => { + const { result, rerender } = renderHook(() => useDialogs(), { wrapper: TestWrapper }); + + const dialogResult = result.current.prompt('Hello'); + + rerender(); + + const dialog = await screen.findByRole('dialog'); + + expect(within(dialog).getByText('Hello')).toBeTruthy(); + + await userEvent.keyboard('Hello, World!'); + + await userEvent.click(within(dialog).getByRole('button', { name: 'Ok' })); + + rerender(); + + expect(await dialogResult).toBe('Hello, World!'); + + expect(screen.queryByRole('dialog')).toBeFalsy(); + }); + + test('can show and cancel', async () => { + const { result, rerender } = renderHook(() => useDialogs(), { wrapper: TestWrapper }); + + const dialogResult = result.current.prompt('Hello'); + + rerender(); + + const dialog = await screen.findByRole('dialog'); + + expect(within(dialog).getByText('Hello')).toBeTruthy(); + + await userEvent.keyboard('Hello, World!'); + + await userEvent.click(within(dialog).getByRole('button', { name: 'Cancel' })); + + rerender(); + + expect(await dialogResult).toBe(null); + + expect(screen.queryByRole('dialog')).toBeFalsy(); + }); + }); + + describe('custom dialog', () => { + test('can show and hide', async () => { + function CustomDialog({ open, onClose }: DialogProps) { + return open ? ( +
+ Hello +
+ ) : null; + } + const { result, rerender } = renderHook(() => useDialogs(), { wrapper: TestWrapper }); + + result.current.open(CustomDialog); + + const dialog = await screen.findByRole('dialog'); + + rerender(); + + expect(within(dialog).getByText('Hello')).toBeTruthy(); + + await userEvent.click(within(dialog).getByRole('button', { name: 'Close me' })); + + rerender(); + + expect(screen.queryByRole('dialog')).toBeFalsy(); + }); + + test('can pass a payload', async () => { + function CustomDialog({ open, onClose, payload }: DialogProps) { + return open ? ( +
+ {payload} +
+ ) : null; + } + const { result, rerender } = renderHook(() => useDialogs(), { wrapper: TestWrapper }); + + result.current.open(CustomDialog, 'I am content'); + + const dialog = await screen.findByRole('dialog'); + + rerender(); + + expect(within(dialog).getByText('I am content')).toBeTruthy(); + }); + + test('can receive result', async () => { + function CustomDialog({ open, onClose }: DialogProps) { + return open ? ( +
+ Hello +
+ ) : null; + } + const { result, rerender } = renderHook(() => useDialogs(), { wrapper: TestWrapper }); + + const dialogResult = result.current.open(CustomDialog); + + const dialog = await screen.findByRole('dialog'); + + rerender(); + + await userEvent.click(within(dialog).getByRole('button', { name: 'Close me' })); + + rerender(); + + expect(await dialogResult).toBe('I am result'); + }); + }); +}); diff --git a/packages/toolpad-core/src/useDialogs/useDialogs.tsx b/packages/toolpad-core/src/useDialogs/useDialogs.tsx new file mode 100644 index 00000000000..c73fe77ff58 --- /dev/null +++ b/packages/toolpad-core/src/useDialogs/useDialogs.tsx @@ -0,0 +1,328 @@ +import { LoadingButton } from '@mui/lab'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + DialogContentText, +} from '@mui/material'; +import { useNonNullableContext } from '@toolpad/utils/react'; +import invariant from 'invariant'; +import * as React from 'react'; +import { DialogsContext } from './DialogsContext'; + +export interface OpenDialogOptions { + /** + * A function that is called before closing the dialog closes. The dialog + * stays open as long as the returned promise is not resolved. Use this if + * you want to perform an async action on close and show a loading state. + * + * @param result The result that the dialog will return after closing. + * @returns A promise that resolves when the dialog can be closed. + */ + onClose?: (result: R) => Promise; +} + +export interface AlertOptions extends OpenDialogOptions { + /** + * A title for the dialog. Defaults to `'Alert'`. + */ + title?: React.ReactNode; + /** + * The text to show in the "Ok" button. Defaults to `'Ok'`. + */ + okText?: React.ReactNode; +} + +export interface ConfirmOptions extends OpenDialogOptions { + /** + * A title for the dialog. Defaults to `'Confirm'`. + */ + title?: React.ReactNode; + /** + * The text to show in the "Ok" button. Defaults to `'Ok'`. + */ + okText?: React.ReactNode; + /** + * Denotes the purpose of the dialog. This will affect the color of the + * "Ok" button. Defaults to `undefined`. + */ + severity?: 'error' | 'info' | 'success' | 'warning'; + /** + * The text to show in the "Cancel" button. Defaults to `'Cancel'`. + */ + cancelText?: React.ReactNode; +} + +export interface PromptOptions extends OpenDialogOptions { + /** + * A title for the dialog. Defaults to `'Prompt'`. + */ + title?: React.ReactNode; + /** + * The text to show in the "Ok" button. Defaults to `'Ok'`. + */ + okText?: React.ReactNode; + /** + * The text to show in the "Cancel" button. Defaults to `'Cancel'`. + */ + cancelText?: React.ReactNode; +} + +/** + * The props that are passed to a dialog component. + */ +export interface DialogProps

{ + /** + * The payload that was passed when the dialog was opened. + */ + payload: P; + /** + * Whether the dialog is open. + */ + open: boolean; + /** + * A function to call when the dialog should be closed. If the dialog has a return + * value, it should be passed as an argument to this function. You should use the promise + * that is returned to show a loading state while the dialog is performing async actions + * on close. + * @param result The result to return from the dialog. + * @returns A promise that resolves when the dialog can be fully closed. + */ + onClose: (result: R) => Promise; +} + +export interface OpenAlertDialog { + /** + * Open an alert dialog. Returns a promise that resolves when the user + * closes the dialog. + * + * @param msg The message to show in the dialog. + * @param options Additional options for the dialog. + * @returns A promise that resolves when the dialog is closed. + */ + (msg: React.ReactNode, options?: AlertOptions): Promise; +} + +export interface OpenConfirmDialog { + /** + * Open a confirmation dialog. Returns a promise that resolves to true if + * the user confirms, false if the user cancels. + * + * @param msg The message to show in the dialog. + * @param options Additional options for the dialog. + * @returns A promise that resolves to true if the user confirms, false if the user cancels. + */ + (msg: React.ReactNode, options?: ConfirmOptions): Promise; +} + +export interface OpenPromptDialog { + /** + * Open a prompt dialog to request user input. Returns a promise that resolves to the input + * if the user confirms, null if the user cancels. + * + * @param msg The message to show in the dialog. + * @param options Additional options for the dialog. + * @returns A promise that resolves to the user input if the user confirms, null if the user cancels. + */ + (msg: React.ReactNode, options?: PromptOptions): Promise; +} + +export type DialogComponent = React.ComponentType>; + +export interface OpenDialog { + /** + * Open a dialog without payload. + * @param Component The dialog component to open. + * @param options Additional options for the dialog. + */ +

( + Component: DialogComponent, + payload?: P, + options?: OpenDialogOptions, + ): Promise; + /** + * Open a dialog and pass a payload. + * @param Component The dialog component to open. + * @param payload The payload to pass to the dialog. + * @param options Additional options for the dialog. + */ + (Component: DialogComponent, payload: P, options?: OpenDialogOptions): Promise; +} + +export interface CloseDialog { + /** + * Close a dialog and return a result. + * @param dialog The dialog to close. The promise returned by `open`. + * @param result The result to return from the dialog. + * @returns A promise that resolves when the dialog is fully closed. + */ + (dialog: Promise, result: R): Promise; +} + +export interface DialogHook { + alert: OpenAlertDialog; + confirm: OpenConfirmDialog; + prompt: OpenPromptDialog; + open: OpenDialog; + close: CloseDialog; +} + +function useDialogLoadingButton(onClose: () => Promise) { + const [loading, setLoading] = React.useState(false); + const handleClick = async () => { + try { + setLoading(true); + await onClose(); + } finally { + setLoading(false); + } + }; + return { + onClick: handleClick, + loading, + }; +} + +export interface AlertDialogPayload extends AlertOptions { + msg: React.ReactNode; +} + +export interface AlertDialogProps extends DialogProps {} + +export function AlertDialog({ open, payload, onClose }: AlertDialogProps) { + const okButtonProps = useDialogLoadingButton(() => onClose()); + return ( +

onClose()}> + {payload.title ?? 'Alert'} + {payload.msg} + + + {payload.okText ?? 'Ok'} + + + + ); +} + +export interface ConfirmDialogPayload extends ConfirmOptions { + msg: React.ReactNode; +} + +export interface ConfirmDialogProps extends DialogProps {} + +export function ConfirmDialog({ open, payload, onClose }: ConfirmDialogProps) { + const cancelButtonProps = useDialogLoadingButton(() => onClose(false)); + const okButtonProps = useDialogLoadingButton(() => onClose(true)); + return ( + onClose(false)}> + {payload.title ?? 'Confirm'} + {payload.msg} + + + {payload.cancelText ?? 'Cancel'} + + + {payload.okText ?? 'Ok'} + + + + ); +} + +export interface PromptDialogPayload extends PromptOptions { + msg: React.ReactNode; +} + +export interface PromptDialogProps extends DialogProps {} + +export function PromptDialog({ open, payload, onClose }: PromptDialogProps) { + const [input, setInput] = React.useState(''); + const cancelButtonProps = useDialogLoadingButton(() => onClose(null)); + + const [loading, setLoading] = React.useState(false); + + const name = 'input'; + return ( + onClose(null)} + PaperProps={{ + component: 'form', + onSubmit: async (event: React.FormEvent) => { + event.preventDefault(); + try { + setLoading(true); + const formData = new FormData(event.currentTarget); + const value = formData.get(name) ?? ''; + invariant(typeof value === 'string', 'Value must come from a text input'); + await onClose(value); + } finally { + setLoading(false); + } + }, + }} + > + {payload.title ?? 'Confirm'} + + {payload.msg} + setInput(e.target.value)} + /> + + + + {payload.cancelText ?? 'Cancel'} + + + {payload.okText ?? 'Ok'} + + + + ); +} + +export function useDialogs(): DialogHook { + const { open, close } = useNonNullableContext(DialogsContext); + + const alert = React.useCallback( + async (msg, { onClose, ...options } = {}) => + open(AlertDialog, { ...options, msg }, { onClose }), + [open], + ); + + const confirm = React.useCallback( + async (msg, { onClose, ...options } = {}) => + open(ConfirmDialog, { ...options, msg }, { onClose }), + [open], + ); + + const prompt = React.useCallback( + async (msg, { onClose, ...options } = {}) => + open(PromptDialog, { ...options, msg }, { onClose }), + [open], + ); + + return React.useMemo( + () => ({ + alert, + confirm, + prompt, + open, + close, + }), + [alert, close, confirm, open, prompt], + ); +} diff --git a/packages/toolpad-core/src/useNotifications/NotificationsContext.ts b/packages/toolpad-core/src/useNotifications/NotificationsContext.ts new file mode 100644 index 00000000000..74305ee2622 --- /dev/null +++ b/packages/toolpad-core/src/useNotifications/NotificationsContext.ts @@ -0,0 +1,13 @@ +import * as React from 'react'; +import type { ShowNotification, CloseNotification } from './useNotifications'; + +/** + * @ignore - internal component. + */ + +export interface NotificationsContextValue { + show: ShowNotification; + close: CloseNotification; +} + +export const NotificationsContext = React.createContext(null); diff --git a/packages/toolpad-core/src/useNotifications/NotificationsProvider.test.tsx b/packages/toolpad-core/src/useNotifications/NotificationsProvider.test.tsx new file mode 100644 index 00000000000..3aa3f618cad --- /dev/null +++ b/packages/toolpad-core/src/useNotifications/NotificationsProvider.test.tsx @@ -0,0 +1,19 @@ +/** + * @vitest-environment jsdom + */ + +import * as React from 'react'; +import { describe, test } from 'vitest'; +import describeConformance from '@toolpad/utils/describeConformance'; +import { NotificationsProvider } from './NotificationsProvider'; + +describe('NotificationsProvider', () => { + describeConformance(, () => ({ + skip: ['themeDefaultProps'], + slots: { + snackbar: {}, + }, + })); + + test('dummy test', () => {}); +}); diff --git a/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx b/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx new file mode 100644 index 00000000000..5eef825b99d --- /dev/null +++ b/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx @@ -0,0 +1,197 @@ +import * as React from 'react'; +import { + Alert, + Badge, + Button, + CloseReason, + IconButton, + Snackbar, + SnackbarCloseReason, + SnackbarContent, + SnackbarProps, +} from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; +import { useNonNullableContext } from '@toolpad/utils/react'; +import { useSlotProps } from '@mui/base/utils'; +import { NotificationsContext } from './NotificationsContext'; +import type { + CloseNotification, + ShowNotification, + ShowNotificationOptions, +} from './useNotifications'; + +const closeText = 'Close'; + +export interface NotificationsProviderSlotProps { + snackbar: SnackbarProps; +} + +export interface NotificationsProviderSlots { + /** + * The component that renders the snackbar. + * @default Snackbar + */ + snackbar: React.ElementType; +} + +const RootPropsContext = React.createContext(null); + +interface NotificationProps { + notificationKey: string; + badge: string | null; + open: boolean; + message: React.ReactNode; + options: ShowNotificationOptions; +} + +function Notification({ notificationKey, open, message, options, badge }: NotificationProps) { + const { close } = useNonNullableContext(NotificationsContext); + + const { severity, actionText, onAction, autoHideDuration } = options; + + const handleClose = React.useCallback( + (event: unknown, reason?: CloseReason | SnackbarCloseReason) => { + if (reason === 'clickaway') { + return; + } + close(notificationKey); + }, + [notificationKey, close], + ); + + const action = ( + + {onAction ? ( + + ) : null} + + + + + ); + + const props = React.useContext(RootPropsContext); + const SnackbarComponent = props?.slots?.snackbar ?? Snackbar; + const snackbarSlotProps = useSlotProps({ + elementType: SnackbarComponent, + ownerState: props, + externalSlotProps: props?.slotProps?.snackbar, + additionalProps: { + open, + autoHideDuration, + onClose: handleClose, + action, + }, + }); + + return ( + + + {severity ? ( + + {message} + + ) : ( + + )} + + + ); +} + +interface NotificationQueueEntry { + notificationKey: string; + options: ShowNotificationOptions; + open: boolean; + message: React.ReactNode; +} + +interface NotificationsState { + queue: NotificationQueueEntry[]; +} + +interface NotificationsProps { + state: NotificationsState; +} + +function Notifications({ state }: NotificationsProps) { + const currentNotification = state.queue[0] ?? null; + + return currentNotification ? ( + 1 ? String(state.queue.length) : null} + /> + ) : null; +} + +export interface NotificationsProviderProps { + children?: React.ReactNode; + // eslint-disable-next-line react/no-unused-prop-types + slots?: Partial; + // eslint-disable-next-line react/no-unused-prop-types + slotProps?: Partial; +} + +let nextId = 1; + +/** + * Provider for Notifications. The subtree of this component can use the `useNotifications` hook to + * access the notifications API. The notifications are shown in the same order they are requested. + * + * Demos: + * + * - [Notifications](https://mui.com/toolpad/core/react-use-notifications/) + * + * API: + * + * - [NotificationsProvider API](https://mui.com/toolpad/core/api/notifications-provider) + */ +function NotificationsProvider(props: NotificationsProviderProps) { + const { children } = props; + const [state, setState] = React.useState({ queue: [] }); + + const show = React.useCallback((message, options = {}) => { + const notificationKey = options.key ?? `::toolpad-internal::notification::${nextId}`; + nextId += 1; + setState((prev) => { + if (prev.queue.some((n) => n.notificationKey === notificationKey)) { + // deduplicate by key + return prev; + } + return { + ...prev, + queue: [...prev.queue, { message, options, notificationKey, open: true }], + }; + }); + return notificationKey; + }, []); + + const close = React.useCallback((key) => { + setState((prev) => ({ + ...prev, + queue: prev.queue.filter((n) => n.notificationKey !== key), + })); + }, []); + + const contextValue = React.useMemo(() => ({ show, close }), [show, close]); + + return ( + + + {children} + + + + ); +} + +export { NotificationsProvider }; diff --git a/packages/toolpad-core/src/useNotifications/index.ts b/packages/toolpad-core/src/useNotifications/index.ts new file mode 100644 index 00000000000..38ae998b741 --- /dev/null +++ b/packages/toolpad-core/src/useNotifications/index.ts @@ -0,0 +1,2 @@ +export * from './useNotifications'; +export * from './NotificationsProvider'; diff --git a/packages/toolpad-core/src/useNotifications/useNotifications.test.tsx b/packages/toolpad-core/src/useNotifications/useNotifications.test.tsx new file mode 100644 index 00000000000..9c3d719200d --- /dev/null +++ b/packages/toolpad-core/src/useNotifications/useNotifications.test.tsx @@ -0,0 +1,40 @@ +/** + * @vitest-environment jsdom + */ + +import * as React from 'react'; +import { describe, test, expect } from 'vitest'; +import { renderHook, within, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; +import { useNotifications } from './useNotifications'; +import { NotificationsProvider } from './NotificationsProvider'; + +interface TestWrapperProps { + children: React.ReactNode; +} + +function TestWrapper({ children }: TestWrapperProps) { + return {children}; +} + +describe('useNotifications', () => { + test('can do basic notifications', async () => { + const { result, rerender } = renderHook(() => useNotifications(), { wrapper: TestWrapper }); + + expect(screen.queryByRole('alert')).toBeNull(); + + const key = result.current.show('Hello'); + expect(key).toBeTypeOf('string'); + + rerender(); + + const snackbar = screen.getByRole('alert'); + expect(snackbar.textContent).toBe('Hello'); + + await userEvent.click(within(snackbar).getByRole('button', { name: 'Close' })); + + rerender(); + + expect(screen.queryByRole('alert')).toBeNull(); + }); +}); diff --git a/packages/toolpad-core/src/useNotifications/useNotifications.tsx b/packages/toolpad-core/src/useNotifications/useNotifications.tsx new file mode 100644 index 00000000000..91dcb340aec --- /dev/null +++ b/packages/toolpad-core/src/useNotifications/useNotifications.tsx @@ -0,0 +1,71 @@ +import * as React from 'react'; +import { NotificationsContext } from './NotificationsContext'; + +export interface ShowNotificationOptions { + /** + * The key to use for deduping notifications. If not provided, a unique key will be generated. + */ + key?: string; + /** + * The severity of the notification. When provided, the snackbar will show an alert with the + * specified severity. + */ + severity?: 'info' | 'warning' | 'error' | 'success'; + /** + * The duration in milliseconds after which the notification will automatically close. + */ + autoHideDuration?: number; + /** + * The text to display on the action button. + */ + actionText?: React.ReactNode; + /** + * The callback to call when the action button is clicked. + */ + onAction?: () => void; +} + +export interface ShowNotification { + /** + * Show a snackbar in the application. + * + * @param message The message to display in the snackbar. + * @param options Options for the snackbar. + * @returns The key that represents the notification. Useful for programmatically + * closing it. + */ + (message: React.ReactNode, options?: ShowNotificationOptions): string; +} + +export interface CloseNotification { + /** + * Close a snackbar in the application. + * + * @param key The key of the notification to close. + */ + (key: string): void; +} + +interface UseNotifications { + show: ShowNotification; + close: CloseNotification; +} + +const serverNotifications: UseNotifications = { + show: () => { + throw new Error('Not supported on server side'); + }, + close: () => { + throw new Error('Not supported on server side'); + }, +}; + +export function useNotifications(): UseNotifications { + const context = React.useContext(NotificationsContext); + + if (context) { + return context; + } + + return serverNotifications; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7cd7681ebff..c657c11780b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -284,7 +284,7 @@ importers: version: 5.0.0-beta.40(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/docs': specifier: 6.0.0-dev.240424162023-9968b4889d - version: 6.0.0-dev.240424162023-9968b4889d(@mui/base@5.0.0-beta.40(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/icons-material@6.0.0-alpha.11(@mui/material@6.0.0-alpha.11(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@mui/material@6.0.0-alpha.11(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@6.0.0-dev.240424162023-9968b4889d(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(next@14.2.4(@babel/core@7.24.7)(@opentelemetry/api@1.8.0)(@playwright/test@1.44.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 6.0.0-dev.240424162023-9968b4889d(@mui/base@5.0.0-beta.40(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/icons-material@6.0.0-alpha.11(@mui/material@6.0.0-alpha.11(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@mui/material@6.0.0-alpha.11(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@6.0.0-alpha.11(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(next@14.2.4(@babel/core@7.24.7)(@opentelemetry/api@1.8.0)(@playwright/test@1.44.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@mui/icons-material': specifier: next version: 6.0.0-alpha.11(@mui/material@6.0.0-alpha.11(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react@18.3.1) @@ -306,6 +306,9 @@ importers: '@mui/styles': specifier: next version: 6.0.0-alpha.11(@types/react@18.3.3)(react@18.3.1) + '@mui/system': + specifier: next + version: 6.0.0-alpha.11(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1) '@mui/utils': specifier: next version: 6.0.0-alpha.11(@types/react@18.3.3)(react@18.3.1) @@ -570,6 +573,9 @@ importers: packages/toolpad-core: dependencies: + '@mui/base': + specifier: 5.0.0-beta.40 + version: 5.0.0-beta.40(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/lab': specifier: 5.0.0-alpha.170 version: 5.0.0-alpha.170(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@mui/material@5.15.20(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2808,8 +2814,8 @@ packages: '@types/react': optional: true - '@mui/system@6.0.0-dev.240424162023-9968b4889d': - resolution: {integrity: sha512-Y3yCFUHN1xMK62hJJBqzZb1YQvHNaHc7JUX01eU6QTPojtIbGMF2jCOP/EQw77/byahNbxeLoAIQx10F0IR3Rw==} + '@mui/system@6.0.0-alpha.11': + resolution: {integrity: sha512-RceH1GU0M7aKNFIaQ92n5uR0moRbsuPfWh08F2Z9jz0k/y/59Tj/IVsdSYnjncnoGxhS3HfL3qJM8gRIkbdF1Q==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -11416,14 +11422,14 @@ snapshots: '@mui/core-downloads-tracker@6.0.0-dev.240424162023-9968b4889d': {} - ? '@mui/docs@6.0.0-dev.240424162023-9968b4889d(@mui/base@5.0.0-beta.40(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/icons-material@6.0.0-alpha.11(@mui/material@6.0.0-alpha.11(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@mui/material@6.0.0-alpha.11(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@6.0.0-dev.240424162023-9968b4889d(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(next@14.2.4(@babel/core@7.24.7)(@opentelemetry/api@1.8.0)(@playwright/test@1.44.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)' + ? '@mui/docs@6.0.0-dev.240424162023-9968b4889d(@mui/base@5.0.0-beta.40(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/icons-material@6.0.0-alpha.11(@mui/material@6.0.0-alpha.11(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@mui/material@6.0.0-alpha.11(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@6.0.0-alpha.11(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(next@14.2.4(@babel/core@7.24.7)(@opentelemetry/api@1.8.0)(@playwright/test@1.44.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)' : dependencies: '@babel/runtime': 7.24.7 '@mui/base': 5.0.0-beta.40(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/icons-material': 6.0.0-alpha.11(@mui/material@6.0.0-alpha.11(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react@18.3.1) '@mui/internal-markdown': 1.0.5 '@mui/material': 6.0.0-alpha.11(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mui/system': 6.0.0-dev.240424162023-9968b4889d(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1) + '@mui/system': 6.0.0-alpha.11(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1) clipboard-copy: 4.0.1 clsx: 2.1.1 next: 14.2.4(@babel/core@7.24.7)(@opentelemetry/api@1.8.0)(@playwright/test@1.44.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -11481,7 +11487,7 @@ snapshots: '@babel/runtime': 7.24.7 '@mui/base': 5.0.0-beta.49(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/core-downloads-tracker': 6.0.0-dev.240424162023-9968b4889d - '@mui/system': 6.0.0-dev.240424162023-9968b4889d(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1) + '@mui/system': 6.0.0-alpha.11(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1) '@mui/types': 7.2.14(@types/react@18.3.3) '@mui/utils': 6.0.0-alpha.11(@types/react@18.3.3)(react@18.3.1) clsx: 2.1.1 @@ -11515,7 +11521,7 @@ snapshots: '@babel/runtime': 7.24.7 '@mui/base': 5.0.0-beta.49(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/material': 6.0.0-alpha.11(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mui/system': 6.0.0-dev.240424162023-9968b4889d(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1) + '@mui/system': 6.0.0-alpha.11(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1) '@mui/types': 7.2.14(@types/react@18.3.3) '@mui/utils': 6.0.0-alpha.11(@types/react@18.3.3)(react@18.3.1) clsx: 2.1.1 @@ -11575,7 +11581,7 @@ snapshots: '@babel/runtime': 7.24.7 '@mui/base': 5.0.0-beta.49(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/core-downloads-tracker': 6.0.0-dev.240424162023-9968b4889d - '@mui/system': 6.0.0-dev.240424162023-9968b4889d(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1) + '@mui/system': 6.0.0-alpha.11(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1) '@mui/types': 7.2.14(@types/react@18.3.3) '@mui/utils': 6.0.0-alpha.11(@types/react@18.3.3)(react@18.3.1) '@types/react-transition-group': 4.4.10 @@ -11685,7 +11691,7 @@ snapshots: '@emotion/styled': 11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1) '@types/react': 18.3.3 - '@mui/system@6.0.0-dev.240424162023-9968b4889d(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)': + '@mui/system@6.0.0-alpha.11(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.7 '@mui/private-theming': 6.0.0-dev.20240529-082515-213b5e33ab(@types/react@18.3.3)(react@18.3.1) diff --git a/scripts/docs/buildCoreApiDocs/config/projectSettings.ts b/scripts/docs/buildCoreApiDocs/config/projectSettings.ts index b804f897fef..5d952eec674 100644 --- a/scripts/docs/buildCoreApiDocs/config/projectSettings.ts +++ b/scripts/docs/buildCoreApiDocs/config/projectSettings.ts @@ -28,7 +28,7 @@ export const projectSettings: ProjectSettings = { return directories[3] === 'nextjs'; }, - skipSlotsAndClasses: true, + skipSlotsAndClasses: false, translationPagesDirectory: 'docs/translations/api-docs', importTranslationPagesDirectory: 'docs-toolpad/translations/api-docs', generateClassName: () => '',