Skip to content

Commit 79acbc8

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

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,16 +1,16 @@
11
"use client";
22

3-
import { useState, useRef, useEffect, useMemo } from "react";
3+
import { useState, useEffect } from "react";
44
import { useSolidAuth } from "@ldo/solid-react";
55
import { useRouter } from "next/navigation";
66
import Image from "next/image";
77
import Button from "./shared/Button";
8-
import { ChevronDownIcon } from "@heroicons/react/24/outline";
8+
import UrlCombobox, { ComboboxOption } from "./shared/UrlCombobox";
99

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

1515
export default function LoginPage() {
1616
const { session, login } = useSolidAuth();
@@ -19,11 +19,7 @@ export default function LoginPage() {
1919
process.env.NEXT_PUBLIC_OIDC_ISSUER || ""
2020
);
2121
const [isLoading, setIsLoading] = useState(false);
22-
const [showDropdown, setShowDropdown] = useState(false);
2322
const [error, setError] = useState<string | null>(null);
24-
const [highlightedIndex, setHighlightedIndex] = useState<number>(-1);
25-
const inputRef = useRef<HTMLInputElement>(null);
26-
const dropdownRef = useRef<HTMLDivElement>(null);
2723

2824
// Redirect to home if already authenticated
2925
useEffect(() => {
@@ -37,25 +33,6 @@ export default function LoginPage() {
3733
return null;
3834
}
3935

40-
// Close dropdown when clicking outside
41-
useEffect(() => {
42-
const handleClickOutside = (event: MouseEvent) => {
43-
if (
44-
dropdownRef.current &&
45-
!dropdownRef.current.contains(event.target as Node) &&
46-
inputRef.current &&
47-
!inputRef.current.contains(event.target as Node)
48-
) {
49-
setShowDropdown(false);
50-
}
51-
};
52-
53-
if (showDropdown) {
54-
document.addEventListener("mousedown", handleClickOutside);
55-
return () => document.removeEventListener("mousedown", handleClickOutside);
56-
}
57-
}, [showDropdown]);
58-
5936
const validateIssuerUrl = (url: string): boolean => {
6037
if (!url.trim()) {
6138
setError("Please enter a Solid Identity Provider URL");
@@ -92,74 +69,13 @@ export default function LoginPage() {
9269
}
9370
};
9471

95-
const handleIssuerSelect = (value: string) => {
72+
const handleIssuerChange = (value: string) => {
9673
setIssuerInput(value);
97-
setShowDropdown(false);
98-
setError(null);
99-
setHighlightedIndex(-1);
100-
inputRef.current?.focus();
101-
};
102-
103-
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
104-
setIssuerInput(e.target.value);
105-
setHighlightedIndex(-1);
10674
if (error) {
10775
setError(null);
10876
}
10977
};
11078

111-
const handleInputFocus = () => {
112-
setShowDropdown(true);
113-
setHighlightedIndex(-1);
114-
};
115-
116-
// Filter preset issuers based on input
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-
};
162-
16379
return (
16480
<main className="flex min-h-screen bg-white" role="main" aria-label="Sign in page">
16581
{/* Left side - Logo and branding */}
@@ -221,98 +137,17 @@ export default function LoginPage() {
221137
noValidate
222138
>
223139
{/* Identity Provider Input */}
224-
<div>
225-
<label
226-
htmlFor="oidc-issuer"
227-
className="mb-2 block text-sm font-medium text-black"
228-
>
229-
Solid Identity Provider
230-
</label>
231-
<div className="relative">
232-
<input
233-
ref={inputRef}
234-
id="oidc-issuer"
235-
name="oidc-issuer"
236-
type="text"
237-
value={issuerInput}
238-
onChange={handleInputChange}
239-
onFocus={handleInputFocus}
240-
onKeyDown={handleKeyDown}
241-
placeholder="Enter your provider URL or select from the list"
242-
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 ${
243-
error
244-
? "border-red-300 focus:border-red-500 focus:ring-red-500"
245-
: "border-gray-300 focus:border-[#7B42F6] focus:ring-[#7B42F6]"
246-
}`}
247-
disabled={isLoading}
248-
required
249-
aria-required="true"
250-
aria-label="Enter or select Solid Identity Provider"
251-
aria-describedby={error ? "oidc-issuer-error" : "oidc-issuer-description"}
252-
aria-invalid={!!error}
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"
259-
/>
260-
<button
261-
type="button"
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}
269-
>
270-
<ChevronDownIcon className={`h-5 w-5 transition-transform ${showDropdown ? "rotate-180" : ""}`} />
271-
</button>
272-
273-
{/* Dropdown with preset options */}
274-
{showDropdown && filteredIssuers.length > 0 && (
275-
<div
276-
ref={dropdownRef}
277-
id="issuer-listbox"
278-
className="absolute z-10 mt-1 w-full rounded-md border border-gray-200 bg-white shadow-lg max-h-60 overflow-auto"
279-
role="listbox"
280-
aria-label="Preset identity providers"
281-
>
282-
{filteredIssuers.map((issuer, index) => (
283-
<button
284-
key={issuer.value}
285-
id={`issuer-option-${index}`}
286-
type="button"
287-
onClick={() => handleIssuerSelect(issuer.value)}
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-
}`}
293-
role="option"
294-
aria-selected={issuerInput === issuer.value}
295-
>
296-
<div className="text-sm font-medium text-gray-900">
297-
{issuer.label}
298-
</div>
299-
<div className="text-xs text-gray-500">
300-
{issuer.value}
301-
</div>
302-
</button>
303-
))}
304-
</div>
305-
)}
306-
</div>
307-
{error && (
308-
<p id="oidc-issuer-error" className="mt-1 text-xs text-red-600" role="alert">
309-
{error}
310-
</p>
311-
)}
312-
<p id="oidc-issuer-description" className="sr-only">
313-
Enter your Solid Identity Provider URL or select from the preset options
314-
</p>
315-
</div>
140+
<UrlCombobox
141+
id="oidc-issuer"
142+
label="Solid Identity Provider"
143+
value={issuerInput}
144+
onChange={handleIssuerChange}
145+
options={PRESET_ISSUERS}
146+
placeholder="Enter your provider URL or select from the list"
147+
error={error || undefined}
148+
disabled={isLoading}
149+
aria-label="Enter or select Solid Identity Provider"
150+
/>
316151

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

0 commit comments

Comments
 (0)