diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp index b76a03881be6..41de087d3f80 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp @@ -708,9 +708,9 @@ static void calculateShadowViewMutationsFlattener( auto unvisitedOtherNodesIt = unvisitedOtherNodes.find(newChild.shadowView.tag); if (unvisitedOtherNodesIt != unvisitedOtherNodes.end()) { - auto unvisitedItPair = *unvisitedOtherNodesIt->second; + auto* unvisitedItPair = unvisitedOtherNodesIt->second; unvisitedRecursiveChildPairs.insert( - {unvisitedItPair.shadowView.tag, &unvisitedItPair}); + {unvisitedItPair->shadowView.tag, unvisitedItPair}); } else { unvisitedRecursiveChildPairs.insert( {newChild.shadowView.tag, &newChild}); @@ -820,6 +820,9 @@ static void calculateShadowViewMutationsFlattener( // Final step: go through creation/deletion candidates and delete/create // subtrees if they were never visited during the execution of the above // loop and recursions. + const auto& subVisitedMap = reparentMode == ReparentMode::Flatten + ? *subVisitedOldMap + : *subVisitedNewMap; for (auto& deletionCreationCandidatePair : deletionCreationCandidatePairs) { auto& treeChildPair = *deletionCreationCandidatePair.second; @@ -828,7 +831,10 @@ static void calculateShadowViewMutationsFlattener( // already created/deleted and we don't need to do that here. // It is always the responsibility of the matcher to update subtrees when // nodes are matched. - if (treeChildPair.inOtherTree()) { + // The recursion can match the node through a different pair instance + // (e.g. when zIndex orders it before its parent), so check its tag too. + if (treeChildPair.inOtherTree() || + subVisitedMap.contains(treeChildPair.shadowView.tag)) { continue; } diff --git a/packages/react-native/src/private/renderer/mounting/__tests__/Mounting-itest.js b/packages/react-native/src/private/renderer/mounting/__tests__/Mounting-itest.js index affe86ef97bb..c260930ad584 100644 --- a/packages/react-native/src/private/renderer/mounting/__tests__/Mounting-itest.js +++ b/packages/react-native/src/private/renderer/mounting/__tests__/Mounting-itest.js @@ -446,6 +446,73 @@ describe('ViewFlattening', () => { />, ); }); + + test('#58647: child with negative zIndex is kept when its parent flattens and its grandparent unflattens', () => { + const root = Fantom.createRoot(); + + function render(opacityOnGrandparent: boolean) { + Fantom.runTask(() => { + root.render( + + + + + + + + + , + ); + }); + } + + const expectedOutput = ( + + + + + + + + ); + + render(false); + root.takeMountingManagerLogs(); + + render(true); + expect(root.takeMountingManagerLogs()).toEqual([ + 'Remove {type: "View", parentNativeID: "Q", index: 0, nativeID: (N/A)}', + 'Remove {type: "View", parentNativeID: (N/A), index: 2, nativeID: "B"}', + 'Remove {type: "View", parentNativeID: (N/A), index: 1, nativeID: "A"}', + 'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "K"}', + 'Delete {type: "View", nativeID: (N/A)}', + 'Create {type: "View", nativeID: (N/A)}', + 'Insert {type: "View", parentNativeID: "Q", index: 0, nativeID: (N/A)}', + 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "K"}', + 'Insert {type: "View", parentNativeID: (N/A), index: 1, nativeID: "A"}', + 'Insert {type: "View", parentNativeID: (N/A), index: 2, nativeID: "B"}', + ]); + expect(root.getRenderedOutput({props: ['nativeID']}).toJSX()).toEqual( + expectedOutput, + ); + + render(false); + expect(root.takeMountingManagerLogs()).toEqual([ + 'Remove {type: "View", parentNativeID: (N/A), index: 2, nativeID: "B"}', + 'Remove {type: "View", parentNativeID: (N/A), index: 1, nativeID: "A"}', + 'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "K"}', + 'Remove {type: "View", parentNativeID: "Q", index: 0, nativeID: (N/A)}', + 'Delete {type: "View", nativeID: (N/A)}', + 'Create {type: "View", nativeID: (N/A)}', + 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "K"}', + 'Insert {type: "View", parentNativeID: (N/A), index: 1, nativeID: "A"}', + 'Insert {type: "View", parentNativeID: (N/A), index: 2, nativeID: "B"}', + 'Insert {type: "View", parentNativeID: "Q", index: 0, nativeID: (N/A)}', + ]); + expect(root.getRenderedOutput({props: ['nativeID']}).toJSX()).toEqual( + expectedOutput, + ); + }); }); describe('reconciliation of setNativeProps and React commit', () => {