From 6503ccffa331b1c95c442fdffbd4508d679f40a3 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Tue, 6 Aug 2019 11:31:32 +0200 Subject: [PATCH 01/47] add Tree component --- .../Tree/Types/TreeExample.shorthand.tsx | 75 ++++++ .../Types/TreeExclusiveExample.shorthand.tsx | 52 ++++ ...reeTitleCustomizationExample.shorthand.tsx | 42 +++ .../examples/components/Tree/Types/index.tsx | 25 ++ docs/src/examples/components/Tree/index.tsx | 10 + packages/react/package.json | 3 +- packages/react/src/components/Tree/Tree.tsx | 252 ++++++++++++++++++ .../react/src/components/Tree/TreeItem.tsx | 221 +++++++++++++++ .../react/src/components/Tree/TreeTitle.tsx | 109 ++++++++ packages/react/src/index.ts | 7 + .../Behaviors/Tree/treeBehavior.ts | 39 +++ .../Behaviors/Tree/treeItemBehavior.ts | 75 ++++++ .../Behaviors/Tree/treeTitleBehavior.ts | 45 ++++ packages/react/src/lib/accessibility/index.ts | 3 + yarn.lock | 32 ++- 15 files changed, 988 insertions(+), 2 deletions(-) create mode 100644 docs/src/examples/components/Tree/Types/TreeExample.shorthand.tsx create mode 100644 docs/src/examples/components/Tree/Types/TreeExclusiveExample.shorthand.tsx create mode 100644 docs/src/examples/components/Tree/Types/TreeTitleCustomizationExample.shorthand.tsx create mode 100644 docs/src/examples/components/Tree/Types/index.tsx create mode 100644 docs/src/examples/components/Tree/index.tsx create mode 100644 packages/react/src/components/Tree/Tree.tsx create mode 100644 packages/react/src/components/Tree/TreeItem.tsx create mode 100644 packages/react/src/components/Tree/TreeTitle.tsx create mode 100644 packages/react/src/lib/accessibility/Behaviors/Tree/treeBehavior.ts create mode 100644 packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts create mode 100644 packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts diff --git a/docs/src/examples/components/Tree/Types/TreeExample.shorthand.tsx b/docs/src/examples/components/Tree/Types/TreeExample.shorthand.tsx new file mode 100644 index 0000000000..e158763393 --- /dev/null +++ b/docs/src/examples/components/Tree/Types/TreeExample.shorthand.tsx @@ -0,0 +1,75 @@ +import * as React from 'react' +import { Tree } from '@stardust-ui/react' + +const items = [ + { + key: '1', + title: 'House Lannister', + items: [ + { + key: '11', + title: 'Tywin', + items: [ + { + key: '111', + title: 'Jaime', + }, + { + key: '112', + title: 'Cersei', + }, + { + key: '113', + title: 'Tyrion', + }, + ], + }, + { + key: '21', + title: 'Kevan', + items: [ + { + key: '211', + title: 'Lancel', + }, + { + key: '212', + title: 'Willem', + }, + { + key: '213', + title: 'Martyn', + }, + ], + }, + ], + }, + { + key: '2', + title: 'House Targaryen', + items: [ + { + key: '21', + title: 'Aerys', + items: [ + { + key: '211', + title: 'Rhaegar', + }, + { + key: '212', + title: 'Viserys', + }, + { + key: '213', + title: 'Daenerys', + }, + ], + }, + ], + }, +] + +const TreeExampleShorthand = () => + +export default TreeExampleShorthand diff --git a/docs/src/examples/components/Tree/Types/TreeExclusiveExample.shorthand.tsx b/docs/src/examples/components/Tree/Types/TreeExclusiveExample.shorthand.tsx new file mode 100644 index 0000000000..8edd835cd2 --- /dev/null +++ b/docs/src/examples/components/Tree/Types/TreeExclusiveExample.shorthand.tsx @@ -0,0 +1,52 @@ +import * as React from 'react' +import { Icon, Tree } from '@stardust-ui/react' + +const items = [ + { + key: '1', + title: 'one', + items: [ + { + key: '2', + title: 'one one', + items: [ + { + key: '3', + title: 'one one one', + }, + ], + }, + { + key: '6', + title: 'one two', + items: [ + { + key: '7', + title: 'one two one', + }, + ], + }, + ], + }, + { + key: '4', + title: 'two', + items: [ + { + key: '5', + title: 'two one', + }, + ], + }, +] + +const titleRenderer = (Component, { content, open, hasSubtree, ...restProps }) => ( + + {hasSubtree && } + {content} + +) + +const TreeExclusiveExample = () => + +export default TreeExclusiveExample diff --git a/docs/src/examples/components/Tree/Types/TreeTitleCustomizationExample.shorthand.tsx b/docs/src/examples/components/Tree/Types/TreeTitleCustomizationExample.shorthand.tsx new file mode 100644 index 0000000000..bcb3bb20d4 --- /dev/null +++ b/docs/src/examples/components/Tree/Types/TreeTitleCustomizationExample.shorthand.tsx @@ -0,0 +1,42 @@ +import * as React from 'react' +import { Icon, Tree } from '@stardust-ui/react' + +const items = [ + { + key: '1', + title: 'one', + items: [ + { + key: '2', + title: 'one one', + items: [ + { + key: '3', + title: 'one one one', + }, + ], + }, + ], + }, + { + key: '4', + title: 'two', + items: [ + { + key: '5', + title: 'two one', + }, + ], + }, +] + +const titleRenderer = (Component, { content, open, hasSubtree, ...restProps }) => ( + + {hasSubtree && } + {content} + +) + +const TreeTitleCustomizationExample = () => + +export default TreeTitleCustomizationExample diff --git a/docs/src/examples/components/Tree/Types/index.tsx b/docs/src/examples/components/Tree/Types/index.tsx new file mode 100644 index 0000000000..c6b19a3743 --- /dev/null +++ b/docs/src/examples/components/Tree/Types/index.tsx @@ -0,0 +1,25 @@ +import * as React from 'react' +import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample' +import ExampleSection from 'docs/src/components/ComponentDoc/ExampleSection' + +const Types = () => ( + + + + + +) + +export default Types diff --git a/docs/src/examples/components/Tree/index.tsx b/docs/src/examples/components/Tree/index.tsx new file mode 100644 index 0000000000..1682fecab8 --- /dev/null +++ b/docs/src/examples/components/Tree/index.tsx @@ -0,0 +1,10 @@ +import * as React from 'react' +import Types from './Types' + +const TreeExamples = () => ( +
+ +
+) + +export default TreeExamples diff --git a/packages/react/package.json b/packages/react/package.json index e6c0ef991b..eab43b648b 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -24,7 +24,8 @@ "lodash": "^4.17.11", "popper.js": "^1.15.0", "prop-types": "^15.6.1", - "react-is": "^16.6.3" + "react-is": "^16.6.3", + "react-virtualized": "^9.21.1" }, "devDependencies": { "@stardust-ui/internal-tooling": "^0.36.0", diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx new file mode 100644 index 0000000000..6b5a5d644e --- /dev/null +++ b/packages/react/src/components/Tree/Tree.tsx @@ -0,0 +1,252 @@ +import * as customPropTypes from '@stardust-ui/react-proptypes' +import * as _ from 'lodash' +import * as PropTypes from 'prop-types' +import * as React from 'react' +import { CellMeasurer, CellMeasurerCache, List as ReactVirtualizedList } from 'react-virtualized' + +import TreeItem, { TreeItemProps } from './TreeItem' +import { + AutoControlledComponent, + childrenExist, + commonPropTypes, + createShorthandFactory, + UIComponentProps, + ChildrenComponentProps, + rtlTextContainer, + applyAccessibilityKeyHandlers, +} from '../../lib' +import { + ShorthandRenderFunction, + WithAsProp, + withSafeTypeForAs, + ShorthandCollection, + ComponentEventHandler, + ShorthandValue, +} from '../../types' +import { Accessibility } from '../../lib/accessibility/types' +import { treeBehavior } from '../../lib/accessibility' + +export interface TreeSlotClassNames { + item: string +} + +export interface TreeProps extends UIComponentProps, ChildrenComponentProps { + /** Index of the currently active subtree. */ + activeItems?: ShorthandCollection + + /** Accessibility behavior if overridden by the user. */ + accessibility?: Accessibility + + /** Initial activeIndex value. */ + defaultActiveItems?: ShorthandCollection + + /** Only allow one subtree to be open at a time. */ + exclusive?: boolean + + /** Shorthand array of props for Tree. */ + items?: ShorthandCollection + + /** + * A custom render function for the title slot. + * + * @param {React.ReactType} Component - The computed component for this slot. + * @param {object} props - The computed props for this slot. + * @param {ReactNode|ReactNodeArray} children - The computed children for this slot. + */ + renderItemTitle?: ShorthandRenderFunction + + /** Called when activeIndex changes. + * + * @param {SyntheticEvent} event - React's original SyntheticEvent. + * @param {object} data - All props and proposed value. + */ + onActiveIndexChange?: ComponentEventHandler +} + +export interface TreeState { + activeItems: ShorthandCollection +} + +class Tree extends AutoControlledComponent, TreeState> { + static create: Function + + static displayName = 'Tree' + + static className = 'ui-tree' + + static slotClassNames: TreeSlotClassNames = { + item: `${Tree.className}__item`, + } + + static propTypes = { + ...commonPropTypes.createCommon({ + content: false, + }), + activeItems: customPropTypes.every([ + customPropTypes.disallow(['children']), + PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.number]), + ]), + defaultActiveItems: customPropTypes.every([ + customPropTypes.disallow(['children']), + PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.number]), + ]), + exclusive: PropTypes.bool, + items: customPropTypes.collectionShorthand, + renderItemTitle: PropTypes.func, + rtlAttributes: PropTypes.func, + onActiveItemsChange: PropTypes.func, + } + + static defaultProps = { + as: 'div', + accessibility: treeBehavior, + } + + static autoControlledProps = ['activeItems'] + + cache = new CellMeasurerCache({ + defaultHeight: 20, + fixedWidth: true, + }) + + actionHandlers = { + expandSiblings: e => { + /* not working yet + const { items, exclusive } = this.props + e.preventDefault() + e.stopPropagation() + + if (exclusive) { + return + } + const activeIndex = items + ? items.reduce((acc, item, index) => { + if (item['items']) { + return [...acc, index] + } + return acc + }, []) + : [] + this.trySetActiveIndexAndTriggerEvent(e, activeIndex) + */ + }, + } + + // trySetActiveIndexAndTriggerEvent = (e, activeIndex) => { + // this.trySetState({ activeItems: activeIndex }) + // _.invoke(this.props, 'onActiveIndexChange', e, { ...this.props, activeIndex }) + // } + + getInitialAutoControlledState(): TreeState { + if (this.props.items) { + const setItemsLevelAndSize = (items = this.props.items, level = 1, parent?) => { + items.forEach((item: ShorthandValue, index: number) => { + item['level'] = level + item['siblings'] = items + item['position'] = index + 1 + if (parent) { + item['parent'] = parent + } + + if (item['items']) { + setItemsLevelAndSize(item['items'], level + 1, item) + } + }) + } + setItemsLevelAndSize() + } + + return { + activeItems: this.props.items || [], + } + } + + handleTreeItemOverrides = (predefinedProps: TreeItemProps) => ({ + onTitleClick: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { + const { index, open, items } = treeItemProps + const { activeItems } = this.state + if (open) { + const end = activeItems.indexOf(_.last(items)) + this.setState({ + activeItems: [...activeItems.slice(0, index + 1), ...activeItems.slice(end + 1)], + }) + } else { + const subItems = activeItems[index]['items'] + if (!subItems) { + return + } + this.setState({ + activeItems: [ + ...activeItems.slice(0, index + 1), + ...subItems, + ...activeItems.slice(index + 1), + ], + }) + } + _.invoke(predefinedProps, 'onTitleClick', e, treeItemProps) + }, + }) + + rowRenderer = ({ index, key, parent, style }) => { + const { activeItems } = this.state + const isSubtree = !!activeItems[index]['items'] + const open = isSubtree && activeItems[index + 1] === activeItems[index]['items'][0] + return ( + + {TreeItem.create(activeItems[index], { + defaultProps: { + className: Tree.slotClassNames.item, + style, + open, + index, + }, + overrideProps: this.handleTreeItemOverrides, + })} + + ) + } + + renderContent() { + const { activeItems } = this.state + + return ( + + ) + } + + renderComponent({ ElementType, classes, accessibility, unhandledProps, styles, variables }) { + const { children } = this.props + + return ( + + {childrenExist(children) ? children : this.renderContent()} + + ) + } +} + +Tree.create = createShorthandFactory({ + Component: Tree, + mappedArrayProp: 'items', +}) + +/** + * A Tree displays data organised in tree hierarchy. + * + * @accessibility + * Implements [ARIA TreeView](https://www.w3.org/TR/wai-aria-practices-1.1/#TreeView) design pattern. + */ +export default withSafeTypeForAs(Tree) diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx new file mode 100644 index 0000000000..35dd726bd5 --- /dev/null +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -0,0 +1,221 @@ +import * as customPropTypes from '@stardust-ui/react-proptypes' +import { Ref } from '@stardust-ui/react-component-ref' +import * as _ from 'lodash' +import * as PropTypes from 'prop-types' +import * as React from 'react' + +import TreeTitle, { TreeTitleProps } from './TreeTitle' +import { treeItemBehavior } from '../../lib/accessibility' +import { Accessibility } from '../../lib/accessibility/types' +import { + UIComponent, + childrenExist, + createShorthandFactory, + commonPropTypes, + UIComponentProps, + ChildrenComponentProps, + rtlTextContainer, + applyAccessibilityKeyHandlers, +} from '../../lib' +import { + ComponentEventHandler, + WithAsProp, + ShorthandRenderFunction, + ShorthandValue, + withSafeTypeForAs, + ShorthandCollection, +} from '../../types' +import { getFirstFocusable } from '../../lib/accessibility/FocusZone/focusUtilities' + +export interface TreeItemSlotClassNames { + title: string + subtree: string +} + +export interface TreeItemProps extends UIComponentProps, ChildrenComponentProps { + /** Accessibility behavior if overridden by the user. */ + accessibility?: Accessibility + + /** Only allow one subtree to be open at a time. */ + exclusive?: boolean + + /** The index of the item among its sibbling */ + index?: number + + /** Array of props for sub tree. */ + items?: ShorthandCollection + + level?: number + + /** Called when a tree title is clicked. */ + onTitleClick?: ComponentEventHandler + + /** Whether or not the subtree of the item is in the open state. */ + open?: boolean + + parent?: ShorthandValue + + position?: number + + siblings?: ShorthandCollection + + /** + * A custom render iterator for rendering each Accordion panel title. + * The default component, props, and children are available for each panel title. + * + * @param {React.ReactType} Component - The computed component for this slot. + * @param {object} props - The computed props for this slot. + * @param {ReactNode|ReactNodeArray} children - The computed children for this slot. + */ + renderItemTitle?: ShorthandRenderFunction + + /** Properties for TreeTitle. */ + title?: ShorthandValue +} + +class TreeItem extends UIComponent> { + static create: Function + + static displayName = 'TreeItem' + + static className = 'ui-tree__item' + + static slotClassNames: TreeItemSlotClassNames = { + title: `${TreeItem.className}__title`, + subtree: `${TreeItem.className}__subtree`, + } + + static propTypes = { + ...commonPropTypes.createCommon({ + content: false, + }), + items: customPropTypes.collectionShorthand, + index: PropTypes.number, + exclusive: PropTypes.bool, + level: PropTypes.number, + onTitleClick: PropTypes.func, + open: PropTypes.bool, + parent: customPropTypes.itemShorthand, + position: PropTypes.number, + renderItemTitle: PropTypes.func, + siblings: customPropTypes.collectionShorthand, + treeItemRtlAttributes: PropTypes.func, + title: customPropTypes.itemShorthand, + } + + static defaultProps = { + as: 'div', + accessibility: treeItemBehavior, + } + + itemRef = React.createRef() + treeRef = React.createRef() + + actionHandlers = { + performClick: e => { + e.preventDefault() + e.stopPropagation() + + _.invoke(this.props, 'onTitleClick', e, this.props) + }, + receiveFocus: e => { + e.preventDefault() + e.stopPropagation() + + // Focuses the title if the event comes from a child item. + if (this.eventComesFromChildItem(e)) { + this.itemRef.current.focus() + } + }, + collapse: e => { + e.preventDefault() + e.stopPropagation() + + // Handle click on title if the keyboard event was dispatched on that title + if (!this.eventComesFromChildItem(e)) { + this.handleTitleClick(e) + } + }, + expand: e => { + e.preventDefault() + e.stopPropagation() + + this.handleTitleClick(e) + }, + focusSubtree: e => { + e.preventDefault() + e.stopPropagation() + + const element = getFirstFocusable(this.treeRef.current, this.treeRef.current, true) + if (element) { + element.focus() + } + }, + } + + eventComesFromChildItem = e => { + return e.currentTarget !== e.target + } + + handleTitleClick = e => { + _.invoke(this.props, 'onTitleClick', e, this.props) + } + + handleTitleOverrides = (predefinedProps: TreeTitleProps) => ({ + onClick: (e, titleProps) => { + this.handleTitleClick(e) + _.invoke(predefinedProps, 'onClick', e, titleProps) + }, + }) + + renderContent() { + const { items, title, renderItemTitle, open, level, siblings, index, position } = this.props + const hasSubtree = !_.isNil(items) + + return TreeTitle.create(title, { + defaultProps: { + className: TreeItem.slotClassNames.title, + open, + hasSubtree, + as: hasSubtree ? 'span' : 'a', + level, + siblingsLength: siblings.length, + index, + position, + }, + render: renderItemTitle, + overrideProps: this.handleTitleOverrides, + }) + } + + renderComponent({ ElementType, accessibility, classes, unhandledProps, styles, variables }) { + const { children } = this.props + + return ( + + + {childrenExist(children) ? children : this.renderContent()} + + + ) + } +} + +TreeItem.create = createShorthandFactory({ + Component: TreeItem, + mappedProp: 'title', +}) + +/** + * A TreeItem renders an item of a Tree. + * + * @accessibility + * Implements [ARIA TreeView](https://www.w3.org/TR/wai-aria-practices-1.1/#TreeView) design pattern. + */ +export default withSafeTypeForAs(TreeItem) diff --git a/packages/react/src/components/Tree/TreeTitle.tsx b/packages/react/src/components/Tree/TreeTitle.tsx new file mode 100644 index 0000000000..f0319daa81 --- /dev/null +++ b/packages/react/src/components/Tree/TreeTitle.tsx @@ -0,0 +1,109 @@ +import * as _ from 'lodash' +import * as PropTypes from 'prop-types' +import * as React from 'react' + +import { + UIComponent, + childrenExist, + createShorthandFactory, + commonPropTypes, + UIComponentProps, + ChildrenComponentProps, + ContentComponentProps, + rtlTextContainer, + applyAccessibilityKeyHandlers, +} from '../../lib' +import { treeTitleBehavior } from '../../lib/accessibility' +import { Accessibility } from '../../lib/accessibility/types' +import { ComponentEventHandler, WithAsProp, withSafeTypeForAs } from '../../types' + +export interface TreeTitleProps + extends UIComponentProps, + ChildrenComponentProps, + ContentComponentProps { + /** Accessibility behavior if overridden by the user. */ + accessibility?: Accessibility + + level?: number + + /** + * Called on click. + * + * @param {SyntheticEvent} event - React's original SyntheticEvent. + * @param {object} data - All props. + */ + onClick?: ComponentEventHandler + + /** Whether or not the subtree of the item is in the open state. */ + open?: boolean + + position?: number + + /** Whether or not the item has a subtree. */ + hasSubtree?: boolean + + siblingsLength?: number + + index?: number +} + +class TreeTitle extends UIComponent> { + static create: Function + + static className = 'ui-tree__title' + + static displayName = 'TreeTitle' + + static propTypes = { + ...commonPropTypes.createCommon(), + level: PropTypes.number, + onClick: PropTypes.func, + open: PropTypes.bool, + hasSubtree: PropTypes.bool, + siblingsLength: PropTypes.number, + index: PropTypes.number, + } + + static defaultProps = { + as: 'a', + accessibility: treeTitleBehavior, + } + + actionHandlers = { + performClick: e => { + e.preventDefault() + this.handleClick(e) + }, + } + + handleClick = e => { + _.invoke(this.props, 'onClick', e, this.props) + } + + renderComponent({ ElementType, classes, accessibility, unhandledProps, styles, variables }) { + const { children, content } = this.props + + return ( + + {childrenExist(children) ? children : content} + + ) + } +} + +TreeTitle.create = createShorthandFactory({ + Component: TreeTitle, + mappedProp: 'content', +}) + +/** + * A TreeTitle renders a title of TreeItem. + */ +export default withSafeTypeForAs(TreeTitle) diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 833f15a6bc..408947159e 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -180,6 +180,13 @@ export { default as HierarchicalTreeTitle, } from './components/HierarchicalTree/HierarchicalTreeTitle' +export * from './components/Tree/Tree' +export { default as Tree } from './components/Tree/Tree' +export * from './components/Tree/TreeItem' +export { default as TreeItem } from './components/Tree/TreeItem' +export * from './components/Tree/TreeTitle' +export { default as TreeTitle } from './components/Tree/TreeTitle' + export * from './components/Reaction/Reaction' export { default as Reaction } from './components/Reaction/Reaction' export * from './components/Reaction/ReactionGroup' diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeBehavior.ts new file mode 100644 index 0000000000..2a45e0493b --- /dev/null +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeBehavior.ts @@ -0,0 +1,39 @@ +import * as keyboardKey from 'keyboard-key' +import { Accessibility, AccessibilityAttributes, FocusZoneMode } from '../../types' +import { FocusZoneDirection } from '../../FocusZone' + +/** + * @specification + * Adds role 'tree' to 'root' slot. + * Adds attribute 'aria-labelledby' based on the property 'aria-labelledby' to 'root' slot. + * Embeds component into FocusZone. + * Provides arrow key navigation in vertical direction. + * Triggers 'expandSiblings' action with '*' on 'root'. + */ +const hierarchicalTreeBehavior: Accessibility = props => { + return { + attributes: { + root: { + role: 'tree', + 'aria-labelledby': props['aria-labelledby'], + }, + }, + keyActions: { + root: { + expandSiblings: { + keyCombinations: [{ keyCode: keyboardKey['*'] }], + }, + }, + }, + focusZone: { + mode: FocusZoneMode.Embed, + props: { + direction: FocusZoneDirection.vertical, + }, + }, + } +} + +type TreeBehaviorProps = {} & Pick + +export default hierarchicalTreeBehavior 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..55ff455588 --- /dev/null +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts @@ -0,0 +1,75 @@ +import { Accessibility } from '../../types' +import * as keyboardKey from 'keyboard-key' +import { IS_FOCUSABLE_ATTRIBUTE } from '../../FocusZone/focusUtilities' + +/** + * @description + * Adds role 'treeitem' to a non-leaf item and 'none' to a leaf item. + * Adds 'aria-expanded' with a value based on the 'open' prop if item is not a leaf. + * Adds 'tabIndex' as '-1' if the item is not a leaf. + * + * @specification + * Triggers 'performClick' action with 'Enter' or 'Spacebar' on 'root'. + * Triggers 'receiveFocus' action with 'ArrowLeft' on 'root', when has an opened subtree. + * Triggers 'collapse' action with 'ArrowLeft' on 'root', when has an opened subtree. + * Triggers 'expand' action with 'ArrowRight' on 'root', when has a closed subtree. + * Triggers 'focusSubtree' action with 'ArrowRight' on 'root', when has an opened subtree. + */ +const hierarchicalTreeItemBehavior: Accessibility = props => ({ + attributes: { + root: { + role: 'none', + ...(props.items && + props.items.length && { + 'aria-expanded': props.open ? 'true' : 'false', + tabIndex: -1, + [IS_FOCUSABLE_ATTRIBUTE]: true, + role: 'treeitem', + 'aria-setsize': props.siblings.length, + 'aria-posinset': props.position, + 'aria-level': props.level, + }), + }, + }, + keyActions: { + root: { + performClick: { + keyCombinations: [{ keyCode: keyboardKey.Enter }, { keyCode: keyboardKey.Spacebar }], + }, + ...(isSubtreeOpen(props) && { + receiveFocus: { + keyCombinations: [{ keyCode: keyboardKey.ArrowLeft }], + }, + collapse: { + keyCombinations: [{ keyCode: keyboardKey.ArrowLeft }], + }, + focusSubtree: { + keyCombinations: [{ keyCode: keyboardKey.ArrowRight }], + }, + }), + ...(!isSubtreeOpen(props) && { + expand: { + keyCombinations: [{ keyCode: keyboardKey.ArrowRight }], + }, + }), + }, + }, +}) + +export type TreeItemBehaviorProps = { + /** If item is a subtree, it contains items. */ + items?: object[] + /** If item is a subtree, it indicates if it's open. */ + open?: boolean + siblings?: object[] + level?: number + position?: number +} + +/** Checks if current tree item has a subtree and it is opened */ +const isSubtreeOpen = (props: TreeItemBehaviorProps): boolean => { + const { items, open } = props + return !!(items && items.length && open) +} + +export default hierarchicalTreeItemBehavior diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts new file mode 100644 index 0000000000..a4dafb3e6b --- /dev/null +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts @@ -0,0 +1,45 @@ +import { Accessibility } from '../../types' +import * as keyboardKey from 'keyboard-key' +import { IS_FOCUSABLE_ATTRIBUTE } from '../../FocusZone/focusUtilities' + +/** + * @description + * Adds role 'treeitem' if the title is a leaf node inside the tree. + * Adds 'tabIndex' as '-1' if the title is a leaf node inside the tree. + * + * @specification + * Triggers 'performClick' action with 'Enter' or 'Spacebar' on 'root'. + */ +const hierarchicalTreeTitleBehavior: Accessibility = props => ({ + attributes: { + root: { + ...(!props.hasSubtree && { + tabIndex: -1, + [IS_FOCUSABLE_ATTRIBUTE]: true, + role: 'treeitem', + 'aria-setsize': props.siblingsLength, + 'aria-posinset': props.position, + 'aria-level': props.level, + }), + }, + }, + keyActions: { + root: { + performClick: { + keyCombinations: [{ keyCode: keyboardKey.Enter }, { keyCode: keyboardKey.Spacebar }], + }, + }, + }, +}) + +export default hierarchicalTreeTitleBehavior + +type TreeTitleBehavior = { + /** Indicated if tree title has a subtree */ + hasSubtree?: boolean + /** If subtree is opened. */ + open?: boolean + level?: number + siblingsLength?: number + position?: number +} diff --git a/packages/react/src/lib/accessibility/index.ts b/packages/react/src/lib/accessibility/index.ts index 2e55b6e5ec..3f67d75bf8 100644 --- a/packages/react/src/lib/accessibility/index.ts +++ b/packages/react/src/lib/accessibility/index.ts @@ -59,3 +59,6 @@ export { default as tooltipBehavior } from './Behaviors/Tooltip/tooltipBehavior' export { default as tooltipAsLabelBehavior } from './Behaviors/Tooltip/tooltipAsLabelBehavior' export { default as sliderBehavior } from './Behaviors/Slider/sliderBehavior' export { default as menuButtonBehavior } from './Behaviors/MenuButton/menuButtonBehavior' +export { default as treeBehavior } from './Behaviors/tree/treeBehavior' +export { default as treeItemBehavior } from './Behaviors/tree/treeItemBehavior' +export { default as treeTitleBehavior } from './Behaviors/tree/treeTitleBehavior' diff --git a/yarn.lock b/yarn.lock index 1ec1cbefa0..e1adccada0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3746,6 +3746,11 @@ cloneable-readable@^1.0.0: process-nextick-args "^2.0.0" readable-stream "^2.3.5" +clsx@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.0.4.tgz#0c0171f6d5cb2fe83848463c15fcc26b4df8c2ec" + integrity sha512-1mQ557MIZTrL/140j+JVdRM6e31/OA4vTYxXgqIIZlndyfjHpyawKZia1Im05Vp9BWmImkcNrNtFYQMyFcgJDg== + cmd-shim@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-2.0.2.tgz#6fcbda99483a8fd15d7d30a196ca69d688a2efdb" @@ -4922,6 +4927,13 @@ dom-css@^2.0.0: prefix-style "2.0.1" to-camel-case "1.0.0" +"dom-helpers@^2.4.0 || ^3.0.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" + integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA== + dependencies: + "@babel/runtime" "^7.1.2" + dom-serializer@0, dom-serializer@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" @@ -9092,6 +9104,11 @@ liftoff@^3.1.0: rechoir "^0.6.2" resolve "^1.1.7" +linear-layout-vector@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/linear-layout-vector/-/linear-layout-vector-0.0.1.tgz#398114d7303b6ecc7fd6b273af7b8401d8ba9c70" + integrity sha1-OYEU1zA7bsx/1rJzr3uEAdi6nHA= + lint-staged@^7.0.2: version "7.2.0" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-7.2.0.tgz#bdf4bb7f2f37fe689acfaec9999db288a5b26888" @@ -9559,7 +9576,7 @@ longest-streak@^1.0.0: resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-1.0.0.tgz#d06597c4d4c31b52ccb1f5d8f8fe7148eafd6965" integrity sha1-0GWXxNTDG1LMsfXY+P5xSOr9aWU= -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -12030,6 +12047,19 @@ react-test-renderer@^16.0.0-0, react-test-renderer@^16.8.5: react-is "^16.8.6" scheduler "^0.13.6" +react-virtualized@^9.21.1: + version "9.21.1" + resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.21.1.tgz#4dbbf8f0a1420e2de3abf28fbb77120815277b3a" + integrity sha512-E53vFjRRMCyUTEKuDLuGH1ld/9TFzjf/fFW816PE4HFXWZorESbSTYtiZz1oAjra0MminaUU1EnvUxoGuEFFPA== + dependencies: + babel-runtime "^6.26.0" + clsx "^1.0.1" + dom-helpers "^2.4.0 || ^3.0.0" + linear-layout-vector "0.0.1" + loose-envify "^1.3.0" + prop-types "^15.6.0" + react-lifecycles-compat "^3.0.4" + react-vis@^1.11.6: version "1.11.6" resolved "https://registry.yarnpkg.com/react-vis/-/react-vis-1.11.6.tgz#4616968ac6dfbd95491d778e70ad26956fd2fdab" From 399600b7498ac36f4b399c85cba56695cc01de4f Mon Sep 17 00:00:00 2001 From: silviuavram Date: Tue, 6 Aug 2019 16:45:41 +0200 Subject: [PATCH 02/47] some progress on the handlers --- packages/react/src/components/Tree/Tree.tsx | 54 +++++++++++++++---- .../react/src/components/Tree/TreeItem.tsx | 32 ++++++----- .../Behaviors/Tree/treeItemBehavior.ts | 2 +- 3 files changed, 64 insertions(+), 24 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 6b5a5d644e..3576ac95c2 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -3,6 +3,7 @@ import * as _ from 'lodash' import * as PropTypes from 'prop-types' import * as React from 'react' import { CellMeasurer, CellMeasurerCache, List as ReactVirtualizedList } from 'react-virtualized' +import { handleRef, Ref } from '@stardust-ui/react-component-ref' import TreeItem, { TreeItemProps } from './TreeItem' import { @@ -109,6 +110,8 @@ class Tree extends AutoControlledComponent, TreeState> { fixedWidth: true, }) + itemRefs = [] + actionHandlers = { expandSiblings: e => { /* not working yet @@ -162,7 +165,11 @@ class Tree extends AutoControlledComponent, TreeState> { } handleTreeItemOverrides = (predefinedProps: TreeItemProps) => ({ - onTitleClick: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { + onTitleClick: ( + e: React.SyntheticEvent, + treeItemProps: TreeItemProps, + predefinedProps: TreeItemProps, + ) => { const { index, open, items } = treeItemProps const { activeItems } = this.state if (open) { @@ -185,6 +192,24 @@ class Tree extends AutoControlledComponent, TreeState> { } _.invoke(predefinedProps, 'onTitleClick', e, treeItemProps) }, + onParentFocus: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { + const { index } = treeItemProps + const { activeItems } = this.state + const parentItem = activeItems[index]['parent'] + if (parentItem) { + const parentItemIndex = activeItems.indexOf(parentItem) + this.itemRefs[parentItemIndex].current.focus() + } + _.invoke(predefinedProps, 'onParentFocus', e, treeItemProps) + }, + onFirstChildFocus: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { + const { index } = treeItemProps + const { activeItems } = this.state + if (activeItems[index]['items']) { + this.itemRefs[index + 1].current.focus() + } + _.invoke(predefinedProps, 'onFirstChildFocus', e, treeItemProps) + }, }) rowRenderer = ({ index, key, parent, style }) => { @@ -193,21 +218,30 @@ class Tree extends AutoControlledComponent, TreeState> { const open = isSubtree && activeItems[index + 1] === activeItems[index]['items'][0] return ( - {TreeItem.create(activeItems[index], { - defaultProps: { - className: Tree.slotClassNames.item, - style, - open, - index, - }, - overrideProps: this.handleTreeItemOverrides, - })} + { + const ref = React.createRef() + this.itemRefs.push(ref) + handleRef(ref, itemElement) + }} + > + {TreeItem.create(activeItems[index], { + defaultProps: { + className: Tree.slotClassNames.item, + style, + open, + index, + }, + overrideProps: this.handleTreeItemOverrides, + })} + ) } renderContent() { const { activeItems } = this.state + this.itemRefs = [] return ( + /** Called when the item's first child is focused. */ + onFirstChildFocus?: ComponentEventHandler + + /** Called when the item's parent is focused. */ + onParentFocus?: ComponentEventHandler + /** Whether or not the subtree of the item is in the open state. */ open?: boolean @@ -94,6 +99,8 @@ class TreeItem extends UIComponent> { exclusive: PropTypes.bool, level: PropTypes.number, onTitleClick: PropTypes.func, + onFirstChildFocus: PropTypes.func, + onParentFocus: PropTypes.func, open: PropTypes.bool, parent: customPropTypes.itemShorthand, position: PropTypes.number, @@ -118,14 +125,11 @@ class TreeItem extends UIComponent> { _.invoke(this.props, 'onTitleClick', e, this.props) }, - receiveFocus: e => { + focusParent: e => { e.preventDefault() e.stopPropagation() - // Focuses the title if the event comes from a child item. - if (this.eventComesFromChildItem(e)) { - this.itemRef.current.focus() - } + this.handleParentFocus(e) }, collapse: e => { e.preventDefault() @@ -143,13 +147,7 @@ class TreeItem extends UIComponent> { this.handleTitleClick(e) }, focusSubtree: e => { - e.preventDefault() - e.stopPropagation() - - const element = getFirstFocusable(this.treeRef.current, this.treeRef.current, true) - if (element) { - element.focus() - } + this.handleFirstChildFocus(e) }, } @@ -161,6 +159,14 @@ class TreeItem extends UIComponent> { _.invoke(this.props, 'onTitleClick', e, this.props) } + handleParentFocus = e => { + _.invoke(this.props, 'onParentFocus', e, this.props) + } + + handleFirstChildFocus = e => { + _.invoke(this.props, 'onFirstChildFocus', e, this.props) + } + handleTitleOverrides = (predefinedProps: TreeTitleProps) => ({ onClick: (e, titleProps) => { this.handleTitleClick(e) diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts index 55ff455588..9468c6e278 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts @@ -37,7 +37,7 @@ const hierarchicalTreeItemBehavior: Accessibility = props keyCombinations: [{ keyCode: keyboardKey.Enter }, { keyCode: keyboardKey.Spacebar }], }, ...(isSubtreeOpen(props) && { - receiveFocus: { + focusParent: { keyCombinations: [{ keyCode: keyboardKey.ArrowLeft }], }, collapse: { From d46d3b495b0773785206bc66d863394e47ec201b Mon Sep 17 00:00:00 2001 From: silviuavram Date: Thu, 8 Aug 2019 14:30:11 +0200 Subject: [PATCH 03/47] fixed left-right navigation --- packages/react/src/components/Tree/Tree.tsx | 130 ++++++++---------- .../react/src/components/Tree/TreeItem.tsx | 33 +++-- .../react/src/components/Tree/TreeTitle.tsx | 7 +- .../Behaviors/Tree/treeItemBehavior.ts | 10 +- .../Behaviors/Tree/treeTitleBehavior.ts | 4 +- 5 files changed, 88 insertions(+), 96 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 3576ac95c2..e73da552b1 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -2,7 +2,6 @@ import * as customPropTypes from '@stardust-ui/react-proptypes' import * as _ from 'lodash' import * as PropTypes from 'prop-types' import * as React from 'react' -import { CellMeasurer, CellMeasurerCache, List as ReactVirtualizedList } from 'react-virtualized' import { handleRef, Ref } from '@stardust-ui/react-component-ref' import TreeItem, { TreeItemProps } from './TreeItem' @@ -26,6 +25,7 @@ import { } from '../../types' import { Accessibility } from '../../lib/accessibility/types' import { treeBehavior } from '../../lib/accessibility' +import { getFirstFocusable } from '../../lib/accessibility/FocusZone/focusUtilities' export interface TreeSlotClassNames { item: string @@ -105,48 +105,15 @@ class Tree extends AutoControlledComponent, TreeState> { static autoControlledProps = ['activeItems'] - cache = new CellMeasurerCache({ - defaultHeight: 20, - fixedWidth: true, - }) - itemRefs = [] - actionHandlers = { - expandSiblings: e => { - /* not working yet - const { items, exclusive } = this.props - e.preventDefault() - e.stopPropagation() - - if (exclusive) { - return - } - const activeIndex = items - ? items.reduce((acc, item, index) => { - if (item['items']) { - return [...acc, index] - } - return acc - }, []) - : [] - this.trySetActiveIndexAndTriggerEvent(e, activeIndex) - */ - }, - } - - // trySetActiveIndexAndTriggerEvent = (e, activeIndex) => { - // this.trySetState({ activeItems: activeIndex }) - // _.invoke(this.props, 'onActiveIndexChange', e, { ...this.props, activeIndex }) - // } - getInitialAutoControlledState(): TreeState { if (this.props.items) { const setItemsLevelAndSize = (items = this.props.items, level = 1, parent?) => { items.forEach((item: ShorthandValue, index: number) => { item['level'] = level item['siblings'] = items - item['position'] = index + 1 + item['indexInSubtree'] = index if (parent) { item['parent'] = parent } @@ -170,93 +137,108 @@ class Tree extends AutoControlledComponent, TreeState> { treeItemProps: TreeItemProps, predefinedProps: TreeItemProps, ) => { - const { index, open, items } = treeItemProps + const { indexInTree, open, siblings, indexInSubtree } = treeItemProps const { activeItems } = this.state if (open) { - const end = activeItems.indexOf(_.last(items)) - this.setState({ - activeItems: [...activeItems.slice(0, index + 1), ...activeItems.slice(end + 1)], - }) + const nextSibling = siblings[indexInSubtree + 1] + if (!nextSibling) { + this.setState({ + activeItems: activeItems.slice(0, indexInTree + 1), + }) + } else { + const nextSiblingIndexInTree = activeItems.indexOf(nextSibling) + this.setState({ + activeItems: [ + ...activeItems.slice(0, indexInTree + 1), + ...activeItems.slice(nextSiblingIndexInTree), + ], + }) + } } else { - const subItems = activeItems[index]['items'] + const subItems = activeItems[indexInTree]['items'] if (!subItems) { return } this.setState({ activeItems: [ - ...activeItems.slice(0, index + 1), + ...activeItems.slice(0, indexInTree + 1), ...subItems, - ...activeItems.slice(index + 1), + ...activeItems.slice(indexInTree + 1), ], }) } _.invoke(predefinedProps, 'onTitleClick', e, treeItemProps) }, onParentFocus: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { - const { index } = treeItemProps + const { indexInTree } = treeItemProps const { activeItems } = this.state - const parentItem = activeItems[index]['parent'] + const parentItem = activeItems[indexInTree]['parent'] + if (parentItem) { const parentItemIndex = activeItems.indexOf(parentItem) this.itemRefs[parentItemIndex].current.focus() } + _.invoke(predefinedProps, 'onParentFocus', e, treeItemProps) }, onFirstChildFocus: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { - const { index } = treeItemProps + const { indexInTree } = treeItemProps const { activeItems } = this.state - if (activeItems[index]['items']) { - this.itemRefs[index + 1].current.focus() + + if (activeItems[indexInTree]['items']) { + const element = getFirstFocusable( + this.itemRefs[indexInTree + 1].current, + this.itemRefs[indexInTree + 1].current, + true, + ) + if (element) { + element.focus() + } } + _.invoke(predefinedProps, 'onFirstChildFocus', e, treeItemProps) }, }) - rowRenderer = ({ index, key, parent, style }) => { + renderContent() { const { activeItems } = this.state - const isSubtree = !!activeItems[index]['items'] - const open = isSubtree && activeItems[index + 1] === activeItems[index]['items'][0] - return ( - + + return _.map(activeItems, (item: ShorthandValue, index: number) => { + const isSubtree = !!item['items'] + const open = isSubtree && activeItems[index + 1] === item['items'][0] + + return ( { + if ( + !itemElement || + (this.itemRefs.length && + this.itemRefs[this.itemRefs.length - 1].current === itemElement) + ) { + return + } + const ref = React.createRef() this.itemRefs.push(ref) handleRef(ref, itemElement) }} > - {TreeItem.create(activeItems[index], { + {TreeItem.create(item, { defaultProps: { className: Tree.slotClassNames.item, - style, open, - index, + indexInTree: index, }, overrideProps: this.handleTreeItemOverrides, })} - - ) - } - - renderContent() { - const { activeItems } = this.state - this.itemRefs = [] - - return ( - - ) + ) + }) } renderComponent({ ElementType, classes, accessibility, unhandledProps, styles, variables }) { const { children } = this.props + this.itemRefs = [] return ( @@ -60,7 +60,7 @@ export interface TreeItemProps extends UIComponentProps, ChildrenComponentProps parent?: ShorthandValue - position?: number + indexInSubtree?: number siblings?: ShorthandCollection @@ -95,7 +95,7 @@ class TreeItem extends UIComponent> { content: false, }), items: customPropTypes.collectionShorthand, - index: PropTypes.number, + indexInTree: PropTypes.number, exclusive: PropTypes.bool, level: PropTypes.number, onTitleClick: PropTypes.func, @@ -103,7 +103,7 @@ class TreeItem extends UIComponent> { onParentFocus: PropTypes.func, open: PropTypes.bool, parent: customPropTypes.itemShorthand, - position: PropTypes.number, + indexInSubtree: PropTypes.number, renderItemTitle: PropTypes.func, siblings: customPropTypes.collectionShorthand, treeItemRtlAttributes: PropTypes.func, @@ -123,7 +123,7 @@ class TreeItem extends UIComponent> { e.preventDefault() e.stopPropagation() - _.invoke(this.props, 'onTitleClick', e, this.props) + this.handleTitleClick(e) }, focusParent: e => { e.preventDefault() @@ -135,10 +135,7 @@ class TreeItem extends UIComponent> { e.preventDefault() e.stopPropagation() - // Handle click on title if the keyboard event was dispatched on that title - if (!this.eventComesFromChildItem(e)) { - this.handleTitleClick(e) - } + this.handleTitleClick(e) }, expand: e => { e.preventDefault() @@ -147,6 +144,9 @@ class TreeItem extends UIComponent> { this.handleTitleClick(e) }, focusSubtree: e => { + e.preventDefault() + e.stopPropagation() + this.handleFirstChildFocus(e) }, } @@ -175,7 +175,16 @@ class TreeItem extends UIComponent> { }) renderContent() { - const { items, title, renderItemTitle, open, level, siblings, index, position } = this.props + const { + items, + title, + renderItemTitle, + open, + level, + siblings, + indexInTree, + indexInSubtree, + } = this.props const hasSubtree = !_.isNil(items) return TreeTitle.create(title, { @@ -186,8 +195,8 @@ class TreeItem extends UIComponent> { as: hasSubtree ? 'span' : 'a', level, siblingsLength: siblings.length, - index, - position, + indexInTree, + indexInSubtree, }, render: renderItemTitle, overrideProps: this.handleTitleOverrides, diff --git a/packages/react/src/components/Tree/TreeTitle.tsx b/packages/react/src/components/Tree/TreeTitle.tsx index f0319daa81..0ecfa1625b 100644 --- a/packages/react/src/components/Tree/TreeTitle.tsx +++ b/packages/react/src/components/Tree/TreeTitle.tsx @@ -37,14 +37,14 @@ export interface TreeTitleProps /** Whether or not the subtree of the item is in the open state. */ open?: boolean - position?: number + indexInSubtree?: number /** Whether or not the item has a subtree. */ hasSubtree?: boolean siblingsLength?: number - index?: number + indexInTree?: number } class TreeTitle extends UIComponent> { @@ -61,7 +61,8 @@ class TreeTitle extends UIComponent> { open: PropTypes.bool, hasSubtree: PropTypes.bool, siblingsLength: PropTypes.number, - index: PropTypes.number, + indexInSubtree: PropTypes.number, + indexInTree: PropTypes.number, } static defaultProps = { diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts index 9468c6e278..56042e62ba 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts @@ -26,7 +26,7 @@ const hierarchicalTreeItemBehavior: Accessibility = props [IS_FOCUSABLE_ATTRIBUTE]: true, role: 'treeitem', 'aria-setsize': props.siblings.length, - 'aria-posinset': props.position, + 'aria-posinset': props.indexInSubtree + 1, 'aria-level': props.level, }), }, @@ -37,9 +37,6 @@ const hierarchicalTreeItemBehavior: Accessibility = props keyCombinations: [{ keyCode: keyboardKey.Enter }, { keyCode: keyboardKey.Spacebar }], }, ...(isSubtreeOpen(props) && { - focusParent: { - keyCombinations: [{ keyCode: keyboardKey.ArrowLeft }], - }, collapse: { keyCombinations: [{ keyCode: keyboardKey.ArrowLeft }], }, @@ -51,6 +48,9 @@ const hierarchicalTreeItemBehavior: Accessibility = props expand: { keyCombinations: [{ keyCode: keyboardKey.ArrowRight }], }, + focusParent: { + keyCombinations: [{ keyCode: keyboardKey.ArrowLeft }], + }, }), }, }, @@ -63,7 +63,7 @@ export type TreeItemBehaviorProps = { open?: boolean siblings?: object[] level?: number - position?: number + indexInSubtree?: number } /** Checks if current tree item has a subtree and it is opened */ diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts index a4dafb3e6b..f05f6cd8f0 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts @@ -18,7 +18,7 @@ const hierarchicalTreeTitleBehavior: Accessibility = props => [IS_FOCUSABLE_ATTRIBUTE]: true, role: 'treeitem', 'aria-setsize': props.siblingsLength, - 'aria-posinset': props.position, + 'aria-posinset': props.indexInSubtree, 'aria-level': props.level, }), }, @@ -41,5 +41,5 @@ type TreeTitleBehavior = { open?: boolean level?: number siblingsLength?: number - position?: number + indexInSubtree?: number } From bc3d663917f616d14450662294aa81bcb0f585c6 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Thu, 8 Aug 2019 19:38:49 +0200 Subject: [PATCH 04/47] fixed more expand/collapse logic --- packages/react/src/components/Tree/Tree.tsx | 114 +++++++++++++----- .../react/src/components/Tree/TreeItem.tsx | 14 +++ .../Behaviors/Tree/treeItemBehavior.ts | 3 + 3 files changed, 102 insertions(+), 29 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index e73da552b1..fb7005578e 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -131,41 +131,66 @@ class Tree extends AutoControlledComponent, TreeState> { } } + handleTitleOpen = (treeItemProps: TreeItemProps) => { + const { activeItems } = this.state + const { indexInTree } = treeItemProps + const subItems = activeItems[indexInTree]['items'] + + if (!subItems) { + return + } + this.trySetState({ + activeItems: [ + ...activeItems.slice(0, indexInTree + 1), + ...subItems, + ...activeItems.slice(indexInTree + 1), + ], + }) + } + + handleTitleClose = (treeItemProps: TreeItemProps) => { + const { indexInTree, siblings, indexInSubtree, parent } = treeItemProps + const { activeItems } = this.state + const nextSibling = siblings[indexInSubtree + 1] + + if (!nextSibling) { + const nextParentSibling = parent ? parent['siblings'][parent['indexInSubtree'] + 1] : null + if (nextParentSibling) { + const nextParentSiblingIndexInTree = activeItems.indexOf(nextParentSibling) + + this.trySetState({ + activeItems: [ + ...activeItems.slice(0, indexInTree + 1), + ...activeItems.slice(nextParentSiblingIndexInTree), + ], + }) + } else { + this.trySetState({ + activeItems: activeItems.slice(0, indexInTree + 1), + }) + } + } else { + const nextSiblingIndexInTree = activeItems.indexOf(nextSibling) + this.trySetState({ + activeItems: [ + ...activeItems.slice(0, indexInTree + 1), + ...activeItems.slice(nextSiblingIndexInTree), + ], + }) + } + } + handleTreeItemOverrides = (predefinedProps: TreeItemProps) => ({ onTitleClick: ( e: React.SyntheticEvent, treeItemProps: TreeItemProps, predefinedProps: TreeItemProps, ) => { - const { indexInTree, open, siblings, indexInSubtree } = treeItemProps - const { activeItems } = this.state + const { open } = treeItemProps if (open) { - const nextSibling = siblings[indexInSubtree + 1] - if (!nextSibling) { - this.setState({ - activeItems: activeItems.slice(0, indexInTree + 1), - }) - } else { - const nextSiblingIndexInTree = activeItems.indexOf(nextSibling) - this.setState({ - activeItems: [ - ...activeItems.slice(0, indexInTree + 1), - ...activeItems.slice(nextSiblingIndexInTree), - ], - }) - } + this.handleTitleClose(treeItemProps) } else { - const subItems = activeItems[indexInTree]['items'] - if (!subItems) { - return - } - this.setState({ - activeItems: [ - ...activeItems.slice(0, indexInTree + 1), - ...subItems, - ...activeItems.slice(indexInTree + 1), - ], - }) + this.handleTitleOpen(treeItemProps) } _.invoke(predefinedProps, 'onTitleClick', e, treeItemProps) }, @@ -198,6 +223,37 @@ class Tree extends AutoControlledComponent, TreeState> { _.invoke(predefinedProps, 'onFirstChildFocus', e, treeItemProps) }, + onSiblingsExpand: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { + const { siblings, indexInSubtree } = treeItemProps + let { activeItems } = this.state + + siblings.forEach((sibling: ShorthandValue) => { + const siblingIndexInTree = activeItems.indexOf(sibling) + const isSubtree = !!sibling['items'] + + if (isSubtree) { + const isSubtreeOpen = activeItems[siblingIndexInTree + 1] === sibling['items'][0] + if (!isSubtreeOpen) { + activeItems = [ + ...activeItems.slice(0, siblingIndexInTree + 1), + ...activeItems[siblingIndexInTree]['items'], + ...activeItems.slice(siblingIndexInTree + 1), + ] + } + } + }) + + this.trySetState({ activeItems }, () => { + const indexInTree = activeItems.indexOf(siblings[indexInSubtree]) + const element = getFirstFocusable( + this.itemRefs[indexInTree].current, + this.itemRefs[indexInTree].current, + true, + ) + + element.focus() + }) + }, }) renderContent() { @@ -205,7 +261,7 @@ class Tree extends AutoControlledComponent, TreeState> { return _.map(activeItems, (item: ShorthandValue, index: number) => { const isSubtree = !!item['items'] - const open = isSubtree && activeItems[index + 1] === item['items'][0] + const isSubtreeOpen = isSubtree && activeItems[index + 1] === item['items'][0] return ( , TreeState> { {TreeItem.create(item, { defaultProps: { className: Tree.slotClassNames.item, - open, + open: isSubtreeOpen, indexInTree: index, }, overrideProps: this.handleTreeItemOverrides, diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx index 5a76dbe013..e6c364a25d 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -52,6 +52,9 @@ export interface TreeItemProps extends UIComponentProps, ChildrenComponentProps /** Called when the item's first child is focused. */ onFirstChildFocus?: ComponentEventHandler + /** Called when the item's first child is focused. */ + onSiblingsExpand?: ComponentEventHandler + /** Called when the item's parent is focused. */ onParentFocus?: ComponentEventHandler @@ -101,6 +104,7 @@ class TreeItem extends UIComponent> { onTitleClick: PropTypes.func, onFirstChildFocus: PropTypes.func, onParentFocus: PropTypes.func, + onSiblingsExpand: PropTypes.func, open: PropTypes.bool, parent: customPropTypes.itemShorthand, indexInSubtree: PropTypes.number, @@ -149,6 +153,12 @@ class TreeItem extends UIComponent> { this.handleFirstChildFocus(e) }, + expandSiblings: e => { + e.preventDefault() + e.stopPropagation() + + this.handleSiblingsExpand(e) + }, } eventComesFromChildItem = e => { @@ -167,6 +177,10 @@ class TreeItem extends UIComponent> { _.invoke(this.props, 'onFirstChildFocus', e, this.props) } + handleSiblingsExpand = e => { + _.invoke(this.props, 'onSiblingsExpand', e, this.props) + } + handleTitleOverrides = (predefinedProps: TreeTitleProps) => ({ onClick: (e, titleProps) => { this.handleTitleClick(e) diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts index 56042e62ba..ebf8a0f9cd 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts @@ -52,6 +52,9 @@ const hierarchicalTreeItemBehavior: Accessibility = props keyCombinations: [{ keyCode: keyboardKey.ArrowLeft }], }, }), + expandSiblings: { + keyCombinations: [{ keyCode: keyboardKey['*'] }], + }, }, }, }) From 8a15e9a8fc88a71cfd0f8a94d38246a8f66fb1ec Mon Sep 17 00:00:00 2001 From: silviuavram Date: Fri, 9 Aug 2019 11:52:37 +0200 Subject: [PATCH 05/47] add styles --- .../themes/teams-dark/componentVariables.ts | 1 + .../components/Tree/treeTitleVariables.ts | 7 ++++++ .../teams-high-contrast/componentVariables.ts | 1 + .../components/Tree/treeTitleVariables.ts | 7 ++++++ .../react/src/themes/teams/componentStyles.ts | 18 ++++++++------ .../src/themes/teams/componentVariables.ts | 8 ++++--- .../teams/components/Tree/treeItemStyles.ts | 24 +++++++++++++++++++ .../teams/components/Tree/treeStyles.ts | 11 +++++++++ .../teams/components/Tree/treeTitleStyles.ts | 18 ++++++++++++++ .../components/Tree/treeTitleVariables.ts | 9 +++++++ 10 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 packages/react/src/themes/teams-dark/components/Tree/treeTitleVariables.ts create mode 100644 packages/react/src/themes/teams-high-contrast/components/Tree/treeTitleVariables.ts create mode 100644 packages/react/src/themes/teams/components/Tree/treeItemStyles.ts create mode 100644 packages/react/src/themes/teams/components/Tree/treeStyles.ts create mode 100644 packages/react/src/themes/teams/components/Tree/treeTitleStyles.ts create mode 100644 packages/react/src/themes/teams/components/Tree/treeTitleVariables.ts diff --git a/packages/react/src/themes/teams-dark/componentVariables.ts b/packages/react/src/themes/teams-dark/componentVariables.ts index ac395a90e6..3bf298fe2e 100644 --- a/packages/react/src/themes/teams-dark/componentVariables.ts +++ b/packages/react/src/themes/teams-dark/componentVariables.ts @@ -22,3 +22,4 @@ export { default as ProviderBox } from './components/Provider/providerBoxVariabl export { default as Dropdown } from './components/Dropdown/dropdownVariables' export { default as Label } from './components/Label/labelVariables' export { default as TooltipContent } from './components/Tooltip/tooltipContentVariables' +export { default as TreeTitle } from './components/Tree/treeTitleVariables' diff --git a/packages/react/src/themes/teams-dark/components/Tree/treeTitleVariables.ts b/packages/react/src/themes/teams-dark/components/Tree/treeTitleVariables.ts new file mode 100644 index 0000000000..fe899e68ec --- /dev/null +++ b/packages/react/src/themes/teams-dark/components/Tree/treeTitleVariables.ts @@ -0,0 +1,7 @@ +import { HierarchicalTreeTitleVariables } from '../../../teams/components/HierarchicalTree/hierarchicalTreeTitleVariables' + +export default (siteVars: any): HierarchicalTreeTitleVariables => { + return { + defaultColor: siteVars.colors.white, + } +} diff --git a/packages/react/src/themes/teams-high-contrast/componentVariables.ts b/packages/react/src/themes/teams-high-contrast/componentVariables.ts index 051fa5df33..91718762af 100644 --- a/packages/react/src/themes/teams-high-contrast/componentVariables.ts +++ b/packages/react/src/themes/teams-high-contrast/componentVariables.ts @@ -24,3 +24,4 @@ export { default as ProviderBox } from './components/Provider/providerBoxVariabl export { default as Dropdown } from './components/Dropdown/dropdownVariables' export { default as Label } from './components/Label/labelVariables' export { default as TooltipContent } from './components/Tooltip/tooltipContentVariables' +export { default as TreeTitle } from './components/Tree/treeTitleVariables' diff --git a/packages/react/src/themes/teams-high-contrast/components/Tree/treeTitleVariables.ts b/packages/react/src/themes/teams-high-contrast/components/Tree/treeTitleVariables.ts new file mode 100644 index 0000000000..fe899e68ec --- /dev/null +++ b/packages/react/src/themes/teams-high-contrast/components/Tree/treeTitleVariables.ts @@ -0,0 +1,7 @@ +import { HierarchicalTreeTitleVariables } from '../../../teams/components/HierarchicalTree/hierarchicalTreeTitleVariables' + +export default (siteVars: any): HierarchicalTreeTitleVariables => { + return { + defaultColor: siteVars.colors.white, + } +} diff --git a/packages/react/src/themes/teams/componentStyles.ts b/packages/react/src/themes/teams/componentStyles.ts index 05bd31bf0d..9bdf9a8d93 100644 --- a/packages/react/src/themes/teams/componentStyles.ts +++ b/packages/react/src/themes/teams/componentStyles.ts @@ -36,6 +36,14 @@ export { default as Grid } from './components/Grid/gridStyles' export { default as Header } from './components/Header/headerStyles' export { default as HeaderDescription } from './components/Header/headerDescriptionStyles' +export { default as HierarchicalTree } from './components/HierarchicalTree/hierarchicalTreeStyles' +export { + default as HierarchicalTreeItem, +} from './components/HierarchicalTree/hierarchicalTreeItemStyles' +export { + default as HierarchicalTreeTitle, +} from './components/HierarchicalTree/hierarchicalTreeTitleStyles' + export { default as Icon } from './components/Icon/iconStyles' export { default as Label } from './components/Label/labelStyles' @@ -80,13 +88,9 @@ export { default as ToolbarMenu } from './components/Toolbar/toolbarMenuStyles' export { default as ToolbarMenuDivider } from './components/Toolbar/toolbarMenuDividerStyles' export { default as ToolbarMenuItem } from './components/Toolbar/toolbarMenuItemStyles' -export { default as HierarchicalTree } from './components/HierarchicalTree/hierarchicalTreeStyles' -export { - default as HierarchicalTreeItem, -} from './components/HierarchicalTree/hierarchicalTreeItemStyles' -export { - default as HierarchicalTreeTitle, -} from './components/HierarchicalTree/hierarchicalTreeTitleStyles' +export { default as Tree } from './components/Tree/treeStyles' +export { default as TreeItem } from './components/Tree/treeItemStyles' +export { default as TreeTitle } from './components/Tree/treeTitleStyles' export { default as Animation } from './components/Animation/animationStyles' diff --git a/packages/react/src/themes/teams/componentVariables.ts b/packages/react/src/themes/teams/componentVariables.ts index e8512b1e4d..9e9eab9f4c 100644 --- a/packages/react/src/themes/teams/componentVariables.ts +++ b/packages/react/src/themes/teams/componentVariables.ts @@ -31,6 +31,10 @@ export { default as Embed } from './components/Embed/embedVariables' export { default as Header } from './components/Header/headerVariables' export { default as HeaderDescription } from './components/Header/headerDescriptionVariables' +export { + default as HierarchicalTreeTitle, +} from './components/HierarchicalTree/hierarchicalTreeTitleVariables' + export { default as Icon } from './components/Icon/iconVariables' export { default as Input } from './components/Input/inputVariables' @@ -75,9 +79,7 @@ export { default as ToolbarMenu } from './components/Toolbar/toolbarMenuVariable export { default as ToolbarMenuDivider } from './components/Toolbar/toolbarMenuDividerVariables' export { default as ToolbarMenuItem } from './components/Toolbar/toolbarMenuItemVariables' -export { - default as HierarchicalTreeTitle, -} from './components/HierarchicalTree/hierarchicalTreeTitleVariables' +export { default as TreeTitle } from './components/Tree/treeTitleVariables' export { default as Animation } from './components/Animation/animationVariables' diff --git a/packages/react/src/themes/teams/components/Tree/treeItemStyles.ts b/packages/react/src/themes/teams/components/Tree/treeItemStyles.ts new file mode 100644 index 0000000000..5d6c46eecf --- /dev/null +++ b/packages/react/src/themes/teams/components/Tree/treeItemStyles.ts @@ -0,0 +1,24 @@ +import { ComponentSlotStylesInput, ICSSInJSStyle } from '../../../types' +import { pxToRem } from '../../../../lib' +import getBorderFocusStyles from '../../getBorderFocusStyles' +import { TreeItemProps } from '../../../../components/Tree/TreeItem' +import TreeTitle from '../../../../components/Tree/TreeTitle' + +const treeItemStyles: ComponentSlotStylesInput = { + root: ({ theme: { siteVariables }, props: { level } }): ICSSInJSStyle => ({ + listStyleType: 'none', + padding: `0 0 0 ${pxToRem(1 + (level - 1) * 10)}`, + ':focus': { + outline: 0, + [`> .${TreeTitle.className}`]: { + position: 'relative', + ...getBorderFocusStyles({ + siteVariables, + isFromKeyboard: true, + })[':focus'], + }, + }, + }), +} + +export default treeItemStyles diff --git a/packages/react/src/themes/teams/components/Tree/treeStyles.ts b/packages/react/src/themes/teams/components/Tree/treeStyles.ts new file mode 100644 index 0000000000..713956a787 --- /dev/null +++ b/packages/react/src/themes/teams/components/Tree/treeStyles.ts @@ -0,0 +1,11 @@ +import { ICSSInJSStyle } from '../../../types' +import { pxToRem } from '../../../../lib' + +const treeStyles = { + root: (): ICSSInJSStyle => ({ + display: 'block', + paddingLeft: `${pxToRem(10)}`, + }), +} + +export default treeStyles diff --git a/packages/react/src/themes/teams/components/Tree/treeTitleStyles.ts b/packages/react/src/themes/teams/components/Tree/treeTitleStyles.ts new file mode 100644 index 0000000000..76d34b34df --- /dev/null +++ b/packages/react/src/themes/teams/components/Tree/treeTitleStyles.ts @@ -0,0 +1,18 @@ +import { ICSSInJSStyle } from '../../../types' +import { pxToRem } from '../../../../lib' +import getBorderFocusStyles from '../../getBorderFocusStyles' + +const treeTitleStyles = { + root: ({ variables, theme: { siteVariables } }): ICSSInJSStyle => ({ + padding: `${pxToRem(1)} 0`, + cursor: 'pointer', + color: variables.defaultColor, + position: 'relative', + ...getBorderFocusStyles({ + siteVariables, + isFromKeyboard: true, + }), + }), +} + +export default treeTitleStyles diff --git a/packages/react/src/themes/teams/components/Tree/treeTitleVariables.ts b/packages/react/src/themes/teams/components/Tree/treeTitleVariables.ts new file mode 100644 index 0000000000..7cfd33c6b3 --- /dev/null +++ b/packages/react/src/themes/teams/components/Tree/treeTitleVariables.ts @@ -0,0 +1,9 @@ +export interface TreeTitleVariables { + defaultColor: string +} + +export default (siteVars: any): TreeTitleVariables => { + return { + defaultColor: siteVars.colors.grey[750], + } +} From 7338e3598939cbf624fb7c154ef6c3450c5d0671 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Fri, 9 Aug 2019 11:53:37 +0200 Subject: [PATCH 06/47] call onActiveItemsChange after state change --- packages/react/src/components/Tree/Tree.tsx | 49 ++++++++++++--------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index fb7005578e..11b7510bb4 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -61,7 +61,7 @@ export interface TreeProps extends UIComponentProps, ChildrenComponentProps { * @param {SyntheticEvent} event - React's original SyntheticEvent. * @param {object} data - All props and proposed value. */ - onActiveIndexChange?: ComponentEventHandler + onActiveItemsChange?: ComponentEventHandler } export interface TreeState { @@ -131,7 +131,7 @@ class Tree extends AutoControlledComponent, TreeState> { } } - handleTitleOpen = (treeItemProps: TreeItemProps) => { + handleTitleOpen = (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { const { activeItems } = this.state const { indexInTree } = treeItemProps const subItems = activeItems[indexInTree]['items'] @@ -139,16 +139,18 @@ class Tree extends AutoControlledComponent, TreeState> { if (!subItems) { return } + const newActiveItems = [ + ...activeItems.slice(0, indexInTree + 1), + ...subItems, + ...activeItems.slice(indexInTree + 1), + ] this.trySetState({ - activeItems: [ - ...activeItems.slice(0, indexInTree + 1), - ...subItems, - ...activeItems.slice(indexInTree + 1), - ], + activeItems: newActiveItems, }) + _.invoke(this.props, 'onActiveItemsChange', e, { ...this.props, newActiveItems }) } - handleTitleClose = (treeItemProps: TreeItemProps) => { + handleTitleClose = (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { const { indexInTree, siblings, indexInSubtree, parent } = treeItemProps const { activeItems } = this.state const nextSibling = siblings[indexInSubtree + 1] @@ -157,26 +159,31 @@ class Tree extends AutoControlledComponent, TreeState> { const nextParentSibling = parent ? parent['siblings'][parent['indexInSubtree'] + 1] : null if (nextParentSibling) { const nextParentSiblingIndexInTree = activeItems.indexOf(nextParentSibling) - + const newActiveItems = [ + ...activeItems.slice(0, indexInTree + 1), + ...activeItems.slice(nextParentSiblingIndexInTree), + ] this.trySetState({ - activeItems: [ - ...activeItems.slice(0, indexInTree + 1), - ...activeItems.slice(nextParentSiblingIndexInTree), - ], + activeItems: newActiveItems, }) + _.invoke(this.props, 'onActiveItemsChange', e, { ...this.props, newActiveItems }) } else { + const newActiveItems = activeItems.slice(0, indexInTree + 1) this.trySetState({ - activeItems: activeItems.slice(0, indexInTree + 1), + activeItems: newActiveItems, }) + _.invoke(this.props, 'onActiveItemsChange', e, { ...this.props, newActiveItems }) } } else { const nextSiblingIndexInTree = activeItems.indexOf(nextSibling) + const newActiveItems = [ + ...activeItems.slice(0, indexInTree + 1), + ...activeItems.slice(nextSiblingIndexInTree), + ] this.trySetState({ - activeItems: [ - ...activeItems.slice(0, indexInTree + 1), - ...activeItems.slice(nextSiblingIndexInTree), - ], + activeItems: newActiveItems, }) + _.invoke(this.props, 'onActiveItemsChange', e, { ...this.props, newActiveItems }) } } @@ -188,9 +195,9 @@ class Tree extends AutoControlledComponent, TreeState> { ) => { const { open } = treeItemProps if (open) { - this.handleTitleClose(treeItemProps) + this.handleTitleClose(e, treeItemProps) } else { - this.handleTitleOpen(treeItemProps) + this.handleTitleOpen(e, treeItemProps) } _.invoke(predefinedProps, 'onTitleClick', e, treeItemProps) }, @@ -252,6 +259,8 @@ class Tree extends AutoControlledComponent, TreeState> { ) element.focus() + + _.invoke(this.props, 'onActiveItemsChange', e, { ...this.props, activeItems }) }) }, }) From 6f009f495212605efbd1d1880ffee7e15e0debdd Mon Sep 17 00:00:00 2001 From: silviuavram Date: Fri, 9 Aug 2019 12:12:46 +0200 Subject: [PATCH 07/47] remove react-virtualized --- packages/react/package.json | 3 +-- yarn.lock | 32 +------------------------------- 2 files changed, 2 insertions(+), 33 deletions(-) diff --git a/packages/react/package.json b/packages/react/package.json index eab43b648b..e6c0ef991b 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -24,8 +24,7 @@ "lodash": "^4.17.11", "popper.js": "^1.15.0", "prop-types": "^15.6.1", - "react-is": "^16.6.3", - "react-virtualized": "^9.21.1" + "react-is": "^16.6.3" }, "devDependencies": { "@stardust-ui/internal-tooling": "^0.36.0", diff --git a/yarn.lock b/yarn.lock index e1adccada0..1ec1cbefa0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3746,11 +3746,6 @@ cloneable-readable@^1.0.0: process-nextick-args "^2.0.0" readable-stream "^2.3.5" -clsx@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.0.4.tgz#0c0171f6d5cb2fe83848463c15fcc26b4df8c2ec" - integrity sha512-1mQ557MIZTrL/140j+JVdRM6e31/OA4vTYxXgqIIZlndyfjHpyawKZia1Im05Vp9BWmImkcNrNtFYQMyFcgJDg== - cmd-shim@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-2.0.2.tgz#6fcbda99483a8fd15d7d30a196ca69d688a2efdb" @@ -4927,13 +4922,6 @@ dom-css@^2.0.0: prefix-style "2.0.1" to-camel-case "1.0.0" -"dom-helpers@^2.4.0 || ^3.0.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" - integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA== - dependencies: - "@babel/runtime" "^7.1.2" - dom-serializer@0, dom-serializer@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" @@ -9104,11 +9092,6 @@ liftoff@^3.1.0: rechoir "^0.6.2" resolve "^1.1.7" -linear-layout-vector@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/linear-layout-vector/-/linear-layout-vector-0.0.1.tgz#398114d7303b6ecc7fd6b273af7b8401d8ba9c70" - integrity sha1-OYEU1zA7bsx/1rJzr3uEAdi6nHA= - lint-staged@^7.0.2: version "7.2.0" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-7.2.0.tgz#bdf4bb7f2f37fe689acfaec9999db288a5b26888" @@ -9576,7 +9559,7 @@ longest-streak@^1.0.0: resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-1.0.0.tgz#d06597c4d4c31b52ccb1f5d8f8fe7148eafd6965" integrity sha1-0GWXxNTDG1LMsfXY+P5xSOr9aWU= -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.0, loose-envify@^1.3.1, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -12047,19 +12030,6 @@ react-test-renderer@^16.0.0-0, react-test-renderer@^16.8.5: react-is "^16.8.6" scheduler "^0.13.6" -react-virtualized@^9.21.1: - version "9.21.1" - resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.21.1.tgz#4dbbf8f0a1420e2de3abf28fbb77120815277b3a" - integrity sha512-E53vFjRRMCyUTEKuDLuGH1ld/9TFzjf/fFW816PE4HFXWZorESbSTYtiZz1oAjra0MminaUU1EnvUxoGuEFFPA== - dependencies: - babel-runtime "^6.26.0" - clsx "^1.0.1" - dom-helpers "^2.4.0 || ^3.0.0" - linear-layout-vector "0.0.1" - loose-envify "^1.3.0" - prop-types "^15.6.0" - react-lifecycles-compat "^3.0.4" - react-vis@^1.11.6: version "1.11.6" resolved "https://registry.yarnpkg.com/react-vis/-/react-vis-1.11.6.tgz#4616968ac6dfbd95491d778e70ad26956fd2fdab" From 752647eb2539af525b0642fc38d8dd74adf86787 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Fri, 9 Aug 2019 12:17:43 +0200 Subject: [PATCH 08/47] fixed posinset in title behavior --- .../src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts index f05f6cd8f0..e60ed4fe22 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts @@ -18,7 +18,7 @@ const hierarchicalTreeTitleBehavior: Accessibility = props => [IS_FOCUSABLE_ATTRIBUTE]: true, role: 'treeitem', 'aria-setsize': props.siblingsLength, - 'aria-posinset': props.indexInSubtree, + 'aria-posinset': props.indexInSubtree + 1, 'aria-level': props.level, }), }, From 3b81e15dea25dbce8e7d01b72a01c6346cafd9c5 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Fri, 9 Aug 2019 12:29:19 +0200 Subject: [PATCH 09/47] fix prop name in onActiveItemsChange call --- packages/react/src/components/Tree/Tree.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 11b7510bb4..ed5a407d7d 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -147,7 +147,7 @@ class Tree extends AutoControlledComponent, TreeState> { this.trySetState({ activeItems: newActiveItems, }) - _.invoke(this.props, 'onActiveItemsChange', e, { ...this.props, newActiveItems }) + _.invoke(this.props, 'onActiveItemsChange', e, { ...this.props, activeItems: newActiveItems }) } handleTitleClose = (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { @@ -166,13 +166,19 @@ class Tree extends AutoControlledComponent, TreeState> { this.trySetState({ activeItems: newActiveItems, }) - _.invoke(this.props, 'onActiveItemsChange', e, { ...this.props, newActiveItems }) + _.invoke(this.props, 'onActiveItemsChange', e, { + ...this.props, + activeItems: newActiveItems, + }) } else { const newActiveItems = activeItems.slice(0, indexInTree + 1) this.trySetState({ activeItems: newActiveItems, }) - _.invoke(this.props, 'onActiveItemsChange', e, { ...this.props, newActiveItems }) + _.invoke(this.props, 'onActiveItemsChange', e, { + ...this.props, + activeItems: newActiveItems, + }) } } else { const nextSiblingIndexInTree = activeItems.indexOf(nextSibling) @@ -183,7 +189,7 @@ class Tree extends AutoControlledComponent, TreeState> { this.trySetState({ activeItems: newActiveItems, }) - _.invoke(this.props, 'onActiveItemsChange', e, { ...this.props, newActiveItems }) + _.invoke(this.props, 'onActiveItemsChange', e, { ...this.props, activeItems: newActiveItems }) } } From d7311c01547a9c1cedba4e1ddb7b738b43e0d32e Mon Sep 17 00:00:00 2001 From: silviuavram Date: Fri, 9 Aug 2019 14:38:19 +0200 Subject: [PATCH 10/47] behavior renames --- .../src/lib/accessibility/Behaviors/Tree/treeBehavior.ts | 4 ++-- .../lib/accessibility/Behaviors/Tree/treeItemBehavior.ts | 4 ++-- .../lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts | 4 ++-- packages/react/test/specs/behaviors/behavior-test.tsx | 6 ++++++ 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeBehavior.ts index 2a45e0493b..320dcd1f94 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeBehavior.ts @@ -10,7 +10,7 @@ import { FocusZoneDirection } from '../../FocusZone' * Provides arrow key navigation in vertical direction. * Triggers 'expandSiblings' action with '*' on 'root'. */ -const hierarchicalTreeBehavior: Accessibility = props => { +const treeBehavior: Accessibility = props => { return { attributes: { root: { @@ -36,4 +36,4 @@ const hierarchicalTreeBehavior: Accessibility = props => { type TreeBehaviorProps = {} & Pick -export default hierarchicalTreeBehavior +export default treeBehavior diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts index ebf8a0f9cd..459e0b8af5 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts @@ -15,7 +15,7 @@ import { IS_FOCUSABLE_ATTRIBUTE } from '../../FocusZone/focusUtilities' * Triggers 'expand' action with 'ArrowRight' on 'root', when has a closed subtree. * Triggers 'focusSubtree' action with 'ArrowRight' on 'root', when has an opened subtree. */ -const hierarchicalTreeItemBehavior: Accessibility = props => ({ +const treeItemBehavior: Accessibility = props => ({ attributes: { root: { role: 'none', @@ -75,4 +75,4 @@ const isSubtreeOpen = (props: TreeItemBehaviorProps): boolean => { return !!(items && items.length && open) } -export default hierarchicalTreeItemBehavior +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 e60ed4fe22..c8cac792d0 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts @@ -10,7 +10,7 @@ import { IS_FOCUSABLE_ATTRIBUTE } from '../../FocusZone/focusUtilities' * @specification * Triggers 'performClick' action with 'Enter' or 'Spacebar' on 'root'. */ -const hierarchicalTreeTitleBehavior: Accessibility = props => ({ +const treeTitleBehavior: Accessibility = props => ({ attributes: { root: { ...(!props.hasSubtree && { @@ -32,7 +32,7 @@ const hierarchicalTreeTitleBehavior: Accessibility = props => }, }) -export default hierarchicalTreeTitleBehavior +export default treeTitleBehavior type TreeTitleBehavior = { /** Indicated if tree title has a subtree */ diff --git a/packages/react/test/specs/behaviors/behavior-test.tsx b/packages/react/test/specs/behaviors/behavior-test.tsx index 2b09c94c14..3edfaa1700 100644 --- a/packages/react/test/specs/behaviors/behavior-test.tsx +++ b/packages/react/test/specs/behaviors/behavior-test.tsx @@ -51,6 +51,9 @@ import { tooltipBehavior, tooltipAsLabelBehavior, menuButtonBehavior, + treeBehavior, + treeItemBehavior, + treeTitleBehavior, } from 'src/lib/accessibility' import { TestHelper } from './testHelper' import definitions from './testDefinitions' @@ -106,5 +109,8 @@ testHelper.addBehavior('toolbarRadioGroupBehavior', toolbarRadioGroupBehavior) testHelper.addBehavior('toolbarRadioGroupItemBehavior', toolbarRadioGroupItemBehavior) testHelper.addBehavior('tooltipBehavior', tooltipBehavior) testHelper.addBehavior('tooltipAsLabelBehavior', tooltipAsLabelBehavior) +testHelper.addBehavior('treeBehavior', treeBehavior) +testHelper.addBehavior('treeItemBehavior', treeItemBehavior) +testHelper.addBehavior('treeTitleBehavior', treeTitleBehavior) testHelper.run(behaviorMenuItems) From 1b11bd4c0be34c94d7d1d9bd15454bbf25d9a6d0 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Mon, 12 Aug 2019 15:22:16 +0200 Subject: [PATCH 11/47] refactor tree state --- packages/react/src/components/Tree/Tree.tsx | 232 ++++++------------ .../react/src/components/Tree/TreeItem.tsx | 40 ++- .../react/src/components/Tree/TreeTitle.tsx | 7 +- .../Behaviors/Tree/treeItemBehavior.ts | 4 +- .../Behaviors/Tree/treeTitleBehavior.ts | 4 +- 5 files changed, 97 insertions(+), 190 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index ed5a407d7d..a466e69b43 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -2,7 +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 { handleRef, Ref } from '@stardust-ui/react-component-ref' +import { Ref } from '@stardust-ui/react-component-ref' import TreeItem, { TreeItemProps } from './TreeItem' import { @@ -20,7 +20,6 @@ import { WithAsProp, withSafeTypeForAs, ShorthandCollection, - ComponentEventHandler, ShorthandValue, } from '../../types' import { Accessibility } from '../../lib/accessibility/types' @@ -32,14 +31,11 @@ export interface TreeSlotClassNames { } export interface TreeProps extends UIComponentProps, ChildrenComponentProps { - /** Index of the currently active subtree. */ - activeItems?: ShorthandCollection - /** Accessibility behavior if overridden by the user. */ accessibility?: Accessibility /** Initial activeIndex value. */ - defaultActiveItems?: ShorthandCollection + defaultActiveItemsList?: ShorthandCollection /** Only allow one subtree to be open at a time. */ exclusive?: boolean @@ -55,17 +51,10 @@ export interface TreeProps extends UIComponentProps, ChildrenComponentProps { * @param {ReactNode|ReactNodeArray} children - The computed children for this slot. */ renderItemTitle?: ShorthandRenderFunction - - /** Called when activeIndex changes. - * - * @param {SyntheticEvent} event - React's original SyntheticEvent. - * @param {object} data - All props and proposed value. - */ - onActiveItemsChange?: ComponentEventHandler } export interface TreeState { - activeItems: ShorthandCollection + activeItems: Map, { open?: boolean; element: HTMLElement }> } class Tree extends AutoControlledComponent, TreeState> { @@ -83,11 +72,11 @@ class Tree extends AutoControlledComponent, TreeState> { ...commonPropTypes.createCommon({ content: false, }), - activeItems: customPropTypes.every([ + activeItemsList: customPropTypes.every([ customPropTypes.disallow(['children']), PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.number]), ]), - defaultActiveItems: customPropTypes.every([ + defaultActiveItemsList: customPropTypes.every([ customPropTypes.disallow(['children']), PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.number]), ]), @@ -95,7 +84,7 @@ class Tree extends AutoControlledComponent, TreeState> { items: customPropTypes.collectionShorthand, renderItemTitle: PropTypes.func, rtlAttributes: PropTypes.func, - onActiveItemsChange: PropTypes.func, + onactiveItemsListChange: PropTypes.func, } static defaultProps = { @@ -103,23 +92,27 @@ class Tree extends AutoControlledComponent, TreeState> { accessibility: treeBehavior, } - static autoControlledProps = ['activeItems'] + static autoControlledProps = ['activeItemsList'] itemRefs = [] getInitialAutoControlledState(): TreeState { + const activeItems = new Map() if (this.props.items) { const setItemsLevelAndSize = (items = this.props.items, level = 1, parent?) => { items.forEach((item: ShorthandValue, index: number) => { item['level'] = level item['siblings'] = items - item['indexInSubtree'] = index + item['index'] = index if (parent) { item['parent'] = parent } - + if (!item['id']) { + item['id'] = `${parent ? parent['id'] : ''}${index}` + } if (item['items']) { setItemsLevelAndSize(item['items'], level + 1, item) + activeItems.set(item['id'], { open: false }) } }) } @@ -127,69 +120,7 @@ class Tree extends AutoControlledComponent, TreeState> { } return { - activeItems: this.props.items || [], - } - } - - handleTitleOpen = (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { - const { activeItems } = this.state - const { indexInTree } = treeItemProps - const subItems = activeItems[indexInTree]['items'] - - if (!subItems) { - return - } - const newActiveItems = [ - ...activeItems.slice(0, indexInTree + 1), - ...subItems, - ...activeItems.slice(indexInTree + 1), - ] - this.trySetState({ - activeItems: newActiveItems, - }) - _.invoke(this.props, 'onActiveItemsChange', e, { ...this.props, activeItems: newActiveItems }) - } - - handleTitleClose = (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { - const { indexInTree, siblings, indexInSubtree, parent } = treeItemProps - const { activeItems } = this.state - const nextSibling = siblings[indexInSubtree + 1] - - if (!nextSibling) { - const nextParentSibling = parent ? parent['siblings'][parent['indexInSubtree'] + 1] : null - if (nextParentSibling) { - const nextParentSiblingIndexInTree = activeItems.indexOf(nextParentSibling) - const newActiveItems = [ - ...activeItems.slice(0, indexInTree + 1), - ...activeItems.slice(nextParentSiblingIndexInTree), - ] - this.trySetState({ - activeItems: newActiveItems, - }) - _.invoke(this.props, 'onActiveItemsChange', e, { - ...this.props, - activeItems: newActiveItems, - }) - } else { - const newActiveItems = activeItems.slice(0, indexInTree + 1) - this.trySetState({ - activeItems: newActiveItems, - }) - _.invoke(this.props, 'onActiveItemsChange', e, { - ...this.props, - activeItems: newActiveItems, - }) - } - } else { - const nextSiblingIndexInTree = activeItems.indexOf(nextSibling) - const newActiveItems = [ - ...activeItems.slice(0, indexInTree + 1), - ...activeItems.slice(nextSiblingIndexInTree), - ] - this.trySetState({ - activeItems: newActiveItems, - }) - _.invoke(this.props, 'onActiveItemsChange', e, { ...this.props, activeItems: newActiveItems }) + activeItems, } } @@ -199,112 +130,101 @@ class Tree extends AutoControlledComponent, TreeState> { treeItemProps: TreeItemProps, predefinedProps: TreeItemProps, ) => { - const { open } = treeItemProps - if (open) { - this.handleTitleClose(e, treeItemProps) - } else { - this.handleTitleOpen(e, treeItemProps) - } + const { activeItems } = this.state + const itemId = treeItemProps['id'] + const activeItemValue = activeItems.get(itemId) + + activeItems.set(itemId, { ...activeItemValue, open: !activeItemValue.open }) + this.trySetState({ + activeItems, + }) + _.invoke(predefinedProps, 'onTitleClick', e, treeItemProps) }, - onParentFocus: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { - const { indexInTree } = treeItemProps - const { activeItems } = this.state - const parentItem = activeItems[indexInTree]['parent'] + onFocusParent: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { + const { parent } = treeItemProps - if (parentItem) { - const parentItemIndex = activeItems.indexOf(parentItem) - this.itemRefs[parentItemIndex].current.focus() + if (!parent) { + return } - _.invoke(predefinedProps, 'onParentFocus', e, treeItemProps) + const { activeItems } = this.state + + activeItems.get(parent['id']).element.focus() + + _.invoke(predefinedProps, 'onFocusParent', e, treeItemProps) }, - onFirstChildFocus: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { - const { indexInTree } = treeItemProps + onFocusFirstChild: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { + const { items } = treeItemProps + + if (!items) { + return + } + const { activeItems } = this.state + const firstChildElement = activeItems.get(items[0]['id']).element - if (activeItems[indexInTree]['items']) { - const element = getFirstFocusable( - this.itemRefs[indexInTree + 1].current, - this.itemRefs[indexInTree + 1].current, - true, - ) - if (element) { - element.focus() - } + const element = getFirstFocusable(firstChildElement, firstChildElement, true) + if (element) { + element.focus() } - _.invoke(predefinedProps, 'onFirstChildFocus', e, treeItemProps) + _.invoke(predefinedProps, 'onFocusFirstChild', e, treeItemProps) }, onSiblingsExpand: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { - const { siblings, indexInSubtree } = treeItemProps - let { activeItems } = this.state - - siblings.forEach((sibling: ShorthandValue) => { - const siblingIndexInTree = activeItems.indexOf(sibling) - const isSubtree = !!sibling['items'] - - if (isSubtree) { - const isSubtreeOpen = activeItems[siblingIndexInTree + 1] === sibling['items'][0] - if (!isSubtreeOpen) { - activeItems = [ - ...activeItems.slice(0, siblingIndexInTree + 1), - ...activeItems[siblingIndexInTree]['items'], - ...activeItems.slice(siblingIndexInTree + 1), - ] - } - } - }) - - this.trySetState({ activeItems }, () => { - const indexInTree = activeItems.indexOf(siblings[indexInSubtree]) - const element = getFirstFocusable( - this.itemRefs[indexInTree].current, - this.itemRefs[indexInTree].current, - true, - ) + const { siblings } = treeItemProps + const { activeItems } = this.state + const itemId = treeItemProps['id'] + const activeItemValue = activeItems.get(itemId) - element.focus() + siblings.forEach(sibling => { + activeItems.set(sibling['id'], { ...activeItemValue, open: true }) + }) - _.invoke(this.props, 'onActiveItemsChange', e, { ...this.props, activeItems }) + this.trySetState({ + activeItems, }) + // todo onSiblignsExpand event }, }) - renderContent() { + renderItems(items = this.props.items) { const { activeItems } = this.state - return _.map(activeItems, (item: ShorthandValue, index: number) => { - const isSubtree = !!item['items'] - const isSubtreeOpen = isSubtree && activeItems[index + 1] === item['items'][0] - - return ( + return items.reduce((renderedItems, item) => { + const isSubtreeOpen = item['items'] && activeItems.get(item['id']).open + const renderedItem = ( { - if ( - !itemElement || - (this.itemRefs.length && - this.itemRefs[this.itemRefs.length - 1].current === itemElement) - ) { - return - } - - const ref = React.createRef() - this.itemRefs.push(ref) - handleRef(ref, itemElement) + const activeItemValue = activeItems.get(item['id']) + + activeItems.set(item['id'], { ...activeItemValue, element: itemElement }) }} > {TreeItem.create(item, { defaultProps: { className: Tree.slotClassNames.item, open: isSubtreeOpen, - indexInTree: index, }, overrideProps: this.handleTreeItemOverrides, })} ) - }) + + return [ + ...(renderedItems as any[]), + renderedItem, + ...[isSubtreeOpen ? this.renderItems(item['items']) : []], + ] + }, []) + } + + renderContent() { + const { items } = this.props + if (!items) return null + + return this.renderItems() } renderComponent({ ElementType, classes, accessibility, unhandledProps, styles, variables }) { diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx index e6c364a25d..2bf00b6558 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -38,8 +38,10 @@ export interface TreeItemProps extends UIComponentProps, ChildrenComponentProps /** Only allow one subtree to be open at a time. */ exclusive?: boolean + id?: string + /** The index of the item among its sibbling */ - indexInTree?: number + index?: number /** Array of props for sub tree. */ items?: ShorthandCollection @@ -50,21 +52,19 @@ export interface TreeItemProps extends UIComponentProps, ChildrenComponentProps onTitleClick?: ComponentEventHandler /** Called when the item's first child is focused. */ - onFirstChildFocus?: ComponentEventHandler + onFocusFirstChild?: ComponentEventHandler /** Called when the item's first child is focused. */ onSiblingsExpand?: ComponentEventHandler /** Called when the item's parent is focused. */ - onParentFocus?: ComponentEventHandler + onFocusParent?: ComponentEventHandler /** Whether or not the subtree of the item is in the open state. */ open?: boolean parent?: ShorthandValue - indexInSubtree?: number - siblings?: ShorthandCollection /** @@ -97,17 +97,17 @@ class TreeItem extends UIComponent> { ...commonPropTypes.createCommon({ content: false, }), + id: PropTypes.string, items: customPropTypes.collectionShorthand, - indexInTree: PropTypes.number, + index: PropTypes.number, exclusive: PropTypes.bool, level: PropTypes.number, onTitleClick: PropTypes.func, - onFirstChildFocus: PropTypes.func, - onParentFocus: PropTypes.func, + onFocusFirstChild: PropTypes.func, + onFocusParent: PropTypes.func, onSiblingsExpand: PropTypes.func, open: PropTypes.bool, parent: customPropTypes.itemShorthand, - indexInSubtree: PropTypes.number, renderItemTitle: PropTypes.func, siblings: customPropTypes.collectionShorthand, treeItemRtlAttributes: PropTypes.func, @@ -151,7 +151,7 @@ class TreeItem extends UIComponent> { e.preventDefault() e.stopPropagation() - this.handleFirstChildFocus(e) + this.handleFocusFirstChild(e) }, expandSiblings: e => { e.preventDefault() @@ -170,11 +170,11 @@ class TreeItem extends UIComponent> { } handleParentFocus = e => { - _.invoke(this.props, 'onParentFocus', e, this.props) + _.invoke(this.props, 'onFocusParent', e, this.props) } - handleFirstChildFocus = e => { - _.invoke(this.props, 'onFirstChildFocus', e, this.props) + handleFocusFirstChild = e => { + _.invoke(this.props, 'onFocusFirstChild', e, this.props) } handleSiblingsExpand = e => { @@ -189,16 +189,7 @@ class TreeItem extends UIComponent> { }) renderContent() { - const { - items, - title, - renderItemTitle, - open, - level, - siblings, - indexInTree, - indexInSubtree, - } = this.props + const { items, title, renderItemTitle, open, level, siblings, index } = this.props const hasSubtree = !_.isNil(items) return TreeTitle.create(title, { @@ -209,8 +200,7 @@ class TreeItem extends UIComponent> { as: hasSubtree ? 'span' : 'a', level, siblingsLength: siblings.length, - indexInTree, - indexInSubtree, + index, }, render: renderItemTitle, overrideProps: this.handleTitleOverrides, diff --git a/packages/react/src/components/Tree/TreeTitle.tsx b/packages/react/src/components/Tree/TreeTitle.tsx index 0ecfa1625b..1f86898074 100644 --- a/packages/react/src/components/Tree/TreeTitle.tsx +++ b/packages/react/src/components/Tree/TreeTitle.tsx @@ -37,14 +37,12 @@ export interface TreeTitleProps /** Whether or not the subtree of the item is in the open state. */ open?: boolean - indexInSubtree?: number - /** Whether or not the item has a subtree. */ hasSubtree?: boolean siblingsLength?: number - indexInTree?: number + index?: number } class TreeTitle extends UIComponent> { @@ -61,8 +59,7 @@ class TreeTitle extends UIComponent> { open: PropTypes.bool, hasSubtree: PropTypes.bool, siblingsLength: PropTypes.number, - indexInSubtree: PropTypes.number, - indexInTree: PropTypes.number, + index: PropTypes.number, } static defaultProps = { diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts index 459e0b8af5..7ef3fd79c4 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts @@ -26,7 +26,7 @@ const treeItemBehavior: Accessibility = props => ({ [IS_FOCUSABLE_ATTRIBUTE]: true, role: 'treeitem', 'aria-setsize': props.siblings.length, - 'aria-posinset': props.indexInSubtree + 1, + 'aria-posinset': props.index + 1, 'aria-level': props.level, }), }, @@ -66,7 +66,7 @@ export type TreeItemBehaviorProps = { open?: boolean siblings?: object[] level?: number - indexInSubtree?: number + index?: number } /** Checks if current tree item has a subtree and it is opened */ diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts index c8cac792d0..1a7e4d1d19 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts @@ -18,7 +18,7 @@ const treeTitleBehavior: Accessibility = props => ({ [IS_FOCUSABLE_ATTRIBUTE]: true, role: 'treeitem', 'aria-setsize': props.siblingsLength, - 'aria-posinset': props.indexInSubtree + 1, + 'aria-posinset': props.index + 1, 'aria-level': props.level, }), }, @@ -41,5 +41,5 @@ type TreeTitleBehavior = { open?: boolean level?: number siblingsLength?: number - indexInSubtree?: number + index?: number } From 82066412f08674ae3bdd9385ca1e66ec25f479df Mon Sep 17 00:00:00 2001 From: silviuavram Date: Mon, 12 Aug 2019 15:38:00 +0200 Subject: [PATCH 12/47] removed autocontrolled for now --- packages/react/src/components/Tree/Tree.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index a466e69b43..114f28135b 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -6,7 +6,6 @@ import { Ref } from '@stardust-ui/react-component-ref' import TreeItem, { TreeItemProps } from './TreeItem' import { - AutoControlledComponent, childrenExist, commonPropTypes, createShorthandFactory, @@ -14,6 +13,7 @@ import { ChildrenComponentProps, rtlTextContainer, applyAccessibilityKeyHandlers, + UIComponent, } from '../../lib' import { ShorthandRenderFunction, @@ -54,10 +54,10 @@ export interface TreeProps extends UIComponentProps, ChildrenComponentProps { } export interface TreeState { - activeItems: Map, { open?: boolean; element: HTMLElement }> + activeItems: Map, { open: boolean; element: HTMLElement }> } -class Tree extends AutoControlledComponent, TreeState> { +class Tree extends UIComponent, TreeState> { static create: Function static displayName = 'Tree' @@ -96,7 +96,9 @@ class Tree extends AutoControlledComponent, TreeState> { itemRefs = [] - getInitialAutoControlledState(): TreeState { + constructor(props, context) { + super(props, context) + const activeItems = new Map() if (this.props.items) { const setItemsLevelAndSize = (items = this.props.items, level = 1, parent?) => { @@ -119,9 +121,7 @@ class Tree extends AutoControlledComponent, TreeState> { setItemsLevelAndSize() } - return { - activeItems, - } + this.state = { activeItems } } handleTreeItemOverrides = (predefinedProps: TreeItemProps) => ({ @@ -135,7 +135,7 @@ class Tree extends AutoControlledComponent, TreeState> { const activeItemValue = activeItems.get(itemId) activeItems.set(itemId, { ...activeItemValue, open: !activeItemValue.open }) - this.trySetState({ + this.setState({ activeItems, }) @@ -181,7 +181,7 @@ class Tree extends AutoControlledComponent, TreeState> { activeItems.set(sibling['id'], { ...activeItemValue, open: true }) }) - this.trySetState({ + this.setState({ activeItems, }) // todo onSiblignsExpand event From c948c33c0ab3a6e000bbafca7f080507c1ed0c89 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Mon, 12 Aug 2019 15:44:39 +0200 Subject: [PATCH 13/47] some minor code improvements --- packages/react/src/components/Tree/Tree.tsx | 7 ++++--- packages/react/src/components/Tree/TreeItem.tsx | 2 +- .../lib/accessibility/Behaviors/Tree/treeItemBehavior.ts | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 114f28135b..4f9b22bbf4 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -184,11 +184,12 @@ class Tree extends UIComponent, TreeState> { this.setState({ activeItems, }) - // todo onSiblignsExpand event + + _.invoke(predefinedProps, 'onSiblingsExpand', e, treeItemProps) }, }) - renderItems(items = this.props.items) { + renderItems(items: ShorthandCollection) { const { activeItems } = this.state return items.reduce((renderedItems, item) => { @@ -224,7 +225,7 @@ class Tree extends UIComponent, TreeState> { const { items } = this.props if (!items) return null - return this.renderItems() + return this.renderItems(items) } renderComponent({ ElementType, classes, accessibility, unhandledProps, styles, variables }) { diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx index 2bf00b6558..877194da2b 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -147,7 +147,7 @@ class TreeItem extends UIComponent> { this.handleTitleClick(e) }, - focusSubtree: e => { + focusFirstChild: e => { e.preventDefault() e.stopPropagation() diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts index 7ef3fd79c4..f1d86af6cc 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts @@ -40,7 +40,7 @@ const treeItemBehavior: Accessibility = props => ({ collapse: { keyCombinations: [{ keyCode: keyboardKey.ArrowLeft }], }, - focusSubtree: { + focusFirstChild: { keyCombinations: [{ keyCode: keyboardKey.ArrowRight }], }, }), From ed0abc93d5b59ef6866a769f8880e722bb19ff47 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Mon, 12 Aug 2019 16:52:17 +0200 Subject: [PATCH 14/47] fix behavior tests --- packages/react/src/components/Tree/Tree.tsx | 3 ++- .../src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts | 4 ++-- packages/react/test/specs/behaviors/testDefinitions.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 4f9b22bbf4..614db83bfc 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -100,7 +100,8 @@ class Tree extends UIComponent, TreeState> { super(props, context) const activeItems = new Map() - if (this.props.items) { + + if (props.items) { const setItemsLevelAndSize = (items = this.props.items, level = 1, parent?) => { items.forEach((item: ShorthandValue, index: number) => { item['level'] = level diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts index f1d86af6cc..ce421c6213 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts @@ -10,10 +10,10 @@ import { IS_FOCUSABLE_ATTRIBUTE } from '../../FocusZone/focusUtilities' * * @specification * Triggers 'performClick' action with 'Enter' or 'Spacebar' on 'root'. - * Triggers 'receiveFocus' action with 'ArrowLeft' on 'root', when has an opened subtree. + * Triggers 'focusParent' action with 'ArrowLeft' on 'root', when has a closed subtree. * Triggers 'collapse' action with 'ArrowLeft' on 'root', when has an opened subtree. * Triggers 'expand' action with 'ArrowRight' on 'root', when has a closed subtree. - * Triggers 'focusSubtree' action with 'ArrowRight' on 'root', when has an opened subtree. + * Triggers 'focusFirstChild' action with 'ArrowRight' on 'root', when has an opened subtree. */ const treeItemBehavior: Accessibility = props => ({ attributes: { diff --git a/packages/react/test/specs/behaviors/testDefinitions.ts b/packages/react/test/specs/behaviors/testDefinitions.ts index 5cee9da0e2..1a6a8999c7 100644 --- a/packages/react/test/specs/behaviors/testDefinitions.ts +++ b/packages/react/test/specs/behaviors/testDefinitions.ts @@ -558,7 +558,7 @@ definitions.push({ regexp: /Triggers '(\w+)' action with '(\w+)' on '([\w-]+)', when has an opened subtree\./g, testMethod: (parameters: TestMethod) => { const [action, key, elementToPerformAction] = [...parameters.props] - const propertyOpenedSubtree = { open: true, items: [{ a: 1 }] } + const propertyOpenedSubtree = { open: true, items: [{ a: 1 }], siblings: [] } const expectedKeyNumberVertical = parameters.behavior(propertyOpenedSubtree).keyActions[ elementToPerformAction ][action].keyCombinations[0].keyCode From 63e98e9a6511bf2ccc1fd3873b17cc3e75c89414 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Mon, 12 Aug 2019 17:45:26 +0200 Subject: [PATCH 15/47] renamed to fix tests --- packages/react/src/lib/accessibility/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react/src/lib/accessibility/index.ts b/packages/react/src/lib/accessibility/index.ts index 3f67d75bf8..366b3dc359 100644 --- a/packages/react/src/lib/accessibility/index.ts +++ b/packages/react/src/lib/accessibility/index.ts @@ -59,6 +59,6 @@ export { default as tooltipBehavior } from './Behaviors/Tooltip/tooltipBehavior' export { default as tooltipAsLabelBehavior } from './Behaviors/Tooltip/tooltipAsLabelBehavior' export { default as sliderBehavior } from './Behaviors/Slider/sliderBehavior' export { default as menuButtonBehavior } from './Behaviors/MenuButton/menuButtonBehavior' -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 treeBehavior } from './Behaviors/Tree/treeBehavior' +export { default as treeItemBehavior } from './Behaviors/Tree/treeItemBehavior' +export { default as treeTitleBehavior } from './Behaviors/Tree/treeTitleBehavior' From 405511263fef4b0d5cd15f67b88aeb90411a351b Mon Sep 17 00:00:00 2001 From: silviuavram Date: Tue, 13 Aug 2019 09:41:49 +0200 Subject: [PATCH 16/47] only keep in memory refs that are needed --- packages/react/src/components/Tree/Tree.tsx | 66 ++++++++++++--------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 614db83bfc..05d797cdd6 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -102,7 +102,7 @@ class Tree extends UIComponent, TreeState> { const activeItems = new Map() if (props.items) { - const setItemsLevelAndSize = (items = this.props.items, level = 1, parent?) => { + const setItemsLevelAndSize = (items = props.items, level = 1, parent?) => { items.forEach((item: ShorthandValue, index: number) => { item['level'] = level item['siblings'] = items @@ -132,13 +132,15 @@ class Tree extends UIComponent, TreeState> { predefinedProps: TreeItemProps, ) => { const { activeItems } = this.state - const itemId = treeItemProps['id'] + const itemId = treeItemProps.id const activeItemValue = activeItems.get(itemId) - activeItems.set(itemId, { ...activeItemValue, open: !activeItemValue.open }) - this.setState({ - activeItems, - }) + if (treeItemProps.items) { + activeItems.set(itemId, { ...activeItemValue, open: !activeItemValue.open }) + this.setState({ + activeItems, + }) + } _.invoke(predefinedProps, 'onTitleClick', e, treeItemProps) }, @@ -179,7 +181,9 @@ class Tree extends UIComponent, TreeState> { const activeItemValue = activeItems.get(itemId) siblings.forEach(sibling => { - activeItems.set(sibling['id'], { ...activeItemValue, open: true }) + if (sibling['items']) { + activeItems.set(sibling['id'], { ...activeItemValue, open: true }) + } }) this.setState({ @@ -193,30 +197,36 @@ class Tree extends UIComponent, TreeState> { renderItems(items: ShorthandCollection) { const { activeItems } = this.state - return items.reduce((renderedItems, item) => { - const isSubtreeOpen = item['items'] && activeItems.get(item['id']).open - const renderedItem = ( - { - const activeItemValue = activeItems.get(item['id']) - - activeItems.set(item['id'], { ...activeItemValue, element: itemElement }) - }} - > - {TreeItem.create(item, { - defaultProps: { - className: Tree.slotClassNames.item, - open: isSubtreeOpen, - }, - overrideProps: this.handleTreeItemOverrides, - })} - - ) + return items.reduce((renderedItems, item, index) => { + const isSubtree = !!item['items'] + const isFirstChild = item['parent'] && index === 0 + const isSubtreeOpen = isSubtree && activeItems.get(item['id']).open + const renderedItem = TreeItem.create(item, { + defaultProps: { + className: Tree.slotClassNames.item, + open: isSubtreeOpen, + }, + overrideProps: this.handleTreeItemOverrides, + }) + const renderedFinalItem = + isSubtree || isFirstChild ? ( + { + const activeItemValue = activeItems.get(item['id']) + + activeItems.set(item['id'], { ...activeItemValue, element: itemElement }) + }} + > + {renderedItem} + + ) : ( + renderedItem + ) return [ ...(renderedItems as any[]), - renderedItem, + renderedFinalItem, ...[isSubtreeOpen ? this.renderItems(item['items']) : []], ] }, []) From dc042576ad9be71fa7286eb6c1b58ecbf1b4d793 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Fri, 23 Aug 2019 17:10:07 +0200 Subject: [PATCH 17/47] remove mutations from items --- packages/react/src/components/Tree/Tree.tsx | 221 ++++++++++-------- .../react/src/components/Tree/TreeItem.tsx | 11 +- .../Behaviors/Tree/treeItemBehavior.ts | 6 +- 3 files changed, 127 insertions(+), 111 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 05d797cdd6..099cf1e82e 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -24,7 +24,7 @@ import { } from '../../types' import { Accessibility } from '../../lib/accessibility/types' import { treeBehavior } from '../../lib/accessibility' -import { getFirstFocusable } from '../../lib/accessibility/FocusZone/focusUtilities' +import { getNextElement } from '../../lib/accessibility/FocusZone/focusUtilities' export interface TreeSlotClassNames { item: string @@ -53,8 +53,21 @@ export interface TreeProps extends UIComponentProps, ChildrenComponentProps { renderItemTitle?: ShorthandRenderFunction } +export interface TreeItemForRenderProps { + item: ShorthandValue + items: TreeItemForRenderProps[] + level: number + index: number + id: string + parentId: string + siblingsLength: number +} + export interface TreeState { - activeItems: Map, { open: boolean; element: HTMLElement }> + activeItems: Map< + string, + { open: boolean; element?: HTMLElement; parentId?: string; siblingSubtreeIds?: string[] } + > } class Tree extends UIComponent, TreeState> { @@ -72,19 +85,10 @@ class Tree extends UIComponent, TreeState> { ...commonPropTypes.createCommon({ content: false, }), - activeItemsList: customPropTypes.every([ - customPropTypes.disallow(['children']), - PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.number]), - ]), - defaultActiveItemsList: customPropTypes.every([ - customPropTypes.disallow(['children']), - PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.number]), - ]), exclusive: PropTypes.bool, items: customPropTypes.collectionShorthand, renderItemTitle: PropTypes.func, rtlAttributes: PropTypes.func, - onactiveItemsListChange: PropTypes.func, } static defaultProps = { @@ -92,38 +96,45 @@ class Tree extends UIComponent, TreeState> { accessibility: treeBehavior, } - static autoControlledProps = ['activeItemsList'] - itemRefs = [] + treeRef = React.createRef() + state: TreeState = { activeItems: new Map() } - constructor(props, context) { - super(props, context) - - const activeItems = new Map() - - if (props.items) { - const setItemsLevelAndSize = (items = props.items, level = 1, parent?) => { - items.forEach((item: ShorthandValue, index: number) => { - item['level'] = level - item['siblings'] = items - item['index'] = index - if (parent) { - item['parent'] = parent - } - if (!item['id']) { - item['id'] = `${parent ? parent['id'] : ''}${index}` - } - if (item['items']) { - setItemsLevelAndSize(item['items'], level + 1, item) - activeItems.set(item['id'], { open: false }) + getItemsForRender = _.memoize((itemsFromProps: ShorthandCollection) => { + let generatedId = 0 + const { activeItems } = this.state + const itemsForRenderGenerator = (items = itemsFromProps, level = 1, parentId?: string) => { + const siblingSubtreeIds = [] + return _.reduce( + items, + (acc: TreeItemForRenderProps[], item: ShorthandValue, index: number) => { + const isSubtree = !!item['items'] + const id = item['id'] || `treeItemId${generatedId++}` + + if (isSubtree) { + activeItems.set(id, { open: !!item['defaultOpen'], siblingSubtreeIds }) + siblingSubtreeIds.push(id) } - }) - } - setItemsLevelAndSize() + + acc.push({ + item, + level, + index, + siblingsLength: items.length, + parentId, + id, + ...(isSubtree && { items: itemsForRenderGenerator(item['items'], level + 1, id) }), + }) + return acc + }, + [], + ) } - this.state = { activeItems } - } + this.setState({ activeItems }) + + return itemsForRenderGenerator() + }) handleTreeItemOverrides = (predefinedProps: TreeItemProps) => ({ onTitleClick: ( @@ -145,29 +156,25 @@ class Tree extends UIComponent, TreeState> { _.invoke(predefinedProps, 'onTitleClick', e, treeItemProps) }, onFocusParent: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { - const { parent } = treeItemProps + const { parentId } = treeItemProps - if (!parent) { + if (!parentId) { return } const { activeItems } = this.state - activeItems.get(parent['id']).element.focus() + activeItems.get(parentId).element.focus() _.invoke(predefinedProps, 'onFocusParent', e, treeItemProps) }, onFocusFirstChild: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { - const { items } = treeItemProps - - if (!items) { - return - } + const { id } = treeItemProps const { activeItems } = this.state - const firstChildElement = activeItems.get(items[0]['id']).element + const currentElement = activeItems.get(id).element - const element = getFirstFocusable(firstChildElement, firstChildElement, true) + const element = getNextElement(this.treeRef.current, currentElement) if (element) { element.focus() } @@ -175,15 +182,13 @@ class Tree extends UIComponent, TreeState> { _.invoke(predefinedProps, 'onFocusFirstChild', e, treeItemProps) }, onSiblingsExpand: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { - const { siblings } = treeItemProps + const { id } = treeItemProps const { activeItems } = this.state - const itemId = treeItemProps['id'] - const activeItemValue = activeItems.get(itemId) + const { siblingSubtreeIds } = activeItems.get(id) - siblings.forEach(sibling => { - if (sibling['items']) { - activeItems.set(sibling['id'], { ...activeItemValue, open: true }) - } + siblingSubtreeIds.forEach(siblingSubtreeId => { + const siblingSubtreeData = activeItems.get(siblingSubtreeId) + activeItems.set(siblingSubtreeId, { ...siblingSubtreeData, open: true }) }) this.setState({ @@ -194,49 +199,57 @@ class Tree extends UIComponent, TreeState> { }, }) - renderItems(items: ShorthandCollection) { - const { activeItems } = this.state - - return items.reduce((renderedItems, item, index) => { - const isSubtree = !!item['items'] - const isFirstChild = item['parent'] && index === 0 - const isSubtreeOpen = isSubtree && activeItems.get(item['id']).open - const renderedItem = TreeItem.create(item, { - defaultProps: { - className: Tree.slotClassNames.item, - open: isSubtreeOpen, - }, - overrideProps: this.handleTreeItemOverrides, - }) - const renderedFinalItem = - isSubtree || isFirstChild ? ( - { - const activeItemValue = activeItems.get(item['id']) - - activeItems.set(item['id'], { ...activeItemValue, element: itemElement }) - }} - > - {renderedItem} - - ) : ( - renderedItem - ) - - return [ - ...(renderedItems as any[]), - renderedFinalItem, - ...[isSubtreeOpen ? this.renderItems(item['items']) : []], - ] - }, []) - } - renderContent() { + const { activeItems } = this.state const { items } = this.props + if (!items) return null - return this.renderItems(items) + const renderItems = (itemsForRender: TreeItemForRenderProps[]) => { + return itemsForRender.reduce( + (renderedItems: any[], itemForRender: TreeItemForRenderProps, index: number) => { + const { item, items, parentId, id, ...rest } = itemForRender + const isFirstChild = index === 0 && !!parentId + const isSubtree = !!items + const isSubtreeOpen = isSubtree && activeItems.get(id).open + + const renderedItem = TreeItem.create(item, { + defaultProps: { + className: Tree.slotClassNames.item, + open: isSubtreeOpen, + id, + parentId, + ...rest, + }, + overrideProps: this.handleTreeItemOverrides, + }) + + const finalRenderedItem = + isSubtree || isFirstChild ? ( + { + const activeItemValue = activeItems.get(id) + activeItems.set(id, { ...activeItemValue, element: itemElement }) + }} + > + {renderedItem} + + ) : ( + renderedItem + ) + + return [ + ...(renderedItems as any[]), + finalRenderedItem, + ...[isSubtreeOpen ? renderItems(itemForRender['items']) : []], + ] + }, + [], + ) + } + + return renderItems(this.getItemsForRender(items)) } renderComponent({ ElementType, classes, accessibility, unhandledProps, styles, variables }) { @@ -244,15 +257,17 @@ class Tree extends UIComponent, TreeState> { this.itemRefs = [] return ( - - {childrenExist(children) ? children : this.renderContent()} - + + + {childrenExist(children) ? children : this.renderContent()} + + ) } } diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx index 877194da2b..94027ad84a 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -63,9 +63,8 @@ export interface TreeItemProps extends UIComponentProps, ChildrenComponentProps /** Whether or not the subtree of the item is in the open state. */ open?: boolean - parent?: ShorthandValue - - siblings?: ShorthandCollection + parentId?: string + siblingsLength?: number /** * A custom render iterator for rendering each Accordion panel title. @@ -112,6 +111,8 @@ class TreeItem extends UIComponent> { siblings: customPropTypes.collectionShorthand, treeItemRtlAttributes: PropTypes.func, title: customPropTypes.itemShorthand, + parentId: PropTypes.string, + siblingsLength: PropTypes.number, } static defaultProps = { @@ -189,7 +190,7 @@ class TreeItem extends UIComponent> { }) renderContent() { - const { items, title, renderItemTitle, open, level, siblings, index } = this.props + const { items, title, renderItemTitle, open, level, siblingsLength, index } = this.props const hasSubtree = !_.isNil(items) return TreeTitle.create(title, { @@ -199,7 +200,7 @@ class TreeItem extends UIComponent> { hasSubtree, as: hasSubtree ? 'span' : 'a', level, - siblingsLength: siblings.length, + siblingsLength, index, }, render: renderItemTitle, diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts index ce421c6213..fe45cea2a8 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts @@ -21,11 +21,11 @@ const treeItemBehavior: Accessibility = props => ({ role: 'none', ...(props.items && props.items.length && { - 'aria-expanded': props.open ? 'true' : 'false', + 'aria-expanded': props.open, tabIndex: -1, [IS_FOCUSABLE_ATTRIBUTE]: true, role: 'treeitem', - 'aria-setsize': props.siblings.length, + 'aria-setsize': props.siblingsLength, 'aria-posinset': props.index + 1, 'aria-level': props.level, }), @@ -64,7 +64,7 @@ export type TreeItemBehaviorProps = { items?: object[] /** If item is a subtree, it indicates if it's open. */ open?: boolean - siblings?: object[] + siblingsLength?: number level?: number index?: number } From ed860d21731c3c8958d974beb01750deebcc8785 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Fri, 23 Aug 2019 17:44:50 +0200 Subject: [PATCH 18/47] fix props and comments --- packages/react/src/components/Tree/Tree.tsx | 31 ++++++---- .../react/src/components/Tree/TreeItem.tsx | 59 ++++++++----------- .../react/src/components/Tree/TreeTitle.tsx | 13 ++-- .../Behaviors/Tree/treeTitleBehavior.ts | 2 - 4 files changed, 51 insertions(+), 54 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 099cf1e82e..db2bf54db8 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -34,8 +34,11 @@ export interface TreeProps extends UIComponentProps, ChildrenComponentProps { /** Accessibility behavior if overridden by the user. */ accessibility?: Accessibility + /** Map with the subtrees and information related to their open/closed state. */ + activeItems?: Map + /** Initial activeIndex value. */ - defaultActiveItemsList?: ShorthandCollection + defaultActiveItems?: Map /** Only allow one subtree to be open at a time. */ exclusive?: boolean @@ -54,20 +57,24 @@ export interface TreeProps extends UIComponentProps, ChildrenComponentProps { } export interface TreeItemForRenderProps { - item: ShorthandValue - items: TreeItemForRenderProps[] level: number - index: number id: string + index: number + item: ShorthandValue + items: TreeItemForRenderProps[] parentId: string siblingsLength: number } +export interface TreeActiveItemProps { + element?: HTMLElement + parentId?: string + open: boolean + siblingSubtreeIds?: string[] +} + export interface TreeState { - activeItems: Map< - string, - { open: boolean; element?: HTMLElement; parentId?: string; siblingSubtreeIds?: string[] } - > + activeItems: Map } class Tree extends UIComponent, TreeState> { @@ -143,11 +150,11 @@ class Tree extends UIComponent, TreeState> { predefinedProps: TreeItemProps, ) => { const { activeItems } = this.state - const itemId = treeItemProps.id - const activeItemValue = activeItems.get(itemId) + const { id } = treeItemProps + const activeItemValue = activeItems.get(id) if (treeItemProps.items) { - activeItems.set(itemId, { ...activeItemValue, open: !activeItemValue.open }) + activeItems.set(id, { ...activeItemValue, open: !activeItemValue.open }) this.setState({ activeItems, }) @@ -242,7 +249,7 @@ class Tree extends UIComponent, TreeState> { return [ ...(renderedItems as any[]), finalRenderedItem, - ...[isSubtreeOpen ? renderItems(itemForRender['items']) : []], + ...[isSubtreeOpen ? renderItems(items) : []], ] }, [], diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx index 94027ad84a..d61201b7da 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -1,5 +1,4 @@ import * as customPropTypes from '@stardust-ui/react-proptypes' -import { Ref } from '@stardust-ui/react-component-ref' import * as _ from 'lodash' import * as PropTypes from 'prop-types' import * as React from 'react' @@ -35,40 +34,42 @@ export interface TreeItemProps extends UIComponentProps, ChildrenComponentProps /** Accessibility behavior if overridden by the user. */ accessibility?: Accessibility - /** Only allow one subtree to be open at a time. */ - exclusive?: boolean - + /** Id needed to identify this item inside the Tree. */ id?: string - /** The index of the item among its sibbling */ + /** The index of the item among its siblings. */ index?: number /** Array of props for sub tree. */ items?: ShorthandCollection + /** Level of the tree/subtree that contains this item. */ level?: number /** Called when a tree title is clicked. */ onTitleClick?: ComponentEventHandler - /** Called when the item's first child is focused. */ + /** Called when the item's first child is about to be focused. */ onFocusFirstChild?: ComponentEventHandler - /** Called when the item's first child is focused. */ + /** Called when the item's siblings are about to be expanded. */ onSiblingsExpand?: ComponentEventHandler - /** Called when the item's parent is focused. */ + /** Called when the item's parent is about to be focused. */ onFocusParent?: ComponentEventHandler - /** Whether or not the subtree of the item is in the open state. */ + /** Whether or not the item is in the open state. Only makes sense if item has children items. */ open?: boolean + /** The id of the parent tree item, if any. */ parentId?: string + + /** Array with the ids of the tree item's siblings, if any. */ siblingsLength?: number /** - * A custom render iterator for rendering each Accordion panel title. - * The default component, props, and children are available for each panel title. + * A custom render iterator for rendering each tree title. + * The default component, props, and children are available for each tree title. * * @param {React.ReactType} Component - The computed component for this slot. * @param {object} props - The computed props for this slot. @@ -96,23 +97,20 @@ class TreeItem extends UIComponent> { ...commonPropTypes.createCommon({ content: false, }), + level: PropTypes.number, id: PropTypes.string, - items: customPropTypes.collectionShorthand, index: PropTypes.number, - exclusive: PropTypes.bool, - level: PropTypes.number, + items: customPropTypes.collectionShorthand, onTitleClick: PropTypes.func, onFocusFirstChild: PropTypes.func, onFocusParent: PropTypes.func, onSiblingsExpand: PropTypes.func, open: PropTypes.bool, - parent: customPropTypes.itemShorthand, - renderItemTitle: PropTypes.func, - siblings: customPropTypes.collectionShorthand, - treeItemRtlAttributes: PropTypes.func, - title: customPropTypes.itemShorthand, parentId: PropTypes.string, + renderItemTitle: PropTypes.func, siblingsLength: PropTypes.number, + title: customPropTypes.itemShorthand, + treeItemRtlAttributes: PropTypes.func, } static defaultProps = { @@ -120,9 +118,6 @@ class TreeItem extends UIComponent> { accessibility: treeItemBehavior, } - itemRef = React.createRef() - treeRef = React.createRef() - actionHandlers = { performClick: e => { e.preventDefault() @@ -212,17 +207,15 @@ class TreeItem extends UIComponent> { const { children } = this.props return ( - - - {childrenExist(children) ? children : this.renderContent()} - - + + {childrenExist(children) ? children : this.renderContent()} + ) } } diff --git a/packages/react/src/components/Tree/TreeTitle.tsx b/packages/react/src/components/Tree/TreeTitle.tsx index 1f86898074..0dff142e0f 100644 --- a/packages/react/src/components/Tree/TreeTitle.tsx +++ b/packages/react/src/components/Tree/TreeTitle.tsx @@ -24,6 +24,10 @@ export interface TreeTitleProps /** Accessibility behavior if overridden by the user. */ accessibility?: Accessibility + /** The index of the item among its siblings. */ + index?: number + + /** Level of the tree/subtree that contains this item. */ level?: number /** @@ -37,12 +41,8 @@ export interface TreeTitleProps /** Whether or not the subtree of the item is in the open state. */ open?: boolean - /** Whether or not the item has a subtree. */ - hasSubtree?: boolean - + /** Array with the ids of the tree item's siblings, if any. */ siblingsLength?: number - - index?: number } class TreeTitle extends UIComponent> { @@ -54,12 +54,11 @@ class TreeTitle extends UIComponent> { static propTypes = { ...commonPropTypes.createCommon(), + index: PropTypes.number, level: PropTypes.number, onClick: PropTypes.func, open: PropTypes.bool, - hasSubtree: PropTypes.bool, siblingsLength: PropTypes.number, - index: PropTypes.number, } static defaultProps = { diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts index 1a7e4d1d19..b93870db0c 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts @@ -37,8 +37,6 @@ export default treeTitleBehavior type TreeTitleBehavior = { /** Indicated if tree title has a subtree */ hasSubtree?: boolean - /** If subtree is opened. */ - open?: boolean level?: number siblingsLength?: number index?: number From e2a562658708715dc850cb03034b8cdd09cdc026 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Mon, 26 Aug 2019 09:50:47 +0200 Subject: [PATCH 19/47] fixed title render --- .../components/Tree/Types/TreeExclusiveExample.shorthand.tsx | 2 +- .../Tree/Types/TreeTitleCustomizationExample.shorthand.tsx | 2 +- packages/react/src/components/Tree/Tree.tsx | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/src/examples/components/Tree/Types/TreeExclusiveExample.shorthand.tsx b/docs/src/examples/components/Tree/Types/TreeExclusiveExample.shorthand.tsx index 8edd835cd2..397bce204c 100644 --- a/docs/src/examples/components/Tree/Types/TreeExclusiveExample.shorthand.tsx +++ b/docs/src/examples/components/Tree/Types/TreeExclusiveExample.shorthand.tsx @@ -42,7 +42,7 @@ const items = [ const titleRenderer = (Component, { content, open, hasSubtree, ...restProps }) => ( - {hasSubtree && } + {hasSubtree && } {content} ) diff --git a/docs/src/examples/components/Tree/Types/TreeTitleCustomizationExample.shorthand.tsx b/docs/src/examples/components/Tree/Types/TreeTitleCustomizationExample.shorthand.tsx index bcb3bb20d4..fdda6d56e8 100644 --- a/docs/src/examples/components/Tree/Types/TreeTitleCustomizationExample.shorthand.tsx +++ b/docs/src/examples/components/Tree/Types/TreeTitleCustomizationExample.shorthand.tsx @@ -32,7 +32,7 @@ const items = [ const titleRenderer = (Component, { content, open, hasSubtree, ...restProps }) => ( - {hasSubtree && } + {hasSubtree && } {content} ) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index db2bf54db8..c4c567f335 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -208,7 +208,7 @@ class Tree extends UIComponent, TreeState> { renderContent() { const { activeItems } = this.state - const { items } = this.props + const { items, renderItemTitle } = this.props if (!items) return null @@ -226,6 +226,7 @@ class Tree extends UIComponent, TreeState> { open: isSubtreeOpen, id, parentId, + renderItemTitle, ...rest, }, overrideProps: this.handleTreeItemOverrides, From 7ea01ae41651edd10c4d1bfbb012ec2e2801761f Mon Sep 17 00:00:00 2001 From: silviuavram Date: Mon, 26 Aug 2019 10:02:11 +0200 Subject: [PATCH 20/47] enhance hasSubtree condition --- 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 d61201b7da..70c943bcac 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -186,7 +186,7 @@ class TreeItem extends UIComponent> { renderContent() { const { items, title, renderItemTitle, open, level, siblingsLength, index } = this.props - const hasSubtree = !_.isNil(items) + const hasSubtree = !_.isNil(items) && items.length > 0 return TreeTitle.create(title, { defaultProps: { From 816b0365163b30177efaa37caa9e660aee3d7e99 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Mon, 26 Aug 2019 10:14:51 +0200 Subject: [PATCH 21/47] fixed some variables --- .../themes/teams-dark/components/Tree/treeTitleVariables.ts | 4 ++-- .../teams-high-contrast/components/Tree/treeTitleVariables.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react/src/themes/teams-dark/components/Tree/treeTitleVariables.ts b/packages/react/src/themes/teams-dark/components/Tree/treeTitleVariables.ts index fe899e68ec..c292c86d64 100644 --- a/packages/react/src/themes/teams-dark/components/Tree/treeTitleVariables.ts +++ b/packages/react/src/themes/teams-dark/components/Tree/treeTitleVariables.ts @@ -1,6 +1,6 @@ -import { HierarchicalTreeTitleVariables } from '../../../teams/components/HierarchicalTree/hierarchicalTreeTitleVariables' +import { TreeTitleVariables } from '../../../teams/components/Tree/treeTitleVariables' -export default (siteVars: any): HierarchicalTreeTitleVariables => { +export default (siteVars: any): TreeTitleVariables => { return { defaultColor: siteVars.colors.white, } diff --git a/packages/react/src/themes/teams-high-contrast/components/Tree/treeTitleVariables.ts b/packages/react/src/themes/teams-high-contrast/components/Tree/treeTitleVariables.ts index fe899e68ec..c292c86d64 100644 --- a/packages/react/src/themes/teams-high-contrast/components/Tree/treeTitleVariables.ts +++ b/packages/react/src/themes/teams-high-contrast/components/Tree/treeTitleVariables.ts @@ -1,6 +1,6 @@ -import { HierarchicalTreeTitleVariables } from '../../../teams/components/HierarchicalTree/hierarchicalTreeTitleVariables' +import { TreeTitleVariables } from '../../../teams/components/Tree/treeTitleVariables' -export default (siteVars: any): HierarchicalTreeTitleVariables => { +export default (siteVars: any): TreeTitleVariables => { return { defaultColor: siteVars.colors.white, } From aba11aee657adbfab44e34de53f876d51706b76e Mon Sep 17 00:00:00 2001 From: silviuavram Date: Mon, 26 Aug 2019 11:19:04 +0200 Subject: [PATCH 22/47] some checks and comments --- packages/react/src/components/Tree/Tree.tsx | 31 ++++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index c4c567f335..1c98340dd1 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -107,6 +107,10 @@ class Tree extends UIComponent, TreeState> { treeRef = React.createRef() state: TreeState = { activeItems: new Map() } + /** + * For each item it adds information needed for accessibility (screen readers and kb navigation). + * Returned values are used for rendering. + */ getItemsForRender = _.memoize((itemsFromProps: ShorthandCollection) => { let generatedId = 0 const { activeItems } = this.state @@ -115,9 +119,10 @@ class Tree extends UIComponent, TreeState> { return _.reduce( items, (acc: TreeItemForRenderProps[], item: ShorthandValue, index: number) => { - const isSubtree = !!item['items'] + const isSubtree = !!item['items'] && item['items'].length > 0 const id = item['id'] || `treeItemId${generatedId++}` + // activeItems will contain only the items that can spawn sub-trees. if (isSubtree) { activeItems.set(id, { open: !!item['defaultOpen'], siblingSubtreeIds }) siblingSubtreeIds.push(id) @@ -153,7 +158,7 @@ class Tree extends UIComponent, TreeState> { const { id } = treeItemProps const activeItemValue = activeItems.get(id) - if (treeItemProps.items) { + if (treeItemProps.items && treeItemProps.items.length > 0) { activeItems.set(id, { ...activeItemValue, open: !activeItemValue.open }) this.setState({ activeItems, @@ -170,9 +175,13 @@ class Tree extends UIComponent, TreeState> { } const { activeItems } = this.state + const elementToBeFocused = activeItems.get(parentId).element - activeItems.get(parentId).element.focus() + if (!elementToBeFocused) { + return + } + elementToBeFocused.focus() _.invoke(predefinedProps, 'onFocusParent', e, treeItemProps) }, onFocusFirstChild: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { @@ -181,11 +190,17 @@ class Tree extends UIComponent, TreeState> { const { activeItems } = this.state const currentElement = activeItems.get(id).element - const element = getNextElement(this.treeRef.current, currentElement) - if (element) { - element.focus() + if (!currentElement) { + return + } + + const elementToBeFocused = getNextElement(this.treeRef.current, currentElement) + + if (!elementToBeFocused) { + return } + elementToBeFocused.focus() _.invoke(predefinedProps, 'onFocusFirstChild', e, treeItemProps) }, onSiblingsExpand: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { @@ -217,7 +232,7 @@ class Tree extends UIComponent, TreeState> { (renderedItems: any[], itemForRender: TreeItemForRenderProps, index: number) => { const { item, items, parentId, id, ...rest } = itemForRender const isFirstChild = index === 0 && !!parentId - const isSubtree = !!items + const isSubtree = !!items && items.length > 0 const isSubtreeOpen = isSubtree && activeItems.get(id).open const renderedItem = TreeItem.create(item, { @@ -232,6 +247,8 @@ class Tree extends UIComponent, TreeState> { overrideProps: this.handleTreeItemOverrides, }) + // Only need refs of the items that spawn subtrees, when they need to be focused + // by any of their children, using Arrow Left. const finalRenderedItem = isSubtree || isFirstChild ? ( Date: Mon, 26 Aug 2019 14:32:47 +0200 Subject: [PATCH 23/47] fixed exclusive --- packages/react/src/components/Tree/Tree.tsx | 91 ++++++++++++++++--- .../react/src/components/Tree/TreeTitle.tsx | 4 + 2 files changed, 84 insertions(+), 11 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 1c98340dd1..bbff3733cb 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -69,7 +69,7 @@ export interface TreeItemForRenderProps { export interface TreeActiveItemProps { element?: HTMLElement parentId?: string - open: boolean + open?: boolean siblingSubtreeIds?: string[] } @@ -114,8 +114,12 @@ class Tree extends UIComponent, TreeState> { getItemsForRender = _.memoize((itemsFromProps: ShorthandCollection) => { let generatedId = 0 const { activeItems } = this.state + const { exclusive } = this.props + const itemsForRenderGenerator = (items = itemsFromProps, level = 1, parentId?: string) => { const siblingSubtreeIds = [] + let subtreeAlreadyOpen = false + return _.reduce( items, (acc: TreeItemForRenderProps[], item: ShorthandValue, index: number) => { @@ -124,7 +128,12 @@ class Tree extends UIComponent, TreeState> { // activeItems will contain only the items that can spawn sub-trees. if (isSubtree) { - activeItems.set(id, { open: !!item['defaultOpen'], siblingSubtreeIds }) + const subtreeOpen = !!item['initialOpen'] + if (subtreeOpen && exclusive) { + // if exclusive, will open only first subtree. + subtreeAlreadyOpen = true + } + activeItems.set(id, { open: subtreeOpen && !subtreeAlreadyOpen, siblingSubtreeIds }) siblingSubtreeIds.push(id) } @@ -143,9 +152,18 @@ class Tree extends UIComponent, TreeState> { ) } + const itemsForRender = itemsForRenderGenerator() + + /* Remove each item's id from its array of siblingSubtreeIds. */ + for (const key of Array.from(activeItems.keys())) { + this.setActiveItem(key, ({ siblingSubtreeIds }) => ({ + siblingSubtreeIds: siblingSubtreeIds.filter(id => id !== key), + })) + } + this.setState({ activeItems }) - return itemsForRenderGenerator() + return itemsForRender }) handleTreeItemOverrides = (predefinedProps: TreeItemProps) => ({ @@ -155,11 +173,12 @@ class Tree extends UIComponent, TreeState> { predefinedProps: TreeItemProps, ) => { const { activeItems } = this.state - const { id } = treeItemProps - const activeItemValue = activeItems.get(id) + const { id, items } = treeItemProps + + if (items && items.length > 0) { + this.closeSiblingWhenExlusive(id) + this.setActiveItem(id, ({ open }) => ({ open: !open })) - if (treeItemProps.items && treeItemProps.items.length > 0) { - activeItems.set(id, { ...activeItemValue, open: !activeItemValue.open }) this.setState({ activeItems, }) @@ -204,13 +223,16 @@ class Tree extends UIComponent, TreeState> { _.invoke(predefinedProps, 'onFocusFirstChild', e, treeItemProps) }, onSiblingsExpand: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { + if (this.props.exclusive) { + return + } + const { id } = treeItemProps const { activeItems } = this.state const { siblingSubtreeIds } = activeItems.get(id) siblingSubtreeIds.forEach(siblingSubtreeId => { - const siblingSubtreeData = activeItems.get(siblingSubtreeId) - activeItems.set(siblingSubtreeId, { ...siblingSubtreeData, open: true }) + this.setActiveItem(siblingSubtreeId, { open: true }) }) this.setState({ @@ -254,8 +276,7 @@ class Tree extends UIComponent, TreeState> { { - const activeItemValue = activeItems.get(id) - activeItems.set(id, { ...activeItemValue, element: itemElement }) + this.setActiveItem(id, { element: itemElement }) }} > {renderedItem} @@ -295,6 +316,54 @@ class Tree extends UIComponent, TreeState> { ) } + + /** + * Similar to how setState works, merges changes on top of old value of an activeItem. + * + * @param id Id of the activeItem. + * @param changes Changes to be merged on top of old value or a callback that takes old + * value as param and returns a new value. + */ + setActiveItem( + id: string, + changes: ((oldValue: TreeActiveItemProps) => TreeActiveItemProps) | TreeActiveItemProps, + ) { + const { activeItems } = this.state + const activeItemValue = activeItems.get(id) + activeItems.set(id, { + ...activeItemValue, + ...(_.isFunction(changes) ? changes(activeItemValue) : changes), + }) + } + + /** + * In the case of exclusive tree, we will close the other open sibling at opening + * a tree item. + * + * @param id The id of the tree item to be opened. + */ + closeSiblingWhenExlusive(id: string) { + const { exclusive } = this.props + + if (!exclusive) { + return + } + + const { activeItems } = this.state + const activeItemValue = activeItems.get(id) + + if (activeItemValue.siblingSubtreeIds.length === 0) { + return + } + + const alreadyOpenSiblingId = activeItemValue.siblingSubtreeIds.find(siblingSubtreeId => { + return activeItems.get(siblingSubtreeId).open + }) + + if (alreadyOpenSiblingId) { + this.setActiveItem(alreadyOpenSiblingId, { open: false }) + } + } } Tree.create = createShorthandFactory({ diff --git a/packages/react/src/components/Tree/TreeTitle.tsx b/packages/react/src/components/Tree/TreeTitle.tsx index 0dff142e0f..4befa8e9ae 100644 --- a/packages/react/src/components/Tree/TreeTitle.tsx +++ b/packages/react/src/components/Tree/TreeTitle.tsx @@ -24,6 +24,9 @@ export interface TreeTitleProps /** Accessibility behavior if overridden by the user. */ accessibility?: Accessibility + /** Whether or not the item has a subtree. */ + hasSubtree?: boolean + /** The index of the item among its siblings. */ index?: number @@ -54,6 +57,7 @@ class TreeTitle extends UIComponent> { static propTypes = { ...commonPropTypes.createCommon(), + hasSubtree: PropTypes.bool, index: PropTypes.number, level: PropTypes.number, onClick: PropTypes.func, From 131b425059d2958d5054cc5f9d4957f809321fdf Mon Sep 17 00:00:00 2001 From: silviuavram Date: Mon, 26 Aug 2019 15:25:28 +0200 Subject: [PATCH 24/47] small change in example --- docs/src/examples/components/Tree/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/examples/components/Tree/index.tsx b/docs/src/examples/components/Tree/index.tsx index 1682fecab8..e6038d87de 100644 --- a/docs/src/examples/components/Tree/index.tsx +++ b/docs/src/examples/components/Tree/index.tsx @@ -2,9 +2,9 @@ import * as React from 'react' import Types from './Types' const TreeExamples = () => ( -
+ <> -
+ ) export default TreeExamples From 50a3f470d1c18508ae950ed226e0fcb732ac4124 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Mon, 26 Aug 2019 15:29:30 +0200 Subject: [PATCH 25/47] some changes in props --- packages/react/src/components/Tree/Tree.tsx | 2 ++ packages/react/src/components/Tree/TreeItem.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index bbff3733cb..498748fbd5 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -92,6 +92,8 @@ class Tree extends UIComponent, TreeState> { ...commonPropTypes.createCommon({ content: false, }), + activeItems: PropTypes.any, + defaultActiveItems: PropTypes.any, exclusive: PropTypes.bool, items: customPropTypes.collectionShorthand, renderItemTitle: PropTypes.func, diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx index 70c943bcac..9e7dd7b235 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -97,10 +97,10 @@ class TreeItem extends UIComponent> { ...commonPropTypes.createCommon({ content: false, }), - level: PropTypes.number, id: PropTypes.string, index: PropTypes.number, items: customPropTypes.collectionShorthand, + level: PropTypes.number, onTitleClick: PropTypes.func, onFocusFirstChild: PropTypes.func, onFocusParent: PropTypes.func, From a06a00f5909ad7ab517ecd36bc5ec58a5b5e7a1e Mon Sep 17 00:00:00 2001 From: silviuavram Date: Mon, 26 Aug 2019 15:59:57 +0200 Subject: [PATCH 26/47] add initialOpen prop --- .../TreeInitiallyOpenExample.shorthand.tsx | 78 +++++++++++++++++++ .../examples/components/Tree/Types/index.tsx | 5 ++ packages/react/src/components/Tree/Tree.tsx | 6 +- .../react/src/components/Tree/TreeItem.tsx | 4 + 4 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 docs/src/examples/components/Tree/Types/TreeInitiallyOpenExample.shorthand.tsx diff --git a/docs/src/examples/components/Tree/Types/TreeInitiallyOpenExample.shorthand.tsx b/docs/src/examples/components/Tree/Types/TreeInitiallyOpenExample.shorthand.tsx new file mode 100644 index 0000000000..b66173f61f --- /dev/null +++ b/docs/src/examples/components/Tree/Types/TreeInitiallyOpenExample.shorthand.tsx @@ -0,0 +1,78 @@ +import * as React from 'react' +import { Tree } from '@stardust-ui/react' + +const items = [ + { + key: '1', + title: 'House Lannister', + initialOpen: true, + items: [ + { + key: '11', + title: 'Tywin', + items: [ + { + key: '111', + title: 'Jaime', + }, + { + key: '112', + title: 'Cersei', + }, + { + key: '113', + title: 'Tyrion', + }, + ], + }, + { + key: '21', + title: 'Kevan', + initialOpen: true, + items: [ + { + key: '211', + title: 'Lancel', + }, + { + key: '212', + title: 'Willem', + }, + { + key: '213', + title: 'Martyn', + }, + ], + }, + ], + }, + { + key: '2', + title: 'House Targaryen', + items: [ + { + key: '21', + title: 'Aerys', + initialOpen: true, + items: [ + { + key: '211', + title: 'Rhaegar', + }, + { + key: '212', + title: 'Viserys', + }, + { + key: '213', + title: 'Daenerys', + }, + ], + }, + ], + }, +] + +const TreeInitiallyOpenExampleShorthand = () => + +export default TreeInitiallyOpenExampleShorthand diff --git a/docs/src/examples/components/Tree/Types/index.tsx b/docs/src/examples/components/Tree/Types/index.tsx index c6b19a3743..709e010f4f 100644 --- a/docs/src/examples/components/Tree/Types/index.tsx +++ b/docs/src/examples/components/Tree/Types/index.tsx @@ -19,6 +19,11 @@ const Types = () => ( description="A Tree with only one subtree open at a time." examplePath="components/Tree/Types/TreeExclusiveExample" /> + ) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 498748fbd5..0ec340626d 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -107,11 +107,12 @@ class Tree extends UIComponent, TreeState> { itemRefs = [] treeRef = React.createRef() + // In state we need only the items that can expand and spawn sub-trees. state: TreeState = { activeItems: new Map() } /** * For each item it adds information needed for accessibility (screen readers and kb navigation). - * Returned values are used for rendering. + * Returned values are used for rendering, while the items prop will remain unchanged. */ getItemsForRender = _.memoize((itemsFromProps: ShorthandCollection) => { let generatedId = 0 @@ -140,12 +141,15 @@ class Tree extends UIComponent, TreeState> { } acc.push({ + // initial item. item, + // added props needed for a11y. level, index, siblingsLength: items.length, parentId, id, + // children items will go through the same process. ...(isSubtree && { items: itemsForRenderGenerator(item['items'], level + 1, id) }), }) return acc diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx index 9e7dd7b235..6f41f430a7 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -40,6 +40,9 @@ export interface TreeItemProps extends UIComponentProps, ChildrenComponentProps /** The index of the item among its siblings. */ index?: number + /** Initial open state. */ + initialOpen?: boolean + /** Array of props for sub tree. */ items?: ShorthandCollection @@ -99,6 +102,7 @@ class TreeItem extends UIComponent> { }), id: PropTypes.string, index: PropTypes.number, + initialOpen: PropTypes.bool, items: customPropTypes.collectionShorthand, level: PropTypes.number, onTitleClick: PropTypes.func, From 7886d1a2809df6ddaf3a7ec3e9caa5e7c2518cd8 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Mon, 26 Aug 2019 16:06:33 +0200 Subject: [PATCH 27/47] add some comments --- packages/react/src/components/Tree/Tree.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 0ec340626d..d14df8683e 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -66,6 +66,10 @@ export interface TreeItemForRenderProps { siblingsLength: number } +/* + * Needed to keep track of sub-trees open state and also for a11y keyboard navigation, + * such as expanding siblings on '*' or focusing parent on Arrow Left. + */ export interface TreeActiveItemProps { element?: HTMLElement parentId?: string From a0946fe2b0778547f600d225fb8aba3e42d24901 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Mon, 26 Aug 2019 16:10:55 +0200 Subject: [PATCH 28/47] add autocontrolled back to Tree --- packages/react/src/components/Tree/Tree.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index d14df8683e..7a68ce3a7e 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -13,7 +13,7 @@ import { ChildrenComponentProps, rtlTextContainer, applyAccessibilityKeyHandlers, - UIComponent, + AutoControlledComponent, } from '../../lib' import { ShorthandRenderFunction, @@ -81,7 +81,7 @@ export interface TreeState { activeItems: Map } -class Tree extends UIComponent, TreeState> { +class Tree extends AutoControlledComponent, TreeState> { static create: Function static displayName = 'Tree' @@ -109,10 +109,15 @@ class Tree extends UIComponent, TreeState> { accessibility: treeBehavior, } + // In state we need only the items that can expand and spawn sub-trees. + static autoControlledProps = ['activeItems'] + + getInitialAutoControlledState() { + return { activeItems: new Map() } + } + itemRefs = [] treeRef = React.createRef() - // In state we need only the items that can expand and spawn sub-trees. - state: TreeState = { activeItems: new Map() } /** * For each item it adds information needed for accessibility (screen readers and kb navigation). From 81b821666cb06cc13d4f54afc229e1773f0f9847 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Tue, 27 Aug 2019 15:50:32 +0200 Subject: [PATCH 29/47] add tests and fix asterisk expand bug --- packages/react/src/components/Tree/Tree.tsx | 11 +- .../test/specs/components/Tree/Tree-test.tsx | 187 ++++++++++++++++++ 2 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 packages/react/test/specs/components/Tree/Tree-test.tsx diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 7a68ce3a7e..5d45c3cda7 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -138,14 +138,15 @@ class Tree extends AutoControlledComponent, TreeState> { const isSubtree = !!item['items'] && item['items'].length > 0 const id = item['id'] || `treeItemId${generatedId++}` - // activeItems will contain only the items that can spawn sub-trees. + activeItems.set(id, { siblingSubtreeIds }) + if (isSubtree) { const subtreeOpen = !!item['initialOpen'] if (subtreeOpen && exclusive) { // if exclusive, will open only first subtree. subtreeAlreadyOpen = true } - activeItems.set(id, { open: subtreeOpen && !subtreeAlreadyOpen, siblingSubtreeIds }) + this.setActiveItem(id, { open: subtreeOpen && !subtreeAlreadyOpen }) siblingSubtreeIds.push(id) } @@ -242,7 +243,7 @@ class Tree extends AutoControlledComponent, TreeState> { return } - const { id } = treeItemProps + const { id, items } = treeItemProps const { activeItems } = this.state const { siblingSubtreeIds } = activeItems.get(id) @@ -250,6 +251,10 @@ class Tree extends AutoControlledComponent, TreeState> { this.setActiveItem(siblingSubtreeId, { open: true }) }) + if (items && items.length > 0) { + this.setActiveItem(id, { open: true }) + } + this.setState({ activeItems, }) diff --git a/packages/react/test/specs/components/Tree/Tree-test.tsx b/packages/react/test/specs/components/Tree/Tree-test.tsx new file mode 100644 index 0000000000..dd88ad5040 --- /dev/null +++ b/packages/react/test/specs/components/Tree/Tree-test.tsx @@ -0,0 +1,187 @@ +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' +import TreeItem from 'src/components/Tree/TreeItem' +import { ReactWrapper, CommonWrapper } from 'enzyme' + +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: '211', + title: '211', + }, + ], + }, + { + key: '22', + title: '22', + }, + ], + }, + { + key: '3', + title: '3', + }, +] + +const getTitles = (wrapper: ReactWrapper): CommonWrapper => + wrapper.find(`.${TreeTitle.className}`).filterWhere(n => typeof n.type() === 'string') +const getItems = (wrapper: ReactWrapper): CommonWrapper => + wrapper.find(`.${TreeItem.className}`).filterWhere(n => typeof n.type() === 'string') + +const checkOpenTitles = (wrapper: ReactWrapper, expected: string[]): void => { + const titles = getTitles(wrapper) + expect(titles.length).toEqual(expected.length) + + expected.forEach((expectedTitle, index) => { + expect(titles.at(index).getDOMNode().textContent).toEqual(expectedTitle) + }) +} + +describe('Tree', () => { + isConformant(Tree) + + describe('activeItems', () => { + it('should have item already open by passing initialOpen', () => { + const copiedItems = JSON.parse(JSON.stringify(items)) + copiedItems[1]['initialOpen'] = true + const wrapper = mountWithProvider() + + checkOpenTitles(wrapper, ['1', '2', '21', '22', '3']) + }) + + it('should contain index of item open at click', () => { + const wrapper = mountWithProvider() + + getTitles(wrapper) + .at(0) // title 1 + .simulate('click') + checkOpenTitles(wrapper, ['1', '11', '12', '2', '3']) + + getTitles(wrapper) + .at(3) // title 2 + .simulate('click') + checkOpenTitles(wrapper, ['1', '11', '12', '2', '21', '22', '3']) + }) + + it('should have index of item removed when closed at click', () => { + const copiedItems = JSON.parse(JSON.stringify(items)) + copiedItems[0]['initialOpen'] = true + copiedItems[1]['initialOpen'] = true + const wrapper = mountWithProvider() + + getTitles(wrapper) + .at(0) // title 1 + .simulate('click') + checkOpenTitles(wrapper, ['1', '2', '21', '22', '3']) + }) + + it('should contain only one index at a time if exclusive', () => { + const wrapper = mountWithProvider() + + getTitles(wrapper) + .at(0) // title 1 + .simulate('click') + checkOpenTitles(wrapper, ['1', '11', '12', '2', '3']) + + getTitles(wrapper) + .at(3) // title 2 + .simulate('click') + checkOpenTitles(wrapper, ['1', '2', '21', '22', '3']) + }) + + it('should contain index of item open by ArrowRight', () => { + const wrapper = mountWithProvider() + + getTitles(wrapper) + .at(0) // title 1 + .simulate('keydown', { keyCode: keyboardKey.ArrowRight }) + checkOpenTitles(wrapper, ['1', '11', '12', '2', '3']) + }) + + it('should have index of item removed if closed by ArrowLeft', () => { + const copiedItems = JSON.parse(JSON.stringify(items)) + copiedItems[0]['initialOpen'] = true + copiedItems[1]['initialOpen'] = true + const wrapper = mountWithProvider() + + getItems(wrapper) + .at(0) // title 1 + .simulate('keydown', { keyCode: keyboardKey.ArrowLeft }) + checkOpenTitles(wrapper, ['1', '2', '21', '22', '3']) + }) + + it('should have all TreeItems with a subtree open on asterisk key', () => { + const wrapper = mountWithProvider() + + getTitles(wrapper) + .at(0) // title 1 + .simulate('keydown', { keyCode: keyboardKey['*'] }) + checkOpenTitles(wrapper, ['1', '11', '12', '2', '21', '22', '3']) + }) + + it('should expand subtrees only on current level on asterisk key', () => { + const copiedItems = JSON.parse(JSON.stringify(items)) + copiedItems[0]['initialOpen'] = true + const wrapper = mountWithProvider() + + getTitles(wrapper) + .at(1) // title 11 + .simulate('keydown', { keyCode: keyboardKey['*'] }) + checkOpenTitles(wrapper, ['1', '11', '12', '121', '2', '3']) + }) + + it('should not be changed on asterisk key if all siblings are already expanded', () => { + const copiedItems = JSON.parse(JSON.stringify(items)) + copiedItems[0]['initialOpen'] = true + copiedItems[1]['initialOpen'] = true + copiedItems[2]['initialOpen'] = true + const wrapper = mountWithProvider() + + getTitles(wrapper) + .at(0) // title 1 + .simulate('keydown', { keyCode: keyboardKey['*'] }) + checkOpenTitles(wrapper, ['1', '11', '12', '2', '21', '22', '3']) + }) + + it('should expand siblings even if the item is not expandable', () => { + const wrapper = mountWithProvider() + + getTitles(wrapper) + .at(2) // title 3 + .simulate('keydown', { keyCode: keyboardKey['*'] }) + checkOpenTitles(wrapper, ['1', '11', '12', '2', '21', '22', '3']) + }) + }) +}) From 7d127fb13fdb2d40dbe2620f36e889f0e7fca93e Mon Sep 17 00:00:00 2001 From: silviuavram Date: Wed, 28 Aug 2019 17:33:12 +0200 Subject: [PATCH 30/47] refactored the state logic --- .../Tree/Types/TreeExample.shorthand.tsx | 28 +- .../Types/TreeExclusiveExample.shorthand.tsx | 14 +- .../TreeInitiallyOpenExample.shorthand.tsx | 28 +- ...reeTitleCustomizationExample.shorthand.tsx | 10 +- packages/react/src/components/Tree/Tree.tsx | 331 +++++++----------- .../react/src/components/Tree/TreeItem.tsx | 16 +- .../Behaviors/Tree/treeItemBehavior.ts | 4 +- 7 files changed, 184 insertions(+), 247 deletions(-) diff --git a/docs/src/examples/components/Tree/Types/TreeExample.shorthand.tsx b/docs/src/examples/components/Tree/Types/TreeExample.shorthand.tsx index e158763393..4f2357b5a3 100644 --- a/docs/src/examples/components/Tree/Types/TreeExample.shorthand.tsx +++ b/docs/src/examples/components/Tree/Types/TreeExample.shorthand.tsx @@ -3,41 +3,41 @@ import { Tree } from '@stardust-ui/react' const items = [ { - key: '1', + id: '1', title: 'House Lannister', items: [ { - key: '11', + id: '11', title: 'Tywin', items: [ { - key: '111', + id: '111', title: 'Jaime', }, { - key: '112', + id: '112', title: 'Cersei', }, { - key: '113', + id: '113', title: 'Tyrion', }, ], }, { - key: '21', + id: '21', title: 'Kevan', items: [ { - key: '211', + id: '211', title: 'Lancel', }, { - key: '212', + id: '212', title: 'Willem', }, { - key: '213', + id: '213', title: 'Martyn', }, ], @@ -45,23 +45,23 @@ const items = [ ], }, { - key: '2', + id: '2', title: 'House Targaryen', items: [ { - key: '21', + id: '21', title: 'Aerys', items: [ { - key: '211', + id: '211', title: 'Rhaegar', }, { - key: '212', + id: '212', title: 'Viserys', }, { - key: '213', + id: '213', title: 'Daenerys', }, ], diff --git a/docs/src/examples/components/Tree/Types/TreeExclusiveExample.shorthand.tsx b/docs/src/examples/components/Tree/Types/TreeExclusiveExample.shorthand.tsx index 397bce204c..d06f883c28 100644 --- a/docs/src/examples/components/Tree/Types/TreeExclusiveExample.shorthand.tsx +++ b/docs/src/examples/components/Tree/Types/TreeExclusiveExample.shorthand.tsx @@ -3,25 +3,25 @@ import { Icon, Tree } from '@stardust-ui/react' const items = [ { - key: '1', + id: '1', title: 'one', items: [ { - key: '2', + id: '2', title: 'one one', items: [ { - key: '3', + id: '3', title: 'one one one', }, ], }, { - key: '6', + id: '6', title: 'one two', items: [ { - key: '7', + id: '7', title: 'one two one', }, ], @@ -29,11 +29,11 @@ const items = [ ], }, { - key: '4', + id: '4', title: 'two', items: [ { - key: '5', + id: '5', title: 'two one', }, ], diff --git a/docs/src/examples/components/Tree/Types/TreeInitiallyOpenExample.shorthand.tsx b/docs/src/examples/components/Tree/Types/TreeInitiallyOpenExample.shorthand.tsx index b66173f61f..1f8dcb699c 100644 --- a/docs/src/examples/components/Tree/Types/TreeInitiallyOpenExample.shorthand.tsx +++ b/docs/src/examples/components/Tree/Types/TreeInitiallyOpenExample.shorthand.tsx @@ -3,43 +3,43 @@ import { Tree } from '@stardust-ui/react' const items = [ { - key: '1', + id: '1', title: 'House Lannister', initialOpen: true, items: [ { - key: '11', + id: '11', title: 'Tywin', items: [ { - key: '111', + id: '111', title: 'Jaime', }, { - key: '112', + id: '112', title: 'Cersei', }, { - key: '113', + id: '113', title: 'Tyrion', }, ], }, { - key: '21', + id: '21', title: 'Kevan', initialOpen: true, items: [ { - key: '211', + id: '211', title: 'Lancel', }, { - key: '212', + id: '212', title: 'Willem', }, { - key: '213', + id: '213', title: 'Martyn', }, ], @@ -47,24 +47,24 @@ const items = [ ], }, { - key: '2', + id: '2', title: 'House Targaryen', items: [ { - key: '21', + id: '21', title: 'Aerys', initialOpen: true, items: [ { - key: '211', + id: '211', title: 'Rhaegar', }, { - key: '212', + id: '212', title: 'Viserys', }, { - key: '213', + id: '213', title: 'Daenerys', }, ], diff --git a/docs/src/examples/components/Tree/Types/TreeTitleCustomizationExample.shorthand.tsx b/docs/src/examples/components/Tree/Types/TreeTitleCustomizationExample.shorthand.tsx index fdda6d56e8..cc60148785 100644 --- a/docs/src/examples/components/Tree/Types/TreeTitleCustomizationExample.shorthand.tsx +++ b/docs/src/examples/components/Tree/Types/TreeTitleCustomizationExample.shorthand.tsx @@ -3,15 +3,15 @@ import { Icon, Tree } from '@stardust-ui/react' const items = [ { - key: '1', + id: '1', title: 'one', items: [ { - key: '2', + id: '2', title: 'one one', items: [ { - key: '3', + id: '3', title: 'one one one', }, ], @@ -19,11 +19,11 @@ const items = [ ], }, { - key: '4', + id: '4', title: 'two', items: [ { - key: '5', + id: '5', title: 'two one', }, ], diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 5d45c3cda7..60a649b62c 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -2,7 +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 { handleRef, Ref } from '@stardust-ui/react-component-ref' import TreeItem, { TreeItemProps } from './TreeItem' import { @@ -35,10 +35,10 @@ export interface TreeProps extends UIComponentProps, ChildrenComponentProps { accessibility?: Accessibility /** Map with the subtrees and information related to their open/closed state. */ - activeItems?: Map + activeItemIds?: string[] /** Initial activeIndex value. */ - defaultActiveItems?: Map + defaultActiveItemIds?: string[] /** Only allow one subtree to be open at a time. */ exclusive?: boolean @@ -57,28 +57,18 @@ export interface TreeProps extends UIComponentProps, ChildrenComponentProps { } export interface TreeItemForRenderProps { - level: number + elementRef: React.RefObject id: string index: number - item: ShorthandValue items: TreeItemForRenderProps[] - parentId: string - siblingsLength: number -} - -/* - * Needed to keep track of sub-trees open state and also for a11y keyboard navigation, - * such as expanding siblings on '*' or focusing parent on Arrow Left. - */ -export interface TreeActiveItemProps { - element?: HTMLElement - parentId?: string - open?: boolean - siblingSubtreeIds?: string[] + level: number + parent: ShorthandValue + siblings: ShorthandCollection } export interface TreeState { - activeItems: Map + activeItemIds: string[] + itemsForRender: { [s: string]: TreeItemForRenderProps } } class Tree extends AutoControlledComponent, TreeState> { @@ -96,8 +86,8 @@ class Tree extends AutoControlledComponent, TreeState> { ...commonPropTypes.createCommon({ content: false, }), - activeItems: PropTypes.any, - defaultActiveItems: PropTypes.any, + activeItemIds: customPropTypes.collectionShorthand, + defaultActiveItemIds: customPropTypes.collectionShorthand, exclusive: PropTypes.bool, items: customPropTypes.collectionShorthand, renderItemTitle: PropTypes.func, @@ -109,78 +99,57 @@ class Tree extends AutoControlledComponent, TreeState> { accessibility: treeBehavior, } - // In state we need only the items that can expand and spawn sub-trees. - static autoControlledProps = ['activeItems'] - - getInitialAutoControlledState() { - return { activeItems: new Map() } - } - - itemRefs = [] - treeRef = React.createRef() - - /** - * For each item it adds information needed for accessibility (screen readers and kb navigation). - * Returned values are used for rendering, while the items prop will remain unchanged. - */ - getItemsForRender = _.memoize((itemsFromProps: ShorthandCollection) => { - let generatedId = 0 - const { activeItems } = this.state - const { exclusive } = this.props - - const itemsForRenderGenerator = (items = itemsFromProps, level = 1, parentId?: string) => { - const siblingSubtreeIds = [] - let subtreeAlreadyOpen = false + static autoControlledProps = ['activeItemIds'] + static getItemsForRender = _.memoize((itemsFromProps: ShorthandCollection) => { + // activeItemIds = [] // if we get new items, we reset the active items. + const itemsForRenderGenerator = ( + items = itemsFromProps, + level = 1, + parent?: ShorthandValue, + ) => { return _.reduce( items, - (acc: TreeItemForRenderProps[], item: ShorthandValue, index: number) => { - const isSubtree = !!item['items'] && item['items'].length > 0 - const id = item['id'] || `treeItemId${generatedId++}` - - activeItems.set(id, { siblingSubtreeIds }) + (acc: Object, item: ShorthandValue, index: number) => { + const id = item['id'] + const isSubtree = item['items'] && item['items'].length > 0 - if (isSubtree) { - const subtreeOpen = !!item['initialOpen'] - if (subtreeOpen && exclusive) { - // if exclusive, will open only first subtree. - subtreeAlreadyOpen = true - } - this.setActiveItem(id, { open: subtreeOpen && !subtreeAlreadyOpen }) - siblingSubtreeIds.push(id) - } - - acc.push({ - // initial item. - item, - // added props needed for a11y. + acc[id] = { + elementRef: React.createRef(), level, index, - siblingsLength: items.length, - parentId, - id, - // children items will go through the same process. - ...(isSubtree && { items: itemsForRenderGenerator(item['items'], level + 1, id) }), - }) - return acc + parent, + siblings: items.filter(currentItem => currentItem !== item), + } + + return { + ...acc, + ...(isSubtree ? itemsForRenderGenerator(item['items'], level + 1, item) : {}), + } }, - [], + {}, ) } - const itemsForRender = itemsForRenderGenerator() + return itemsForRenderGenerator(itemsFromProps) + }) + + static getAutoControlledStateFromProps(nextProps: TreeProps, prevState: TreeState) { + const { activeItemIds } = prevState + + const itemsForRender = Tree.getItemsForRender(nextProps.items) - /* Remove each item's id from its array of siblingSubtreeIds. */ - for (const key of Array.from(activeItems.keys())) { - this.setActiveItem(key, ({ siblingSubtreeIds }) => ({ - siblingSubtreeIds: siblingSubtreeIds.filter(id => id !== key), - })) + return { + itemsForRender, + activeItemIds, } + } - this.setState({ activeItems }) + getInitialAutoControlledState() { + return { activeItemIds: [] } + } - return itemsForRender - }) + treeRef = React.createRef() handleTreeItemOverrides = (predefinedProps: TreeItemProps) => ({ onTitleClick: ( @@ -188,48 +157,67 @@ class Tree extends AutoControlledComponent, TreeState> { treeItemProps: TreeItemProps, predefinedProps: TreeItemProps, ) => { - const { activeItems } = this.state - const { id, items } = treeItemProps + const { activeItemIds } = this.state + const { id, items, siblings } = treeItemProps + const { exclusive } = this.props - if (items && items.length > 0) { - this.closeSiblingWhenExlusive(id) - this.setActiveItem(id, ({ open }) => ({ open: !open })) + if (!items || items.length === 0) { + return + } - this.setState({ - activeItems, - }) + const indexOfActiveItem = activeItemIds.indexOf(id) + + if (indexOfActiveItem > -1) { + activeItemIds.splice(indexOfActiveItem, 1) + } else { + if (exclusive) { + siblings.some(sibling => { + const activeSiblingIndex = activeItemIds.indexOf(sibling['id']) + if (activeSiblingIndex > -1) { + activeItemIds.splice(activeSiblingIndex, 1) + return true + } + return false + }) + } + + activeItemIds.push(id) } + this.setState({ + activeItemIds, + }) + _.invoke(predefinedProps, 'onTitleClick', e, treeItemProps) }, onFocusParent: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { - const { parentId } = treeItemProps + const { parent } = treeItemProps - if (!parentId) { + if (!parent) { return } - const { activeItems } = this.state - const elementToBeFocused = activeItems.get(parentId).element + const { itemsForRender } = this.state + const elementToBeFocused = itemsForRender[parent['id']].elementRef if (!elementToBeFocused) { return } - elementToBeFocused.focus() + elementToBeFocused.current.focus() _.invoke(predefinedProps, 'onFocusParent', e, treeItemProps) }, onFocusFirstChild: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { const { id } = treeItemProps - const { activeItems } = this.state - const currentElement = activeItems.get(id).element + const { itemsForRender } = this.state + const currentElement = itemsForRender[id].elementRef - if (!currentElement) { + if (!currentElement && currentElement.current) { return } - const elementToBeFocused = getNextElement(this.treeRef.current, currentElement) + const elementToBeFocused = getNextElement(this.treeRef.current, currentElement.current) if (!elementToBeFocused) { return @@ -244,19 +232,21 @@ class Tree extends AutoControlledComponent, TreeState> { } const { id, items } = treeItemProps - const { activeItems } = this.state - const { siblingSubtreeIds } = activeItems.get(id) + const { itemsForRender, activeItemIds } = this.state + const { siblings } = itemsForRender[id] - siblingSubtreeIds.forEach(siblingSubtreeId => { - this.setActiveItem(siblingSubtreeId, { open: true }) + siblings.forEach(sibling => { + if (activeItemIds.indexOf(sibling['id']) < 0) { + activeItemIds.push(sibling['id']) + } }) - if (items && items.length > 0) { - this.setActiveItem(id, { open: true }) + if (items && items.length > 0 && activeItemIds.indexOf(id) < 0) { + activeItemIds.push(id) } this.setState({ - activeItems, + activeItemIds, }) _.invoke(predefinedProps, 'onSiblingsExpand', e, treeItemProps) @@ -264,63 +254,58 @@ class Tree extends AutoControlledComponent, TreeState> { }) renderContent() { - const { activeItems } = this.state + const { activeItemIds, itemsForRender } = this.state const { items, renderItemTitle } = this.props if (!items) return null - const renderItems = (itemsForRender: TreeItemForRenderProps[]) => { - return itemsForRender.reduce( - (renderedItems: any[], itemForRender: TreeItemForRenderProps, index: number) => { - const { item, items, parentId, id, ...rest } = itemForRender - const isFirstChild = index === 0 && !!parentId - const isSubtree = !!items && items.length > 0 - const isSubtreeOpen = isSubtree && activeItems.get(id).open - - const renderedItem = TreeItem.create(item, { - defaultProps: { - className: Tree.slotClassNames.item, - open: isSubtreeOpen, - id, - parentId, - renderItemTitle, - ...rest, - }, - overrideProps: this.handleTreeItemOverrides, - }) + const renderItems = (items: ShorthandCollection): any[] => { + return items.reduce((renderedItems: any[], item: ShorthandValue) => { + const itemForRender = itemsForRender[item['id']] + const items = item['items'] + const { elementRef, ...rest } = itemForRender + const isSubtree = !!items && items.length > 0 + const isSubtreeOpen = activeItemIds.indexOf(item['id']) > -1 + + const renderedItem = TreeItem.create(item, { + defaultProps: { + className: Tree.slotClassNames.item, + open: isSubtreeOpen, + renderItemTitle, + key: item['id'], + ...rest, + }, + overrideProps: this.handleTreeItemOverrides, + }) - // Only need refs of the items that spawn subtrees, when they need to be focused - // by any of their children, using Arrow Left. - const finalRenderedItem = - isSubtree || isFirstChild ? ( - { - this.setActiveItem(id, { element: itemElement }) - }} - > - {renderedItem} - - ) : ( - renderedItem - ) - - return [ - ...(renderedItems as any[]), - finalRenderedItem, - ...[isSubtreeOpen ? renderItems(items) : []], - ] - }, - [], - ) + // Only need refs of the items that spawn subtrees, when they need to be focused + // by any of their children, using Arrow Left. + const finalRenderedItem = isSubtree ? ( + { + handleRef(elementRef, itemElement) + }} + > + {renderedItem} + + ) : ( + renderedItem + ) + + return [ + ...(renderedItems as any[]), + finalRenderedItem, + ...[isSubtreeOpen ? renderItems(items) : []], + ] + }, []) } - return renderItems(this.getItemsForRender(items)) + return renderItems(items) } renderComponent({ ElementType, classes, accessibility, unhandledProps, styles, variables }) { const { children } = this.props - this.itemRefs = [] return ( @@ -336,54 +321,6 @@ class Tree extends AutoControlledComponent, TreeState> { ) } - - /** - * Similar to how setState works, merges changes on top of old value of an activeItem. - * - * @param id Id of the activeItem. - * @param changes Changes to be merged on top of old value or a callback that takes old - * value as param and returns a new value. - */ - setActiveItem( - id: string, - changes: ((oldValue: TreeActiveItemProps) => TreeActiveItemProps) | TreeActiveItemProps, - ) { - const { activeItems } = this.state - const activeItemValue = activeItems.get(id) - activeItems.set(id, { - ...activeItemValue, - ...(_.isFunction(changes) ? changes(activeItemValue) : changes), - }) - } - - /** - * In the case of exclusive tree, we will close the other open sibling at opening - * a tree item. - * - * @param id The id of the tree item to be opened. - */ - closeSiblingWhenExlusive(id: string) { - const { exclusive } = this.props - - if (!exclusive) { - return - } - - const { activeItems } = this.state - const activeItemValue = activeItems.get(id) - - if (activeItemValue.siblingSubtreeIds.length === 0) { - return - } - - const alreadyOpenSiblingId = activeItemValue.siblingSubtreeIds.find(siblingSubtreeId => { - return activeItems.get(siblingSubtreeId).open - }) - - if (alreadyOpenSiblingId) { - this.setActiveItem(alreadyOpenSiblingId, { open: false }) - } - } } Tree.create = createShorthandFactory({ diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx index 6f41f430a7..51861fb3c4 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -35,7 +35,7 @@ export interface TreeItemProps extends UIComponentProps, ChildrenComponentProps accessibility?: Accessibility /** Id needed to identify this item inside the Tree. */ - id?: string + id: string /** The index of the item among its siblings. */ index?: number @@ -65,10 +65,10 @@ export interface TreeItemProps extends UIComponentProps, ChildrenComponentProps open?: boolean /** The id of the parent tree item, if any. */ - parentId?: string + parent?: ShorthandValue /** Array with the ids of the tree item's siblings, if any. */ - siblingsLength?: number + siblings?: ShorthandCollection /** * A custom render iterator for rendering each tree title. @@ -100,7 +100,7 @@ class TreeItem extends UIComponent> { ...commonPropTypes.createCommon({ content: false, }), - id: PropTypes.string, + id: PropTypes.string.isRequired, index: PropTypes.number, initialOpen: PropTypes.bool, items: customPropTypes.collectionShorthand, @@ -110,9 +110,9 @@ class TreeItem extends UIComponent> { onFocusParent: PropTypes.func, onSiblingsExpand: PropTypes.func, open: PropTypes.bool, - parentId: PropTypes.string, + parent: customPropTypes.itemShorthand, renderItemTitle: PropTypes.func, - siblingsLength: PropTypes.number, + siblings: customPropTypes.collectionShorthand, title: customPropTypes.itemShorthand, treeItemRtlAttributes: PropTypes.func, } @@ -189,7 +189,7 @@ class TreeItem extends UIComponent> { }) renderContent() { - const { items, title, renderItemTitle, open, level, siblingsLength, index } = this.props + const { items, title, renderItemTitle, open, level, siblings, index } = this.props const hasSubtree = !_.isNil(items) && items.length > 0 return TreeTitle.create(title, { @@ -199,7 +199,7 @@ class TreeItem extends UIComponent> { hasSubtree, as: hasSubtree ? 'span' : 'a', level, - siblingsLength, + siblingsLength: siblings.length, index, }, render: renderItemTitle, diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts index fe45cea2a8..fc46b11f7c 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts @@ -25,7 +25,7 @@ const treeItemBehavior: Accessibility = props => ({ tabIndex: -1, [IS_FOCUSABLE_ATTRIBUTE]: true, role: 'treeitem', - 'aria-setsize': props.siblingsLength, + 'aria-setsize': props.siblings.length, 'aria-posinset': props.index + 1, 'aria-level': props.level, }), @@ -64,7 +64,7 @@ export type TreeItemBehaviorProps = { items?: object[] /** If item is a subtree, it indicates if it's open. */ open?: boolean - siblingsLength?: number + siblings?: object[] level?: number index?: number } From 1883352106d07b3259bd1faf76b16e11cfaeaa00 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Thu, 29 Aug 2019 09:48:38 +0200 Subject: [PATCH 31/47] fixed examples --- .../Tree/Types/TreeExample.shorthand.tsx | 8 +-- .../TreeInitiallyOpenExample.shorthand.tsx | 8 +-- .../test/specs/components/Tree/Tree-test.tsx | 59 +++++-------------- 3 files changed, 24 insertions(+), 51 deletions(-) diff --git a/docs/src/examples/components/Tree/Types/TreeExample.shorthand.tsx b/docs/src/examples/components/Tree/Types/TreeExample.shorthand.tsx index 4f2357b5a3..f226402d91 100644 --- a/docs/src/examples/components/Tree/Types/TreeExample.shorthand.tsx +++ b/docs/src/examples/components/Tree/Types/TreeExample.shorthand.tsx @@ -25,19 +25,19 @@ const items = [ ], }, { - id: '21', + id: '12', title: 'Kevan', items: [ { - id: '211', + id: '121', title: 'Lancel', }, { - id: '212', + id: '122', title: 'Willem', }, { - id: '213', + id: '123', title: 'Martyn', }, ], diff --git a/docs/src/examples/components/Tree/Types/TreeInitiallyOpenExample.shorthand.tsx b/docs/src/examples/components/Tree/Types/TreeInitiallyOpenExample.shorthand.tsx index 1f8dcb699c..99a9022cd7 100644 --- a/docs/src/examples/components/Tree/Types/TreeInitiallyOpenExample.shorthand.tsx +++ b/docs/src/examples/components/Tree/Types/TreeInitiallyOpenExample.shorthand.tsx @@ -26,20 +26,20 @@ const items = [ ], }, { - id: '21', + id: '12', title: 'Kevan', initialOpen: true, items: [ { - id: '211', + id: '121', title: 'Lancel', }, { - id: '212', + id: '121', title: 'Willem', }, { - id: '213', + id: '123', title: 'Martyn', }, ], diff --git a/packages/react/test/specs/components/Tree/Tree-test.tsx b/packages/react/test/specs/components/Tree/Tree-test.tsx index dd88ad5040..7818b7d5d3 100644 --- a/packages/react/test/specs/components/Tree/Tree-test.tsx +++ b/packages/react/test/specs/components/Tree/Tree-test.tsx @@ -10,19 +10,19 @@ import { ReactWrapper, CommonWrapper } from 'enzyme' const items = [ { - key: '1', + id: '1', title: '1', items: [ { - key: '11', + id: '11', title: '11', }, { - key: '12', + id: '12', title: '12', items: [ { - key: '121', + id: '121', title: '121', }, ], @@ -30,27 +30,27 @@ const items = [ ], }, { - key: '2', + id: '2', title: '2', items: [ { - key: '21', + id: '21', title: '21', items: [ { - key: '211', + id: '211', title: '211', }, ], }, { - key: '22', + id: '22', title: '22', }, ], }, { - key: '3', + id: '3', title: '3', }, ] @@ -72,15 +72,7 @@ const checkOpenTitles = (wrapper: ReactWrapper, expected: string[]): void => { describe('Tree', () => { isConformant(Tree) - describe('activeItems', () => { - it('should have item already open by passing initialOpen', () => { - const copiedItems = JSON.parse(JSON.stringify(items)) - copiedItems[1]['initialOpen'] = true - const wrapper = mountWithProvider() - - checkOpenTitles(wrapper, ['1', '2', '21', '22', '3']) - }) - + describe('activeItemIds', () => { it('should contain index of item open at click', () => { const wrapper = mountWithProvider() @@ -96,10 +88,7 @@ describe('Tree', () => { }) it('should have index of item removed when closed at click', () => { - const copiedItems = JSON.parse(JSON.stringify(items)) - copiedItems[0]['initialOpen'] = true - copiedItems[1]['initialOpen'] = true - const wrapper = mountWithProvider() + const wrapper = mountWithProvider() getTitles(wrapper) .at(0) // title 1 @@ -131,10 +120,7 @@ describe('Tree', () => { }) it('should have index of item removed if closed by ArrowLeft', () => { - const copiedItems = JSON.parse(JSON.stringify(items)) - copiedItems[0]['initialOpen'] = true - copiedItems[1]['initialOpen'] = true - const wrapper = mountWithProvider() + const wrapper = mountWithProvider() getItems(wrapper) .at(0) // title 1 @@ -152,9 +138,7 @@ describe('Tree', () => { }) it('should expand subtrees only on current level on asterisk key', () => { - const copiedItems = JSON.parse(JSON.stringify(items)) - copiedItems[0]['initialOpen'] = true - const wrapper = mountWithProvider() + const wrapper = mountWithProvider() getTitles(wrapper) .at(1) // title 11 @@ -163,25 +147,14 @@ describe('Tree', () => { }) it('should not be changed on asterisk key if all siblings are already expanded', () => { - const copiedItems = JSON.parse(JSON.stringify(items)) - copiedItems[0]['initialOpen'] = true - copiedItems[1]['initialOpen'] = true - copiedItems[2]['initialOpen'] = true - const wrapper = mountWithProvider() + const wrapper = mountWithProvider( + , + ) getTitles(wrapper) .at(0) // title 1 .simulate('keydown', { keyCode: keyboardKey['*'] }) checkOpenTitles(wrapper, ['1', '11', '12', '2', '21', '22', '3']) }) - - it('should expand siblings even if the item is not expandable', () => { - const wrapper = mountWithProvider() - - getTitles(wrapper) - .at(2) // title 3 - .simulate('keydown', { keyCode: keyboardKey['*'] }) - checkOpenTitles(wrapper, ['1', '11', '12', '2', '21', '22', '3']) - }) }) }) From 01e0535f32c9347059e6895d1aef3acd22df1f81 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Thu, 29 Aug 2019 09:49:56 +0200 Subject: [PATCH 32/47] fixed some a11y issues --- packages/react/src/components/Tree/Tree.tsx | 14 ++++++++------ .../Behaviors/Tree/treeItemBehavior.ts | 2 +- .../Behaviors/Tree/treeTitleBehavior.ts | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 60a649b62c..219e9c2be8 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -60,7 +60,6 @@ export interface TreeItemForRenderProps { elementRef: React.RefObject id: string index: number - items: TreeItemForRenderProps[] level: number parent: ShorthandValue siblings: ShorthandCollection @@ -231,12 +230,15 @@ class Tree extends AutoControlledComponent, TreeState> { return } - const { id, items } = treeItemProps - const { itemsForRender, activeItemIds } = this.state - const { siblings } = itemsForRender[id] + const { id, items, siblings } = treeItemProps + const { activeItemIds } = this.state siblings.forEach(sibling => { - if (activeItemIds.indexOf(sibling['id']) < 0) { + if ( + sibling['items'] && + sibling['items'].length > 0 && + activeItemIds.indexOf(sibling['id']) < 0 + ) { activeItemIds.push(sibling['id']) } }) @@ -265,7 +267,7 @@ class Tree extends AutoControlledComponent, TreeState> { const items = item['items'] const { elementRef, ...rest } = itemForRender const isSubtree = !!items && items.length > 0 - const isSubtreeOpen = activeItemIds.indexOf(item['id']) > -1 + const isSubtreeOpen = isSubtree && activeItemIds.indexOf(item['id']) > -1 const renderedItem = TreeItem.create(item, { defaultProps: { diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts index fc46b11f7c..5741e07e71 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts @@ -25,7 +25,7 @@ const treeItemBehavior: Accessibility = props => ({ tabIndex: -1, [IS_FOCUSABLE_ATTRIBUTE]: true, role: 'treeitem', - 'aria-setsize': props.siblings.length, + 'aria-setsize': props.siblings.length + 1, 'aria-posinset': props.index + 1, 'aria-level': props.level, }), diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts index b93870db0c..fcfac75fe4 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts @@ -17,7 +17,7 @@ const treeTitleBehavior: Accessibility = props => ({ tabIndex: -1, [IS_FOCUSABLE_ATTRIBUTE]: true, role: 'treeitem', - 'aria-setsize': props.siblingsLength, + 'aria-setsize': props.siblingsLength + 1, 'aria-posinset': props.index + 1, 'aria-level': props.level, }), From dc19756540ff77bdfc82f6ed6885af0e4263df80 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Thu, 29 Aug 2019 10:10:42 +0200 Subject: [PATCH 33/47] extracted logic into a method --- packages/react/src/components/Tree/Tree.tsx | 27 +++++++++------------ 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 219e9c2be8..70ca57bcf3 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -100,8 +100,11 @@ class Tree extends AutoControlledComponent, TreeState> { static autoControlledProps = ['activeItemIds'] + static isSubtree(item: TreeItemProps | ShorthandValue): boolean { + return !!item['items'] && item['items'].length > 0 + } + static getItemsForRender = _.memoize((itemsFromProps: ShorthandCollection) => { - // activeItemIds = [] // if we get new items, we reset the active items. const itemsForRenderGenerator = ( items = itemsFromProps, level = 1, @@ -111,7 +114,7 @@ class Tree extends AutoControlledComponent, TreeState> { items, (acc: Object, item: ShorthandValue, index: number) => { const id = item['id'] - const isSubtree = item['items'] && item['items'].length > 0 + const isSubtree = Tree.isSubtree(item) acc[id] = { elementRef: React.createRef(), @@ -134,13 +137,10 @@ class Tree extends AutoControlledComponent, TreeState> { }) static getAutoControlledStateFromProps(nextProps: TreeProps, prevState: TreeState) { - const { activeItemIds } = prevState - const itemsForRender = Tree.getItemsForRender(nextProps.items) return { itemsForRender, - activeItemIds, } } @@ -157,10 +157,10 @@ class Tree extends AutoControlledComponent, TreeState> { predefinedProps: TreeItemProps, ) => { const { activeItemIds } = this.state - const { id, items, siblings } = treeItemProps + const { id, siblings } = treeItemProps const { exclusive } = this.props - if (!items || items.length === 0) { + if (!Tree.isSubtree(treeItemProps)) { return } @@ -230,20 +230,16 @@ class Tree extends AutoControlledComponent, TreeState> { return } - const { id, items, siblings } = treeItemProps + const { id, siblings } = treeItemProps const { activeItemIds } = this.state siblings.forEach(sibling => { - if ( - sibling['items'] && - sibling['items'].length > 0 && - activeItemIds.indexOf(sibling['id']) < 0 - ) { + if (Tree.isSubtree(sibling) && activeItemIds.indexOf(sibling['id']) < 0) { activeItemIds.push(sibling['id']) } }) - if (items && items.length > 0 && activeItemIds.indexOf(id) < 0) { + if (Tree.isSubtree(treeItemProps) && activeItemIds.indexOf(id) < 0) { activeItemIds.push(id) } @@ -264,9 +260,8 @@ class Tree extends AutoControlledComponent, TreeState> { const renderItems = (items: ShorthandCollection): any[] => { return items.reduce((renderedItems: any[], item: ShorthandValue) => { const itemForRender = itemsForRender[item['id']] - const items = item['items'] const { elementRef, ...rest } = itemForRender - const isSubtree = !!items && items.length > 0 + const isSubtree = Tree.isSubtree(item) const isSubtreeOpen = isSubtree && activeItemIds.indexOf(item['id']) > -1 const renderedItem = TreeItem.create(item, { From 424a866a53997b51e2f05d50c20489c6089b4538 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Thu, 29 Aug 2019 10:23:04 +0200 Subject: [PATCH 34/47] fixed initial open state --- .../Tree/Types/TreeInitiallyOpenExample.shorthand.tsx | 7 +++---- packages/react/src/components/Tree/Tree.tsx | 6 +++--- packages/react/src/components/Tree/TreeItem.tsx | 4 ---- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/docs/src/examples/components/Tree/Types/TreeInitiallyOpenExample.shorthand.tsx b/docs/src/examples/components/Tree/Types/TreeInitiallyOpenExample.shorthand.tsx index 99a9022cd7..037abe91f0 100644 --- a/docs/src/examples/components/Tree/Types/TreeInitiallyOpenExample.shorthand.tsx +++ b/docs/src/examples/components/Tree/Types/TreeInitiallyOpenExample.shorthand.tsx @@ -5,7 +5,6 @@ const items = [ { id: '1', title: 'House Lannister', - initialOpen: true, items: [ { id: '11', @@ -28,7 +27,6 @@ const items = [ { id: '12', title: 'Kevan', - initialOpen: true, items: [ { id: '121', @@ -53,7 +51,6 @@ const items = [ { id: '21', title: 'Aerys', - initialOpen: true, items: [ { id: '211', @@ -73,6 +70,8 @@ const items = [ }, ] -const TreeInitiallyOpenExampleShorthand = () => +const TreeInitiallyOpenExampleShorthand = () => ( + +) export default TreeInitiallyOpenExampleShorthand diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 70ca57bcf3..7e672052ab 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -260,7 +260,7 @@ class Tree extends AutoControlledComponent, TreeState> { const renderItems = (items: ShorthandCollection): any[] => { return items.reduce((renderedItems: any[], item: ShorthandValue) => { const itemForRender = itemsForRender[item['id']] - const { elementRef, ...rest } = itemForRender + const { elementRef, ...restItemForRender } = itemForRender const isSubtree = Tree.isSubtree(item) const isSubtreeOpen = isSubtree && activeItemIds.indexOf(item['id']) > -1 @@ -270,7 +270,7 @@ class Tree extends AutoControlledComponent, TreeState> { open: isSubtreeOpen, renderItemTitle, key: item['id'], - ...rest, + ...restItemForRender, }, overrideProps: this.handleTreeItemOverrides, }) @@ -293,7 +293,7 @@ class Tree extends AutoControlledComponent, TreeState> { return [ ...(renderedItems as any[]), finalRenderedItem, - ...[isSubtreeOpen ? renderItems(items) : []], + ...[isSubtreeOpen ? renderItems(item['items']) : []], ] }, []) } diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx index 51861fb3c4..507d67032b 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -40,9 +40,6 @@ export interface TreeItemProps extends UIComponentProps, ChildrenComponentProps /** The index of the item among its siblings. */ index?: number - /** Initial open state. */ - initialOpen?: boolean - /** Array of props for sub tree. */ items?: ShorthandCollection @@ -102,7 +99,6 @@ class TreeItem extends UIComponent> { }), id: PropTypes.string.isRequired, index: PropTypes.number, - initialOpen: PropTypes.bool, items: customPropTypes.collectionShorthand, level: PropTypes.number, onTitleClick: PropTypes.func, From 49d35fd428da154970744e718036483966bc847f Mon Sep 17 00:00:00 2001 From: silviuavram Date: Thu, 29 Aug 2019 10:26:20 +0200 Subject: [PATCH 35/47] fixed duplicated key --- .../Tree/Types/TreeInitiallyOpenExample.shorthand.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/examples/components/Tree/Types/TreeInitiallyOpenExample.shorthand.tsx b/docs/src/examples/components/Tree/Types/TreeInitiallyOpenExample.shorthand.tsx index 037abe91f0..008855f042 100644 --- a/docs/src/examples/components/Tree/Types/TreeInitiallyOpenExample.shorthand.tsx +++ b/docs/src/examples/components/Tree/Types/TreeInitiallyOpenExample.shorthand.tsx @@ -33,7 +33,7 @@ const items = [ title: 'Lancel', }, { - id: '121', + id: '122', title: 'Willem', }, { From 92f5fb41e3289222015d3b3b78d6823c541d3a16 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Thu, 29 Aug 2019 10:30:17 +0200 Subject: [PATCH 36/47] fixed some comments --- packages/react/src/components/Tree/TreeTitle.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/react/src/components/Tree/TreeTitle.tsx b/packages/react/src/components/Tree/TreeTitle.tsx index 4befa8e9ae..bc14bf4c22 100644 --- a/packages/react/src/components/Tree/TreeTitle.tsx +++ b/packages/react/src/components/Tree/TreeTitle.tsx @@ -24,13 +24,13 @@ export interface TreeTitleProps /** Accessibility behavior if overridden by the user. */ accessibility?: Accessibility - /** Whether or not the item has a subtree. */ + /** Whether or not the title has a subtree. */ hasSubtree?: boolean - /** The index of the item among its siblings. */ + /** The index of the title among its siblings. */ index?: number - /** Level of the tree/subtree that contains this item. */ + /** Level of the tree/subtree that contains this title. */ level?: number /** @@ -41,10 +41,10 @@ export interface TreeTitleProps */ onClick?: ComponentEventHandler - /** Whether or not the subtree of the item is in the open state. */ + /** Whether or not the subtree of the title is in the open state. */ open?: boolean - /** Array with the ids of the tree item's siblings, if any. */ + /** Size of this title's siblings. */ siblingsLength?: number } From 0339f9fafcfb6677c0e7b3b1e2314d50d5c7e4ba Mon Sep 17 00:00:00 2001 From: silviuavram Date: Thu, 29 Aug 2019 11:12:12 +0200 Subject: [PATCH 37/47] prevent mutation --- packages/react/src/components/Tree/Tree.tsx | 28 +++++++++++---------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 7e672052ab..691be66e3f 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -151,12 +151,8 @@ class Tree extends AutoControlledComponent, TreeState> { treeRef = React.createRef() handleTreeItemOverrides = (predefinedProps: TreeItemProps) => ({ - onTitleClick: ( - e: React.SyntheticEvent, - treeItemProps: TreeItemProps, - predefinedProps: TreeItemProps, - ) => { - const { activeItemIds } = this.state + onTitleClick: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { + let { activeItemIds } = this.state const { id, siblings } = treeItemProps const { exclusive } = this.props @@ -164,23 +160,29 @@ class Tree extends AutoControlledComponent, TreeState> { return } - const indexOfActiveItem = activeItemIds.indexOf(id) + const activeItemIdIndex = activeItemIds.indexOf(id) - if (indexOfActiveItem > -1) { - activeItemIds.splice(indexOfActiveItem, 1) + if (activeItemIdIndex > -1) { + activeItemIds = [ + ...activeItemIds.slice(0, activeItemIdIndex), + ...activeItemIds.slice(activeItemIdIndex + 1), + ] } else { if (exclusive) { siblings.some(sibling => { - const activeSiblingIndex = activeItemIds.indexOf(sibling['id']) - if (activeSiblingIndex > -1) { - activeItemIds.splice(activeSiblingIndex, 1) + const activeSiblingIdIndex = activeItemIds.indexOf(sibling['id']) + if (activeSiblingIdIndex > -1) { + activeItemIds = [ + ...activeItemIds.slice(0, activeSiblingIdIndex), + ...activeItemIds.slice(activeSiblingIdIndex + 1), + ] return true } return false }) } - activeItemIds.push(id) + activeItemIds = [...activeItemIds, id] } this.setState({ From 6e88bc4750b006350eaa180e1852703f04002f8a Mon Sep 17 00:00:00 2001 From: silviuavram Date: Thu, 29 Aug 2019 13:05:41 +0200 Subject: [PATCH 38/47] updates on tree item state --- packages/react/src/components/Tree/Tree.tsx | 31 +++++------- .../react/src/components/Tree/TreeItem.tsx | 50 ++++++++++--------- .../react/src/components/Tree/TreeTitle.tsx | 8 +-- .../Behaviors/Tree/treeItemBehavior.ts | 28 +++++------ .../Behaviors/Tree/treeTitleBehavior.ts | 6 +-- 5 files changed, 59 insertions(+), 64 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 691be66e3f..a6c17e1c4f 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -2,7 +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 { handleRef, Ref } from '@stardust-ui/react-component-ref' +import { Ref } from '@stardust-ui/react-component-ref' import TreeItem, { TreeItemProps } from './TreeItem' import { @@ -67,7 +67,7 @@ export interface TreeItemForRenderProps { export interface TreeState { activeItemIds: string[] - itemsForRender: { [s: string]: TreeItemForRenderProps } + itemsForRender: Record } class Tree extends AutoControlledComponent, TreeState> { @@ -100,8 +100,8 @@ class Tree extends AutoControlledComponent, TreeState> { static autoControlledProps = ['activeItemIds'] - static isSubtree(item: TreeItemProps | ShorthandValue): boolean { - return !!item['items'] && item['items'].length > 0 + static hasSubtree(item: TreeItemProps | ShorthandValue): boolean { + return !_.isNil(item['items']) && item['items'].length > 0 } static getItemsForRender = _.memoize((itemsFromProps: ShorthandCollection) => { @@ -114,12 +114,12 @@ class Tree extends AutoControlledComponent, TreeState> { items, (acc: Object, item: ShorthandValue, index: number) => { const id = item['id'] - const isSubtree = Tree.isSubtree(item) + const isSubtree = Tree.hasSubtree(item) acc[id] = { elementRef: React.createRef(), level, - index, + index: index + 1, // Used for aria-posinset and it's 1-based. parent, siblings: items.filter(currentItem => currentItem !== item), } @@ -156,7 +156,7 @@ class Tree extends AutoControlledComponent, TreeState> { const { id, siblings } = treeItemProps const { exclusive } = this.props - if (!Tree.isSubtree(treeItemProps)) { + if (!Tree.hasSubtree(treeItemProps)) { return } @@ -236,12 +236,12 @@ class Tree extends AutoControlledComponent, TreeState> { const { activeItemIds } = this.state siblings.forEach(sibling => { - if (Tree.isSubtree(sibling) && activeItemIds.indexOf(sibling['id']) < 0) { + if (Tree.hasSubtree(sibling) && activeItemIds.indexOf(sibling['id']) < 0) { activeItemIds.push(sibling['id']) } }) - if (Tree.isSubtree(treeItemProps) && activeItemIds.indexOf(id) < 0) { + if (Tree.hasSubtree(treeItemProps) && activeItemIds.indexOf(id) < 0) { activeItemIds.push(id) } @@ -253,17 +253,17 @@ class Tree extends AutoControlledComponent, TreeState> { }, }) - renderContent() { + renderContent(): React.ReactElement[] { const { activeItemIds, itemsForRender } = this.state const { items, renderItemTitle } = this.props if (!items) return null - const renderItems = (items: ShorthandCollection): any[] => { + const renderItems = (items: ShorthandCollection): React.ReactElement[] => { return items.reduce((renderedItems: any[], item: ShorthandValue) => { const itemForRender = itemsForRender[item['id']] const { elementRef, ...restItemForRender } = itemForRender - const isSubtree = Tree.isSubtree(item) + const isSubtree = Tree.hasSubtree(item) const isSubtreeOpen = isSubtree && activeItemIds.indexOf(item['id']) > -1 const renderedItem = TreeItem.create(item, { @@ -280,12 +280,7 @@ class Tree extends AutoControlledComponent, TreeState> { // Only need refs of the items that spawn subtrees, when they need to be focused // by any of their children, using Arrow Left. const finalRenderedItem = isSubtree ? ( - { - handleRef(elementRef, itemElement) - }} - > + {renderedItem} ) : ( diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx index 507d67032b..98ece63d7e 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -3,6 +3,7 @@ import * as _ from 'lodash' import * as PropTypes from 'prop-types' import * as React from 'react' +import Tree from './Tree' import TreeTitle, { TreeTitleProps } from './TreeTitle' import { treeItemBehavior } from '../../lib/accessibility' import { Accessibility } from '../../lib/accessibility/types' @@ -37,7 +38,7 @@ export interface TreeItemProps extends UIComponentProps, ChildrenComponentProps /** Id needed to identify this item inside the Tree. */ id: string - /** The index of the item among its siblings. */ + /** The index of the item among its siblings. Count starts at 1. */ index?: number /** Array of props for sub tree. */ @@ -81,7 +82,12 @@ export interface TreeItemProps extends UIComponentProps, ChildrenComponentProps title?: ShorthandValue } -class TreeItem extends UIComponent> { +export interface TreeState { + treeSize: number // size of the tree without children. + hasSubtree: boolean +} + +class TreeItem extends UIComponent, TreeState> { static create: Function static displayName = 'TreeItem' @@ -118,6 +124,18 @@ class TreeItem extends UIComponent> { accessibility: treeItemBehavior, } + state = { + hasSubtree: false, + treeSize: 0, + } + + static getDerivedStateFromProps(props: TreeItemProps) { + return { + hasSubtree: Tree.hasSubtree(props), + treeSize: props.siblings.length + 1, + } + } + actionHandlers = { performClick: e => { e.preventDefault() @@ -129,7 +147,7 @@ class TreeItem extends UIComponent> { e.preventDefault() e.stopPropagation() - this.handleParentFocus(e) + _.invoke(this.props, 'onFocusParent', e, this.props) }, collapse: e => { e.preventDefault() @@ -147,36 +165,20 @@ class TreeItem extends UIComponent> { e.preventDefault() e.stopPropagation() - this.handleFocusFirstChild(e) + _.invoke(this.props, 'onFocusFirstChild', e, this.props) }, expandSiblings: e => { e.preventDefault() e.stopPropagation() - this.handleSiblingsExpand(e) + _.invoke(this.props, 'onSiblingsExpand', e, this.props) }, } - eventComesFromChildItem = e => { - return e.currentTarget !== e.target - } - handleTitleClick = e => { _.invoke(this.props, 'onTitleClick', e, this.props) } - handleParentFocus = e => { - _.invoke(this.props, 'onFocusParent', e, this.props) - } - - handleFocusFirstChild = e => { - _.invoke(this.props, 'onFocusFirstChild', e, this.props) - } - - handleSiblingsExpand = e => { - _.invoke(this.props, 'onSiblingsExpand', e, this.props) - } - handleTitleOverrides = (predefinedProps: TreeTitleProps) => ({ onClick: (e, titleProps) => { this.handleTitleClick(e) @@ -185,8 +187,8 @@ class TreeItem extends UIComponent> { }) renderContent() { - const { items, title, renderItemTitle, open, level, siblings, index } = this.props - const hasSubtree = !_.isNil(items) && items.length > 0 + const { title, renderItemTitle, open, level, index } = this.props + const { hasSubtree, treeSize } = this.state return TreeTitle.create(title, { defaultProps: { @@ -195,7 +197,7 @@ class TreeItem extends UIComponent> { hasSubtree, as: hasSubtree ? 'span' : 'a', level, - siblingsLength: siblings.length, + treeSize, index, }, render: renderItemTitle, diff --git a/packages/react/src/components/Tree/TreeTitle.tsx b/packages/react/src/components/Tree/TreeTitle.tsx index bc14bf4c22..3fc5c6d8ea 100644 --- a/packages/react/src/components/Tree/TreeTitle.tsx +++ b/packages/react/src/components/Tree/TreeTitle.tsx @@ -27,7 +27,7 @@ export interface TreeTitleProps /** Whether or not the title has a subtree. */ hasSubtree?: boolean - /** The index of the title among its siblings. */ + /** The index of the title among its siblings. Count starts at 1. */ index?: number /** Level of the tree/subtree that contains this title. */ @@ -44,8 +44,8 @@ export interface TreeTitleProps /** Whether or not the subtree of the title is in the open state. */ open?: boolean - /** Size of this title's siblings. */ - siblingsLength?: number + /** Size of the tree containing this title without any children. */ + treeSize?: number } class TreeTitle extends UIComponent> { @@ -62,7 +62,7 @@ class TreeTitle extends UIComponent> { level: PropTypes.number, onClick: PropTypes.func, open: PropTypes.bool, - siblingsLength: PropTypes.number, + treeSize: PropTypes.number, } static defaultProps = { diff --git a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts index 5741e07e71..ba3702487b 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeItemBehavior.ts @@ -19,16 +19,15 @@ const treeItemBehavior: Accessibility = props => ({ attributes: { root: { role: 'none', - ...(props.items && - props.items.length && { - 'aria-expanded': props.open, - tabIndex: -1, - [IS_FOCUSABLE_ATTRIBUTE]: true, - role: 'treeitem', - 'aria-setsize': props.siblings.length + 1, - 'aria-posinset': props.index + 1, - 'aria-level': props.level, - }), + ...(props.hasSubtree && { + 'aria-expanded': props.open, + tabIndex: -1, + [IS_FOCUSABLE_ATTRIBUTE]: true, + role: 'treeitem', + 'aria-setsize': props.treeSize, + 'aria-posinset': props.index, + 'aria-level': props.level, + }), }, }, keyActions: { @@ -60,19 +59,18 @@ const treeItemBehavior: Accessibility = props => ({ }) export type TreeItemBehaviorProps = { - /** If item is a subtree, it contains items. */ - items?: object[] /** If item is a subtree, it indicates if it's open. */ open?: boolean - siblings?: object[] level?: number index?: number + hasSubtree?: boolean + treeSize?: number } /** Checks if current tree item has a subtree and it is opened */ const isSubtreeOpen = (props: TreeItemBehaviorProps): boolean => { - const { items, open } = props - return !!(items && items.length && open) + const { hasSubtree, open } = props + return !!(hasSubtree && open) } 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 fcfac75fe4..47708b2640 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Tree/treeTitleBehavior.ts @@ -17,8 +17,8 @@ const treeTitleBehavior: Accessibility = props => ({ tabIndex: -1, [IS_FOCUSABLE_ATTRIBUTE]: true, role: 'treeitem', - 'aria-setsize': props.siblingsLength + 1, - 'aria-posinset': props.index + 1, + 'aria-setsize': props.treeSize, + 'aria-posinset': props.index, 'aria-level': props.level, }), }, @@ -38,6 +38,6 @@ type TreeTitleBehavior = { /** Indicated if tree title has a subtree */ hasSubtree?: boolean level?: number - siblingsLength?: number + treeSize?: number index?: number } From a14f76a50ee7a7e1527b430034456976b816f063 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Thu, 29 Aug 2019 13:10:39 +0200 Subject: [PATCH 39/47] fix behavior tests --- packages/react/test/specs/behaviors/testDefinitions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/test/specs/behaviors/testDefinitions.ts b/packages/react/test/specs/behaviors/testDefinitions.ts index 1a6a8999c7..feb4fa5add 100644 --- a/packages/react/test/specs/behaviors/testDefinitions.ts +++ b/packages/react/test/specs/behaviors/testDefinitions.ts @@ -558,7 +558,7 @@ definitions.push({ regexp: /Triggers '(\w+)' action with '(\w+)' on '([\w-]+)', when has an opened subtree\./g, testMethod: (parameters: TestMethod) => { const [action, key, elementToPerformAction] = [...parameters.props] - const propertyOpenedSubtree = { open: true, items: [{ a: 1 }], siblings: [] } + const propertyOpenedSubtree = { open: true, items: [{ a: 1 }], siblings: [], hasSubtree: true } const expectedKeyNumberVertical = parameters.behavior(propertyOpenedSubtree).keyActions[ elementToPerformAction ][action].keyCombinations[0].keyCode @@ -571,7 +571,7 @@ definitions.push({ regexp: /Triggers '(\w+)' action with '(\w+)' on '([\w-]+)', when has a closed subtree\./g, testMethod: (parameters: TestMethod) => { const [action, key, elementToPerformAction] = [...parameters.props] - const propertyClosedSubtree = { open: false } + const propertyClosedSubtree = { open: false, hasSubtree: false } const expectedKeyNumberVertical = parameters.behavior(propertyClosedSubtree).keyActions[ elementToPerformAction ][action].keyCombinations[0].keyCode From 8633f0e448803da12e138a36564fd3af134e5c06 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Thu, 29 Aug 2019 13:26:21 +0200 Subject: [PATCH 40/47] fixed a bad interface name --- 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 98ece63d7e..f7362f3d0d 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -82,12 +82,12 @@ export interface TreeItemProps extends UIComponentProps, ChildrenComponentProps title?: ShorthandValue } -export interface TreeState { +export interface TreeItemState { treeSize: number // size of the tree without children. hasSubtree: boolean } -class TreeItem extends UIComponent, TreeState> { +class TreeItem extends UIComponent, TreeItemState> { static create: Function static displayName = 'TreeItem' From eb925770b8a4eae2baea1cc482a9d7e597a74f8e Mon Sep 17 00:00:00 2001 From: silviuavram Date: Thu, 29 Aug 2019 13:42:50 +0200 Subject: [PATCH 41/47] small improvements --- packages/react/src/components/Tree/Tree.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index a6c17e1c4f..e5bca808c8 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -152,14 +152,14 @@ class Tree extends AutoControlledComponent, TreeState> { handleTreeItemOverrides = (predefinedProps: TreeItemProps) => ({ onTitleClick: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { - let { activeItemIds } = this.state - const { id, siblings } = treeItemProps - const { exclusive } = this.props - if (!Tree.hasSubtree(treeItemProps)) { return } + let { activeItemIds } = this.state + const { id, siblings } = treeItemProps + const { exclusive } = this.props + const activeItemIdIndex = activeItemIds.indexOf(id) if (activeItemIdIndex > -1) { @@ -214,7 +214,7 @@ class Tree extends AutoControlledComponent, TreeState> { const { itemsForRender } = this.state const currentElement = itemsForRender[id].elementRef - if (!currentElement && currentElement.current) { + if (!currentElement || !currentElement.current) { return } From d15756ae8f4becfc5000214f3144daa3ed38b899 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Thu, 29 Aug 2019 15:39:47 +0200 Subject: [PATCH 42/47] using lib methods --- packages/react/src/components/Tree/Tree.tsx | 39 +++++++++---------- .../react/src/components/Tree/lib/index.ts | 13 +++++++ 2 files changed, 31 insertions(+), 21 deletions(-) create mode 100644 packages/react/src/components/Tree/lib/index.ts diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index e5bca808c8..5f907e888f 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -25,6 +25,7 @@ import { import { Accessibility } from '../../lib/accessibility/types' import { treeBehavior } from '../../lib/accessibility' import { getNextElement } from '../../lib/accessibility/FocusZone/focusUtilities' +import { hasSubtree, removeItemAtIndex } from './lib' export interface TreeSlotClassNames { item: string @@ -34,10 +35,10 @@ export interface TreeProps extends UIComponentProps, ChildrenComponentProps { /** Accessibility behavior if overridden by the user. */ accessibility?: Accessibility - /** Map with the subtrees and information related to their open/closed state. */ + /** Ids of opened items. */ activeItemIds?: string[] - /** Initial activeIndex value. */ + /** Initial activeItemIds value. */ defaultActiveItemIds?: string[] /** Only allow one subtree to be open at a time. */ @@ -100,10 +101,6 @@ class Tree extends AutoControlledComponent, TreeState> { static autoControlledProps = ['activeItemIds'] - static hasSubtree(item: TreeItemProps | ShorthandValue): boolean { - return !_.isNil(item['items']) && item['items'].length > 0 - } - static getItemsForRender = _.memoize((itemsFromProps: ShorthandCollection) => { const itemsForRenderGenerator = ( items = itemsFromProps, @@ -114,7 +111,7 @@ class Tree extends AutoControlledComponent, TreeState> { items, (acc: Object, item: ShorthandValue, index: number) => { const id = item['id'] - const isSubtree = Tree.hasSubtree(item) + const isSubtree = hasSubtree(item) acc[id] = { elementRef: React.createRef(), @@ -152,7 +149,7 @@ class Tree extends AutoControlledComponent, TreeState> { handleTreeItemOverrides = (predefinedProps: TreeItemProps) => ({ onTitleClick: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { - if (!Tree.hasSubtree(treeItemProps)) { + if (!hasSubtree(treeItemProps)) { return } @@ -163,19 +160,14 @@ class Tree extends AutoControlledComponent, TreeState> { const activeItemIdIndex = activeItemIds.indexOf(id) if (activeItemIdIndex > -1) { - activeItemIds = [ - ...activeItemIds.slice(0, activeItemIdIndex), - ...activeItemIds.slice(activeItemIdIndex + 1), - ] + activeItemIds = removeItemAtIndex(activeItemIds, activeItemIdIndex) } else { if (exclusive) { siblings.some(sibling => { const activeSiblingIdIndex = activeItemIds.indexOf(sibling['id']) if (activeSiblingIdIndex > -1) { - activeItemIds = [ - ...activeItemIds.slice(0, activeSiblingIdIndex), - ...activeItemIds.slice(activeSiblingIdIndex + 1), - ] + activeItemIds = removeItemAtIndex(activeItemIds, activeSiblingIdIndex) + return true } return false @@ -236,12 +228,12 @@ class Tree extends AutoControlledComponent, TreeState> { const { activeItemIds } = this.state siblings.forEach(sibling => { - if (Tree.hasSubtree(sibling) && activeItemIds.indexOf(sibling['id']) < 0) { + if (hasSubtree(sibling) && this.isActiveItem(sibling['id'])) { activeItemIds.push(sibling['id']) } }) - if (Tree.hasSubtree(treeItemProps) && activeItemIds.indexOf(id) < 0) { + if (hasSubtree(treeItemProps) && this.isActiveItem(id)) { activeItemIds.push(id) } @@ -254,7 +246,7 @@ class Tree extends AutoControlledComponent, TreeState> { }) renderContent(): React.ReactElement[] { - const { activeItemIds, itemsForRender } = this.state + const { itemsForRender } = this.state const { items, renderItemTitle } = this.props if (!items) return null @@ -263,8 +255,8 @@ class Tree extends AutoControlledComponent, TreeState> { return items.reduce((renderedItems: any[], item: ShorthandValue) => { const itemForRender = itemsForRender[item['id']] const { elementRef, ...restItemForRender } = itemForRender - const isSubtree = Tree.hasSubtree(item) - const isSubtreeOpen = isSubtree && activeItemIds.indexOf(item['id']) > -1 + const isSubtree = hasSubtree(item) + const isSubtreeOpen = isSubtree && this.isActiveItem(item['id']) const renderedItem = TreeItem.create(item, { defaultProps: { @@ -315,6 +307,11 @@ class Tree extends AutoControlledComponent, TreeState> { ) } + + isActiveItem = (id: string): boolean => { + const { activeItemIds } = this.state + return activeItemIds.indexOf(id) > -1 + } } Tree.create = createShorthandFactory({ diff --git a/packages/react/src/components/Tree/lib/index.ts b/packages/react/src/components/Tree/lib/index.ts new file mode 100644 index 0000000000..7f2f62628e --- /dev/null +++ b/packages/react/src/components/Tree/lib/index.ts @@ -0,0 +1,13 @@ +import * as _ from 'lodash' +import { TreeItemProps } from '../TreeItem' +import { ShorthandValue } from '../../../types' + +const hasSubtree = (item: TreeItemProps | ShorthandValue): boolean => { + return !_.isNil(item['items']) && item['items'].length > 0 +} + +const removeItemAtIndex = (items: any[], itemIndex: number): any[] => { + return [...items.slice(0, itemIndex), ...items.slice(itemIndex + 1)] +} + +export { hasSubtree, removeItemAtIndex } From d5304f2c8d257857bde90adb3cb27bec713004c1 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Thu, 29 Aug 2019 15:56:05 +0200 Subject: [PATCH 43/47] more code review --- packages/react/src/components/Tree/Tree.tsx | 14 +++++++++----- packages/react/src/components/Tree/TreeItem.tsx | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 5f907e888f..ef02689905 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -191,13 +191,17 @@ class Tree extends AutoControlledComponent, TreeState> { } const { itemsForRender } = this.state - const elementToBeFocused = itemsForRender[parent['id']].elementRef + const parentItemForRender = itemsForRender[parent['id']] - if (!elementToBeFocused) { + if ( + !parentItemForRender || + !parentItemForRender.elementRef || + !parentItemForRender.elementRef.current + ) { return } - elementToBeFocused.current.focus() + parentItemForRender.elementRef.current.focus() _.invoke(predefinedProps, 'onFocusParent', e, treeItemProps) }, onFocusFirstChild: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => { @@ -228,12 +232,12 @@ class Tree extends AutoControlledComponent, TreeState> { const { activeItemIds } = this.state siblings.forEach(sibling => { - if (hasSubtree(sibling) && this.isActiveItem(sibling['id'])) { + if (hasSubtree(sibling) && !this.isActiveItem(sibling['id'])) { activeItemIds.push(sibling['id']) } }) - if (hasSubtree(treeItemProps) && this.isActiveItem(id)) { + if (hasSubtree(treeItemProps) && !this.isActiveItem(id)) { activeItemIds.push(id) } diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx index f7362f3d0d..77d542bf03 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -3,7 +3,6 @@ import * as _ from 'lodash' import * as PropTypes from 'prop-types' import * as React from 'react' -import Tree from './Tree' import TreeTitle, { TreeTitleProps } from './TreeTitle' import { treeItemBehavior } from '../../lib/accessibility' import { Accessibility } from '../../lib/accessibility/types' @@ -25,6 +24,7 @@ import { withSafeTypeForAs, ShorthandCollection, } from '../../types' +import { hasSubtree } from './lib' export interface TreeItemSlotClassNames { title: string @@ -131,7 +131,7 @@ class TreeItem extends UIComponent, TreeItemState> { static getDerivedStateFromProps(props: TreeItemProps) { return { - hasSubtree: Tree.hasSubtree(props), + hasSubtree: hasSubtree(props), treeSize: props.siblings.length + 1, } } From 2d2a1cd643a97dd01dd6bc16fe304a8f7fef5808 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Thu, 29 Aug 2019 16:02:06 +0200 Subject: [PATCH 44/47] move some examples to usage --- .../examples/components/Tree/Types/index.tsx | 10 --------- .../examples/components/Tree/Usage/index.tsx | 21 +++++++++++++++++++ 2 files changed, 21 insertions(+), 10 deletions(-) create mode 100644 docs/src/examples/components/Tree/Usage/index.tsx diff --git a/docs/src/examples/components/Tree/Types/index.tsx b/docs/src/examples/components/Tree/Types/index.tsx index 709e010f4f..6698242482 100644 --- a/docs/src/examples/components/Tree/Types/index.tsx +++ b/docs/src/examples/components/Tree/Types/index.tsx @@ -9,21 +9,11 @@ const Types = () => ( description="A default Tree." examplePath="components/Tree/Types/TreeExample" /> - - ) diff --git a/docs/src/examples/components/Tree/Usage/index.tsx b/docs/src/examples/components/Tree/Usage/index.tsx new file mode 100644 index 0000000000..bec9268090 --- /dev/null +++ b/docs/src/examples/components/Tree/Usage/index.tsx @@ -0,0 +1,21 @@ +import * as React from 'react' + +import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample' +import ExampleSection from 'docs/src/components/ComponentDoc/ExampleSection' + +const Usage = () => ( + + + + +) + +export default Usage From 84a2068109482f55698d518f5096b7aabc0e2695 Mon Sep 17 00:00:00 2001 From: silviuavram Date: Thu, 29 Aug 2019 16:26:02 +0200 Subject: [PATCH 45/47] last round of review --- .../HierarchicalTree/HierarchicalTree.tsx | 1 - .../HierarchicalTree/HierarchicalTreeItem.tsx | 1 - packages/react/src/components/Tree/Tree.tsx | 75 ++++++++++--------- .../react/src/components/Tree/TreeItem.tsx | 1 - .../themes/teams-dark/componentVariables.ts | 1 - .../components/Tree/treeTitleVariables.ts | 7 -- .../teams-high-contrast/componentVariables.ts | 1 - .../components/Tree/treeTitleVariables.ts | 7 -- .../teams/components/Tree/treeItemStyles.ts | 4 +- .../teams/components/Tree/treeTitleStyles.ts | 7 +- .../components/Tree/treeTitleVariables.ts | 8 +- 11 files changed, 50 insertions(+), 63 deletions(-) delete mode 100644 packages/react/src/themes/teams-dark/components/Tree/treeTitleVariables.ts delete mode 100644 packages/react/src/themes/teams-high-contrast/components/Tree/treeTitleVariables.ts diff --git a/packages/react/src/components/HierarchicalTree/HierarchicalTree.tsx b/packages/react/src/components/HierarchicalTree/HierarchicalTree.tsx index fcbe883a2c..8f1722c9f7 100644 --- a/packages/react/src/components/HierarchicalTree/HierarchicalTree.tsx +++ b/packages/react/src/components/HierarchicalTree/HierarchicalTree.tsx @@ -95,7 +95,6 @@ class HierarchicalTree extends AutoControlledComponent< exclusive: PropTypes.bool, items: customPropTypes.collectionShorthand, renderItemTitle: PropTypes.func, - rtlAttributes: PropTypes.func, onActiveIndexChange: PropTypes.func, } diff --git a/packages/react/src/components/HierarchicalTree/HierarchicalTreeItem.tsx b/packages/react/src/components/HierarchicalTree/HierarchicalTreeItem.tsx index 4b3432d024..fafa7623a0 100644 --- a/packages/react/src/components/HierarchicalTree/HierarchicalTreeItem.tsx +++ b/packages/react/src/components/HierarchicalTree/HierarchicalTreeItem.tsx @@ -88,7 +88,6 @@ class HierarchicalTreeItem extends UIComponent, TreeState> { exclusive: PropTypes.bool, items: customPropTypes.collectionShorthand, renderItemTitle: PropTypes.func, - rtlAttributes: PropTypes.func, } static defaultProps = { @@ -101,7 +100,8 @@ class Tree extends AutoControlledComponent, TreeState> { static autoControlledProps = ['activeItemIds'] - static getItemsForRender = _.memoize((itemsFromProps: ShorthandCollection) => { + // memoize this function if performance issue occurs. + static getItemsForRender = (itemsFromProps: ShorthandCollection) => { const itemsForRenderGenerator = ( items = itemsFromProps, level = 1, @@ -131,7 +131,7 @@ class Tree extends AutoControlledComponent, TreeState> { } return itemsForRenderGenerator(itemsFromProps) - }) + } static getAutoControlledStateFromProps(nextProps: TreeProps, prevState: TreeState) { const itemsForRender = Tree.getItemsForRender(nextProps.items) @@ -256,39 +256,42 @@ class Tree extends AutoControlledComponent, TreeState> { if (!items) return null const renderItems = (items: ShorthandCollection): React.ReactElement[] => { - return items.reduce((renderedItems: any[], item: ShorthandValue) => { - const itemForRender = itemsForRender[item['id']] - const { elementRef, ...restItemForRender } = itemForRender - const isSubtree = hasSubtree(item) - const isSubtreeOpen = isSubtree && this.isActiveItem(item['id']) - - const renderedItem = TreeItem.create(item, { - defaultProps: { - className: Tree.slotClassNames.item, - open: isSubtreeOpen, - renderItemTitle, - key: item['id'], - ...restItemForRender, - }, - overrideProps: this.handleTreeItemOverrides, - }) - - // Only need refs of the items that spawn subtrees, when they need to be focused - // by any of their children, using Arrow Left. - const finalRenderedItem = isSubtree ? ( - - {renderedItem} - - ) : ( - renderedItem - ) - - return [ - ...(renderedItems as any[]), - finalRenderedItem, - ...[isSubtreeOpen ? renderItems(item['items']) : []], - ] - }, []) + return items.reduce( + (renderedItems: React.ReactElement[], item: ShorthandValue) => { + const itemForRender = itemsForRender[item['id']] + const { elementRef, ...restItemForRender } = itemForRender + const isSubtree = hasSubtree(item) + const isSubtreeOpen = isSubtree && this.isActiveItem(item['id']) + + const renderedItem = TreeItem.create(item, { + defaultProps: { + className: Tree.slotClassNames.item, + open: isSubtreeOpen, + renderItemTitle, + key: item['id'], + ...restItemForRender, + }, + overrideProps: this.handleTreeItemOverrides, + }) + + // Only need refs of the items that spawn subtrees, when they need to be focused + // by any of their children, using Arrow Left. + const finalRenderedItem = isSubtree ? ( + + {renderedItem} + + ) : ( + renderedItem + ) + + return [ + ...renderedItems, + finalRenderedItem, + ...[isSubtreeOpen ? renderItems(item['items']) : []], + ] + }, + [], + ) } return renderItems(items) diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx index 77d542bf03..1b6b4947f4 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -116,7 +116,6 @@ class TreeItem extends UIComponent, TreeItemState> { renderItemTitle: PropTypes.func, siblings: customPropTypes.collectionShorthand, title: customPropTypes.itemShorthand, - treeItemRtlAttributes: PropTypes.func, } static defaultProps = { diff --git a/packages/react/src/themes/teams-dark/componentVariables.ts b/packages/react/src/themes/teams-dark/componentVariables.ts index 11b3a7522d..8a49890c8c 100644 --- a/packages/react/src/themes/teams-dark/componentVariables.ts +++ b/packages/react/src/themes/teams-dark/componentVariables.ts @@ -20,4 +20,3 @@ export { default as Alert } from './components/Alert/alertVariables' export { default as ProviderBox } from './components/Provider/providerBoxVariables' export { default as Dropdown } from './components/Dropdown/dropdownVariables' export { default as Label } from './components/Label/labelVariables' -export { default as TreeTitle } from './components/Tree/treeTitleVariables' diff --git a/packages/react/src/themes/teams-dark/components/Tree/treeTitleVariables.ts b/packages/react/src/themes/teams-dark/components/Tree/treeTitleVariables.ts deleted file mode 100644 index c292c86d64..0000000000 --- a/packages/react/src/themes/teams-dark/components/Tree/treeTitleVariables.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { TreeTitleVariables } from '../../../teams/components/Tree/treeTitleVariables' - -export default (siteVars: any): TreeTitleVariables => { - return { - defaultColor: siteVars.colors.white, - } -} diff --git a/packages/react/src/themes/teams-high-contrast/componentVariables.ts b/packages/react/src/themes/teams-high-contrast/componentVariables.ts index 91718762af..051fa5df33 100644 --- a/packages/react/src/themes/teams-high-contrast/componentVariables.ts +++ b/packages/react/src/themes/teams-high-contrast/componentVariables.ts @@ -24,4 +24,3 @@ export { default as ProviderBox } from './components/Provider/providerBoxVariabl export { default as Dropdown } from './components/Dropdown/dropdownVariables' export { default as Label } from './components/Label/labelVariables' export { default as TooltipContent } from './components/Tooltip/tooltipContentVariables' -export { default as TreeTitle } from './components/Tree/treeTitleVariables' diff --git a/packages/react/src/themes/teams-high-contrast/components/Tree/treeTitleVariables.ts b/packages/react/src/themes/teams-high-contrast/components/Tree/treeTitleVariables.ts deleted file mode 100644 index c292c86d64..0000000000 --- a/packages/react/src/themes/teams-high-contrast/components/Tree/treeTitleVariables.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { TreeTitleVariables } from '../../../teams/components/Tree/treeTitleVariables' - -export default (siteVars: any): TreeTitleVariables => { - return { - defaultColor: siteVars.colors.white, - } -} diff --git a/packages/react/src/themes/teams/components/Tree/treeItemStyles.ts b/packages/react/src/themes/teams/components/Tree/treeItemStyles.ts index 5d6c46eecf..560033b4b4 100644 --- a/packages/react/src/themes/teams/components/Tree/treeItemStyles.ts +++ b/packages/react/src/themes/teams/components/Tree/treeItemStyles.ts @@ -5,9 +5,9 @@ import { TreeItemProps } from '../../../../components/Tree/TreeItem' import TreeTitle from '../../../../components/Tree/TreeTitle' const treeItemStyles: ComponentSlotStylesInput = { - root: ({ theme: { siteVariables }, props: { level } }): ICSSInJSStyle => ({ + root: ({ theme: { siteVariables }, props: p }): ICSSInJSStyle => ({ listStyleType: 'none', - padding: `0 0 0 ${pxToRem(1 + (level - 1) * 10)}`, + padding: `0 0 0 ${pxToRem(1 + (p.level - 1) * 10)}`, ':focus': { outline: 0, [`> .${TreeTitle.className}`]: { diff --git a/packages/react/src/themes/teams/components/Tree/treeTitleStyles.ts b/packages/react/src/themes/teams/components/Tree/treeTitleStyles.ts index 76d34b34df..89d375bb89 100644 --- a/packages/react/src/themes/teams/components/Tree/treeTitleStyles.ts +++ b/packages/react/src/themes/teams/components/Tree/treeTitleStyles.ts @@ -1,12 +1,11 @@ import { ICSSInJSStyle } from '../../../types' -import { pxToRem } from '../../../../lib' import getBorderFocusStyles from '../../getBorderFocusStyles' const treeTitleStyles = { - root: ({ variables, theme: { siteVariables } }): ICSSInJSStyle => ({ - padding: `${pxToRem(1)} 0`, + root: ({ variables: v, theme: { siteVariables } }): ICSSInJSStyle => ({ + padding: v.padding, cursor: 'pointer', - color: variables.defaultColor, + color: v.color, position: 'relative', ...getBorderFocusStyles({ siteVariables, diff --git a/packages/react/src/themes/teams/components/Tree/treeTitleVariables.ts b/packages/react/src/themes/teams/components/Tree/treeTitleVariables.ts index 7cfd33c6b3..f59909f119 100644 --- a/packages/react/src/themes/teams/components/Tree/treeTitleVariables.ts +++ b/packages/react/src/themes/teams/components/Tree/treeTitleVariables.ts @@ -1,9 +1,13 @@ +import { pxToRem } from '../../../../lib' + export interface TreeTitleVariables { - defaultColor: string + color: string + padding: string } export default (siteVars: any): TreeTitleVariables => { return { - defaultColor: siteVars.colors.grey[750], + color: siteVars.colorScheme.default.foreground, + padding: `${pxToRem(1)} 0`, } } From 2de17c2b1ad94ee0fb729f3321e09e5f378a0aac Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Thu, 29 Aug 2019 17:34:18 +0200 Subject: [PATCH 46/47] Update docs/src/examples/components/Tree/Usage/index.tsx --- docs/src/examples/components/Tree/Usage/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/examples/components/Tree/Usage/index.tsx b/docs/src/examples/components/Tree/Usage/index.tsx index bec9268090..0476340b31 100644 --- a/docs/src/examples/components/Tree/Usage/index.tsx +++ b/docs/src/examples/components/Tree/Usage/index.tsx @@ -4,7 +4,7 @@ import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample' import ExampleSection from 'docs/src/components/ComponentDoc/ExampleSection' const Usage = () => ( - + Date: Fri, 30 Aug 2019 10:26:53 +0200 Subject: [PATCH 47/47] add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1f657e6e3..3ef72e4953 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Features - Add `yellow`, `green`, `orange`, `pink`, `amethyst`, `silver` and `onyx` color schemes in Teams theme @mnajdova ([#1826](https://github.com/stardust-ui/react/pull/1826)) +- Add `Tree` component that is flat DOM structured @silviuavram ([#1779](https://github.com/stardust-ui/react/pull/1779)) ### Fixes - Fix `muted` prop in `Video` component @layershifter ([#1847](https://github.com/stardust-ui/react/pull/1847))