import { HTMLProps, useMemo, CSSProperties } from "react";
import { TreeItem, TreeItemActions, TreeItemIndex, TreeItemRenderFlags } from "../types";
import { defaultMatcher } from "../search/defaultMatcher";
import { useTree } from "../tree/Tree";
import { useTreeEnvironment } from "../controlledEnvironment/ControlledTreeEnvironment";
import { useInteractionManager } from "../controlledEnvironment/InteractionManagerProvider";
import { useDragAndDrop } from "../controlledEnvironment/DragAndDropProvider";
import { useSelectUpTo } from "../tree/useSelectUpTo";
// TODO restructure file. Everything into one hook file without helper methods, let all props be generated outside (InteractionManager and AccessibilityPropsManager), ...
export const useTreeItemRenderContext = (item: TreeItem, parentId: TreeItemIndex, depth: number) => {
const { treeId, search, renamingItem, setRenamingItem } = useTree();
const environment = useTreeEnvironment();
const interactionManager = useInteractionManager();
const dnd = useDragAndDrop();
const selectUpTo = useSelectUpTo("last-focus");
const itemTitle = item && environment.getItemTitle(item);
const isSearchMatching = useMemo(
() =>
search === null || search.length === 0 || !item || !itemTitle
? false
: (environment.doesSearchMatchItem ?? defaultMatcher)(search, item, itemTitle),
[search, item, itemTitle, environment.doesSearchMatchItem]
);
const isSelected = item && environment.viewState[treeId]?.selectedItems?.includes(item.index);
const isExpanded = item && environment.viewState[treeId]?.expandedItems?.includes(item.index);
const isRenaming = item && renamingItem === item.index;
return useMemo(() => {
if (!item) {
return undefined;
}
const viewState = environment.viewState[treeId];
const currentlySelectedItems = (
viewState?.selectedItems?.map((item) => environment.items[item]) ??
(viewState?.focusedItem ? [environment.items[viewState?.focusedItem]] : [])
).filter((item) => !!item);
const isItemPartOfSelectedItems = !!currentlySelectedItems.find(
(selectedItem) => selectedItem.index === item.index
);
const canDragCurrentlySelectedItems =
currentlySelectedItems &&
(environment.canDrag?.(currentlySelectedItems) ?? true) &&
currentlySelectedItems.map((item) => item.canMove ?? true).reduce((a, b) => a && b, true);
const canDragThisItem = (environment.canDrag?.([item]) ?? true) && (item.canMove ?? true);
const canDrag =
environment.canDragAndDrop &&
((isItemPartOfSelectedItems && canDragCurrentlySelectedItems) || (!isItemPartOfSelectedItems && canDragThisItem));
const canDropOn =
environment.canDragAndDrop &&
!!dnd.viableDragPositions?.[treeId]?.find(
(position) => position.targetType === "item" && position.targetItem === item.index
);
const actions: TreeItemActions = {
// TODO disable most actions during rename
primaryAction: () => {
environment.onPrimaryAction?.(environment.items[item.index], treeId);
},
collapseItem: () => {
environment.onCollapseItem?.(item, treeId);
},
expandItem: () => {
environment.onExpandItem?.(item, treeId);
},
toggleExpandedState: () => {
if (isExpanded) {
environment.onCollapseItem?.(item, treeId);
} else {
environment.onExpandItem?.(item, treeId);
}
},
selectItem: () => {
environment.onSelectItems?.([item.index], treeId);
},
addToSelectedItems: () => {
environment.onSelectItems?.([...(viewState?.selectedItems ?? []), item.index], treeId);
},
unselectItem: () => {
environment.onSelectItems?.(viewState?.selectedItems?.filter((id) => id !== item.index) ?? [], treeId);
},
selectUpTo: (overrideOldSelection) => {
selectUpTo(item, overrideOldSelection);
},
startRenamingItem: () => {
setRenamingItem(item.index);
},
focusItem: () => {
environment.onFocusItem?.(item, treeId);
},
startDragging: () => {
let selectedItems = viewState?.selectedItems ?? [];
if (!selectedItems.includes(item.index)) {
selectedItems = [item.index];
environment.onSelectItems?.(selectedItems, treeId);
}
if (canDrag) {
dnd.onStartDraggingItems(
selectedItems.map((id) => environment.items[id]),
treeId
);
}
},
};
const renderFlags: TreeItemRenderFlags = {
isSelected,
isExpanded,
isFocused: viewState?.focusedItem === item.index,
isRenaming,
isSearchMatching,
canDrag,
canDropOn,
};
const interactiveElementProps: HTMLProps = {
...interactionManager.createInteractiveElementProps(item, treeId, actions, renderFlags, viewState),
// @ts-expect-error non-standard attribute
"data-rct-item-interactive": true,
"data-rct-item-focus": renderFlags.isFocused ? "true" : "false",
};
const itemContainerWithoutChildrenProps: HTMLProps = {
// @ts-expect-error non-standard attribute
"data-rct-item-container": "true",
};
const itemContainerWithChildrenProps: HTMLProps = {
role: "treeitem",
"aria-selected": renderFlags.isSelected,
"aria-expanded": item.isFolder ? (renderFlags.isExpanded ? "true" : "false") : undefined,
// @ts-expect-error non-standard attribute
"data-rct-item-id": item.index,
"data-rct-is-folder": item.isFolder ? "true" : "false",
"data-rct-parent-id": parentId,
style: {
"--depth": depth,
} as CSSProperties,
onDragEnter: dnd.onDragEnterTreeItemHandler,
};
const viewStateFlags = !viewState
? {}
: Object.entries(viewState).reduce((acc, [key, value]) => {
acc[key] = Array.isArray(value) ? value.includes(item.index) : value === item.index;
return acc;
}, {} as { [key: string]: boolean });
return {
...actions,
...renderFlags,
interactiveElementProps,
itemContainerWithChildrenProps,
itemContainerWithoutChildrenProps,
viewStateFlags,
};
}, [
item,
environment,
treeId,
dnd,
isSelected,
isExpanded,
isRenaming,
isSearchMatching,
interactionManager,
selectUpTo,
setRenamingItem,
]);
};