diff --git a/web/src/components/overlay/ClassificationSelectionDialog.tsx b/web/src/components/overlay/ClassificationSelectionDialog.tsx index 7959e67bf9..ed16e42135 100644 --- a/web/src/components/overlay/ClassificationSelectionDialog.tsx +++ b/web/src/components/overlay/ClassificationSelectionDialog.tsx @@ -102,6 +102,19 @@ export default function ClassificationSelectionDialog({ // control 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 const Selector = isDesktop ? DropdownMenu : Drawer; const SelectorTrigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger; @@ -122,60 +135,62 @@ export default function ClassificationSelectionDialog({ title={t("createCategory.new")} onSave={(newCat) => onCategorizeImage(newCat)} /> - - - - - {children} - - e.preventDefault()} - > - {isMobile && ( - - Details - Details - - )} - - {dialogLabel ?? t("categorizeImageAs")} - -
+ + + {children} + + + {tooltipLabel ?? t("categorizeImage")} + + + e.preventDefault()} + > + {isMobile && ( + + Details + Details + + )} + + {dialogLabel ?? t("categorizeImageAs")} + +
+ {filteredClasses + .sort((a, b) => { + if (a === "none") return 1; + if (b === "none") return -1; + return a.localeCompare(b); + }) + .map((category) => ( + onCategorizeImage(category)} + > + {category === "none" + ? t("details.none") + : category.replaceAll("_", " ")} + + ))} + + setNewClass(true)} > - {filteredClasses - .sort((a, b) => { - if (a === "none") return 1; - if (b === "none") return -1; - return a.localeCompare(b); - }) - .map((category) => ( - onCategorizeImage(category)} - > - {category === "none" - ? t("details.none") - : category.replaceAll("_", " ")} - - ))} - - setNewClass(true)} - > - {t("createCategory.new")} - -
-
- - {tooltipLabel ?? t("categorizeImage")} - + {t("createCategory.new")} + +
+
+
); } diff --git a/web/src/components/overlay/FaceSelectionDialog.tsx b/web/src/components/overlay/FaceSelectionDialog.tsx index 1b049f4220..fe1f96678c 100644 --- a/web/src/components/overlay/FaceSelectionDialog.tsx +++ b/web/src/components/overlay/FaceSelectionDialog.tsx @@ -23,7 +23,7 @@ import { import { isDesktop, isMobile } from "react-device-detect"; import { useTranslation } from "react-i18next"; 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 { Button } from "../ui/button"; @@ -61,6 +61,19 @@ export default function FaceSelectionDialog({ // control 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 const Selector = isDesktop ? DropdownMenu : Drawer; const SelectorTrigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger; @@ -83,52 +96,58 @@ export default function FaceSelectionDialog({ onSave={(newName) => onTrainAttempt(newName)} /> )} - - - - - {children} - - e.preventDefault()} - > - {isMobile && ( - - Details - Details - + // keep modal false on desktop to prevent dismissable layer pointer events + // issue with dialog auto-close + + + + {children} + + {tooltipLabel ?? t("trainFace")} + + e.preventDefault()} + > + {isMobile && ( + + Details + Details + + )} + + {dialogLabel ?? t("trainFaceAs")} + +
- {dialogLabel ?? t("trainFaceAs")} - -
- {filteredNames.sort().map((faceName) => ( - onTrainAttempt(faceName)} - > - {faceName} - - ))} - + > + {filteredNames.sort().map((faceName) => ( setNewFace(true)} + onClick={() => onTrainAttempt(faceName)} > - {t("createFaceLibrary.new")} + {faceName} -
- - - {tooltipLabel ?? t("trainFace")} - + ))} + + setNewFace(true)} + > + {t("createFaceLibrary.new")} + +
+
+
); }