diff --git a/web/app/components/workflow/block-selector/__tests__/main.spec.tsx b/web/app/components/workflow/block-selector/__tests__/main.spec.tsx
index b7248dcb90a..decb090ab25 100644
--- a/web/app/components/workflow/block-selector/__tests__/main.spec.tsx
+++ b/web/app/components/workflow/block-selector/__tests__/main.spec.tsx
@@ -287,6 +287,39 @@ describe('NodeSelector', () => {
expect(screen.getByPlaceholderText('workflow.tabs.searchBlock')).toBeInTheDocument()
})
+ it('isolates popup keyboard events when opened from another keyboard-managed overlay', async () => {
+ const user = userEvent.setup()
+ const handleParentKeyDown = vi.fn()
+
+ renderNodeSelector(
+ ,
+ )
+
+ const searchInput = screen.getByPlaceholderText('workflow.tabs.searchBlock') as HTMLInputElement
+ document.body.addEventListener('keydown', handleParentKeyDown)
+
+ try {
+ await user.type(searchInput, 'LLM')
+ }
+ finally {
+ document.body.removeEventListener('keydown', handleParentKeyDown)
+ }
+
+ expect(searchInput.value).toBe('LLM')
+ expect(handleParentKeyDown).not.toHaveBeenCalled()
+ expect(screen.getByText('LLM')).toBeInTheDocument()
+ expect(screen.queryByText('End')).not.toBeInTheDocument()
+ })
+
it('disables the start tab with a setup tooltip when an unconfigured start node is on the canvas', async () => {
const user = userEvent.setup()
diff --git a/web/app/components/workflow/block-selector/main.tsx b/web/app/components/workflow/block-selector/main.tsx
index 6678f081fe9..51f49d46b20 100644
--- a/web/app/components/workflow/block-selector/main.tsx
+++ b/web/app/components/workflow/block-selector/main.tsx
@@ -64,6 +64,7 @@ export type NodeSelectorProps = {
forceEnableStartTab?: boolean // Force enabling Start tab regardless of existing trigger/user input nodes (e.g., when changing Start node type).
allowUserInputSelection?: boolean // Override user-input availability; default logic blocks it when triggers exist.
snippetInsertPayload?: Parameters[1]
+ isolateKeyboardEvents?: boolean
}
function NodeSelector({
open: openFromProps,
@@ -90,6 +91,7 @@ function NodeSelector({
forceEnableStartTab = false,
allowUserInputSelection,
snippetInsertPayload,
+ isolateKeyboardEvents = false,
}: NodeSelectorProps) {
const { t } = useTranslation()
const nodes = useNodes()
@@ -182,6 +184,10 @@ function NodeSelector({
if (open && newActiveTab === TabsEnum.Snippets)
setSnippetsLoading(true)
}, [open, setActiveTab])
+ const handlePopupKeyDown = useCallback((event: React.KeyboardEvent) => {
+ if (isolateKeyboardEvents)
+ event.stopPropagation()
+ }, [isolateKeyboardEvents])
useEffect(() => {
if (!snippetsLoading)
@@ -263,6 +269,7 @@ function NodeSelector({
sideOffset={sideOffset}
alignOffset={alignOffset}
popupClassName="border-none bg-transparent shadow-none"
+ popupProps={isolateKeyboardEvents ? { onKeyDown: handlePopupKeyDown } : undefined}
>
{
showStartTab: true,
placement: 'right-start',
popupClassName: 'min-w-[256px]!',
+ isolateKeyboardEvents: undefined,
})
expect(latestBlockSelectorProps?.defaultActiveTab).toBeUndefined()
expect(latestBlockSelectorProps?.offset).toEqual({
@@ -241,6 +243,14 @@ describe('AddBlock', () => {
expect(latestBlockSelectorProps?.showStartTab).toBe(true)
expect(latestBlockSelectorProps?.defaultActiveTab).toBeUndefined()
})
+
+ it('should pass keyboard isolation to the selector when requested by the caller', async () => {
+ renderWorkflowFlowComponent(, { nodes: [], edges: [] })
+
+ await waitFor(() => expect(latestBlockSelectorProps).not.toBeNull())
+
+ expect(latestBlockSelectorProps?.isolateKeyboardEvents).toBe(true)
+ })
})
// User interactions that bridge selector state and workflow state.
diff --git a/web/app/components/workflow/operator/add-block.tsx b/web/app/components/workflow/operator/add-block.tsx
index e2937f28642..364c55ee93e 100644
--- a/web/app/components/workflow/operator/add-block.tsx
+++ b/web/app/components/workflow/operator/add-block.tsx
@@ -46,12 +46,14 @@ type AddBlockProps = {
renderTriggerAsButtonRoot?: boolean
offset?: OffsetOptions
onClose?: () => void
+ isolateKeyboardEvents?: boolean
}
const AddBlock = ({
renderTrigger,
renderTriggerAsButtonRoot,
offset,
onClose,
+ isolateKeyboardEvents,
}: AddBlockProps) => {
const { t } = useTranslation()
const store = useStoreApi()
@@ -183,6 +185,7 @@ const AddBlock = ({
popupClassName="min-w-[256px]!"
availableBlocksTypes={availableNextBlocks}
showStartTab={showStartTab}
+ isolateKeyboardEvents={isolateKeyboardEvents}
/>
)
}
diff --git a/web/app/components/workflow/panel-contextmenu.tsx b/web/app/components/workflow/panel-contextmenu.tsx
index 2e620e26909..f4f37c17fbf 100644
--- a/web/app/components/workflow/panel-contextmenu.tsx
+++ b/web/app/components/workflow/panel-contextmenu.tsx
@@ -97,6 +97,7 @@ export function PanelContextmenu({
renderTrigger={renderAddBlockTrigger}
renderTriggerAsButtonRoot
onClose={onClose}
+ isolateKeyboardEvents
offset={{
mainAxis: -36,
crossAxis: -4,