From 4a9ef76725259fafd88bbfe4dbc3dede9edaee17 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 12 Sep 2024 17:42:36 -0500 Subject: [PATCH] Use arrow keys to navigate Explore view --- web/src/views/explore/ExploreView.tsx | 8 +-- web/src/views/search/SearchView.tsx | 71 ++++++++++++++++++++++++--- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/web/src/views/explore/ExploreView.tsx b/web/src/views/explore/ExploreView.tsx index b8ab51d80..48fee439c 100644 --- a/web/src/views/explore/ExploreView.tsx +++ b/web/src/views/explore/ExploreView.tsx @@ -17,7 +17,7 @@ import useImageLoaded from "@/hooks/use-image-loaded"; import ActivityIndicator from "@/components/indicators/activity-indicator"; type ExploreViewProps = { - onSelectSearch: (searchResult: SearchResult) => void; + onSelectSearch: (searchResult: SearchResult, index: number) => void; }; export default function ExploreView({ onSelectSearch }: ExploreViewProps) { @@ -76,7 +76,7 @@ export default function ExploreView({ onSelectSearch }: ExploreViewProps) { type ThumbnailRowType = { objectType: string; searchResults?: SearchResult[]; - onSelectSearch: (searchResult: SearchResult) => void; + onSelectSearch: (searchResult: SearchResult, index: number) => void; }; function ThumbnailRow({ @@ -145,7 +145,7 @@ function ThumbnailRow({ type ExploreThumbnailImageProps = { event: SearchResult; - onSelectSearch: (searchResult: SearchResult) => void; + onSelectSearch: (searchResult: SearchResult, index: number) => void; }; function ExploreThumbnailImage({ event, @@ -176,7 +176,7 @@ function ExploreThumbnailImage({ loading={isSafari ? "eager" : "lazy"} draggable={false} src={`${apiHost}api/events/${event.id}/thumbnail.jpg`} - onClick={() => onSelectSearch(event)} + onClick={() => onSelectSearch(event, 0)} onLoad={() => { onImgLoad(); }} diff --git a/web/src/views/search/SearchView.tsx b/web/src/views/search/SearchView.tsx index f1f706dab..3b6fb49e3 100644 --- a/web/src/views/search/SearchView.tsx +++ b/web/src/views/search/SearchView.tsx @@ -13,11 +13,15 @@ import { import { cn } from "@/lib/utils"; import { FrigateConfig } from "@/types/frigateConfig"; import { SearchFilter, SearchResult } from "@/types/search"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { isMobileOnly } from "react-device-detect"; import { LuImage, LuSearchX, LuText, LuXCircle } from "react-icons/lu"; import useSWR from "swr"; import ExploreView from "../explore/ExploreView"; +import useKeyboardListener, { + KeyModifiers, +} from "@/hooks/use-keyboard-listener"; +import scrollIntoView from "scroll-into-view-if-needed"; type SearchViewProps = { search: string; @@ -59,8 +63,12 @@ export default function SearchView({ // search interaction - const onSelectSearch = useCallback((item: SearchResult) => { + const [selectedIndex, setSelectedIndex] = useState(null); + const itemRefs = useRef<(HTMLDivElement | null)[]>([]); + + const onSelectSearch = useCallback((item: SearchResult, index: number) => { setSearchDetail(item); + setSelectedIndex(index); }, []); // confidence score - probably needs tweaking @@ -87,6 +95,56 @@ export default function SearchView({ [searchResults, searchFilter], ); + // keyboard listener + + const onKeyboardShortcut = useCallback( + (key: string | null, modifiers: KeyModifiers) => { + if (!modifiers.down || !uniqueResults) { + return; + } + + switch (key) { + case "ArrowLeft": + setSelectedIndex((prevIndex) => { + const newIndex = + prevIndex === null + ? uniqueResults.length - 1 + : (prevIndex - 1 + uniqueResults.length) % uniqueResults.length; + setSearchDetail(uniqueResults[newIndex]); + return newIndex; + }); + break; + case "ArrowRight": + setSelectedIndex((prevIndex) => { + const newIndex = + prevIndex === null ? 0 : (prevIndex + 1) % uniqueResults.length; + setSearchDetail(uniqueResults[newIndex]); + return newIndex; + }); + break; + } + }, + [uniqueResults], + ); + + useKeyboardListener(["ArrowLeft", "ArrowRight"], onKeyboardShortcut); + + // scroll into view + + useEffect(() => { + if ( + selectedIndex !== null && + uniqueResults && + itemRefs.current?.[selectedIndex] + ) { + scrollIntoView(itemRefs.current[selectedIndex], { + block: "center", + behavior: "smooth", + scrollMode: "if-needed", + }); + } + }, [selectedIndex, uniqueResults]); + return (
@@ -156,12 +214,13 @@ export default function SearchView({ {uniqueResults && (
{uniqueResults && - uniqueResults.map((value) => { - const selected = false; + uniqueResults.map((value, index) => { + const selected = selectedIndex === index; return (
(itemRefs.current[index] = item)} data-start={value.start_time} className="review-item relative rounded-lg" > @@ -173,7 +232,7 @@ export default function SearchView({ setSimilaritySearch(value)} - onClick={() => onSelectSearch(value)} + onClick={() => onSelectSearch(value, index)} /> {searchTerm && (
@@ -207,7 +266,7 @@ export default function SearchView({ )}
);