Skip to content

Commit df5af5a

Browse files
Copilotjeswr
authored andcommitted
Add keyboard navigation and accessibility improvements to IDP selector
Co-authored-by: jeswr <63333554+jeswr@users.noreply.github.com>
1 parent 4fab1de commit df5af5a

1 file changed

Lines changed: 77 additions & 17 deletions

File tree

app/components/LoginPage.tsx

Lines changed: 77 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useState, useRef, useEffect } from "react";
3+
import { useState, useRef, useEffect, useMemo } from "react";
44
import { useSolidAuth } from "@ldo/solid-react";
55
import { useRouter } from "next/navigation";
66
import 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

Comments
 (0)