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

Code before:
  let scrollPaddingTopNumber = parseInt(scrollPaddingTop, 10) || 0;
  let scrollPaddingBottomNumber = parseInt(scrollPaddingBottom, 10) || 0;
  let scrollPaddingRightNumber = parseInt(scrollPaddingRight, 10) || 0;
  let scrollPaddingLeftNumber = parseInt(scrollPaddingLeft, 10) || 0;

  if (offsetX <= x + scrollPaddingLeftNumber) {
    x = offsetX - parseInt(borderLeftWidth, 10) - scrollPaddingLeftNumber;
  } else if (offsetX + width > maxX - scrollPaddingRightNumber) {
    x += offsetX + width - maxX + scrollPaddingRightNumber;
  }
  if (offsetY <= borderAdjustedY + scrollPaddingTopNumber) {
    y = offsetY - parseInt(borderTopWidth, 10) - scrollPaddingTopNumber;
  } else if (offsetY + height > maxY - scrollPaddingBottomNumber) {
    y += offsetY + height - maxY + scrollPaddingBottomNumber;
  }

  scrollView.scrollLeft = x;
  scrollView.scrollTop = y;
}

/**
 * Computes the offset left or top from child to ancestor by accumulating
 * offsetLeft or offsetTop through intervening offsetParents.
 */
function relativeOffset(ancestor: HTMLElement, child: HTMLElement, axis: 'left'|'top') {
  let childRect = child.getBoundingClientRect();
  let ancestorRect = ancestor.getBoundingClientRect();
  
  let viewportOffset = axis === 'left' 
      ? childRect.left - ancestorRect.left 
      : childRect.top - ancestorRect.top;

  let scrollAdjustment = axis === 'left' 
      ? ancestor.scrollLeft 
      : ancestor.scrollTop;

  return viewportOffset + scrollAdjustment;
}

/**
 * Scrolls the `targetElement` so it is visible in the viewport. Accepts an optional `opts.containingElement`
 * that will be centered in the viewport prior to scrolling the targetElement into view. If scrolling is prevented on
 * the body (e.g. targetElement is in a popover), this will only scroll the scroll parents of the targetElement up to but not including the body itself.
 */
export function scrollIntoViewport(targetElement: Element | null, opts?: ScrollIntoViewportOpts) {
  if (targetElement && document.contains(targetElement)) {
    let root = document.scrollingElement || document.documentElement;
    let isScrollPrevented = window.getComputedStyle(root).overflow === 'hidden';
    // If scrolling is not currently prevented then we aren’t in a overlay nor is a overlay open, just use element.scrollIntoView to bring the element into view
    if (!isScrollPrevented) {
      let {left: originalLeft, top: originalTop} = targetElement.getBoundingClientRect();

Review comment (performance):
We're a little nervous about this change. We think it's missing some cases. Looking at some other libraries, there is a fair amount of complexity in determining this. For example https://www.npmjs.com/package/compute-scroll-into-view

We might be able to use this library, though are reluctant to bring in the size increase. So the thing we need to prove is that this change is "enough" for our use cases or decide we're ok with the size increase.

Code after:
  let scrollPaddingTopNumber = parseInt(scrollPaddingTop, 10) || 0;
  let scrollPaddingBottomNumber = parseInt(scrollPaddingBottom, 10) || 0;
  let scrollPaddingRightNumber = parseInt(scrollPaddingRight, 10) || 0;
  let scrollPaddingLeftNumber = parseInt(scrollPaddingLeft, 10) || 0;

  if (offsetX <= x + scrollPaddingLeftNumber) {
    x = offsetX - parseInt(borderLeftWidth, 10) - scrollPaddingLeftNumber;
  } else if (offsetX + width > maxX - scrollPaddingRightNumber) {
    x += offsetX + width - maxX + scrollPaddingRightNumber;
  }
  if (offsetY <= borderAdjustedY + scrollPaddingTopNumber) {
    y = offsetY - parseInt(borderTopWidth, 10) - scrollPaddingTopNumber;
  } else if (offsetY + height > maxY - scrollPaddingBottomNumber) {
    y += offsetY + height - maxY + scrollPaddingBottomNumber;
  }

  scrollView.scrollLeft = x;
  scrollView.scrollTop = y;
}

/**
 * Computes the offset left or top from child to ancestor by accumulating
 * offsetLeft or offsetTop through intervening offsetParents.
 */
function relativeOffset(ancestor: HTMLElement, child: HTMLElement, axis: 'left'|'top') {
  let childRect = child.getBoundingClientRect();
  let ancestorRect = ancestor.getBoundingClientRect();
  
  let viewportOffset = axis === 'left' 
      ? childRect.left - ancestorRect.left 
      : childRect.top - ancestorRect.top;

  let scrollAdjustment = axis === 'left' 
      ? ancestor.scrollLeft 
      : ancestor.scrollTop;

  return viewportOffset + scrollAdjustment;
}

/**
 * Scrolls the `targetElement` so it is visible in the viewport. Accepts an optional `opts.containingElement`
 * that will be centered in the viewport prior to scrolling the targetElement into view. If scrolling is prevented on
 * the body (e.g. targetElement is in a popover), this will only scroll the scroll parents of the targetElement up to but not including the body itself.
 */
export function scrollIntoViewport(targetElement: Element | null, opts?: ScrollIntoViewportOpts): void {
  if (targetElement && document.contains(targetElement)) {
    let root = document.scrollingElement || document.documentElement;
    let isScrollPrevented = window.getComputedStyle(root).overflow === 'hidden';
    // If scrolling is not currently prevented then we aren’t in a overlay nor is a overlay open, just use element.scrollIntoView to bring the element into view
    if (!isScrollPrevented) {
      let {left: originalLeft, top: originalTop} = targetElement.getBoundingClientRect();