Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 63 additions & 65 deletions packages/react-dom/src/events/DOMEventResponderSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

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'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list is missing a few elements: iframe, embed and contenteditable.
Should this component skip subtrees that have the inert attribute set on the root?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can deal with inert in a future PR. It's a bit more involved as what if a parent tree has inert and its sub-trees have the FocusScope?

Copy link
Contributor

@giuseppeg giuseppeg Apr 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that is not straightforward, I guess that FocusScope should noop when an ancestor has the inert attribute i.e. (force) disable trap and restore.

I am afraid that, because of portals, React needs to polyfill inert and can't rely on the native implementation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be pretty great if React polyfilled inert anyway, as it is only supported in Chrome behind a flag.

Copy link
Contributor

@necolas necolas Apr 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WICG have a polyfill https://github.com/WICG/inert. There doesn't seem to be vendor agreement on whether to proceed with this attribute or use a different approach. I don't think we need to be too concerned about it here at this stage

);
}

function processTimers(timers: Map<Symbol, ResponderTimer>): void {
const timersArr = Array.from(timers.values());
currentEventQueue = createEventQueue();
Expand All @@ -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,
Expand Down Expand Up @@ -467,7 +459,7 @@ export function processEventQueue(): void {
}
}

function getTargetEventTypes(
function getTargetEventTypesSet(
eventTypes: Array<ReactEventResponderEventType>,
): Set<DOMTopLevelEventType> {
let cachedSet = targetEventTypeCached.get(eventTypes);
Expand Down Expand Up @@ -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);
}
}
}
}
Expand Down Expand Up @@ -716,6 +709,10 @@ export function dispatchEventForResponderEventSystem(
eventSystemFlags: EventSystemFlags,
): void {
if (enableEventAPI) {
if (alreadyDispatching) {
return;
}
alreadyDispatching = true;
currentEventQueue = createEventQueue();
try {
traverseAndHandleEventResponderInstances(
Expand All @@ -730,6 +727,7 @@ export function dispatchEventForResponderEventSystem(
currentTimers = null;
currentInstance = null;
currentEventQueue = null;
alreadyDispatching = false;
}
}
}
Expand Down
Loading