diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSlots.js b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSlots.js new file mode 100644 index 00000000000..e6f1418c873 --- /dev/null +++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSlots.js @@ -0,0 +1,127 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import { createTheme } from '@mui/material/styles'; +import DashboardIcon from '@mui/icons-material/Dashboard'; +import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; +import SearchIcon from '@mui/icons-material/Search'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { DashboardLayout } from '@toolpad/core/DashboardLayout'; + +const NAVIGATION = [ + { + kind: 'header', + title: 'Main items', + }, + { + segment: 'dashboard', + title: 'Dashboard', + icon: , + }, + { + segment: 'orders', + title: 'Orders', + icon: , + }, +]; + +const demoTheme = createTheme({ + cssVariables: { + colorSchemeSelector: 'data-toolpad-color-scheme', + }, + colorSchemes: { light: true, dark: true }, + breakpoints: { + values: { + xs: 0, + sm: 600, + md: 600, + lg: 1200, + xl: 1536, + }, + }, +}); + +function DemoPageContent({ pathname }) { + return ( + + Dashboard content for {pathname} + + ); +} + +DemoPageContent.propTypes = { + pathname: PropTypes.string.isRequired, +}; + +function SearchBar() { + return ( + + + + ), + sx: { pr: 0.5 }, + }, + }} + sx={{ mr: 1 }} + /> + ); +} + +function DashboardLayoutSlots(props) { + const { window } = props; + + const [pathname, setPathname] = React.useState('/dashboard'); + + const router = React.useMemo(() => { + return { + pathname, + searchParams: new URLSearchParams(), + navigate: (path) => setPathname(String(path)), + }; + }, [pathname]); + + // Remove this const when copying and pasting into your project. + const demoWindow = window !== undefined ? window() : undefined; + + return ( + + + + + + ); +} + +DashboardLayoutSlots.propTypes = { + /** + * Injected by the documentation to work in an iframe. + * Remove this when copying and pasting into your project. + */ + window: PropTypes.func, +}; + +export default DashboardLayoutSlots; diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSlots.tsx b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSlots.tsx new file mode 100644 index 00000000000..a237c269ad9 --- /dev/null +++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSlots.tsx @@ -0,0 +1,120 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import { createTheme } from '@mui/material/styles'; +import DashboardIcon from '@mui/icons-material/Dashboard'; +import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; +import SearchIcon from '@mui/icons-material/Search'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { DashboardLayout } from '@toolpad/core/DashboardLayout'; + +const NAVIGATION: Navigation = [ + { + kind: 'header', + title: 'Main items', + }, + { + segment: 'dashboard', + title: 'Dashboard', + icon: , + }, + { + segment: 'orders', + title: 'Orders', + icon: , + }, +]; + +const demoTheme = createTheme({ + cssVariables: { + colorSchemeSelector: 'data-toolpad-color-scheme', + }, + colorSchemes: { light: true, dark: true }, + breakpoints: { + values: { + xs: 0, + sm: 600, + md: 600, + lg: 1200, + xl: 1536, + }, + }, +}); + +function DemoPageContent({ pathname }: { pathname: string }) { + return ( + + Dashboard content for {pathname} + + ); +} + +function SearchBar() { + return ( + + + + ), + sx: { pr: 0.5 }, + }, + }} + sx={{ mr: 1 }} + /> + ); +} + +interface DemoProps { + /** + * Injected by the documentation to work in an iframe. + * Remove this when copying and pasting into your project. + */ + window?: () => Window; +} + +export default function DashboardLayoutSlots(props: DemoProps) { + const { window } = props; + + const [pathname, setPathname] = React.useState('/dashboard'); + + const router = React.useMemo(() => { + return { + pathname, + searchParams: new URLSearchParams(), + navigate: (path) => setPathname(String(path)), + }; + }, [pathname]); + + // Remove this const when copying and pasting into your project. + const demoWindow = window !== undefined ? window() : undefined; + + return ( + + + + + + ); +} diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSlots.tsx.preview b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSlots.tsx.preview new file mode 100644 index 00000000000..a504a4b624d --- /dev/null +++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSlots.tsx.preview @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md b/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md index 13eb5a5746b..3b20296f08b 100644 --- a/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md +++ b/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md @@ -91,3 +91,10 @@ The use of an `iframe` may cause some spacing issues in the following demo. ::: {{"demo": "DashboardLayoutAccount.js", "height": 400, "iframe": true}} + +## Customization + +Some areas of the layout can be replaced with custom components by using the `slots` and `slotProps` props. +For example, this allows you to add new items to the toolbar in the header, such as a search bar. + +{{"demo": "DashboardLayoutSlots.js", "height": 400, "iframe": true}} diff --git a/docs/pages/toolpad/core/api/dashboard-layout.json b/docs/pages/toolpad/core/api/dashboard-layout.json index 87b9dec9b3b..c1d6e51523b 100644 --- a/docs/pages/toolpad/core/api/dashboard-layout.json +++ b/docs/pages/toolpad/core/api/dashboard-layout.json @@ -1,10 +1,41 @@ { - "props": { "children": { "type": { "name": "node" }, "required": true } }, + "props": { + "children": { "type": { "name": "node" }, "required": true }, + "slotProps": { + "type": { + "name": "shape", + "description": "{ toolbarAccount?: { signInLabel?: string, signOutLabel?: string, slotProps?: { avatar?: object, iconButton?: object, signInButton?: object, signOutButton?: object } }, toolbarActions?: object }" + }, + "default": "{}" + }, + "slots": { + "type": { + "name": "shape", + "description": "{ toolbarAccount?: elementType, toolbarActions?: elementType }" + }, + "default": "{}", + "additionalInfo": { "slotsApi": true } + } + }, "name": "DashboardLayout", "imports": [ "import { DashboardLayout } from '@toolpad/core/DashboardLayout';", "import { DashboardLayout } from '@toolpad/core';" ], + "slots": [ + { + "name": "toolbarActions", + "description": "The toolbar actions component used in the layout header.", + "default": "ToolbarActions", + "class": null + }, + { + "name": "toolbarAccount", + "description": "The toolbar account component used in the layout header.", + "default": "Account", + "class": null + } + ], "classes": [], "spread": true, "themeDefaultProps": null, diff --git a/docs/pages/toolpad/core/api/sign-in-page.json b/docs/pages/toolpad/core/api/sign-in-page.json index 4621baecf70..606d4cd0994 100644 --- a/docs/pages/toolpad/core/api/sign-in-page.json +++ b/docs/pages/toolpad/core/api/sign-in-page.json @@ -33,6 +33,38 @@ "import { SignInPage } from '@toolpad/core/SignInPage';", "import { SignInPage } from '@toolpad/core';" ], + "slots": [ + { + "name": "emailField", + "description": "The custom email field component used in the credentials form.", + "default": "TextField", + "class": null + }, + { + "name": "passwordField", + "description": "The custom password field component used in the credentials form.", + "default": "TextField", + "class": null + }, + { + "name": "submitButton", + "description": "The custom submit button component used in the credentials form.", + "default": "LoadingButton", + "class": null + }, + { + "name": "forgotPasswordLink", + "description": "The custom forgot password link component used in the credentials form.", + "default": "Link", + "class": null + }, + { + "name": "signUpLink", + "description": "The custom sign up link component used in the credentials form.", + "default": "Link", + "class": null + } + ], "classes": [], "spread": true, "themeDefaultProps": false, diff --git a/docs/translations/api-docs/dashboard-layout/dashboard-layout.json b/docs/translations/api-docs/dashboard-layout/dashboard-layout.json index 8e461ca2aaa..369522018ed 100644 --- a/docs/translations/api-docs/dashboard-layout/dashboard-layout.json +++ b/docs/translations/api-docs/dashboard-layout/dashboard-layout.json @@ -1,5 +1,13 @@ { "componentDescription": "", - "propDescriptions": { "children": { "description": "The content of the dashboard." } }, - "classDescriptions": {} + "propDescriptions": { + "children": { "description": "The content of the dashboard." }, + "slotProps": { "description": "The props used for each slot inside." }, + "slots": { "description": "The components used for each slot inside." } + }, + "classDescriptions": {}, + "slotDescriptions": { + "toolbarAccount": "The toolbar account component used in the layout header.", + "toolbarActions": "The toolbar actions component used in the layout header." + } } diff --git a/docs/translations/api-docs/sign-in-page/sign-in-page.json b/docs/translations/api-docs/sign-in-page/sign-in-page.json index d27f08ae393..066ea0ed30d 100644 --- a/docs/translations/api-docs/sign-in-page/sign-in-page.json +++ b/docs/translations/api-docs/sign-in-page/sign-in-page.json @@ -13,5 +13,12 @@ "slotProps": { "description": "The props used for each slot inside." }, "slots": { "description": "The components used for each slot inside." } }, - "classDescriptions": {} + "classDescriptions": {}, + "slotDescriptions": { + "emailField": "The custom email field component used in the credentials form.", + "forgotPasswordLink": "The custom forgot password link component used in the credentials form.", + "passwordField": "The custom password field component used in the credentials form.", + "signUpLink": "The custom sign up link component used in the credentials form.", + "submitButton": "The custom submit button component used in the credentials form." + } } diff --git a/packages/toolpad-core/src/AppProvider/AppProvider.tsx b/packages/toolpad-core/src/AppProvider/AppProvider.tsx index f600a19ead1..d1732a3211a 100644 --- a/packages/toolpad-core/src/AppProvider/AppProvider.tsx +++ b/packages/toolpad-core/src/AppProvider/AppProvider.tsx @@ -70,10 +70,10 @@ export interface Authentication { signOut: () => void; } -export const SessionContext = React.createContext(null); - export const AuthenticationContext = React.createContext(null); +export const SessionContext = React.createContext(null); + export type AppTheme = Theme | { light: Theme; dark: Theme }; export interface AppProviderProps { diff --git a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx index 7c5285b6fdc..4c8ecb9f184 100644 --- a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx +++ b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import { styled, useTheme } from '@mui/material'; +import { styled } from '@mui/material'; import MuiAppBar from '@mui/material/AppBar'; import Box from '@mui/material/Box'; import Collapse from '@mui/material/Collapse'; @@ -19,30 +19,28 @@ import Toolbar from '@mui/material/Toolbar'; import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; import type {} from '@mui/material/themeCssVarsAugmentation'; -import DarkModeIcon from '@mui/icons-material/DarkMode'; -import LightModeIcon from '@mui/icons-material/LightMode'; import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import MenuIcon from '@mui/icons-material/Menu'; import MenuOpenIcon from '@mui/icons-material/MenuOpen'; -import useSsr from '@toolpad/utils/hooks/useSsr'; -import { Account } from '../Account'; import { Link } from '../shared/Link'; import { BrandingContext, NavigationContext, - PaletteModeContext, RouterContext, WindowContext, } from '../shared/context'; import type { Navigation } from '../AppProvider'; -import { ToolpadLogo } from './ToolpadLogo'; +import { Account, type AccountProps } from '../Account'; import { getItemTitle, getPageItemFullPath, hasSelectedNavigationChildren, } from '../shared/navigation'; import { useApplicationTitle } from '../shared/branding'; +import { ToolbarActions } from './ToolbarActions'; +import { ThemeSwitcher } from './ThemeSwitcher'; +import { ToolpadLogo } from './ToolpadLogo'; const DRAWER_WIDTH = 320; // px @@ -86,65 +84,6 @@ const NavigationListItemButton = styled(ListItemButton)(({ theme }) => ({ }, })); -function ThemeSwitcher() { - const isSsr = useSsr(); - const theme = useTheme(); - - const { paletteMode, setPaletteMode, isDualTheme } = React.useContext(PaletteModeContext); - - const toggleMode = React.useCallback(() => { - setPaletteMode(paletteMode === 'dark' ? 'light' : 'dark'); - }, [paletteMode, setPaletteMode]); - - return isDualTheme ? ( - -
- - {theme.getColorSchemeSelector ? ( - - - - - ) : ( - - {isSsr || paletteMode !== 'dark' ? : } - - )} - -
-
- ) : null; -} - interface DashboardSidebarSubNavigationProps { subNavigation: Navigation; basePath?: string; @@ -313,11 +252,37 @@ function DashboardSidebarSubNavigation({ ); } +export interface DashboardLayoutSlots { + /** + * The toolbar actions component used in the layout header. + * @default ToolbarActions + */ + toolbarActions?: React.JSXElementConstructor<{}>; + /** + * The toolbar account component used in the layout header. + * @default Account + */ + toolbarAccount?: React.JSXElementConstructor; +} + export interface DashboardLayoutProps { /** * The content of the dashboard. */ children: React.ReactNode; + /** + * The components used for each slot inside. + * @default {} + */ + slots?: DashboardLayoutSlots; + /** + * The props used for each slot inside. + * @default {} + */ + slotProps?: { + toolbarActions?: {}; + toolbarAccount?: AccountProps; + }; } /** @@ -331,7 +296,7 @@ export interface DashboardLayoutProps { * - [DashboardLayout API](https://mui.com/toolpad/core/api/dashboard-layout) */ function DashboardLayout(props: DashboardLayoutProps) { - const { children } = props; + const { children, slots, slotProps } = props; const branding = React.useContext(BrandingContext); const navigation = React.useContext(NavigationContext); @@ -379,6 +344,9 @@ function DashboardLayout(props: DashboardLayoutProps) { ); + const ToolbarActionsSlot = slots?.toolbarActions ?? ToolbarActions; + const ToolbarAccountSlot = slots?.toolbarAccount ?? Account; + return ( @@ -428,8 +396,9 @@ function DashboardLayout(props: DashboardLayoutProps) { + - + { + setPaletteMode(paletteMode === 'dark' ? 'light' : 'dark'); + }, [paletteMode, setPaletteMode]); + + return isDualTheme ? ( + +
+ + {theme.getColorSchemeSelector ? ( + + + + + ) : ( + + {isSsr || paletteMode !== 'dark' ? : } + + )} + +
+
+ ) : null; +} + +export { ThemeSwitcher }; diff --git a/packages/toolpad-core/src/DashboardLayout/ToolbarActions.tsx b/packages/toolpad-core/src/DashboardLayout/ToolbarActions.tsx new file mode 100644 index 00000000000..76c058f88fb --- /dev/null +++ b/packages/toolpad-core/src/DashboardLayout/ToolbarActions.tsx @@ -0,0 +1,10 @@ +'use client'; + +/** + * @ignore - internal component. + */ +function ToolbarActions() { + return null; +} + +export { ToolbarActions }; diff --git a/packages/toolpad-core/src/DashboardLayout/index.ts b/packages/toolpad-core/src/DashboardLayout/index.ts index fc36c152c7f..f65e521b923 100644 --- a/packages/toolpad-core/src/DashboardLayout/index.ts +++ b/packages/toolpad-core/src/DashboardLayout/index.ts @@ -1 +1,4 @@ export * from './DashboardLayout'; + +// Default slots components +export * from './ToolbarActions'; diff --git a/packages/toolpad-core/src/SignInPage/SignInPage.tsx b/packages/toolpad-core/src/SignInPage/SignInPage.tsx index f51c13a471c..a533761eea9 100644 --- a/packages/toolpad-core/src/SignInPage/SignInPage.tsx +++ b/packages/toolpad-core/src/SignInPage/SignInPage.tsx @@ -79,6 +79,34 @@ export interface AuthResponse { type?: string; } +export interface SignInPageSlots { + /** + * The custom email field component used in the credentials form. + * @default TextField + */ + emailField?: React.JSXElementConstructor; + /** + * The custom password field component used in the credentials form. + * @default TextField + */ + passwordField?: React.JSXElementConstructor; + /** + * The custom submit button component used in the credentials form. + * @default LoadingButton + */ + submitButton?: React.JSXElementConstructor; + /** + * The custom forgot password link component used in the credentials form. + * @default Link + */ + forgotPasswordLink?: React.JSXElementConstructor; + /** + * The custom sign up link component used in the credentials form. + * @default Link + */ + signUpLink?: React.JSXElementConstructor; +} + export interface SignInPageProps { /** * The list of authentication providers to display. @@ -104,33 +132,7 @@ export interface SignInPageProps { * @example { forgotPasswordLink: Forgot password? } * @example { signUpLink: Sign up } */ - slots?: { - /** - * The custom email field component used in the credentials form. - * @default TextField - */ - emailField?: React.JSXElementConstructor; - /** - * The custom password field component used in the credentials form. - * @default TextField - */ - passwordField?: React.JSXElementConstructor; - /** - * The custom submit button component used in the credentials form. - * @default LoadingButton - */ - submitButton?: React.JSXElementConstructor; - /** - * The custom forgot password link component used in the credentials form. - * @default Link - */ - forgotPasswordLink?: React.JSXElementConstructor; - /** - * The custom sign up link component used in the credentials form. - * @default Link - */ - signUpLink?: React.JSXElementConstructor; - }; + slots?: SignInPageSlots; /** * The props used for each slot inside. * @default {}