1010#include < react/renderer/core/LayoutMetrics.h>
1111#include < react/renderer/core/LayoutableShadowNode.h>
1212#include < react/renderer/core/ShadowNodeFamily.h>
13+ #include < react/renderer/css/CSSLength.h>
14+ #include < react/renderer/css/CSSPercentage.h>
15+ #include < react/renderer/css/CSSValueParser.h>
16+ #include < react/renderer/graphics/RectangleEdges.h>
1317#include < utility>
1418
1519namespace facebook ::react {
1620
21+ // Parse a normalized rootMargin string that's always in the format:
22+ // "top right bottom left" where each value is either "Npx" or "N%"
23+ // The string is already validated and normalized by JS.
24+ // Returns a vector of 4 MarginValue structures (top, right, bottom, left).
25+ // Returns an empty vector if parsing fails.
26+ std::vector<MarginValue> parseNormalizedRootMargin (
27+ const std::string& marginStr) {
28+ // If unset or set to default, return empty so we don't apply any margins
29+ if (marginStr.empty () || marginStr == " 0px 0px 0px 0px" ) {
30+ return {};
31+ }
32+
33+ std::vector<MarginValue> values;
34+ CSSSyntaxParser syntaxParser (marginStr);
35+
36+ // Parse exactly 4 space-separated values
37+ while (!syntaxParser.isFinished () && values.size () < 4 ) {
38+ syntaxParser.consumeWhitespace ();
39+ if (syntaxParser.isFinished ()) {
40+ break ;
41+ }
42+
43+ auto parsed = parseNextCSSValue<CSSLength, CSSPercentage>(syntaxParser);
44+
45+ if (std::holds_alternative<CSSLength>(parsed)) {
46+ auto length = std::get<CSSLength>(parsed);
47+ // Only support px units for rootMargin (per W3C spec)
48+ if (length.unit != CSSLengthUnit::Px) {
49+ return {};
50+ }
51+ values.push_back ({length.value , false });
52+ } else if (std::holds_alternative<CSSPercentage>(parsed)) {
53+ auto percentage = std::get<CSSPercentage>(parsed);
54+ values.push_back ({percentage.value , true });
55+ } else {
56+ // Invalid token
57+ return {};
58+ }
59+ }
60+
61+ // Should have exactly 4 values since JS normalizes to this format
62+ if (values.size () != 4 ) {
63+ return {};
64+ }
65+
66+ return values;
67+ }
68+
69+ namespace {
70+
71+ // Convert margin values to actual pixel values based on root rect.
72+ // Per W3C spec: top/bottom percentages use height, left/right use width.
73+ EdgeInsets calculateRootMarginInsets (
74+ const std::vector<MarginValue>& margins,
75+ const Rect& rootRect) {
76+ Float top = margins[0 ].isPercentage
77+ ? (margins[0 ].value / 100 .0f ) * rootRect.size .height
78+ : margins[0 ].value ;
79+ Float right = margins[1 ].isPercentage
80+ ? (margins[1 ].value / 100 .0f ) * rootRect.size .width
81+ : margins[1 ].value ;
82+ Float bottom = margins[2 ].isPercentage
83+ ? (margins[2 ].value / 100 .0f ) * rootRect.size .height
84+ : margins[2 ].value ;
85+ Float left = margins[3 ].isPercentage
86+ ? (margins[3 ].value / 100 .0f ) * rootRect.size .width
87+ : margins[3 ].value ;
88+
89+ return EdgeInsets{left, top, right, bottom};
90+ }
91+
92+ } // namespace
93+
1794IntersectionObserver::IntersectionObserver (
1895 IntersectionObserverObserverId intersectionObserverId,
1996 std::optional<ShadowNodeFamily::Shared> observationRootShadowNodeFamily,
2097 ShadowNodeFamily::Shared targetShadowNodeFamily,
2198 std::vector<Float> thresholds,
22- std::optional<std::vector<Float>> rootThresholds)
99+ std::optional<std::vector<Float>> rootThresholds,
100+ std::vector<MarginValue> rootMargins)
23101 : intersectionObserverId_(intersectionObserverId),
24102 observationRootShadowNodeFamily_ (
25103 std::move (observationRootShadowNodeFamily)),
26104 targetShadowNodeFamily_(std::move(targetShadowNodeFamily)),
27105 thresholds_(std::move(thresholds)),
28- rootThresholds_(std::move(rootThresholds)) {}
106+ rootThresholds_(std::move(rootThresholds)),
107+ rootMargins_(std::move(rootMargins)) {}
29108
30109static std::shared_ptr<const ShadowNode> getShadowNode (
31110 const ShadowNodeFamily::AncestorList& ancestors) {
@@ -113,13 +192,14 @@ static std::optional<Rect> intersectOrNull(
113192// https://w3c.github.io/IntersectionObserver/#calculate-intersection-rect-algo
114193static std::optional<Rect> computeIntersection (
115194 const Rect& rootBoundingRect,
195+ const Rect& rootMarginBoundingRect,
116196 const Rect& targetBoundingRect,
117197 const ShadowNodeFamily::AncestorList& targetToRootAncestors,
118198 bool hasExplicitRoot) {
119199 // Use intersectOrNull to properly distinguish between edge-adjacent
120200 // (valid intersection) and separated rectangles (no intersection)
121201 auto absoluteIntersectionRect =
122- intersectOrNull (rootBoundingRect , targetBoundingRect);
202+ intersectOrNull (rootMarginBoundingRect , targetBoundingRect);
123203 if (!absoluteIntersectionRect.has_value ()) {
124204 return std::nullopt ;
125205 }
@@ -130,12 +210,14 @@ static std::optional<Rect> computeIntersection(
130210 auto clippedTargetFromRoot =
131211 getClippedTargetBoundingRect (targetToRootAncestors);
132212
213+ // Use root origin (without rootMargins) to translate coordinates of
214+ // clippedTarget from relative to root, to top-level coordinate system
133215 auto clippedTargetBoundingRect = hasExplicitRoot ? Rect{
134- .origin =rootBoundingRect.origin + clippedTargetFromRoot.origin ,
216+ .origin = rootBoundingRect.origin + clippedTargetFromRoot.origin ,
135217 .size =clippedTargetFromRoot.size }
136218 : clippedTargetFromRoot;
137219
138- return intersectOrNull (rootBoundingRect , clippedTargetBoundingRect);
220+ return intersectOrNull (rootMarginBoundingRect , clippedTargetBoundingRect);
139221}
140222
141223static Float getHighestThresholdCrossed (
@@ -167,6 +249,18 @@ IntersectionObserver::updateIntersectionObservation(
167249 ? getBoundingRect (rootAncestors)
168250 : getRootNodeBoundingRect (rootShadowNode);
169251
252+ auto rootMarginBoundingRect = rootBoundingRect;
253+
254+ // Apply rootMargin to expand/contract the root bounding rect
255+ if (!rootMargins_.empty ()) {
256+ // Calculate the actual insets based on current root dimensions
257+ // (converts percentages to pixels)
258+ auto insets = calculateRootMarginInsets (rootMargins_, rootBoundingRect);
259+ // Use outsetBy to expand the root rect (positive values expand, negative
260+ // contract)
261+ rootMarginBoundingRect = outsetBy (rootBoundingRect, insets);
262+ }
263+
170264 auto targetAncestors = targetShadowNodeFamily_->getAncestors (rootShadowNode);
171265
172266 // Absolute coordinates of the target
@@ -175,7 +269,7 @@ IntersectionObserver::updateIntersectionObservation(
175269 if ((hasExplicitRoot && rootAncestors.empty ()) || targetAncestors.empty ()) {
176270 // If observation root or target is not a descendant of `rootShadowNode`
177271 return setNotIntersectingState (
178- rootBoundingRect , targetBoundingRect, {}, time);
272+ rootMarginBoundingRect , targetBoundingRect, {}, time);
179273 }
180274
181275 auto targetToRootAncestors = hasExplicitRoot
@@ -184,6 +278,7 @@ IntersectionObserver::updateIntersectionObservation(
184278
185279 auto intersection = computeIntersection (
186280 rootBoundingRect,
281+ rootMarginBoundingRect,
187282 targetBoundingRect,
188283 targetToRootAncestors,
189284 hasExplicitRoot);
@@ -203,31 +298,31 @@ IntersectionObserver::updateIntersectionObservation(
203298
204299 if (!intersection.has_value ()) {
205300 return setNotIntersectingState (
206- rootBoundingRect , targetBoundingRect, intersectionRect, time);
301+ rootMarginBoundingRect , targetBoundingRect, intersectionRect, time);
207302 }
208303
209304 auto highestThresholdCrossed =
210305 getHighestThresholdCrossed (intersectionRatio, thresholds_);
211306
212307 auto highestRootThresholdCrossed = -1 .0f ;
213308 if (rootThresholds_.has_value ()) {
214- Float rootBoundingRectArea =
215- rootBoundingRect .size .width * rootBoundingRect .size .height ;
216- Float rootThresholdIntersectionRatio = rootBoundingRectArea == 0
309+ Float rootMarginBoundingRectArea =
310+ rootMarginBoundingRect .size .width * rootMarginBoundingRect .size .height ;
311+ Float rootThresholdIntersectionRatio = rootMarginBoundingRectArea == 0
217312 ? 0
218- : intersectionRectArea / rootBoundingRectArea ;
313+ : intersectionRectArea / rootMarginBoundingRectArea ;
219314 highestRootThresholdCrossed = getHighestThresholdCrossed (
220315 rootThresholdIntersectionRatio, rootThresholds_.value ());
221316 }
222317
223318 if (highestThresholdCrossed == -1 .0f &&
224319 highestRootThresholdCrossed == -1 .0f ) {
225320 return setNotIntersectingState (
226- rootBoundingRect , targetBoundingRect, intersectionRect, time);
321+ rootMarginBoundingRect , targetBoundingRect, intersectionRect, time);
227322 }
228323
229324 return setIntersectingState (
230- rootBoundingRect ,
325+ rootMarginBoundingRect ,
231326 targetBoundingRect,
232327 intersectionRect,
233328 highestThresholdCrossed,
0 commit comments