Skip to content

Commit d1f667a

Browse files
authored
Event API: follow up fixes for FocusScope + context changes (#15496)
1 parent c530639 commit d1f667a

14 files changed

+340
-144
lines changed

packages/react-dom/src/client/ReactDOMHostConfig.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -896,8 +896,8 @@ export function mountEventComponent(
896896
eventComponentInstance: ReactEventComponentInstance,
897897
): void {
898898
if (enableEventAPI) {
899-
mountEventResponder(eventComponentInstance);
900899
updateEventComponent(eventComponentInstance);
900+
mountEventResponder(eventComponentInstance);
901901
}
902902
}
903903

packages/react-dom/src/events/DOMEventResponderSystem.js

Lines changed: 162 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,13 @@ const eventListeners:
8484
($Shape<PartialEventObject>) => void,
8585
> = new PossiblyWeakMap();
8686

87-
let alreadyDispatching = false;
87+
const responderOwners: Map<
88+
ReactEventResponder,
89+
ReactEventComponentInstance,
90+
> = new Map();
91+
let globalOwner = null;
8892

8993
let currentTimers = new Map();
90-
let currentOwner = null;
9194
let currentInstance: null | ReactEventComponentInstance = null;
9295
let currentEventQueue: null | EventQueue = null;
9396

@@ -131,8 +134,9 @@ const eventResponderContext: ReactResponderContext = {
131134
eventListeners.set(eventObject, listener);
132135
eventQueue.events.push(eventObject);
133136
},
134-
isPositionWithinTouchHitTarget(doc: Document, x: number, y: number): boolean {
137+
isPositionWithinTouchHitTarget(x: number, y: number): boolean {
135138
validateResponderContext();
139+
const doc = getActiveDocument();
136140
// This isn't available in some environments (JSDOM)
137141
if (typeof doc.elementFromPoint !== 'function') {
138142
return false;
@@ -188,6 +192,27 @@ const eventResponderContext: ReactResponderContext = {
188192
}
189193
return false;
190194
},
195+
isTargetWithinEventResponderScope(target: Element | Document): boolean {
196+
validateResponderContext();
197+
const responder = ((currentInstance: any): ReactEventComponentInstance)
198+
.responder;
199+
if (target != null) {
200+
let fiber = getClosestInstanceFromNode(target);
201+
while (fiber !== null) {
202+
if (fiber.stateNode === currentInstance) {
203+
return true;
204+
}
205+
if (
206+
fiber.tag === EventComponent &&
207+
fiber.stateNode.responder === responder
208+
) {
209+
return false;
210+
}
211+
fiber = fiber.return;
212+
}
213+
}
214+
return false;
215+
},
191216
isTargetWithinElement(
192217
childTarget: Element | Document,
193218
parentTarget: Element | Document,
@@ -204,12 +229,10 @@ const eventResponderContext: ReactResponderContext = {
204229
}
205230
return false;
206231
},
207-
addRootEventTypes(
208-
doc: Document,
209-
rootEventTypes: Array<ReactEventResponderEventType>,
210-
): void {
232+
addRootEventTypes(rootEventTypes: Array<ReactEventResponderEventType>): void {
211233
validateResponderContext();
212-
listenToResponderEventTypesImpl(rootEventTypes, doc);
234+
const activeDocument = getActiveDocument();
235+
listenToResponderEventTypesImpl(rootEventTypes, activeDocument);
213236
for (let i = 0; i < rootEventTypes.length; i++) {
214237
const rootEventType = rootEventTypes[i];
215238
const topLevelEventType =
@@ -265,25 +288,38 @@ const eventResponderContext: ReactResponderContext = {
265288
},
266289
hasOwnership(): boolean {
267290
validateResponderContext();
268-
return currentOwner === currentInstance;
291+
const responder = ((currentInstance: any): ReactEventComponentInstance)
292+
.responder;
293+
return (
294+
globalOwner === currentInstance ||
295+
responderOwners.get(responder) === currentInstance
296+
);
269297
},
270-
requestOwnership(): boolean {
298+
requestGlobalOwnership(): boolean {
271299
validateResponderContext();
272-
if (currentOwner !== null) {
300+
if (globalOwner !== null) {
273301
return false;
274302
}
275-
currentOwner = currentInstance;
276-
triggerOwnershipListeners();
303+
globalOwner = currentInstance;
304+
triggerOwnershipListeners(null);
277305
return true;
278306
},
279-
releaseOwnership(): boolean {
307+
requestResponderOwnership(): boolean {
280308
validateResponderContext();
281-
if (currentOwner !== currentInstance) {
309+
const eventComponentInstance = ((currentInstance: any): ReactEventComponentInstance);
310+
const responder = eventComponentInstance.responder;
311+
if (responderOwners.has(responder)) {
282312
return false;
283313
}
284-
currentOwner = null;
285-
triggerOwnershipListeners();
286-
return false;
314+
responderOwners.set(responder, eventComponentInstance);
315+
triggerOwnershipListeners(responder);
316+
return true;
317+
},
318+
releaseOwnership(): boolean {
319+
validateResponderContext();
320+
return releaseOwnershipForEventComponentInstance(
321+
((currentInstance: any): ReactEventComponentInstance),
322+
);
287323
},
288324
setTimeout(func: () => void, delay): Symbol {
289325
validateResponderContext();
@@ -330,9 +366,6 @@ const eventResponderContext: ReactResponderContext = {
330366
let node = ((eventComponentInstance.currentFiber: any): Fiber).child;
331367

332368
while (node !== null) {
333-
if (node.stateNode === currentInstance) {
334-
break;
335-
}
336369
if (isFiberHostComponentFocusable(node)) {
337370
focusableElements.push(node.stateNode);
338371
} else {
@@ -353,13 +386,44 @@ const eventResponderContext: ReactResponderContext = {
353386
if (parent === null) {
354387
break;
355388
}
389+
if (parent.stateNode === currentInstance) {
390+
break;
391+
}
356392
node = parent.sibling;
357393
}
358394

359395
return focusableElements;
360396
},
397+
getActiveDocument,
361398
};
362399

400+
function getActiveDocument(): Document {
401+
const eventComponentInstance = ((currentInstance: any): ReactEventComponentInstance);
402+
const rootElement = ((eventComponentInstance.rootInstance: any): Element);
403+
return rootElement.ownerDocument;
404+
}
405+
406+
function releaseOwnershipForEventComponentInstance(
407+
eventComponentInstance: ReactEventComponentInstance,
408+
): boolean {
409+
const responder = eventComponentInstance.responder;
410+
let triggerOwnershipListenersWith;
411+
if (responderOwners.get(responder) === eventComponentInstance) {
412+
responderOwners.delete(responder);
413+
triggerOwnershipListenersWith = responder;
414+
}
415+
if (globalOwner === eventComponentInstance) {
416+
globalOwner = null;
417+
triggerOwnershipListenersWith = null;
418+
}
419+
if (triggerOwnershipListenersWith !== undefined) {
420+
triggerOwnershipListeners(triggerOwnershipListenersWith);
421+
return true;
422+
} else {
423+
return false;
424+
}
425+
}
426+
363427
function isFiberHostComponentFocusable(fiber: Fiber): boolean {
364428
if (fiber.tag !== HostComponent) {
365429
return false;
@@ -368,18 +432,22 @@ function isFiberHostComponentFocusable(fiber: Fiber): boolean {
368432
if (memoizedProps.tabIndex === -1 || memoizedProps.disabled) {
369433
return false;
370434
}
371-
if (memoizedProps.tabIndex === 0) {
435+
if (memoizedProps.tabIndex === 0 || memoizedProps.contentEditable === true) {
372436
return true;
373437
}
374438
if (type === 'a' || type === 'area') {
375-
return !!memoizedProps.href;
439+
return !!memoizedProps.href && memoizedProps.rel !== 'ignore';
440+
}
441+
if (type === 'input') {
442+
return memoizedProps.type !== 'hidden' && memoizedProps.type !== 'file';
376443
}
377444
return (
378445
type === 'button' ||
379446
type === 'textarea' ||
380-
type === 'input' ||
381447
type === 'object' ||
382-
type === 'select'
448+
type === 'select' ||
449+
type === 'iframe' ||
450+
type === 'embed'
383451
);
384452
}
385453

@@ -487,15 +555,13 @@ function getTargetEventResponderInstances(
487555
// Traverse up the fiber tree till we find event component fibers.
488556
if (node.tag === EventComponent) {
489557
const eventComponentInstance = node.stateNode;
490-
if (currentOwner === null || currentOwner === eventComponentInstance) {
491-
const responder = eventComponentInstance.responder;
492-
const targetEventTypes = responder.targetEventTypes;
493-
// Validate the target event type exists on the responder
494-
if (targetEventTypes !== undefined) {
495-
const targetEventTypesSet = getTargetEventTypesSet(targetEventTypes);
496-
if (targetEventTypesSet.has(topLevelType)) {
497-
eventResponderInstances.push(eventComponentInstance);
498-
}
558+
const responder = eventComponentInstance.responder;
559+
const targetEventTypes = responder.targetEventTypes;
560+
// Validate the target event type exists on the responder
561+
if (targetEventTypes !== undefined) {
562+
const targetEventTypesSet = getTargetEventTypesSet(targetEventTypes);
563+
if (targetEventTypesSet.has(topLevelType)) {
564+
eventResponderInstances.push(eventComponentInstance);
499565
}
500566
}
501567
}
@@ -516,18 +582,35 @@ function getRootEventResponderInstances(
516582

517583
for (let i = 0; i < rootEventComponentInstances.length; i++) {
518584
const rootEventComponentInstance = rootEventComponentInstances[i];
519-
520-
if (
521-
currentOwner === null ||
522-
currentOwner === rootEventComponentInstance
523-
) {
524-
eventResponderInstances.push(rootEventComponentInstance);
525-
}
585+
eventResponderInstances.push(rootEventComponentInstance);
526586
}
527587
}
528588
return eventResponderInstances;
529589
}
530590

591+
function shouldSkipEventComponent(
592+
eventResponderInstance: ReactEventComponentInstance,
593+
propagatedEventResponders: null | Set<ReactEventResponder>,
594+
): boolean {
595+
const responder = eventResponderInstance.responder;
596+
if (propagatedEventResponders !== null && responder.stopLocalPropagation) {
597+
if (propagatedEventResponders.has(responder)) {
598+
return true;
599+
}
600+
propagatedEventResponders.add(responder);
601+
}
602+
if (globalOwner && globalOwner !== eventResponderInstance) {
603+
return true;
604+
}
605+
if (
606+
responderOwners.has(responder) &&
607+
responderOwners.get(responder) !== eventResponderInstance
608+
) {
609+
return true;
610+
}
611+
return false;
612+
}
613+
531614
function traverseAndHandleEventResponderInstances(
532615
topLevelType: DOMTopLevelEventType,
533616
targetFiber: null | Fiber,
@@ -564,14 +647,16 @@ function traverseAndHandleEventResponderInstances(
564647
for (i = length; i-- > 0; ) {
565648
const targetEventResponderInstance = targetEventResponderInstances[i];
566649
const {responder, props, state} = targetEventResponderInstance;
567-
if (responder.stopLocalPropagation) {
568-
if (propagatedEventResponders.has(responder)) {
569-
continue;
570-
}
571-
propagatedEventResponders.add(responder);
572-
}
573650
const eventListener = responder.onEventCapture;
574651
if (eventListener !== undefined) {
652+
if (
653+
shouldSkipEventComponent(
654+
targetEventResponderInstance,
655+
propagatedEventResponders,
656+
)
657+
) {
658+
continue;
659+
}
575660
currentInstance = targetEventResponderInstance;
576661
eventListener(responderEvent, eventResponderContext, props, state);
577662
}
@@ -582,14 +667,16 @@ function traverseAndHandleEventResponderInstances(
582667
for (i = 0; i < length; i++) {
583668
const targetEventResponderInstance = targetEventResponderInstances[i];
584669
const {responder, props, state} = targetEventResponderInstance;
585-
if (responder.stopLocalPropagation) {
586-
if (propagatedEventResponders.has(responder)) {
587-
continue;
588-
}
589-
propagatedEventResponders.add(responder);
590-
}
591670
const eventListener = responder.onEvent;
592671
if (eventListener !== undefined) {
672+
if (
673+
shouldSkipEventComponent(
674+
targetEventResponderInstance,
675+
propagatedEventResponders,
676+
)
677+
) {
678+
continue;
679+
}
593680
currentInstance = targetEventResponderInstance;
594681
eventListener(responderEvent, eventResponderContext, props, state);
595682
}
@@ -606,20 +693,28 @@ function traverseAndHandleEventResponderInstances(
606693
const {responder, props, state} = rootEventResponderInstance;
607694
const eventListener = responder.onRootEvent;
608695
if (eventListener !== undefined) {
696+
if (shouldSkipEventComponent(rootEventResponderInstance, null)) {
697+
continue;
698+
}
609699
currentInstance = rootEventResponderInstance;
610700
eventListener(responderEvent, eventResponderContext, props, state);
611701
}
612702
}
613703
}
614704
}
615705

616-
function triggerOwnershipListeners(): void {
706+
function triggerOwnershipListeners(
707+
limitByResponder: null | ReactEventResponder,
708+
): void {
617709
const listeningInstances = Array.from(ownershipChangeListeners);
618710
const previousInstance = currentInstance;
619711
try {
620712
for (let i = 0; i < listeningInstances.length; i++) {
621713
const instance = listeningInstances[i];
622714
const {props, responder, state} = instance;
715+
if (limitByResponder !== null && limitByResponder !== responder) {
716+
continue;
717+
}
623718
currentInstance = instance;
624719
const onOwnershipChange = responder.onOwnershipChange;
625720
if (onOwnershipChange !== undefined) {
@@ -670,9 +765,12 @@ export function unmountEventResponder(
670765
currentTimers = null;
671766
}
672767
}
673-
if (currentOwner === eventComponentInstance) {
674-
currentOwner = null;
675-
triggerOwnershipListeners();
768+
try {
769+
currentEventQueue = createEventQueue();
770+
releaseOwnershipForEventComponentInstance(eventComponentInstance);
771+
processEventQueue();
772+
} finally {
773+
currentEventQueue = null;
676774
}
677775
if (responder.onOwnershipChange !== undefined) {
678776
ownershipChangeListeners.delete(eventComponentInstance);
@@ -709,10 +807,10 @@ export function dispatchEventForResponderEventSystem(
709807
eventSystemFlags: EventSystemFlags,
710808
): void {
711809
if (enableEventAPI) {
712-
if (alreadyDispatching) {
713-
return;
714-
}
715-
alreadyDispatching = true;
810+
const previousEventQueue = currentEventQueue;
811+
const previousInstance = currentInstance;
812+
const previousTimers = currentTimers;
813+
currentTimers = null;
716814
currentEventQueue = createEventQueue();
717815
try {
718816
traverseAndHandleEventResponderInstances(
@@ -724,10 +822,9 @@ export function dispatchEventForResponderEventSystem(
724822
);
725823
processEventQueue();
726824
} finally {
727-
currentTimers = null;
728-
currentInstance = null;
729-
currentEventQueue = null;
730-
alreadyDispatching = false;
825+
currentTimers = previousTimers;
826+
currentInstance = previousInstance;
827+
currentEventQueue = previousEventQueue;
731828
}
732829
}
733830
}

0 commit comments

Comments
 (0)