@samitouri / QOS-React-2 / commits / b0657fde6a

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

Dominic Gannaway committed May 15, 2019 at 01:08 UTC b0657fde6aef2e067e6d7808809deb194a857278
3 files changed +112 -9
packages/react-dom/src/events/DOMEventResponderSystem.js
+61 -9
@@ -16,6 +16,8 @@ import {
16 EventComponent,
17 EventTarget as EventTargetWorkTag,
18 HostComponent,
19 + SuspenseComponent,
20 + Fragment,
21 } from 'shared/ReactWorkTags';
22 import type {
23 ReactEventResponder,
@@ -352,14 +354,24 @@ const eventResponderContext: ReactResponderContext = {
354 let node = ((eventComponentInstance.currentFiber: any): Fiber).child;
355
356 while (node !== null) {
355 - if (isFiberHostComponentFocusable(node)) {
356 - focusableElements.push(node.stateNode);
357 + if (node.tag === SuspenseComponent) {
358 + const suspendedChild = isFiberSuspenseAndTimedOut(node)
359 + ? getSuspenseFallbackChild(node)
360 + : getSuspenseChild(node);
361 + if (suspendedChild !== null) {
362 + node = suspendedChild;
363 + continue;
364 + }
365 } else {
358 - const child = node.child;
366 + if (isFiberHostComponentFocusable(node)) {
367 + focusableElements.push(node.stateNode);
368 + } else {
369 + const child = node.child;
370
360 - if (child !== null) {
361 - node = child;
362 - continue;
371 + if (child !== null) {
372 + node = child;
373 + continue;
374 + }
375 }
376 }
377 const sibling = node.sibling;
@@ -368,9 +380,14 @@ const eventResponderContext: ReactResponderContext = {
380 node = sibling;
381 continue;
382 }
371 - const parent = node.return;
372 - if (parent === null) {
373 - break;
383 + let parent;
384 + if (isFiberSuspenseChild(node)) {
385 + parent = getSuspenseFiberFromChild(node);
386 + } else {
387 + parent = node.return;
388 + if (parent === null) {
389 + break;
390 + }
391 }
392 if (parent.stateNode === currentInstance) {
393 break;
@@ -588,6 +605,41 @@ export function processEventQueue(): void {
605 }
606 }
607
608 +function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
609 + return fiber.tag === SuspenseComponent && fiber.memoizedState !== null;
610 +}
611 +
612 +function isFiberSuspenseChild(fiber: Fiber | null): boolean {
613 + if (fiber === null) {
614 + return false;
615 + }
616 + const parent = fiber.return;
617 + if (parent !== null && parent.tag === Fragment) {
618 + const grandParent = parent.return;
619 +
620 + if (
621 + grandParent !== null &&
622 + grandParent.tag === SuspenseComponent &&
623 + grandParent.stateNode !== null
624 + ) {
625 + return true;
626 + }
627 + }
628 + return false;
629 +}
630 +
631 +function getSuspenseFiberFromChild(fiber: Fiber): Fiber {
632 + return ((((fiber.return: any): Fiber).return: any): Fiber);
633 +}
634 +
635 +function getSuspenseFallbackChild(fiber: Fiber): Fiber | null {
636 + return ((((fiber.child: any): Fiber).sibling: any): Fiber).child;
637 +}
638 +
639 +function getSuspenseChild(fiber: Fiber): Fiber | null {
640 + return (((fiber.child: any): Fiber): Fiber).child;
641 +}
642 +
643 function getTargetEventTypesSet(
644 eventTypes: Array<ReactEventResponderEventType>,
645 ): Set<string> {
packages/react-dom/src/shared/assertValidProps.js
+1
@@ -31,6 +31,7 @@ function assertValidProps(tag: string, props: ?Object) {
31 invariant(
32 (props.children == null ||
33 (enableEventAPI &&
34 + props.children.type &&
35 props.children.type.$$typeof === REACT_EVENT_TARGET_TYPE)) &&
36 props.dangerouslySetInnerHTML == null,
37 '%s is a void element tag and must neither have `children` nor ' +
packages/react-events/src/__tests__/FocusScope-test.internal.js
+50
@@ -191,4 +191,54 @@ describe('FocusScope event responder', () => {
191 document.activeElement.dispatchEvent(createTabBackward());
192 expect(document.activeElement).toBe(button2Ref.current);
193 });
194 +
195 + it('should work as expected with suspense fallbacks', () => {
196 + const buttonRef = React.createRef();
197 + const button2Ref = React.createRef();
198 + const button3Ref = React.createRef();
199 + const button4Ref = React.createRef();
200 + const button5Ref = React.createRef();
201 +
202 + function SuspendedComponent() {
203 + throw new Promise(() => {
204 + // Never resolve
205 + });
206 + }
207 +
208 + function Component() {
209 + return (
210 + <React.Fragment>
211 + <button ref={button5Ref} id={5} />
212 + <SuspendedComponent />
213 + </React.Fragment>
214 + );
215 + }
216 +
217 + const SimpleFocusScope = () => (
218 + <div>
219 + <FocusScope>
220 + <button ref={buttonRef} id={1} />
221 + <button ref={button2Ref} id={2} />
222 + <React.Suspense fallback={<button ref={button3Ref} id={3} />}>
223 + <Component />
224 + </React.Suspense>
225 + <button ref={button4Ref} id={4} />
226 + </FocusScope>
227 + </div>
228 + );
229 +
230 + ReactDOM.render(<SimpleFocusScope />, container);
231 + buttonRef.current.focus();
232 + expect(document.activeElement).toBe(buttonRef.current);
233 + document.activeElement.dispatchEvent(createTabForward());
234 + expect(document.activeElement).toBe(button2Ref.current);
235 + document.activeElement.dispatchEvent(createTabForward());
236 + expect(document.activeElement).toBe(button3Ref.current);
237 + document.activeElement.dispatchEvent(createTabForward());
238 + expect(document.activeElement).toBe(button4Ref.current);
239 + document.activeElement.dispatchEvent(createTabBackward());
240 + expect(document.activeElement).toBe(button3Ref.current);
241 + document.activeElement.dispatchEvent(createTabBackward());
242 + expect(document.activeElement).toBe(button2Ref.current);
243 + });
244 });