From 477c2d7166b65b6477eaeae1eef0a2622bafb423 Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Fri, 24 Mar 2023 12:00:03 +0000 Subject: [PATCH 1/3] chore(react-tree): adds e2e flat tree tests --- ...-0d72ec74-bc60-458b-a601-cf9a0def3918.json | 7 + .../src/components/Tree/FlatTree.cy.tsx | 250 ++++++++++++++++++ .../react-tree/src/hooks/useFlatTree.ts | 12 +- .../react-tree/src/utils/flattenTree.ts | 59 +++-- 4 files changed, 310 insertions(+), 18 deletions(-) create mode 100644 change/@fluentui-react-tree-0d72ec74-bc60-458b-a601-cf9a0def3918.json create mode 100644 packages/react-components/react-tree/src/components/Tree/FlatTree.cy.tsx diff --git a/change/@fluentui-react-tree-0d72ec74-bc60-458b-a601-cf9a0def3918.json b/change/@fluentui-react-tree-0d72ec74-bc60-458b-a601-cf9a0def3918.json new file mode 100644 index 0000000000000..45922c0a89545 --- /dev/null +++ b/change/@fluentui-react-tree-0d72ec74-bc60-458b-a601-cf9a0def3918.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "chore: adds e2e flat tree tests", + "packageName": "@fluentui/react-tree", + "email": "bernardo.sunderhus@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-tree/src/components/Tree/FlatTree.cy.tsx b/packages/react-components/react-tree/src/components/Tree/FlatTree.cy.tsx new file mode 100644 index 0000000000000..5a10cd6e7d989 --- /dev/null +++ b/packages/react-components/react-tree/src/components/Tree/FlatTree.cy.tsx @@ -0,0 +1,250 @@ +import * as React from 'react'; +import { mount as mountBase } from '@cypress/react'; +import { FluentProvider } from '@fluentui/react-provider'; +import { teamsLightTheme } from '@fluentui/react-theme'; +import { + Tree, + TreeItem, + TreeItemLayout, + TreeProps, + treeItemLayoutClassNames, + treeItemClassNames, + flattenTree_unstable, + useFlatTree_unstable, +} from '@fluentui/react-tree'; +import { Button } from '@fluentui/react-button'; + +const mount = (element: JSX.Element) => { + mountBase({element}); +}; + +const items = flattenTree_unstable([ + { + id: 'item1', + children: level 1, item 1, + subtree: [ + { id: 'item1__item1', children: level 2, item 1 }, + { id: 'item1__item2', children: level 2, item 2 }, + { id: 'item1__item3', children: level 2, item 3 }, + ], + }, + { + id: 'item2', + children: level 1, item 2, + subtree: [ + { + id: 'item2__item1', + children: level 2, item 1, + subtree: [{ id: 'item2__item1__item1', children: level 3, item 1 }], + }, + ], + }, +]); + +const Example = (props: TreeProps) => { + const flatTree = useFlatTree_unstable(items, props); + return ( + + {Array.from(flatTree.items(), item => ( + + ))} + + ); +}; + +describe('FlatTree', () => { + 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'); + }); + + 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'); + }); + + 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'); + }); + it('should expand/collapse item on expandIcon click only', () => { + mount( + { + if (data.type === 'Click') { + event.preventDefault(); + } + }} + />, + ); + 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(`#item1 .${treeItemLayoutClassNames.root}`).realClick(); + cy.get('#item1__item1').should('not.exist'); + }); + it('should not expand/collapse item on actions click', () => { + mount( + + + + + } + id="item1" + > + level 1, item 1 + + + level 2, item 1 + + + + , + ); + cy.get('#item1__item1').should('not.exist'); + cy.get('#item1').focus(); + cy.get(`#action`).realClick(); + cy.get('#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'); + }); + 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'); + }); + 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'); + }); + it('should focus on actions when pressing tab key', () => { + mount( + + + + + } + id="item1" + > + level 1, item 1 + + + level 2, item 1 + + + + , + ); + cy.focused().should('not.exist'); + cy.document().realPress('Tab'); + cy.get('#item1').should('be.focused'); + cy.document().realPress('Tab'); + cy.get('#action').should('be.focused'); + }); + it('should not expand/collapse item on actions Enter/Space key', () => { + mount( + + + + + } + id="item1" + > + level 1, item 1 + + + level 2, item 1 + + + + , + ); + cy.focused().should('not.exist'); + cy.document().realPress('Tab'); + cy.get('#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('#action').should('be.focused').realPress('Space'); + cy.get('#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'); + }); + 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.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.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'); + }); + it('should move to last item with End key', () => { + mount(); + cy.get('#item1').focus().realPress('{end}'); + cy.get('#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'); + }); + }); + }); +}); diff --git a/packages/react-components/react-tree/src/hooks/useFlatTree.ts b/packages/react-components/react-tree/src/hooks/useFlatTree.ts index 3d3ee2b425881..0a84a703c3d70 100644 --- a/packages/react-components/react-tree/src/hooks/useFlatTree.ts +++ b/packages/react-components/react-tree/src/hooks/useFlatTree.ts @@ -108,21 +108,27 @@ export type FlatTree = { */ export function useFlatTree_unstable( flatTreeItemProps: FlatTreeItemProps[], - options: Pick = {}, + options: Pick = {}, ): FlatTree { const [openItems, updateOpenItems] = useOpenItemsState(options); const flatTreeItems = React.useMemo(() => createFlatTreeItems(flatTreeItemProps), [flatTreeItemProps]); const [navigate, navigationRef] = useFlatTreeNavigation(flatTreeItems); const handleOpenChange = useEventCallback((event: TreeOpenChangeEvent, data: TreeOpenChangeData) => { + options.onOpenChange?.(event, data); + if (!event.isDefaultPrevented()) { + updateOpenItems(data); + } event.preventDefault(); - updateOpenItems(data); }); const handleNavigation = useEventCallback( (event: TreeNavigationEvent_unstable, data: TreeNavigationData_unstable) => { + options.onNavigation_unstable?.(event, data); + if (!event.isDefaultPrevented()) { + navigate(data); + } event.preventDefault(); - navigate(data); }, ); diff --git a/packages/react-components/react-tree/src/utils/flattenTree.ts b/packages/react-components/react-tree/src/utils/flattenTree.ts index c611b7f673498..61fe1c1269dfd 100644 --- a/packages/react-components/react-tree/src/utils/flattenTree.ts +++ b/packages/react-components/react-tree/src/utils/flattenTree.ts @@ -6,16 +6,8 @@ export type NestedTreeItem = Omit & { }; let count = 1; - -// eslint-disable-next-line @typescript-eslint/naming-convention -function flattenTreeRecursive_unstable( - items: NestedTreeItem[], - parent?: FlatTreeItemProps, - level = 1, -): FlatTreeItemProps[] { - const flatTreeItems: FlatTreeItemProps[] = []; - for (let index = 0; index < items.length; index++) { - const { subtree, ...item } = items[index]; +function flattenTreeRecursive(items: NestedTreeItem[], parent?: FlatTreeItemProps, level = 1): FlatTreeItemProps[] { + return items.reduce((acc, { subtree, ...item }, index) => { const flatTreeItem: FlatTreeItemProps = { 'aria-level': level, 'aria-posinset': index + 1, @@ -25,16 +17,53 @@ function flattenTreeRecursive_unstable( leaf: subtree === undefined, ...item, }; - flatTreeItems.push(flatTreeItem); + acc.push(flatTreeItem); if (subtree !== undefined) { - flatTreeItems.push(...flattenTreeRecursive_unstable(subtree, flatTreeItem, level + 1)); + acc.push(...flattenTreeRecursive(subtree, flatTreeItem, level + 1)); } - } - return flatTreeItems; + return acc; + }, []); } /** * Converts a nested structure to a flat one which can be consumed by `useFlatTreeItems` + * @example + * ```tsx + * const defaultItems = flattenTree_unstable([ + * { + * children: level 1, item 1, + * subtree: [ + * { + * children: level 2, item 1, + * }, + * { + * children: level 2, item 2, + * }, + * { + * children: level 2, item 3, + * }, + * ], + * }, + * { + * children: level 1, item 2, + * subtree: [ + * { + * children: level 2, item 1, + * subtree: [ + * { + * children: level 3, item 1, + * subtree: [ + * { + * children: level 4, item 1, + * }, + * ], + * }, + * ], + * }, + * ], + * }, + * ]); + * ``` */ // eslint-disable-next-line @typescript-eslint/naming-convention -export const flattenTree_unstable: (items: NestedTreeItem[]) => FlatTreeItemProps[] = flattenTreeRecursive_unstable; +export const flattenTree_unstable = (items: NestedTreeItem[]): FlatTreeItemProps[] => flattenTreeRecursive(items); From 5d450d88f0cf58e6f678059a8238f02e92102c99 Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Fri, 24 Mar 2023 12:05:02 +0000 Subject: [PATCH 2/3] chore: updates API --- packages/react-components/react-tree/etc/react-tree.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/react-tree/etc/react-tree.api.md b/packages/react-components/react-tree/etc/react-tree.api.md index d7ed16a865d16..5aa543e4730ae 100644 --- a/packages/react-components/react-tree/etc/react-tree.api.md +++ b/packages/react-components/react-tree/etc/react-tree.api.md @@ -258,7 +258,7 @@ export type TreeSlots = { export type TreeState = ComponentState & TreeContextValue; // @public -export function useFlatTree_unstable(flatTreeItemProps: FlatTreeItemProps[], options?: Pick): FlatTree; +export function useFlatTree_unstable(flatTreeItemProps: FlatTreeItemProps[], options?: Pick): FlatTree; // @public export const useTree_unstable: (props: TreeProps, ref: React_2.Ref) => TreeState; From f3c520f0ce019ca22ea99605e2fb09b4fcd8c35f Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Fri, 24 Mar 2023 13:46:56 +0000 Subject: [PATCH 3/3] chore: improves tests --- .../src/components/Tree/FlatTree.cy.tsx | 250 ---------- .../src/components/Tree/Tree.cy.tsx | 448 ++++++++++-------- .../react-tree/src/utils/flattenTree.ts | 35 ++ 3 files changed, 288 insertions(+), 445 deletions(-) delete mode 100644 packages/react-components/react-tree/src/components/Tree/FlatTree.cy.tsx diff --git a/packages/react-components/react-tree/src/components/Tree/FlatTree.cy.tsx b/packages/react-components/react-tree/src/components/Tree/FlatTree.cy.tsx deleted file mode 100644 index 5a10cd6e7d989..0000000000000 --- a/packages/react-components/react-tree/src/components/Tree/FlatTree.cy.tsx +++ /dev/null @@ -1,250 +0,0 @@ -import * as React from 'react'; -import { mount as mountBase } from '@cypress/react'; -import { FluentProvider } from '@fluentui/react-provider'; -import { teamsLightTheme } from '@fluentui/react-theme'; -import { - Tree, - TreeItem, - TreeItemLayout, - TreeProps, - treeItemLayoutClassNames, - treeItemClassNames, - flattenTree_unstable, - useFlatTree_unstable, -} from '@fluentui/react-tree'; -import { Button } from '@fluentui/react-button'; - -const mount = (element: JSX.Element) => { - mountBase({element}); -}; - -const items = flattenTree_unstable([ - { - id: 'item1', - children: level 1, item 1, - subtree: [ - { id: 'item1__item1', children: level 2, item 1 }, - { id: 'item1__item2', children: level 2, item 2 }, - { id: 'item1__item3', children: level 2, item 3 }, - ], - }, - { - id: 'item2', - children: level 1, item 2, - subtree: [ - { - id: 'item2__item1', - children: level 2, item 1, - subtree: [{ id: 'item2__item1__item1', children: level 3, item 1 }], - }, - ], - }, -]); - -const Example = (props: TreeProps) => { - const flatTree = useFlatTree_unstable(items, props); - return ( - - {Array.from(flatTree.items(), item => ( - - ))} - - ); -}; - -describe('FlatTree', () => { - 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'); - }); - - 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'); - }); - - 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'); - }); - it('should expand/collapse item on expandIcon click only', () => { - mount( - { - if (data.type === 'Click') { - event.preventDefault(); - } - }} - />, - ); - 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(`#item1 .${treeItemLayoutClassNames.root}`).realClick(); - cy.get('#item1__item1').should('not.exist'); - }); - it('should not expand/collapse item on actions click', () => { - mount( - - - - - } - id="item1" - > - level 1, item 1 - - - level 2, item 1 - - - - , - ); - cy.get('#item1__item1').should('not.exist'); - cy.get('#item1').focus(); - cy.get(`#action`).realClick(); - cy.get('#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'); - }); - 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'); - }); - 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'); - }); - it('should focus on actions when pressing tab key', () => { - mount( - - - - - } - id="item1" - > - level 1, item 1 - - - level 2, item 1 - - - - , - ); - cy.focused().should('not.exist'); - cy.document().realPress('Tab'); - cy.get('#item1').should('be.focused'); - cy.document().realPress('Tab'); - cy.get('#action').should('be.focused'); - }); - it('should not expand/collapse item on actions Enter/Space key', () => { - mount( - - - - - } - id="item1" - > - level 1, item 1 - - - level 2, item 1 - - - - , - ); - cy.focused().should('not.exist'); - cy.document().realPress('Tab'); - cy.get('#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('#action').should('be.focused').realPress('Space'); - cy.get('#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'); - }); - 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.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.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'); - }); - it('should move to last item with End key', () => { - mount(); - cy.get('#item1').focus().realPress('{end}'); - cy.get('#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'); - }); - }); - }); -}); 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 c99ffea48c0b0..17f52be40fd26 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 @@ -9,240 +9,298 @@ import { TreeProps, treeItemLayoutClassNames, treeItemClassNames, + useFlatTree_unstable, } from '@fluentui/react-tree'; import { Button } from '@fluentui/react-button'; +import { flattenTreeFromElement } from '../../utils/flattenTree'; const mount = (element: JSX.Element) => { mountBase({element}); }; -const Example = (props: TreeProps) => { +const NestedTree: React.FC = props => { return ( - - level 1, item 1 - - - level 2, item 1 - - - level 2, item 2 - - - level 2, item 3 + {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 1, item 2 - - level 3, item 1 + + level 2, item 1 + + + level 3, item 1 + + - - + + )} ); }; +NestedTree.displayName = 'NestedTree'; -describe('Tree', () => { - 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'); - }); - - 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'); - }); - - 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'); - }); - it('should expand/collapse item on expandIcon click only', () => { - mount( - { - if (data.type === 'Click') { - event.preventDefault(); - } - }} - />, - ); - 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(`#item1 .${treeItemLayoutClassNames.root}`).realClick(); - cy.get('#item1__item1').should('not.exist'); - }); - it('should not expand/collapse item on actions click', () => { - mount( - - - - - } - id="item1" - > +const FlatTree: React.FC = (props: TreeProps) => { + const flatTree = useFlatTree_unstable( + flattenTreeFromElement( + props.children ? ( + <>{props.children} + ) : ( + <> + level 1, item 1 level 2, item 1 - - - , - ); - cy.get('#item1__item1').should('not.exist'); - cy.get('#item1').focus(); - cy.get(`#action`).realClick(); - cy.get('#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'); - }); - 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'); - }); - 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'); - }); - it('should focus on actions when pressing tab key', () => { - mount( - - - - - } - id="item1" - > - level 1, item 1 - - - level 2, item 1 + + level 2, item 2 + + + level 2, item 3 - , - ); - cy.focused().should('not.exist'); - cy.document().realPress('Tab'); - cy.get('#item1').should('be.focused'); - cy.document().realPress('Tab'); - cy.get('#action').should('be.focused'); - }); - it('should not expand/collapse item on actions Enter/Space key', () => { - mount( - - - - - } - id="item1" - > - level 1, item 1 + + level 1, item 2 - + level 2, item 1 + + + level 3, item 1 + + - , - ); - cy.focused().should('not.exist'); - cy.document().realPress('Tab'); - cy.get('#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('#action').should('be.focused').realPress('Space'); + + ), + ), + props, + ); + return ( + + {Array.from(flatTree.items(), item => ( + + ))} + + ); +}; +FlatTree.displayName = 'FlatTree'; + +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'); }); - 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'); + + 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'); }); - 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.focused().realPress('Tab'); - cy.focused().should('not.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'); + }); + it('should expand/collapse item on expandIcon click only', () => { + mount( + { + if (data.type === 'Click') { + event.preventDefault(); + } + }} + />, + ); + 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(`#item1 .${treeItemLayoutClassNames.root}`).realClick(); + cy.get('#item1__item1').should('not.exist'); + }); + it('should not expand/collapse item on actions click', () => { + mount( + + + + + } + id="item1" + > + level 1, item 1 + + + level 2, item 1 + + + + , + ); + cy.get('#item1__item1').should('not.exist'); + cy.get('#item1').focus(); + cy.get(`#action`).realClick(); + cy.get('#item1__item1').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.focused().realPress('Tab').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'); + }); + 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'); + }); + 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'); + }); + it('should focus on actions when pressing tab key', () => { + mount( + + + + + } + id="item1" + > + level 1, item 1 + + + level 2, item 1 + + + + , + ); + cy.focused().should('not.exist'); + cy.document().realPress('Tab'); + cy.get('#item1').should('be.focused'); + cy.document().realPress('Tab'); + cy.get('#action').should('be.focused'); }); - 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'); + it('should not expand/collapse item on actions Enter/Space key', () => { + mount( + + + + + } + id="item1" + > + level 1, item 1 + + + level 2, item 1 + + + + , + ); + cy.focused().should('not.exist'); + cy.document().realPress('Tab'); + cy.get('#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('#action').should('be.focused').realPress('Space'); + cy.get('#item1__item1').should('not.exist'); }); - it('should move to last item with End key', () => { - mount(); - cy.get('#item1').focus().realPress('{end}'); - cy.get('#item2__item1__item1').should('be.focused'); + 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'); }); - 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}'); + 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.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.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'); + }); + it('should move to last item with End key', () => { + mount(); + cy.get('#item1').focus().realPress('{end}'); + cy.get('#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'); + }); }); }); }); -}); +} diff --git a/packages/react-components/react-tree/src/utils/flattenTree.ts b/packages/react-components/react-tree/src/utils/flattenTree.ts index 61fe1c1269dfd..3a609b6692f2c 100644 --- a/packages/react-components/react-tree/src/utils/flattenTree.ts +++ b/packages/react-components/react-tree/src/utils/flattenTree.ts @@ -1,3 +1,4 @@ +import * as React from 'react'; import { FlatTreeItemProps } from '../hooks/useFlatTree'; import { TreeItemProps } from '../TreeItem'; @@ -67,3 +68,37 @@ function flattenTreeRecursive(items: NestedTreeItem[], parent?: FlatTreeItemProp */ // eslint-disable-next-line @typescript-eslint/naming-convention export const flattenTree_unstable = (items: NestedTreeItem[]): FlatTreeItemProps[] => flattenTreeRecursive(items); + +/** + * @internal + */ +export const flattenTreeFromElement = ( + root: React.ReactElement<{ + children?: React.ReactElement | React.ReactElement[]; + }>, + parent?: FlatTreeItemProps, + level = 1, +): FlatTreeItemProps[] => { + const children = React.Children.toArray(root.props.children) as React.ReactElement[]; + return children.reduce((acc, curr, index) => { + const [content, subtree] = React.Children.toArray(curr.props.children) as [ + React.ReactNode, + typeof root | undefined, + ]; + const flatTreeItem: FlatTreeItemProps = { + 'aria-level': level, + 'aria-posinset': index + 1, + 'aria-setsize': children.length, + parentId: parent?.id, + id: curr.props.id ?? `fui-FlatTreeItem-${count++}`, + leaf: subtree === undefined, + ...curr.props, + children: content, + }; + acc.push(flatTreeItem); + if (subtree !== undefined) { + acc.push(...flattenTreeFromElement(subtree, flatTreeItem, level + 1)); + } + return acc; + }, []); +};