import React, {
FormHTMLAttributes,
HTMLProps,
InputHTMLAttributes,
Ref,
} from 'react';
export type TreeItemIndex = string | number;
export interface TreeItem {
index: TreeItemIndex;
children?: Array;
isFolder?: boolean;
// isChildrenLoading?: boolean;
canMove?: boolean;
canRename?: boolean;
data: T;
}
export interface TreePosition {
treeId: string;
parent: TreeItemIndex;
index: number;
}
export interface TreeItemActions {
primaryAction: () => void;
startRenamingItem: () => void;
stopRenamingItem: () => void;
expandItem: () => void;
collapseItem: () => void;
toggleExpandedState: () => void;
selectItem: () => void;
unselectItem: () => void;
addToSelectedItems: () => void;
selectUpTo: (overrideOldSelection?: boolean) => void;
startDragging: () => void;
/** @param setDomFocus - Defaults to true. */
focusItem: (setDomFocus?: boolean) => void;
// toggleSelectedState: () => void;
}
export interface TreeItemRenderFlags {
isSelected?: boolean;
isExpanded?: boolean;
isFocused?: boolean;
isRenaming?: boolean;
isDraggingOver?: boolean;
isDraggingOverParent?: boolean;
isSearchMatching?: boolean;
canDrag?: boolean;
canDropOn?: boolean;
}
export interface TreeItemRenderContext
extends TreeItemActions,
TreeItemRenderFlags {
interactiveElementProps: HTMLProps;
itemContainerWithoutChildrenProps: HTMLProps;
itemContainerWithChildrenProps: HTMLProps;
arrowProps: HTMLProps;
viewStateFlags: { [collection in C]: boolean };
}
export interface TreeInformation extends TreeConfiguration {
areItemsSelected: boolean;
isRenaming: boolean;
isFocused: boolean;
isSearching: boolean;
search: string | null;
isProgrammaticallyDragging: boolean;
}
export interface TreeRenderProps {
renderItem?: (props: {
item: TreeItem;
depth: number;
children: React.ReactNode | null;
title: React.ReactNode;
arrow: React.ReactNode;
context: TreeItemRenderContext;
info: TreeInformation;
}) => React.ReactElement | null;
renderItemTitle?: (props: {
title: string;
item: TreeItem;
context: TreeItemRenderContext;
info: TreeInformation;
}) => React.ReactElement | null | string;
renderItemArrow?: (props: {
item: TreeItem;
context: TreeItemRenderContext;
info: TreeInformation;
}) => React.ReactElement | null;
renderRenameInput?: (props: {
item: TreeItem;
inputProps: InputHTMLAttributes;
inputRef: Ref;
submitButtonProps: HTMLProps;
submitButtonRef: Ref;
formProps: FormHTMLAttributes;
}) => React.ReactElement | null;
renderItemsContainer?: (props: {
children: React.ReactNode;
containerProps: HTMLProps;
info: TreeInformation;
depth: number;
parentId: TreeItemIndex;
}) => React.ReactElement | null;
renderTreeContainer?: (props: {
children: React.ReactNode;
containerProps: HTMLProps;
info: TreeInformation;
}) => React.ReactElement | null;
renderDragBetweenLine?: (props: {
draggingPosition: DraggingPosition;
lineProps: HTMLProps;
}) => React.ReactElement | null;
renderSearchInput?: (props: {
inputProps: HTMLProps;
}) => React.ReactElement | null;
renderLiveDescriptorContainer?: (props: {
children: React.ReactNode;
tree: TreeConfiguration;
}) => React.ReactElement | null;
renderDepthOffset?: number;
}
export type AllTreeRenderProps = Required<
TreeRenderProps
>;
export enum InteractionMode {
DoubleClickItemToExpand = 'double-click-item-to-expand',
ClickItemToExpand = 'click-item-to-expand',
ClickArrowToExpand = 'click-arrow-to-expand',
}
export interface InteractionManager {
mode: InteractionMode | string;
extends?: InteractionMode;
createInteractiveElementProps: (
item: TreeItem,
treeId: string,
actions: TreeItemActions,
renderFlags: TreeItemRenderFlags,
/** See https://github.com/lukasbach/react-complex-tree/issues/48 */
__unsafeViewState?: IndividualTreeViewState
) => HTMLProps;
}
export interface TreeCapabilities {
defaultInteractionMode?: InteractionMode | InteractionManager;
canDragAndDrop?: boolean;
canDropOnFolder?: boolean;
canDropOnNonFolder?: boolean;
canReorderItems?: boolean;
canDrag?: (items: TreeItem[]) => boolean;
canDropAt?: (items: TreeItem[], target: DraggingPosition) => boolean;
canInvokePrimaryActionOnItemContainer?: boolean;
canSearch?: boolean;
canSearchByStartingTyping?: boolean;
canRename?: boolean;
autoFocus?: boolean;
doesSearchMatchItem?: (
search: string,
item: TreeItem,
itemTitle: string
) => boolean;
showLiveDescription?: boolean;
shouldRenderChildren?: (
item: TreeItem,
context: TreeItemRenderContext
) => boolean;
/**
* See Issue #148 or the sample at
* https://rct.lukasbach.com/storybook/?path=/story/core-basic-examples--single-tree?path=/story/core-drag-and-drop-configurability--can-drop-below-open-folders
* for details.
*
* If enabled, dropping at the bottom of an open folder will drop the items
* in the parent folder below the hovered item instead of inside the folder
* at the top.
*/
canDropBelowOpenFolders?: boolean;
disableArrowKeys?: boolean;
}
export type IndividualTreeViewState = {
selectedItems?: TreeItemIndex[];
expandedItems?: TreeItemIndex[];
focusedItem?: TreeItemIndex;
} & { [c in C]: TreeItemIndex | TreeItemIndex[] | undefined };
export interface TreeViewState {
[treeId: string]: IndividualTreeViewState | undefined;
}
export interface ExplicitDataSource {
items: Record>;
}
export interface ImplicitDataSource {
dataProvider: TreeDataProvider;
}
export type DataSource = ExplicitDataSource | ImplicitDataSource;
export interface TreeChangeHandlers {
onStartRenamingItem?: (item: TreeItem, treeId: string) => void;
onRenameItem?: (item: TreeItem, name: string, treeId: string) => void;
onAbortRenamingItem?: (item: TreeItem, treeId: string) => void;
onCollapseItem?: (item: TreeItem, treeId: string) => void;
onExpandItem?: (item: TreeItem, treeId: string) => void;
onSelectItems?: (items: TreeItemIndex[], treeId: string) => void; // TODO TreeItem instead of just index
onFocusItem?: (
item: TreeItem,
treeId: string,
setDomFocus?: boolean
) => void;
onDrop?: (items: TreeItem[], target: DraggingPosition) => void;
onPrimaryAction?: (items: TreeItem, treeId: string) => void;
onRegisterTree?: (tree: TreeConfiguration) => void;
onUnregisterTree?: (tree: TreeConfiguration) => void;
onMissingItems?: (itemIds: TreeItemIndex[]) => void;
onMissingChildren?: (itemIds: TreeItemIndex[]) => void; // TODO
}
export interface TreeEnvironmentChangeActions {
focusTree: (treeId: string, autoFocus?: boolean) => void;
renameItem: (itemId: TreeItemIndex, name: string, treeId: string) => void;
collapseItem: (itemId: TreeItemIndex, treeId: string) => void;
expandItem: (itemId: TreeItemIndex, treeId: string) => void;
toggleItemExpandedState: (itemId: TreeItemIndex, treeId: string) => void;
selectItems: (itemsIds: TreeItemIndex[], treeId: string) => void;
toggleItemSelectStatus: (itemId: TreeItemIndex, treeId: string) => void;
invokePrimaryAction: (itemId: TreeItemIndex, treeID: string) => void;
/** @param setDomFocus - Defaults to true. */
focusItem: (
itemId: TreeItemIndex,
treeId: string,
setDomFocus?: boolean
) => void;
moveFocusUp: (treeId: string) => void;
moveFocusDown: (treeId: string) => void;
startProgrammaticDrag: () => void;
abortProgrammaticDrag: () => void;
completeProgrammaticDrag: () => void;
moveProgrammaticDragPositionUp: () => void;
moveProgrammaticDragPositionDown: () => void;
expandAll: (treeId: string) => void;
collapseAll: (treeId: string) => void;
expandSubsequently: (
treeId: string,
itemIds: TreeItemIndex[]
) => Promise;
}
export type TreeEnvironmentActionsContextProps = TreeEnvironmentChangeActions;
export interface TreeEnvironmentRef
extends TreeEnvironmentChangeActions,
Omit, keyof TreeChangeHandlers> {
treeEnvironmentContext: TreeEnvironmentContextProps;
dragAndDropContext: DragAndDropContextProps;
activeTreeId?: string;
treeIds: string[];
trees: Record;
}
export interface TreeEnvironmentConfiguration
extends TreeRenderProps,
TreeCapabilities,
TreeChangeHandlers,
ExplicitDataSource {
viewState: TreeViewState;
keyboardBindings?: KeyboardBindings;
liveDescriptors?: LiveDescriptors;
getItemTitle: (item: TreeItem) => string;
}
export interface TreeEnvironmentContextProps
extends Omit, keyof TreeRenderProps>,
AllTreeRenderProps {
registerTree: (tree: TreeConfiguration) => void;
unregisterTree: (treeId: string) => void;
activeTreeId?: string;
setActiveTree: (
treeIdOrSetStateFunction:
| string
| undefined
| ((prevState: string | undefined) => string | undefined),
autoFocus?: boolean
) => void;
treeIds: string[];
trees: Record;
linearItems: Record;
}
export interface DragAndDropContextProps {
onStartDraggingItems: (items: TreeItem[], treeId: string) => void;
draggingItems?: TreeItem[];
itemHeight: number;
isProgrammaticallyDragging?: boolean;
startProgrammaticDrag: () => void;
abortProgrammaticDrag: () => void;
completeProgrammaticDrag: () => void;
programmaticDragUp: () => void;
programmaticDragDown: () => void;
draggingPosition?: DraggingPosition;
viableDragPositions?: { [treeId: string]: DraggingPosition[] };
linearItems?: Array<{ item: TreeItemIndex; depth: number }>;
onDragOverTreeHandler: (
e: DragEvent,
treeId: string,
containerRef: React.MutableRefObject
) => void;
onDragLeaveContainerHandler: (
e: DragEvent,
containerRef: React.MutableRefObject
) => void;
}
export type DraggingPosition =
| DraggingPositionItem
| DraggingPositionBetweenItems
| DraggingPositionRoot;
export interface AbstractDraggingPosition {
targetType: 'item' | 'between-items' | 'root';
treeId: string;
linearIndex: number;
depth: number;
}
export interface DraggingPositionItem extends AbstractDraggingPosition {
targetType: 'item';
targetItem: TreeItemIndex;
parentItem: TreeItemIndex;
}
export interface DraggingPositionBetweenItems extends AbstractDraggingPosition {
targetType: 'between-items';
childIndex: number;
linePosition: 'top' | 'bottom';
parentItem: TreeItemIndex;
}
export interface DraggingPositionRoot extends AbstractDraggingPosition {
targetType: 'root';
targetItem: TreeItemIndex;
}
export interface ControlledTreeEnvironmentProps<
T = any,
C extends string = never
> extends TreeEnvironmentConfiguration {
children?: React.ReactElement | (React.ReactElement | null)[] | null;
}
export interface UncontrolledTreeEnvironmentProps<
T = any,
C extends string = never
> extends TreeRenderProps,
TreeCapabilities,
ImplicitDataSource,
TreeChangeHandlers {
viewState: TreeViewState;
keyboardBindings?: KeyboardBindings;
liveDescriptors?: LiveDescriptors;
getItemTitle: (item: TreeItem) => string;
children: React.ReactElement | (React.ReactElement | null)[] | null;
disableMultiselect?: boolean;
}
export interface TreeConfiguration {
treeId: string;
rootItem: string;
treeLabel?: string;
treeLabelledBy?: string;
}
export interface TreeProps
extends TreeConfiguration,
Partial> {}
export interface TreeContextProps extends TreeConfiguration {
search: string | null;
setSearch: (searchValue: string | null) => void;
renamingItem: TreeItemIndex | null;
setRenamingItem: (item: TreeItemIndex | null) => void;
renderers: AllTreeRenderProps;
treeInformation: TreeInformation;
getItemsLinearly: () => Array<{ item: TreeItemIndex; depth: number }>;
}
export interface TreeChangeActions {
focusTree: (autoFocus?: boolean) => void;
startRenamingItem: (itemId: TreeItemIndex) => void;
stopRenamingItem: () => void;
completeRenamingItem: () => void;
abortRenamingItem: () => void;
renameItem: (itemId: TreeItemIndex, name: string) => void;
collapseItem: (itemId: TreeItemIndex) => void;
expandItem: (itemId: TreeItemIndex) => void;
toggleItemExpandedState: (itemId: TreeItemIndex) => void;
selectItems: (itemsIds: TreeItemIndex[]) => void;
toggleItemSelectStatus: (itemId: TreeItemIndex) => void;
/** @param setDomFocus - Defaults to true. */
focusItem: (itemId: TreeItemIndex, setDomFocus?: boolean) => void;
moveFocusUp: () => void;
moveFocusDown: () => void;
invokePrimaryAction: (itemId: TreeItemIndex) => void;
setSearch: (search: string | null) => void;
abortSearch: () => void;
expandAll: () => void;
collapseAll: () => void;
expandSubsequently: (itemIds: TreeItemIndex[]) => Promise;
}
export type TreeChangeActionsContextProps = TreeChangeActions;
export interface TreeRef extends TreeChangeActions, TreeInformation {
treeContext: TreeContextProps;
treeEnvironmentContext: TreeEnvironmentContextProps;
dragAndDropContext: DragAndDropContextProps;
search: string | null;
}
export interface TreeDataProvider {
onDidChangeTreeData?: (
listener: (changedItemIds: TreeItemIndex[]) => void
) => Disposable;
getTreeItem: (itemId: TreeItemIndex) => Promise>;
getTreeItems?: (itemIds: TreeItemIndex[]) => Promise;
onRenameItem?: (item: TreeItem, name: string) => Promise;
onChangeItemChildren?: (
itemId: TreeItemIndex,
newChildren: TreeItemIndex[]
) => Promise;
}
export type Disposable = {
dispose: () => void;
};
export interface LinearItem {
item: TreeItemIndex;
depth: number;
}
export interface KeyboardBindings {
primaryAction?: string[];
moveFocusToFirstItem?: string[];
moveFocusToLastItem?: string[];
expandSiblings?: string[];
renameItem?: string[];
abortRenameItem?: string[];
toggleSelectItem?: string[];
abortSearch?: string[];
startSearch?: string[];
selectAll?: string[];
startProgrammaticDnd?: string[];
abortProgrammaticDnd?: string[];
completeProgrammaticDnd?: string[];
}
/**
* Live descriptors are written in an aria live region describing the state of the
* tree to accessibility readers. They are displayed in a visually hidden area at the
* bottom of the tree. Each descriptor composes a HTML string. Variables in the form
* of \{variableName\} can be used.
*
* The \{keybinding:bindingname\} variable refers to a specific keybinding, i.e. \{keybinding:primaryAction\}
* is a valid variable.
*
* See the implementation of the `defaultLiveDescriptors` for more details.
*/
export interface LiveDescriptors {
/**
* Supports the following variables:
* \{treeLabel\} \{keybinding:bindingname\}
*/
introduction: string;
/**
* Supports the following variables:
* \{renamingItem\} \{keybinding:bindingname\}
*/
renamingItem: string;
/**
* Supports the following variables:
* \{keybinding:bindingname\}
*/
searching: string;
/**
* Supports the following variables:
* \{dropTarget\} \{dragItems\} \{keybinding:bindingname\}
*/
programmaticallyDragging: string;
/**
* Will be displayed in addition to the programmaticallyDragging description,
* but with the aria-live attribute assertive.
*
* Supports the following variables:
* \{dropTarget\} \{dragItems\} \{keybinding:bindingname\}
*/
programmaticallyDraggingTarget: string;
}
export type HoveringPosition = {
linearIndex: number;
offset: 'bottom' | 'top' | undefined;
// is undefined if tree renderDepthOffset is not set or zero
indentation: number | undefined;
};