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
});