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+ namespace {
22+
23+ // Structure to hold a margin value that can be either pixels or percentage
24+ struct MarginValue {
25+ Float value;
26+ bool isPercentage;
27+ };
28+
29+ // Parse a CSS-style margin string (e.g., "10px 20px 30px 40px") into EdgeInsets
30+ // Follows W3C spec:
31+ // https://w3c.github.io/IntersectionObserver/#dom-intersectionobserverinit-rootmargin
32+ std::optional<EdgeInsets> parseRootMargin (
33+ const std::string& marginStr,
34+ const Rect& rootRect) {
35+ if (marginStr.empty ()) {
36+ return std::nullopt ;
37+ }
38+
39+ std::vector<MarginValue> values;
40+ CSSSyntaxParser syntaxParser (marginStr);
41+
42+ // Parse up to 4 space-separated length/percentage values
43+ while (!syntaxParser.isFinished ()) {
44+ syntaxParser.consumeWhitespace ();
45+ if (syntaxParser.isFinished ()) {
46+ break ;
47+ }
48+
49+ auto parsed = parseNextCSSValue<CSSLength, CSSPercentage>(syntaxParser);
50+
51+ if (std::holds_alternative<CSSLength>(parsed)) {
52+ auto length = std::get<CSSLength>(parsed);
53+ // Only support px units for rootMargin (per W3C spec)
54+ if (length.unit != CSSLengthUnit::Px) {
55+ return std::nullopt ;
56+ }
57+ values.push_back ({length.value , false });
58+ } else if (std::holds_alternative<CSSPercentage>(parsed)) {
59+ auto percentage = std::get<CSSPercentage>(parsed);
60+ values.push_back ({percentage.value , true });
61+ } else {
62+ // Invalid token, stop parsing
63+ return std::nullopt ;
64+ }
65+ }
66+
67+ // CSS margin shorthand: 1 value = all, 2 values = vertical/horizontal,
68+ // 3 values = top/horizontal/bottom, 4 values = top/right/bottom/left
69+ std::vector<MarginValue> expandedValues;
70+ switch (values.size ()) {
71+ case 1 :
72+ expandedValues = {values[0 ], values[0 ], values[0 ], values[0 ]};
73+ break ;
74+ case 2 :
75+ expandedValues = {values[0 ], values[1 ], values[0 ], values[1 ]};
76+ break ;
77+ case 3 :
78+ expandedValues = {values[0 ], values[1 ], values[2 ], values[1 ]};
79+ break ;
80+ case 4 :
81+ expandedValues = {values[0 ], values[1 ], values[2 ], values[3 ]};
82+ break ;
83+ default :
84+ return std::nullopt ;
85+ }
86+
87+ // Calculate actual pixel values, converting percentages based on root
88+ // dimensions Per W3C spec: top/bottom percentages use height, left/right use
89+ // width
90+ Float top = expandedValues[0 ].isPercentage
91+ ? (expandedValues[0 ].value / 100 .0f ) * rootRect.size .height
92+ : expandedValues[0 ].value ;
93+ Float right = expandedValues[1 ].isPercentage
94+ ? (expandedValues[1 ].value / 100 .0f ) * rootRect.size .width
95+ : expandedValues[1 ].value ;
96+ Float bottom = expandedValues[2 ].isPercentage
97+ ? (expandedValues[2 ].value / 100 .0f ) * rootRect.size .height
98+ : expandedValues[2 ].value ;
99+ Float left = expandedValues[3 ].isPercentage
100+ ? (expandedValues[3 ].value / 100 .0f ) * rootRect.size .width
101+ : expandedValues[3 ].value ;
102+
103+ return EdgeInsets{left, top, right, bottom};
104+ }
105+
106+ } // namespace
107+
17108IntersectionObserver::IntersectionObserver (
18109 IntersectionObserverObserverId intersectionObserverId,
19110 std::optional<ShadowNodeFamily::Shared> observationRootShadowNodeFamily,
20111 ShadowNodeFamily::Shared targetShadowNodeFamily,
21112 std::vector<Float> thresholds,
22- std::optional<std::vector<Float>> rootThresholds)
113+ std::optional<std::vector<Float>> rootThresholds,
114+ std::optional<std::string> rootMargin)
23115 : intersectionObserverId_(intersectionObserverId),
24116 observationRootShadowNodeFamily_ (
25117 std::move (observationRootShadowNodeFamily)),
26118 targetShadowNodeFamily_(std::move(targetShadowNodeFamily)),
27119 thresholds_(std::move(thresholds)),
28- rootThresholds_(std::move(rootThresholds)) {}
120+ rootThresholds_(std::move(rootThresholds)),
121+ rootMargin_(std::move(rootMargin)) {}
29122
30123static std::shared_ptr<const ShadowNode> getShadowNode (
31124 const ShadowNodeFamily::AncestorList& ancestors) {
@@ -113,13 +206,14 @@ static std::optional<Rect> intersectOrNull(
113206// https://w3c.github.io/IntersectionObserver/#calculate-intersection-rect-algo
114207static std::optional<Rect> computeIntersection (
115208 const Rect& rootBoundingRect,
209+ const Rect& rootMarginBoundingRect,
116210 const Rect& targetBoundingRect,
117211 const ShadowNodeFamily::AncestorList& targetToRootAncestors,
118212 bool hasExplicitRoot) {
119213 // Use intersectOrNull to properly distinguish between edge-adjacent
120214 // (valid intersection) and separated rectangles (no intersection)
121215 auto absoluteIntersectionRect =
122- intersectOrNull (rootBoundingRect , targetBoundingRect);
216+ intersectOrNull (rootMarginBoundingRect , targetBoundingRect);
123217 if (!absoluteIntersectionRect.has_value ()) {
124218 return std::nullopt ;
125219 }
@@ -130,12 +224,14 @@ static std::optional<Rect> computeIntersection(
130224 auto clippedTargetFromRoot =
131225 getClippedTargetBoundingRect (targetToRootAncestors);
132226
227+ // Use root origin (without rootMargins) to translate coordinates of
228+ // clippedTarget from relative to root, to top-level coordinate system
133229 auto clippedTargetBoundingRect = hasExplicitRoot ? Rect{
134- .origin =rootBoundingRect.origin + clippedTargetFromRoot.origin ,
230+ .origin = rootBoundingRect.origin + clippedTargetFromRoot.origin ,
135231 .size =clippedTargetFromRoot.size }
136232 : clippedTargetFromRoot;
137233
138- return intersectOrNull (rootBoundingRect , clippedTargetBoundingRect);
234+ return intersectOrNull (rootMarginBoundingRect , clippedTargetBoundingRect);
139235}
140236
141237static Float getHighestThresholdCrossed (
@@ -167,6 +263,18 @@ IntersectionObserver::updateIntersectionObservation(
167263 ? getBoundingRect (rootAncestors)
168264 : getRootNodeBoundingRect (rootShadowNode);
169265
266+ auto rootMarginBoundingRect = rootBoundingRect;
267+
268+ // Apply rootMargin to expand/contract the root bounding rect
269+ if (rootMargin_.has_value ()) {
270+ auto parsedMargin = parseRootMargin (rootMargin_.value (), rootBoundingRect);
271+ if (parsedMargin.has_value ()) {
272+ // Use outsetBy to expand the root rect (positive values expand, negative
273+ // contract)
274+ rootMarginBoundingRect = outsetBy (rootBoundingRect, parsedMargin.value ());
275+ }
276+ }
277+
170278 auto targetAncestors = targetShadowNodeFamily_->getAncestors (rootShadowNode);
171279
172280 // Absolute coordinates of the target
@@ -175,7 +283,7 @@ IntersectionObserver::updateIntersectionObservation(
175283 if ((hasExplicitRoot && rootAncestors.empty ()) || targetAncestors.empty ()) {
176284 // If observation root or target is not a descendant of `rootShadowNode`
177285 return setNotIntersectingState (
178- rootBoundingRect , targetBoundingRect, {}, time);
286+ rootMarginBoundingRect , targetBoundingRect, {}, time);
179287 }
180288
181289 auto targetToRootAncestors = hasExplicitRoot
@@ -184,6 +292,7 @@ IntersectionObserver::updateIntersectionObservation(
184292
185293 auto intersection = computeIntersection (
186294 rootBoundingRect,
295+ rootMarginBoundingRect,
187296 targetBoundingRect,
188297 targetToRootAncestors,
189298 hasExplicitRoot);
@@ -203,31 +312,31 @@ IntersectionObserver::updateIntersectionObservation(
203312
204313 if (!intersection.has_value ()) {
205314 return setNotIntersectingState (
206- rootBoundingRect , targetBoundingRect, intersectionRect, time);
315+ rootMarginBoundingRect , targetBoundingRect, intersectionRect, time);
207316 }
208317
209318 auto highestThresholdCrossed =
210319 getHighestThresholdCrossed (intersectionRatio, thresholds_);
211320
212321 auto highestRootThresholdCrossed = -1 .0f ;
213322 if (rootThresholds_.has_value ()) {
214- Float rootBoundingRectArea =
215- rootBoundingRect .size .width * rootBoundingRect .size .height ;
216- Float rootThresholdIntersectionRatio = rootBoundingRectArea == 0
323+ Float rootMarginBoundingRectArea =
324+ rootMarginBoundingRect .size .width * rootMarginBoundingRect .size .height ;
325+ Float rootThresholdIntersectionRatio = rootMarginBoundingRectArea == 0
217326 ? 0
218- : intersectionRectArea / rootBoundingRectArea ;
327+ : intersectionRectArea / rootMarginBoundingRectArea ;
219328 highestRootThresholdCrossed = getHighestThresholdCrossed (
220329 rootThresholdIntersectionRatio, rootThresholds_.value ());
221330 }
222331
223332 if (highestThresholdCrossed == -1 .0f &&
224333 highestRootThresholdCrossed == -1 .0f ) {
225334 return setNotIntersectingState (
226- rootBoundingRect , targetBoundingRect, intersectionRect, time);
335+ rootMarginBoundingRect , targetBoundingRect, intersectionRect, time);
227336 }
228337
229338 return setIntersectingState (
230- rootBoundingRect ,
339+ rootMarginBoundingRect ,
231340 targetBoundingRect,
232341 intersectionRect,
233342 highestThresholdCrossed,
0 commit comments