diff --git a/docs/data/pages.ts b/docs/data/pages.ts
index d8fe2a79ce4f1..97bf3da4366a4 100644
--- a/docs/data/pages.ts
+++ b/docs/data/pages.ts
@@ -491,6 +491,7 @@ const pages: MuiPage[] = [
{ pathname: '/x/react-tree-view/simple-tree-view/selection' },
{ pathname: '/x/react-tree-view/simple-tree-view/expansion' },
{ pathname: '/x/react-tree-view/simple-tree-view/customization' },
+ { pathname: '/x/react-tree-view/simple-tree-view/focus' },
],
},
{
@@ -500,6 +501,7 @@ const pages: MuiPage[] = [
{ pathname: '/x/react-tree-view/rich-tree-view/items' },
{ pathname: '/x/react-tree-view/rich-tree-view/selection' },
{ pathname: '/x/react-tree-view/rich-tree-view/expansion' },
+ { pathname: '/x/react-tree-view/rich-tree-view/focus' },
],
},
{
diff --git a/docs/data/tree-view/rich-tree-view/focus/FocusedRichTreeView.js b/docs/data/tree-view/rich-tree-view/focus/FocusedRichTreeView.js
new file mode 100644
index 0000000000000..b55bbc89843cf
--- /dev/null
+++ b/docs/data/tree-view/rich-tree-view/focus/FocusedRichTreeView.js
@@ -0,0 +1,54 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import Button from '@mui/material/Button';
+import { RichTreeView } from '@mui/x-tree-view/RichTreeView';
+
+import { useTreeViewApiRef } from '@mui/x-tree-view/hooks/useTreeViewApiRef';
+
+const MUI_X_PRODUCTS = [
+ {
+ id: 'grid',
+ label: 'Data Grid',
+ children: [
+ { id: 'grid-community', label: '@mui/x-data-grid' },
+ { id: 'grid-pro', label: '@mui/x-data-grid-pro' },
+ { id: 'grid-premium', label: '@mui/x-data-grid-premium' },
+ ],
+ },
+ {
+ id: 'pickers',
+ label: 'Date and Time Pickers',
+ children: [
+ { id: 'pickers-community', label: '@mui/x-date-pickers' },
+ { id: 'pickers-pro', label: '@mui/x-date-pickers-pro' },
+ ],
+ },
+ {
+ id: 'charts',
+ label: 'Charts',
+ children: [{ id: 'charts-community', label: '@mui/x-charts' }],
+ },
+ {
+ id: 'tree-view',
+ label: 'Tree View',
+ children: [{ id: 'tree-view-community', label: '@mui/x-tree-view' }],
+ },
+];
+
+export default function FocusedRichTreeView() {
+ const apiRef = useTreeViewApiRef();
+ const handleButtonClick = (event) => {
+ apiRef.current?.focusNode(event, 'pickers');
+ };
+
+ return (
+
+
+ Focus pickers node
+
+
+
+
+
+ );
+}
diff --git a/docs/data/tree-view/rich-tree-view/focus/FocusedRichTreeView.tsx b/docs/data/tree-view/rich-tree-view/focus/FocusedRichTreeView.tsx
new file mode 100644
index 0000000000000..5d83b6cec1162
--- /dev/null
+++ b/docs/data/tree-view/rich-tree-view/focus/FocusedRichTreeView.tsx
@@ -0,0 +1,54 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import Button from '@mui/material/Button';
+import { RichTreeView } from '@mui/x-tree-view/RichTreeView';
+import { TreeViewBaseItem } from '@mui/x-tree-view/models';
+import { useTreeViewApiRef } from '@mui/x-tree-view/hooks/useTreeViewApiRef';
+
+const MUI_X_PRODUCTS: TreeViewBaseItem[] = [
+ {
+ id: 'grid',
+ label: 'Data Grid',
+ children: [
+ { id: 'grid-community', label: '@mui/x-data-grid' },
+ { id: 'grid-pro', label: '@mui/x-data-grid-pro' },
+ { id: 'grid-premium', label: '@mui/x-data-grid-premium' },
+ ],
+ },
+ {
+ id: 'pickers',
+ label: 'Date and Time Pickers',
+ children: [
+ { id: 'pickers-community', label: '@mui/x-date-pickers' },
+ { id: 'pickers-pro', label: '@mui/x-date-pickers-pro' },
+ ],
+ },
+ {
+ id: 'charts',
+ label: 'Charts',
+ children: [{ id: 'charts-community', label: '@mui/x-charts' }],
+ },
+ {
+ id: 'tree-view',
+ label: 'Tree View',
+ children: [{ id: 'tree-view-community', label: '@mui/x-tree-view' }],
+ },
+];
+
+export default function FocusedRichTreeView() {
+ const apiRef = useTreeViewApiRef();
+ const handleButtonClick = (event: React.SyntheticEvent) => {
+ apiRef.current?.focusNode(event, 'pickers');
+ };
+
+ return (
+
+
+ Focus pickers node
+
+
+
+
+
+ );
+}
diff --git a/docs/data/tree-view/rich-tree-view/focus/FocusedRichTreeView.tsx.preview b/docs/data/tree-view/rich-tree-view/focus/FocusedRichTreeView.tsx.preview
new file mode 100644
index 0000000000000..bc63fee6e7fc3
--- /dev/null
+++ b/docs/data/tree-view/rich-tree-view/focus/FocusedRichTreeView.tsx.preview
@@ -0,0 +1,6 @@
+
+ Focus pickers node
+
+
+
+
\ No newline at end of file
diff --git a/docs/data/tree-view/rich-tree-view/focus/focus.md b/docs/data/tree-view/rich-tree-view/focus/focus.md
new file mode 100644
index 0000000000000..1f201af613a9a
--- /dev/null
+++ b/docs/data/tree-view/rich-tree-view/focus/focus.md
@@ -0,0 +1,36 @@
+---
+productId: x-tree-view
+title: Rich Tree View - Focus
+components: RichTreeView, TreeItem
+packageName: '@mui/x-tree-view'
+githubLabel: 'component: tree view'
+waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/
+---
+
+# Rich Tree View - Focus
+
+
Learn how to focus Tree View items.
+
+## Focus a specific node
+
+You can use the the `apiRef.focusNode` method to focus a specific node.
+This methods receives two parameters: `event` and `nodeId`.
+
+:::success
+To use the `apiRef` object, you need to initialize it using the `useTreeViewApiRef` hook as follows:
+
+```tsx
+const apiRef = useTreeViewApiRef();
+
+return ;
+```
+
+`apiRef` will be undefined during the first render and will then contain methods allowing you to imperatively interact with the Tree View.
+:::
+
+:::info
+This method only works with nodes that are currently visible.
+Calling `apiRef.focusNode` on a node whose parent is collapsed will do nothing.
+:::
+
+{{"demo": "FocusedRichTreeView.js"}}
diff --git a/docs/data/tree-view/simple-tree-view/focus/FocusedSimpleTreeView.js b/docs/data/tree-view/simple-tree-view/focus/FocusedSimpleTreeView.js
new file mode 100644
index 0000000000000..ae3dc438a6970
--- /dev/null
+++ b/docs/data/tree-view/simple-tree-view/focus/FocusedSimpleTreeView.js
@@ -0,0 +1,40 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import Button from '@mui/material/Button';
+import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView';
+import { TreeItem } from '@mui/x-tree-view/TreeItem';
+import { useTreeViewApiRef } from '@mui/x-tree-view/hooks/useTreeViewApiRef';
+
+export default function FocusedSimpleTreeView() {
+ const apiRef = useTreeViewApiRef();
+ const handleButtonClick = (event) => {
+ apiRef.current?.focusNode(event, 'pickers');
+ };
+
+ return (
+
+
+ Focus pickers node
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/data/tree-view/simple-tree-view/focus/FocusedSimpleTreeView.tsx b/docs/data/tree-view/simple-tree-view/focus/FocusedSimpleTreeView.tsx
new file mode 100644
index 0000000000000..4f2e6eac445de
--- /dev/null
+++ b/docs/data/tree-view/simple-tree-view/focus/FocusedSimpleTreeView.tsx
@@ -0,0 +1,40 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import Button from '@mui/material/Button';
+import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView';
+import { TreeItem } from '@mui/x-tree-view/TreeItem';
+import { useTreeViewApiRef } from '@mui/x-tree-view/hooks/useTreeViewApiRef';
+
+export default function FocusedSimpleTreeView() {
+ const apiRef = useTreeViewApiRef();
+ const handleButtonClick = (event: React.SyntheticEvent) => {
+ apiRef.current?.focusNode(event, 'pickers');
+ };
+
+ return (
+
+
+ Focus pickers node
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/data/tree-view/simple-tree-view/focus/focus.md b/docs/data/tree-view/simple-tree-view/focus/focus.md
new file mode 100644
index 0000000000000..9f85933666c05
--- /dev/null
+++ b/docs/data/tree-view/simple-tree-view/focus/focus.md
@@ -0,0 +1,36 @@
+---
+productId: x-tree-view
+title: Simple Tree View - Focus
+components: SimpleTreeView, TreeItem
+packageName: '@mui/x-tree-view'
+githubLabel: 'component: tree view'
+waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/
+---
+
+# Simple Tree View - Focus
+
+Learn how to focus Tree View items.
+
+## Focus a specific node
+
+You can use the the `apiRef.focusNode` method to focus a specific node.
+This methods receives two parameters: `event` and `nodeId`.
+
+:::success
+To use the `apiRef` object, you need to initialize it using the `useTreeViewApiRef` hook as follows:
+
+```tsx
+const apiRef = useTreeViewApiRef();
+
+return {children} ;
+```
+
+`apiRef` will be undefined during the first render and will then contain methods allowing you to imperatively interact with the Tree View.
+:::
+
+:::info
+This method only works with nodes that are currently visible.
+Calling `apiRef.focusNode` on a node whose parent is collapsed will do nothing.
+:::
+
+{{"demo": "FocusedSimpleTreeView.js"}}
diff --git a/docs/pages/x/api/tree-view/rich-tree-view.json b/docs/pages/x/api/tree-view/rich-tree-view.json
index 891044b6b1873..ab76e0f2953bf 100644
--- a/docs/pages/x/api/tree-view/rich-tree-view.json
+++ b/docs/pages/x/api/tree-view/rich-tree-view.json
@@ -1,5 +1,6 @@
{
"props": {
+ "apiRef": { "type": { "name": "shape", "description": "{ current?: { focusNode: func } }" } },
"classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } },
"defaultExpandedNodes": {
"type": { "name": "arrayOf", "description": "Array<string>" },
@@ -128,6 +129,6 @@
"forwardsRefTo": "HTMLUListElement",
"filename": "/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx",
"inheritance": null,
- "demos": "",
+ "demos": "",
"cssComponent": false
}
diff --git a/docs/pages/x/api/tree-view/simple-tree-view.json b/docs/pages/x/api/tree-view/simple-tree-view.json
index f24417dee5b0d..10a3eae04caab 100644
--- a/docs/pages/x/api/tree-view/simple-tree-view.json
+++ b/docs/pages/x/api/tree-view/simple-tree-view.json
@@ -1,5 +1,6 @@
{
"props": {
+ "apiRef": { "type": { "name": "shape", "description": "{ current?: { focusNode: func } }" } },
"children": { "type": { "name": "node" } },
"classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } },
"defaultExpandedNodes": {
@@ -93,6 +94,6 @@
"forwardsRefTo": "HTMLUListElement",
"filename": "/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx",
"inheritance": null,
- "demos": "",
+ "demos": "",
"cssComponent": false
}
diff --git a/docs/pages/x/api/tree-view/tree-item.json b/docs/pages/x/api/tree-view/tree-item.json
index 81a9ed2ce85bb..8b7bef6f16cc8 100644
--- a/docs/pages/x/api/tree-view/tree-item.json
+++ b/docs/pages/x/api/tree-view/tree-item.json
@@ -104,6 +104,6 @@
"forwardsRefTo": "HTMLLIElement",
"filename": "/packages/x-tree-view/src/TreeItem/TreeItem.tsx",
"inheritance": null,
- "demos": "",
+ "demos": "",
"cssComponent": false
}
diff --git a/docs/pages/x/api/tree-view/tree-view.json b/docs/pages/x/api/tree-view/tree-view.json
index 5eb73fe0d4ad9..d15094b303d86 100644
--- a/docs/pages/x/api/tree-view/tree-view.json
+++ b/docs/pages/x/api/tree-view/tree-view.json
@@ -1,5 +1,6 @@
{
"props": {
+ "apiRef": { "type": { "name": "shape", "description": "{ current?: { focusNode: func } }" } },
"children": { "type": { "name": "node" } },
"classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } },
"defaultExpandedNodes": {
diff --git a/docs/pages/x/react-tree-view/rich-tree-view/focus.js b/docs/pages/x/react-tree-view/rich-tree-view/focus.js
new file mode 100644
index 0000000000000..6b2b63c962650
--- /dev/null
+++ b/docs/pages/x/react-tree-view/rich-tree-view/focus.js
@@ -0,0 +1,7 @@
+import * as React from 'react';
+import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs';
+import * as pageProps from 'docsx/data/tree-view/rich-tree-view/focus/focus.md?@mui/markdown';
+
+export default function Page() {
+ return ;
+}
diff --git a/docs/pages/x/react-tree-view/simple-tree-view/focus.js b/docs/pages/x/react-tree-view/simple-tree-view/focus.js
new file mode 100644
index 0000000000000..a545ed30a37ff
--- /dev/null
+++ b/docs/pages/x/react-tree-view/simple-tree-view/focus.js
@@ -0,0 +1,7 @@
+import * as React from 'react';
+import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs';
+import * as pageProps from 'docsx/data/tree-view/simple-tree-view/focus/focus.md?@mui/markdown';
+
+export default function Page() {
+ return ;
+}
diff --git a/docs/translations/api-docs/tree-view/rich-tree-view/rich-tree-view.json b/docs/translations/api-docs/tree-view/rich-tree-view/rich-tree-view.json
index aa886207beaa2..883829d9ab0d7 100644
--- a/docs/translations/api-docs/tree-view/rich-tree-view/rich-tree-view.json
+++ b/docs/translations/api-docs/tree-view/rich-tree-view/rich-tree-view.json
@@ -1,6 +1,9 @@
{
"componentDescription": "",
"propDescriptions": {
+ "apiRef": {
+ "description": "The ref object that allows Tree View manipulation. Can be instantiated with useTreeViewApiRef()
."
+ },
"classes": { "description": "Override or extend the styles applied to the component." },
"defaultExpandedNodes": {
"description": "Expanded node ids. Used when the item's expansion is not controlled."
diff --git a/docs/translations/api-docs/tree-view/simple-tree-view/simple-tree-view.json b/docs/translations/api-docs/tree-view/simple-tree-view/simple-tree-view.json
index cda74e515ac52..2cb82f5e46440 100644
--- a/docs/translations/api-docs/tree-view/simple-tree-view/simple-tree-view.json
+++ b/docs/translations/api-docs/tree-view/simple-tree-view/simple-tree-view.json
@@ -1,6 +1,9 @@
{
"componentDescription": "",
"propDescriptions": {
+ "apiRef": {
+ "description": "The ref object that allows Tree View manipulation. Can be instantiated with useTreeViewApiRef()
."
+ },
"children": { "description": "The content of the component." },
"classes": { "description": "Override or extend the styles applied to the component." },
"defaultExpandedNodes": {
diff --git a/docs/translations/api-docs/tree-view/tree-view/tree-view.json b/docs/translations/api-docs/tree-view/tree-view/tree-view.json
index e07f766020c0b..739dc69511641 100644
--- a/docs/translations/api-docs/tree-view/tree-view/tree-view.json
+++ b/docs/translations/api-docs/tree-view/tree-view/tree-view.json
@@ -1,6 +1,9 @@
{
"componentDescription": "This component has been deprecated in favor of the new `SimpleTreeView` component.\nYou can have a look at how to migrate to the new component in the v7 [migration guide](https://next.mui.com/x/migration/migration-tree-view-v6/#use-simpletreeview-instead-of-treeview)",
"propDescriptions": {
+ "apiRef": {
+ "description": "The ref object that allows Tree View manipulation. Can be instantiated with useTreeViewApiRef()
."
+ },
"children": { "description": "The content of the component." },
"classes": { "description": "Override or extend the styles applied to the component." },
"defaultExpandedNodes": {
diff --git a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx
index 6ae6d863ce945..c05701e690841 100644
--- a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx
+++ b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx
@@ -146,6 +146,14 @@ RichTreeView.propTypes = {
// | These PropTypes are generated from the TypeScript type definitions |
// | To update them edit the TypeScript types and run "yarn proptypes" |
// ----------------------------------------------------------------------
+ /**
+ * The ref object that allows Tree View manipulation. Can be instantiated with `useTreeViewApiRef()`.
+ */
+ apiRef: PropTypes.shape({
+ current: PropTypes.shape({
+ focusNode: PropTypes.func.isRequired,
+ }),
+ }),
/**
* Override or extend the styles applied to the component.
*/
diff --git a/packages/x-tree-view/src/RichTreeView/RichTreeView.types.ts b/packages/x-tree-view/src/RichTreeView/RichTreeView.types.ts
index 21625d9d946ef..124821c8aaf15 100644
--- a/packages/x-tree-view/src/RichTreeView/RichTreeView.types.ts
+++ b/packages/x-tree-view/src/RichTreeView/RichTreeView.types.ts
@@ -7,9 +7,11 @@ import {
DefaultTreeViewPluginParameters,
DefaultTreeViewPluginSlotProps,
DefaultTreeViewPluginSlots,
+ DefaultTreeViewPlugins,
} from '../internals/plugins/defaultPlugins';
import { TreeItem, TreeItemProps } from '../TreeItem';
import { TreeViewItemId } from '../models';
+import { TreeViewPublicAPI } from '../internals/models';
interface RichTreeViewItemSlotOwnerState {
nodeId: TreeViewItemId;
@@ -35,6 +37,10 @@ export interface RichTreeViewSlotProps;
}
+export type RichTreeViewApiRef = React.MutableRefObject<
+ TreeViewPublicAPI | undefined
+>;
+
export interface RichTreeViewPropsBase extends React.HTMLAttributes {
className?: string;
/**
@@ -60,4 +66,8 @@ export interface RichTreeViewProps;
+ /**
+ * The ref object that allows Tree View manipulation. Can be instantiated with `useTreeViewApiRef()`.
+ */
+ apiRef?: RichTreeViewApiRef;
}
diff --git a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.test.tsx b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.test.tsx
index b49c3f3f3d790..e0d149a6d5e71 100644
--- a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.test.tsx
+++ b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.test.tsx
@@ -6,6 +6,8 @@ import Portal from '@mui/material/Portal';
import { SimpleTreeView, simpleTreeViewClasses as classes } from '@mui/x-tree-view/SimpleTreeView';
import { TreeItem } from '@mui/x-tree-view/TreeItem';
import { describeConformance } from 'test/utils/describeConformance';
+import { useTreeViewApiRef } from '../hooks';
+import { SimpleTreeViewApiRef } from './SimpleTreeView.types';
describe(' ', () => {
const { render } = createRenderer();
@@ -531,6 +533,57 @@ describe(' ', () => {
expect(onNodeFocus.lastCall.lastArg).to.equal('1');
});
+
+ it('should focus specific node using `apiRef`', () => {
+ let apiRef: SimpleTreeViewApiRef;
+ const onNodeFocus = spy();
+
+ function TestCase() {
+ apiRef = useTreeViewApiRef();
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ const { getByRole } = render( );
+
+ act(() => {
+ apiRef.current?.focusNode({} as React.SyntheticEvent, '2');
+ });
+
+ expect(getByRole('tree')).toHaveFocus();
+ expect(onNodeFocus.lastCall.lastArg).to.equal('2');
+ });
+
+ it('should not focus node if parent is collapsed', () => {
+ let apiRef: SimpleTreeViewApiRef;
+ const onNodeFocus = spy();
+
+ function TestCase() {
+ apiRef = useTreeViewApiRef();
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ const { getByRole } = render( );
+
+ act(() => {
+ apiRef.current?.focusNode({} as React.SyntheticEvent, '1.1');
+ });
+
+ expect(getByRole('tree')).not.toHaveFocus();
+ });
});
describe('Accessibility', () => {
diff --git a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx
index 7ccff7c09ec61..7a00d05f19bda 100644
--- a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx
+++ b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx
@@ -109,6 +109,14 @@ SimpleTreeView.propTypes = {
// | These PropTypes are generated from the TypeScript type definitions |
// | To update them edit the TypeScript types and run "yarn proptypes" |
// ----------------------------------------------------------------------
+ /**
+ * The ref object that allows Tree View manipulation. Can be instantiated with `useTreeViewApiRef()`.
+ */
+ apiRef: PropTypes.shape({
+ current: PropTypes.shape({
+ focusNode: PropTypes.func.isRequired,
+ }),
+ }),
/**
* The content of the component.
*/
diff --git a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.types.ts b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.types.ts
index c351aeb87e39d..b5be79437a156 100644
--- a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.types.ts
+++ b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.types.ts
@@ -7,7 +7,9 @@ import {
SimpleTreeViewPluginParameters,
SimpleTreeViewPluginSlotProps,
SimpleTreeViewPluginSlots,
+ SimpleTreeViewPlugins,
} from './SimpleTreeView.plugins';
+import { TreeViewPublicAPI } from '../internals/models';
export interface SimpleTreeViewSlots extends SimpleTreeViewPluginSlots {
/**
@@ -21,6 +23,10 @@ export interface SimpleTreeViewSlotProps extends SimpleTreeViewPluginSlotProps {
root?: SlotComponentProps<'ul', {}, {}>;
}
+export type SimpleTreeViewApiRef = React.MutableRefObject<
+ TreeViewPublicAPI | undefined
+>;
+
export interface SimpleTreeViewProps
extends SimpleTreeViewPluginParameters,
React.HTMLAttributes {
@@ -45,4 +51,8 @@ export interface SimpleTreeViewProps
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx?: SxProps;
+ /**
+ * The ref object that allows Tree View manipulation. Can be instantiated with `useTreeViewApiRef()`.
+ */
+ apiRef?: SimpleTreeViewApiRef;
}
diff --git a/packages/x-tree-view/src/TreeView/TreeView.tsx b/packages/x-tree-view/src/TreeView/TreeView.tsx
index a17d55405f3ff..814b58c494ae6 100644
--- a/packages/x-tree-view/src/TreeView/TreeView.tsx
+++ b/packages/x-tree-view/src/TreeView/TreeView.tsx
@@ -86,6 +86,14 @@ TreeView.propTypes = {
// | These PropTypes are generated from the TypeScript type definitions |
// | To update them edit the TypeScript types and run "yarn proptypes" |
// ----------------------------------------------------------------------
+ /**
+ * The ref object that allows Tree View manipulation. Can be instantiated with `useTreeViewApiRef()`.
+ */
+ apiRef: PropTypes.shape({
+ current: PropTypes.shape({
+ focusNode: PropTypes.func.isRequired,
+ }),
+ }),
/**
* The content of the component.
*/
diff --git a/packages/x-tree-view/src/hooks/index.ts b/packages/x-tree-view/src/hooks/index.ts
new file mode 100644
index 0000000000000..e07d07a202b5a
--- /dev/null
+++ b/packages/x-tree-view/src/hooks/index.ts
@@ -0,0 +1 @@
+export { useTreeViewApiRef } from './useTreeViewApiRef';
diff --git a/packages/x-tree-view/src/hooks/useTreeViewApiRef.tsx b/packages/x-tree-view/src/hooks/useTreeViewApiRef.tsx
new file mode 100644
index 0000000000000..f003fe5f124bb
--- /dev/null
+++ b/packages/x-tree-view/src/hooks/useTreeViewApiRef.tsx
@@ -0,0 +1,10 @@
+import * as React from 'react';
+import { TreeViewAnyPluginSignature, TreeViewUsedPublicAPI } from '../internals/models';
+
+/**
+ * Hook that instantiates a [[TreeViewApiRef]].
+ */
+export const useTreeViewApiRef = <
+ T extends TreeViewAnyPluginSignature,
+ Api extends TreeViewUsedPublicAPI,
+>() => React.useRef(undefined) as React.MutableRefObject;
diff --git a/packages/x-tree-view/src/index.ts b/packages/x-tree-view/src/index.ts
index d19660c797110..15599f35a6aba 100644
--- a/packages/x-tree-view/src/index.ts
+++ b/packages/x-tree-view/src/index.ts
@@ -6,3 +6,5 @@ export { unstable_resetCleanupTracking } from './internals/hooks/useInstanceEven
export * from './models';
export * from './icons';
+
+export * from './hooks';
diff --git a/packages/x-tree-view/src/internals/models/helpers.ts b/packages/x-tree-view/src/internals/models/helpers.ts
index 127a5e29cb679..de88ea87b8f6f 100644
--- a/packages/x-tree-view/src/internals/models/helpers.ts
+++ b/packages/x-tree-view/src/internals/models/helpers.ts
@@ -35,6 +35,7 @@ export type ConvertPluginsIntoSignatures =
export interface MergePlugins {
state: MergePluginsProperty;
instance: MergePluginsProperty;
+ publicAPI: MergePluginsProperty;
params: MergePluginsProperty;
defaultizedParams: MergePluginsProperty;
dependantPlugins: MergePluginsProperty;
diff --git a/packages/x-tree-view/src/internals/models/plugin.ts b/packages/x-tree-view/src/internals/models/plugin.ts
index d735f430eb53b..9ffae62154798 100644
--- a/packages/x-tree-view/src/internals/models/plugin.ts
+++ b/packages/x-tree-view/src/internals/models/plugin.ts
@@ -8,6 +8,7 @@ import type { TreeItemProps } from '../../TreeItem';
export interface TreeViewPluginOptions {
instance: TreeViewUsedInstance;
+ publicAPI: TreeViewUsedPublicAPI;
params: TreeViewUsedDefaultizedParams;
state: TreeViewUsedState;
slots: TSignature['slots'];
@@ -36,6 +37,7 @@ export type TreeViewPluginSignature<
params?: {};
defaultizedParams?: {};
instance?: {};
+ publicAPI?: {};
events?: { [key in keyof T['events']]: TreeViewEventLookupElement };
state?: {};
contextValue?: {};
@@ -48,6 +50,7 @@ export type TreeViewPluginSignature<
params: T extends { params: {} } ? T['params'] : {};
defaultizedParams: T extends { defaultizedParams: {} } ? T['defaultizedParams'] : {};
instance: T extends { instance: {} } ? T['instance'] : {};
+ publicAPI: T extends { publicAPI: {} } ? T['publicAPI'] : {};
events: T extends { events: {} } ? T['events'] : {};
state: T extends { state: {} } ? T['state'] : {};
contextValue: T extends { contextValue: {} } ? T['contextValue'] : {};
@@ -74,6 +77,7 @@ export type TreeViewAnyPluginSignature = {
slots: any;
slotProps: any;
models: any;
+ publicAPI: any;
};
type TreeViewUsedPlugins = [
@@ -97,6 +101,15 @@ export type TreeViewUsedInstance
$$signature: TSignature;
};
+export type TreeViewUsedPublicAPI =
+ TSignature['publicAPI'] &
+ MergePluginsProperty, 'publicAPI'> & {
+ /**
+ * Private property only defined in TypeScript to be able to access the plugin signature from the publicAPI object.
+ */
+ $$signature: TSignature;
+ };
+
type TreeViewUsedState = TSignature['state'] &
MergePluginsProperty, 'state'>;
diff --git a/packages/x-tree-view/src/internals/models/treeView.ts b/packages/x-tree-view/src/internals/models/treeView.ts
index c7ee9a8d8c365..8a8e9ca221066 100644
--- a/packages/x-tree-view/src/internals/models/treeView.ts
+++ b/packages/x-tree-view/src/internals/models/treeView.ts
@@ -29,3 +29,6 @@ export interface TreeViewModel {
export type TreeViewInstance =
MergePluginsProperty;
+
+export type TreeViewPublicAPI =
+ MergePluginsProperty;
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts
index d37a7256a3d29..7dda6e8cd4b8d 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts
@@ -3,12 +3,14 @@ import useEventCallback from '@mui/utils/useEventCallback';
import { EventHandlers } from '@mui/base/utils';
import ownerDocument from '@mui/utils/ownerDocument';
import { TreeViewPlugin } from '../../models';
-import { populateInstance } from '../../useTreeView/useTreeView.utils';
+import { populateInstance, populatePublicAPI } from '../../useTreeView/useTreeView.utils';
import { UseTreeViewFocusSignature } from './useTreeViewFocus.types';
import { useInstanceEventHandler } from '../../hooks/useInstanceEventHandler';
+import { getActiveElement } from '../../utils/utils';
export const useTreeViewFocus: TreeViewPlugin = ({
instance,
+ publicAPI,
params,
state,
setState,
@@ -17,24 +19,57 @@ export const useTreeViewFocus: TreeViewPlugin = ({
}) => {
const setFocusedNodeId = useEventCallback((nodeId: React.SetStateAction) => {
const cleanNodeId = typeof nodeId === 'function' ? nodeId(state.focusedNodeId) : nodeId;
- setState((prevState) => ({ ...prevState, focusedNodeId: cleanNodeId }));
+ if (state.focusedNodeId !== cleanNodeId) {
+ setState((prevState) => ({ ...prevState, focusedNodeId: cleanNodeId }));
+ }
});
+ const isTreeViewFocused = React.useCallback(
+ () => !!rootRef.current && rootRef.current === getActiveElement(ownerDocument(rootRef.current)),
+ [rootRef],
+ );
+
const isNodeFocused = React.useCallback(
- (nodeId: string) => state.focusedNodeId === nodeId,
- [state.focusedNodeId],
+ (nodeId: string) => state.focusedNodeId === nodeId && isTreeViewFocused(),
+ [state.focusedNodeId, isTreeViewFocused],
);
+ const isNodeVisible = (nodeId: string) => {
+ const node = instance.getNode(nodeId);
+ return node && (node.parentId == null || instance.isNodeExpanded(node.parentId));
+ };
+
const focusNode = useEventCallback((event: React.SyntheticEvent, nodeId: string | null) => {
- if (nodeId) {
+ // if we receive a nodeId, and it is visible, the focus will be set to it
+ if (nodeId && isNodeVisible(nodeId)) {
+ if (!isTreeViewFocused()) {
+ instance.focusRoot();
+ }
setFocusedNodeId(nodeId);
-
if (params.onNodeFocus) {
params.onNodeFocus(event, nodeId);
}
}
});
+ const focusDefaultNode = useEventCallback((event: React.SyntheticEvent) => {
+ let nodeToFocusId: string | null | undefined;
+ if (Array.isArray(models.selectedNodes.value)) {
+ nodeToFocusId = models.selectedNodes.value.find(isNodeVisible);
+ } else if (models.selectedNodes.value != null && isNodeVisible(models.selectedNodes.value)) {
+ nodeToFocusId = models.selectedNodes.value;
+ }
+
+ if (nodeToFocusId == null) {
+ nodeToFocusId = instance.getNavigableChildrenIds(null)[0];
+ }
+
+ setFocusedNodeId(nodeToFocusId);
+ if (params.onNodeFocus) {
+ params.onNodeFocus(event, nodeToFocusId);
+ }
+ });
+
const focusRoot = useEventCallback(() => {
rootRef.current?.focus({ preventScroll: true });
});
@@ -43,6 +78,11 @@ export const useTreeViewFocus: TreeViewPlugin = ({
isNodeFocused,
focusNode,
focusRoot,
+ focusDefaultNode,
+ });
+
+ populatePublicAPI(publicAPI, {
+ focusNode,
});
useInstanceEventHandler(instance, 'removeNode', ({ id }) => {
@@ -60,29 +100,9 @@ export const useTreeViewFocus: TreeViewPlugin = ({
const createHandleFocus =
(otherHandlers: EventHandlers) => (event: React.FocusEvent) => {
otherHandlers.onFocus?.(event);
-
// if the event bubbled (which is React specific) we don't want to steal focus
if (event.target === event.currentTarget) {
- const isNodeVisible = (nodeId: string) => {
- const node = instance.getNode(nodeId);
- return node && (node.parentId == null || instance.isNodeExpanded(node.parentId));
- };
-
- let nodeToFocusId: string | null | undefined;
- if (Array.isArray(models.selectedNodes.value)) {
- nodeToFocusId = models.selectedNodes.value.find(isNodeVisible);
- } else if (
- models.selectedNodes.value != null &&
- isNodeVisible(models.selectedNodes.value)
- ) {
- nodeToFocusId = models.selectedNodes.value;
- }
-
- if (nodeToFocusId == null) {
- nodeToFocusId = instance.getNavigableChildrenIds(null)[0];
- }
-
- instance.focusNode(event, nodeToFocusId);
+ instance.focusDefaultNode(event);
}
};
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.types.ts
index e7a181d86c19a..2bb293715f34d 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.types.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.types.ts
@@ -8,8 +8,12 @@ import { UseTreeViewExpansionSignature } from '../useTreeViewExpansion';
export interface UseTreeViewFocusInstance {
isNodeFocused: (nodeId: string) => boolean;
focusNode: (event: React.SyntheticEvent, nodeId: string | null) => void;
+ focusDefaultNode: (event: React.SyntheticEvent) => void;
focusRoot: () => void;
}
+export interface UseTreeViewFocusPublicAPI {
+ focusNode: (event: React.SyntheticEvent, nodeId: string | null) => void;
+}
export interface UseTreeViewFocusParameters {
/**
@@ -31,6 +35,7 @@ export type UseTreeViewFocusSignature = TreeViewPluginSignature<{
params: UseTreeViewFocusParameters;
defaultizedParams: UseTreeViewFocusDefaultizedParameters;
instance: UseTreeViewFocusInstance;
+ publicAPI: UseTreeViewFocusPublicAPI;
state: UseTreeViewFocusState;
dependantPlugins: [
UseTreeViewIdSignature,
diff --git a/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts b/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts
index 06d6d9e0ca648..3584f61115f8a 100644
--- a/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts
+++ b/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts
@@ -7,6 +7,7 @@ import {
TreeViewPlugin,
ConvertPluginsIntoSignatures,
MergePluginsProperty,
+ TreeViewPublicAPI,
} from '../models';
import {
UseTreeViewDefaultizedParameters,
@@ -18,6 +19,21 @@ import { useTreeViewModels } from './useTreeViewModels';
import { TreeViewContextValue } from '../TreeViewProvider';
import { TREE_VIEW_CORE_PLUGINS } from '../corePlugins';
+export function useTreeViewApiInitialization(
+ inputApiRef: React.MutableRefObject | undefined,
+): T {
+ const fallbackPublicApiRef = React.useRef({}) as React.MutableRefObject;
+
+ if (inputApiRef) {
+ if (inputApiRef.current == null) {
+ inputApiRef.current = {} as T;
+ }
+ return inputApiRef.current;
+ }
+
+ return fallbackPublicApiRef.current;
+}
+
export const useTreeView = []>(
inParams: UseTreeViewParameters,
): UseTreeViewReturnValue> => {
@@ -40,6 +56,9 @@ export const useTreeView = ,
);
const instance = instanceRef.current as TreeViewInstance;
+
+ const publicAPI = useTreeViewApiInitialization>(inParams.apiRef);
+
const innerRootRef = React.useRef(null);
const handleRootRef = useForkRef(innerRootRef, inParams.rootRef);
@@ -68,6 +87,7 @@ export const useTreeView = [],
> {
+ apiRef:
+ | React.MutableRefObject>>
+ | undefined;
rootRef?: React.Ref | undefined;
plugins: TPlugins;
slots: MergePluginsProperty, 'slots'>;
diff --git a/packages/x-tree-view/src/internals/useTreeView/useTreeView.utils.ts b/packages/x-tree-view/src/internals/useTreeView/useTreeView.utils.ts
index 0215d05ab79bc..a960b0e3e6403 100644
--- a/packages/x-tree-view/src/internals/useTreeView/useTreeView.utils.ts
+++ b/packages/x-tree-view/src/internals/useTreeView/useTreeView.utils.ts
@@ -1,4 +1,9 @@
-import { TreeViewAnyPluginSignature, TreeViewInstance, TreeViewUsedInstance } from '../models';
+import {
+ TreeViewAnyPluginSignature,
+ TreeViewInstance,
+ TreeViewUsedInstance,
+ TreeViewUsedPublicAPI,
+} from '../models';
import type { UseTreeViewExpansionSignature } from '../plugins/useTreeViewExpansion';
import type { UseTreeViewNodesSignature } from '../plugins/useTreeViewNodes';
@@ -71,3 +76,10 @@ export const populateInstance = (
) => {
Object.assign(instance, methods);
};
+
+export const populatePublicAPI = (
+ publicAPI: TreeViewUsedPublicAPI,
+ methods: T['publicAPI'],
+) => {
+ Object.assign(publicAPI, methods);
+};
diff --git a/packages/x-tree-view/src/internals/utils/extractPluginParamsFromProps.ts b/packages/x-tree-view/src/internals/utils/extractPluginParamsFromProps.ts
index 81200813402bb..6f0ae94843dbd 100644
--- a/packages/x-tree-view/src/internals/utils/extractPluginParamsFromProps.ts
+++ b/packages/x-tree-view/src/internals/utils/extractPluginParamsFromProps.ts
@@ -1,14 +1,25 @@
import * as React from 'react';
-import { ConvertPluginsIntoSignatures, MergePluginsProperty, TreeViewPlugin } from '../models';
+import {
+ ConvertPluginsIntoSignatures,
+ MergePluginsProperty,
+ TreeViewPlugin,
+ TreeViewPublicAPI,
+} from '../models';
import { UseTreeViewBaseParameters } from '../useTreeView/useTreeView.types';
export const extractPluginParamsFromProps = <
TPlugins extends readonly TreeViewPlugin[],
TSlots extends MergePluginsProperty,
TSlotProps extends MergePluginsProperty,
- TProps extends { slots?: TSlots; slotProps?: TSlotProps },
+ TProps extends {
+ slots?: TSlots;
+ slotProps?: TSlotProps;
+ apiRef?: React.MutableRefObject<
+ TreeViewPublicAPI> | undefined
+ >;
+ },
>({
- props: { slots, slotProps, ...props },
+ props: { slots, slotProps, apiRef, ...props },
plugins,
rootRef,
}: {
@@ -28,6 +39,7 @@ export const extractPluginParamsFromProps = <
rootRef,
slots: slots ?? {},
slotProps: slotProps ?? {},
+ apiRef,
} as UseTreeViewBaseParameters & PluginParams;
const otherProps = {} as Omit;
diff --git a/packages/x-tree-view/src/internals/utils/utils.ts b/packages/x-tree-view/src/internals/utils/utils.ts
new file mode 100644
index 0000000000000..71dc45b2f144d
--- /dev/null
+++ b/packages/x-tree-view/src/internals/utils/utils.ts
@@ -0,0 +1,13 @@
+export const getActiveElement = (root: Document | ShadowRoot = document): Element | null => {
+ const activeEl = root.activeElement;
+
+ if (!activeEl) {
+ return null;
+ }
+
+ if (activeEl.shadowRoot) {
+ return getActiveElement(activeEl.shadowRoot);
+ }
+
+ return activeEl;
+};
diff --git a/scripts/x-tree-view.exports.json b/scripts/x-tree-view.exports.json
index 25189b09d2e2f..19663f9de2115 100644
--- a/scripts/x-tree-view.exports.json
+++ b/scripts/x-tree-view.exports.json
@@ -44,5 +44,6 @@
{ "name": "TreeViewSlotProps", "kind": "Interface" },
{ "name": "TreeViewSlots", "kind": "Interface" },
{ "name": "unstable_resetCleanupTracking", "kind": "Variable" },
- { "name": "useTreeItemState", "kind": "Function" }
+ { "name": "useTreeItemState", "kind": "Function" },
+ { "name": "useTreeViewApiRef", "kind": "Variable" }
]