From d4cb5f1b52b58962750e34955a7e94daa7187577 Mon Sep 17 00:00:00 2001 From: Dominik Hryshaiev Date: Sun, 15 Feb 2026 19:25:49 +0100 Subject: [PATCH 1/3] fix(s2-docs): auto-scroll and expand section for group selected nav link --- packages/dev/s2-docs/src/Nav.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/dev/s2-docs/src/Nav.tsx b/packages/dev/s2-docs/src/Nav.tsx index eaa6002bbaf..8411a675abd 100644 --- a/packages/dev/s2-docs/src/Nav.tsx +++ b/packages/dev/s2-docs/src/Nav.tsx @@ -188,7 +188,7 @@ export function Nav() { ); } return ( - + {name}
{nav}
@@ -255,10 +255,18 @@ export function SideNavItem(props) { } export function SideNavLink(props) { - let linkRef = useRef(null); + let linkRef = useRef(null); let selected = useContext(SideNavContext); let {isExternal, ...linkProps} = props; - + + useEffect(() => { + if (!linkRef.current || props.isSelected !== true) { + return; + } + + linkRef.current.scrollIntoView({block: 'start', behavior: 'smooth'}); + }, [props.isSelected]); + return ( {(renderProps) => (<> Date: Tue, 17 Feb 2026 23:10:40 +0100 Subject: [PATCH 2/3] refactor(s2-docs): utilize scrollIntoViewport with added behavior and alignment options --- .../@react-aria/utils/src/scrollIntoView.ts | 27 ++++++++++++------- packages/dev/s2-docs/src/Nav.tsx | 3 ++- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/@react-aria/utils/src/scrollIntoView.ts b/packages/@react-aria/utils/src/scrollIntoView.ts index 2d69a34bc01..d6a2b5c60df 100644 --- a/packages/@react-aria/utils/src/scrollIntoView.ts +++ b/packages/@react-aria/utils/src/scrollIntoView.ts @@ -17,13 +17,19 @@ interface ScrollIntoViewOpts { /** The position to align items along the block axis in. */ block?: ScrollLogicalPosition, /** The position to align items along the inline axis in. */ - inline?: ScrollLogicalPosition + inline?: ScrollLogicalPosition, + /** The behavior to determine whether scrolling should animate smoothly or happen instantly. */ + behavior?: ScrollIntoViewOptions['behavior'] } interface ScrollIntoViewportOpts { /** The optional containing element of the target to be centered in the viewport. */ - containingElement?: Element | null + containingElement?: Element | null, + /** The optional alignment of the target element within the viewport. */ + block?: ScrollIntoViewOptions['block'], + /** The optional behavior that determines whether scrolling is instant or animates smoothly. */ + behavior?: ScrollIntoViewOptions['behavior'] } /** @@ -32,7 +38,7 @@ interface ScrollIntoViewportOpts { * but doesn't affect parents above `scrollView`. */ export function scrollIntoView(scrollView: HTMLElement, element: HTMLElement, opts: ScrollIntoViewOpts = {}): void { - let {block = 'nearest', inline = 'nearest'} = opts; + let {block = 'nearest', inline = 'nearest', behavior = 'auto'} = opts; if (scrollView === element) { return; } @@ -120,16 +126,17 @@ export function scrollIntoView(scrollView: HTMLElement, element: HTMLElement, op return; } - scrollView.scrollTo({left: x, top: y}); + scrollView.scrollTo({left: x, top: y, behavior}); } /** * 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 + * that will be centered in the viewport prior to scrolling the targetElement into view, as well as optional `opts.block` and `opts.behavior` + * to determine the alignment and animation behavior of the target element. 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 { - let {containingElement} = opts; + let {containingElement, block = 'nearest', behavior = 'auto'} = opts; if (targetElement && targetElement.isConnected) { let root = document.scrollingElement || document.documentElement; let isScrollPrevented = window.getComputedStyle(root).overflow === 'hidden'; @@ -140,12 +147,12 @@ export function scrollIntoViewport(targetElement: Element | null, opts: ScrollIn // 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'}); + targetElement?.scrollIntoView?.({block, behavior}); 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)) { containingElement?.scrollIntoView?.({block: 'center', inline: 'center'}); - targetElement.scrollIntoView?.({block: 'nearest'}); + targetElement.scrollIntoView?.({block, behavior}); } } else { let {left: originalLeft, top: originalTop} = targetElement.getBoundingClientRect(); @@ -153,14 +160,14 @@ export function scrollIntoViewport(targetElement: Element | null, opts: ScrollIn // 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); + scrollIntoView(scrollParent as HTMLElement, targetElement as HTMLElement, {block: opts.block || 'start', behavior}); } 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) : []; for (let scrollParent of scrollParents) { - scrollIntoView(scrollParent as HTMLElement, containingElement as HTMLElement, {block: 'center', inline: 'center'}); + scrollIntoView(scrollParent as HTMLElement, containingElement as HTMLElement, {block: opts.block || 'center', inline: 'center', behavior}); } } } diff --git a/packages/dev/s2-docs/src/Nav.tsx b/packages/dev/s2-docs/src/Nav.tsx index 8411a675abd..2a31c8f3072 100644 --- a/packages/dev/s2-docs/src/Nav.tsx +++ b/packages/dev/s2-docs/src/Nav.tsx @@ -7,6 +7,7 @@ import {getLibraryFromPage} from './library'; import LinkOutIcon from '../../../@react-spectrum/s2/ui-icons/LinkOut'; import type {Page} from '@parcel/rsc'; import React, {createContext, useContext, useEffect, useRef, useState} from 'react'; +import {scrollIntoViewport} from '@react-aria/utils'; import {usePendingPage, useRouter} from './Router'; type SectionValue = Page[] | Map; @@ -264,7 +265,7 @@ export function SideNavLink(props) { return; } - linkRef.current.scrollIntoView({block: 'start', behavior: 'smooth'}); + scrollIntoViewport(linkRef.current, {block: 'start', behavior: 'smooth'}); }, [props.isSelected]); return ( From 97c50b315741e9a26e242755576c0bb25755fe99 Mon Sep 17 00:00:00 2001 From: Dominik Hryshaiev Date: Tue, 17 Feb 2026 23:38:55 +0100 Subject: [PATCH 3/3] refactor(s2-docs): remove redundant type annotation and verbose boolean comparison --- packages/dev/s2-docs/src/Nav.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dev/s2-docs/src/Nav.tsx b/packages/dev/s2-docs/src/Nav.tsx index 2a31c8f3072..209732ae78f 100644 --- a/packages/dev/s2-docs/src/Nav.tsx +++ b/packages/dev/s2-docs/src/Nav.tsx @@ -256,12 +256,12 @@ export function SideNavItem(props) { } export function SideNavLink(props) { - let linkRef = useRef(null); + let linkRef = useRef(null); let selected = useContext(SideNavContext); let {isExternal, ...linkProps} = props; useEffect(() => { - if (!linkRef.current || props.isSelected !== true) { + if (!linkRef.current || !props.isSelected) { return; }