mirror of
https://github.com/langgenius/dify.git
synced 2026-07-31 17:29:37 +08:00
fix: isolate node selector keyboard events (#37998)
This commit is contained in:
parent
113d6d7e00
commit
6ab1dd06ac
@ -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()
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -97,6 +97,7 @@ export function PanelContextmenu({
|
||||
renderTrigger={renderAddBlockTrigger}
|
||||
renderTriggerAsButtonRoot
|
||||
onClose={onClose}
|
||||
isolateKeyboardEvents
|
||||
offset={{
|
||||
mainAxis: -36,
|
||||
crossAxis: -4,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user