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) => {