diff --git a/docs/data/tree-view/rich-tree-view/customization/FileExplorer.js b/docs/data/tree-view/rich-tree-view/customization/FileExplorer.js new file mode 100644 index 000000000000..2ac57640beea --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/customization/FileExplorer.js @@ -0,0 +1,267 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { animated, useSpring } from '@react-spring/web'; +import { styled, alpha } from '@mui/material/styles'; + +import Box from '@mui/material/Box'; +import Collapse from '@mui/material/Collapse'; +import Typography from '@mui/material/Typography'; +import ArticleIcon from '@mui/icons-material/Article'; +import DeleteIcon from '@mui/icons-material/Delete'; +import FolderOpenIcon from '@mui/icons-material/FolderOpen'; +import FolderRounded from '@mui/icons-material/FolderRounded'; +import ImageIcon from '@mui/icons-material/Image'; +import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; +import VideoCameraBackIcon from '@mui/icons-material/VideoCameraBack'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { treeItemClasses } from '@mui/x-tree-view/TreeItem'; +import { unstable_useTreeItem2 as useTreeItem2 } from '@mui/x-tree-view/useTreeItem2'; +import { + TreeItem2Content, + TreeItem2IconContainer, + TreeItem2Label, + TreeItem2Root, +} from '@mui/x-tree-view/TreeItem2'; +import { TreeItem2Icon } from '@mui/x-tree-view/TreeItem2Icon'; +import { TreeItem2Provider } from '@mui/x-tree-view/TreeItem2Provider'; + +const ITEMS = [ + { + id: '1', + label: 'Documents', + children: [ + { + id: '1.1', + label: 'Company', + children: [ + { id: '1.1.1', label: 'Invoice', fileType: 'pdf' }, + { id: '1.1.2', label: 'Meeting notes', fileType: 'doc' }, + { id: '1.1.3', label: 'Tasks list', fileType: 'doc' }, + { id: '1.1.4', label: 'Equipment', fileType: 'pdf' }, + { id: '1.1.5', label: 'Video conference', fileType: 'video' }, + ], + }, + { id: '1.2', label: 'Personal', fileType: 'folder' }, + { id: '1.3', label: 'Group photo', fileType: 'image' }, + ], + }, + { + id: '2', + label: 'Bookmarked', + fileType: 'pinned', + children: [ + { id: '2.1', label: 'Learning materials', fileType: 'folder' }, + { id: '2.2', label: 'News', fileType: 'folder' }, + { id: '2.3', label: 'Forums', fileType: 'folder' }, + { id: '2.4', label: 'Travel documents', fileType: 'pdf' }, + ], + }, + { id: '3', label: 'History', fileType: 'folder' }, + { id: '4', label: 'Trash', fileType: 'trash' }, +]; + +function DotIcon() { + return ( + + ); +} + +const StyledTreeItemRoot = styled(TreeItem2Root)(({ theme }) => ({ + color: + theme.palette.mode === 'light' + ? theme.palette.grey[800] + : theme.palette.grey[400], + position: 'relative', + [`& .${treeItemClasses.groupTransition}`]: { + marginLeft: theme.spacing(3.5), + }, +})); + +const CustomTreeItemContent = styled(TreeItem2Content)(({ theme }) => ({ + flexDirection: 'row-reverse', + borderRadius: theme.spacing(0.7), + marginBottom: theme.spacing(0.5), + marginTop: theme.spacing(0.5), + padding: theme.spacing(0.5), + paddingRight: theme.spacing(1), + fontWeight: 500, + [`& .${treeItemClasses.iconContainer}`]: { + marginRight: theme.spacing(2), + }, + [`&.Mui-expanded `]: { + '&:not(.Mui-focused, .Mui-selected, .Mui-selected.Mui-focused) .labelIcon': { + color: + theme.palette.mode === 'light' + ? theme.palette.primary.main + : theme.palette.primary.dark, + }, + '&::before': { + content: '""', + display: 'block', + position: 'absolute', + left: '16px', + top: '44px', + height: 'calc(100% - 48px)', + width: '1.5px', + backgroundColor: + theme.palette.mode === 'light' + ? theme.palette.grey[300] + : theme.palette.grey[700], + }, + }, + '&:hover': { + backgroundColor: alpha(theme.palette.primary.main, 0.1), + color: theme.palette.mode === 'light' ? theme.palette.primary.main : 'white', + }, + [`&.Mui-focused, &.Mui-selected, &.Mui-selected.Mui-focused`]: { + backgroundColor: + theme.palette.mode === 'light' + ? theme.palette.primary.main + : theme.palette.primary.dark, + color: theme.palette.primary.contrastText, + }, +})); + +const AnimatedCollapse = animated(Collapse); + +function TransitionComponent(props) { + const style = useSpring({ + to: { + opacity: props.in ? 1 : 0, + transform: `translate3d(0,${props.in ? 0 : 20}px,0)`, + }, + }); + + return ; +} + +const StyledTreeItemLabelText = styled(Typography)({ + color: 'inherit', + fontFamily: 'General Sans', + fontWeight: 500, +}); + +function CustomLabel({ icon: Icon, expandable, children, ...other }) { + return ( + + {Icon && ( + + )} + + {children} + {expandable && } + + ); +} + +const isExpandable = (reactChildren) => { + if (Array.isArray(reactChildren)) { + return reactChildren.length > 0 && reactChildren.some(isExpandable); + } + return Boolean(reactChildren); +}; + +const getIconFromFileType = (fileType) => { + switch (fileType) { + case 'image': + return ImageIcon; + case 'pdf': + return PictureAsPdfIcon; + case 'doc': + return ArticleIcon; + case 'video': + return VideoCameraBackIcon; + case 'folder': + return FolderRounded; + case 'pinned': + return FolderOpenIcon; + case 'trash': + return DeleteIcon; + default: + return ArticleIcon; + } +}; + +const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { + const { id, itemId, label, disabled, children, ...other } = props; + + const { + getRootProps, + getContentProps, + getIconContainerProps, + getLabelProps, + getGroupTransitionProps, + status, + publicAPI, + } = useTreeItem2({ id, itemId, children, label, disabled, rootRef: ref }); + + const item = publicAPI.getItem(itemId); + const expandable = isExpandable(children); + let icon; + if (expandable) { + icon = FolderRounded; + } else if (item.fileType) { + icon = getIconFromFileType(item.fileType); + } + + return ( + + + + + + + + + + {children && } + + + ); +}); + +export default function FileExplorer() { + return ( + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/customization/FileExplorer.tsx b/docs/data/tree-view/rich-tree-view/customization/FileExplorer.tsx new file mode 100644 index 000000000000..d12cf981003a --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/customization/FileExplorer.tsx @@ -0,0 +1,303 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { animated, useSpring } from '@react-spring/web'; +import { styled, alpha } from '@mui/material/styles'; +import { TransitionProps } from '@mui/material/transitions'; +import Box from '@mui/material/Box'; +import Collapse from '@mui/material/Collapse'; +import Typography from '@mui/material/Typography'; +import ArticleIcon from '@mui/icons-material/Article'; +import DeleteIcon from '@mui/icons-material/Delete'; +import FolderOpenIcon from '@mui/icons-material/FolderOpen'; +import FolderRounded from '@mui/icons-material/FolderRounded'; +import ImageIcon from '@mui/icons-material/Image'; +import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; +import VideoCameraBackIcon from '@mui/icons-material/VideoCameraBack'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { treeItemClasses } from '@mui/x-tree-view/TreeItem'; +import { + unstable_useTreeItem2 as useTreeItem2, + UseTreeItem2Parameters, +} from '@mui/x-tree-view/useTreeItem2'; +import { + TreeItem2Content, + TreeItem2IconContainer, + TreeItem2Label, + TreeItem2Root, +} from '@mui/x-tree-view/TreeItem2'; +import { TreeItem2Icon } from '@mui/x-tree-view/TreeItem2Icon'; +import { TreeItem2Provider } from '@mui/x-tree-view/TreeItem2Provider'; +import { TreeViewBaseItem } from '@mui/x-tree-view/models'; + +type FileType = 'image' | 'pdf' | 'doc' | 'video' | 'folder' | 'pinned' | 'trash'; + +type ExtendedTreeItemProps = { + fileType?: FileType; + id: string; + label: string; +}; + +const ITEMS: TreeViewBaseItem[] = [ + { + id: '1', + label: 'Documents', + children: [ + { + id: '1.1', + label: 'Company', + children: [ + { id: '1.1.1', label: 'Invoice', fileType: 'pdf' }, + { id: '1.1.2', label: 'Meeting notes', fileType: 'doc' }, + { id: '1.1.3', label: 'Tasks list', fileType: 'doc' }, + { id: '1.1.4', label: 'Equipment', fileType: 'pdf' }, + { id: '1.1.5', label: 'Video conference', fileType: 'video' }, + ], + }, + { id: '1.2', label: 'Personal', fileType: 'folder' }, + { id: '1.3', label: 'Group photo', fileType: 'image' }, + ], + }, + { + id: '2', + label: 'Bookmarked', + fileType: 'pinned', + children: [ + { id: '2.1', label: 'Learning materials', fileType: 'folder' }, + { id: '2.2', label: 'News', fileType: 'folder' }, + { id: '2.3', label: 'Forums', fileType: 'folder' }, + { id: '2.4', label: 'Travel documents', fileType: 'pdf' }, + ], + }, + { id: '3', label: 'History', fileType: 'folder' }, + { id: '4', label: 'Trash', fileType: 'trash' }, +]; + +function DotIcon() { + return ( + + ); +} +declare module 'react' { + interface CSSProperties { + '--tree-view-color'?: string; + '--tree-view-bg-color'?: string; + } +} + +const StyledTreeItemRoot = styled(TreeItem2Root)(({ theme }) => ({ + color: + theme.palette.mode === 'light' + ? theme.palette.grey[800] + : theme.palette.grey[400], + position: 'relative', + [`& .${treeItemClasses.groupTransition}`]: { + marginLeft: theme.spacing(3.5), + }, +})) as unknown as typeof TreeItem2Root; + +const CustomTreeItemContent = styled(TreeItem2Content)(({ theme }) => ({ + flexDirection: 'row-reverse', + borderRadius: theme.spacing(0.7), + marginBottom: theme.spacing(0.5), + marginTop: theme.spacing(0.5), + padding: theme.spacing(0.5), + paddingRight: theme.spacing(1), + fontWeight: 500, + [`& .${treeItemClasses.iconContainer}`]: { + marginRight: theme.spacing(2), + }, + [`&.Mui-expanded `]: { + '&:not(.Mui-focused, .Mui-selected, .Mui-selected.Mui-focused) .labelIcon': { + color: + theme.palette.mode === 'light' + ? theme.palette.primary.main + : theme.palette.primary.dark, + }, + '&::before': { + content: '""', + display: 'block', + position: 'absolute', + left: '16px', + top: '44px', + height: 'calc(100% - 48px)', + width: '1.5px', + backgroundColor: + theme.palette.mode === 'light' + ? theme.palette.grey[300] + : theme.palette.grey[700], + }, + }, + '&:hover': { + backgroundColor: alpha(theme.palette.primary.main, 0.1), + color: theme.palette.mode === 'light' ? theme.palette.primary.main : 'white', + }, + [`&.Mui-focused, &.Mui-selected, &.Mui-selected.Mui-focused`]: { + backgroundColor: + theme.palette.mode === 'light' + ? theme.palette.primary.main + : theme.palette.primary.dark, + color: theme.palette.primary.contrastText, + }, +})); + +const AnimatedCollapse = animated(Collapse); + +function TransitionComponent(props: TransitionProps) { + const style = useSpring({ + to: { + opacity: props.in ? 1 : 0, + transform: `translate3d(0,${props.in ? 0 : 20}px,0)`, + }, + }); + + return ; +} + +const StyledTreeItemLabelText = styled(Typography)({ + color: 'inherit', + fontFamily: 'General Sans', + fontWeight: 500, +}) as unknown as typeof Typography; + +interface CustomLabelProps { + children: React.ReactNode; + icon?: React.ElementType; + expandable?: boolean; +} + +function CustomLabel({ + icon: Icon, + expandable, + children, + ...other +}: CustomLabelProps) { + return ( + + {Icon && ( + + )} + + {children} + {expandable && } + + ); +} + +const isExpandable = (reactChildren: React.ReactNode) => { + if (Array.isArray(reactChildren)) { + return reactChildren.length > 0 && reactChildren.some(isExpandable); + } + return Boolean(reactChildren); +}; + +const getIconFromFileType = (fileType: FileType) => { + switch (fileType) { + case 'image': + return ImageIcon; + case 'pdf': + return PictureAsPdfIcon; + case 'doc': + return ArticleIcon; + case 'video': + return VideoCameraBackIcon; + case 'folder': + return FolderRounded; + case 'pinned': + return FolderOpenIcon; + case 'trash': + return DeleteIcon; + default: + return ArticleIcon; + } +}; + +interface CustomTreeItemProps + extends Omit, + Omit, 'onFocus'> {} + +const CustomTreeItem = React.forwardRef(function CustomTreeItem( + props: CustomTreeItemProps, + ref: React.Ref, +) { + const { id, itemId, label, disabled, children, ...other } = props; + + const { + getRootProps, + getContentProps, + getIconContainerProps, + getLabelProps, + getGroupTransitionProps, + status, + publicAPI, + } = useTreeItem2({ id, itemId, children, label, disabled, rootRef: ref }); + + const item = publicAPI.getItem(itemId); + const expandable = isExpandable(children); + let icon; + if (expandable) { + icon = FolderRounded; + } else if (item.fileType) { + icon = getIconFromFileType(item.fileType); + } + + return ( + + + + + + + + + + {children && } + + + ); +}); + +export default function FileExplorer() { + return ( + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/customization/FileExplorer.tsx.preview b/docs/data/tree-view/rich-tree-view/customization/FileExplorer.tsx.preview new file mode 100644 index 000000000000..5f87458bdbbe --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/customization/FileExplorer.tsx.preview @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/docs/data/tree-view/rich-tree-view/customization/customization.md b/docs/data/tree-view/rich-tree-view/customization/customization.md index 5d33efde72b5..da4b3e57a2cc 100644 --- a/docs/data/tree-view/rich-tree-view/customization/customization.md +++ b/docs/data/tree-view/rich-tree-view/customization/customization.md @@ -69,3 +69,16 @@ The demo below shows how to add an avatar and custom typography elements. The demo below shows how to trigger the expansion interaction just by clicking on the icon container instead of the whole Tree Item surface. {{"demo": "IconExpansionTreeView.js", "defaultCodeOpen": false}} + +### File explorer + +:::warning +This example is built using the new `TreeItem2` component +which adds several slots to modify the content of the Tree Item or change its behavior. + +You can learn more about this new component in the [Overview page](/x/react-tree-view/#tree-item-components). +::: + +The demo below shows many of the previous customization examples brought together to make the Tree View component look completely different than its default design. + +{{"demo": "FileExplorer.js", "defaultCodeOpen": false}} diff --git a/docs/data/tree-view/simple-tree-view/customization/CustomizedTreeView.js b/docs/data/tree-view/simple-tree-view/customization/CustomizedTreeView.js deleted file mode 100644 index 572f804db372..000000000000 --- a/docs/data/tree-view/simple-tree-view/customization/CustomizedTreeView.js +++ /dev/null @@ -1,179 +0,0 @@ -import * as React from 'react'; -import { styled, alpha } from '@mui/material/styles'; -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import DeleteIcon from '@mui/icons-material/Delete'; -import Label from '@mui/icons-material/Label'; -import FolderRounded from '@mui/icons-material/FolderRounded'; -import AirplanemodeActiveIcon from '@mui/icons-material/AirplanemodeActive'; -import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; -import { TreeItem, treeItemClasses } from '@mui/x-tree-view/TreeItem'; -import Collapse from '@mui/material/Collapse'; - -import { animated, useSpring } from '@react-spring/web'; - -function DotIcon() { - return ( - - ); -} - -const StyledTreeItemLabel = styled(Typography)({ - color: 'inherit', - fontFamily: 'General Sans', - fontWeight: 'inherit', - flexGrow: 1, -}); - -const StyledTreeItemRoot = styled(TreeItem)(({ theme }) => ({ - color: - theme.palette.mode === 'light' - ? theme.palette.grey[800] - : theme.palette.grey[400], - position: 'relative', - [`& .${treeItemClasses.content}`]: { - flexDirection: 'row-reverse', - borderRadius: theme.spacing(0.7), - marginBottom: theme.spacing(0.5), - marginTop: theme.spacing(0.5), - padding: theme.spacing(0.5), - paddingRight: theme.spacing(1), - fontWeight: 500, - [`& .${treeItemClasses.label}`]: { - fontWeight: 'inherit', - }, - [`& .${treeItemClasses.iconContainer}`]: { - marginRight: theme.spacing(2), - }, - [`&.Mui-expanded `]: { - '&:not(.Mui-focused, .Mui-selected, .Mui-selected.Mui-focused) .labelIcon': { - color: - theme.palette.mode === 'light' - ? theme.palette.primary.main - : theme.palette.secondary.dark, - }, - '&::before': { - content: '""', - display: 'block', - position: 'absolute', - left: '16px', - top: '44px', - height: 'calc(100% - 48px)', - width: '1.5px', - backgroundColor: - theme.palette.mode === 'light' - ? theme.palette.grey[300] - : theme.palette.grey[700], - }, - }, - '&:hover': { - backgroundColor: alpha(theme.palette.primary.main, 0.1), - color: theme.palette.mode === 'light' ? theme.palette.primary.main : 'white', - }, - [`&.Mui-focused, &.Mui-selected, &.Mui-selected.Mui-focused`]: { - backgroundColor: - theme.palette.mode === 'light' - ? theme.palette.primary.main - : theme.palette.secondary.dark, - color: 'white', - }, - }, - [`& .${treeItemClasses.groupTransition}`]: { - marginLeft: theme.spacing(3.5), - [`& .${treeItemClasses.content}`]: { - fontWeight: 500, - }, - }, -})); - -const AnimatedCollapse = animated(Collapse); - -function TransitionComponent(props) { - const style = useSpring({ - to: { - opacity: props.in ? 1 : 0, - transform: `translate3d(0,${props.in ? 0 : 20}px,0)`, - }, - }); - - return ; -} - -const StyledTreeItem = React.forwardRef(function StyledTreeItem(props, ref) { - const { labelIcon: LabelIcon, labelText, ...other } = props; - - return ( - - - {labelText} - - } - {...other} - ref={ref} - /> - ); -}); - -export default function CustomizedTreeView() { - return ( - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/docs/data/tree-view/simple-tree-view/customization/CustomizedTreeView.tsx b/docs/data/tree-view/simple-tree-view/customization/CustomizedTreeView.tsx deleted file mode 100644 index f28c321d880f..000000000000 --- a/docs/data/tree-view/simple-tree-view/customization/CustomizedTreeView.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import * as React from 'react'; -import { styled, alpha } from '@mui/material/styles'; -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import DeleteIcon from '@mui/icons-material/Delete'; -import Label from '@mui/icons-material/Label'; -import FolderRounded from '@mui/icons-material/FolderRounded'; -import AirplanemodeActiveIcon from '@mui/icons-material/AirplanemodeActive'; -import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; -import { TreeItem, TreeItemProps, treeItemClasses } from '@mui/x-tree-view/TreeItem'; -import Collapse from '@mui/material/Collapse'; -import { TransitionProps } from '@mui/material/transitions'; -import { animated, useSpring } from '@react-spring/web'; - -function DotIcon() { - return ( - - ); -} - -declare module 'react' { - interface CSSProperties { - '--tree-view-color'?: string; - '--tree-view-bg-color'?: string; - } -} - -type StyledTreeItemProps = Omit & { - labelIcon: React.ElementType; - labelText: string; -}; - -const StyledTreeItemLabel = styled(Typography)({ - color: 'inherit', - fontFamily: 'General Sans', - fontWeight: 'inherit', - flexGrow: 1, -}) as unknown as typeof Typography; - -const StyledTreeItemRoot = styled(TreeItem)(({ theme }) => ({ - color: - theme.palette.mode === 'light' - ? theme.palette.grey[800] - : theme.palette.grey[400], - position: 'relative', - [`& .${treeItemClasses.content}`]: { - flexDirection: 'row-reverse', - borderRadius: theme.spacing(0.7), - marginBottom: theme.spacing(0.5), - marginTop: theme.spacing(0.5), - padding: theme.spacing(0.5), - paddingRight: theme.spacing(1), - fontWeight: 500, - [`& .${treeItemClasses.label}`]: { - fontWeight: 'inherit', - }, - [`& .${treeItemClasses.iconContainer}`]: { - marginRight: theme.spacing(2), - }, - [`&.Mui-expanded `]: { - '&:not(.Mui-focused, .Mui-selected, .Mui-selected.Mui-focused) .labelIcon': { - color: - theme.palette.mode === 'light' - ? theme.palette.primary.main - : theme.palette.secondary.dark, - }, - '&::before': { - content: '""', - display: 'block', - position: 'absolute', - left: '16px', - top: '44px', - height: 'calc(100% - 48px)', - width: '1.5px', - backgroundColor: - theme.palette.mode === 'light' - ? theme.palette.grey[300] - : theme.palette.grey[700], - }, - }, - '&:hover': { - backgroundColor: alpha(theme.palette.primary.main, 0.1), - color: theme.palette.mode === 'light' ? theme.palette.primary.main : 'white', - }, - [`&.Mui-focused, &.Mui-selected, &.Mui-selected.Mui-focused`]: { - backgroundColor: - theme.palette.mode === 'light' - ? theme.palette.primary.main - : theme.palette.secondary.dark, - color: 'white', - }, - }, - [`& .${treeItemClasses.groupTransition}`]: { - marginLeft: theme.spacing(3.5), - [`& .${treeItemClasses.content}`]: { - fontWeight: 500, - }, - }, -})) as unknown as typeof TreeItem; - -const AnimatedCollapse = animated(Collapse); - -function TransitionComponent(props: TransitionProps) { - const style = useSpring({ - to: { - opacity: props.in ? 1 : 0, - transform: `translate3d(0,${props.in ? 0 : 20}px,0)`, - }, - }); - - return ; -} - -const StyledTreeItem = React.forwardRef(function StyledTreeItem( - props: StyledTreeItemProps, - ref: React.Ref, -) { - const { labelIcon: LabelIcon, labelText, ...other } = props; - - return ( - - - {labelText} - - } - {...other} - ref={ref} - /> - ); -}); - -export default function CustomizedTreeView() { - return ( - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/docs/data/tree-view/simple-tree-view/customization/customization.md b/docs/data/tree-view/simple-tree-view/customization/customization.md index 819205b2d815..4e74f185137d 100644 --- a/docs/data/tree-view/simple-tree-view/customization/customization.md +++ b/docs/data/tree-view/simple-tree-view/customization/customization.md @@ -83,12 +83,6 @@ The demo below shows how to trigger the expansion interaction just by clicking o {{"demo": "IconExpansionTreeView.js", "defaultCodeOpen": false}} -### File explorer - -The demo below shows many of the previous customization examples brought together to make the Tree View component look completely different than its default design. - -{{"demo": "CustomizedTreeView.js"}} - ### Gmail clone :::warning