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