From 82a863e91b83aaf7efc9d0f9ac3d8e849e58dbf4 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Wed, 11 Mar 2026 13:26:39 -0700 Subject: [PATCH 1/3] fix: properly scroll body if keyboard focusing a item with no other scroll parents --- packages/@react-aria/utils/src/getScrollParents.ts | 7 +++++-- packages/@react-aria/utils/src/scrollIntoView.ts | 6 +++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/@react-aria/utils/src/getScrollParents.ts b/packages/@react-aria/utils/src/getScrollParents.ts index 7266229339a..0b98228f817 100644 --- a/packages/@react-aria/utils/src/getScrollParents.ts +++ b/packages/@react-aria/utils/src/getScrollParents.ts @@ -16,12 +16,15 @@ export function getScrollParents(node: Element, checkForOverflow?: boolean): Ele let parentElements: Element[] = []; let root = document.scrollingElement || document.documentElement; - do { + while (node) { if (isScrollable(node, checkForOverflow)) { parentElements.push(node); } + if (node === root) { + break; + } node = node.parentElement as Element; - } while (node && node !== root); + } return parentElements; } diff --git a/packages/@react-aria/utils/src/scrollIntoView.ts b/packages/@react-aria/utils/src/scrollIntoView.ts index 2d69a34bc01..23de1cc82b9 100644 --- a/packages/@react-aria/utils/src/scrollIntoView.ts +++ b/packages/@react-aria/utils/src/scrollIntoView.ts @@ -73,7 +73,7 @@ export function scrollIntoView(scrollView: HTMLElement, element: HTMLElement, op let scrollBarOffsetX = scrollView === root ? 0 : borderLeftWidth + borderRightWidth; let scrollBarOffsetY = scrollView === root ? 0 : borderTopWidth + borderBottomWidth; let scrollBarWidth = scrollView.offsetWidth - scrollView.clientWidth - scrollBarOffsetX; - let scrollBarHeight = scrollView.offsetHeight - scrollView.clientHeight - scrollBarOffsetY; + let scrollBarHeight = scrollView === root ? 0 : scrollView.offsetHeight - scrollView.clientHeight - scrollBarOffsetY; let scrollPortTop = viewTop + borderTopWidth + scrollPaddingTop; let scrollPortBottom = viewBottom - borderBottomWidth - scrollPaddingBottom - scrollBarHeight; @@ -159,9 +159,13 @@ export function scrollIntoViewport(targetElement: Element | null, opts: ScrollIn // Account for sub pixel differences from rounding if ((Math.abs(originalLeft - newLeft) > 1) || (Math.abs(originalTop - newTop) > 1)) { scrollParents = containingElement ? getScrollParents(containingElement, true) : []; + // scroll containing element into view first, then rescroll target element into view like the non chrome flow above for (let scrollParent of scrollParents) { scrollIntoView(scrollParent as HTMLElement, containingElement as HTMLElement, {block: 'center', inline: 'center'}); } + for (let scrollParent of getScrollParents(targetElement, true)) { + scrollIntoView(scrollParent as HTMLElement, targetElement as HTMLElement); + } } } } From b4130a139676d495fff9d1b17dacc2068db9b7c0 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 13 Mar 2026 16:07:00 -0700 Subject: [PATCH 2/3] fix scrollbar width case and add basic tests --- .../@react-aria/utils/src/scrollIntoView.ts | 2 +- .../utils/test/getScrollParents.test.ts | 78 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 packages/@react-aria/utils/test/getScrollParents.test.ts diff --git a/packages/@react-aria/utils/src/scrollIntoView.ts b/packages/@react-aria/utils/src/scrollIntoView.ts index 23de1cc82b9..19da2420b94 100644 --- a/packages/@react-aria/utils/src/scrollIntoView.ts +++ b/packages/@react-aria/utils/src/scrollIntoView.ts @@ -72,7 +72,7 @@ export function scrollIntoView(scrollView: HTMLElement, element: HTMLElement, op let scrollBarOffsetX = scrollView === root ? 0 : borderLeftWidth + borderRightWidth; let scrollBarOffsetY = scrollView === root ? 0 : borderTopWidth + borderBottomWidth; - let scrollBarWidth = scrollView.offsetWidth - scrollView.clientWidth - scrollBarOffsetX; + let scrollBarWidth = scrollView === root ? 0 : scrollView.offsetWidth - scrollView.clientWidth - scrollBarOffsetX; let scrollBarHeight = scrollView === root ? 0 : scrollView.offsetHeight - scrollView.clientHeight - scrollBarOffsetY; let scrollPortTop = viewTop + borderTopWidth + scrollPaddingTop; diff --git a/packages/@react-aria/utils/test/getScrollParents.test.ts b/packages/@react-aria/utils/test/getScrollParents.test.ts new file mode 100644 index 00000000000..f76ce48f4a1 --- /dev/null +++ b/packages/@react-aria/utils/test/getScrollParents.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {getScrollParents} from '../src/getScrollParents'; + +describe('getScrollParents', () => { + let root: Element; + + beforeEach(() => { + root = document.documentElement; + }); + + afterEach(() => { + document.body.innerHTML = ''; + jest.restoreAllMocks(); + }); + + it('includes root as a scroll parent for a node in the document', () => { + let div = document.createElement('div'); + document.body.appendChild(div); + + let parents = getScrollParents(div); + expect(parents).toContain(root); + }); + + it('does not include root when root has overflow: hidden', () => { + let div = document.createElement('div'); + document.body.appendChild(div); + + jest.spyOn(window, 'getComputedStyle').mockImplementation((el) => { + if (el === root) { + return {overflow: 'hidden'} as CSSStyleDeclaration; + } + return {overflow: 'visible'} as CSSStyleDeclaration; + }); + + let parents = getScrollParents(div); + expect(parents).not.toContain(root); + }); + + it('includes a scrollable intermediate parent', () => { + let scrollable = document.createElement('div'); + let child = document.createElement('div'); + document.body.appendChild(scrollable); + scrollable.appendChild(child); + + jest.spyOn(window, 'getComputedStyle').mockImplementation((el) => { + if (el === scrollable) { + return {overflow: 'auto'} as CSSStyleDeclaration; + } + return {overflow: 'visible'} as CSSStyleDeclaration; + }); + + let parents = getScrollParents(child); + expect(parents).toContain(scrollable); + expect(parents).toContain(root); + }); + + it('excludes non-scrollable ancestors', () => { + let plain = document.createElement('div'); + let child = document.createElement('div'); + document.body.appendChild(plain); + plain.appendChild(child); + + let parents = getScrollParents(child); + expect(parents).not.toContain(plain); + expect(parents).not.toContain(document.body); + }); +}); From 200a2c53646a3b2e5967b091be99cecc075373cb Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 13 Mar 2026 16:47:52 -0700 Subject: [PATCH 3/3] tentative fix to accomodate for borders on root when scrolling into view --- packages/@react-aria/utils/src/scrollIntoView.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/@react-aria/utils/src/scrollIntoView.ts b/packages/@react-aria/utils/src/scrollIntoView.ts index 19da2420b94..d19b77bf55e 100644 --- a/packages/@react-aria/utils/src/scrollIntoView.ts +++ b/packages/@react-aria/utils/src/scrollIntoView.ts @@ -44,6 +44,7 @@ export function scrollIntoView(scrollView: HTMLElement, element: HTMLElement, op let itemStyle = window.getComputedStyle(element); let viewStyle = window.getComputedStyle(scrollView); let root = document.scrollingElement || document.documentElement; + let isRoot = scrollView === root; let viewTop = scrollView === root ? 0 : view.top; let viewBottom = scrollView === root ? scrollView.clientHeight : view.bottom; @@ -75,10 +76,10 @@ export function scrollIntoView(scrollView: HTMLElement, element: HTMLElement, op let scrollBarWidth = scrollView === root ? 0 : scrollView.offsetWidth - scrollView.clientWidth - scrollBarOffsetX; let scrollBarHeight = scrollView === root ? 0 : scrollView.offsetHeight - scrollView.clientHeight - scrollBarOffsetY; - let scrollPortTop = viewTop + borderTopWidth + scrollPaddingTop; - let scrollPortBottom = viewBottom - borderBottomWidth - scrollPaddingBottom - scrollBarHeight; - let scrollPortLeft = viewLeft + borderLeftWidth + scrollPaddingLeft; - let scrollPortRight = viewRight - borderRightWidth - scrollPaddingRight; + let scrollPortTop = viewTop + (isRoot ? 0 : borderTopWidth) + scrollPaddingTop; + let scrollPortBottom = viewBottom - (isRoot ? 0 : borderBottomWidth) - scrollPaddingBottom - scrollBarHeight; + let scrollPortLeft = viewLeft + (isRoot ? 0 : borderLeftWidth) + scrollPaddingLeft; + let scrollPortRight = viewRight - (isRoot ? 0 : borderRightWidth) - scrollPaddingRight; // IOS always positions the scrollbar on the right ¯\_(ツ)_/¯ if (viewStyle.direction === 'rtl' && !isIOS()) {