-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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] Set focus on the focused Tree Item instead of the Tree View #12226
Conversation
daac4c2
to
302ee63
Compare
Deploy preview: https://deploy-preview-12226--material-ui-x.netlify.app/ Updated pages: |
f0eae01
to
40f6c9f
Compare
@@ -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 => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copied from the pickers and the grid
@@ -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<HTMLUListElement>, nodeId: string) => void; | |||
rangeSelectToLast: (event: React.KeyboardEvent<HTMLUListElement>, nodeId: string) => void; | |||
rangeSelectToFirst: (event: React.KeyboardEvent, nodeId: string) => void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was always fired by the Tree View component
But it was not needed an UL specifically so I reduced the type precision
.filter((node) => node.parentId === nodeId) | ||
.sort((a, b) => a.index - b.index) | ||
.map((child) => child.id), | ||
const getChildrenIds = React.useCallback( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useEventCallback
is not compatible with methods fired in the render because they are always outdated (since the update is made in a useLayoutEffect
.
This method is now needed to compute the tabbable node.
// 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All the changes below just replace state.focusedNodeId
with nodeId
return node && (node.parentId == null || instance.isNodeExpanded(node.parentId)); | ||
}; | ||
|
||
let tabbableNodeId: string | null | undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think this is worth memoizing?
expect(getByTestId('one')).toHaveVirtualFocus(); | ||
}); | ||
|
||
it('should focus the selected node if a node is selected before the tree receives focus', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replaced by should set tabIndex={0} on the selected item (multi select)
and should set tabIndex={0} on the selected item
expect(getByTestId('parent')).toHaveVirtualFocus(); | ||
}); | ||
|
||
it('should focus on tree with scroll prevented', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't make sense anymore
expect(getByTestId('two')).toHaveVirtualFocus(); | ||
}); | ||
|
||
it('should be focused on tree focus', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replaced by should not prevent programmatic focus
40f6c9f
to
2558231
Compare
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
} | ||
export interface UseTreeViewFocusPublicAPI { | ||
focusNode: (event: React.SyntheticEvent, nodeId: string | null) => void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@noraleonte with the introduction of focusDefaultNode
, do we need to support null
here?
isTreeViewFocused: () => boolean; | ||
canNodeBeTabbed: (nodeId: string) => boolean; | ||
focusNode: (event: React.SyntheticEvent, nodeId: string) => void; | ||
focusDefaultNode: (event: React.SyntheticEvent | null) => void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm adding null
on the event so that when you remove the focused node, it can switch to another node (was already the case before) AND call onNodeFocus
for this new node (was not the case before).
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work, looks and works nicely! 👏
docs/data/migration/migration-tree-view-v6/migration-tree-view-v6.md
Outdated
Show resolved
Hide resolved
</SimpleTreeView>, | ||
); | ||
act(() => { | ||
getByRole('tree').focus(); | ||
getByTestId('one').focus(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: It seems possible to avoid the usage of testId
by keeping the getByRole
.
We could add the explicit "name" field if we so desire extra stability.
Nitpick2: Is there a specific reason why we are using the methods de-structured from the render
function instead of the root import of screen
? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Relates a lot to #12428
Nitpick: It seems possible to avoid the usage of testId by keeping the getByRole.
We could add the explicit "name" field if we so desire extra stability.
It's mostly for consistency across the tests, the vast majority uses testId
.
I think we can align on a single approach in #12428 and use it everywhere while migrating all the tests to describeTreeView
.
Nitpick2: Is there a specific reason why we are using the methods de-structured from the render function instead of the root import of screen?
screen
is the advised way by the created or react-testing-library
from what I remember, so no specific reason.
This topic will go away with #12428 if we create the right abstractions (which I think we should for readability).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's mostly for consistency across the tests, the vast majority uses
testId
.
getByRole
has ~2x more usages than getByTestId
(and almost all of them are on TreeView tests). 🤔
IMHO, it would be best to use getByTestId
when there is no other way to select elements. 🙈
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(and all getByRole
are for tree
AFAIK)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I was referring to the global usage in the mui-x
codebase, sorry for the confusion. 🙈
packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.test.tsx
Outdated
Show resolved
Hide resolved
</SimpleTreeView>, | ||
); | ||
act(() => { | ||
getByRole('tree').focus(); | ||
getByTestId('one').focus(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same question/nitpick regarding the usage of getByRole
applies here and in the whole test file. 🤔
packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.test.tsx
Outdated
Show resolved
Hide resolved
</SimpleTreeView>, | ||
); | ||
|
||
act(() => { | ||
getByRole('tree').focus(); | ||
getByTestId('one').focus(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A friendly suggestion just like in the other test file to prefer using screen
, getByRole
and saving the result of getByRole
in a variable to reuse instead of calling it multiple times.
packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts
Show resolved
Hide resolved
...ee-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts
Outdated
Show resolved
Hide resolved
packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts
Show resolved
Hide resolved
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks and works great! Nice improvement! 🎉
See mui/material-ui#21695 as prior work on this topic.
Preview: https://deploy-preview-12226--material-ui-x.netlify.app/x/react-tree-view/#simple-tree-view
Fixes #9961
Fixes #9958
Fixes #10234
I did not explore if totally removing the focus from the state of the Tree View was doable, because the timing is tight before stable and this would only be an internal change 👍
Changelog
Breaking changes
The focus is now applied to the Tree Item root element instead of the Tree View root element.
This change will allow new features that require the focus to be on the Tree Item,
like the drag and drop reordering of items.
It also solves several issues with focus management,
like the inability to scroll to the focused item when a lot of items are rendered.
This will mostly impact how you write tests to interact with the Tree View:
For example, if you were writing a test with
react-testing-library
, here is what the changes could look like: