From 8bf6510eaf1fe039c4e7b73c1bae3401144fd13b Mon Sep 17 00:00:00 2001 From: mrjvs Date: Wed, 29 Nov 2023 18:20:31 +0100 Subject: [PATCH] shortcut for enter to unfocus + slash to focus searchbar --- src/components/form/SearchBar.tsx | 87 ++++++++++--------- src/components/player/hooks/useSlashFocus.ts | 22 +++++ .../text-inputs/TextInputControl.tsx | 82 +++++++++-------- src/pages/parts/home/HeroPart.tsx | 7 +- 4 files changed, 121 insertions(+), 77 deletions(-) create mode 100644 src/components/player/hooks/useSlashFocus.ts diff --git a/src/components/form/SearchBar.tsx b/src/components/form/SearchBar.tsx index 26b09c8a..0573a285 100644 --- a/src/components/form/SearchBar.tsx +++ b/src/components/form/SearchBar.tsx @@ -1,5 +1,5 @@ import c from "classnames"; -import { useState } from "react"; +import { forwardRef, useState } from "react"; import { Flare } from "@/components/utils/Flare"; @@ -13,50 +13,53 @@ export interface SearchBarProps { value: string; } -export function SearchBarInput(props: SearchBarProps) { - const [focused, setFocused] = useState(false); +export const SearchBarInput = forwardRef( + (props, ref) => { + const [focused, setFocused] = useState(false); - function setSearch(value: string) { - props.onChange(value, false); - } + function setSearch(value: string) { + props.onChange(value, false); + } - return ( - - - - -
- -
- - { - setFocused(false); - props.onUnFocus(); - }} - onFocus={() => setFocused(true)} - onChange={(val) => setSearch(val)} - value={props.value} - className="w-full flex-1 bg-transparent px-4 py-4 pl-12 text-search-text placeholder-search-placeholder focus:outline-none sm:py-4 sm:pr-2" - placeholder={props.placeholder} + > + -
-
- ); -} + + +
+ +
+ + { + setFocused(false); + props.onUnFocus(); + }} + onFocus={() => setFocused(true)} + onChange={(val) => setSearch(val)} + value={props.value} + className="w-full flex-1 bg-transparent px-4 py-4 pl-12 text-search-text placeholder-search-placeholder focus:outline-none sm:py-4 sm:pr-2" + placeholder={props.placeholder} + /> +
+ + ); + } +); diff --git a/src/components/player/hooks/useSlashFocus.ts b/src/components/player/hooks/useSlashFocus.ts new file mode 100644 index 00000000..2c44e33f --- /dev/null +++ b/src/components/player/hooks/useSlashFocus.ts @@ -0,0 +1,22 @@ +import { useEffect } from "react"; + +export function useSlashFocus(ref: React.RefObject) { + useEffect(() => { + const listener = (e: KeyboardEvent) => { + if (e.key === "/") { + if ( + document.activeElement && + document.activeElement.tagName.toLowerCase() === "input" + ) + return; + e.preventDefault(); + ref.current?.focus(); + } + }; + + window.addEventListener("keydown", listener); + return () => { + window.removeEventListener("keydown", listener); + }; + }, [ref]); +} diff --git a/src/components/text-inputs/TextInputControl.tsx b/src/components/text-inputs/TextInputControl.tsx index 7b95f219..b555c4f8 100644 --- a/src/components/text-inputs/TextInputControl.tsx +++ b/src/components/text-inputs/TextInputControl.tsx @@ -1,3 +1,5 @@ +import { forwardRef } from "react"; + export interface TextInputControlPropsNoLabel { onChange?: (data: string) => void; onUnFocus?: () => void; @@ -13,39 +15,51 @@ export interface TextInputControlProps extends TextInputControlPropsNoLabel { label?: string; } -export function TextInputControl({ - onChange, - onUnFocus, - value, - label, - name, - autoComplete, - className, - placeholder, - onFocus, -}: TextInputControlProps) { - const input = ( - onChange && onChange(e.target.value)} - value={value} - name={name} - autoComplete={autoComplete} - onBlur={() => onUnFocus && onUnFocus()} - onFocus={() => onFocus?.()} - /> - ); - - if (label) { - return ( - +export const TextInputControl = forwardRef< + HTMLInputElement, + TextInputControlProps +>( + ( + { + onChange, + onUnFocus, + value, + label, + name, + autoComplete, + className, + placeholder, + onFocus, + }, + ref + ) => { + const input = ( + onChange && onChange(e.target.value)} + value={value} + name={name} + autoComplete={autoComplete} + onBlur={() => onUnFocus && onUnFocus()} + onFocus={() => onFocus?.()} + onKeyDown={(e) => + e.key === "Enter" ? (e.target as HTMLInputElement).blur() : null + } + /> ); - } - return input; -} + if (label) { + return ( + + ); + } + + return input; + } +); diff --git a/src/pages/parts/home/HeroPart.tsx b/src/pages/parts/home/HeroPart.tsx index 07cef8ec..ea6e1112 100644 --- a/src/pages/parts/home/HeroPart.tsx +++ b/src/pages/parts/home/HeroPart.tsx @@ -1,8 +1,9 @@ -import { useCallback, useState } from "react"; +import { useCallback, useRef, useState } from "react"; import Sticky from "react-sticky-el"; import { SearchBarInput } from "@/components/form/SearchBar"; import { ThinContainer } from "@/components/layout/ThinContainer"; +import { useSlashFocus } from "@/components/player/hooks/useSlashFocus"; import { HeroTitle } from "@/components/text/HeroTitle"; import { useRandomTranslation } from "@/hooks/useRandomTranslation"; import { useSearchQuery } from "@/hooks/useSearchQuery"; @@ -33,6 +34,9 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) { const title = t(`home.titles.${time}`); + const inputRef = useRef(null); + useSlashFocus(inputRef); + return (
@@ -48,6 +52,7 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) { onFixedToggle={stickStateChanged} >