mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Event API: Add FocusScope surface (#15487)
This commit is contained in:
+62
-64
@@ -84,6 +84,8 @@ const eventListeners:
|
||||
($Shape<PartialEventObject>) => void,
|
||||
> = new PossiblyWeakMap();
|
||||
|
||||
let alreadyDispatching = false;
|
||||
|
||||
let currentTimers = new Map();
|
||||
let currentOwner = null;
|
||||
let currentInstance: null | ReactEventComponentInstance = null;
|
||||
@@ -322,61 +324,65 @@ const eventResponderContext: ReactResponderContext = {
|
||||
}
|
||||
}
|
||||
},
|
||||
getEventTargetsFromTarget(
|
||||
target: Element | Document,
|
||||
queryType?: Symbol | number,
|
||||
queryKey?: string,
|
||||
): Array<{
|
||||
node: Element,
|
||||
props: null | Object,
|
||||
}> {
|
||||
validateResponderContext();
|
||||
const eventTargetHostComponents = [];
|
||||
let node = getClosestInstanceFromNode(target);
|
||||
// We traverse up the fiber tree from the target fiber, to the
|
||||
// current event component fiber. Along the way, we check if
|
||||
// the fiber has any children that are event targets. If there
|
||||
// are, we query them (optionally) to ensure they match the
|
||||
// specified type and key. We then push the event target props
|
||||
// along with the associated parent host component of that event
|
||||
// target.
|
||||
getFocusableElementsInScope(): Array<HTMLElement> {
|
||||
const focusableElements = [];
|
||||
const eventComponentInstance = ((currentInstance: any): ReactEventComponentInstance);
|
||||
let node = ((eventComponentInstance.currentFiber: any): Fiber).child;
|
||||
|
||||
while (node !== null) {
|
||||
if (node.stateNode === currentInstance) {
|
||||
break;
|
||||
}
|
||||
let child = node.child;
|
||||
if (isFiberHostComponentFocusable(node)) {
|
||||
focusableElements.push(node.stateNode);
|
||||
} else {
|
||||
const child = node.child;
|
||||
|
||||
while (child !== null) {
|
||||
if (
|
||||
child.tag === EventTargetWorkTag &&
|
||||
queryEventTarget(child, queryType, queryKey)
|
||||
) {
|
||||
const props = child.stateNode.props;
|
||||
let parent = child.return;
|
||||
|
||||
if (parent !== null) {
|
||||
if (parent.stateNode === currentInstance) {
|
||||
break;
|
||||
}
|
||||
if (parent.tag === HostComponent) {
|
||||
eventTargetHostComponents.push({
|
||||
node: parent.stateNode,
|
||||
props,
|
||||
});
|
||||
break;
|
||||
}
|
||||
parent = parent.return;
|
||||
}
|
||||
break;
|
||||
if (child !== null) {
|
||||
node = child;
|
||||
continue;
|
||||
}
|
||||
child = child.sibling;
|
||||
}
|
||||
node = node.return;
|
||||
const sibling = node.sibling;
|
||||
|
||||
if (sibling !== null) {
|
||||
node = sibling;
|
||||
continue;
|
||||
}
|
||||
const parent = node.return;
|
||||
if (parent === null) {
|
||||
break;
|
||||
}
|
||||
node = parent.sibling;
|
||||
}
|
||||
return eventTargetHostComponents;
|
||||
|
||||
return focusableElements;
|
||||
},
|
||||
};
|
||||
|
||||
function isFiberHostComponentFocusable(fiber: Fiber): boolean {
|
||||
if (fiber.tag !== HostComponent) {
|
||||
return false;
|
||||
}
|
||||
const {type, memoizedProps} = fiber;
|
||||
if (memoizedProps.tabIndex === -1 || memoizedProps.disabled) {
|
||||
return false;
|
||||
}
|
||||
if (memoizedProps.tabIndex === 0) {
|
||||
return true;
|
||||
}
|
||||
if (type === 'a' || type === 'area') {
|
||||
return !!memoizedProps.href;
|
||||
}
|
||||
return (
|
||||
type === 'button' ||
|
||||
type === 'textarea' ||
|
||||
type === 'input' ||
|
||||
type === 'object' ||
|
||||
type === 'select'
|
||||
);
|
||||
}
|
||||
|
||||
function processTimers(timers: Map<Symbol, ResponderTimer>): void {
|
||||
const timersArr = Array.from(timers.values());
|
||||
currentEventQueue = createEventQueue();
|
||||
@@ -398,20 +404,6 @@ function processTimers(timers: Map<Symbol, ResponderTimer>): void {
|
||||
}
|
||||
}
|
||||
|
||||
function queryEventTarget(
|
||||
child: Fiber,
|
||||
queryType: void | Symbol | number,
|
||||
queryKey: void | string,
|
||||
): boolean {
|
||||
if (queryType !== undefined && child.type.type !== queryType) {
|
||||
return false;
|
||||
}
|
||||
if (queryKey !== undefined && child.key !== queryKey) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function createResponderEvent(
|
||||
topLevelType: string,
|
||||
nativeEvent: AnyNativeEvent,
|
||||
@@ -467,7 +459,7 @@ export function processEventQueue(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function getTargetEventTypes(
|
||||
function getTargetEventTypesSet(
|
||||
eventTypes: Array<ReactEventResponderEventType>,
|
||||
): Set<DOMTopLevelEventType> {
|
||||
let cachedSet = targetEventTypeCached.get(eventTypes);
|
||||
@@ -497,12 +489,13 @@ function getTargetEventResponderInstances(
|
||||
const eventComponentInstance = node.stateNode;
|
||||
if (currentOwner === null || currentOwner === eventComponentInstance) {
|
||||
const responder = eventComponentInstance.responder;
|
||||
const targetEventTypes = responder.targetEventTypes;
|
||||
// Validate the target event type exists on the responder
|
||||
const targetEventTypes = getTargetEventTypes(
|
||||
responder.targetEventTypes,
|
||||
);
|
||||
if (targetEventTypes.has(topLevelType)) {
|
||||
eventResponderInstances.push(eventComponentInstance);
|
||||
if (targetEventTypes !== undefined) {
|
||||
const targetEventTypesSet = getTargetEventTypesSet(targetEventTypes);
|
||||
if (targetEventTypesSet.has(topLevelType)) {
|
||||
eventResponderInstances.push(eventComponentInstance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -716,6 +709,10 @@ export function dispatchEventForResponderEventSystem(
|
||||
eventSystemFlags: EventSystemFlags,
|
||||
): void {
|
||||
if (enableEventAPI) {
|
||||
if (alreadyDispatching) {
|
||||
return;
|
||||
}
|
||||
alreadyDispatching = true;
|
||||
currentEventQueue = createEventQueue();
|
||||
try {
|
||||
traverseAndHandleEventResponderInstances(
|
||||
@@ -730,6 +727,7 @@ export function dispatchEventForResponderEventSystem(
|
||||
currentTimers = null;
|
||||
currentInstance = null;
|
||||
currentEventQueue = null;
|
||||
alreadyDispatching = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
let ReactDOM;
|
||||
let ReactSymbols;
|
||||
|
||||
function createReactEventComponent(
|
||||
targetEventTypes,
|
||||
@@ -57,14 +56,6 @@ function dispatchClickEvent(element) {
|
||||
dispatchEvent(element, 'click');
|
||||
}
|
||||
|
||||
function createReactEventTarget(type) {
|
||||
return {
|
||||
$$typeof: ReactSymbols.REACT_EVENT_TARGET_TYPE,
|
||||
displayName: 'TestEventTarget',
|
||||
type,
|
||||
};
|
||||
}
|
||||
|
||||
// This is a new feature in Fiber so I put it in its own test file. It could
|
||||
// probably move to one of the other test files once it is official.
|
||||
describe('DOMEventResponderSystem', () => {
|
||||
@@ -78,7 +69,6 @@ describe('DOMEventResponderSystem', () => {
|
||||
ReactDOM = require('react-dom');
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
ReactSymbols = require('shared/ReactSymbols');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -676,235 +666,6 @@ describe('DOMEventResponderSystem', () => {
|
||||
expect(onOwnershipChangeFired).toEqual(1);
|
||||
});
|
||||
|
||||
it('should be possible to get event targets', () => {
|
||||
let queryResult = null;
|
||||
const buttonRef = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
const eventTargetType = Symbol.for('react.event_target.test');
|
||||
const EventTarget = createReactEventTarget(eventTargetType);
|
||||
|
||||
const EventComponent = createReactEventComponent(
|
||||
['click'],
|
||||
undefined,
|
||||
undefined,
|
||||
(event, context, props, state) => {
|
||||
queryResult = Array.from(
|
||||
context.getEventTargetsFromTarget(event.target),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const Test = () => (
|
||||
<EventComponent>
|
||||
<div ref={divRef}>
|
||||
<EventTarget foo={1} />
|
||||
<button ref={buttonRef}>
|
||||
<EventTarget foo={2} />
|
||||
Press me!
|
||||
</button>
|
||||
</div>
|
||||
</EventComponent>
|
||||
);
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
let divElement = divRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(queryResult).toEqual([
|
||||
{
|
||||
node: buttonElement,
|
||||
props: {
|
||||
foo: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: divElement,
|
||||
props: {
|
||||
foo: 1,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should be possible to query event targets by type', () => {
|
||||
let queryResult = null;
|
||||
const buttonRef = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
const eventTargetType = Symbol.for('react.event_target.test');
|
||||
const EventTarget = createReactEventTarget(eventTargetType);
|
||||
|
||||
const eventTargetType2 = Symbol.for('react.event_target.test2');
|
||||
const EventTarget2 = createReactEventTarget(eventTargetType2);
|
||||
|
||||
const EventComponent = createReactEventComponent(
|
||||
['click'],
|
||||
undefined,
|
||||
undefined,
|
||||
(event, context, props, state) => {
|
||||
queryResult = context.getEventTargetsFromTarget(
|
||||
event.target,
|
||||
eventTargetType2,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const Test = () => (
|
||||
<EventComponent>
|
||||
<div ref={divRef}>
|
||||
<EventTarget2 foo={1} />
|
||||
<button ref={buttonRef}>
|
||||
<EventTarget foo={2} />
|
||||
Press me!
|
||||
</button>
|
||||
</div>
|
||||
</EventComponent>
|
||||
);
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
let divElement = divRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(queryResult).toEqual([
|
||||
{
|
||||
node: divElement,
|
||||
props: {
|
||||
foo: 1,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should be possible to query event targets by key', () => {
|
||||
let queryResult = null;
|
||||
const buttonRef = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
const eventTargetType = Symbol.for('react.event_target.test');
|
||||
const EventTarget = createReactEventTarget(eventTargetType);
|
||||
|
||||
const EventComponent = createReactEventComponent(
|
||||
['click'],
|
||||
undefined,
|
||||
undefined,
|
||||
(event, context, props, state) => {
|
||||
queryResult = context.getEventTargetsFromTarget(
|
||||
event.target,
|
||||
undefined,
|
||||
'a',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const Test = () => (
|
||||
<EventComponent>
|
||||
<div ref={divRef}>
|
||||
<EventTarget foo={1} />
|
||||
<button ref={buttonRef}>
|
||||
<EventTarget key="a" foo={2} />
|
||||
Press me!
|
||||
</button>
|
||||
</div>
|
||||
</EventComponent>
|
||||
);
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(queryResult).toEqual([
|
||||
{
|
||||
node: buttonElement,
|
||||
props: {
|
||||
foo: 2,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should be possible to query event targets by type and key', () => {
|
||||
let queryResult = null;
|
||||
let queryResult2 = null;
|
||||
let queryResult3 = null;
|
||||
const buttonRef = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
const eventTargetType = Symbol.for('react.event_target.test');
|
||||
const EventTarget = createReactEventTarget(eventTargetType);
|
||||
|
||||
const eventTargetType2 = Symbol.for('react.event_target.test2');
|
||||
const EventTarget2 = createReactEventTarget(eventTargetType2);
|
||||
|
||||
const EventComponent = createReactEventComponent(
|
||||
['click'],
|
||||
undefined,
|
||||
undefined,
|
||||
(event, context, props, state) => {
|
||||
queryResult = context.getEventTargetsFromTarget(
|
||||
event.target,
|
||||
eventTargetType2,
|
||||
'a',
|
||||
);
|
||||
|
||||
queryResult2 = context.getEventTargetsFromTarget(
|
||||
event.target,
|
||||
eventTargetType,
|
||||
'c',
|
||||
);
|
||||
|
||||
// Should return an empty array as this doesn't exist
|
||||
queryResult3 = context.getEventTargetsFromTarget(
|
||||
event.target,
|
||||
eventTargetType,
|
||||
'd',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const Test = () => (
|
||||
<EventComponent>
|
||||
<div ref={divRef}>
|
||||
<EventTarget2 key="a" foo={1} />
|
||||
<EventTarget2 key="b" foo={2} />
|
||||
<button ref={buttonRef}>
|
||||
<EventTarget key="c" foo={3} />
|
||||
Press me!
|
||||
</button>
|
||||
</div>
|
||||
</EventComponent>
|
||||
);
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
let divElement = divRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(queryResult).toEqual([
|
||||
{
|
||||
node: divElement,
|
||||
props: {
|
||||
foo: 1,
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(queryResult2).toEqual([
|
||||
{
|
||||
node: buttonElement,
|
||||
props: {
|
||||
foo: 3,
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(queryResult3).toEqual([]);
|
||||
});
|
||||
|
||||
it('the event responder root listeners should fire on a root click event', () => {
|
||||
let eventResponderFiredCount = 0;
|
||||
let eventLog = [];
|
||||
|
||||
+6
-1
@@ -12,6 +12,8 @@ import warning from 'shared/warning';
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
|
||||
import voidElementTags from './voidElementTags';
|
||||
import {enableEventAPI} from 'shared/ReactFeatureFlags';
|
||||
import {REACT_EVENT_TARGET_TYPE} from 'shared/ReactSymbols';
|
||||
|
||||
const HTML = '__html';
|
||||
|
||||
@@ -27,7 +29,10 @@ function assertValidProps(tag: string, props: ?Object) {
|
||||
// Note the use of `==` which checks for null or undefined.
|
||||
if (voidElementTags[tag]) {
|
||||
invariant(
|
||||
props.children == null && props.dangerouslySetInnerHTML == null,
|
||||
(props.children == null ||
|
||||
(enableEventAPI &&
|
||||
props.children.type.$$typeof === REACT_EVENT_TARGET_TYPE)) &&
|
||||
props.dangerouslySetInnerHTML == null,
|
||||
'%s is a void element tag and must neither have `children` nor ' +
|
||||
'use `dangerouslySetInnerHTML`.%s',
|
||||
tag,
|
||||
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Drag = require('./src/Drag');
|
||||
|
||||
module.exports = Drag.default || Drag;
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Focus = require('./src/Focus');
|
||||
|
||||
module.exports = Focus.default || Focus;
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const FocusScope = require('./src/FocusScope');
|
||||
|
||||
module.exports = FocusScope.default || FocusScope;
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Hover = require('./src/Hover');
|
||||
|
||||
module.exports = Hover.default || Hover;
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Press = require('./src/Press');
|
||||
|
||||
module.exports = Press.default || Press;
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Swipe = require('./src/Swipe');
|
||||
|
||||
module.exports = Swipe.default || Swipe;
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./cjs/react-events-drag.production.min.js');
|
||||
} else {
|
||||
module.exports = require('./cjs/react-events-drag.development.js');
|
||||
}
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./cjs/react-events-focus.production.min.js');
|
||||
} else {
|
||||
module.exports = require('./cjs/react-events-focus.development.js');
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./cjs/react-events-focus-scope.production.min.js');
|
||||
} else {
|
||||
module.exports = require('./cjs/react-events-focus-scope.development.js');
|
||||
}
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./cjs/react-events-hover.production.min.js');
|
||||
} else {
|
||||
module.exports = require('./cjs/react-events-hover.development.js');
|
||||
}
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./cjs/react-events-press.production.min.js');
|
||||
} else {
|
||||
module.exports = require('./cjs/react-events-press.development.js');
|
||||
}
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./cjs/react-events-swipe.production.min.js');
|
||||
} else {
|
||||
module.exports = require('./cjs/react-events-swipe.development.js');
|
||||
}
|
||||
@@ -12,11 +12,12 @@
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"press.js",
|
||||
"hover.js",
|
||||
"focus.js",
|
||||
"swipe.js",
|
||||
"drag.js",
|
||||
"Press.js",
|
||||
"Hover.js",
|
||||
"Focus.js",
|
||||
"Swipe.js",
|
||||
"Drag.js",
|
||||
"FocusScope.js",
|
||||
"index.js",
|
||||
"build-info.json",
|
||||
"cjs/",
|
||||
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* 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 {
|
||||
ReactResponderEvent,
|
||||
ReactResponderContext,
|
||||
} from 'shared/ReactTypes';
|
||||
import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
|
||||
|
||||
type FocusScopeProps = {
|
||||
autoFocus: Boolean,
|
||||
restoreFocus: Boolean,
|
||||
trap: Boolean,
|
||||
};
|
||||
|
||||
type FocusScopeState = {
|
||||
nodeToRestore: null | HTMLElement,
|
||||
currentFocusedNode: null | HTMLElement,
|
||||
};
|
||||
|
||||
const rootEventTypes = [
|
||||
{name: 'focus', passive: true, capture: true},
|
||||
{name: 'keydown', passive: false},
|
||||
];
|
||||
|
||||
function focusFirstChildEventTarget(
|
||||
context: ReactResponderContext,
|
||||
state: FocusScopeState,
|
||||
): void {
|
||||
const elements = context.getFocusableElementsInScope();
|
||||
if (elements.length > 0) {
|
||||
const firstElement = elements[0];
|
||||
firstElement.focus();
|
||||
state.currentFocusedNode = firstElement;
|
||||
}
|
||||
}
|
||||
|
||||
function focusLastChildEventTarget(
|
||||
context: ReactResponderContext,
|
||||
state: FocusScopeState,
|
||||
): void {
|
||||
const elements = context.getFocusableElementsInScope();
|
||||
const length = elements.length;
|
||||
if (elements.length > 0) {
|
||||
const lastElement = elements[length - 1];
|
||||
lastElement.focus();
|
||||
state.currentFocusedNode = lastElement;
|
||||
}
|
||||
}
|
||||
|
||||
const FocusScopeResponder = {
|
||||
rootEventTypes,
|
||||
createInitialState(): FocusScopeState {
|
||||
return {
|
||||
nodeToRestore: null,
|
||||
currentFocusedNode: null,
|
||||
};
|
||||
},
|
||||
onRootEvent(
|
||||
event: ReactResponderEvent,
|
||||
context: ReactResponderContext,
|
||||
props: FocusScopeProps,
|
||||
state: FocusScopeState,
|
||||
) {
|
||||
const {type, target, nativeEvent} = event;
|
||||
|
||||
if (type === 'focus') {
|
||||
if (context.isTargetWithinEventComponent(target)) {
|
||||
state.currentFocusedNode = ((target: any): HTMLElement);
|
||||
} else if (props.trap) {
|
||||
if (state.currentFocusedNode !== null) {
|
||||
state.currentFocusedNode.focus();
|
||||
} else {
|
||||
focusFirstChildEventTarget(context, state);
|
||||
}
|
||||
}
|
||||
} else if (type === 'keydown' && nativeEvent.key === 'Tab') {
|
||||
const currentFocusedNode = state.currentFocusedNode;
|
||||
if (currentFocusedNode !== null) {
|
||||
const {altkey, ctrlKey, metaKey, shiftKey} = (nativeEvent: any);
|
||||
// Skip if any of these keys are being pressed
|
||||
if (altkey || ctrlKey || metaKey) {
|
||||
return;
|
||||
}
|
||||
const elements = context.getFocusableElementsInScope();
|
||||
const position = elements.indexOf(currentFocusedNode);
|
||||
if (shiftKey) {
|
||||
if (position === 0) {
|
||||
if (props.trap) {
|
||||
focusLastChildEventTarget(context, state);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const previousElement = elements[position - 1];
|
||||
previousElement.focus();
|
||||
state.currentFocusedNode = previousElement;
|
||||
}
|
||||
} else {
|
||||
if (position === elements.length - 1) {
|
||||
if (props.trap) {
|
||||
focusFirstChildEventTarget(context, state);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const nextElement = elements[position + 1];
|
||||
nextElement.focus();
|
||||
state.currentFocusedNode = nextElement;
|
||||
}
|
||||
}
|
||||
((nativeEvent: any): KeyboardEvent).preventDefault();
|
||||
}
|
||||
}
|
||||
},
|
||||
onMount(
|
||||
context: ReactResponderContext,
|
||||
props: FocusScopeProps,
|
||||
state: FocusScopeState,
|
||||
) {
|
||||
if (props.restoreFocus) {
|
||||
state.nodeToRestore = document.activeElement;
|
||||
}
|
||||
if (props.autoFocus) {
|
||||
focusFirstChildEventTarget(context, state);
|
||||
}
|
||||
},
|
||||
onUnmount(
|
||||
context: ReactResponderContext,
|
||||
props: FocusScopeProps,
|
||||
state: FocusScopeState,
|
||||
) {
|
||||
if (props.restoreFocus && state.nodeToRestore !== null) {
|
||||
state.nodeToRestore.focus();
|
||||
}
|
||||
},
|
||||
onOwnershipChange(
|
||||
context: ReactResponderContext,
|
||||
props: FocusScopeProps,
|
||||
state: FocusScopeState,
|
||||
) {
|
||||
// unmountResponder(context, props, state);
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
$$typeof: REACT_EVENT_COMPONENT_TYPE,
|
||||
displayName: 'FocusScope',
|
||||
props: null,
|
||||
responder: FocusScopeResponder,
|
||||
};
|
||||
-12
@@ -10,8 +10,6 @@
|
||||
import {
|
||||
REACT_EVENT_TARGET_TYPE,
|
||||
REACT_EVENT_TARGET_TOUCH_HIT,
|
||||
REACT_EVENT_FOCUS_TARGET,
|
||||
REACT_EVENT_PRESS_TARGET,
|
||||
} from 'shared/ReactSymbols';
|
||||
import type {ReactEventTarget} from 'shared/ReactTypes';
|
||||
|
||||
@@ -19,13 +17,3 @@ export const TouchHitTarget: ReactEventTarget = {
|
||||
$$typeof: REACT_EVENT_TARGET_TYPE,
|
||||
type: REACT_EVENT_TARGET_TOUCH_HIT,
|
||||
};
|
||||
|
||||
export const FocusTarget: ReactEventTarget = {
|
||||
$$typeof: REACT_EVENT_TARGET_TYPE,
|
||||
type: REACT_EVENT_FOCUS_TARGET,
|
||||
};
|
||||
|
||||
export const PressTarget: ReactEventTarget = {
|
||||
$$typeof: REACT_EVENT_TARGET_TYPE,
|
||||
type: REACT_EVENT_PRESS_TARGET,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
let ReactDOM;
|
||||
let FocusScope;
|
||||
|
||||
const createTabForward = type => {
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Tab',
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
return event;
|
||||
};
|
||||
|
||||
const createTabBackward = type => {
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Tab',
|
||||
shiftKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
return event;
|
||||
};
|
||||
|
||||
describe('FocusScope event responder', () => {
|
||||
let container;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.enableEventAPI = true;
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
FocusScope = require('react-events/FocusScope');
|
||||
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(container);
|
||||
container = null;
|
||||
});
|
||||
|
||||
it('when using a simple focus scope with autofocus', () => {
|
||||
const inputRef = React.createRef();
|
||||
const input2Ref = React.createRef();
|
||||
const buttonRef = React.createRef();
|
||||
const butto2nRef = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
|
||||
const SimpleFocusScope = () => (
|
||||
<div>
|
||||
<FocusScope autoFocus={true}>
|
||||
<input ref={inputRef} />
|
||||
<button ref={buttonRef} />
|
||||
<div ref={divRef} tabIndex={0} />
|
||||
<input ref={input2Ref} tabIndex={-1} />
|
||||
<button ref={butto2nRef} />
|
||||
</FocusScope>
|
||||
</div>
|
||||
);
|
||||
|
||||
ReactDOM.render(<SimpleFocusScope />, container);
|
||||
expect(document.activeElement).toBe(inputRef.current);
|
||||
document.dispatchEvent(createTabForward());
|
||||
expect(document.activeElement).toBe(buttonRef.current);
|
||||
document.dispatchEvent(createTabForward());
|
||||
expect(document.activeElement).toBe(divRef.current);
|
||||
document.dispatchEvent(createTabForward());
|
||||
expect(document.activeElement).toBe(butto2nRef.current);
|
||||
document.dispatchEvent(createTabBackward());
|
||||
expect(document.activeElement).toBe(divRef.current);
|
||||
});
|
||||
|
||||
it('when using a simple focus scope with autofocus and trapping', () => {
|
||||
const inputRef = React.createRef();
|
||||
const input2Ref = React.createRef();
|
||||
const buttonRef = React.createRef();
|
||||
const button2Ref = React.createRef();
|
||||
|
||||
const SimpleFocusScope = () => (
|
||||
<div>
|
||||
<FocusScope autoFocus={true} trap={true}>
|
||||
<input ref={inputRef} tabIndex={-1} />
|
||||
<button ref={buttonRef} id={1} />
|
||||
<button ref={button2Ref} id={2} />
|
||||
<input ref={input2Ref} tabIndex={-1} />
|
||||
</FocusScope>
|
||||
</div>
|
||||
);
|
||||
|
||||
ReactDOM.render(<SimpleFocusScope />, container);
|
||||
expect(document.activeElement).toBe(buttonRef.current);
|
||||
document.dispatchEvent(createTabForward());
|
||||
expect(document.activeElement).toBe(button2Ref.current);
|
||||
document.dispatchEvent(createTabForward());
|
||||
expect(document.activeElement).toBe(buttonRef.current);
|
||||
document.dispatchEvent(createTabForward());
|
||||
expect(document.activeElement).toBe(button2Ref.current);
|
||||
document.dispatchEvent(createTabBackward());
|
||||
expect(document.activeElement).toBe(buttonRef.current);
|
||||
document.dispatchEvent(createTabBackward());
|
||||
expect(document.activeElement).toBe(button2Ref.current);
|
||||
});
|
||||
});
|
||||
@@ -812,6 +812,7 @@ function completeWork(
|
||||
responderState = responder.createInitialState(newProps);
|
||||
}
|
||||
eventComponentInstance = workInProgress.stateNode = {
|
||||
currentFiber: workInProgress,
|
||||
props: newProps,
|
||||
responder,
|
||||
rootEventTypes: null,
|
||||
@@ -824,6 +825,8 @@ function completeWork(
|
||||
eventComponentInstance.props = newProps;
|
||||
// Update the root container, so we can properly unmount events at some point
|
||||
eventComponentInstance.rootInstance = rootContainerInstance;
|
||||
// Update the current fiber
|
||||
eventComponentInstance.currentFiber = workInProgress;
|
||||
updateEventComponent(eventComponentInstance);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,12 +57,6 @@ export const REACT_EVENT_TARGET_TYPE = hasSymbol
|
||||
export const REACT_EVENT_TARGET_TOUCH_HIT = hasSymbol
|
||||
? Symbol.for('react.event_target.touch_hit')
|
||||
: 0xead7;
|
||||
export const REACT_EVENT_FOCUS_TARGET = hasSymbol
|
||||
? Symbol.for('react.event_target.focus')
|
||||
: 0xead8;
|
||||
export const REACT_EVENT_PRESS_TARGET = hasSymbol
|
||||
? Symbol.for('react.event_target.press')
|
||||
: 0xead9;
|
||||
|
||||
const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
|
||||
const FAUX_ITERATOR_SYMBOL = '@@iterator';
|
||||
|
||||
@@ -126,6 +126,7 @@ export type ReactEventResponder = {
|
||||
};
|
||||
|
||||
export type ReactEventComponentInstance = {|
|
||||
currentFiber: mixed,
|
||||
props: null | Object,
|
||||
responder: ReactEventResponder,
|
||||
rootEventTypes: null | Set<string>,
|
||||
@@ -189,12 +190,5 @@ export type ReactResponderContext = {
|
||||
releaseOwnership: () => boolean,
|
||||
setTimeout: (func: () => void, timeout: number) => Symbol,
|
||||
clearTimeout: (timerId: Symbol) => void,
|
||||
getEventTargetsFromTarget: (
|
||||
target: Element | Document,
|
||||
queryType?: Symbol | number,
|
||||
queryKey?: string,
|
||||
) => Array<{
|
||||
node: Element,
|
||||
props: null | Object,
|
||||
}>,
|
||||
getFocusableElementsInScope(): Array<HTMLElement>,
|
||||
};
|
||||
|
||||
@@ -25,8 +25,6 @@ import {
|
||||
REACT_EVENT_COMPONENT_TYPE,
|
||||
REACT_EVENT_TARGET_TYPE,
|
||||
REACT_EVENT_TARGET_TOUCH_HIT,
|
||||
REACT_EVENT_FOCUS_TARGET,
|
||||
REACT_EVENT_PRESS_TARGET,
|
||||
} from 'shared/ReactSymbols';
|
||||
import {refineResolvedLazyComponent} from 'shared/ReactLazyComponent';
|
||||
import type {ReactEventComponent, ReactEventTarget} from 'shared/ReactTypes';
|
||||
@@ -112,10 +110,6 @@ function getComponentName(type: mixed): string | null {
|
||||
const eventTarget = ((type: any): ReactEventTarget);
|
||||
if (eventTarget.type === REACT_EVENT_TARGET_TOUCH_HIT) {
|
||||
return 'TouchHitTarget';
|
||||
} else if (eventTarget.type === REACT_EVENT_FOCUS_TARGET) {
|
||||
return 'FocusTarget';
|
||||
} else if (eventTarget.type === REACT_EVENT_PRESS_TARGET) {
|
||||
return 'PressTarget';
|
||||
}
|
||||
const displayName = eventTarget.displayName;
|
||||
if (displayName !== undefined) {
|
||||
|
||||
@@ -490,7 +490,7 @@ const bundles = [
|
||||
FB_WWW_PROD,
|
||||
],
|
||||
moduleType: NON_FIBER_RENDERER,
|
||||
entry: 'react-events/press',
|
||||
entry: 'react-events/Press',
|
||||
global: 'ReactEventsPress',
|
||||
externals: [],
|
||||
},
|
||||
@@ -505,7 +505,7 @@ const bundles = [
|
||||
FB_WWW_PROD,
|
||||
],
|
||||
moduleType: NON_FIBER_RENDERER,
|
||||
entry: 'react-events/hover',
|
||||
entry: 'react-events/Hover',
|
||||
global: 'ReactEventsHover',
|
||||
externals: [],
|
||||
},
|
||||
@@ -520,7 +520,7 @@ const bundles = [
|
||||
FB_WWW_PROD,
|
||||
],
|
||||
moduleType: NON_FIBER_RENDERER,
|
||||
entry: 'react-events/focus',
|
||||
entry: 'react-events/Focus',
|
||||
global: 'ReactEventsFocus',
|
||||
externals: [],
|
||||
},
|
||||
@@ -535,7 +535,22 @@ const bundles = [
|
||||
FB_WWW_PROD,
|
||||
],
|
||||
moduleType: NON_FIBER_RENDERER,
|
||||
entry: 'react-events/swipe',
|
||||
entry: 'react-events/FocusScope',
|
||||
global: 'ReactEventsFocusScope',
|
||||
externals: [],
|
||||
},
|
||||
|
||||
{
|
||||
bundleTypes: [
|
||||
UMD_DEV,
|
||||
UMD_PROD,
|
||||
NODE_DEV,
|
||||
NODE_PROD,
|
||||
FB_WWW_DEV,
|
||||
FB_WWW_PROD,
|
||||
],
|
||||
moduleType: NON_FIBER_RENDERER,
|
||||
entry: 'react-events/Swipe',
|
||||
global: 'ReactEventsSwipe',
|
||||
externals: [],
|
||||
},
|
||||
@@ -550,7 +565,7 @@ const bundles = [
|
||||
FB_WWW_PROD,
|
||||
],
|
||||
moduleType: NON_FIBER_RENDERER,
|
||||
entry: 'react-events/drag',
|
||||
entry: 'react-events/Drag',
|
||||
global: 'ReactEventsDrag',
|
||||
externals: [],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user