diff --git a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.test.tsx b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.test.tsx
index b49c3f3f3d790..c446605450e0e 100644
--- a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.test.tsx
+++ b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.test.tsx
@@ -77,16 +77,6 @@ describe('', () => {
expect(screen.getByTestId('item-2')).to.have.attribute('aria-selected', 'true');
});
- it('should not crash on keydown on an empty tree', () => {
- render();
-
- act(() => {
- screen.getByRole('tree').focus();
- });
-
- fireEvent.keyDown(screen.getByRole('tree'), { key: ' ' });
- });
-
it('should not crash when unmounting with duplicate ids', () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function CustomTreeItem(props: any) {
@@ -126,76 +116,45 @@ describe('', () => {
});
it('should call onKeyDown when a key is pressed', () => {
- const handleKeyDown = spy();
+ const handleTreeViewKeyDown = spy();
+ const handleTreeItemKeyDown = spy();
- const { getByRole } = render(
-
-
+ const { getByTestId } = render(
+
+
,
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- fireEvent.keyDown(getByRole('tree'), { key: 'Enter' });
- fireEvent.keyDown(getByRole('tree'), { key: 'A' });
- fireEvent.keyDown(getByRole('tree'), { key: ']' });
+ fireEvent.keyDown(getByTestId('one'), { key: 'Enter' });
+ fireEvent.keyDown(getByTestId('one'), { key: 'A' });
+ fireEvent.keyDown(getByTestId('one'), { key: ']' });
- expect(handleKeyDown.callCount).to.equal(3);
+ expect(handleTreeViewKeyDown.callCount).to.equal(3);
+ expect(handleTreeItemKeyDown.callCount).to.equal(3);
});
it('should select node when Enter key is pressed ', () => {
const handleKeyDown = spy();
- const { getByRole, getByTestId } = render(
+ const { getByTestId } = render(
-
+
,
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
expect(getByTestId('one')).not.to.have.attribute('aria-selected');
- fireEvent.keyDown(getByRole('tree'), { key: 'Enter' });
+ fireEvent.keyDown(getByTestId('one'), { key: 'Enter' });
expect(getByTestId('one')).to.have.attribute('aria-selected');
});
- it('should call onFocus when tree is focused', () => {
- const handleFocus = spy();
- const { getByRole } = render(
-
-
- ,
- );
-
- act(() => {
- getByRole('tree').focus();
- });
-
- expect(handleFocus.callCount).to.equal(1);
- });
-
- it('should call onBlur when tree is blurred', () => {
- const handleBlur = spy();
- const { getByRole } = render(
-
-
- ,
- );
-
- act(() => {
- getByRole('tree').focus();
- });
- act(() => {
- getByRole('tree').blur();
- });
-
- expect(handleBlur.callCount).to.equal(1);
- });
-
it('should be able to be controlled with the expandedNodes prop', () => {
function MyComponent() {
const [expandedState, setExpandedState] = React.useState([]);
@@ -204,20 +163,20 @@ describe('', () => {
};
return (
-
-
+
+
);
}
- const { getByRole, getByTestId, getByText } = render();
+ const { getByTestId, getByText } = render();
expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false');
fireEvent.click(getByText('one'));
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
@@ -226,7 +185,7 @@ describe('', () => {
expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false');
- fireEvent.keyDown(getByRole('tree'), { key: '*' });
+ fireEvent.keyDown(getByTestId('one'), { key: '*' });
expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
});
@@ -301,39 +260,35 @@ describe('', () => {
return (
{
+ defaultExpandedNodes={['one']}
+ onNodeFocus={() => {
setState(Math.random);
}}
- id="tree"
>
-
-
+
+
);
}
- const { getByRole, getByText, getByTestId } = render();
-
- fireEvent.click(getByText('one'));
- // Clicks would normally focus tree
- act(() => {
- getByRole('tree').focus();
- });
+ const { getByTestId } = render();
- expect(getByTestId('one')).toHaveVirtualFocus();
+ fireEvent.focus(getByTestId('one'));
+ fireEvent.focus(getByTestId('one'));
+ expect(getByTestId('one')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' });
+ fireEvent.keyDown(getByTestId('one'), { key: 'ArrowDown' });
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp' });
+ fireEvent.keyDown(getByTestId('two'), { key: 'ArrowUp' });
- expect(getByTestId('one')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' });
+ fireEvent.keyDown(getByTestId('one'), { key: 'ArrowDown' });
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
});
it('should support conditional rendered tree items', () => {
@@ -358,49 +313,48 @@ describe('', () => {
});
it('should work in a portal', () => {
- const { getByRole, getByTestId } = render(
+ const { getByTestId } = render(
-
-
-
-
-
+
+
+
+
+
,
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' });
+ fireEvent.keyDown(getByTestId('one'), { key: 'ArrowDown' });
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' });
+ fireEvent.keyDown(getByTestId('two'), { key: 'ArrowDown' });
- expect(getByTestId('three')).toHaveVirtualFocus();
+ expect(getByTestId('three')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' });
+ fireEvent.keyDown(getByTestId('three'), { key: 'ArrowDown' });
- expect(getByTestId('four')).toHaveVirtualFocus();
+ expect(getByTestId('four')).toHaveFocus();
});
describe('onNodeFocus', () => {
- it('should be called when node is focused', () => {
- const focusSpy = spy();
- const { getByRole } = render(
-
-
+ it('should be called when a node is focused', () => {
+ const onFocus = spy();
+ const { getByTestId } = render(
+
+
,
);
- // First node receives focus when tree focused
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- expect(focusSpy.callCount).to.equal(1);
- expect(focusSpy.args[0][1]).to.equal('1');
+ expect(onFocus.callCount).to.equal(1);
+ expect(onFocus.args[0][1]).to.equal('one');
});
});
@@ -441,95 +395,70 @@ describe('', () => {
});
describe('useTreeViewFocus', () => {
- it('should focus the selected item when the tree is focused', () => {
- const onNodeFocus = spy();
-
- const { getByRole } = render(
-
-
-
+ it('should set tabIndex={0} on the selected item', () => {
+ const { getByTestId } = render(
+
+
+
,
);
- act(() => {
- getByRole('tree').focus();
- });
-
- expect(onNodeFocus.lastCall.lastArg).to.equal('2');
+ expect(getByTestId('one').tabIndex).to.equal(0);
+ expect(getByTestId('two').tabIndex).to.equal(-1);
});
- it('should focus the selected item when the tree is focused (multi select)', () => {
- const onNodeFocus = spy();
-
- const { getByRole } = render(
-
-
-
+ it('should set tabIndex={0} on the selected item (multi select)', () => {
+ const { getByTestId } = render(
+
+
+
,
);
- act(() => {
- getByRole('tree').focus();
- });
-
- expect(onNodeFocus.lastCall.lastArg).to.equal('2');
+ expect(getByTestId('one').tabIndex).to.equal(0);
+ expect(getByTestId('two').tabIndex).to.equal(-1);
});
- it('should focus the first visible selected item when the tree is focused (multi select)', () => {
- const onNodeFocus = spy();
-
- const { getByRole } = render(
-
-
-
+ it('should set tabIndex={0} on the first visible selected item (multi select)', () => {
+ const { getByTestId } = render(
+
+
+
-
+
,
);
- act(() => {
- getByRole('tree').focus();
- });
-
- expect(onNodeFocus.lastCall.lastArg).to.equal('2');
+ expect(getByTestId('one').tabIndex).to.equal(-1);
+ expect(getByTestId('three').tabIndex).to.equal(0);
});
- it('should focus the first item if the selected item is not visible', () => {
- const onNodeFocus = spy();
-
- const { getByRole } = render(
-
-
-
+ it('should set tabIndex={0} on the first item if the selected item is not visible', () => {
+ const { getByTestId } = render(
+
+
+
-
+
,
);
- act(() => {
- getByRole('tree').focus();
- });
-
- expect(onNodeFocus.lastCall.lastArg).to.equal('1');
+ expect(getByTestId('one').tabIndex).to.equal(0);
+ expect(getByTestId('three').tabIndex).to.equal(-1);
});
- it('should focus the first item if no selected item is visible (multi select)', () => {
- const onNodeFocus = spy();
-
- const { getByRole } = render(
-
-
-
+ it('should set tabIndex={0} on the first item if no selected item is visible (multi select)', () => {
+ const { getByTestId } = render(
+
+
+
-
+
,
);
- act(() => {
- getByRole('tree').focus();
- });
-
- expect(onNodeFocus.lastCall.lastArg).to.equal('1');
+ expect(getByTestId('one').tabIndex).to.equal(0);
+ expect(getByTestId('three').tabIndex).to.equal(-1);
});
});
diff --git a/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx b/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx
index fc0e06562cbc0..a09031a70ce00 100644
--- a/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx
+++ b/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx
@@ -2,7 +2,7 @@ import * as React from 'react';
import { expect } from 'chai';
import PropTypes from 'prop-types';
import { spy } from 'sinon';
-import { act, createEvent, createRenderer, fireEvent, screen } from '@mui-internal/test-utils';
+import { act, createEvent, createRenderer, fireEvent } from '@mui-internal/test-utils';
import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView';
import { TreeItem, treeItemClasses as classes } from '@mui/x-tree-view/TreeItem';
import { TreeViewContextValue } from '@mui/x-tree-view/internals/TreeViewProvider';
@@ -19,6 +19,7 @@ const TEST_TREE_VIEW_CONTEXT_VALUE: TreeViewContextValue
isNodeDisabled: () => false,
getTreeItemId: () => '',
mapFirstCharFromJSX: () => {},
+ canNodeBeTabbed: () => false,
} as any,
runItemPlugins: ({ props, ref }) => ({ props, ref, wrapItem: (children) => children }),
disabledItemsFocusable: false,
@@ -243,14 +244,14 @@ describe('', () => {
});
it('should be able to use a custom id', () => {
- const { getByRole } = render(
+ const { getByRole, getByTestId } = render(
-
+
,
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
expect(getByRole('tree')).to.have.attribute('aria-activedescendant', 'customId');
@@ -393,10 +394,10 @@ describe('', () => {
});
});
- describe('when a tree receives focus', () => {
+ describe('when an item receives focus', () => {
it('should focus the first node if none of the nodes are selected before the tree receives focus', () => {
- const { getByRole, getByTestId, queryAllByRole } = render(
-
+ const { getByTestId, queryAllByRole } = render(
+
@@ -406,51 +407,33 @@ describe('', () => {
expect(queryAllByRole('treeitem', { selected: true })).to.have.length(0);
act(() => {
- getByRole('tree').focus();
- });
-
- expect(getByTestId('one')).toHaveVirtualFocus();
- });
-
- it('should focus the selected node if a node is selected before the tree receives focus', () => {
- const { getByTestId, getByRole } = render(
-
-
-
-
- ,
- );
-
- expect(getByTestId('two')).to.have.attribute('aria-selected', 'true');
-
- act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
});
it('should work with programmatic focus', () => {
- const { getByRole, getByTestId } = render(
+ const { getByTestId } = render(
-
-
+
+
,
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- expect(getByTestId('one')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
act(() => {
getByTestId('two').focus();
});
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
});
- it('should work when focused node is removed', () => {
+ it.only('should work when focused node is removed', () => {
let removeActiveItem;
// a TreeItem which can remove from the tree by calling `removeActiveItem`
function ControlledTreeItem(props) {
@@ -463,25 +446,19 @@ describe('', () => {
return ;
}
- const { getByRole, getByTestId, getByText } = render(
-
-
-
-
+ const { getByTestId } = render(
+
+
+
+
,
);
- const tree = getByRole('tree');
act(() => {
- tree.focus();
+ getByTestId('three').focus();
});
-
- expect(getByTestId('parent')).toHaveVirtualFocus();
-
- fireEvent.click(getByText('two'));
-
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('three')).toHaveFocus();
// generic action that removes an item.
// Could be promise based, or timeout, or another user interaction
@@ -489,31 +466,15 @@ describe('', () => {
removeActiveItem();
});
- expect(getByTestId('parent')).toHaveVirtualFocus();
- });
-
- it('should focus on tree with scroll prevented', () => {
- const { getByRole, getByTestId } = render(
-
-
-
- ,
- );
- const focus = spy(getByRole('tree'), 'focus');
-
- act(() => {
- getByTestId('one').focus();
- });
-
- expect(focus.calledOnceWithExactly({ preventScroll: true })).to.equals(true);
+ expect(getByTestId('one')).toHaveFocus();
});
});
describe('Navigation', () => {
describe('right arrow interaction', () => {
it('should open the node and not move the focus if focus is on a closed node', () => {
- const { getByRole, getByTestId } = render(
-
+ const { getByTestId } = render(
+
@@ -523,17 +484,17 @@ describe('', () => {
expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false');
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowRight' });
+ fireEvent.keyDown(getByTestId('one'), { key: 'ArrowRight' });
expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
- expect(getByTestId('one')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
});
it('should move focus to the first child if focus is on an open node', () => {
- const { getByTestId, getByRole } = render(
-
+ const { getByTestId } = render(
+
@@ -543,87 +504,81 @@ describe('', () => {
expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowRight' });
+ fireEvent.keyDown(getByTestId('one'), { key: 'ArrowRight' });
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
});
it('should do nothing if focus is on an end node', () => {
- const { getByRole, getByTestId, getByText } = render(
-
+ const { getByTestId } = render(
+
,
);
- fireEvent.click(getByText('two'));
act(() => {
- getByRole('tree').focus();
+ getByTestId('two').focus();
});
- expect(getByTestId('two')).toHaveVirtualFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowRight' });
+ expect(getByTestId('two')).toHaveFocus();
+ fireEvent.keyDown(getByTestId('two'), { key: 'ArrowRight' });
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
});
});
describe('left arrow interaction', () => {
it('should close the node if focus is on an open node', () => {
- render(
-
+ const { getByTestId, getByText } = render(
+
,
);
- const [firstItem] = screen.getAllByRole('treeitem');
- const firstItemLabel = screen.getByText('one');
-
- fireEvent.click(firstItemLabel);
-
- expect(firstItem).to.have.attribute('aria-expanded', 'true');
+ fireEvent.click(getByText('one'));
act(() => {
- screen.getByRole('tree').focus();
+ getByTestId('one').focus();
});
- fireEvent.keyDown(screen.getByRole('tree'), { key: 'ArrowLeft' });
- expect(firstItem).to.have.attribute('aria-expanded', 'false');
- expect(screen.getByTestId('one')).toHaveVirtualFocus();
+ expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
+
+ fireEvent.keyDown(getByTestId('one'), { key: 'ArrowLeft' });
+
+ expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false');
+ expect(getByTestId('one')).toHaveFocus();
});
it("should move focus to the node's parent node if focus is on a child node that is an end node", () => {
- render(
-
+ const { getByTestId } = render(
+
,
);
- const [firstItem] = screen.getAllByRole('treeitem');
- const secondItemLabel = screen.getByText('two');
- expect(firstItem).to.have.attribute('aria-expanded', 'true');
+ expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
- fireEvent.click(secondItemLabel);
act(() => {
- screen.getByRole('tree').focus();
+ getByTestId('two').focus();
});
- expect(screen.getByTestId('two')).toHaveVirtualFocus();
- fireEvent.keyDown(screen.getByRole('tree'), { key: 'ArrowLeft' });
+ expect(getByTestId('two')).toHaveFocus();
+ fireEvent.keyDown(getByTestId('two'), { key: 'ArrowLeft' });
- expect(screen.getByTestId('one')).toHaveVirtualFocus();
- expect(firstItem).to.have.attribute('aria-expanded', 'true');
+ expect(getByTestId('one')).toHaveFocus();
+ 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", () => {
- render(
-
+ const { getByTestId } = render(
+
@@ -632,25 +587,23 @@ describe('', () => {
,
);
- fireEvent.click(screen.getByText('one'));
-
- expect(screen.getByTestId('one')).to.have.attribute('aria-expanded', 'true');
+ expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
act(() => {
- screen.getByTestId('two').focus();
+ getByTestId('two').focus();
});
- expect(screen.getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
- fireEvent.keyDown(screen.getByRole('tree'), { key: 'ArrowLeft' });
+ fireEvent.keyDown(getByTestId('two'), { key: 'ArrowLeft' });
- expect(screen.getByTestId('one')).toHaveVirtualFocus();
- expect(screen.getByTestId('one')).to.have.attribute('aria-expanded', 'true');
+ expect(getByTestId('one')).toHaveFocus();
+ expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
});
it('should do nothing if focus is on a root node that is closed', () => {
- const { getByRole, getByTestId } = render(
-
+ const { getByTestId } = render(
+
@@ -658,50 +611,50 @@ describe('', () => {
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false');
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowLeft' });
- expect(getByTestId('one')).toHaveVirtualFocus();
+ fireEvent.keyDown(getByTestId('one'), { key: 'ArrowLeft' });
+ expect(getByTestId('one')).toHaveFocus();
});
it('should do nothing if focus is on a root node that is an end node', () => {
- const { getByRole, getByTestId } = render(
-
+ const { getByTestId } = render(
+
,
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowLeft' });
+ fireEvent.keyDown(getByTestId('one'), { key: 'ArrowLeft' });
- expect(getByTestId('one')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
});
});
describe('down arrow interaction', () => {
it('moves focus to a sibling node', () => {
- const { getByRole, getByTestId } = render(
-
+ const { getByTestId } = render(
+
,
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' });
+ fireEvent.keyDown(getByTestId('one'), { key: 'ArrowDown' });
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
});
it('moves focus to a child node', () => {
- const { getByRole, getByTestId } = render(
-
+ const { getByTestId } = render(
+
@@ -711,11 +664,11 @@ describe('', () => {
expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' });
+ fireEvent.keyDown(getByTestId('one'), { key: 'ArrowDown' });
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
});
it('moves focus to a child node works with a dynamic tree', () => {
@@ -731,7 +684,7 @@ describe('', () => {
>
Toggle Hide
-
+
{!hide && (
@@ -743,7 +696,7 @@ describe('', () => {
);
}
- const { getByRole, queryByTestId, getByTestId, getByText } = render();
+ const { queryByTestId, getByTestId, getByText } = render();
expect(getByTestId('one')).not.to.equal(null);
fireEvent.click(getByText('Toggle Hide'));
@@ -752,16 +705,16 @@ describe('', () => {
expect(getByTestId('one')).not.to.equal(null);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' });
+ fireEvent.keyDown(getByTestId('one'), { key: 'ArrowDown' });
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
});
it("moves focus to a parent's sibling", () => {
- const { getByRole, getByTestId, getByText } = render(
-
+ const { getByTestId } = render(
+
@@ -771,43 +724,41 @@ describe('', () => {
expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
- fireEvent.click(getByText('two'));
act(() => {
- getByRole('tree').focus();
+ getByTestId('two').focus();
});
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' });
+ fireEvent.keyDown(getByTestId('two'), { key: 'ArrowDown' });
- expect(getByTestId('three')).toHaveVirtualFocus();
+ expect(getByTestId('three')).toHaveFocus();
});
});
describe('up arrow interaction', () => {
it('moves focus to a sibling node', () => {
- const { getByRole, getByTestId, getByText } = render(
-
+ const { getByTestId } = render(
+
,
);
- fireEvent.click(getByText('two'));
act(() => {
- getByRole('tree').focus();
+ getByTestId('two').focus();
});
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp' });
+ fireEvent.keyDown(getByTestId('two'), { key: 'ArrowUp' });
- expect(getByTestId('one')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
});
it('moves focus to a parent', () => {
- const { getByRole, getByTestId, getByText } = render(
-
+ const { getByTestId } = render(
+
@@ -816,21 +767,20 @@ describe('', () => {
expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
- fireEvent.click(getByText('two'));
act(() => {
- getByRole('tree').focus();
+ getByTestId('two').focus();
});
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp' });
+ fireEvent.keyDown(getByTestId('two'), { key: 'ArrowUp' });
- expect(getByTestId('one')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
});
it("moves focus to a sibling's child", () => {
- const { getByRole, getByTestId, getByText } = render(
-
+ const { getByTestId } = render(
+
@@ -840,23 +790,22 @@ describe('', () => {
expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
- fireEvent.click(getByText('three'));
act(() => {
- getByRole('tree').focus();
+ getByTestId('three').focus();
});
- expect(getByTestId('three')).toHaveVirtualFocus();
+ expect(getByTestId('three')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp' });
+ fireEvent.keyDown(getByTestId('three'), { key: 'ArrowUp' });
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
});
});
describe('home key interaction', () => {
it('moves focus to the first node in the tree', () => {
- const { getByRole, getByTestId, getByText } = render(
-
+ const { getByTestId } = render(
+
@@ -864,23 +813,22 @@ describe('', () => {
,
);
- fireEvent.click(getByText('four'));
act(() => {
- getByRole('tree').focus();
+ getByTestId('four').focus();
});
- expect(getByTestId('four')).toHaveVirtualFocus();
+ expect(getByTestId('four')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'Home' });
+ fireEvent.keyDown(getByTestId('four'), { key: 'Home' });
- expect(getByTestId('one')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
});
});
describe('end key interaction', () => {
it('moves focus to the last node in the tree without expanded items', () => {
- const { getByRole, getByTestId } = render(
-
+ const { getByTestId } = render(
+
@@ -889,19 +837,19 @@ describe('', () => {
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- expect(getByTestId('one')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'End' });
+ fireEvent.keyDown(getByTestId('one'), { key: 'End' });
- expect(getByTestId('four')).toHaveVirtualFocus();
+ expect(getByTestId('four')).toHaveFocus();
});
it('moves focus to the last node in the tree with expanded items', () => {
- const { getByRole, getByTestId } = render(
-
+ const { getByTestId } = render(
+
@@ -914,21 +862,21 @@ describe('', () => {
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- expect(getByTestId('one')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'End' });
+ fireEvent.keyDown(getByTestId('one'), { key: 'End' });
- expect(getByTestId('six')).toHaveVirtualFocus();
+ expect(getByTestId('six')).toHaveFocus();
});
});
describe('type-ahead functionality', () => {
it('moves focus to the next node with a name that starts with the typed character', () => {
- const { getByRole, getByTestId } = render(
-
+ const { getByTestId } = render(
+
two} data-testid="two" />
@@ -937,27 +885,27 @@ describe('', () => {
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- expect(getByTestId('one')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 't' });
+ fireEvent.keyDown(getByTestId('one'), { key: 't' });
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'f' });
+ fireEvent.keyDown(getByTestId('two'), { key: 'f' });
- expect(getByTestId('four')).toHaveVirtualFocus();
+ expect(getByTestId('four')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'o' });
+ fireEvent.keyDown(getByTestId('four'), { key: 'o' });
- expect(getByTestId('one')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
});
it('moves focus to the next node with the same starting character', () => {
- const { getByRole, getByTestId } = render(
-
+ const { getByTestId } = render(
+
@@ -966,51 +914,51 @@ describe('', () => {
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- expect(getByTestId('one')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 't' });
+ fireEvent.keyDown(getByTestId('one'), { key: 't' });
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 't' });
+ fireEvent.keyDown(getByTestId('two'), { key: 't' });
- expect(getByTestId('three')).toHaveVirtualFocus();
+ expect(getByTestId('three')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 't' });
+ fireEvent.keyDown(getByTestId('three'), { key: 't' });
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
});
it('should not move focus when pressing a modifier key + letter', () => {
- const { getByRole, getByTestId } = render(
-
-
-
-
-
+ const { getByTestId } = render(
+
+
+
+
+
,
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- expect(getByTestId('apple')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'v', ctrlKey: true });
+ fireEvent.keyDown(getByTestId('one'), { key: 'f', ctrlKey: true });
- expect(getByTestId('apple')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'v', metaKey: true });
+ fireEvent.keyDown(getByTestId('one'), { key: 'f', metaKey: true });
- expect(getByTestId('apple')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'v', shiftKey: true });
+ fireEvent.keyDown(getByTestId('one'), { key: 'f', shiftKey: true });
- expect(getByTestId('apple')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
});
it('should not throw when an item is removed', () => {
@@ -1021,7 +969,7 @@ describe('', () => {
-
+
{!hide && }
@@ -1030,21 +978,21 @@ describe('', () => {
);
}
- const { getByRole, getByText, getByTestId } = render();
+ const { getByText, getByTestId } = render();
fireEvent.click(getByText('Hide'));
- expect(getByTestId('navTo')).not.toHaveVirtualFocus();
+ expect(getByTestId('navTo')).not.toHaveFocus();
expect(() => {
act(() => {
- getByRole('tree').focus();
+ getByTestId('keyDown').focus();
});
- expect(getByTestId('keyDown')).toHaveVirtualFocus();
+ expect(getByTestId('keyDown')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'a' });
+ fireEvent.keyDown(getByTestId('keyDown'), { key: 'a' });
}).not.to.throw();
- expect(getByTestId('navTo')).toHaveVirtualFocus();
+ expect(getByTestId('navTo')).toHaveFocus();
});
});
@@ -1052,7 +1000,7 @@ describe('', () => {
it('expands all siblings that are at the same level as the current node', () => {
const onExpandedNodesChange = spy();
- const { getByRole, getByTestId } = render(
+ const { getByTestId } = render(
@@ -1070,14 +1018,14 @@ describe('', () => {
);
act(() => {
- getByRole('tree').focus();
+ 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(getByRole('tree'), { key: '*' });
+ fireEvent.keyDown(getByTestId('one'), { key: '*' });
expect(onExpandedNodesChange.args[0][1]).to.have.length(3);
@@ -1093,7 +1041,7 @@ describe('', () => {
describe('Expansion', () => {
describe('enter key interaction', () => {
it('expands a node with children', () => {
- const { getByRole, getByTestId } = render(
+ const { getByTestId } = render(
@@ -1102,18 +1050,18 @@ describe('', () => {
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false');
- fireEvent.keyDown(getByRole('tree'), { key: 'Enter' });
+ fireEvent.keyDown(getByTestId('one'), { key: 'Enter' });
expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
});
it('collapses a node with children', () => {
- const { getByRole, getByTestId, getByText } = render(
+ const { getByTestId } = render(
@@ -1121,15 +1069,16 @@ describe('', () => {
,
);
- fireEvent.click(getByText('one'));
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
+ expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false');
- fireEvent.keyDown(getByRole('tree'), { key: 'Enter' });
+ fireEvent.keyDown(getByTestId('one'), { key: 'Enter' });
+ expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
+ fireEvent.keyDown(getByTestId('one'), { key: 'Enter' });
expect(getByTestId('one')).to.have.attribute('aria-expanded', 'false');
});
});
@@ -1138,83 +1087,83 @@ describe('', () => {
describe('Single Selection', () => {
describe('keyboard', () => {
it('should select a node when space is pressed', () => {
- const { getByRole, getByTestId } = render(
+ const { getByTestId } = render(
,
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
expect(getByTestId('one')).not.to.have.attribute('aria-selected');
- fireEvent.keyDown(getByRole('tree'), { key: ' ' });
+ fireEvent.keyDown(getByTestId('one'), { key: ' ' });
expect(getByTestId('one')).to.have.attribute('aria-selected', 'true');
});
it('should not deselect a node when space is pressed on a selected node', () => {
- const { getByRole, getByTestId } = render(
+ const { getByTestId } = render(
,
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- expect(getByTestId('one')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
expect(getByTestId('one')).to.have.attribute('aria-selected', 'true');
- fireEvent.keyDown(getByRole('tree'), { key: ' ' });
+ fireEvent.keyDown(getByTestId('one'), { key: ' ' });
expect(getByTestId('one')).to.have.attribute('aria-selected', 'true');
});
it('should not select a node when space is pressed and disableSelection', () => {
- const { getByRole, getByTestId } = render(
+ const { getByTestId } = render(
,
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- fireEvent.keyDown(getByRole('tree'), { key: ' ' });
+ fireEvent.keyDown(getByTestId('one'), { key: ' ' });
expect(getByTestId('one')).not.to.have.attribute('aria-selected');
});
it('should select a node when Enter is pressed and the node is not selected', () => {
- const { getByRole, getByTestId } = render(
+ const { getByTestId } = render(
,
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- fireEvent.keyDown(getByRole('tree'), { key: 'Enter' });
+ fireEvent.keyDown(getByTestId('one'), { key: 'Enter' });
expect(getByTestId('one')).to.have.attribute('aria-selected');
});
it('should not un-select a node when Enter is pressed and the node is selected', () => {
- const { getByRole, getByTestId } = render(
+ const { getByTestId } = render(
,
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- fireEvent.keyDown(getByRole('tree'), { key: 'Enter' });
+ fireEvent.keyDown(getByTestId('one'), { key: 'Enter' });
expect(getByTestId('one')).to.have.attribute('aria-selected');
});
@@ -1261,7 +1210,7 @@ describe('', () => {
describe('Multi Selection', () => {
describe('deselection', () => {
describe('mouse behavior when multiple nodes are selected', () => {
- specify('clicking a selected node holding ctrl should deselect the node', () => {
+ it('clicking a selected node holding ctrl should deselect the node', () => {
const { getByText, getByTestId } = render(
@@ -1276,7 +1225,7 @@ describe('', () => {
expect(getByTestId('two')).to.have.attribute('aria-selected', 'true');
});
- specify('clicking a selected node holding meta should deselect the node', () => {
+ it('clicking a selected node holding meta should deselect the node', () => {
const { getByText, getByTestId } = render(
@@ -1310,27 +1259,27 @@ describe('', () => {
});
it('should deselect the node when pressing space on a selected node', () => {
- const { getByTestId, getByRole } = render(
+ const { getByTestId } = render(
,
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- expect(getByTestId('one')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
expect(getByTestId('one')).to.have.attribute('aria-selected', 'true');
- fireEvent.keyDown(getByRole('tree'), { key: ' ' });
+ fireEvent.keyDown(getByTestId('one'), { key: ' ' });
expect(getByTestId('one')).to.have.attribute('aria-selected', 'false');
});
});
describe('range selection', () => {
- specify('keyboard arrow', () => {
- const { getByRole, getByTestId, getByText, queryAllByRole } = render(
-
+ it('keyboard arrow', () => {
+ const { getByTestId, queryAllByRole, getByText } = render(
+
@@ -1341,37 +1290,37 @@ describe('', () => {
fireEvent.click(getByText('three'));
act(() => {
- getByRole('tree').focus();
+ getByTestId('three').focus();
});
expect(getByTestId('three')).to.have.attribute('aria-selected', 'true');
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown', shiftKey: true });
+ fireEvent.keyDown(getByTestId('three'), { key: 'ArrowDown', shiftKey: true });
- expect(getByTestId('four')).toHaveVirtualFocus();
+ expect(getByTestId('four')).toHaveFocus();
expect(queryAllByRole('treeitem', { selected: true })).to.have.length(2);
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown', shiftKey: true });
+ fireEvent.keyDown(getByTestId('four'), { key: 'ArrowDown', shiftKey: true });
expect(getByTestId('three')).to.have.attribute('aria-selected', 'true');
expect(getByTestId('four')).to.have.attribute('aria-selected', 'true');
expect(getByTestId('five')).to.have.attribute('aria-selected', 'true');
expect(queryAllByRole('treeitem', { selected: true })).to.have.length(3);
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp', shiftKey: true });
+ fireEvent.keyDown(getByTestId('five'), { key: 'ArrowUp', shiftKey: true });
- expect(getByTestId('four')).toHaveVirtualFocus();
+ expect(getByTestId('four')).toHaveFocus();
expect(queryAllByRole('treeitem', { selected: true })).to.have.length(2);
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp', shiftKey: true });
+ fireEvent.keyDown(getByTestId('four'), { key: 'ArrowUp', shiftKey: true });
expect(queryAllByRole('treeitem', { selected: true })).to.have.length(1);
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp', shiftKey: true });
+ fireEvent.keyDown(getByTestId('three'), { key: 'ArrowUp', shiftKey: true });
expect(queryAllByRole('treeitem', { selected: true })).to.have.length(2);
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp', shiftKey: true });
+ fireEvent.keyDown(getByTestId('two'), { key: 'ArrowUp', shiftKey: true });
expect(getByTestId('one')).to.have.attribute('aria-selected', 'true');
expect(getByTestId('two')).to.have.attribute('aria-selected', 'true');
@@ -1381,9 +1330,9 @@ describe('', () => {
expect(queryAllByRole('treeitem', { selected: true })).to.have.length(3);
});
- specify('keyboard arrow does not select when selectionDisabled', () => {
- const { getByRole, getByTestId, queryAllByRole } = render(
-
+ it('keyboard arrow does not select when selectionDisabled', () => {
+ const { getByTestId, queryAllByRole } = render(
+
@@ -1393,21 +1342,21 @@ describe('', () => {
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown', shiftKey: true });
+ fireEvent.keyDown(getByTestId('one'), { key: 'ArrowDown', shiftKey: true });
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
expect(queryAllByRole('treeitem', { selected: true })).to.have.length(0);
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp', shiftKey: true });
+ fireEvent.keyDown(getByTestId('two'), { key: 'ArrowUp', shiftKey: true });
expect(queryAllByRole('treeitem', { selected: true })).to.have.length(0);
});
- specify('keyboard arrow merge', () => {
- const { getByRole, getByTestId, getByText, queryAllByRole } = render(
+ it('keyboard arrow merge', () => {
+ const { getByTestId, getByText, queryAllByRole } = render(
@@ -1420,28 +1369,28 @@ describe('', () => {
fireEvent.click(getByText('three'));
act(() => {
- getByRole('tree').focus();
+ getByTestId('three').focus();
});
expect(getByTestId('three')).to.have.attribute('aria-selected', 'true');
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp', shiftKey: true });
+ fireEvent.keyDown(getByTestId('three'), { key: 'ArrowUp', shiftKey: true });
fireEvent.click(getByText('six'), { ctrlKey: true });
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp', shiftKey: true });
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp', shiftKey: true });
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp', shiftKey: true });
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowUp', shiftKey: true });
+ fireEvent.keyDown(getByTestId('six'), { key: 'ArrowUp', shiftKey: true });
+ fireEvent.keyDown(getByTestId('five'), { key: 'ArrowUp', shiftKey: true });
+ fireEvent.keyDown(getByTestId('four'), { key: 'ArrowUp', shiftKey: true });
+ fireEvent.keyDown(getByTestId('three'), { key: 'ArrowUp', shiftKey: true });
expect(queryAllByRole('treeitem', { selected: true })).to.have.length(5);
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown', shiftKey: true });
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown', shiftKey: true });
+ fireEvent.keyDown(getByTestId('two'), { key: 'ArrowDown', shiftKey: true });
+ fireEvent.keyDown(getByTestId('three'), { key: 'ArrowDown', shiftKey: true });
expect(queryAllByRole('treeitem', { selected: true })).to.have.length(3);
});
- specify('keyboard space', () => {
- const { getByRole, getByTestId, getByText } = render(
+ it('keyboard space', () => {
+ const { getByTestId, getByText } = render(
@@ -1456,26 +1405,36 @@ describe('', () => {
,
);
- const tree = getByRole('tree');
fireEvent.click(getByText('five'));
act(() => {
- tree.focus();
+ getByTestId('five').focus();
});
- for (let i = 0; i < 5; i += 1) {
- fireEvent.keyDown(tree, { key: 'ArrowDown' });
- }
- fireEvent.keyDown(tree, { key: ' ', shiftKey: true });
+
+ fireEvent.keyDown(getByTestId('five'), { key: 'ArrowDown' });
+ fireEvent.keyDown(getByTestId('six'), { key: 'ArrowDown' });
+ fireEvent.keyDown(getByTestId('seven'), { key: 'ArrowDown' });
+ fireEvent.keyDown(getByTestId('eight'), { key: 'ArrowDown' });
+ fireEvent.keyDown(getByTestId('nine'), { key: 'ArrowDown' });
+ fireEvent.keyDown(getByTestId('nine'), { key: ' ', shiftKey: true });
expect(getByTestId('five')).to.have.attribute('aria-selected', 'true');
expect(getByTestId('six')).to.have.attribute('aria-selected', 'true');
expect(getByTestId('seven')).to.have.attribute('aria-selected', 'true');
expect(getByTestId('eight')).to.have.attribute('aria-selected', 'true');
expect(getByTestId('nine')).to.have.attribute('aria-selected', 'true');
- for (let i = 0; i < 9; i += 1) {
- fireEvent.keyDown(tree, { key: 'ArrowUp' });
- }
- fireEvent.keyDown(tree, { key: ' ', shiftKey: true });
+
+ fireEvent.keyDown(getByTestId('nine'), { key: 'ArrowUp' });
+ fireEvent.keyDown(getByTestId('eight'), { key: 'ArrowUp' });
+ fireEvent.keyDown(getByTestId('seven'), { key: 'ArrowUp' });
+ fireEvent.keyDown(getByTestId('six'), { key: 'ArrowUp' });
+ fireEvent.keyDown(getByTestId('five'), { key: 'ArrowUp' });
+ fireEvent.keyDown(getByTestId('four'), { key: 'ArrowUp' });
+ fireEvent.keyDown(getByTestId('three'), { key: 'ArrowUp' });
+ fireEvent.keyDown(getByTestId('two'), { key: 'ArrowUp' });
+ fireEvent.keyDown(getByTestId('one'), { key: 'ArrowUp' });
+
+ fireEvent.keyDown(getByTestId('one'), { 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');
@@ -1487,8 +1446,8 @@ describe('', () => {
expect(getByTestId('nine')).to.have.attribute('aria-selected', 'false');
});
- specify('keyboard home and end', () => {
- const { getByRole, getByTestId } = render(
+ it('keyboard home and end', () => {
+ const { getByTestId } = render(
@@ -1508,7 +1467,7 @@ describe('', () => {
getByTestId('five').focus();
});
- fireEvent.keyDown(getByRole('tree'), {
+ fireEvent.keyDown(getByTestId('five'), {
key: 'End',
shiftKey: true,
ctrlKey: true,
@@ -1520,7 +1479,7 @@ describe('', () => {
expect(getByTestId('eight')).to.have.attribute('aria-selected', 'true');
expect(getByTestId('nine')).to.have.attribute('aria-selected', 'true');
- fireEvent.keyDown(getByRole('tree'), {
+ fireEvent.keyDown(getByTestId('nine'), {
key: 'Home',
shiftKey: true,
ctrlKey: true,
@@ -1537,8 +1496,8 @@ describe('', () => {
expect(getByTestId('nine')).to.have.attribute('aria-selected', 'false');
});
- specify('keyboard home and end do not select when selectionDisabled', () => {
- const { getByRole, getByText, queryAllByRole } = render(
+ it('keyboard home and end do not select when selectionDisabled', () => {
+ const { getByTestId, getByText, queryAllByRole } = render(
@@ -1555,12 +1514,10 @@ describe('', () => {
);
fireEvent.click(getByText('five'));
- fireEvent.click(getByText('five'));
- // Focus node five
act(() => {
- getByRole('tree').focus();
+ getByTestId('five').focus();
});
- fireEvent.keyDown(getByRole('tree'), {
+ fireEvent.keyDown(getByTestId('five'), {
key: 'End',
shiftKey: true,
ctrlKey: true,
@@ -1568,7 +1525,7 @@ describe('', () => {
expect(queryAllByRole('treeitem', { selected: true })).to.have.length(0);
- fireEvent.keyDown(getByRole('tree'), {
+ fireEvent.keyDown(getByTestId('nine'), {
key: 'Home',
shiftKey: true,
ctrlKey: true,
@@ -1577,7 +1534,7 @@ describe('', () => {
expect(queryAllByRole('treeitem', { selected: true })).to.have.length(0);
});
- specify('mouse', () => {
+ it('mouse', () => {
const { getByTestId, getByText } = render(
@@ -1642,7 +1599,7 @@ describe('', () => {
expect(getByTestId('five')).to.have.attribute('aria-selected', 'false');
});
- specify('mouse does not range select when selectionDisabled', () => {
+ it('mouse does not range select when selectionDisabled', () => {
const { getByText, queryAllByRole } = render(
@@ -1666,8 +1623,8 @@ describe('', () => {
});
describe('multi selection', () => {
- specify('keyboard', () => {
- const { getByRole, getByTestId } = render(
+ it('keyboard', () => {
+ const { getByTestId } = render(
@@ -1675,26 +1632,26 @@ describe('', () => {
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
expect(getByTestId('one')).to.have.attribute('aria-selected', 'false');
expect(getByTestId('two')).to.have.attribute('aria-selected', 'false');
- fireEvent.keyDown(getByRole('tree'), { key: ' ' });
+ fireEvent.keyDown(getByTestId('one'), { key: ' ' });
expect(getByTestId('one')).to.have.attribute('aria-selected', 'true');
expect(getByTestId('two')).to.have.attribute('aria-selected', 'false');
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' });
- fireEvent.keyDown(getByRole('tree'), { key: ' ' });
+ fireEvent.keyDown(getByTestId('one'), { key: 'ArrowDown' });
+ fireEvent.keyDown(getByTestId('two'), { key: ' ' });
expect(getByTestId('one')).to.have.attribute('aria-selected', 'true');
expect(getByTestId('two')).to.have.attribute('aria-selected', 'true');
});
- specify('keyboard holding ctrl', () => {
- const { getByRole, getByTestId } = render(
+ it('keyboard holding ctrl', () => {
+ const { getByTestId } = render(
@@ -1702,25 +1659,25 @@ describe('', () => {
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
expect(getByTestId('one')).to.have.attribute('aria-selected', 'false');
expect(getByTestId('two')).to.have.attribute('aria-selected', 'false');
- fireEvent.keyDown(getByRole('tree'), { key: ' ' });
+ fireEvent.keyDown(getByTestId('one'), { key: ' ' });
expect(getByTestId('one')).to.have.attribute('aria-selected', 'true');
expect(getByTestId('two')).to.have.attribute('aria-selected', 'false');
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' });
- fireEvent.keyDown(getByRole('tree'), { key: ' ', ctrlKey: true });
+ fireEvent.keyDown(getByTestId('one'), { key: 'ArrowDown' });
+ fireEvent.keyDown(getByTestId('two'), { key: ' ', ctrlKey: true });
expect(getByTestId('one')).to.have.attribute('aria-selected', 'true');
expect(getByTestId('two')).to.have.attribute('aria-selected', 'true');
});
- specify('mouse', () => {
+ it('mouse', () => {
const { getByText, getByTestId } = render(
@@ -1742,7 +1699,7 @@ describe('', () => {
expect(getByTestId('two')).to.have.attribute('aria-selected', 'true');
});
- specify('mouse using ctrl', () => {
+ it('mouse using ctrl', () => {
const { getByTestId, getByText } = render(
@@ -1760,7 +1717,7 @@ describe('', () => {
expect(getByTestId('two')).to.have.attribute('aria-selected', 'true');
});
- specify('mouse using meta', () => {
+ it('mouse using meta', () => {
const { getByTestId, getByText } = render(
@@ -1779,8 +1736,8 @@ describe('', () => {
});
});
- specify('ctrl + a selects all', () => {
- const { getByRole, queryAllByRole } = render(
+ it('ctrl + a selects all', () => {
+ const { getByTestId, queryAllByRole } = render(
@@ -1791,15 +1748,15 @@ describe('', () => {
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- fireEvent.keyDown(getByRole('tree'), { key: 'a', ctrlKey: true });
+ fireEvent.keyDown(getByTestId('one'), { key: 'a', ctrlKey: true });
expect(queryAllByRole('treeitem', { selected: true })).to.have.length(5);
});
- specify('ctrl + a does not select all when disableSelection', () => {
- const { getByRole, queryAllByRole } = render(
+ it('ctrl + a does not select all when disableSelection', () => {
+ const { getByTestId, queryAllByRole } = render(
@@ -1810,9 +1767,9 @@ describe('', () => {
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- fireEvent.keyDown(getByRole('tree'), { key: 'a', ctrlKey: true });
+ fireEvent.keyDown(getByTestId('one'), { key: 'a', ctrlKey: true });
expect(queryAllByRole('treeitem', { selected: true })).to.have.length(0);
});
@@ -1891,7 +1848,7 @@ describe('', () => {
describe('keyboard', () => {
describe('`disabledItemsFocusable={true}`', () => {
it('should prevent selection by keyboard', () => {
- const { getByRole, getByTestId } = render(
+ const { getByTestId } = render(
,
@@ -1900,13 +1857,13 @@ describe('', () => {
act(() => {
getByTestId('one').focus();
});
- expect(getByTestId('one')).toHaveVirtualFocus();
- fireEvent.keyDown(getByRole('tree'), { key: ' ' });
+ expect(getByTestId('one')).toHaveFocus();
+ fireEvent.keyDown(getByTestId('one'), { key: ' ' });
expect(getByTestId('one')).not.to.have.attribute('aria-selected');
});
it('should not prevent next node being range selected by keyboard', () => {
- const { getByRole, getByTestId } = render(
+ const { getByTestId } = render(
@@ -1918,15 +1875,15 @@ describe('', () => {
act(() => {
getByTestId('one').focus();
});
- expect(getByTestId('one')).toHaveVirtualFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown', shiftKey: true });
+ expect(getByTestId('one')).toHaveFocus();
+ fireEvent.keyDown(getByTestId('one'), { key: 'ArrowDown', shiftKey: true });
expect(getByTestId('one')).to.have.attribute('aria-selected', 'false');
expect(getByTestId('two')).to.have.attribute('aria-selected', 'true');
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
});
it('should prevent range selection by keyboard + arrow down', () => {
- const { getByRole, getByTestId } = render(
+ const { getByTestId } = render(
@@ -1936,17 +1893,17 @@ describe('', () => {
act(() => {
getByTestId('one').focus();
});
- expect(getByTestId('one')).toHaveVirtualFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown', shiftKey: true });
+ expect(getByTestId('one')).toHaveFocus();
+ fireEvent.keyDown(getByTestId('one'), { key: 'ArrowDown', shiftKey: true });
expect(getByTestId('one')).to.have.attribute('aria-selected', 'false');
expect(getByTestId('two')).to.have.attribute('aria-selected', 'false');
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
});
});
- describe('`disabledItemsFocusable=false`', () => {
+ describe('`disabledItemsFocusable={false}`', () => {
it('should select the next non disabled node by keyboard + arrow down', () => {
- const { getByRole, getByTestId } = render(
+ const { getByTestId } = render(
@@ -1957,11 +1914,11 @@ describe('', () => {
act(() => {
getByTestId('one').focus();
});
- expect(getByTestId('one')).toHaveVirtualFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown', shiftKey: true });
+ expect(getByTestId('one')).toHaveFocus();
+ fireEvent.keyDown(getByTestId('one'), { key: 'ArrowDown', shiftKey: true });
expect(getByTestId('one')).to.have.attribute('aria-selected', 'false');
expect(getByTestId('two')).to.have.attribute('aria-selected', 'false');
- expect(getByTestId('three')).toHaveVirtualFocus();
+ expect(getByTestId('three')).toHaveFocus();
expect(getByTestId('one')).to.have.attribute('aria-selected', 'false');
expect(getByTestId('two')).to.have.attribute('aria-selected', 'false');
expect(getByTestId('three')).to.have.attribute('aria-selected', 'true');
@@ -1969,7 +1926,7 @@ describe('', () => {
});
it('should prevent range selection by keyboard + space', () => {
- const { getByRole, getByTestId, getByText } = render(
+ const { getByTestId, getByText } = render(
@@ -1978,16 +1935,17 @@ describe('', () => {
,
);
- const tree = getByRole('tree');
fireEvent.click(getByText('one'));
act(() => {
- tree.focus();
+ getByTestId('one').focus();
});
- for (let i = 0; i < 5; i += 1) {
- fireEvent.keyDown(tree, { key: 'ArrowDown' });
- }
- fireEvent.keyDown(tree, { key: ' ', shiftKey: true });
+
+ fireEvent.keyDown(getByTestId('one'), { key: 'ArrowDown' });
+ fireEvent.keyDown(getByTestId('two'), { key: 'ArrowDown' });
+ fireEvent.keyDown(getByTestId('four'), { key: 'ArrowDown' });
+
+ fireEvent.keyDown(getByTestId('five'), { key: ' ', shiftKey: true });
expect(getByTestId('one')).to.have.attribute('aria-selected', 'true');
expect(getByTestId('two')).to.have.attribute('aria-selected', 'true');
@@ -1997,7 +1955,7 @@ describe('', () => {
});
it('should prevent selection by ctrl + a', () => {
- const { getByRole, queryAllByRole } = render(
+ const { getByTestId, queryAllByRole } = render(
@@ -2008,15 +1966,15 @@ describe('', () => {
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- fireEvent.keyDown(getByRole('tree'), { key: 'a', ctrlKey: true });
+ fireEvent.keyDown(getByTestId('one'), { key: 'a', ctrlKey: true });
expect(queryAllByRole('treeitem', { selected: true })).to.have.length(4);
});
it('should prevent selection by keyboard end', () => {
- const { getByRole, getByTestId } = render(
+ const { getByTestId } = render(
@@ -2027,10 +1985,10 @@ describe('', () => {
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- expect(getByTestId('one')).toHaveVirtualFocus();
- fireEvent.keyDown(getByRole('tree'), {
+ expect(getByTestId('one')).toHaveFocus();
+ fireEvent.keyDown(getByTestId('one'), {
key: 'End',
shiftKey: true,
ctrlKey: true,
@@ -2044,7 +2002,7 @@ describe('', () => {
});
it('should prevent selection by keyboard home', () => {
- const { getByRole, getByTestId } = render(
+ const { getByTestId } = render(
@@ -2057,8 +2015,8 @@ describe('', () => {
act(() => {
getByTestId('five').focus();
});
- expect(getByTestId('five')).toHaveVirtualFocus();
- fireEvent.keyDown(getByRole('tree'), {
+ expect(getByTestId('five')).toHaveFocus();
+ fireEvent.keyDown(getByTestId('five'), {
key: 'Home',
shiftKey: true,
ctrlKey: true,
@@ -2076,16 +2034,16 @@ describe('', () => {
describe('focus', () => {
describe('`disabledItemsFocusable={true}`', () => {
it('should prevent focus by mouse', () => {
- const focusSpy = spy();
+ const onNodeFocus = spy();
const { getByText } = render(
-
+
,
);
fireEvent.click(getByText('two'));
- expect(focusSpy.callCount).to.equal(0);
+ expect(onNodeFocus.callCount).to.equal(0);
});
it('should not prevent programmatic focus', () => {
@@ -2099,11 +2057,11 @@ describe('', () => {
act(() => {
getByTestId('one').focus();
});
- expect(getByTestId('one')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
});
it('should not prevent focus by type-ahead', () => {
- const { getByRole, getByTestId } = render(
+ const { getByTestId } = render(
@@ -2111,15 +2069,15 @@ describe('', () => {
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- expect(getByTestId('one')).toHaveVirtualFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 't' });
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
+ fireEvent.keyDown(getByTestId('one'), { key: 't' });
+ expect(getByTestId('two')).toHaveFocus();
});
it('should not prevent focus by arrow keys', () => {
- const { getByRole, getByTestId } = render(
+ const { getByTestId } = render(
@@ -2127,61 +2085,52 @@ describe('', () => {
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- expect(getByTestId('one')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' });
- expect(getByTestId('two')).toHaveVirtualFocus();
- });
-
- it('should be focused on tree focus', () => {
- const { getByRole, getByTestId } = render(
-
-
-
- ,
- );
-
- act(() => {
- getByRole('tree').focus();
- });
-
- expect(getByTestId('one')).toHaveVirtualFocus();
+ fireEvent.keyDown(getByTestId('one'), { key: 'ArrowDown' });
+ expect(getByTestId('two')).toHaveFocus();
});
});
describe('`disabledItemsFocusable=false`', () => {
it('should prevent focus by mouse', () => {
- const focusSpy = spy();
+ const onNodeFocus = spy();
const { getByText } = render(
-
+
,
);
fireEvent.click(getByText('two'));
- expect(focusSpy.callCount).to.equal(0);
+ expect(onNodeFocus.callCount).to.equal(0);
});
- it('should prevent programmatic focus', () => {
- const { getByTestId } = render(
+ it('should prevent focus when clicking', () => {
+ const handleMouseDown = spy();
+
+ const { getByText } = render(
-
+
,
);
- act(() => {
- getByTestId('one').focus();
- });
- expect(getByTestId('one')).not.toHaveVirtualFocus();
+ fireEvent.mouseDown(getByText('one'));
+ expect(handleMouseDown.lastCall.firstArg.defaultPrevented).to.equal(true);
});
it('should prevent focus by type-ahead', () => {
- const { getByRole, getByTestId } = render(
+ const { getByTestId } = render(
@@ -2189,15 +2138,15 @@ describe('', () => {
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- expect(getByTestId('one')).toHaveVirtualFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 't' });
- expect(getByTestId('one')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
+ fireEvent.keyDown(getByTestId('one'), { key: 't' });
+ expect(getByTestId('one')).toHaveFocus();
});
it('should be skipped on navigation with arrow keys', () => {
- const { getByRole, getByTestId } = render(
+ const { getByTestId } = render(
@@ -2206,36 +2155,33 @@ describe('', () => {
);
act(() => {
- getByRole('tree').focus();
+ getByTestId('one').focus();
});
- expect(getByTestId('one')).toHaveVirtualFocus();
+ expect(getByTestId('one')).toHaveFocus();
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowDown' });
- expect(getByTestId('three')).toHaveVirtualFocus();
+ fireEvent.keyDown(getByTestId('one'), { key: 'ArrowDown' });
+ expect(getByTestId('three')).toHaveFocus();
});
- it('should not be focused on tree focus', () => {
- const { getByRole, getByTestId } = render(
+ it('should set tabIndex={-1} and tabIndex={0} on next item', () => {
+ const { getByTestId } = render(
,
);
- act(() => {
- getByRole('tree').focus();
- });
-
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('one').tabIndex).to.equal(-1);
+ expect(getByTestId('two').tabIndex).to.equal(0);
});
});
});
describe('expansion', () => {
describe('`disabledItemsFocusable={true}`', () => {
- it('should prevent expansion on enter', () => {
- const { getByRole, getByTestId } = render(
+ it('should prevent expansion on Enter', () => {
+ const { getByTestId } = render(
@@ -2247,14 +2193,14 @@ describe('', () => {
act(() => {
getByTestId('two').focus();
});
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
expect(getByTestId('two')).to.have.attribute('aria-expanded', 'false');
- fireEvent.keyDown(getByRole('tree'), { key: 'Enter' });
+ fireEvent.keyDown(getByTestId('two'), { key: 'Enter' });
expect(getByTestId('two')).to.have.attribute('aria-expanded', 'false');
});
it('should prevent expansion on right arrow', () => {
- const { getByRole, getByTestId } = render(
+ const { getByTestId } = render(
@@ -2266,14 +2212,14 @@ describe('', () => {
act(() => {
getByTestId('two').focus();
});
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
expect(getByTestId('two')).to.have.attribute('aria-expanded', 'false');
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowRight' });
+ fireEvent.keyDown(getByTestId('two'), { key: 'ArrowRight' });
expect(getByTestId('two')).to.have.attribute('aria-expanded', 'false');
});
it('should prevent collapse on left arrow', () => {
- const { getByRole, getByTestId } = render(
+ const { getByTestId } = render(
@@ -2285,9 +2231,9 @@ describe('', () => {
act(() => {
getByTestId('two').focus();
});
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
expect(getByTestId('two')).to.have.attribute('aria-expanded', 'true');
- fireEvent.keyDown(getByRole('tree'), { key: 'ArrowLeft' });
+ fireEvent.keyDown(getByTestId('two'), { key: 'ArrowLeft' });
expect(getByTestId('two')).to.have.attribute('aria-expanded', 'true');
});
});
@@ -2410,7 +2356,7 @@ describe('', () => {
const { getByText, getByTestId, getByRole } = render(
-
+
@@ -2419,10 +2365,10 @@ describe('', () => {
fireEvent.click(getByText('two'));
act(() => {
- getByRole('tree').focus();
+ getByTestId('two').focus();
});
- expect(getByTestId('two')).toHaveVirtualFocus();
+ expect(getByTestId('two')).toHaveFocus();
act(() => {
getByRole('button').focus();
diff --git a/packages/x-tree-view/src/TreeItem/TreeItem.tsx b/packages/x-tree-view/src/TreeItem/TreeItem.tsx
index c863841c26dc2..803b557df2455 100644
--- a/packages/x-tree-view/src/TreeItem/TreeItem.tsx
+++ b/packages/x-tree-view/src/TreeItem/TreeItem.tsx
@@ -6,6 +6,7 @@ import { resolveComponentProps, useSlotProps } from '@mui/base/utils';
import { alpha, styled, useThemeProps } from '@mui/material/styles';
import unsupportedProp from '@mui/utils/unsupportedProp';
import elementTypeAcceptingRef from '@mui/utils/elementTypeAcceptingRef';
+import useForkRef from '@mui/utils/useForkRef';
import { unstable_composeClasses as composeClasses } from '@mui/base';
import { TreeItemContent } from './TreeItemContent';
import { treeItemClasses, getTreeItemUtilityClass } from './treeItemClasses';
@@ -177,9 +178,18 @@ export const TreeItem = React.forwardRef(function TreeItem(
onMouseDown,
TransitionComponent = Collapse,
TransitionProps,
+ onFocus,
+ onBlur,
+ onKeyDown,
...other
} = props;
+ const handleContentRef = React.useRef(null);
+ const contentRef = useForkRef(ContentProps?.ref, handleContentRef);
+
+ const handleRootRef = React.useRef(null);
+ const rootRef = useForkRef(ref, handleRootRef);
+
const slots = {
expandIcon: inSlots?.expandIcon ?? contextIcons.slots.expandIcon ?? TreeViewExpandIcon,
collapseIcon: inSlots?.collapseIcon ?? contextIcons.slots.collapseIcon ?? TreeViewCollapseIcon,
@@ -269,18 +279,24 @@ export const TreeItem = React.forwardRef(function TreeItem(
}
function handleFocus(event: React.FocusEvent) {
- // DOM focus stays on the tree which manages focus with aria-activedescendant
- if (event.target === event.currentTarget) {
- instance.focusRoot();
- }
-
const canBeFocused = !disabled || disabledItemsFocusable;
if (!focused && canBeFocused && event.currentTarget === event.target) {
instance.focusNode(event, nodeId);
}
}
+ function handleBlur(event: React.FocusEvent) {
+ onBlur?.(event);
+ instance.focusNode(event, null);
+ }
+
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ onKeyDown?.(event);
+ instance.handleItemKeyDown(event, nodeId);
+ };
+
const idAttribute = instance.getTreeItemId(nodeId, id);
+ const tabIndex = instance.canNodeBeTabbed(nodeId) ? 0 : -1;
const item = (
{children && (
,
/**
* Props applied to ContentComponent.
*/
- ContentProps?: React.HTMLAttributes;
+ ContentProps?: React.HTMLAttributes & { ref?: React.Ref };
/**
* If `true`, the node is disabled.
* @default false
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts
index e26bcd3f6cd9c..b53c17ef84a18 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts
@@ -51,7 +51,7 @@ export const useTreeViewExpansion: TreeViewPlugin
},
);
- const expandAllSiblings = (event: React.KeyboardEvent, nodeId: string) => {
+ const expandAllSiblings = (event: React.KeyboardEvent, nodeId: string) => {
const node = instance.getNode(nodeId);
const siblings = instance.getChildrenIds(node.parentId);
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.types.ts
index 46a2e2c80fa57..2283afef0f2b4 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.types.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.types.ts
@@ -6,7 +6,7 @@ export interface UseTreeViewExpansionInstance {
isNodeExpanded: (nodeId: string) => boolean;
isNodeExpandable: (nodeId: string) => boolean;
toggleNodeExpansion: (event: React.SyntheticEvent, value: string) => void;
- expandAllSiblings: (event: React.KeyboardEvent, nodeId: string) => void;
+ expandAllSiblings: (event: React.KeyboardEvent, nodeId: string) => void;
}
export interface UseTreeViewExpansionParameters {
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..f8b65f6559e99 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts
@@ -2,10 +2,34 @@ import * as React from 'react';
import useEventCallback from '@mui/utils/useEventCallback';
import { EventHandlers } from '@mui/base/utils';
import ownerDocument from '@mui/utils/ownerDocument';
-import { TreeViewPlugin } from '../../models';
+import { TreeViewPlugin, TreeViewUsedInstance } from '../../models';
import { populateInstance } from '../../useTreeView/useTreeView.utils';
import { UseTreeViewFocusSignature } from './useTreeViewFocus.types';
import { useInstanceEventHandler } from '../../hooks/useInstanceEventHandler';
+import { getActiveElement } from '../../utils/utils';
+
+const useTabbableNodeId = (
+ instance: TreeViewUsedInstance,
+ selectedNodes: string | string[] | null,
+) => {
+ const isNodeVisible = (nodeId: string) => {
+ const node = instance.getNode(nodeId);
+ return node && (node.parentId == null || instance.isNodeExpanded(node.parentId));
+ };
+
+ let tabbableNodeId: string | null | undefined;
+ if (Array.isArray(selectedNodes)) {
+ tabbableNodeId = selectedNodes.find(isNodeVisible);
+ } else if (selectedNodes != null && isNodeVisible(selectedNodes)) {
+ tabbableNodeId = selectedNodes;
+ }
+
+ if (tabbableNodeId == null) {
+ tabbableNodeId = instance.getNavigableChildrenIds(null)[0];
+ }
+
+ return tabbableNodeId;
+};
export const useTreeViewFocus: TreeViewPlugin = ({
instance,
@@ -15,6 +39,8 @@ export const useTreeViewFocus: TreeViewPlugin = ({
models,
rootRef,
}) => {
+ const tabbableNodeId = useTabbableNodeId(instance, models.selectedNodes.value);
+
const setFocusedNodeId = useEventCallback((nodeId: React.SetStateAction) => {
const cleanNodeId = typeof nodeId === 'function' ? nodeId(state.focusedNodeId) : nodeId;
setState((prevState) => ({ ...prevState, focusedNodeId: cleanNodeId }));
@@ -25,36 +51,44 @@ export const useTreeViewFocus: TreeViewPlugin = ({
[state.focusedNodeId],
);
- const focusNode = useEventCallback((event: React.SyntheticEvent, nodeId: string | null) => {
- if (nodeId) {
- setFocusedNodeId(nodeId);
+ const isTreeViewFocused = () =>
+ !!rootRef.current && rootRef.current.contains(getActiveElement(ownerDocument(rootRef.current)));
+
+ const focusNode = useEventCallback(
+ (event: React.SyntheticEvent | null, nodeId: string | null) => {
+ if (nodeId) {
+ const node = instance.getNode(nodeId);
+ const nodeElement = document.getElementById(
+ instance.getTreeItemId(nodeId, node.idAttribute),
+ );
+ if (nodeElement) {
+ nodeElement.focus({ preventScroll: true });
+ }
+
+ setFocusedNodeId(nodeId);
- if (params.onNodeFocus) {
- params.onNodeFocus(event, nodeId);
+ if (params.onNodeFocus) {
+ params.onNodeFocus(event, nodeId);
+ }
+ } else {
+ setFocusedNodeId(null);
}
- }
- });
+ },
+ );
- const focusRoot = useEventCallback(() => {
- rootRef.current?.focus({ preventScroll: true });
- });
+ const canNodeBeTabbed = (nodeId: string) => nodeId === tabbableNodeId;
populateInstance(instance, {
isNodeFocused,
+ isTreeViewFocused,
+ canNodeBeTabbed,
focusNode,
- focusRoot,
});
useInstanceEventHandler(instance, 'removeNode', ({ id }) => {
- setFocusedNodeId((oldFocusedNodeId) => {
- if (
- oldFocusedNodeId === id &&
- rootRef.current === ownerDocument(rootRef.current).activeElement
- ) {
- return instance.getChildrenIds(null)[0];
- }
- return oldFocusedNodeId;
- });
+ if (state.focusedNodeId === id) {
+ instance.focusNode(null, instance.getChildrenIds(null)[0]);
+ }
});
const createHandleFocus =
@@ -86,12 +120,6 @@ export const useTreeViewFocus: TreeViewPlugin = ({
}
};
- const createHandleBlur =
- (otherHandlers: EventHandlers) => (event: React.FocusEvent) => {
- otherHandlers.onBlur?.(event);
- setFocusedNodeId(null);
- };
-
const focusedNode = instance.getNode(state.focusedNodeId!);
const activeDescendant = focusedNode
? instance.getTreeItemId(focusedNode.id, focusedNode.idAttribute)
@@ -100,7 +128,6 @@ export const useTreeViewFocus: TreeViewPlugin = ({
return {
getRootProps: (otherHandlers) => ({
onFocus: createHandleFocus(otherHandlers),
- onBlur: createHandleBlur(otherHandlers),
'aria-activedescendant': activeDescendant ?? undefined,
}),
};
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..ca52f3251275f 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
@@ -7,8 +7,9 @@ import { UseTreeViewExpansionSignature } from '../useTreeViewExpansion';
export interface UseTreeViewFocusInstance {
isNodeFocused: (nodeId: string) => boolean;
- focusNode: (event: React.SyntheticEvent, nodeId: string | null) => void;
- focusRoot: () => void;
+ isTreeViewFocused: () => boolean;
+ canNodeBeTabbed: (nodeId: string) => boolean;
+ focusNode: (event: React.SyntheticEvent | null, nodeId: string | null) => void;
}
export interface UseTreeViewFocusParameters {
@@ -18,7 +19,7 @@ export interface UseTreeViewFocusParameters {
* @param {string} nodeId The id of the node focused.
* @param {string} value of the focused node.
*/
- onNodeFocus?: (event: React.SyntheticEvent, nodeId: string) => void;
+ onNodeFocus?: (event: React.SyntheticEvent | null, nodeId: string) => void;
}
export type UseTreeViewFocusDefaultizedParameters = UseTreeViewFocusParameters;
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts
index 544ed0a911f40..176b3041a8574 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts
@@ -1,6 +1,5 @@
import * as React from 'react';
import { useTheme } from '@mui/material/styles';
-import { EventHandlers } from '@mui/base/utils';
import useEventCallback from '@mui/utils/useEventCallback';
import { TreeViewPlugin } from '../../models';
import {
@@ -32,7 +31,7 @@ function findNextFirstChar(firstChars: string[], startIndex: number, char: strin
export const useTreeViewKeyboardNavigation: TreeViewPlugin<
UseTreeViewKeyboardNavigationSignature
-> = ({ instance, params, state }) => {
+> = ({ instance, params }) => {
const theme = useTheme();
const isRTL = theme.direction === 'rtl';
const firstCharMap = React.useRef({});
@@ -63,10 +62,6 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin<
firstCharMap.current = newFirstCharMap;
}, [params.items, params.getItemId, instance]);
- populateInstance(instance, {
- updateFirstCharMap,
- });
-
const getFirstMatchingNode = (nodeId: string, firstChar: string) => {
let start: number;
let index: number;
@@ -117,209 +112,197 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin<
!instance.isNodeDisabled(nodeId) && instance.isNodeExpandable(nodeId);
// ARIA specification: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/#keyboardinteraction
- const createHandleKeyDown =
- (otherHandlers: EventHandlers) =>
- (event: React.KeyboardEvent & MuiCancellableEvent) => {
- otherHandlers.onKeyDown?.(event);
+ const handleItemKeyDown = (
+ event: React.KeyboardEvent & MuiCancellableEvent,
+ nodeId: string,
+ ) => {
+ if (event.defaultMuiPrevented) {
+ return;
+ }
- if (event.defaultMuiPrevented) {
- return;
- }
+ if (event.altKey || event.currentTarget !== event.target) {
+ return;
+ }
- // If the tree is empty there will be no focused node
- if (event.altKey || event.currentTarget !== event.target || state.focusedNodeId == null) {
- return;
+ const ctrlPressed = event.ctrlKey || event.metaKey;
+ const key = event.key;
+
+ // eslint-disable-next-line default-case
+ switch (true) {
+ // Select the node when pressing "Space"
+ case key === ' ' && canToggleNodeSelection(nodeId): {
+ event.preventDefault();
+ if (params.multiSelect && event.shiftKey) {
+ instance.selectRange(event, { end: nodeId });
+ } else if (params.multiSelect) {
+ instance.selectNode(event, nodeId, true);
+ } else {
+ instance.selectNode(event, nodeId);
+ }
+ break;
}
- const ctrlPressed = event.ctrlKey || event.metaKey;
- const key = event.key;
-
- // eslint-disable-next-line default-case
- switch (true) {
- // Select the node when pressing "Space"
- case key === ' ' && canToggleNodeSelection(state.focusedNodeId): {
+ // If the focused node has children, we expand it.
+ // If the focused node has no children, we select it.
+ case key === 'Enter': {
+ if (canToggleNodeExpansion(nodeId)) {
+ instance.toggleNodeExpansion(event, nodeId);
event.preventDefault();
- if (params.multiSelect && event.shiftKey) {
- instance.selectRange(event, { end: state.focusedNodeId });
- } else if (params.multiSelect) {
- instance.selectNode(event, state.focusedNodeId, true);
- } else {
- instance.selectNode(event, state.focusedNodeId);
+ } else if (canToggleNodeSelection(nodeId)) {
+ if (params.multiSelect) {
+ event.preventDefault();
+ instance.selectNode(event, nodeId, true);
+ } else if (!instance.isNodeSelected(nodeId)) {
+ instance.selectNode(event, nodeId);
+ event.preventDefault();
}
- break;
}
- // If the focused node has children, we expand it.
- // If the focused node has no children, we select it.
- case key === 'Enter': {
- if (canToggleNodeExpansion(state.focusedNodeId)) {
- instance.toggleNodeExpansion(event, state.focusedNodeId);
- event.preventDefault();
- } else if (canToggleNodeSelection(state.focusedNodeId)) {
- if (params.multiSelect) {
- event.preventDefault();
- instance.selectNode(event, state.focusedNodeId, true);
- } else if (!instance.isNodeSelected(state.focusedNodeId)) {
- instance.selectNode(event, state.focusedNodeId);
- event.preventDefault();
- }
- }
+ break;
+ }
- break;
+ // Focus the next focusable node
+ case key === 'ArrowDown': {
+ const nextNode = getNextNode(instance, nodeId);
+ if (nextNode) {
+ event.preventDefault();
+ instance.focusNode(event, nextNode);
+
+ // Multi select behavior when pressing Shift + ArrowDown
+ // Toggles the selection state of the next node
+ if (params.multiSelect && event.shiftKey && canToggleNodeSelection(nextNode)) {
+ instance.selectRange(
+ event,
+ {
+ end: nextNode,
+ current: nodeId,
+ },
+ true,
+ );
+ }
}
- // Focus the next focusable node
- case key === 'ArrowDown': {
- const nextNode = getNextNode(instance, state.focusedNodeId);
- if (nextNode) {
- event.preventDefault();
- instance.focusNode(event, nextNode);
-
- // Multi select behavior when pressing Shift + ArrowDown
- // Toggles the selection state of the next node
- if (params.multiSelect && event.shiftKey && canToggleNodeSelection(nextNode)) {
- instance.selectRange(
- event,
- {
- end: nextNode,
- current: state.focusedNodeId,
- },
- true,
- );
- }
- }
+ break;
+ }
- break;
+ // Focuses the previous focusable node
+ case key === 'ArrowUp': {
+ const previousNode = getPreviousNode(instance, nodeId);
+ if (previousNode) {
+ event.preventDefault();
+ instance.focusNode(event, previousNode);
+
+ // Multi select behavior when pressing Shift + ArrowUp
+ // Toggles the selection state of the previous node
+ if (params.multiSelect && event.shiftKey && canToggleNodeSelection(previousNode)) {
+ instance.selectRange(
+ event,
+ {
+ end: previousNode,
+ current: nodeId,
+ },
+ true,
+ );
+ }
}
- // Focuses the previous focusable node
- case key === 'ArrowUp': {
- const previousNode = getPreviousNode(instance, state.focusedNodeId);
- if (previousNode) {
- event.preventDefault();
- instance.focusNode(event, previousNode);
-
- // Multi select behavior when pressing Shift + ArrowUp
- // Toggles the selection state of the previous node
- if (params.multiSelect && event.shiftKey && canToggleNodeSelection(previousNode)) {
- instance.selectRange(
- event,
- {
- end: previousNode,
- current: state.focusedNodeId,
- },
- true,
- );
- }
- }
+ break;
+ }
- break;
+ // If the focused node is expanded, we move the focus to its first child
+ // If the focused node is collapsed and has children, we expand it
+ case (key === 'ArrowRight' && !isRTL) || (key === 'ArrowLeft' && isRTL): {
+ if (instance.isNodeExpanded(nodeId)) {
+ instance.focusNode(event, getNextNode(instance, nodeId));
+ event.preventDefault();
+ } else if (canToggleNodeExpansion(nodeId)) {
+ instance.toggleNodeExpansion(event, nodeId);
+ event.preventDefault();
}
- // If the focused node is expanded, we move the focus to its first child
- // If the focused node is collapsed and has children, we expand it
- case (key === 'ArrowRight' && !isRTL) || (key === 'ArrowLeft' && isRTL): {
- if (instance.isNodeExpanded(state.focusedNodeId)) {
- instance.focusNode(event, getNextNode(instance, state.focusedNodeId));
- event.preventDefault();
- } else if (canToggleNodeExpansion(state.focusedNodeId)) {
- instance.toggleNodeExpansion(event, state.focusedNodeId);
+ break;
+ }
+
+ // If the focused node is expanded, we collapse it
+ // If the focused node is collapsed and has a parent, we move the focus to this parent
+ case (key === 'ArrowLeft' && !isRTL) || (key === 'ArrowRight' && isRTL): {
+ if (canToggleNodeExpansion(nodeId) && instance.isNodeExpanded(nodeId)) {
+ instance.toggleNodeExpansion(event, nodeId);
+ event.preventDefault();
+ } else {
+ const parent = instance.getNode(nodeId).parentId;
+ if (parent) {
+ instance.focusNode(event, parent);
event.preventDefault();
}
-
- break;
}
- // If the focused node is expanded, we collapse it
- // If the focused node is collapsed and has a parent, we move the focus to this parent
- case (key === 'ArrowLeft' && !isRTL) || (key === 'ArrowRight' && isRTL): {
- if (
- canToggleNodeExpansion(state.focusedNodeId) &&
- instance.isNodeExpanded(state.focusedNodeId)
- ) {
- instance.toggleNodeExpansion(event, state.focusedNodeId!);
- event.preventDefault();
- } else {
- const parent = instance.getNode(state.focusedNodeId).parentId;
- if (parent) {
- instance.focusNode(event, parent);
- event.preventDefault();
- }
- }
+ break;
+ }
- break;
+ // Focuses the first node in the tree
+ case key === 'Home': {
+ instance.focusNode(event, getFirstNode(instance));
+
+ // Multi select behavior when pressing Ctrl + Shift + Home
+ // Selects the focused node and all nodes up to the first node.
+ if (canToggleNodeSelection(nodeId) && params.multiSelect && ctrlPressed && event.shiftKey) {
+ instance.rangeSelectToFirst(event, nodeId);
}
- // Focuses the first node in the tree
- case key === 'Home': {
- instance.focusNode(event, getFirstNode(instance));
-
- // Multi select behavior when pressing Ctrl + Shift + Home
- // Selects the focused node and all nodes up to the first node.
- if (
- canToggleNodeSelection(state.focusedNodeId) &&
- params.multiSelect &&
- ctrlPressed &&
- event.shiftKey
- ) {
- instance.rangeSelectToFirst(event, state.focusedNodeId);
- }
+ event.preventDefault();
+ break;
+ }
- event.preventDefault();
- break;
+ // Focuses the last node in the tree
+ case key === 'End': {
+ instance.focusNode(event, getLastNode(instance));
+
+ // Multi select behavior when pressing Ctrl + Shirt + End
+ // Selects the focused node and all the nodes down to the last node.
+ if (canToggleNodeSelection(nodeId) && params.multiSelect && ctrlPressed && event.shiftKey) {
+ instance.rangeSelectToLast(event, nodeId);
}
- // Focuses the last node in the tree
- case key === 'End': {
- instance.focusNode(event, getLastNode(instance));
-
- // Multi select behavior when pressing Ctrl + Shirt + End
- // Selects the focused node and all the nodes down to the last node.
- if (
- canToggleNodeSelection(state.focusedNodeId) &&
- params.multiSelect &&
- ctrlPressed &&
- event.shiftKey
- ) {
- instance.rangeSelectToLast(event, state.focusedNodeId);
- }
+ event.preventDefault();
+ break;
+ }
- event.preventDefault();
- break;
- }
+ // Expand all siblings that are at the same level as the focused node
+ case key === '*': {
+ instance.expandAllSiblings(event, nodeId);
+ event.preventDefault();
+ break;
+ }
- // Expand all siblings that are at the same level as the focused node
- case key === '*': {
- instance.expandAllSiblings(event, state.focusedNodeId);
- event.preventDefault();
- break;
- }
+ // Multi select behavior when pressing Ctrl + a
+ // Selects all the nodes
+ case key === 'a' && ctrlPressed && params.multiSelect && !params.disableSelection: {
+ instance.selectRange(event, {
+ start: getFirstNode(instance),
+ end: getLastNode(instance),
+ });
+ event.preventDefault();
+ break;
+ }
- // Multi select behavior when pressing Ctrl + a
- // Selects all the nodes
- case key === 'a' && ctrlPressed && params.multiSelect && !params.disableSelection: {
- instance.selectRange(event, {
- start: getFirstNode(instance),
- end: getLastNode(instance),
- });
+ // Type-ahead
+ // TODO: Support typing multiple characters
+ case !ctrlPressed && !event.shiftKey && isPrintableCharacter(key): {
+ const matchingNode = getFirstMatchingNode(nodeId, key);
+ if (matchingNode != null) {
+ instance.focusNode(event, matchingNode);
event.preventDefault();
- break;
- }
-
- // Type-ahead
- // TODO: Support typing multiple characters
- case !ctrlPressed && !event.shiftKey && isPrintableCharacter(key): {
- const matchingNode = getFirstMatchingNode(state.focusedNodeId, key);
- if (matchingNode != null) {
- instance.focusNode(event, matchingNode);
- event.preventDefault();
- }
- break;
}
+ break;
}
- };
+ }
+ };
- return { getRootProps: (otherHandlers) => ({ onKeyDown: createHandleKeyDown(otherHandlers) }) };
+ populateInstance(instance, {
+ updateFirstCharMap,
+ handleItemKeyDown,
+ });
};
useTreeViewKeyboardNavigation.params = {};
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.types.ts
index a3502830bdcb9..92b3d81dc981c 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.types.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.types.ts
@@ -1,11 +1,17 @@
+import * as React from 'react';
import { TreeViewPluginSignature } from '../../models';
import { UseTreeViewNodesSignature } from '../useTreeViewNodes';
import { UseTreeViewSelectionSignature } from '../useTreeViewSelection';
import { UseTreeViewFocusSignature } from '../useTreeViewFocus';
import { UseTreeViewExpansionSignature } from '../useTreeViewExpansion';
+import { MuiCancellableEvent } from '../../models/MuiCancellableEvent';
export interface UseTreeViewKeyboardNavigationInstance {
updateFirstCharMap: (updater: (map: TreeViewFirstCharMap) => TreeViewFirstCharMap) => void;
+ handleItemKeyDown: (
+ event: React.KeyboardEvent & MuiCancellableEvent,
+ nodeId: string,
+ ) => void;
}
export type UseTreeViewKeyboardNavigationSignature = TreeViewPluginSignature<{
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewNodes/useTreeViewNodes.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewNodes/useTreeViewNodes.ts
index 43aef3ff53a2e..43447abde6975 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewNodes/useTreeViewNodes.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewNodes/useTreeViewNodes.ts
@@ -22,7 +22,6 @@ const updateState = ({
'items' | 'isItemDisabled' | 'getItemLabel' | 'getItemId'
>): UseTreeViewNodesState => {
const nodeMap: TreeViewNodeMap = {};
-
const processItem = (
item: TreeViewBaseItem,
index: number,
@@ -124,11 +123,13 @@ export const useTreeViewNodes: TreeViewPlugin = ({
[instance],
);
- const getChildrenIds = useEventCallback((nodeId: string | null) =>
- Object.values(state.nodeMap)
- .filter((node) => node.parentId === nodeId)
- .sort((a, b) => a.index - b.index)
- .map((child) => child.id),
+ const getChildrenIds = React.useCallback(
+ (nodeId: string | null) =>
+ Object.values(state.nodeMap)
+ .filter((node) => node.parentId === nodeId)
+ .sort((a, b) => a.index - b.index)
+ .map((child) => child.id),
+ [state.nodeMap],
);
const getNavigableChildrenIds = (nodeId: string | null) => {
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts
index 3d30f49d8f5a4..c236a947ae6cd 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts
@@ -161,7 +161,7 @@ export const useTreeViewSelection: TreeViewPlugin
lastSelectionWasRange.current = true;
};
- const rangeSelectToFirst = (event: React.KeyboardEvent, nodeId: string) => {
+ const rangeSelectToFirst = (event: React.KeyboardEvent, nodeId: string) => {
if (!lastSelectedNode.current) {
lastSelectedNode.current = nodeId;
}
@@ -174,7 +174,7 @@ export const useTreeViewSelection: TreeViewPlugin
});
};
- const rangeSelectToLast = (event: React.KeyboardEvent, nodeId: string) => {
+ const rangeSelectToLast = (event: React.KeyboardEvent, nodeId: string) => {
if (!lastSelectedNode.current) {
lastSelectedNode.current = nodeId;
}
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts
index 6afbfaaafd8e5..2d94f19734c98 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts
@@ -7,8 +7,8 @@ export interface UseTreeViewSelectionInstance {
isNodeSelected: (nodeId: string) => boolean;
selectNode: (event: React.SyntheticEvent, nodeId: string, multiple?: boolean) => void;
selectRange: (event: React.SyntheticEvent, nodes: TreeViewItemRange, stacked?: boolean) => void;
- rangeSelectToFirst: (event: React.KeyboardEvent, nodeId: string) => void;
- rangeSelectToLast: (event: React.KeyboardEvent, nodeId: string) => void;
+ rangeSelectToFirst: (event: React.KeyboardEvent, nodeId: string) => void;
+ rangeSelectToLast: (event: React.KeyboardEvent, nodeId: string) => void;
}
type TreeViewSelectionValue = Multiple extends true
diff --git a/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts b/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts
index 06d6d9e0ca648..83fb129c501d6 100644
--- a/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts
+++ b/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts
@@ -129,7 +129,6 @@ export const useTreeView = {
const rootProps: UseTreeViewRootSlotProps = {
role: 'tree',
- tabIndex: 0,
...otherHandlers,
ref: handleRootRef,
};
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..5401ae664aab2
--- /dev/null
+++ b/packages/x-tree-view/src/internals/utils/utils.ts
@@ -0,0 +1,14 @@
+// https://www.abeautifulsite.net/posts/finding-the-active-element-in-a-shadow-root/
+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;
+};