mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
Add dispatchEvent to fragment instances (#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.
This commit is contained in:
parent
946da518eb
commit
8a8df5dbdd
|
|
@ -0,0 +1,157 @@
|
||||||
|
import TestCase from '../../TestCase';
|
||||||
|
import Fixture from '../../Fixture';
|
||||||
|
|
||||||
|
const React = window.React;
|
||||||
|
const {Fragment, useRef, useState} = React;
|
||||||
|
|
||||||
|
function WrapperComponent(props) {
|
||||||
|
return props.children;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
child: false,
|
||||||
|
parent: false,
|
||||||
|
grandparent: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function EventListenerCase() {
|
||||||
|
const fragmentRef = useRef(null);
|
||||||
|
const [clickedState, setClickedState] = useState({...initialState});
|
||||||
|
const [fragmentEventFired, setFragmentEventFired] = useState(false);
|
||||||
|
const [bubblesState, setBubblesState] = useState(true);
|
||||||
|
|
||||||
|
function setClick(id) {
|
||||||
|
setClickedState(prev => ({...prev, [id]: true}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function fragmentClickHandler(e) {
|
||||||
|
setFragmentEventFired(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TestCase title="Event Dispatch">
|
||||||
|
<TestCase.Steps>
|
||||||
|
<li>
|
||||||
|
Each box has regular click handlers, you can click each one to observe
|
||||||
|
the status changing through standard bubbling.
|
||||||
|
</li>
|
||||||
|
<li>Clear the clicked state</li>
|
||||||
|
<li>
|
||||||
|
Click the "Dispatch click event" button to dispatch a click event on
|
||||||
|
the Fragment. The event will be dispatched on the Fragment's parent,
|
||||||
|
so the child will not change state.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Click the "Add event listener" button to add a click event listener on
|
||||||
|
the Fragment. This registers a handler that will turn the child blue
|
||||||
|
on click.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Now click the "Dispatch click event" button again. You can see that it
|
||||||
|
will fire the Fragment's event handler in addition to bubbling the
|
||||||
|
click from the parent.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
If you turn off bubbling, only the Fragment's event handler will be
|
||||||
|
called.
|
||||||
|
</li>
|
||||||
|
</TestCase.Steps>
|
||||||
|
|
||||||
|
<TestCase.ExpectedResult>
|
||||||
|
<p>
|
||||||
|
Dispatching an event on a Fragment will forward the dispatch to its
|
||||||
|
parent for the standard case. You can observe when dispatching that
|
||||||
|
the parent handler is called in additional to bubbling from there. A
|
||||||
|
delay is added to make the bubbling more clear.{' '}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
When there have been event handlers added to the Fragment, the
|
||||||
|
Fragment's event handler will be called in addition to bubbling from
|
||||||
|
the parent. Without bubbling, only the Fragment's event handler will
|
||||||
|
be called.
|
||||||
|
</p>
|
||||||
|
</TestCase.ExpectedResult>
|
||||||
|
|
||||||
|
<Fixture>
|
||||||
|
<Fixture.Controls>
|
||||||
|
<select
|
||||||
|
value={bubblesState ? 'true' : 'false'}
|
||||||
|
onChange={e => {
|
||||||
|
setBubblesState(e.target.value === 'true');
|
||||||
|
}}>
|
||||||
|
<option value="true">Bubbles: true</option>
|
||||||
|
<option value="false">Bubbles: false</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
fragmentRef.current.dispatchEvent(
|
||||||
|
new MouseEvent('click', {bubbles: bubblesState})
|
||||||
|
);
|
||||||
|
}}>
|
||||||
|
Dispatch click event
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setClickedState({...initialState});
|
||||||
|
setFragmentEventFired(false);
|
||||||
|
}}>
|
||||||
|
Reset clicked state
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
fragmentRef.current.addEventListener(
|
||||||
|
'click',
|
||||||
|
fragmentClickHandler
|
||||||
|
);
|
||||||
|
}}>
|
||||||
|
Add event listener
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
fragmentRef.current.removeEventListener(
|
||||||
|
'click',
|
||||||
|
fragmentClickHandler
|
||||||
|
);
|
||||||
|
}}>
|
||||||
|
Remove event listener
|
||||||
|
</button>
|
||||||
|
</Fixture.Controls>
|
||||||
|
<div
|
||||||
|
id="grandparent"
|
||||||
|
onClick={e => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setClick('grandparent');
|
||||||
|
}, 200);
|
||||||
|
}}
|
||||||
|
className="card">
|
||||||
|
Fragment grandparent - clicked:{' '}
|
||||||
|
{clickedState.grandparent ? 'true' : 'false'}
|
||||||
|
<div
|
||||||
|
id="parent"
|
||||||
|
onClick={e => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setClick('parent');
|
||||||
|
}, 100);
|
||||||
|
}}
|
||||||
|
className="card">
|
||||||
|
Fragment parent - clicked: {clickedState.parent ? 'true' : 'false'}
|
||||||
|
<Fragment ref={fragmentRef}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: fragmentEventFired ? 'lightblue' : 'inherit',
|
||||||
|
}}
|
||||||
|
id="child"
|
||||||
|
className="card"
|
||||||
|
onClick={e => {
|
||||||
|
setClick('child');
|
||||||
|
}}>
|
||||||
|
Fragment child - clicked:{' '}
|
||||||
|
{clickedState.child ? 'true' : 'false'}
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Fixture>
|
||||||
|
</TestCase>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import FixtureSet from '../../FixtureSet';
|
import FixtureSet from '../../FixtureSet';
|
||||||
import EventListenerCase from './EventListenerCase';
|
import EventListenerCase from './EventListenerCase';
|
||||||
|
import EventDispatchCase from './EventDispatchCase';
|
||||||
import IntersectionObserverCase from './IntersectionObserverCase';
|
import IntersectionObserverCase from './IntersectionObserverCase';
|
||||||
import ResizeObserverCase from './ResizeObserverCase';
|
import ResizeObserverCase from './ResizeObserverCase';
|
||||||
import FocusCase from './FocusCase';
|
import FocusCase from './FocusCase';
|
||||||
|
|
@ -11,6 +12,7 @@ export default function FragmentRefsPage() {
|
||||||
return (
|
return (
|
||||||
<FixtureSet title="Fragment Refs">
|
<FixtureSet title="Fragment Refs">
|
||||||
<EventListenerCase />
|
<EventListenerCase />
|
||||||
|
<EventDispatchCase />
|
||||||
<IntersectionObserverCase />
|
<IntersectionObserverCase />
|
||||||
<ResizeObserverCase />
|
<ResizeObserverCase />
|
||||||
<FocusCase />
|
<FocusCase />
|
||||||
|
|
|
||||||
|
|
@ -2598,6 +2598,7 @@ export type FragmentInstanceType = {
|
||||||
listener: EventListener,
|
listener: EventListener,
|
||||||
optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
|
optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
|
||||||
): void,
|
): void,
|
||||||
|
dispatchEvent(event: Event): boolean,
|
||||||
focus(focusOptions?: FocusOptions): void,
|
focus(focusOptions?: FocusOptions): void,
|
||||||
focusLast(focusOptions?: FocusOptions): void,
|
focusLast(focusOptions?: FocusOptions): void,
|
||||||
blur(): void,
|
blur(): void,
|
||||||
|
|
@ -2695,6 +2696,43 @@ function removeEventListenerFromChild(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// $FlowFixMe[prop-missing]
|
// $FlowFixMe[prop-missing]
|
||||||
|
FragmentInstance.prototype.dispatchEvent = function (
|
||||||
|
this: FragmentInstanceType,
|
||||||
|
event: Event,
|
||||||
|
): boolean {
|
||||||
|
const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber);
|
||||||
|
if (parentHostFiber === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const parentHostInstance =
|
||||||
|
getInstanceFromHostFiber<Instance>(parentHostFiber);
|
||||||
|
const eventListeners = this._eventListeners;
|
||||||
|
if (
|
||||||
|
(eventListeners !== null && eventListeners.length > 0) ||
|
||||||
|
!event.bubbles
|
||||||
|
) {
|
||||||
|
const temp = document.createTextNode('');
|
||||||
|
if (eventListeners) {
|
||||||
|
for (let i = 0; i < eventListeners.length; i++) {
|
||||||
|
const {type, listener, optionsOrUseCapture} = eventListeners[i];
|
||||||
|
temp.addEventListener(type, listener, optionsOrUseCapture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parentHostInstance.appendChild(temp);
|
||||||
|
const cancelable = temp.dispatchEvent(event);
|
||||||
|
if (eventListeners) {
|
||||||
|
for (let i = 0; i < eventListeners.length; i++) {
|
||||||
|
const {type, listener, optionsOrUseCapture} = eventListeners[i];
|
||||||
|
temp.removeEventListener(type, listener, optionsOrUseCapture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parentHostInstance.removeChild(temp);
|
||||||
|
return cancelable;
|
||||||
|
} else {
|
||||||
|
return parentHostInstance.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// $FlowFixMe[prop-missing]
|
||||||
FragmentInstance.prototype.focus = function (
|
FragmentInstance.prototype.focus = function (
|
||||||
this: FragmentInstanceType,
|
this: FragmentInstanceType,
|
||||||
focusOptions?: FocusOptions,
|
focusOptions?: FocusOptions,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user