fix console errors with tooltips and dropdown in single camera live view

This commit is contained in:
Josh Hawkins 2025-09-22 19:37:39 -05:00
parent a525dd0ff7
commit 303471242f

View File

@ -28,7 +28,6 @@ import {
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { useResizeObserver } from "@/hooks/resize-observer"; import { useResizeObserver } from "@/hooks/resize-observer";
@ -116,6 +115,7 @@ import {
SelectGroup, SelectGroup,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { usePersistence } from "@/hooks/use-persistence"; import { usePersistence } from "@/hooks/use-persistence";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
@ -499,122 +499,118 @@ export default function LiveCameraView({
) : ( ) : (
<div /> <div />
)} )}
<TooltipProvider> <div
<div className={`flex flex-row items-center gap-2 *:rounded-lg ${isMobile ? "landscape:flex-col" : ""}`}
className={`flex flex-row items-center gap-2 *:rounded-lg ${isMobile ? "landscape:flex-col" : ""}`} >
> {fullscreen && (
{fullscreen && ( <Button
<Button className="bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-primary"
className="bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-primary" aria-label={t("label.back", { ns: "common" })}
aria-label={t("label.back", { ns: "common" })} size="sm"
size="sm" onClick={() => navigate(-1)}
onClick={() => navigate(-1)} >
> <IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" /> {isDesktop && (
{isDesktop && ( <div className="text-secondary-foreground">
<div className="text-secondary-foreground"> {t("button.back", { ns: "common" })}
{t("button.back", { ns: "common" })} </div>
</div> )}
)} </Button>
</Button> )}
)} {supportsFullscreen && (
{supportsFullscreen && ( <CameraFeatureToggle
<CameraFeatureToggle className="p-2 md:p-0"
className="p-2 md:p-0" variant={fullscreen ? "overlay" : "primary"}
variant={fullscreen ? "overlay" : "primary"} Icon={fullscreen ? FaCompress : FaExpand}
Icon={fullscreen ? FaCompress : FaExpand} isActive={fullscreen}
isActive={fullscreen} title={
title={ fullscreen
fullscreen ? t("button.close", { ns: "common" })
? t("button.close", { ns: "common" }) : t("button.fullscreen", { ns: "common" })
: t("button.fullscreen", { ns: "common" })
}
onClick={toggleFullscreen}
/>
)}
{!isIOS && !isFirefox && preferredLiveMode != "jsmpeg" && (
<CameraFeatureToggle
className="p-2 md:p-0"
variant={fullscreen ? "overlay" : "primary"}
Icon={LuPictureInPicture}
isActive={pip}
title={
pip
? t("button.close", { ns: "common" })
: t("button.pictureInPicture", { ns: "common" })
}
onClick={() => {
if (!pip) {
setPip(true);
} else {
document.exitPictureInPicture();
setPip(false);
}
}}
disabled={!cameraEnabled}
/>
)}
{supports2WayTalk && (
<CameraFeatureToggle
className="p-2 md:p-0"
variant={fullscreen ? "overlay" : "primary"}
Icon={mic ? FaMicrophone : FaMicrophoneSlash}
isActive={mic}
title={
mic
? t("twoWayTalk.disable", { ns: "views/live" })
: t("twoWayTalk.enable", { ns: "views/live" })
}
onClick={() => {
setMic(!mic);
if (!mic && !audio) {
setAudio(true);
}
}}
disabled={!cameraEnabled}
/>
)}
{supportsAudioOutput && preferredLiveMode != "jsmpeg" && (
<CameraFeatureToggle
className="p-2 md:p-0"
variant={fullscreen ? "overlay" : "primary"}
Icon={audio ? GiSpeaker : GiSpeakerOff}
isActive={audio ?? false}
title={
audio
? t("cameraAudio.disable", { ns: "views/live" })
: t("cameraAudio.enable", { ns: "views/live" })
}
onClick={() => setAudio(!audio)}
disabled={!cameraEnabled}
/>
)}
<FrigateCameraFeatures
camera={camera}
recordingEnabled={camera.record.enabled_in_config}
audioDetectEnabled={camera.audio.enabled_in_config}
autotrackingEnabled={
camera.onvif.autotracking.enabled_in_config
} }
transcriptionEnabled={ onClick={toggleFullscreen}
camera.audio_transcription.enabled_in_config
}
fullscreen={fullscreen}
streamName={streamName ?? ""}
setStreamName={setStreamName}
preferredLiveMode={preferredLiveMode}
playInBackground={playInBackground ?? false}
setPlayInBackground={setPlayInBackground}
showStats={showStats}
setShowStats={setShowStats}
isRestreamed={isRestreamed ?? false}
setLowBandwidth={setLowBandwidth}
supportsAudioOutput={supportsAudioOutput}
supports2WayTalk={supports2WayTalk}
cameraEnabled={cameraEnabled}
/> />
</div> )}
</TooltipProvider> {!isIOS && !isFirefox && preferredLiveMode != "jsmpeg" && (
<CameraFeatureToggle
className="p-2 md:p-0"
variant={fullscreen ? "overlay" : "primary"}
Icon={LuPictureInPicture}
isActive={pip}
title={
pip
? t("button.close", { ns: "common" })
: t("button.pictureInPicture", { ns: "common" })
}
onClick={() => {
if (!pip) {
setPip(true);
} else {
document.exitPictureInPicture();
setPip(false);
}
}}
disabled={!cameraEnabled}
/>
)}
{supports2WayTalk && (
<CameraFeatureToggle
className="p-2 md:p-0"
variant={fullscreen ? "overlay" : "primary"}
Icon={mic ? FaMicrophone : FaMicrophoneSlash}
isActive={mic}
title={
mic
? t("twoWayTalk.disable", { ns: "views/live" })
: t("twoWayTalk.enable", { ns: "views/live" })
}
onClick={() => {
setMic(!mic);
if (!mic && !audio) {
setAudio(true);
}
}}
disabled={!cameraEnabled}
/>
)}
{supportsAudioOutput && preferredLiveMode != "jsmpeg" && (
<CameraFeatureToggle
className="p-2 md:p-0"
variant={fullscreen ? "overlay" : "primary"}
Icon={audio ? GiSpeaker : GiSpeakerOff}
isActive={audio ?? false}
title={
audio
? t("cameraAudio.disable", { ns: "views/live" })
: t("cameraAudio.enable", { ns: "views/live" })
}
onClick={() => setAudio(!audio)}
disabled={!cameraEnabled}
/>
)}
<FrigateCameraFeatures
camera={camera}
recordingEnabled={camera.record.enabled_in_config}
audioDetectEnabled={camera.audio.enabled_in_config}
autotrackingEnabled={camera.onvif.autotracking.enabled_in_config}
transcriptionEnabled={
camera.audio_transcription.enabled_in_config
}
fullscreen={fullscreen}
streamName={streamName ?? ""}
setStreamName={setStreamName}
preferredLiveMode={preferredLiveMode}
playInBackground={playInBackground ?? false}
setPlayInBackground={setPlayInBackground}
showStats={showStats}
setShowStats={setShowStats}
isRestreamed={isRestreamed ?? false}
setLowBandwidth={setLowBandwidth}
supportsAudioOutput={supportsAudioOutput}
supports2WayTalk={supports2WayTalk}
cameraEnabled={cameraEnabled}
/>
</div>
</div> </div>
<div id="player-container" className="size-full" ref={containerRef}> <div id="player-container" className="size-full" ref={containerRef}>
<TransformComponent <TransformComponent
@ -707,27 +703,25 @@ function TooltipButton({
...props ...props
}: TooltipButtonProps) { }: TooltipButtonProps) {
return ( return (
<TooltipProvider> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <Button
<Button aria-label={label}
aria-label={label} onClick={onClick}
onClick={onClick} onMouseDown={onMouseDown}
onMouseDown={onMouseDown} onMouseUp={onMouseUp}
onMouseUp={onMouseUp} onTouchStart={onTouchStart}
onTouchStart={onTouchStart} onTouchEnd={onTouchEnd}
onTouchEnd={onTouchEnd} className={className}
className={className} {...props}
{...props} >
> {children}
{children} </Button>
</Button> </TooltipTrigger>
</TooltipTrigger> <TooltipContent>
<TooltipContent> <p>{label}</p>
<p>{label}</p> </TooltipContent>
</TooltipContent> </Tooltip>
</Tooltip>
</TooltipProvider>
); );
} }
@ -961,59 +955,56 @@ function PtzControlPanel({
)} )}
{ptz?.features?.includes("pt-r-fov") && ( {ptz?.features?.includes("pt-r-fov") && (
<TooltipProvider> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <Button
<Button className={`${clickOverlay ? "text-selected" : "text-primary"}`}
className={`${clickOverlay ? "text-selected" : "text-primary"}`} aria-label={t("ptz.move.clickMove.label")}
aria-label={t("ptz.move.clickMove.label")} onClick={() => setClickOverlay(!clickOverlay)}
onClick={() => setClickOverlay(!clickOverlay)} >
> <TbViewfinder />
<TbViewfinder /> </Button>
</Button> </TooltipTrigger>
</TooltipTrigger> <TooltipContent>
<TooltipContent> <p>
<p> {clickOverlay
{clickOverlay ? t("ptz.move.clickMove.disable")
? t("ptz.move.clickMove.disable") : t("ptz.move.clickMove.enable")}
: t("ptz.move.clickMove.enable")} </p>
</p> </TooltipContent>
</TooltipContent> </Tooltip>
</Tooltip>
</TooltipProvider>
)} )}
{(ptz?.presets?.length ?? 0) > 0 && ( {(ptz?.presets?.length ?? 0) > 0 && (
<TooltipProvider> <DropdownMenu modal={!isDesktop}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<DropdownMenu modal={!isDesktop}> <DropdownMenuTrigger asChild>
<DropdownMenuTrigger asChild> <Button aria-label={t("ptz.presets")}>
<Button aria-label={t("ptz.presets")}> <BsThreeDotsVertical />
<BsThreeDotsVertical /> </Button>
</Button> </DropdownMenuTrigger>
</DropdownMenuTrigger>
<DropdownMenuContent
className="scrollbar-container max-h-[40dvh] overflow-y-auto"
onCloseAutoFocus={(e) => e.preventDefault()}
>
{ptz?.presets.map((preset) => (
<DropdownMenuItem
key={preset}
aria-label={preset}
className="cursor-pointer"
onSelect={() => sendPtz(`preset_${preset}`)}
>
{preset}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>{t("ptz.presets")}</p> <p>{t("ptz.presets")}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider>
<DropdownMenuContent
className="scrollbar-container max-h-[40dvh] overflow-y-auto"
onCloseAutoFocus={(e) => e.preventDefault()}
>
{ptz?.presets.map((preset) => (
<DropdownMenuItem
key={preset}
aria-label={preset}
className="cursor-pointer"
onSelect={() => sendPtz(`preset_${preset}`)}
>
{preset}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)} )}
</div> </div>
); );
@ -1401,9 +1392,11 @@ function FrigateCameraFeatures({
}} }}
> >
<SelectTrigger className="w-full"> <SelectTrigger className="w-full">
{Object.keys(camera.live.streams).find( <SelectValue>
(key) => camera.live.streams[key] === streamName, {Object.keys(camera.live.streams).find(
)} (key) => camera.live.streams[key] === streamName,
)}
</SelectValue>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -1733,9 +1726,11 @@ function FrigateCameraFeatures({
}} }}
> >
<SelectTrigger className="w-full"> <SelectTrigger className="w-full">
{Object.keys(camera.live.streams).find( <SelectValue>
(key) => camera.live.streams[key] === streamName, {Object.keys(camera.live.streams).find(
)} (key) => camera.live.streams[key] === streamName,
)}
</SelectValue>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>