Skip to content

Commit 8a8df5d

Browse files
authored
Add dispatchEvent to fragment instances (facebook#32813)
`fragmentInstance.dispatchEvent(evt)` calls `element.dispatchEvent(evt)` on the fragment's host parent. This mimics bubbling if the `fragmentInstance` could receive an event itself. If the parent is disconnected, there is a dev warning and no event is dispatched.
1 parent 946da51 commit 8a8df5d

File tree

4 files changed

+673
-365
lines changed

4 files changed

+673
-365
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import TestCase from '../../TestCase';
2+
import Fixture from '../../Fixture';
3+
4+
const React = window.React;
5+
const {Fragment, useRef, useState} = React;
6+
7+
function WrapperComponent(props) {
8+
return props.children;
9+
}
10+
11+
const initialState = {
12+
child: false,
13+
parent: false,
14+
grandparent: false,
15+
};
16+
17+
export default function EventListenerCase() {
18+
const fragmentRef = useRef(null);
19+
const [clickedState, setClickedState] = useState({...initialState});
20+
const [fragmentEventFired, setFragmentEventFired] = useState(false);
21+
const [bubblesState, setBubblesState] = useState(true);
22+
23+
function setClick(id) {
24+
setClickedState(prev => ({...prev, [id]: true}));
25+
}
26+
27+
function fragmentClickHandler(e) {
28+
setFragmentEventFired(true);
29+
}
30+
31+
return (
32+
<TestCase title="Event Dispatch">
33+
<TestCase.Steps>
34+
<li>
35+
Each box has regular click handlers, you can click each one to observe
36+
the status changing through standard bubbling.
37+
</li>
38+
<li>Clear the clicked state</li>
39+
<li>
40+
Click the "Dispatch click event" button to dispatch a click event on
41+
the Fragment. The event will be dispatched on the Fragment's parent,
42+
so the child will not change state.
43+
</li>
44+
<li>
45+
Click the "Add event listener" button to add a click event listener on
46+
the Fragment. This registers a handler that will turn the child blue
47+
on click.
48+
</li>
49+
<li>
50+
Now click the "Dispatch click event" button again. You can see that it
51+
will fire the Fragment's event handler in addition to bubbling the
52+
click from the parent.
53+
</li>
54+
<li>
55+
If you turn off bubbling, only the Fragment's event handler will be
56+
called.
57+
</li>
58+
</TestCase.Steps>
59+
60+
<TestCase.ExpectedResult>
61+
<p>
62+
Dispatching an event on a Fragment will forward the dispatch to its
63+
parent for the standard case. You can observe when dispatching that
64+
the parent handler is called in additional to bubbling from there. A
65+
delay is added to make the bubbling more clear.{' '}
66+
</p>
67+
<p>
68+
When there have been event handlers added to the Fragment, the
69+
Fragment's event handler will be called in addition to bubbling from
70+
the parent. Without bubbling, only the Fragment's event handler will
71+
be called.
72+
</p>
73+
</TestCase.ExpectedResult>
74+
75+
<Fixture>
76+
<Fixture.Controls>
77+
<select
78+
value={bubblesState ? 'true' : 'false'}
79+
onChange={e => {
80+
setBubblesState(e.target.value === 'true');
81+
}}>
82+
<option value="true">Bubbles: true</option>
83+
<option value="false">Bubbles: false</option>
84+
</select>
85+
<button
86+
onClick={() => {
87+
fragmentRef.current.dispatchEvent(
88+
new MouseEvent('click', {bubbles: bubblesState})
89+
);
90+
}}>
91+
Dispatch click event
92+
</button>
93+
<button
94+
onClick={() => {
95+
setClickedState({...initialState});
96+
setFragmentEventFired(false);
97+
}}>
98+
Reset clicked state
99+
</button>
100+
<button
101+
onClick={() => {
102+
fragmentRef.current.addEventListener(
103+
'click',
104+
fragmentClickHandler
105+
);
106+
}}>
107+
Add event listener
108+
</button>
109+
<button
110+
onClick={() => {
111+
fragmentRef.current.removeEventListener(
112+
'click',
113+
fragmentClickHandler
114+
);
115+
}}>
116+
Remove event listener
117+
</button>
118+
</Fixture.Controls>
119+
<div
120+
id="grandparent"
121+
onClick={e => {
122+
setTimeout(() => {
123+
setClick('grandparent');
124+
}, 200);
125+
}}
126+
className="card">
127+
Fragment grandparent - clicked:{' '}
128+
{clickedState.grandparent ? 'true' : 'false'}
129+
<div
130+
id="parent"
131+
onClick={e => {
132+
setTimeout(() => {
133+
setClick('parent');
134+
}, 100);
135+
}}
136+
className="card">
137+
Fragment parent - clicked: {clickedState.parent ? 'true' : 'false'}
138+
<Fragment ref={fragmentRef}>
139+
<div
140+
style={{
141+
backgroundColor: fragmentEventFired ? 'lightblue' : 'inherit',
142+
}}
143+
id="child"
144+
className="card"
145+
onClick={e => {
146+
setClick('child');
147+
}}>
148+
Fragment child - clicked:{' '}
149+
{clickedState.child ? 'true' : 'false'}
150+
</div>
151+
</Fragment>
152+
</div>
153+
</div>
154+
</Fixture>
155+
</TestCase>
156+
);
157+
}

fixtures/dom/src/components/fixtures/fragment-refs/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import FixtureSet from '../../FixtureSet';
22
import EventListenerCase from './EventListenerCase';
3+
import EventDispatchCase from './EventDispatchCase';
34
import IntersectionObserverCase from './IntersectionObserverCase';
45
import ResizeObserverCase from './ResizeObserverCase';
56
import FocusCase from './FocusCase';
@@ -11,6 +12,7 @@ export default function FragmentRefsPage() {
1112
return (
1213
<FixtureSet title="Fragment Refs">
1314
<EventListenerCase />
15+
<EventDispatchCase />
1416
<IntersectionObserverCase />
1517
<ResizeObserverCase />
1618
<FocusCase />

packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

+38
Original file line numberDiff line numberDiff line change
@@ -2598,6 +2598,7 @@ export type FragmentInstanceType = {
25982598
listener: EventListener,
25992599
optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
26002600
): void,
2601+
dispatchEvent(event: Event): boolean,
26012602
focus(focusOptions?: FocusOptions): void,
26022603
focusLast(focusOptions?: FocusOptions): void,
26032604
blur(): void,
@@ -2695,6 +2696,43 @@ function removeEventListenerFromChild(
26952696
return false;
26962697
}
26972698
// $FlowFixMe[prop-missing]
2699+
FragmentInstance.prototype.dispatchEvent = function (
2700+
this: FragmentInstanceType,
2701+
event: Event,
2702+
): boolean {
2703+
const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber);
2704+
if (parentHostFiber === null) {
2705+
return true;
2706+
}
2707+
const parentHostInstance =
2708+
getInstanceFromHostFiber<Instance>(parentHostFiber);
2709+
const eventListeners = this._eventListeners;
2710+
if (
2711+
(eventListeners !== null && eventListeners.length > 0) ||
2712+
!event.bubbles
2713+
) {
2714+
const temp = document.createTextNode('');
2715+
if (eventListeners) {
2716+
for (let i = 0; i < eventListeners.length; i++) {
2717+
const {type, listener, optionsOrUseCapture} = eventListeners[i];
2718+
temp.addEventListener(type, listener, optionsOrUseCapture);
2719+
}
2720+
}
2721+
parentHostInstance.appendChild(temp);
2722+
const cancelable = temp.dispatchEvent(event);
2723+
if (eventListeners) {
2724+
for (let i = 0; i < eventListeners.length; i++) {
2725+
const {type, listener, optionsOrUseCapture} = eventListeners[i];
2726+
temp.removeEventListener(type, listener, optionsOrUseCapture);
2727+
}
2728+
}
2729+
parentHostInstance.removeChild(temp);
2730+
return cancelable;
2731+
} else {
2732+
return parentHostInstance.dispatchEvent(event);
2733+
}
2734+
};
2735+
// $FlowFixMe[prop-missing]
26982736
FragmentInstance.prototype.focus = function (
26992737
this: FragmentInstanceType,
27002738
focusOptions?: FocusOptions,

0 commit comments

Comments
 (0)