Pass selected instance selector to tree (#1250)

Selector is a list of ids from root to target to allow identify
instances even with duplicated ids.

Here is the first step to support selectors in navigator. Passing
selected instance selector already helped to get rid of one getItemPath
usage.
This commit is contained in:
Bogdan Chadkin authored and GitHub committed 2023-03-15 22:48:21 +03:00
1 parent 6d4fab40b5
commit 55aae4491c
4 files changed
+92 -47

No files matched your search

@@ -20,7 +20,6 @@ type NavigatorProps = {
export const Navigator = ({ isClosable, onClose }: NavigatorProps) => {
const selectedInstanceSelector = useStore(selectedInstanceSelectorStore);
const selectedInstanceId = selectedInstanceSelector?.[0];
const [rootInstance] = useRootInstance();
const handleSelect = useCallback((instanceId: Instance["id"]) => {
@@ -64,8 +63,8 @@ export const Navigator = ({ isClosable, onClose }: NavigatorProps) => {
<Flex css={{ flexGrow: 1, flexDirection: "column" }}>
<InstanceTree
root={rootInstance}
// @todo accept and provide in callback instance selector instead of just id
selectedItemId={selectedInstanceId}
selectedItemSelector={selectedInstanceSelector}
// @todo provide in callback instance selector instead of just id
onSelect={handleSelect}
onHover={handleHover}
onDragEnd={handleDragEnd}
@@ -0,0 +1,29 @@
export type ItemId = string;
export type ItemSelector = string[];
export const getElementByItemSelector = (
root: undefined | Element,
itemSelector: ItemSelector
) => {
// query item from root to target
const domSelector = itemSelector
.map((id) => `[data-drop-target-id="${id}"]`)
.reverse()
.join(" ");
const [itemId] = itemSelector;
return (
root?.querySelector(`${domSelector} [data-item-button-id="${itemId}"]`) ??
undefined
);
};
export const areItemSelectorsEqual = (
left?: ItemSelector,
right?: ItemSelector
) => {
if (left === undefined || right === undefined) {
return false;
}
return left.join(",") === right.join(",");
};
@@ -11,6 +11,7 @@ import {
} from "./test-tree-data";
import { Flex } from "../flex";
import { TreeItemLabel, TreeItemBody } from "./tree-node";
import type { ItemSelector } from "./item-utils";
export const StressTest = ({ animate }: { animate: boolean }) => {
const [root, setRoot] = useState<Item>((): Item => {
@@ -72,7 +73,9 @@ export const StressTest = ({ animate }: { animate: boolean }) => {
};
});
const [selectedItemId, setSelectedItemId] = useState<string | undefined>();
const [selectedItemSelector, setSelectedItemSelector] = useState<
undefined | ItemSelector
>();
return (
<Flex css={{ width: 300, height: 500, flexDirection: "column" }}>
@@ -84,8 +87,14 @@ export const StressTest = ({ animate }: { animate: boolean }) => {
getItemChildren={getItemChildren}
animate={animate}
root={root}
selectedItemId={selectedItemId}
onSelect={(instanceId) => setSelectedItemId(instanceId)}
selectedItemSelector={selectedItemSelector}
onSelect={(instanceId) => {
setSelectedItemSelector(
getItemPath(root, instanceId)
.reverse()
.map((item) => item.id)
);
}}
renderItem={(props) => (
<TreeItemBody {...props}>
<TreeItemLabel>{props.itemData.id}</TreeItemLabel>
@@ -13,9 +13,15 @@ import {
} from "../primitives/dnd";
import { Box } from "../box";
import { useHorizontalShift } from "./horizontal-shift";
import {
type ItemSelector,
areItemSelectorsEqual,
getElementByItemSelector,
} from "./item-utils";
export type TreeProps<Data extends { id: string }> = {
root: Data;
selectedItemSelector: undefined | ItemSelector;
canLeaveParent: (item: Data) => boolean;
canAcceptChild: (item: Data) => boolean;
@@ -24,7 +30,6 @@ export type TreeProps<Data extends { id: string }> = {
getItemChildren: (item: Data) => Data[];
renderItem: (props: TreeItemRenderProps<Data>) => React.ReactNode;
selectedItemId?: string;
onSelect?: (itemId: string) => void;
onHover?: (item: Data | undefined) => void;
animate?: boolean;
@@ -36,13 +41,13 @@ export type TreeProps<Data extends { id: string }> = {
export const Tree = <Data extends { id: string }>({
root,
selectedItemSelector,
canLeaveParent,
canAcceptChild,
findItemById,
getItemPath,
getItemChildren,
renderItem,
selectedItemId,
onSelect,
onHover,
animate,
@@ -50,8 +55,7 @@ export const Tree = <Data extends { id: string }>({
}: TreeProps<Data>) => {
const { getIsExpanded, setIsExpanded } = useExpandState({
root,
selectedItemId,
getItemPath,
selectedItemSelector,
getItemChildren,
});
@@ -230,7 +234,7 @@ export const Tree = <Data extends { id: string }>({
root,
getItemChildren,
findItemById,
selectedItemId,
selectedItemSelector,
getIsExpanded,
setIsExpanded,
onEsc: dragHandlers.cancelCurrentDrag,
@@ -273,7 +277,7 @@ export const Tree = <Data extends { id: string }>({
onSelect={onSelect}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
selectedItemId={selectedItemId}
selectedItemId={selectedItemSelector?.[0]}
itemData={root}
level={0}
getIsExpanded={getIsExpanded}
@@ -299,37 +303,40 @@ export const Tree = <Data extends { id: string }>({
const useKeyboardNavigation = <Data extends { id: string }>({
root,
selectedItemSelector,
getItemChildren,
findItemById,
selectedItemId,
getIsExpanded,
setIsExpanded,
onEsc,
}: {
root: Data;
selectedItemSelector: undefined | ItemSelector;
getItemChildren: (item: Data) => Data[];
findItemById: (root: Data, id: string) => Data | undefined;
selectedItemId: string | undefined;
getIsExpanded: (instance: Data) => boolean;
setIsExpanded: (instance: Data, isExpanded: boolean) => void;
onEsc: () => void;
}) => {
const selectedItem = useMemo(() => {
if (selectedItemId === undefined) {
if (selectedItemSelector === undefined) {
return undefined;
}
const [selectedItemId] = selectedItemSelector;
return findItemById(root, selectedItemId);
}, [root, selectedItemId, findItemById]);
}, [root, selectedItemSelector, findItemById]);
const flatCurrentlyExpandedTree = useMemo(() => {
const result = [] as string[];
const traverse = (instance: Data) => {
result.push(instance.id);
if (getIsExpanded(instance)) {
getItemChildren(instance).forEach(traverse);
const result: ItemSelector[] = [];
const traverse = (item: Data, itemSelector: ItemSelector) => {
result.push(itemSelector);
if (getIsExpanded(item)) {
for (const child of getItemChildren(item)) {
traverse(child, [child.id, ...itemSelector]);
}
}
};
traverse(root);
traverse(root, [root.id]);
return result;
}, [root, getIsExpanded, getItemChildren]);
@@ -353,7 +360,9 @@ const useKeyboardNavigation = <Data extends { id: string }>({
event.preventDefault();
}
if (event.key === "ArrowUp") {
const index = flatCurrentlyExpandedTree.indexOf(selectedItem.id);
const index = flatCurrentlyExpandedTree.findIndex((itemSelector) =>
areItemSelectorsEqual(itemSelector, selectedItemSelector)
);
if (index > 0) {
setFocus(flatCurrentlyExpandedTree[index - 1], "changing");
// prevent scrolling
@@ -361,7 +370,9 @@ const useKeyboardNavigation = <Data extends { id: string }>({
}
}
if (event.key === "ArrowDown") {
const index = flatCurrentlyExpandedTree.indexOf(selectedItem.id);
const index = flatCurrentlyExpandedTree.findIndex((itemSelector) =>
areItemSelectorsEqual(itemSelector, selectedItemSelector)
);
if (index < flatCurrentlyExpandedTree.length - 1) {
setFocus(flatCurrentlyExpandedTree[index + 1], "changing");
// prevent scrolling
@@ -374,9 +385,10 @@ const useKeyboardNavigation = <Data extends { id: string }>({
};
const setFocus = useCallback(
(itemId: string, reason: "restoring" | "changing") => {
const itemButton = rootRef.current?.querySelector(
`[data-item-button-id="${itemId}"]`
(itemSelector: ItemSelector, reason: "restoring" | "changing") => {
const itemButton = getElementByItemSelector(
rootRef.current ?? undefined,
itemSelector
);
if (itemButton instanceof HTMLElement) {
itemButton.focus({ preventScroll: reason === "restoring" });
@@ -400,11 +412,11 @@ const useKeyboardNavigation = <Data extends { id: string }>({
isRootChanged &&
haveFocus === false &&
hadFocus.current === true &&
selectedItemId !== undefined
selectedItemSelector !== undefined
) {
setFocus(selectedItemId, "restoring");
setFocus(selectedItemSelector, "restoring");
}
}, [root, rootRef, selectedItemId, setFocus]);
}, [root, rootRef, selectedItemSelector, setFocus]);
// onBlur doesn't fire when the activeElement is removed from the DOM
useEffect(() => {
@@ -429,8 +441,8 @@ const useKeyboardNavigation = <Data extends { id: string }>({
// When clicking anywhere else in the tree,
// make sure the selected item doesn't loose focus.
if (selectedItemId !== undefined) {
setFocus(selectedItemId, "restoring");
if (selectedItemSelector !== undefined) {
setFocus(selectedItemSelector, "restoring");
}
},
handleBlur() {
@@ -440,36 +452,32 @@ const useKeyboardNavigation = <Data extends { id: string }>({
};
const useExpandState = <Data extends { id: string }>({
selectedItemId,
selectedItemSelector,
root,
getItemPath,
getItemChildren,
}: {
root: Data;
getItemPath: (root: Data, id: string) => Data[];
getItemChildren: (item: Data) => Data[];
selectedItemId?: string;
selectedItemSelector: undefined | ItemSelector;
}) => {
const [record, setRecord] = useState<Record<string, boolean>>({});
// We want to automatically expand all parents of the selected instance whenever it changes
const prevSelectedItemId = useRef(selectedItemId);
const prevRoot = useRef(root);
const prevSelectedItemSelector = useRef(selectedItemSelector);
useEffect(() => {
if (
selectedItemId === prevSelectedItemId.current &&
prevRoot.current === root
areItemSelectorsEqual(
selectedItemSelector,
prevSelectedItemSelector.current
)
) {
return;
}
prevSelectedItemId.current = selectedItemId;
prevRoot.current = root;
if (selectedItemId === undefined) {
prevSelectedItemSelector.current = selectedItemSelector;
if (selectedItemSelector === undefined) {
return;
}
const path = getItemPath(root, selectedItemId).map(
(instance) => instance.id
);
const path = selectedItemSelector.slice().reverse();
// Don't want to expand the selected instance itself
path.pop();
const toExpand = path.filter((id) => record[id] !== true);
@@ -483,7 +491,7 @@ const useExpandState = <Data extends { id: string }>({
}
return newRecord;
});
}, [record, root, selectedItemId, getItemPath]);
}, [record, selectedItemSelector]);
const getIsExpanded = useCallback(
(instance: Data) => {