Event API: ensure getFocusableElementsInScope handles suspended trees (#15651)

This commit is contained in:
Dominic Gannaway
2019-05-15 01:08:30 +01:00
committed by GitHub
parent 8af90c8972
commit b0657fde6a
3 changed files with 113 additions and 10 deletions
+62 -10
View File
@@ -16,6 +16,8 @@ import {
EventComponent,
EventTarget as EventTargetWorkTag,
HostComponent,
SuspenseComponent,
Fragment,
} from 'shared/ReactWorkTags';
import type {
ReactEventResponder,
@@ -352,15 +354,25 @@ const eventResponderContext: ReactResponderContext = {
let node = ((eventComponentInstance.currentFiber: any): Fiber).child;
while (node !== null) {
if (isFiberHostComponentFocusable(node)) {
focusableElements.push(node.stateNode);
} else {
const child = node.child;
if (child !== null) {
node = child;
if (node.tag === SuspenseComponent) {
const suspendedChild = isFiberSuspenseAndTimedOut(node)
? getSuspenseFallbackChild(node)
: getSuspenseChild(node);
if (suspendedChild !== null) {
node = suspendedChild;
continue;
}
} else {
if (isFiberHostComponentFocusable(node)) {
focusableElements.push(node.stateNode);
} else {
const child = node.child;
if (child !== null) {
node = child;
continue;
}
}
}
const sibling = node.sibling;
@@ -368,9 +380,14 @@ const eventResponderContext: ReactResponderContext = {
node = sibling;
continue;
}
const parent = node.return;
if (parent === null) {
break;
let parent;
if (isFiberSuspenseChild(node)) {
parent = getSuspenseFiberFromChild(node);
} else {
parent = node.return;
if (parent === null) {
break;
}
}
if (parent.stateNode === currentInstance) {
break;
@@ -588,6 +605,41 @@ export function processEventQueue(): void {
}
}
function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
return fiber.tag === SuspenseComponent && fiber.memoizedState !== null;
}
function isFiberSuspenseChild(fiber: Fiber | null): boolean {
if (fiber === null) {
return false;
}
const parent = fiber.return;
if (parent !== null && parent.tag === Fragment) {
const grandParent = parent.return;
if (
grandParent !== null &&
grandParent.tag === SuspenseComponent &&
grandParent.stateNode !== null
) {
return true;
}
}
return false;
}
function getSuspenseFiberFromChild(fiber: Fiber): Fiber {
return ((((fiber.return: any): Fiber).return: any): Fiber);
}
function getSuspenseFallbackChild(fiber: Fiber): Fiber | null {
return ((((fiber.child: any): Fiber).sibling: any): Fiber).child;
}
function getSuspenseChild(fiber: Fiber): Fiber | null {
return (((fiber.child: any): Fiber): Fiber).child;
}
function getTargetEventTypesSet(
eventTypes: Array<ReactEventResponderEventType>,
): Set<string> {
+1
View File
@@ -31,6 +31,7 @@ function assertValidProps(tag: string, props: ?Object) {
invariant(
(props.children == null ||
(enableEventAPI &&
props.children.type &&
props.children.type.$$typeof === REACT_EVENT_TARGET_TYPE)) &&
props.dangerouslySetInnerHTML == null,
'%s is a void element tag and must neither have `children` nor ' +
@@ -191,4 +191,54 @@ describe('FocusScope event responder', () => {
document.activeElement.dispatchEvent(createTabBackward());
expect(document.activeElement).toBe(button2Ref.current);
});
it('should work as expected with suspense fallbacks', () => {
const buttonRef = React.createRef();
const button2Ref = React.createRef();
const button3Ref = React.createRef();
const button4Ref = React.createRef();
const button5Ref = React.createRef();
function SuspendedComponent() {
throw new Promise(() => {
// Never resolve
});
}
function Component() {
return (
<React.Fragment>
<button ref={button5Ref} id={5} />
<SuspendedComponent />
</React.Fragment>
);
}
const SimpleFocusScope = () => (
<div>
<FocusScope>
<button ref={buttonRef} id={1} />
<button ref={button2Ref} id={2} />
<React.Suspense fallback={<button ref={button3Ref} id={3} />}>
<Component />
</React.Suspense>
<button ref={button4Ref} id={4} />
</FocusScope>
</div>
);
ReactDOM.render(<SimpleFocusScope />, container);
buttonRef.current.focus();
expect(document.activeElement).toBe(buttonRef.current);
document.activeElement.dispatchEvent(createTabForward());
expect(document.activeElement).toBe(button2Ref.current);
document.activeElement.dispatchEvent(createTabForward());
expect(document.activeElement).toBe(button3Ref.current);
document.activeElement.dispatchEvent(createTabForward());
expect(document.activeElement).toBe(button4Ref.current);
document.activeElement.dispatchEvent(createTabBackward());
expect(document.activeElement).toBe(button3Ref.current);
document.activeElement.dispatchEvent(createTabBackward());
expect(document.activeElement).toBe(button2Ref.current);
});
});