Match-id-b191193225d35dcd637d7ef1902db54e05f62f6f
This commit is contained in:
parent
c2b21d887f
commit
914a0ce6e5
|
@ -18,7 +18,8 @@ import { createPortal } from '../renderer/components/CreatePortal';
|
|||
import type { Container } from './DOMOperator';
|
||||
import { isElement } from './utils/Common';
|
||||
import { findDOMByClassInst } from '../renderer/vnode/VNodeUtils';
|
||||
import { Callback } from '../renderer/UpdateHandler';
|
||||
import {Callback} from '../renderer/UpdateHandler';
|
||||
import {listenSimulatedDelegatedEvents} from '../event/EventBinding';
|
||||
|
||||
function createRoot(children: any, container: Container, callback?: Callback) {
|
||||
// 清空容器
|
||||
|
@ -31,6 +32,7 @@ function createRoot(children: any, container: Container, callback?: Callback) {
|
|||
// 调度器创建根节点,并给容器dom赋vNode结构体
|
||||
const treeRoot = createTreeRootVNode(container);
|
||||
container._treeRoot = treeRoot;
|
||||
listenSimulatedDelegatedEvents(treeRoot);
|
||||
|
||||
// 执行回调
|
||||
if (typeof callback === 'function') {
|
||||
|
|
|
@ -16,16 +16,14 @@
|
|||
/**
|
||||
* 事件绑定实现,分为绑定委托事件和非委托事件
|
||||
*/
|
||||
import { allDelegatedHorizonEvents, allDelegatedNativeEvents } from './EventHub';
|
||||
import { isDocument } from '../dom/utils/Common';
|
||||
import {allDelegatedHorizonEvents, simulatedDelegatedEvents} from './EventHub';
|
||||
import {isDocument} from '../dom/utils/Common';
|
||||
import { getNearestVNode, getNonDelegatedListenerMap } from '../dom/DOMInternalKeys';
|
||||
import { asyncUpdates, runDiscreteUpdates } from '../renderer/TreeBuilder';
|
||||
import { handleEventMain } from './HorizonEventMain';
|
||||
import { decorateNativeEvent } from './EventWrapper';
|
||||
import { VNode } from '../renderer/vnode/VNode';
|
||||
|
||||
const listeningMarker = '_horizonListening' + Math.random().toString(36).slice(4);
|
||||
|
||||
// 触发委托事件
|
||||
function triggerDelegatedEvent(
|
||||
nativeEvtName: string,
|
||||
|
@ -64,6 +62,13 @@ function isCaptureEvent(horizonEventName) {
|
|||
return horizonEventName.slice(-7) === 'Capture';
|
||||
}
|
||||
|
||||
// 利用冒泡事件模拟不冒泡事件,需要直接在根节点绑定
|
||||
export function listenSimulatedDelegatedEvents(root: VNode) {
|
||||
for (let i = 0; i < simulatedDelegatedEvents.length; i++) {
|
||||
lazyDelegateOnRoot(root, simulatedDelegatedEvents[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// 事件懒委托,当用户定义事件后,再进行委托到根节点
|
||||
export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) {
|
||||
currentRoot.delegatedEvents.add(eventName);
|
||||
|
|
|
@ -15,6 +15,9 @@
|
|||
|
||||
// 需要委托的horizon事件和原生事件对应关系
|
||||
export const allDelegatedHorizonEvents = new Map();
|
||||
|
||||
// 模拟委托事件,事件本事不冒泡,需要其他事件来触发冒泡过程
|
||||
export const simulatedDelegatedEvents = ['onMouseEnter', 'onMouseLeave'];
|
||||
// 所有委托的原生事件集合
|
||||
export const allDelegatedNativeEvents = new Set();
|
||||
|
||||
|
@ -49,6 +52,8 @@ export const horizonEventToNativeMap = new Map([
|
|||
['onCompositionUpdate', ['compositionupdate']],
|
||||
['onChange', ['change', 'click', 'focusout', 'input']],
|
||||
['onSelect', ['select']],
|
||||
['onMouseEnter', ['mouseout', 'mouseover']],
|
||||
['onMouseLeave', ['mouseout', 'mouseover']],
|
||||
|
||||
['onAnimationEnd', ['animationend']],
|
||||
['onAnimationIteration', ['animationiteration']],
|
||||
|
|
|
@ -37,6 +37,9 @@ export class WrappedEvent {
|
|||
key: string;
|
||||
currentTarget: EventTarget | null = null;
|
||||
|
||||
target: HTMLElement;
|
||||
relatedTarget: HTMLElement;
|
||||
|
||||
stopPropagation: () => void;
|
||||
preventDefault: () => void;
|
||||
|
||||
|
|
|
@ -30,7 +30,8 @@ import {
|
|||
import { getDomTag } from '../dom/utils/Common';
|
||||
import { updateInputHandlerIfChanged } from '../dom/valueHandler/ValueChangeHandler';
|
||||
import { getDom } from '../dom/DOMInternalKeys';
|
||||
import { recordChangeEventTargets, shouldControlValue, tryControlValue } from './FormValueController';
|
||||
import {recordChangeEventTargets, shouldControlValue, tryControlValue} from './FormValueController';
|
||||
import {getMouseEnterListeners} from './MouseEvent';
|
||||
|
||||
// web规范,鼠标右键key值
|
||||
const RIGHT_MOUSE_BUTTON = 2;
|
||||
|
@ -141,18 +142,26 @@ function triggerHorizonEvents(
|
|||
const target = nativeEvent.target || nativeEvent.srcElement!;
|
||||
|
||||
// 触发普通委托事件
|
||||
let listenerList: ListenerUnitList = getCommonListeners(nativeEvtName, vNode, nativeEvent, target, isCapture);
|
||||
const listenerList: ListenerUnitList = getCommonListeners(nativeEvtName, vNode, nativeEvent, target, isCapture);
|
||||
|
||||
let mouseEnterListeners: ListenerUnitList = [];
|
||||
if (horizonEventToNativeMap.get('onMouseEnter')!.includes(nativeEvtName)) {
|
||||
mouseEnterListeners = getMouseEnterListeners(
|
||||
nativeEvtName,
|
||||
vNode,
|
||||
nativeEvent,
|
||||
target,
|
||||
);
|
||||
}
|
||||
|
||||
let changeEvents: ListenerUnitList = [];
|
||||
// 触发特殊handler委托事件
|
||||
if (!isCapture && horizonEventToNativeMap.get('onChange')!.includes(nativeEvtName)) {
|
||||
const changeListeners = getChangeListeners(nativeEvtName, nativeEvent, vNode, target);
|
||||
if (changeListeners.length) {
|
||||
listenerList = listenerList.concat(changeListeners);
|
||||
}
|
||||
changeEvents = getChangeListeners(nativeEvtName, nativeEvent, vNode, target);
|
||||
}
|
||||
|
||||
// 处理触发的事件队列
|
||||
processListeners(listenerList);
|
||||
processListeners([...listenerList, ...mouseEnterListeners, ...changeEvents]);
|
||||
}
|
||||
|
||||
// 其他事件正在执行中标记
|
||||
|
|
|
@ -86,3 +86,93 @@ export function getListenersFromTree(
|
|||
|
||||
return listeners;
|
||||
}
|
||||
|
||||
// 获取enter和leave事件队列
|
||||
export function collectMouseListeners(
|
||||
leaveEvent: null | WrappedEvent,
|
||||
enterEvent: null | WrappedEvent,
|
||||
from: VNode | null,
|
||||
to: VNode | null,
|
||||
): ListenerUnitList {
|
||||
// 确定公共父节点,作为在树上遍历的终点
|
||||
const commonParent = from && to ? getCommonAncestor(from, to) : null;
|
||||
let leaveEventList: ListenerUnitList = [];
|
||||
if (from && leaveEvent) {
|
||||
// 遍历树,获取绑定的leave事件
|
||||
leaveEventList = getMouseListenersFromTree(
|
||||
leaveEvent,
|
||||
from,
|
||||
commonParent,
|
||||
);
|
||||
}
|
||||
let enterEventList: ListenerUnitList = [];
|
||||
if (to && enterEvent) {
|
||||
// 先触发父节点enter事件,所以需要逆序
|
||||
enterEventList = getMouseListenersFromTree(
|
||||
enterEvent,
|
||||
to,
|
||||
commonParent,
|
||||
).reverse();
|
||||
}
|
||||
return [...leaveEventList, ...enterEventList];
|
||||
}
|
||||
|
||||
function getMouseListenersFromTree(
|
||||
event: WrappedEvent,
|
||||
target: VNode,
|
||||
commonParent: VNode | null,
|
||||
): ListenerUnitList {
|
||||
const registrationName = event.customEventName;
|
||||
const listeners: ListenerUnitList = [];
|
||||
|
||||
let vNode = target;
|
||||
while (vNode !== null) {
|
||||
// commonParent作为终点
|
||||
if (vNode === commonParent) {
|
||||
break;
|
||||
}
|
||||
const {realNode, tag} = vNode;
|
||||
if (tag === DomComponent && realNode !== null) {
|
||||
const currentTarget = realNode;
|
||||
const listener = getListenerFromVNode(vNode, registrationName);
|
||||
if (listener) {
|
||||
listeners.push({
|
||||
vNode,
|
||||
listener,
|
||||
currentTarget,
|
||||
event,
|
||||
});
|
||||
}
|
||||
}
|
||||
vNode = vNode.parent;
|
||||
}
|
||||
return listeners;
|
||||
}
|
||||
|
||||
// 寻找两个节点的共同最近祖先,如果没有则返回null
|
||||
function getCommonAncestor(instA: VNode, instB: VNode): VNode | null {
|
||||
const parentsSet = new Set<VNode>();
|
||||
for (let tempA: VNode | null = instA; tempA; tempA = getParent(tempA)) {
|
||||
parentsSet.add(tempA);
|
||||
}
|
||||
for (let tempB: VNode | null = instB; tempB; tempB = getParent(tempB)) {
|
||||
if (parentsSet.has(tempB)) {
|
||||
return tempB;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取父节点
|
||||
function getParent(inst: VNode | null): VNode | null {
|
||||
if (inst === null) {
|
||||
return null;
|
||||
}
|
||||
do {
|
||||
inst = inst.parent;
|
||||
} while (inst && inst.tag !== DomComponent);
|
||||
if (inst) {
|
||||
return inst;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openGauss is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import {getNearestVNode} from '../dom/DOMInternalKeys';
|
||||
import {WrappedEvent} from './EventWrapper';
|
||||
import {VNode} from '../renderer/vnode/VNode';
|
||||
import {AnyNativeEvent, ListenerUnitList} from './Types';
|
||||
import {DomComponent, DomText} from '../renderer/vnode/VNodeTags';
|
||||
import {collectMouseListeners} from './ListenerGetter';
|
||||
import {getNearestMountedVNode} from './utils';
|
||||
|
||||
/**
|
||||
* 背景: mouseEnter和mouseLeave事件不冒泡,所以无法直接委托给根节点进行代理
|
||||
* 实现方案:利用mouseout、mouseover事件的,找到事件触发的起点和终点,判断出鼠标移动轨迹,在轨迹中的节点触发mouseEnter和mouseLeave事件
|
||||
* 步骤:
|
||||
* 1. 根节点绑定mouseout和mouseover事件
|
||||
* 2. 事件触发后找到事件的起点和终点
|
||||
* 3. 封装装enter和leave事件
|
||||
* 4. 根据起止点找到公共父节点,作为事件冒泡的终点
|
||||
* 5. 遍历treeNode,找到每个节点绑定的mouseEnter和mouseLeave监听方法
|
||||
* 例如: mouseOut事件由D->C, A节点作为公共父节点,将触发 D、B的mouseLeave事件和C节点的mouseEnter事件
|
||||
* A
|
||||
* / \
|
||||
* B C
|
||||
* / \
|
||||
* D E
|
||||
*
|
||||
*/
|
||||
|
||||
function getWrapperEvents(nativeEventTarget, fromInst, toInst, nativeEvent, targetInst): (WrappedEvent | null)[] {
|
||||
const vWindow = nativeEventTarget.window === nativeEventTarget ? nativeEventTarget : nativeEventTarget.ownerDocument.defaultView;
|
||||
|
||||
// 起点或者终点为空的话默认值为所在window
|
||||
const fromNode = fromInst?.realNode || vWindow;
|
||||
const toNode = toInst?.realNode || vWindow;
|
||||
let leave: WrappedEvent | null = null;
|
||||
let enter: WrappedEvent | null = null;
|
||||
const nativeTargetInst = getNearestVNode(nativeEventTarget);
|
||||
|
||||
// 在Mounted的dom节点上render一个子组件,系统中存在两个根节点,子节点的mouseout事件触发两次,取离target近的根节点生效
|
||||
if (nativeTargetInst === targetInst) {
|
||||
leave = new WrappedEvent('onMouseLeave', 'mouseleave', nativeEvent);
|
||||
leave.target = fromNode;
|
||||
leave.relatedTarget = toNode;
|
||||
|
||||
enter = new WrappedEvent('onMouseEnter', 'mouseenter', nativeEvent);
|
||||
enter.target = toNode;
|
||||
enter.relatedTarget = fromNode;
|
||||
}
|
||||
return [leave, enter];
|
||||
}
|
||||
|
||||
function getEndpointVNode(
|
||||
domEventName: string,
|
||||
targetInst: null | VNode,
|
||||
nativeEvent: AnyNativeEvent,
|
||||
): (VNode | null)[] {
|
||||
let fromVNode;
|
||||
let toVNode;
|
||||
if (domEventName === 'mouseover') {
|
||||
fromVNode = null;
|
||||
toVNode = targetInst;
|
||||
} else {
|
||||
const related = nativeEvent.relatedTarget || nativeEvent.toElement;
|
||||
fromVNode = targetInst;
|
||||
toVNode = related ? getNearestVNode(related) : null;
|
||||
if (toVNode !== null) {
|
||||
const nearestMounted = getNearestMountedVNode(toVNode);
|
||||
if (toVNode !== nearestMounted || (toVNode.tag !== DomComponent && toVNode.tag !== DomText)) {
|
||||
toVNode = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [fromVNode, toVNode];
|
||||
}
|
||||
|
||||
export function getMouseEnterListeners(
|
||||
domEventName: string,
|
||||
targetInst: null | VNode,
|
||||
nativeEvent: AnyNativeEvent,
|
||||
nativeEventTarget: null | EventTarget,
|
||||
): ListenerUnitList {
|
||||
|
||||
// 获取起点和终点的VNode
|
||||
const [fromVNode, toVNode] = getEndpointVNode(domEventName, targetInst, nativeEvent);
|
||||
if (fromVNode === toVNode) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 获取包装后的leave和enter事件
|
||||
const [leave, enter] = getWrapperEvents(nativeEventTarget, fromVNode, toVNode, nativeEvent, targetInst);
|
||||
|
||||
// 收集事件的监听方法
|
||||
return collectMouseListeners(leave, enter, fromVNode, toVNode);
|
||||
}
|
||||
|
||||
|
|
@ -13,6 +13,10 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import {VNode} from '../renderer/vnode/VNode';
|
||||
import {Addition, FlagUtils} from '../renderer/vnode/VNodeFlags';
|
||||
import {TreeRoot} from '../renderer/vnode/VNodeTags';
|
||||
|
||||
export function isInputElement(dom?: HTMLElement): boolean {
|
||||
return dom instanceof HTMLInputElement || dom instanceof HTMLTextAreaElement;
|
||||
}
|
||||
|
@ -20,6 +24,26 @@ export function isInputElement(dom?: HTMLElement): boolean {
|
|||
export function setPropertyWritable(obj, propName) {
|
||||
const desc = Object.getOwnPropertyDescriptor(obj, propName);
|
||||
if (!desc || !desc.writable) {
|
||||
Object.defineProperty(obj, propName, { writable: true });
|
||||
Object.defineProperty(obj, propName, {writable: true});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取离 vNode 最近的已挂载 vNode,包含它自己
|
||||
export function getNearestMountedVNode(vNode: VNode): null | VNode {
|
||||
let node = vNode;
|
||||
let target = vNode;
|
||||
// 如果没有alternate,说明是可能是未插入的新树,需要处理插入的副作用。
|
||||
while (node.parent) {
|
||||
// 存在更新,节点未挂载,查找父节点,但是父节点也可能未挂载,需要继续往上查找无更新节点
|
||||
if (FlagUtils.hasFlag(node, Addition)) {
|
||||
target = node.parent;
|
||||
}
|
||||
node = node.parent;
|
||||
}
|
||||
// 如果根节点是 Dom 类型节点,表示已经挂载
|
||||
if (node.tag === TreeRoot) {
|
||||
return target;
|
||||
}
|
||||
// 如果没有找到根节点,意味着Tree已经卸载或者未挂载
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -16,10 +16,12 @@
|
|||
import type { VNode } from '../Types';
|
||||
import { resetNamespaceCtx, setNamespaceCtx } from '../ContextSaver';
|
||||
import { createChildrenByDiff } from '../diff/nodeDiffComparator';
|
||||
import { popCurrentRoot, pushCurrentRoot } from '../RootStack';
|
||||
import {popCurrentRoot, pushCurrentRoot} from '../RootStack';
|
||||
import {listenSimulatedDelegatedEvents} from '../../event/EventBinding';
|
||||
|
||||
export function bubbleRender(processing: VNode) {
|
||||
resetNamespaceCtx(processing);
|
||||
listenSimulatedDelegatedEvents(processing);
|
||||
popCurrentRoot();
|
||||
}
|
||||
|
||||
|
|
|
@ -40,14 +40,20 @@ export class FlagUtils {
|
|||
static removeFlag(node: VNode, flag: number) {
|
||||
node.flags &= ~flag;
|
||||
}
|
||||
|
||||
static removeLifecycleEffectFlags(node) {
|
||||
node.flags &= ~LifecycleEffectArr;
|
||||
}
|
||||
|
||||
static hasAnyFlag(node: VNode) {
|
||||
// 有标志位
|
||||
return node.flags !== InitFlag;
|
||||
}
|
||||
|
||||
static hasFlag(node: VNode, flag) {
|
||||
return (node.flags & flag) !== 0;
|
||||
}
|
||||
|
||||
static setNoFlags(node: VNode) {
|
||||
node.flags = InitFlag;
|
||||
}
|
||||
|
@ -55,6 +61,7 @@ export class FlagUtils {
|
|||
static markAddition(node: VNode) {
|
||||
node.flags |= Addition;
|
||||
}
|
||||
|
||||
static setAddition(node: VNode) {
|
||||
node.flags = Addition;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,301 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openGauss is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import * as Renderer from '../../../libs/horizon/src/renderer/Renderer';
|
||||
import { doc } from 'prettier';
|
||||
|
||||
describe('EnterLeaveEventPlugin', () => {
|
||||
let container;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
// The container has to be attached for events to fire.
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(container);
|
||||
container = null;
|
||||
});
|
||||
|
||||
it('should set onMouseLeave relatedTarget properly in iframe', () => {
|
||||
const iframe = document.createElement('iframe');
|
||||
container.appendChild(iframe);
|
||||
const iframeDocument = iframe.contentDocument;
|
||||
iframeDocument.write(
|
||||
'<!DOCTYPE html><html><head></head><body><div></div></body></html>',
|
||||
);
|
||||
iframeDocument.close();
|
||||
|
||||
const leaveEvents = [];
|
||||
const node = Horizon.render(
|
||||
<div
|
||||
onMouseLeave={e => {
|
||||
e.persist();
|
||||
leaveEvents.push(e);
|
||||
}}
|
||||
/>,
|
||||
iframeDocument.body.getElementsByTagName('div')[0],
|
||||
);
|
||||
|
||||
node.dispatchEvent(
|
||||
new MouseEvent('mouseout', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
relatedTarget: iframe.contentWindow,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(leaveEvents.length).toBe(1);
|
||||
expect(leaveEvents[0].target).toBe(node);
|
||||
expect(leaveEvents[0].relatedTarget).toBe(iframe.contentWindow);
|
||||
});
|
||||
|
||||
it('should set onMouseEnter relatedTarget properly in iframe', () => {
|
||||
const iframe = document.createElement('iframe');
|
||||
container.appendChild(iframe);
|
||||
const iframeDocument = iframe.contentDocument;
|
||||
iframeDocument.write(
|
||||
'<!DOCTYPE html><html><head></head><body><div></div></body></html>',
|
||||
);
|
||||
iframeDocument.close();
|
||||
|
||||
const enterEvents = [];
|
||||
const node = Horizon.render(
|
||||
<div
|
||||
onMouseEnter={e => {
|
||||
e.persist();
|
||||
enterEvents.push(e);
|
||||
}}
|
||||
/>,
|
||||
iframeDocument.body.getElementsByTagName('div')[0],
|
||||
);
|
||||
|
||||
node.dispatchEvent(
|
||||
new MouseEvent('mouseover', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
relatedTarget: null,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(enterEvents.length).toBe(1);
|
||||
expect(enterEvents[0].target).toBe(node);
|
||||
expect(enterEvents[0].relatedTarget).toBe(iframe.contentWindow);
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/facebook/Horizon/issues/10906.
|
||||
it('should find the common parent after updates', () => {
|
||||
let parentEnterCalls = 0;
|
||||
let childEnterCalls = 0;
|
||||
let parent = null;
|
||||
|
||||
class Parent extends Horizon.Component {
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => parentEnterCalls++}
|
||||
ref={node => (parent = node)}>
|
||||
{this.props.showChild && (
|
||||
<div onMouseEnter={() => childEnterCalls++}/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Horizon.render(<Parent/>, container);
|
||||
// The issue only reproduced on insertion during the first update.
|
||||
Horizon.render(<Parent showChild={true}/>, container);
|
||||
|
||||
// Enter from parent into the child.
|
||||
parent.dispatchEvent(
|
||||
new MouseEvent('mouseout', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
relatedTarget: parent.firstChild,
|
||||
}),
|
||||
);
|
||||
|
||||
// Entering a child should fire on the child, not on the parent.
|
||||
expect(childEnterCalls).toBe(1);
|
||||
expect(parentEnterCalls).toBe(0);
|
||||
});
|
||||
|
||||
it('should call mouseEnter once from sibling rendered inside a rendered component', done => {
|
||||
const mockFn1 = jest.fn();
|
||||
const mockFn2 = jest.fn();
|
||||
const mockFn3 = jest.fn();
|
||||
|
||||
class Parent extends Horizon.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.parentEl = Horizon.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
Horizon.render(<MouseEnterDetect/>, this.parentEl.current);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div ref={this.parentEl} id="parent" onMouseLeave={mockFn3}/>;
|
||||
}
|
||||
}
|
||||
|
||||
class MouseEnterDetect extends Horizon.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.firstEl = Horizon.createRef();
|
||||
this.siblingEl = Horizon.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.siblingEl.current.dispatchEvent(
|
||||
new MouseEvent('mouseout', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
relatedTarget: this.firstEl.current,
|
||||
}),
|
||||
);
|
||||
expect(mockFn1.mock.calls.length).toBe(1);
|
||||
expect(mockFn2.mock.calls.length).toBe(1);
|
||||
expect(mockFn3.mock.calls.length).toBe(0);
|
||||
done();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Horizon.Fragment>
|
||||
<div ref={this.firstEl} id="first" onMouseEnter={mockFn1}/>
|
||||
<div ref={this.siblingEl} id="sibling" onMouseLeave={mockFn2}/>
|
||||
</Horizon.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Horizon.render(<Parent/>, container);
|
||||
});
|
||||
|
||||
it('should call mouseEnter when pressing a non tracked Horizon node', done => {
|
||||
const mockFn = jest.fn();
|
||||
|
||||
class Parent extends Horizon.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.parentEl = Horizon.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
Horizon.render(<MouseEnterDetect/>, this.parentEl.current);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div ref={this.parentEl}/>;
|
||||
}
|
||||
}
|
||||
|
||||
class MouseEnterDetect extends Horizon.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.divRef = Horizon.createRef();
|
||||
this.siblingEl = Horizon.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const attachedNode = document.createElement('div');
|
||||
this.divRef.current.appendChild(attachedNode);
|
||||
attachedNode.dispatchEvent(
|
||||
new MouseEvent('mouseout', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
relatedTarget: this.siblingEl.current,
|
||||
}),
|
||||
);
|
||||
expect(mockFn.mock.calls.length).toBe(1);
|
||||
done();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div ref={this.divRef}>
|
||||
<div ref={this.siblingEl} onMouseEnter={mockFn}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Horizon.render(<Parent/>, container);
|
||||
});
|
||||
|
||||
it('should work with portals outside of the root that has onMouseLeave', () => {
|
||||
const divRef = Horizon.createRef();
|
||||
const onMouseLeave = jest.fn();
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div onMouseLeave={onMouseLeave} id="parent">
|
||||
{Horizon.createPortal(<div ref={divRef} id="sub"/>, document.body)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<Component/>, container);
|
||||
|
||||
// Leave from the portal div
|
||||
divRef.current.dispatchEvent(
|
||||
new MouseEvent('mouseout', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
relatedTarget: document.body,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(onMouseLeave).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should work with portals that have onMouseEnter outside of the root ', () => {
|
||||
const divRef = Horizon.createRef();
|
||||
const otherDivRef = Horizon.createRef();
|
||||
const onMouseEnter = jest.fn();
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div ref={divRef}>
|
||||
{Horizon.createPortal(
|
||||
<div ref={otherDivRef} onMouseEnter={onMouseEnter}/>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<Component/>, container);
|
||||
|
||||
// Leave from the portal div
|
||||
divRef.current.dispatchEvent(
|
||||
new MouseEvent('mouseout', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
relatedTarget: otherDivRef.current,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(onMouseEnter).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue