fix: scroll active sidebar items into view (#4965)

* Add centering of settings sidebar link when isActive

* abstract redundant logic into a reusable hook

* add jsdocs | refactor hook to consume behavior and block args

* remove unused import

* dev

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
This commit is contained in:
Marcello Fitton 2026-02-06 19:23:14 -08:00 committed by GitHub
parent 71cfff8091
commit 8f7e0fb1f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 52 additions and 7 deletions

View File

@ -6,7 +6,7 @@ concurrency:
on: on:
push: push:
branches: ["web-push-notifications-bootstrap"] # put your current branch to create a build. Core team only. branches: ["4963-sidebar-selection-srcoll-into-view"] # put your current branch to create a build. Core team only.
paths-ignore: paths-ignore:
- "**.md" - "**.md"
- "cloud-deployments/*" - "cloud-deployments/*"

View File

@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
import { CaretRight } from "@phosphor-icons/react"; import { CaretRight } from "@phosphor-icons/react";
import { Link, useLocation } from "react-router-dom"; import { Link, useLocation } from "react-router-dom";
import { safeJsonParse } from "@/utils/request"; import { safeJsonParse } from "@/utils/request";
import useScrollActiveItemIntoView from "@/hooks/useScrollActiveItemIntoView";
export default function MenuOption({ export default function MenuOption({
btnText, btnText,
@ -25,6 +26,18 @@ export default function MenuOption({
location: location.pathname, location: location.pathname,
}); });
const isActive = hasChildren
? (!isExpanded &&
childOptions.some((child) => child.href === location.pathname)) ||
location.pathname === href
: location.pathname === href;
const { ref } = useScrollActiveItemIntoView({
isActive,
behavior: "instant",
block: "center",
});
if (hidden) return null; if (hidden) return null;
// If this option is a parent level option // If this option is a parent level option
@ -43,12 +56,6 @@ export default function MenuOption({
if (flex && !!user && !roles.includes(user?.role)) return null; if (flex && !!user && !roles.includes(user?.role)) return null;
} }
const isActive = hasChildren
? (!isExpanded &&
childOptions.some((child) => child.href === location.pathname)) ||
location.pathname === href
: location.pathname === href;
const handleClick = (e) => { const handleClick = (e) => {
if (hasChildren) { if (hasChildren) {
e.preventDefault(); e.preventDefault();
@ -73,6 +80,7 @@ export default function MenuOption({
`} `}
> >
<Link <Link
ref={ref}
to={href} to={href}
className={`flex flex-grow items-center px-[12px] h-[32px] font-medium ${ className={`flex flex-grow items-center px-[12px] h-[32px] font-medium ${
isChild ? "hover:text-white" : "text-white light:text-black" isChild ? "hover:text-white" : "text-white light:text-black"

View File

@ -1,3 +1,4 @@
import useScrollActiveItemIntoView from "@/hooks/useScrollActiveItemIntoView";
import Workspace from "@/models/workspace"; import Workspace from "@/models/workspace";
import paths from "@/utils/paths"; import paths from "@/utils/paths";
import showToast from "@/utils/toast"; import showToast from "@/utils/toast";
@ -30,6 +31,11 @@ export default function ThreadItem({
? paths.workspace.chat(slug) ? paths.workspace.chat(slug)
: paths.workspace.thread(slug, thread.slug); : paths.workspace.thread(slug, thread.slug);
const { ref } = useScrollActiveItemIntoView({
isActive,
behavior: "instant",
block: "center",
});
return ( return (
<div <div
className="w-full relative flex h-[38px] items-center border-none rounded-lg" className="w-full relative flex h-[38px] items-center border-none rounded-lg"
@ -88,6 +94,7 @@ export default function ThreadItem({
</div> </div>
) : ( ) : (
<a <a
ref={ref}
href={ href={
window.location.pathname === linkTo || ctrlPressed ? "#" : linkTo window.location.pathname === linkTo || ctrlPressed ? "#" : linkTo
} }

View File

@ -0,0 +1,30 @@
import { useEffect, useRef } from "react";
/**
* Hook that scrolls an element into view when it becomes active.
* @param {Object} options - The options for the hook.
* @param {boolean} options.isActive - Whether the element is currently active.
* @param {"smooth" | "instant" | "auto"} options.behavior - The scroll behavior.
* @param {"start" | "center" | "end" | "nearest"} options.block - The vertical alignment of the element within the scrollable container.
* @returns {{ ref: React.RefObject<HTMLElement> }} An object containing the ref to attach to the target element.
*/
export default function useScrollActiveItemIntoView({
isActive,
behavior,
block,
}) {
const ref = useRef(null);
useEffect(() => {
if (isActive) {
ref.current.scrollIntoView({
behavior,
block,
});
}
}, [isActive]);
return {
ref,
};
}