11import { autoUpdate , FloatingPortal , Middleware , offset , useDismiss , useFloating } from "@floating-ui/react" ;
22import clsx from "clsx" ;
3- import { atom , PrimitiveAtom , useAtom , useAtomValue } from "jotai" ;
4- import { memo , useCallback , useRef , useState } from "react" ;
3+ import { atom , useAtom } from "jotai" ;
4+ import { memo , useCallback , useEffect , useMemo , useRef } from "react" ;
55import { IconButton } from "./iconbutton" ;
66import { Input } from "./input" ;
77import "./search.scss" ;
88
9- type SearchProps = {
10- searchAtom : PrimitiveAtom < string > ;
11- indexAtom : PrimitiveAtom < number > ;
12- numResultsAtom : PrimitiveAtom < number > ;
13- isOpenAtom : PrimitiveAtom < boolean > ;
9+ type SearchProps = SearchAtoms & {
1410 anchorRef ?: React . RefObject < HTMLElement > ;
1511 offsetX ?: number ;
1612 offsetY ?: number ;
13+ onSearch ?: ( search : string ) => void ;
14+ onNext ?: ( ) => void ;
15+ onPrev ?: ( ) => void ;
1716} ;
1817
1918const SearchComponent = ( {
@@ -24,23 +23,54 @@ const SearchComponent = ({
2423 anchorRef,
2524 offsetX = 10 ,
2625 offsetY = 10 ,
26+ onSearch,
27+ onNext,
28+ onPrev,
2729} : SearchProps ) => {
28- const [ isOpen , setIsOpen ] = useAtom ( isOpenAtom ) ;
29- const [ search , setSearch ] = useAtom ( searchAtom ) ;
30- const [ index , setIndex ] = useAtom ( indexAtom ) ;
31- const numResults = useAtomValue ( numResultsAtom ) ;
30+ const [ isOpen , setIsOpen ] = useAtom < boolean > ( isOpenAtom ) ;
31+ const [ search , setSearch ] = useAtom < string > ( searchAtom ) ;
32+ const [ index , setIndex ] = useAtom < number > ( indexAtom ) ;
33+ const [ numResults , setNumResults ] = useAtom < number > ( numResultsAtom ) ;
3234
3335 const handleOpenChange = useCallback ( ( open : boolean ) => {
3436 setIsOpen ( open ) ;
3537 } , [ ] ) ;
3638
39+ useEffect ( ( ) => {
40+ setSearch ( "" ) ;
41+ setIndex ( 0 ) ;
42+ setNumResults ( 0 ) ;
43+ } , [ isOpen ] ) ;
44+
45+ useEffect ( ( ) => {
46+ setIndex ( 0 ) ;
47+ setNumResults ( 0 ) ;
48+ onSearch ?.( search ) ;
49+ } , [ search ] ) ;
50+
3751 const middleware : Middleware [ ] = [ ] ;
38- middleware . push (
39- offset ( ( { rects } ) => ( {
40- mainAxis : - rects . floating . height - offsetY ,
41- crossAxis : - offsetX ,
42- } ) )
52+ const offsetCallback = useCallback (
53+ ( { rects } ) => {
54+ const docRect = document . documentElement . getBoundingClientRect ( ) ;
55+ let yOffsetCalc = - rects . floating . height - offsetY ;
56+ let xOffsetCalc = - offsetX ;
57+ const floatingBottom = rects . reference . y + rects . floating . height + offsetY ;
58+ const floatingLeft = rects . reference . x + rects . reference . width - ( rects . floating . width + offsetX ) ;
59+ if ( floatingBottom > docRect . bottom ) {
60+ yOffsetCalc -= docRect . bottom - floatingBottom ;
61+ }
62+ if ( floatingLeft < 5 ) {
63+ xOffsetCalc += 5 - floatingLeft ;
64+ }
65+ console . log ( "offsetCalc" , yOffsetCalc , xOffsetCalc ) ;
66+ return {
67+ mainAxis : yOffsetCalc ,
68+ crossAxis : xOffsetCalc ,
69+ } ;
70+ } ,
71+ [ offsetX , offsetY ]
4372 ) ;
73+ middleware . push ( offset ( offsetCallback ) ) ;
4474
4575 const { refs, floatingStyles, context } = useFloating ( {
4676 placement : "top-end" ,
@@ -55,26 +85,47 @@ const SearchComponent = ({
5585
5686 const dismiss = useDismiss ( context ) ;
5787
88+ const onPrevWrapper = useCallback (
89+ ( ) => ( onPrev ? onPrev ( ) : setIndex ( ( index - 1 ) % numResults ) ) ,
90+ [ onPrev , index , numResults ]
91+ ) ;
92+ const onNextWrapper = useCallback (
93+ ( ) => ( onNext ? onNext ( ) : setIndex ( ( index + 1 ) % numResults ) ) ,
94+ [ onNext , index , numResults ]
95+ ) ;
96+
97+ const onKeyDown = useCallback (
98+ ( e : React . KeyboardEvent ) => {
99+ if ( e . key === "Enter" ) {
100+ if ( e . shiftKey ) {
101+ onPrevWrapper ( ) ;
102+ } else {
103+ onNextWrapper ( ) ;
104+ }
105+ e . preventDefault ( ) ;
106+ }
107+ } ,
108+ [ onPrevWrapper , onNextWrapper , setIsOpen ]
109+ ) ;
110+
58111 const prevDecl : IconButtonDecl = {
59112 elemtype : "iconbutton" ,
60113 icon : "chevron-up" ,
61- title : "Previous Result" ,
62- disabled : index === 0 ,
63- click : ( ) => setIndex ( index - 1 ) ,
114+ title : "Previous Result (Shift+Enter)" ,
115+ click : onPrevWrapper ,
64116 } ;
65117
66118 const nextDecl : IconButtonDecl = {
67119 elemtype : "iconbutton" ,
68120 icon : "chevron-down" ,
69- title : "Next Result" ,
70- disabled : ! numResults || index === numResults - 1 ,
71- click : ( ) => setIndex ( index + 1 ) ,
121+ title : "Next Result (Enter)" ,
122+ click : onNextWrapper ,
72123 } ;
73124
74125 const closeDecl : IconButtonDecl = {
75126 elemtype : "iconbutton" ,
76127 icon : "xmark-large" ,
77- title : "Close" ,
128+ title : "Close (Esc) " ,
78129 click : ( ) => setIsOpen ( false ) ,
79130 } ;
80131
@@ -83,7 +134,13 @@ const SearchComponent = ({
83134 { isOpen && (
84135 < FloatingPortal >
85136 < div className = "search-container" style = { { ...floatingStyles } } { ...dismiss } ref = { refs . setFloating } >
86- < Input placeholder = "Search" value = { search } onChange = { setSearch } />
137+ < Input
138+ placeholder = "Search"
139+ value = { search }
140+ onChange = { setSearch }
141+ onKeyDown = { onKeyDown }
142+ autoFocus
143+ />
87144 < div
88145 className = { clsx ( "search-results" , { hidden : numResults === 0 } ) }
89146 aria-live = "polite"
@@ -105,11 +162,16 @@ const SearchComponent = ({
105162
106163export const Search = memo ( SearchComponent ) as typeof SearchComponent ;
107164
108- export function useSearch ( anchorRef ?: React . RefObject < HTMLElement > ) : SearchProps {
109- const [ searchAtom ] = useState ( atom ( "" ) ) ;
110- const [ indexAtom ] = useState ( atom ( 0 ) ) ;
111- const [ numResultsAtom ] = useState ( atom ( 0 ) ) ;
112- const [ isOpenAtom ] = useState ( atom ( false ) ) ;
165+ export function useSearch ( anchorRef ?: React . RefObject < HTMLElement > , viewModel ?: ViewModel ) : SearchProps {
166+ const searchAtoms : SearchAtoms = useMemo (
167+ ( ) => ( { searchAtom : atom ( "" ) , indexAtom : atom ( 0 ) , numResultsAtom : atom ( 0 ) , isOpenAtom : atom ( false ) } ) ,
168+ [ ]
169+ ) ;
113170 anchorRef ??= useRef ( null ) ;
114- return { searchAtom, indexAtom, numResultsAtom, isOpenAtom, anchorRef } ;
171+ useEffect ( ( ) => {
172+ if ( viewModel ) {
173+ viewModel . searchAtoms = searchAtoms ;
174+ }
175+ } , [ viewModel ] ) ;
176+ return { ...searchAtoms , anchorRef } ;
115177}
0 commit comments