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:
parent
71cfff8091
commit
8f7e0fb1f8
2
.github/workflows/dev-build.yaml
vendored
2
.github/workflows/dev-build.yaml
vendored
@ -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/*"
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
30
frontend/src/hooks/useScrollActiveItemIntoView.js
Normal file
30
frontend/src/hooks/useScrollActiveItemIntoView.js
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user