import { HTMLProps, useMemo } from 'react';
import {
TreeItem,
TreeItemActions,
TreeItemRenderContext,
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 '../drag/DragAndDropProvider';
import { useSelectUpTo } from '../tree/useSelectUpTo';
import { useGetOriginalItemOrder } from '../useGetOriginalItemOrder';
// 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) => {
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 getOriginalItemOrder = useGetOriginalItemOrder();
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);
},
stopRenamingItem: () => {
setRenamingItem(null);
},
focusItem: (setDomFocus = true) => {
environment.onFocusItem?.(item, treeId, setDomFocus);
},
startDragging: () => {
let selectedItems = viewState?.selectedItems ?? [];
if (!selectedItems.includes(item.index)) {
selectedItems = [item.index];
environment.onSelectItems?.(selectedItems, treeId);
}
if (canDrag) {
const orderedItems = getOriginalItemOrder(
treeId,
selectedItems.map(id => environment.items[id])
);
dnd.onStartDraggingItems(orderedItems, treeId);
}
},
};
const renderFlags: TreeItemRenderFlags = {
isSelected,
isExpanded,
isFocused: viewState?.focusedItem === item.index,
isRenaming,
isDraggingOver:
dnd.draggingPosition &&
dnd.draggingPosition.targetType === 'item' &&
dnd.draggingPosition.targetItem === item.index &&
dnd.draggingPosition.treeId === treeId,
isDraggingOverParent: false,
isSearchMatching,
canDrag,
canDropOn,
};
const interactiveElementProps: HTMLProps = {
...interactionManager.createInteractiveElementProps(
item,
treeId,
actions,
renderFlags,
viewState
),
...({
'data-rct-item-interactive': true,
'data-rct-item-focus': renderFlags.isFocused ? 'true' : 'false',
'data-rct-item-id': item.index,
} as any),
};
const itemContainerWithoutChildrenProps: HTMLProps = {
...({
'data-rct-item-container': 'true',
} as any),
};
const itemContainerWithChildrenProps: HTMLProps = {
role: 'treeitem',
'aria-selected': renderFlags.isSelected,
'aria-expanded': item.isFolder
? renderFlags.isExpanded
? 'true'
: 'false'
: undefined,
};
const arrowProps: HTMLProps = {
onClick: () => {
if (item.isFolder) {
actions.toggleExpandedState();
}
actions.selectItem();
},
onFocus: () => {
actions.focusItem();
},
onDragOver: e => {
e.preventDefault(); // Allow drop
},
'aria-hidden': true,
tabIndex: -1,
};
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,
arrowProps,
viewStateFlags,
};
}, [
item,
environment,
treeId,
dnd,
isSelected,
isExpanded,
isRenaming,
isSearchMatching,
interactionManager,
selectUpTo,
setRenamingItem,
getOriginalItemOrder,
]);
};