11'use client'
22import Link from 'next/link'
3- import { useEffect , useState } from 'react'
3+ import { useEffect , useRef , useState } from 'react'
44import { useDebounceValue } from 'usehooks-ts'
55
66import { getPages } from '@/api/requests/cms/getPages'
@@ -26,6 +26,9 @@ export function Search({ label, placeholder }: SearchProps) {
2626 const [ searchInputValue , setSearchInputValue ] = useState ( '' )
2727 const [ debouncedSearchValue ] = useDebounceValue ( searchInputValue , DEBOUNCE_MILLISECONDS )
2828 const [ searchResults , setSearchResults ] = useState < SearchResult [ ] | undefined > ( [ ] )
29+ const [ isDropdownOpen , setIsDropdownOpen ] = useState ( false )
30+ const searchContainerRef = useRef < HTMLDivElement > ( null )
31+ const searchInputRef = useRef < HTMLInputElement > ( null )
2932
3033 const getSearchResults = async ( { query } : { query : string } ) => {
3134 const pages = await getPages ( { limit : limit . toString ( ) , search : query } )
@@ -40,41 +43,84 @@ export function Search({ label, placeholder }: SearchProps) {
4043 }
4144 } , [ debouncedSearchValue ] )
4245
46+ useEffect ( ( ) => {
47+ const handleClickOutside = ( event : MouseEvent ) => {
48+ if ( searchContainerRef . current && ! searchContainerRef . current . contains ( event . target as Node ) ) {
49+ setIsDropdownOpen ( false )
50+ }
51+ }
52+
53+ document . addEventListener ( 'mousedown' , handleClickOutside )
54+
55+ return ( ) => {
56+ document . removeEventListener ( 'mousedown' , handleClickOutside )
57+ }
58+ } , [ ] )
59+
4360 return (
4461 < form method = "GET" aria-label = { label } >
45- { /* <div className="govuk-form-group mb-0 mt-2"> */ }
46- < div className = "govuk-form-group" >
62+ < div className = "govuk-form-group relative" ref = { searchContainerRef } >
4763 < label className = "govuk-label" htmlFor = "search" hidden = { true } >
4864 { placeholder }
4965 </ label >
50- < input
51- className = "govuk-input govuk-!-font-size-16"
52- id = "search"
53- name = "search"
54- type = "text"
55- placeholder = { placeholder }
56- value = { searchInputValue }
57- onChange = { ( event ) => {
58- setSearchInputValue ( event . currentTarget . value )
59- } }
60- />
61- < div className = "absolute z-[1000] max-h-[200px] overflow-y-auto bg-white shadow-[0px_15px_15px_0px_rgba(0,0,0,0.35)] sm:w-5/12" >
62- < ul >
63- { searchResults ?. map ( ( { title, meta : { html_url } } , i ) => (
64- < li
65- key = { `result-${ i } ` }
66- value = "{i}"
67- className = "govuk-!-margin-left-2 govuk-!-margin-right-2 border-b shadow-[0_1px_0_#929191]"
68- >
69- < Link className = "govuk-link group block focus:border-b-0 focus:outline-none" href = { html_url || '' } >
70- < div className = "size-full border-2 border-transparent p-3 group-focus:border-black group-focus:bg-yellow" >
71- { title }
72- </ div >
73- </ Link >
74- </ li >
75- ) ) }
76- </ ul >
66+ < div className = "relative" >
67+ < input
68+ ref = { searchInputRef }
69+ className = "govuk-input govuk-!-font-size-16 govuk-!-width-full pr-[2.5rem]"
70+ id = "search"
71+ name = "search"
72+ type = "text"
73+ placeholder = { placeholder }
74+ value = { searchInputValue }
75+ onFocus = { ( ) => {
76+ setIsDropdownOpen ( true )
77+ } }
78+ onKeyDown = { ( event ) => {
79+ if ( event . key === 'Escape' ) {
80+ setIsDropdownOpen ( false )
81+ }
82+ } }
83+ onChange = { ( event ) => {
84+ setSearchInputValue ( event . currentTarget . value )
85+ setIsDropdownOpen ( true )
86+ } }
87+ />
88+ { searchInputValue && (
89+ < button
90+ type = "button"
91+ aria-label = "Clear search"
92+ className = "absolute inset-y-0 right-0 mt-[2px] flex h-[36px] items-center justify-center px-3 text-black"
93+ style = { { fontSize : '1.5rem' , lineHeight : 1 } }
94+ onClick = { ( ) => {
95+ setSearchInputValue ( '' )
96+ setSearchResults ( [ ] )
97+ setIsDropdownOpen ( false )
98+ searchInputRef . current ?. focus ( )
99+ } }
100+ >
101+ ×
102+ </ button >
103+ ) }
77104 </ div >
105+ { isDropdownOpen && searchResults && searchResults . length > 0 && (
106+ < div className = "absolute z-[1000] max-h-[200px] w-full overflow-y-auto bg-white shadow-[0px_15px_15px_0px_rgba(0,0,0,0.35)]" >
107+ < ul >
108+ { searchResults . map ( ( { title, meta : { html_url } } , i ) => (
109+ < li
110+ key = { `result-${ i } ` }
111+ value = "{i}"
112+ className = "govuk-!-margin-left-2 govuk-!-margin-right-2 border-b shadow-[0_1px_0_#929191]"
113+ >
114+ < Link className = "govuk-link group block focus:border-b-0 focus:outline-none" href = { html_url || '' } >
115+ < div className = "size-full border-2 border-transparent p-3 group-focus:border-black group-focus:bg-yellow" >
116+ { title }
117+ </ div >
118+ </ Link >
119+ </ li >
120+ ) ) }
121+ </ ul >
122+ </ div >
123+ ) }
78124 < noscript >
79125 < style > { 'input#search {display: none; }' } </ style >
80126 </ noscript >
0 commit comments