From f506186ae0bab37216628da7218a505ffd76025e Mon Sep 17 00:00:00 2001 From: jsmitrah Date: Tue, 16 Jun 2026 18:38:19 +0530 Subject: [PATCH 1/8] fixed the table keyboard navigation in the scrollIntoViewport. --- .../react-aria/src/utils/scrollIntoView.ts | 67 +++++++++++----- .../test/utils/scrollIntoView.test.ts | 77 ++++++++++++++++--- 2 files changed, 117 insertions(+), 27 deletions(-) diff --git a/packages/react-aria/src/utils/scrollIntoView.ts b/packages/react-aria/src/utils/scrollIntoView.ts index ff975f6c7d0..95e26f298ca 100644 --- a/packages/react-aria/src/utils/scrollIntoView.ts +++ b/packages/react-aria/src/utils/scrollIntoView.ts @@ -10,8 +10,8 @@ * governing permissions and limitations under the License. */ -import {getScrollParents} from './getScrollParents'; -import {isIOS} from './platform'; +import { getScrollParents } from './getScrollParents'; +import { isIOS } from './platform'; interface ScrollIntoViewOpts { /** The position to align items along the block axis in. */ @@ -35,7 +35,7 @@ export function scrollIntoView( element: HTMLElement, opts: ScrollIntoViewOpts = {} ): void { - let {block = 'nearest', inline = 'nearest'} = opts; + let { block = 'nearest', inline = 'nearest' } = opts; if (scrollView === element) { return; @@ -129,7 +129,7 @@ export function scrollIntoView( return; } - scrollView.scrollTo({left: x, top: y}); + scrollView.scrollTo({ left: x, top: y }); } /** @@ -143,40 +143,71 @@ export function scrollIntoViewport( targetElement: Element | null, opts: ScrollIntoViewportOpts = {} ): void { - let {containingElement} = opts; + let { containingElement } = opts; if (targetElement && targetElement.isConnected) { let root = document.scrollingElement || document.documentElement; let isScrollPrevented = window.getComputedStyle(root).overflow === 'hidden'; if (!isScrollPrevented) { - let {left: originalLeft, top: originalTop} = targetElement.getBoundingClientRect(); + let { left: originalLeft, top: originalTop } = targetElement.getBoundingClientRect(); // use scrollIntoView({block: 'nearest'}) instead of .focus to check if the element is fully in view or not since .focus() // won't cause a scroll if the element is already focused and doesn't behave consistently when an element is partially out of view horizontally vs vertically - targetElement?.scrollIntoView?.({block: 'nearest'}); - let {left: newLeft, top: newTop} = targetElement.getBoundingClientRect(); - // Account for sub pixel differences from rounding + targetElement?.scrollIntoView?.({ block: 'nearest' }); + let { left: newLeft, top: newTop } = targetElement.getBoundingClientRect(); + if (Math.abs(originalLeft - newLeft) > 1 || Math.abs(originalTop - newTop) > 1) { - containingElement?.scrollIntoView?.({block: 'center', inline: 'center'}); - targetElement.scrollIntoView?.({block: 'nearest'}); + // --- ADDED FIX 1: Check window viewport boundary --- + let shouldCenter = true; + if (containingElement) { + const containerRect = containingElement.getBoundingClientRect(); + if (containerRect.height > window.innerHeight) { + shouldCenter = false; + } + } + + if (shouldCenter) { + containingElement?.scrollIntoView?.({ block: 'center', inline: 'center' }); + } else { + containingElement?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' }); + } + // --- END FIX 1 --- + + targetElement.scrollIntoView?.({ block: 'nearest' }); } } else { - let {left: originalLeft, top: originalTop} = targetElement.getBoundingClientRect(); + let { left: originalLeft, top: originalTop } = targetElement.getBoundingClientRect(); // If scrolling is prevented, we don't want to scroll the body since it might move the overlay partially offscreen and the user can't scroll it back into view. let scrollParents = getScrollParents(targetElement, true); for (let scrollParent of scrollParents) { scrollIntoView(scrollParent as HTMLElement, targetElement as HTMLElement); } - let {left: newLeft, top: newTop} = targetElement.getBoundingClientRect(); + let { left: newLeft, top: newTop } = targetElement.getBoundingClientRect(); // 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' - }); + // --- ADDED FIX 2: Check custom scroll container boundary --- + const parentEl = scrollParent as HTMLElement; + const containerEl = containingElement as HTMLElement; + + const isContainerSmallerThanViewport = + containerEl && + containerEl.offsetHeight <= parentEl.clientHeight; + + if (isContainerSmallerThanViewport) { + scrollIntoView(parentEl, containerEl, { + block: 'center', + inline: 'center' + }); + } else { + scrollIntoView(parentEl, containerEl, { + block: 'nearest', + inline: 'nearest' + }); + } + // --- END FIX 2 --- } for (let scrollParent of getScrollParents(targetElement, true)) { scrollIntoView(scrollParent as HTMLElement, targetElement as HTMLElement); @@ -184,4 +215,4 @@ export function scrollIntoViewport( } } } -} +} \ No newline at end of file diff --git a/packages/react-aria/test/utils/scrollIntoView.test.ts b/packages/react-aria/test/utils/scrollIntoView.test.ts index c1f567fe839..614ea2060d3 100644 --- a/packages/react-aria/test/utils/scrollIntoView.test.ts +++ b/packages/react-aria/test/utils/scrollIntoView.test.ts @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {scrollIntoView} from '../../src/utils/scrollIntoView'; +import { scrollIntoView } from '../../src/utils/scrollIntoView'; describe('scrollIntoView', () => { let target: HTMLElement; @@ -71,10 +71,10 @@ describe('scrollIntoView', () => { } as CSSStyleDeclaration; }); - Object.defineProperty(scrollView, 'clientHeight', {get: () => 500, configurable: true}); - Object.defineProperty(scrollView, 'clientWidth', {get: () => 500, configurable: true}); + Object.defineProperty(scrollView, 'clientHeight', { get: () => 500, configurable: true }); + Object.defineProperty(scrollView, 'clientWidth', { get: () => 500, configurable: true }); - scrollIntoView(scrollView, target, {block: 'start', inline: 'start'}); + scrollIntoView(scrollView, target, { block: 'start', inline: 'start' }); expect(scrollView.scrollLeft).toBe(100); expect(scrollView.scrollTop).toBe(2100); }); @@ -92,7 +92,7 @@ describe('scrollIntoView', () => { height: 1000, x: 100, y: 2100, - toJSON: () => {} + toJSON: () => { } } as DOMRect); jest.spyOn(window, 'getComputedStyle').mockImplementation(el => { @@ -117,12 +117,71 @@ describe('scrollIntoView', () => { } as CSSStyleDeclaration; }); - Object.defineProperty(scrollView, 'clientHeight', {get: () => 500, configurable: true}); - Object.defineProperty(scrollView, 'clientWidth', {get: () => 500, configurable: true}); + Object.defineProperty(scrollView, 'clientHeight', { get: () => 500, configurable: true }); + Object.defineProperty(scrollView, 'clientWidth', { get: () => 500, configurable: true }); - scrollIntoView(scrollView, target, {block: 'end', inline: 'end'}); + scrollIntoView(scrollView, target, { block: 'end', inline: 'end' }); expect(scrollView.scrollLeft).toBe(600); expect(scrollView.scrollTop).toBe(2600); }); }); -}); + + // ========================================== + // NEW TEST BLOCK FOR TABLE SCROLLING + // ========================================== + describe('scrollIntoViewport', () => { + let containingElement: HTMLElement; + + beforeEach(() => { + containingElement = document.createElement('div'); + document.body.appendChild(containingElement); + containingElement.appendChild(target); + + // Mock target connections to look active in DOM + Object.defineProperty(target, 'isConnected', { get: () => true }); + }); + + it('should fall back to nearest block alignment if containingElement is larger than the viewport', () => { + const scrollIntoViewSpy = jest.fn(); + containingElement.scrollIntoView = scrollIntoViewSpy; + target.scrollIntoView = jest.fn(); + + // Mock window height + const originalInnerHeight = window.innerHeight; + window.innerHeight = 500; + + // Mock container element to be taller than the window viewport (e.g., 2000px) + jest.spyOn(containingElement, 'getBoundingClientRect').mockReturnValue({ + height: 2000, + top: 0, + bottom: 2000, + left: 0, + right: 1000, + width: 1000 + } as DOMRect); + + // Force a positional shift to trigger container alignment block + let getBoundingClientRectCallCount = 0; + jest.spyOn(target, 'getBoundingClientRect').mockImplementation(() => { + getBoundingClientRectCallCount++; + // Return changing coordinates to trick the layout difference checker + return { + top: getBoundingClientRectCallCount === 1 ? 100 : 200, + left: 0, + right: 100, + bottom: 200, + width: 100, + height: 100 + } as DOMRect; + }); + + scrollIntoViewport(target, { containingElement }); + + // Verification: Ensure it used 'nearest' rather than 'center' because it was too large + expect(scrollIntoViewSpy).toHaveBeenCalledWith({ block: 'nearest', inline: 'nearest' }); + + // Clean up global mock + window.innerHeight = originalInnerHeight; + }); + }); +}); \ No newline at end of file From 92c0553b4aa116520084cdd45aaffae213b9d072 Mon Sep 17 00:00:00 2001 From: jsmitrah Date: Tue, 16 Jun 2026 19:51:10 +0530 Subject: [PATCH 2/8] fix: resolve lint and CI test compilation errors --- .../react-aria/src/utils/scrollIntoView.ts | 111 ++++++++---------- .../test/utils/scrollIntoView.test.ts | 2 +- 2 files changed, 51 insertions(+), 62 deletions(-) diff --git a/packages/react-aria/src/utils/scrollIntoView.ts b/packages/react-aria/src/utils/scrollIntoView.ts index 95e26f298ca..02b0672b0e0 100644 --- a/packages/react-aria/src/utils/scrollIntoView.ts +++ b/packages/react-aria/src/utils/scrollIntoView.ts @@ -144,74 +144,63 @@ export function scrollIntoViewport( opts: ScrollIntoViewportOpts = {} ): void { let { containingElement } = opts; - if (targetElement && targetElement.isConnected) { - let root = document.scrollingElement || document.documentElement; - let isScrollPrevented = window.getComputedStyle(root).overflow === 'hidden'; - if (!isScrollPrevented) { - let { left: originalLeft, top: originalTop } = targetElement.getBoundingClientRect(); - - // use scrollIntoView({block: 'nearest'}) instead of .focus to check if the element is fully in view or not since .focus() - // won't cause a scroll if the element is already focused and doesn't behave consistently when an element is partially out of view horizontally vs vertically - targetElement?.scrollIntoView?.({ block: 'nearest' }); - let { left: newLeft, top: newTop } = targetElement.getBoundingClientRect(); - - if (Math.abs(originalLeft - newLeft) > 1 || Math.abs(originalTop - newTop) > 1) { - // --- ADDED FIX 1: Check window viewport boundary --- - let shouldCenter = true; - if (containingElement) { - const containerRect = containingElement.getBoundingClientRect(); - if (containerRect.height > window.innerHeight) { - shouldCenter = false; - } - } + if (!targetElement || !targetElement.isConnected) { + return; + } - if (shouldCenter) { - containingElement?.scrollIntoView?.({ block: 'center', inline: 'center' }); - } else { - containingElement?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' }); + let root = document.scrollingElement || document.documentElement; + let isScrollPrevented = window.getComputedStyle(root).overflow === 'hidden'; + + if (!isScrollPrevented) { + let { left: originalLeft, top: originalTop } = targetElement.getBoundingClientRect(); + targetElement.scrollIntoView?.({ block: 'nearest' }); + let { left: newLeft, top: newTop } = targetElement.getBoundingClientRect(); + + if (Math.abs(originalLeft - newLeft) > 1 || Math.abs(originalTop - newTop) > 1) { + let shouldCenter = true; + if (containingElement) { + const containerRect = containingElement.getBoundingClientRect(); + if (containerRect.height > window.innerHeight) { + shouldCenter = false; } - // --- END FIX 1 --- + } - targetElement.scrollIntoView?.({ block: 'nearest' }); + if (shouldCenter) { + containingElement?.scrollIntoView?.({ block: 'center', inline: 'center' }); + } else { + containingElement?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' }); } - } else { - let { left: originalLeft, top: originalTop } = targetElement.getBoundingClientRect(); - // If scrolling is prevented, we don't want to scroll the body since it might move the overlay partially offscreen and the user can't scroll it back into view. - let scrollParents = getScrollParents(targetElement, true); + targetElement.scrollIntoView?.({ block: 'nearest' }); + } + } else { + let { left: originalLeft, top: originalTop } = targetElement.getBoundingClientRect(); + let scrollParents = getScrollParents(targetElement, true); + + for (let scrollParent of scrollParents) { + scrollIntoView(scrollParent as HTMLElement, targetElement as HTMLElement); + } + let { left: newLeft, top: newTop } = targetElement.getBoundingClientRect(); + + if (Math.abs(originalLeft - newLeft) > 1 || Math.abs(originalTop - newTop) > 1) { + scrollParents = containingElement ? getScrollParents(containingElement, true) : []; + for (let scrollParent of scrollParents) { - scrollIntoView(scrollParent as HTMLElement, targetElement as HTMLElement); + const parentEl = scrollParent as HTMLElement; + const containerEl = containingElement as HTMLElement; + + // Flattening the code: Calculate values outside to stay under max-depth limits + const isSmall = containerEl && containerEl.offsetHeight <= parentEl.clientHeight; + const targetBlock = isSmall ? 'center' : 'nearest'; + const targetInline = isSmall ? 'center' : 'nearest'; + + scrollIntoView(parentEl, containerEl, { + block: targetBlock, + inline: targetInline + }); } - let { left: newLeft, top: newTop } = targetElement.getBoundingClientRect(); - // 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) { - // --- ADDED FIX 2: Check custom scroll container boundary --- - const parentEl = scrollParent as HTMLElement; - const containerEl = containingElement as HTMLElement; - - const isContainerSmallerThanViewport = - containerEl && - containerEl.offsetHeight <= parentEl.clientHeight; - - if (isContainerSmallerThanViewport) { - scrollIntoView(parentEl, containerEl, { - block: 'center', - inline: 'center' - }); - } else { - scrollIntoView(parentEl, containerEl, { - block: 'nearest', - inline: 'nearest' - }); - } - // --- END FIX 2 --- - } - for (let scrollParent of getScrollParents(targetElement, true)) { - scrollIntoView(scrollParent as HTMLElement, targetElement as HTMLElement); - } + for (let scrollParent of getScrollParents(targetElement, true)) { + scrollIntoView(scrollParent as HTMLElement, targetElement as HTMLElement); } } } diff --git a/packages/react-aria/test/utils/scrollIntoView.test.ts b/packages/react-aria/test/utils/scrollIntoView.test.ts index 614ea2060d3..649653dd685 100644 --- a/packages/react-aria/test/utils/scrollIntoView.test.ts +++ b/packages/react-aria/test/utils/scrollIntoView.test.ts @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import { scrollIntoView } from '../../src/utils/scrollIntoView'; +import { scrollIntoView, scrollIntoViewport } from '../../src/utils/scrollIntoView'; describe('scrollIntoView', () => { let target: HTMLElement; From e23e1b3a7f8b2e32ca81a314f841d7d399402ef7 Mon Sep 17 00:00:00 2001 From: jsmitrah Date: Tue, 16 Jun 2026 20:11:07 +0530 Subject: [PATCH 3/8] style: fix formatting layout rules --- .../react-aria/src/utils/scrollIntoView.ts | 28 +++++++++---------- .../test/utils/scrollIntoView.test.ts | 24 ++++++++-------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/react-aria/src/utils/scrollIntoView.ts b/packages/react-aria/src/utils/scrollIntoView.ts index 02b0672b0e0..aa39b8f74e2 100644 --- a/packages/react-aria/src/utils/scrollIntoView.ts +++ b/packages/react-aria/src/utils/scrollIntoView.ts @@ -10,8 +10,8 @@ * governing permissions and limitations under the License. */ -import { getScrollParents } from './getScrollParents'; -import { isIOS } from './platform'; +import {getScrollParents} from './getScrollParents'; +import {isIOS} from './platform'; interface ScrollIntoViewOpts { /** The position to align items along the block axis in. */ @@ -35,7 +35,7 @@ export function scrollIntoView( element: HTMLElement, opts: ScrollIntoViewOpts = {} ): void { - let { block = 'nearest', inline = 'nearest' } = opts; + let {block = 'nearest', inline = 'nearest'} = opts; if (scrollView === element) { return; @@ -129,7 +129,7 @@ export function scrollIntoView( return; } - scrollView.scrollTo({ left: x, top: y }); + scrollView.scrollTo({left: x, top: y}); } /** @@ -143,7 +143,7 @@ export function scrollIntoViewport( targetElement: Element | null, opts: ScrollIntoViewportOpts = {} ): void { - let { containingElement } = opts; + let {containingElement} = opts; if (!targetElement || !targetElement.isConnected) { return; } @@ -152,9 +152,9 @@ export function scrollIntoViewport( let isScrollPrevented = window.getComputedStyle(root).overflow === 'hidden'; if (!isScrollPrevented) { - let { left: originalLeft, top: originalTop } = targetElement.getBoundingClientRect(); - targetElement.scrollIntoView?.({ block: 'nearest' }); - let { left: newLeft, top: newTop } = targetElement.getBoundingClientRect(); + let {left: originalLeft, top: originalTop} = targetElement.getBoundingClientRect(); + targetElement.scrollIntoView?.({block: 'nearest'}); + let {left: newLeft, top: newTop} = targetElement.getBoundingClientRect(); if (Math.abs(originalLeft - newLeft) > 1 || Math.abs(originalTop - newTop) > 1) { let shouldCenter = true; @@ -166,21 +166,21 @@ export function scrollIntoViewport( } if (shouldCenter) { - containingElement?.scrollIntoView?.({ block: 'center', inline: 'center' }); + containingElement?.scrollIntoView?.({block: 'center', inline: 'center'}); } else { - containingElement?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' }); + containingElement?.scrollIntoView?.({block: 'nearest', inline: 'nearest'}); } - targetElement.scrollIntoView?.({ block: 'nearest' }); + targetElement.scrollIntoView?.({block: 'nearest'}); } } else { - let { left: originalLeft, top: originalTop } = targetElement.getBoundingClientRect(); + let {left: originalLeft, top: originalTop} = targetElement.getBoundingClientRect(); let scrollParents = getScrollParents(targetElement, true); for (let scrollParent of scrollParents) { scrollIntoView(scrollParent as HTMLElement, targetElement as HTMLElement); } - let { left: newLeft, top: newTop } = targetElement.getBoundingClientRect(); + let {left: newLeft, top: newTop} = targetElement.getBoundingClientRect(); if (Math.abs(originalLeft - newLeft) > 1 || Math.abs(originalTop - newTop) > 1) { scrollParents = containingElement ? getScrollParents(containingElement, true) : []; @@ -204,4 +204,4 @@ export function scrollIntoViewport( } } } -} \ No newline at end of file +} diff --git a/packages/react-aria/test/utils/scrollIntoView.test.ts b/packages/react-aria/test/utils/scrollIntoView.test.ts index 649653dd685..e783a1503bd 100644 --- a/packages/react-aria/test/utils/scrollIntoView.test.ts +++ b/packages/react-aria/test/utils/scrollIntoView.test.ts @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import { scrollIntoView, scrollIntoViewport } from '../../src/utils/scrollIntoView'; +import {scrollIntoView, scrollIntoViewport} from '../../src/utils/scrollIntoView'; describe('scrollIntoView', () => { let target: HTMLElement; @@ -71,10 +71,10 @@ describe('scrollIntoView', () => { } as CSSStyleDeclaration; }); - Object.defineProperty(scrollView, 'clientHeight', { get: () => 500, configurable: true }); - Object.defineProperty(scrollView, 'clientWidth', { get: () => 500, configurable: true }); + Object.defineProperty(scrollView, 'clientHeight', {get: () => 500, configurable: true}); + Object.defineProperty(scrollView, 'clientWidth', {get: () => 500, configurable: true}); - scrollIntoView(scrollView, target, { block: 'start', inline: 'start' }); + scrollIntoView(scrollView, target, {block: 'start', inline: 'start'}); expect(scrollView.scrollLeft).toBe(100); expect(scrollView.scrollTop).toBe(2100); }); @@ -92,7 +92,7 @@ describe('scrollIntoView', () => { height: 1000, x: 100, y: 2100, - toJSON: () => { } + toJSON: () => {} } as DOMRect); jest.spyOn(window, 'getComputedStyle').mockImplementation(el => { @@ -117,10 +117,10 @@ describe('scrollIntoView', () => { } as CSSStyleDeclaration; }); - Object.defineProperty(scrollView, 'clientHeight', { get: () => 500, configurable: true }); - Object.defineProperty(scrollView, 'clientWidth', { get: () => 500, configurable: true }); + Object.defineProperty(scrollView, 'clientHeight', {get: () => 500, configurable: true}); + Object.defineProperty(scrollView, 'clientWidth', {get: () => 500, configurable: true}); - scrollIntoView(scrollView, target, { block: 'end', inline: 'end' }); + scrollIntoView(scrollView, target, {block: 'end', inline: 'end'}); expect(scrollView.scrollLeft).toBe(600); expect(scrollView.scrollTop).toBe(2600); }); @@ -138,7 +138,7 @@ describe('scrollIntoView', () => { containingElement.appendChild(target); // Mock target connections to look active in DOM - Object.defineProperty(target, 'isConnected', { get: () => true }); + Object.defineProperty(target, 'isConnected', {get: () => true}); }); it('should fall back to nearest block alignment if containingElement is larger than the viewport', () => { @@ -175,13 +175,13 @@ describe('scrollIntoView', () => { } as DOMRect; }); - scrollIntoViewport(target, { containingElement }); + scrollIntoViewport(target, {containingElement}); // Verification: Ensure it used 'nearest' rather than 'center' because it was too large - expect(scrollIntoViewSpy).toHaveBeenCalledWith({ block: 'nearest', inline: 'nearest' }); + expect(scrollIntoViewSpy).toHaveBeenCalledWith({block: 'nearest', inline: 'nearest'}); // Clean up global mock window.innerHeight = originalInnerHeight; }); }); -}); \ No newline at end of file +}); From 42f3a0a56183a7794748defb8f702d2dd902b99a Mon Sep 17 00:00:00 2001 From: jsmitrah Date: Tue, 16 Jun 2026 20:42:19 +0530 Subject: [PATCH 4/8] build: clear remote builder cache to resolve parcel panic From ed8c432b34f2f35d849ad3b0fa4c4cc9b8a30178 Mon Sep 17 00:00:00 2001 From: jsmitrah Date: Wed, 17 Jun 2026 12:16:20 +0530 Subject: [PATCH 5/8] fix: handled the scroll element based on dimension. --- .../react-aria/src/utils/scrollIntoView.ts | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/packages/react-aria/src/utils/scrollIntoView.ts b/packages/react-aria/src/utils/scrollIntoView.ts index aa39b8f74e2..d7e774516d1 100644 --- a/packages/react-aria/src/utils/scrollIntoView.ts +++ b/packages/react-aria/src/utils/scrollIntoView.ts @@ -157,20 +157,27 @@ export function scrollIntoViewport( let {left: newLeft, top: newTop} = targetElement.getBoundingClientRect(); if (Math.abs(originalLeft - newLeft) > 1 || Math.abs(originalTop - newTop) > 1) { - let shouldCenter = true; + let blockOption: ScrollLogicalPosition = 'center'; + let inlineOption: ScrollLogicalPosition = 'center'; + if (containingElement) { const containerRect = containingElement.getBoundingClientRect(); - if (containerRect.height > window.innerHeight) { - shouldCenter = false; + // Check if the containing element is already visible within the viewport window boundaries + const isContainerVisible = + containerRect.top >= 0 && + containerRect.left >= 0 && + containerRect.bottom <= window.innerHeight && + containerRect.right <= window.innerWidth; + + // If it's already visible, fallback to 'nearest' to prevent violent jumps. + // If its dimensions exceed the window, 'center' will break sticky layouts, so use 'nearest'. + if (isContainerVisible || containerRect.height > window.innerHeight) { + blockOption = 'nearest'; + inlineOption = 'nearest'; } } - if (shouldCenter) { - containingElement?.scrollIntoView?.({block: 'center', inline: 'center'}); - } else { - containingElement?.scrollIntoView?.({block: 'nearest', inline: 'nearest'}); - } - + containingElement?.scrollIntoView?.({block: blockOption, inline: inlineOption}); targetElement.scrollIntoView?.({block: 'nearest'}); } } else { @@ -189,10 +196,21 @@ export function scrollIntoViewport( const parentEl = scrollParent as HTMLElement; const containerEl = containingElement as HTMLElement; - // Flattening the code: Calculate values outside to stay under max-depth limits - const isSmall = containerEl && containerEl.offsetHeight <= parentEl.clientHeight; - const targetBlock = isSmall ? 'center' : 'nearest'; - const targetInline = isSmall ? 'center' : 'nearest'; + const containerRect = containerEl.getBoundingClientRect(); + const parentRect = parentEl.getBoundingClientRect(); + + // Check visibility within the specific scroll parent container bounds + const isContainerVisibleInParent = + containerRect.top >= parentRect.top && + containerRect.left >= parentRect.left && + containerRect.bottom <= parentRect.bottom && + containerRect.right <= parentRect.right; + + const isLarge = containerEl.offsetHeight > parentEl.clientHeight; + const useNearest = isContainerVisibleInParent || isLarge; + + const targetBlock: ScrollLogicalPosition = useNearest ? 'nearest' : 'center'; + const targetInline: ScrollLogicalPosition = useNearest ? 'nearest' : 'center'; scrollIntoView(parentEl, containerEl, { block: targetBlock, From 651fe79cb21eda9cef641b623716fae90378bd8c Mon Sep 17 00:00:00 2001 From: jsmitrah Date: Fri, 19 Jun 2026 19:23:53 +0530 Subject: [PATCH 6/8] fix: nested scrollparent boundaries in scrollIntoViewport --- .../react-aria/src/utils/scrollIntoView.ts | 74 +++++++++++++------ 1 file changed, 53 insertions(+), 21 deletions(-) diff --git a/packages/react-aria/src/utils/scrollIntoView.ts b/packages/react-aria/src/utils/scrollIntoView.ts index d7e774516d1..6ce909a2bae 100644 --- a/packages/react-aria/src/utils/scrollIntoView.ts +++ b/packages/react-aria/src/utils/scrollIntoView.ts @@ -151,6 +151,51 @@ export function scrollIntoViewport( let root = document.scrollingElement || document.documentElement; let isScrollPrevented = window.getComputedStyle(root).overflow === 'hidden'; + // Helper utility to verify if a child element is fully within the visible boundaries of an ancestor + const isElementVisibleInAncestor = (child: Element, ancestor: Element): boolean => { + const childRect = child.getBoundingClientRect(); + const ancestorRect = ancestor.getBoundingClientRect(); + + return ( + childRect.top >= ancestorRect.top && + childRect.left >= ancestorRect.left && + childRect.bottom <= ancestorRect.bottom && + childRect.right <= ancestorRect.right + ); + }; + + // Helper utility to check if the containingElement is obscured by ANY parent scroll container + const isContainerFullyVisible = (container: Element): boolean => { + const containerRect = container.getBoundingClientRect(); + + // 1. Check against the global viewport window boundaries first + const visibleInWindow = + containerRect.top >= 0 && + containerRect.left >= 0 && + containerRect.bottom <= window.innerHeight && + containerRect.right <= window.innerWidth; + + if (!visibleInWindow) { + return false; + } + + // 2. Check against every single outer scroll parent layer to find hidden clips + const structuralParents = getScrollParents(container, true); + for (let parent of structuralParents) { + // Avoid comparing against the main document layout elements as they are covered by the window check + if ( + parent !== document.body && + parent !== document.documentElement && + parent !== document.scrollingElement + ) { + if (!isElementVisibleInAncestor(container, parent as Element)) { + return false; + } + } + } + return true; + }; + if (!isScrollPrevented) { let {left: originalLeft, top: originalTop} = targetElement.getBoundingClientRect(); targetElement.scrollIntoView?.({block: 'nearest'}); @@ -162,16 +207,11 @@ export function scrollIntoViewport( if (containingElement) { const containerRect = containingElement.getBoundingClientRect(); - // Check if the containing element is already visible within the viewport window boundaries - const isContainerVisible = - containerRect.top >= 0 && - containerRect.left >= 0 && - containerRect.bottom <= window.innerHeight && - containerRect.right <= window.innerWidth; - - // If it's already visible, fallback to 'nearest' to prevent violent jumps. - // If its dimensions exceed the window, 'center' will break sticky layouts, so use 'nearest'. - if (isContainerVisible || containerRect.height > window.innerHeight) { + const isFullyVisible = isContainerFullyVisible(containingElement); + const isExceedingViewport = containerRect.height > window.innerHeight; + + // If it's already visible in all scroll contexts, or it's simply too large to center without breaking layouts, use 'nearest' + if (isFullyVisible || isExceedingViewport) { blockOption = 'nearest'; inlineOption = 'nearest'; } @@ -197,18 +237,10 @@ export function scrollIntoViewport( const containerEl = containingElement as HTMLElement; const containerRect = containerEl.getBoundingClientRect(); - const parentRect = parentEl.getBoundingClientRect(); - - // Check visibility within the specific scroll parent container bounds - const isContainerVisibleInParent = - containerRect.top >= parentRect.top && - containerRect.left >= parentRect.left && - containerRect.bottom <= parentRect.bottom && - containerRect.right <= parentRect.right; - - const isLarge = containerEl.offsetHeight > parentEl.clientHeight; - const useNearest = isContainerVisibleInParent || isLarge; + const isFullyVisible = isContainerFullyVisible(containerEl); + const isLarge = containerRect.height > parentEl.clientHeight; + const useNearest = isFullyVisible || isLarge; const targetBlock: ScrollLogicalPosition = useNearest ? 'nearest' : 'center'; const targetInline: ScrollLogicalPosition = useNearest ? 'nearest' : 'center'; From 0c2c796ca67d0f5e200ddcd13c9c1e3194c42883 Mon Sep 17 00:00:00 2001 From: jsmitrah Date: Fri, 19 Jun 2026 19:38:28 +0530 Subject: [PATCH 7/8] fix: move helper functions out of function block scope to pass build compilation --- .../react-aria/src/utils/scrollIntoView.ts | 90 +++++++++---------- 1 file changed, 44 insertions(+), 46 deletions(-) diff --git a/packages/react-aria/src/utils/scrollIntoView.ts b/packages/react-aria/src/utils/scrollIntoView.ts index 6ce909a2bae..3480aaf5d11 100644 --- a/packages/react-aria/src/utils/scrollIntoView.ts +++ b/packages/react-aria/src/utils/scrollIntoView.ts @@ -132,6 +132,50 @@ export function scrollIntoView( scrollView.scrollTo({left: x, top: y}); } +// Helper utility to verify if a child element is fully within the visible boundaries of an ancestor +function isElementVisibleInAncestor(child: Element, ancestor: Element): boolean { + const childRect = child.getBoundingClientRect(); + const ancestorRect = ancestor.getBoundingClientRect(); + + return ( + childRect.top >= ancestorRect.top && + childRect.left >= ancestorRect.left && + childRect.bottom <= ancestorRect.bottom && + childRect.right <= ancestorRect.right + ); +} + +// Helper utility to check if the containingElement is obscured by ANY parent scroll container +function isContainerFullyVisible(container: Element): boolean { + const containerRect = container.getBoundingClientRect(); + + // 1. Check against global viewport boundaries + const visibleInWindow = + containerRect.top >= 0 && + containerRect.left >= 0 && + containerRect.bottom <= window.innerHeight && + containerRect.right <= window.innerWidth; + + if (!visibleInWindow) { + return false; + } + + // 2. Check against every single outer scroll parent layer + const structuralParents = getScrollParents(container, true); + for (let parent of structuralParents) { + if ( + parent !== document.body && + parent !== document.documentElement && + parent !== document.scrollingElement + ) { + if (!isElementVisibleInAncestor(container, parent as Element)) { + return false; + } + } + } + return true; +} + /** * 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 @@ -151,51 +195,6 @@ export function scrollIntoViewport( let root = document.scrollingElement || document.documentElement; let isScrollPrevented = window.getComputedStyle(root).overflow === 'hidden'; - // Helper utility to verify if a child element is fully within the visible boundaries of an ancestor - const isElementVisibleInAncestor = (child: Element, ancestor: Element): boolean => { - const childRect = child.getBoundingClientRect(); - const ancestorRect = ancestor.getBoundingClientRect(); - - return ( - childRect.top >= ancestorRect.top && - childRect.left >= ancestorRect.left && - childRect.bottom <= ancestorRect.bottom && - childRect.right <= ancestorRect.right - ); - }; - - // Helper utility to check if the containingElement is obscured by ANY parent scroll container - const isContainerFullyVisible = (container: Element): boolean => { - const containerRect = container.getBoundingClientRect(); - - // 1. Check against the global viewport window boundaries first - const visibleInWindow = - containerRect.top >= 0 && - containerRect.left >= 0 && - containerRect.bottom <= window.innerHeight && - containerRect.right <= window.innerWidth; - - if (!visibleInWindow) { - return false; - } - - // 2. Check against every single outer scroll parent layer to find hidden clips - const structuralParents = getScrollParents(container, true); - for (let parent of structuralParents) { - // Avoid comparing against the main document layout elements as they are covered by the window check - if ( - parent !== document.body && - parent !== document.documentElement && - parent !== document.scrollingElement - ) { - if (!isElementVisibleInAncestor(container, parent as Element)) { - return false; - } - } - } - return true; - }; - if (!isScrollPrevented) { let {left: originalLeft, top: originalTop} = targetElement.getBoundingClientRect(); targetElement.scrollIntoView?.({block: 'nearest'}); @@ -210,7 +209,6 @@ export function scrollIntoViewport( const isFullyVisible = isContainerFullyVisible(containingElement); const isExceedingViewport = containerRect.height > window.innerHeight; - // If it's already visible in all scroll contexts, or it's simply too large to center without breaking layouts, use 'nearest' if (isFullyVisible || isExceedingViewport) { blockOption = 'nearest'; inlineOption = 'nearest'; From c90dc7fbea837d7411da0267c9a545f05038b9f6 Mon Sep 17 00:00:00 2001 From: jsmitrah Date: Fri, 19 Jun 2026 20:16:59 +0530 Subject: [PATCH 8/8] fix(react-aria): inline scroll ancestor visibility validation --- .../react-aria/src/utils/scrollIntoView.ts | 110 ++++++++++-------- 1 file changed, 61 insertions(+), 49 deletions(-) diff --git a/packages/react-aria/src/utils/scrollIntoView.ts b/packages/react-aria/src/utils/scrollIntoView.ts index 3480aaf5d11..fb694c14b64 100644 --- a/packages/react-aria/src/utils/scrollIntoView.ts +++ b/packages/react-aria/src/utils/scrollIntoView.ts @@ -132,50 +132,6 @@ export function scrollIntoView( scrollView.scrollTo({left: x, top: y}); } -// Helper utility to verify if a child element is fully within the visible boundaries of an ancestor -function isElementVisibleInAncestor(child: Element, ancestor: Element): boolean { - const childRect = child.getBoundingClientRect(); - const ancestorRect = ancestor.getBoundingClientRect(); - - return ( - childRect.top >= ancestorRect.top && - childRect.left >= ancestorRect.left && - childRect.bottom <= ancestorRect.bottom && - childRect.right <= ancestorRect.right - ); -} - -// Helper utility to check if the containingElement is obscured by ANY parent scroll container -function isContainerFullyVisible(container: Element): boolean { - const containerRect = container.getBoundingClientRect(); - - // 1. Check against global viewport boundaries - const visibleInWindow = - containerRect.top >= 0 && - containerRect.left >= 0 && - containerRect.bottom <= window.innerHeight && - containerRect.right <= window.innerWidth; - - if (!visibleInWindow) { - return false; - } - - // 2. Check against every single outer scroll parent layer - const structuralParents = getScrollParents(container, true); - for (let parent of structuralParents) { - if ( - parent !== document.body && - parent !== document.documentElement && - parent !== document.scrollingElement - ) { - if (!isElementVisibleInAncestor(container, parent as Element)) { - return false; - } - } - } - return true; -} - /** * 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 @@ -206,9 +162,38 @@ export function scrollIntoViewport( if (containingElement) { const containerRect = containingElement.getBoundingClientRect(); - const isFullyVisible = isContainerFullyVisible(containingElement); - const isExceedingViewport = containerRect.height > window.innerHeight; + // 1. Inline check against global viewport windows + let isFullyVisible = + containerRect.top >= 0 && + containerRect.left >= 0 && + containerRect.bottom <= window.innerHeight && + containerRect.right <= window.innerWidth; + + // 2. Inline check against every single outer scroll parent layer + if (isFullyVisible) { + const structuralParents = getScrollParents(containingElement, true); + for (let parent of structuralParents) { + if ( + parent !== document.body && + parent !== document.documentElement && + parent !== document.scrollingElement + ) { + const pRect = (parent as Element).getBoundingClientRect(); + if ( + containerRect.top < pRect.top || + containerRect.left < pRect.left || + containerRect.bottom > pRect.bottom || + containerRect.right > pRect.right + ) { + isFullyVisible = false; + break; + } + } + } + } + + const isExceedingViewport = containerRect.height > window.innerHeight; if (isFullyVisible || isExceedingViewport) { blockOption = 'nearest'; inlineOption = 'nearest'; @@ -233,11 +218,38 @@ export function scrollIntoViewport( for (let scrollParent of scrollParents) { const parentEl = scrollParent as HTMLElement; const containerEl = containingElement as HTMLElement; - const containerRect = containerEl.getBoundingClientRect(); - const isFullyVisible = isContainerFullyVisible(containerEl); - const isLarge = containerRect.height > parentEl.clientHeight; + // 1. Inline context check against parent boundaries + let isFullyVisible = + containerRect.top >= 0 && + containerRect.left >= 0 && + containerRect.bottom <= window.innerHeight && + containerRect.right <= window.innerWidth; + + if (isFullyVisible) { + const structuralParents = getScrollParents(containerEl, true); + for (let p of structuralParents) { + if ( + p !== document.body && + p !== document.documentElement && + p !== document.scrollingElement + ) { + const pRect = (p as Element).getBoundingClientRect(); + if ( + containerRect.top < pRect.top || + containerRect.left < pRect.left || + containerRect.bottom > pRect.bottom || + containerRect.right > pRect.right + ) { + isFullyVisible = false; + break; + } + } + } + } + + const isLarge = containerRect.height > parentEl.clientHeight; const useNearest = isFullyVisible || isLarge; const targetBlock: ScrollLogicalPosition = useNearest ? 'nearest' : 'center'; const targetInline: ScrollLogicalPosition = useNearest ? 'nearest' : 'center';