Event API: follow up fixes for FocusScope + context changes (#15496)
Dominic Gannaway committed
Apr 25, 2019 at 20:10 UTC
d1f667acc065f2bd9fa4921af6c4d67f5c7b253b
14 files changed
+340
-144
packages/react-dom/src/client/ReactDOMHostConfig.js
+1
-1
@@ -896,8 +896,8 @@ export function mountEventComponent(
896
eventComponentInstance: ReactEventComponentInstance,
897
): void {
898
if (enableEventAPI) {
899
- mountEventResponder(eventComponentInstance);
899
updateEventComponent(eventComponentInstance);
900
+ mountEventResponder(eventComponentInstance);
901
}
902
}
903
packages/react-dom/src/events/DOMEventResponderSystem.js
+162
-65
@@ -84,10 +84,13 @@ const eventListeners:
84
($Shape<PartialEventObject>) => void,
85
> = new PossiblyWeakMap();
86
87
-let alreadyDispatching = false;
87
+const responderOwners: Map<
88
+ ReactEventResponder,
89
+ ReactEventComponentInstance,
90
+> = new Map();
91
+let globalOwner = null;
92
93
let currentTimers = new Map();
90
-let currentOwner = null;
94
let currentInstance: null | ReactEventComponentInstance = null;
95
let currentEventQueue: null | EventQueue = null;
96
@@ -131,8 +134,9 @@ const eventResponderContext: ReactResponderContext = {
134
eventListeners.set(eventObject, listener);
135
eventQueue.events.push(eventObject);
136
},
134
- isPositionWithinTouchHitTarget(doc: Document, x: number, y: number): boolean {
137
+ isPositionWithinTouchHitTarget(x: number, y: number): boolean {
138
validateResponderContext();
139
+ const doc = getActiveDocument();
140
// This isn't available in some environments (JSDOM)
141
if (typeof doc.elementFromPoint !== 'function') {
142
return false;
@@ -188,6 +192,27 @@ const eventResponderContext: ReactResponderContext = {
192
}
193
return false;
194
},
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
+ },
216
isTargetWithinElement(
217
childTarget: Element | Document,
218
parentTarget: Element | Document,
@@ -204,12 +229,10 @@ const eventResponderContext: ReactResponderContext = {
229
}
230
return false;
231
},
207
- addRootEventTypes(
208
- doc: Document,
209
- rootEventTypes: Array<ReactEventResponderEventType>,
210
- ): void {
232
+ addRootEventTypes(rootEventTypes: Array<ReactEventResponderEventType>): void {
233
validateResponderContext();
212
- listenToResponderEventTypesImpl(rootEventTypes, doc);
234
+ const activeDocument = getActiveDocument();
235
+ listenToResponderEventTypesImpl(rootEventTypes, activeDocument);
236
for (let i = 0; i < rootEventTypes.length; i++) {
237
const rootEventType = rootEventTypes[i];
238
const topLevelEventType =
@@ -265,25 +288,38 @@ const eventResponderContext: ReactResponderContext = {
288
},
289
hasOwnership(): boolean {
290
validateResponderContext();
268
- return currentOwner === currentInstance;
291
+ const responder = ((currentInstance: any): ReactEventComponentInstance)
292
+ .responder;
293
+ return (
294
+ globalOwner === currentInstance ||
295
+ responderOwners.get(responder) === currentInstance
296
+ );
297
},
270
- requestOwnership(): boolean {
298
+ requestGlobalOwnership(): boolean {
299
validateResponderContext();
272
- if (currentOwner !== null) {
300
+ if (globalOwner !== null) {
301
return false;
302
}
275
- currentOwner = currentInstance;
276
- triggerOwnershipListeners();
303
+ globalOwner = currentInstance;
304
+ triggerOwnershipListeners(null);
305
return true;
306
},
279
- releaseOwnership(): boolean {
307
+ requestResponderOwnership(): boolean {
308
validateResponderContext();
281
- if (currentOwner !== currentInstance) {
309
+ const eventComponentInstance = ((currentInstance: any): ReactEventComponentInstance);
310
+ const responder = eventComponentInstance.responder;
311
+ if (responderOwners.has(responder)) {
312
return false;
313
}
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
+ );
323
},
324
setTimeout(func: () => void, delay): Symbol {
325
validateResponderContext();
@@ -330,9 +366,6 @@ const eventResponderContext: ReactResponderContext = {
366
let node = ((eventComponentInstance.currentFiber: any): Fiber).child;
367
368
while (node !== null) {
333
- if (node.stateNode === currentInstance) {
334
- break;
335
- }
369
if (isFiberHostComponentFocusable(node)) {
370
focusableElements.push(node.stateNode);
371
} else {
@@ -353,13 +386,44 @@ const eventResponderContext: ReactResponderContext = {
386
if (parent === null) {
387
break;
388
}
389
+ if (parent.stateNode === currentInstance) {
390
+ break;
391
+ }
392
node = parent.sibling;
393
}
394
395
return focusableElements;
396
},
397
+ getActiveDocument,
398
};
399
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
+
427
function isFiberHostComponentFocusable(fiber: Fiber): boolean {
428
if (fiber.tag !== HostComponent) {
429
return false;
@@ -368,18 +432,22 @@ function isFiberHostComponentFocusable(fiber: Fiber): boolean {
432
if (memoizedProps.tabIndex === -1 || memoizedProps.disabled) {
433
return false;
434
}
371
- if (memoizedProps.tabIndex === 0) {
435
+ if (memoizedProps.tabIndex === 0 || memoizedProps.contentEditable === true) {
436
return true;
437
}
438
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';
443
}
444
return (
445
type === 'button' ||
446
type === 'textarea' ||
380
- type === 'input' ||
447
type === 'object' ||
382
- type === 'select'
448
+ type === 'select' ||
449
+ type === 'iframe' ||
450
+ type === 'embed'
451
);
452
}
453
@@ -487,15 +555,13 @@ function getTargetEventResponderInstances(
555
// Traverse up the fiber tree till we find event component fibers.
556
if (node.tag === EventComponent) {
557
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);
565
}
566
}
567
}
@@ -516,18 +582,35 @@ function getRootEventResponderInstances(
582
583
for (let i = 0; i < rootEventComponentInstances.length; i++) {
584
const rootEventComponentInstance = rootEventComponentInstances[i];
519
-
520
- if (
521
- currentOwner === null ||
522
- currentOwner === rootEventComponentInstance
523
- ) {
524
- eventResponderInstances.push(rootEventComponentInstance);
525
- }
585
+ eventResponderInstances.push(rootEventComponentInstance);
586
}
587
}
588
return eventResponderInstances;
589
}
590
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
+
614
function traverseAndHandleEventResponderInstances(
615
topLevelType: DOMTopLevelEventType,
616
targetFiber: null | Fiber,
@@ -564,14 +647,16 @@ function traverseAndHandleEventResponderInstances(
647
for (i = length; i-- > 0; ) {
648
const targetEventResponderInstance = targetEventResponderInstances[i];
649
const {responder, props, state} = targetEventResponderInstance;
567
- if (responder.stopLocalPropagation) {
568
- if (propagatedEventResponders.has(responder)) {
569
- continue;
570
- }
571
- propagatedEventResponders.add(responder);
572
- }
650
const eventListener = responder.onEventCapture;
651
if (eventListener !== undefined) {
652
+ if (
653
+ shouldSkipEventComponent(
654
+ targetEventResponderInstance,
655
+ propagatedEventResponders,
656
+ )
657
+ ) {
658
+ continue;
659
+ }
660
currentInstance = targetEventResponderInstance;
661
eventListener(responderEvent, eventResponderContext, props, state);
662
}
@@ -582,14 +667,16 @@ function traverseAndHandleEventResponderInstances(
667
for (i = 0; i < length; i++) {
668
const targetEventResponderInstance = targetEventResponderInstances[i];
669
const {responder, props, state} = targetEventResponderInstance;
585
- if (responder.stopLocalPropagation) {
586
- if (propagatedEventResponders.has(responder)) {
587
- continue;
588
- }
589
- propagatedEventResponders.add(responder);
590
- }
670
const eventListener = responder.onEvent;
671
if (eventListener !== undefined) {
672
+ if (
673
+ shouldSkipEventComponent(
674
+ targetEventResponderInstance,
675
+ propagatedEventResponders,
676
+ )
677
+ ) {
678
+ continue;
679
+ }
680
currentInstance = targetEventResponderInstance;
681
eventListener(responderEvent, eventResponderContext, props, state);
682
}
@@ -606,6 +693,9 @@ function traverseAndHandleEventResponderInstances(
693
const {responder, props, state} = rootEventResponderInstance;
694
const eventListener = responder.onRootEvent;
695
if (eventListener !== undefined) {
696
+ if (shouldSkipEventComponent(rootEventResponderInstance, null)) {
697
+ continue;
698
+ }
699
currentInstance = rootEventResponderInstance;
700
eventListener(responderEvent, eventResponderContext, props, state);
701
}
@@ -613,13 +703,18 @@ function traverseAndHandleEventResponderInstances(
703
}
704
}
705
616
-function triggerOwnershipListeners(): void {
706
+function triggerOwnershipListeners(
707
+ limitByResponder: null | ReactEventResponder,
708
+): void {
709
const listeningInstances = Array.from(ownershipChangeListeners);
710
const previousInstance = currentInstance;
711
try {
712
for (let i = 0; i < listeningInstances.length; i++) {
713
const instance = listeningInstances[i];
714
const {props, responder, state} = instance;
715
+ if (limitByResponder !== null && limitByResponder !== responder) {
716
+ continue;
717
+ }
718
currentInstance = instance;
719
const onOwnershipChange = responder.onOwnershipChange;
720
if (onOwnershipChange !== undefined) {
@@ -670,9 +765,12 @@ export function unmountEventResponder(
765
currentTimers = null;
766
}
767
}
673
- if (currentOwner === eventComponentInstance) {
674
- currentOwner = null;
675
- triggerOwnershipListeners();
768
+ try {
769
+ currentEventQueue = createEventQueue();
770
+ releaseOwnershipForEventComponentInstance(eventComponentInstance);
771
+ processEventQueue();
772
+ } finally {
773
+ currentEventQueue = null;
774
}
775
if (responder.onOwnershipChange !== undefined) {
776
ownershipChangeListeners.delete(eventComponentInstance);
@@ -709,10 +807,10 @@ export function dispatchEventForResponderEventSystem(
807
eventSystemFlags: EventSystemFlags,
808
): void {
809
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;
814
currentEventQueue = createEventQueue();
815
try {
816
traverseAndHandleEventResponderInstances(
@@ -724,10 +822,9 @@ export function dispatchEventForResponderEventSystem(
822
);
823
processEventQueue();
824
} finally {
727
- currentTimers = null;
728
- currentInstance = null;
729
- currentEventQueue = null;
730
- alreadyDispatching = false;
825
+ currentTimers = previousTimers;
826
+ currentInstance = previousInstance;
827
+ currentEventQueue = previousEventQueue;
828
}
829
}
830
}
packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
+1
-1
@@ -638,7 +638,7 @@ describe('DOMEventResponderSystem', () => {
638
undefined,
639
undefined,
640
(event, context, props, state) => {
641
- ownershipGained = context.requestOwnership();
641
+ ownershipGained = context.requestGlobalOwnership();
642
},
643
undefined,
644
undefined,
packages/react-events/src/Drag.js
+2
-2
@@ -129,7 +129,7 @@ const DragResponder = {
129
);
130
}
131
132
- context.addRootEventTypes(target.ownerDocument, rootEventTypes);
132
+ context.addRootEventTypes(rootEventTypes);
133
}
134
break;
135
}
@@ -166,7 +166,7 @@ const DragResponder = {
166
props.onShouldClaimOwnership &&
167
props.onShouldClaimOwnership()
168
) {
169
- shouldEnableDragging = context.requestOwnership();
169
+ shouldEnableDragging = context.requestGlobalOwnership();
170
}
171
if (shouldEnableDragging) {
172
state.isDragging = true;
packages/react-events/src/FocusScope.js
+71
-50
@@ -24,10 +24,8 @@ type FocusScopeState = {
24
currentFocusedNode: null | HTMLElement,
25
};
26
27
-const rootEventTypes = [
28
- {name: 'focus', passive: true, capture: true},
29
- {name: 'keydown', passive: false},
30
-];
27
+const targetEventTypes = [{name: 'keydown', passive: false}];
28
+const rootEventTypes = [{name: 'focus', passive: true, capture: true}];
29
30
function focusFirstChildEventTarget(
31
context: ReactResponderContext,
@@ -37,24 +35,11 @@ function focusFirstChildEventTarget(
35
if (elements.length > 0) {
36
const firstElement = elements[0];
37
firstElement.focus();
40
- state.currentFocusedNode = firstElement;
41
- }
42
-}
43
-
44
-function focusLastChildEventTarget(
45
- context: ReactResponderContext,
46
- state: FocusScopeState,
47
-): void {
48
- const elements = context.getFocusableElementsInScope();
49
- const length = elements.length;
50
- if (elements.length > 0) {
51
- const lastElement = elements[length - 1];
52
- lastElement.focus();
53
- state.currentFocusedNode = lastElement;
38
}
39
}
40
41
const FocusScopeResponder = {
42
+ targetEventTypes,
43
rootEventTypes,
44
createInitialState(): FocusScopeState {
45
return {
@@ -62,70 +47,100 @@ const FocusScopeResponder = {
47
currentFocusedNode: null,
48
};
49
},
65
- onRootEvent(
50
+ onEvent(
51
event: ReactResponderEvent,
52
context: ReactResponderContext,
53
props: FocusScopeProps,
54
state: FocusScopeState,
55
) {
71
- const {type, target, nativeEvent} = event;
56
+ const {type, nativeEvent} = event;
57
+ const hasOwnership =
58
+ context.hasOwnership() || context.requestResponderOwnership();
59
73
- if (type === 'focus') {
74
- if (context.isTargetWithinEventComponent(target)) {
75
- state.currentFocusedNode = ((target: any): HTMLElement);
76
- } else if (props.trap) {
77
- if (state.currentFocusedNode !== null) {
78
- state.currentFocusedNode.focus();
79
- } else {
80
- focusFirstChildEventTarget(context, state);
81
- }
82
- }
83
- } else if (type === 'keydown' && nativeEvent.key === 'Tab') {
84
- const currentFocusedNode = state.currentFocusedNode;
85
- if (currentFocusedNode !== null) {
60
+ if (!hasOwnership) {
61
+ return;
62
+ }
63
+ if (type === 'keydown' && nativeEvent.key === 'Tab') {
64
+ const focusedElement = context.getActiveDocument().activeElement;
65
+ if (
66
+ focusedElement !== null &&
67
+ context.isTargetWithinEventComponent(focusedElement)
68
+ ) {
69
const {altkey, ctrlKey, metaKey, shiftKey} = (nativeEvent: any);
70
// Skip if any of these keys are being pressed
71
if (altkey || ctrlKey || metaKey) {
72
return;
73
}
74
const elements = context.getFocusableElementsInScope();
92
- const position = elements.indexOf(currentFocusedNode);
75
+ const position = elements.indexOf(focusedElement);
76
+ const lastPosition = elements.length - 1;
77
+ let nextElement = null;
78
+
79
if (shiftKey) {
80
if (position === 0) {
81
if (props.trap) {
96
- focusLastChildEventTarget(context, state);
82
+ nextElement = elements[lastPosition];
83
} else {
84
+ // Out of bounds
85
+ context.releaseOwnership();
86
return;
87
}
88
} else {
101
- const previousElement = elements[position - 1];
102
- previousElement.focus();
103
- state.currentFocusedNode = previousElement;
89
+ nextElement = elements[position - 1];
90
}
91
} else {
106
- if (position === elements.length - 1) {
92
+ if (position === lastPosition) {
93
if (props.trap) {
108
- focusFirstChildEventTarget(context, state);
94
+ nextElement = elements[0];
95
} else {
96
+ // Out of bounds
97
+ context.releaseOwnership();
98
return;
99
}
100
} else {
113
- const nextElement = elements[position + 1];
114
- nextElement.focus();
115
- state.currentFocusedNode = nextElement;
101
+ nextElement = elements[position + 1];
102
+ }
103
+ }
104
+ // If this element is possibly inside the scope of another
105
+ // FocusScope responder or is out of bounds, then we release ownership.
106
+ if (nextElement !== null) {
107
+ if (!context.isTargetWithinEventResponderScope(nextElement)) {
108
+ context.releaseOwnership();
109
}
110
+ nextElement.focus();
111
+ state.currentFocusedNode = nextElement;
112
+ ((nativeEvent: any): KeyboardEvent).preventDefault();
113
}
118
- ((nativeEvent: any): KeyboardEvent).preventDefault();
114
}
115
}
116
},
122
- onMount(
117
+ onRootEvent(
118
+ event: ReactResponderEvent,
119
context: ReactResponderContext,
120
props: FocusScopeProps,
121
state: FocusScopeState,
122
) {
123
+ const {target} = event;
124
+
125
+ // Handle global trapping
126
+ if (props.trap) {
127
+ if (!context.isTargetWithinEventComponent(target)) {
128
+ const currentFocusedNode = state.currentFocusedNode;
129
+ if (currentFocusedNode !== null) {
130
+ currentFocusedNode.focus();
131
+ } else if (props.autoFocus) {
132
+ focusFirstChildEventTarget(context, state);
133
+ }
134
+ }
135
+ }
136
+ },
137
+ onMount(
138
+ context: ReactResponderContext,
139
+ props: FocusScopeProps,
140
+ state: FocusScopeState,
141
+ ): void {
142
if (props.restoreFocus) {
128
- state.nodeToRestore = document.activeElement;
143
+ state.nodeToRestore = context.getActiveDocument().activeElement;
144
}
145
if (props.autoFocus) {
146
focusFirstChildEventTarget(context, state);
@@ -135,8 +150,12 @@ const FocusScopeResponder = {
150
context: ReactResponderContext,
151
props: FocusScopeProps,
152
state: FocusScopeState,
138
- ) {
139
- if (props.restoreFocus && state.nodeToRestore !== null) {
153
+ ): void {
154
+ if (
155
+ props.restoreFocus &&
156
+ state.nodeToRestore !== null &&
157
+ context.hasOwnership()
158
+ ) {
159
state.nodeToRestore.focus();
160
}
161
},
@@ -144,8 +163,10 @@ const FocusScopeResponder = {
163
context: ReactResponderContext,
164
props: FocusScopeProps,
165
state: FocusScopeState,
147
- ) {
148
- // unmountResponder(context, props, state);
166
+ ): void {
167
+ if (!context.hasOwnership()) {
168
+ state.currentFocusedNode = null;
169
+ }
170
},
171
};
172
packages/react-events/src/Press.js
+1
-1
@@ -472,7 +472,7 @@ const PressResponder = {
472
state.pressTarget = target;
473
state.isPressWithinResponderRegion = true;
474
dispatchPressStartEvents(context, props, state);
475
- context.addRootEventTypes(target.ownerDocument, rootEventTypes);
475
+ context.addRootEventTypes(rootEventTypes);
476
} else {
477
// Prevent spacebar press from scrolling the window
478
if (isValidKeyPress(nativeEvent.key) && nativeEvent.key === ' ') {
packages/react-events/src/Swipe.js
+2
-2
@@ -120,7 +120,7 @@ const SwipeResponder = {
120
let shouldEnableSwiping = true;
121
122
if (props.onShouldClaimOwnership && props.onShouldClaimOwnership()) {
123
- shouldEnableSwiping = context.requestOwnership();
123
+ shouldEnableSwiping = context.requestGlobalOwnership();
124
}
125
if (shouldEnableSwiping) {
126
state.isSwiping = true;
@@ -129,7 +129,7 @@ const SwipeResponder = {
129
state.x = x;
130
state.y = y;
131
state.swipeTarget = target;
132
- context.addRootEventTypes(target.ownerDocument, rootEventTypes);
132
+ context.addRootEventTypes(rootEventTypes);
133
} else {
134
state.touchId = null;
135
}
packages/react-events/src/__tests__/Drag-test.internal.js
+1
@@ -29,6 +29,7 @@ describe('Drag event responder', () => {
29
});
30
31
afterEach(() => {
32
+ ReactDOM.render(null, container);
33
document.body.removeChild(container);
34
container = null;
35
});
packages/react-events/src/__tests__/Focus-test.internal.js
+1
@@ -36,6 +36,7 @@ describe('Focus event responder', () => {
36
});
37
38
afterEach(() => {
39
+ ReactDOM.render(null, container);
40
document.body.removeChild(container);
41
container = null;
42
});
packages/react-events/src/__tests__/FocusScope-test.internal.js
+88
-11
@@ -49,11 +49,12 @@ describe('FocusScope event responder', () => {
49
});
50
51
afterEach(() => {
52
+ ReactDOM.render(null, container);
53
document.body.removeChild(container);
54
container = null;
55
});
56
56
- it('when using a simple focus scope with autofocus', () => {
57
+ it('should work as expected with autofocus', () => {
58
const inputRef = React.createRef();
59
const input2Ref = React.createRef();
60
const buttonRef = React.createRef();
@@ -74,17 +75,17 @@ describe('FocusScope event responder', () => {
75
76
ReactDOM.render(<SimpleFocusScope />, container);
77
expect(document.activeElement).toBe(inputRef.current);
77
- document.dispatchEvent(createTabForward());
78
+ document.activeElement.dispatchEvent(createTabForward());
79
expect(document.activeElement).toBe(buttonRef.current);
79
- document.dispatchEvent(createTabForward());
80
+ document.activeElement.dispatchEvent(createTabForward());
81
expect(document.activeElement).toBe(divRef.current);
81
- document.dispatchEvent(createTabForward());
82
+ document.activeElement.dispatchEvent(createTabForward());
83
expect(document.activeElement).toBe(butto2nRef.current);
83
- document.dispatchEvent(createTabBackward());
84
+ document.activeElement.dispatchEvent(createTabBackward());
85
expect(document.activeElement).toBe(divRef.current);
86
});
87
87
- it('when using a simple focus scope with autofocus and trapping', () => {
88
+ it('should work as expected with autofocus and trapping', () => {
89
const inputRef = React.createRef();
90
const input2Ref = React.createRef();
91
const buttonRef = React.createRef();
@@ -103,15 +104,91 @@ describe('FocusScope event responder', () => {
104
105
ReactDOM.render(<SimpleFocusScope />, container);
106
expect(document.activeElement).toBe(buttonRef.current);
106
- document.dispatchEvent(createTabForward());
107
+ document.activeElement.dispatchEvent(createTabForward());
108
expect(document.activeElement).toBe(button2Ref.current);
108
- document.dispatchEvent(createTabForward());
109
+ document.activeElement.dispatchEvent(createTabForward());
110
expect(document.activeElement).toBe(buttonRef.current);
110
- document.dispatchEvent(createTabForward());
111
+ document.activeElement.dispatchEvent(createTabForward());
112
expect(document.activeElement).toBe(button2Ref.current);
112
- document.dispatchEvent(createTabBackward());
113
+ document.activeElement.dispatchEvent(createTabBackward());
114
expect(document.activeElement).toBe(buttonRef.current);
114
- document.dispatchEvent(createTabBackward());
115
+ document.activeElement.dispatchEvent(createTabBackward());
116
+ expect(document.activeElement).toBe(button2Ref.current);
117
+ });
118
+
119
+ it('should work as expected when nested', () => {
120
+ const inputRef = React.createRef();
121
+ const input2Ref = React.createRef();
122
+ const buttonRef = React.createRef();
123
+ const button2Ref = React.createRef();
124
+ const button3Ref = React.createRef();
125
+ const button4Ref = React.createRef();
126
+
127
+ const SimpleFocusScope = () => (
128
+ <div>
129
+ <FocusScope>
130
+ <input ref={inputRef} tabIndex={-1} />
131
+ <button ref={buttonRef} id={1} />
132
+ <FocusScope>
133
+ <button ref={button2Ref} id={2} />
134
+ <button ref={button3Ref} id={3} />
135
+ </FocusScope>
136
+ <input ref={input2Ref} tabIndex={-1} />
137
+ <button ref={button4Ref} id={4} />
138
+ </FocusScope>
139
+ </div>
140
+ );
141
+
142
+ ReactDOM.render(<SimpleFocusScope />, container);
143
+ buttonRef.current.focus();
144
+ expect(document.activeElement).toBe(buttonRef.current);
145
+ document.activeElement.dispatchEvent(createTabForward());
146
+ expect(document.activeElement).toBe(button2Ref.current);
147
+ document.activeElement.dispatchEvent(createTabForward());
148
+ expect(document.activeElement).toBe(button3Ref.current);
149
+ document.activeElement.dispatchEvent(createTabForward());
150
+ expect(document.activeElement).toBe(button4Ref.current);
151
+ document.activeElement.dispatchEvent(createTabBackward());
152
+ expect(document.activeElement).toBe(button3Ref.current);
153
+ document.activeElement.dispatchEvent(createTabBackward());
154
+ expect(document.activeElement).toBe(button2Ref.current);
155
+ });
156
+
157
+ it('should work as expected when nested with scope that is trapped', () => {
158
+ const inputRef = React.createRef();
159
+ const input2Ref = React.createRef();
160
+ const buttonRef = React.createRef();
161
+ const button2Ref = React.createRef();
162
+ const button3Ref = React.createRef();
163
+ const button4Ref = React.createRef();
164
+
165
+ const SimpleFocusScope = () => (
166
+ <div>
167
+ <FocusScope>
168
+ <input ref={inputRef} tabIndex={-1} />
169
+ <button ref={buttonRef} id={1} />
170
+ <FocusScope trap={true}>
171
+ <button ref={button2Ref} id={2} />
172
+ <button ref={button3Ref} id={3} />
173
+ </FocusScope>
174
+ <input ref={input2Ref} tabIndex={-1} />
175
+ <button ref={button4Ref} id={4} />
176
+ </FocusScope>
177
+ </div>
178
+ );
179
+
180
+ ReactDOM.render(<SimpleFocusScope />, container);
181
+ buttonRef.current.focus();
182
+ expect(document.activeElement).toBe(buttonRef.current);
183
+ document.activeElement.dispatchEvent(createTabForward());
184
+ expect(document.activeElement).toBe(button2Ref.current);
185
+ document.activeElement.dispatchEvent(createTabForward());
186
+ expect(document.activeElement).toBe(button3Ref.current);
187
+ document.activeElement.dispatchEvent(createTabForward());
188
+ expect(document.activeElement).toBe(button2Ref.current);
189
+ document.activeElement.dispatchEvent(createTabBackward());
190
+ expect(document.activeElement).toBe(button3Ref.current);
191
+ document.activeElement.dispatchEvent(createTabBackward());
192
expect(document.activeElement).toBe(button2Ref.current);
193
});
194
});
packages/react-events/src/__tests__/Hover-test.internal.js
+1
@@ -41,6 +41,7 @@ describe('Hover event responder', () => {
41
});
42
43
afterEach(() => {
44
+ ReactDOM.render(null, container);
45
document.body.removeChild(container);
46
container = null;
47
});
packages/react-events/src/__tests__/Press-test.internal.js
+1
@@ -51,6 +51,7 @@ describe('Event responder: Press', () => {
51
});
52
53
afterEach(() => {
54
+ ReactDOM.render(null, container);
55
document.body.removeChild(container);
56
container = null;
57
});
packages/react-events/src/utils.js
+3
-4
@@ -51,10 +51,9 @@ export function isEventPositionWithinTouchHitTarget(
51
context: ReactResponderContext,
52
) {
53
const nativeEvent: any = event.nativeEvent;
54
- const target: any = event.target;
54
return context.isPositionWithinTouchHitTarget(
56
- target.ownerDocument,
57
- nativeEvent.x,
58
- nativeEvent.y,
55
+ // x and y can be doubles, so ensure they are integers
56
+ parseInt(nativeEvent.x, 10),
57
+ parseInt(nativeEvent.y, 10),
58
);
59
}
packages/shared/ReactTypes.js
+5
-7
@@ -173,22 +173,20 @@ export type ReactResponderContext = {
173
) => boolean,
174
isTargetWithinEventComponent: (Element | Document) => boolean,
175
isTargetDirectlyWithinEventComponent: (Element | Document) => boolean,
176
- isPositionWithinTouchHitTarget: (
177
- doc: Document,
178
- x: number,
179
- y: number,
180
- ) => boolean,
176
+ isTargetWithinEventResponderScope: (Element | Document) => boolean,
177
+ isPositionWithinTouchHitTarget: (x: number, y: number) => boolean,
178
addRootEventTypes: (
182
- document: Document,
179
rootEventTypes: Array<ReactEventResponderEventType>,
180
) => void,
181
removeRootEventTypes: (
182
rootEventTypes: Array<ReactEventResponderEventType>,
183
) => void,
184
hasOwnership: () => boolean,
189
- requestOwnership: () => boolean,
185
+ requestResponderOwnership: () => boolean,
186
+ requestGlobalOwnership: () => boolean,
187
releaseOwnership: () => boolean,
188
setTimeout: (func: () => void, timeout: number) => Symbol,
189
clearTimeout: (timerId: Symbol) => void,
190
getFocusableElementsInScope(): Array<HTMLElement>,
191
+ getActiveDocument(): Document,
192
};