mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Search UI tweaks (#13965)
* Prevent keyboard shortcuts from running when input is focused * fix reset button and update time pickers when using input * simplify css * consistent button order and spacing
This commit is contained in:
		
							parent
							
								
									fef30bc671
								
							
						
					
					
						commit
						32c7669b28
					
				@ -430,6 +430,13 @@ function TimeRangeFilterButton({
 | 
				
			|||||||
  const formattedSelectedAfter = useFormattedHour(config, selectedAfterHour);
 | 
					  const formattedSelectedAfter = useFormattedHour(config, selectedAfterHour);
 | 
				
			||||||
  const formattedSelectedBefore = useFormattedHour(config, selectedBeforeHour);
 | 
					  const formattedSelectedBefore = useFormattedHour(config, selectedBeforeHour);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    setSelectedAfterHour(afterHour);
 | 
				
			||||||
 | 
					    setSelectedBeforeHour(beforeHour);
 | 
				
			||||||
 | 
					    // only refresh when state changes
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, [timeRange]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const trigger = (
 | 
					  const trigger = (
 | 
				
			||||||
    <Button
 | 
					    <Button
 | 
				
			||||||
      size="sm"
 | 
					      size="sm"
 | 
				
			||||||
@ -447,18 +454,8 @@ function TimeRangeFilterButton({
 | 
				
			|||||||
    </Button>
 | 
					    </Button>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  const content = (
 | 
					  const content = (
 | 
				
			||||||
    <div
 | 
					    <div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
 | 
				
			||||||
      className={cn(
 | 
					      <div className="my-5 flex flex-row items-center justify-center gap-2">
 | 
				
			||||||
        "scrollbar-container flex h-auto max-h-[80dvh] flex-col overflow-y-auto overflow-x-hidden",
 | 
					 | 
				
			||||||
        isDesktop ? "w-64" : "w-full gap-2 pt-2",
 | 
					 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <div
 | 
					 | 
				
			||||||
        className={cn(
 | 
					 | 
				
			||||||
          "mt-3 flex w-full items-center rounded-lg text-secondary-foreground",
 | 
					 | 
				
			||||||
          isDesktop ? "mx-6 gap-2 px-2" : "justify-center gap-2",
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <Popover
 | 
					        <Popover
 | 
				
			||||||
          open={startOpen}
 | 
					          open={startOpen}
 | 
				
			||||||
          onOpenChange={(open) => {
 | 
					          onOpenChange={(open) => {
 | 
				
			||||||
@ -480,7 +477,7 @@ function TimeRangeFilterButton({
 | 
				
			|||||||
              {formattedSelectedAfter}
 | 
					              {formattedSelectedAfter}
 | 
				
			||||||
            </Button>
 | 
					            </Button>
 | 
				
			||||||
          </PopoverTrigger>
 | 
					          </PopoverTrigger>
 | 
				
			||||||
          <PopoverContent className="flex flex-col items-center">
 | 
					          <PopoverContent className="flex flex-row items-center justify-center">
 | 
				
			||||||
            <input
 | 
					            <input
 | 
				
			||||||
              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]"
 | 
					              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"
 | 
				
			||||||
@ -534,8 +531,8 @@ function TimeRangeFilterButton({
 | 
				
			|||||||
            />
 | 
					            />
 | 
				
			||||||
          </PopoverContent>
 | 
					          </PopoverContent>
 | 
				
			||||||
        </Popover>
 | 
					        </Popover>
 | 
				
			||||||
        <DropdownMenuSeparator />
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					      <DropdownMenuSeparator />
 | 
				
			||||||
      <div className="flex items-center justify-evenly p-2">
 | 
					      <div className="flex items-center justify-evenly p-2">
 | 
				
			||||||
        <Button
 | 
					        <Button
 | 
				
			||||||
          variant="select"
 | 
					          variant="select"
 | 
				
			||||||
@ -558,6 +555,7 @@ function TimeRangeFilterButton({
 | 
				
			|||||||
          onClick={() => {
 | 
					          onClick={() => {
 | 
				
			||||||
            setSelectedAfterHour(DEFAULT_TIME_RANGE_AFTER);
 | 
					            setSelectedAfterHour(DEFAULT_TIME_RANGE_AFTER);
 | 
				
			||||||
            setSelectedBeforeHour(DEFAULT_TIME_RANGE_BEFORE);
 | 
					            setSelectedBeforeHour(DEFAULT_TIME_RANGE_BEFORE);
 | 
				
			||||||
 | 
					            updateTimeRange(undefined);
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          Reset
 | 
					          Reset
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import { useState, useRef, useEffect, useCallback } from "react";
 | 
					import React, { useState, useRef, useEffect, useCallback } from "react";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  LuX,
 | 
					  LuX,
 | 
				
			||||||
  LuFilter,
 | 
					  LuFilter,
 | 
				
			||||||
@ -45,6 +45,8 @@ import useSWR from "swr";
 | 
				
			|||||||
import { FrigateConfig } from "@/types/frigateConfig";
 | 
					import { FrigateConfig } from "@/types/frigateConfig";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type InputWithTagsProps = {
 | 
					type InputWithTagsProps = {
 | 
				
			||||||
 | 
					  inputFocused: boolean;
 | 
				
			||||||
 | 
					  setInputFocused: React.Dispatch<React.SetStateAction<boolean>>;
 | 
				
			||||||
  filters: SearchFilter;
 | 
					  filters: SearchFilter;
 | 
				
			||||||
  setFilters: (filter: SearchFilter) => void;
 | 
					  setFilters: (filter: SearchFilter) => void;
 | 
				
			||||||
  search: string;
 | 
					  search: string;
 | 
				
			||||||
@ -55,6 +57,8 @@ type InputWithTagsProps = {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function InputWithTags({
 | 
					export default function InputWithTags({
 | 
				
			||||||
 | 
					  inputFocused,
 | 
				
			||||||
 | 
					  setInputFocused,
 | 
				
			||||||
  filters,
 | 
					  filters,
 | 
				
			||||||
  setFilters,
 | 
					  setFilters,
 | 
				
			||||||
  search,
 | 
					  search,
 | 
				
			||||||
@ -69,7 +73,6 @@ export default function InputWithTags({
 | 
				
			|||||||
  const [currentFilterType, setCurrentFilterType] = useState<FilterType | null>(
 | 
					  const [currentFilterType, setCurrentFilterType] = useState<FilterType | null>(
 | 
				
			||||||
    null,
 | 
					    null,
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  const [inputFocused, setInputFocused] = useState(false);
 | 
					 | 
				
			||||||
  const [isSimilaritySearch, setIsSimilaritySearch] = useState(false);
 | 
					  const [isSimilaritySearch, setIsSimilaritySearch] = useState(false);
 | 
				
			||||||
  const inputRef = useRef<HTMLInputElement>(null);
 | 
					  const inputRef = useRef<HTMLInputElement>(null);
 | 
				
			||||||
  const commandRef = useRef<HTMLDivElement>(null);
 | 
					  const commandRef = useRef<HTMLDivElement>(null);
 | 
				
			||||||
@ -381,7 +384,7 @@ export default function InputWithTags({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const handleInputFocus = useCallback(() => {
 | 
					  const handleInputFocus = useCallback(() => {
 | 
				
			||||||
    setInputFocused(true);
 | 
					    setInputFocused(true);
 | 
				
			||||||
  }, []);
 | 
					  }, [setInputFocused]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleClearInput = useCallback(() => {
 | 
					  const handleClearInput = useCallback(() => {
 | 
				
			||||||
    setInputFocused(false);
 | 
					    setInputFocused(false);
 | 
				
			||||||
@ -392,16 +395,19 @@ export default function InputWithTags({
 | 
				
			|||||||
    setFilters({});
 | 
					    setFilters({});
 | 
				
			||||||
    setCurrentFilterType(null);
 | 
					    setCurrentFilterType(null);
 | 
				
			||||||
    setIsSimilaritySearch(false);
 | 
					    setIsSimilaritySearch(false);
 | 
				
			||||||
  }, [setFilters, resetSuggestions, setSearch]);
 | 
					  }, [setFilters, resetSuggestions, setSearch, setInputFocused]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleInputBlur = useCallback((e: React.FocusEvent) => {
 | 
					  const handleInputBlur = useCallback(
 | 
				
			||||||
    if (
 | 
					    (e: React.FocusEvent) => {
 | 
				
			||||||
      commandRef.current &&
 | 
					      if (
 | 
				
			||||||
      !commandRef.current.contains(e.relatedTarget as Node)
 | 
					        commandRef.current &&
 | 
				
			||||||
    ) {
 | 
					        !commandRef.current.contains(e.relatedTarget as Node)
 | 
				
			||||||
      setInputFocused(false);
 | 
					      ) {
 | 
				
			||||||
    }
 | 
					        setInputFocused(false);
 | 
				
			||||||
  }, []);
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [setInputFocused],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleSuggestionClick = useCallback(
 | 
					  const handleSuggestionClick = useCallback(
 | 
				
			||||||
    (suggestion: string) => {
 | 
					    (suggestion: string) => {
 | 
				
			||||||
@ -449,7 +455,7 @@ export default function InputWithTags({
 | 
				
			|||||||
      setInputFocused(false);
 | 
					      setInputFocused(false);
 | 
				
			||||||
      inputRef?.current?.blur();
 | 
					      inputRef?.current?.blur();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    [setSearch],
 | 
					    [setSearch, setInputFocused],
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleInputKeyDown = useCallback(
 | 
					  const handleInputKeyDown = useCallback(
 | 
				
			||||||
 | 
				
			|||||||
@ -414,17 +414,7 @@ export function DateRangePicker({
 | 
				
			|||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div className="flex justify-center gap-2 py-2 pr-4">
 | 
					      <div className="mx-auto flex w-64 items-center justify-evenly gap-2 py-2">
 | 
				
			||||||
        <Button
 | 
					 | 
				
			||||||
          onClick={() => {
 | 
					 | 
				
			||||||
            setIsOpen(false);
 | 
					 | 
				
			||||||
            resetValues();
 | 
					 | 
				
			||||||
            onReset?.();
 | 
					 | 
				
			||||||
          }}
 | 
					 | 
				
			||||||
          variant="ghost"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          Reset
 | 
					 | 
				
			||||||
        </Button>
 | 
					 | 
				
			||||||
        <Button
 | 
					        <Button
 | 
				
			||||||
          variant="select"
 | 
					          variant="select"
 | 
				
			||||||
          onClick={() => {
 | 
					          onClick={() => {
 | 
				
			||||||
@ -439,6 +429,16 @@ export function DateRangePicker({
 | 
				
			|||||||
        >
 | 
					        >
 | 
				
			||||||
          Apply
 | 
					          Apply
 | 
				
			||||||
        </Button>
 | 
					        </Button>
 | 
				
			||||||
 | 
					        <Button
 | 
				
			||||||
 | 
					          onClick={() => {
 | 
				
			||||||
 | 
					            setIsOpen(false);
 | 
				
			||||||
 | 
					            resetValues();
 | 
				
			||||||
 | 
					            onReset?.();
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					          variant="ghost"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          Reset
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
				
			|||||||
@ -10,6 +10,7 @@ export type KeyModifiers = {
 | 
				
			|||||||
export default function useKeyboardListener(
 | 
					export default function useKeyboardListener(
 | 
				
			||||||
  keys: string[],
 | 
					  keys: string[],
 | 
				
			||||||
  listener: (key: string | null, modifiers: KeyModifiers) => void,
 | 
					  listener: (key: string | null, modifiers: KeyModifiers) => void,
 | 
				
			||||||
 | 
					  preventDefault: boolean = true,
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  const keyDownListener = useCallback(
 | 
					  const keyDownListener = useCallback(
 | 
				
			||||||
    (e: KeyboardEvent) => {
 | 
					    (e: KeyboardEvent) => {
 | 
				
			||||||
@ -25,13 +26,13 @@ export default function useKeyboardListener(
 | 
				
			|||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (keys.includes(e.key)) {
 | 
					      if (keys.includes(e.key)) {
 | 
				
			||||||
        e.preventDefault();
 | 
					        if (preventDefault) e.preventDefault();
 | 
				
			||||||
        listener(e.key, modifiers);
 | 
					        listener(e.key, modifiers);
 | 
				
			||||||
      } else if (e.key === "Shift" || e.key === "Control" || e.key === "Meta") {
 | 
					      } else if (e.key === "Shift" || e.key === "Control" || e.key === "Meta") {
 | 
				
			||||||
        listener(null, modifiers);
 | 
					        listener(null, modifiers);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    [keys, listener],
 | 
					    [keys, listener, preventDefault],
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const keyUpListener = useCallback(
 | 
					  const keyUpListener = useCallback(
 | 
				
			||||||
 | 
				
			|||||||
@ -209,9 +209,11 @@ export default function SearchView({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // keyboard listener
 | 
					  // keyboard listener
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [inputFocused, setInputFocused] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const onKeyboardShortcut = useCallback(
 | 
					  const onKeyboardShortcut = useCallback(
 | 
				
			||||||
    (key: string | null, modifiers: KeyModifiers) => {
 | 
					    (key: string | null, modifiers: KeyModifiers) => {
 | 
				
			||||||
      if (!modifiers.down || !uniqueResults) {
 | 
					      if (!modifiers.down || !uniqueResults || inputFocused) {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -236,10 +238,14 @@ export default function SearchView({
 | 
				
			|||||||
          break;
 | 
					          break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    [uniqueResults],
 | 
					    [uniqueResults, inputFocused],
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useKeyboardListener(["ArrowLeft", "ArrowRight"], onKeyboardShortcut);
 | 
					  useKeyboardListener(
 | 
				
			||||||
 | 
					    ["ArrowLeft", "ArrowRight"],
 | 
				
			||||||
 | 
					    onKeyboardShortcut,
 | 
				
			||||||
 | 
					    !inputFocused,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // scroll into view
 | 
					  // scroll into view
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -310,6 +316,8 @@ export default function SearchView({
 | 
				
			|||||||
        {config?.semantic_search?.enabled && (
 | 
					        {config?.semantic_search?.enabled && (
 | 
				
			||||||
          <div className={cn("z-[41] w-full lg:absolute lg:top-0 lg:w-1/3")}>
 | 
					          <div className={cn("z-[41] w-full lg:absolute lg:top-0 lg:w-1/3")}>
 | 
				
			||||||
            <InputWithTags
 | 
					            <InputWithTags
 | 
				
			||||||
 | 
					              inputFocused={inputFocused}
 | 
				
			||||||
 | 
					              setInputFocused={setInputFocused}
 | 
				
			||||||
              filters={searchFilter ?? {}}
 | 
					              filters={searchFilter ?? {}}
 | 
				
			||||||
              setFilters={setSearchFilter}
 | 
					              setFilters={setSearchFilter}
 | 
				
			||||||
              search={search}
 | 
					              search={search}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user