Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 41 additions & 5 deletions frontend/app/element/tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
useHover,
useInteractions,
} from "@floating-ui/react";
import { useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";

interface TooltipProps {
children: React.ReactNode;
Expand All @@ -24,6 +24,8 @@ interface TooltipProps {
divClassName?: string;
divStyle?: React.CSSProperties;
divOnClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
divRef?: React.RefObject<HTMLDivElement>;
hideOnClick?: boolean;
}

function TooltipInner({
Expand All @@ -35,9 +37,12 @@ function TooltipInner({
divClassName,
divStyle,
divOnClick,
divRef,
hideOnClick = false,
}: Omit<TooltipProps, "disable">) {
const [isOpen, setIsOpen] = useState(forceOpen);
const [isVisible, setIsVisible] = useState(false);
const [clickDisabled, setClickDisabled] = useState(false);
const timeoutRef = useRef<number | null>(null);
const prevForceOpenRef = useRef<boolean>(forceOpen);

Expand Down Expand Up @@ -106,17 +111,44 @@ function TooltipInner({
};
}, []);

const hover = useHover(context);
const hover = useHover(context, { enabled: !clickDisabled });
const { getReferenceProps, getFloatingProps } = useInteractions([hover]);

const handleClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (hideOnClick) {
setIsVisible(false);
setIsOpen(false);
if (timeoutRef.current !== null) {
window.clearTimeout(timeoutRef.current);
}
setClickDisabled(true);
}
divOnClick?.(e);
},
[hideOnClick, divOnClick]
);

const handlePointerEnter = useCallback(() => {
if (hideOnClick && clickDisabled) {
setClickDisabled(false);
}
}, [hideOnClick, clickDisabled]);

return (
<>
<div
ref={refs.setReference}
ref={(node) => {
refs.setReference(node);
if (divRef) {
(divRef as React.RefObject<HTMLDivElement>).current = node;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Invalid ref assignment - RefObject.current is readonly

The code attempts to assign to divRef.current, but React.RefObject<T>.current is a readonly property. This violates TypeScript's type system and React's ref contract.

Suggested change
(divRef as React.RefObject<HTMLDivElement>).current = node;
if (divRef && 'current' in divRef) {
(divRef as React.MutableRefObject<HTMLDivElement>).current = node;
}

Alternatively, consider using a callback ref pattern or checking if the ref is mutable before assignment.

}
}}
{...getReferenceProps()}
className={divClassName}
style={divStyle}
onClick={divOnClick}
onClick={handleClick}
onPointerEnter={handlePointerEnter}
>
{children}
</div>
Expand Down Expand Up @@ -152,10 +184,12 @@ export function Tooltip({
divClassName,
divStyle,
divOnClick,
divRef,
hideOnClick = false,
}: TooltipProps) {
if (disable) {
return (
<div className={divClassName} style={divStyle} onClick={divOnClick}>
<div ref={divRef} className={divClassName} style={divStyle} onClick={divOnClick}>
{children}
</div>
);
Expand All @@ -171,6 +205,8 @@ export function Tooltip({
divClassName={divClassName}
divStyle={divStyle}
divOnClick={divOnClick}
divRef={divRef}
hideOnClick={hideOnClick}
/>
);
}
34 changes: 25 additions & 9 deletions frontend/app/tab/tabbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import { Button } from "@/app/element/button";
import { Tooltip } from "@/app/element/tooltip";
import { modalsModel } from "@/app/store/modalmodel";
import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model";
import { deleteLayoutModelForTab } from "@/layout/index";
Expand Down Expand Up @@ -42,7 +43,7 @@ interface TabBarProps {
workspace: Workspace;
}

const WaveAIButton = memo(() => {
const WaveAIButton = memo(({ divRef }: { divRef?: React.RefObject<HTMLDivElement> }) => {
const aiPanelOpen = useAtomValue(WorkspaceLayoutModel.getInstance().panelVisibleAtom);
const hideAiButton = useAtomValue(getSettingsKeyAtom("app:hideaibutton"));

Expand All @@ -56,14 +57,18 @@ const WaveAIButton = memo(() => {
}

return (
<div
className={`flex h-[26px] px-1.5 justify-end items-center rounded-md mr-1 box-border cursor-pointer bg-hover hover:bg-hoverbg transition-colors text-[12px] ${aiPanelOpen ? "text-accent" : "text-secondary"}`}
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
onClick={onClick}
<Tooltip
content="Toggle Wave AI Panel"
placement="bottom"
hideOnClick
divClassName={`flex h-[26px] px-1.5 justify-end items-center rounded-md mr-1 box-border cursor-pointer bg-hover hover:bg-hoverbg transition-colors text-[12px] ${aiPanelOpen ? "text-accent" : "text-secondary"}`}
divStyle={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
divOnClick={onClick}
divRef={divRef}
>
<i className="fa fa-sparkles" />
<span className="font-bold ml-1 -top-px font-mono">AI</span>
</div>
</Tooltip>
);
});
WaveAIButton.displayName = "WaveAIButton";
Expand Down Expand Up @@ -190,6 +195,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
const draggerLeftRef = useRef<HTMLDivElement>(null);
const draggerRightRef = useRef<HTMLDivElement>(null);
const workspaceSwitcherRef = useRef<HTMLDivElement>(null);
const waveAIButtonRef = useRef<HTMLDivElement>(null);
const appMenuButtonRef = useRef<HTMLDivElement>(null);
const tabWidthRef = useRef<number>(TabDefaultWidth);
const scrollableRef = useRef<boolean>(false);
Expand Down Expand Up @@ -251,6 +257,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
const configErrorWidth = configErrorButtonRef.current?.getBoundingClientRect().width ?? 0;
const appMenuButtonWidth = appMenuButtonRef.current?.getBoundingClientRect().width ?? 0;
const workspaceSwitcherWidth = workspaceSwitcherRef.current?.getBoundingClientRect().width ?? 0;
const waveAIButtonWidth = waveAIButtonRef.current?.getBoundingClientRect().width ?? 0;

const nonTabElementsWidth =
windowDragLeftWidth +
Expand All @@ -259,7 +266,8 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
updateStatusLabelWidth +
configErrorWidth +
appMenuButtonWidth +
workspaceSwitcherWidth;
workspaceSwitcherWidth +
waveAIButtonWidth;
const spaceForTabs = tabbarWrapperWidth - nonTabElementsWidth;

const numberOfTabs = tabIds.length;
Expand Down Expand Up @@ -670,8 +678,16 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
<i className="fa fa-ellipsis" />
</div>
)}
<WaveAIButton />
<WorkspaceSwitcher ref={workspaceSwitcherRef} />
<WaveAIButton divRef={waveAIButtonRef} />
<Tooltip
content="Workspace Switcher"
placement="bottom"
hideOnClick
divRef={workspaceSwitcherRef}
divClassName="flex items-center h-full"
>
<WorkspaceSwitcher />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Ref forwarding issue with WorkspaceSwitcher wrapper

The WorkspaceSwitcher component uses forwardRef to forward refs to its internal Popover component (line 98 in workspaceswitcher.tsx). However, wrapping it in a Tooltip creates an extra div wrapper, so workspaceSwitcherRef will point to the Tooltip's wrapper div instead of the WorkspaceSwitcher's root Popover element.

This breaks the width calculation on line 259: workspaceSwitcherRef.current?.getBoundingClientRect().width will measure the Tooltip wrapper instead of the actual WorkspaceSwitcher button.

Recommendation: Either:

  1. Don't wrap WorkspaceSwitcher in a Tooltip (add tooltip directly to WorkspaceSwitcher component)
  2. Or pass the ref directly to WorkspaceSwitcher and let it handle the ref forwarding: <WorkspaceSwitcher ref={workspaceSwitcherRef} />

</Tooltip>
<div className="tab-bar" ref={tabBarRef} data-overlayscrollbars-initialize>
<div className="tabs-wrapper" ref={tabsWrapperRef} style={{ width: `${tabsWrapperWidth}px` }}>
{tabIds.map((tabId, index) => {
Expand Down
Loading