From 55aae4491c46dca7e81fc7e5c6ddd6ff99d9533e Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Wed, 15 Mar 2023 22:48:21 +0300 Subject: [PATCH] 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. --- .../sidebar-left/navigator/navigator.tsx | 5 +- .../src/components/tree/item-utils.ts | 29 ++++++ .../src/components/tree/tree.stories.tsx | 15 +++- .../src/components/tree/tree.tsx | 90 ++++++++++--------- 4 files changed, 92 insertions(+), 47 deletions(-) create mode 100644 packages/design-system/src/components/tree/item-utils.ts diff --git a/apps/builder/app/builder/features/sidebar-left/navigator/navigator.tsx b/apps/builder/app/builder/features/sidebar-left/navigator/navigator.tsx index 914ba9748b..0cbc084ff8 100644 --- a/apps/builder/app/builder/features/sidebar-left/navigator/navigator.tsx +++ b/apps/builder/app/builder/features/sidebar-left/navigator/navigator.tsx @@ -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) => { { + // 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(","); +}; diff --git a/packages/design-system/src/components/tree/tree.stories.tsx b/packages/design-system/src/components/tree/tree.stories.tsx index 09549fe00f..bc52edd343 100644 --- a/packages/design-system/src/components/tree/tree.stories.tsx +++ b/packages/design-system/src/components/tree/tree.stories.tsx @@ -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 => { @@ -72,7 +73,9 @@ export const StressTest = ({ animate }: { animate: boolean }) => { }; }); - const [selectedItemId, setSelectedItemId] = useState(); + const [selectedItemSelector, setSelectedItemSelector] = useState< + undefined | ItemSelector + >(); return ( @@ -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) => ( {props.itemData.id} diff --git a/packages/design-system/src/components/tree/tree.tsx b/packages/design-system/src/components/tree/tree.tsx index 2ec9e5bb52..84a8d6f6a2 100644 --- a/packages/design-system/src/components/tree/tree.tsx +++ b/packages/design-system/src/components/tree/tree.tsx @@ -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 = { root: Data; + selectedItemSelector: undefined | ItemSelector; canLeaveParent: (item: Data) => boolean; canAcceptChild: (item: Data) => boolean; @@ -24,7 +30,6 @@ export type TreeProps = { getItemChildren: (item: Data) => Data[]; renderItem: (props: TreeItemRenderProps) => React.ReactNode; - selectedItemId?: string; onSelect?: (itemId: string) => void; onHover?: (item: Data | undefined) => void; animate?: boolean; @@ -36,13 +41,13 @@ export type TreeProps = { export const Tree = ({ root, + selectedItemSelector, canLeaveParent, canAcceptChild, findItemById, getItemPath, getItemChildren, renderItem, - selectedItemId, onSelect, onHover, animate, @@ -50,8 +55,7 @@ export const Tree = ({ }: TreeProps) => { const { getIsExpanded, setIsExpanded } = useExpandState({ root, - selectedItemId, - getItemPath, + selectedItemSelector, getItemChildren, }); @@ -230,7 +234,7 @@ export const Tree = ({ root, getItemChildren, findItemById, - selectedItemId, + selectedItemSelector, getIsExpanded, setIsExpanded, onEsc: dragHandlers.cancelCurrentDrag, @@ -273,7 +277,7 @@ export const Tree = ({ onSelect={onSelect} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} - selectedItemId={selectedItemId} + selectedItemId={selectedItemSelector?.[0]} itemData={root} level={0} getIsExpanded={getIsExpanded} @@ -299,37 +303,40 @@ export const Tree = ({ const useKeyboardNavigation = ({ 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 = ({ 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 = ({ } } 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 = ({ }; 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 = ({ 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 = ({ // 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 = ({ }; const useExpandState = ({ - 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>({}); // 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 = ({ } return newRecord; }); - }, [record, root, selectedItemId, getItemPath]); + }, [record, selectedItemSelector]); const getIsExpanded = useCallback( (instance: Data) => {