Skip to content

Commit fc9cdab

Browse files
committed
Trigger a fragment instances own listeners with dispatchEvent
1 parent 9fda63f commit fc9cdab

File tree

3 files changed

+155
-35
lines changed

3 files changed

+155
-35
lines changed

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

Lines changed: 80 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,6 @@ function WrapperComponent(props) {
88
return props.children;
99
}
1010

11-
function handler(e) {
12-
const text = e.currentTarget.innerText;
13-
alert('You clicked: ' + text);
14-
}
15-
1611
const initialState = {
1712
child: false,
1813
parent: false,
@@ -22,67 +17,133 @@ const initialState = {
2217
export default function EventListenerCase() {
2318
const fragmentRef = useRef(null);
2419
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+
}
2530

2631
return (
2732
<TestCase title="Event Dispatch">
2833
<TestCase.Steps>
2934
<li>
30-
Each div has regular click handlers, you can click each one to observe
31-
the status changing
35+
Each box has regular click handlers, you can click each one to observe
36+
the status changing through standard bubbling.
3237
</li>
3338
<li>Clear the clicked state</li>
3439
<li>
3540
Click the "Dispatch click event" button to dispatch a click event on
36-
the Fragment
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.
3757
</li>
3858
</TestCase.Steps>
3959

4060
<TestCase.ExpectedResult>
41-
Dispatching an event on a Fragment will forward the dispatch to its
42-
parent. You can observe when dispatching that the parent handler is
43-
called in additional to bubbling from there. A delay is added to make
44-
the bubbling more clear.
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>
4573
</TestCase.ExpectedResult>
4674

4775
<Fixture>
4876
<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>
4985
<button
5086
onClick={() => {
5187
fragmentRef.current.dispatchEvent(
52-
new MouseEvent('click', {bubbles: true})
88+
new MouseEvent('click', {bubbles: bubblesState})
5389
);
5490
}}>
5591
Dispatch click event
5692
</button>
5793
<button
5894
onClick={() => {
5995
setClickedState({...initialState});
96+
setFragmentEventFired(false);
6097
}}>
6198
Reset clicked state
6299
</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>
63118
</Fixture.Controls>
64119
<div
65-
onClick={() => {
120+
id="grandparent"
121+
onClick={e => {
66122
setTimeout(() => {
67-
setClickedState(prev => ({...prev, grandparent: true}));
123+
setClick('grandparent');
68124
}, 200);
69125
}}
70126
className="card">
71127
Fragment grandparent - clicked:{' '}
72128
{clickedState.grandparent ? 'true' : 'false'}
73129
<div
74-
onClick={() => {
130+
id="parent"
131+
onClick={e => {
75132
setTimeout(() => {
76-
setClickedState(prev => ({...prev, parent: true}));
133+
setClick('parent');
77134
}, 100);
78135
}}
79136
className="card">
80137
Fragment parent - clicked: {clickedState.parent ? 'true' : 'false'}
81138
<Fragment ref={fragmentRef}>
82139
<div
140+
style={{
141+
backgroundColor: fragmentEventFired ? 'lightblue' : 'inherit',
142+
}}
143+
id="child"
83144
className="card"
84-
onClick={() => {
85-
setClickedState(prev => ({...prev, child: true}));
145+
onClick={e => {
146+
setClick('child');
86147
}}>
87148
Fragment child - clicked:{' '}
88149
{clickedState.child ? 'true' : 'false'}

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

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2699,17 +2699,36 @@ FragmentInstance.prototype.dispatchEvent = function (
26992699
this: FragmentInstanceType,
27002700
event: Event,
27012701
): boolean {
2702-
const parentHostInstance = getFragmentParentHostInstance(this._fragmentFiber);
2703-
if (parentHostInstance === null) {
2704-
if (__DEV__) {
2705-
console.error(
2706-
'You are attempting to dispatch an event on a disconnected ' +
2707-
'FragmentInstance. No event was dispatched.',
2708-
);
2709-
}
2702+
const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber);
2703+
if (parentHostFiber === null) {
27102704
return true;
27112705
}
2712-
return parentHostInstance.dispatchEvent(event);
2706+
const parentHostInstance = getInstanceFromHostFiber(parentHostFiber);
2707+
const eventListeners = this._eventListeners;
2708+
if (
2709+
(eventListeners !== null && eventListeners.length > 0) ||
2710+
!event.bubbles
2711+
) {
2712+
const temp = document.createTextNode('');
2713+
if (eventListeners) {
2714+
for (let i = 0; i < eventListeners.length; i++) {
2715+
const {type, listener, optionsOrUseCapture} = eventListeners[i];
2716+
temp.addEventListener(type, listener, optionsOrUseCapture);
2717+
}
2718+
}
2719+
parentHostInstance.appendChild(temp);
2720+
const cancelable = temp.dispatchEvent(event);
2721+
if (eventListeners) {
2722+
for (let i = 0; i < eventListeners.length; i++) {
2723+
const {type, listener, optionsOrUseCapture} = eventListeners[i];
2724+
temp.removeEventListener(type, listener, optionsOrUseCapture);
2725+
}
2726+
}
2727+
parentHostInstance.removeChild(temp);
2728+
return cancelable;
2729+
} else {
2730+
return parentHostInstance.dispatchEvent(event);
2731+
}
27132732
};
27142733
// $FlowFixMe[prop-missing]
27152734
FragmentInstance.prototype.focus = function (

packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -789,7 +789,7 @@ describe('FragmentRefs', () => {
789789

790790
describe('dispatchEvent()', () => {
791791
// @gate enableFragmentRefs
792-
it('fires events on the host parent', async () => {
792+
it('fires events on the host parent if bubbles=true', async () => {
793793
const fragmentRef = React.createRef();
794794
const root = ReactDOMClient.createRoot(container);
795795
let logs = [];
@@ -835,16 +835,56 @@ describe('FragmentRefs', () => {
835835
isCancelable = !fragmentInstanceHandle.dispatchEvent(
836836
new MouseEvent('click', {bubbles: true}),
837837
);
838-
assertConsoleErrorDev(
839-
[
840-
'You are attempting to dispatch an event on a disconnected ' +
841-
'FragmentInstance. No event was dispatched.',
842-
],
843-
{withoutStack: true},
838+
expect(logs).toEqual([]);
839+
expect(isCancelable).toBe(false);
840+
841+
logs = [];
842+
isCancelable = !fragmentInstanceHandle.dispatchEvent(
843+
new MouseEvent('click', {bubbles: false}),
844844
);
845845
expect(logs).toEqual([]);
846846
expect(isCancelable).toBe(false);
847847
});
848+
849+
// @gate enableFragmentRefs
850+
it('fires events on self, and only self if bubbles=false', async () => {
851+
const fragmentRef = React.createRef();
852+
const root = ReactDOMClient.createRoot(container);
853+
let logs = [];
854+
855+
function handleClick(e) {
856+
logs.push([e.type, e.target.id, e.currentTarget.id]);
857+
}
858+
859+
function Test() {
860+
return (
861+
<div id="parent" onClick={handleClick}>
862+
<Fragment ref={fragmentRef} />
863+
</div>
864+
);
865+
}
866+
867+
await act(() => {
868+
root.render(<Test />);
869+
});
870+
871+
fragmentRef.current.addEventListener('click', handleClick);
872+
873+
fragmentRef.current.dispatchEvent(
874+
new MouseEvent('click', {bubbles: true}),
875+
);
876+
expect(logs).toEqual([
877+
['click', undefined, undefined],
878+
['click', 'parent', 'parent'],
879+
]);
880+
881+
logs = [];
882+
883+
fragmentRef.current.dispatchEvent(
884+
new MouseEvent('click', {bubbles: false}),
885+
);
886+
expect(logs).toEqual([['click', undefined, undefined]]);
887+
});
848888
});
849889
});
850890

0 commit comments

Comments
 (0)