diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/code/code.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/code/code.tsx index d2df682ea37..b3e165009cc 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/code/code.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/code/code.tsx @@ -28,7 +28,7 @@ import { import { checkTagTrigger, TagDropdown, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown' +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-value' import { WandPromptBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/wand-prompt-bar/wand-prompt-bar' import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/condition-input/condition-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/condition-input/condition-input.tsx index f93fd96dd59..0e3f2d8c624 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/condition-input/condition-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/condition-input/condition-input.tsx @@ -28,7 +28,7 @@ import { import { checkTagTrigger, TagDropdown, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown' +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-value' import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' import { useTagSelection } from '@/hooks/use-tag-selection' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/document-tag-entry/document-tag-entry.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/document-tag-entry/document-tag-entry.tsx index 03b58b46d73..ed29e96464b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/document-tag-entry/document-tag-entry.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/document-tag-entry/document-tag-entry.tsx @@ -7,7 +7,7 @@ import { Input } from '@/components/ui/input' import { MAX_TAG_SLOTS } from '@/lib/knowledge/consts' import { cn } from '@/lib/utils' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/formatted-text' -import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown' +import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-input' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-value' import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/eval-input/eval-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/eval-input/eval-input.tsx index 902b06eb793..25e104aa0be 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/eval-input/eval-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/eval-input/eval-input.tsx @@ -7,7 +7,7 @@ import { Textarea } from '@/components/emcn/components/textarea/textarea' import { Label } from '@/components/ui/label' import { cn } from '@/lib/utils' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/formatted-text' -import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown' +import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-input' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-value' import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/input-mapping/input-mapping.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/input-mapping/input-mapping.tsx index 05696a37c79..e908931d446 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/input-mapping/input-mapping.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/input-mapping/input-mapping.tsx @@ -3,7 +3,7 @@ import { Input } from '@/components/emcn/components/input/input' import { Label } from '@/components/ui/label' import { cn } from '@/lib/utils' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/formatted-text' -import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown' +import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-input' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-value' import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx index 320fbcef07c..d5f98da202a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx @@ -9,7 +9,7 @@ import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/ import { checkTagTrigger, TagDropdown, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown' +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' import type { SubBlockConfig } from '@/blocks/types' import { useKnowledgeBaseTagDefinitions } from '@/hooks/use-knowledge-base-tag-definitions' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/mcp-dynamic-args/mcp-dynamic-args.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/mcp-dynamic-args/mcp-dynamic-args.tsx index 08fd87efa59..685730fd15f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/mcp-dynamic-args/mcp-dynamic-args.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/mcp-dynamic-args/mcp-dynamic-args.tsx @@ -18,7 +18,7 @@ import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/ import { checkTagTrigger, TagDropdown, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown' +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-value' import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' import { useMcpTools } from '@/hooks/use-mcp-tools' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/starter/input-format.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/starter/input-format.tsx index 0642ffff438..d44b5658f37 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/starter/input-format.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/starter/input-format.tsx @@ -14,7 +14,7 @@ import type { ComboboxOption } from '@/components/emcn/components/combobox/combo import { Label } from '@/components/ui/label' import { cn } from '@/lib/utils' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/formatted-text' -import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown' +import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-input' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-value' import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/sub-block-dropdowns.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/sub-block-dropdowns.tsx index eb3f42db75c..d1473321f2b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/sub-block-dropdowns.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/sub-block-dropdowns.tsx @@ -1,5 +1,5 @@ import { EnvVarDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/env-var-dropdown' -import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown' +import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' /** * Props for the SubBlockDropdowns component. diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/sub-block-input-controller.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/sub-block-input-controller.tsx index f6e08d8b8ce..f845720b5b2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/sub-block-input-controller.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/sub-block-input-controller.tsx @@ -1,6 +1,6 @@ import type React from 'react' import { EnvVarDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/env-var-dropdown' -import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown' +import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-input' import type { SubBlockConfig } from '@/blocks/types' import { useTagSelection } from '@/hooks/use-tag-selection' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/table/table.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/table/table.tsx index 5b49e34fc13..7726028de2f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/table/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/table/table.tsx @@ -7,7 +7,7 @@ import { createLogger } from '@/lib/logs/console/logger' import { cn } from '@/lib/utils' import { EnvVarDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/env-var-dropdown' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/formatted-text' -import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown' +import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-input' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-value' import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/components/index.ts new file mode 100644 index 00000000000..9882487d1ee --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/components/index.ts @@ -0,0 +1 @@ +export { KeyboardNavigationHandler } from './keyboard-navigation-handler' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/components/keyboard-navigation-handler.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/components/keyboard-navigation-handler.tsx new file mode 100644 index 00000000000..b4fc6b7ae40 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/components/keyboard-navigation-handler.tsx @@ -0,0 +1,256 @@ +import { useEffect, useMemo } from 'react' +import { usePopoverContext } from '@/components/emcn' +import type { BlockTagGroup, NestedBlockTagGroup } from '../types' + +/** + * Keyboard navigation handler component that uses popover context + * to enable folder navigation with arrow keys + */ +interface KeyboardNavigationHandlerProps { + visible: boolean + selectedIndex: number + setSelectedIndex: (index: number) => void + flatTagList: Array<{ tag: string; group?: BlockTagGroup }> + nestedBlockTagGroups: NestedBlockTagGroup[] + handleTagSelect: (tag: string, group?: BlockTagGroup) => void +} + +export const KeyboardNavigationHandler: React.FC = ({ + visible, + selectedIndex, + setSelectedIndex, + flatTagList, + nestedBlockTagGroups, + handleTagSelect, +}) => { + const { openFolder, closeFolder, isInFolder, currentFolder } = usePopoverContext() + + const visibleIndices = useMemo(() => { + const indices: number[] = [] + + if (isInFolder && currentFolder) { + for (const group of nestedBlockTagGroups) { + for (const nestedTag of group.nestedTags) { + const folderId = `${group.blockId}-${nestedTag.key}` + if (folderId === currentFolder && nestedTag.children) { + // First, add the parent tag itself (so it's navigable as the first item) + if (nestedTag.parentTag) { + const parentIdx = flatTagList.findIndex((item) => item.tag === nestedTag.parentTag) + if (parentIdx >= 0) { + indices.push(parentIdx) + } + } + // Then add all children + for (const child of nestedTag.children) { + const idx = flatTagList.findIndex((item) => item.tag === child.fullTag) + if (idx >= 0) { + indices.push(idx) + } + } + break + } + } + } + } else { + // We're at root level, show all non-child items + // (variables and parent tags, but not their children) + for (let i = 0; i < flatTagList.length; i++) { + const tag = flatTagList[i].tag + + // Check if this is a child of a parent folder + let isChild = false + for (const group of nestedBlockTagGroups) { + for (const nestedTag of group.nestedTags) { + if (nestedTag.children) { + for (const child of nestedTag.children) { + if (child.fullTag === tag) { + isChild = true + break + } + } + } + if (isChild) break + } + if (isChild) break + } + + if (!isChild) { + indices.push(i) + } + } + } + + return indices + }, [isInFolder, currentFolder, flatTagList, nestedBlockTagGroups]) + + // Auto-select first visible item when entering/exiting folders + useEffect(() => { + if (!visible || visibleIndices.length === 0) return + + if (!visibleIndices.includes(selectedIndex)) { + setSelectedIndex(visibleIndices[0]) + } + }, [visible, isInFolder, currentFolder, visibleIndices, selectedIndex, setSelectedIndex]) + + useEffect(() => { + if (!visible || !flatTagList.length) return + + // Helper to open a folder with proper selection callback and parent selection + const openFolderWithSelection = ( + folderId: string, + folderTitle: string, + parentTag: string, + group: BlockTagGroup + ) => { + const selectionCallback = () => handleTagSelect(parentTag, group) + + // Find parent tag index (which is first in visible items when in folder) + let parentIndex = 0 + for (const g of nestedBlockTagGroups) { + for (const nestedTag of g.nestedTags) { + if (nestedTag.parentTag === parentTag) { + const idx = flatTagList.findIndex((item) => item.tag === nestedTag.parentTag) + parentIndex = idx >= 0 ? idx : 0 + break + } + } + } + + openFolder(folderId, folderTitle, undefined, selectionCallback) + setSelectedIndex(parentIndex) + } + + const handleKeyboardEvent = (e: KeyboardEvent) => { + const selected = flatTagList[selectedIndex] + if (!selected && e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return + + let currentFolderInfo: { + id: string + title: string + parentTag: string + group: BlockTagGroup + } | null = null + + if (selected) { + for (const group of nestedBlockTagGroups) { + for (const nestedTag of group.nestedTags) { + if ( + nestedTag.parentTag === selected.tag && + nestedTag.children && + nestedTag.children.length > 0 + ) { + currentFolderInfo = { + id: `${selected.group?.blockId}-${nestedTag.key}`, + title: nestedTag.display, + parentTag: nestedTag.parentTag, + group, + } + break + } + } + if (currentFolderInfo) break + } + } + + switch (e.key) { + case 'ArrowDown': + e.preventDefault() + e.stopPropagation() + if (visibleIndices.length > 0) { + const currentVisibleIndex = visibleIndices.indexOf(selectedIndex) + if (currentVisibleIndex === -1) { + setSelectedIndex(visibleIndices[0]) + } else if (currentVisibleIndex < visibleIndices.length - 1) { + setSelectedIndex(visibleIndices[currentVisibleIndex + 1]) + } + } + break + case 'ArrowUp': + e.preventDefault() + e.stopPropagation() + if (visibleIndices.length > 0) { + const currentVisibleIndex = visibleIndices.indexOf(selectedIndex) + if (currentVisibleIndex === -1) { + setSelectedIndex(visibleIndices[0]) + } else if (currentVisibleIndex > 0) { + setSelectedIndex(visibleIndices[currentVisibleIndex - 1]) + } + } + break + case 'Enter': + e.preventDefault() + e.stopPropagation() + if (selected && selectedIndex >= 0 && selectedIndex < flatTagList.length) { + if (currentFolderInfo && !isInFolder) { + // It's a folder, open it + openFolderWithSelection( + currentFolderInfo.id, + currentFolderInfo.title, + currentFolderInfo.parentTag, + currentFolderInfo.group + ) + } else { + // Not a folder, select it + handleTagSelect(selected.tag, selected.group) + } + } + break + case 'ArrowRight': + if (currentFolderInfo && !isInFolder) { + e.preventDefault() + e.stopPropagation() + openFolderWithSelection( + currentFolderInfo.id, + currentFolderInfo.title, + currentFolderInfo.parentTag, + currentFolderInfo.group + ) + } + break + case 'ArrowLeft': + if (isInFolder) { + e.preventDefault() + e.stopPropagation() + closeFolder() + let firstRootIndex = 0 + for (let i = 0; i < flatTagList.length; i++) { + const tag = flatTagList[i].tag + const isVariable = !tag.includes('.') + let isParent = false + for (const group of nestedBlockTagGroups) { + for (const nestedTag of group.nestedTags) { + if (nestedTag.parentTag === tag) { + isParent = true + break + } + } + if (isParent) break + } + if (isVariable || isParent) { + firstRootIndex = i + break + } + } + setSelectedIndex(firstRootIndex) + } + break + } + } + + window.addEventListener('keydown', handleKeyboardEvent, true) + return () => window.removeEventListener('keydown', handleKeyboardEvent, true) + }, [ + visible, + selectedIndex, + visibleIndices, + flatTagList, + nestedBlockTagGroups, + openFolder, + closeFolder, + isInFolder, + setSelectedIndex, + handleTagSelect, + ]) + + return null +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx similarity index 91% rename from apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown.tsx rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx index df9164c8050..bd5cbcb2743 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { shallow } from 'zustand/shallow' import { Popover, @@ -9,6 +9,7 @@ import { PopoverItem, PopoverScrollArea, PopoverSection, + usePopoverContext, } from '@/components/emcn' import { createLogger } from '@/lib/logs/console/logger' import { extractFieldsFromSchema, parseResponseFormatSafely } from '@/lib/response-format' @@ -25,37 +26,11 @@ import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' import type { BlockState } from '@/stores/workflows/workflow/types' import { getTool } from '@/tools/utils' +import { KeyboardNavigationHandler } from './components/keyboard-navigation-handler' +import type { BlockTagGroup, NestedBlockTagGroup, NestedTag } from './types' const logger = createLogger('TagDropdown') -/** - * Block tag group for organizing tags by block - */ -interface BlockTagGroup { - blockName: string - blockId: string - blockType: string - tags: string[] - distance: number -} - -/** - * Nested tag structure for hierarchical display - */ -interface NestedTag { - key: string - display: string - fullTag?: string - children?: Array<{ key: string; display: string; fullTag: string }> -} - -/** - * Block tag group with nested tag structure - */ -interface NestedBlockTagGroup extends BlockTagGroup { - nestedTags: NestedTag[] -} - /** * Props for the TagDropdown component */ @@ -378,6 +353,55 @@ const TagIcon: React.FC<{ icon: string; color: string }> = ({ icon, color }) => ) +/** + * Wrapper for PopoverBackButton that handles parent tag navigation + */ +const TagDropdownBackButton: React.FC<{ + selectedIndex: number + setSelectedIndex: (index: number) => void + flatTagList: Array<{ tag: string; group?: BlockTagGroup }> + nestedBlockTagGroups: NestedBlockTagGroup[] + itemRefs: React.MutableRefObject> +}> = ({ selectedIndex, setSelectedIndex, flatTagList, nestedBlockTagGroups, itemRefs }) => { + const { currentFolder } = usePopoverContext() + + // Find parent tag info for current folder + const parentTagInfo = useMemo(() => { + if (!currentFolder) return null + + for (const group of nestedBlockTagGroups) { + for (const nestedTag of group.nestedTags) { + const folderId = `${group.blockId}-${nestedTag.key}` + if (folderId === currentFolder && nestedTag.parentTag) { + const parentIdx = flatTagList.findIndex((item) => item.tag === nestedTag.parentTag) + return parentIdx >= 0 ? { index: parentIdx } : null + } + } + } + return null + }, [currentFolder, nestedBlockTagGroups, flatTagList]) + + if (!parentTagInfo) { + return + } + + const isActive = parentTagInfo.index === selectedIndex + + return ( + { + if (el) { + itemRefs.current.set(parentTagInfo.index, el) + } + }} + folderTitleActive={isActive} + onFolderTitleMouseEnter={() => { + setSelectedIndex(parentTagInfo.index) + }} + /> + ) +} + /** * TagDropdown component that displays available tags (variables and block outputs) * for selection in input fields. Uses the Popover component system for consistent styling. @@ -395,6 +419,7 @@ export const TagDropdown: React.FC = ({ inputRef, }) => { const [selectedIndex, setSelectedIndex] = useState(0) + const itemRefs = useRef>(new Map()) const { blocks, edges, loops, parallels } = useWorkflowStore( (state) => ({ @@ -1053,11 +1078,23 @@ export const TagDropdown: React.FC = ({ }) Object.entries(groupedTags).forEach(([parent, children]) => { - nestedTags.push({ - key: parent, - display: parent, - children: children, - }) + const firstChildTag = children[0]?.fullTag + if (firstChildTag) { + const tagParts = firstChildTag.split('.') + const parentTag = `${tagParts[0]}.${parent}` + nestedTags.push({ + key: parent, + display: parent, + parentTag: parentTag, + children: children, + }) + } else { + nestedTags.push({ + key: parent, + display: parent, + children: children, + }) + } }) directTags.forEach((directTag) => { @@ -1080,15 +1117,36 @@ export const TagDropdown: React.FC = ({ nestedBlockTagGroups.forEach((group) => { group.nestedTags.forEach((nestedTag) => { + if (nestedTag.parentTag) { + list.push({ tag: nestedTag.parentTag, group }) + } if (nestedTag.fullTag) { list.push({ tag: nestedTag.fullTag, group }) } + if (nestedTag.children) { + nestedTag.children.forEach((child) => { + list.push({ tag: child.fullTag, group }) + }) + } }) }) return list }, [variableTags, nestedBlockTagGroups]) + // Auto-scroll selected item into view + useEffect(() => { + if (!visible || selectedIndex < 0) return + + const element = itemRefs.current.get(selectedIndex) + if (element) { + element.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + }) + } + }, [selectedIndex, visible]) + const handleTagSelect = useCallback( (tag: string, blockGroup?: BlockTagGroup) => { let liveCursor = cursorPosition @@ -1192,27 +1250,7 @@ export const TagDropdown: React.FC = ({ useEffect(() => { if (visible) { const handleKeyboardEvent = (e: KeyboardEvent) => { - if (!flatTagList.length) return - switch (e.key) { - case 'ArrowDown': - e.preventDefault() - e.stopPropagation() - setSelectedIndex((prev) => Math.min(prev + 1, flatTagList.length - 1)) - break - case 'ArrowUp': - e.preventDefault() - e.stopPropagation() - setSelectedIndex((prev) => Math.max(prev - 1, 0)) - break - case 'Enter': - e.preventDefault() - e.stopPropagation() - if (selectedIndex >= 0 && selectedIndex < flatTagList.length) { - const selected = flatTagList[selectedIndex] - handleTagSelect(selected.tag, selected.group) - } - break case 'Escape': e.preventDefault() e.stopPropagation() @@ -1224,7 +1262,7 @@ export const TagDropdown: React.FC = ({ window.addEventListener('keydown', handleKeyboardEvent, true) return () => window.removeEventListener('keydown', handleKeyboardEvent, true) } - }, [visible, selectedIndex, flatTagList, handleTagSelect, onClose]) + }, [visible, onClose]) if (!visible || tags.length === 0 || flatTagList.length === 0) return null @@ -1257,6 +1295,14 @@ export const TagDropdown: React.FC = ({ }} /> + = ({ onOpenAutoFocus={(e) => e.preventDefault()} onCloseAutoFocus={(e) => e.preventDefault()} > - + {flatTagList.length === 0 ? (
@@ -1277,7 +1329,7 @@ export const TagDropdown: React.FC = ({ {variableTags.length > 0 && ( <> Variables - {variableTags.map((tag: string, index: number) => { + {variableTags.map((tag: string) => { const variableInfo = variableInfoMap?.[tag] || null const globalIndex = flatTagList.findIndex((item) => item.tag === tag) @@ -1294,6 +1346,11 @@ export const TagDropdown: React.FC = ({ e.stopPropagation() handleTagSelect(tag) }} + ref={(el) => { + if (el && globalIndex >= 0) { + itemRefs.current.set(globalIndex, el) + } + }} > @@ -1333,15 +1390,31 @@ export const TagDropdown: React.FC = ({ if (hasChildren) { const folderId = `${group.blockId}-${nestedTag.key}` + const parentGlobalIndex = nestedTag.parentTag + ? flatTagList.findIndex((item) => item.tag === nestedTag.parentTag) + : -1 + return ( } + active={parentGlobalIndex === selectedIndex && parentGlobalIndex >= 0} + onSelect={() => { + if (nestedTag.parentTag) { + handleTagSelect(nestedTag.parentTag, group) + } + }} onMouseEnter={() => { - // Clear selection when hovering folder to prevent highlighting items - setSelectedIndex(-1) + if (parentGlobalIndex >= 0) { + setSelectedIndex(parentGlobalIndex) + } + }} + ref={(el) => { + if (el && parentGlobalIndex >= 0) { + itemRefs.current.set(parentGlobalIndex, el) + } }} > {nestedTag.children!.map((child) => { @@ -1383,6 +1456,11 @@ export const TagDropdown: React.FC = ({ e.stopPropagation() handleTagSelect(child.fullTag, group) }} + ref={(el) => { + if (el && childGlobalIndex >= 0) { + itemRefs.current.set(childGlobalIndex, el) + } + }} > {child.display} @@ -1457,6 +1535,11 @@ export const TagDropdown: React.FC = ({ handleTagSelect(nestedTag.fullTag, group) } }} + ref={(el) => { + if (el && globalIndex >= 0) { + itemRefs.current.set(globalIndex, el) + } + }} > {nestedTag.display} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/types.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/types.ts new file mode 100644 index 00000000000..64f256babdf --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/types.ts @@ -0,0 +1,28 @@ +/** + * Block tag group for organizing tags by block + */ +export interface BlockTagGroup { + blockName: string + blockId: string + blockType: string + tags: string[] + distance: number +} + +/** + * Nested tag structure for hierarchical display + */ +export interface NestedTag { + key: string + display: string + fullTag?: string + parentTag?: string // Tag for the parent object when it has children + children?: Array<{ key: string; display: string; fullTag: string }> +} + +/** + * Block tag group with nested tag structure + */ +export interface NestedBlockTagGroup extends BlockTagGroup { + nestedTags: NestedTag[] +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx index 04480fe6fe8..9c9963990e0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx @@ -31,7 +31,7 @@ import { import { checkTagTrigger, TagDropdown, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown' +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { CodeEditor } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/components/code-editor/code-editor' import { WandPromptBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/wand-prompt-bar/wand-prompt-bar' import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/variables-input/variables-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/variables-input/variables-input.tsx index d9569720962..6c7f20054ec 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/variables-input/variables-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/variables-input/variables-input.tsx @@ -17,7 +17,7 @@ import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/ import { checkTagTrigger, TagDropdown, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown' +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-value' import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' import { useVariablesStore } from '@/stores/panel/variables/store' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-dropdowns.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-dropdowns.ts index db4fa0960c5..f9ebcb8d08b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-dropdowns.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-dropdowns.ts @@ -1,7 +1,7 @@ import { useCallback, useRef, useState } from 'react' import { useParams } from 'next/navigation' import { checkEnvVarTrigger } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/env-var-dropdown' -import { checkTagTrigger } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown' +import { checkTagTrigger } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import type { SubBlockConfig } from '@/blocks/types' import { useTagSelection } from '@/hooks/use-tag-selection' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-input.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-input.ts index dc45bb1ad1f..141296ecf63 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-input.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-input.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useParams } from 'next/navigation' import { createLogger } from '@/lib/logs/console/logger' import { checkEnvVarTrigger } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/env-var-dropdown' -import { checkTagTrigger } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown' +import { checkTagTrigger } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-value' import type { SubBlockConfig } from '@/blocks/types' import { useTagSelection } from '@/hooks/use-tag-selection' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/subflow-editor/subflow-editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/subflow-editor/subflow-editor.tsx index a8f9cd81f25..abe07e78b23 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/subflow-editor/subflow-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/subflow-editor/subflow-editor.tsx @@ -3,7 +3,7 @@ import { ChevronUp } from 'lucide-react' import SimpleCodeEditor from 'react-simple-code-editor' import { Code as CodeEditor, Combobox, getCodeEditorProps, Input, Label } from '@/components/emcn' -import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown' +import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import type { BlockState } from '@/stores/workflows/workflow/types' import type { ConnectedBlock } from '../../hooks/use-block-connections' import { useSubflowEditor } from '../../hooks/use-subflow-editor' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/hooks/use-subflow-editor.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/hooks/use-subflow-editor.ts index a5265bb7efa..c238c280107 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/hooks/use-subflow-editor.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/hooks/use-subflow-editor.ts @@ -5,7 +5,7 @@ import { SYSTEM_REFERENCE_PREFIXES, splitReferenceSegment, } from '@/lib/workflows/references' -import { checkTagTrigger } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown' +import { checkTagTrigger } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { normalizeBlockName } from '@/stores/workflows/utils' diff --git a/apps/sim/components/emcn/components/index.ts b/apps/sim/components/emcn/components/index.ts index 7e978a06595..b4164136cff 100644 --- a/apps/sim/components/emcn/components/index.ts +++ b/apps/sim/components/emcn/components/index.ts @@ -40,6 +40,7 @@ export { PopoverSection, type PopoverSectionProps, PopoverTrigger, + usePopoverContext, } from './popover/popover' export { Textarea } from './textarea/textarea' export { Tooltip } from './tooltip/tooltip' diff --git a/apps/sim/components/emcn/components/popover/popover.tsx b/apps/sim/components/emcn/components/popover/popover.tsx index 771c3afff43..89006f35dcf 100644 --- a/apps/sim/components/emcn/components/popover/popover.tsx +++ b/apps/sim/components/emcn/components/popover/popover.tsx @@ -84,11 +84,17 @@ const POPOVER_ITEM_HOVER_CLASSES = { type PopoverVariant = 'default' | 'primary' interface PopoverContextValue { - openFolder: (id: string, title: string, onLoad?: () => void | Promise) => void + openFolder: ( + id: string, + title: string, + onLoad?: () => void | Promise, + onSelect?: () => void + ) => void closeFolder: () => void currentFolder: string | null isInFolder: boolean folderTitle: string | null + onFolderSelect: (() => void) | null variant: PopoverVariant searchQuery: string setSearchQuery: (query: string) => void @@ -126,12 +132,14 @@ export interface PopoverProps extends PopoverPrimitive.PopoverProps { const Popover: React.FC = ({ children, variant = 'default', ...props }) => { const [currentFolder, setCurrentFolder] = React.useState(null) const [folderTitle, setFolderTitle] = React.useState(null) + const [onFolderSelect, setOnFolderSelect] = React.useState<(() => void) | null>(null) const [searchQuery, setSearchQuery] = React.useState('') const openFolder = React.useCallback( - (id: string, title: string, onLoad?: () => void | Promise) => { + (id: string, title: string, onLoad?: () => void | Promise, onSelect?: () => void) => { setCurrentFolder(id) setFolderTitle(title) + setOnFolderSelect(onSelect ?? null) if (onLoad) { void Promise.resolve(onLoad()) } @@ -142,6 +150,7 @@ const Popover: React.FC = ({ children, variant = 'default', ...pro const closeFolder = React.useCallback(() => { setCurrentFolder(null) setFolderTitle(null) + setOnFolderSelect(null) }, []) const contextValue: PopoverContextValue = React.useMemo( @@ -151,11 +160,12 @@ const Popover: React.FC = ({ children, variant = 'default', ...pro currentFolder, isInFolder: currentFolder !== null, folderTitle, + onFolderSelect, variant, searchQuery, setSearchQuery, }), - [openFolder, closeFolder, currentFolder, folderTitle, variant, searchQuery] + [openFolder, closeFolder, currentFolder, folderTitle, onFolderSelect, variant, searchQuery] ) return ( @@ -426,6 +436,10 @@ export interface PopoverFolderProps extends Omit void | Promise + /** + * Function to call when the folder title is selected (from within the folder view) + */ + onSelect?: () => void /** * Children to render when folder is open */ @@ -449,7 +463,7 @@ export interface PopoverFolderProps extends Omit( - ({ className, id, title, icon, onOpen, children, active, ...props }, ref) => { + ({ className, id, title, icon, onOpen, onSelect, children, active, ...props }, ref) => { const { openFolder, currentFolder, isInFolder, variant } = usePopoverContext() // Don't render if we're in a different folder @@ -462,6 +476,12 @@ const PopoverFolder = React.forwardRef( return <>{children} } + // Handle click anywhere on folder item + const handleClick = (e: React.MouseEvent) => { + e.stopPropagation() + openFolder(id, title, onOpen, onSelect) + } + // Otherwise, render as a clickable folder item return (
( role='menuitem' aria-haspopup='true' aria-expanded={false} - onClick={() => openFolder(id, title, onOpen)} + onClick={handleClick} {...props} > {icon} @@ -487,7 +507,20 @@ const PopoverFolder = React.forwardRef( PopoverFolder.displayName = 'PopoverFolder' -export interface PopoverBackButtonProps extends React.HTMLAttributes {} +export interface PopoverBackButtonProps extends React.HTMLAttributes { + /** + * Ref callback for the folder title element (when selectable) + */ + folderTitleRef?: (el: HTMLElement | null) => void + /** + * Whether the folder title is currently active/selected + */ + folderTitleActive?: boolean + /** + * Callback when mouse enters the folder title + */ + onFolderTitleMouseEnter?: () => void +} /** * Back button component that appears when inside a folder. @@ -504,8 +537,8 @@ export interface PopoverBackButtonProps extends React.HTMLAttributes( - ({ className, ...props }, ref) => { - const { isInFolder, closeFolder, folderTitle, variant } = usePopoverContext() + ({ className, folderTitleRef, folderTitleActive, onFolderTitleMouseEnter, ...props }, ref) => { + const { isInFolder, closeFolder, folderTitle, onFolderSelect, variant } = usePopoverContext() if (!isInFolder) { return null @@ -523,7 +556,26 @@ const PopoverBackButton = React.forwardRef Back
- {folderTitle && ( + {folderTitle && onFolderSelect && ( +
{ + e.stopPropagation() + onFolderSelect() + }} + onMouseEnter={onFolderTitleMouseEnter} + > + {folderTitle} +
+ )} + {folderTitle && !onFolderSelect && (
{folderTitle}
@@ -605,4 +657,5 @@ export { PopoverFolder, PopoverBackButton, PopoverSearch, + usePopoverContext, }