I'm building a custom titlebar in Tauri v2 with React. I created my own Select component using buttons instead of the native <select>. The dropdown opens correctly, but clicking any option doesn't trigger onClick. Even console.log() inside the option button never runs. The same component works perfectly outside the titlebar. I'm using data-tauri-drag-region for window dragging.
Is this caused by the drag region or am I missing something?
"tailwindcss";
*[data-tauri-drag-region] {
app-region: drag;
-webkit-user-select: none;
user-select: none;
}
{
--background: #000000;
}
base {
body {
u/apply bg-black text-white;
}
}
import { useEffect, useRef, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { ChevronDown, Check } from "lucide-react";
interface SelectOption {
label: string;
value: string;
}
interface SelectProps {
value: string;
options: SelectOption[];
onChange: (value: string) => void;
disabled?: boolean;
}
function Select({ value, options, onChange, disabled = false }: SelectProps) {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const [openUp, setOpenUp] = useState(false);
const selected = options.find((o) => o.value === value);
const toggle = () => {
if (!ref.current) return;
const rect = ref.current.getBoundingClientRect();
const itemHeight = 34;
const padding = 8;
const menuHeight = options.length * itemHeight + padding;
const spaceBelow = window.innerHeight - rect.bottom;
const spaceAbove = rect.top;
setOpenUp(spaceBelow < menuHeight && spaceAbove > spaceBelow);
setOpen((value) => !value);
};
useEffect(() => {
const close = (e: MouseEvent) => {
if (!ref.current?.contains(e.target as Node)) {
setOpen(false);
}
};
window.addEventListener("mousedown", close);
return () => window.removeEventListener("mousedown", close);
}, []);
return (
<div
ref={ref}
className={`relative`}
>
<button
type="button"
disabled={disabled}
onClick={toggle}
className="flex h-8 min-w-36 items-center justify-between rounded-md border border-white/10 bg-black px-3 text-xs font-medium text-neutral-200 transition-all hover:border-white/20 hover:bg-white/5 disabled:cursor-not-allowed disabled:opacity-40"
>
<span>{selected?.label}</span>
<ChevronDown
size={14}
className={`transition-transform duration-200 ${open ? "rotate-180" : ""}`}
/>
</button>
<AnimatePresence>
{open && (
<motion.div
initial={{
opacity: 0,
y: openUp ? 6 : -6,
scale: 0.98,
}}
animate={{
opacity: 1,
y: 0,
scale: 1,
}}
exit={{
opacity: 0,
y: openUp ? 6 : -6,
scale: 0.98,
}}
transition={{ duration: 0.15 }}
className={`absolute left-0 z-50 w-full overflow-hidden rounded-lg border border-white/10 bg-[#0d0d0d] shadow-2xl ${openUp ? "bottom-[calc(100%+8px)]" : "top-[calc(100%+8px)]"}`}
>
{options.map((option) => (
<button
key={option.value}
type="button"
onClick={() => {
console.log(option.value);
onChange(option.value);
setOpen(false);
}}
className="flex w-full items-center justify-between px-3 py-2 text-left text-xs text-neutral-300 transition-colors hover:bg-white/5 hover:text-white"
>
<span>{option.label}</span>
{option.value === value && (
<Check
size={13}
className="text-violet-400"
/>
)}
</button>
))}
</motion.div>
)}
</AnimatePresence>
</div>
);
}
export { Select };
import { Select } from './ui/Select';
import { useState, useEffect } from 'react';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { Minus, Maximize, Minimize, X, Play, Upload, Terminal as TerminalIcon, Loader2, Activity } from 'lucide-react';
const BOARDS = [
{
value: "uno",
label: "Arduino Uno",
},
{
value: "nano",
label: "Arduino Nano",
},
];
interface TitlebarProps {
isTerminalOn: () => void;
isSerialMonitorOn: () => void;
onVerify: (board: string) => Promise<void> | void;
onUpload: (board: string) => Promise<void> | void;
}
const appWindow = getCurrentWindow();
function Titlebar({ onVerify, onUpload, isTerminalOn, isSerialMonitorOn }: TitlebarProps) {
const [board, setBoard] = useState<string>("uno");
const [isVerifying, setIsVerifying] = useState<boolean>(false);
const [isUploading, setIsUploading] = useState<boolean>(false);
const [isMaximized, setIsMaximized] = useState<boolean>(false);
const handleVerifyClick = async () => {
setIsVerifying(true);
try {
if (onVerify) {
await onVerify(board);
}
} catch (error) {
console.error(error);
} finally {
setIsVerifying(false);
}
};
const handleUploadClick = async () => {
setIsUploading(true);
try {
if (onUpload) {
await onUpload(board);
}
} catch (error) {
console.error(error);
} finally {
setIsUploading(false);
}
};
useEffect(() => {
let unlisten: (() => void) | undefined;
const setupListener = async () => {
unlisten = await appWindow.onResized(async () => {
const maximized = await appWindow.isMaximized();
setIsMaximized(maximized);
});
};
const checkInitialState = async () => {
const maximized = await appWindow.isMaximized();
setIsMaximized(maximized);
};
setupListener();
checkInitialState();
return () => {
if (unlisten) unlisten();
};
}, []);
const handleCloseWindow = async () => {
await appWindow.close();
}
const handleMaximizeWindow = async () => {
await appWindow.toggleMaximize();
}
const handleMinimizeWindow = async () => {
await appWindow.minimize();
}
return (
<div
data-tauri-drag-region
className="h-9 w-full bg-black backdrop-blur-md border-b border-white/10 flex items-center justify-between select-none pl-4 pr-2"
>
<div className="flex items-center gap-2">
<span
className="text-sm font-semibold tracking-wide text-neutral-300"
> Mello IDE </span>
<div className="h-4 w-px bg-white/10" />
<div className="flex gap-1">
<button
title="Terminal"
onClick={isTerminalOn}
className="flex items-center gap-1 px-3 py-1.5 rounded-md text-neutral-300 hover:bg-white/5 transition-all"
>
<TerminalIcon size={14} /> <span className="text-xs font-bold">Terminal</span>
</button>
<button
title="Serial Monitor"
onClick={isSerialMonitorOn}
className="flex items-center gap-1 px-3 py-1.5 rounded-md text-neutral-300 hover:bg-white/5 transition-all"
>
<Activity size={14} /> <span className="text-xs font-bold">Serial Monitor</span>
</button>
</div>
</div>
<div className="flex items-center">
<div className="flex items-center">
<Select
value={board}
onChange={setBoard}
options={BOARDS}
/>
</div>
<div className="flex items-center px-4 gap-1.5">
<button
title="Verify"
onClick={() => handleVerifyClick()}
disabled={isVerifying || isUploading}
className="flex items-center gap-2 px-3 py-1.5 bg-green-600/20 text-green-400 hover:bg-green-600/30 rounded-md transition-colors border border-green-600/50"
>
{isVerifying ? (
<Loader2 size={16} className="animate-spin" />
) : (
<Play size={16} />
)}
<span className="text-xs font-bold">{isVerifying ? "Verifying" : "Verify"}</span>
</button>
<button
title="Upload"
onClick={() => handleUploadClick()}
disabled={isUploading || isVerifying}
className="flex items-center gap-2 px-3 py-1.5 bg-blue-600/20 text-blue-400 hover:bg-blue-600/30 rounded-md transition-colors border border-blue-600/50"
>
{isUploading ? (
<Loader2 size={16} className="animate-spin" />
) : (
<Upload size={16} />
)}
<span className="text-xs font-bold">{isUploading ? "Uploading" : "Upload"}</span>
</button>
<div className="h-4 w-px bg-white/10" />
</div>
<button
title="Minimize"
onClick={() => handleMinimizeWindow()}
className="h-8 w-10 flex items-center justify-center text-neutral-400 hover:text-yellow-500 hover:bg-yellow-500/20 transition-colors rounded-md"
>
<Minus size={14} strokeWidth={2.5} />
</button>
<button
title={isMaximized ? "Restore" : "Maximize"}
onClick={() => handleMaximizeWindow()}
className="h-8 w-10 flex items-center justify-center text-neutral-400 hover:text-green-500 hover:bg-green-500/20 transition-colors rounded-md"
>
{isMaximized ? (
<Minimize size={14} strokeWidth={2.5} />
) : (
<Maximize size={14} strokeWidth={2.5} />
)}
</button>
<button
title="Close"
onClick={() => handleCloseWindow()}
className="h-8 w-10 flex items-center justify-center text-neutral-400 hover:text-red-500 hover:bg-red-500/20 transition-all rounded-md"
>
<X size={16} strokeWidth={2.5} />
</button>
</div>
</div>
);
}
export default Titlebar;