@samitouri / QOS-React / commits / 64e3da286f

Event API: Add `FocusScope` surface (#15487)

Dominic Gannaway committed Apr 25, 2019 at 02:01 UTC 64e3da286f2c1e66303d2ae1dc06276b2f866e9d
24 files changed +500 -347
packages/react-dom/src/events/DOMEventResponderSystem.js
+63 -65
@@ -84,6 +84,8 @@ const eventListeners:
84 ($Shape<PartialEventObject>) => void,
85 > = new PossiblyWeakMap();
86
87 +let alreadyDispatching = false;
88 +
89 let currentTimers = new Map();
90 let currentOwner = null;
91 let currentInstance: null | ReactEventComponentInstance = null;
@@ -322,61 +324,65 @@ const eventResponderContext: ReactResponderContext = {
324 }
325 }
326 },
325 - getEventTargetsFromTarget(
326 - target: Element | Document,
327 - queryType?: Symbol | number,
328 - queryKey?: string,
329 - ): Array<{
330 - node: Element,
331 - props: null | Object,
332 - }> {
333 - validateResponderContext();
334 - const eventTargetHostComponents = [];
335 - let node = getClosestInstanceFromNode(target);
336 - // We traverse up the fiber tree from the target fiber, to the
337 - // current event component fiber. Along the way, we check if
338 - // the fiber has any children that are event targets. If there
339 - // are, we query them (optionally) to ensure they match the
340 - // specified type and key. We then push the event target props
341 - // along with the associated parent host component of that event
342 - // target.
327 + getFocusableElementsInScope(): Array<HTMLElement> {
328 + const focusableElements = [];
329 + const eventComponentInstance = ((currentInstance: any): ReactEventComponentInstance);
330 + let node = ((eventComponentInstance.currentFiber: any): Fiber).child;
331 +
332 while (node !== null) {
333 if (node.stateNode === currentInstance) {
334 break;
335 }
347 - let child = node.child;
348 -
349 - while (child !== null) {
350 - if (
351 - child.tag === EventTargetWorkTag &&
352 - queryEventTarget(child, queryType, queryKey)
353 - ) {
354 - const props = child.stateNode.props;
355 - let parent = child.return;
356 -
357 - if (parent !== null) {
358 - if (parent.stateNode === currentInstance) {
359 - break;
360 - }
361 - if (parent.tag === HostComponent) {
362 - eventTargetHostComponents.push({
363 - node: parent.stateNode,
364 - props,
365 - });
366 - break;
367 - }
368 - parent = parent.return;
369 - }
370 - break;
336 + if (isFiberHostComponentFocusable(node)) {
337 + focusableElements.push(node.stateNode);
338 + } else {
339 + const child = node.child;
340 +
341 + if (child !== null) {
342 + node = child;
343 + continue;
344 }
372 - child = child.sibling;
345 }
374 - node = node.return;
346 + const sibling = node.sibling;
347 +
348 + if (sibling !== null) {
349 + node = sibling;
350 + continue;
351 + }
352 + const parent = node.return;
353 + if (parent === null) {
354 + break;
355 + }
356 + node = parent.sibling;
357 }
376 - return eventTargetHostComponents;
358 +
359 + return focusableElements;
360 },
361 };
362
363 +function isFiberHostComponentFocusable(fiber: Fiber): boolean {
364 + if (fiber.tag !== HostComponent) {
365 + return false;
366 + }
367 + const {type, memoizedProps} = fiber;
368 + if (memoizedProps.tabIndex === -1 || memoizedProps.disabled) {
369 + return false;
370 + }
371 + if (memoizedProps.tabIndex === 0) {
372 + return true;
373 + }
374 + if (type === 'a' || type === 'area') {
375 + return !!memoizedProps.href;
376 + }
377 + return (
378 + type === 'button' ||
379 + type === 'textarea' ||
380 + type === 'input' ||
381 + type === 'object' ||
382 + type === 'select'
383 + );
384 +}
385 +
386 function processTimers(timers: Map<Symbol, ResponderTimer>): void {
387 const timersArr = Array.from(timers.values());
388 currentEventQueue = createEventQueue();
@@ -398,20 +404,6 @@ function processTimers(timers: Map<Symbol, ResponderTimer>): void {
404 }
405 }
406
401 -function queryEventTarget(
402 - child: Fiber,
403 - queryType: void | Symbol | number,
404 - queryKey: void | string,
405 -): boolean {
406 - if (queryType !== undefined && child.type.type !== queryType) {
407 - return false;
408 - }
409 - if (queryKey !== undefined && child.key !== queryKey) {
410 - return false;
411 - }
412 - return true;
413 -}
414 -
407 function createResponderEvent(
408 topLevelType: string,
409 nativeEvent: AnyNativeEvent,
@@ -467,7 +459,7 @@ export function processEventQueue(): void {
459 }
460 }
461
470 -function getTargetEventTypes(
462 +function getTargetEventTypesSet(
463 eventTypes: Array<ReactEventResponderEventType>,
464 ): Set<DOMTopLevelEventType> {
465 let cachedSet = targetEventTypeCached.get(eventTypes);
@@ -497,12 +489,13 @@ function getTargetEventResponderInstances(
489 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
501 - const targetEventTypes = getTargetEventTypes(
502 - responder.targetEventTypes,
503 - );
504 - if (targetEventTypes.has(topLevelType)) {
505 - eventResponderInstances.push(eventComponentInstance);
494 + if (targetEventTypes !== undefined) {
495 + const targetEventTypesSet = getTargetEventTypesSet(targetEventTypes);
496 + if (targetEventTypesSet.has(topLevelType)) {
497 + eventResponderInstances.push(eventComponentInstance);
498 + }
499 }
500 }
501 }
@@ -716,6 +709,10 @@ export function dispatchEventForResponderEventSystem(
709 eventSystemFlags: EventSystemFlags,
710 ): void {
711 if (enableEventAPI) {
712 + if (alreadyDispatching) {
713 + return;
714 + }
715 + alreadyDispatching = true;
716 currentEventQueue = createEventQueue();
717 try {
718 traverseAndHandleEventResponderInstances(
@@ -730,6 +727,7 @@ export function dispatchEventForResponderEventSystem(
727 currentTimers = null;
728 currentInstance = null;
729 currentEventQueue = null;
730 + alreadyDispatching = false;
731 }
732 }
733 }
packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
-239
@@ -12,7 +12,6 @@
12 let React;
13 let ReactFeatureFlags;
14 let ReactDOM;
15 -let ReactSymbols;
15
16 function createReactEventComponent(
17 targetEventTypes,
@@ -57,14 +56,6 @@ function dispatchClickEvent(element) {
56 dispatchEvent(element, 'click');
57 }
58
60 -function createReactEventTarget(type) {
61 - return {
62 - $$typeof: ReactSymbols.REACT_EVENT_TARGET_TYPE,
63 - displayName: 'TestEventTarget',
64 - type,
65 - };
66 -}
67 -
59 // This is a new feature in Fiber so I put it in its own test file. It could
60 // probably move to one of the other test files once it is official.
61 describe('DOMEventResponderSystem', () => {
@@ -78,7 +69,6 @@ describe('DOMEventResponderSystem', () => {
69 ReactDOM = require('react-dom');
70 container = document.createElement('div');
71 document.body.appendChild(container);
81 - ReactSymbols = require('shared/ReactSymbols');
72 });
73
74 afterEach(() => {
@@ -676,235 +666,6 @@ describe('DOMEventResponderSystem', () => {
666 expect(onOwnershipChangeFired).toEqual(1);
667 });
668
679 - it('should be possible to get event targets', () => {
680 - let queryResult = null;
681 - const buttonRef = React.createRef();
682 - const divRef = React.createRef();
683 - const eventTargetType = Symbol.for('react.event_target.test');
684 - const EventTarget = createReactEventTarget(eventTargetType);
685 -
686 - const EventComponent = createReactEventComponent(
687 - ['click'],
688 - undefined,
689 - undefined,
690 - (event, context, props, state) => {
691 - queryResult = Array.from(
692 - context.getEventTargetsFromTarget(event.target),
693 - );
694 - },
695 - );
696 -
697 - const Test = () => (
698 - <EventComponent>
699 - <div ref={divRef}>
700 - <EventTarget foo={1} />
701 - <button ref={buttonRef}>
702 - <EventTarget foo={2} />
703 - Press me!
704 - </button>
705 - </div>
706 - </EventComponent>
707 - );
708 -
709 - ReactDOM.render(<Test />, container);
710 -
711 - let buttonElement = buttonRef.current;
712 - let divElement = divRef.current;
713 - dispatchClickEvent(buttonElement);
714 - jest.runAllTimers();
715 -
716 - expect(queryResult).toEqual([
717 - {
718 - node: buttonElement,
719 - props: {
720 - foo: 2,
721 - },
722 - },
723 - {
724 - node: divElement,
725 - props: {
726 - foo: 1,
727 - },
728 - },
729 - ]);
730 - });
731 -
732 - it('should be possible to query event targets by type', () => {
733 - let queryResult = null;
734 - const buttonRef = React.createRef();
735 - const divRef = React.createRef();
736 - const eventTargetType = Symbol.for('react.event_target.test');
737 - const EventTarget = createReactEventTarget(eventTargetType);
738 -
739 - const eventTargetType2 = Symbol.for('react.event_target.test2');
740 - const EventTarget2 = createReactEventTarget(eventTargetType2);
741 -
742 - const EventComponent = createReactEventComponent(
743 - ['click'],
744 - undefined,
745 - undefined,
746 - (event, context, props, state) => {
747 - queryResult = context.getEventTargetsFromTarget(
748 - event.target,
749 - eventTargetType2,
750 - );
751 - },
752 - );
753 -
754 - const Test = () => (
755 - <EventComponent>
756 - <div ref={divRef}>
757 - <EventTarget2 foo={1} />
758 - <button ref={buttonRef}>
759 - <EventTarget foo={2} />
760 - Press me!
761 - </button>
762 - </div>
763 - </EventComponent>
764 - );
765 -
766 - ReactDOM.render(<Test />, container);
767 -
768 - let buttonElement = buttonRef.current;
769 - let divElement = divRef.current;
770 - dispatchClickEvent(buttonElement);
771 - jest.runAllTimers();
772 -
773 - expect(queryResult).toEqual([
774 - {
775 - node: divElement,
776 - props: {
777 - foo: 1,
778 - },
779 - },
780 - ]);
781 - });
782 -
783 - it('should be possible to query event targets by key', () => {
784 - let queryResult = null;
785 - const buttonRef = React.createRef();
786 - const divRef = React.createRef();
787 - const eventTargetType = Symbol.for('react.event_target.test');
788 - const EventTarget = createReactEventTarget(eventTargetType);
789 -
790 - const EventComponent = createReactEventComponent(
791 - ['click'],
792 - undefined,
793 - undefined,
794 - (event, context, props, state) => {
795 - queryResult = context.getEventTargetsFromTarget(
796 - event.target,
797 - undefined,
798 - 'a',
799 - );
800 - },
801 - );
802 -
803 - const Test = () => (
804 - <EventComponent>
805 - <div ref={divRef}>
806 - <EventTarget foo={1} />
807 - <button ref={buttonRef}>
808 - <EventTarget key="a" foo={2} />
809 - Press me!
810 - </button>
811 - </div>
812 - </EventComponent>
813 - );
814 -
815 - ReactDOM.render(<Test />, container);
816 -
817 - let buttonElement = buttonRef.current;
818 - dispatchClickEvent(buttonElement);
819 - jest.runAllTimers();
820 -
821 - expect(queryResult).toEqual([
822 - {
823 - node: buttonElement,
824 - props: {
825 - foo: 2,
826 - },
827 - },
828 - ]);
829 - });
830 -
831 - it('should be possible to query event targets by type and key', () => {
832 - let queryResult = null;
833 - let queryResult2 = null;
834 - let queryResult3 = null;
835 - const buttonRef = React.createRef();
836 - const divRef = React.createRef();
837 - const eventTargetType = Symbol.for('react.event_target.test');
838 - const EventTarget = createReactEventTarget(eventTargetType);
839 -
840 - const eventTargetType2 = Symbol.for('react.event_target.test2');
841 - const EventTarget2 = createReactEventTarget(eventTargetType2);
842 -
843 - const EventComponent = createReactEventComponent(
844 - ['click'],
845 - undefined,
846 - undefined,
847 - (event, context, props, state) => {
848 - queryResult = context.getEventTargetsFromTarget(
849 - event.target,
850 - eventTargetType2,
851 - 'a',
852 - );
853 -
854 - queryResult2 = context.getEventTargetsFromTarget(
855 - event.target,
856 - eventTargetType,
857 - 'c',
858 - );
859 -
860 - // Should return an empty array as this doesn't exist
861 - queryResult3 = context.getEventTargetsFromTarget(
862 - event.target,
863 - eventTargetType,
864 - 'd',
865 - );
866 - },
867 - );
868 -
869 - const Test = () => (
870 - <EventComponent>
871 - <div ref={divRef}>
872 - <EventTarget2 key="a" foo={1} />
873 - <EventTarget2 key="b" foo={2} />
874 - <button ref={buttonRef}>
875 - <EventTarget key="c" foo={3} />
876 - Press me!
877 - </button>
878 - </div>
879 - </EventComponent>
880 - );
881 -
882 - ReactDOM.render(<Test />, container);
883 -
884 - let buttonElement = buttonRef.current;
885 - let divElement = divRef.current;
886 - dispatchClickEvent(buttonElement);
887 - jest.runAllTimers();
888 -
889 - expect(queryResult).toEqual([
890 - {
891 - node: divElement,
892 - props: {
893 - foo: 1,
894 - },
895 - },
896 - ]);
897 - expect(queryResult2).toEqual([
898 - {
899 - node: buttonElement,
900 - props: {
901 - foo: 3,
902 - },
903 - },
904 - ]);
905 - expect(queryResult3).toEqual([]);
906 - });
907 -
669 it('the event responder root listeners should fire on a root click event', () => {
670 let eventResponderFiredCount = 0;
671 let eventLog = [];
packages/react-dom/src/shared/assertValidProps.js
+6 -1
@@ -12,6 +12,8 @@ import warning from 'shared/warning';
12 import ReactSharedInternals from 'shared/ReactSharedInternals';
13
14 import voidElementTags from './voidElementTags';
15 +import {enableEventAPI} from 'shared/ReactFeatureFlags';
16 +import {REACT_EVENT_TARGET_TYPE} from 'shared/ReactSymbols';
17
18 const HTML = '__html';
19
@@ -27,7 +29,10 @@ function assertValidProps(tag: string, props: ?Object) {
29 // Note the use of `==` which checks for null or undefined.
30 if (voidElementTags[tag]) {
31 invariant(
30 - props.children == null && props.dangerouslySetInnerHTML == null,
32 + (props.children == null ||
33 + (enableEventAPI &&
34 + props.children.type.$$typeof === REACT_EVENT_TARGET_TYPE)) &&
35 + props.dangerouslySetInnerHTML == null,
36 '%s is a void element tag and must neither have `children` nor ' +
37 'use `dangerouslySetInnerHTML`.%s',
38 tag,
packages/react-events/Drag.js new
+14
@@ -0,0 +1,14 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +'use strict';
11 +
12 +const Drag = require('./src/Drag');
13 +
14 +module.exports = Drag.default || Drag;
packages/react-events/Focus.js new
+14
@@ -0,0 +1,14 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +'use strict';
11 +
12 +const Focus = require('./src/Focus');
13 +
14 +module.exports = Focus.default || Focus;
packages/react-events/FocusScope.js new
+14
@@ -0,0 +1,14 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +'use strict';
11 +
12 +const FocusScope = require('./src/FocusScope');
13 +
14 +module.exports = FocusScope.default || FocusScope;
packages/react-events/Hover.js new
+14
@@ -0,0 +1,14 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +'use strict';
11 +
12 +const Hover = require('./src/Hover');
13 +
14 +module.exports = Hover.default || Hover;
packages/react-events/Press.js new
+14
@@ -0,0 +1,14 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +'use strict';
11 +
12 +const Press = require('./src/Press');
13 +
14 +module.exports = Press.default || Press;
packages/react-events/Swipe.js new
+14
@@ -0,0 +1,14 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +'use strict';
11 +
12 +const Swipe = require('./src/Swipe');
13 +
14 +module.exports = Swipe.default || Swipe;
packages/react-events/npm/Drag.js new
+7
@@ -0,0 +1,7 @@
1 +'use strict';
2 +
3 +if (process.env.NODE_ENV === 'production') {
4 + module.exports = require('./cjs/react-events-drag.production.min.js');
5 +} else {
6 + module.exports = require('./cjs/react-events-drag.development.js');
7 +}
packages/react-events/npm/Focus.js new
+7
@@ -0,0 +1,7 @@
1 +'use strict';
2 +
3 +if (process.env.NODE_ENV === 'production') {
4 + module.exports = require('./cjs/react-events-focus.production.min.js');
5 +} else {
6 + module.exports = require('./cjs/react-events-focus.development.js');
7 +}
packages/react-events/npm/FocusScope.js new
+7
@@ -0,0 +1,7 @@
1 +'use strict';
2 +
3 +if (process.env.NODE_ENV === 'production') {
4 + module.exports = require('./cjs/react-events-focus-scope.production.min.js');
5 +} else {
6 + module.exports = require('./cjs/react-events-focus-scope.development.js');
7 +}
packages/react-events/npm/Hover.js new
+7
@@ -0,0 +1,7 @@
1 +'use strict';
2 +
3 +if (process.env.NODE_ENV === 'production') {
4 + module.exports = require('./cjs/react-events-hover.production.min.js');
5 +} else {
6 + module.exports = require('./cjs/react-events-hover.development.js');
7 +}
packages/react-events/npm/Press.js new
+7
@@ -0,0 +1,7 @@
1 +'use strict';
2 +
3 +if (process.env.NODE_ENV === 'production') {
4 + module.exports = require('./cjs/react-events-press.production.min.js');
5 +} else {
6 + module.exports = require('./cjs/react-events-press.development.js');
7 +}
packages/react-events/npm/Swipe.js new
+7
@@ -0,0 +1,7 @@
1 +'use strict';
2 +
3 +if (process.env.NODE_ENV === 'production') {
4 + module.exports = require('./cjs/react-events-swipe.production.min.js');
5 +} else {
6 + module.exports = require('./cjs/react-events-swipe.development.js');
7 +}
packages/react-events/package.json
+6 -5
@@ -12,11 +12,12 @@
12 "files": [
13 "LICENSE",
14 "README.md",
15 - "press.js",
16 - "hover.js",
17 - "focus.js",
18 - "swipe.js",
19 - "drag.js",
15 + "Press.js",
16 + "Hover.js",
17 + "Focus.js",
18 + "Swipe.js",
19 + "Drag.js",
20 + "FocusScope.js",
21 "index.js",
22 "build-info.json",
23 "cjs/",
packages/react-events/src/FocusScope.js new
+157
@@ -0,0 +1,157 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {
11 + ReactResponderEvent,
12 + ReactResponderContext,
13 +} from 'shared/ReactTypes';
14 +import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
15 +
16 +type FocusScopeProps = {
17 + autoFocus: Boolean,
18 + restoreFocus: Boolean,
19 + trap: Boolean,
20 +};
21 +
22 +type FocusScopeState = {
23 + nodeToRestore: null | HTMLElement,
24 + currentFocusedNode: null | HTMLElement,
25 +};
26 +
27 +const rootEventTypes = [
28 + {name: 'focus', passive: true, capture: true},
29 + {name: 'keydown', passive: false},
30 +];
31 +
32 +function focusFirstChildEventTarget(
33 + context: ReactResponderContext,
34 + state: FocusScopeState,
35 +): void {
36 + const elements = context.getFocusableElementsInScope();
37 + if (elements.length > 0) {
38 + const firstElement = elements[0];
39 + 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;
54 + }
55 +}
56 +
57 +const FocusScopeResponder = {
58 + rootEventTypes,
59 + createInitialState(): FocusScopeState {
60 + return {
61 + nodeToRestore: null,
62 + currentFocusedNode: null,
63 + };
64 + },
65 + onRootEvent(
66 + event: ReactResponderEvent,
67 + context: ReactResponderContext,
68 + props: FocusScopeProps,
69 + state: FocusScopeState,
70 + ) {
71 + const {type, target, nativeEvent} = event;
72 +
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) {
86 + const {altkey, ctrlKey, metaKey, shiftKey} = (nativeEvent: any);
87 + // Skip if any of these keys are being pressed
88 + if (altkey || ctrlKey || metaKey) {
89 + return;
90 + }
91 + const elements = context.getFocusableElementsInScope();
92 + const position = elements.indexOf(currentFocusedNode);
93 + if (shiftKey) {
94 + if (position === 0) {
95 + if (props.trap) {
96 + focusLastChildEventTarget(context, state);
97 + } else {
98 + return;
99 + }
100 + } else {
101 + const previousElement = elements[position - 1];
102 + previousElement.focus();
103 + state.currentFocusedNode = previousElement;
104 + }
105 + } else {
106 + if (position === elements.length - 1) {
107 + if (props.trap) {
108 + focusFirstChildEventTarget(context, state);
109 + } else {
110 + return;
111 + }
112 + } else {
113 + const nextElement = elements[position + 1];
114 + nextElement.focus();
115 + state.currentFocusedNode = nextElement;
116 + }
117 + }
118 + ((nativeEvent: any): KeyboardEvent).preventDefault();
119 + }
120 + }
121 + },
122 + onMount(
123 + context: ReactResponderContext,
124 + props: FocusScopeProps,
125 + state: FocusScopeState,
126 + ) {
127 + if (props.restoreFocus) {
128 + state.nodeToRestore = document.activeElement;
129 + }
130 + if (props.autoFocus) {
131 + focusFirstChildEventTarget(context, state);
132 + }
133 + },
134 + onUnmount(
135 + context: ReactResponderContext,
136 + props: FocusScopeProps,
137 + state: FocusScopeState,
138 + ) {
139 + if (props.restoreFocus && state.nodeToRestore !== null) {
140 + state.nodeToRestore.focus();
141 + }
142 + },
143 + onOwnershipChange(
144 + context: ReactResponderContext,
145 + props: FocusScopeProps,
146 + state: FocusScopeState,
147 + ) {
148 + // unmountResponder(context, props, state);
149 + },
150 +};
151 +
152 +export default {
153 + $$typeof: REACT_EVENT_COMPONENT_TYPE,
154 + displayName: 'FocusScope',
155 + props: null,
156 + responder: FocusScopeResponder,
157 +};
packages/react-events/src/ReactEvents.js
-12
@@ -10,8 +10,6 @@
10 import {
11 REACT_EVENT_TARGET_TYPE,
12 REACT_EVENT_TARGET_TOUCH_HIT,
13 - REACT_EVENT_FOCUS_TARGET,
14 - REACT_EVENT_PRESS_TARGET,
13 } from 'shared/ReactSymbols';
14 import type {ReactEventTarget} from 'shared/ReactTypes';
15
@@ -19,13 +17,3 @@ export const TouchHitTarget: ReactEventTarget = {
17 $$typeof: REACT_EVENT_TARGET_TYPE,
18 type: REACT_EVENT_TARGET_TOUCH_HIT,
19 };
22 -
23 -export const FocusTarget: ReactEventTarget = {
24 - $$typeof: REACT_EVENT_TARGET_TYPE,
25 - type: REACT_EVENT_FOCUS_TARGET,
26 -};
27 -
28 -export const PressTarget: ReactEventTarget = {
29 - $$typeof: REACT_EVENT_TARGET_TYPE,
30 - type: REACT_EVENT_PRESS_TARGET,
31 -};
packages/react-events/src/__tests__/FocusScope-test.internal.js new
+117
@@ -0,0 +1,117 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +let React;
13 +let ReactFeatureFlags;
14 +let ReactDOM;
15 +let FocusScope;
16 +
17 +const createTabForward = type => {
18 + const event = new KeyboardEvent('keydown', {
19 + key: 'Tab',
20 + bubbles: true,
21 + cancelable: true,
22 + });
23 + return event;
24 +};
25 +
26 +const createTabBackward = type => {
27 + const event = new KeyboardEvent('keydown', {
28 + key: 'Tab',
29 + shiftKey: true,
30 + bubbles: true,
31 + cancelable: true,
32 + });
33 + return event;
34 +};
35 +
36 +describe('FocusScope event responder', () => {
37 + let container;
38 +
39 + beforeEach(() => {
40 + jest.resetModules();
41 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
42 + ReactFeatureFlags.enableEventAPI = true;
43 + React = require('react');
44 + ReactDOM = require('react-dom');
45 + FocusScope = require('react-events/FocusScope');
46 +
47 + container = document.createElement('div');
48 + document.body.appendChild(container);
49 + });
50 +
51 + afterEach(() => {
52 + document.body.removeChild(container);
53 + container = null;
54 + });
55 +
56 + it('when using a simple focus scope with autofocus', () => {
57 + const inputRef = React.createRef();
58 + const input2Ref = React.createRef();
59 + const buttonRef = React.createRef();
60 + const butto2nRef = React.createRef();
61 + const divRef = React.createRef();
62 +
63 + const SimpleFocusScope = () => (
64 + <div>
65 + <FocusScope autoFocus={true}>
66 + <input ref={inputRef} />
67 + <button ref={buttonRef} />
68 + <div ref={divRef} tabIndex={0} />
69 + <input ref={input2Ref} tabIndex={-1} />
70 + <button ref={butto2nRef} />
71 + </FocusScope>
72 + </div>
73 + );
74 +
75 + ReactDOM.render(<SimpleFocusScope />, container);
76 + expect(document.activeElement).toBe(inputRef.current);
77 + document.dispatchEvent(createTabForward());
78 + expect(document.activeElement).toBe(buttonRef.current);
79 + document.dispatchEvent(createTabForward());
80 + expect(document.activeElement).toBe(divRef.current);
81 + document.dispatchEvent(createTabForward());
82 + expect(document.activeElement).toBe(butto2nRef.current);
83 + document.dispatchEvent(createTabBackward());
84 + expect(document.activeElement).toBe(divRef.current);
85 + });
86 +
87 + it('when using a simple focus scope with autofocus and trapping', () => {
88 + const inputRef = React.createRef();
89 + const input2Ref = React.createRef();
90 + const buttonRef = React.createRef();
91 + const button2Ref = React.createRef();
92 +
93 + const SimpleFocusScope = () => (
94 + <div>
95 + <FocusScope autoFocus={true} trap={true}>
96 + <input ref={inputRef} tabIndex={-1} />
97 + <button ref={buttonRef} id={1} />
98 + <button ref={button2Ref} id={2} />
99 + <input ref={input2Ref} tabIndex={-1} />
100 + </FocusScope>
101 + </div>
102 + );
103 +
104 + ReactDOM.render(<SimpleFocusScope />, container);
105 + expect(document.activeElement).toBe(buttonRef.current);
106 + document.dispatchEvent(createTabForward());
107 + expect(document.activeElement).toBe(button2Ref.current);
108 + document.dispatchEvent(createTabForward());
109 + expect(document.activeElement).toBe(buttonRef.current);
110 + document.dispatchEvent(createTabForward());
111 + expect(document.activeElement).toBe(button2Ref.current);
112 + document.dispatchEvent(createTabBackward());
113 + expect(document.activeElement).toBe(buttonRef.current);
114 + document.dispatchEvent(createTabBackward());
115 + expect(document.activeElement).toBe(button2Ref.current);
116 + });
117 +});
packages/react-reconciler/src/ReactFiberCompleteWork.js
+3
@@ -812,6 +812,7 @@ function completeWork(
812 responderState = responder.createInitialState(newProps);
813 }
814 eventComponentInstance = workInProgress.stateNode = {
815 + currentFiber: workInProgress,
816 props: newProps,
817 responder,
818 rootEventTypes: null,
@@ -824,6 +825,8 @@ function completeWork(
825 eventComponentInstance.props = newProps;
826 // Update the root container, so we can properly unmount events at some point
827 eventComponentInstance.rootInstance = rootContainerInstance;
828 + // Update the current fiber
829 + eventComponentInstance.currentFiber = workInProgress;
830 updateEventComponent(eventComponentInstance);
831 }
832 }
packages/shared/ReactSymbols.js
-6
@@ -57,12 +57,6 @@ export const REACT_EVENT_TARGET_TYPE = hasSymbol
57 export const REACT_EVENT_TARGET_TOUCH_HIT = hasSymbol
58 ? Symbol.for('react.event_target.touch_hit')
59 : 0xead7;
60 -export const REACT_EVENT_FOCUS_TARGET = hasSymbol
61 - ? Symbol.for('react.event_target.focus')
62 - : 0xead8;
63 -export const REACT_EVENT_PRESS_TARGET = hasSymbol
64 - ? Symbol.for('react.event_target.press')
65 - : 0xead9;
60
61 const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
62 const FAUX_ITERATOR_SYMBOL = '@@iterator';
packages/shared/ReactTypes.js
+2 -8
@@ -126,6 +126,7 @@ export type ReactEventResponder = {
126 };
127
128 export type ReactEventComponentInstance = {|
129 + currentFiber: mixed,
130 props: null | Object,
131 responder: ReactEventResponder,
132 rootEventTypes: null | Set<string>,
@@ -189,12 +190,5 @@ export type ReactResponderContext = {
190 releaseOwnership: () => boolean,
191 setTimeout: (func: () => void, timeout: number) => Symbol,
192 clearTimeout: (timerId: Symbol) => void,
192 - getEventTargetsFromTarget: (
193 - target: Element | Document,
194 - queryType?: Symbol | number,
195 - queryKey?: string,
196 - ) => Array<{
197 - node: Element,
198 - props: null | Object,
199 - }>,
193 + getFocusableElementsInScope(): Array<HTMLElement>,
194 };
packages/shared/getComponentName.js
-6
@@ -25,8 +25,6 @@ import {
25 REACT_EVENT_COMPONENT_TYPE,
26 REACT_EVENT_TARGET_TYPE,
27 REACT_EVENT_TARGET_TOUCH_HIT,
28 - REACT_EVENT_FOCUS_TARGET,
29 - REACT_EVENT_PRESS_TARGET,
28 } from 'shared/ReactSymbols';
29 import {refineResolvedLazyComponent} from 'shared/ReactLazyComponent';
30 import type {ReactEventComponent, ReactEventTarget} from 'shared/ReactTypes';
@@ -112,10 +110,6 @@ function getComponentName(type: mixed): string | null {
110 const eventTarget = ((type: any): ReactEventTarget);
111 if (eventTarget.type === REACT_EVENT_TARGET_TOUCH_HIT) {
112 return 'TouchHitTarget';
115 - } else if (eventTarget.type === REACT_EVENT_FOCUS_TARGET) {
116 - return 'FocusTarget';
117 - } else if (eventTarget.type === REACT_EVENT_PRESS_TARGET) {
118 - return 'PressTarget';
113 }
114 const displayName = eventTarget.displayName;
115 if (displayName !== undefined) {
scripts/rollup/bundles.js
+20 -5
@@ -490,7 +490,7 @@ const bundles = [
490 FB_WWW_PROD,
491 ],
492 moduleType: NON_FIBER_RENDERER,
493 - entry: 'react-events/press',
493 + entry: 'react-events/Press',
494 global: 'ReactEventsPress',
495 externals: [],
496 },
@@ -505,7 +505,7 @@ const bundles = [
505 FB_WWW_PROD,
506 ],
507 moduleType: NON_FIBER_RENDERER,
508 - entry: 'react-events/hover',
508 + entry: 'react-events/Hover',
509 global: 'ReactEventsHover',
510 externals: [],
511 },
@@ -520,7 +520,7 @@ const bundles = [
520 FB_WWW_PROD,
521 ],
522 moduleType: NON_FIBER_RENDERER,
523 - entry: 'react-events/focus',
523 + entry: 'react-events/Focus',
524 global: 'ReactEventsFocus',
525 externals: [],
526 },
@@ -535,7 +535,22 @@ const bundles = [
535 FB_WWW_PROD,
536 ],
537 moduleType: NON_FIBER_RENDERER,
538 - entry: 'react-events/swipe',
538 + entry: 'react-events/FocusScope',
539 + global: 'ReactEventsFocusScope',
540 + externals: [],
541 + },
542 +
543 + {
544 + bundleTypes: [
545 + UMD_DEV,
546 + UMD_PROD,
547 + NODE_DEV,
548 + NODE_PROD,
549 + FB_WWW_DEV,
550 + FB_WWW_PROD,
551 + ],
552 + moduleType: NON_FIBER_RENDERER,
553 + entry: 'react-events/Swipe',
554 global: 'ReactEventsSwipe',
555 externals: [],
556 },
@@ -550,7 +565,7 @@ const bundles = [
565 FB_WWW_PROD,
566 ],
567 moduleType: NON_FIBER_RENDERER,
553 - entry: 'react-events/drag',
568 + entry: 'react-events/Drag',
569 global: 'ReactEventsDrag',
570 externals: [],
571 },