diff --git a/change/@fluentui-react-tree-409b60b2-2543-40ee-9e92-f53b02cecb5d.json b/change/@fluentui-react-tree-409b60b2-2543-40ee-9e92-f53b02cecb5d.json new file mode 100644 index 0000000000000..0899b9b325e16 --- /dev/null +++ b/change/@fluentui-react-tree-409b60b2-2543-40ee-9e92-f53b02cecb5d.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "bugfix: fix parent navigation after independency from id", + "packageName": "@fluentui/react-tree", + "email": "bernardo.sunderhus@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-tree/src/components/Tree/Tree.cy.tsx b/packages/react-components/react-tree/src/components/Tree/Tree.cy.tsx index 5fbe860fe3aa4..b5bf3164c6a14 100644 --- a/packages/react-components/react-tree/src/components/Tree/Tree.cy.tsx +++ b/packages/react-components/react-tree/src/components/Tree/Tree.cy.tsx @@ -18,40 +18,42 @@ const mount = (element: JSX.Element) => { mountBase({element}); }; +const treeItems = ( + <> + + level 1, item 1 + + + level 2, item 1 + + + level 2, item 2 + + + level 2, item 3 + + + + + level 1, item 2 + + + level 2, item 1 + + + level 3, item 1 + + + + + + +); + const NestedTree: React.FC = props => { return ( - {props.children ?? ( - <> - - level 1, item 1 - - - level 2, item 1 - - - level 2, item 2 - - - level 2, item 3 - - - - - level 1, item 2 - - - level 2, item 1 - - - level 3, item 1 - - - - - - - )} + {props.children ?? treeItems} ); }; @@ -59,41 +61,7 @@ NestedTree.displayName = 'NestedTree'; const FlatTree: React.FC = (props: TreeProps) => { const flatTree = useFlatTree_unstable( - flattenTreeFromElement( - props.children ? ( - <>{props.children} - ) : ( - <> - - level 1, item 1 - - - level 2, item 1 - - - level 2, item 2 - - - level 2, item 3 - - - - - level 1, item 2 - - - level 2, item 1 - - - level 3, item 1 - - - - - - - ), - ), + flattenTreeFromElement(props.children ? <>{props.children} : treeItems), props, ); return ( @@ -110,30 +78,30 @@ for (const TreeTest of [NestedTree, FlatTree]) { describe(TreeTest.displayName!, () => { it('should have all but first level items hidden', () => { mount(); - cy.get('#item1__item1').should('not.exist'); - cy.get('#item1__item2').should('not.exist'); - cy.get('#item1__item3').should('not.exist'); - cy.get('#item2__item1').should('not.exist'); - cy.get('#item2__item1__item1').should('not.exist'); + cy.get('[data-testid="item1__item1"]').should('not.exist'); + cy.get('[data-testid="item1__item2"]').should('not.exist'); + cy.get('[data-testid="item1__item3"]').should('not.exist'); + cy.get('[data-testid="item2__item1"]').should('not.exist'); + cy.get('[data-testid="item2__item1__item1"]').should('not.exist'); }); it('should have all items visible', () => { mount(); - cy.get('#item1__item1').should('exist'); - cy.get('#item1__item2').should('exist'); - cy.get('#item1__item3').should('exist'); - cy.get('#item2__item1').should('exist'); - cy.get('#item2__item1__item1').should('exist'); + cy.get('[data-testid="item1__item1"]').should('exist'); + cy.get('[data-testid="item1__item2"]').should('exist'); + cy.get('[data-testid="item1__item3"]').should('exist'); + cy.get('[data-testid="item2__item1"]').should('exist'); + cy.get('[data-testid="item2__item1__item1"]').should('exist'); }); describe('Mouse interactions', () => { it('should expand/collapse item on layout click', () => { mount(); - cy.get('#item1__item1').should('not.exist'); - cy.get(`#item1 .${treeItemLayoutClassNames.root}`).realClick(); - cy.get('#item1__item1').should('exist'); - cy.get(`#item1 .${treeItemLayoutClassNames.root}`).realClick(); - cy.get('#item1__item1').should('not.exist'); + cy.get('[data-testid="item1__item1"]').should('not.exist'); + cy.get(`[data-testid="item1"] .${treeItemLayoutClassNames.root}`).realClick(); + cy.get('[data-testid="item1__item1"]').should('exist'); + cy.get(`[data-testid="item1"] .${treeItemLayoutClassNames.root}`).realClick(); + cy.get('[data-testid="item1__item1"]').should('not.exist'); }); it('should expand/collapse item on expandIcon click only', () => { mount( @@ -145,14 +113,14 @@ for (const TreeTest of [NestedTree, FlatTree]) { }} />, ); - cy.get('#item1__item1').should('not.exist'); - cy.get(`#item1 .${treeItemClassNames.expandIcon}`).realClick(); - cy.get('#item1__item1').should('exist'); - cy.get(`#item1 .${treeItemClassNames.expandIcon}`).realClick(); - cy.get('#item1__item1').should('not.exist'); + cy.get('[data-testid="item1__item1"]').should('not.exist'); + cy.get(`[data-testid="item1"] .${treeItemClassNames.expandIcon}`).realClick(); + cy.get('[data-testid="item1__item1"]').should('exist'); + cy.get(`[data-testid="item1"] .${treeItemClassNames.expandIcon}`).realClick(); + cy.get('[data-testid="item1__item1"]').should('not.exist'); - cy.get(`#item1 .${treeItemLayoutClassNames.root}`).realClick(); - cy.get('#item1__item1').should('not.exist'); + cy.get(`[data-testid="item1"] .${treeItemLayoutClassNames.root}`).realClick(); + cy.get('[data-testid="item1__item1"]').should('not.exist'); }); it('should not expand/collapse item on actions click', () => { mount( @@ -163,46 +131,47 @@ for (const TreeTest of [NestedTree, FlatTree]) { } - id="item1" + value="item1" + data-testid="item1" > level 1, item 1 - + level 2, item 1 , ); - cy.get('#item1__item1').should('not.exist'); - cy.get('#item1').focus(); + cy.get('[data-testid="item1__item1"]').should('not.exist'); + cy.get('[data-testid="item1"]').focus(); cy.get(`#action`).realClick(); - cy.get('#item1__item1').should('not.exist'); + cy.get('[data-testid="item1__item1"]').should('not.exist'); }); }); describe('Keyboard interactions', () => { it('should expand/collapse item on Enter key', () => { mount(); - cy.get('#item1').focus(); - cy.get('#item1__item1').should('not.exist'); - cy.get(`#item1 .${treeItemLayoutClassNames.root}`).realPress('{enter}'); - cy.get('#item1__item1').should('exist'); + cy.get('[data-testid="item1"]').focus(); + cy.get('[data-testid="item1__item1"]').should('not.exist'); + cy.get(`[data-testid="item1"] .${treeItemLayoutClassNames.root}`).realPress('{enter}'); + cy.get('[data-testid="item1__item1"]').should('exist'); }); it('should expand item on Right key', () => { mount(); - cy.get('#item1').focus(); - cy.get('#item1__item1').should('not.exist'); - cy.get(`#item1 .${treeItemLayoutClassNames.root}`).realPress('{rightarrow}'); - cy.get('#item1__item1').should('exist'); + cy.get('[data-testid="item1"]').focus(); + cy.get('[data-testid="item1__item1"]').should('not.exist'); + cy.get(`[data-testid="item1"] .${treeItemLayoutClassNames.root}`).realPress('{rightarrow}'); + cy.get('[data-testid="item1__item1"]').should('exist'); }); it('should collapse item on Left key', () => { mount(); - cy.get('#item1').focus(); - cy.get('#item1__item1').should('not.exist'); - cy.get(`#item1 .${treeItemLayoutClassNames.root}`).realPress('{rightarrow}'); - cy.get('#item1__item1').should('exist'); - cy.get(`#item1 .${treeItemLayoutClassNames.root}`).realPress('{leftarrow}'); - cy.get('#item1__item1').should('not.exist'); + cy.get('[data-testid="item1"]').focus(); + cy.get('[data-testid="item1__item1"]').should('not.exist'); + cy.get(`[data-testid="item1"] .${treeItemLayoutClassNames.root}`).realPress('{rightarrow}'); + cy.get('[data-testid="item1__item1"]').should('exist'); + cy.get(`[data-testid="item1"] .${treeItemLayoutClassNames.root}`).realPress('{leftarrow}'); + cy.get('[data-testid="item1__item1"]').should('not.exist'); }); it('should focus on actions when pressing tab key', () => { mount( @@ -213,11 +182,12 @@ for (const TreeTest of [NestedTree, FlatTree]) { } - id="item1" + value="item1" + data-testid="item1" > level 1, item 1 - + level 2, item 1 @@ -226,7 +196,7 @@ for (const TreeTest of [NestedTree, FlatTree]) { ); cy.focused().should('not.exist'); cy.document().realPress('Tab'); - cy.get('#item1').should('be.focused'); + cy.get('[data-testid="item1"]').should('be.focused'); cy.document().realPress('Tab'); cy.get('#action').should('be.focused'); }); @@ -239,11 +209,12 @@ for (const TreeTest of [NestedTree, FlatTree]) { } - id="item1" + value="item1" + data-testid="item1" > level 1, item 1 - + level 2, item 1 @@ -252,53 +223,53 @@ for (const TreeTest of [NestedTree, FlatTree]) { ); cy.focused().should('not.exist'); cy.document().realPress('Tab'); - cy.get('#item1').should('be.focused'); + cy.get('[data-testid="item1"]').should('be.focused'); cy.document().realPress('Tab'); cy.get('#action').should('be.focused').realPress('{enter}'); - cy.get('#item1__item1').should('not.exist'); + cy.get('[data-testid="item1__item1"]').should('not.exist'); cy.get('#action').should('be.focused').realPress('Space'); - cy.get('#item1__item1').should('not.exist'); + cy.get('[data-testid="item1__item1"]').should('not.exist'); }); it('should focus on first item when pressing tab key', () => { mount(); cy.focused().should('not.exist'); cy.document().realPress('Tab'); - cy.get('#item1').should('be.focused'); + cy.get('[data-testid="item1"]').should('be.focused'); }); it('should focus out of tree when pressing tab key inside tree.', () => { mount(); cy.focused().should('not.exist'); cy.document().realPress('Tab'); - cy.get('#item1').should('be.focused'); + cy.get('[data-testid="item1"]').should('be.focused'); cy.focused().realPress('Tab'); cy.focused().should('not.exist'); }); describe('Navigation', () => { it('should move with Up/Down keys', () => { mount(); - cy.get('#item1').focus().realPress('{downarrow}'); - cy.get('#item2').should('be.focused'); + cy.get('[data-testid="item1"]').focus().realPress('{downarrow}'); + cy.get('[data-testid="item2"]').should('be.focused'); cy.focused().realPress('Tab').should('not.exist'); }); it('should move with Left/Right keys', () => { mount(); - cy.get('#item1').focus().realPress('{downarrow}'); - cy.get('#item2').should('be.focused').realPress('{rightarrow}'); - cy.get('#item2__item1').should('be.focused').realPress('{rightarrow}'); - cy.get('#item2__item1__item1').should('be.focused').realPress('{leftarrow}'); - cy.get('#item2__item1').should('be.focused').realPress('{leftarrow}').realPress('{leftarrow}'); - cy.get('#item2').should('be.focused'); + cy.get('[data-testid="item1"]').focus().realPress('{downarrow}'); + cy.get('[data-testid="item2"]').should('be.focused').realPress('{rightarrow}'); + cy.get('[data-testid="item2__item1"]').should('be.focused').realPress('{rightarrow}'); + cy.get('[data-testid="item2__item1__item1"]').should('be.focused').realPress('{leftarrow}'); + cy.get('[data-testid="item2__item1"]').should('be.focused').realPress('{leftarrow}').realPress('{leftarrow}'); + cy.get('[data-testid="item2"]').should('be.focused'); }); it('should move to last item with End key', () => { mount(); - cy.get('#item1').focus().realPress('{end}'); - cy.get('#item2__item1__item1').should('be.focused'); + cy.get('[data-testid="item1"]').focus().realPress('{end}'); + cy.get('[data-testid="item2__item1__item1"]').should('be.focused'); }); it('should move to first item with Home key', () => { mount(); - cy.get('#item1').focus().realPress('{end}'); - cy.get('#item2__item1__item1').should('be.focused').realPress('{home}'); - cy.get('#item1').should('be.focused'); + cy.get('[data-testid="item1"]').focus().realPress('{end}'); + cy.get('[data-testid="item2__item1__item1"]').should('be.focused').realPress('{home}'); + cy.get('[data-testid="item1"]').should('be.focused'); }); }); }); diff --git a/packages/react-components/react-tree/src/hooks/useFlatTree.ts b/packages/react-components/react-tree/src/hooks/useFlatTree.ts index 6c125c1fd570e..b3def6b57e043 100644 --- a/packages/react-components/react-tree/src/hooks/useFlatTree.ts +++ b/packages/react-components/react-tree/src/hooks/useFlatTree.ts @@ -31,6 +31,7 @@ export type MutableFlatTreeItem = { index: number; value: Value; level: number; + ref: React.RefObject; getTreeItemProps(): Required< Pick, 'value' | 'aria-setsize' | 'aria-level' | 'aria-posinset' | 'leaf'> > & diff --git a/packages/react-components/react-tree/src/hooks/useFlatTreeNavigation.ts b/packages/react-components/react-tree/src/hooks/useFlatTreeNavigation.ts index 79eb5773f1d5c..b1b9e6d55cc7c 100644 --- a/packages/react-components/react-tree/src/hooks/useFlatTreeNavigation.ts +++ b/packages/react-components/react-tree/src/hooks/useFlatTreeNavigation.ts @@ -25,7 +25,7 @@ export function useFlatTreeNavigation(flatTreeItems: FlatTreeIte treeItemWalker.currentElement = data.target; return nextTypeAheadElement(treeItemWalker, data.event.key); case treeDataTypes.arrowLeft: - return parentElement(flatTreeItems, data.value, targetDocument); + return parentElement(flatTreeItems, data.value); case treeDataTypes.arrowRight: treeItemWalker.currentElement = data.target; return firstChild(data.target, treeItemWalker); @@ -66,13 +66,11 @@ function firstChild(target: HTMLElement, treeWalker: HTMLElementWalker): HTMLEle return null; } -function parentElement(flatTreeItems: FlatTreeItems, value: Value, document: Document) { +function parentElement(flatTreeItems: FlatTreeItems, value: Value) { const flatTreeItem = flatTreeItems.get(value); - if (flatTreeItem && flatTreeItem.parentValue) { - const parentId = flatTreeItems.get(flatTreeItem.parentValue)?.getTreeItemProps().id; - if (parentId) { - return document.getElementById(parentId); - } + if (flatTreeItem?.parentValue) { + const parentItem = flatTreeItems.get(flatTreeItem.parentValue); + return parentItem?.ref.current ?? null; } return null; } diff --git a/packages/react-components/react-tree/src/utils/createFlatTreeItems.ts b/packages/react-components/react-tree/src/utils/createFlatTreeItems.ts index b04f9a6b635d0..1cb3d00eb1fe4 100644 --- a/packages/react-components/react-tree/src/utils/createFlatTreeItems.ts +++ b/packages/react-components/react-tree/src/utils/createFlatTreeItems.ts @@ -1,5 +1,6 @@ import type { ImmutableSet } from './ImmutableSet'; import type { FlatTreeItem, FlatTreeItemProps, MutableFlatTreeItem } from '../hooks/useFlatTree'; +import * as React from 'react'; /** * @internal @@ -40,8 +41,9 @@ export function createFlatTreeItems( const isLeaf = nextItemProps?.parentValue !== treeItemProps.value; const currentLevel = (currentParent.level ?? 0) + 1; const currentChildrenSize = ++currentParent.childrenSize; + const ref = React.createRef(); - const flatTreeItem: FlatTreeItem = { + const flatTreeItem: MutableFlatTreeItem = { value: treeItemProps.value, getTreeItemProps: () => ({ ...treeItemProps, @@ -49,7 +51,10 @@ export function createFlatTreeItems( 'aria-posinset': currentChildrenSize, 'aria-setsize': currentParent.childrenSize, leaf: isLeaf, + // a reference to every parent element is necessary to ensure navigation + ref: flatTreeItem.childrenSize > 0 ? ref : undefined, }), + ref, level: currentLevel, parentValue, childrenSize: 0, @@ -72,6 +77,7 @@ export const flatTreeRootId = '__fuiFlatTreeRoot' as unknown; function createFlatTreeRootItem(): FlatTreeItem { return { + ref: { current: null }, value: flatTreeRootId as Value, getTreeItemProps: () => { if (process.env.NODE_ENV !== 'production') {