diff --git a/packages/core/__tests__/bugs/2406-spec.test.ts b/packages/core/__tests__/bugs/2406-spec.test.ts new file mode 100644 index 000000000..391bfd057 --- /dev/null +++ b/packages/core/__tests__/bugs/2406-spec.test.ts @@ -0,0 +1,89 @@ +/** + * @jest-environment jsdom + */ +import { createElement as h } from 'preact/compat' + +import { BaseNode } from '../../src' + +class TestNode extends BaseNode { + getShape() { + return h('g', null) + } +} + +const createNode = () => { + const model = { + id: 'node_1', + isDragging: false, + isSelected: false, + autoToFront: false, + text: { + editable: false, + }, + getData: jest.fn(() => ({ id: 'node_1' })), + setSelected: jest.fn(), + } + const graphModel = { + gridSize: 1, + eventCenter: { + emit: jest.fn(), + }, + editConfigModel: { + isSilentMode: false, + multipleSelectKey: 'meta', + nodeTextEdit: false, + textMode: 'TEXT', + }, + getPointByClient: jest.fn(() => ({ + canvasOverlayPosition: { x: 0, y: 0 }, + domOverlayPosition: { x: 0, y: 0 }, + })), + selectNodeById: jest.fn(), + setElementStateById: jest.fn(), + toFront: jest.fn(), + } + const node = new TestNode({ model, graphModel } as any) + ;(node as any).props = { model, graphModel } + node.startTime = Date.now() + node.mouseUpDrag = true + return { node, model, graphModel } +} + +describe('issue 2406', () => { + const originalRequestAnimationFrame = window.requestAnimationFrame + + beforeEach(() => { + window.requestAnimationFrame = ((callback: FrameRequestCallback) => { + callback(0) + return 0 + }) as typeof window.requestAnimationFrame + }) + + afterEach(() => { + window.requestAnimationFrame = originalRequestAnimationFrame + jest.restoreAllMocks() + }) + + test('does not steal focus from nested form controls on node click', () => { + const { node } = createNode() + const nodeElement = document.createElement('g') as any + nodeElement.focus = jest.fn() + const input = document.createElement('input') + nodeElement.appendChild(input) + + node.handleClick({ + button: 0, + clientX: 0, + clientY: 0, + currentTarget: nodeElement, + target: input, + detail: 1, + metaKey: false, + altKey: false, + shiftKey: false, + ctrlKey: false, + } as any) + + expect(nodeElement.focus).not.toHaveBeenCalled() + }) +}) diff --git a/packages/core/src/view/node/BaseNode.tsx b/packages/core/src/view/node/BaseNode.tsx index c4847f353..cd0438974 100644 --- a/packages/core/src/view/node/BaseNode.tsx +++ b/packages/core/src/view/node/BaseNode.tsx @@ -19,6 +19,9 @@ import { import RotateControlPoint from '../Rotate' import ResizeControlGroup from '../Control' +const NESTED_FOCUS_CONTROL_SELECTOR = + 'input, textarea, select, button, [contenteditable="true"], [contenteditable=""]' + type IProps = { model: BaseNodeModel graphModel: GraphModel @@ -413,9 +416,17 @@ export abstract class BaseNode

extends Component< !isNil(window) && isFunction(window.requestAnimationFrame) ? window.requestAnimationFrame.bind(window) : (fn: () => void) => setTimeout(fn, 0) - rAF(() => { - el.focus() - }) + const target = e.target as Element | null + const nestedFocusControl = + target && + target !== el && + isFunction(target.closest) && + target.closest(NESTED_FOCUS_CONTROL_SELECTOR) + if (!nestedFocusControl || !el.contains(nestedFocusControl)) { + rAF(() => { + el.focus() + }) + } } }