Skip to content

Commit

Permalink
Event API: follow up fixes for FocusScope + context changes (#15496)
Browse files Browse the repository at this point in the history
  • Loading branch information
trueadm authored Apr 25, 2019
1 parent c530639 commit d1f667a
Show file tree
Hide file tree
Showing 14 changed files with 340 additions and 144 deletions.
2 changes: 1 addition & 1 deletion packages/react-dom/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -896,8 +896,8 @@ export function mountEventComponent(
eventComponentInstance: ReactEventComponentInstance,
): void {
if (enableEventAPI) {
mountEventResponder(eventComponentInstance);
updateEventComponent(eventComponentInstance);
mountEventResponder(eventComponentInstance);
}
}

Expand Down
227 changes: 162 additions & 65 deletions packages/react-dom/src/events/DOMEventResponderSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,13 @@ const eventListeners:
($Shape<PartialEventObject>) => void,
> = new PossiblyWeakMap();

let alreadyDispatching = false;
const responderOwners: Map<
ReactEventResponder,
ReactEventComponentInstance,
> = new Map();
let globalOwner = null;

let currentTimers = new Map();
let currentOwner = null;
let currentInstance: null | ReactEventComponentInstance = null;
let currentEventQueue: null | EventQueue = null;

Expand Down Expand Up @@ -131,8 +134,9 @@ const eventResponderContext: ReactResponderContext = {
eventListeners.set(eventObject, listener);
eventQueue.events.push(eventObject);
},
isPositionWithinTouchHitTarget(doc: Document, x: number, y: number): boolean {
isPositionWithinTouchHitTarget(x: number, y: number): boolean {
validateResponderContext();
const doc = getActiveDocument();
// This isn't available in some environments (JSDOM)
if (typeof doc.elementFromPoint !== 'function') {
return false;
Expand Down Expand Up @@ -188,6 +192,27 @@ const eventResponderContext: ReactResponderContext = {
}
return false;
},
isTargetWithinEventResponderScope(target: Element | Document): boolean {
validateResponderContext();
const responder = ((currentInstance: any): ReactEventComponentInstance)
.responder;
if (target != null) {
let fiber = getClosestInstanceFromNode(target);
while (fiber !== null) {
if (fiber.stateNode === currentInstance) {
return true;
}
if (
fiber.tag === EventComponent &&
fiber.stateNode.responder === responder
) {
return false;
}
fiber = fiber.return;
}
}
return false;
},
isTargetWithinElement(
childTarget: Element | Document,
parentTarget: Element | Document,
Expand All @@ -204,12 +229,10 @@ const eventResponderContext: ReactResponderContext = {
}
return false;
},
addRootEventTypes(
doc: Document,
rootEventTypes: Array<ReactEventResponderEventType>,
): void {
addRootEventTypes(rootEventTypes: Array<ReactEventResponderEventType>): void {
validateResponderContext();
listenToResponderEventTypesImpl(rootEventTypes, doc);
const activeDocument = getActiveDocument();
listenToResponderEventTypesImpl(rootEventTypes, activeDocument);
for (let i = 0; i < rootEventTypes.length; i++) {
const rootEventType = rootEventTypes[i];
const topLevelEventType =
Expand Down Expand Up @@ -265,25 +288,38 @@ const eventResponderContext: ReactResponderContext = {
},
hasOwnership(): boolean {
validateResponderContext();
return currentOwner === currentInstance;
const responder = ((currentInstance: any): ReactEventComponentInstance)
.responder;
return (
globalOwner === currentInstance ||
responderOwners.get(responder) === currentInstance
);
},
requestOwnership(): boolean {
requestGlobalOwnership(): boolean {
validateResponderContext();
if (currentOwner !== null) {
if (globalOwner !== null) {
return false;
}
currentOwner = currentInstance;
triggerOwnershipListeners();
globalOwner = currentInstance;
triggerOwnershipListeners(null);
return true;
},
releaseOwnership(): boolean {
requestResponderOwnership(): boolean {
validateResponderContext();
if (currentOwner !== currentInstance) {
const eventComponentInstance = ((currentInstance: any): ReactEventComponentInstance);
const responder = eventComponentInstance.responder;
if (responderOwners.has(responder)) {
return false;
}
currentOwner = null;
triggerOwnershipListeners();
return false;
responderOwners.set(responder, eventComponentInstance);
triggerOwnershipListeners(responder);
return true;
},
releaseOwnership(): boolean {
validateResponderContext();
return releaseOwnershipForEventComponentInstance(
((currentInstance: any): ReactEventComponentInstance),
);
},
setTimeout(func: () => void, delay): Symbol {
validateResponderContext();
Expand Down Expand Up @@ -330,9 +366,6 @@ const eventResponderContext: ReactResponderContext = {
let node = ((eventComponentInstance.currentFiber: any): Fiber).child;

while (node !== null) {
if (node.stateNode === currentInstance) {
break;
}
if (isFiberHostComponentFocusable(node)) {
focusableElements.push(node.stateNode);
} else {
Expand All @@ -353,13 +386,44 @@ const eventResponderContext: ReactResponderContext = {
if (parent === null) {
break;
}
if (parent.stateNode === currentInstance) {
break;
}
node = parent.sibling;
}

return focusableElements;
},
getActiveDocument,
};

function getActiveDocument(): Document {
const eventComponentInstance = ((currentInstance: any): ReactEventComponentInstance);
const rootElement = ((eventComponentInstance.rootInstance: any): Element);
return rootElement.ownerDocument;
}

function releaseOwnershipForEventComponentInstance(
eventComponentInstance: ReactEventComponentInstance,
): boolean {
const responder = eventComponentInstance.responder;
let triggerOwnershipListenersWith;
if (responderOwners.get(responder) === eventComponentInstance) {
responderOwners.delete(responder);
triggerOwnershipListenersWith = responder;
}
if (globalOwner === eventComponentInstance) {
globalOwner = null;
triggerOwnershipListenersWith = null;
}
if (triggerOwnershipListenersWith !== undefined) {
triggerOwnershipListeners(triggerOwnershipListenersWith);
return true;
} else {
return false;
}
}

function isFiberHostComponentFocusable(fiber: Fiber): boolean {
if (fiber.tag !== HostComponent) {
return false;
Expand All @@ -368,18 +432,22 @@ function isFiberHostComponentFocusable(fiber: Fiber): boolean {
if (memoizedProps.tabIndex === -1 || memoizedProps.disabled) {
return false;
}
if (memoizedProps.tabIndex === 0) {
if (memoizedProps.tabIndex === 0 || memoizedProps.contentEditable === true) {
return true;
}
if (type === 'a' || type === 'area') {
return !!memoizedProps.href;
return !!memoizedProps.href && memoizedProps.rel !== 'ignore';
}
if (type === 'input') {
return memoizedProps.type !== 'hidden' && memoizedProps.type !== 'file';
}
return (
type === 'button' ||
type === 'textarea' ||
type === 'input' ||
type === 'object' ||
type === 'select'
type === 'select' ||
type === 'iframe' ||
type === 'embed'
);
}

Expand Down Expand Up @@ -487,15 +555,13 @@ function getTargetEventResponderInstances(
// Traverse up the fiber tree till we find event component fibers.
if (node.tag === EventComponent) {
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
if (targetEventTypes !== undefined) {
const targetEventTypesSet = getTargetEventTypesSet(targetEventTypes);
if (targetEventTypesSet.has(topLevelType)) {
eventResponderInstances.push(eventComponentInstance);
}
const responder = eventComponentInstance.responder;
const targetEventTypes = responder.targetEventTypes;
// Validate the target event type exists on the responder
if (targetEventTypes !== undefined) {
const targetEventTypesSet = getTargetEventTypesSet(targetEventTypes);
if (targetEventTypesSet.has(topLevelType)) {
eventResponderInstances.push(eventComponentInstance);
}
}
}
Expand All @@ -516,18 +582,35 @@ function getRootEventResponderInstances(

for (let i = 0; i < rootEventComponentInstances.length; i++) {
const rootEventComponentInstance = rootEventComponentInstances[i];

if (
currentOwner === null ||
currentOwner === rootEventComponentInstance
) {
eventResponderInstances.push(rootEventComponentInstance);
}
eventResponderInstances.push(rootEventComponentInstance);
}
}
return eventResponderInstances;
}

function shouldSkipEventComponent(
eventResponderInstance: ReactEventComponentInstance,
propagatedEventResponders: null | Set<ReactEventResponder>,
): boolean {
const responder = eventResponderInstance.responder;
if (propagatedEventResponders !== null && responder.stopLocalPropagation) {
if (propagatedEventResponders.has(responder)) {
return true;
}
propagatedEventResponders.add(responder);
}
if (globalOwner && globalOwner !== eventResponderInstance) {
return true;
}
if (
responderOwners.has(responder) &&
responderOwners.get(responder) !== eventResponderInstance
) {
return true;
}
return false;
}

function traverseAndHandleEventResponderInstances(
topLevelType: DOMTopLevelEventType,
targetFiber: null | Fiber,
Expand Down Expand Up @@ -564,14 +647,16 @@ function traverseAndHandleEventResponderInstances(
for (i = length; i-- > 0; ) {
const targetEventResponderInstance = targetEventResponderInstances[i];
const {responder, props, state} = targetEventResponderInstance;
if (responder.stopLocalPropagation) {
if (propagatedEventResponders.has(responder)) {
continue;
}
propagatedEventResponders.add(responder);
}
const eventListener = responder.onEventCapture;
if (eventListener !== undefined) {
if (
shouldSkipEventComponent(
targetEventResponderInstance,
propagatedEventResponders,
)
) {
continue;
}
currentInstance = targetEventResponderInstance;
eventListener(responderEvent, eventResponderContext, props, state);
}
Expand All @@ -582,14 +667,16 @@ function traverseAndHandleEventResponderInstances(
for (i = 0; i < length; i++) {
const targetEventResponderInstance = targetEventResponderInstances[i];
const {responder, props, state} = targetEventResponderInstance;
if (responder.stopLocalPropagation) {
if (propagatedEventResponders.has(responder)) {
continue;
}
propagatedEventResponders.add(responder);
}
const eventListener = responder.onEvent;
if (eventListener !== undefined) {
if (
shouldSkipEventComponent(
targetEventResponderInstance,
propagatedEventResponders,
)
) {
continue;
}
currentInstance = targetEventResponderInstance;
eventListener(responderEvent, eventResponderContext, props, state);
}
Expand All @@ -606,20 +693,28 @@ function traverseAndHandleEventResponderInstances(
const {responder, props, state} = rootEventResponderInstance;
const eventListener = responder.onRootEvent;
if (eventListener !== undefined) {
if (shouldSkipEventComponent(rootEventResponderInstance, null)) {
continue;
}
currentInstance = rootEventResponderInstance;
eventListener(responderEvent, eventResponderContext, props, state);
}
}
}
}

function triggerOwnershipListeners(): void {
function triggerOwnershipListeners(
limitByResponder: null | ReactEventResponder,
): void {
const listeningInstances = Array.from(ownershipChangeListeners);
const previousInstance = currentInstance;
try {
for (let i = 0; i < listeningInstances.length; i++) {
const instance = listeningInstances[i];
const {props, responder, state} = instance;
if (limitByResponder !== null && limitByResponder !== responder) {
continue;
}
currentInstance = instance;
const onOwnershipChange = responder.onOwnershipChange;
if (onOwnershipChange !== undefined) {
Expand Down Expand Up @@ -670,9 +765,12 @@ export function unmountEventResponder(
currentTimers = null;
}
}
if (currentOwner === eventComponentInstance) {
currentOwner = null;
triggerOwnershipListeners();
try {
currentEventQueue = createEventQueue();
releaseOwnershipForEventComponentInstance(eventComponentInstance);
processEventQueue();
} finally {
currentEventQueue = null;
}
if (responder.onOwnershipChange !== undefined) {
ownershipChangeListeners.delete(eventComponentInstance);
Expand Down Expand Up @@ -709,10 +807,10 @@ export function dispatchEventForResponderEventSystem(
eventSystemFlags: EventSystemFlags,
): void {
if (enableEventAPI) {
if (alreadyDispatching) {
return;
}
alreadyDispatching = true;
const previousEventQueue = currentEventQueue;
const previousInstance = currentInstance;
const previousTimers = currentTimers;
currentTimers = null;
currentEventQueue = createEventQueue();
try {
traverseAndHandleEventResponderInstances(
Expand All @@ -724,10 +822,9 @@ export function dispatchEventForResponderEventSystem(
);
processEventQueue();
} finally {
currentTimers = null;
currentInstance = null;
currentEventQueue = null;
alreadyDispatching = false;
currentTimers = previousTimers;
currentInstance = previousInstance;
currentEventQueue = previousEventQueue;
}
}
}
Expand Down
Loading

0 comments on commit d1f667a

Please sign in to comment.