|
16 | 16 |
|
17 | 17 | import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; |
18 | 18 | import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue'; |
19 | | -import type {ReactNodeList} from 'shared/ReactTypes'; |
| 19 | +import type {ReactNodeList, OffscreenMode} from 'shared/ReactTypes'; |
20 | 20 | import type {RootTag} from 'react-reconciler/src/ReactRootTags'; |
21 | 21 |
|
22 | 22 | import * as Scheduler from 'scheduler/unstable_mock'; |
@@ -258,6 +258,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { |
258 | 258 | type: string, |
259 | 259 | rootcontainerInstance: Container, |
260 | 260 | ) { |
| 261 | + if (type === 'offscreen') { |
| 262 | + return parentHostContext; |
| 263 | + } |
261 | 264 | if (type === 'uppercase') { |
262 | 265 | return UPPERCASE_CONTEXT; |
263 | 266 | } |
@@ -539,47 +542,18 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { |
539 | 542 | container.children = newChildren; |
540 | 543 | }, |
541 | 544 |
|
542 | | - cloneHiddenInstance( |
543 | | - instance: Instance, |
544 | | - type: string, |
545 | | - props: Props, |
546 | | - internalInstanceHandle: Object, |
547 | | - ): Instance { |
548 | | - const clone = cloneInstance( |
549 | | - instance, |
550 | | - null, |
551 | | - type, |
552 | | - props, |
553 | | - props, |
554 | | - internalInstanceHandle, |
555 | | - true, |
556 | | - null, |
557 | | - ); |
558 | | - clone.hidden = true; |
559 | | - return clone; |
| 545 | + getOffscreenContainerType(): string { |
| 546 | + return 'offscreen'; |
560 | 547 | }, |
561 | 548 |
|
562 | | - cloneHiddenTextInstance( |
563 | | - instance: TextInstance, |
564 | | - text: string, |
565 | | - internalInstanceHandle: Object, |
566 | | - ): TextInstance { |
567 | | - const clone = { |
568 | | - text: instance.text, |
569 | | - id: instanceCounter++, |
570 | | - hidden: true, |
571 | | - context: instance.context, |
| 549 | + getOffscreenContainerProps( |
| 550 | + mode: OffscreenMode, |
| 551 | + children: ReactNodeList, |
| 552 | + ): Props { |
| 553 | + return { |
| 554 | + hidden: mode === 'hidden', |
| 555 | + children, |
572 | 556 | }; |
573 | | - // Hide from unit tests |
574 | | - Object.defineProperty(clone, 'id', { |
575 | | - value: clone.id, |
576 | | - enumerable: false, |
577 | | - }); |
578 | | - Object.defineProperty(clone, 'context', { |
579 | | - value: clone.context, |
580 | | - enumerable: false, |
581 | | - }); |
582 | | - return clone; |
583 | 557 | }, |
584 | 558 | }; |
585 | 559 |
|
@@ -646,20 +620,164 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { |
646 | 620 |
|
647 | 621 | function getChildren(root) { |
648 | 622 | if (root) { |
649 | | - return root.children; |
| 623 | + return useMutation |
| 624 | + ? root.children |
| 625 | + : removeOffscreenContainersFromChildren(root.children, false); |
650 | 626 | } else { |
651 | 627 | return null; |
652 | 628 | } |
653 | 629 | } |
654 | 630 |
|
655 | 631 | function getPendingChildren(root) { |
656 | 632 | if (root) { |
657 | | - return root.pendingChildren; |
| 633 | + return useMutation |
| 634 | + ? root.children |
| 635 | + : removeOffscreenContainersFromChildren(root.pendingChildren, false); |
658 | 636 | } else { |
659 | 637 | return null; |
660 | 638 | } |
661 | 639 | } |
662 | 640 |
|
| 641 | + function removeOffscreenContainersFromChildren(children, hideNearestNode) { |
| 642 | + // Mutation mode and persistent mode have different outputs for Offscreen |
| 643 | + // and Suspense trees. Persistent mode adds an additional host node wrapper, |
| 644 | + // whereas mutation mode does not. |
| 645 | + // |
| 646 | + // This function removes the offscreen host wrappers so that the output is |
| 647 | + // consistent. If the offscreen node is hidden, it transfers the hiddenness |
| 648 | + // to the child nodes, to mimic how it works in mutation mode. That way our |
| 649 | + // tests don't have to fork tree assertions. |
| 650 | + // |
| 651 | + // So, it takes a tree that looks like this: |
| 652 | + // |
| 653 | + // <offscreen hidden={true}> |
| 654 | + // <span>A</span> |
| 655 | + // <span>B</span> |
| 656 | + // </offscren> |
| 657 | + // |
| 658 | + // And turns it into this: |
| 659 | + // |
| 660 | + // <span hidden={true}>A</span> |
| 661 | + // <span hidden={true}>B</span> |
| 662 | + // |
| 663 | + // We don't mutate the original tree, but instead return a copy. |
| 664 | + // |
| 665 | + // This function is only used by our test assertions, via the `getChildren` |
| 666 | + // and `getChildrenAsJSX` methods. |
| 667 | + let didClone = false; |
| 668 | + const newChildren = []; |
| 669 | + for (let i = 0; i < children.length; i++) { |
| 670 | + const child = children[i]; |
| 671 | + const innerChildren = child.children; |
| 672 | + if (innerChildren !== undefined) { |
| 673 | + // This is a host instance instance |
| 674 | + const instance: Instance = (child: any); |
| 675 | + if (instance.type === 'offscreen') { |
| 676 | + // This is an offscreen wrapper instance. Remove it from the tree |
| 677 | + // and recursively return its children, as if it were a fragment. |
| 678 | + didClone = true; |
| 679 | + if (instance.text !== null) { |
| 680 | + // If this offscreen tree contains only text, we replace it with |
| 681 | + // a text child. Related to `shouldReplaceTextContent` feature. |
| 682 | + const offscreenTextInstance: TextInstance = { |
| 683 | + text: instance.text, |
| 684 | + id: instanceCounter++, |
| 685 | + hidden: hideNearestNode || instance.hidden, |
| 686 | + context: instance.context, |
| 687 | + }; |
| 688 | + // Hide from unit tests |
| 689 | + Object.defineProperty(offscreenTextInstance, 'id', { |
| 690 | + value: offscreenTextInstance.id, |
| 691 | + enumerable: false, |
| 692 | + }); |
| 693 | + Object.defineProperty(offscreenTextInstance, 'context', { |
| 694 | + value: offscreenTextInstance.context, |
| 695 | + enumerable: false, |
| 696 | + }); |
| 697 | + newChildren.push(offscreenTextInstance); |
| 698 | + } else { |
| 699 | + // Skip the offscreen node and replace it with its children |
| 700 | + const offscreenChildren = removeOffscreenContainersFromChildren( |
| 701 | + innerChildren, |
| 702 | + hideNearestNode || instance.hidden, |
| 703 | + ); |
| 704 | + newChildren.push.apply(newChildren, offscreenChildren); |
| 705 | + } |
| 706 | + } else { |
| 707 | + // This is a regular (non-offscreen) instance. If the nearest |
| 708 | + // offscreen boundary is hidden, hide this node. |
| 709 | + const hidden = hideNearestNode ? true : instance.hidden; |
| 710 | + const clonedChildren = removeOffscreenContainersFromChildren( |
| 711 | + instance.children, |
| 712 | + // We never need to hide the children of this node, since if we're |
| 713 | + // inside a hidden tree, then the hidden style will be applied to |
| 714 | + // this node. |
| 715 | + false, |
| 716 | + ); |
| 717 | + if ( |
| 718 | + clonedChildren === instance.children && |
| 719 | + hidden === instance.hidden |
| 720 | + ) { |
| 721 | + // No changes. Reuse the original instance without cloning. |
| 722 | + newChildren.push(instance); |
| 723 | + } else { |
| 724 | + didClone = true; |
| 725 | + const clone: Instance = { |
| 726 | + id: instance.id, |
| 727 | + type: instance.type, |
| 728 | + children: clonedChildren, |
| 729 | + text: instance.text, |
| 730 | + prop: instance.prop, |
| 731 | + hidden: hideNearestNode ? true : instance.hidden, |
| 732 | + context: instance.context, |
| 733 | + }; |
| 734 | + Object.defineProperty(clone, 'id', { |
| 735 | + value: clone.id, |
| 736 | + enumerable: false, |
| 737 | + }); |
| 738 | + Object.defineProperty(clone, 'text', { |
| 739 | + value: clone.text, |
| 740 | + enumerable: false, |
| 741 | + }); |
| 742 | + Object.defineProperty(clone, 'context', { |
| 743 | + value: clone.context, |
| 744 | + enumerable: false, |
| 745 | + }); |
| 746 | + newChildren.push(clone); |
| 747 | + } |
| 748 | + } |
| 749 | + } else { |
| 750 | + // This is a text instance |
| 751 | + const textInstance: TextInstance = (child: any); |
| 752 | + if (hideNearestNode) { |
| 753 | + didClone = true; |
| 754 | + const clone = { |
| 755 | + text: textInstance.text, |
| 756 | + id: textInstance.id, |
| 757 | + hidden: textInstance.hidden || hideNearestNode, |
| 758 | + context: textInstance.context, |
| 759 | + }; |
| 760 | + Object.defineProperty(clone, 'id', { |
| 761 | + value: clone.id, |
| 762 | + enumerable: false, |
| 763 | + }); |
| 764 | + Object.defineProperty(clone, 'context', { |
| 765 | + value: clone.context, |
| 766 | + enumerable: false, |
| 767 | + }); |
| 768 | + |
| 769 | + newChildren.push(clone); |
| 770 | + } else { |
| 771 | + newChildren.push(textInstance); |
| 772 | + } |
| 773 | + } |
| 774 | + } |
| 775 | + // There are some tests that assume reference equality, so preserve it |
| 776 | + // when possible. Alternatively, we could update the tests to compare the |
| 777 | + // ids instead. |
| 778 | + return didClone ? newChildren : children; |
| 779 | + } |
| 780 | + |
663 | 781 | function getChildrenAsJSX(root) { |
664 | 782 | const children = childToJSX(getChildren(root), null); |
665 | 783 | if (children === null) { |
|
0 commit comments