diff --git a/.changeset/calm-fabric-props.md b/.changeset/calm-fabric-props.md new file mode 100644 index 0000000000..a1c5c9a883 --- /dev/null +++ b/.changeset/calm-fabric-props.md @@ -0,0 +1,5 @@ +--- +'@fluentui-react-native/native-core': patch +--- + +Document the repository-local RNmacOS native-props synchronization backport and its clean-build requirement for animated Fabric window resizing. Publishing Native Core does not install this renderer patch in a consumer. diff --git a/.yarn/patches/react-native-macos-npm-0.81.9-native-props.patch b/.yarn/patches/react-native-macos-npm-0.81.9-native-props.patch new file mode 100644 index 0000000000..ff30d085af --- /dev/null +++ b/.yarn/patches/react-native-macos-npm-0.81.9-native-props.patch @@ -0,0 +1,202 @@ +diff --git a/ReactCommon/react/renderer/core/ShadowNode.cpp b/ReactCommon/react/renderer/core/ShadowNode.cpp +--- a/ReactCommon/react/renderer/core/ShadowNode.cpp ++++ b/ReactCommon/react/renderer/core/ShadowNode.cpp +@@ -140,27 +140,32 @@ + const ShadowNodeFragment& fragment) const { + const auto& family = *family_; + const auto& componentDescriptor = family.componentDescriptor_; +- if (family.nativeProps_DEPRECATED != nullptr) { ++ ++ std::optional propsOverride; ++ { ++ std::lock_guard lock(family.nativePropsMutex); ++ if (family.nativeProps_DEPRECATED != nullptr) { ++ if (fragment.props == ShadowNodeFragment::propsPlaceholder()) { ++ propsOverride.emplace(*family.nativeProps_DEPRECATED); ++ } else { ++ // TODO: We might need to merge fragment.props with ++ // `family.nativeProps_DEPRECATED`. ++ } ++ } ++ } ++ ++ if (propsOverride) { ++ // Copy under the family lock, then parse and clone without holding it. + auto propsParserContext = PropsParserContext{family_->getSurfaceId(), {}}; +- if (fragment.props == ShadowNodeFragment::propsPlaceholder()) { +- // Clone existing `props_` with `family.nativeProps_DEPRECATED` to apply +- // previously set props via `setNativeProps` API. +- auto props = componentDescriptor.cloneProps( +- propsParserContext, props_, RawProps(*family.nativeProps_DEPRECATED)); +- auto clonedNode = componentDescriptor.cloneShadowNode( +- *this, +- {.props = props, +- .children = fragment.children, +- .state = fragment.state}); +- return clonedNode; +- } else { +- // TODO: We might need to merge fragment.priops with +- // `family.nativeProps_DEPRECATED`. +- return componentDescriptor.cloneShadowNode(*this, fragment); +- } +- } else { +- return componentDescriptor.cloneShadowNode(*this, fragment); +- } ++ return componentDescriptor.cloneShadowNode( ++ *this, ++ {.props = componentDescriptor.cloneProps( ++ propsParserContext, props_, std::move(*propsOverride)), ++ .children = fragment.children, ++ .state = fragment.state}); ++ } ++ ++ return componentDescriptor.cloneShadowNode(*this, fragment); + } + + ContextContainer::Shared ShadowNode::getContextContainer() const { +diff --git a/ReactCommon/react/renderer/core/ShadowNodeFamily.h b/ReactCommon/react/renderer/core/ShadowNodeFamily.h +--- a/ReactCommon/react/renderer/core/ShadowNodeFamily.h ++++ b/ReactCommon/react/renderer/core/ShadowNodeFamily.h +@@ -8,6 +8,7 @@ + #pragma once + + #include ++#include + #include + + #include +@@ -116,6 +117,7 @@ + * architecture and will be removed in the future. + */ + mutable std::unique_ptr nativeProps_DEPRECATED; ++ mutable std::mutex nativePropsMutex; + + /** + * @return tag for the ShadowNodeFamily. +diff --git a/ReactCommon/react/renderer/uimanager/UIManager.cpp b/ReactCommon/react/renderer/uimanager/UIManager.cpp +--- a/ReactCommon/react/renderer/uimanager/UIManager.cpp ++++ b/ReactCommon/react/renderer/uimanager/UIManager.cpp +@@ -118,46 +118,48 @@ + auto props = ShadowNodeFragment::propsPlaceholder(); + + if (!rawProps.isEmpty()) { +- if (family.nativeProps_DEPRECATED != nullptr) { +- // 1. update the nativeProps_DEPRECATED props. +- // +- // In this step, we want the most recent value for the props +- // managed by setNativeProps. +- // Values in `rawProps` patch (take precedence over) +- // `nativeProps_DEPRECATED`. For example, if both +- // `nativeProps_DEPRECATED` and `rawProps` contain key 'A'. +- // Value from `rawProps` overrides what was previously in +- // `nativeProps_DEPRECATED`. Notice that the `nativeProps_DEPRECATED` +- // patch will not get more props from `rawProps`: if the key is not +- // present in `nativeProps_DEPRECATED`, it will not be added. +- // +- // The result of this operation is the new `nativeProps_DEPRECATED`. +- family.nativeProps_DEPRECATED = +- std::make_unique(mergeDynamicProps( +- *family.nativeProps_DEPRECATED, // source +- (folly::dynamic)rawProps, // patch +- NullValueStrategy::Ignore)); +- +- // 2. Compute the final set of props. +- // +- // This step takes the new props handled by `setNativeProps` and +- // merges them in the `rawProps` managed by React. +- // The new props handled by `nativeProps` now takes precedence +- // on the props handled by React, as we want to make sure that +- // all the props are applied to the component. +- // We use these finalProps as source of truth for the component. +- auto finalProps = mergeDynamicProps( +- (folly::dynamic)rawProps, // source +- *family.nativeProps_DEPRECATED, // patch +- NullValueStrategy::Override); +- +- // 3. Clone the props by using finalProps. +- props = componentDescriptor.cloneProps( +- propsParserContext, shadowNode.getProps(), RawProps(finalProps)); +- } else { +- props = componentDescriptor.cloneProps( +- propsParserContext, shadowNode.getProps(), std::move(rawProps)); ++ std::optional finalProps; ++ { ++ std::lock_guard lock(family.nativePropsMutex); ++ if (family.nativeProps_DEPRECATED != nullptr) { ++ // 1. update the nativeProps_DEPRECATED props. ++ // ++ // In this step, we want the most recent value for the props ++ // managed by setNativeProps. ++ // Values in `rawProps` patch (take precedence over) ++ // `nativeProps_DEPRECATED`. For example, if both ++ // `nativeProps_DEPRECATED` and `rawProps` contain key 'A'. ++ // Value from `rawProps` overrides what was previously in ++ // `nativeProps_DEPRECATED`. Notice that the `nativeProps_DEPRECATED` ++ // patch will not get more props from `rawProps`: if the key is not ++ // present in `nativeProps_DEPRECATED`, it will not be added. ++ // ++ // The result of this operation is the new `nativeProps_DEPRECATED`. ++ family.nativeProps_DEPRECATED = ++ std::make_unique(mergeDynamicProps( ++ *family.nativeProps_DEPRECATED, // source ++ (folly::dynamic)rawProps, // patch ++ NullValueStrategy::Ignore)); ++ ++ // 2. Compute the final set of props. ++ // ++ // This step takes the new props handled by `setNativeProps` and ++ // merges them in the `rawProps` managed by React. ++ // The new props handled by `nativeProps` now takes precedence ++ // on the props handled by React, as we want to make sure that ++ // all the props are applied to the component. ++ // We use these finalProps as source of truth for the component. ++ finalProps = mergeDynamicProps( ++ (folly::dynamic)rawProps, // source ++ *family.nativeProps_DEPRECATED, // patch ++ NullValueStrategy::Override); ++ } + } ++ ++ props = componentDescriptor.cloneProps( ++ propsParserContext, ++ shadowNode.getProps(), ++ finalProps ? RawProps(std::move(*finalProps)) : std::move(rawProps)); + } + + auto clonedShadowNode = componentDescriptor.cloneShadowNode( +@@ -426,19 +428,22 @@ + const std::shared_ptr& shadowNode, + RawProps rawProps) const { + auto& family = shadowNode->getFamily(); +- if (family.nativeProps_DEPRECATED) { +- // Values in `rawProps` patch (take precedence over) +- // `nativeProps_DEPRECATED`. For example, if both `nativeProps_DEPRECATED` +- // and `rawProps` contain key 'A'. Value from `rawProps` overrides what was +- // previously in `nativeProps_DEPRECATED`. +- family.nativeProps_DEPRECATED = +- std::make_unique(mergeDynamicProps( +- *family.nativeProps_DEPRECATED, +- (folly::dynamic)rawProps, +- NullValueStrategy::Override)); +- } else { +- family.nativeProps_DEPRECATED = +- std::make_unique((folly::dynamic)rawProps); ++ { ++ std::lock_guard lock(family.nativePropsMutex); ++ if (family.nativeProps_DEPRECATED) { ++ // Values in `rawProps` patch (take precedence over) ++ // `nativeProps_DEPRECATED`. For example, if both `nativeProps_DEPRECATED` ++ // and `rawProps` contain key 'A'. Value from `rawProps` overrides what was ++ // previously in `nativeProps_DEPRECATED`. ++ family.nativeProps_DEPRECATED = ++ std::make_unique(mergeDynamicProps( ++ *family.nativeProps_DEPRECATED, ++ (folly::dynamic)rawProps, ++ NullValueStrategy::Override)); ++ } else { ++ family.nativeProps_DEPRECATED = ++ std::make_unique((folly::dynamic)rawProps); ++ } + } + + shadowTreeRegistry_.visit( diff --git a/package.json b/package.json index b51946272f..9161dc747d 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "moment@npm:^2.24.0": "2.31.0", "moment@npm:^2.29.4": "2.31.0", "path-to-regexp@npm:8.3.0": "^8.4.2", - "react-native-macos": "patch:react-native-macos@npm%3A0.81.9#~/.yarn/patches/react-native-macos-npm-0.81.9-aa583a0648.patch", + "react-native-macos": "patch:react-native-macos@npm%3A0.81.9#~/.yarn/patches/react-native-macos-npm-0.81.9-aa583a0648.patch&~/.yarn/patches/react-native-macos-npm-0.81.9-native-props.patch", "react-native-windows": "0.81.35", "shell-quote": "1.11.0", "smol-toml@npm:^1.6.1": "1.9.0", diff --git a/packages/agentic/native-core/references/remaining-work.md b/packages/agentic/native-core/references/remaining-work.md index ad5a77fc60..6655c4bf1b 100644 --- a/packages/agentic/native-core/references/remaining-work.md +++ b/packages/agentic/native-core/references/remaining-work.md @@ -35,9 +35,12 @@ issue or pull request; compilation and skipped cases are not native qualificatio ## RNmacOS upstream replacement -- **Outcome:** consumers receive the macOS accessibility fixes without this repository's RNmacOS patch. +- **Outcome:** consumers receive the macOS accessibility and native-props synchronization fixes without this repository's RNmacOS patches. - **Current state:** the [repository-local patch](rnmacos-patch.md) is applied only to this repository's native hosts; publishing native-core or components does not install it in a consumer. - **Done when:** the fixes land upstream or in an equivalent consuming-renderer integration, and the same native cases pass against that replacement before the patch is removed. Checked-state actions, AX relationships, and VoiceOver speech remain separate, still-unqualified semantics. + The native-props synchronization already has an upstream implementation; remove + that backport only after the pinned RNmacOS fork includes it and the animated + window-resize regression passes against the replacement. diff --git a/packages/agentic/native-core/references/rnmacos-patch.md b/packages/agentic/native-core/references/rnmacos-patch.md index 15b8fd6485..ca5577ed3f 100644 --- a/packages/agentic/native-core/references/rnmacos-patch.md +++ b/packages/agentic/native-core/references/rnmacos-patch.md @@ -20,6 +20,42 @@ text on macOS. RNmacOS otherwise rejects them unconditionally because its iOS element provider is compiled out, which hides every Text label and `testID` from accessibility clients. +## Native-props synchronization backport + +The root also applies +`.yarn/patches/react-native-macos-npm-0.81.9-native-props.patch`, a minimal +backport of MIT-licensed React Native code from +[upstream commit `923e7ddcaf4d` by OrfeasZ, #52646](https://github.com/facebook/react-native/commit/923e7ddcaf4d2de45ca7dedafbb84c6b87dbffc4). +The upstream copyright and license in the patched source files remain intact. +Fabric's window-resize layout can clone animated nodes on the main thread while +JavaScript replaces their `nativeProps_DEPRECATED` map. This concurrent-access +cause is inferred from the failing reader, the unguarded source, and the upstream +fix; no ThreadSanitizer run established the concurrent writer. The backport guards +both writers and the cloning reader with one family-owned mutex, copying the +props before releasing the lock and parsing them afterward. This preserves +merge precedence without holding the mutex across shadow-tree commits. +Regenerate Pods and clean-build all native dependencies after changing this +patch: the family header's layout changes. + +The race was exposed by the PGB-006 NativeSemantics window-resize callback +during Fabric Storybook qualification. A clean native rebuild still failed, +and an owned-process AddressSanitizer report recorded +`BUS` in `folly::dynamic::type()` while `ShadowNode::clone` copied the +unprotected native-props map during AppKit layout. This is a renderer +concurrency fix, not a click-through or driver retry workaround. PGB-006 runs +in the full Fabric physical lane as well as the accessible lane; the original +Checkbox, keyboard-clear, and Accordion reproductions passed without this +backport. + +### Qualification + +A clean Pods and Fabric Storybook native build with AddressSanitizer passed, +as did the PGB-006 resize callback and Native Core's CocoaPods-module and +Office-style bridging-header probes. This qualifies the Storybook host only: +macOS FluentTester and other native application hosts were not built locally. + +## Scope and replacement + These fixes belong in RNmacOS, not in a MenuItem-specific native host or a JavaScript accessibility-value workaround. The patch is used by this repository's native hosts; publishing native-core or components does **not** install it in a diff --git a/yarn.lock b/yarn.lock index 6be5d02e4a..964a8c9d44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19597,9 +19597,9 @@ __metadata: languageName: node linkType: hard -"react-native-macos@patch:react-native-macos@npm%3A0.81.9#~/.yarn/patches/react-native-macos-npm-0.81.9-aa583a0648.patch": +"react-native-macos@patch:react-native-macos@npm%3A0.81.9#~/.yarn/patches/react-native-macos-npm-0.81.9-aa583a0648.patch&~/.yarn/patches/react-native-macos-npm-0.81.9-native-props.patch": version: 0.81.9 - resolution: "react-native-macos@patch:react-native-macos@npm%3A0.81.9#~/.yarn/patches/react-native-macos-npm-0.81.9-aa583a0648.patch::version=0.81.9&hash=fdc083" + resolution: "react-native-macos@patch:react-native-macos@npm%3A0.81.9#~/.yarn/patches/react-native-macos-npm-0.81.9-aa583a0648.patch&~/.yarn/patches/react-native-macos-npm-0.81.9-native-props.patch::version=0.81.9&hash=63578b" dependencies: "@jest/create-cache-key-function": "npm:^29.7.0" "@react-native-macos/virtualized-lists": "npm:0.81.9" @@ -19645,7 +19645,7 @@ __metadata: bin: react-native: cli.js react-native-macos: cli.js - checksum: 10c0/c4dd2eda84eea704dc205b31bb5ddbc6f58f26a23fa99f14d66ebf6b4637407a0ebb66096df4c34f8923910426a326e586184e2efa62e006257b984608d39f86 + checksum: 10c0/3d565e0d0a494b81519e5d86a8322aec279170317e7b5dc77ef1f3b8b786706cb342fc1626f5348bc3e3f67a501f46616b13c43f1f02842b21022f91597c8806 languageName: node linkType: hard