Repository: mui/material-ui (TypeScript) — Feature

Issue:
[Treeview] Node selection support
Thanks very much for the release of the brand new TreeView component! ❤️
I'd like to request a basic feature for the Treeview component to emit an event when any node is selected.

You may have already planned this, but I couldn't find any information for it, hence the request.

<!--
    Thank you very much for contributing to Material-UI by creating an issue! ❤️
    To avoid duplicate issues we ask you to check off the following list.
-->

<!-- Checked checkbox should look like this: [x] -->
- [x] This is not a v0.x issue. <!-- (v0.x is no longer maintained) -->
- [x] I have searched the [issues](https://github.com/mui-org/material-ui/issues) of this repository and believe that this is not a duplicate.

## Expected Behavior 🤔
<!---
    Describe how it should work.
-->
A node selected event fired when a node is selected or focused.

## Current Behavior 😯
<!---
    Explain the difference from current behavior.
-->
No event is fired at node selection, `onNodeToggle` event doesn't fire for leaf nodes,  onClick  event can be attached to each TreeItem as a workaround, however it doesn't support keyboard action. 

## Examples 🌈
<!---
    Provide a link to the Material design specification, other implementations,
    or screenshots of the expected behavior.
-->

- https://demos.telerik.com/kendo-ui/treeview/index
- https://ej2.syncfusion.com/react/demos/#/material/treeview/multiple-selection
- https://www.jqwidgets.com/react/react-tree/index.htm#https://www.jqwidgets.com/react/react-tree/react-tree-defaultfunctionality.htm
- https://js.devexpress.com/Demos/WidgetsGallery/Demo/TreeView/HierarchicalDataStructure/React/Light/
- https://www.primefaces.org/primeng/#/tree
- https://docs.sencha.com/extjs/7.0.0/guides/components/trees.html
- https://www.igniteui.com/tree/overview
- https://www.jqueryscript.net/demo/Tree-Plugin-jQuery-jsTree/
- https://github.com/mar10/fancytree

## Context 🔦
<!---
    What are you trying to accomplish? How has the lack of this feature affected you?
    Providing context helps us come up with a solution that is most useful in the real world.
-->
I'm implementing something where the rest of the UI needs to be updated according to the selection of the node. This is a much needed feature.




Discussion:

Resolving patch:
diff --git a/docs/pages/api/tree-item.md b/docs/pages/api/tree-item.md
--- a/docs/pages/api/tree-item.md
+++ b/docs/pages/api/tree-item.md
@@ -48,6 +48,7 @@ Any other props supplied will be provided to the root element (native element).
 |:-----|:-------------|:------------|
 | <span class="prop-name">root</span> | <span class="prop-name">.MuiTreeItem-root</span> | Styles applied to the root element.
 | <span class="prop-name">expanded</span> | <span class="prop-name">.Mui-expanded</span> | Pseudo-class applied to the root element when expanded.
+| <span class="prop-name">selected</span> | <span class="prop-name">.Mui-selected</span> | Pseudo-class applied to the root element when selected.
 | <span class="prop-name">group</span> | <span class="prop-name">.MuiTreeItem-group</span> | Styles applied to the `role="group"` element.
 | <span class="prop-name">content</span> | <span class="prop-name">.MuiTreeItem-content</span> | Styles applied to the tree node content.
 | <span class="prop-name">iconContainer</span> | <span class="prop-name">.MuiTreeItem-iconContainer</span> | Styles applied to the tree node icon and collapse/expand icon.
diff --git a/docs/pages/api/tree-view.md b/docs/pages/api/tree-view.md
--- a/docs/pages/api/tree-view.md
+++ b/docs/pages/api/tree-view.md
@@ -31,8 +31,13 @@ You can learn more about the difference by [reading this guide](/guides/minimizi
 | <span class="prop-name">defaultExpanded</span> | <span class="prop-type">Array&lt;string&gt;</span> | <span class="prop-default">[]</span> | Expanded node ids. (Uncontrolled) |
 | <span class="prop-name">defaultExpandIcon</span> | <span class="prop-type">node</span> |  | The default icon used to expand the node. |
 | <span class="prop-name">defaultParentIcon</span> | <span class="prop-type">node</span> |  | 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. |
+| <span class="prop-name">defaultSelected</span> | <span class="prop-type">Array&lt;string&gt;<br>&#124;&nbsp;string</span> | <span class="prop-default">[]</span> | Selected node ids. (Uncontrolled) When `multiSelect` is true this takes an array of strings; when false (default) a string. |
+| <span class="prop-name">disableSelection</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true` selection is disabled. |
 | <span class="prop-name">expanded</span> | <span class="prop-type">Array&lt;string&gt;</span> |  | Expanded node ids. (Controlled) |
+| <span class="prop-name">multiSelect</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If true `ctrl` and `shift` will trigger multiselect. |
+| <span class="prop-name">onNodeSelect</span> | <span class="prop-type">func</span> |  | Callback fired when tree items are selected/unselected.<br><br>**Signature:**<br>`function(event: object, value: undefined) => void`<br>*event:* The event source of the callback<br>*value:* of the selected nodes. When `multiSelect` is true this is an array of strings; when false (default) a string. |
 | <span class="prop-name">onNodeToggle</span> | <span class="prop-type">func</span> |  | Callback fired when tree items are expanded/collapsed.<br><br>**Signature:**<br>`function(event: object, nodeIds: array) => void`<br>*event:* The event source of the callback.<br>*nodeIds:* The ids of the expanded nodes. |
+| <span class="prop-name">selected</span> | <span class="prop-type">Array&lt;string&gt;<br>&#124;&nbsp;string</span> |  | Selected node ids. (Controlled) When `multiSelect` is true this takes an array of strings; when false (default) a string. |
 
 The `ref` is forwarded to the root element.
 
diff --git a/docs/src/pages/components/tree-view/ControlledTreeView.js b/docs/src/pages/components/tree-view/ControlledTreeView.js
--- a/docs/src/pages/components/tree-view/ControlledTreeView.js
+++ b/docs/src/pages/components/tree-view/ControlledTreeView.js
@@ -16,9 +16,14 @@ const useStyles = makeStyles({
 export default function ControlledTreeView() {
   const classes = useStyles();
   const [expanded, setExpanded] = React.useState([]);
+  const [selected, setSelected] = React.useState([]);
 
-  const handleChange = (event, nodes) => {
-    setExpanded(nodes);
+  const handleToggle = (event, nodeIds) => {
+    setExpanded(nodeIds);
+  };
+
+  const handleSelect = (event, nodeIds) => {
+    setSelected(nodeIds);
   };
 
   return (
@@ -27,7 +32,9 @@ export default function ControlledTreeView() {
       defaultCollapseIcon={<ExpandMoreIcon />}
       defaultExpandIcon={<ChevronRightIcon />}
       expanded={expanded}
-      onNodeToggle={handleChange}
+      selected={selected}
+      onNodeToggle={handleToggle}
+      onNodeSelect={handleSelect}
     >
       <TreeItem nodeId="1" label="Applications">
         <TreeItem nodeId="2" label="Calendar" />
diff --git a/docs/src/pages/components/tree-view/ControlledTreeView.tsx b/docs/src/pages/components/tree-view/ControlledTreeView.tsx
--- a/docs/src/pages/components/tree-view/ControlledTreeView.tsx
+++ b/docs/src/pages/components/tree-view/ControlledTreeView.tsx
@@ -16,9 +16,14 @@ const useStyles = makeStyles({
 export default function ControlledTreeView() {
   const classes = useStyles();
   const [expanded, setExpanded] = React.useState<string[]>([]);
+  const [selected, setSelected] = React.useState<string[]>([]);
 
-  const handleChange = (event: React.ChangeEvent<{}>, nodes: string[]) => {
-    setExpanded(nodes);
+  const handleToggle = (event: React.ChangeEvent<{}>, nodeIds: string[]) => {
+    setExpanded(nodeIds);
+  };
+
+  const handleSelect = (event: React.ChangeEvent<{}>, nodeIds: string[]) => {
+    setSelected(nodeIds);
   };
 
   return (
@@ -27,7 +32,9 @@ export default function ControlledTreeView() {
       defaultCollapseIcon={<ExpandMoreIcon />}
       defaultExpandIcon={<ChevronRightIcon />}
       expanded={expanded}
-      onNodeToggle={handleChange}
+      selected={selected}
+      onNodeToggle={handleToggle}
+      onNodeSelect={handleSelect}
     >
       <TreeItem nodeId="1" label="Applications">
         <TreeItem nodeId="2" label="Calendar" />
diff --git a/docs/src/pages/components/tree-view/CustomizedTreeView.js b/docs/src/pages/components/tree-view/CustomizedTreeView.js
--- a/docs/src/pages/components/tree-view/CustomizedTreeView.js
+++ b/docs/src/pages/components/tree-view/CustomizedTreeView.js
@@ -9,7 +9,7 @@ import { useSpring, animated } from 'react-spring/web.cjs'; // web.cjs is requir
 
 function MinusSquare(props) {
   return (
-    <SvgIcon fontSize="inherit" {...props}>
+    <SvgIcon fontSize="inherit" style={{ width: 14, height: 14 }} {...props}>
       {/* tslint:disable-next-line: max-line-length */}
       <path d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z" />
     </SvgIcon>
@@ -18,7 +18,7 @@ function MinusSquare(props) {
 
 function PlusSquare(props) {
   return (
-    <SvgIcon fontSize="inherit" {...props}>
+    <SvgIcon fontSize="inherit" style={{ width: 14, height: 14 }} {...props}>
       {/* tslint:disable-next-line: max-line-length */}
       <path d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 12.977h-4.923v4.896q0 .401-.281.682t-.682.281v0q-.375 0-.669-.281t-.294-.682v-4.896h-4.923q-.401 0-.682-.294t-.281-.669v0q0-.401.281-.682t.682-.281h4.923v-4.896q0-.401.294-.682t.669-.281v0q.401 0 .682.281t.281.682v4.896h4.923q.401 0 .682.281t.281.682v0q0 .375-.281.669t-.682.294z" />
     </SvgIcon>
@@ -27,7 +27,7 @@ function PlusSquare(props) {
 
 function CloseSquare(props) {
   return (
-    <SvgIcon className="close" fontSize="inherit" {...props}>
+    <SvgIcon className="close" fontSize="inherit" style={{ width: 14, height: 14 }} {...props}>
       {/* tslint:disable-next-line: max-line-length */}
       <path d="M17.485 17.512q-.281.281-.682.281t-.696-.268l-4.12-4.147-4.12 4.147q-.294.268-.696.268t-.682-.281-.281-.682.294-.669l4.12-4.147-4.12-4.147q-.294-.268-.294-.669t.281-.682.682-.281.696 .268l4.12 4.147 4.12-4.147q.294-.268.696-.268t.682.281 .281.669-.294.682l-4.12 4.147 4.12 4.147q.294.268 .294.669t-.281.682zM22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0z" />
     </SvgIcon>
@@ -61,8 +61,8 @@ const StyledTreeItem = withStyles(theme => ({
     },
   },
   group: {
-    marginLeft: 12,
-    paddingLeft: 12,
+    marginLeft: 7,
+    paddingLeft: 18,
     borderLeft: `1px dashed ${fade(theme.palette.text.primary, 0.4)}`,
   },
 }))(props => <TreeItem {...props} TransitionComponent={TransitionComponent} />);
diff --git a/docs/src/pages/components/tree-view/CustomizedTreeView.tsx b/docs/src/pages/components/tree-view/CustomizedTreeView.tsx
--- a/docs/src/pages/components/tree-view/CustomizedTreeView.tsx
+++ b/docs/src/pages/components/tree-view/CustomizedTreeView.tsx
@@ -9,7 +9,7 @@ import { TransitionProps } from '@material-ui/core/transitions';
 
 function MinusSquare(props: SvgIconProps) {
   return (
-    <SvgIcon fontSize="inherit" {...props}>
+    <SvgIcon fontSize="inherit" style={{ width: 14, height: 14 }} {...props}>
       {/* tslint:disable-next-line: max-line-length */}
       <path d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z" />
     </SvgIcon>
@@ -18,7 +18,7 @@ function MinusSquare(props: SvgIconProps) {
 
 function PlusSquare(props: SvgIconProps) {
   return (
-    <SvgIcon fontSize="inherit" {...props}>
+    <SvgIcon fontSize="inherit" style={{ width: 14, height: 14 }} {...props}>
       {/* tslint:disable-next-line: max-line-length */}
       <path d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 12.977h-4.923v4.896q0 .401-.281.682t-.682.281v0q-.375 0-.669-.281t-.294-.682v-4.896h-4.923q-.401 0-.682-.294t-.281-.669v0q0-.401.281-.682t.682-.281h4.923v-4.896q0-.401.294-.682t.669-.281v0q.401 0 .682.281t.281.682v4.896h4.923q.401 0 .682.281t.281.682v0q0 .375-.281.669t-.682.294z" />
     </SvgIcon>
@@ -27,7 +27,7 @@ function PlusSquare(props: SvgIconProps) {
 
 function CloseSquare(props: SvgIconProps) {
   return (
-    <SvgIcon className="close" fontSize="inherit" {...props}>
+    <SvgIcon className="close" fontSize="inherit" style={{ width: 14, height: 14 }} {...props}>
       {/* tslint:disable-next-line: max-line-length */}
       <path d="M17.485 17.512q-.281.281-.682.281t-.696-.268l-4.12-4.147-4.12 4.147q-.294.268-.696.268t-.682-.281-.281-.682.294-.669l4.12-4.147-4.12-4.147q-.294-.268-.294-.669t.281-.682.682-.281.696 .268l4.12 4.147 4.12-4.147q.294-.268.696-.268t.682.281 .281.669-.294.682l-4.12 4.147 4.12 4.147q.294.268 .294.669t-.281.682zM22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0z" />
     </SvgIcon>
@@ -55,8 +55,8 @@ const StyledTreeItem = withStyles((theme: Theme) =>
       },
     },
     group: {
-      marginLeft: 12,
-      paddingLeft: 12,
+      marginLeft: 7,
+      paddingLeft: 18,
       borderLeft: `1px dashed ${fade(theme.palette.text.primary, 0.4)}`,
     },
   }),
diff --git a/docs/src/pages/components/tree-view/FileSystemNavigator.js b/docs/src/pages/components/tree-view/FileSystemNavigator.js
--- a/docs/src/pages/components/tree-view/FileSystemNavigator.js
+++ b/docs/src/pages/components/tree-view/FileSystemNavigator.js
@@ -7,7 +7,7 @@ import TreeItem from '@material-ui/lab/TreeItem';
 
 const useStyles = makeStyles({
   root: {
-    height: 216,
+    height: 240,
     flexGrow: 1,
     maxWidth: 400,
   },
@@ -28,6 +28,7 @@ export default function FileSystemNavigator() {
         <TreeItem nodeId="4" label="Webstorm" />
       </TreeItem>
       <TreeItem nodeId="5" label="Documents">
+        <TreeItem nodeId="10" label="OSS" />
         <TreeItem nodeId="6" label="Material-UI">
           <TreeItem nodeId="7" label="src">
             <TreeItem nodeId="8" label="index.js" />
diff --git a/docs/src/pages/components/tree-view/FileSystemNavigator.tsx b/docs/src/pages/components/tree-view/FileSystemNavigator.tsx
--- a/docs/src/pages/components/tree-view/FileSystemNavigator.tsx
+++ b/docs/src/pages/components/tree-view/FileSystemNavigator.tsx
@@ -7,7 +7,7 @@ import TreeItem from '@material-ui/lab/TreeItem';
 
 const useStyles = makeStyles({
   root: {
-    height: 216,
+    height: 240,
     flexGrow: 1,
     maxWidth: 400,
   },
@@ -28,6 +28,7 @@ export default function FileSystemNavigator() {
         <TreeItem nodeId="4" label="Webstorm" />
       </TreeItem>
       <TreeItem nodeId="5" label="Documents">
+        <TreeItem nodeId="10" label="OSS" />
         <TreeItem nodeId="6" label="Material-UI">
           <TreeItem nodeId="7" label="src">
             <TreeItem nodeId="8" label="index.js" />
diff --git a/docs/src/pages/components/tree-view/GmailTreeView.js b/docs/src/pages/components/tree-view/GmailTreeView.js
--- a/docs/src/pages/components/tree-view/GmailTreeView.js
+++ b/docs/src/pages/components/tree-view/GmailTreeView.js
@@ -17,10 +17,16 @@ import ArrowRightIcon from '@material-ui/icons/ArrowRight';
 const useTreeItemStyles = makeStyles(theme => ({
   root: {
     color: theme.palette.text.secondary,
-    '&:focus > $content': {
+    '&:hover > $content': {
+      backgroundColor: theme.palette.action.hover,
+    },
+    '&:focus > $content, &$selected > $content': {
       backgroundColor: `var(--tree-view-bg-color, ${theme.palette.grey[400]})`,
       color: 'var(--tree-view-color)',
     },
+    '&:focus > $content $label, &:hover > $content $label, &$selected > $content $label': {
+      backgroundColor: 'transparent',
+    },
   },
   content: {
     color: theme.palette.text.secondary,
@@ -39,6 +45,7 @@ const useTreeItemStyles = makeStyles(theme => ({
     },
   },
   expanded: {},
+  selected: {},
   label: {
     fontWeight: 'inherit',
     color: 'inherit',
@@ -82,6 +89,7 @@ function StyledTreeItem(props) {
         root: classes.root,
         content: classes.content,
         expanded: classes.expanded,
+        selected: classes.selected,
         group: classes.group,
         label: classes.label,
       }}
diff --git a/docs/src/pages/components/tree-view/GmailTreeView.tsx b/docs/src/pages/components/tree-view/GmailTreeView.tsx
--- a/docs/src/pages/components/tree-view/GmailTreeView.tsx
+++ b/docs/src/pages/components/tree-view/GmailTreeView.tsx
@@ -33,10 +33,16 @@ const useTreeItemStyles = makeStyles((theme: Theme) =>
   createStyles({
     root: {
       color: theme.palette.text.secondary,
-      '&:focus > $content': {
+      '&:hover > $content': {
+        backgroundColor: theme.palette.action.hover,
+      },
+      '&:focus > $content, &$selected > $content': {
         backgroundColor: `var(--tree-view-bg-color, ${theme.palette.grey[400]})`,
         color: 'var(--tree-view-color)',
       },
+      '&:focus > $content $label, &:hover > $content $label, &$selected > $content $label': {
+        backgroundColor: 'transparent',
+      },
     },
     content: {
       color: theme.palette.text.secondary,
@@ -55,6 +61,7 @@ const useTreeItemStyles = makeStyles((theme: Theme) =>
       },
     },
     expanded: {},
+    selected: {},
     label: {
       fontWeight: 'inherit',
       color: 'inherit',
@@ -99,6 +106,7 @@ function StyledTreeItem(props: StyledTreeItemProps) {
         root: classes.root,
         content: classes.content,
         expanded: classes.expanded,
+        selected: classes.selected,
         group: classes.group,
         label: classes.label,
       }}
diff --git a/docs/src/pages/components/tree-view/MultiSelectTreeView.js b/docs/src/pages/components/tree-view/MultiSelectTreeView.js
new file mode 100644
--- /dev/null
+++ b/docs/src/pages/components/tree-view/MultiSelectTreeView.js
@@ -0,0 +1,41 @@
+import React from 'react';
+import { makeStyles } from '@material-ui/core/styles';
+import TreeView from '@material-ui/lab/TreeView';
+import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
+import ChevronRightIcon from '@material-ui/icons/ChevronRight';
+import TreeItem from '@material-ui/lab/TreeItem';
+
+const useStyles = makeStyles({
+  root: {
+    height: 216,
+    flexGrow: 1,
+    maxWidth: 400,
+  },
+});
+
+export default function MultiSelectTreeView() {
+  const classes = useStyles();
+
+  return (
+    <TreeView
+      className={classes.root}
+      defaultCollapseIcon={<ExpandMoreIcon />}
+      defaultExpandIcon={<ChevronRightIcon />}
+      multiSelect
+    >
+      <TreeItem nodeId="1" label="Applications">
+        <TreeItem nodeId="2" label="Calendar" />
+        <TreeItem nodeId="3" label="Chrome" />
+        <TreeItem nodeId="4" label="Webstorm" />
+      </TreeItem>
+      <TreeItem nodeId="5" label="Documents">
+        <TreeItem nodeId="6" label="Material-UI">
+          <TreeItem nodeId="7" label="src">
+            <TreeItem nodeId="8" label="index.js" />
+            <TreeItem nodeId="9" label="tree-view.js" />
+          </TreeItem>
+        </TreeItem>
+      </TreeItem>
+    </TreeView>
+  );
+}
diff --git a/docs/src/pages/components/tree-view/MultiSelectTreeView.tsx b/docs/src/pages/components/tree-view/MultiSelectTreeView.tsx
new file mode 100644
--- /dev/null
+++ b/docs/src/pages/components/tree-view/MultiSelectTreeView.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import { makeStyles } from '@material-ui/core/styles';
+import TreeView from '@material-ui/lab/TreeView';
+import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
+import ChevronRightIcon from '@material-ui/icons/ChevronRight';
+import TreeItem from '@material-ui/lab/TreeItem';
+
+const useStyles = makeStyles({
+  root: {
+    height: 216,
+    flexGrow: 1,
+    maxWidth: 400,
+  },
+});
+
+export default function MultiSelectTreeView() {
+  const classes = useStyles();
+
+  return (
+    <TreeView
+      className={classes.root}
+      defaultCollapseIcon={<ExpandMoreIcon />}
+      defaultExpandIcon={<ChevronRightIcon />}
+      multiSelect
+    >
+      <TreeItem nodeId="1" label="Applications">
+        <TreeItem nodeId="2" label="Calendar" />
+        <TreeItem nodeId="3" label="Chrome" />
+        <TreeItem nodeId="4" label="Webstorm" />
+      </TreeItem>
+      <TreeItem nodeId="5" label="Documents">
+        <TreeItem nodeId="6" label="Material-UI">
+          <TreeItem nodeId="7" label="src">
+            <TreeItem nodeId="8" label="index.js" />
+            <TreeItem nodeId="9" label="tree-view.js" />
+          </TreeItem>
+        </TreeItem>
+      </TreeItem>
+    </TreeView>
+  );
+}
diff --git a/docs/src/pages/components/tree-view/tree-view.md b/docs/src/pages/components/tree-view/tree-view.md
--- a/docs/src/pages/components/tree-view/tree-view.md
+++ b/docs/src/pages/components/tree-view/tree-view.md
@@ -9,9 +9,17 @@ components: TreeView, TreeItem
 
 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.
 
+## Basic tree view
+
 {{"demo": "pages/components/tree-view/FileSystemNavigator.js"}}
 
-## Controlled
+## Multi selection
+
+Tree views also support multi selection.
+
+{{"demo": "pages/components/tree-view/MultiSelectTreeView.js"}}
+
+### Controlled tree view
 
 The tree view also offers a controlled API.
 
diff --git a/packages/material-ui-lab/src/TreeItem/TreeItem.d.ts b/packages/material-ui-lab/src/TreeItem/TreeItem.d.ts
--- a/packages/material-ui-lab/src/TreeItem/TreeItem.d.ts
+++ b/packages/material-ui-lab/src/TreeItem/TreeItem.d.ts
@@ -42,6 +42,7 @@ export interface TreeItemProps
 export type TreeItemClassKey =
   | 'root'
   | 'expanded'
+  | 'selected'
   | 'group'
   | 'content'
   | 'iconContainer'
diff --git a/packages/material-ui-lab/src/TreeItem/TreeItem.js b/packages/material-ui-lab/src/TreeItem/TreeItem.js
--- a/packages/material-ui-lab/src/TreeItem/TreeItem.js
+++ b/packages/material-ui-lab/src/TreeItem/TreeItem.js
@@ -4,7 +4,7 @@ import clsx from 'clsx';
 import PropTypes from 'prop-types';
 import Typography from '@material-ui/core/Typography';
 import Collapse from '@material-ui/core/Collapse';
-import { withStyles, useTheme } from '@material-ui/core/styles';
+import { fade, withStyles, useTheme } from '@material-ui/core/styles';
 import { useForkRef } from '@material-ui/core/utils';
 import TreeViewContext from '../TreeView/TreeViewContext';
 
@@ -16,17 +16,32 @@ export const styles = theme => ({
     padding: 0,
     outline: 0,
     WebkitTapHighlightColor: 'transparent',
-    '&:focus > $content': {
-      backgroundColor: theme.palette.grey[400],
+    '&:focus > $content $label': {
+      backgroundColor: theme.palette.action.hover,
+    },
+    '&$selected > $content $label': {
+      backgroundColor: fade(theme.palette.primary.main, theme.palette.action.selectedOpacity),
+    },
+    '&$selected > $content $label:hover, &$selected:focus > $content $label': {
+      backgroundColor: fade(
+        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: 'transparent',
+      },
     },
   },
   /* Pseudo-class applied to the root element when expanded. */
   expanded: {},
+  /* Pseudo-class applied to the root element when selected. */
+  selected: {},
   /* Styles applied to the `role="group"` element. */
   group: {
     margin: 0,
     padding: 0,
-    marginLeft: 26,
+    marginLeft: 17,
   },
   /* Styles applied to the tree node content. */
   content: {
@@ -34,20 +49,30 @@ export const styles = theme => ({
     display: 'flex',
     alignItems: 'center',
     cursor: 'pointer',
-    '&:hover': {
-      backgroundColor: theme.palette.action.hover,
-    },
   },
   /* Styles applied to the tree node icon and collapse/expand icon. */
   iconContainer: {
-    marginRight: 2,
-    width: 24,
+    marginRight: 4,
+    width: 15,
     display: 'flex',
+    flexShrink: 0,
     justifyContent: 'center',
+    '& svg': {
+      fontSize: 18,
+    },
   },
   /* Styles applied to the label element. */
   label: {
     width: '100%',
+    paddingLeft: 4,
+    position: 'relative',
+    '&:hover': {
+      backgroundColor: theme.palette.action.hover,
+      // Reset on touch devices, it doesn't add specificity
+      '@media (hover: none)': {
+        backgroundColor: 'transparent',
+      },
+    },
   },
 });
 
@@ -69,28 +94,39 @@ const TreeItem = React.forwardRef(function TreeItem(props, ref) {
     onClick,
     onFocus,
     onKeyDown,
+    onMouseDown,
     TransitionComponent = Collapse,
     TransitionProps,
     ...other
   } = props;
 
   const {
-    expandAllSiblings,
+    icons: contextIcons,
     focus,
     focusFirstNode,
     focusLastNode,
     focusNextNode,
     focusPreviousNode,
-    handleFirstChars,
-    handleLeftArrow,
-    addNodeToNodeMap,
-    removeNodeFromNodeMap,
-    icons: contextIcons,
+    focusByFirstCharacter,
+    selectNode,
+    selectRange,
+    selectNextNode,
+    selectPreviousNode,
+    rangeSelectToFirst,
+    rangeSelectToLast,
+    selectAllNodes,
+    expandAllSiblings,
+    toggleExpansion,
     isExpanded,
     isFocused,
+    isSelected,
     isTabbable,
-    setFocusByFirstCharacter,
-    toggle,
+    multiSelect,
+    selectionDisabled,
+    getParent,
+    mapFirstChar,
+    addNodeToNodeMap,
+    removeNodeFromNodeMap,
   } = React.useContext(TreeViewContext);
 
   const nodeRef = React.useRef(null);
@@ -103,6 +139,7 @@ const TreeItem = React.forwardRef(function TreeItem(props, ref) {
   const expanded = isExpanded ? isExpanded(nodeId) : false;
   const focused = isFocused ? isFocused(nodeId) : false;
   const tabbable = isTabbable ? isTabbable(nodeId) : false;
+  const selected = isSelected ? isSelected(nodeId) : false;
   const icons = contextIcons || {};
   const theme = useTheme();
 
@@ -127,8 +164,23 @@ const TreeItem = React.forwardRef(function TreeItem(props, ref) {
       focus(nodeId);
     }
 
-    if (expandable) {
-      toggle(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);
+    }
+
+    if (!selectionDisabled) {
+      if (multiple) {
+        if (event.shiftKey) {
+          selectRange(event, { end: nodeId });
+        } else {
+          selectNode(event, nodeId, true);
+        }
+      } else {
+        selectNode(event, nodeId);
+      }
     }
 
     if (onClick) {
@@ -136,14 +188,19 @@ const TreeItem = React.forwardRef(function TreeItem(props, ref) {
     }
   };
 
-  const printableCharacter = (event, key) => {
-    if (key === '*') {
-      expandAllSiblings(event, nodeId);
-      return true;
+  const handleMouseDown = event => {
+    if (event.shiftKey || event.ctrlKey || event.metaKey) {
+      event.preventDefault();
+    }
+
+    if (onMouseDown) {
+      onMouseDown(event);
     }
+  };
 
+  const printableCharacter = (event, key) => {
     if (isPrintableCharacter(key)) {
-      setFocusByFirstCharacter(nodeId, key);
+      focusByFirstCharacter(nodeId, key);
       return true;
     }
     return false;
@@ -154,75 +211,110 @@ const TreeItem = React.forwardRef(function TreeItem(props, ref) {
       if (expanded) {
         focusNextNode(nodeId);
       } else {
-        toggle(event);
+        toggleExpansion(event);
       }
     }
+    return true;
   };
 
   const handlePreviousArrow = event => {
-    handleLeftArrow(nodeId, event);
+    if (expanded) {
+      toggleExpansion(event, nodeId);
+      return true;
+    }
+
+    const parent = getParent(nodeId);
+    if (parent) {
+      focus(parent);
+      return true;
+    }
+    return false;
   };
 
   const handleKeyDown = event => {
     let flag = false;
     const key = event.key;
 
-    if (event.altKey || event.ctrlKey || event.metaKey || event.currentTarget !== event.target) {
+    if (event.altKey || event.currentTarget !== event.target) {
       return;
     }
-    if (event.shift) {
-      if (key === ' ' || key === 'Enter') {
-        event.stopPropagation();
-      } else if (isPrintableCharacter(key)) {
-        flag = printableCharacter(event, key);
-      }
-    } else {
-      switch (key) {
-        case 'Enter':
-        case ' ':
-          if (nodeRef.current === event.currentTarget && expandable) {
-            toggle(event);
-            flag = true;
+
+    const ctrlPressed = event.ctrlKey || event.metaKey;
+
+    switch (key) {
+      case ' ':
+        if (nodeRef.current === event.currentTarget) {
+          if (multiSelect && event.shiftKey) {
+            selectRange(event, { end: nodeId });
+          } else if (multiSelect) {
+            selectNode(event, nodeId, true);
+          } else {
+            selectNode(event, nodeId);
           }
-          event.stopPropagation();
-          break;
-        case 'ArrowDown':
-          focusNextNode(nodeId);
+
           flag = true;
-          break;
-        case 'ArrowUp':
-          focusPreviousNode(nodeId);
+        }
+        event.stopPropagation();
+        break;
+      case 'Enter':
+        if (nodeRef.current === event.currentTarget && expandable) {
+          toggleExpansion(event);
           flag = true;
-          break;
-        case 'ArrowRight':
-          if (theme.direction === 'rtl') {
-            handlePreviousArrow(event);
-          } else {
-            handleNextArrow(event);
-            flag = true;
-          }
-          break;
-        case 'ArrowLeft':
-          if (theme.direction === 'rtl') {
-            handleNextArrow(event);
-            flag = true;
-          } else {
-            handlePreviousArrow(event);
-          }
-          break;
-        case 'Home':
-          focusFirstNode();
+        }
+        event.stopPropagation();
+        break;
+      case 'ArrowDown':
+        if (multiSelect && event.shiftKey) {
+          selectNextNode(event, nodeId);
+        }
+        focusNextNode(nodeId);
+        flag = true;
+        break;
+      case 'ArrowUp':
+        if (multiSelect && event.shiftKey) {
+          selectPreviousNode(event, nodeId);
+        }
+        focusPreviousNode(nodeId);
+        flag = true;
+        break;
+      case 'ArrowRight':
+        if (theme.direction === 'rtl') {
+          flag = handlePreviousArrow(event);
+        } else {
+          flag = handleNextArrow(event);
+        }
+        break;
+      case 'ArrowLeft':
+        if (theme.direction === 'rtl') {
+          flag = handleNextArrow(event);
+        } else {
+          flag = handlePreviousArrow(event);
+        }
+        break;
+      case 'Home':
+        if (multiSelect && ctrlPressed && event.shiftKey) {
+          rangeSelectToFirst(event, nodeId);
+        }
+        focusFirstNode();
+        flag = true;
+        break;
+      case 'End':
+        if (multiSelect && ctrlPressed && event.shiftKey) {
+          rangeSelectToLast(event, nodeId);
+        }
+        focusLastNode();
+        flag = true;
+        break;
+      default:
+        if (key === '*') {
+          expandAllSiblings(event, nodeId);
           flag = true;
-          break;
-        case 'End':
-          focusLastNode();
+        } else if (multiSelect && ctrlPressed && key.toLowerCase() === 'a') {
+          selectAllNodes(event);
           flag = true;
-          break;
-        default:
-          if (isPrintableCharacter(key)) {
-            flag = printableCharacter(event, key);
-          }
-      }
+        } else if (isPrintableCharacter(key)) {
+          flag = printableCharacter(event, key);
+        }
     }
 
     if (flag) {
@@ -262,10 +354,10 @@ const TreeItem = React.forwardRef(function TreeItem(props, ref) {
   }, [nodeId, removeNodeFromNodeMap]);
 
   React.useEffect(() => {
-    if (handleFirstChars && label) {
-      handleFirstChars(nodeId, contentRef.current.textContent.substring(0, 1).toLowerCase());
+    if (mapFirstChar && label) {
+      mapFirstChar(nodeId, contentRef.current.textContent.substring(0, 1).toLowerCase());
     }
-  }, [handleFirstChars, nodeId, label]);
+  }, [mapFirstChar, nodeId, label]);
 
   React.useEffect(() => {
     if (focused) {
@@ -277,17 +369,24 @@ const TreeItem = React.forwardRef(function TreeItem(props, ref) {
     <li
       className={clsx(classes.root, className, {
         [classes.expanded]: expanded,
+        [classes.selected]: selected,
       })}
       role="treeitem"
       onKeyDown={handleKeyDown}
       onFocus={handleFocus}
       aria-expanded={expandable ? expanded : null}
+      aria-selected={!selectionDisabled && isSelected ? isSelected(nodeId) : undefined}
       ref={handleRef}
       tabIndex={tabbable ? 0 : -1}
       {...other}
     >
-      <div className={classes.content} onClick={handleClick} ref={contentRef}>
-        {icon ? <div className={classes.iconContainer}>{icon}</div> : null}
+      <div
+        className={classes.content}
+        onClick={handleClick}
+        onMouseDown={handleMouseDown}
+        ref={contentRef}
+      >
+        <div className={classes.iconContainer}>{icon}</div>
         <Typography component="div" className={classes.label}>
           {label}
         </Typography>
@@ -362,6 +461,10 @@ TreeItem.propTypes = {
    * @ignore
    */
   onKeyDown: PropTypes.func,
+  /**
+   * @ignore
+   */
+  onMouseDown: PropTypes.func,
   /**
    * The component used for the transition.
    * [Follow this guide](/components/transitions/#transitioncomponent-prop) to learn more about the requirements for this component.
diff --git a/packages/material-ui-lab/src/TreeView/TreeView.d.ts b/packages/material-ui-lab/src/TreeView/TreeView.d.ts
--- a/packages/material-ui-lab/src/TreeView/TreeView.d.ts
+++ b/packages/material-ui-lab/src/TreeView/TreeView.d.ts
@@ -1,7 +1,7 @@
 import * as React from 'react';
 import { StandardProps } from '@material-ui/core';
 
-export interface TreeViewProps
+export interface TreeViewPropsBase
   extends StandardProps<React.HTMLAttributes<HTMLUListElement>, TreeViewClassKey> {
   /**
    * The default icon used to collapse the node.
@@ -25,6 +25,10 @@ export interface TreeViewProps
    * parent nodes and can be overridden by the TreeItem `icon` prop.
    */
   defaultParentIcon?: React.ReactNode;
+  /**
+   * If `true` selection is disabled.
+   */
+  disableSelection?: boolean;
   /**
    * Expanded node ids. (Controlled)
    */
@@ -38,6 +42,58 @@ export interface TreeViewProps
   onNodeToggle?: (event: React.ChangeEvent<{}>, nodeIds: string[]) => void;
 }
 
+export interface MultiSelectTreeViewProps extends TreeViewPropsBase {
+  /**
+   * Selected node ids. (Uncontrolled)
+   * When `multiSelect` is true this takes an array of strings; when false (default) a string.
+   */
+  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.
+   */
+  multiSelect?: true;
+  /**
+   * Callback fired when tree items are selected/unselected.
+   *
+   * @param {object} event The event source of the callback
+   * @param {(array|string)} value of the selected nodes. When `multiSelect` is true
+   * this is an array of strings; when false (default) a string.
+   */
+  onNodeSelect?: (event: React.ChangeEvent<{}>, 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.
+   */
+  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.
+   */
+  multiSelect?: false;
+  /**
+   * Callback fired when tree items are selected/unselected.
+   *
+   * @param {object} event The event source of the callback
+   * @param {(array|string)} value of the selected nodes. When `multiSelect` is true
+   * this is an array of strings; when false (default) a string.
+   */
+  onNodeSelect?: (event: React.ChangeEvent<{}>, nodeIds: string) => void;
+}
+
+export type TreeViewProps = SingleSelectTreeViewProps | MultiSelectTreeViewProps;
+
 export type TreeViewClassKey = 'root';
 
 export default function TreeView(props: TreeViewProps): JSX.Element;
diff --git a/packages/material-ui-lab/src/TreeView/TreeView.js b/packages/material-ui-lab/src/TreeView/TreeView.js
--- a/packages/material-ui-lab/src/TreeView/TreeView.js
+++ b/packages/material-ui-lab/src/TreeView/TreeView.js
@@ -24,7 +24,17 @@ function arrayDiff(arr1, arr2) {
   return false;
 }
 
+const findNextFirstChar = (firstChars, startIndex, char) => {
+  for (let i = startIndex; i < firstChars.length; i += 1) {
+    if (char === firstChars[i]) {
+      return i;
+    }
+  }
+  return -1;
+};
+
 const defaultExpandedDefault = [];
+const defaultSelectedDefault = [];
 
 const TreeView = React.forwardRef(function TreeView(props, ref) {
   const {
@@ -36,145 +46,151 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
     defaultExpanded = defaultExpandedDefault,
     defaultExpandIcon,
     defaultParentIcon,
+    defaultSelected = defaultSelectedDefault,
+    disableSelection = false,
+    multiSelect = false,
     expanded: expandedProp,
+    onNodeSelect,
     onNodeToggle,
+    selected: selectedProp,
     ...other
   } = props;
-  const [tabable, setTabable] = React.useState(null);
+  const [tabbable, setTabbable] = React.useState(null);
   const [focused, setFocused] = React.useState(null);
 
-  const firstNode = React.useRef(null);
   const nodeMap = React.useRef({});
   const firstCharMap = React.useRef({});
+  const visibleNodes = React.useRef([]);
 
-  const [expandedState, setExpandedState] = useControlled({
+  const [expanded, setExpandedState] = useControlled({
     controlled: expandedProp,
     default: defaultExpanded,
     name: 'TreeView',
   });
 
-  const expanded = expandedState || defaultExpandedDefault;
+  const [selected, setSelectedState] = useControlled({
+    controlled: selectedProp,
+    default: defaultSelected,
+    name: 'TreeView',
+  });
 
-  const prevChildIds = React.useRef([]);
-  React.useEffect(() => {
-    const childIds = React.Children.map(children, child => child.props.nodeId) || [];
-    if (arrayDiff(prevChildIds.current, childIds)) {
-      nodeMap.current[-1] = { parent: null, children: childIds };
+  /*
+   * Status Helpers
+   */
+  const isExpanded = React.useCallback(
+    id => (Array.isArray(expanded) ? expanded.indexOf(id) !== -1 : false),
+    [expanded],
+  );
 
-      childIds.forEach((id, index) => {
-        if (index === 0) {
-          firstNode.current = id;
-          setTabable(id);
-        }
-        nodeMap.current[id] = { parent: null };
-      });
-      prevChildIds.current = childIds;
-    }
-  }, [children]);
+  const isSelected = React.useCallback(
+    id => (Array.isArray(selected) ? selected.indexOf(id) !== -1 : selected === id),
+    [selected],
+  );
 
-  const isExpanded = React.useCallback(id => expanded.indexOf(id) !== -1, [expanded]);
-  const isTabbable = id => tabable === id;
+  const isTabbable = id => tabbable === id;
   const isFocused = id => focused === id;
 
-  const getLastNode = React.useCallback(
-    id => {
-      const map = nodeMap.current[id];
-      if (isExpanded(id) && map.children && map.children.length > 0) {
-        return getLastNode(map.children[map.children.length - 1]);
-      }
-      return id;
-    },
-    [isExpanded],
-  );
+  /*
+   * Node Helpers
+   */
 
-  const focus = id => {
-    if (id) {
-      setTabable(id);
+  const getNextNode = id => {
+    const nodeIndex = visibleNodes.current.indexOf(id);
+    if (nodeIndex !== -1 && nodeIndex + 1 < visibleNodes.current.length) {
+      return visibleNodes.current[nodeIndex + 1];
     }
-    setFocused(id);
+    return null;
   };
 
-  const getNextNode = (id, end) => {
-    const map = nodeMap.current[id];
-    const parent = nodeMap.current[map.parent];
-
-    if (!end) {
-      if (isExpanded(id)) {
-        return map.children[0];
-      }
-    }
-    if (parent) {
-      const nodeIndex = parent.children.indexOf(id);
-      const nextIndex = nodeIndex + 1;
-      if (parent.children.length > nextIndex) {
-        return parent.children[nextIndex];
-      }
-      return getNextNode(parent.id, true);
-    }
-    const topLevelNodes = nodeMap.current[-1].children;
-    const topLevelNodeIndex = topLevelNodes.indexOf(id);
-    if (topLevelNodeIndex !== -1 && topLevelNodeIndex !== topLevelNodes.length - 1) {
-      return topLevelNodes[topLevelNodeIndex + 1];
+  const getPreviousNode = id => {
+    const nodeIndex = visibleNodes.current.indexOf(id);
+    if (nodeIndex !== -1 && nodeIndex - 1 >= 0) {
+      return visibleNodes.current[nodeIndex - 1];
     }
-
     return null;
   };
 
-  const getPreviousNode = id => {
-    const map = nodeMap.current[id];
-    const parent = nodeMap.current[map.parent];
-
-    if (parent) {
-      const nodeIndex = parent.children.indexOf(id);
-      if (nodeIndex !== 0) {
-        const nextIndex = nodeIndex - 1;
-        return getLastNode(parent.children[nextIndex]);
-      }
-      return parent.id;
-    }
-    const topLevelNodes = nodeMap.current[-1].children;
-    const topLevelNodeIndex = topLevelNodes.indexOf(id);
-    if (topLevelNodeIndex > 0) {
-      return getLastNode(topLevelNodes[topLevelNodeIndex - 1]);
-    }
+  const getLastNode = () => visibleNodes.current[visibleNodes.current.length - 1];
+  const getFirstNode = () => visibleNodes.current[0];
+  const getParent = id => nodeMap.current[id].parent;
 
-    return null;
+  const getNodesInRange = (a, b) => {
+    const aIndex = visibleNodes.current.indexOf(a);
+    const bIndex = visibleNodes.current.indexOf(b);
+    const start = Math.min(aIndex, bIndex);
+    const end = Math.max(aIndex, bIndex);
+    return visibleNodes.current.slice(start, end + 1);
   };
 
-  const focusNextNode = id => {
-    const nextNode = getNextNode(id);
-    if (nextNode) {
-      focus(nextNode);
+  /*
+   * Focus Helpers
+   */
+
+  const focus = id => {
+    if (id) {
+      setTabbable(id);
+      setFocused(id);
     }
   };
-  const focusPreviousNode = id => {
-    const previousNode = getPreviousNode(id);
-    if (previousNode) {
-      focus(previousNode);
+
+  const focusNextNode = id => focus(getNextNode(id));
+  const focusPreviousNode = id => focus(getPreviousNode(id));
+  const focusFirstNode = () => focus(getFirstNode());
+  const focusLastNode = () => focus(getLastNode());
+
+  const focusByFirstCharacter = (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.parent ? isExpanded(map.parent) : true;
+
+      if (visible) {
+        firstCharIds.push(nodeId);
+        firstChars.push(firstChar);
+      }
+    });
+
+    // Get start index for search based on position of currentItem
+    start = firstCharIds.indexOf(id) + 1;
+    if (start === nodeMap.current.length) {
+      start = 0;
     }
-  };
-  const focusFirstNode = () => {
-    if (firstNode.current) {
-      focus(firstNode.current);
+
+    // 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);
     }
-  };
 
-  const focusLastNode = () => {
-    const topLevelNodes = nodeMap.current[-1].children;
-    const lastNode = getLastNode(topLevelNodes[topLevelNodes.length - 1]);
-    focus(lastNode);
+    // If match was found...
+    if (index > -1) {
+      focus(firstCharIds[index]);
+    }
   };
 
-  const toggle = (event, value = focused) => {
+  /*
+   * Expansion Helpers
+   */
+
+  const toggleExpansion = (event, value = focused) => {
     let newExpanded;
     if (expanded.indexOf(value) !== -1) {
       newExpanded = expanded.filter(id => id !== value);
-      setTabable(oldTabable => {
-        const map = nodeMap.current[oldTabable];
-        if (oldTabable && (map && map.parent ? map.parent.id : null) === value) {
+      setTabbable(oldTabbable => {
+        const map = nodeMap.current[oldTabbable];
+        if (oldTabbable && (map && map.parent ? map.parent.id : null) === value) {
           return value;
         }
-        return oldTabable;
+        return oldTabbable;
       });
     } else {
       newExpanded = [value, ...expanded];
@@ -207,75 +223,172 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
     }
   };
 
-  const handleLeftArrow = (id, event) => {
-    let flag = false;
-    if (isExpanded(id)) {
-      toggle(event, id);
-      flag = true;
-    } else {
-      const parent = nodeMap.current[id].parent;
-      if (parent) {
-        focus(parent);
-        flag = true;
+  /*
+   * Selection Helpers
+   */
+
+  const lastSelectedNode = React.useRef(null);
+  const lastSelectionWasRange = React.useRef(false);
+  const currentRangeSelection = React.useRef([]);
+
+  const handleRangeArrowSelect = (event, nodes) => {
+    let base = selected;
+    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 (flag && event) {
-      event.preventDefault();
-      event.stopPropagation();
+    if (onNodeSelect) {
+      onNodeSelect(event, base);
     }
+
+    setSelectedState(base);
   };
 
-  const getIndexFirstChars = (firstChars, startIndex, char) => {
-    for (let i = startIndex; i < firstChars.length; i += 1) {
-      if (char === firstChars[i]) {
-        return i;
-      }
+  const handleRangeSelect = (event, nodes) => {
+    let base = selected;
+    const { start, end } = nodes;
+    // If last selection was a range selection ignore nodes that were selected.
+    if (lastSelectionWasRange.current) {
+      base = selected.filter(id => currentRangeSelection.current.indexOf(id) === -1);
+    }
+
+    const range = getNodesInRange(start, end);
+    currentRangeSelection.current = range;
+    let newSelected = base.concat(range);
+    newSelected = newSelected.filter((id, i) => newSelected.indexOf(id) === i);
+
+    if (onNodeSelect) {
+      onNodeSelect(event, newSelected);
     }
-    return -1;
+
+    setSelectedState(newSelected);
   };
 
-  const setFocusByFirstCharacter = (id, char) => {
-    let start;
-    let index;
-    const lowercaseChar = char.toLowerCase();
+  const handleMultipleSelect = (event, value) => {
+    let newSelected = [];
+    if (selected.indexOf(value) !== -1) {
+      newSelected = selected.filter(id => id !== value);
+    } else {
+      newSelected = [value, ...selected];
+    }
 
-    const firstCharIds = [];
-    const firstChars = [];
-    // This really only works since the ids are strings
-    Object.entries(firstCharMap.current).forEach(([nodeId, firstChar]) => {
-      const map = nodeMap.current[nodeId];
-      const visible = map.parent ? isExpanded(map.parent) : true;
+    if (onNodeSelect) {
+      onNodeSelect(event, newSelected);
+    }
 
-      if (visible) {
-        firstCharIds.push(nodeId);
-        firstChars.push(firstChar);
-      }
-    });
+    setSelectedState(newSelected);
+  };
 
-    // Get start index for search based on position of currentItem
-    start = firstCharIds.indexOf(id) + 1;
-    if (start === nodeMap.current.length) {
-      start = 0;
+  const handleSingleSelect = (event, value) => {
+    const newSelected = multiSelect ? [value] : value;
+
+    if (onNodeSelect) {
+      onNodeSelect(event, newSelected);
     }
 
-    // Check remaining slots in the menu
-    index = getIndexFirstChars(firstChars, start, lowercaseChar);
+    setSelectedState(newSelected);
+  };
 
-    // If not found in remaining slots, check from beginning
-    if (index === -1) {
-      index = getIndexFirstChars(firstChars, 0, lowercaseChar);
+  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 = [];
+    }
+  };
+
+  const selectRange = (event, nodes, stacked = false) => {
+    const { start = lastSelectedNode.current, end, current } = nodes;
+    if (stacked) {
+      handleRangeArrowSelect(event, { start, next: end, current });
+    } else {
+      handleRangeSelect(event, { start, end });
     }
+    lastSelectionWasRange.current = true;
+  };
 
-    // If match was found...
-    if (index > -1) {
-      focus(firstCharIds[index]);
+  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) =>
+    selectRange(
+      event,
+      {
+        end: getNextNode(id),
+        current: id,
+      },
+      true,
+    );
+
+  const selectPreviousNode = (event, id) =>
+    selectRange(
+      event,
+      {
+        end: getPreviousNode(id),
+        current: id,
+      },
+      true,
+    );
+
+  const selectAllNodes = event => selectRange(event, { start: getFirstNode(), end: getLastNode() });
+
+  /*
+   * Mapping Helpers
+   */
+
   const addNodeToNodeMap = (id, childrenIds) => {
     const currentMap = nodeMap.current[id];
     nodeMap.current[id] = { ...currentMap, children: childrenIds, id };
+
     childrenIds.forEach(childId => {
       const currentChildMap = nodeMap.current[childId];
       nodeMap.current[childId] = { ...currentChildMap, parent: id, id: childId };
@@ -297,32 +410,86 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
     }
   };
 
-  const handleFirstChars = (id, firstChar) => {
+  const mapFirstChar = (id, firstChar) => {
     firstCharMap.current[id] = firstChar;
   };
 
+  const prevChildIds = React.useRef([]);
+  const [childrenCalculated, setChildrenCalculated] = React.useState(false);
+  React.useEffect(() => {
+    const childIds = React.Children.map(children, child => child.props.nodeId) || [];
+    if (arrayDiff(prevChildIds.current, childIds)) {
+      nodeMap.current[-1] = { parent: null, children: childIds };
+
+      childIds.forEach((id, index) => {
+        if (index === 0) {
+          setTabbable(id);
+        }
+        nodeMap.current[id] = { parent: null };
+      });
+      visibleNodes.current = nodeMap.current[-1].children;
+      prevChildIds.current = childIds;
+      setChildrenCalculated(true);
+    }
+  }, [children]);
+
+  React.useEffect(() => {
+    const buildVisible = nodes => {
+      let list = [];
+      for (let i = 0; i < nodes.length; i += 1) {
+        const item = nodes[i];
+        list.push(item);
+        const childs = nodeMap.current[item].children;
+        if (isExpanded(item) && childs) {
+          list = list.concat(buildVisible(childs));
+        }
+      }
+      return list;
+    };
+
+    if (childrenCalculated) {
+      visibleNodes.current = buildVisible(nodeMap.current[-1].children);
+    }
+  }, [expanded, childrenCalculated, isExpanded]);
+
   return (
     <TreeViewContext.Provider
       value={{
-        expandAllSiblings,
+        icons: { defaultCollapseIcon, defaultExpandIcon, defaultParentIcon, defaultEndIcon },
         focus,
         focusFirstNode,
         focusLastNode,
         focusNextNode,
         focusPreviousNode,
-        handleFirstChars,
-        handleLeftArrow,
-        addNodeToNodeMap,
-        removeNodeFromNodeMap,
-        icons: { defaultCollapseIcon, defaultExpandIcon, defaultParentIcon, defaultEndIcon },
+        focusByFirstCharacter,
+        expandAllSiblings,
+        toggleExpansion,
         isExpanded,
         isFocused,
+        isSelected,
+        selectNode,
+        selectRange,
+        selectNextNode,
+        selectPreviousNode,
+        rangeSelectToFirst,
+        rangeSelectToLast,
+        selectAllNodes,
         isTabbable,
-        setFocusByFirstCharacter,
-        toggle,
+        multiSelect,
+        selectionDisabled: disableSelection,
+        getParent,
+        mapFirstChar,
+        addNodeToNodeMap,
+        removeNodeFromNodeMap,
       }}
     >
-      <ul role="tree" className={clsx(classes.root, className)} ref={ref} {...other}>
+      <ul
+        role="tree"
+        aria-multiselectable={multiSelect}
+        className={clsx(classes.root, className)}
+        ref={ref}
+        {...other}
+      >
         {children}
       </ul>
     </TreeViewContext.Provider>
@@ -369,10 +536,31 @@ TreeView.propTypes = {
    * 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.
+   */
+  defaultSelected: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.string), PropTypes.string]),
+  /**
+   * If `true` selection is disabled.
+   */
+  disableSelection: PropTypes.bool,
   /**
    * Expanded node ids. (Controlled)
    */
   expanded: PropTypes.arrayOf(PropTypes.string),
+  /**
+   * If true `ctrl` and `shift` will trigger multiselect.
+   */
+  multiSelect: PropTypes.bool,
+  /**
+   * Callback fired when tree items are selected/unselected.
+   *
+   * @param {object} event The event source of the callback
+   * @param {(array|string)} value 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.
    *
@@ -380,6 +568,11 @@ TreeView.propTypes = {
    * @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]),
 };
 
 export default withStyles(styles, { name: 'MuiTreeView' })(TreeView);


Test patch:
diff --git a/packages/material-ui-lab/src/TreeItem/TreeItem.test.js b/packages/material-ui-lab/src/TreeItem/TreeItem.test.js
--- a/packages/material-ui-lab/src/TreeItem/TreeItem.test.js
+++ b/packages/material-ui-lab/src/TreeItem/TreeItem.test.js
@@ -151,13 +151,13 @@ describe('<TreeItem />', () => {
 
   describe('Accessibility', () => {
     it('should have the role `treeitem`', () => {
-      const { getByRole } = render(
+      const { getByTestId } = render(
         <TreeView>
-          <TreeItem nodeId="test" label="test" />
+          <TreeItem nodeId="test" label="test" data-testid="test" />
         </TreeView>,
       );
 
-      expect(getByRole('treeitem')).to.be.ok;
+      expect(getByTestId('test')).to.have.attribute('role', 'treeitem');
     });
 
     it('should add the role `group` to a component containing children', () => {
@@ -172,38 +172,72 @@ describe('<TreeItem />', () => {
       expect(getByRole('group')).to.contain(getByText('test2'));
     });
 
-    it('should have the attribute `aria-expanded=false` if collapsed', () => {
-      const { getByTestId } = render(
-        <TreeView>
-          <TreeItem nodeId="test" label="test" data-testid="test">
-            <TreeItem nodeId="test2" label="test2" />
-          </TreeItem>
-        </TreeView>,
-      );
+    describe('aria-expanded', () => {
+      it('should have the attribute `aria-expanded=false` if collapsed', () => {
+        const { getByTestId } = render(
+          <TreeView>
+            <TreeItem nodeId="test" label="test" data-testid="test">
+              <TreeItem nodeId="test2" label="test2" />
+            </TreeItem>
+          </TreeView>,
+        );
 
-      expect(getByTestId('test')).to.have.attribute('aria-expanded', 'false');
-    });
+        expect(getByTestId('test')).to.have.attribute('aria-expanded', 'false');
+      });
 
-    it('should have the attribute `aria-expanded=true` if expanded', () => {
-      const { container } = render(
-        <TreeView defaultExpanded={['test']}>
-          <TreeItem nodeId="test" label="test">
-            <TreeItem nodeId="test2" label="test" />
-          </TreeItem>
-        </TreeView>,
-      );
+      it('should have the attribute `aria-expanded=true` if expanded', () => {
+        const { getByTestId } = render(
+          <TreeView defaultExpanded={['test']}>
+            <TreeItem nodeId="test" label="test" data-testid="test">
+              <TreeItem nodeId="test2" label="test2" />
+            </TreeItem>
+          </TreeView>,
+        );
+
+        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(
+          <TreeView>
+            <TreeItem nodeId="test" label="test" data-testid="test" />
+          </TreeView>,
+        );
 
-      expect(container.querySelector('[aria-expanded=true]')).to.be.ok;
+        expect(getByTestId('test')).to.not.have.attribute('aria-expanded');
+      });
     });
 
-    it('should not have the attribute `aria-expanded` if no children are present', () => {
-      const { container } = render(
-        <TreeView>
-          <TreeItem nodeId="test" label="test" />
-        </TreeView>,
-      );
+    describe('aria-selected', () => {
+      it('should have the attribute `aria-selected=false` if not selected', () => {
+        const { getByTestId } = render(
+          <TreeView>
+            <TreeItem nodeId="test" label="test" data-testid="test" />
+          </TreeView>,
+        );
 
-      expect(container.querySelector('[aria-expanded]')).to.not.be.ok;
+        expect(getByTestId('test')).to.have.attribute('aria-selected', 'false');
+      });
+
+      it('should have the attribute `aria-selected=true` if selected', () => {
+        const { getByTestId } = render(
+          <TreeView defaultSelected={'test'}>
+            <TreeItem nodeId="test" label="test" data-testid="test" />
+          </TreeView>,
+        );
+
+        expect(getByTestId('test')).to.have.attribute('aria-selected', 'true');
+      });
+
+      it('should not have the attribute `aria-selected` if disableSelection is true', () => {
+        const { getByTestId } = render(
+          <TreeView disableSelection>
+            <TreeItem nodeId="test" label="test" data-testid="test" />
+          </TreeView>,
+        );
+
+        expect(getByTestId('test')).to.not.have.attribute('aria-selected');
+      });
     });
 
     describe('when a tree receives focus', () => {
@@ -255,395 +289,681 @@ describe('<TreeItem />', () => {
       });
     });
 
-    describe('right arrow interaction', () => {
-      it('should open the node and not move the focus if focus is on a closed node', () => {
-        const { getByTestId } = render(
-          <TreeView>
-            <TreeItem nodeId="one" label="one" data-testid="one">
-              <TreeItem nodeId="two" label="two" />
-            </TreeItem>
-          </TreeView>,
-        );
-
-        expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false');
-        getByTestId('one').focus();
-        fireEvent.keyDown(document.activeElement, { key: 'ArrowRight' });
-        expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
-        expect(getByTestId('one')).to.have.focus;
+    describe('Navigation', () => {
+      describe('right arrow interaction', () => {
+        it('should open the node and not move the focus if focus is on a closed node', () => {
+          const { getByTestId } = render(
+            <TreeView>
+              <TreeItem nodeId="one" label="one" data-testid="one">
+                <TreeItem nodeId="two" label="two" />
+              </TreeItem>
+            </TreeView>,
+          );
+
+          expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false');
+          getByTestId('one').focus();
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowRight' });
+          expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
+          expect(getByTestId('one')).to.have.focus;
+        });
+
+        it('should move focus to the first child if focus is on an open node', () => {
+          const { getByTestId } = render(
+            <TreeView defaultExpanded={['one']}>
+              <TreeItem nodeId="one" label="one" data-testid="one">
+                <TreeItem nodeId="two" label="two" data-testid="two" />
+              </TreeItem>
+            </TreeView>,
+          );
+
+          expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
+          getByTestId('one').focus();
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowRight' });
+          expect(getByTestId('two')).to.have.focus;
+        });
+
+        it('should do nothing if focus is on an end node', () => {
+          const { getByTestId, getByText } = render(
+            <TreeView defaultExpanded={['one']}>
+              <TreeItem nodeId="one" label="one" data-testid="one">
+                <TreeItem nodeId="two" label="two" data-testid="two" />
+              </TreeItem>
+            </TreeView>,
+          );
+
+          fireEvent.click(getByText('two'));
+          expect(getByTestId('two')).to.have.focus;
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowRight' });
+          expect(getByTestId('two')).to.have.focus;
+        });
       });
 
-      it('should move focus to the first child if focus is on an open node', () => {
-        const { getByTestId } = render(
-          <TreeView defaultExpanded={['one']}>
-            <TreeItem nodeId="one" label="one" data-testid="one">
-              <TreeItem nodeId="two" label="two" data-testid="two" />
-            </TreeItem>
-          </TreeView>,
-        );
+      describe('left arrow interaction', () => {
+        it('should close the node if focus is on an open node', () => {
+          const { getByTestId, getByText } = render(
+            <TreeView>
+              <TreeItem nodeId="one" label="one" data-testid="one">
+                <TreeItem nodeId="two" label="two" />
+              </TreeItem>
+            </TreeView>,
+          );
+
+          fireEvent.click(getByText('one'));
+          expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
+          getByTestId('one').focus();
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowLeft' });
+          expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false');
+          expect(getByTestId('one')).to.have.focus;
+        });
+
+        it("should move focus to the node's parent node if focus is on a child node that is an end node", () => {
+          const { getByTestId, getByText } = render(
+            <TreeView defaultExpanded={['one']}>
+              <TreeItem nodeId="one" label="one" data-testid="one">
+                <TreeItem nodeId="two" label="two" data-testid="two" />
+              </TreeItem>
+            </TreeView>,
+          );
+
+          expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
+          fireEvent.click(getByText('two'));
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowLeft' });
+          expect(getByTestId('one')).to.have.focus;
+          expect(getByTestId('one')).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", () => {
+          const { getByTestId, getByText } = render(
+            <TreeView>
+              <TreeItem nodeId="one" label="one" data-testid="one">
+                <TreeItem nodeId="two" label="two" data-testid="two">
+                  <TreeItem nodeId="three" label="three" />
+                </TreeItem>
+              </TreeItem>
+            </TreeView>,
+          );
+
+          fireEvent.click(getByText('one'));
+          expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
+          // move focus to node two
+          fireEvent.click(getByText('two'));
+          fireEvent.click(getByText('two'));
+          expect(getByTestId('two')).to.have.attribute('aria-expanded', 'false');
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowLeft' });
+          expect(getByTestId('one')).to.have.focus;
+          expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
+        });
+
+        it('should do nothing if focus is on a root node that is closed', () => {
+          const { getByTestId } = render(
+            <TreeView>
+              <TreeItem nodeId="one" label="one" data-testid="one">
+                <TreeItem nodeId="two" label="two" />
+              </TreeItem>
+            </TreeView>,
+          );
 
-        expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
-        getByTestId('one').focus();
-        fireEvent.keyDown(document.activeElement, { key: 'ArrowRight' });
-        expect(getByTestId('two')).to.have.focus;
+          getByTestId('one').focus();
+          expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false');
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowLeft' });
+          expect(getByTestId('one')).to.have.focus;
+        });
+
+        it('should do nothing if focus is on a root node that is an end node', () => {
+          const { getByTestId } = render(
+            <TreeView>
+              <TreeItem nodeId="one" label="one" data-testid="one" />
+            </TreeView>,
+          );
+
+          getByTestId('one').focus();
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowLeft' });
+          expect(getByTestId('one')).to.have.focus;
+        });
       });
 
-      it('should do nothing if focus is on an end node', () => {
-        const { getByTestId, getByText } = render(
-          <TreeView defaultExpanded={['one']}>
-            <TreeItem nodeId="one" label="one" data-testid="one">
+      describe('down arrow interaction', () => {
+        it('moves focus to a sibling node', () => {
+          const { getByTestId } = render(
+            <TreeView>
+              <TreeItem nodeId="one" label="one" data-testid="one" />
               <TreeItem nodeId="two" label="two" data-testid="two" />
-            </TreeItem>
-          </TreeView>,
-        );
-
-        fireEvent.click(getByText('two'));
-        expect(getByTestId('two')).to.have.focus;
-        fireEvent.keyDown(document.activeElement, { key: 'ArrowRight' });
-        expect(getByTestId('two')).to.have.focus;
+            </TreeView>,
+          );
+
+          getByTestId('one').focus();
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' });
+          expect(getByTestId('two')).to.have.focus;
+        });
+
+        it('moves focus to a child node', () => {
+          const { getByTestId } = render(
+            <TreeView defaultExpanded={['one']}>
+              <TreeItem nodeId="one" label="one" data-testid="one">
+                <TreeItem nodeId="two" label="two" data-testid="two" />
+              </TreeItem>
+            </TreeView>,
+          );
+
+          expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
+          getByTestId('one').focus();
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' });
+          expect(getByTestId('two')).to.have.focus;
+        });
+
+        it("moves focus to a parent's sibling", () => {
+          const { getByTestId, getByText } = render(
+            <TreeView defaultExpanded={['one']}>
+              <TreeItem nodeId="one" label="one" data-testid="one">
+                <TreeItem nodeId="two" label="two" data-testid="two" />
+              </TreeItem>
+              <TreeItem nodeId="three" label="three" data-testid="three" />
+            </TreeView>,
+          );
+
+          expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
+          fireEvent.click(getByText('two'));
+          expect(getByTestId('two')).to.have.focus;
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' });
+          expect(getByTestId('three')).to.have.focus;
+        });
       });
-    });
 
-    describe('left arrow interaction', () => {
-      it('should close the node if focus is on an open node', () => {
-        const { getByTestId, getByText } = render(
-          <TreeView>
-            <TreeItem nodeId="one" label="one" data-testid="one">
-              <TreeItem nodeId="two" label="two" />
-            </TreeItem>
-          </TreeView>,
-        );
-
-        fireEvent.click(getByText('one'));
-        expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
-        getByTestId('one').focus();
-        fireEvent.keyDown(document.activeElement, { key: 'ArrowLeft' });
-        expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false');
-        expect(getByTestId('one')).to.have.focus;
+      describe('up arrow interaction', () => {
+        it('moves focus to a sibling node', () => {
+          const { getByTestId, getByText } = render(
+            <TreeView>
+              <TreeItem nodeId="one" label="one" data-testid="one" />
+              <TreeItem nodeId="two" label="two" data-testid="two" />
+            </TreeView>,
+          );
+
+          fireEvent.click(getByText('two'));
+          expect(getByTestId('two')).to.have.focus;
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowUp' });
+          expect(getByTestId('one')).to.have.focus;
+        });
+
+        it('moves focus to a parent', () => {
+          const { getByTestId, getByText } = render(
+            <TreeView defaultExpanded={['one']}>
+              <TreeItem nodeId="one" label="one" data-testid="one">
+                <TreeItem nodeId="two" label="two" data-testid="two" />
+              </TreeItem>
+            </TreeView>,
+          );
+
+          expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
+          fireEvent.click(getByText('two'));
+          expect(getByTestId('two')).to.have.focus;
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowUp' });
+          expect(getByTestId('one')).to.have.focus;
+        });
+
+        it("moves focus to a sibling's child", () => {
+          const { getByTestId, getByText } = render(
+            <TreeView defaultExpanded={['one']}>
+              <TreeItem nodeId="one" label="one" data-testid="one">
+                <TreeItem nodeId="two" label="two" data-testid="two" />
+              </TreeItem>
+              <TreeItem nodeId="three" label="three" data-testid="three" />
+            </TreeView>,
+          );
+
+          expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
+          fireEvent.click(getByText('three'));
+          expect(getByTestId('three')).to.have.focus;
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowUp' });
+          expect(getByTestId('two')).to.have.focus;
+        });
       });
 
-      it("should move focus to the node's parent node if focus is on a child node that is an end node", () => {
-        const { getByTestId, getByText } = render(
-          <TreeView defaultExpanded={['one']}>
-            <TreeItem nodeId="one" label="one" data-testid="one">
+      describe('home key interaction', () => {
+        it('moves focus to the first node in the tree', () => {
+          const { getByTestId, getByText } = render(
+            <TreeView>
+              <TreeItem nodeId="one" label="one" data-testid="one" />
               <TreeItem nodeId="two" label="two" data-testid="two" />
-            </TreeItem>
-          </TreeView>,
-        );
-
-        expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
-        fireEvent.click(getByText('two'));
-        fireEvent.keyDown(document.activeElement, { key: 'ArrowLeft' });
-        expect(getByTestId('one')).to.have.focus;
-        expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
+              <TreeItem nodeId="three" label="three" data-testid="three" />
+              <TreeItem nodeId="four" label="four" data-testid="four" />
+            </TreeView>,
+          );
+
+          fireEvent.click(getByText('four'));
+          expect(getByTestId('four')).to.have.focus;
+          fireEvent.keyDown(document.activeElement, { key: 'Home' });
+          expect(getByTestId('one')).to.have.focus;
+        });
       });
 
-      it("should move focus to the node's parent node if focus is on a child node that is closed", () => {
-        const { getByTestId, getByText } = render(
-          <TreeView>
-            <TreeItem nodeId="one" label="one" data-testid="one">
-              <TreeItem nodeId="two" label="two" data-testid="two">
-                <TreeItem nodeId="three" label="three" />
+      describe('end key interaction', () => {
+        it('moves focus to the last node in the tree without expanded items', () => {
+          const { getByTestId } = render(
+            <TreeView>
+              <TreeItem nodeId="one" label="one" data-testid="one" />
+              <TreeItem nodeId="two" label="two" data-testid="two" />
+              <TreeItem nodeId="three" label="three" data-testid="three" />
+              <TreeItem nodeId="four" label="four" data-testid="four" />
+            </TreeView>,
+          );
+
+          getByTestId('one').focus();
+          expect(getByTestId('one')).to.have.focus;
+          fireEvent.keyDown(document.activeElement, { key: 'End' });
+          expect(getByTestId('four')).to.have.focus;
+        });
+
+        it('moves focus to the last node in the tree with expanded items', () => {
+          const { getByTestId } = render(
+            <TreeView defaultExpanded={['four', 'five']}>
+              <TreeItem nodeId="one" label="one" data-testid="one" />
+              <TreeItem nodeId="two" label="two" data-testid="two" />
+              <TreeItem nodeId="three" label="three" data-testid="three" />
+              <TreeItem nodeId="four" label="four" data-testid="four">
+                <TreeItem nodeId="five" label="five" data-testid="five">
+                  <TreeItem nodeId="six" label="six" data-testid="six" />
+                </TreeItem>
               </TreeItem>
-            </TreeItem>
-          </TreeView>,
-        );
-
-        fireEvent.click(getByText('one'));
-        expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
-        // move focus to node two
-        fireEvent.click(getByText('two'));
-        fireEvent.click(getByText('two'));
-        expect(getByTestId('two')).to.have.attribute('aria-expanded', 'false');
-        fireEvent.keyDown(document.activeElement, { key: 'ArrowLeft' });
-        expect(getByTestId('one')).to.have.focus;
-        expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
-      });
-
-      it('should do nothing if focus is on a root node that is closed', () => {
-        const { getByTestId } = render(
-          <TreeView>
-            <TreeItem nodeId="one" label="one" data-testid="one">
-              <TreeItem nodeId="two" label="two" />
-            </TreeItem>
-          </TreeView>,
-        );
-
-        getByTestId('one').focus();
-        expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false');
-        fireEvent.keyDown(document.activeElement, { key: 'ArrowLeft' });
-        expect(getByTestId('one')).to.have.focus;
+            </TreeView>,
+          );
+
+          getByTestId('one').focus();
+          expect(getByTestId('one')).to.have.focus;
+          fireEvent.keyDown(document.activeElement, { key: 'End' });
+          expect(getByTestId('six')).to.have.focus;
+        });
       });
 
-      it('should do nothing if focus is on a root node that is an end node', () => {
-        const { getByTestId } = render(
-          <TreeView>
-            <TreeItem nodeId="one" label="one" data-testid="one" />
-          </TreeView>,
-        );
+      describe('type-ahead functionality', () => {
+        it('moves focus to the next node with a name that starts with the typed character', () => {
+          const { getByTestId } = render(
+            <TreeView>
+              <TreeItem nodeId="one" label="one" data-testid="one" />
+              <TreeItem nodeId="two" label={<span>two</span>} data-testid="two" />
+              <TreeItem nodeId="three" label="three" data-testid="three" />
+              <TreeItem nodeId="four" label="four" data-testid="four" />
+            </TreeView>,
+          );
 
-        getByTestId('one').focus();
-        fireEvent.keyDown(document.activeElement, { key: 'ArrowLeft' });
-        expect(getByTestId('one')).to.have.focus;
-      });
-    });
+          getByTestId('one').focus();
+          expect(getByTestId('one')).to.have.focus;
+          fireEvent.keyDown(document.activeElement, { key: 't' });
+          expect(getByTestId('two')).to.have.focus;
 
-    describe('down arrow interaction', () => {
-      it('moves focus to a non-nested sibling node', () => {
-        const { getByTestId } = render(
-          <TreeView>
-            <TreeItem nodeId="one" label="one" data-testid="one" />
-            <TreeItem nodeId="two" label="two" data-testid="two" />
-          </TreeView>,
-        );
+          fireEvent.keyDown(document.activeElement, { key: 'f' });
+          expect(getByTestId('four')).to.have.focus;
 
-        getByTestId('one').focus();
-        fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' });
-        expect(getByTestId('two')).to.have.focus;
-      });
+          fireEvent.keyDown(document.activeElement, { key: 'o' });
+          expect(getByTestId('one')).to.have.focus;
+        });
 
-      it('moves focus to a nested node', () => {
-        const { getByTestId } = render(
-          <TreeView defaultExpanded={['one']}>
-            <TreeItem nodeId="one" label="one" data-testid="one">
+        it('moves focus to the next node with the same starting character', () => {
+          const { getByTestId } = render(
+            <TreeView>
+              <TreeItem nodeId="one" label="one" data-testid="one" />
               <TreeItem nodeId="two" label="two" data-testid="two" />
-            </TreeItem>
-          </TreeView>,
-        );
+              <TreeItem nodeId="three" label="three" data-testid="three" />
+              <TreeItem nodeId="four" label="four" data-testid="four" />
+            </TreeView>,
+          );
 
-        expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
-        getByTestId('one').focus();
-        fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' });
-        expect(getByTestId('two')).to.have.focus;
-      });
+          getByTestId('one').focus();
+          expect(getByTestId('one')).to.have.focus;
+          fireEvent.keyDown(document.activeElement, { key: 't' });
+          expect(getByTestId('two')).to.have.focus;
 
-      it("moves focus to a parent's sibling", () => {
-        const { getByTestId, getByText } = render(
-          <TreeView defaultExpanded={['one']}>
-            <TreeItem nodeId="one" label="one" data-testid="one">
-              <TreeItem nodeId="two" label="two" data-testid="two" />
-            </TreeItem>
-            <TreeItem nodeId="three" label="three" data-testid="three" />
-          </TreeView>,
-        );
+          fireEvent.keyDown(document.activeElement, { key: 't' });
+          expect(getByTestId('three')).to.have.focus;
 
-        expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
-        fireEvent.click(getByText('two'));
-        expect(getByTestId('two')).to.have.focus;
-        fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' });
-        expect(getByTestId('three')).to.have.focus;
+          fireEvent.keyDown(document.activeElement, { key: 't' });
+          expect(getByTestId('two')).to.have.focus;
+        });
       });
-    });
-
-    describe('up arrow interaction', () => {
-      it('moves focus to a non-nested sibling node', () => {
-        const { getByTestId, getByText } = render(
-          <TreeView>
-            <TreeItem nodeId="one" label="one" data-testid="one" />
-            <TreeItem nodeId="two" label="two" data-testid="two" />
-          </TreeView>,
-        );
 
-        fireEvent.click(getByText('two'));
-        expect(getByTestId('two')).to.have.focus;
-        fireEvent.keyDown(document.activeElement, { key: 'ArrowUp' });
-        expect(getByTestId('one')).to.have.focus;
+      describe('asterisk key interaction', () => {
+        it('expands all siblings that are at the same level as the current node', () => {
+          const { getByTestId } = render(
+            <TreeView>
+              <TreeItem nodeId="one" label="one" data-testid="one">
+                <TreeItem nodeId="two" label="two" data-testid="two" />
+              </TreeItem>
+              <TreeItem nodeId="three" label="three" data-testid="three">
+                <TreeItem nodeId="four" label="four" data-testid="four" />
+              </TreeItem>
+              <TreeItem nodeId="five" label="five" data-testid="five">
+                <TreeItem nodeId="six" label="six" data-testid="six">
+                  <TreeItem nodeId="seven" label="seven" data-testid="seven" />
+                </TreeItem>
+              </TreeItem>
+            </TreeView>,
+          );
+
+          getByTestId('one').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(document.activeElement, { key: '*' });
+          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');
+        });
       });
+    });
 
-      it('moves focus to a parent', () => {
-        const { getByTestId, getByText } = render(
-          <TreeView defaultExpanded={['one']}>
-            <TreeItem nodeId="one" label="one" data-testid="one">
-              <TreeItem nodeId="two" label="two" data-testid="two" />
-            </TreeItem>
-          </TreeView>,
-        );
+    describe('Expansion', () => {
+      describe('enter key interaction', () => {
+        it('expands a node with children', () => {
+          const { getByTestId } = render(
+            <TreeView>
+              <TreeItem nodeId="one" label="one" data-testid="one">
+                <TreeItem nodeId="two" label="two" data-testid="two" />
+              </TreeItem>
+            </TreeView>,
+          );
 
-        expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
-        fireEvent.click(getByText('two'));
-        expect(getByTestId('two')).to.have.focus;
-        fireEvent.keyDown(document.activeElement, { key: 'ArrowUp' });
-        expect(getByTestId('one')).to.have.focus;
-      });
+          getByTestId('one').focus();
+          expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false');
+          fireEvent.keyDown(document.activeElement, { key: 'Enter' });
+          expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
+        });
 
-      it("moves focus to a sibling's child", () => {
-        const { getByTestId, getByText } = render(
-          <TreeView defaultExpanded={['one']}>
-            <TreeItem nodeId="one" label="one" data-testid="one">
-              <TreeItem nodeId="two" label="two" data-testid="two" />
-            </TreeItem>
-            <TreeItem nodeId="three" label="three" data-testid="three" />
-          </TreeView>,
-        );
-
-        expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
-        fireEvent.click(getByText('three'));
-        expect(getByTestId('three')).to.have.focus;
-        fireEvent.keyDown(document.activeElement, { key: 'ArrowUp' });
-        expect(getByTestId('two')).to.have.focus;
+        it('collapses a node with children', () => {
+          const { getByTestId, getByText } = render(
+            <TreeView>
+              <TreeItem nodeId="one" label="one" data-testid="one">
+                <TreeItem nodeId="two" label="two" data-testid="two" />
+              </TreeItem>
+            </TreeView>,
+          );
+
+          fireEvent.click(getByText('one'));
+          expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
+          fireEvent.keyDown(document.activeElement, { key: 'Enter' });
+          expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false');
+        });
       });
     });
 
-    describe('home key interaction', () => {
-      it('moves focus to the first node in the tree', () => {
-        const { getByTestId, getByText } = render(
-          <TreeView>
-            <TreeItem nodeId="one" label="one" data-testid="one" />
-            <TreeItem nodeId="two" label="two" data-testid="two" />
-            <TreeItem nodeId="three" label="three" data-testid="three" />
-            <TreeItem nodeId="four" label="four" data-testid="four" />
-          </TreeView>,
-        );
-
-        fireEvent.click(getByText('four'));
-        expect(getByTestId('four')).to.have.focus;
-        fireEvent.keyDown(document.activeElement, { key: 'Home' });
-        expect(getByTestId('one')).to.have.focus;
+    describe('Single Selection', () => {
+      describe('keyboard', () => {
+        it('selects a node', () => {
+          const { getByTestId } = render(
+            <TreeView>
+              <TreeItem nodeId="one" label="one" data-testid="one" />
+            </TreeView>,
+          );
+
+          getByTestId('one').focus();
+          expect(getByTestId('one')).to.have.attribute('aria-selected', 'false');
+          fireEvent.keyDown(document.activeElement, { key: ' ' });
+          expect(getByTestId('one')).to.have.attribute('aria-selected', 'true');
+        });
       });
-    });
-
-    describe('end key interaction', () => {
-      it('moves focus to the last node in the tree without expanded items', () => {
-        const { getByTestId } = render(
-          <TreeView>
-            <TreeItem nodeId="one" label="one" data-testid="one" />
-            <TreeItem nodeId="two" label="two" data-testid="two" />
-            <TreeItem nodeId="three" label="three" data-testid="three" />
-            <TreeItem nodeId="four" label="four" data-testid="four" />
-          </TreeView>,
-        );
 
-        getByTestId('one').focus();
-        expect(getByTestId('one')).to.have.focus;
-        fireEvent.keyDown(document.activeElement, { key: 'End' });
-        expect(getByTestId('four')).to.have.focus;
+      describe('mouse', () => {
+        it('selects a node', () => {
+          const { getByText, getByTestId } = render(
+            <TreeView>
+              <TreeItem nodeId="one" label="one" data-testid="one" />
+            </TreeView>,
+          );
+
+          expect(getByTestId('one')).to.have.attribute('aria-selected', 'false');
+          fireEvent.click(getByText('one'));
+          expect(getByTestId('one')).to.have.attribute('aria-selected', 'true');
+        });
       });
+    });
 
-      it('moves focus to the last node in the tree with expanded items', () => {
-        const { getByTestId } = render(
-          <TreeView defaultExpanded={['four', 'five']}>
-            <TreeItem nodeId="one" label="one" data-testid="one" />
-            <TreeItem nodeId="two" label="two" data-testid="two" />
-            <TreeItem nodeId="three" label="three" data-testid="three" />
-            <TreeItem nodeId="four" label="four" data-testid="four">
+    describe('Multi Selection', () => {
+      describe('range selection', () => {
+        specify('keyboard arrow', () => {
+          const { getByTestId, getByText, container } = render(
+            <TreeView multiSelect defaultExpanded={['two']}>
+              <TreeItem nodeId="one" label="one" data-testid="one" />
+              <TreeItem nodeId="two" label="two" data-testid="two" />
+              <TreeItem nodeId="three" label="three" data-testid="three" />
+              <TreeItem nodeId="four" label="four" data-testid="four" />
+              <TreeItem nodeId="five" label="five" data-testid="five" />
+            </TreeView>,
+          );
+
+          fireEvent.click(getByText('three'));
+          expect(getByTestId('three')).to.have.attribute('aria-selected', 'true');
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowDown', shiftKey: true });
+          expect(getByTestId('four')).to.have.focus;
+          expect(container.querySelectorAll('[aria-selected=true]').length).to.equal(2);
+          fireEvent.keyDown(document.activeElement, { 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(container.querySelectorAll('[aria-selected=true]').length).to.equal(3);
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowUp', shiftKey: true });
+          expect(getByTestId('four')).to.have.focus;
+          expect(container.querySelectorAll('[aria-selected=true]').length).to.equal(2);
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowUp', shiftKey: true });
+          expect(container.querySelectorAll('[aria-selected=true]').length).to.equal(1);
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowUp', shiftKey: true });
+          expect(container.querySelectorAll('[aria-selected=true]').length).to.equal(2);
+          fireEvent.keyDown(document.activeElement, { 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(container.querySelectorAll('[aria-selected=true]').length).to.equal(3);
+        });
+
+        specify('keyboard arrow merge', () => {
+          const { getByTestId, getByText, container } = render(
+            <TreeView multiSelect defaultExpanded={['two']}>
+              <TreeItem nodeId="one" label="one" data-testid="one" />
+              <TreeItem nodeId="two" label="two" data-testid="two" />
+              <TreeItem nodeId="three" label="three" data-testid="three" />
+              <TreeItem nodeId="four" label="four" data-testid="four" />
+              <TreeItem nodeId="five" label="five" data-testid="five" />
+              <TreeItem nodeId="six" label="six" data-testid="six" />
+            </TreeView>,
+          );
+
+          fireEvent.click(getByText('three'));
+          expect(getByTestId('three')).to.have.attribute('aria-selected', 'true');
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowUp', shiftKey: true });
+          fireEvent.click(getByText('six'), { ctrlKey: true });
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowUp', shiftKey: true });
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowUp', shiftKey: true });
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowUp', shiftKey: true });
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowUp', shiftKey: true });
+          expect(container.querySelectorAll('[aria-selected=true]').length).to.equal(5);
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowDown', shiftKey: true });
+          fireEvent.keyDown(document.activeElement, { key: 'ArrowDown', shiftKey: true });
+          expect(container.querySelectorAll('[aria-selected=true]').length).to.equal(3);
+        });
+
+        specify('keyboard space', () => {
+          const { getByTestId, getByText } = render(
+            <TreeView multiSelect defaultExpanded={['two']}>
+              <TreeItem nodeId="one" label="one" data-testid="one" />
+              <TreeItem nodeId="two" label="two" data-testid="two">
+                <TreeItem nodeId="three" label="three" data-testid="three" />
+                <TreeItem nodeId="four" label="four" data-testid="four" />
+              </TreeItem>
               <TreeItem nodeId="five" label="five" data-testid="five">
                 <TreeItem nodeId="six" label="six" data-testid="six" />
+                <TreeItem nodeId="seven" label="seven" data-testid="seven" />
               </TreeItem>
-            </TreeItem>
-          </TreeView>,
-        );
-
-        getByTestId('one').focus();
-        expect(getByTestId('one')).to.have.focus;
-        fireEvent.keyDown(document.activeElement, { key: 'End' });
-        expect(getByTestId('six')).to.have.focus;
+              <TreeItem nodeId="eight" label="eight" data-testid="eight" />
+              <TreeItem nodeId="nine" label="nine" data-testid="nine" />
+            </TreeView>,
+          );
+
+          fireEvent.click(getByText('five'));
+          for (let i = 0; i < 5; i += 1) {
+            fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' });
+          }
+          fireEvent.keyDown(document.activeElement, { 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(document.activeElement, { key: 'ArrowUp' });
+          }
+          fireEvent.keyDown(document.activeElement, { 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 { getByTestId } = render(
+            <TreeView multiSelect defaultExpanded={['two', 'five']}>
+              <TreeItem nodeId="one" label="one" data-testid="one" />
+              <TreeItem nodeId="two" label="two" data-testid="two">
+                <TreeItem nodeId="three" label="three" data-testid="three" />
+                <TreeItem nodeId="four" label="four" data-testid="four" />
+              </TreeItem>
+              <TreeItem nodeId="five" label="five" data-testid="five">
+                <TreeItem nodeId="six" label="six" data-testid="six" />
+                <TreeItem nodeId="seven" label="seven" data-testid="seven" />
+              </TreeItem>
+              <TreeItem nodeId="eight" label="eight" data-testid="eight" />
+              <TreeItem nodeId="nine" label="nine" data-testid="nine" />
+            </TreeView>,
+          );
+
+          getByTestId('five').focus();
+          fireEvent.keyDown(document.activeElement, { 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(document.activeElement, { 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('mouse', () => {
+          const { getByTestId, getByText } = render(
+            <TreeView multiSelect defaultExpanded={['two']}>
+              <TreeItem nodeId="one" label="one" data-testid="one" />
+              <TreeItem nodeId="two" label="two" data-testid="two">
+                <TreeItem nodeId="three" label="three" data-testid="three" />
+                <TreeItem nodeId="four" label="four" data-testid="four" />
+              </TreeItem>
+              <TreeItem nodeId="five" label="five" data-testid="five">
+                <TreeItem nodeId="six" label="six" data-testid="six" />
+                <TreeItem nodeId="seven" label="seven" data-testid="seven" />
+              </TreeItem>
+              <TreeItem nodeId="eight" label="eight" data-testid="eight" />
+              <TreeItem nodeId="nine" label="nine" data-testid="nine" />
+            </TreeView>,
+          );
+
+          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');
+        });
       });
-    });
 
-    describe('enter key interaction', () => {
-      it('expands a node with children', () => {
-        const { getByTestId } = render(
-          <TreeView>
-            <TreeItem nodeId="one" label="one" data-testid="one">
+      describe('multi selection', () => {
+        specify('keyboard', () => {
+          const { getByTestId } = render(
+            <TreeView multiSelect>
+              <TreeItem nodeId="one" label="one" data-testid="one" />
               <TreeItem nodeId="two" label="two" data-testid="two" />
-            </TreeItem>
-          </TreeView>,
-        );
-
-        getByTestId('one').focus();
-        expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false');
-        fireEvent.keyDown(document.activeElement, { key: 'Enter' });
-        expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
-      });
-
-      it('collapses a node with children', () => {
-        const { getByTestId, getByText } = render(
-          <TreeView>
-            <TreeItem nodeId="one" label="one" data-testid="one">
+            </TreeView>,
+          );
+
+          getByTestId('one').focus();
+          expect(getByTestId('one')).to.have.attribute('aria-selected', 'false');
+          expect(getByTestId('two')).to.have.attribute('aria-selected', 'false');
+          fireEvent.keyDown(document.activeElement, { key: ' ' });
+          expect(getByTestId('one')).to.have.attribute('aria-selected', 'true');
+          expect(getByTestId('two')).to.have.attribute('aria-selected', 'false');
+          getByTestId('two').focus();
+          fireEvent.keyDown(document.activeElement, { key: ' ', ctrlKey: true });
+          expect(getByTestId('one')).to.have.attribute('aria-selected', 'true');
+          expect(getByTestId('two')).to.have.attribute('aria-selected', 'true');
+        });
+
+        specify('mouse using ctrl', () => {
+          const { getByTestId, getByText } = render(
+            <TreeView multiSelect>
+              <TreeItem nodeId="one" label="one" data-testid="one" />
               <TreeItem nodeId="two" label="two" data-testid="two" />
-            </TreeItem>
-          </TreeView>,
-        );
-
-        fireEvent.click(getByText('one'));
-        expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
-        fireEvent.keyDown(document.activeElement, { key: 'Enter' });
-        expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false');
-      });
-    });
-
-    describe('type-ahead functionality', () => {
-      it('moves focus to the next node with a name that starts with the typed character', () => {
-        const { getByTestId } = render(
-          <TreeView>
-            <TreeItem nodeId="one" label="one" data-testid="one" />
-            <TreeItem nodeId="two" label={<span>two</span>} data-testid="two" />
-            <TreeItem nodeId="three" label="three" data-testid="three" />
-            <TreeItem nodeId="four" label="four" data-testid="four" />
-          </TreeView>,
-        );
-
-        getByTestId('one').focus();
-        expect(getByTestId('one')).to.have.focus;
-        fireEvent.keyDown(document.activeElement, { key: 't' });
-        expect(getByTestId('two')).to.have.focus;
-
-        fireEvent.keyDown(document.activeElement, { key: 'f' });
-        expect(getByTestId('four')).to.have.focus;
-
-        fireEvent.keyDown(document.activeElement, { key: 'o' });
-        expect(getByTestId('one')).to.have.focus;
+            </TreeView>,
+          );
+
+          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(
+            <TreeView multiSelect>
+              <TreeItem nodeId="one" label="one" data-testid="one" />
+              <TreeItem nodeId="two" label="two" data-testid="two" />
+            </TreeView>,
+          );
+
+          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');
+        });
       });
 
-      it('moves focus to the next node with the same starting character', () => {
-        const { getByTestId } = render(
-          <TreeView>
+      specify('ctrl + a selects all', () => {
+        const { getByTestId, container } = render(
+          <TreeView multiSelect>
             <TreeItem nodeId="one" label="one" data-testid="one" />
             <TreeItem nodeId="two" label="two" data-testid="two" />
             <TreeItem nodeId="three" label="three" data-testid="three" />
             <TreeItem nodeId="four" label="four" data-testid="four" />
+            <TreeItem nodeId="five" label="five" data-testid="five" />
           </TreeView>,
         );
 
         getByTestId('one').focus();
-        expect(getByTestId('one')).to.have.focus;
-        fireEvent.keyDown(document.activeElement, { key: 't' });
-        expect(getByTestId('two')).to.have.focus;
-
-        fireEvent.keyDown(document.activeElement, { key: 't' });
-        expect(getByTestId('three')).to.have.focus;
-
-        fireEvent.keyDown(document.activeElement, { key: 't' });
-        expect(getByTestId('two')).to.have.focus;
-      });
-    });
-
-    describe('asterisk key interaction', () => {
-      it('expands all siblings that are at the same level as the current node', () => {
-        const { getByTestId } = render(
-          <TreeView>
-            <TreeItem nodeId="one" label="one" data-testid="one">
-              <TreeItem nodeId="two" label="two" data-testid="two" />
-            </TreeItem>
-            <TreeItem nodeId="three" label="three" data-testid="three">
-              <TreeItem nodeId="four" label="four" data-testid="four" />
-            </TreeItem>
-            <TreeItem nodeId="five" label="five" data-testid="five">
-              <TreeItem nodeId="six" label="six" data-testid="six">
-                <TreeItem nodeId="seven" label="seven" data-testid="seven" />
-              </TreeItem>
-            </TreeItem>
-          </TreeView>,
-        );
-
-        getByTestId('one').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(document.activeElement, { key: '*' });
-        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');
+        fireEvent.keyDown(document.activeElement, { key: 'a', ctrlKey: true });
+        expect(container.querySelectorAll('[aria-selected=true]').length).to.equal(5);
       });
     });
   });
diff --git a/packages/material-ui-lab/src/TreeView/TreeView.test.js b/packages/material-ui-lab/src/TreeView/TreeView.test.js
--- a/packages/material-ui-lab/src/TreeView/TreeView.test.js
+++ b/packages/material-ui-lab/src/TreeView/TreeView.test.js
@@ -37,7 +37,7 @@ describe('<TreeView />', () => {
       consoleErrorMock.reset();
     });
 
-    it('should warn when switching from controlled to uncontrolled', () => {
+    it('should warn when switching from controlled to uncontrolled of the expanded prop', () => {
       const { setProps } = render(
         <TreeView expanded={[]}>
           <TreeItem nodeId="1" label="one" />
@@ -46,12 +46,25 @@ describe('<TreeView />', () => {
 
       setProps({ expanded: undefined });
       expect(consoleErrorMock.args()[0][0]).to.include(
-        'A component is changing a controlled TreeView to be uncontrolled.',
+        'A component is changing a controlled TreeView to be uncontrolled',
+      );
+    });
+
+    it('should warn when switching from controlled to uncontrolled of the selected prop', () => {
+      const { setProps } = render(
+        <TreeView selected={[]}>
+          <TreeItem nodeId="1" label="one" />
+        </TreeView>,
+      );
+
+      setProps({ selected: undefined });
+      expect(consoleErrorMock.args()[0][0]).to.include(
+        'A component is changing a controlled TreeView to be uncontrolled',
       );
     });
   });
 
-  it('should be able to be controlled', () => {
+  it('should be able to be controlled with the expanded prop', () => {
     function MyComponent() {
       const [expandedState, setExpandedState] = React.useState([]);
       const handleNodeToggle = (event, nodes) => {
@@ -77,6 +90,58 @@ describe('<TreeView />', () => {
     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 (
+        <TreeView selected={selectedState} onNodeSelect={handleNodeSelect}>
+          <TreeItem nodeId="1" label="one" data-testid="one" />
+          <TreeItem nodeId="2" label="two" data-testid="two" />
+        </TreeView>
+      );
+    }
+
+    const { getByTestId, getByText } = render(<MyComponent />);
+
+    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');
+  });
+
+  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 (
+        <TreeView selected={selectedState} onNodeSelect={handleNodeSelect} multiSelect>
+          <TreeItem nodeId="1" label="one" data-testid="one" />
+          <TreeItem nodeId="2" label="two" data-testid="two" />
+        </TreeView>
+      );
+    }
+
+    const { getByTestId, getByText } = render(<MyComponent />);
+
+    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);
@@ -134,5 +199,17 @@ describe('<TreeView />', () => {
 
       expect(getByRole('tree')).to.be.ok;
     });
+
+    it('(TreeView) should have the attribute `aria-multiselectable=false if using single select`', () => {
+      const { getByRole } = render(<TreeView />);
+
+      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(<TreeView multiSelect />);
+
+      expect(getByRole('tree')).to.have.attribute('aria-multiselectable', 'true');
+    });
   });
 });