Skip to content

Commit 80e6318

Browse files
Copilotjeswr
andcommitted
Refactor IDP selector into reusable UrlCombobox component
Co-authored-by: jeswr <63333554+jeswr@users.noreply.github.com>
1 parent 22fbb5a commit 80e6318

3 files changed

Lines changed: 314 additions & 302 deletions

File tree

app/components/LoginPage.tsx

Lines changed: 18 additions & 183 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,22 @@
11
"use client";
22

3-
import { useState, useRef, useEffect, useMemo } from "react";
3+
import { useState } from "react";
44
import { login } from "@inrupt/solid-client-authn-browser";
55
import Image from "next/image";
66
import Button from "./shared/Button";
7-
import { ChevronDownIcon } from "@heroicons/react/24/outline";
7+
import UrlCombobox, { ComboboxOption } from "./shared/UrlCombobox";
88

9-
const PRESET_ISSUERS = [
10-
{ label: "Solid Community", value: "https://solidcommunity.net/" },
11-
{ label: "Inrupt", value: "https://login.inrupt.com" },
12-
] as const;
9+
const PRESET_ISSUERS: ComboboxOption[] = [
10+
{ label: "Solid Community", value: "https://solidcommunity.net/", secondaryLabel: "https://solidcommunity.net/" },
11+
{ label: "Inrupt", value: "https://login.inrupt.com", secondaryLabel: "https://login.inrupt.com" },
12+
];
1313

1414
export default function LoginPage() {
1515
const [issuerInput, setIssuerInput] = useState<string>(
1616
process.env.NEXT_PUBLIC_OIDC_ISSUER || ""
1717
);
1818
const [isLoading, setIsLoading] = useState(false);
19-
const [showDropdown, setShowDropdown] = useState(false);
2019
const [error, setError] = useState<string | null>(null);
21-
const [highlightedIndex, setHighlightedIndex] = useState<number>(-1);
22-
const inputRef = useRef<HTMLInputElement>(null);
23-
const dropdownRef = useRef<HTMLDivElement>(null);
24-
25-
// Close dropdown when clicking outside
26-
useEffect(() => {
27-
const handleClickOutside = (event: MouseEvent) => {
28-
if (
29-
dropdownRef.current &&
30-
!dropdownRef.current.contains(event.target as Node) &&
31-
inputRef.current &&
32-
!inputRef.current.contains(event.target as Node)
33-
) {
34-
setShowDropdown(false);
35-
}
36-
};
37-
38-
if (showDropdown) {
39-
document.addEventListener("mousedown", handleClickOutside);
40-
return () => document.removeEventListener("mousedown", handleClickOutside);
41-
}
42-
}, [showDropdown]);
4320

4421
const validateIssuerUrl = (url: string): boolean => {
4522
if (!url.trim()) {
@@ -82,74 +59,13 @@ export default function LoginPage() {
8259
}
8360
};
8461

85-
const handleIssuerSelect = (value: string) => {
62+
const handleIssuerChange = (value: string) => {
8663
setIssuerInput(value);
87-
setShowDropdown(false);
88-
setError(null);
89-
setHighlightedIndex(-1);
90-
inputRef.current?.focus();
91-
};
92-
93-
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
94-
setIssuerInput(e.target.value);
95-
setHighlightedIndex(-1);
9664
if (error) {
9765
setError(null);
9866
}
9967
};
10068

101-
const handleInputFocus = () => {
102-
setShowDropdown(true);
103-
setHighlightedIndex(-1);
104-
};
105-
106-
// Filter preset issuers based on input
107-
const filteredIssuers = useMemo(() => {
108-
return PRESET_ISSUERS.filter((issuer) => {
109-
if (!issuerInput.trim()) return true;
110-
const query = issuerInput.toLowerCase();
111-
return (
112-
issuer.label.toLowerCase().includes(query) ||
113-
issuer.value.toLowerCase().includes(query)
114-
);
115-
});
116-
}, [issuerInput]);
117-
118-
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
119-
if (!showDropdown) {
120-
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
121-
e.preventDefault();
122-
setShowDropdown(true);
123-
setHighlightedIndex(-1);
124-
}
125-
return;
126-
}
127-
128-
switch (e.key) {
129-
case "ArrowDown":
130-
e.preventDefault();
131-
setHighlightedIndex((prev) =>
132-
prev < filteredIssuers.length - 1 ? prev + 1 : prev
133-
);
134-
break;
135-
case "ArrowUp":
136-
e.preventDefault();
137-
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : prev));
138-
break;
139-
case "Enter":
140-
if (highlightedIndex >= 0 && highlightedIndex < filteredIssuers.length) {
141-
e.preventDefault();
142-
handleIssuerSelect(filteredIssuers[highlightedIndex].value);
143-
}
144-
break;
145-
case "Escape":
146-
e.preventDefault();
147-
setShowDropdown(false);
148-
setHighlightedIndex(-1);
149-
break;
150-
}
151-
};
152-
15369
return (
15470
<main className="flex min-h-screen bg-white" role="main" aria-label="Sign in page">
15571
{/* Left side - Logo and branding */}
@@ -211,98 +127,17 @@ export default function LoginPage() {
211127
noValidate
212128
>
213129
{/* Identity Provider Input */}
214-
<div>
215-
<label
216-
htmlFor="oidc-issuer"
217-
className="mb-2 block text-sm font-medium text-black"
218-
>
219-
Solid Identity Provider
220-
</label>
221-
<div className="relative">
222-
<input
223-
ref={inputRef}
224-
id="oidc-issuer"
225-
name="oidc-issuer"
226-
type="text"
227-
value={issuerInput}
228-
onChange={handleInputChange}
229-
onFocus={handleInputFocus}
230-
onKeyDown={handleKeyDown}
231-
placeholder="Enter your provider URL or select from the list"
232-
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 ${
233-
error
234-
? "border-red-300 focus:border-red-500 focus:ring-red-500"
235-
: "border-gray-300 focus:border-[#7B42F6] focus:ring-[#7B42F6]"
236-
}`}
237-
disabled={isLoading}
238-
required
239-
aria-required="true"
240-
aria-label="Enter or select Solid Identity Provider"
241-
aria-describedby={error ? "oidc-issuer-error" : "oidc-issuer-description"}
242-
aria-invalid={!!error}
243-
aria-expanded={showDropdown}
244-
aria-activedescendant={highlightedIndex >= 0 ? `issuer-option-${highlightedIndex}` : undefined}
245-
role="combobox"
246-
aria-autocomplete="list"
247-
aria-controls="issuer-listbox"
248-
autoComplete="off"
249-
/>
250-
<button
251-
type="button"
252-
onClick={() => {
253-
setShowDropdown(!showDropdown);
254-
inputRef.current?.focus();
255-
}}
256-
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"
257-
aria-label={showDropdown ? "Hide provider options" : "Show provider options"}
258-
aria-expanded={showDropdown}
259-
>
260-
<ChevronDownIcon className={`h-5 w-5 transition-transform ${showDropdown ? "rotate-180" : ""}`} />
261-
</button>
262-
263-
{/* Dropdown with preset options */}
264-
{showDropdown && filteredIssuers.length > 0 && (
265-
<div
266-
ref={dropdownRef}
267-
id="issuer-listbox"
268-
className="absolute z-10 mt-1 w-full rounded-md border border-gray-200 bg-white shadow-lg max-h-60 overflow-auto"
269-
role="listbox"
270-
aria-label="Preset identity providers"
271-
>
272-
{filteredIssuers.map((issuer, index) => (
273-
<button
274-
key={issuer.value}
275-
id={`issuer-option-${index}`}
276-
type="button"
277-
onClick={() => handleIssuerSelect(issuer.value)}
278-
className={`w-full px-4 py-3 text-left focus:outline-none ${
279-
highlightedIndex === index
280-
? "bg-gray-100"
281-
: "hover:bg-gray-100"
282-
}`}
283-
role="option"
284-
aria-selected={issuerInput === issuer.value}
285-
>
286-
<div className="text-sm font-medium text-gray-900">
287-
{issuer.label}
288-
</div>
289-
<div className="text-xs text-gray-500">
290-
{issuer.value}
291-
</div>
292-
</button>
293-
))}
294-
</div>
295-
)}
296-
</div>
297-
{error && (
298-
<p id="oidc-issuer-error" className="mt-1 text-xs text-red-600" role="alert">
299-
{error}
300-
</p>
301-
)}
302-
<p id="oidc-issuer-description" className="sr-only">
303-
Enter your Solid Identity Provider URL or select from the preset options
304-
</p>
305-
</div>
130+
<UrlCombobox
131+
id="oidc-issuer"
132+
label="Solid Identity Provider"
133+
value={issuerInput}
134+
onChange={handleIssuerChange}
135+
options={PRESET_ISSUERS}
136+
placeholder="Enter your provider URL or select from the list"
137+
error={error || undefined}
138+
disabled={isLoading}
139+
aria-label="Enter or select Solid Identity Provider"
140+
/>
306141

307142
{/* Action button */}
308143
<div className="flex items-center justify-end pt-4">

0 commit comments

Comments
 (0)