Skip to content

Commit

Permalink
Event API: ensure getFocusableElementsInScope handles suspended trees (
Browse files Browse the repository at this point in the history
  • Loading branch information
trueadm committed May 15, 2019
1 parent 8af90c8 commit b0657fd
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 9 deletions.
70 changes: 61 additions & 9 deletions packages/react-dom/src/events/DOMEventResponderSystem.js
Expand Up @@ -16,6 +16,8 @@ import {
EventComponent,
EventTarget as EventTargetWorkTag,
HostComponent,
SuspenseComponent,
Fragment,
} from 'shared/ReactWorkTags';
import type {
ReactEventResponder,
Expand Down Expand Up @@ -352,14 +354,24 @@ const eventResponderContext: ReactResponderContext = {
let node = ((eventComponentInstance.currentFiber: any): Fiber).child;

while (node !== null) {
if (isFiberHostComponentFocusable(node)) {
focusableElements.push(node.stateNode);
if (node.tag === SuspenseComponent) {
const suspendedChild = isFiberSuspenseAndTimedOut(node)
? getSuspenseFallbackChild(node)
: getSuspenseChild(node);
if (suspendedChild !== null) {
node = suspendedChild;
continue;
}
} else {
const child = node.child;
if (isFiberHostComponentFocusable(node)) {
focusableElements.push(node.stateNode);
} else {
const child = node.child;

if (child !== null) {
node = child;
continue;
if (child !== null) {
node = child;
continue;
}
}
}
const sibling = node.sibling;
Expand All @@ -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;
Expand Down Expand Up @@ -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> {
Expand Down
1 change: 1 addition & 0 deletions packages/react-dom/src/shared/assertValidProps.js
Expand Up @@ -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 ' +
Expand Down
50 changes: 50 additions & 0 deletions packages/react-events/src/__tests__/FocusScope-test.internal.js
Expand Up @@ -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);
});
});

0 comments on commit b0657fd

Please sign in to comment.