From ae9db9a8df63b19e81d67ea74a63f9400feac6ff Mon Sep 17 00:00:00 2001 From: silviuavram Date: Tue, 4 Jun 2019 17:20:07 +0200 Subject: [PATCH 01/14] removed circular navigation property --- .../react/src/lib/accessibility/Behaviors/Tree/treeBehavior.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeBehavior.ts index 9de73a9920..c837b42103 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeBehavior.ts @@ -5,7 +5,6 @@ import { FocusZoneDirection } from '../../FocusZone' * @specification * Embeds component into FocusZone. * Provides arrow key navigation in vertical direction. - * Keyboard navigation is circular. */ const treeBehavior: Accessibility = (props: any) => ({ attributes: { @@ -14,7 +13,7 @@ const treeBehavior: Accessibility = (props: any) => ({ focusZone: { mode: FocusZoneMode.Embed, props: { - isCircularNavigation: true, + isCircularNavigation: false, direction: FocusZoneDirection.vertical, }, }, From 3f0684a46db3cfc20e378b0e73457dcf41afc8a6 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Tue, 4 Jun 2019 17:23:25 +0200 Subject: [PATCH 02/14] implemented left-right kb navigation --- packages/react/src/components/Tree/Tree.tsx | 27 ++++++++++-- .../react/src/components/Tree/TreeItem.tsx | 35 ++++++++++++++-- .../react/src/components/Tree/TreeTitle.tsx | 41 +++++++++++++++++-- .../Behaviors/Tree/treeItemBehavior.ts | 22 ++++++++++ .../Behaviors/Tree/treeTitleBehavior.ts | 6 +++ packages/react/src/lib/accessibility/index.ts | 1 + 6 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 9594452e31..dfa1adf3e8 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -16,6 +16,7 @@ import { import { ShorthandValue, ShorthandRenderFunction, WithAsProp, withSafeTypeForAs } from '../../types' import { Accessibility } from '../../lib/accessibility/types' import { treeBehavior } from '../../lib/accessibility' +import { Ref, handleRef } from '@stardust-ui/react-component-ref' export interface TreeSlotClassNames { item: string @@ -40,6 +41,9 @@ export interface TreeProps extends UIComponentProps, ChildrenComponentProps { /** Shorthand array of props for Tree. */ items: ShorthandValue[] + /** Ref for this tree. */ + contentRef?: React.Ref + /** * A custom render function for the title slot. * @@ -79,6 +83,7 @@ class Tree extends AutoControlledComponent, TreeState> { ]), exclusive: PropTypes.bool, items: customPropTypes.collectionShorthand, + contentRef: customPropTypes.ref, renderItemTitle: PropTypes.func, rtlAttributes: PropTypes.func, } @@ -102,10 +107,13 @@ class Tree extends AutoControlledComponent, TreeState> { } computeNewIndex = (index: number) => { + const activeIndexes = this.getActiveIndexes() + if (!this.props.items || this.props.items.length === 0) { + return activeIndexes + } const { exclusive } = this.props if (exclusive) return index - const activeIndexes = this.getActiveIndexes() // check to see if index is in array, and remove it, if not then add it return _.includes(activeIndexes, index) ? _.without(activeIndexes, index) @@ -139,9 +147,8 @@ class Tree extends AutoControlledComponent, TreeState> { } renderComponent({ ElementType, classes, accessibility, unhandledProps, styles, variables }) { - const { children } = this.props - - return ( + const { children, contentRef } = this.props + const component = ( , TreeState> { {childrenExist(children) ? children : this.renderContent()} ) + + return contentRef ? ( + { + handleRef(contentRef, treeElement) + }} + > + {component} + + ) : ( + component + ) } } diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx index 4309264b8b..22423caa23 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -5,8 +5,8 @@ import * as React from 'react' import Tree from './Tree' import TreeTitle, { TreeTitleProps } from './TreeTitle' -import { defaultBehavior } from '../../lib/accessibility' -import { Accessibility } from '../../lib/accessibility/types' +import { defaultBehavior, treeItemBehavior } from '../../lib/accessibility' +import { Accessibility, AccessibilityActionHandlers } from '../../lib/accessibility/types' import { UIComponent, childrenExist, @@ -15,6 +15,7 @@ import { UIComponentProps, ChildrenComponentProps, rtlTextContainer, + applyAccessibilityKeyHandlers, } from '../../lib' import { ComponentEventHandler, @@ -23,6 +24,7 @@ import { ShorthandValue, withSafeTypeForAs, } from '../../types' +import { getFirstFocusable } from '../../lib/accessibility/FocusZone/focusUtilities' export interface TreeItemSlotClassNames { title: string @@ -99,10 +101,32 @@ class TreeItem extends UIComponent> { public static defaultProps = { as: 'li', - accessibility: defaultBehavior, + accessibility: treeItemBehavior, } - handleTitleOverrides = (predefinedProps: TreeTitleProps) => ({ + private titleRef = React.createRef() + private treeRef = React.createRef() + + protected actionHandlers: AccessibilityActionHandlers = { + getFocusFromParent: e => { + const { open } = this.props + if (open) { + e.stopPropagation() + this.titleRef.current.focus() + } + }, + setFocusToFirstChild: e => { + if (open) { + e.stopPropagation() + const element = getFirstFocusable(this.treeRef.current, this.treeRef.current, true) + if (element) { + element.focus() + } + } + }, + } + + private handleTitleOverrides = (predefinedProps: TreeTitleProps) => ({ onClick: (e, titleProps) => { _.invoke(this.props, 'onTitleClick', e, this.props) _.invoke(predefinedProps, 'onClick', e, titleProps) @@ -118,6 +142,7 @@ class TreeItem extends UIComponent> { {TreeTitle.create(title, { defaultProps: { className: TreeItem.slotClassNames.title, + contentRef: this.titleRef, open, hasSubtree, }, @@ -130,6 +155,7 @@ class TreeItem extends UIComponent> { accessibility: defaultBehavior, className: TreeItem.slotClassNames.subtree, exclusive, + contentRef: this.treeRef, renderItemTitle, }, })} @@ -146,6 +172,7 @@ class TreeItem extends UIComponent> { {...accessibility.attributes.root} {...rtlTextContainer.getAttributes({ forElements: [children] })} {...unhandledProps} + {...applyAccessibilityKeyHandlers(accessibility.keyHandlers.root, unhandledProps)} > {childrenExist(children) ? children : this.renderContent()} diff --git a/packages/react/src/components/Tree/TreeTitle.tsx b/packages/react/src/components/Tree/TreeTitle.tsx index 397641cddd..dca8965726 100644 --- a/packages/react/src/components/Tree/TreeTitle.tsx +++ b/packages/react/src/components/Tree/TreeTitle.tsx @@ -1,6 +1,8 @@ import * as _ from 'lodash' import * as PropTypes from 'prop-types' import * as React from 'react' +import * as customPropTypes from '@stardust-ui/react-proptypes' +import { Ref, handleRef } from '@stardust-ui/react-component-ref' import { UIComponent, @@ -27,6 +29,9 @@ export interface TreeTitleProps */ accessibility?: Accessibility + /** Ref to the clickable element that contains the title. */ + contentRef?: React.Ref + /** * Called on click. * @@ -51,6 +56,7 @@ class TreeTitle extends UIComponent> { static propTypes = { ...commonPropTypes.createCommon(), + contentRef: customPropTypes.ref, onClick: PropTypes.func, open: PropTypes.bool, hasSubtree: PropTypes.bool, @@ -66,16 +72,31 @@ class TreeTitle extends UIComponent> { e.preventDefault() this.handleClick(e) }, + expandOrFocusFirstChild: e => { + const { open } = this.props + e.preventDefault() + if (!open) { + e.stopPropagation() + this.handleClick(e) + } + }, + collapseOrFocusParent: e => { + const { open } = this.props + e.preventDefault() + if (open) { + e.stopPropagation() + this.handleClick(e) + } + }, } - handleClick = e => { + private handleClick = e => { _.invoke(this.props, 'onClick', e, this.props) } renderComponent({ ElementType, classes, accessibility, unhandledProps, styles, variables }) { - const { children, content } = this.props - - return ( + const { children, content, contentRef } = this.props + const element = ( > { {childrenExist(children) ? children : content} ) + + return contentRef ? ( + { + handleRef(contentRef, titleElement) + }} + > + {element} + + ) : ( + element + ) } } diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts new file mode 100644 index 0000000000..a3ce3b9521 --- /dev/null +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts @@ -0,0 +1,22 @@ +import { Accessibility } from '../../types' +import * as keyboardKey from 'keyboard-key' + +/** + */ +const treeItemBehavior: Accessibility = (props: any) => ({ + attributes: { + root: {}, + }, + keyActions: { + root: { + getFocusFromParent: { + keyCombinations: [{ keyCode: keyboardKey.ArrowLeft }], + }, + setFocusToFirstChild: { + keyCombinations: [{ keyCode: keyboardKey.ArrowRight }], + }, + }, + }, +}) + +export default treeItemBehavior diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts index 1c6c18be70..7ca9510820 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts @@ -21,6 +21,12 @@ const treeTitleBehavior: Accessibility = (props: any) => ({ performClick: { keyCombinations: [{ keyCode: keyboardKey.Enter }, { keyCode: keyboardKey.Spacebar }], }, + expandOrFocusFirstChild: { + keyCombinations: [{ keyCode: keyboardKey.ArrowRight }], + }, + collapseOrFocusParent: { + keyCombinations: [{ keyCode: keyboardKey.ArrowLeft }], + }, }, }, }) diff --git a/packages/react/src/lib/accessibility/index.ts b/packages/react/src/lib/accessibility/index.ts index 19ac60ba7b..6bebe00294 100644 --- a/packages/react/src/lib/accessibility/index.ts +++ b/packages/react/src/lib/accessibility/index.ts @@ -31,6 +31,7 @@ export { default as chatBehavior } from './Behaviors/Chat/chatBehavior' export { default as chatMessageBehavior } from './Behaviors/Chat/chatMessageBehavior' export { default as gridBehavior } from './Behaviors/Grid/gridBehavior' export { default as treeBehavior } from './Behaviors/Tree/treeBehavior' +export { default as treeItemBehavior } from './Behaviors/Tree/treeItemBehavior' export { default as treeTitleBehavior } from './Behaviors/Tree/treeTitleBehavior' export { default as dialogBehavior } from './Behaviors/Dialog/dialogBehavior' export { default as statusBehavior } from './Behaviors/Status/statusBehavior' From d0f5cba773b3b863b23c0cf0584ad50410ff999e Mon Sep 17 00:00:00 2001 From: silviuavram Date: Wed, 5 Jun 2019 11:01:28 +0200 Subject: [PATCH 03/14] added tests to behaviors --- .../src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts | 3 +++ .../src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts | 2 ++ packages/react/test/specs/behaviors/behavior-test.tsx | 2 ++ 3 files changed, 7 insertions(+) diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts index a3ce3b9521..83f521ecfd 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts @@ -2,6 +2,9 @@ import { Accessibility } from '../../types' import * as keyboardKey from 'keyboard-key' /** + * @specification + * Triggers 'getFocusFromParent' action with 'ArrowLeft' on 'root'. + * Triggers 'setFocusToFirstChild' action with 'ArrowRight' on 'root'. */ const treeItemBehavior: Accessibility = (props: any) => ({ attributes: { diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts index 7ca9510820..15c47b15e6 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts @@ -6,6 +6,8 @@ import { IS_FOCUSABLE_ATTRIBUTE } from '../../FocusZone/focusUtilities' * @specification * Adds attribute 'aria-expanded=true' based on the property 'open' if the component has 'hasSubtree' property. * Triggers 'performClick' action with 'Enter' or 'Spacebar' on 'root'. + * Triggers 'expandOrFocusFirstChild' action with 'ArrowRight' on 'root'. + * Triggers 'collapseOrFocusParent' action with 'ArrowLeft' on 'root'. */ const treeTitleBehavior: Accessibility = (props: any) => ({ attributes: { diff --git a/packages/react/test/specs/behaviors/behavior-test.tsx b/packages/react/test/specs/behaviors/behavior-test.tsx index 13e2271a6e..8998245e1f 100644 --- a/packages/react/test/specs/behaviors/behavior-test.tsx +++ b/packages/react/test/specs/behaviors/behavior-test.tsx @@ -33,6 +33,7 @@ import { toolbarButtonBehavior, treeBehavior, treeTitleBehavior, + treeItemBehavior, gridBehavior, statusBehavior, alertWarningBehavior, @@ -77,6 +78,7 @@ testHelper.addBehavior('toggleButtonBehavior', toggleButtonBehavior) testHelper.addBehavior('toolbarButtonBehavior', toolbarButtonBehavior) testHelper.addBehavior('treeTitleBehavior', treeTitleBehavior) testHelper.addBehavior('treeBehavior', treeBehavior) +testHelper.addBehavior('treeItemBehavior', treeItemBehavior) testHelper.addBehavior('gridBehavior', gridBehavior) testHelper.addBehavior('dialogBehavior', dialogBehavior) testHelper.addBehavior('statusBehavior', statusBehavior) From d1df8c2c728849cfd5aa033207dbf94ad1f2db47 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Wed, 5 Jun 2019 15:00:57 +0200 Subject: [PATCH 04/14] fixed a bug caused by merge --- packages/react/src/components/Tree/TreeItem.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx index 22423caa23..8c75ee5e76 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -6,7 +6,7 @@ import * as React from 'react' import Tree from './Tree' import TreeTitle, { TreeTitleProps } from './TreeTitle' import { defaultBehavior, treeItemBehavior } from '../../lib/accessibility' -import { Accessibility, AccessibilityActionHandlers } from '../../lib/accessibility/types' +import { Accessibility } from '../../lib/accessibility/types' import { UIComponent, childrenExist, @@ -107,7 +107,7 @@ class TreeItem extends UIComponent> { private titleRef = React.createRef() private treeRef = React.createRef() - protected actionHandlers: AccessibilityActionHandlers = { + protected actionHandlers = { getFocusFromParent: e => { const { open } = this.props if (open) { From a4ce9d617940ba88319aaac920624196f480cfb1 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Thu, 6 Jun 2019 15:29:23 +0200 Subject: [PATCH 05/14] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33e13707a1..077d67eecf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Features - Add `Toolbar` component @miroslavstastny ([#1408](https://github.com/stardust-ui/react/pull/1408)) - Add `disableAnimations` boolean prop on the `Provider` @mnajdova ([#1377](https://github.com/stardust-ui/react/pull/1377)) +- Add expand/collapse and navigation with `ArrowUp` and `ArrowDown` to `Tree` @silviuavram ([#1457](https://github.com/stardust-ui/react/pull/1457)) ### Fixes - Fix click handling on focus for `action` slot in `Attachment` component @Bugaa92 ([#1444](https://github.com/stardust-ui/react/pull/1444)) From db99a936781c9b3a12225b7baaed8705b4fde143 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Thu, 6 Jun 2019 15:41:02 +0200 Subject: [PATCH 06/14] used refs directly in TreeItem --- packages/react/src/components/Tree/Tree.tsx | 31 ++++--------- .../react/src/components/Tree/TreeItem.tsx | 46 ++++++++++--------- .../react/src/components/Tree/TreeTitle.tsx | 25 ++-------- 3 files changed, 37 insertions(+), 65 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 13f58fd896..eb63f3960a 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -16,7 +16,6 @@ import { import { ShorthandValue, ShorthandRenderFunction, WithAsProp, withSafeTypeForAs } from '../../types' import { Accessibility } from '../../lib/accessibility/types' import { treeBehavior } from '../../lib/accessibility' -import { Ref, handleRef } from '@stardust-ui/react-component-ref' export interface TreeSlotClassNames { item: string @@ -41,9 +40,6 @@ export interface TreeProps extends UIComponentProps, ChildrenComponentProps { /** Shorthand array of props for Tree. */ items: ShorthandValue[] - /** Ref for this tree. */ - contentRef?: React.Ref - /** * A custom render function for the title slot. * @@ -83,7 +79,6 @@ class Tree extends AutoControlledComponent, TreeState> { ]), exclusive: PropTypes.bool, items: customPropTypes.collectionShorthand, - contentRef: customPropTypes.ref, renderItemTitle: PropTypes.func, rtlAttributes: PropTypes.func, } @@ -107,13 +102,14 @@ class Tree extends AutoControlledComponent, TreeState> { } computeNewIndex = (index: number) => { - const activeIndexes = this.getActiveIndexes() - if (!this.props.items || this.props.items.length === 0) { - return activeIndexes + if (this.props.items.length === 0) { + return [] } - const { exclusive } = this.props + const { exclusive } = this.props if (exclusive) return index + + const activeIndexes = this.getActiveIndexes() // check to see if index is in array, and remove it, if not then add it return _.includes(activeIndexes, index) ? _.without(activeIndexes, index) @@ -147,8 +143,9 @@ class Tree extends AutoControlledComponent, TreeState> { } renderComponent({ ElementType, classes, accessibility, unhandledProps, styles, variables }) { - const { children, contentRef } = this.props - const component = ( + const { children } = this.props + + return ( , TreeState> { {childrenExist(children) ? children : this.renderContent()} ) - - return contentRef ? ( - { - handleRef(contentRef, treeElement) - }} - > - {component} - - ) : ( - component - ) } } diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx index 2a76bca619..11c019eda0 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -2,6 +2,7 @@ import * as customPropTypes from '@stardust-ui/react-proptypes' import * as _ from 'lodash' import * as PropTypes from 'prop-types' import * as React from 'react' +import { Ref } from '@stardust-ui/react-component-ref' import Tree from './Tree' import TreeTitle, { TreeTitleProps } from './TreeTitle' @@ -104,10 +105,10 @@ class TreeItem extends UIComponent> { accessibility: treeItemBehavior, } - private titleRef = React.createRef() - private treeRef = React.createRef() + titleRef = React.createRef() + treeRef = React.createRef() - protected actionHandlers = { + actionHandlers = { getFocusFromParent: e => { const { open } = this.props if (open) { @@ -126,7 +127,7 @@ class TreeItem extends UIComponent> { }, } - private handleTitleOverrides = (predefinedProps: TreeTitleProps) => ({ + handleTitleOverrides = (predefinedProps: TreeTitleProps) => ({ onClick: (e, titleProps) => { _.invoke(this.props, 'onTitleClick', e, this.props) _.invoke(predefinedProps, 'onClick', e, titleProps) @@ -139,26 +140,29 @@ class TreeItem extends UIComponent> { return ( <> - {TreeTitle.create(title, { - defaultProps: { - className: TreeItem.slotClassNames.title, - contentRef: this.titleRef, - open, - hasSubtree, - }, - render: renderItemTitle, - overrideProps: this.handleTitleOverrides, - })} - {open && - Tree.create(items, { + + {TreeTitle.create(title, { defaultProps: { - accessibility: defaultBehavior, - className: TreeItem.slotClassNames.subtree, - exclusive, - contentRef: this.treeRef, - renderItemTitle, + className: TreeItem.slotClassNames.title, + open, + hasSubtree, }, + render: renderItemTitle, + overrideProps: this.handleTitleOverrides, })} + + {open && ( + + {Tree.create(items, { + defaultProps: { + accessibility: defaultBehavior, + className: TreeItem.slotClassNames.subtree, + exclusive, + renderItemTitle, + }, + })} + + )} ) } diff --git a/packages/react/src/components/Tree/TreeTitle.tsx b/packages/react/src/components/Tree/TreeTitle.tsx index 98a6f6b3fc..1957df9323 100644 --- a/packages/react/src/components/Tree/TreeTitle.tsx +++ b/packages/react/src/components/Tree/TreeTitle.tsx @@ -1,8 +1,6 @@ import * as _ from 'lodash' import * as PropTypes from 'prop-types' import * as React from 'react' -import * as customPropTypes from '@stardust-ui/react-proptypes' -import { Ref, handleRef } from '@stardust-ui/react-component-ref' import { UIComponent, @@ -29,9 +27,6 @@ export interface TreeTitleProps */ accessibility?: Accessibility - /** Ref to the clickable element that contains the title. */ - contentRef?: React.Ref - /** * Called on click. * @@ -56,7 +51,6 @@ class TreeTitle extends UIComponent> { static propTypes = { ...commonPropTypes.createCommon(), - contentRef: customPropTypes.ref, onClick: PropTypes.func, open: PropTypes.bool, hasSubtree: PropTypes.bool, @@ -90,13 +84,14 @@ class TreeTitle extends UIComponent> { }, } - private handleClick = e => { + handleClick = e => { _.invoke(this.props, 'onClick', e, this.props) } renderComponent({ ElementType, classes, accessibility, unhandledProps, styles, variables }) { - const { children, content, contentRef } = this.props - const element = ( + const { children, content } = this.props + + return ( > { {childrenExist(children) ? children : content} ) - - return contentRef ? ( - { - handleRef(contentRef, titleElement) - }} - > - {element} - - ) : ( - element - ) } } From 4efebd7857f20141046bdf04411fe13599039472 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Thu, 6 Jun 2019 15:43:53 +0200 Subject: [PATCH 07/14] removed unneeded parameter --- .../react/src/lib/accessibility/Behaviors/Tree/treeBehavior.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeBehavior.ts index c837b42103..c6e5f5b942 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeBehavior.ts @@ -13,7 +13,6 @@ const treeBehavior: Accessibility = (props: any) => ({ focusZone: { mode: FocusZoneMode.Embed, props: { - isCircularNavigation: false, direction: FocusZoneDirection.vertical, }, }, From 7caeb9aeae657e55ca84ffdbd365a368d75c1e62 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Thu, 6 Jun 2019 16:52:43 +0200 Subject: [PATCH 08/14] refactored the open logic --- packages/react/src/components/Tree/Tree.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index eb63f3960a..edec7add98 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -102,14 +102,14 @@ class Tree extends AutoControlledComponent, TreeState> { } computeNewIndex = (index: number) => { - if (this.props.items.length === 0) { - return [] + const activeIndexes = this.getActiveIndexes() + if (!this.props.items[index]['items']) { + return activeIndexes } const { exclusive } = this.props if (exclusive) return index - const activeIndexes = this.getActiveIndexes() // check to see if index is in array, and remove it, if not then add it return _.includes(activeIndexes, index) ? _.without(activeIndexes, index) From 8fe9c72441975367d92f68723df388e81ae9528b Mon Sep 17 00:00:00 2001 From: silviuavram Date: Fri, 7 Jun 2019 12:08:02 +0200 Subject: [PATCH 09/14] added unit tests --- .../test/specs/components/Tree/Tree-test.tsx | 122 +++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/packages/react/test/specs/components/Tree/Tree-test.tsx b/packages/react/test/specs/components/Tree/Tree-test.tsx index 0999e2b17f..8ddf2ab826 100644 --- a/packages/react/test/specs/components/Tree/Tree-test.tsx +++ b/packages/react/test/specs/components/Tree/Tree-test.tsx @@ -1,7 +1,127 @@ -import { isConformant } from 'test/specs/commonTests' +import * as React from 'react' +import * as keyboardKey from 'keyboard-key' +import { isConformant } from 'test/specs/commonTests' +import { mountWithProvider } from 'test/utils' import Tree from 'src/components/Tree/Tree' +import TreeTitle from 'src/components/Tree/TreeTitle' + +const items = [ + { + key: '1', + title: '1', + items: [ + { + key: '11', + title: '11', + }, + { + key: '12', + title: '12', + items: [ + { + key: '121', + title: '121', + }, + ], + }, + ], + }, + { + key: '2', + title: '2', + items: [ + { + key: '21', + title: '21', + items: [ + { + key: '221', + title: '221', + }, + ], + }, + { + key: '22', + title: '22', + }, + ], + }, + { + key: '3', + title: '3', + }, +] describe('Tree', () => { isConformant(Tree) + + describe('activeIndex', () => { + it('should contain index of item open at click', () => { + const wrapper = mountWithProvider() + const tree = wrapper.find(Tree).at(0) + + wrapper + .find(`.${TreeTitle.className}`) + .at(0) // title 1 + .simulate('click') + expect(tree.state('activeIndex')).toEqual([0]) + + wrapper + .find(`.${TreeTitle.className}`) + .at(3) // title 2 + .simulate('click') + expect(tree.state('activeIndex')).toEqual([0, 1]) + }) + + it('should have index of item removed when closed at click', () => { + const wrapper = mountWithProvider() + const tree = wrapper.find(Tree).at(0) + + wrapper + .find(`.${TreeTitle.className}`) + .at(0) // title 1 + .simulate('click') + expect(tree.state('activeIndex')).toEqual([1]) + }) + + it('should contain only one index at a time if exclusive', () => { + const wrapper = mountWithProvider() + const tree = wrapper.find(Tree).at(0) + + wrapper + .find(`.${TreeTitle.className}`) + .at(0) // title 1 + .simulate('click') + expect(tree.state('activeIndex')).toEqual(0) + + wrapper + .find(`.${TreeTitle.className}`) + .at(3) // title 2 + .simulate('click') + expect(tree.state('activeIndex')).toEqual(1) + }) + + it('should contain index of item open by ArrowRight', () => { + const wrapper = mountWithProvider() + const tree = wrapper.find(Tree).at(0) + + wrapper + .find(`.${TreeTitle.className}`) + .at(0) // title 1 + .simulate('keydown', { keyCode: keyboardKey.ArrowRight }) + expect(tree.state('activeIndex')).toEqual([0]) + }) + + it('should have index of item removed if closed by ArrowLeft', () => { + const wrapper = mountWithProvider() + const tree = wrapper.find(Tree).at(0) + + wrapper + .find(`.${TreeTitle.className}`) + .at(0) // title 1 + .simulate('keydown', { keyCode: keyboardKey.ArrowLeft }) + expect(tree.state('activeIndex')).toEqual([1]) + }) + }) }) From 8bcfd472f4d119afb54e667090cb5adb2c38aa7f Mon Sep 17 00:00:00 2001 From: silviuavram Date: Fri, 7 Jun 2019 15:17:49 +0200 Subject: [PATCH 10/14] implemented code review --- packages/react/src/components/Tree/Tree.tsx | 9 +++--- .../react/src/components/Tree/TreeItem.tsx | 15 ++++++---- .../test/specs/components/Tree/Tree-test.tsx | 29 +++++++++++-------- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index edec7add98..35d05bdecb 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -101,13 +101,14 @@ class Tree extends AutoControlledComponent, TreeState> { return _.isArray(activeIndex) ? activeIndex : [activeIndex] } - computeNewIndex = (index: number) => { + computeNewIndex = (treeItemProps: TreeItemProps) => { + const { index, items } = treeItemProps const activeIndexes = this.getActiveIndexes() - if (!this.props.items[index]['items']) { + const { exclusive } = this.props + if (!items) { return activeIndexes } - const { exclusive } = this.props if (exclusive) return index // check to see if index is in array, and remove it, if not then add it @@ -118,7 +119,7 @@ class Tree extends AutoControlledComponent, TreeState> { handleTreeItemOverrides = (predefinedProps: TreeItemProps) => ({ onTitleClick: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { - this.trySetState({ activeIndex: this.computeNewIndex(treeItemProps.index) }) + this.trySetState({ activeIndex: this.computeNewIndex(treeItemProps) }) _.invoke(predefinedProps, 'onTitleClick', e, treeItemProps) }, }) diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx index 11c019eda0..5d08da4a89 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -117,12 +117,15 @@ class TreeItem extends UIComponent> { } }, setFocusToFirstChild: e => { - if (open) { - e.stopPropagation() - const element = getFirstFocusable(this.treeRef.current, this.treeRef.current, true) - if (element) { - element.focus() - } + if (!open) { + return + } + + e.stopPropagation() + + const element = getFirstFocusable(this.treeRef.current, this.treeRef.current, true) + if (element) { + element.focus() } }, } diff --git a/packages/react/test/specs/components/Tree/Tree-test.tsx b/packages/react/test/specs/components/Tree/Tree-test.tsx index 8ddf2ab826..b654b0f94d 100644 --- a/packages/react/test/specs/components/Tree/Tree-test.tsx +++ b/packages/react/test/specs/components/Tree/Tree-test.tsx @@ -5,6 +5,7 @@ import { isConformant } from 'test/specs/commonTests' import { mountWithProvider } from 'test/utils' import Tree from 'src/components/Tree/Tree' import TreeTitle from 'src/components/Tree/TreeTitle' +import { ReactWrapper } from 'enzyme' const items = [ { @@ -53,75 +54,79 @@ const items = [ }, ] +const checkOpenTitles = (wrapper: ReactWrapper, expected: string[]): void => { + const titles = wrapper.find(`.${TreeTitle.className}`) + expect(titles.length).toEqual(expected.length) + + expected.forEach((expectedTitle, index) => { + expect(titles.at(index).getDOMNode().textContent).toEqual(expectedTitle) + }) +} + describe('Tree', () => { isConformant(Tree) describe('activeIndex', () => { it('should contain index of item open at click', () => { const wrapper = mountWithProvider() - const tree = wrapper.find(Tree).at(0) wrapper .find(`.${TreeTitle.className}`) .at(0) // title 1 .simulate('click') - expect(tree.state('activeIndex')).toEqual([0]) + checkOpenTitles(wrapper, ['1', '11', '12', '2', '3']) wrapper .find(`.${TreeTitle.className}`) .at(3) // title 2 .simulate('click') - expect(tree.state('activeIndex')).toEqual([0, 1]) + checkOpenTitles(wrapper, ['1', '11', '12', '2', '21', '22', '3']) }) it('should have index of item removed when closed at click', () => { const wrapper = mountWithProvider() - const tree = wrapper.find(Tree).at(0) wrapper .find(`.${TreeTitle.className}`) .at(0) // title 1 .simulate('click') - expect(tree.state('activeIndex')).toEqual([1]) + checkOpenTitles(wrapper, ['1', '2', '21', '22', '3']) }) it('should contain only one index at a time if exclusive', () => { const wrapper = mountWithProvider() - const tree = wrapper.find(Tree).at(0) wrapper .find(`.${TreeTitle.className}`) .at(0) // title 1 .simulate('click') - expect(tree.state('activeIndex')).toEqual(0) + checkOpenTitles(wrapper, ['1', '11', '12', '2', '3']) wrapper .find(`.${TreeTitle.className}`) .at(3) // title 2 .simulate('click') - expect(tree.state('activeIndex')).toEqual(1) + checkOpenTitles(wrapper, ['1', '2', '21', '22', '3']) }) it('should contain index of item open by ArrowRight', () => { const wrapper = mountWithProvider() - const tree = wrapper.find(Tree).at(0) wrapper .find(`.${TreeTitle.className}`) .at(0) // title 1 .simulate('keydown', { keyCode: keyboardKey.ArrowRight }) - expect(tree.state('activeIndex')).toEqual([0]) + checkOpenTitles(wrapper, ['1', '11', '12', '2', '3']) }) it('should have index of item removed if closed by ArrowLeft', () => { const wrapper = mountWithProvider() - const tree = wrapper.find(Tree).at(0) wrapper .find(`.${TreeTitle.className}`) .at(0) // title 1 .simulate('keydown', { keyCode: keyboardKey.ArrowLeft }) - expect(tree.state('activeIndex')).toEqual([1]) + checkOpenTitles(wrapper, ['1', '2', '21', '22', '3']) }) }) }) From 58eea3f0af03c669d3c74d54abb6a9291c437874 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Fri, 7 Jun 2019 15:20:16 +0200 Subject: [PATCH 11/14] updated behavior in doc --- packages/react/src/components/Tree/TreeItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx index 5d08da4a89..6932e140a2 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -35,7 +35,7 @@ export interface TreeItemSlotClassNames { export interface TreeItemProps extends UIComponentProps, ChildrenComponentProps { /** * Accessibility behavior if overridden by the user. - * @default defaultBehavior + * @default treeItemBehavior */ accessibility?: Accessibility From 0d0f630b2fd549bb593ac49287b9b47aecc019e3 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Mon, 10 Jun 2019 09:46:13 +0200 Subject: [PATCH 12/14] fixed a destructuring issue --- packages/react/src/components/Tree/TreeItem.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx index 6932e140a2..6c2d7c3e4f 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -117,6 +117,7 @@ class TreeItem extends UIComponent> { } }, setFocusToFirstChild: e => { + const { open } = this.props if (!open) { return } From 4da3d84cca28e862e100894313b08102dde96b34 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Mon, 10 Jun 2019 10:47:24 +0200 Subject: [PATCH 13/14] renamed the handlers --- packages/react/src/components/Tree/TreeTitle.tsx | 4 ++-- .../lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/react/src/components/Tree/TreeTitle.tsx b/packages/react/src/components/Tree/TreeTitle.tsx index 1957df9323..cbcd2b3efc 100644 --- a/packages/react/src/components/Tree/TreeTitle.tsx +++ b/packages/react/src/components/Tree/TreeTitle.tsx @@ -66,7 +66,7 @@ class TreeTitle extends UIComponent> { e.preventDefault() this.handleClick(e) }, - expandOrFocusFirstChild: e => { + expand: e => { const { open } = this.props e.preventDefault() if (!open) { @@ -74,7 +74,7 @@ class TreeTitle extends UIComponent> { this.handleClick(e) } }, - collapseOrFocusParent: e => { + collapse: e => { const { open } = this.props e.preventDefault() if (open) { diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts index 15c47b15e6..5d108be045 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts @@ -6,8 +6,8 @@ import { IS_FOCUSABLE_ATTRIBUTE } from '../../FocusZone/focusUtilities' * @specification * Adds attribute 'aria-expanded=true' based on the property 'open' if the component has 'hasSubtree' property. * Triggers 'performClick' action with 'Enter' or 'Spacebar' on 'root'. - * Triggers 'expandOrFocusFirstChild' action with 'ArrowRight' on 'root'. - * Triggers 'collapseOrFocusParent' action with 'ArrowLeft' on 'root'. + * Triggers 'expand' action with 'ArrowRight' on 'root'. + * Triggers 'collapse' action with 'ArrowLeft' on 'root'. */ const treeTitleBehavior: Accessibility = (props: any) => ({ attributes: { @@ -23,10 +23,10 @@ const treeTitleBehavior: Accessibility = (props: any) => ({ performClick: { keyCombinations: [{ keyCode: keyboardKey.Enter }, { keyCode: keyboardKey.Spacebar }], }, - expandOrFocusFirstChild: { + expand: { keyCombinations: [{ keyCode: keyboardKey.ArrowRight }], }, - collapseOrFocusParent: { + collapse: { keyCombinations: [{ keyCode: keyboardKey.ArrowLeft }], }, }, From cdfaaf30a92c6f32af6f533dfc85d5168b7cff20 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Mon, 10 Jun 2019 16:32:05 +0200 Subject: [PATCH 14/14] improved tests --- packages/react/src/components/Tree/TreeItem.tsx | 2 +- .../react/test/specs/components/Tree/Tree-test.tsx | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx index c0e6bcd5eb..30f4d4badf 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -156,7 +156,7 @@ class TreeItem extends UIComponent> { overrideProps: this.handleTitleOverrides, })} - {open && ( + {hasSubtree && open && ( {Tree.create(items, { defaultProps: { diff --git a/packages/react/test/specs/components/Tree/Tree-test.tsx b/packages/react/test/specs/components/Tree/Tree-test.tsx index ffd2afd541..3c8394e4d3 100644 --- a/packages/react/test/specs/components/Tree/Tree-test.tsx +++ b/packages/react/test/specs/components/Tree/Tree-test.tsx @@ -139,14 +139,24 @@ describe('Tree', () => { checkOpenTitles(wrapper, ['1', '11', '12', '2', '21', '22', '3']) }) - it('should expand subtrees only on current level', () => { + it('should expand subtrees only on current level on asterisk key', () => { const wrapper = mountWithProvider() wrapper .find(`.${TreeTitle.className}`) .at(1) // title 11 .simulate('keydown', { keyCode: keyboardKey['*'] }) - checkOpenTitles(wrapper, ['1', '11', '12', '121', '2', '21', '22', '3']) + checkOpenTitles(wrapper, ['1', '11', '12', '121', '2', '3']) + }) + + it('should not be changed on asterisk key if all siblings are already expanded', () => { + const wrapper = mountWithProvider() + + wrapper + .find(`.${TreeTitle.className}`) + .at(0) // title 1 + .simulate('keydown', { keyCode: keyboardKey['*'] }) + checkOpenTitles(wrapper, ['1', '11', '12', '2', '21', '22', '3']) }) }) })