Skip to content

Commit

Permalink
feat(@clayui/core): adds multiple selection implementation for Tree View
Browse files Browse the repository at this point in the history
This implementation is a lightweight and efficient solution for multiple Node selection, it also supports intermediate state and tree selection behaviors if the Node has children.

We maintain a light, flat tree structure and build it along with React's render stream to avoid traversing the tree in a separate stream, so we can have big performance gains on very large trees.
  • Loading branch information
matuzalemsteles committed Sep 13, 2021
1 parent 9315cbc commit 281ec20
Show file tree
Hide file tree
Showing 9 changed files with 400 additions and 110 deletions.
14 changes: 7 additions & 7 deletions packages/clay-core/src/tree-view/Collection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ export interface ICollectionProps<T> {
}

function getKey(index: number, key?: React.Key | null, parentKey?: React.Key) {
if (key != null) {
if (
key != null &&
(!String(key).startsWith('.') || String(key).startsWith('.$'))
) {
return key;
}

Expand All @@ -26,7 +29,7 @@ export function Collection<T extends Record<any, any>>({
children,
items,
}: ICollectionProps<T>) {
const {parentKey} = useItem();
const {key: parentKey} = useItem();

return (
<>
Expand All @@ -43,7 +46,7 @@ export function Collection<T extends Record<any, any>>({
return (
<ItemContextProvider
key={key}
value={{...item, key, parentKey: key}}
value={{...item, key}}
>
{child}
</ItemContextProvider>
Expand All @@ -57,10 +60,7 @@ export function Collection<T extends Record<any, any>>({
const key = getKey(index, child.key, parentKey);

return (
<ItemContextProvider
key={key}
value={{key, parentKey: key}}
>
<ItemContextProvider key={key} value={{key}}>
{child}
</ItemContextProvider>
);
Expand Down
9 changes: 6 additions & 3 deletions packages/clay-core/src/tree-view/TreeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@ import {ChildrenFunction, Collection, ICollectionProps} from './Collection';
import {TreeViewGroup} from './TreeViewGroup';
import {TreeViewItem, TreeViewItemStack} from './TreeViewItem';
import {Icons, TreeViewContext} from './context';
import {IExpandable, IMultipleSelection, useTree} from './useTree';
import {ITreeProps, useTree} from './useTree';

interface ITreeViewProps<T>
extends Omit<React.HTMLAttributes<HTMLUListElement>, 'children'>,
IMultipleSelection,
IExpandable,
ITreeProps,
ICollectionProps<T> {
displayType?: 'light' | 'dark';
expanderIcons?: Icons;
Expand All @@ -40,6 +39,8 @@ export function TreeView<T>({
items,
nestedKey,
onExpandedChange,
onSelectionChange,
selectedKeys,
showExpanderOnHover = true,
...otherProps
}: ITreeViewProps<T>) {
Expand All @@ -55,6 +56,8 @@ export function TreeView<T>({
: undefined,
expanderIcons,
nestedKey,
onSelectionChange,
selectedKeys,
showExpanderOnHover,
...state,
};
Expand Down
4 changes: 2 additions & 2 deletions packages/clay-core/src/tree-view/TreeViewGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function TreeViewGroup<T extends Record<any, any>>({
return (
<CSSTransition
className={classNames('collapse', {
show: expandedKeys!.has(item.key),
show: expandedKeys.has(item.key),
})}
classNames={{
enter: 'collapsing',
Expand All @@ -39,7 +39,7 @@ export function TreeViewGroup<T extends Record<any, any>>({
exitActive: 'collapsing',
}}
id={item.key}
in={expandedKeys!.has(item.key)}
in={expandedKeys.has(item.key)}
onEnter={(el: HTMLElement) =>
el.setAttribute('style', 'height: 0px')
}
Expand Down
29 changes: 21 additions & 8 deletions packages/clay-core/src/tree-view/TreeViewItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ export function TreeViewItem({children}: TreeViewItemProps) {
<li className="treeview-item" role="none">
<div
aria-expanded={
group ? expandedKeys!.has(item.key) : undefined
group ? expandedKeys.has(item.key) : undefined
}
className={classNames('treeview-link', {
collapsed: group && !expandedKeys!.has(item.key),
collapsed: group && expandedKeys.has(item.key),
})}
onClick={() => group && toggle!(item.key)}
onClick={() => group && toggle(item.key)}
role="treeitem"
style={{paddingLeft: `${spacing}px`}}
tabIndex={0}
Expand Down Expand Up @@ -89,7 +89,12 @@ export function TreeViewItemStack({
children,
expandable = true,
}: TreeViewItemStackProps) {
const {expandedKeys, expanderIcons, toggle} = useTreeViewContext();
const {
expandedKeys,
expanderIcons,
selection,
toggle,
} = useTreeViewContext();

const item = useItem();

Expand All @@ -100,14 +105,14 @@ export function TreeViewItemStack({
{expandable && (
<Layout.ContentCol>
<Button
aria-controls={item.key}
aria-expanded={expandedKeys!.has(item.key)}
aria-controls={`${item.key}`}
aria-expanded={expandedKeys.has(item.key)}
className={classNames('component-expander', {
collapsed: !expandedKeys!.has(item.key),
collapsed: expandedKeys.has(item.key),
})}
displayType={null}
monospaced
onClick={() => toggle!(item.key)}
onClick={() => toggle(item.key)}
>
<span className="c-inner" tabIndex={-2}>
{expanderIcons?.close ? (
Expand Down Expand Up @@ -143,6 +148,14 @@ export function TreeViewItemStack({
// @ts-ignore
} else if (child?.type.displayName === 'ClayIcon') {
content = <div className="component-icon">{child}</div>;

// @ts-ignore
} else if (child?.type.displayName === 'ClayCheckbox') {
content = React.cloneElement(child as React.ReactElement, {
checked: selection.selectedKeys.has(item.key),
indeterminate: selection.isIntermediate(item.key),
onChange: () => selection.toggleSelection(item.key),
});
}

return (
Expand Down
12 changes: 7 additions & 5 deletions packages/clay-core/src/tree-view/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,25 @@
* SPDX-License-Identifier: BSD-3-Clause
*/

import React, {Key, useContext} from 'react';
import React, {useContext} from 'react';

import type {ITreeState} from './useTree';

export type Icons = {
open: React.ReactElement;
close: React.ReactElement;
};

export interface ITreeViewContext {
export interface ITreeViewContext extends ITreeState {
childrenRoot?: (item: Object) => React.ReactElement;
expandedKeys?: Set<Key>;
expanderIcons?: Icons;
nestedKey?: string;
showExpanderOnHover?: boolean;
toggle?: (key: Key) => void;
}

export const TreeViewContext = React.createContext<ITreeViewContext>({});
export const TreeViewContext = React.createContext<ITreeViewContext>(
{} as ITreeViewContext
);

export function useTreeViewContext(): ITreeViewContext {
return useContext(TreeViewContext);
Expand Down
22 changes: 18 additions & 4 deletions packages/clay-core/src/tree-view/useItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,38 @@
* SPDX-License-Identifier: BSD-3-Clause
*/

import React, {useContext, useRef} from 'react';
import React, {useContext, useEffect, useRef} from 'react';

type Value = Record<string, any>;
import {useTreeViewContext} from './context';

type Value = {
[propName: string]: any;
key: React.Key;
};

type Props = {
children: React.ReactNode;
value: Value;
};

const ItemContext = React.createContext<Value>({});
const ItemContext = React.createContext<Value>({} as Value);

function getKey(key: React.Key) {
return `${key}`.replace('.$', '');
}

export function ItemContextProvider({children, value = {}}: Props) {
export function ItemContextProvider({children, value}: Props) {
const {selection} = useTreeViewContext();
const {key: parentKey} = useItem();

const keyRef = useRef(getKey(value.key));

useEffect(() => selection.mount(keyRef.current, parentKey), [
selection.mount,
keyRef,
parentKey,
]);

const props = {
...value,
key: keyRef.current,
Expand Down
Loading

0 comments on commit 281ec20

Please sign in to comment.