Repository: adobe/react-spectrum
File: packages/@react-aria/overlays/src/calculatePosition.ts (TypeScript)

Code before:

// Determines the amount of space required when moving the overlay to ensure it remains in the boundary
function getDelta(
  axis: Axis,
  offset: number,
  size: number,
  // The dimensions of the boundary element that the popover is
  // positioned within (most of the time this is the <body>).
  boundaryDimensions: Dimensions,
  // The dimensions of the containing block element that the popover is
  // positioned relative to (e.g. parent with position: relative).
  // Usually this is the same as the boundary element, but if the popover
  // is portaled somewhere other than the body and has an ancestor with
  // position: relative/absolute, it will be different.
  containerDimensions: Dimensions,
  padding: number,
  containerOffsetWithBoundary: Offset
) {
  let containerScroll = containerDimensions.scroll[axis] ?? 0;
  // The height/width of the boundary. Matches the axis along which we are adjusting the overlay position
  let boundarySize = boundaryDimensions[AXIS_SIZE[axis]];
  // Calculate the edges of the boundary (accomodating for the boundary padding) and the edges of the overlay.
  // Note that these values are with respect to the visual viewport (aka 0,0 is the top left of the viewport)
  let boundaryStartEdge = boundaryDimensions.scroll[AXIS[axis]] + padding;
  let boundaryEndEdge = boundarySize + boundaryDimensions.scroll[AXIS[axis]] - padding;
  let startEdgeOffset = offset - containerScroll + containerOffsetWithBoundary[axis];
  let endEdgeOffset = offset - containerScroll + size + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];

  // If any of the overlay edges falls outside of the boundary, shift the overlay the required amount to align one of the overlay's
  // edges with the closest boundary edge.
  if (startEdgeOffset < boundaryStartEdge) {
    return boundaryStartEdge - startEdgeOffset;
  } else if (endEdgeOffset > boundaryEndEdge) {
    return Math.max(boundaryEndEdge - endEdgeOffset, boundaryStartEdge - startEdgeOffset);
  } else {
    return 0;
  }
}

function getMargins(node: Element): Position {
  let style = window.getComputedStyle(node);
  return {
    top: parseInt(style.marginTop, 10) || 0,
    bottom: parseInt(style.marginBottom, 10) || 0,
    left: parseInt(style.marginLeft, 10) || 0,
    right: parseInt(style.marginRight, 10) || 0
  };
}

function parsePlacement(input: Placement): ParsedPlacement {
  if (PARSED_PLACEMENT_CACHE[input]) {

Review comment (bug):
I double checked this and found that it was tied to pinch zoom positioning: https://github.com/adobe/react-spectrum/commit/3722d3071486662c4270b73f686ac518454f7cef. If you comment out what the startEdge/endEdge values here are, try opening the RAC Popover story outside the iframe, and open the popover after pinch zooming on Safari in such a way that the trigger is close to the edge of the left of the window you'll see that the startEdgeOffset calculation is incorrect (the boundary will return 12, but the start edge will return a value that is much larger). Each of the values here should return coordinate values transformed in such a way that a zoomed in window should have its origin at the top left.

As a result, opening a dropdown when the trigger is partially off screen won't do the following (aka preserve the visibility of the dropdown by keeping it in the bounds of the screen)
<img width="707" height="651" alt="image" src="https://github.com/user-attachments/assets/cfc4dc78-bdd2-468d-b287-6f2e8270c024" />

instead this happens, where the dropdown falls outside of its defined boundary
<img width="547" height="617" alt="image" src="https://github.com/user-attachments/assets/681601f1-7e51-47a1-b3aa-c2e4296cf443" />


I see the original issue you alluded to in your description, but I think the calculated boundary edges are also incorrect in that story (they currently return 0 and 300 respectively but should be the left and right edge of the box with respect to the top left corner of the story). 

In addition, the `startEdgeOffset` should just be equal to the `offset` in that case since that value is already in the proper coordinate system, but at the moment it is receiving a `containerOffsetWithBoundary` that has negative values that should instead be offsetting the value of `boundaryDimensions` (aka `containerOffsetWithBoundary` is representing how far offset the `boundary` element is with respect to the containing element aka the page). I think we'll need to
1.  fix the  `boundaryStartEdge` and `boundaryEndEdge` calcs here
2. Change it to be `- containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];` or update how `containerOffsetWithBoundary` is being calculated to return positive values (this one is probably more correct if we consider the top left corner to be the `0,0` origin and there for the offset should be positive instead of negative if the container is  centered in the page)

Code after:
    width: node.scrollWidth,
    height: node.scrollHeight
  };
}

// Determines the amount of space required when moving the overlay to ensure it remains in the boundary
function getDelta(
  axis: Axis,
  offset: number,
  size: number,
  // The dimensions of the boundary element that the popover is
  // positioned within (most of the time this is the <body>).
  boundaryDimensions: Dimensions,
  // The dimensions of the containing block element that the popover is
  // positioned relative to (e.g. parent with position: relative).
  // Usually this is the same as the boundary element, but if the popover
  // is portaled somewhere other than the body and has an ancestor with
  // position: relative/absolute, it will be different.
  containerDimensions: Dimensions,
  padding: number,
  containerOffsetWithBoundary: Offset
) {
  let containerScroll = containerDimensions.scroll[axis] ?? 0;
  // The height/width of the boundary. Matches the axis along which we are adjusting the overlay position
  let boundarySize = boundaryDimensions[AXIS_SIZE[axis]];
  // Calculate the edges of the boundary (accomodating for the boundary padding) and the edges of the overlay.
  // Note that these values are with respect to the visual viewport (aka 0,0 is the top left of the viewport)

  let boundaryStartEdge =  containerOffsetWithBoundary[axis] + boundaryDimensions.scroll[AXIS[axis]] +  padding;
  let boundaryEndEdge =  containerOffsetWithBoundary[axis] + boundaryDimensions.scroll[AXIS[axis]] + boundarySize - padding;
  // transformed value of the left edge of the overlay
  let startEdgeOffset = offset - containerScroll + boundaryDimensions.scroll[AXIS[axis]] + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];
  // transformed value of the right edge of the overlay
  let endEdgeOffset = offset - containerScroll + size + boundaryDimensions.scroll[AXIS[axis]]  + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];

  // If any of the overlay edges falls outside of the boundary, shift the overlay the required amount to align one of the overlay's
  // edges with the closest boundary edge.
  if (startEdgeOffset < boundaryStartEdge) {
    return boundaryStartEdge - startEdgeOffset;
  } else if (endEdgeOffset > boundaryEndEdge) {
    return Math.max(boundaryEndEdge - endEdgeOffset, boundaryStartEdge - startEdgeOffset);
  } else {
    return 0;
  }
}

function getMargins(node: Element): Position {
  let style = window.getComputedStyle(node);
  return {
    top: parseInt(style.marginTop, 10) || 0,
    bottom: parseInt(style.marginBottom, 10) || 0,