Skip to content
This repository has been archived by the owner on Mar 4, 2020. It is now read-only.

feat(Tree): Adding exclusive flag #1018

Merged
merged 20 commits into from
Mar 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

### Features
- Add `inline` prop in the `Popup` for rendering the content next to the trigger element @mnajdova ([#1017](https://github.com/stardust-ui/react/pull/1017))
- Add `exclusive` prop in the `Tree` for expanding one tree item at a time
priyankar205 marked this conversation as resolved.
Show resolved Hide resolved
@priyankar205 ([#1018](https://github.com/stardust-ui/react/pull/1018))
- Export `call-pstn` and `skype-logo` SVG icons to the Teams theme @thewulf7([#929](https://github.com/stardust-ui/react/pull/968))
- Export some Office brand SVG icons to the Teams theme, including `word`, `word-color`, `excel`, `excel-color`, `powerpoint`, `powerpoint-color`, `onenote`, `onenote-color` @codepretty ([#938](https://github.com/stardust-ui/react/pull/938))

Expand Down
Original file line number Diff line number Diff line change
@@ -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 }) => (
<Component open={open} hasSubtree={hasSubtree} {...restProps}>
{hasSubtree && <Icon name={open ? 'arrow down' : 'arrow right'} />}
<span>{content}</span>
</Component>
)

const TreeExclusiveExample = () => <Tree items={items} renderItemTitle={titleRenderer} exclusive />

export default TreeExclusiveExample
5 changes: 5 additions & 0 deletions docs/src/examples/components/Tree/Types/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ const Types = () => (
description="A Tree with customized title rendering."
examplePath="components/Tree/Types/TreeTitleCustomizationExample"
/>
<ComponentExample
title="Exclusive"
description="A Tree with only one subtree open at a time."
examplePath="components/Tree/Types/TreeExclusiveExample"
/>
</ExampleSection>
)

Expand Down
72 changes: 67 additions & 5 deletions packages/react/src/components/Tree/Tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import * as _ from 'lodash'
import * as PropTypes from 'prop-types'
import * as React from 'react'

import TreeItem from './TreeItem'
import TreeItem, { TreeItemProps } from './TreeItem'
import {
UIComponent,
AutoControlledComponent,
childrenExist,
commonPropTypes,
createShorthandFactory,
customPropTypes,
UIComponentProps,
ChildrenComponentProps,
Expand All @@ -17,12 +18,21 @@ import { Accessibility } from '../../lib/accessibility/types'
import { defaultBehavior } from '../../lib/accessibility'

export interface TreeProps extends UIComponentProps, ChildrenComponentProps {
/** Index of the currently active subtree. */
activeIndex?: number[] | number

/**
* Accessibility behavior if overridden by the user.
* @default defaultBehavior
*/
accessibility?: Accessibility

/** Initial activeIndex value. */
defaultActiveIndex?: number[] | number

/** Only allow one subtree to be open at a time. */
exclusive?: boolean

/** Shorthand array of props for Tree. */
items: ShorthandValue[]

Expand All @@ -36,10 +46,14 @@ export interface TreeProps extends UIComponentProps, ChildrenComponentProps {
renderItemTitle?: ShorthandRenderFunction
}

export interface TreeState {
activeIndex: number[] | number
}

/**
* Allows users to display data organised in tree-hierarchy.
*/
class Tree extends UIComponent<ReactProps<TreeProps>> {
class Tree extends AutoControlledComponent<ReactProps<TreeProps>, TreeState> {
static create: Function

static className = 'ui-tree'
Expand All @@ -50,6 +64,15 @@ class Tree extends UIComponent<ReactProps<TreeProps>> {
...commonPropTypes.createCommon({
content: false,
}),
activeIndex: customPropTypes.every([
customPropTypes.disallow(['children']),
PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.number]),
]),
defaultActiveIndex: 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,
Expand All @@ -60,14 +83,51 @@ class Tree extends UIComponent<ReactProps<TreeProps>> {
accessibility: defaultBehavior,
}

static autoControlledProps = ['activeIndex']
priyankar205 marked this conversation as resolved.
Show resolved Hide resolved

getInitialAutoControlledState({ exclusive }): TreeState {
return {
activeIndex: exclusive ? -1 : [],
}
}

getActiveIndexes(): number[] {
const { activeIndex } = this.state
return _.isArray(activeIndex) ? activeIndex : [activeIndex]
}

computeNewIndex = (index: number) => {
const { exclusive } = this.props

if (exclusive) return index
const activeIndexes = this.getActiveIndexes()
// check to see if index is in array, and remove it, if not then add it
return _.includes(activeIndexes, index)
? _.without(activeIndexes, index)
: [...activeIndexes, index]
}

handleTreeItemOverrides = (predefinedProps: TreeItemProps) => ({
onTitleClick: (e: React.SyntheticEvent, treeItemProps: TreeItemProps) => {
this.trySetState({ activeIndex: this.computeNewIndex(treeItemProps.index) })
_.invoke(predefinedProps, 'onTitleClick', e, treeItemProps)
},
})

renderContent() {
const { items, renderItemTitle } = this.props
const { items, renderItemTitle, exclusive } = this.props
priyankar205 marked this conversation as resolved.
Show resolved Hide resolved
const { activeIndex } = this.state
const activeIndexes = this.getActiveIndexes()

return _.map(items, item =>
return _.map(items, (item: ShorthandValue, index: number) =>
TreeItem.create(item, {
defaultProps: {
index,
exclusive,
renderItemTitle,
open: exclusive ? index === activeIndex : _.includes(activeIndexes, index),
},
overrideProps: this.handleTreeItemOverrides,
}),
priyankar205 marked this conversation as resolved.
Show resolved Hide resolved
)
}
Expand All @@ -88,4 +148,6 @@ class Tree extends UIComponent<ReactProps<TreeProps>> {
}
}

Tree.create = createShorthandFactory({ Component: Tree, mappedArrayProp: 'items' })

export default Tree
47 changes: 31 additions & 16 deletions packages/react/src/components/Tree/TreeItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import * as PropTypes from 'prop-types'
import * as React from 'react'

import Tree from './Tree'
import TreeTitle from './TreeTitle'
import TreeTitle, { TreeTitleProps } from './TreeTitle'
import { defaultBehavior } from '../../lib/accessibility'
import { Accessibility } from '../../lib/accessibility/types'
import {
AutoControlledComponent,
UIComponent,
childrenExist,
customPropTypes,
createShorthandFactory,
Expand All @@ -16,7 +16,12 @@ import {
ChildrenComponentProps,
rtlTextContainer,
} from '../../lib'
import { ReactProps, ShorthandRenderFunction, ShorthandValue } from '../../types'
import {
ComponentEventHandler,
ReactProps,
ShorthandRenderFunction,
ShorthandValue,
} from '../../types'

export interface TreeItemProps extends UIComponentProps, ChildrenComponentProps {
/**
Expand All @@ -25,12 +30,21 @@ export interface TreeItemProps extends UIComponentProps, ChildrenComponentProps
*/
accessibility?: Accessibility

/** Only allow one subtree to be open at a time. */
exclusive?: boolean

/** Initial open value. */
defaultOpen?: boolean

/** The index of the item among its sibbling */
index: number

/** Array of props for sub tree. */
items?: ShorthandValue[]

/** Called when a tree title is clicked. */
onTitleClick?: ComponentEventHandler<TreeItemProps>

/** Whether or not the subtree of the item is in the open state. */
open?: boolean

Expand All @@ -48,11 +62,7 @@ export interface TreeItemProps extends UIComponentProps, ChildrenComponentProps
title?: ShorthandValue
}

export interface TreeItemState {
open?: boolean
}

class TreeItem extends AutoControlledComponent<ReactProps<TreeItemProps>, TreeItemState> {
class TreeItem extends UIComponent<ReactProps<TreeItemProps>> {
static create: Function

static className = 'ui-tree__item'
Expand All @@ -67,6 +77,9 @@ class TreeItem extends AutoControlledComponent<ReactProps<TreeItemProps>, TreeIt
}),
defaultOpen: PropTypes.bool,
items: customPropTypes.collectionShorthand,
index: PropTypes.number,
priyankar205 marked this conversation as resolved.
Show resolved Hide resolved
exclusive: PropTypes.bool,
onTitleClick: PropTypes.func,
open: PropTypes.bool,
renderItemTitle: PropTypes.func,
treeItemRtlAttributes: PropTypes.func,
Expand All @@ -78,20 +91,16 @@ class TreeItem extends AutoControlledComponent<ReactProps<TreeItemProps>, TreeIt
accessibility: defaultBehavior,
}

handleTitleOverrides = predefinedProps => ({
handleTitleOverrides = (predefinedProps: TreeTitleProps) => ({
onClick: (e, titleProps) => {
e.preventDefault()
this.trySetState({
open: !this.state.open,
})
_.invoke(this.props, 'onTitleClick', e, this.props)
_.invoke(predefinedProps, 'onClick', e, titleProps)
},
})

renderContent() {
const { items, title, renderItemTitle } = this.props
const { open } = this.state

const { items, title, renderItemTitle, open, exclusive } = this.props
const hasSubtree = !!(items && items.length)

return (
Expand All @@ -104,7 +113,13 @@ class TreeItem extends AutoControlledComponent<ReactProps<TreeItemProps>, TreeIt
render: renderItemTitle,
overrideProps: this.handleTitleOverrides,
})}
{hasSubtree && open && <Tree items={items} renderItemTitle={renderItemTitle} />}
{open &&
Tree.create(items, {
defaultProps: {
exclusive,
renderItemTitle,
},
})}
</>
)
}
Expand Down
11 changes: 10 additions & 1 deletion packages/react/src/components/Tree/TreeTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from '../../lib'
import { treeTitleBehavior } from '../../lib/accessibility'
import { Accessibility } from '../../lib/accessibility/types'
import { ReactProps } from '../../types'
import { ComponentEventHandler, ReactProps } from '../../types'

export interface TreeTitleProps
extends UIComponentProps,
Expand All @@ -26,6 +26,14 @@ export interface TreeTitleProps
*/
accessibility?: Accessibility

/**
* Called on click.
*
* @param {SyntheticEvent} event - React's original SyntheticEvent.
* @param {object} data - All props.
*/
onClick?: ComponentEventHandler<TreeTitleProps>

/** Whether or not the subtree of the item is in the open state. */
open?: boolean

Expand All @@ -42,6 +50,7 @@ class TreeTitle extends UIComponent<ReactProps<TreeTitleProps>> {

static propTypes = {
...commonPropTypes.createCommon(),
onClick: PropTypes.func,
open: PropTypes.bool,
hasSubtree: PropTypes.bool,
}
Expand Down