diff --git a/packages/react-dom/src/client/focus/ReactTabFocus.js b/packages/react-dom/src/client/focus/ReactTabFocus.js
new file mode 100644
index 00000000000..68401552611
--- /dev/null
+++ b/packages/react-dom/src/client/focus/ReactTabFocus.js
@@ -0,0 +1,188 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import type {ReactScopeMethods} from 'shared/ReactTypes';
+
+import React from 'react';
+import {TabbableScope} from './TabbableScope';
+import {useKeyboard} from 'react-events/keyboard';
+
+type TabFocusControllerProps = {
+ children: React.Node,
+ contain?: boolean,
+};
+
+type KeyboardEventType = 'keydown' | 'keyup';
+
+type KeyboardEvent = {|
+ altKey: boolean,
+ ctrlKey: boolean,
+ isComposing: boolean,
+ key: string,
+ metaKey: boolean,
+ shiftKey: boolean,
+ target: Element | Document,
+ type: KeyboardEventType,
+ timeStamp: number,
+ defaultPrevented: boolean,
+|};
+
+const {useRef} = React;
+
+function getTabbableNodes(scope: ReactScopeMethods) {
+ const tabbableNodes = scope.getScopedNodes();
+ if (tabbableNodes === null || tabbableNodes.length === 0) {
+ return [null, null, null, 0, null];
+ }
+ const firstTabbableElem = tabbableNodes[0];
+ const lastTabbableElem = tabbableNodes[tabbableNodes.length - 1];
+ const currentIndex = tabbableNodes.indexOf(document.activeElement);
+ let focusedElement = null;
+ if (currentIndex !== -1) {
+ focusedElement = tabbableNodes[currentIndex];
+ }
+ return [
+ tabbableNodes,
+ firstTabbableElem,
+ lastTabbableElem,
+ currentIndex,
+ focusedElement,
+ ];
+}
+
+export function focusFirst(scope: ReactScopeMethods): void {
+ const [, firstTabbableElem] = getTabbableNodes(scope);
+ focusElem(firstTabbableElem);
+}
+
+function focusElem(elem: null | HTMLElement): void {
+ if (elem !== null) {
+ elem.focus();
+ }
+}
+
+export function focusNext(
+ scope: ReactScopeMethods,
+ contain?: boolean,
+): boolean {
+ const [
+ tabbableNodes,
+ firstTabbableElem,
+ lastTabbableElem,
+ currentIndex,
+ focusedElement,
+ ] = getTabbableNodes(scope);
+
+ if (focusedElement === null) {
+ focusElem(firstTabbableElem);
+ } else if (focusedElement === lastTabbableElem) {
+ if (contain === true) {
+ focusElem(firstTabbableElem);
+ } else {
+ return true;
+ }
+ } else {
+ focusElem((tabbableNodes: any)[currentIndex + 1]);
+ }
+ return false;
+}
+
+export function focusPrevious(
+ scope: ReactScopeMethods,
+ contain?: boolean,
+): boolean {
+ const [
+ tabbableNodes,
+ firstTabbableElem,
+ lastTabbableElem,
+ currentIndex,
+ focusedElement,
+ ] = getTabbableNodes(scope);
+
+ if (focusedElement === null) {
+ focusElem(firstTabbableElem);
+ } else if (focusedElement === firstTabbableElem) {
+ if (contain === true) {
+ focusElem(lastTabbableElem);
+ } else {
+ return true;
+ }
+ } else {
+ focusElem((tabbableNodes: any)[currentIndex - 1]);
+ }
+ return false;
+}
+
+export function getNextController(
+ scope: ReactScopeMethods,
+): null | ReactScopeMethods {
+ const allScopes = scope.getChildrenFromRoot();
+ if (allScopes === null) {
+ return null;
+ }
+ const currentScopeIndex = allScopes.indexOf(scope);
+ if (currentScopeIndex === -1 || currentScopeIndex === allScopes.length - 1) {
+ return null;
+ }
+ return allScopes[currentScopeIndex + 1];
+}
+
+export function getPreviousController(
+ scope: ReactScopeMethods,
+): null | ReactScopeMethods {
+ const allScopes = scope.getChildrenFromRoot();
+ if (allScopes === null) {
+ return null;
+ }
+ const currentScopeIndex = allScopes.indexOf(scope);
+ if (currentScopeIndex <= 0) {
+ return null;
+ }
+ return allScopes[currentScopeIndex - 1];
+}
+
+export const TabFocusController = React.forwardRef(
+ ({children, contain}: TabFocusControllerProps, ref): React.Node => {
+ const scopeRef = useRef(null);
+ const keyboard = useKeyboard({
+ onKeyDown(event: KeyboardEvent): boolean {
+ if (event.key !== 'Tab') {
+ return true;
+ }
+ const scope = scopeRef.current;
+ if (scope !== null) {
+ if (event.shiftKey) {
+ return focusPrevious(scope, contain);
+ } else {
+ return focusNext(scope, contain);
+ }
+ }
+ return true;
+ },
+ preventKeys: ['Tab', ['Tab', {shiftKey: true}]],
+ });
+
+ return (
+ {
+ if (ref) {
+ if (typeof ref === 'function') {
+ ref(node);
+ } else {
+ ref.current = node;
+ }
+ }
+ scopeRef.current = node;
+ }}
+ listeners={keyboard}>
+ {children}
+
+ );
+ },
+);
diff --git a/packages/react-dom/src/client/focus/TabFocusController.js b/packages/react-dom/src/client/focus/TabFocusController.js
deleted file mode 100644
index fbf941fcefb..00000000000
--- a/packages/react-dom/src/client/focus/TabFocusController.js
+++ /dev/null
@@ -1,179 +0,0 @@
-/**
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- *
- * @flow
- */
-
-import React from 'react';
-import {TabbableScope} from './TabbableScope';
-import {useKeyboard} from 'react-events/keyboard';
-
-type TabFocusControllerProps = {
- children: React.Node,
- contain?: boolean,
-};
-
-type KeyboardEventType = 'keydown' | 'keyup';
-
-type KeyboardEvent = {|
- altKey: boolean,
- ctrlKey: boolean,
- isComposing: boolean,
- key: string,
- metaKey: boolean,
- shiftKey: boolean,
- target: Element | Document,
- type: KeyboardEventType,
- timeStamp: number,
- defaultPrevented: boolean,
-|};
-
-type ControllerHandle = {|
- focusFirst: () => void,
- focusNext: () => boolean,
- focusPrevious: () => boolean,
- getNextController: () => null | ControllerHandle,
- getPreviousController: () => null | ControllerHandle,
-|};
-
-const {useImperativeHandle, useRef} = React;
-
-function getTabbableNodes(scopeRef) {
- const tabbableScope = scopeRef.current;
- const tabbableNodes = tabbableScope.getScopedNodes();
- const firstTabbableElem = tabbableNodes[0];
- const lastTabbableElem = tabbableNodes[tabbableNodes.length - 1];
- const currentIndex = tabbableNodes.indexOf(document.activeElement);
- let focusedElement = null;
- if (currentIndex !== -1) {
- focusedElement = tabbableNodes[currentIndex];
- }
- return [
- tabbableNodes,
- firstTabbableElem,
- lastTabbableElem,
- currentIndex,
- focusedElement,
- ];
-}
-
-export const TabFocusController = React.forwardRef(
- ({children, contain}: TabFocusControllerProps, ref): React.Node => {
- const scopeRef = useRef(null);
- const keyboard = useKeyboard({
- onKeyDown(event: KeyboardEvent): boolean {
- if (event.key !== 'Tab') {
- return true;
- }
- if (event.shiftKey) {
- return focusPrevious();
- } else {
- return focusNext();
- }
- },
- preventKeys: ['Tab', ['Tab', {shiftKey: true}]],
- });
-
- function focusFirst(): void {
- const [, firstTabbableElem] = getTabbableNodes(scopeRef);
- firstTabbableElem.focus();
- }
-
- function focusNext(): boolean {
- const [
- tabbableNodes,
- firstTabbableElem,
- lastTabbableElem,
- currentIndex,
- focusedElement,
- ] = getTabbableNodes(scopeRef);
-
- if (focusedElement === null) {
- firstTabbableElem.focus();
- } else if (focusedElement === lastTabbableElem) {
- if (contain === true) {
- firstTabbableElem.focus();
- } else {
- return true;
- }
- } else {
- tabbableNodes[currentIndex + 1].focus();
- }
- return false;
- }
-
- function focusPrevious(): boolean {
- const [
- tabbableNodes,
- firstTabbableElem,
- lastTabbableElem,
- currentIndex,
- focusedElement,
- ] = getTabbableNodes(scopeRef);
-
- if (focusedElement === null) {
- firstTabbableElem.focus();
- } else if (focusedElement === firstTabbableElem) {
- if (contain === true) {
- lastTabbableElem.focus();
- } else {
- return true;
- }
- } else {
- tabbableNodes[currentIndex - 1].focus();
- }
- return false;
- }
-
- function getPreviousController(): null | ControllerHandle {
- const tabbableScope = scopeRef.current;
- const allScopes = tabbableScope.getChildrenFromRoot();
- if (allScopes === null) {
- return null;
- }
- const currentScopeIndex = allScopes.indexOf(tabbableScope);
- if (currentScopeIndex <= 0) {
- return null;
- }
- return allScopes[currentScopeIndex - 1].getHandle();
- }
-
- function getNextController(): null | ControllerHandle {
- const tabbableScope = scopeRef.current;
- const allScopes = tabbableScope.getChildrenFromRoot();
- if (allScopes === null) {
- return null;
- }
- const currentScopeIndex = allScopes.indexOf(tabbableScope);
- if (
- currentScopeIndex === -1 ||
- currentScopeIndex === allScopes.length - 1
- ) {
- return null;
- }
- return allScopes[currentScopeIndex + 1].getHandle();
- }
-
- const controllerHandle: ControllerHandle = {
- focusFirst,
- focusNext,
- focusPrevious,
- getNextController,
- getPreviousController,
- };
-
- useImperativeHandle(ref, () => controllerHandle);
-
- return (
-
- {children}
-
- );
- },
-);
diff --git a/packages/react-dom/src/client/focus/__tests__/TabFocusController-test.internal.js b/packages/react-dom/src/client/focus/__tests__/TabFocusController-test.internal.js
index 71cc56301f5..b9846f40541 100644
--- a/packages/react-dom/src/client/focus/__tests__/TabFocusController-test.internal.js
+++ b/packages/react-dom/src/client/focus/__tests__/TabFocusController-test.internal.js
@@ -12,6 +12,7 @@ import {createEventTarget} from 'react-events/src/dom/testing-library';
let React;
let ReactFeatureFlags;
let TabFocusController;
+let ReactTabFocus;
describe('TabFocusController', () => {
beforeEach(() => {
@@ -19,7 +20,8 @@ describe('TabFocusController', () => {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableScopeAPI = true;
ReactFeatureFlags.enableFlareAPI = true;
- TabFocusController = require('../TabFocusController').TabFocusController;
+ ReactTabFocus = require('../ReactTabFocus');
+ TabFocusController = ReactTabFocus.TabFocusController;
React = require('react');
});
@@ -242,21 +244,25 @@ describe('TabFocusController', () => {
const firstFocusController = firstFocusControllerRef.current;
const secondFocusController = secondFocusControllerRef.current;
- firstFocusController.focusFirst();
+ ReactTabFocus.focusFirst(firstFocusController);
expect(document.activeElement).toBe(buttonRef.current);
- firstFocusController.focusNext();
+ ReactTabFocus.focusNext(firstFocusController);
expect(document.activeElement).toBe(button2Ref.current);
- firstFocusController.focusPrevious();
+ ReactTabFocus.focusPrevious(firstFocusController);
expect(document.activeElement).toBe(buttonRef.current);
- const nextController = firstFocusController.getNextController();
+ const nextController = ReactTabFocus.getNextController(
+ firstFocusController,
+ );
expect(nextController).toBe(secondFocusController);
- nextController.focusNext();
+ ReactTabFocus.focusNext(nextController);
expect(document.activeElement).toBe(divRef.current);
- const previousController = nextController.getPreviousController();
+ const previousController = ReactTabFocus.getPreviousController(
+ nextController,
+ );
expect(previousController).toBe(firstFocusController);
- previousController.focusNext();
+ ReactTabFocus.focusNext(previousController);
expect(document.activeElement).toBe(buttonRef.current);
});
});
diff --git a/packages/react-reconciler/src/ReactFiberScope.js b/packages/react-reconciler/src/ReactFiberScope.js
index bdbae5e697b..64c08df649c 100644
--- a/packages/react-reconciler/src/ReactFiberScope.js
+++ b/packages/react-reconciler/src/ReactFiberScope.js
@@ -133,10 +133,6 @@ export function createScopeMethods(
collectNearestChildScopeMethods(node.child, scope, childrenScopes);
return childrenScopes.length === 0 ? null : childrenScopes;
},
- getHandle(): null | mixed {
- const currentFiber = ((instance.fiber: any): Fiber);
- return currentFiber.memoizedProps.handle || null;
- },
getParent(): null | ReactScopeMethods {
let node = ((instance.fiber: any): Fiber).return;
while (node !== null) {
diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js
index ddc713980ca..cd60a570c64 100644
--- a/packages/shared/ReactTypes.js
+++ b/packages/shared/ReactTypes.js
@@ -167,7 +167,6 @@ export type ReactScope = {|
export type ReactScopeMethods = {|
getChildren(): null | Array,
getChildrenFromRoot(): null | Array,
- getHandle(): null | mixed,
getParent(): null | ReactScopeMethods,
getScopedNodes(): null | Array