diff --git a/src/TableOfContents/index.tsx b/src/TableOfContents/index.tsx index 776994b7..687ca969 100644 --- a/src/TableOfContents/index.tsx +++ b/src/TableOfContents/index.tsx @@ -1,6 +1,6 @@ import { Button, Drawer, InputGroup } from '@blueprintjs/core'; import cn from 'classnames'; -import { flatMap } from 'lodash'; +import { flatMap, range } from 'lodash'; import * as React from 'react'; import { FAIcon, FAIconProp } from '../FAIcon'; @@ -36,24 +36,22 @@ export type ITableOfContentsLink = TableOfContentsItem & { isExternalLink?: boolean; }; -export type RowRendererType = (props: { +export type RowComponentProps = { item: T; - key: number | string; - getProps: ( - node: ITableOfContentsNode, - ) => { - onClick: ((e: React.MouseEvent) => void) | undefined; - style: React.CSSProperties; - className: string; - }; - DefaultRow: React.FC>; -}) => React.ReactElement | undefined; + index: number; + isExpanded: boolean; + toggleExpanded: () => void; +}; + +export type RowComponentType = React.ComponentType>; export interface ITableOfContents { contents: T[]; - // Caller should return undefined if they don't want to provide custom elem - rowRenderer?: RowRendererType; + /** + * Optionally customize how a row is rendered. Defaults to `DefaultRow`. + */ + rowComponent?: RowComponentType; // Padding that will be used for (default: 10) padding?: string; @@ -64,10 +62,6 @@ export interface ITableOfContents { function TableOfContentsInner({ className, contents, - rowRenderer, - forceStateStyle, -}: Pick, 'className' | 'contents' | 'rowRenderer' | 'forceStateStyle'>) { + rowComponent: RowComponent = DefaultRow, +}: Pick, 'className' | 'contents' | 'rowComponent'>) { const [expanded, setExpanded] = React.useState({}); + // an array of functions. Invoking the N-th function toggles the expanded flag on the N-th content item + const toggleExpandedFunctions = React.useMemo(() => { + return range(contents.length).map(i => () => + setExpanded(current => ({ + ...current, + [i]: !current[i], + })), + ); + }, [contents.length]); + // expand ancestors of active items by default React.useEffect(() => { const activeItems = contents.filter(item => item.isActive); @@ -130,62 +133,17 @@ function TableOfContentsInner { - if (item.isDisabled) { - e.preventDefault(); - return; - } - if (item.onClick) { - item.onClick(); - } - - if (isDivider) { - e.preventDefault(); - return; - } - - if (!isGroup) return; - - e.preventDefault(); - setExpanded({ ...expanded, [String(index)]: !isExpanded }); - }; - - let elem; - if (rowRenderer) { - elem = rowRenderer({ - item, - key: index, - getProps: computeTableOfContentsItemProps, - DefaultRow: props => ( -
- -
- ), - }); - } - - if (!elem) { - elem = ( -
- -
- ); - } - return elem; + return ( + + ); })} ); @@ -249,148 +207,120 @@ export function TableOfContents { - item: T; - isSelected?: boolean; - isActive?: boolean; - forceStateStyle?: 'active' | 'selected'; - isExpanded?: boolean; - isDisabled?: boolean; - onClick?: (e: React.MouseEvent) => void; -} - -const computeTableOfContentsItemProps = ({ - item, - isSelected: _isSelected, - isActive: _isActive, - onClick, -}: ITableOfContentsNode) => { - const depth = item.depth || 0; - const isChild = item.type !== 'group' && depth > 0; - const isGroup = item.type === 'group'; - const isDivider = item.type === 'divider'; - const showSkeleton = item.showSkeleton; - const isSelected = !showSkeleton && (_isSelected || item.isSelected); - const isActive = !showSkeleton && (_isActive || item.isActive); - - return { - onClick: showSkeleton ? undefined : onClick, - className: cn('TableOfContentsItem border-transparent', item.className, { - 'border-l': !isGroup, - 'TableOfContentsItem--selected': isActive, - 'TableOfContentsItem--active': isSelected, - 'TableOfContentsItem--group': isGroup, - 'TableOfContentsItem--divider': isDivider, - 'TableOfContentsItem--child border-gray-3 dark:border-lighten-3': isChild, - }), - style: { - marginLeft: depth * 24, - }, - }; -}; - -const TableOfContentsItemInner = ({ - item, - onClick, - isSelected: _isSelected, - isActive: _isActive, - isDisabled: _isDisabled, - forceStateStyle, - isExpanded, -}: ITableOfContentsNode) => { +function DefaultRowImpl({ item, isExpanded, toggleExpanded }: RowComponentProps) { const isGroup = item.type === 'group'; + const isChild = item.type !== 'group' && (item.depth ?? 0) > 0; const isDivider = item.type === 'divider'; const showSkeleton = item.showSkeleton; - let isSelected = _isSelected || item.isSelected; - let isActive = _isActive || item.isActive; - const isDisabled = _isDisabled || item.isDisabled; + const isSelected = item.isSelected && !showSkeleton; + const isActive = item.isActive && !showSkeleton; + const isDisabled = item.isDisabled; let icon = item.icon; - if (isActive || isSelected) { - if (item.activeIcon) { - icon = item.activeIcon; - } - - if (forceStateStyle === 'active') { - isActive = true; - isSelected = false; - } else if (forceStateStyle === 'selected') { - isActive = false; - isSelected = true; - } + if (item.activeIcon && (isActive || isSelected)) { + icon = item.activeIcon; } - if (showSkeleton) { - isActive = false; - isSelected = false; - } + const onClick = showSkeleton + ? undefined + : (e: React.MouseEvent) => { + if (item.isDisabled) { + e.preventDefault(); + return; + } + if (item.onClick) { + item.onClick(); + } + + if (isDivider) { + e.preventDefault(); + return; + } - const className = cn( + if (!isGroup) return; + + e.preventDefault(); + toggleExpanded(); + }; + + const outerClassName = cn('TableOfContentsItem border-transparent', item.className, { + 'border-l': !isGroup, + 'TableOfContentsItem--selected': isActive, + 'TableOfContentsItem--active': isSelected, + 'TableOfContentsItem--group': isGroup, + 'TableOfContentsItem--divider': isDivider, + 'TableOfContentsItem--child border-gray-3 dark:border-lighten-3': isChild, + }); + + const innerClassName = cn( 'TableOfContentsItem__inner relative flex flex-col justify-center border-transparent border-l-4', { - 'cursor-pointer': (item.onClick || onClick) && !showSkeleton && !isDisabled, + 'cursor-pointer': onClick && !isDisabled, 'cursor-not-allowed': isDisabled, 'dark-hover:bg-lighten-2 hover:bg-darken-2': !isDisabled && !isDivider && !isSelected && !isActive && !showSkeleton, 'dark:text-white bg-darken-2 dark:bg-lighten-2': isSelected || isActive, 'text-gray-7 dark:text-white': isActive, 'border-primary text-blue-6': isSelected, - 'text-gray-6 dark:text-gray-6 font-semibold h-10': isDivider, 'text-gray-5 dark:text-gray-5 hover:text-gray-6': !isDivider && !isSelected && !isActive, }, ); - let loadingElem; - if (item.isLoading) { - loadingElem = ; - } - - let actionElem; - if (item.action) { - actionElem = ( -