diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutAccountSidebar.js b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutAccountSidebar.js new file mode 100644 index 00000000000..91d4e58d0bc --- /dev/null +++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutAccountSidebar.js @@ -0,0 +1,295 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Stack from '@mui/material/Stack'; +import MenuList from '@mui/material/MenuList'; +import MenuItem from '@mui/material/MenuItem'; +import ListItemText from '@mui/material/ListItemText'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import Avatar from '@mui/material/Avatar'; +import Divider from '@mui/material/Divider'; +import { createTheme } from '@mui/material/styles'; +import DashboardIcon from '@mui/icons-material/Dashboard'; +import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { DashboardLayout } from '@toolpad/core/DashboardLayout'; +import { + Account, + AccountPreview, + AccountPopoverFooter, + SignOutButton, +} from '@toolpad/core/Account'; + +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 AccountSidebarPreview(props) { + const { handleClick, open, mini } = props; + return ( + + + + + ); +} + +AccountSidebarPreview.propTypes = { + /** + * The handler used when the preview is expanded + */ + handleClick: PropTypes.func, + mini: PropTypes.bool.isRequired, + /** + * The state of the Account popover + * @default false + */ + open: PropTypes.bool, +}; + +const accounts = [ + { + id: 1, + name: 'Bharat Kashyap', + email: 'bharatkashyap@outlook.com', + image: 'https://avatars.githubusercontent.com/u/19550456', + projects: [ + { + id: 3, + title: 'Project X', + }, + ], + }, + { + id: 2, + name: 'Bharat MUI', + email: 'bharat@mui.com', + color: '#8B4513', // Brown color + projects: [{ id: 4, title: 'Project A' }], + }, +]; + +function SidebarFooterAccountPopover() { + return ( + + + Accounts + + + {accounts.map((account) => ( + + + + {account.name[0]} + + + + + ))} + + + + + + + ); +} + +const createPreviewComponent = (mini) => { + function PreviewComponent(props) { + return ; + } + return PreviewComponent; +}; + +function SidebarFooterAccount({ mini }) { + const PreviewComponent = React.useMemo(() => createPreviewComponent(mini), [mini]); + return ( + + `drop-shadow(0px 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.10)' : 'rgba(0,0,0,0.32)'})`, + mt: 1, + '&::before': { + content: '""', + display: 'block', + position: 'absolute', + bottom: 10, + left: 0, + width: 10, + height: 10, + bgcolor: 'background.paper', + transform: 'translate(-50%, -50%) rotate(45deg)', + zIndex: 0, + }, + }, + }, + }, + }, + }} + /> + ); +} + +SidebarFooterAccount.propTypes = { + mini: PropTypes.bool.isRequired, +}; + +const demoSession = { + user: { + name: 'Bharat Kashyap', + email: 'bharatkashyap@outlook.com', + image: 'https://avatars.githubusercontent.com/u/19550456', + }, +}; + +function DashboardLayoutAccountSidebar(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; + + const [session, setSession] = React.useState(demoSession); + const authentication = React.useMemo(() => { + return { + signIn: () => { + setSession(demoSession); + }, + signOut: () => { + setSession(null); + }, + }; + }, []); + + return ( + + null, sidebarFooter: SidebarFooterAccount }} + > + + + + ); +} + +DashboardLayoutAccountSidebar.propTypes = { + /** + * Injected by the documentation to work in an iframe. + * Remove this when copying and pasting into your project. + */ + window: PropTypes.func, +}; + +export default DashboardLayoutAccountSidebar; diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutAccountSidebar.tsx b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutAccountSidebar.tsx new file mode 100644 index 00000000000..d88c885b3a4 --- /dev/null +++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutAccountSidebar.tsx @@ -0,0 +1,272 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Stack from '@mui/material/Stack'; +import MenuList from '@mui/material/MenuList'; +import MenuItem from '@mui/material/MenuItem'; +import ListItemText from '@mui/material/ListItemText'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import Avatar from '@mui/material/Avatar'; +import Divider from '@mui/material/Divider'; +import { createTheme } from '@mui/material/styles'; +import DashboardIcon from '@mui/icons-material/Dashboard'; +import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { DashboardLayout, SidebarFooterProps } from '@toolpad/core/DashboardLayout'; +import { + Account, + AccountPreview, + AccountPopoverFooter, + SignOutButton, + AccountPreviewProps, +} from '@toolpad/core/Account'; +import type { Navigation, Router, Session } from '@toolpad/core/AppProvider'; + +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 AccountSidebarPreview(props: AccountPreviewProps & { mini: boolean }) { + const { handleClick, open, mini } = props; + return ( + + + + + ); +} + +const accounts = [ + { + id: 1, + name: 'Bharat Kashyap', + email: 'bharatkashyap@outlook.com', + image: 'https://avatars.githubusercontent.com/u/19550456', + projects: [ + { + id: 3, + title: 'Project X', + }, + ], + }, + { + id: 2, + name: 'Bharat MUI', + email: 'bharat@mui.com', + color: '#8B4513', // Brown color + projects: [{ id: 4, title: 'Project A' }], + }, +]; + +function SidebarFooterAccountPopover() { + return ( + + + Accounts + + + {accounts.map((account) => ( + + + + {account.name[0]} + + + + + ))} + + + + + + + ); +} + +const createPreviewComponent = (mini: boolean) => { + function PreviewComponent(props: AccountPreviewProps) { + return ; + } + return PreviewComponent; +}; + +function SidebarFooterAccount({ mini }: SidebarFooterProps) { + const PreviewComponent = React.useMemo(() => createPreviewComponent(mini), [mini]); + return ( + + `drop-shadow(0px 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.10)' : 'rgba(0,0,0,0.32)'})`, + mt: 1, + '&::before': { + content: '""', + display: 'block', + position: 'absolute', + bottom: 10, + left: 0, + width: 10, + height: 10, + bgcolor: 'background.paper', + transform: 'translate(-50%, -50%) rotate(45deg)', + zIndex: 0, + }, + }, + }, + }, + }, + }} + /> + ); +} + +interface DemoProps { + /** + * Injected by the documentation to work in an iframe. + * Remove this when copying and pasting into your project. + */ + window?: () => Window; +} + +const demoSession = { + user: { + name: 'Bharat Kashyap', + email: 'bharatkashyap@outlook.com', + image: 'https://avatars.githubusercontent.com/u/19550456', + }, +}; + +export default function DashboardLayoutAccountSidebar(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; + + const [session, setSession] = React.useState(demoSession); + const authentication = React.useMemo(() => { + return { + signIn: () => { + setSession(demoSession); + }, + signOut: () => { + setSession(null); + }, + }; + }, []); + + return ( + + null, sidebarFooter: SidebarFooterAccount }} + > + + + + ); +} diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutAccountSidebar.tsx.preview b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutAccountSidebar.tsx.preview new file mode 100644 index 00000000000..f567d5793e9 --- /dev/null +++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutAccountSidebar.tsx.preview @@ -0,0 +1,5 @@ + null, sidebarFooter: SidebarFooterAccount }} +> + + \ 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 92196943efb..91572f19ed6 100644 --- a/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md +++ b/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md @@ -146,3 +146,7 @@ This allows you to add, for example: - footer content in the sidebar. {{"demo": "DashboardLayoutSlots.js", "height": 400, "iframe": true}} + +Through this, you can also modify the position of the `` component and use it inside the sidebar: + +{{"demo": "DashboardLayoutAccountSidebar.js", "height": 400, "iframe": true}} diff --git a/docs/pages/toolpad/core/api/account-preview.json b/docs/pages/toolpad/core/api/account-preview.json index 8608aa72476..bca8fcd7acb 100644 --- a/docs/pages/toolpad/core/api/account-preview.json +++ b/docs/pages/toolpad/core/api/account-preview.json @@ -9,7 +9,10 @@ } }, "slots": { - "type": { "name": "shape", "description": "{ avatar?: elementType }" }, + "type": { + "name": "shape", + "description": "{ avatar?: elementType, avatarIconButton?: elementType, moreIconButton?: elementType }" + }, "additionalInfo": { "slotsApi": true } }, "variant": { diff --git a/packages/toolpad-core/src/Account/Account.test.tsx b/packages/toolpad-core/src/Account/Account.test.tsx index ab7df9e0b0a..a94e4e4c4d2 100644 --- a/packages/toolpad-core/src/Account/Account.test.tsx +++ b/packages/toolpad-core/src/Account/Account.test.tsx @@ -51,7 +51,7 @@ describe('AppProvider', () => { await userEvent.click(userButton); - expect(screen.getByText('John Doe')).toBeInTheDocument(); + expect(screen.getByText('John Doe', { selector: 'p' })).toBeInTheDocument(); expect(screen.getByText('john@example.com')).toBeInTheDocument(); const signOutButton = screen.getByRole('button', { name: 'Sign Out' }); diff --git a/packages/toolpad-core/src/Account/Account.tsx b/packages/toolpad-core/src/Account/Account.tsx index 0c6a105f5e1..c4b0876ad8a 100644 --- a/packages/toolpad-core/src/Account/Account.tsx +++ b/packages/toolpad-core/src/Account/Account.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import Button from '@mui/material/Button'; -import Popover from '@mui/material/Popover'; +import Button, { ButtonProps } from '@mui/material/Button'; +import Popover, { PopoverProps } from '@mui/material/Popover'; import Divider from '@mui/material/Divider'; -import Stack from '@mui/material/Stack'; +import Stack, { StackProps } from '@mui/material/Stack'; import { SignInButton } from './SignInButton'; import { SignOutButton } from './SignOutButton'; import { AccountPreview, AccountPreviewProps } from './AccountPreview'; @@ -17,27 +17,27 @@ export interface AccountSlots { * The component used for the account preview * @default AccountPreview */ - preview?: React.ElementType; + preview?: React.JSXElementConstructor; /** * The component used for the account popover menu * @default Popover */ - popover?: React.ElementType; + popover?: React.JSXElementConstructor; /** * The component used for the content of account popover * @default Stack */ - popoverContent?: React.ElementType; + popoverContent?: React.JSXElementConstructor; /** * The component used for the sign in button. * @default Button */ - signInButton?: React.ElementType; + signInButton?: React.JSXElementConstructor; /** * The component used for the sign out button. * @default Button */ - signOutButton?: React.ElementType; + signOutButton?: React.JSXElementConstructor; } export interface AccountProps { @@ -50,7 +50,7 @@ export interface AccountProps { */ slotProps?: { preview?: AccountPreviewProps; - popover?: React.ComponentProps; + popover?: Omit, 'open'>; popoverContent?: React.ComponentProps; signInButton?: React.ComponentProps; signOutButton?: React.ComponentProps; @@ -118,7 +118,12 @@ function Account(props: AccountProps) { /> )} {slots?.popover ? ( - + ) : ( + `drop-shadow(0px 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.10)' : 'rgba(0,0,0,0.32)'})`, mt: 1, '&::before': { content: '""', diff --git a/packages/toolpad-core/src/Account/AccountPreview.tsx b/packages/toolpad-core/src/Account/AccountPreview.tsx index dc4455269c5..a165e771e40 100644 --- a/packages/toolpad-core/src/Account/AccountPreview.tsx +++ b/packages/toolpad-core/src/Account/AccountPreview.tsx @@ -93,15 +93,17 @@ function AccountPreview(props: AccountPreviewProps) { if (variant === 'expanded') { return ( - - {avatarContent} - - - {session.user?.name} - - - {session.user?.email} - + + + {avatarContent} + + + {session.user?.name} + + + {session.user?.email} + + {handleClick && (slots?.moreIconButton ? ( @@ -111,7 +113,7 @@ function AccountPreview(props: AccountPreviewProps) { size="small" onClick={handleClick} {...slotProps?.moreIconButton} - sx={{ alignSelf: 'flex-start', ...slotProps?.moreIconButton?.sx }} + sx={{ alignSelf: 'center', ...slotProps?.moreIconButton?.sx }} > @@ -129,6 +131,7 @@ function AccountPreview(props: AccountPreviewProps) { onClick={handleClick} aria-label={localeText.iconButtonAriaLabel || 'Current User'} size="small" + sx={{ width: 'fit-content', margin: '0 auto' }} aria-controls={open ? 'account-menu' : undefined} aria-haspopup="true" aria-expanded={open ? 'true' : undefined} @@ -168,6 +171,8 @@ AccountPreview.propTypes /* remove-proptypes */ = { */ slots: PropTypes.shape({ avatar: PropTypes.elementType, + avatarIconButton: PropTypes.elementType, + moreIconButton: PropTypes.elementType, }), /** * The type of account details to display. diff --git a/playground/nextjs/src/app/(dashboard)/SidebarFooterAccount.tsx b/playground/nextjs/src/app/(dashboard)/SidebarFooterAccount.tsx deleted file mode 100644 index aceb55524f4..00000000000 --- a/playground/nextjs/src/app/(dashboard)/SidebarFooterAccount.tsx +++ /dev/null @@ -1,104 +0,0 @@ -'use client'; - -import * as React from 'react'; - -import Typography from '@mui/material/Typography'; -import Stack from '@mui/material/Stack'; -import MenuList from '@mui/material/MenuList'; -import MenuItem from '@mui/material/MenuItem'; -import ListItemText from '@mui/material/ListItemText'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import Avatar from '@mui/material/Avatar'; -import Divider from '@mui/material/Divider'; - -import { - AccountPreview, - AccountPopoverFooter, - SignOutButton, - AccountPreviewProps, -} from '@toolpad/core/Account'; - -export function AccountSidebarPreview(props: AccountPreviewProps) { - const { handleClick, open } = props; - return ( - - - - - ); -} - -const accounts = [ - { - id: 1, - name: 'Bharat Kashyap', - email: 'bharatkashyap@outlook.com', - image: 'https://avatars.githubusercontent.com/u/19550456', - projects: [ - { - id: 3, - title: 'Project X', - }, - ], - }, - { - id: 2, - name: 'Bharat MUI', - email: 'bharat@mui.com', - color: '#8B4513', // Brown color - projects: [{ id: 4, title: 'Project A' }], - }, -]; - -export function SidebarFooterAccountPopover() { - return ( - - - Accounts - - - {accounts.map((account) => ( - - - - {account.name[0]} - - - - - ))} - - - - - - ); -} diff --git a/playground/nextjs/src/app/(dashboard)/layout.tsx b/playground/nextjs/src/app/(dashboard)/layout.tsx index e481f628f1c..e4bb596b791 100644 --- a/playground/nextjs/src/app/(dashboard)/layout.tsx +++ b/playground/nextjs/src/app/(dashboard)/layout.tsx @@ -1,10 +1,164 @@ +'use client'; import * as React from 'react'; -import { DashboardLayout } from '@toolpad/core/DashboardLayout'; +import Typography from '@mui/material/Typography'; +import Stack from '@mui/material/Stack'; +import MenuList from '@mui/material/MenuList'; +import MenuItem from '@mui/material/MenuItem'; +import ListItemText from '@mui/material/ListItemText'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import Avatar from '@mui/material/Avatar'; +import Divider from '@mui/material/Divider'; +import { + Account, + AccountPreview, + AccountPopoverFooter, + SignOutButton, + AccountPreviewProps, +} from '@toolpad/core/Account'; +import { DashboardLayout, SidebarFooterProps } from '@toolpad/core/DashboardLayout'; import { PageContainer } from '@toolpad/core/PageContainer'; +const accounts = [ + { + id: 1, + name: 'Bharat Kashyap', + email: 'bharatkashyap@outlook.com', + image: 'https://avatars.githubusercontent.com/u/19550456', + projects: [ + { + id: 3, + title: 'Project X', + }, + ], + }, + { + id: 2, + name: 'Bharat MUI', + email: 'bharat@mui.com', + color: '#8B4513', // Brown color + projects: [{ id: 4, title: 'Project A' }], + }, +]; + +function AccountSidebarPreview(props: AccountPreviewProps & { mini: boolean }) { + const { handleClick, open, mini } = props; + return ( + + + + + ); +} + +function SidebarFooterAccountPopover() { + return ( + + + Accounts + + + {accounts.map((account) => ( + + + + {account.name[0]} + + + + + ))} + + + + + + + ); +} + +const createPreviewComponent = (mini: boolean) => { + function PreviewComponent(props: AccountPreviewProps) { + return ; + } + return PreviewComponent; +}; + +function SidebarFooterAccount({ mini }: SidebarFooterProps) { + const PreviewComponent = React.useMemo(() => createPreviewComponent(mini), [mini]); + return ( + + `drop-shadow(0px 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.10)' : 'rgba(0,0,0,0.32)'})`, + mt: 1, + '&::before': { + content: '""', + display: 'block', + position: 'absolute', + bottom: 10, + left: 0, + width: 10, + height: 10, + bgcolor: 'background.paper', + transform: 'translate(-50%, -50%) rotate(45deg)', + zIndex: 0, + }, + }, + }, + }, + }, + }} + /> + ); +} + export default function DashboardPagesLayout(props: { children: React.ReactNode }) { return ( - + null }}> {props.children} );