From ca6998ab582830af42ba8fd50f38517cf46014ae Mon Sep 17 00:00:00 2001 From: Flavien DELANGLE Date: Mon, 4 Sep 2023 11:34:32 +0200 Subject: [PATCH] [TreeView] Use Tree View from MUI X in the lab (#38261) --- .../components/tree-view/BarTreeView.js | 176 -- .../components/tree-view/BarTreeView.tsx | 151 -- .../tree-view/ControlledTreeView.js | 69 - .../tree-view/ControlledTreeView.tsx | 69 - .../tree-view/CustomizedTreeView.js | 109 - .../tree-view/CustomizedTreeView.tsx | 102 - .../components/tree-view/DisabledTreeItems.js | 57 - .../tree-view/DisabledTreeItems.tsx | 57 - .../tree-view/FileSystemNavigator.js | 26 - .../tree-view/FileSystemNavigator.tsx | 26 - .../tree-view/FileSystemNavigator.tsx.preview | 16 - .../components/tree-view/GmailTreeView.js | 161 -- .../components/tree-view/GmailTreeView.tsx | 168 -- .../tree-view/IconExpansionTreeView.js | 131 - .../tree-view/IconExpansionTreeView.tsx | 110 - .../tree-view/MultiSelectTreeView.js | 31 - .../tree-view/MultiSelectTreeView.tsx | 31 - .../tree-view/RichObjectTreeView.js | 48 - .../tree-view/RichObjectTreeView.tsx | 54 - .../tree-view/RichObjectTreeView.tsx.preview | 9 - .../components/tree-view/tree-view-pt.md | 108 - .../components/tree-view/tree-view-zh.md | 108 - .../components/tree-view/tree-view.md | 116 - .../material/discover-more/roadmap/roadmap.md | 70 +- .../MaterialUIComponents.js | 2 +- docs/data/material/pages.ts | 1 - docs/data/material/pagesApi.js | 2 - docs/next.config.js | 3 +- docs/pages/blog/2019.md | 2 +- docs/pages/blog/july-2019-update.md | 2 +- docs/pages/material-ui/api/tree-item.js | 19 - docs/pages/material-ui/api/tree-item.json | 58 - docs/pages/material-ui/api/tree-view.js | 19 - docs/pages/material-ui/api/tree-view.json | 65 - docs/pages/material-ui/react-tree-view.js | 7 - docs/public/_redirects | 3 + .../src/components/productX/XTreeViewDemo.tsx | 34 +- .../components/showcase/FolderTreeView.tsx | 20 +- docs/src/modules/components/ThemeViewer.tsx | 4 +- .../api-docs/tree-item/tree-item-pt.json | 61 - .../api-docs/tree-item/tree-item-zh.json | 61 - .../api-docs/tree-item/tree-item.json | 67 - .../api-docs/tree-view/tree-view-pt.json | 28 - .../api-docs/tree-view/tree-view-zh.json | 28 - .../api-docs/tree-view/tree-view.json | 58 - docs/translations/translations-pt.json | 1 - docs/translations/translations-zh.json | 1 - docs/translations/translations.json | 1 - packages/mui-lab/package.json | 1 + packages/mui-lab/src/TreeItem/TreeItem.d.ts | 88 - packages/mui-lab/src/TreeItem/TreeItem.js | 440 ---- .../mui-lab/src/TreeItem/TreeItem.test.js | 2318 ----------------- packages/mui-lab/src/TreeItem/TreeItem.tsx | 48 + .../mui-lab/src/TreeItem/TreeItemContent.d.ts | 50 - .../mui-lab/src/TreeItem/TreeItemContent.js | 116 - packages/mui-lab/src/TreeItem/index.d.ts | 7 - packages/mui-lab/src/TreeItem/index.js | 5 - packages/mui-lab/src/TreeItem/index.ts | 8 + .../mui-lab/src/TreeItem/treeItemClasses.ts | 43 - .../mui-lab/src/TreeItem/useTreeItem.d.ts | 11 - packages/mui-lab/src/TreeItem/useTreeItem.js | 75 - packages/mui-lab/src/TreeView/TreeView.d.ts | 144 - packages/mui-lab/src/TreeView/TreeView.js | 958 ------- .../mui-lab/src/TreeView/TreeView.test.js | 463 ---- packages/mui-lab/src/TreeView/TreeView.tsx | 48 + .../mui-lab/src/TreeView/TreeViewContext.js | 12 - packages/mui-lab/src/TreeView/descendants.js | 202 -- packages/mui-lab/src/TreeView/index.d.ts | 5 - packages/mui-lab/src/TreeView/index.js | 4 - packages/mui-lab/src/TreeView/index.ts | 10 + .../mui-lab/src/TreeView/treeViewClasses.ts | 17 - test/karma.conf.js | 2 +- test/utils/setupBabel.js | 8 +- yarn.lock | 13 +- 74 files changed, 210 insertions(+), 7436 deletions(-) delete mode 100644 docs/data/material/components/tree-view/BarTreeView.js delete mode 100644 docs/data/material/components/tree-view/BarTreeView.tsx delete mode 100644 docs/data/material/components/tree-view/ControlledTreeView.js delete mode 100644 docs/data/material/components/tree-view/ControlledTreeView.tsx delete mode 100644 docs/data/material/components/tree-view/CustomizedTreeView.js delete mode 100644 docs/data/material/components/tree-view/CustomizedTreeView.tsx delete mode 100644 docs/data/material/components/tree-view/DisabledTreeItems.js delete mode 100644 docs/data/material/components/tree-view/DisabledTreeItems.tsx delete mode 100644 docs/data/material/components/tree-view/FileSystemNavigator.js delete mode 100644 docs/data/material/components/tree-view/FileSystemNavigator.tsx delete mode 100644 docs/data/material/components/tree-view/FileSystemNavigator.tsx.preview delete mode 100644 docs/data/material/components/tree-view/GmailTreeView.js delete mode 100644 docs/data/material/components/tree-view/GmailTreeView.tsx delete mode 100644 docs/data/material/components/tree-view/IconExpansionTreeView.js delete mode 100644 docs/data/material/components/tree-view/IconExpansionTreeView.tsx delete mode 100644 docs/data/material/components/tree-view/MultiSelectTreeView.js delete mode 100644 docs/data/material/components/tree-view/MultiSelectTreeView.tsx delete mode 100644 docs/data/material/components/tree-view/RichObjectTreeView.js delete mode 100644 docs/data/material/components/tree-view/RichObjectTreeView.tsx delete mode 100644 docs/data/material/components/tree-view/RichObjectTreeView.tsx.preview delete mode 100644 docs/data/material/components/tree-view/tree-view-pt.md delete mode 100644 docs/data/material/components/tree-view/tree-view-zh.md delete mode 100644 docs/data/material/components/tree-view/tree-view.md delete mode 100644 docs/pages/material-ui/api/tree-item.js delete mode 100644 docs/pages/material-ui/api/tree-item.json delete mode 100644 docs/pages/material-ui/api/tree-view.js delete mode 100644 docs/pages/material-ui/api/tree-view.json delete mode 100644 docs/pages/material-ui/react-tree-view.js delete mode 100644 docs/translations/api-docs/tree-item/tree-item-pt.json delete mode 100644 docs/translations/api-docs/tree-item/tree-item-zh.json delete mode 100644 docs/translations/api-docs/tree-item/tree-item.json delete mode 100644 docs/translations/api-docs/tree-view/tree-view-pt.json delete mode 100644 docs/translations/api-docs/tree-view/tree-view-zh.json delete mode 100644 docs/translations/api-docs/tree-view/tree-view.json delete mode 100644 packages/mui-lab/src/TreeItem/TreeItem.d.ts delete mode 100644 packages/mui-lab/src/TreeItem/TreeItem.js delete mode 100644 packages/mui-lab/src/TreeItem/TreeItem.test.js create mode 100644 packages/mui-lab/src/TreeItem/TreeItem.tsx delete mode 100644 packages/mui-lab/src/TreeItem/TreeItemContent.d.ts delete mode 100644 packages/mui-lab/src/TreeItem/TreeItemContent.js delete mode 100644 packages/mui-lab/src/TreeItem/index.d.ts delete mode 100644 packages/mui-lab/src/TreeItem/index.js create mode 100644 packages/mui-lab/src/TreeItem/index.ts delete mode 100644 packages/mui-lab/src/TreeItem/treeItemClasses.ts delete mode 100644 packages/mui-lab/src/TreeItem/useTreeItem.d.ts delete mode 100644 packages/mui-lab/src/TreeItem/useTreeItem.js delete mode 100644 packages/mui-lab/src/TreeView/TreeView.d.ts delete mode 100644 packages/mui-lab/src/TreeView/TreeView.js delete mode 100644 packages/mui-lab/src/TreeView/TreeView.test.js create mode 100644 packages/mui-lab/src/TreeView/TreeView.tsx delete mode 100644 packages/mui-lab/src/TreeView/TreeViewContext.js delete mode 100644 packages/mui-lab/src/TreeView/descendants.js delete mode 100644 packages/mui-lab/src/TreeView/index.d.ts delete mode 100644 packages/mui-lab/src/TreeView/index.js create mode 100644 packages/mui-lab/src/TreeView/index.ts delete mode 100644 packages/mui-lab/src/TreeView/treeViewClasses.ts diff --git a/docs/data/material/components/tree-view/BarTreeView.js b/docs/data/material/components/tree-view/BarTreeView.js deleted file mode 100644 index 798cb9ab3e51d4..00000000000000 --- a/docs/data/material/components/tree-view/BarTreeView.js +++ /dev/null @@ -1,176 +0,0 @@ -import * as React from 'react'; -import PropTypes from 'prop-types'; -import { styled, alpha } from '@mui/material/styles'; -import TreeView from '@mui/lab/TreeView'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import TreeItem, { useTreeItem } from '@mui/lab/TreeItem'; -import clsx from 'clsx'; -import Typography from '@mui/material/Typography'; - -const CustomContentRoot = styled('div')(({ theme }) => ({ - WebkitTapHighlightColor: 'transparent', - '&:hover, &.Mui-disabled, &.Mui-focused, &.Mui-selected, &.Mui-selected.Mui-focused, &.Mui-selected:hover': - { - backgroundColor: 'transparent', - }, - [`& .MuiTreeItem-contentBar`]: { - position: 'absolute', - width: '100%', - height: 24, - left: 0, - '&:hover &': { - backgroundColor: theme.palette.action.hover, - // Reset on touch devices, it doesn't add specificity - '@media (hover: none)': { - backgroundColor: 'transparent', - }, - }, - '&.Mui-disabled &': { - opacity: theme.palette.action.disabledOpacity, - backgroundColor: 'transparent', - }, - '&.Mui-focused &': { - backgroundColor: theme.palette.action.focus, - }, - '&.Mui-selected &': { - backgroundColor: alpha( - theme.palette.primary.main, - theme.palette.action.selectedOpacity, - ), - }, - '&.Mui-selected:hover &': { - backgroundColor: alpha( - theme.palette.primary.main, - theme.palette.action.selectedOpacity + theme.palette.action.hoverOpacity, - ), - // Reset on touch devices, it doesn't add specificity - '@media (hover: none)': { - backgroundColor: alpha( - theme.palette.primary.main, - theme.palette.action.selectedOpacity, - ), - }, - }, - '&.Mui-selected.Mui-focused &': { - backgroundColor: alpha( - theme.palette.primary.main, - theme.palette.action.selectedOpacity + theme.palette.action.focusOpacity, - ), - }, - }, -})); - -const CustomContent = React.forwardRef(function CustomContent(props, ref) { - const { - className, - classes, - label, - nodeId, - icon: iconProp, - expansionIcon, - displayIcon, - } = props; - - const { - disabled, - expanded, - selected, - focused, - handleExpansion, - handleSelection, - preventSelection, - } = useTreeItem(nodeId); - - const icon = iconProp || expansionIcon || displayIcon; - - const handleMouseDown = (event) => { - preventSelection(event); - }; - - const handleClick = (event) => { - handleExpansion(event); - handleSelection(event); - }; - - return ( - -
-
{icon}
- - {label} - - - ); -}); - -CustomContent.propTypes = { - /** - * Override or extend the styles applied to the component. - */ - classes: PropTypes.object.isRequired, - /** - * className applied to the root element. - */ - className: PropTypes.string, - /** - * The icon to display next to the tree node's label. Either a parent or end icon. - */ - displayIcon: PropTypes.node, - /** - * The icon to display next to the tree node's label. Either an expansion or collapse icon. - */ - expansionIcon: PropTypes.node, - /** - * The icon to display next to the tree node's label. - */ - icon: PropTypes.node, - /** - * The tree node label. - */ - label: PropTypes.node, - /** - * The id of the node. - */ - nodeId: PropTypes.string.isRequired, -}; - -function CustomTreeItem(props) { - return ; -} - -export default function BarTreeView() { - return ( - } - defaultExpandIcon={} - sx={{ height: 240, flexGrow: 1, maxWidth: 400, position: 'relative' }} - > - - - - - - - - - - - - - - - - ); -} diff --git a/docs/data/material/components/tree-view/BarTreeView.tsx b/docs/data/material/components/tree-view/BarTreeView.tsx deleted file mode 100644 index 188ab5ccf851a4..00000000000000 --- a/docs/data/material/components/tree-view/BarTreeView.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import * as React from 'react'; -import { styled, alpha } from '@mui/material/styles'; -import TreeView from '@mui/lab/TreeView'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import TreeItem, { - TreeItemProps, - useTreeItem, - TreeItemContentProps, -} from '@mui/lab/TreeItem'; -import clsx from 'clsx'; -import Typography from '@mui/material/Typography'; - -const CustomContentRoot = styled('div')(({ theme }) => ({ - WebkitTapHighlightColor: 'transparent', - '&:hover, &.Mui-disabled, &.Mui-focused, &.Mui-selected, &.Mui-selected.Mui-focused, &.Mui-selected:hover': - { - backgroundColor: 'transparent', - }, - [`& .MuiTreeItem-contentBar`]: { - position: 'absolute', - width: '100%', - height: 24, - left: 0, - '&:hover &': { - backgroundColor: theme.palette.action.hover, - // Reset on touch devices, it doesn't add specificity - '@media (hover: none)': { - backgroundColor: 'transparent', - }, - }, - '&.Mui-disabled &': { - opacity: theme.palette.action.disabledOpacity, - backgroundColor: 'transparent', - }, - '&.Mui-focused &': { - backgroundColor: theme.palette.action.focus, - }, - '&.Mui-selected &': { - backgroundColor: alpha( - theme.palette.primary.main, - theme.palette.action.selectedOpacity, - ), - }, - '&.Mui-selected:hover &': { - backgroundColor: alpha( - theme.palette.primary.main, - theme.palette.action.selectedOpacity + theme.palette.action.hoverOpacity, - ), - // Reset on touch devices, it doesn't add specificity - '@media (hover: none)': { - backgroundColor: alpha( - theme.palette.primary.main, - theme.palette.action.selectedOpacity, - ), - }, - }, - '&.Mui-selected.Mui-focused &': { - backgroundColor: alpha( - theme.palette.primary.main, - theme.palette.action.selectedOpacity + theme.palette.action.focusOpacity, - ), - }, - }, -})); - -const CustomContent = React.forwardRef(function CustomContent( - props: TreeItemContentProps, - ref, -) { - const { - className, - classes, - label, - nodeId, - icon: iconProp, - expansionIcon, - displayIcon, - } = props; - - const { - disabled, - expanded, - selected, - focused, - handleExpansion, - handleSelection, - preventSelection, - } = useTreeItem(nodeId); - - const icon = iconProp || expansionIcon || displayIcon; - - const handleMouseDown = (event: React.MouseEvent) => { - preventSelection(event); - }; - - const handleClick = (event: React.MouseEvent) => { - handleExpansion(event); - handleSelection(event); - }; - - return ( - } - > -
-
{icon}
- - {label} - - - ); -}); - -function CustomTreeItem(props: TreeItemProps) { - return ; -} - -export default function BarTreeView() { - return ( - } - defaultExpandIcon={} - sx={{ height: 240, flexGrow: 1, maxWidth: 400, position: 'relative' }} - > - - - - - - - - - - - - - - - - ); -} diff --git a/docs/data/material/components/tree-view/ControlledTreeView.js b/docs/data/material/components/tree-view/ControlledTreeView.js deleted file mode 100644 index b6f007766bc03b..00000000000000 --- a/docs/data/material/components/tree-view/ControlledTreeView.js +++ /dev/null @@ -1,69 +0,0 @@ -import * as React from 'react'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import TreeView from '@mui/lab/TreeView'; -import TreeItem from '@mui/lab/TreeItem'; - -export default function ControlledTreeView() { - const [expanded, setExpanded] = React.useState([]); - const [selected, setSelected] = React.useState([]); - - const handleToggle = (event, nodeIds) => { - setExpanded(nodeIds); - }; - - const handleSelect = (event, nodeIds) => { - setSelected(nodeIds); - }; - - const handleExpandClick = () => { - setExpanded((oldExpanded) => - oldExpanded.length === 0 ? ['1', '5', '6', '7'] : [], - ); - }; - - const handleSelectClick = () => { - setSelected((oldSelected) => - oldSelected.length === 0 ? ['1', '2', '3', '4', '5', '6', '7', '8', '9'] : [], - ); - }; - - return ( - - - - - - } - defaultExpandIcon={} - expanded={expanded} - selected={selected} - onNodeToggle={handleToggle} - onNodeSelect={handleSelect} - multiSelect - > - - - - - - - - - - - - - - - - ); -} diff --git a/docs/data/material/components/tree-view/ControlledTreeView.tsx b/docs/data/material/components/tree-view/ControlledTreeView.tsx deleted file mode 100644 index bb44ba0e832d08..00000000000000 --- a/docs/data/material/components/tree-view/ControlledTreeView.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import * as React from 'react'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import TreeView from '@mui/lab/TreeView'; -import TreeItem from '@mui/lab/TreeItem'; - -export default function ControlledTreeView() { - const [expanded, setExpanded] = React.useState([]); - const [selected, setSelected] = React.useState([]); - - const handleToggle = (event: React.SyntheticEvent, nodeIds: string[]) => { - setExpanded(nodeIds); - }; - - const handleSelect = (event: React.SyntheticEvent, nodeIds: string[]) => { - setSelected(nodeIds); - }; - - const handleExpandClick = () => { - setExpanded((oldExpanded) => - oldExpanded.length === 0 ? ['1', '5', '6', '7'] : [], - ); - }; - - const handleSelectClick = () => { - setSelected((oldSelected) => - oldSelected.length === 0 ? ['1', '2', '3', '4', '5', '6', '7', '8', '9'] : [], - ); - }; - - return ( - - - - - - } - defaultExpandIcon={} - expanded={expanded} - selected={selected} - onNodeToggle={handleToggle} - onNodeSelect={handleSelect} - multiSelect - > - - - - - - - - - - - - - - - - ); -} diff --git a/docs/data/material/components/tree-view/CustomizedTreeView.js b/docs/data/material/components/tree-view/CustomizedTreeView.js deleted file mode 100644 index ead861b3d1fa90..00000000000000 --- a/docs/data/material/components/tree-view/CustomizedTreeView.js +++ /dev/null @@ -1,109 +0,0 @@ -import * as React from 'react'; -import PropTypes from 'prop-types'; -import SvgIcon from '@mui/material/SvgIcon'; -import { alpha, styled } from '@mui/material/styles'; -import TreeView from '@mui/lab/TreeView'; -import TreeItem, { treeItemClasses } from '@mui/lab/TreeItem'; -import Collapse from '@mui/material/Collapse'; -import { useSpring, animated } from '@react-spring/web'; - -function MinusSquare(props) { - return ( - - {/* tslint:disable-next-line: max-line-length */} - - - ); -} - -function PlusSquare(props) { - return ( - - {/* tslint:disable-next-line: max-line-length */} - - - ); -} - -function CloseSquare(props) { - return ( - - {/* tslint:disable-next-line: max-line-length */} - - - ); -} - -function TransitionComponent(props) { - const style = useSpring({ - from: { - opacity: 0, - transform: 'translate3d(20px,0,0)', - }, - to: { - opacity: props.in ? 1 : 0, - transform: `translate3d(${props.in ? 0 : 20}px,0,0)`, - }, - }); - - return ( - - - - ); -} - -TransitionComponent.propTypes = { - /** - * Show the component; triggers the enter or exit states - */ - in: PropTypes.bool, -}; - -const StyledTreeItem = styled((props) => ( - -))(({ theme }) => ({ - [`& .${treeItemClasses.iconContainer}`]: { - '& .close': { - opacity: 0.3, - }, - }, - [`& .${treeItemClasses.group}`]: { - marginLeft: 15, - paddingLeft: 18, - borderLeft: `1px dashed ${alpha(theme.palette.text.primary, 0.4)}`, - }, -})); - -export default function CustomizedTreeView() { - return ( - } - defaultExpandIcon={} - defaultEndIcon={} - sx={{ height: 264, flexGrow: 1, maxWidth: 400, overflowY: 'auto' }} - > - - - - - - - - - - - - - - - - ); -} diff --git a/docs/data/material/components/tree-view/CustomizedTreeView.tsx b/docs/data/material/components/tree-view/CustomizedTreeView.tsx deleted file mode 100644 index a634ff141b8f9d..00000000000000 --- a/docs/data/material/components/tree-view/CustomizedTreeView.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import * as React from 'react'; -import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon'; -import { alpha, styled } from '@mui/material/styles'; -import TreeView from '@mui/lab/TreeView'; -import TreeItem, { TreeItemProps, treeItemClasses } from '@mui/lab/TreeItem'; -import Collapse from '@mui/material/Collapse'; -import { useSpring, animated } from '@react-spring/web'; -import { TransitionProps } from '@mui/material/transitions'; - -function MinusSquare(props: SvgIconProps) { - return ( - - {/* tslint:disable-next-line: max-line-length */} - - - ); -} - -function PlusSquare(props: SvgIconProps) { - return ( - - {/* tslint:disable-next-line: max-line-length */} - - - ); -} - -function CloseSquare(props: SvgIconProps) { - return ( - - {/* tslint:disable-next-line: max-line-length */} - - - ); -} - -function TransitionComponent(props: TransitionProps) { - const style = useSpring({ - from: { - opacity: 0, - transform: 'translate3d(20px,0,0)', - }, - to: { - opacity: props.in ? 1 : 0, - transform: `translate3d(${props.in ? 0 : 20}px,0,0)`, - }, - }); - - return ( - - - - ); -} - -const StyledTreeItem = styled((props: TreeItemProps) => ( - -))(({ theme }) => ({ - [`& .${treeItemClasses.iconContainer}`]: { - '& .close': { - opacity: 0.3, - }, - }, - [`& .${treeItemClasses.group}`]: { - marginLeft: 15, - paddingLeft: 18, - borderLeft: `1px dashed ${alpha(theme.palette.text.primary, 0.4)}`, - }, -})); - -export default function CustomizedTreeView() { - return ( - } - defaultExpandIcon={} - defaultEndIcon={} - sx={{ height: 264, flexGrow: 1, maxWidth: 400, overflowY: 'auto' }} - > - - - - - - - - - - - - - - - - ); -} diff --git a/docs/data/material/components/tree-view/DisabledTreeItems.js b/docs/data/material/components/tree-view/DisabledTreeItems.js deleted file mode 100644 index 8b68005b4f84a6..00000000000000 --- a/docs/data/material/components/tree-view/DisabledTreeItems.js +++ /dev/null @@ -1,57 +0,0 @@ -import * as React from 'react'; -import Box from '@mui/material/Box'; -import TreeView from '@mui/lab/TreeView'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import TreeItem from '@mui/lab/TreeItem'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import Switch from '@mui/material/Switch'; - -export default function DisabledTreeItems() { - const [focusDisabledItems, setFocusDisabledItems] = React.useState(false); - const handleToggle = (event) => { - setFocusDisabledItems(event.target.checked); - }; - - return ( - - - - } - label="Focus disabled items" - /> - - } - defaultExpandIcon={} - disabledItemsFocusable={focusDisabledItems} - multiSelect - > - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/docs/data/material/components/tree-view/DisabledTreeItems.tsx b/docs/data/material/components/tree-view/DisabledTreeItems.tsx deleted file mode 100644 index 83c26c8a8db781..00000000000000 --- a/docs/data/material/components/tree-view/DisabledTreeItems.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import * as React from 'react'; -import Box from '@mui/material/Box'; -import TreeView from '@mui/lab/TreeView'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import TreeItem from '@mui/lab/TreeItem'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import Switch from '@mui/material/Switch'; - -export default function DisabledTreeItems() { - const [focusDisabledItems, setFocusDisabledItems] = React.useState(false); - const handleToggle = (event: React.ChangeEvent) => { - setFocusDisabledItems(event.target.checked); - }; - - return ( - - - - } - label="Focus disabled items" - /> - - } - defaultExpandIcon={} - disabledItemsFocusable={focusDisabledItems} - multiSelect - > - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/docs/data/material/components/tree-view/FileSystemNavigator.js b/docs/data/material/components/tree-view/FileSystemNavigator.js deleted file mode 100644 index 0caf59ddf00879..00000000000000 --- a/docs/data/material/components/tree-view/FileSystemNavigator.js +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from 'react'; -import TreeView from '@mui/lab/TreeView'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import TreeItem from '@mui/lab/TreeItem'; - -export default function FileSystemNavigator() { - return ( - } - defaultExpandIcon={} - sx={{ height: 240, flexGrow: 1, maxWidth: 400, overflowY: 'auto' }} - > - - - - - - - - - - - ); -} diff --git a/docs/data/material/components/tree-view/FileSystemNavigator.tsx b/docs/data/material/components/tree-view/FileSystemNavigator.tsx deleted file mode 100644 index 0caf59ddf00879..00000000000000 --- a/docs/data/material/components/tree-view/FileSystemNavigator.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from 'react'; -import TreeView from '@mui/lab/TreeView'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import TreeItem from '@mui/lab/TreeItem'; - -export default function FileSystemNavigator() { - return ( - } - defaultExpandIcon={} - sx={{ height: 240, flexGrow: 1, maxWidth: 400, overflowY: 'auto' }} - > - - - - - - - - - - - ); -} diff --git a/docs/data/material/components/tree-view/FileSystemNavigator.tsx.preview b/docs/data/material/components/tree-view/FileSystemNavigator.tsx.preview deleted file mode 100644 index 0c2091d2b2ef36..00000000000000 --- a/docs/data/material/components/tree-view/FileSystemNavigator.tsx.preview +++ /dev/null @@ -1,16 +0,0 @@ -} - defaultExpandIcon={} - sx={{ height: 240, flexGrow: 1, maxWidth: 400, overflowY: 'auto' }} -> - - - - - - - - - - \ No newline at end of file diff --git a/docs/data/material/components/tree-view/GmailTreeView.js b/docs/data/material/components/tree-view/GmailTreeView.js deleted file mode 100644 index c72a9f8487319b..00000000000000 --- a/docs/data/material/components/tree-view/GmailTreeView.js +++ /dev/null @@ -1,161 +0,0 @@ -import * as React from 'react'; -import PropTypes from 'prop-types'; -import { styled, useTheme } from '@mui/material/styles'; -import Box from '@mui/material/Box'; -import TreeView from '@mui/lab/TreeView'; -import TreeItem, { treeItemClasses } from '@mui/lab/TreeItem'; -import Typography from '@mui/material/Typography'; -import MailIcon from '@mui/icons-material/Mail'; -import DeleteIcon from '@mui/icons-material/Delete'; -import Label from '@mui/icons-material/Label'; -import SupervisorAccountIcon from '@mui/icons-material/SupervisorAccount'; -import InfoIcon from '@mui/icons-material/Info'; -import ForumIcon from '@mui/icons-material/Forum'; -import LocalOfferIcon from '@mui/icons-material/LocalOffer'; -import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; -import ArrowRightIcon from '@mui/icons-material/ArrowRight'; - -const StyledTreeItemRoot = styled(TreeItem)(({ theme }) => ({ - color: theme.palette.text.secondary, - [`& .${treeItemClasses.content}`]: { - color: theme.palette.text.secondary, - borderTopRightRadius: theme.spacing(2), - borderBottomRightRadius: theme.spacing(2), - paddingRight: theme.spacing(1), - fontWeight: theme.typography.fontWeightMedium, - '&.Mui-expanded': { - fontWeight: theme.typography.fontWeightRegular, - }, - '&:hover': { - backgroundColor: theme.palette.action.hover, - }, - '&.Mui-focused, &.Mui-selected, &.Mui-selected.Mui-focused': { - backgroundColor: `var(--tree-view-bg-color, ${theme.palette.action.selected})`, - color: 'var(--tree-view-color)', - }, - [`& .${treeItemClasses.label}`]: { - fontWeight: 'inherit', - color: 'inherit', - }, - }, - [`& .${treeItemClasses.group}`]: { - marginLeft: 0, - [`& .${treeItemClasses.content}`]: { - paddingLeft: theme.spacing(2), - }, - }, -})); - -function StyledTreeItem(props) { - const theme = useTheme(); - const { - bgColor, - color, - labelIcon: LabelIcon, - labelInfo, - labelText, - colorForDarkMode, - bgColorForDarkMode, - ...other - } = props; - - const styleProps = { - '--tree-view-color': theme.palette.mode !== 'dark' ? color : colorForDarkMode, - '--tree-view-bg-color': - theme.palette.mode !== 'dark' ? bgColor : bgColorForDarkMode, - }; - - return ( - - - - {labelText} - - - {labelInfo} - - - } - style={styleProps} - {...other} - /> - ); -} - -StyledTreeItem.propTypes = { - bgColor: PropTypes.string, - bgColorForDarkMode: PropTypes.string, - color: PropTypes.string, - colorForDarkMode: PropTypes.string, - labelIcon: PropTypes.elementType.isRequired, - labelInfo: PropTypes.string, - labelText: PropTypes.string.isRequired, -}; - -export default function GmailTreeView() { - return ( - } - defaultExpandIcon={} - defaultEndIcon={
} - sx={{ height: 264, flexGrow: 1, maxWidth: 400, overflowY: 'auto' }} - > - - - - - - - - - - - ); -} diff --git a/docs/data/material/components/tree-view/GmailTreeView.tsx b/docs/data/material/components/tree-view/GmailTreeView.tsx deleted file mode 100644 index 4991d4bbbdfa00..00000000000000 --- a/docs/data/material/components/tree-view/GmailTreeView.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import * as React from 'react'; -import { styled, useTheme } from '@mui/material/styles'; -import Box from '@mui/material/Box'; -import TreeView from '@mui/lab/TreeView'; -import TreeItem, { TreeItemProps, treeItemClasses } from '@mui/lab/TreeItem'; -import Typography from '@mui/material/Typography'; -import MailIcon from '@mui/icons-material/Mail'; -import DeleteIcon from '@mui/icons-material/Delete'; -import Label from '@mui/icons-material/Label'; -import SupervisorAccountIcon from '@mui/icons-material/SupervisorAccount'; -import InfoIcon from '@mui/icons-material/Info'; -import ForumIcon from '@mui/icons-material/Forum'; -import LocalOfferIcon from '@mui/icons-material/LocalOffer'; -import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; -import ArrowRightIcon from '@mui/icons-material/ArrowRight'; -import { SvgIconProps } from '@mui/material/SvgIcon'; - -declare module 'react' { - interface CSSProperties { - '--tree-view-color'?: string; - '--tree-view-bg-color'?: string; - } -} - -type StyledTreeItemProps = TreeItemProps & { - bgColor?: string; - bgColorForDarkMode?: string; - color?: string; - colorForDarkMode?: string; - labelIcon: React.ElementType; - labelInfo?: string; - labelText: string; -}; - -const StyledTreeItemRoot = styled(TreeItem)(({ theme }) => ({ - color: theme.palette.text.secondary, - [`& .${treeItemClasses.content}`]: { - color: theme.palette.text.secondary, - borderTopRightRadius: theme.spacing(2), - borderBottomRightRadius: theme.spacing(2), - paddingRight: theme.spacing(1), - fontWeight: theme.typography.fontWeightMedium, - '&.Mui-expanded': { - fontWeight: theme.typography.fontWeightRegular, - }, - '&:hover': { - backgroundColor: theme.palette.action.hover, - }, - '&.Mui-focused, &.Mui-selected, &.Mui-selected.Mui-focused': { - backgroundColor: `var(--tree-view-bg-color, ${theme.palette.action.selected})`, - color: 'var(--tree-view-color)', - }, - [`& .${treeItemClasses.label}`]: { - fontWeight: 'inherit', - color: 'inherit', - }, - }, - [`& .${treeItemClasses.group}`]: { - marginLeft: 0, - [`& .${treeItemClasses.content}`]: { - paddingLeft: theme.spacing(2), - }, - }, -})); - -function StyledTreeItem(props: StyledTreeItemProps) { - const theme = useTheme(); - const { - bgColor, - color, - labelIcon: LabelIcon, - labelInfo, - labelText, - colorForDarkMode, - bgColorForDarkMode, - ...other - } = props; - - const styleProps = { - '--tree-view-color': theme.palette.mode !== 'dark' ? color : colorForDarkMode, - '--tree-view-bg-color': - theme.palette.mode !== 'dark' ? bgColor : bgColorForDarkMode, - }; - - return ( - - - - {labelText} - - - {labelInfo} - - - } - style={styleProps} - {...other} - /> - ); -} - -export default function GmailTreeView() { - return ( - } - defaultExpandIcon={} - defaultEndIcon={
} - sx={{ height: 264, flexGrow: 1, maxWidth: 400, overflowY: 'auto' }} - > - - - - - - - - - - - ); -} diff --git a/docs/data/material/components/tree-view/IconExpansionTreeView.js b/docs/data/material/components/tree-view/IconExpansionTreeView.js deleted file mode 100644 index 6fc5c733697084..00000000000000 --- a/docs/data/material/components/tree-view/IconExpansionTreeView.js +++ /dev/null @@ -1,131 +0,0 @@ -import * as React from 'react'; -import PropTypes from 'prop-types'; -import TreeView from '@mui/lab/TreeView'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import TreeItem, { useTreeItem } from '@mui/lab/TreeItem'; -import clsx from 'clsx'; -import Typography from '@mui/material/Typography'; - -const CustomContent = React.forwardRef(function CustomContent(props, ref) { - const { - classes, - className, - label, - nodeId, - icon: iconProp, - expansionIcon, - displayIcon, - } = props; - - const { - disabled, - expanded, - selected, - focused, - handleExpansion, - handleSelection, - preventSelection, - } = useTreeItem(nodeId); - - const icon = iconProp || expansionIcon || displayIcon; - - const handleMouseDown = (event) => { - preventSelection(event); - }; - - const handleExpansionClick = (event) => { - handleExpansion(event); - }; - - const handleSelectionClick = (event) => { - handleSelection(event); - }; - - return ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions -
- {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} -
- {icon} -
- - {label} - -
- ); -}); - -CustomContent.propTypes = { - /** - * Override or extend the styles applied to the component. - */ - classes: PropTypes.object.isRequired, - /** - * className applied to the root element. - */ - className: PropTypes.string, - /** - * The icon to display next to the tree node's label. Either a parent or end icon. - */ - displayIcon: PropTypes.node, - /** - * The icon to display next to the tree node's label. Either an expansion or collapse icon. - */ - expansionIcon: PropTypes.node, - /** - * The icon to display next to the tree node's label. - */ - icon: PropTypes.node, - /** - * The tree node label. - */ - label: PropTypes.node, - /** - * The id of the node. - */ - nodeId: PropTypes.string.isRequired, -}; - -function CustomTreeItem(props) { - return ; -} - -export default function IconExpansionTreeView() { - return ( - } - defaultExpandIcon={} - sx={{ height: 240, flexGrow: 1, maxWidth: 400, overflowY: 'auto' }} - > - - - - - - - - - - - - - - - - ); -} diff --git a/docs/data/material/components/tree-view/IconExpansionTreeView.tsx b/docs/data/material/components/tree-view/IconExpansionTreeView.tsx deleted file mode 100644 index f9d1964f8d4b36..00000000000000 --- a/docs/data/material/components/tree-view/IconExpansionTreeView.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import * as React from 'react'; -import TreeView from '@mui/lab/TreeView'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import TreeItem, { - TreeItemProps, - useTreeItem, - TreeItemContentProps, -} from '@mui/lab/TreeItem'; -import clsx from 'clsx'; -import Typography from '@mui/material/Typography'; - -const CustomContent = React.forwardRef(function CustomContent( - props: TreeItemContentProps, - ref, -) { - const { - classes, - className, - label, - nodeId, - icon: iconProp, - expansionIcon, - displayIcon, - } = props; - - const { - disabled, - expanded, - selected, - focused, - handleExpansion, - handleSelection, - preventSelection, - } = useTreeItem(nodeId); - - const icon = iconProp || expansionIcon || displayIcon; - - const handleMouseDown = (event: React.MouseEvent) => { - preventSelection(event); - }; - - const handleExpansionClick = ( - event: React.MouseEvent, - ) => { - handleExpansion(event); - }; - - const handleSelectionClick = ( - event: React.MouseEvent, - ) => { - handleSelection(event); - }; - - return ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions -
} - > - {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} -
- {icon} -
- - {label} - -
- ); -}); - -function CustomTreeItem(props: TreeItemProps) { - return ; -} - -export default function IconExpansionTreeView() { - return ( - } - defaultExpandIcon={} - sx={{ height: 240, flexGrow: 1, maxWidth: 400, overflowY: 'auto' }} - > - - - - - - - - - - - - - - - - ); -} diff --git a/docs/data/material/components/tree-view/MultiSelectTreeView.js b/docs/data/material/components/tree-view/MultiSelectTreeView.js deleted file mode 100644 index ae137220e9c046..00000000000000 --- a/docs/data/material/components/tree-view/MultiSelectTreeView.js +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from 'react'; -import TreeView from '@mui/lab/TreeView'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import TreeItem from '@mui/lab/TreeItem'; - -export default function MultiSelectTreeView() { - return ( - } - defaultExpandIcon={} - multiSelect - sx={{ height: 216, flexGrow: 1, maxWidth: 400, overflowY: 'auto' }} - > - - - - - - - - - - - - - - - ); -} diff --git a/docs/data/material/components/tree-view/MultiSelectTreeView.tsx b/docs/data/material/components/tree-view/MultiSelectTreeView.tsx deleted file mode 100644 index ae137220e9c046..00000000000000 --- a/docs/data/material/components/tree-view/MultiSelectTreeView.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from 'react'; -import TreeView from '@mui/lab/TreeView'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import TreeItem from '@mui/lab/TreeItem'; - -export default function MultiSelectTreeView() { - return ( - } - defaultExpandIcon={} - multiSelect - sx={{ height: 216, flexGrow: 1, maxWidth: 400, overflowY: 'auto' }} - > - - - - - - - - - - - - - - - ); -} diff --git a/docs/data/material/components/tree-view/RichObjectTreeView.js b/docs/data/material/components/tree-view/RichObjectTreeView.js deleted file mode 100644 index d984ffd4fe6c2a..00000000000000 --- a/docs/data/material/components/tree-view/RichObjectTreeView.js +++ /dev/null @@ -1,48 +0,0 @@ -import * as React from 'react'; -import TreeView from '@mui/lab/TreeView'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import TreeItem from '@mui/lab/TreeItem'; - -const data = { - id: 'root', - name: 'Parent', - children: [ - { - id: '1', - name: 'Child - 1', - }, - { - id: '3', - name: 'Child - 3', - children: [ - { - id: '4', - name: 'Child - 4', - }, - ], - }, - ], -}; - -export default function RichObjectTreeView() { - const renderTree = (nodes) => ( - - {Array.isArray(nodes.children) - ? nodes.children.map((node) => renderTree(node)) - : null} - - ); - - return ( - } - defaultExpanded={['root']} - defaultExpandIcon={} - sx={{ height: 110, flexGrow: 1, maxWidth: 400, overflowY: 'auto' }} - > - {renderTree(data)} - - ); -} diff --git a/docs/data/material/components/tree-view/RichObjectTreeView.tsx b/docs/data/material/components/tree-view/RichObjectTreeView.tsx deleted file mode 100644 index 12e853195018e3..00000000000000 --- a/docs/data/material/components/tree-view/RichObjectTreeView.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import * as React from 'react'; -import TreeView from '@mui/lab/TreeView'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import TreeItem from '@mui/lab/TreeItem'; - -interface RenderTree { - id: string; - name: string; - children?: readonly RenderTree[]; -} - -const data: RenderTree = { - id: 'root', - name: 'Parent', - children: [ - { - id: '1', - name: 'Child - 1', - }, - { - id: '3', - name: 'Child - 3', - children: [ - { - id: '4', - name: 'Child - 4', - }, - ], - }, - ], -}; - -export default function RichObjectTreeView() { - const renderTree = (nodes: RenderTree) => ( - - {Array.isArray(nodes.children) - ? nodes.children.map((node) => renderTree(node)) - : null} - - ); - - return ( - } - defaultExpanded={['root']} - defaultExpandIcon={} - sx={{ height: 110, flexGrow: 1, maxWidth: 400, overflowY: 'auto' }} - > - {renderTree(data)} - - ); -} diff --git a/docs/data/material/components/tree-view/RichObjectTreeView.tsx.preview b/docs/data/material/components/tree-view/RichObjectTreeView.tsx.preview deleted file mode 100644 index 16c48b7dea6a27..00000000000000 --- a/docs/data/material/components/tree-view/RichObjectTreeView.tsx.preview +++ /dev/null @@ -1,9 +0,0 @@ -} - defaultExpanded={['root']} - defaultExpandIcon={} - sx={{ height: 110, flexGrow: 1, maxWidth: 400, overflowY: 'auto' }} -> - {renderTree(data)} - \ No newline at end of file diff --git a/docs/data/material/components/tree-view/tree-view-pt.md b/docs/data/material/components/tree-view/tree-view-pt.md deleted file mode 100644 index af49445cdfaaae..00000000000000 --- a/docs/data/material/components/tree-view/tree-view-pt.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -productId: material-ui -title: Tree view React component -components: TreeView, TreeItem -githubLabel: 'component: tree view' -waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/ -packageName: '@material-ui/lab' ---- - -# Tree view - -

Um modo de visualização em árvore apresentando uma lista hierárquica.

- -As visualizações em árvore podem ser usadas para representar um navegação no sistema de arquivos para exibir pastas e arquivos, um item representando uma pasta pode ser expandido para revelar o conteúdo da pasta, que pode ser arquivos, pastas ou ambos. - -{{"component": "modules/components/ComponentLinkHeader.js"}} - -## Modo básico de visualização em árvore - -{{"demo": "FileSystemNavigator.js"}} - -## Seleção múltipla - -Visualizações de árvore também suportam seleção múltipla. - -{{"demo": "MultiSelectTreeView.js"}} - -## Visualização em árvore controlada - -A visualização em árvore também oferece uma API para controle. - -{{"demo": "ControlledTreeView.js"}} - -## Objeto complexo - -Enquanto o componente `TreeView`/`TreeItem` maximiza a flexibilidade, um passo extra é necessário para lidar com um objetos complexos. - -Vamos considerar uma variável de dados com a seguinte estrutura, a recursão pode ser usada para lidar com este cenário. - -```js -const data = { - id: 'root', - name: 'Parent', - children: [ - { - id: '1', - name: 'Child - 1', - }, - // … - ], -}; -``` - -{{"demo": "RichObjectTreeView.js", "defaultCodeOpen": false}} - -## Propriedade ContentComponent - -Você pode usar a propriedade `ContentComponent` e o hook `useTreeItem` para customizar ainda mais o comportamento do TreeItem. - -Como limitar a expansão para clicar no ícone: - -{{"demo": "IconExpansionTreeView.js", "defaultCodeOpen": false}} - -Ou aumentando a largura do indicador de estado: - -{{"demo": "BarTreeView.js", "defaultCodeOpen": false}} - -## Visualização em árvore customizada - -### Ícones customizados, borda e animação - -{{"demo": "CustomizedTreeView.js"}} - -### Clone do Gmail - -{{"demo": "GmailTreeView.js"}} - -## Itens desabilitados na árvore - -{{"demo": "DisabledTreeItems.js"}} - -O comportamento dos itens desabilitados da árvore depende da propriedade `disabledItemsFocusable`. - -Se é falsa: - -- As teclas de setas não focarão nos itens desabilitados e o próximo item não desabilitado será focado. -- Digitar o primeiro caractere do rótulo de um item desabilitado não focará no item. -- Interação do mouse ou teclado não irá expandir/recolher itens desabilitados. -- Interação do mouse ou teclado não selecionará itens desabilitados. -- Shift + teclas de setas irão pular itens desabilitados e o próximo item não desabilitado será selecionado. -- Foco programático não focará itens desabilitados. - -Se é verdadeira: - -- As teclas de setas focarão em itens desabilitados. -- Digitar o primeiro caractere do rótulo de um item desabilitado focará no item. -- Interação do mouse ou teclado não irá expandir/recolher itens desabilitados. -- Interação do mouse ou teclado não selecionará itens desabilitados. -- Shift + teclas de setas não irão pular itens desabilitados, mas o item desabilitado não será selecionado. -- Foco programático focará itens desabilitados. - -## Acessibilidade - -(WAI-ARIA: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/) - -O componente segue as práticas de autoria da WAI-ARIA. - -Para ter uma exibição em árvore acessível, você deve usar `aria-labelledby` ou `aria-label` para fazer referência ou fornecer um rótulo na TreeView, caso contrário, os leitores de tela irão anunciá-lo como "tree", tornando difícil entender o contexto de um item específico da árvore. diff --git a/docs/data/material/components/tree-view/tree-view-zh.md b/docs/data/material/components/tree-view/tree-view-zh.md deleted file mode 100644 index b4476986a8958e..00000000000000 --- a/docs/data/material/components/tree-view/tree-view-zh.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -productId: material-ui -title: Tree view React component -components: TreeView, TreeItem -githubLabel: 'component: tree view' -waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/ -packageName: '@mui/lab' ---- - -# Tree view - -

树视图组件能够展现一个分层的列表。

- -树视图可用来展现一个显示文件夹和文件的文件系统,一个代表文件夹的项目可以展开,此时可以显示文件夹的内容,而这个内容可以是文件,也可以是文件夹,或者两者皆可。 - -{{"component": "modules/components/ComponentLinkHeader.js"}} - -## 基本的树视图 - -{{"demo": "FileSystemNavigator.js"}} - -## 多种选择 - -树视图也支持多选。 - -{{"demo": "MultiSelectTreeView.js"}} - -## 可控的树视图 - -树视图也提供了一个可控制的 API。 - -{{"demo": "ControlledTreeView.js"}} - -## 丰富的对象 - -当使用 `TreeView`/`TreeItem` 组件 API 将灵活性最大化时,将需要额外的一步来处理一个丰富的对象。 - -请参照带有以下形状的一个数据变量,您可以用递归方法来处理它。 - -```js -const data = { - id: 'root', - name: 'Parent', - children: [ - { - id: '1', - name: 'Child - 1', - }, - // … - ], -}; -``` - -{{"demo": "RichObjectTreeView.js", "defaultCodeOpen": false}} - -## ContentComponent 属性 - -你可以使用 `ContentComponent` 属性和 `useTreeItem` hook 来进一步定制 TreeItem 的行为。 - -比如限制扩展动作,只能够点击图标。 - -{{"demo": "IconExpansionTreeView.js", "defaultCodeOpen": false}} - -或者增加状态指示器的宽度: - -{{"demo": "BarTreeView.js", "defaultCodeOpen": false}} - -## 自定义的树视图 - -### 自定义的图标,边框和动画 - -{{"demo": "CustomizedTreeView.js"}} - -### 仿 Gmail - -{{"demo": "GmailTreeView.js"}} - -## 禁用树项 - -{{"demo": "DisabledTreeItems.js"}} - -被禁用的树项的行为取决于 `disabledItemsFocusable` 属性。 - -如果为假(false): - -- 箭头键不会聚焦已禁用的项目,下一个非禁用的项目将会被聚焦。 -- 键入所被禁用的项目标签的第一个字符是无法聚焦该项目的。 -- 鼠标或键盘交互不会展开/折叠所被禁用的项目。 -- 鼠标或键盘交互不会选择所被禁用的项目。 -- Shift + 方向键将跳过所被禁用的项目,并且会选择到下一个非禁用的项目。 -- 编程焦点将不会聚焦到已禁用的项目。 - -如果为真(true): - -- 箭头键将会聚焦到已禁用的项目。 -- 键入所被禁用的项目标签的第一个字符将聚焦到该项目。 -- 鼠标或键盘交互不会展开/折叠所被禁用的项目。 -- 鼠标或键盘交互不会选择所被禁用的项目。 -- Shift + 方向键不会跳过禁用的项目,但是已被禁用项目也不会被选中。 -- 编程焦点将会聚焦到已禁用的项目。 - -## 无障碍设计 - -(WAI-ARIA: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/) - -组件遵循了 WAI-ARIA 授权的一些标准。 - -如果你想让树视图具有无障碍设计,那么你必须使用 `aria-labelledby` 或 `aria-label` 在树视图上引用或提供标签,否则屏幕阅读器会将其声明为“树(tree)”,从而会使人很难理解特定树项的上下文的含义。 diff --git a/docs/data/material/components/tree-view/tree-view.md b/docs/data/material/components/tree-view/tree-view.md deleted file mode 100644 index 44200f111c06ef..00000000000000 --- a/docs/data/material/components/tree-view/tree-view.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -productId: material-ui -title: Tree View React component -components: TreeView, TreeItem -githubLabel: 'component: tree view' -waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/ -packageName: '@mui/lab' ---- - -# Tree View - -

A tree view widget presents a hierarchical list.

- -Tree views can be used to represent a file system navigator displaying folders and files, an item representing a folder can be expanded to reveal the contents of the folder, which may be files, folders, or both. - -{{"component": "modules/components/ComponentLinkHeader.js"}} - -## Basic tree view - -{{"demo": "FileSystemNavigator.js"}} - -## Multi-selection - -Tree views also support multi-selection. - -{{"demo": "MultiSelectTreeView.js"}} - -## Controlled tree view - -The tree view also offers a controlled API. - -:::info - -- A component is **controlled** when it's managed by its parent using props. -- A component is **uncontrolled** when it's managed by its own local state. - -Learn more about controlled and uncontrolled components in the [React documentation](https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components). -::: - -{{"demo": "ControlledTreeView.js"}} - -## Rich object - -While the `TreeView`/`TreeItem` component API maximizes flexibility, an extra step is needed to handle a rich object. - -Let's consider a data variable with the following shape, recursion can be used to handle it. - -```js -const data = { - id: 'root', - name: 'Parent', - children: [ - { - id: '1', - name: 'Child - 1', - }, - // … - ], -}; -``` - -{{"demo": "RichObjectTreeView.js", "defaultCodeOpen": false}} - -## ContentComponent prop - -You can use the `ContentComponent` prop and the `useTreeItem` hook to further customize the behavior of the TreeItem. - -Such as limiting expansion to clicking the icon: - -{{"demo": "IconExpansionTreeView.js", "defaultCodeOpen": false}} - -Or increasing the width of the state indicator: - -{{"demo": "BarTreeView.js", "defaultCodeOpen": false}} - -## Customization - -### Custom icons, border and animation - -{{"demo": "CustomizedTreeView.js"}} - -### Gmail clone - -{{"demo": "GmailTreeView.js"}} - -## Disabled tree items - -{{"demo": "DisabledTreeItems.js"}} - -The behavior of disabled tree items depends on the `disabledItemsFocusable` prop. - -If it is false: - -- Arrow keys will not focus disabled items and, the next non-disabled item will be focused. -- Typing the first character of a disabled item's label will not focus the item. -- Mouse or keyboard interaction will not expand/collapse disabled items. -- Mouse or keyboard interaction will not select disabled items. -- Shift + arrow keys will skip disabled items and, the next non-disabled item will be selected. -- Programmatic focus will not focus disabled items. - -If it is true: - -- Arrow keys will focus disabled items. -- Typing the first character of a disabled item's label will focus the item. -- Mouse or keyboard interaction will not expand/collapse disabled items. -- Mouse or keyboard interaction will not select disabled items. -- Shift + arrow keys will not skip disabled items but, the disabled item will not be selected. -- Programmatic focus will focus disabled items. - -## Accessibility - -(WAI-ARIA: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/) - -The component follows the WAI-ARIA authoring practices. - -To have an accessible tree view you must use `aria-labelledby` or `aria-label` to reference or provide a label on the TreeView, otherwise screen readers will announce it as "tree", making it hard to understand the context of a specific tree item. diff --git a/docs/data/material/discover-more/roadmap/roadmap.md b/docs/data/material/discover-more/roadmap/roadmap.md index c76592406b4b3f..848a559fe16ff4 100644 --- a/docs/data/material/discover-more/roadmap/roadmap.md +++ b/docs/data/material/discover-more/roadmap/roadmap.md @@ -47,41 +47,41 @@ Here are the components we will work on being supported in the MUI ecosystem: - 🛠 Work in progress, will be or already released as unstable - ⏳ Planning to build -| Name | Product | Status | -| :----------------------------------------------------------------------- | :------- | :----- | -| Advanced Layout | MUI X | ⏳ | -| Carousel | MUI X | ⏳ | -| [Charts](https://mui.com/x/react-charts/) | MUI X | 🧪 | -| [Data Grid](/x/react-data-grid/) | MUI X | ✅ | -| [Date Picker](/x/react-date-pickers/date-picker/) | MUI X | ✅ | -| [Time Picker](/x/react-date-pickers/time-picker/) | MUI X | ✅ | -| [Date Time Picker](/x/react-date-pickers/date-time-picker/) | MUI X | ✅ | -| [Date Range Picker](/x/react-date-pickers/date-range-picker/) | MUI X | ✅ | -| Time Range Picker | MUI X | ⏳ | -| Date Time Range Picker | MUI X | ⏳ | -| Dropdown | MUI Core | ⏳ | -| Dropzone | MUI X | ⏳ | -| File Upload | MUI X | ⏳ | -| Gantt Chart | MUI X | ⏳ | -| Gauge | MUI X | ⏳ | -| Image | MUI Core | ⏳ | -| [Masonry](/material-ui/react-masonry/) | MUI Core | 🧪 | -| Navbar | MUI Core | ⏳ | -| Nested Menu | MUI X | ⏳ | -| NProgress | MUI Core | ⏳ | -| Numeric Input | MUI Core | ⏳ | -| Rich Text Editor | MUI X | ⏳ | -| Scheduler | MUI X | ⏳ | -| Scrollspy | MUI Core | ⏳ | -| Sparkline | MUI X | ⏳ | -| [Timeline](/material-ui/react-timeline/) | MUI Core | 🧪 | -| Tree select | MUI X | ⏳ | -| [Tree View](/material-ui/react-tree-view/) | MUI X | 🧪 | -| Tree View - Checkbox | MUI X | ⏳ | -| Tree View - Drag & Drop | MUI X | ⏳ | -| [Tree View - Multiselect](/material-ui/react-tree-view/#multi-selection) | MUI X | 🧪 | -| Tree View - Virtualization | MUI X | ⏳ | -| Window Splitter | MUI X | ⏳ | +| Name | Product | Status | +| :------------------------------------------------------------- | :------- | :----- | +| Advanced Layout | MUI X | ⏳ | +| Carousel | MUI X | ⏳ | +| [Charts](https://mui.com/x/react-charts/) | MUI X | 🧪 | +| [Data Grid](/x/react-data-grid/) | MUI X | ✅ | +| [Date Picker](/x/react-date-pickers/date-picker/) | MUI X | ✅ | +| [Time Picker](/x/react-date-pickers/time-picker/) | MUI X | ✅ | +| [Date Time Picker](/x/react-date-pickers/date-time-picker/) | MUI X | ✅ | +| [Date Range Picker](/x/react-date-pickers/date-range-picker/) | MUI X | ✅ | +| Time Range Picker | MUI X | ⏳ | +| Date Time Range Picker | MUI X | ⏳ | +| Dropdown | MUI Core | ⏳ | +| Dropzone | MUI X | ⏳ | +| File Upload | MUI X | ⏳ | +| Gantt Chart | MUI X | ⏳ | +| Gauge | MUI X | ⏳ | +| Image | MUI Core | ⏳ | +| [Masonry](/material-ui/react-masonry/) | MUI Core | 🧪 | +| Navbar | MUI Core | ⏳ | +| Nested Menu | MUI X | ⏳ | +| NProgress | MUI Core | ⏳ | +| Numeric Input | MUI Core | ⏳ | +| Rich Text Editor | MUI X | ⏳ | +| Scheduler | MUI X | ⏳ | +| Scrollspy | MUI Core | ⏳ | +| Sparkline | MUI X | ⏳ | +| [Timeline](/material-ui/react-timeline/) | MUI Core | 🧪 | +| Tree select | MUI X | ⏳ | +| [Tree View](/x/react-tree-view/) | MUI X | 🧪 | +| Tree View - Checkbox | MUI X | ⏳ | +| Tree View - Drag & Drop | MUI X | ⏳ | +| [Tree View - Multiselect](/x/react-tree-view/#multi-selection) | MUI X | 🧪 | +| Tree View - Virtualization | MUI X | ⏳ | +| Window Splitter | MUI X | ⏳ | :::warning **Disclaimer**: We operate in a dynamic environment, and things are subject to change. The information provided is intended to outline the general framework direction, for informational purposes only. We may decide to add or remove new items at any time, depending on our capability to deliver while meeting our quality standards. The development, releases, and timing of any features or functionality remains at the sole discretion of MUI. The roadmap does not represent a commitment, obligation, or promise to deliver at any time. diff --git a/docs/data/material/getting-started/supported-components/MaterialUIComponents.js b/docs/data/material/getting-started/supported-components/MaterialUIComponents.js index 43e3ab9c5a988a..3c9465a407ecea 100644 --- a/docs/data/material/getting-started/supported-components/MaterialUIComponents.js +++ b/docs/data/material/getting-started/supported-components/MaterialUIComponents.js @@ -194,7 +194,7 @@ const components = [ materialDesign: 'https://m2.material.io/components/tooltips', }, { name: 'Transfer List', materialUI: '/material-ui/react-transfer-list/' }, - { name: 'Tree View', materialUI: '/material-ui/react-tree-view/' }, + { name: 'Tree View', materialUI: '/x/react-tree-view/' }, { name: 'Typography', materialUI: '/material-ui/react-typography/', diff --git a/docs/data/material/pages.ts b/docs/data/material/pages.ts index cf8f0003a89ccf..e046d612b8c7db 100644 --- a/docs/data/material/pages.ts +++ b/docs/data/material/pages.ts @@ -143,7 +143,6 @@ const pages: MuiPage[] = [ { pathname: '/material-ui/about-the-lab', title: 'About the lab 🧪' }, { pathname: '/material-ui/react-masonry' }, { pathname: '/material-ui/react-timeline' }, - { pathname: '/material-ui/react-tree-view', title: 'Tree View' }, ], }, ], diff --git a/docs/data/material/pagesApi.js b/docs/data/material/pagesApi.js index 4f1c9df177f38e..7bb6fe6df78aa8 100644 --- a/docs/data/material/pagesApi.js +++ b/docs/data/material/pagesApi.js @@ -133,8 +133,6 @@ module.exports = [ { pathname: '/material-ui/api/toggle-button-group' }, { pathname: '/material-ui/api/toolbar' }, { pathname: '/material-ui/api/tooltip' }, - { pathname: '/material-ui/api/tree-item' }, - { pathname: '/material-ui/api/tree-view' }, { pathname: '/material-ui/api/typography' }, { pathname: '/material-ui/api/zoom' }, ]; diff --git a/docs/next.config.js b/docs/next.config.js index 985c81386d6019..69338cc64e730f 100644 --- a/docs/next.config.js +++ b/docs/next.config.js @@ -58,6 +58,7 @@ module.exports = withDocsInfra({ '@mui/x-date-pickers-pro', '@mui/x-data-grid-generator', '@mui/x-charts', + '@mui/x-tree-view', '@mui/x-license-pro', ].some((dep) => request.startsWith(dep)); @@ -119,7 +120,7 @@ module.exports = withDocsInfra({ test: /\.(js|mjs|jsx)$/, resourceQuery: { not: [/raw/] }, include: - /node_modules(\/|\\)(notistack|@mui(\/|\\)x-data-grid|@mui(\/|\\)x-data-grid-pro|@mui(\/|\\)x-license-pro|@mui(\/|\\)x-data-grid-generator|@mui(\/|\\)x-date-pickers-pro|@mui(\/|\\)x-date-pickers|@mui(\/|\\)x-charts)/, + /node_modules(\/|\\)(notistack|@mui(\/|\\)x-data-grid|@mui(\/|\\)x-data-grid-pro|@mui(\/|\\)x-license-pro|@mui(\/|\\)x-data-grid-generator|@mui(\/|\\)x-date-pickers-pro|@mui(\/|\\)x-date-pickers|@mui(\/|\\)x-charts|@mui(\/|\\)x-tree-view)/, use: { loader: 'babel-loader', options: { diff --git a/docs/pages/blog/2019.md b/docs/pages/blog/2019.md index e12b38477e5234..e1dbf8f38b5e72 100644 --- a/docs/pages/blog/2019.md +++ b/docs/pages/blog/2019.md @@ -59,7 +59,7 @@ Some of the key factors: - [Skeleton](/material-ui/react-skeleton/) - [Slider](/material-ui/react-slider/) - [TextareaAutosize](/material-ui/react-textarea-autosize/) - - [TreeView](/material-ui/react-tree-view/) + - [TreeView](/x/react-tree-view/) - We have fixed a significant number of [accessibility issues](https://github.com/mui/material-ui/issues?q=is%3Aissue+label%3Aaccessibility+is%3Aclosed). - We have introduced global class names. - We have migrated the whole codebase to hooks. diff --git a/docs/pages/blog/july-2019-update.md b/docs/pages/blog/july-2019-update.md index 270714c86838aa..cfea7d84fd0fe3 100644 --- a/docs/pages/blog/july-2019-update.md +++ b/docs/pages/blog/july-2019-update.md @@ -8,7 +8,7 @@ tags: ['Company'] Here are the most significant improvements in July: -- 🌳 We have introduced a new [Tree View component](/material-ui/react-tree-view/) in the lab. Big thanks to Josh for it. +- 🌳 We have introduced a new [Tree View component](/x/react-tree-view/) in the lab. Big thanks to Josh for it. ![Tree View](/static/blog/july-2019-update/tree-view.gif) diff --git a/docs/pages/material-ui/api/tree-item.js b/docs/pages/material-ui/api/tree-item.js deleted file mode 100644 index 938a0e13404ecc..00000000000000 --- a/docs/pages/material-ui/api/tree-item.js +++ /dev/null @@ -1,19 +0,0 @@ -import * as React from 'react'; -import ApiPage from 'docs/src/modules/components/ApiPage'; -import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; -import jsonPageContent from './tree-item.json'; - -export default function Page(props) { - const { descriptions, pageContent } = props; - return ; -} - -Page.getInitialProps = () => { - const req = require.context('docs/translations/api-docs/tree-item', false, /tree-item.*.json$/); - const descriptions = mapApiPageTranslations(req); - - return { - descriptions, - pageContent: jsonPageContent, - }; -}; diff --git a/docs/pages/material-ui/api/tree-item.json b/docs/pages/material-ui/api/tree-item.json deleted file mode 100644 index 0f2f44e92b7eac..00000000000000 --- a/docs/pages/material-ui/api/tree-item.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "props": { - "nodeId": { "type": { "name": "string" }, "required": true }, - "children": { "type": { "name": "node" } }, - "classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } }, - "collapseIcon": { "type": { "name": "node" } }, - "ContentComponent": { - "type": { "name": "custom", "description": "element type" }, - "default": "TreeItemContent" - }, - "ContentProps": { "type": { "name": "object" } }, - "disabled": { "type": { "name": "bool" }, "default": "false" }, - "endIcon": { "type": { "name": "node" } }, - "expandIcon": { "type": { "name": "node" } }, - "icon": { "type": { "name": "node" } }, - "label": { "type": { "name": "node" } }, - "onFocus": { "type": { "name": "custom", "description": "unsupportedProp" } }, - "sx": { - "type": { - "name": "union", - "description": "Array<func
| object
| bool>
| func
| object" - }, - "additionalInfo": { "sx": true } - }, - "TransitionComponent": { "type": { "name": "elementType" }, "default": "Collapse" }, - "TransitionProps": { "type": { "name": "object" } } - }, - "name": "TreeItem", - "imports": ["import TreeItem from '@mui/lab/TreeItem';", "import { TreeItem } from '@mui/lab';"], - "styles": { - "classes": [ - "root", - "group", - "content", - "expanded", - "selected", - "focused", - "disabled", - "iconContainer", - "label" - ], - "globalClasses": { - "expanded": "Mui-expanded", - "selected": "Mui-selected", - "focused": "Mui-focused", - "disabled": "Mui-disabled" - }, - "name": "MuiTreeItem" - }, - "spread": true, - "themeDefaultProps": true, - "muiName": "MuiTreeItem", - "forwardsRefTo": "HTMLLIElement", - "filename": "/packages/mui-lab/src/TreeItem/TreeItem.js", - "inheritance": null, - "demos": "", - "cssComponent": false -} diff --git a/docs/pages/material-ui/api/tree-view.js b/docs/pages/material-ui/api/tree-view.js deleted file mode 100644 index 079bf4c37b3e53..00000000000000 --- a/docs/pages/material-ui/api/tree-view.js +++ /dev/null @@ -1,19 +0,0 @@ -import * as React from 'react'; -import ApiPage from 'docs/src/modules/components/ApiPage'; -import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; -import jsonPageContent from './tree-view.json'; - -export default function Page(props) { - const { descriptions, pageContent } = props; - return ; -} - -Page.getInitialProps = () => { - const req = require.context('docs/translations/api-docs/tree-view', false, /tree-view.*.json$/); - const descriptions = mapApiPageTranslations(req); - - return { - descriptions, - pageContent: jsonPageContent, - }; -}; diff --git a/docs/pages/material-ui/api/tree-view.json b/docs/pages/material-ui/api/tree-view.json deleted file mode 100644 index 5336f9fa99af75..00000000000000 --- a/docs/pages/material-ui/api/tree-view.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "props": { - "children": { "type": { "name": "node" } }, - "classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } }, - "defaultCollapseIcon": { "type": { "name": "node" } }, - "defaultEndIcon": { "type": { "name": "node" } }, - "defaultExpanded": { - "type": { "name": "arrayOf", "description": "Array<string>" }, - "default": "[]" - }, - "defaultExpandIcon": { "type": { "name": "node" } }, - "defaultParentIcon": { "type": { "name": "node" } }, - "defaultSelected": { - "type": { "name": "union", "description": "Array<string>
| string" }, - "default": "[]" - }, - "disabledItemsFocusable": { "type": { "name": "bool" }, "default": "false" }, - "disableSelection": { "type": { "name": "bool" }, "default": "false" }, - "expanded": { "type": { "name": "arrayOf", "description": "Array<string>" } }, - "id": { "type": { "name": "string" } }, - "multiSelect": { "type": { "name": "bool" }, "default": "false" }, - "onNodeFocus": { - "type": { "name": "func" }, - "signature": { - "type": "function(event: React.SyntheticEvent, value: string) => void", - "describedArgs": ["event", "value"] - } - }, - "onNodeSelect": { - "type": { "name": "func" }, - "signature": { - "type": "function(event: React.SyntheticEvent, nodeIds: Array | string) => void", - "describedArgs": ["event", "nodeIds"] - } - }, - "onNodeToggle": { - "type": { "name": "func" }, - "signature": { - "type": "function(event: React.SyntheticEvent, nodeIds: array) => void", - "describedArgs": ["event", "nodeIds"] - } - }, - "selected": { - "type": { "name": "union", "description": "Array<string>
| string" } - }, - "sx": { - "type": { - "name": "union", - "description": "Array<func
| object
| bool>
| func
| object" - }, - "additionalInfo": { "sx": true } - } - }, - "name": "TreeView", - "imports": ["import TreeView from '@mui/lab/TreeView';", "import { TreeView } from '@mui/lab';"], - "styles": { "classes": ["root"], "globalClasses": {}, "name": "MuiTreeView" }, - "spread": true, - "themeDefaultProps": true, - "muiName": "MuiTreeView", - "forwardsRefTo": "HTMLUListElement", - "filename": "/packages/mui-lab/src/TreeView/TreeView.js", - "inheritance": null, - "demos": "", - "cssComponent": false -} diff --git a/docs/pages/material-ui/react-tree-view.js b/docs/pages/material-ui/react-tree-view.js deleted file mode 100644 index 8885df153e6035..00000000000000 --- a/docs/pages/material-ui/react-tree-view.js +++ /dev/null @@ -1,7 +0,0 @@ -import * as React from 'react'; -import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; -import * as pageProps from 'docs/data/material/components/tree-view/tree-view.md?@mui/markdown'; - -export default function Page() { - return ; -} diff --git a/docs/public/_redirects b/docs/public/_redirects index f2fa0249463a46..1a1ebd34a1b0fc 100644 --- a/docs/public/_redirects +++ b/docs/public/_redirects @@ -493,6 +493,9 @@ https://v4.material-ui.com/* https://v4.mui.com/:splat 301! /joy-ui/guides/using-icon-libraries/ /joy-ui/integrations/icon-libraries/ 301 /joy-ui/guides/themeable-component/ /joy-ui/customization/creating-themed-components/ 301 /joy-ui/guides/overriding-component-structure/ /joy-ui/customization/overriding-component-structure/ 301 +/material-ui/react-tree-view/ /x/react-tree-view/ 301 +/material-ui/api/tree-view/ /x/api/tree-view/tree-view/ 301 +/material-ui/api/tree-item/ /x/api/tree-view/tree-item/ 301 # Proxies diff --git a/docs/src/components/productX/XTreeViewDemo.tsx b/docs/src/components/productX/XTreeViewDemo.tsx index 156c899b3e736a..df4eba0ada0505 100644 --- a/docs/src/components/productX/XTreeViewDemo.tsx +++ b/docs/src/components/productX/XTreeViewDemo.tsx @@ -4,8 +4,13 @@ import { styled, alpha } from '@mui/material/styles'; import Box from '@mui/material/Box'; import Paper from '@mui/material/Paper'; import Chip from '@mui/material/Chip'; -import TreeView from '@mui/lab/TreeView'; -import MuiTreeItem, { useTreeItem, TreeItemProps, TreeItemContentProps } from '@mui/lab/TreeItem'; +import { TreeView } from '@mui/x-tree-view/TreeView'; +import { + TreeItem as MuiTreeItem, + useTreeItem, + TreeItemProps, + TreeItemContentProps, +} from '@mui/x-tree-view/TreeItem'; import Typography from '@mui/material/Typography'; import AddBoxOutlined from '@mui/icons-material/AddBoxOutlined'; import IndeterminateCheckBoxOutlined from '@mui/icons-material/IndeterminateCheckBoxOutlined'; @@ -45,16 +50,16 @@ const CustomContent = React.forwardRef(function CustomContent( const icon = iconProp || expansionIcon || displayIcon; - const handleMouseDown = (event: React.MouseEvent) => { + const handleMouseDown = (event: React.MouseEvent) => { preventSelection(event); }; - const handleExpansionClick = (event: React.MouseEvent) => { + const handleExpansionClick = (event: React.MouseEvent) => { handleExpansion(event); handleSelection(event); }; - const handleSelectionClick = (event: React.MouseEvent) => { + const handleSelectionClick = (event: React.MouseEvent) => { handleSelection(event); }; @@ -204,13 +209,14 @@ const StyledTreeItem = styled(MuiTreeItem)(({ theme }) => [ }), ]); -function TreeItem( +const TreeItem = React.forwardRef(function TreeItem( props: TreeItemProps & { ContentProps?: { lastNestedChild?: boolean }; }, + ref: React.Ref, ) { - return ; -} + return ; +}); export default function XDateRangeDemo() { return ( @@ -302,15 +308,15 @@ export default function XDateRangeDemo() { ({ pb: 0.2, fontWeight: theme.typography.fontWeightSemiBold, - color: (theme.vars || theme).palette.primary[300], - borderColor: alpha(theme.palette.primary[300], 0.3), - background: alpha(theme.palette.primary[800], 0.3), + color: (theme.vars || theme).palette.warning[300], + borderColor: alpha(theme.palette.warning[300], 0.3), + background: alpha(theme.palette.warning[800], 0.3), })} /> @@ -319,7 +325,7 @@ export default function XDateRangeDemo() { - - - {!hide && } - - - - ); - } - const { getByTestId, queryByTestId } = render(); - - expect(getByTestId('1')).to.have.attribute('aria-expanded', 'true'); - expect(getByTestId('2')).not.to.equal(null); - fireEvent.click(getByTestId('button')); - expect(getByTestId('1')).not.to.have.attribute('aria-expanded'); - expect(queryByTestId('2')).to.equal(null); - }); - - it('should treat an empty array equally to no children', () => { - const { getByTestId } = render( - - - - {[]} - - - , - ); - - expect(getByTestId('2')).not.to.have.attribute('aria-expanded'); - }); - - it('should not call onClick when children are clicked', () => { - const handleClick = spy(); - - const { getByText } = render( - - - - - , - ); - - fireEvent.click(getByText('two')); - - expect(handleClick.callCount).to.equal(0); - }); - - it('should be able to use a custom id', () => { - const { getByRole } = render( - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - - expect(getByRole('tree')).to.have.attribute('aria-activedescendant', 'customId'); - }); - - describe('Accessibility', () => { - it('should have the role `treeitem`', () => { - const { getByTestId } = render( - - - , - ); - - expect(getByTestId('test')).to.have.attribute('role', 'treeitem'); - }); - - it('should add the role `group` to a component containing children', () => { - const { getByRole, getByText } = render( - - - - - , - ); - - expect(getByRole('group')).to.contain(getByText('test2')); - }); - - describe('aria-expanded', () => { - it('should have the attribute `aria-expanded=false` if collapsed', () => { - const { getByTestId } = render( - - - - - , - ); - - expect(getByTestId('test')).to.have.attribute('aria-expanded', 'false'); - }); - - it('should have the attribute `aria-expanded={true}` if expanded', () => { - const { getByTestId } = render( - - - - - , - ); - - expect(getByTestId('test')).to.have.attribute('aria-expanded', 'true'); - }); - - it('should not have the attribute `aria-expanded` if no children are present', () => { - const { getByTestId } = render( - - - , - ); - - expect(getByTestId('test')).not.to.have.attribute('aria-expanded'); - }); - }); - - describe('aria-disabled', () => { - it('should not have the attribute `aria-disabled` if disabled is false', () => { - const { getByTestId } = render( - - - , - ); - - expect(getByTestId('one')).not.to.have.attribute('aria-disabled'); - }); - - it('should have the attribute `aria-disabled={true}` if disabled', () => { - const { getByTestId } = render( - - - , - ); - - expect(getByTestId('one')).to.have.attribute('aria-disabled', 'true'); - }); - }); - - describe('aria-selected', () => { - describe('single-select', () => { - it('should not have the attribute `aria-selected` if not selected', () => { - const { getByTestId } = render( - - - , - ); - - expect(getByTestId('test')).not.to.have.attribute('aria-selected'); - }); - - it('should have the attribute `aria-selected={true}` if selected', () => { - const { getByTestId } = render( - - - , - ); - - expect(getByTestId('test')).to.have.attribute('aria-selected', 'true'); - }); - }); - - describe('multi-select', () => { - it('should have the attribute `aria-selected=false` if not selected', () => { - const { getByTestId } = render( - - - , - ); - - expect(getByTestId('test')).to.have.attribute('aria-selected', 'false'); - }); - - it('should have the attribute `aria-selected={true}` if selected', () => { - const { getByTestId } = render( - - - , - ); - - expect(getByTestId('test')).to.have.attribute('aria-selected', 'true'); - }); - - it('should have the attribute `aria-selected` if disableSelection is true', () => { - const { getByTestId } = render( - - - , - ); - - expect(getByTestId('test')).to.have.attribute('aria-selected', 'false'); - }); - }); - }); - - describe('when a tree receives focus', () => { - it('should focus the first node if none of the nodes are selected before the tree receives focus', () => { - const { getByRole, getByTestId, queryAllByRole } = render( - - - - - , - ); - - expect(queryAllByRole('treeitem', { selected: true })).to.have.length(0); - - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('one')).toHaveVirtualFocus(); - }); - - it('should focus the selected node if a node is selected before the tree receives focus', () => { - const { getByTestId, getByRole } = render( - - - - - , - ); - - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('two')).toHaveVirtualFocus(); - }); - - it('should work with programmatic focus', () => { - const { getByRole, getByTestId } = render( - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('one')).toHaveVirtualFocus(); - - act(() => { - getByTestId('two').focus(); - }); - expect(getByTestId('two')).toHaveVirtualFocus(); - }); - - it('should work when focused node is removed', () => { - let removeActiveItem; - // a TreeItem which can remove from the tree by calling `removeActiveItem` - function ControlledTreeItem(props) { - const [mounted, setMounted] = React.useReducer(() => false, true); - removeActiveItem = setMounted; - - if (!mounted) { - return null; - } - return ; - } - - const { getByRole, getByTestId, getByText } = render( - - - - - - , - ); - const tree = getByRole('tree'); - - act(() => { - tree.focus(); - }); - - expect(getByTestId('parent')).toHaveVirtualFocus(); - - fireEvent.click(getByText('two')); - - expect(getByTestId('two')).toHaveVirtualFocus(); - - // generic action that removes an item. - // Could be promise based, or timeout, or another user interaction - act(() => { - removeActiveItem(); - }); - - expect(getByTestId('parent')).toHaveVirtualFocus(); - }); - - it('should focus on tree with scroll prevented', () => { - const { getByRole, getByTestId } = render( - - - - , - ); - const focus = spy(getByRole('tree'), 'focus'); - - act(() => { - getByTestId('one').focus(); - }); - - expect(focus.calledOnceWithExactly({ preventScroll: true })).to.equals(true); - }); - }); - - describe('Navigation', () => { - describe('right arrow interaction', () => { - it('should open the node and not move the focus if focus is on a closed node', () => { - const { getByRole, getByTestId } = render( - - - - - , - ); - - expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false'); - - act(() => { - getByRole('tree').focus(); - }); - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowRight' }); - - expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true'); - expect(getByTestId('one')).toHaveVirtualFocus(); - }); - - it('should move focus to the first child if focus is on an open node', () => { - const { getByTestId, getByRole } = render( - - - - - , - ); - - expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true'); - - act(() => { - getByRole('tree').focus(); - }); - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowRight' }); - - expect(getByTestId('two')).toHaveVirtualFocus(); - }); - - it('should do nothing if focus is on an end node', () => { - const { getByRole, getByTestId, getByText } = render( - - - - - , - ); - - fireEvent.click(getByText('two')); - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('two')).toHaveVirtualFocus(); - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowRight' }); - - expect(getByTestId('two')).toHaveVirtualFocus(); - }); - }); - - describe('left arrow interaction', () => { - it('should close the node if focus is on an open node', () => { - render( - - - - - , - ); - const [firstItem] = screen.getAllByRole('treeitem'); - const firstItemLabel = screen.getByText('one'); - - fireEvent.click(firstItemLabel); - - expect(firstItem).to.have.attribute('aria-expanded', 'true'); - - act(() => { - screen.getByRole('tree').focus(); - }); - fireEvent.keyDown(screen.getByRole('tree'), { key: 'ArrowLeft' }); - - expect(firstItem).to.have.attribute('aria-expanded', 'false'); - expect(screen.getByTestId('one')).toHaveVirtualFocus(); - }); - - it("should move focus to the node's parent node if focus is on a child node that is an end node", () => { - render( - - - - - , - ); - const [firstItem] = screen.getAllByRole('treeitem'); - const secondItemLabel = screen.getByText('two'); - - expect(firstItem).to.have.attribute('aria-expanded', 'true'); - - fireEvent.click(secondItemLabel); - act(() => { - screen.getByRole('tree').focus(); - }); - - expect(screen.getByTestId('two')).toHaveVirtualFocus(); - fireEvent.keyDown(screen.getByRole('tree'), { key: 'ArrowLeft' }); - - expect(screen.getByTestId('one')).toHaveVirtualFocus(); - expect(firstItem).to.have.attribute('aria-expanded', 'true'); - }); - - it("should move focus to the node's parent node if focus is on a child node that is closed", () => { - render( - - - - - - - , - ); - - fireEvent.click(screen.getByText('one')); - - expect(screen.getByTestId('one')).to.have.attribute('aria-expanded', 'true'); - - act(() => { - screen.getByTestId('two').focus(); - }); - - expect(screen.getByTestId('two')).toHaveVirtualFocus(); - - fireEvent.keyDown(screen.getByRole('tree'), { key: 'ArrowLeft' }); - - expect(screen.getByTestId('one')).toHaveVirtualFocus(); - expect(screen.getByTestId('one')).to.have.attribute('aria-expanded', 'true'); - }); - - it('should do nothing if focus is on a root node that is closed', () => { - const { getByRole, getByTestId } = render( - - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false'); - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowLeft' }); - expect(getByTestId('one')).toHaveVirtualFocus(); - }); - - it('should do nothing if focus is on a root node that is an end node', () => { - const { getByRole, getByTestId } = render( - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowLeft' }); - - expect(getByTestId('one')).toHaveVirtualFocus(); - }); - }); - - describe('down arrow interaction', () => { - it('moves focus to a sibling node', () => { - const { getByRole, getByTestId } = render( - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' }); - - expect(getByTestId('two')).toHaveVirtualFocus(); - }); - - it('moves focus to a child node', () => { - const { getByRole, getByTestId } = render( - - - - - , - ); - - expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true'); - - act(() => { - getByRole('tree').focus(); - }); - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' }); - - expect(getByTestId('two')).toHaveVirtualFocus(); - }); - - it('moves focus to a child node works with a dynamic tree', () => { - function TestComponent() { - const [hide, setState] = React.useState(false); - - return ( - - - - {!hide && ( - - - - )} - - - - ); - } - - const { getByRole, queryByTestId, getByTestId, getByText } = render(); - - expect(getByTestId('one')).not.to.equal(null); - fireEvent.click(getByText('Toggle Hide')); - expect(queryByTestId('one')).to.equal(null); - fireEvent.click(getByText('Toggle Hide')); - expect(getByTestId('one')).not.to.equal(null); - - act(() => { - getByRole('tree').focus(); - }); - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' }); - - expect(getByTestId('two')).toHaveVirtualFocus(); - }); - - it("moves focus to a parent's sibling", () => { - const { getByRole, getByTestId, getByText } = render( - - - - - - , - ); - - expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true'); - - fireEvent.click(getByText('two')); - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('two')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' }); - - expect(getByTestId('three')).toHaveVirtualFocus(); - }); - }); - - describe('up arrow interaction', () => { - it('moves focus to a sibling node', () => { - const { getByRole, getByTestId, getByText } = render( - - - - , - ); - - fireEvent.click(getByText('two')); - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('two')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp' }); - - expect(getByTestId('one')).toHaveVirtualFocus(); - }); - - it('moves focus to a parent', () => { - const { getByRole, getByTestId, getByText } = render( - - - - - , - ); - - expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true'); - - fireEvent.click(getByText('two')); - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('two')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp' }); - - expect(getByTestId('one')).toHaveVirtualFocus(); - }); - - it("moves focus to a sibling's child", () => { - const { getByRole, getByTestId, getByText } = render( - - - - - - , - ); - - expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true'); - - fireEvent.click(getByText('three')); - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('three')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp' }); - - expect(getByTestId('two')).toHaveVirtualFocus(); - }); - }); - - describe('home key interaction', () => { - it('moves focus to the first node in the tree', () => { - const { getByRole, getByTestId, getByText } = render( - - - - - - , - ); - - fireEvent.click(getByText('four')); - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('four')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 'Home' }); - - expect(getByTestId('one')).toHaveVirtualFocus(); - }); - }); - - describe('end key interaction', () => { - it('moves focus to the last node in the tree without expanded items', () => { - const { getByRole, getByTestId } = render( - - - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('one')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 'End' }); - - expect(getByTestId('four')).toHaveVirtualFocus(); - }); - - it('moves focus to the last node in the tree with expanded items', () => { - const { getByRole, getByTestId } = render( - - - - - - - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('one')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 'End' }); - - expect(getByTestId('six')).toHaveVirtualFocus(); - }); - }); - - describe('type-ahead functionality', () => { - it('moves focus to the next node with a name that starts with the typed character', () => { - const { getByRole, getByTestId } = render( - - - two} data-testid="two" /> - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('one')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 't' }); - - expect(getByTestId('two')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 'f' }); - - expect(getByTestId('four')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 'o' }); - - expect(getByTestId('one')).toHaveVirtualFocus(); - }); - - it('moves focus to the next node with the same starting character', () => { - const { getByRole, getByTestId } = render( - - - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('one')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 't' }); - - expect(getByTestId('two')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 't' }); - - expect(getByTestId('three')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 't' }); - - expect(getByTestId('two')).toHaveVirtualFocus(); - }); - - it('should not move focus when pressing a modifier key + letter', () => { - const { getByRole, getByTestId } = render( - - - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('apple')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 'v', ctrlKey: true }); - - expect(getByTestId('apple')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 'v', metaKey: true }); - - expect(getByTestId('apple')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 'v', shiftKey: true }); - - expect(getByTestId('apple')).toHaveVirtualFocus(); - }); - - it('should not throw when an item is removed', () => { - function TestComponent() { - const [hide, setState] = React.useState(false); - return ( - - - - {!hide && } - - - - - ); - } - - const { getByRole, getByText, getByTestId } = render(); - fireEvent.click(getByText('Hide')); - expect(getByTestId('navTo')).not.toHaveVirtualFocus(); - - expect(() => { - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('keyDown')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 'a' }); - }).not.to.throw(); - - expect(getByTestId('navTo')).toHaveVirtualFocus(); - }); - }); - - describe('asterisk key interaction', () => { - it('expands all siblings that are at the same level as the current node', () => { - const toggleSpy = spy(); - const { getByRole, getByTestId } = render( - - - - - - - - - - - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false'); - expect(getByTestId('three')).to.have.attribute('aria-expanded', 'false'); - expect(getByTestId('five')).to.have.attribute('aria-expanded', 'false'); - - fireEvent.keyDown(getByRole('tree'), { key: '*' }); - - expect(toggleSpy.args[0][1]).to.have.length(3); - - expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true'); - expect(getByTestId('three')).to.have.attribute('aria-expanded', 'true'); - expect(getByTestId('five')).to.have.attribute('aria-expanded', 'true'); - expect(getByTestId('six')).to.have.attribute('aria-expanded', 'false'); - expect(getByTestId('eight')).not.to.have.attribute('aria-expanded'); - }); - }); - }); - - describe('Expansion', () => { - describe('enter key interaction', () => { - it('expands a node with children', () => { - const { getByRole, getByTestId } = render( - - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false'); - - fireEvent.keyDown(getByRole('tree'), { key: 'Enter' }); - - expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true'); - }); - - it('collapses a node with children', () => { - const { getByRole, getByTestId, getByText } = render( - - - - - , - ); - - fireEvent.click(getByText('one')); - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true'); - - fireEvent.keyDown(getByRole('tree'), { key: 'Enter' }); - - expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false'); - }); - }); - }); - - describe('Single Selection', () => { - describe('keyboard', () => { - it('should select a node when space is pressed', () => { - const { getByRole, getByTestId } = render( - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('one')).not.to.have.attribute('aria-selected'); - - fireEvent.keyDown(getByRole('tree'), { key: ' ' }); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - }); - - it('should not deselect a node when space is pressed on a selected node', () => { - const { getByRole, getByTestId } = render( - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('one')).toHaveVirtualFocus(); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - - fireEvent.keyDown(getByRole('tree'), { key: ' ' }); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - }); - - it('should not select a node when space is pressed and disableSelection', () => { - const { getByRole, getByTestId } = render( - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - fireEvent.keyDown(getByRole('tree'), { key: ' ' }); - - expect(getByTestId('one')).not.to.have.attribute('aria-selected'); - }); - }); - - describe('mouse', () => { - it('should select a node when click', () => { - const { getByText, getByTestId } = render( - - - , - ); - - expect(getByTestId('one')).not.to.have.attribute('aria-selected'); - fireEvent.click(getByText('one')); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - }); - - it('should not deselect a node when clicking a selected node', () => { - const { getByText, getByTestId } = render( - - - , - ); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - fireEvent.click(getByText('one')); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - }); - - it('should not select a node when click and disableSelection', () => { - const { getByText, getByTestId } = render( - - - , - ); - - fireEvent.click(getByText('one')); - expect(getByTestId('one')).not.to.have.attribute('aria-selected'); - }); - }); - }); - - describe('Multi Selection', () => { - describe('deselection', () => { - describe('mouse behavior when multiple nodes are selected', () => { - specify('clicking a selected node holding ctrl should deselect the node', () => { - const { getByText, getByTestId } = render( - - - - , - ); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - fireEvent.click(getByText('one'), { ctrlKey: true }); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - }); - - specify('clicking a selected node holding meta should deselect the node', () => { - const { getByText, getByTestId } = render( - - - - , - ); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - fireEvent.click(getByText('one'), { metaKey: true }); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - }); - }); - - describe('mouse behavior when one node is selected', () => { - it('clicking a selected node shout not deselect the node', () => { - const { getByText, getByTestId } = render( - - - - , - ); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'false'); - fireEvent.click(getByText('one')); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'false'); - }); - }); - - it('should deselect the node when pressing space on a selected node', () => { - const { getByTestId, getByRole } = render( - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('one')).toHaveVirtualFocus(); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - fireEvent.keyDown(getByRole('tree'), { key: ' ' }); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'false'); - }); - }); - - describe('range selection', () => { - specify('keyboard arrow', () => { - const { getByRole, getByTestId, getByText, queryAllByRole } = render( - - - - - - - , - ); - - fireEvent.click(getByText('three')); - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('three')).to.have.attribute('aria-selected', 'true'); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown', shiftKey: true }); - - expect(getByTestId('four')).toHaveVirtualFocus(); - expect(queryAllByRole('treeitem', { selected: true })).to.have.length(2); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown', shiftKey: true }); - - expect(getByTestId('three')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('four')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('five')).to.have.attribute('aria-selected', 'true'); - expect(queryAllByRole('treeitem', { selected: true })).to.have.length(3); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp', shiftKey: true }); - - expect(getByTestId('four')).toHaveVirtualFocus(); - expect(queryAllByRole('treeitem', { selected: true })).to.have.length(2); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp', shiftKey: true }); - - expect(queryAllByRole('treeitem', { selected: true })).to.have.length(1); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp', shiftKey: true }); - - expect(queryAllByRole('treeitem', { selected: true })).to.have.length(2); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp', shiftKey: true }); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('three')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('four')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('five')).to.have.attribute('aria-selected', 'false'); - expect(queryAllByRole('treeitem', { selected: true })).to.have.length(3); - }); - - specify('keyboard arrow does not select when selectionDisabled', () => { - const { getByRole, getByTestId, queryAllByRole } = render( - - - - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown', shiftKey: true }); - - expect(getByTestId('two')).toHaveVirtualFocus(); - expect(queryAllByRole('treeitem', { selected: true })).to.have.length(0); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp', shiftKey: true }); - - expect(queryAllByRole('treeitem', { selected: true })).to.have.length(0); - }); - - specify('keyboard arrow merge', () => { - const { getByRole, getByTestId, getByText, queryAllByRole } = render( - - - - - - - - , - ); - - fireEvent.click(getByText('three')); - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('three')).to.have.attribute('aria-selected', 'true'); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp', shiftKey: true }); - fireEvent.click(getByText('six'), { ctrlKey: true }); - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp', shiftKey: true }); - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp', shiftKey: true }); - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp', shiftKey: true }); - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp', shiftKey: true }); - - expect(queryAllByRole('treeitem', { selected: true })).to.have.length(5); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown', shiftKey: true }); - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown', shiftKey: true }); - - expect(queryAllByRole('treeitem', { selected: true })).to.have.length(3); - }); - - specify('keyboard space', () => { - const { getByRole, getByTestId, getByText } = render( - - - - - - - - - - - - - , - ); - const tree = getByRole('tree'); - - fireEvent.click(getByText('five')); - act(() => { - tree.focus(); - }); - for (let i = 0; i < 5; i += 1) { - fireEvent.keyDown(tree, { key: 'ArrowDown' }); - } - fireEvent.keyDown(tree, { key: ' ', shiftKey: true }); - - expect(getByTestId('five')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('six')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('seven')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('eight')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('nine')).to.have.attribute('aria-selected', 'true'); - for (let i = 0; i < 9; i += 1) { - fireEvent.keyDown(tree, { key: 'ArrowUp' }); - } - fireEvent.keyDown(tree, { key: ' ', shiftKey: true }); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('three')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('four')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('five')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('six')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('seven')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('eight')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('nine')).to.have.attribute('aria-selected', 'false'); - }); - - specify('keyboard home and end', () => { - const { getByRole, getByTestId } = render( - - - - - - - - - - - - - , - ); - - act(() => { - getByTestId('five').focus(); - }); - - fireEvent.keyDown(getByRole('tree'), { - key: 'End', - shiftKey: true, - ctrlKey: true, - }); - - expect(getByTestId('five')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('six')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('seven')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('eight')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('nine')).to.have.attribute('aria-selected', 'true'); - - fireEvent.keyDown(getByRole('tree'), { - key: 'Home', - shiftKey: true, - ctrlKey: true, - }); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('three')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('four')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('five')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('six')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('seven')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('eight')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('nine')).to.have.attribute('aria-selected', 'false'); - }); - - specify('keyboard home and end do not select when selectionDisabled', () => { - const { getByRole, getByText, queryAllByRole } = render( - - - - - - - - - - - - - , - ); - - fireEvent.click(getByText('five')); - fireEvent.click(getByText('five')); - // Focus node five - act(() => { - getByRole('tree').focus(); - }); - fireEvent.keyDown(getByRole('tree'), { - key: 'End', - shiftKey: true, - ctrlKey: true, - }); - - expect(queryAllByRole('treeitem', { selected: true })).to.have.length(0); - - fireEvent.keyDown(getByRole('tree'), { - key: 'Home', - shiftKey: true, - ctrlKey: true, - }); - - expect(queryAllByRole('treeitem', { selected: true })).to.have.length(0); - }); - - specify('mouse', () => { - const { getByTestId, getByText } = render( - - - - - - - - - - - - - , - ); - - fireEvent.click(getByText('five')); - fireEvent.click(getByText('nine'), { shiftKey: true }); - expect(getByTestId('five')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('six')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('seven')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('eight')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('nine')).to.have.attribute('aria-selected', 'true'); - fireEvent.click(getByText('one'), { shiftKey: true }); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('three')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('four')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('five')).to.have.attribute('aria-selected', 'true'); - }); - - it('mouse behavior after deselection', () => { - const { getByTestId, getByText } = render( - - - - - - - , - ); - - fireEvent.click(getByText('one')); - fireEvent.click(getByText('two'), { ctrlKey: true }); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - fireEvent.click(getByText('two'), { ctrlKey: true }); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'false'); - - fireEvent.click(getByText('five'), { shiftKey: true }); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('three')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('four')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('five')).to.have.attribute('aria-selected', 'true'); - fireEvent.click(getByText('one'), { shiftKey: true }); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('three')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('four')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('five')).to.have.attribute('aria-selected', 'false'); - }); - - specify('mouse does not range select when selectionDisabled', () => { - const { getByText, queryAllByRole } = render( - - - - - - - - - - - - - , - ); - - fireEvent.click(getByText('five')); - fireEvent.click(getByText('nine'), { shiftKey: true }); - expect(queryAllByRole('treeitem', { selected: true })).to.have.length(0); - }); - }); - - describe('multi selection', () => { - specify('keyboard', () => { - const { getByRole, getByTestId } = render( - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'false'); - - fireEvent.keyDown(getByRole('tree'), { key: ' ' }); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'false'); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' }); - fireEvent.keyDown(getByRole('tree'), { key: ' ' }); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - }); - - specify('keyboard holding ctrl', () => { - const { getByRole, getByTestId } = render( - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'false'); - - fireEvent.keyDown(getByRole('tree'), { key: ' ' }); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'false'); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' }); - fireEvent.keyDown(getByRole('tree'), { key: ' ', ctrlKey: true }); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - }); - - specify('mouse', () => { - const { getByText, getByTestId } = render( - - - - , - ); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'false'); - - fireEvent.click(getByText('one')); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'false'); - - fireEvent.click(getByText('two')); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - }); - - specify('mouse using ctrl', () => { - const { getByTestId, getByText } = render( - - - - , - ); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'false'); - fireEvent.click(getByText('one')); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'false'); - fireEvent.click(getByText('two'), { ctrlKey: true }); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - }); - - specify('mouse using meta', () => { - const { getByTestId, getByText } = render( - - - - , - ); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'false'); - fireEvent.click(getByText('one')); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'false'); - fireEvent.click(getByText('two'), { metaKey: true }); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - }); - }); - - specify('ctrl + a selects all', () => { - const { getByRole, queryAllByRole } = render( - - - - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - fireEvent.keyDown(getByRole('tree'), { key: 'a', ctrlKey: true }); - - expect(queryAllByRole('treeitem', { selected: true })).to.have.length(5); - }); - - specify('ctrl + a does not select all when disableSelection', () => { - const { getByRole, queryAllByRole } = render( - - - - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - fireEvent.keyDown(getByRole('tree'), { key: 'a', ctrlKey: true }); - - expect(queryAllByRole('treeitem', { selected: true })).to.have.length(0); - }); - }); - }); - - describe('prop: disabled', () => { - describe('selection', () => { - describe('mouse', () => { - it('should prevent selection by mouse', () => { - const { getByText, getByTestId } = render( - - - , - ); - - fireEvent.click(getByText('one')); - expect(getByTestId('one')).not.to.have.attribute('aria-selected'); - }); - - it('should prevent node triggering start of range selection', () => { - const { getByText, getByTestId } = render( - - - - - - , - ); - - fireEvent.click(getByText('one')); - fireEvent.click(getByText('four'), { shiftKey: true }); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('three')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('four')).to.have.attribute('aria-selected', 'false'); - }); - - it('should prevent node being selected as part of range selection', () => { - const { getByText, getByTestId } = render( - - - - - - , - ); - - fireEvent.click(getByText('one')); - fireEvent.click(getByText('four'), { shiftKey: true }); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('three')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('four')).to.have.attribute('aria-selected', 'true'); - }); - - it('should prevent node triggering end of range selection', () => { - const { getByText, getByTestId } = render( - - - - - - , - ); - - fireEvent.click(getByText('one')); - fireEvent.click(getByText('four'), { shiftKey: true }); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('three')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('four')).to.have.attribute('aria-selected', 'false'); - }); - }); - - describe('keyboard', () => { - describe('`disabledItemsFocusable={true}`', () => { - it('should prevent selection by keyboard', () => { - const { getByRole, getByTestId } = render( - - - , - ); - - act(() => { - getByTestId('one').focus(); - }); - expect(getByTestId('one')).toHaveVirtualFocus(); - fireEvent.keyDown(getByRole('tree'), { key: ' ' }); - expect(getByTestId('one')).not.to.have.attribute('aria-selected'); - }); - - it('should not prevent next node being range selected by keyboard', () => { - const { getByRole, getByTestId } = render( - - - - - - , - ); - - act(() => { - getByTestId('one').focus(); - }); - expect(getByTestId('one')).toHaveVirtualFocus(); - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown', shiftKey: true }); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).toHaveVirtualFocus(); - }); - - it('should prevent range selection by keyboard + arrow down', () => { - const { getByRole, getByTestId } = render( - - - - , - ); - - act(() => { - getByTestId('one').focus(); - }); - expect(getByTestId('one')).toHaveVirtualFocus(); - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown', shiftKey: true }); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('two')).toHaveVirtualFocus(); - }); - }); - - describe('`disabledItemsFocusable=false`', () => { - it('should select the next non disabled node by keyboard + arrow down', () => { - const { getByRole, getByTestId } = render( - - - - - , - ); - - act(() => { - getByTestId('one').focus(); - }); - expect(getByTestId('one')).toHaveVirtualFocus(); - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown', shiftKey: true }); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('three')).toHaveVirtualFocus(); - expect(getByTestId('one')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('three')).to.have.attribute('aria-selected', 'true'); - }); - }); - - it('should prevent range selection by keyboard + space', () => { - const { getByRole, getByTestId, getByText } = render( - - - - - - - , - ); - const tree = getByRole('tree'); - - fireEvent.click(getByText('one')); - act(() => { - tree.focus(); - }); - for (let i = 0; i < 5; i += 1) { - fireEvent.keyDown(tree, { key: 'ArrowDown' }); - } - fireEvent.keyDown(tree, { key: ' ', shiftKey: true }); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('three')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('four')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('five')).to.have.attribute('aria-selected', 'true'); - }); - - it('should prevent selection by ctrl + a', () => { - const { getByRole, queryAllByRole } = render( - - - - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - - fireEvent.keyDown(getByRole('tree'), { key: 'a', ctrlKey: true }); - expect(queryAllByRole('treeitem', { selected: true })).to.have.length(4); - }); - - it('should prevent selection by keyboard end', () => { - const { getByRole, getByTestId } = render( - - - - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - expect(getByTestId('one')).toHaveVirtualFocus(); - fireEvent.keyDown(getByRole('tree'), { - key: 'End', - shiftKey: true, - ctrlKey: true, - }); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('three')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('four')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('five')).to.have.attribute('aria-selected', 'true'); - }); - - it('should prevent selection by keyboard home', () => { - const { getByRole, getByTestId } = render( - - - - - - - , - ); - - act(() => { - getByTestId('five').focus(); - }); - expect(getByTestId('five')).toHaveVirtualFocus(); - fireEvent.keyDown(getByRole('tree'), { - key: 'Home', - shiftKey: true, - ctrlKey: true, - }); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('three')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('four')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('five')).to.have.attribute('aria-selected', 'true'); - }); - }); - }); - - describe('focus', () => { - describe('`disabledItemsFocusable={true}`', () => { - it('should prevent focus by mouse', () => { - const focusSpy = spy(); - const { getByText } = render( - - - - , - ); - - fireEvent.click(getByText('two')); - expect(focusSpy.callCount).to.equal(0); - }); - - it('should not prevent programmatic focus', () => { - const { getByTestId } = render( - - - - , - ); - - act(() => { - getByTestId('one').focus(); - }); - expect(getByTestId('one')).toHaveVirtualFocus(); - }); - - it('should not prevent focus by type-ahead', () => { - const { getByRole, getByTestId } = render( - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - expect(getByTestId('one')).toHaveVirtualFocus(); - fireEvent.keyDown(getByRole('tree'), { key: 't' }); - expect(getByTestId('two')).toHaveVirtualFocus(); - }); - - it('should not prevent focus by arrow keys', () => { - const { getByRole, getByTestId } = render( - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('one')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' }); - expect(getByTestId('two')).toHaveVirtualFocus(); - }); - - it('should be focused on tree focus', () => { - const { getByRole, getByTestId } = render( - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('one')).toHaveVirtualFocus(); - }); - }); - - describe('`disabledItemsFocusable=false`', () => { - it('should prevent focus by mouse', () => { - const focusSpy = spy(); - const { getByText } = render( - - - - , - ); - - fireEvent.click(getByText('two')); - expect(focusSpy.callCount).to.equal(0); - }); - - it('should prevent programmatic focus', () => { - const { getByTestId } = render( - - - - , - ); - - act(() => { - getByTestId('one').focus(); - }); - expect(getByTestId('one')).not.toHaveVirtualFocus(); - }); - - it('should prevent focus by type-ahead', () => { - const { getByRole, getByTestId } = render( - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - expect(getByTestId('one')).toHaveVirtualFocus(); - fireEvent.keyDown(getByRole('tree'), { key: 't' }); - expect(getByTestId('one')).toHaveVirtualFocus(); - }); - - it('should be skipped on navigation with arrow keys', () => { - const { getByRole, getByTestId } = render( - - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('one')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' }); - expect(getByTestId('three')).toHaveVirtualFocus(); - }); - - it('should not be focused on tree focus', () => { - const { getByRole, getByTestId } = render( - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('two')).toHaveVirtualFocus(); - }); - }); - }); - - describe('expansion', () => { - describe('`disabledItemsFocusable={true}`', () => { - it('should prevent expansion on enter', () => { - const { getByRole, getByTestId } = render( - - - - - - , - ); - - act(() => { - getByTestId('two').focus(); - }); - expect(getByTestId('two')).toHaveVirtualFocus(); - expect(getByTestId('two')).to.have.attribute('aria-expanded', 'false'); - fireEvent.keyDown(getByRole('tree'), { key: 'Enter' }); - expect(getByTestId('two')).to.have.attribute('aria-expanded', 'false'); - }); - - it('should prevent expansion on right arrow', () => { - const { getByRole, getByTestId } = render( - - - - - - , - ); - - act(() => { - getByTestId('two').focus(); - }); - expect(getByTestId('two')).toHaveVirtualFocus(); - expect(getByTestId('two')).to.have.attribute('aria-expanded', 'false'); - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowRight' }); - expect(getByTestId('two')).to.have.attribute('aria-expanded', 'false'); - }); - - it('should prevent collapse on left arrow', () => { - const { getByRole, getByTestId } = render( - - - - - - , - ); - - act(() => { - getByTestId('two').focus(); - }); - expect(getByTestId('two')).toHaveVirtualFocus(); - expect(getByTestId('two')).to.have.attribute('aria-expanded', 'true'); - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowLeft' }); - expect(getByTestId('two')).to.have.attribute('aria-expanded', 'true'); - }); - }); - - it('should prevent expansion on click', () => { - const { getByText, getByTestId } = render( - - - - - , - ); - - fireEvent.click(getByText('one')); - expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false'); - }); - }); - - describe('event bindings', () => { - it('should not prevent onClick being fired', () => { - const handleClick = spy(); - - const { getByText } = render( - - - , - ); - - fireEvent.click(getByText('test')); - - expect(handleClick.callCount).to.equal(1); - }); - }); - - it('should disable child items when parent item is disabled', () => { - const { getByTestId } = render( - - - - - - , - ); - - expect(getByTestId('one')).to.have.attribute('aria-disabled', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-disabled', 'true'); - expect(getByTestId('three')).to.have.attribute('aria-disabled', 'true'); - }); - }); - - describe('content customisation', () => { - it('should allow a custom ContentComponent', () => { - const mockContent = React.forwardRef((props, ref) => ( -
MOCK CONTENT COMPONENT
- )); - const { container } = render(); - expect(container.textContent).to.equal('MOCK CONTENT COMPONENT'); - }); - - it('should allow props to be passed to a custom ContentComponent', () => { - const mockContent = React.forwardRef((props, ref) =>
{props.customProp}
); - const { container } = render( - , - ); - expect(container.textContent).to.equal('ABCDEF'); - }); - }); - - it('should be able to type in an child input', () => { - const { getByRole } = render( - - - - -
- } - data-testid="two" - /> - -
, - ); - const input = getByRole('textbox'); - const keydownEvent = createEvent.keyDown(input, { - key: 'a', - }); - keydownEvent.preventDefault = spy(); - fireEvent(input, keydownEvent); - expect(keydownEvent.preventDefault.callCount).to.equal(0); - }); - - it('should not focus steal', () => { - let setActiveItemMounted; - // a TreeItem whose mounted state we can control with `setActiveItemMounted` - function ControlledTreeItem(props) { - const [mounted, setMounted] = React.useState(true); - setActiveItemMounted = setMounted; - - if (!mounted) { - return null; - } - return ; - } - const { getByText, getByTestId, getByRole } = render( - - - - - - - , - ); - - fireEvent.click(getByText('two')); - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('two')).toHaveVirtualFocus(); - - act(() => { - getByRole('button').focus(); - }); - - expect(getByRole('button')).toHaveFocus(); - - act(() => { - setActiveItemMounted(false); - }); - act(() => { - setActiveItemMounted(true); - }); - - expect(getByRole('button')).toHaveFocus(); - }); -}); diff --git a/packages/mui-lab/src/TreeItem/TreeItem.tsx b/packages/mui-lab/src/TreeItem/TreeItem.tsx new file mode 100644 index 00000000000000..bc1473aa839931 --- /dev/null +++ b/packages/mui-lab/src/TreeItem/TreeItem.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { TreeItem as XTreeItem, TreeItemProps } from '@mui/x-tree-view/TreeItem'; + +let warnedOnce = false; + +const warn = () => { + if (!warnedOnce) { + console.warn( + [ + 'MUI: The TreeItem component was moved from `@mui/lab` to `@mui/x-tree-view`.', + 'The component will no longer be exported from `@mui/lab` in the first release of October 2023.', + '', + "You should use `import { TreeItem } from '@mui/x-tree-view'`", + "or `import { TreeItem } from '@mui/x-tree-view/TreeItem'`", + '', + 'More information about this migration on our blog: https://mui.com/blog/lab-tree-view-to-mui-x/.', + ].join('\n'), + ); + + warnedOnce = true; + } +}; + +/** + * @ignore - do not document. + */ +const TreeItem = React.forwardRef(function DeprecatedTreeItem( + props: TreeItemProps, + ref: React.Ref, +) { + warn(); + + return ; +}); + +TreeItem.propTypes /* remove-proptypes */ = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * The content of the component. + */ + children: PropTypes.node, +} as any; + +export default TreeItem; diff --git a/packages/mui-lab/src/TreeItem/TreeItemContent.d.ts b/packages/mui-lab/src/TreeItem/TreeItemContent.d.ts deleted file mode 100644 index 97bf66d5634472..00000000000000 --- a/packages/mui-lab/src/TreeItem/TreeItemContent.d.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { InternalStandardProps as StandardProps } from '@mui/material'; -import * as React from 'react'; - -export interface TreeItemContentProps extends StandardProps> { - /** - * className applied to the root element. - */ - className?: string; - /** - * Override or extend the styles applied to the component. - */ - classes: { - /** Styles applied to the root element. */ - root: string; - /** State class applied to the content element when expanded. */ - expanded: string; - /** State class applied to the content element when selected. */ - selected: string; - /** State class applied to the content element when focused. */ - focused: string; - /** State class applied to the element when disabled. */ - disabled: string; - /** Styles applied to the tree node icon and collapse/expand icon. */ - iconContainer: string; - /** Styles applied to the label element. */ - label: string; - }; - /** - * The tree node label. - */ - label?: React.ReactNode; - /** - * The id of the node. - */ - nodeId: string; - /** - * The icon to display next to the tree node's label. - */ - icon?: React.ReactNode; - /** - * The icon to display next to the tree node's label. Either an expansion or collapse icon. - */ - expansionIcon?: React.ReactNode; - /** - * The icon to display next to the tree node's label. Either a parent or end icon. - */ - displayIcon?: React.ReactNode; -} - -export type TreeItemContentClassKey = keyof NonNullable; diff --git a/packages/mui-lab/src/TreeItem/TreeItemContent.js b/packages/mui-lab/src/TreeItem/TreeItemContent.js deleted file mode 100644 index 12b558021908d2..00000000000000 --- a/packages/mui-lab/src/TreeItem/TreeItemContent.js +++ /dev/null @@ -1,116 +0,0 @@ -import * as React from 'react'; -import clsx from 'clsx'; -import PropTypes from 'prop-types'; -import useTreeItem from './useTreeItem'; - -/** - * @ignore - internal component. - */ -const TreeItemContent = React.forwardRef(function TreeItemContent(props, ref) { - const { - classes, - className, - displayIcon, - expansionIcon, - icon: iconProp, - label, - nodeId, - onClick, - onMouseDown, - ...other - } = props; - - const { - disabled, - expanded, - selected, - focused, - handleExpansion, - handleSelection, - preventSelection, - } = useTreeItem(nodeId); - - const icon = iconProp || expansionIcon || displayIcon; - - const handleMouseDown = (event) => { - preventSelection(event); - - if (onMouseDown) { - onMouseDown(event); - } - }; - - const handleClick = (event) => { - handleExpansion(event); - handleSelection(event); - - if (onClick) { - onClick(event); - } - }; - - return ( - /* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions -- Key event is handled by the TreeView */ -
-
{icon}
-
{label}
-
- ); -}); - -TreeItemContent.propTypes = { - // ----------------------------- Warning -------------------------------- - // | These PropTypes are generated from the TypeScript type definitions | - // | To update them edit the d.ts file and run "yarn proptypes" | - // ---------------------------------------------------------------------- - /** - * Override or extend the styles applied to the component. - * See [CSS API](#css) below for more details. - */ - classes: PropTypes.object, - /** - * className applied to the root element. - */ - className: PropTypes.string, - /** - * The icon to display next to the tree node's label. Either a parent or end icon. - */ - displayIcon: PropTypes.node, - /** - * The icon to display next to the tree node's label. Either an expansion or collapse icon. - */ - expansionIcon: PropTypes.node, - /** - * The icon to display next to the tree node's label. - */ - icon: PropTypes.node, - /** - * The tree node label. - */ - label: PropTypes.node, - /** - * The id of the node. - */ - nodeId: PropTypes.string.isRequired, - /** - * @ignore - */ - onClick: PropTypes.func, - /** - * @ignore - */ - onMouseDown: PropTypes.func, -}; - -export default TreeItemContent; diff --git a/packages/mui-lab/src/TreeItem/index.d.ts b/packages/mui-lab/src/TreeItem/index.d.ts deleted file mode 100644 index 99e17f1eb99aec..00000000000000 --- a/packages/mui-lab/src/TreeItem/index.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { default } from './TreeItem'; -export * from './TreeItem'; -export * from './TreeItemContent'; -export { default as useTreeItem } from './useTreeItem'; - -export { default as treeItemClasses } from './treeItemClasses'; -export * from './treeItemClasses'; diff --git a/packages/mui-lab/src/TreeItem/index.js b/packages/mui-lab/src/TreeItem/index.js deleted file mode 100644 index 6ed8fe956296d6..00000000000000 --- a/packages/mui-lab/src/TreeItem/index.js +++ /dev/null @@ -1,5 +0,0 @@ -export { default } from './TreeItem'; -export { default as useTreeItem } from './useTreeItem'; - -export { default as treeItemClasses } from './treeItemClasses'; -export * from './treeItemClasses'; diff --git a/packages/mui-lab/src/TreeItem/index.ts b/packages/mui-lab/src/TreeItem/index.ts new file mode 100644 index 00000000000000..806aec49566307 --- /dev/null +++ b/packages/mui-lab/src/TreeItem/index.ts @@ -0,0 +1,8 @@ +export { default } from './TreeItem'; +export { getTreeItemUtilityClass, treeItemClasses, useTreeItem } from '@mui/x-tree-view/TreeItem'; +export type { + TreeItemProps, + TreeItemClasses, + TreeItemClassKey, + TreeItemContentProps, +} from '@mui/x-tree-view/TreeItem'; diff --git a/packages/mui-lab/src/TreeItem/treeItemClasses.ts b/packages/mui-lab/src/TreeItem/treeItemClasses.ts deleted file mode 100644 index 368a888384af45..00000000000000 --- a/packages/mui-lab/src/TreeItem/treeItemClasses.ts +++ /dev/null @@ -1,43 +0,0 @@ -import generateUtilityClass from '@mui/material/generateUtilityClass'; -import generateUtilityClasses from '@mui/material/generateUtilityClasses'; - -export interface TreeItemClasses { - /** Styles applied to the root element. */ - root: string; - /** Styles applied to the transition component. */ - group: string; - /** Styles applied to the content element. */ - content: string; - /** State class applied to the content element when expanded. */ - expanded: string; - /** State class applied to the content element when selected. */ - selected: string; - /** State class applied to the content element when focused. */ - focused: string; - /** State class applied to the element when disabled. */ - disabled: string; - /** Styles applied to the tree node icon. */ - iconContainer: string; - /** Styles applied to the label element. */ - label: string; -} - -export type TreeItemClassKey = keyof TreeItemClasses; - -export function getTreeItemUtilityClass(slot: string): string { - return generateUtilityClass('MuiTreeItem', slot); -} - -const treeItemClasses: TreeItemClasses = generateUtilityClasses('MuiTreeItem', [ - 'root', - 'group', - 'content', - 'expanded', - 'selected', - 'focused', - 'disabled', - 'iconContainer', - 'label', -]); - -export default treeItemClasses; diff --git a/packages/mui-lab/src/TreeItem/useTreeItem.d.ts b/packages/mui-lab/src/TreeItem/useTreeItem.d.ts deleted file mode 100644 index abec248fa67b02..00000000000000 --- a/packages/mui-lab/src/TreeItem/useTreeItem.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as React from 'react'; - -export default function useTreeItem(nodeId: string): { - disabled: boolean; - expanded: boolean; - selected: boolean; - focused: boolean; - handleExpansion: (event: React.SyntheticEvent) => void; - handleSelection: (event: React.SyntheticEvent) => void; - preventSelection: (event: React.SyntheticEvent) => void; -}; diff --git a/packages/mui-lab/src/TreeItem/useTreeItem.js b/packages/mui-lab/src/TreeItem/useTreeItem.js deleted file mode 100644 index 6fcbcd07febf4e..00000000000000 --- a/packages/mui-lab/src/TreeItem/useTreeItem.js +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from 'react'; -import TreeViewContext from '../TreeView/TreeViewContext'; - -export default function useTreeItem(nodeId) { - const { - focus, - isExpanded, - isExpandable, - isFocused, - isDisabled, - isSelected, - multiSelect, - selectNode, - selectRange, - toggleExpansion, - } = React.useContext(TreeViewContext); - - const expandable = isExpandable ? isExpandable(nodeId) : false; - const expanded = isExpanded ? isExpanded(nodeId) : false; - const focused = isFocused ? isFocused(nodeId) : false; - const disabled = isDisabled ? isDisabled(nodeId) : false; - const selected = isSelected ? isSelected(nodeId) : false; - - const handleExpansion = (event) => { - if (!disabled) { - if (!focused) { - focus(event, nodeId); - } - - const multiple = multiSelect && (event.shiftKey || event.ctrlKey || event.metaKey); - - // If already expanded and trying to toggle selection don't close - if (expandable && !(multiple && isExpanded(nodeId))) { - toggleExpansion(event, nodeId); - } - } - }; - - const handleSelection = (event) => { - if (!disabled) { - if (!focused) { - focus(event, nodeId); - } - - const multiple = multiSelect && (event.shiftKey || event.ctrlKey || event.metaKey); - - if (multiple) { - if (event.shiftKey) { - selectRange(event, { end: nodeId }); - } else { - selectNode(event, nodeId, true); - } - } else { - selectNode(event, nodeId); - } - } - }; - - const preventSelection = (event) => { - if (event.shiftKey || event.ctrlKey || event.metaKey || disabled) { - // Prevent text selection - event.preventDefault(); - } - }; - - return { - disabled, - expanded, - selected, - focused, - handleExpansion, - handleSelection, - preventSelection, - }; -} diff --git a/packages/mui-lab/src/TreeView/TreeView.d.ts b/packages/mui-lab/src/TreeView/TreeView.d.ts deleted file mode 100644 index 2c7c39d8586b72..00000000000000 --- a/packages/mui-lab/src/TreeView/TreeView.d.ts +++ /dev/null @@ -1,144 +0,0 @@ -import * as React from 'react'; -import { InternalStandardProps as StandardProps } from '@mui/material'; -import { Theme } from '@mui/material/styles'; -import { SxProps } from '@mui/system'; -import { TreeViewClasses } from './treeViewClasses'; - -export interface TreeViewPropsBase extends StandardProps> { - /** - * The content of the component. - */ - children?: React.ReactNode; - /** - * Override or extend the styles applied to the component. - */ - classes?: Partial; - /** - * The default icon used to collapse the node. - */ - defaultCollapseIcon?: React.ReactNode; - /** - * The default icon displayed next to a end node. This is applied to all - * tree nodes and can be overridden by the TreeItem `icon` prop. - */ - defaultEndIcon?: React.ReactNode; - /** - * Expanded node ids. (Uncontrolled) - * @default [] - */ - defaultExpanded?: string[]; - /** - * The default icon used to expand the node. - */ - defaultExpandIcon?: React.ReactNode; - /** - * The default icon displayed next to a parent node. This is applied to all - * parent nodes and can be overridden by the TreeItem `icon` prop. - */ - defaultParentIcon?: React.ReactNode; - /** - * If `true`, will allow focus on disabled items. - * @default false - */ - disabledItemsFocusable?: boolean; - /** - * If `true` selection is disabled. - * @default false - */ - disableSelection?: boolean; - /** - * Expanded node ids. (Controlled) - */ - expanded?: string[]; - /** - * This prop is used to help implement the accessibility logic. - * If you don't provide this prop. It falls back to a randomly generated id. - */ - id?: string; - /** - * Callback fired when tree items are focused. - * - * @param {React.SyntheticEvent} event The event source of the callback **Warning**: This is a generic event not a focus event. - * @param {string} value of the focused node. - */ - onNodeFocus?: (event: React.SyntheticEvent, nodeId: string) => void; - /** - * Callback fired when tree items are expanded/collapsed. - * - * @param {React.SyntheticEvent} event The event source of the callback. - * @param {array} nodeIds The ids of the expanded nodes. - */ - onNodeToggle?: (event: React.SyntheticEvent, nodeIds: string[]) => void; - /** - * The system prop that allows defining system overrides as well as additional CSS styles. - */ - sx?: SxProps; -} - -export interface MultiSelectTreeViewProps extends TreeViewPropsBase { - /** - * Selected node ids. (Uncontrolled) - * When `multiSelect` is true this takes an array of strings; when false (default) a string. - * @default [] - */ - defaultSelected?: string[]; - /** - * Selected node ids. (Controlled) - * When `multiSelect` is true this takes an array of strings; when false (default) a string. - */ - selected?: string[]; - /** - * If true `ctrl` and `shift` will trigger multiselect. - * @default false - */ - multiSelect?: true; - /** - * Callback fired when tree items are selected/unselected. - * - * @param {React.SyntheticEvent} event The event source of the callback - * @param {string[] | string} nodeIds Ids of the selected nodes. When `multiSelect` is true - * this is an array of strings; when false (default) a string. - */ - onNodeSelect?: (event: React.SyntheticEvent, nodeIds: string[]) => void; -} - -export interface SingleSelectTreeViewProps extends TreeViewPropsBase { - /** - * Selected node ids. (Uncontrolled) - * When `multiSelect` is true this takes an array of strings; when false (default) a string. - * @default [] - */ - defaultSelected?: string; - /** - * Selected node ids. (Controlled) - * When `multiSelect` is true this takes an array of strings; when false (default) a string. - */ - selected?: string; - /** - * If true `ctrl` and `shift` will trigger multiselect. - * @default false - */ - multiSelect?: false; - /** - * Callback fired when tree items are selected/unselected. - * - * @param {React.SyntheticEvent} event The event source of the callback - * @param {string[] | string} nodeIds Ids of the selected nodes. When `multiSelect` is true - * this is an array of strings; when false (default) a string. - */ - onNodeSelect?: (event: React.SyntheticEvent, nodeIds: string) => void; -} - -export type TreeViewProps = SingleSelectTreeViewProps | MultiSelectTreeViewProps; - -/** - * - * Demos: - * - * - [Tree View](https://mui.com/material-ui/react-tree-view/) - * - * API: - * - * - [TreeView API](https://mui.com/material-ui/api/tree-view/) - */ -export default function TreeView(props: TreeViewProps): JSX.Element; diff --git a/packages/mui-lab/src/TreeView/TreeView.js b/packages/mui-lab/src/TreeView/TreeView.js deleted file mode 100644 index 37017852292bac..00000000000000 --- a/packages/mui-lab/src/TreeView/TreeView.js +++ /dev/null @@ -1,958 +0,0 @@ -import * as React from 'react'; -import clsx from 'clsx'; -import PropTypes from 'prop-types'; -import { styled, useTheme, useThemeProps } from '@mui/material/styles'; -import { unstable_composeClasses as composeClasses } from '@mui/base'; -import { - useControlled, - useForkRef, - ownerDocument, - unstable_useId as useId, -} from '@mui/material/utils'; -import TreeViewContext from './TreeViewContext'; -import { DescendantProvider } from './descendants'; -import { getTreeViewUtilityClass } from './treeViewClasses'; - -const useUtilityClasses = (ownerState) => { - const { classes } = ownerState; - - const slots = { - root: ['root'], - }; - - return composeClasses(slots, getTreeViewUtilityClass, classes); -}; - -const TreeViewRoot = styled('ul', { - name: 'MuiTreeView', - slot: 'Root', - overridesResolver: (props, styles) => styles.root, -})({ - padding: 0, - margin: 0, - listStyle: 'none', - outline: 0, -}); - -function isPrintableCharacter(string) { - return string && string.length === 1 && string.match(/\S/); -} - -function findNextFirstChar(firstChars, startIndex, char) { - for (let i = startIndex; i < firstChars.length; i += 1) { - if (char === firstChars[i]) { - return i; - } - } - return -1; -} - -function noopSelection() { - return false; -} - -const defaultDefaultExpanded = []; -const defaultDefaultSelected = []; - -const TreeView = React.forwardRef(function TreeView(inProps, ref) { - const props = useThemeProps({ props: inProps, name: 'MuiTreeView' }); - const { - children, - className, - defaultCollapseIcon, - defaultEndIcon, - defaultExpanded = defaultDefaultExpanded, - defaultExpandIcon, - defaultParentIcon, - defaultSelected = defaultDefaultSelected, - disabledItemsFocusable = false, - disableSelection = false, - expanded: expandedProp, - id: idProp, - multiSelect = false, - onBlur, - onFocus, - onKeyDown, - onNodeFocus, - onNodeSelect, - onNodeToggle, - selected: selectedProp, - ...other - } = props; - - const theme = useTheme(); - const isRtl = theme.direction === 'rtl'; - - const ownerState = { - ...props, - defaultExpanded, - defaultSelected, - disabledItemsFocusable, - disableSelection, - multiSelect, - }; - - const classes = useUtilityClasses(ownerState); - - const treeId = useId(idProp); - - const treeRef = React.useRef(null); - const handleRef = useForkRef(treeRef, ref); - - const [focusedNodeId, setFocusedNodeId] = React.useState(null); - - const nodeMap = React.useRef({}); - - const firstCharMap = React.useRef({}); - - const [expanded, setExpandedState] = useControlled({ - controlled: expandedProp, - default: defaultExpanded, - name: 'TreeView', - state: 'expanded', - }); - - const [selected, setSelectedState] = useControlled({ - controlled: selectedProp, - default: defaultSelected, - name: 'TreeView', - state: 'selected', - }); - - /* - * Status Helpers - */ - const isExpanded = React.useCallback( - (id) => (Array.isArray(expanded) ? expanded.indexOf(id) !== -1 : false), - [expanded], - ); - - const isExpandable = React.useCallback( - (id) => nodeMap.current[id] && nodeMap.current[id].expandable, - [], - ); - - const isSelected = React.useCallback( - (id) => (Array.isArray(selected) ? selected.indexOf(id) !== -1 : selected === id), - [selected], - ); - - const isDisabled = React.useCallback((id) => { - let node = nodeMap.current[id]; - - // This can be called before the node has been added to the node map. - if (!node) { - return false; - } - - if (node.disabled) { - return true; - } - - while (node.parentId != null) { - node = nodeMap.current[node.parentId]; - if (node.disabled) { - return true; - } - } - - return false; - }, []); - - const isFocused = (id) => focusedNodeId === id; - - /* - * Child Helpers - */ - - // Using Object.keys -> .map to mimic Object.values we should replace with Object.values() once we stop IE11 support. - const getChildrenIds = (id) => - Object.keys(nodeMap.current) - .map((key) => { - return nodeMap.current[key]; - }) - .filter((node) => node.parentId === id) - .sort((a, b) => a.index - b.index) - .map((child) => child.id); - - const getNavigableChildrenIds = (id) => { - let childrenIds = getChildrenIds(id); - - if (!disabledItemsFocusable) { - childrenIds = childrenIds.filter((node) => !isDisabled(node)); - } - return childrenIds; - }; - - /* - * Node Helpers - */ - - const getNextNode = (id) => { - // If expanded get first child - if (isExpanded(id) && getNavigableChildrenIds(id).length > 0) { - return getNavigableChildrenIds(id)[0]; - } - - let node = nodeMap.current[id]; - while (node != null) { - // Try to get next sibling - const siblings = getNavigableChildrenIds(node.parentId); - const nextSibling = siblings[siblings.indexOf(node.id) + 1]; - - if (nextSibling) { - return nextSibling; - } - - // If the sibling does not exist, go up a level to the parent and try again. - node = nodeMap.current[node.parentId]; - } - - return null; - }; - - const getPreviousNode = (id) => { - const node = nodeMap.current[id]; - const siblings = getNavigableChildrenIds(node.parentId); - const nodeIndex = siblings.indexOf(id); - - if (nodeIndex === 0) { - return node.parentId; - } - - let currentNode = siblings[nodeIndex - 1]; - while (isExpanded(currentNode) && getNavigableChildrenIds(currentNode).length > 0) { - currentNode = getNavigableChildrenIds(currentNode).pop(); - } - - return currentNode; - }; - - const getLastNode = () => { - let lastNode = getNavigableChildrenIds(null).pop(); - - while (isExpanded(lastNode)) { - lastNode = getNavigableChildrenIds(lastNode).pop(); - } - return lastNode; - }; - const getFirstNode = () => getNavigableChildrenIds(null)[0]; - const getParent = (id) => nodeMap.current[id].parentId; - - /** - * This is used to determine the start and end of a selection range so - * we can get the nodes between the two border nodes. - * - * It finds the nodes' common ancestor using - * a naive implementation of a lowest common ancestor algorithm - * (https://en.wikipedia.org/wiki/Lowest_common_ancestor). - * Then compares the ancestor's 2 children that are ancestors of nodeA and NodeB - * so we can compare their indexes to work out which node comes first in a depth first search. - * (https://en.wikipedia.org/wiki/Depth-first_search) - * - * Another way to put it is which node is shallower in a trémaux tree - * https://en.wikipedia.org/wiki/Tr%C3%A9maux_tree - */ - const findOrderInTremauxTree = (nodeAId, nodeBId) => { - if (nodeAId === nodeBId) { - return [nodeAId, nodeBId]; - } - - const nodeA = nodeMap.current[nodeAId]; - const nodeB = nodeMap.current[nodeBId]; - - if (nodeA.parentId === nodeB.id || nodeB.parentId === nodeA.id) { - return nodeB.parentId === nodeA.id ? [nodeA.id, nodeB.id] : [nodeB.id, nodeA.id]; - } - - const aFamily = [nodeA.id]; - const bFamily = [nodeB.id]; - - let aAncestor = nodeA.parentId; - let bAncestor = nodeB.parentId; - - let aAncestorIsCommon = bFamily.indexOf(aAncestor) !== -1; - let bAncestorIsCommon = aFamily.indexOf(bAncestor) !== -1; - - let continueA = true; - let continueB = true; - - while (!bAncestorIsCommon && !aAncestorIsCommon) { - if (continueA) { - aFamily.push(aAncestor); - aAncestorIsCommon = bFamily.indexOf(aAncestor) !== -1; - continueA = aAncestor !== null; - if (!aAncestorIsCommon && continueA) { - aAncestor = nodeMap.current[aAncestor].parentId; - } - } - - if (continueB && !aAncestorIsCommon) { - bFamily.push(bAncestor); - bAncestorIsCommon = aFamily.indexOf(bAncestor) !== -1; - continueB = bAncestor !== null; - if (!bAncestorIsCommon && continueB) { - bAncestor = nodeMap.current[bAncestor].parentId; - } - } - } - - const commonAncestor = aAncestorIsCommon ? aAncestor : bAncestor; - const ancestorFamily = getChildrenIds(commonAncestor); - - const aSide = aFamily[aFamily.indexOf(commonAncestor) - 1]; - const bSide = bFamily[bFamily.indexOf(commonAncestor) - 1]; - - return ancestorFamily.indexOf(aSide) < ancestorFamily.indexOf(bSide) - ? [nodeAId, nodeBId] - : [nodeBId, nodeAId]; - }; - - const getNodesInRange = (nodeA, nodeB) => { - const [first, last] = findOrderInTremauxTree(nodeA, nodeB); - const nodes = [first]; - - let current = first; - - while (current !== last) { - current = getNextNode(current); - nodes.push(current); - } - - return nodes; - }; - - /* - * Focus Helpers - */ - - const focus = (event, id) => { - if (id) { - setFocusedNodeId(id); - - if (onNodeFocus) { - onNodeFocus(event, id); - } - } - }; - - const focusNextNode = (event, id) => focus(event, getNextNode(id)); - const focusPreviousNode = (event, id) => focus(event, getPreviousNode(id)); - const focusFirstNode = (event) => focus(event, getFirstNode()); - const focusLastNode = (event) => focus(event, getLastNode()); - - const focusByFirstCharacter = (event, id, char) => { - let start; - let index; - const lowercaseChar = char.toLowerCase(); - - const firstCharIds = []; - const firstChars = []; - // This really only works since the ids are strings - Object.keys(firstCharMap.current).forEach((nodeId) => { - const firstChar = firstCharMap.current[nodeId]; - const map = nodeMap.current[nodeId]; - const visible = map.parentId ? isExpanded(map.parentId) : true; - const shouldBeSkipped = disabledItemsFocusable ? false : isDisabled(nodeId); - - if (visible && !shouldBeSkipped) { - firstCharIds.push(nodeId); - firstChars.push(firstChar); - } - }); - - // Get start index for search based on position of currentItem - start = firstCharIds.indexOf(id) + 1; - if (start >= firstCharIds.length) { - start = 0; - } - - // Check remaining slots in the menu - index = findNextFirstChar(firstChars, start, lowercaseChar); - - // If not found in remaining slots, check from beginning - if (index === -1) { - index = findNextFirstChar(firstChars, 0, lowercaseChar); - } - - // If match was found... - if (index > -1) { - focus(event, firstCharIds[index]); - } - }; - - /* - * Expansion Helpers - */ - - const toggleExpansion = (event, value = focusedNodeId) => { - let newExpanded; - - if (expanded.indexOf(value) !== -1) { - newExpanded = expanded.filter((id) => id !== value); - } else { - newExpanded = [value].concat(expanded); - } - - if (onNodeToggle) { - onNodeToggle(event, newExpanded); - } - - setExpandedState(newExpanded); - }; - - const expandAllSiblings = (event, id) => { - const map = nodeMap.current[id]; - const siblings = getChildrenIds(map.parentId); - - const diff = siblings.filter((child) => isExpandable(child) && !isExpanded(child)); - - const newExpanded = expanded.concat(diff); - - if (diff.length > 0) { - setExpandedState(newExpanded); - - if (onNodeToggle) { - onNodeToggle(event, newExpanded); - } - } - }; - - /* - * Selection Helpers - */ - - const lastSelectedNode = React.useRef(null); - const lastSelectionWasRange = React.useRef(false); - const currentRangeSelection = React.useRef([]); - - const handleRangeArrowSelect = (event, nodes) => { - let base = selected.slice(); - const { start, next, current } = nodes; - - if (!next || !current) { - return; - } - - if (currentRangeSelection.current.indexOf(current) === -1) { - currentRangeSelection.current = []; - } - - if (lastSelectionWasRange.current) { - if (currentRangeSelection.current.indexOf(next) !== -1) { - base = base.filter((id) => id === start || id !== current); - currentRangeSelection.current = currentRangeSelection.current.filter( - (id) => id === start || id !== current, - ); - } else { - base.push(next); - currentRangeSelection.current.push(next); - } - } else { - base.push(next); - currentRangeSelection.current.push(current, next); - } - - if (onNodeSelect) { - onNodeSelect(event, base); - } - - setSelectedState(base); - }; - - const handleRangeSelect = (event, nodes) => { - let base = selected.slice(); - const { start, end } = nodes; - // If last selection was a range selection ignore nodes that were selected. - if (lastSelectionWasRange.current) { - base = base.filter((id) => currentRangeSelection.current.indexOf(id) === -1); - } - - let range = getNodesInRange(start, end); - range = range.filter((node) => !isDisabled(node)); - currentRangeSelection.current = range; - let newSelected = base.concat(range); - newSelected = newSelected.filter((id, i) => newSelected.indexOf(id) === i); - - if (onNodeSelect) { - onNodeSelect(event, newSelected); - } - - setSelectedState(newSelected); - }; - - const handleMultipleSelect = (event, value) => { - let newSelected; - if (selected.indexOf(value) !== -1) { - newSelected = selected.filter((id) => id !== value); - } else { - newSelected = [value].concat(selected); - } - - if (onNodeSelect) { - onNodeSelect(event, newSelected); - } - - setSelectedState(newSelected); - }; - - const handleSingleSelect = (event, value) => { - const newSelected = multiSelect ? [value] : value; - - if (onNodeSelect) { - onNodeSelect(event, newSelected); - } - - setSelectedState(newSelected); - }; - - const selectNode = (event, id, multiple = false) => { - if (id) { - if (multiple) { - handleMultipleSelect(event, id); - } else { - handleSingleSelect(event, id); - } - lastSelectedNode.current = id; - lastSelectionWasRange.current = false; - currentRangeSelection.current = []; - - return true; - } - return false; - }; - - const selectRange = (event, nodes, stacked = false) => { - const { start = lastSelectedNode.current, end, current } = nodes; - if (stacked) { - handleRangeArrowSelect(event, { start, next: end, current }); - } else if (start != null && end != null) { - handleRangeSelect(event, { start, end }); - } - lastSelectionWasRange.current = true; - }; - - const rangeSelectToFirst = (event, id) => { - if (!lastSelectedNode.current) { - lastSelectedNode.current = id; - } - - const start = lastSelectionWasRange.current ? lastSelectedNode.current : id; - - selectRange(event, { - start, - end: getFirstNode(), - }); - }; - - const rangeSelectToLast = (event, id) => { - if (!lastSelectedNode.current) { - lastSelectedNode.current = id; - } - - const start = lastSelectionWasRange.current ? lastSelectedNode.current : id; - - selectRange(event, { - start, - end: getLastNode(), - }); - }; - - const selectNextNode = (event, id) => { - if (!isDisabled(getNextNode(id))) { - selectRange( - event, - { - end: getNextNode(id), - current: id, - }, - true, - ); - } - }; - - const selectPreviousNode = (event, id) => { - if (!isDisabled(getPreviousNode(id))) { - selectRange( - event, - { - end: getPreviousNode(id), - current: id, - }, - true, - ); - } - }; - - const selectAllNodes = (event) => { - selectRange(event, { start: getFirstNode(), end: getLastNode() }); - }; - - /* - * Mapping Helpers - */ - const registerNode = React.useCallback((node) => { - const { id, index, parentId, expandable, idAttribute, disabled } = node; - - nodeMap.current[id] = { id, index, parentId, expandable, idAttribute, disabled }; - }, []); - - const unregisterNode = React.useCallback((id) => { - const newMap = { ...nodeMap.current }; - delete newMap[id]; - nodeMap.current = newMap; - - setFocusedNodeId((oldFocusedNodeId) => { - if ( - oldFocusedNodeId === id && - treeRef.current === ownerDocument(treeRef.current).activeElement - ) { - return getChildrenIds(null)[0]; - } - return oldFocusedNodeId; - }); - }, []); - - const mapFirstChar = React.useCallback((id, firstChar) => { - firstCharMap.current[id] = firstChar; - }, []); - - const unMapFirstChar = React.useCallback((id) => { - const newMap = { ...firstCharMap.current }; - delete newMap[id]; - firstCharMap.current = newMap; - }, []); - - /** - * Event handlers and Navigation - */ - - const handleNextArrow = (event) => { - if (isExpandable(focusedNodeId)) { - if (isExpanded(focusedNodeId)) { - focusNextNode(event, focusedNodeId); - } else if (!isDisabled(focusedNodeId)) { - toggleExpansion(event); - } - } - return true; - }; - - const handlePreviousArrow = (event) => { - if (isExpanded(focusedNodeId) && !isDisabled(focusedNodeId)) { - toggleExpansion(event, focusedNodeId); - return true; - } - - const parent = getParent(focusedNodeId); - if (parent) { - focus(event, parent); - return true; - } - return false; - }; - - const handleKeyDown = (event) => { - let flag = false; - const key = event.key; - - // If the tree is empty there will be no focused node - if (event.altKey || event.currentTarget !== event.target || !focusedNodeId) { - return; - } - - const ctrlPressed = event.ctrlKey || event.metaKey; - switch (key) { - case ' ': - if (!disableSelection && !isDisabled(focusedNodeId)) { - if (multiSelect && event.shiftKey) { - selectRange(event, { end: focusedNodeId }); - flag = true; - } else if (multiSelect) { - flag = selectNode(event, focusedNodeId, true); - } else { - flag = selectNode(event, focusedNodeId); - } - } - event.stopPropagation(); - break; - case 'Enter': - if (!isDisabled(focusedNodeId)) { - if (isExpandable(focusedNodeId)) { - toggleExpansion(event); - flag = true; - } else if (multiSelect) { - flag = selectNode(event, focusedNodeId, true); - } else { - flag = selectNode(event, focusedNodeId); - } - } - event.stopPropagation(); - break; - case 'ArrowDown': - if (multiSelect && event.shiftKey && !disableSelection) { - selectNextNode(event, focusedNodeId); - } - focusNextNode(event, focusedNodeId); - flag = true; - break; - case 'ArrowUp': - if (multiSelect && event.shiftKey && !disableSelection) { - selectPreviousNode(event, focusedNodeId); - } - focusPreviousNode(event, focusedNodeId); - flag = true; - break; - case 'ArrowRight': - if (isRtl) { - flag = handlePreviousArrow(event); - } else { - flag = handleNextArrow(event); - } - break; - case 'ArrowLeft': - if (isRtl) { - flag = handleNextArrow(event); - } else { - flag = handlePreviousArrow(event); - } - break; - case 'Home': - if ( - multiSelect && - ctrlPressed && - event.shiftKey && - !disableSelection && - !isDisabled(focusedNodeId) - ) { - rangeSelectToFirst(event, focusedNodeId); - } - focusFirstNode(event); - flag = true; - break; - case 'End': - if ( - multiSelect && - ctrlPressed && - event.shiftKey && - !disableSelection && - !isDisabled(focusedNodeId) - ) { - rangeSelectToLast(event, focusedNodeId); - } - focusLastNode(event); - flag = true; - break; - default: - if (key === '*') { - expandAllSiblings(event, focusedNodeId); - flag = true; - } else if (multiSelect && ctrlPressed && key.toLowerCase() === 'a' && !disableSelection) { - selectAllNodes(event); - flag = true; - } else if (!ctrlPressed && !event.shiftKey && isPrintableCharacter(key)) { - focusByFirstCharacter(event, focusedNodeId, key); - flag = true; - } - } - - if (flag) { - event.preventDefault(); - event.stopPropagation(); - } - - if (onKeyDown) { - onKeyDown(event); - } - }; - - const handleFocus = (event) => { - // if the event bubbled (which is React specific) we don't want to steal focus - if (event.target === event.currentTarget) { - const firstSelected = Array.isArray(selected) ? selected[0] : selected; - focus(event, firstSelected || getNavigableChildrenIds(null)[0]); - } - - if (onFocus) { - onFocus(event); - } - }; - - const handleBlur = (event) => { - setFocusedNodeId(null); - - if (onBlur) { - onBlur(event); - } - }; - - const activeDescendant = nodeMap.current[focusedNodeId] - ? nodeMap.current[focusedNodeId].idAttribute - : null; - - return ( - - - - {children} - - - - ); -}); - -TreeView.propTypes /* remove-proptypes */ = { - // ----------------------------- Warning -------------------------------- - // | These PropTypes are generated from the TypeScript type definitions | - // | To update them edit the d.ts file and run "yarn proptypes" | - // ---------------------------------------------------------------------- - /** - * The content of the component. - */ - children: PropTypes.node, - /** - * Override or extend the styles applied to the component. - */ - classes: PropTypes.object, - /** - * @ignore - */ - className: PropTypes.string, - /** - * The default icon used to collapse the node. - */ - defaultCollapseIcon: PropTypes.node, - /** - * The default icon displayed next to a end node. This is applied to all - * tree nodes and can be overridden by the TreeItem `icon` prop. - */ - defaultEndIcon: PropTypes.node, - /** - * Expanded node ids. (Uncontrolled) - * @default [] - */ - defaultExpanded: PropTypes.arrayOf(PropTypes.string), - /** - * The default icon used to expand the node. - */ - defaultExpandIcon: PropTypes.node, - /** - * The default icon displayed next to a parent node. This is applied to all - * parent nodes and can be overridden by the TreeItem `icon` prop. - */ - defaultParentIcon: PropTypes.node, - /** - * Selected node ids. (Uncontrolled) - * When `multiSelect` is true this takes an array of strings; when false (default) a string. - * @default [] - */ - defaultSelected: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.string), PropTypes.string]), - /** - * If `true`, will allow focus on disabled items. - * @default false - */ - disabledItemsFocusable: PropTypes.bool, - /** - * If `true` selection is disabled. - * @default false - */ - disableSelection: PropTypes.bool, - /** - * Expanded node ids. (Controlled) - */ - expanded: PropTypes.arrayOf(PropTypes.string), - /** - * This prop is used to help implement the accessibility logic. - * If you don't provide this prop. It falls back to a randomly generated id. - */ - id: PropTypes.string, - /** - * If true `ctrl` and `shift` will trigger multiselect. - * @default false - */ - multiSelect: PropTypes.bool, - /** - * @ignore - */ - onBlur: PropTypes.func, - /** - * @ignore - */ - onFocus: PropTypes.func, - /** - * @ignore - */ - onKeyDown: PropTypes.func, - /** - * Callback fired when tree items are focused. - * - * @param {React.SyntheticEvent} event The event source of the callback **Warning**: This is a generic event not a focus event. - * @param {string} value of the focused node. - */ - onNodeFocus: PropTypes.func, - /** - * Callback fired when tree items are selected/unselected. - * - * @param {React.SyntheticEvent} event The event source of the callback - * @param {string[] | string} nodeIds Ids of the selected nodes. When `multiSelect` is true - * this is an array of strings; when false (default) a string. - */ - onNodeSelect: PropTypes.func, - /** - * Callback fired when tree items are expanded/collapsed. - * - * @param {React.SyntheticEvent} event The event source of the callback. - * @param {array} nodeIds The ids of the expanded nodes. - */ - onNodeToggle: PropTypes.func, - /** - * Selected node ids. (Controlled) - * When `multiSelect` is true this takes an array of strings; when false (default) a string. - */ - selected: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.string), PropTypes.string]), - /** - * The system prop that allows defining system overrides as well as additional CSS styles. - */ - sx: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), - PropTypes.func, - PropTypes.object, - ]), -}; - -export default TreeView; diff --git a/packages/mui-lab/src/TreeView/TreeView.test.js b/packages/mui-lab/src/TreeView/TreeView.test.js deleted file mode 100644 index 7916cd7fad0735..00000000000000 --- a/packages/mui-lab/src/TreeView/TreeView.test.js +++ /dev/null @@ -1,463 +0,0 @@ -import * as React from 'react'; -import { expect } from 'chai'; -import { spy } from 'sinon'; -import { - act, - createRenderer, - ErrorBoundary, - fireEvent, - screen, - describeConformance, -} from 'test/utils'; -import Portal from '@mui/material/Portal'; -import TreeView, { treeViewClasses as classes } from '@mui/lab/TreeView'; -import TreeItem from '@mui/lab/TreeItem'; - -describe('', () => { - const { render } = createRenderer(); - - describeConformance(, () => ({ - classes, - inheritComponent: 'ul', - render, - refInstanceof: window.HTMLUListElement, - muiName: 'MuiTreeView', - skip: ['componentProp', 'componentsProp', 'themeVariants'], - })); - - describe('warnings', () => { - it('should warn when switching from controlled to uncontrolled of the expanded prop', () => { - const { setProps } = render( - - - , - ); - - expect(() => { - setProps({ expanded: undefined }); - }).toErrorDev( - 'MUI: A component is changing the controlled expanded state of TreeView to be uncontrolled.', - ); - }); - - it('should warn when switching from controlled to uncontrolled of the selected prop', () => { - const { setProps } = render( - - - , - ); - - expect(() => { - setProps({ selected: undefined }); - }).toErrorDev( - 'MUI: A component is changing the controlled selected state of TreeView to be uncontrolled.', - ); - }); - - it('should not crash when shift clicking a clean tree', () => { - render( - - - - , - ); - - fireEvent.click(screen.getByText('one'), { shiftKey: true }); - }); - - it('should not crash when selecting multiple items in a deeply nested tree', () => { - render( - - - - - - - - , - ); - fireEvent.click(screen.getByText('Item 1.1.1')); - fireEvent.click(screen.getByText('Item 2'), { shiftKey: true }); - - expect(screen.getByTestId('item-1.1.1')).to.have.attribute('aria-selected', 'true'); - expect(screen.getByTestId('item-2')).to.have.attribute('aria-selected', 'true'); - }); - - it('should not crash on keydown on an empty tree', () => { - render(); - - act(() => { - screen.getByRole('tree').focus(); - }); - - fireEvent.keyDown(screen.getByRole('tree'), { key: ' ' }); - }); - - it('should not crash when unmounting with duplicate ids', () => { - function CustomTreeItem() { - return ; - } - function App() { - const [isVisible, hideTreeView] = React.useReducer(() => false, true); - - return ( - - - {isVisible && ( - - - - - - )} - - ); - } - const errorRef = React.createRef(); - render( - - - , - ); - - expect(() => { - act(() => { - screen.getByRole('button').click(); - }); - }).not.toErrorDev(); - }); - }); - - it('should call onKeyDown when a key is pressed', () => { - const handleKeyDown = spy(); - - const { getByRole } = render( - - - , - ); - act(() => { - getByRole('tree').focus(); - }); - - fireEvent.keyDown(getByRole('tree'), { key: 'Enter' }); - fireEvent.keyDown(getByRole('tree'), { key: 'A' }); - fireEvent.keyDown(getByRole('tree'), { key: ']' }); - - expect(handleKeyDown.callCount).to.equal(3); - }); - - it('should select node when Enter key is pressed ', () => { - const handleKeyDown = spy(); - - const { getByRole, getByTestId } = render( - - - , - ); - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('one')).not.to.have.attribute('aria-selected'); - - fireEvent.keyDown(getByRole('tree'), { key: 'Enter' }); - - expect(getByTestId('one')).to.have.attribute('aria-selected'); - }); - - it('should call onFocus when tree is focused', () => { - const handleFocus = spy(); - const { getByRole } = render( - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - - expect(handleFocus.callCount).to.equal(1); - }); - - it('should call onBlur when tree is blurred', () => { - const handleBlur = spy(); - const { getByRole } = render( - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - act(() => { - getByRole('tree').blur(); - }); - - expect(handleBlur.callCount).to.equal(1); - }); - - it('should be able to be controlled with the expanded prop', () => { - function MyComponent() { - const [expandedState, setExpandedState] = React.useState([]); - const handleNodeToggle = (event, nodes) => { - setExpandedState(nodes); - }; - return ( - - - - - - ); - } - - const { getByRole, getByTestId, getByText } = render(); - - expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false'); - - fireEvent.click(getByText('one')); - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true'); - - fireEvent.click(getByText('one')); - - expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false'); - - fireEvent.keyDown(getByRole('tree'), { key: '*' }); - - expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true'); - }); - - it('should be able to be controlled with the selected prop and singleSelect', () => { - function MyComponent() { - const [selectedState, setSelectedState] = React.useState(null); - const handleNodeSelect = (event, nodes) => { - setSelectedState(nodes); - }; - return ( - - - - - ); - } - - const { getByTestId, getByText } = render(); - - expect(getByTestId('one')).not.to.have.attribute('aria-selected'); - expect(getByTestId('two')).not.to.have.attribute('aria-selected'); - - fireEvent.click(getByText('one')); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).not.to.have.attribute('aria-selected'); - - fireEvent.click(getByText('two')); - - expect(getByTestId('one')).not.to.have.attribute('aria-selected'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - }); - - it('should be able to be controlled with the selected prop and multiSelect', () => { - function MyComponent() { - const [selectedState, setSelectedState] = React.useState([]); - const handleNodeSelect = (event, nodes) => { - setSelectedState(nodes); - }; - return ( - - - - - ); - } - - const { getByTestId, getByText } = render(); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'false'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'false'); - - fireEvent.click(getByText('one')); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'false'); - - fireEvent.click(getByText('two'), { ctrlKey: true }); - - expect(getByTestId('one')).to.have.attribute('aria-selected', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-selected', 'true'); - }); - - it('should not error when component state changes', () => { - function MyComponent() { - const [, setState] = React.useState(1); - - return ( - { - setState(Math.random); - }} - id="tree" - > - - - - - ); - } - - const { getByRole, getByText, getByTestId } = render(); - - fireEvent.click(getByText('one')); - // Clicks would normally focus tree - act(() => { - getByRole('tree').focus(); - }); - - expect(getByTestId('one')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' }); - - expect(getByTestId('two')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp' }); - - expect(getByTestId('one')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' }); - - expect(getByTestId('two')).toHaveVirtualFocus(); - }); - - it('should support conditional rendered tree items', () => { - function TestComponent() { - const [hide, setState] = React.useState(false); - - return ( - - - {!hide && } - - ); - } - - const { getByText, queryByText } = render(); - - expect(getByText('test')).not.to.equal(null); - fireEvent.click(getByText('Hide')); - expect(queryByText('test')).to.equal(null); - }); - - it('should work in a portal', () => { - const { getByRole, getByTestId } = render( - - - - - - - - , - ); - - act(() => { - getByRole('tree').focus(); - }); - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' }); - - expect(getByTestId('two')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' }); - - expect(getByTestId('three')).toHaveVirtualFocus(); - - fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' }); - - expect(getByTestId('four')).toHaveVirtualFocus(); - }); - - describe('onNodeFocus', () => { - it('should be called when node is focused', () => { - const focusSpy = spy(); - const { getByRole } = render( - - - , - ); - - // First node receives focus when tree focused - act(() => { - getByRole('tree').focus(); - }); - - expect(focusSpy.callCount).to.equal(1); - expect(focusSpy.args[0][1]).to.equal('1'); - }); - }); - - describe('onNodeToggle', () => { - it('should be called when a parent node label is clicked', () => { - const handleNodeToggle = spy(); - - const { getByText } = render( - - - - - , - ); - - fireEvent.click(getByText('outer')); - - expect(handleNodeToggle.callCount).to.equal(1); - expect(handleNodeToggle.args[0][1]).to.deep.equal(['1']); - }); - - it('should be called when a parent node icon is clicked', () => { - const handleNodeToggle = spy(); - - const { getByTestId } = render( - - } nodeId="1" label="outer"> - - - , - ); - - fireEvent.click(getByTestId('icon')); - - expect(handleNodeToggle.callCount).to.equal(1); - expect(handleNodeToggle.args[0][1]).to.deep.equal(['1']); - }); - }); - - describe('Accessibility', () => { - it('(TreeView) should have the role `tree`', () => { - const { getByRole } = render(); - - expect(getByRole('tree')).not.to.equal(null); - }); - - it('(TreeView) should have the attribute `aria-multiselectable=false if using single select`', () => { - const { getByRole } = render(); - - expect(getByRole('tree')).to.have.attribute('aria-multiselectable', 'false'); - }); - - it('(TreeView) should have the attribute `aria-multiselectable=true if using multi select`', () => { - const { getByRole } = render(); - - expect(getByRole('tree')).to.have.attribute('aria-multiselectable', 'true'); - }); - }); -}); diff --git a/packages/mui-lab/src/TreeView/TreeView.tsx b/packages/mui-lab/src/TreeView/TreeView.tsx new file mode 100644 index 00000000000000..d4a816b2ee3b27 --- /dev/null +++ b/packages/mui-lab/src/TreeView/TreeView.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { TreeView as XTreeView, TreeViewProps } from '@mui/x-tree-view/TreeView'; + +let warnedOnce = false; + +const warn = () => { + if (!warnedOnce) { + console.warn( + [ + 'MUI: The TreeView component was moved from `@mui/lab` to `@mui/x-tree-view`.', + 'The component will no longer be exported from `@mui/lab` in the first release of October 2023.', + '', + "You should use `import { TreeView } from '@mui/x-tree-view'`", + "or `import { TreeView } from '@mui/x-tree-view/TreeView'`", + '', + 'More information about this migration on our blog: https://mui.com/blog/lab-tree-view-to-mui-x/.', + ].join('\n'), + ); + + warnedOnce = true; + } +}; + +/** + * @ignore - do not document. + */ +const TreeView = React.forwardRef(function DeprecatedTreeView( + props: TreeViewProps, + ref: React.Ref, +) { + warn(); + + return ; +}); + +TreeView.propTypes /* remove-proptypes */ = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * The content of the component. + */ + children: PropTypes.node, +} as any; + +export default TreeView; diff --git a/packages/mui-lab/src/TreeView/TreeViewContext.js b/packages/mui-lab/src/TreeView/TreeViewContext.js deleted file mode 100644 index 2531ce79b4be83..00000000000000 --- a/packages/mui-lab/src/TreeView/TreeViewContext.js +++ /dev/null @@ -1,12 +0,0 @@ -import * as React from 'react'; - -/** - * @ignore - internal component. - */ -const TreeViewContext = React.createContext({}); - -if (process.env.NODE_ENV !== 'production') { - TreeViewContext.displayName = 'TreeViewContext'; -} - -export default TreeViewContext; diff --git a/packages/mui-lab/src/TreeView/descendants.js b/packages/mui-lab/src/TreeView/descendants.js deleted file mode 100644 index 8585eed5bde150..00000000000000 --- a/packages/mui-lab/src/TreeView/descendants.js +++ /dev/null @@ -1,202 +0,0 @@ -import * as React from 'react'; -import PropTypes from 'prop-types'; -import { unstable_useEnhancedEffect as useEnhancedEffect } from '@mui/material/utils'; - -/** Credit: https://github.com/reach/reach-ui/blob/86a046f54d53b6420e392b3fa56dd991d9d4e458/packages/descendants/README.md - * Modified slightly to suit our purposes. - */ - -// To replace with .findIndex() once we stop IE11 support. -function findIndex(array, comp) { - for (let i = 0; i < array.length; i += 1) { - if (comp(array[i])) { - return i; - } - } - - return -1; -} - -function binaryFindElement(array, element) { - let start = 0; - let end = array.length - 1; - - while (start <= end) { - const middle = Math.floor((start + end) / 2); - - if (array[middle].element === element) { - return middle; - } - - // eslint-disable-next-line no-bitwise - if (array[middle].element.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_PRECEDING) { - end = middle - 1; - } else { - start = middle + 1; - } - } - - return start; -} - -const DescendantContext = React.createContext({}); - -if (process.env.NODE_ENV !== 'production') { - DescendantContext.displayName = 'DescendantContext'; -} - -function usePrevious(value) { - const ref = React.useRef(null); - React.useEffect(() => { - ref.current = value; - }, [value]); - return ref.current; -} - -const noop = () => {}; - -/** - * This hook registers our descendant by passing it into an array. We can then - * search that array by to find its index when registering it in the component. - * We use this for focus management, keyboard navigation, and typeahead - * functionality for some components. - * - * The hook accepts the element node - * - * Our main goals with this are: - * 1) maximum composability, - * 2) minimal API friction - * 3) SSR compatibility* - * 4) concurrent safe - * 5) index always up-to-date with the tree despite changes - * 6) works with memoization of any component in the tree (hopefully) - * - * * As for SSR, the good news is that we don't actually need the index on the - * server for most use-cases, as we are only using it to determine the order of - * composed descendants for keyboard navigation. - */ -export function useDescendant(descendant) { - const [, forceUpdate] = React.useState(); - const { - registerDescendant = noop, - unregisterDescendant = noop, - descendants = [], - parentId = null, - } = React.useContext(DescendantContext); - - // This will initially return -1 because we haven't registered the descendant - // on the first render. After we register, this will then return the correct - // index on the following render and we will re-register descendants - // so that everything is up-to-date before the user interacts with a - // collection. - const index = findIndex(descendants, (item) => item.element === descendant.element); - - const previousDescendants = usePrevious(descendants); - - // We also need to re-register descendants any time ANY of the other - // descendants have changed. My brain was melting when I wrote this and it - // feels a little off, but checking in render and using the result in the - // effect's dependency array works well enough. - const someDescendantsHaveChanged = descendants.some((newDescendant, position) => { - return ( - previousDescendants && - previousDescendants[position] && - previousDescendants[position].element !== newDescendant.element - ); - }); - - // Prevent any flashing - useEnhancedEffect(() => { - if (descendant.element) { - registerDescendant({ - ...descendant, - index, - }); - return () => { - unregisterDescendant(descendant.element); - }; - } - forceUpdate({}); - - return undefined; - }, [registerDescendant, unregisterDescendant, index, someDescendantsHaveChanged, descendant]); - - return { parentId, index }; -} - -export function DescendantProvider(props) { - const { children, id } = props; - - const [items, set] = React.useState([]); - - const registerDescendant = React.useCallback(({ element, ...other }) => { - set((oldItems) => { - let newItems; - if (oldItems.length === 0) { - // If there are no items, register at index 0 and bail. - return [ - { - ...other, - element, - index: 0, - }, - ]; - } - - const index = binaryFindElement(oldItems, element); - - if (oldItems[index] && oldItems[index].element === element) { - // If the element is already registered, just use the same array - newItems = oldItems; - } else { - // When registering a descendant, we need to make sure we insert in - // into the array in the same order that it appears in the DOM. So as - // new descendants are added or maybe some are removed, we always know - // that the array is up-to-date and correct. - // - // So here we look at our registered descendants and see if the new - // element we are adding appears earlier than an existing descendant's - // DOM node via `node.compareDocumentPosition`. If it does, we insert - // the new element at this index. Because `registerDescendant` will be - // called in an effect every time the descendants state value changes, - // we should be sure that this index is accurate when descendent - // elements come or go from our component. - - const newItem = { - ...other, - element, - index, - }; - - // If an index is not found we will push the element to the end. - newItems = oldItems.slice(); - newItems.splice(index, 0, newItem); - } - newItems.forEach((item, position) => { - item.index = position; - }); - return newItems; - }); - }, []); - - const unregisterDescendant = React.useCallback((element) => { - set((oldItems) => oldItems.filter((item) => element !== item.element)); - }, []); - - const value = React.useMemo( - () => ({ - descendants: items, - registerDescendant, - unregisterDescendant, - parentId: id, - }), - [items, registerDescendant, unregisterDescendant, id], - ); - - return {children}; -} - -DescendantProvider.propTypes = { - children: PropTypes.node, - id: PropTypes.string, -}; diff --git a/packages/mui-lab/src/TreeView/index.d.ts b/packages/mui-lab/src/TreeView/index.d.ts deleted file mode 100644 index ce9c3ee836af8f..00000000000000 --- a/packages/mui-lab/src/TreeView/index.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { default } from './TreeView'; -export * from './TreeView'; - -export { default as treeViewClasses } from './treeViewClasses'; -export * from './treeViewClasses'; diff --git a/packages/mui-lab/src/TreeView/index.js b/packages/mui-lab/src/TreeView/index.js deleted file mode 100644 index 8d842839e9aec0..00000000000000 --- a/packages/mui-lab/src/TreeView/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export { default } from './TreeView'; - -export { default as treeViewClasses } from './treeViewClasses'; -export * from './treeViewClasses'; diff --git a/packages/mui-lab/src/TreeView/index.ts b/packages/mui-lab/src/TreeView/index.ts new file mode 100644 index 00000000000000..b711d11f019118 --- /dev/null +++ b/packages/mui-lab/src/TreeView/index.ts @@ -0,0 +1,10 @@ +export { default } from './TreeView'; +export { treeViewClasses, getTreeViewUtilityClass } from '@mui/x-tree-view/TreeView'; +export type { + TreeViewClassKey, + TreeViewClasses, + SingleSelectTreeViewProps, + MultiSelectTreeViewProps, + TreeViewPropsBase, + TreeViewProps, +} from '@mui/x-tree-view/TreeView'; diff --git a/packages/mui-lab/src/TreeView/treeViewClasses.ts b/packages/mui-lab/src/TreeView/treeViewClasses.ts deleted file mode 100644 index dfaee08490bd4f..00000000000000 --- a/packages/mui-lab/src/TreeView/treeViewClasses.ts +++ /dev/null @@ -1,17 +0,0 @@ -import generateUtilityClass from '@mui/material/generateUtilityClass'; -import generateUtilityClasses from '@mui/material/generateUtilityClasses'; - -export interface TreeViewClasses { - /** Styles applied to the root element. */ - root: string; -} - -export type TreeViewClassKey = keyof TreeViewClasses; - -export function getTreeViewUtilityClass(slot: string): string { - return generateUtilityClass('MuiTreeView', slot); -} - -const treeViewClasses: TreeViewClasses = generateUtilityClasses('MuiTreeView', ['root']); - -export default treeViewClasses; diff --git a/test/karma.conf.js b/test/karma.conf.js index 9fa46fe93e3547..2f975fb47822bc 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -137,7 +137,7 @@ module.exports = function setKarmaConfig(config) { { test: /\.(js|mjs|jsx)$/, include: - /node_modules(\/|\\)(notistack|@mui(\/|\\)x-data-grid|@mui(\/|\\)x-data-grid-pro|@mui(\/|\\)x-license-pro|@mui(\/|\\)x-data-grid-generator|@mui(\/|\\)x-date-pickers-pro|@mui(\/|\\)x-date-pickers)/, + /node_modules(\/|\\)(notistack|@mui(\/|\\)x-data-grid|@mui(\/|\\)x-data-grid-pro|@mui(\/|\\)x-license-pro|@mui(\/|\\)x-data-grid-generator|@mui(\/|\\)x-date-pickers-pro|@mui(\/|\\)x-date-pickers|@mui(\/|\\)x-tree-view)/, use: { loader: 'babel-loader', options: { diff --git a/test/utils/setupBabel.js b/test/utils/setupBabel.js index 175716b176ac01..d1280280793940 100644 --- a/test/utils/setupBabel.js +++ b/test/utils/setupBabel.js @@ -1,7 +1,7 @@ require('@babel/register')({ extensions: ['.js', '.ts', '.tsx'], - // We have to apply `babel-plugin-module-resolve` to the files in `@mui/x-date-pickers`. - // Otherwise we can't import `@mui/material` from `@mui/x-date-pickers` in `yarn test:unit`. - // TODO: Remove once the lab do not export the pickers - ignore: [/node_modules(\/|\\)(?!.*(@mui(\/|\\)x-date-pickers(\/|\\)([a-zA-Z/\\])+)\.js)/], + // We have to apply `babel-plugin-module-resolve` to the files in `@mui/x-tree-view`. + // Otherwise we can't import `@mui/material` from `@mui/x-tree-view` in `yarn test:unit`. + // TODO: Remove once the lab do not export the tree view + ignore: [/node_modules(\/|\\)(?!.*(@mui(\/|\\)x-tree-view(\/|\\)([a-zA-Z/\\])+)\.js)/], }); diff --git a/yarn.lock b/yarn.lock index 25cd7f54f3cb28..a94781f96b6f16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2199,6 +2199,17 @@ "@babel/runtime" "^7.22.6" "@mui/utils" "^5.13.7" +"@mui/x-tree-view@https://pkg.csb.dev/mui/mui-x/commit/1f23b33d/@mui/x-tree-view": + version "6.0.0-alpha.0" + resolved "https://pkg.csb.dev/mui/mui-x/commit/1f23b33d/@mui/x-tree-view#93cbc61c818ed2d6c40554cdbada969f7dcbaab1" + dependencies: + "@babel/runtime" "^7.22.6" + "@mui/utils" "^5.13.7" + "@types/react-transition-group" "^4.4.6" + clsx "^1.2.1" + prop-types "^15.8.1" + react-transition-group "^4.4.5" + "@next/env@13.4.19": version "13.4.19" resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.19.tgz#46905b4e6f62da825b040343cbc233144e9578d3" @@ -5818,7 +5829,7 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== -clsx@^1.1.0, clsx@^1.1.1: +clsx@^1.1.0, clsx@^1.1.1, clsx@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==