Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TreeView] Add public API and expose focus method #12143

Merged
merged 23 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/data/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,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' },
],
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Box sx={{ flexGrow: 1, maxWidth: 400 }}>
<Box sx={{ mb: 1 }}>
<Button onClick={handleButtonClick}>focus pickers node</Button>
</Box>
<Box sx={{ minHeight: 264, flexGrow: 1 }}>
<SimpleTreeView apiRef={apiRef}>
<TreeItem nodeId="grid" label="Data Grid">
<TreeItem nodeId="grid-community" label="@mui/x-data-grid" />
<TreeItem nodeId="grid-pro" label="@mui/x-data-grid-pro" />
<TreeItem nodeId="grid-premium" label="@mui/x-data-grid-premium" />
</TreeItem>
<TreeItem nodeId="pickers" label="Date and Time Pickers">
<TreeItem nodeId="pickers-community" label="@mui/x-date-pickers" />
<TreeItem nodeId="pickers-pro" label="@mui/x-date-pickers-pro" />
</TreeItem>
<TreeItem nodeId="charts" label="Charts">
<TreeItem nodeId="charts-community" label="@mui/x-charts" />
</TreeItem>
<TreeItem nodeId="tree-view" label="Tree View">
<TreeItem nodeId="tree-view-community" label="@mui/x-tree-view" />
</TreeItem>
</SimpleTreeView>
</Box>
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -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 (
<Box sx={{ flexGrow: 1, maxWidth: 400 }}>
<Box sx={{ mb: 1 }}>
<Button onClick={handleButtonClick}>focus pickers node</Button>
</Box>
<Box sx={{ minHeight: 264, flexGrow: 1 }}>
<SimpleTreeView apiRef={apiRef}>
<TreeItem nodeId="grid" label="Data Grid">
<TreeItem nodeId="grid-community" label="@mui/x-data-grid" />
<TreeItem nodeId="grid-pro" label="@mui/x-data-grid-pro" />
<TreeItem nodeId="grid-premium" label="@mui/x-data-grid-premium" />
</TreeItem>
<TreeItem nodeId="pickers" label="Date and Time Pickers">
<TreeItem nodeId="pickers-community" label="@mui/x-date-pickers" />
<TreeItem nodeId="pickers-pro" label="@mui/x-date-pickers-pro" />
</TreeItem>
<TreeItem nodeId="charts" label="Charts">
<TreeItem nodeId="charts-community" label="@mui/x-charts" />
</TreeItem>
<TreeItem nodeId="tree-view" label="Tree View">
<TreeItem nodeId="tree-view-community" label="@mui/x-tree-view" />
</TreeItem>
</SimpleTreeView>
</Box>
</Box>
);
}
16 changes: 16 additions & 0 deletions docs/data/tree-view/simple-tree-view/focus/focus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
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

<p class="description">Learn how to focus Tree View items.</p>

## Focus a specific node

{{"demo": "FocusedSimpleTreeView.js"}}
noraleonte marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 2 additions & 1 deletion docs/pages/x/api/tree-view/simple-tree-view.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down Expand Up @@ -93,6 +94,6 @@
"forwardsRefTo": "HTMLUListElement",
"filename": "/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx",
"inheritance": null,
"demos": "<ul><li><a href=\"/x/react-tree-view/getting-started/\">Tree View - Getting Started</a></li>\n<li><a href=\"/x/react-tree-view/simple-tree-view/customization/\">Simple Tree View - Customization</a></li>\n<li><a href=\"/x/react-tree-view/simple-tree-view/expansion/\">Simple Tree View - Expansion</a></li>\n<li><a href=\"/x/react-tree-view/simple-tree-view/items/\">Simple Tree View - Items</a></li>\n<li><a href=\"/x/react-tree-view/simple-tree-view/selection/\">Simple Tree View - Selection</a></li></ul>",
"demos": "<ul><li><a href=\"/x/react-tree-view/getting-started/\">Tree View - Getting Started</a></li>\n<li><a href=\"/x/react-tree-view/simple-tree-view/customization/\">Simple Tree View - Customization</a></li>\n<li><a href=\"/x/react-tree-view/simple-tree-view/expansion/\">Simple Tree View - Expansion</a></li>\n<li><a href=\"/x/react-tree-view/simple-tree-view/focus/\">Simple Tree View - Focus</a></li>\n<li><a href=\"/x/react-tree-view/simple-tree-view/items/\">Simple Tree View - Items</a></li>\n<li><a href=\"/x/react-tree-view/simple-tree-view/selection/\">Simple Tree View - Selection</a></li></ul>",
"cssComponent": false
}
2 changes: 1 addition & 1 deletion docs/pages/x/api/tree-view/tree-item.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,6 @@
"forwardsRefTo": "HTMLLIElement",
"filename": "/packages/x-tree-view/src/TreeItem/TreeItem.tsx",
"inheritance": null,
"demos": "<ul><li><a href=\"/x/react-tree-view/getting-started/\">Tree View - Getting Started</a></li>\n<li><a href=\"/x/react-tree-view/rich-tree-view/expansion/\">Rich Tree View - Expansion</a></li>\n<li><a href=\"/x/react-tree-view/rich-tree-view/items/\">Rich Tree View - Items</a></li>\n<li><a href=\"/x/react-tree-view/rich-tree-view/selection/\">Rich Tree View - Selection</a></li>\n<li><a href=\"/x/react-tree-view/simple-tree-view/customization/\">Simple Tree View - Customization</a></li>\n<li><a href=\"/x/react-tree-view/simple-tree-view/expansion/\">Simple Tree View - Expansion</a></li>\n<li><a href=\"/x/react-tree-view/simple-tree-view/items/\">Simple Tree View - Items</a></li>\n<li><a href=\"/x/react-tree-view/simple-tree-view/selection/\">Simple Tree View - Selection</a></li></ul>",
"demos": "<ul><li><a href=\"/x/react-tree-view/getting-started/\">Tree View - Getting Started</a></li>\n<li><a href=\"/x/react-tree-view/rich-tree-view/expansion/\">Rich Tree View - Expansion</a></li>\n<li><a href=\"/x/react-tree-view/rich-tree-view/items/\">Rich Tree View - Items</a></li>\n<li><a href=\"/x/react-tree-view/rich-tree-view/selection/\">Rich Tree View - Selection</a></li>\n<li><a href=\"/x/react-tree-view/simple-tree-view/customization/\">Simple Tree View - Customization</a></li>\n<li><a href=\"/x/react-tree-view/simple-tree-view/expansion/\">Simple Tree View - Expansion</a></li>\n<li><a href=\"/x/react-tree-view/simple-tree-view/focus/\">Simple Tree View - Focus</a></li>\n<li><a href=\"/x/react-tree-view/simple-tree-view/items/\">Simple Tree View - Items</a></li>\n<li><a href=\"/x/react-tree-view/simple-tree-view/selection/\">Simple Tree View - Selection</a></li></ul>",
"cssComponent": false
}
1 change: 1 addition & 0 deletions docs/pages/x/api/tree-view/tree-view.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
7 changes: 7 additions & 0 deletions docs/pages/x/react-tree-view/simple-tree-view/focus.js
Original file line number Diff line number Diff line change
@@ -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 <MarkdownDocs {...pageProps} />;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"componentDescription": "",
"propDescriptions": {
"apiRef": {
"description": "The ref object that allows Tree View manipulation. Can be instantiated with <code>useTreeViewApiRef()</code>."
},
"children": { "description": "The content of the component." },
"classes": { "description": "Override or extend the styles applied to the component." },
"defaultExpandedNodes": {
Expand Down
3 changes: 3 additions & 0 deletions docs/translations/api-docs/tree-view/tree-view/tree-view.json
Original file line number Diff line number Diff line change
@@ -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 <code>useTreeViewApiRef()</code>."
},
"children": { "description": "The content of the component." },
"classes": { "description": "Override or extend the styles applied to the component." },
"defaultExpandedNodes": {
Expand Down
51 changes: 51 additions & 0 deletions packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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('<SimpleTreeView />', () => {
const { render } = createRenderer();
Expand Down Expand Up @@ -531,6 +533,55 @@ describe('<SimpleTreeView />', () => {

expect(onNodeFocus.lastCall.lastArg).to.equal('1');
});
it('should focus specific node using `apiRef`', () => {
noraleonte marked this conversation as resolved.
Show resolved Hide resolved
let apiRef: SimpleTreeViewApiRef;
const onNodeFocus = spy();

function TestCase() {
apiRef = useTreeViewApiRef();
return (
<SimpleTreeView apiRef={apiRef} onNodeFocus={onNodeFocus}>
<TreeItem nodeId="1" label="1">
<TreeItem nodeId="1.1" label="1.1" />
</TreeItem>
<TreeItem nodeId="2" label="2" />
</SimpleTreeView>
);
}

const { getByRole } = render(<TestCase />);

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 (
<SimpleTreeView apiRef={apiRef} onNodeFocus={onNodeFocus}>
<TreeItem nodeId="1" label="1">
<TreeItem nodeId="1.1" label="1.1" />
</TreeItem>
<TreeItem nodeId="2" label="2" />
</SimpleTreeView>
);
}

const { getByRole } = render(<TestCase />);

act(() => {
apiRef.current.focusNode!({} as React.SyntheticEvent, '1.1');
});

expect(getByRole('tree')).not.toHaveFocus();
});
});

describe('Accessibility', () => {
Expand Down
8 changes: 8 additions & 0 deletions packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {
SimpleTreeViewPluginParameters,
SimpleTreeViewPluginSlotProps,
SimpleTreeViewPluginSlots,
SimpleTreeViewPlugins,
} from './SimpleTreeView.plugins';
import { TreeViewPublicAPI } from '../internals/models';

export interface SimpleTreeViewSlots extends SimpleTreeViewPluginSlots {
/**
Expand All @@ -21,6 +23,8 @@ export interface SimpleTreeViewSlotProps extends SimpleTreeViewPluginSlotProps {
root?: SlotComponentProps<'ul', {}, {}>;
}

export type SimpleTreeViewApiRef = React.MutableRefObject<TreeViewPublicAPI<SimpleTreeViewPlugins>>;

export interface SimpleTreeViewProps<Multiple extends boolean | undefined>
extends SimpleTreeViewPluginParameters<Multiple>,
React.HTMLAttributes<HTMLUListElement> {
Expand All @@ -45,4 +49,8 @@ export interface SimpleTreeViewProps<Multiple extends boolean | undefined>
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx?: SxProps<Theme>;
/**
* The ref object that allows Tree View manipulation. Can be instantiated with `useTreeViewApiRef()`.
*/
apiRef?: SimpleTreeViewApiRef;
}
8 changes: 8 additions & 0 deletions packages/x-tree-view/src/TreeView/TreeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/x-tree-view/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useTreeViewApiRef } from './useTreeViewApiRef';
10 changes: 10 additions & 0 deletions packages/x-tree-view/src/hooks/useTreeViewApiRef.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as React from 'react';
import { TreeViewAnyPluginSignature, TreeViewUsedPublicAPI } from '../internals/models';

/**
* Hook that instantiate a [[TreeViewApiRef]].
noraleonte marked this conversation as resolved.
Show resolved Hide resolved
*/
export const useTreeViewApiRef = <
T extends TreeViewAnyPluginSignature,
Api extends TreeViewUsedPublicAPI<T>,
>() => React.useRef({}) as React.MutableRefObject<Api>;
2 changes: 2 additions & 0 deletions packages/x-tree-view/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export { unstable_resetCleanupTracking } from './internals/hooks/useInstanceEven

export * from './models';
export * from './icons';

export * from './hooks';
1 change: 1 addition & 0 deletions packages/x-tree-view/src/internals/models/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type ConvertPluginsIntoSignatures<TPlugins extends readonly any[]> =
export interface MergePlugins<TPlugins extends readonly any[]> {
state: MergePluginsProperty<TPlugins, 'state'>;
instance: MergePluginsProperty<TPlugins, 'instance'>;
publicAPI: MergePluginsProperty<TPlugins, 'publicAPI'>;
params: MergePluginsProperty<TPlugins, 'params'>;
defaultizedParams: MergePluginsProperty<TPlugins, 'defaultizedParams'>;
dependantPlugins: MergePluginsProperty<TPlugins, 'dependantPlugins'>;
Expand Down
13 changes: 13 additions & 0 deletions packages/x-tree-view/src/internals/models/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { TreeItemProps } from '../../TreeItem';

export interface TreeViewPluginOptions<TSignature extends TreeViewAnyPluginSignature> {
instance: TreeViewUsedInstance<TSignature>;
publicAPI: TreeViewUsedPublicAPI<TSignature>;
params: TreeViewUsedDefaultizedParams<TSignature>;
state: TreeViewUsedState<TSignature>;
slots: TSignature['slots'];
Expand Down Expand Up @@ -36,6 +37,7 @@ export type TreeViewPluginSignature<
params?: {};
defaultizedParams?: {};
instance?: {};
publicAPI?: {};
events?: { [key in keyof T['events']]: TreeViewEventLookupElement };
state?: {};
contextValue?: {};
Expand All @@ -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'] : {};
Expand All @@ -74,6 +77,7 @@ export type TreeViewAnyPluginSignature = {
slots: any;
slotProps: any;
models: any;
publicAPI: any;
};

type TreeViewUsedPlugins<TSignature extends TreeViewAnyPluginSignature> = [
Expand All @@ -97,6 +101,15 @@ export type TreeViewUsedInstance<TSignature extends TreeViewAnyPluginSignature>
$$signature: TSignature;
};

export type TreeViewUsedPublicAPI<TSignature extends TreeViewAnyPluginSignature> =
TSignature['publicAPI'] &
MergePluginsProperty<TreeViewUsedPlugins<TSignature>, 'publicAPI'> & {
/**
* Private property only defined in TypeScript to be able to access the plugin signature from the publicAPI object.
*/
$$signature: TSignature;
};

type TreeViewUsedState<TSignature extends TreeViewAnyPluginSignature> = TSignature['state'] &
MergePluginsProperty<TreeViewUsedPlugins<TSignature>, 'state'>;

Expand Down
3 changes: 3 additions & 0 deletions packages/x-tree-view/src/internals/models/treeView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ export interface TreeViewModel<TValue> {

export type TreeViewInstance<TSignatures extends readonly TreeViewAnyPluginSignature[]> =
MergePluginsProperty<TSignatures, 'instance'>;

export type TreeViewPublicAPI<TSignatures extends readonly TreeViewAnyPluginSignature[]> =
MergePluginsProperty<TSignatures, 'publicAPI'>;
Loading
Loading