@samitouri / QOS-React / commits / d1f667acc0

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 };