fix: isolate node selector keyboard events (#37998)

This commit is contained in:
yyh 2026-06-26 13:38:10 +08:00 committed by GitHub
parent 113d6d7e00
commit 6ab1dd06ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 54 additions and 0 deletions

View File

@ -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(
<NodeSelector
open
isolateKeyboardEvents
onSelect={vi.fn()}
blocks={[
createBlock(BlockEnum.LLM, 'LLM'),
createBlock(BlockEnum.End, 'End'),
]}
availableBlocksTypes={[BlockEnum.LLM, BlockEnum.End]}
/>,
)
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()

View File

@ -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<OnNodeAdd>[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}
>
<div className={cn('w-[400px] min-w-0 overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg', popupClassName)}>
<Tabs

View File

@ -20,6 +20,7 @@ type BlockSelectorMockProps = {
popupClassName: string
availableBlocksTypes: BlockEnum[]
showStartTab: boolean
isolateKeyboardEvents?: boolean
defaultActiveTab?: unknown
}
@ -204,6 +205,7 @@ describe('AddBlock', () => {
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(<AddBlock isolateKeyboardEvents />, { nodes: [], edges: [] })
await waitFor(() => expect(latestBlockSelectorProps).not.toBeNull())
expect(latestBlockSelectorProps?.isolateKeyboardEvents).toBe(true)
})
})
// User interactions that bridge selector state and workflow state.

View File

@ -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}
/>
)
}

View File

@ -97,6 +97,7 @@ export function PanelContextmenu({
renderTrigger={renderAddBlockTrigger}
renderTriggerAsButtonRoot
onClose={onClose}
isolateKeyboardEvents
offset={{
mainAxis: -36,
crossAxis: -4,