From d3dbd0fa8066862d7efbc152efad8aaff93ea53b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matuzal=C3=A9m=20Teles?= Date: Fri, 19 Aug 2022 14:24:07 -0500 Subject: [PATCH 1/3] refactor(@clayui/core): move TreeViewItem conditionals to `switch case` to be more readable --- .../clay-core/src/tree-view/TreeViewItem.tsx | 184 ++++++++++-------- 1 file changed, 98 insertions(+), 86 deletions(-) diff --git a/packages/clay-core/src/tree-view/TreeViewItem.tsx b/packages/clay-core/src/tree-view/TreeViewItem.tsx index cacb5ebc9f..74d7f64094 100644 --- a/packages/clay-core/src/tree-view/TreeViewItem.tsx +++ b/packages/clay-core/src/tree-view/TreeViewItem.tsx @@ -276,101 +276,113 @@ export const TreeViewItem = React.forwardRef< const {key} = event; - if (key === Keys.Left) { - if ( - !close(item.key) && - item.parentItemRef?.current - ) { - item.parentItemRef.current.focus(); - } - } + switch (key) { + case Keys.Left: + if ( + !close(item.key) && + item.parentItemRef?.current + ) { + item.parentItemRef.current.focus(); + } + break; + case Keys.Right: + if (!group) { + if (onLoadMore) { + setLoading(true); + onLoadMore(item) + .then((items) => { + setLoading(false); + + if (!items) { + return; + } - if (key === Keys.Right) { - if (!group) { - if (onLoadMore) { - setLoading(true); - onLoadMore(item) - .then((items) => { - setLoading(false); + insert( + [...item.indexes, 0], + items + ); + }) + .catch((error) => { + console.error(error); + }); + } else { + return; + } + } - if (!items) { - return; - } + if (!open(item.key) && item.itemRef.current) { + const group = + item.itemRef.current.parentElement?.querySelector( + '.treeview-group' + ); + const firstItemElement = + group?.querySelector( + '.treeview-link:not(.disabled)' + ); + + firstItemElement?.focus(); + } else { + item.itemRef.current?.focus(); + } + break; + case Keys.Backspace: + case Keys.Del: { + remove(item.indexes); + item.parentItemRef.current?.focus(); + break; + } + case Keys.Home: { + const firstListElement = rootRef.current + ?.firstElementChild as HTMLLinkElement; + const linkElement = + firstListElement.firstElementChild as HTMLDivElement; + + linkElement.focus(); + break; + } + case Keys.End: { + const lastListElement = rootRef.current + ?.lastElementChild as HTMLLinkElement; + const linkElement = + lastListElement.firstElementChild as HTMLDivElement; + + linkElement.focus(); + break; + } + case Keys.Spacebar: { + selection.toggleSelection(item.key); - insert([...item.indexes, 0], items); + if (onSelect) { + onSelect(removeItemInternalProps(item)); + } + break; + } + case Keys.R.toLowerCase(): + case Keys.R: + case Keys.F2: { + if (onRenameItem) { + onRenameItem({...item}) + .then((newItem) => { + replace(item.indexes, { + ...newItem, + index: item.index, + indexes: item.indexes, + itemRef: item.itemRef, + key: item.key, + parentItemRef: + item.parentItemRef, + }); + + item.itemRef.current?.focus(); }) .catch((error) => { console.error(error); }); - } else { - return; } + break; } - if (!open(item.key) && item.itemRef.current) { - const group = - item.itemRef.current.parentElement?.querySelector( - '.treeview-group' - ); - const firstItemElement = - group?.querySelector( - '.treeview-link:not(.disabled)' - ); - firstItemElement?.focus(); - } else { - item.itemRef.current?.focus(); - } - } - - if (key === Keys.Backspace || key === Keys.Del) { - remove(item.indexes); - - item.parentItemRef.current?.focus(); - } - - if (key === Keys.End) { - const lastListElement = rootRef.current - ?.lastElementChild as HTMLLinkElement; - const linkElement = - lastListElement.firstElementChild as HTMLDivElement; - linkElement.focus(); - } - - if (key === Keys.Home) { - const firstListElement = rootRef.current - ?.firstElementChild as HTMLLinkElement; - const linkElement = - firstListElement.firstElementChild as HTMLDivElement; - linkElement.focus(); - } - - if ( - (key.toUpperCase() === Keys.R || key === Keys.F2) && - onRenameItem - ) { - onRenameItem({...item}) - .then((newItem) => { - replace(item.indexes, { - ...newItem, - index: item.index, - indexes: item.indexes, - itemRef: item.itemRef, - key: item.key, - parentItemRef: item.parentItemRef, - }); - - item.itemRef.current?.focus(); - }) - .catch((error) => { - console.error(error); - }); - } - - if (key === Keys.Spacebar) { - selection.toggleSelection(item.key); - - if (onSelect) { - onSelect(removeItemInternalProps(item)); - } + default: + break; } }} ref={ref} From 312e1a82da415792393a7b6ba874f693ea6b1c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matuzal=C3=A9m=20Teles?= Date: Fri, 19 Aug 2022 18:23:19 -0500 Subject: [PATCH 2/3] chore(@clayui/core): add new features in the collection to be used nested and pass custom props in children function --- .../clay-core/src/collection/Collection.tsx | 162 +++++++++++++++--- packages/clay-core/src/collection/index.ts | 8 +- packages/clay-core/src/vertical-bar/Bar.tsx | 2 +- .../clay-core/src/vertical-bar/Content.tsx | 2 +- 4 files changed, 147 insertions(+), 27 deletions(-) diff --git a/packages/clay-core/src/collection/Collection.tsx b/packages/clay-core/src/collection/Collection.tsx index fd4ab9e43e..791f2da86a 100644 --- a/packages/clay-core/src/collection/Collection.tsx +++ b/packages/clay-core/src/collection/Collection.tsx @@ -6,41 +6,83 @@ import {useVirtualizer} from '@tanstack/react-virtual'; import React from 'react'; -export type ChildrenFunction = (item: T) => React.ReactElement; +export type ChildrenFunction = P extends Array + ? (item: T, ...args: P) => React.ReactElement + : (item: T) => React.ReactElement; -interface IInternalProps { +export interface ICollectionProps { + /** + * Children content to render a dynamic or static content. + */ + children: React.ReactNode | ChildrenFunction; + + /** + * Property to render content with dynamic data. + */ + items?: Array; + + /** + * Flag to indicate whether the list should be virtualized. + */ + virtualize?: boolean; +} + +interface IProps { /** * Component to render. */ as?: 'div' | React.ComponentType | React.ForwardRefExoticComponent; + /** + * Flag to estimate the default height of a list item in pixels. + */ estimateSize?: number; + /** + * Properties that should be omitted from item data when passed to + * children function. + */ + exclude?: Set; + + /** + * Add the reference of the parent element that will be used to define the + * scroll and get the height of the element for virtualization of the + * collection. + */ parentRef: React.RefObject; -} -export interface ICollectionProps { /** - * Children content to render a dynamic or static content. + * Set for the parent's key to create the unique key of the list items, if + * the collection is rendered nested. */ - children: React.ReactNode | ChildrenFunction; + parentKey?: React.Key; /** - * Property to render content with dynamic data. + * Defines the public APIs that are passed in the children function when + * it is a dynamic collection. */ - items?: Array; + publicApi?: P; /** - * Flag to indicate whether the list should be virtualized. + * Defines a component that will be used as a wrapper for items in the + * collection if defined. */ - virtualize?: boolean; + itemContainer?: React.ComponentType>; } type ChildElement = React.ReactElement & { ref?: (node: HTMLElement | null) => void; }; -export function getKey(index: number, key?: React.Key | null) { +/** + * Helper function to create a unique key for list or tree when defined by + * developer data or obtained by component in React. + */ +export function getKey( + index: number, + key?: React.Key | null, + parentKey?: React.Key +) { if ( key != null && (!String(key).startsWith('.') || String(key).startsWith('.$')) @@ -48,21 +90,41 @@ export function getKey(index: number, key?: React.Key | null) { return key; } - return `$.${index}`; + return parentKey ? `${parentKey}.${index}` : `$.${index}`; } -type VirtualProps = IInternalProps & { - children: ChildrenFunction; +/** + * Helper function for omitting properties of an object, similar to + * TypeScript's Omit. + */ +export function excludeProps, K extends keyof T>( + props: T, + items: Set +) { + return (Object.keys(props) as Array).reduce((previous, key) => { + if (!items.has(key)) { + previous[key] = props[key]; + } + + return previous; + }, {} as T); +} + +type VirtualProps = IProps & { + children: ChildrenFunction; items: Array; }; -function VirtualDynamicCollection>({ +function VirtualDynamicCollection, P, K>({ as: Container = 'div', children, estimateSize = 37, + exclude, items, + parentKey, parentRef, -}: VirtualProps) { + publicApi, +}: VirtualProps) { const virtualizer = useVirtualizer({ count: items.length, estimateSize: () => estimateSize, @@ -79,10 +141,17 @@ function VirtualDynamicCollection>({ > {virtualizer.getVirtualItems().map((virtual) => { const item = items[virtual.index]; + const publicItem = exclude ? excludeProps(item, exclude) : item; - const child: ChildElement = children(item); + const child: ChildElement = Array.isArray(publicApi) + ? children(publicItem, ...publicApi) + : children(publicItem); - const key = getKey(virtual.index, item.id ?? child.key); + const key = getKey( + virtual.index, + item.id ?? child.key, + parentKey + ); return React.cloneElement(child, { key, @@ -107,21 +176,31 @@ function VirtualDynamicCollection>({ ); } -export function Collection>({ +export function Collection< + T extends Record, + P = unknown, + K = unknown +>({ as, children, estimateSize, + exclude, + itemContainer: ItemContainer, items, + parentKey, parentRef, + publicApi, virtualize = false, -}: ICollectionProps & Partial) { +}: ICollectionProps & Partial>) { if (virtualize && children instanceof Function && items && parentRef) { return ( {children} @@ -132,11 +211,33 @@ export function Collection>({ return ( - {typeof children === 'function' && items + {children instanceof Function && items ? items.map((item, index) => { - const child: ChildElement = children(item); + const publicItem = exclude + ? excludeProps(item, exclude) + : item; + const child: ChildElement = Array.isArray(publicApi) + ? children(publicItem, ...publicApi) + : children(publicItem); - const key = getKey(index, item.id ?? child.key); + const key = getKey( + index, + item.id ?? child.key, + parentKey + ); + + if (ItemContainer) { + return ( + + {child} + + ); + } return React.cloneElement(child, { key, @@ -148,11 +249,24 @@ export function Collection>({ return null; } - const key = getKey(index, child.key); + const key = getKey(index, child.key, parentKey); + + if (ItemContainer) { + return ( + + {child} + + ); + } return React.cloneElement( child as React.ReactElement<{keyValue?: React.Key}>, { + key, keyValue: key, } ); diff --git a/packages/clay-core/src/collection/index.ts b/packages/clay-core/src/collection/index.ts index 906ecdea8c..8192846a75 100644 --- a/packages/clay-core/src/collection/index.ts +++ b/packages/clay-core/src/collection/index.ts @@ -3,4 +3,10 @@ * SPDX-License-Identifier: BSD-3-Clause */ -export {Collection, ICollectionProps} from './Collection'; +export { + Collection, + ChildrenFunction, + excludeProps, + ICollectionProps, + getKey, +} from './Collection'; diff --git a/packages/clay-core/src/vertical-bar/Bar.tsx b/packages/clay-core/src/vertical-bar/Bar.tsx index f459a6e51c..fbb6cd961d 100644 --- a/packages/clay-core/src/vertical-bar/Bar.tsx +++ b/packages/clay-core/src/vertical-bar/Bar.tsx @@ -10,7 +10,7 @@ import {Collection} from '../collection'; import type {ICollectionProps} from '../collection'; -interface IProps extends ICollectionProps { +interface IProps extends ICollectionProps { /** * Flag to determine which style the Bar will display. */ diff --git a/packages/clay-core/src/vertical-bar/Content.tsx b/packages/clay-core/src/vertical-bar/Content.tsx index eb4e48995c..2f60dda701 100644 --- a/packages/clay-core/src/vertical-bar/Content.tsx +++ b/packages/clay-core/src/vertical-bar/Content.tsx @@ -15,7 +15,7 @@ type Context = { export const ContentContext = React.createContext({} as Context); -interface IProps extends Omit, 'virtualize'> { +interface IProps extends Omit, 'virtualize'> { /** * Flag to determine which style the VerticalBar will display. */ From ce767215159f82c00ba2e8702251eff7471f5ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matuzal=C3=A9m=20Teles?= Date: Fri, 19 Aug 2022 18:24:34 -0500 Subject: [PATCH 3/3] refactor(@clayui/core): uses the Collection component for the TreeView --- jest.config.js | 2 +- .../clay-core/src/tree-view/Collection.tsx | 113 ++++++++---------- .../clay-core/src/tree-view/TreeViewGroup.tsx | 14 ++- packages/clay-core/src/tree-view/context.ts | 4 +- .../src/tree-view/useMultipleSelection.tsx | 2 +- 5 files changed, 62 insertions(+), 73 deletions(-) diff --git a/jest.config.js b/jest.config.js index f0f52a6514..acc4c1f421 100644 --- a/jest.config.js +++ b/jest.config.js @@ -58,7 +58,7 @@ module.exports = { './packages/clay-core/src/tree-view/': { branches: 68, functions: 73, - lines: 77, + lines: 76, statements: 75, }, './packages/clay-data-provider/src/': { diff --git a/packages/clay-core/src/tree-view/Collection.tsx b/packages/clay-core/src/tree-view/Collection.tsx index 79c1eb90f6..f444bf7624 100644 --- a/packages/clay-core/src/tree-view/Collection.tsx +++ b/packages/clay-core/src/tree-view/Collection.tsx @@ -5,14 +5,16 @@ import React from 'react'; +import { + ChildrenFunction as ChildrenFunctionBase, + Collection as CollectionBase, + excludeProps, +} from '../collection'; import {Expand, Selection, useAPI} from './context'; import {ItemContextProvider, useItem} from './useItem'; -export type ChildrenFunction = ( - item: Omit, - selection: Selection, - expand: Expand -) => React.ReactElement; +export type ChildrenFunction> = + ChildrenFunctionBase; export interface ICollectionProps { children: React.ReactNode | ChildrenFunction; @@ -28,72 +30,51 @@ export interface ICollectionProps { items?: Array; } -export function getKey( - index: number, - key?: React.Key | null, - parentKey?: React.Key -) { - if ( - key != null && - (!String(key).startsWith('.') || String(key).startsWith('.$')) - ) { - return key; - } - - return parentKey ? `${parentKey}.${index}` : `$.${index}`; -} - -export function removeItemInternalProps>(props: T) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const {index, indexes, itemRef, key, parentItemRef, ...item} = props; - - return item; -} - -export function Collection>({ +const exclude = new Set([ + 'index', + 'indexes', + 'itemRef', + 'key', + 'parentItemRef', +]); + +const ItemContainer: React.ComponentType> = ({ + item = {}, + index, + keyValue, + children, +}) => ( + + {children} + +); + +type Props = { + as?: 'div' | React.ComponentType | React.ForwardRefExoticComponent; +}; + +export function Collection>({ + as, children, items, -}: ICollectionProps) { +}: Props & ICollectionProps) { const api = useAPI(); const {key: parentKey} = useItem(); return ( - <> - {typeof children === 'function' && items - ? items.map((item, index) => { - const child = children( - removeItemInternalProps(item), - ...api - ); - - const key = getKey( - index, - item.id ?? child.key, - parentKey - ); - - return ( - - {child} - - ); - }) - : React.Children.map(children, (child, index) => { - if (!React.isValidElement(child)) { - return null; - } - - const key = getKey(index, child.key, parentKey); - - return ( - - {child} - - ); - })} - + + {children} + ); } + +export function removeItemInternalProps>(props: T) { + return excludeProps(props, exclude); +} diff --git a/packages/clay-core/src/tree-view/TreeViewGroup.tsx b/packages/clay-core/src/tree-view/TreeViewGroup.tsx index fc16b47844..4eb8c0e273 100644 --- a/packages/clay-core/src/tree-view/TreeViewGroup.tsx +++ b/packages/clay-core/src/tree-view/TreeViewGroup.tsx @@ -16,6 +16,14 @@ interface ITreeViewGroupProps extends ICollectionProps, Omit, 'children'> {} +function List({children}: React.HTMLAttributes) { + return ( +
    + {children} +
+ ); +} + export function TreeViewGroup(props: ITreeViewGroupProps): JSX.Element & { displayName: string; }; @@ -60,9 +68,9 @@ export function TreeViewGroup>({ unmountOnExit >
-
    - items={items}>{children} -
+ as={List} items={items}> + {children} +
); diff --git a/packages/clay-core/src/tree-view/context.ts b/packages/clay-core/src/tree-view/context.ts index 73b82e9479..3745c83b3e 100644 --- a/packages/clay-core/src/tree-view/context.ts +++ b/packages/clay-core/src/tree-view/context.ts @@ -48,7 +48,7 @@ export type Expand = { has: (key: Key) => boolean; }; -export function useAPI() { +export function useAPI(): [Selection, Expand] { const {expandedKeys, selection, toggle} = useTreeViewContext(); const hasKey = useCallback( @@ -69,5 +69,5 @@ export function useAPI() { toggle: selection.toggleSelection, }, {has: hasExpandedKey, toggle}, - ] as const; + ]; } diff --git a/packages/clay-core/src/tree-view/useMultipleSelection.tsx b/packages/clay-core/src/tree-view/useMultipleSelection.tsx index 68cfa1b5f6..08f20f28e5 100644 --- a/packages/clay-core/src/tree-view/useMultipleSelection.tsx +++ b/packages/clay-core/src/tree-view/useMultipleSelection.tsx @@ -6,7 +6,7 @@ import {useInternalState} from '@clayui/shared'; import {Key, useCallback, useMemo, useRef} from 'react'; -import {getKey} from './Collection'; +import {getKey} from '../collection'; import {ITreeProps, createImmutableTree} from './useTree'; import type {ICollectionProps} from './Collection';