Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Event API: follow up fixes for FocusScope + context changes #15496

Merged
merged 5 commits into from
Apr 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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