diff --git a/packages/inula/scripts/__tests__/ComponentTest/PortalComponent.test.js b/packages/inula/scripts/__tests__/ComponentTest/PortalComponent.test.js index 4c6924b9..98e55340 100644 --- a/packages/inula/scripts/__tests__/ComponentTest/PortalComponent.test.js +++ b/packages/inula/scripts/__tests__/ComponentTest/PortalComponent.test.js @@ -286,4 +286,98 @@ describe('PortalComponent Test', () => { dispatchChangeEvent(inputRef.current, 'test'); expect(fn).toHaveBeenCalledTimes(1); }); + + + it('portal场景下,portal下元素点击事件冒泡到父元素', () => { + class Dialog extends Inula.Component { + node; + + constructor(props) { + super(props); + this.node = window.document.createElement('div'); + window.document.body.appendChild(this.node); + } + + render() { + return Inula.createPortal(this.props.children, this.node); + } + } + + const fn = jest.fn(); + const subRef = Inula.createRef(); + + function App() { + return ( +
+ +
+
+
+ ); + } + + Inula.render(, container); + Inula.act(() => { + subRef.current.dispatchEvent(new Event('click', { bubbles: true })); + }); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('portal嵌套场景下事件委托', () => { + class Dialog extends Inula.Component { + node; + + constructor(props) { + super(props); + this.node = window.document.createElement('div'); + window.document.body.appendChild(this.node); + } + + render() { + return Inula.createPortal(this.props.children, this.node); + } + } + + const fn = jest.fn(); + const inputRef = Inula.createRef(); + let value = ''; + const onChange = (evt) => { + value = evt.target.value; + } + + let showSubPortal = () => {}; + + function App() { + return ( +
+ + + + +
+ ); + } + + function Sub() { + const [show, setShow] = Inula.useState(false); + showSubPortal = setShow; + return ( +
+ { + show && + + + + } +
+ ) + } + + Inula.render(, container); + Inula.act(() => { + showSubPortal(true); + }); + dispatchChangeEvent(inputRef.current, 'test'); + expect(value).toEqual('test'); + }); }); diff --git a/packages/inula/src/event/EventBinding.ts b/packages/inula/src/event/EventBinding.ts index 01b3ea54..f3e9f58e 100644 --- a/packages/inula/src/event/EventBinding.ts +++ b/packages/inula/src/event/EventBinding.ts @@ -16,7 +16,7 @@ /** * 事件绑定实现,分为绑定委托事件和非委托事件 */ -import { allDelegatedInulaEvents, simulatedDelegatedEvents } from './EventHub'; +import { allDelegatedInulaEvents, portalDefaultDelegatedEvents, simulatedDelegatedEvents } from './EventHub'; import { isDocument } from '../dom/utils/Common'; import { getNearestVNode, getNonDelegatedListenerMap } from '../dom/DOMInternalKeys'; import { asyncUpdates, runDiscreteUpdates } from '../renderer/TreeBuilder'; @@ -89,6 +89,13 @@ export function listenSimulatedDelegatedEvents(root: VNode) { } } +// portal绑定默认事件 +export function listenPortalEvents(root: VNode) { + for (let i = 0; i < portalDefaultDelegatedEvents.length; i++) { + lazyDelegateOnRoot(root, portalDefaultDelegatedEvents[i]); + } +} + // 通过inula事件名获取到native事件名 function getNativeEvtName(inulaEventName, capture) { let nativeName; diff --git a/packages/inula/src/event/EventHub.ts b/packages/inula/src/event/EventHub.ts index 2f8e8a1f..0dc01c50 100644 --- a/packages/inula/src/event/EventHub.ts +++ b/packages/inula/src/event/EventHub.ts @@ -16,6 +16,13 @@ // 需要委托的inula事件和原生事件对应关系 export const allDelegatedInulaEvents = new Map(); +/** + * Portal根节点默认绑定事件,解决常见事件无法冒泡到parent vnode的问题 + * 例如:parent vNode节点绑定了mousedown事件,子节点为portal节点,子节点下元素未绑定mousedown事件 + * 此时,点击portal下子元素,mousedown事件无法冒泡到parentNode + */ +export const portalDefaultDelegatedEvents = ['onMouseDown', 'onMouseUp', 'onKeyDown', 'onKeyUp', 'onFocus', 'onBlur', 'onClick']; + // 模拟委托事件,不冒泡事件需要利用其他事件来触发冒泡过程 export const simulatedDelegatedEvents = ['onMouseEnter', 'onMouseLeave']; // 所有委托的原生事件集合 diff --git a/packages/inula/src/renderer/TreeBuilder.ts b/packages/inula/src/renderer/TreeBuilder.ts index 50326e25..c29126fb 100644 --- a/packages/inula/src/renderer/TreeBuilder.ts +++ b/packages/inula/src/renderer/TreeBuilder.ts @@ -244,17 +244,22 @@ export function calcStartUpdateVNode(treeRoot: VNode) { // 在局部更新时,从上到下恢复父节点的context和PortalStack function recoverTreeContext(vNode: VNode) { const contextProviders: VNode[] = []; + const portalRoots: VNode[] = []; let parent = vNode.parent; while (parent !== null) { if (parent.tag === ContextProvider) { contextProviders.unshift(parent); + } else if (parent.tag === DomPortal) { + portalRoots.unshift(parent); } if (parent.tag === DomPortal) { pushCurrentRoot(parent); } parent = parent.parent; } - + portalRoots.forEach(node => { + pushCurrentRoot(node); + }); contextProviders.forEach(node => { setContext(node, node.props.value); }); diff --git a/packages/inula/src/renderer/render/DomPortal.ts b/packages/inula/src/renderer/render/DomPortal.ts index 27bad601..24628605 100644 --- a/packages/inula/src/renderer/render/DomPortal.ts +++ b/packages/inula/src/renderer/render/DomPortal.ts @@ -17,11 +17,12 @@ import type { VNode } from '../Types'; import { resetNamespaceCtx, setNamespaceCtx } from '../ContextSaver'; import { createChildrenByDiff } from '../diff/nodeDiffComparator'; import { popCurrentRoot, pushCurrentRoot } from '../RootStack'; -import { listenSimulatedDelegatedEvents } from '../../event/EventBinding'; +import { listenPortalEvents, listenSimulatedDelegatedEvents } from '../../event/EventBinding'; export function bubbleRender(processing: VNode) { resetNamespaceCtx(processing); listenSimulatedDelegatedEvents(processing); + listenPortalEvents(processing); popCurrentRoot(); }