From 7cf8180c60cd963ffa7fd2a8e0e36681526d8d6b Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Thu, 5 Dec 2024 13:06:43 -0500 Subject: [PATCH] fix(scrollIntoView): respect scroll padding Fixes #7037 --- .../@react-aria/utils/src/scrollIntoView.ts | 40 +++++++++++----- .../stories/Menu.stories.tsx | 48 +++++++++++++++++++ 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/packages/@react-aria/utils/src/scrollIntoView.ts b/packages/@react-aria/utils/src/scrollIntoView.ts index cafcdf08a02..bf4623b2ade 100644 --- a/packages/@react-aria/utils/src/scrollIntoView.ts +++ b/packages/@react-aria/utils/src/scrollIntoView.ts @@ -30,24 +30,40 @@ export function scrollIntoView(scrollView: HTMLElement, element: HTMLElement) { let x = scrollView.scrollLeft; let y = scrollView.scrollTop; - // Account for top/left border offsetting the scroll top/Left - let {borderTopWidth, borderLeftWidth} = getComputedStyle(scrollView); - let borderAdjustedX = scrollView.scrollLeft + parseInt(borderLeftWidth, 10); - let borderAdjustedY = scrollView.scrollTop + parseInt(borderTopWidth, 10); + // Account for top/left border offsetting the scroll top/Left + scroll padding + let { + borderTopWidth, + borderLeftWidth, + scrollPaddingTop, + scrollPaddingRight, + scrollPaddingBottom, + scrollPaddingLeft + } = getComputedStyle(scrollView); + + let borderAdjustedX = x + parseInt(borderLeftWidth, 10); + let borderAdjustedY = y + parseInt(borderTopWidth, 10); // Ignore end/bottom border via clientHeight/Width instead of offsetHeight/Width let maxX = borderAdjustedX + scrollView.clientWidth; let maxY = borderAdjustedY + scrollView.clientHeight; - if (offsetX <= x) { - x = offsetX - parseInt(borderLeftWidth, 10); - } else if (offsetX + width > maxX) { - x += offsetX + width - maxX; + // Get scroll padding values as pixels - defaults to 0 if no scroll padding + // is used. + 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) { - y = offsetY - parseInt(borderTopWidth, 10); - } else if (offsetY + height > maxY) { - y += offsetY + height - maxY; + 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; } diff --git a/packages/react-aria-components/stories/Menu.stories.tsx b/packages/react-aria-components/stories/Menu.stories.tsx index d8876499cf5..6c19981c5fe 100644 --- a/packages/react-aria-components/stories/Menu.stories.tsx +++ b/packages/react-aria-components/stories/Menu.stories.tsx @@ -68,6 +68,54 @@ export const MenuComplex = () => ( ); +export const MenuScrollPaddingExample = () => ( + + + + +
+ Section 1 +
+ {Array.from({length: 30}).map((_, i) => ( + Item {i + 1} + ))} +
+ {/* Menu doesn't have a footer, so have to put one outside to + and position it absolutely to demo scroll padding bottom support. */} +
+ A footer +
+
+
+); + export const SubmenuExample = (args) => (