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, ]); };