diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/components/keyboard-navigation-handler.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/components/keyboard-navigation-handler.tsx index aa1fb1df5..ba3443567 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/components/keyboard-navigation-handler.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/components/keyboard-navigation-handler.tsx @@ -53,7 +53,6 @@ const findFolderInfoForTag = ( nestedTag: NestedTag } | null => { for (const nestedTag of nestedTags) { - // Check if this tag is a folder (has children or nestedChildren) if ( nestedTag.parentTag === targetTag && (nestedTag.children?.length || nestedTag.nestedChildren?.length) @@ -228,6 +227,17 @@ export const KeyboardNavigationHandler: React.FC } } + const scrollIntoView = () => { + setTimeout(() => { + const selectedItem = document.querySelector( + '[data-radix-popper-content-wrapper] [aria-selected="true"]' + ) + if (selectedItem) { + selectedItem.scrollIntoView({ behavior: 'auto', block: 'nearest' }) + } + }, 0) + } + switch (e.key) { case 'ArrowDown': e.preventDefault() @@ -235,11 +245,16 @@ export const KeyboardNavigationHandler: React.FC setKeyboardNav(true) if (visibleIndices.length > 0) { const currentVisibleIndex = visibleIndices.indexOf(selectedIndex) + let newIndex: number if (currentVisibleIndex === -1) { - setSelectedIndex(visibleIndices[0]) + newIndex = visibleIndices[0] } else if (currentVisibleIndex < visibleIndices.length - 1) { - setSelectedIndex(visibleIndices[currentVisibleIndex + 1]) + newIndex = visibleIndices[currentVisibleIndex + 1] + } else { + newIndex = visibleIndices[0] } + setSelectedIndex(newIndex) + scrollIntoView() } break case 'ArrowUp': @@ -248,11 +263,16 @@ export const KeyboardNavigationHandler: React.FC setKeyboardNav(true) if (visibleIndices.length > 0) { const currentVisibleIndex = visibleIndices.indexOf(selectedIndex) + let newIndex: number if (currentVisibleIndex === -1) { - setSelectedIndex(visibleIndices[0]) + newIndex = visibleIndices[visibleIndices.length - 1] } else if (currentVisibleIndex > 0) { - setSelectedIndex(visibleIndices[currentVisibleIndex - 1]) + newIndex = visibleIndices[currentVisibleIndex - 1] + } else { + newIndex = visibleIndices[visibleIndices.length - 1] } + setSelectedIndex(newIndex) + scrollIntoView() } break case 'Enter': diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx index 70acdb7fa..d5fde3119 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx @@ -476,6 +476,7 @@ const FolderContentsInner: React.FC = ({ nestedTag, onNavigateIn, }) => { + const { isKeyboardNav, setKeyboardNav } = usePopoverContext() const currentNestedTag = nestedPath.length > 0 ? nestedPath[nestedPath.length - 1] : nestedTag const parentTagIndex = currentNestedTag.parentTag @@ -489,6 +490,9 @@ const FolderContentsInner: React.FC = ({ = 0} onMouseEnter={() => { + // Skip selection update during keyboard navigation to prevent scroll-triggered selection changes + if (isKeyboardNav) return + setKeyboardNav(false) if (parentTagIndex >= 0) setSelectedIndex(parentTagIndex) }} onMouseDown={(e) => { @@ -533,6 +537,8 @@ const FolderContentsInner: React.FC = ({ key={child.key} active={childGlobalIndex === selectedIndex && childGlobalIndex >= 0} onMouseEnter={() => { + if (isKeyboardNav) return + setKeyboardNav(false) if (childGlobalIndex >= 0) setSelectedIndex(childGlobalIndex) }} onMouseDown={(e) => { @@ -567,6 +573,8 @@ const FolderContentsInner: React.FC = ({ key={`${group.blockId}-${nestedChild.key}`} active={parentGlobalIndex === selectedIndex && parentGlobalIndex >= 0} onMouseEnter={() => { + if (isKeyboardNav) return + setKeyboardNav(false) if (parentGlobalIndex >= 0) setSelectedIndex(parentGlobalIndex) }} onMouseDown={(e) => { @@ -634,6 +642,7 @@ const NestedTagRenderer: React.FC = ({ blocks, getMergedSubBlocks, }) => { + const { isKeyboardNav, setKeyboardNav } = usePopoverContext() const hasChildren = nestedTag.children && nestedTag.children.length > 0 const hasNestedChildren = nestedTag.nestedChildren && nestedTag.nestedChildren.length > 0 @@ -656,6 +665,8 @@ const NestedTagRenderer: React.FC = ({ } }} onMouseEnter={() => { + if (isKeyboardNav) return + setKeyboardNav(false) if (parentGlobalIndex >= 0) { setSelectedIndex(parentGlobalIndex) } @@ -725,6 +736,8 @@ const NestedTagRenderer: React.FC = ({ rootOnly active={globalIndex === selectedIndex && globalIndex >= 0} onMouseEnter={() => { + if (isKeyboardNav) return + setKeyboardNav(false) if (globalIndex >= 0) setSelectedIndex(globalIndex) }} onMouseDown={(e) => { @@ -750,6 +763,126 @@ const NestedTagRenderer: React.FC = ({ ) } +/** + * Hook to get mouse enter handler that respects keyboard navigation mode. + * Returns a handler that only updates selection if not in keyboard mode. + */ +const useKeyboardAwareMouseEnter = ( + setSelectedIndex: (index: number) => void +): ((index: number) => void) => { + const { isKeyboardNav, setKeyboardNav } = usePopoverContext() + + return useCallback( + (index: number) => { + if (isKeyboardNav) return + setKeyboardNav(false) + if (index >= 0) setSelectedIndex(index) + }, + [isKeyboardNav, setKeyboardNav, setSelectedIndex] + ) +} + +/** + * Wrapper for variable tag items that has access to popover context + */ +const VariableTagItem: React.FC<{ + tag: string + globalIndex: number + selectedIndex: number + setSelectedIndex: (index: number) => void + handleTagSelect: (tag: string) => void + itemRefs: React.RefObject> + variableInfo: { type: string; id: string } | null +}> = ({ + tag, + globalIndex, + selectedIndex, + setSelectedIndex, + handleTagSelect, + itemRefs, + variableInfo, +}) => { + const handleMouseEnter = useKeyboardAwareMouseEnter(setSelectedIndex) + + return ( + = 0} + onMouseEnter={() => handleMouseEnter(globalIndex)} + onMouseDown={(e) => { + e.preventDefault() + e.stopPropagation() + handleTagSelect(tag) + }} + ref={(el) => { + if (el && globalIndex >= 0) { + itemRefs.current?.set(globalIndex, el) + } + }} + > + + {tag.startsWith(TAG_PREFIXES.VARIABLE) ? tag.substring(TAG_PREFIXES.VARIABLE.length) : tag} + + {variableInfo && ( + + {variableInfo.type} + + )} + + ) +} + +/** + * Wrapper for block root tag items that has access to popover context + */ +const BlockRootTagItem: React.FC<{ + rootTag: string + rootTagGlobalIndex: number + selectedIndex: number + setSelectedIndex: (index: number) => void + handleTagSelect: (tag: string, group?: BlockTagGroup) => void + itemRefs: React.RefObject> + group: BlockTagGroup + tagIcon: string | React.ComponentType<{ className?: string }> + blockColor: string + blockName: string +}> = ({ + rootTag, + rootTagGlobalIndex, + selectedIndex, + setSelectedIndex, + handleTagSelect, + itemRefs, + group, + tagIcon, + blockColor, + blockName, +}) => { + const handleMouseEnter = useKeyboardAwareMouseEnter(setSelectedIndex) + + return ( + = 0} + onMouseEnter={() => handleMouseEnter(rootTagGlobalIndex)} + onMouseDown={(e) => { + e.preventDefault() + e.stopPropagation() + handleTagSelect(rootTag, group) + }} + ref={(el) => { + if (el && rootTagGlobalIndex >= 0) { + itemRefs.current?.set(rootTagGlobalIndex, el) + } + }} + > + + {blockName} + + ) +} + /** * Helper component to capture popover context for nested navigation */ @@ -1613,7 +1746,7 @@ export const TagDropdown: React.FC = ({ const element = itemRefs.current.get(selectedIndex) if (element) { element.scrollIntoView({ - behavior: 'smooth', + behavior: 'auto', block: 'nearest', }) } @@ -1888,35 +2021,16 @@ export const TagDropdown: React.FC = ({ const globalIndex = flatTagList.findIndex((item) => item.tag === tag) return ( - = 0} - onMouseEnter={() => { - if (globalIndex >= 0) setSelectedIndex(globalIndex) - }} - onMouseDown={(e) => { - e.preventDefault() - e.stopPropagation() - handleTagSelect(tag) - }} - ref={(el) => { - if (el && globalIndex >= 0) { - itemRefs.current.set(globalIndex, el) - } - }} - > - - {tag.startsWith(TAG_PREFIXES.VARIABLE) - ? tag.substring(TAG_PREFIXES.VARIABLE.length) - : tag} - - {variableInfo && ( - - {variableInfo.type} - - )} - + tag={tag} + globalIndex={globalIndex} + selectedIndex={selectedIndex} + setSelectedIndex={setSelectedIndex} + handleTagSelect={handleTagSelect} + itemRefs={itemRefs} + variableInfo={variableInfo} + /> ) })} {nestedBlockTagGroups.length > 0 && } @@ -1951,26 +2065,18 @@ export const TagDropdown: React.FC = ({ return (
- = 0} - onMouseEnter={() => { - if (rootTagGlobalIndex >= 0) setSelectedIndex(rootTagGlobalIndex) - }} - onMouseDown={(e) => { - e.preventDefault() - e.stopPropagation() - handleTagSelect(rootTag, group) - }} - ref={(el) => { - if (el && rootTagGlobalIndex >= 0) { - itemRefs.current.set(rootTagGlobalIndex, el) - } - }} - > - - {group.blockName} - + {group.nestedTags.map((nestedTag) => { if (nestedTag.fullTag === rootTag) { return null