This commit is contained in:
2026-01-23 20:35:40 +03:00
parent cf3b0e48ec
commit 66c151653e
137 changed files with 41495 additions and 0 deletions

View File

@@ -0,0 +1,214 @@
import { useState, useRef, useEffect } from "react";
import { Input } from "./Input";
interface AutocompleteOption {
value: string;
label: string;
data?: any;
}
interface AutocompleteInputProps {
name?: string;
label?: string;
placeholder?: string;
value: string;
onChange: (value: string) => void;
onSelect?: (option: AutocompleteOption) => void;
options: AutocompleteOption[];
error?: string;
required?: boolean;
disabled?: boolean;
loading?: boolean;
}
export function AutocompleteInput({
name,
label,
placeholder,
value,
onChange,
onSelect,
options,
error,
required,
disabled,
loading
}: AutocompleteInputProps) {
const [isOpen, setIsOpen] = useState(false);
const [highlightedIndex, setHighlightedIndex] = useState(-1);
const inputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLUListElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
// Filter options based on input value
const filteredOptions = options.filter(option =>
option.label.toLowerCase().includes(value.toLowerCase()) ||
option.value.toLowerCase().includes(value.toLowerCase())
);
// Handle input change
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
onChange(newValue);
setIsOpen(true);
setHighlightedIndex(-1);
};
// Handle option selection
const handleOptionSelect = (option: AutocompleteOption) => {
onChange(option.value);
onSelect?.(option);
setIsOpen(false);
setHighlightedIndex(-1);
};
// Handle keyboard navigation
const handleKeyDown = (e: React.KeyboardEvent) => {
if (!isOpen) {
if (e.key === "ArrowDown" || e.key === "Enter") {
setIsOpen(true);
return;
}
return;
}
switch (e.key) {
case "ArrowDown":
e.preventDefault();
setHighlightedIndex(prev =>
prev < filteredOptions.length - 1 ? prev + 1 : 0
);
break;
case "ArrowUp":
e.preventDefault();
setHighlightedIndex(prev =>
prev > 0 ? prev - 1 : filteredOptions.length - 1
);
break;
case "Enter":
e.preventDefault();
if (highlightedIndex >= 0 && filteredOptions[highlightedIndex]) {
handleOptionSelect(filteredOptions[highlightedIndex]);
}
break;
case "Escape":
setIsOpen(false);
setHighlightedIndex(-1);
break;
}
};
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
// Scroll highlighted option into view
useEffect(() => {
if (highlightedIndex >= 0 && listRef.current) {
const highlightedElement = listRef.current.children[highlightedIndex] as HTMLElement;
if (highlightedElement) {
highlightedElement.scrollIntoView({
block: "nearest",
behavior: "smooth"
});
}
}
}, [highlightedIndex]);
return (
<div ref={containerRef} className="relative">
<Input
ref={inputRef}
name={name}
label={label}
placeholder={placeholder}
value={value}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
onFocus={() => setIsOpen(true)}
error={error}
required={required}
disabled={disabled}
endIcon={
<div className="pointer-events-auto">
{loading ? (
<svg className="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
) : isOpen ? (
<button
type="button"
onClick={() => setIsOpen(false)}
className="text-gray-400 hover:text-gray-600"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
</svg>
</button>
) : (
<button
type="button"
onClick={() => setIsOpen(true)}
className="text-gray-400 hover:text-gray-600"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
)}
</div>
}
/>
{/* Dropdown */}
{isOpen && filteredOptions.length > 0 && (
<div className="absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-auto">
<ul ref={listRef} className="py-1">
{filteredOptions.map((option, index) => (
<li
key={option.value}
className={`px-3 py-2 cursor-pointer text-sm ${
index === highlightedIndex
? "bg-blue-50 text-blue-900"
: "text-gray-900 hover:bg-gray-50"
}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleOptionSelect(option);
}}
onMouseDown={(e) => {
e.preventDefault(); // Prevent input from losing focus
}}
onMouseEnter={() => setHighlightedIndex(index)}
>
<div className="font-medium">{option.value}</div>
{option.label !== option.value && (
<div className="text-xs text-gray-500 mt-1">{option.label}</div>
)}
</li>
))}
</ul>
</div>
)}
{/* No results */}
{isOpen && filteredOptions.length === 0 && value.length > 0 && (
<div className="absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg">
<div className="px-3 py-2 text-sm text-gray-500 text-center">
لا توجد نتائج مطابقة
</div>
</div>
)}
</div>
);
}