11"use client" ;
22
3- import { useState , useRef , useEffect } from "react" ;
3+ import { useState , useRef , useEffect , useMemo } from "react" ;
44import { useSolidAuth } from "@ldo/solid-react" ;
55import { useRouter } from "next/navigation" ;
66import Image from "next/image" ;
@@ -21,6 +21,7 @@ export default function LoginPage() {
2121 const [ isLoading , setIsLoading ] = useState ( false ) ;
2222 const [ showDropdown , setShowDropdown ] = useState ( false ) ;
2323 const [ error , setError ] = useState < string | null > ( null ) ;
24+ const [ highlightedIndex , setHighlightedIndex ] = useState < number > ( - 1 ) ;
2425 const inputRef = useRef < HTMLInputElement > ( null ) ;
2526 const dropdownRef = useRef < HTMLDivElement > ( null ) ;
2627
@@ -95,25 +96,69 @@ export default function LoginPage() {
9596 setIssuerInput ( value ) ;
9697 setShowDropdown ( false ) ;
9798 setError ( null ) ;
99+ setHighlightedIndex ( - 1 ) ;
98100 inputRef . current ?. focus ( ) ;
99101 } ;
100102
101103 const handleInputChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
102104 setIssuerInput ( e . target . value ) ;
105+ setHighlightedIndex ( - 1 ) ;
103106 if ( error ) {
104107 setError ( null ) ;
105108 }
106109 } ;
107110
111+ const handleInputFocus = ( ) => {
112+ setShowDropdown ( true ) ;
113+ setHighlightedIndex ( - 1 ) ;
114+ } ;
115+
108116 // Filter preset issuers based on input
109- const filteredIssuers = PRESET_ISSUERS . filter ( ( issuer ) => {
110- if ( ! issuerInput . trim ( ) ) return true ;
111- const query = issuerInput . toLowerCase ( ) ;
112- return (
113- issuer . label . toLowerCase ( ) . includes ( query ) ||
114- issuer . value . toLowerCase ( ) . includes ( query )
115- ) ;
116- } ) ;
117+ const filteredIssuers = useMemo ( ( ) => {
118+ return PRESET_ISSUERS . filter ( ( issuer ) => {
119+ if ( ! issuerInput . trim ( ) ) return true ;
120+ const query = issuerInput . toLowerCase ( ) ;
121+ return (
122+ issuer . label . toLowerCase ( ) . includes ( query ) ||
123+ issuer . value . toLowerCase ( ) . includes ( query )
124+ ) ;
125+ } ) ;
126+ } , [ issuerInput ] ) ;
127+
128+ const handleKeyDown = ( e : React . KeyboardEvent < HTMLInputElement > ) => {
129+ if ( ! showDropdown ) {
130+ if ( e . key === "ArrowDown" || e . key === "ArrowUp" ) {
131+ e . preventDefault ( ) ;
132+ setShowDropdown ( true ) ;
133+ setHighlightedIndex ( - 1 ) ;
134+ }
135+ return ;
136+ }
137+
138+ switch ( e . key ) {
139+ case "ArrowDown" :
140+ e . preventDefault ( ) ;
141+ setHighlightedIndex ( ( prev ) =>
142+ prev < filteredIssuers . length - 1 ? prev + 1 : prev
143+ ) ;
144+ break ;
145+ case "ArrowUp" :
146+ e . preventDefault ( ) ;
147+ setHighlightedIndex ( ( prev ) => ( prev > 0 ? prev - 1 : prev ) ) ;
148+ break ;
149+ case "Enter" :
150+ if ( highlightedIndex >= 0 && highlightedIndex < filteredIssuers . length ) {
151+ e . preventDefault ( ) ;
152+ handleIssuerSelect ( filteredIssuers [ highlightedIndex ] . value ) ;
153+ }
154+ break ;
155+ case "Escape" :
156+ e . preventDefault ( ) ;
157+ setShowDropdown ( false ) ;
158+ setHighlightedIndex ( - 1 ) ;
159+ break ;
160+ }
161+ } ;
117162
118163 return (
119164 < main className = "flex min-h-screen bg-white" role = "main" aria-label = "Sign in page" >
@@ -191,7 +236,8 @@ export default function LoginPage() {
191236 type = "text"
192237 value = { issuerInput }
193238 onChange = { handleInputChange }
194- onFocus = { ( ) => setShowDropdown ( true ) }
239+ onFocus = { handleInputFocus }
240+ onKeyDown = { handleKeyDown }
195241 placeholder = "Enter your provider URL or select from the list"
196242 className = { `h-12 w-full rounded-md border bg-white px-4 pr-10 text-black placeholder:text-gray-500 focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:opacity-50 ${
197243 error
@@ -204,14 +250,22 @@ export default function LoginPage() {
204250 aria-label = "Enter or select Solid Identity Provider"
205251 aria-describedby = { error ? "oidc-issuer-error" : "oidc-issuer-description" }
206252 aria-invalid = { ! ! error }
207- autoComplete = "url"
253+ aria-expanded = { showDropdown }
254+ aria-activedescendant = { highlightedIndex >= 0 ? `issuer-option-${ highlightedIndex } ` : undefined }
255+ role = "combobox"
256+ aria-autocomplete = "list"
257+ aria-controls = "issuer-listbox"
258+ autoComplete = "off"
208259 />
209260 < button
210261 type = "button"
211- onClick = { ( ) => setShowDropdown ( ! showDropdown ) }
212- className = "absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
213- aria-label = "Show provider options"
214- tabIndex = { - 1 }
262+ onClick = { ( ) => {
263+ setShowDropdown ( ! showDropdown ) ;
264+ inputRef . current ?. focus ( ) ;
265+ } }
266+ className = "absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600"
267+ aria-label = { showDropdown ? "Hide provider options" : "Show provider options" }
268+ aria-expanded = { showDropdown }
215269 >
216270 < ChevronDownIcon className = { `h-5 w-5 transition-transform ${ showDropdown ? "rotate-180" : "" } ` } />
217271 </ button >
@@ -220,16 +274,22 @@ export default function LoginPage() {
220274 { showDropdown && filteredIssuers . length > 0 && (
221275 < div
222276 ref = { dropdownRef }
277+ id = "issuer-listbox"
223278 className = "absolute z-10 mt-1 w-full rounded-md border border-gray-200 bg-white shadow-lg max-h-60 overflow-auto"
224279 role = "listbox"
225280 aria-label = "Preset identity providers"
226281 >
227- { filteredIssuers . map ( ( issuer ) => (
282+ { filteredIssuers . map ( ( issuer , index ) => (
228283 < button
229284 key = { issuer . value }
285+ id = { `issuer-option-${ index } ` }
230286 type = "button"
231287 onClick = { ( ) => handleIssuerSelect ( issuer . value ) }
232- className = "w-full px-4 py-3 text-left hover:bg-gray-100 focus:bg-gray-100 focus:outline-none"
288+ className = { `w-full px-4 py-3 text-left focus:outline-none ${
289+ highlightedIndex === index
290+ ? "bg-gray-100"
291+ : "hover:bg-gray-100"
292+ } `}
233293 role = "option"
234294 aria-selected = { issuerInput === issuer . value }
235295 >
0 commit comments