* Fix dialog autofocus problems on mobile

* set font size to prevent mobile zooming

* Use arrow keys to navigate Explore view
This commit is contained in:
Josh Hawkins
2024-09-12 22:07:35 -05:00
committed by GitHub
parent 1f9ba1d625
commit 61854f1d6a
11 changed files with 92 additions and 27 deletions

View File

@@ -2,7 +2,7 @@ import ActivityIndicator from "../indicators/activity-indicator";
import { LuTrash } from "react-icons/lu"; import { LuTrash } from "react-icons/lu";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { isDesktop } from "react-device-detect"; import { isDesktop, isMobile } from "react-device-detect";
import { FaDownload, FaPlay, FaShareAlt } from "react-icons/fa"; import { FaDownload, FaPlay, FaShareAlt } from "react-icons/fa";
import Chip from "../indicators/Chip"; import Chip from "../indicators/Chip";
import { Skeleton } from "../ui/skeleton"; import { Skeleton } from "../ui/skeleton";
@@ -82,7 +82,13 @@ export default function ExportCard({
} }
}} }}
> >
<DialogContent> <DialogContent
onOpenAutoFocus={(e) => {
if (isMobile) {
e.preventDefault();
}
}}
>
<DialogTitle>Rename Export</DialogTitle> <DialogTitle>Rename Export</DialogTitle>
<DialogDescription> <DialogDescription>
Enter a new name for this export. Enter a new name for this export.
@@ -90,7 +96,7 @@ export default function ExportCard({
{editName && ( {editName && (
<> <>
<Input <Input
className="mt-3" className="text-md mt-3"
type="search" type="search"
placeholder={editName?.original} placeholder={editName?.original}
value={ value={

View File

@@ -710,7 +710,7 @@ export function CameraGroupEdit({
<FormLabel>Name</FormLabel> <FormLabel>Name</FormLabel>
<FormControl> <FormControl>
<Input <Input
className="w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]" className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
placeholder="Enter a name..." placeholder="Enter a name..."
disabled={editingGroup !== undefined} disabled={editingGroup !== undefined}
{...field} {...field}

View File

@@ -74,7 +74,7 @@ export default function CreateUserDialog({
<FormLabel>User</FormLabel> <FormLabel>User</FormLabel>
<FormControl> <FormControl>
<Input <Input
className="w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]" className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
{...field} {...field}
/> />
</FormControl> </FormControl>
@@ -89,7 +89,7 @@ export default function CreateUserDialog({
<FormLabel>Password</FormLabel> <FormLabel>Password</FormLabel>
<FormControl> <FormControl>
<Input <Input
className="w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]" className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
type="password" type="password"
{...field} {...field}
/> />

View File

@@ -273,7 +273,7 @@ export function ExportContent({
/> />
)} )}
<Input <Input
className="my-6" className="text-md my-6"
type="search" type="search"
placeholder="Name the Export" placeholder="Name the Export"
value={name} value={name}
@@ -431,7 +431,7 @@ function CustomTimeSelector({
/> />
<SelectSeparator className="bg-secondary" /> <SelectSeparator className="bg-secondary" />
<input <input
className="mx-4 w-full border border-input bg-background p-1 text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]" className="text-md mx-4 w-full border border-input bg-background p-1 text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
id="startTime" id="startTime"
type="time" type="time"
value={startClock} value={startClock}
@@ -493,7 +493,7 @@ function CustomTimeSelector({
/> />
<SelectSeparator className="bg-secondary" /> <SelectSeparator className="bg-secondary" />
<input <input
className="mx-4 w-full border border-input bg-background p-1 text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]" className="text-md mx-4 w-full border border-input bg-background p-1 text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
id="startTime" id="startTime"
type="time" type="time"
value={endClock} value={endClock}

View File

@@ -23,12 +23,12 @@ export default function SetPasswordDialog({
return ( return (
<Dialog open={show} onOpenChange={onCancel}> <Dialog open={show} onOpenChange={onCancel}>
<DialogContent> <DialogContent onOpenAutoFocus={(e) => e.preventDefault()}>
<DialogHeader> <DialogHeader>
<DialogTitle>Set Password</DialogTitle> <DialogTitle>Set Password</DialogTitle>
</DialogHeader> </DialogHeader>
<Input <Input
className="w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]" className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
type="password" type="password"
value={password} value={password}
onChange={(event) => setPassword(event.target.value)} onChange={(event) => setPassword(event.target.value)}

View File

@@ -178,7 +178,7 @@ export function AnnotationSettingsPane({
<div className="flex flex-col"> <div className="flex flex-col">
<FormControl> <FormControl>
<Input <Input
className="w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]" className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
placeholder="0" placeholder="0"
{...field} {...field}
/> />

View File

@@ -373,7 +373,7 @@ export default function ZoneEditPane({
<FormLabel>Name</FormLabel> <FormLabel>Name</FormLabel>
<FormControl> <FormControl>
<Input <Input
className="w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]" className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
placeholder="Enter a name..." placeholder="Enter a name..."
{...field} {...field}
/> />
@@ -395,7 +395,7 @@ export default function ZoneEditPane({
<FormLabel>Inertia</FormLabel> <FormLabel>Inertia</FormLabel>
<FormControl> <FormControl>
<Input <Input
className="w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]" className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
placeholder="3" placeholder="3"
{...field} {...field}
/> />
@@ -417,7 +417,7 @@ export default function ZoneEditPane({
<FormLabel>Loitering Time</FormLabel> <FormLabel>Loitering Time</FormLabel>
<FormControl> <FormControl>
<Input <Input
className="w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]" className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
placeholder="0" placeholder="0"
{...field} {...field}
/> />

View File

@@ -175,7 +175,7 @@ function Exports() {
{exports && ( {exports && (
<div className="flex w-full items-center justify-center p-2"> <div className="flex w-full items-center justify-center p-2">
<Input <Input
className="w-full bg-muted md:w-1/3" className="text-md w-full bg-muted md:w-1/3"
placeholder="Search" placeholder="Search"
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}

View File

@@ -17,7 +17,7 @@ import useImageLoaded from "@/hooks/use-image-loaded";
import ActivityIndicator from "@/components/indicators/activity-indicator"; import ActivityIndicator from "@/components/indicators/activity-indicator";
type ExploreViewProps = { type ExploreViewProps = {
onSelectSearch: (searchResult: SearchResult) => void; onSelectSearch: (searchResult: SearchResult, index: number) => void;
}; };
export default function ExploreView({ onSelectSearch }: ExploreViewProps) { export default function ExploreView({ onSelectSearch }: ExploreViewProps) {
@@ -76,7 +76,7 @@ export default function ExploreView({ onSelectSearch }: ExploreViewProps) {
type ThumbnailRowType = { type ThumbnailRowType = {
objectType: string; objectType: string;
searchResults?: SearchResult[]; searchResults?: SearchResult[];
onSelectSearch: (searchResult: SearchResult) => void; onSelectSearch: (searchResult: SearchResult, index: number) => void;
}; };
function ThumbnailRow({ function ThumbnailRow({
@@ -145,7 +145,7 @@ function ThumbnailRow({
type ExploreThumbnailImageProps = { type ExploreThumbnailImageProps = {
event: SearchResult; event: SearchResult;
onSelectSearch: (searchResult: SearchResult) => void; onSelectSearch: (searchResult: SearchResult, index: number) => void;
}; };
function ExploreThumbnailImage({ function ExploreThumbnailImage({
event, event,
@@ -176,7 +176,7 @@ function ExploreThumbnailImage({
loading={isSafari ? "eager" : "lazy"} loading={isSafari ? "eager" : "lazy"}
draggable={false} draggable={false}
src={`${apiHost}api/events/${event.id}/thumbnail.jpg`} src={`${apiHost}api/events/${event.id}/thumbnail.jpg`}
onClick={() => onSelectSearch(event)} onClick={() => onSelectSearch(event, 0)}
onLoad={() => { onLoad={() => {
onImgLoad(); onImgLoad();
}} }}

View File

@@ -13,11 +13,15 @@ import {
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { SearchFilter, SearchResult } from "@/types/search"; 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 { isMobileOnly } from "react-device-detect";
import { LuImage, LuSearchX, LuText, LuXCircle } from "react-icons/lu"; import { LuImage, LuSearchX, LuText, LuXCircle } from "react-icons/lu";
import useSWR from "swr"; import useSWR from "swr";
import ExploreView from "../explore/ExploreView"; import ExploreView from "../explore/ExploreView";
import useKeyboardListener, {
KeyModifiers,
} from "@/hooks/use-keyboard-listener";
import scrollIntoView from "scroll-into-view-if-needed";
type SearchViewProps = { type SearchViewProps = {
search: string; search: string;
@@ -59,8 +63,12 @@ export default function SearchView({
// search interaction // search interaction
const onSelectSearch = useCallback((item: SearchResult) => { const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
const onSelectSearch = useCallback((item: SearchResult, index: number) => {
setSearchDetail(item); setSearchDetail(item);
setSelectedIndex(index);
}, []); }, []);
// confidence score - probably needs tweaking // confidence score - probably needs tweaking
@@ -87,6 +95,56 @@ export default function SearchView({
[searchResults, searchFilter], [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 ( return (
<div className="flex size-full flex-col pt-2 md:py-2"> <div className="flex size-full flex-col pt-2 md:py-2">
<Toaster closeButton={true} /> <Toaster closeButton={true} />
@@ -156,12 +214,13 @@ export default function SearchView({
{uniqueResults && ( {uniqueResults && (
<div className="mt-2 grid w-full gap-2 px-1 sm:grid-cols-2 md:mx-2 md:grid-cols-4 md:gap-4 3xl:grid-cols-6"> <div className="mt-2 grid w-full gap-2 px-1 sm:grid-cols-2 md:mx-2 md:grid-cols-4 md:gap-4 3xl:grid-cols-6">
{uniqueResults && {uniqueResults &&
uniqueResults.map((value) => { uniqueResults.map((value, index) => {
const selected = false; const selected = selectedIndex === index;
return ( return (
<div <div
key={value.id} key={value.id}
ref={(item) => (itemRefs.current[index] = item)}
data-start={value.start_time} data-start={value.start_time}
className="review-item relative rounded-lg" className="review-item relative rounded-lg"
> >
@@ -173,7 +232,7 @@ export default function SearchView({
<SearchThumbnail <SearchThumbnail
searchResult={value} searchResult={value}
findSimilar={() => setSimilaritySearch(value)} findSimilar={() => setSimilaritySearch(value)}
onClick={() => onSelectSearch(value)} onClick={() => onSelectSearch(value, index)}
/> />
{searchTerm && ( {searchTerm && (
<div className={cn("absolute right-2 top-2 z-40")}> <div className={cn("absolute right-2 top-2 z-40")}>
@@ -207,7 +266,7 @@ export default function SearchView({
)} )}
</div> </div>
<div <div
className={`review-item-ring pointer-events-none absolute inset-0 z-10 size-full rounded-lg outline outline-[3px] -outline-offset-[2.8px] ${selected ? `shadow-severity_alert outline-severity_alert` : "outline-transparent duration-500"}`} className={`review-item-ring pointer-events-none absolute inset-0 z-10 size-full rounded-lg outline outline-[3px] -outline-offset-[2.8px] ${selected ? `shadow-selected outline-selected` : "outline-transparent duration-500"}`}
/> />
</div> </div>
); );

View File

@@ -254,7 +254,7 @@ export default function NotificationView({
<FormLabel>Email</FormLabel> <FormLabel>Email</FormLabel>
<FormControl> <FormControl>
<Input <Input
className="w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark] md:w-72" className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark] md:w-72"
placeholder="example@email.com" placeholder="example@email.com"
{...field} {...field}
/> />