mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-06-29 16:41:16 +03:00
improve scroll handling for non-modal DropdownMenu in classification and face selection dialogs
This commit is contained in:
parent
4e90d254ed
commit
077f4a601a
@ -102,6 +102,19 @@ export default function ClassificationSelectionDialog({
|
|||||||
// control
|
// control
|
||||||
const [newClass, setNewClass] = useState(false);
|
const [newClass, setNewClass] = useState(false);
|
||||||
|
|
||||||
|
// Non-modal Radix DropdownMenu doesn't propagate wheel events to nested
|
||||||
|
// scroll containers, so attach a non-passive listener that scrolls manually.
|
||||||
|
const scrollContainerRef = useCallback((el: HTMLDivElement | null) => {
|
||||||
|
if (!el || !isDesktop) return;
|
||||||
|
const handleWheel = (e: WheelEvent) => {
|
||||||
|
if (el.scrollHeight <= el.clientHeight) return;
|
||||||
|
e.preventDefault();
|
||||||
|
el.scrollTop += e.deltaY;
|
||||||
|
};
|
||||||
|
el.addEventListener("wheel", handleWheel, { passive: false });
|
||||||
|
return () => el.removeEventListener("wheel", handleWheel);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// components
|
// components
|
||||||
const Selector = isDesktop ? DropdownMenu : Drawer;
|
const Selector = isDesktop ? DropdownMenu : Drawer;
|
||||||
const SelectorTrigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger;
|
const SelectorTrigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger;
|
||||||
@ -122,60 +135,62 @@ export default function ClassificationSelectionDialog({
|
|||||||
title={t("createCategory.new")}
|
title={t("createCategory.new")}
|
||||||
onSave={(newCat) => onCategorizeImage(newCat)}
|
onSave={(newCat) => onCategorizeImage(newCat)}
|
||||||
/>
|
/>
|
||||||
|
// keep modal false on desktop to prevent dismissable layer pointer events
|
||||||
<Tooltip>
|
// issue with dialog auto-close
|
||||||
<Selector {...(isDesktop ? { modal: false } : {})}>
|
<Selector {...(isDesktop ? { modal: false } : {})}>
|
||||||
<SelectorTrigger asChild>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild={isChildButton}>{children}</TooltipTrigger>
|
<TooltipTrigger asChild={isChildButton}>
|
||||||
</SelectorTrigger>
|
<SelectorTrigger asChild>{children}</SelectorTrigger>
|
||||||
<SelectorContent
|
</TooltipTrigger>
|
||||||
className={cn("", isMobile && "mx-1 gap-2 rounded-t-2xl px-4")}
|
<TooltipContent>
|
||||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
{tooltipLabel ?? t("categorizeImage")}
|
||||||
>
|
</TooltipContent>
|
||||||
{isMobile && (
|
</Tooltip>
|
||||||
<DrawerHeader className="sr-only">
|
<SelectorContent
|
||||||
<DrawerTitle>Details</DrawerTitle>
|
ref={scrollContainerRef}
|
||||||
<DrawerDescription>Details</DrawerDescription>
|
className={cn(
|
||||||
</DrawerHeader>
|
isDesktop && "scrollbar-container max-h-[40dvh] overflow-y-auto",
|
||||||
)}
|
isMobile && "mx-1 gap-2 rounded-t-2xl px-4",
|
||||||
<DropdownMenuLabel>
|
)}
|
||||||
{dialogLabel ?? t("categorizeImageAs")}
|
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||||
</DropdownMenuLabel>
|
>
|
||||||
<div
|
{isMobile && (
|
||||||
className={cn(
|
<DrawerHeader className="sr-only">
|
||||||
"flex max-h-[40dvh] flex-col overflow-y-auto",
|
<DrawerTitle>Details</DrawerTitle>
|
||||||
isMobile && "gap-2 pb-4",
|
<DrawerDescription>Details</DrawerDescription>
|
||||||
)}
|
</DrawerHeader>
|
||||||
|
)}
|
||||||
|
<DropdownMenuLabel>
|
||||||
|
{dialogLabel ?? t("categorizeImageAs")}
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<div className={cn("flex flex-col", isMobile && "gap-2 pb-4")}>
|
||||||
|
{filteredClasses
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a === "none") return 1;
|
||||||
|
if (b === "none") return -1;
|
||||||
|
return a.localeCompare(b);
|
||||||
|
})
|
||||||
|
.map((category) => (
|
||||||
|
<SelectorItem
|
||||||
|
key={category}
|
||||||
|
className="flex cursor-pointer gap-2 smart-capitalize"
|
||||||
|
onClick={() => onCategorizeImage(category)}
|
||||||
|
>
|
||||||
|
{category === "none"
|
||||||
|
? t("details.none")
|
||||||
|
: category.replaceAll("_", " ")}
|
||||||
|
</SelectorItem>
|
||||||
|
))}
|
||||||
|
<Separator />
|
||||||
|
<SelectorItem
|
||||||
|
className="flex cursor-pointer gap-2 smart-capitalize"
|
||||||
|
onClick={() => setNewClass(true)}
|
||||||
>
|
>
|
||||||
{filteredClasses
|
{t("createCategory.new")}
|
||||||
.sort((a, b) => {
|
</SelectorItem>
|
||||||
if (a === "none") return 1;
|
</div>
|
||||||
if (b === "none") return -1;
|
</SelectorContent>
|
||||||
return a.localeCompare(b);
|
</Selector>
|
||||||
})
|
|
||||||
.map((category) => (
|
|
||||||
<SelectorItem
|
|
||||||
key={category}
|
|
||||||
className="flex cursor-pointer gap-2 smart-capitalize"
|
|
||||||
onClick={() => onCategorizeImage(category)}
|
|
||||||
>
|
|
||||||
{category === "none"
|
|
||||||
? t("details.none")
|
|
||||||
: category.replaceAll("_", " ")}
|
|
||||||
</SelectorItem>
|
|
||||||
))}
|
|
||||||
<Separator />
|
|
||||||
<SelectorItem
|
|
||||||
className="flex cursor-pointer gap-2 smart-capitalize"
|
|
||||||
onClick={() => setNewClass(true)}
|
|
||||||
>
|
|
||||||
{t("createCategory.new")}
|
|
||||||
</SelectorItem>
|
|
||||||
</div>
|
|
||||||
</SelectorContent>
|
|
||||||
</Selector>
|
|
||||||
<TooltipContent>{tooltipLabel ?? t("categorizeImage")}</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import {
|
|||||||
import { isDesktop, isMobile } from "react-device-detect";
|
import { isDesktop, isMobile } from "react-device-detect";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import React, { ReactNode, useMemo, useState } from "react";
|
import React, { ReactNode, useCallback, useMemo, useState } from "react";
|
||||||
import TextEntryDialog from "./dialog/TextEntryDialog";
|
import TextEntryDialog from "./dialog/TextEntryDialog";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
@ -61,6 +61,19 @@ export default function FaceSelectionDialog({
|
|||||||
// control
|
// control
|
||||||
const [newFace, setNewFace] = useState(false);
|
const [newFace, setNewFace] = useState(false);
|
||||||
|
|
||||||
|
// Non-modal Radix DropdownMenu doesn't propagate wheel events to nested
|
||||||
|
// scroll containers, so attach a non-passive listener that scrolls manually.
|
||||||
|
const scrollContainerRef = useCallback((el: HTMLDivElement | null) => {
|
||||||
|
if (!el || !isDesktop) return;
|
||||||
|
const handleWheel = (e: WheelEvent) => {
|
||||||
|
if (el.scrollHeight <= el.clientHeight) return;
|
||||||
|
e.preventDefault();
|
||||||
|
el.scrollTop += e.deltaY;
|
||||||
|
};
|
||||||
|
el.addEventListener("wheel", handleWheel, { passive: false });
|
||||||
|
return () => el.removeEventListener("wheel", handleWheel);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// components
|
// components
|
||||||
const Selector = isDesktop ? DropdownMenu : Drawer;
|
const Selector = isDesktop ? DropdownMenu : Drawer;
|
||||||
const SelectorTrigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger;
|
const SelectorTrigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger;
|
||||||
@ -83,52 +96,58 @@ export default function FaceSelectionDialog({
|
|||||||
onSave={(newName) => onTrainAttempt(newName)}
|
onSave={(newName) => onTrainAttempt(newName)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
// keep modal false on desktop to prevent dismissable layer pointer events
|
||||||
<Tooltip>
|
// issue with dialog auto-close
|
||||||
<Selector {...(isDesktop ? { modal: false } : {})}>
|
<Selector {...(isDesktop ? { modal: false } : {})}>
|
||||||
<SelectorTrigger asChild>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild={isChildButton}>{children}</TooltipTrigger>
|
<TooltipTrigger asChild={isChildButton}>
|
||||||
</SelectorTrigger>
|
<SelectorTrigger asChild>{children}</SelectorTrigger>
|
||||||
<SelectorContent
|
</TooltipTrigger>
|
||||||
className={cn("", isMobile && "mx-1 gap-2 rounded-t-2xl px-4")}
|
<TooltipContent>{tooltipLabel ?? t("trainFace")}</TooltipContent>
|
||||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
</Tooltip>
|
||||||
>
|
<SelectorContent
|
||||||
{isMobile && (
|
ref={scrollContainerRef}
|
||||||
<DrawerHeader className="sr-only">
|
className={cn(
|
||||||
<DrawerTitle>Details</DrawerTitle>
|
isDesktop && "scrollbar-container max-h-[40dvh] overflow-y-auto",
|
||||||
<DrawerDescription>Details</DrawerDescription>
|
isMobile && "mx-1 gap-2 rounded-t-2xl px-4",
|
||||||
</DrawerHeader>
|
)}
|
||||||
|
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{isMobile && (
|
||||||
|
<DrawerHeader className="sr-only">
|
||||||
|
<DrawerTitle>Details</DrawerTitle>
|
||||||
|
<DrawerDescription>Details</DrawerDescription>
|
||||||
|
</DrawerHeader>
|
||||||
|
)}
|
||||||
|
<DropdownMenuLabel>
|
||||||
|
{dialogLabel ?? t("trainFaceAs")}
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col",
|
||||||
|
isMobile &&
|
||||||
|
"max-h-[40dvh] gap-2 overflow-y-auto overflow-x-hidden pb-4",
|
||||||
)}
|
)}
|
||||||
<DropdownMenuLabel>
|
>
|
||||||
{dialogLabel ?? t("trainFaceAs")}
|
{filteredNames.sort().map((faceName) => (
|
||||||
</DropdownMenuLabel>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex max-h-[40dvh] flex-col overflow-y-auto overflow-x-hidden",
|
|
||||||
isMobile && "gap-2 pb-4",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{filteredNames.sort().map((faceName) => (
|
|
||||||
<SelectorItem
|
|
||||||
key={faceName}
|
|
||||||
className="flex cursor-pointer gap-2 smart-capitalize"
|
|
||||||
onClick={() => onTrainAttempt(faceName)}
|
|
||||||
>
|
|
||||||
{faceName}
|
|
||||||
</SelectorItem>
|
|
||||||
))}
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<SelectorItem
|
<SelectorItem
|
||||||
|
key={faceName}
|
||||||
className="flex cursor-pointer gap-2 smart-capitalize"
|
className="flex cursor-pointer gap-2 smart-capitalize"
|
||||||
onClick={() => setNewFace(true)}
|
onClick={() => onTrainAttempt(faceName)}
|
||||||
>
|
>
|
||||||
{t("createFaceLibrary.new")}
|
{faceName}
|
||||||
</SelectorItem>
|
</SelectorItem>
|
||||||
</div>
|
))}
|
||||||
</SelectorContent>
|
<DropdownMenuSeparator />
|
||||||
</Selector>
|
<SelectorItem
|
||||||
<TooltipContent>{tooltipLabel ?? t("trainFace")}</TooltipContent>
|
className="flex cursor-pointer gap-2 smart-capitalize"
|
||||||
</Tooltip>
|
onClick={() => setNewFace(true)}
|
||||||
|
>
|
||||||
|
{t("createFaceLibrary.new")}
|
||||||
|
</SelectorItem>
|
||||||
|
</div>
|
||||||
|
</SelectorContent>
|
||||||
|
</Selector>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user