Skip to content

Commit 2364339

Browse files
NicolappsConvex, Inc.
authored andcommitted
Fix buggy navbar active element indicator (#40936)
This PR makes improves the navbar active element indicator by: - fixing a bug where the indicator could be mispositioned when the window resizes - making the animation buttery smooth with `transform` - making the animation work on small screens too - making the element accessible by disabling the animation when `prefers-reduced-motion` is set to `reduce` GitOrigin-RevId: cc75c44f31f150002fdea9a957ecb8cd316c79e6
1 parent 3d89c6e commit 2364339

File tree

2 files changed

+32
-27
lines changed

2 files changed

+32
-27
lines changed

npm-packages/dashboard/src/components/header/Header/Header.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import Link from "next/link";
55
import { SupportWidget, useSupportFormOpen } from "elements/SupportWidget";
66
import { Portal } from "@headlessui/react";
77
import { Button } from "@ui/Button";
8-
import { useReducer } from "react";
98
import { AskAI } from "elements/AskAI";
109
import { DeploymentDisplay } from "elements/DeploymentDisplay";
1110
import { useCurrentProject } from "api/projects";
@@ -41,19 +40,13 @@ function Support() {
4140
}
4241

4342
export function Header({ children, logoLink = "/", user }: HeaderProps) {
44-
const [headerKey, forceRerender] = useReducer((x) => x + 1, 0);
45-
4643
const project = useCurrentProject();
4744

4845
return (
4946
<header
5047
className={classNames(
5148
"flex justify-between min-h-[56px] overflow-x-auto scrollbar-none bg-background-secondary border-b",
5249
)}
53-
// Re-render the header content when the user scrolls so
54-
// the underline on the active nav item can be updated.
55-
// TODO: Don't absolutely position the underline
56-
onScroll={forceRerender}
5750
>
5851
<div className="flex items-center bg-background-secondary px-2">
5952
<div className="rounded-full p-2 transition-colors hover:bg-background-tertiary">
@@ -70,7 +63,7 @@ export function Header({ children, logoLink = "/", user }: HeaderProps) {
7063
/>
7164
</Link>
7265
</div>
73-
<div key={headerKey}>{children}</div>
66+
<div>{children}</div>
7467
</div>
7568
{project && <DeploymentDisplay project={project} />}
7669
<div className="flex items-center bg-background-secondary px-2">

npm-packages/dashboard/src/components/header/NavBar/NavBar.tsx

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import classNames from "classnames";
22
import Link from "next/link";
3-
import { useState } from "react";
3+
import { useEffect, useRef } from "react";
44

55
type NavItem = {
66
label: string;
@@ -13,43 +13,55 @@ type NavBarProps = {
1313
};
1414

1515
export function NavBar({ items, activeLabel }: NavBarProps) {
16-
const [ref, setRef] = useState<HTMLAnchorElement | null>(null);
17-
const rect = ref?.getBoundingClientRect();
16+
const parentRef = useRef<HTMLDivElement | null>(null);
17+
const activeLinkRef = useRef<HTMLAnchorElement | null>(null);
18+
const activeIndicatorRef = useRef<HTMLDivElement | null>(null);
19+
20+
useEffect(() => {
21+
// Reposition indicator
22+
if (
23+
!activeLinkRef.current ||
24+
!activeIndicatorRef.current ||
25+
!parentRef.current
26+
) {
27+
return;
28+
}
29+
30+
const rect = activeLinkRef.current.getBoundingClientRect();
31+
const parentRect = parentRef.current.getBoundingClientRect();
32+
33+
activeIndicatorRef.current.style.width = `100%`;
34+
activeIndicatorRef.current.style.top = `${rect.y + rect.height + 4}px`;
35+
activeIndicatorRef.current.style.transform = `translateX(${rect.x - parentRect.x}px) scaleX(${parentRect.width === 0 ? 0 : rect.width / parentRect.width})`;
36+
}, [activeLabel]);
1837

1938
return (
20-
<div>
21-
<div className="flex gap-1 truncate select-none">
39+
<div className="relative">
40+
<div className="flex gap-1 truncate select-none" ref={parentRef}>
2241
{items.map(({ label, href }) => (
2342
<div className="flex flex-col" key={label}>
2443
<Link
2544
href={href}
2645
passHref
27-
ref={(r) => (activeLabel === label ? setRef(r) : undefined)}
46+
ref={activeLabel === label ? activeLinkRef : undefined}
2847
className={classNames(
2948
"p-2 my-2 mx-1 text-sm",
3049
"text-content-primary",
3150
"hover:bg-background-tertiary rounded-full",
3251
{
33-
"underline-offset-[1rem] decoration-4 font-medium underline sm:no-underline":
34-
activeLabel === label,
52+
"decoration-4 font-medium": activeLabel === label,
3553
},
3654
)}
3755
>
3856
{label}
3957
</Link>
4058
</div>
4159
))}
42-
{rect && (
43-
<div
44-
className="absolute mt-auto hidden h-1 w-full bg-content-secondary sm:block"
45-
style={{
46-
width: rect.width,
47-
top: rect.y + rect.height + 4,
48-
left: rect.x,
49-
transition: "left 150ms",
50-
}}
51-
/>
52-
)}
60+
61+
<div
62+
className="absolute mt-auto h-1 w-0 origin-top-left bg-content-secondary transition-transform will-change-transform motion-reduce:transition-none"
63+
ref={activeIndicatorRef}
64+
/>
5365
</div>
5466
</div>
5567
);

0 commit comments

Comments
 (0)