diff --git a/packages/react-dom/src/events/DOMEventResponderSystem.js b/packages/react-dom/src/events/DOMEventResponderSystem.js index 9a87f147378f0..9aed034d03e9f 100644 --- a/packages/react-dom/src/events/DOMEventResponderSystem.js +++ b/packages/react-dom/src/events/DOMEventResponderSystem.js @@ -84,6 +84,8 @@ const eventListeners: ($Shape) => 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 { + 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; - - 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 (isFiberHostComponentFocusable(node)) { + focusableElements.push(node.stateNode); + } else { + const child = node.child; + + 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): void { const timersArr = Array.from(timers.values()); currentEventQueue = createEventQueue(); @@ -398,20 +404,6 @@ function processTimers(timers: Map): 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, ): Set { 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; } } } diff --git a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js index f85f003f1f241..dfb20db7a002d 100644 --- a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js @@ -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 = () => ( - -
- - -
-
- ); - - ReactDOM.render(, 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 = () => ( - -
- - -
-
- ); - - ReactDOM.render(, 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 = () => ( - -
- - -
-
- ); - - ReactDOM.render(, 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 = () => ( - -
- - - -
-
- ); - - ReactDOM.render(, 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 = []; diff --git a/packages/react-dom/src/shared/assertValidProps.js b/packages/react-dom/src/shared/assertValidProps.js index 9fe255b58dac7..4c6d9eb2f04d3 100644 --- a/packages/react-dom/src/shared/assertValidProps.js +++ b/packages/react-dom/src/shared/assertValidProps.js @@ -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, diff --git a/packages/react-events/Drag.js b/packages/react-events/Drag.js new file mode 100644 index 0000000000000..f8148f1273cbf --- /dev/null +++ b/packages/react-events/Drag.js @@ -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; diff --git a/packages/react-events/Focus.js b/packages/react-events/Focus.js new file mode 100644 index 0000000000000..0b9288a790fef --- /dev/null +++ b/packages/react-events/Focus.js @@ -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; diff --git a/packages/react-events/FocusScope.js b/packages/react-events/FocusScope.js new file mode 100644 index 0000000000000..2ff785ef79266 --- /dev/null +++ b/packages/react-events/FocusScope.js @@ -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; diff --git a/packages/react-events/Hover.js b/packages/react-events/Hover.js new file mode 100644 index 0000000000000..a53675ca5c1ab --- /dev/null +++ b/packages/react-events/Hover.js @@ -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; diff --git a/packages/react-events/Press.js b/packages/react-events/Press.js new file mode 100644 index 0000000000000..2add5ba8ed9d5 --- /dev/null +++ b/packages/react-events/Press.js @@ -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; diff --git a/packages/react-events/Swipe.js b/packages/react-events/Swipe.js new file mode 100644 index 0000000000000..3c2ad195ef4b2 --- /dev/null +++ b/packages/react-events/Swipe.js @@ -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; diff --git a/packages/react-events/npm/Drag.js b/packages/react-events/npm/Drag.js new file mode 100644 index 0000000000000..4b8838b9658c8 --- /dev/null +++ b/packages/react-events/npm/Drag.js @@ -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'); +} diff --git a/packages/react-events/npm/Focus.js b/packages/react-events/npm/Focus.js new file mode 100644 index 0000000000000..06b656a761ed3 --- /dev/null +++ b/packages/react-events/npm/Focus.js @@ -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'); +} diff --git a/packages/react-events/npm/FocusScope.js b/packages/react-events/npm/FocusScope.js new file mode 100644 index 0000000000000..0608f1826aa62 --- /dev/null +++ b/packages/react-events/npm/FocusScope.js @@ -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'); +} diff --git a/packages/react-events/npm/Hover.js b/packages/react-events/npm/Hover.js new file mode 100644 index 0000000000000..1000d87449067 --- /dev/null +++ b/packages/react-events/npm/Hover.js @@ -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'); +} diff --git a/packages/react-events/npm/Press.js b/packages/react-events/npm/Press.js new file mode 100644 index 0000000000000..deaba326bba07 --- /dev/null +++ b/packages/react-events/npm/Press.js @@ -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'); +} diff --git a/packages/react-events/npm/Swipe.js b/packages/react-events/npm/Swipe.js new file mode 100644 index 0000000000000..aa2b1f2fe13f9 --- /dev/null +++ b/packages/react-events/npm/Swipe.js @@ -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'); +} diff --git a/packages/react-events/package.json b/packages/react-events/package.json index e293b88fed26b..a5be8d74fdf44 100644 --- a/packages/react-events/package.json +++ b/packages/react-events/package.json @@ -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/", diff --git a/packages/react-events/src/FocusScope.js b/packages/react-events/src/FocusScope.js new file mode 100644 index 0000000000000..e5b05cb1bcb59 --- /dev/null +++ b/packages/react-events/src/FocusScope.js @@ -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, +}; diff --git a/packages/react-events/src/ReactEvents.js b/packages/react-events/src/ReactEvents.js index 0230b9cdb90ca..af5c08b0b8e95 100644 --- a/packages/react-events/src/ReactEvents.js +++ b/packages/react-events/src/ReactEvents.js @@ -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, -}; diff --git a/packages/react-events/src/__tests__/FocusScope-test.internal.js b/packages/react-events/src/__tests__/FocusScope-test.internal.js new file mode 100644 index 0000000000000..665cd11c3f539 --- /dev/null +++ b/packages/react-events/src/__tests__/FocusScope-test.internal.js @@ -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 = () => ( +
+ + +
+ ); + + ReactDOM.render(, 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 = () => ( +
+ + +
+ ); + + ReactDOM.render(, 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); + }); +}); diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 9bf288fc63ee2..eb10a0f14c045 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -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); } } diff --git a/packages/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js index 152e360773da0..cde9f89c5b463 100644 --- a/packages/shared/ReactSymbols.js +++ b/packages/shared/ReactSymbols.js @@ -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'; diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 7b1177ada6d23..f5026e7ba1eb1 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -126,6 +126,7 @@ export type ReactEventResponder = { }; export type ReactEventComponentInstance = {| + currentFiber: mixed, props: null | Object, responder: ReactEventResponder, rootEventTypes: null | Set, @@ -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, }; diff --git a/packages/shared/getComponentName.js b/packages/shared/getComponentName.js index 5b09f0c69bcb1..32f174e2b4e6b 100644 --- a/packages/shared/getComponentName.js +++ b/packages/shared/getComponentName.js @@ -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) { diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 47005d3c81338..fddd076889fda 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -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: [], },