2026-02-12 18:15:06 +05:30
|
|
|
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
|
import { createPortal } from 'react-dom';
|
|
|
|
|
import { IoIosArrowDown, IoIosArrowUp, IoIosCloseCircle } from 'react-icons/io';
|
|
|
|
|
import '../../../Styles/Product/TurboAddForm.scss';
|
|
|
|
|
|
|
|
|
|
const CellSelect = memo(
|
|
|
|
|
({
|
|
|
|
|
value,
|
|
|
|
|
onChange,
|
|
|
|
|
onKeyDown,
|
|
|
|
|
dataCell,
|
|
|
|
|
options,
|
|
|
|
|
hasError,
|
|
|
|
|
placeholder = 'Select option',
|
|
|
|
|
onClick,
|
|
|
|
|
}) => {
|
2026-01-27 18:27:29 +05:30
|
|
|
const [isOpen, setIsOpen] = useState(false);
|
2026-02-12 18:15:06 +05:30
|
|
|
const [dropdownDirection, setDropdownDirection] = useState('down');
|
|
|
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
|
|
|
const [dropdownPosition, setDropdownPosition] = useState({
|
|
|
|
|
top: 0,
|
|
|
|
|
left: 0,
|
|
|
|
|
width: 0,
|
|
|
|
|
});
|
2026-01-27 18:27:29 +05:30
|
|
|
const selectRef = useRef(null);
|
|
|
|
|
const dropdownRef = useRef(null);
|
|
|
|
|
const searchInputRef = useRef(null);
|
|
|
|
|
|
|
|
|
|
// Filter options based on search term
|
2026-02-12 18:15:06 +05:30
|
|
|
const filteredOptions =
|
|
|
|
|
options?.filter((option) =>
|
2026-01-27 18:27:29 +05:30
|
|
|
option.label.toLowerCase().includes(searchTerm.toLowerCase())
|
2026-02-12 18:15:06 +05:30
|
|
|
) || [];
|
2026-01-27 18:27:29 +05:30
|
|
|
|
|
|
|
|
// Calculate dropdown position
|
|
|
|
|
const calculateDropdownPosition = useCallback(() => {
|
2026-02-12 18:15:06 +05:30
|
|
|
if (!selectRef.current) return 'down';
|
|
|
|
|
const rect = selectRef.current.getBoundingClientRect();
|
|
|
|
|
const spaceBelow = window.innerHeight - rect.bottom;
|
|
|
|
|
const spaceAbove = rect.top;
|
|
|
|
|
return spaceBelow < 200 && spaceAbove > spaceBelow ? 'up' : 'down';
|
2026-01-27 18:27:29 +05:30
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// Close dropdown on outside click or scroll
|
|
|
|
|
useEffect(() => {
|
2026-02-12 18:15:06 +05:30
|
|
|
if (!isOpen) return;
|
|
|
|
|
|
|
|
|
|
const handler = (e) => {
|
|
|
|
|
if (
|
|
|
|
|
!dropdownRef.current?.contains(e.target) &&
|
|
|
|
|
!selectRef.current?.contains(e.target)
|
|
|
|
|
) {
|
|
|
|
|
setIsOpen(false);
|
|
|
|
|
// Reset search term when closing dropdown
|
|
|
|
|
setSearchTerm('');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const scrollHandler = (e) => {
|
|
|
|
|
if (
|
|
|
|
|
dropdownRef.current &&
|
|
|
|
|
!dropdownRef.current.contains(e.target) &&
|
|
|
|
|
selectRef.current &&
|
|
|
|
|
!selectRef.current.contains(e.target)
|
|
|
|
|
) {
|
|
|
|
|
setIsOpen(false);
|
|
|
|
|
setSearchTerm('');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
document.addEventListener('mousedown', handler);
|
|
|
|
|
window.addEventListener('scroll', scrollHandler, true);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
document.removeEventListener('mousedown', handler);
|
|
|
|
|
window.removeEventListener('scroll', scrollHandler, true);
|
|
|
|
|
};
|
2026-01-27 18:27:29 +05:30
|
|
|
}, [isOpen]);
|
|
|
|
|
|
|
|
|
|
// Focus search input when dropdown opens
|
|
|
|
|
useEffect(() => {
|
2026-02-12 18:15:06 +05:30
|
|
|
if (isOpen && searchInputRef.current) {
|
|
|
|
|
searchInputRef.current.focus();
|
|
|
|
|
}
|
2026-01-27 18:27:29 +05:30
|
|
|
}, [isOpen]);
|
|
|
|
|
|
2026-02-12 18:15:06 +05:30
|
|
|
// Update dropdown direction and position
|
2026-01-27 18:27:29 +05:30
|
|
|
useEffect(() => {
|
2026-02-12 18:15:06 +05:30
|
|
|
if (isOpen && selectRef.current) {
|
|
|
|
|
const rect = selectRef.current.getBoundingClientRect();
|
|
|
|
|
const spaceBelow = window.innerHeight - rect.bottom;
|
|
|
|
|
const spaceAbove = rect.top;
|
|
|
|
|
const direction =
|
|
|
|
|
spaceBelow < 200 && spaceAbove > spaceBelow ? 'up' : 'down';
|
|
|
|
|
|
|
|
|
|
setDropdownDirection(direction);
|
|
|
|
|
setDropdownPosition({
|
|
|
|
|
top: direction === 'up' ? rect.top : rect.bottom,
|
|
|
|
|
left: rect.left,
|
|
|
|
|
width: rect.width,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, [isOpen]);
|
2026-01-27 18:27:29 +05:30
|
|
|
|
|
|
|
|
// Keyboard navigation
|
2026-02-12 18:15:06 +05:30
|
|
|
const handleKeyDown = (e) => {
|
|
|
|
|
if (
|
|
|
|
|
!isOpen &&
|
|
|
|
|
['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)
|
|
|
|
|
) {
|
|
|
|
|
e.preventDefault();
|
2026-01-27 18:27:29 +05:30
|
|
|
onKeyDown(e);
|
2026-02-12 18:15:06 +05:30
|
|
|
return;
|
|
|
|
|
}
|
2026-01-27 18:27:29 +05:30
|
|
|
|
2026-02-12 18:15:06 +05:30
|
|
|
if (isOpen && ['ArrowLeft', 'ArrowRight'].includes(e.key)) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (e.key === 'Enter') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (isOpen && filteredOptions.length > 0) {
|
|
|
|
|
// Select the first option when pressing Enter
|
|
|
|
|
handleSelectChange(filteredOptions[0].value);
|
|
|
|
|
} else {
|
|
|
|
|
setIsOpen((v) => !v);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (e.key === 'Escape') {
|
|
|
|
|
e.preventDefault();
|
2026-01-27 18:27:29 +05:30
|
|
|
setIsOpen(false);
|
2026-02-12 18:15:06 +05:30
|
|
|
setSearchTerm('');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Allow typing to search when dropdown is open
|
|
|
|
|
if (isOpen && e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
|
|
|
|
|
setSearchTerm((prev) => prev + e.key);
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onKeyDown(e);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSelectChange = (v) => {
|
|
|
|
|
onChange({ target: { value: v } });
|
|
|
|
|
setIsOpen(false);
|
|
|
|
|
setSearchTerm(''); // Clear search term after selection
|
2026-01-27 18:27:29 +05:30
|
|
|
};
|
|
|
|
|
|
2026-02-12 18:15:06 +05:30
|
|
|
const handleSearchChange = (e) => {
|
|
|
|
|
setSearchTerm(e.target.value);
|
2026-01-27 18:27:29 +05:30
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const clearSearch = () => {
|
2026-02-12 18:15:06 +05:30
|
|
|
setSearchTerm('');
|
|
|
|
|
if (searchInputRef.current) {
|
|
|
|
|
searchInputRef.current.focus();
|
|
|
|
|
}
|
2026-01-27 18:27:29 +05:30
|
|
|
};
|
|
|
|
|
|
2026-02-12 18:15:06 +05:30
|
|
|
const selectedOption = options?.find((opt) => opt.value === value);
|
2026-01-27 18:27:29 +05:30
|
|
|
const displayValue = selectedOption ? selectedOption.label : placeholder;
|
|
|
|
|
|
|
|
|
|
return (
|
2026-02-12 18:15:06 +05:30
|
|
|
<div className="custom-select-container" ref={selectRef}>
|
|
|
|
|
<div
|
|
|
|
|
className={`custom-select-trigger${hasError ? ' error' : ''}${isOpen ? ' open' : ''}`}
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
setIsOpen((v) => !v);
|
|
|
|
|
onClick?.(e);
|
|
|
|
|
}}
|
|
|
|
|
onKeyDown={handleKeyDown}
|
|
|
|
|
data-cell={dataCell}
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
>
|
|
|
|
|
<span className={!value ? 'placeholder-text' : ''}>
|
|
|
|
|
{isOpen ? (
|
|
|
|
|
<input
|
|
|
|
|
ref={searchInputRef}
|
|
|
|
|
className="search-input"
|
|
|
|
|
value={searchTerm}
|
|
|
|
|
onChange={handleSearchChange}
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
placeholder={selectedOption?.label || placeholder}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
displayValue
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="dropdown-arrow">
|
|
|
|
|
{isOpen ? <IoIosArrowUp /> : <IoIosArrowDown />}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
{isOpen &&
|
|
|
|
|
createPortal(
|
2026-01-27 18:27:29 +05:30
|
|
|
<div
|
2026-02-12 18:15:06 +05:30
|
|
|
className={`custom-select-dropdown ${dropdownDirection}`}
|
|
|
|
|
ref={dropdownRef}
|
|
|
|
|
style={{
|
|
|
|
|
position: 'fixed',
|
|
|
|
|
top:
|
|
|
|
|
dropdownDirection === 'up'
|
|
|
|
|
? 'auto'
|
|
|
|
|
: `${dropdownPosition.top}px`,
|
|
|
|
|
bottom:
|
|
|
|
|
dropdownDirection === 'up'
|
|
|
|
|
? `${window.innerHeight - dropdownPosition.top}px`
|
|
|
|
|
: 'auto',
|
|
|
|
|
left: `${dropdownPosition.left}px`,
|
|
|
|
|
width: `${dropdownPosition.width}px`,
|
|
|
|
|
zIndex: 99999,
|
|
|
|
|
fontFamily:"Poppins"
|
|
|
|
|
}}
|
2026-01-27 18:27:29 +05:30
|
|
|
>
|
2026-02-12 18:15:06 +05:30
|
|
|
<div className="dropdown-content">
|
|
|
|
|
{filteredOptions.length === 0 ? (
|
|
|
|
|
<div className="dropdown-option disabled">
|
|
|
|
|
{searchTerm ? 'No matches found' : 'No data'}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
filteredOptions.map((option) => (
|
|
|
|
|
<div
|
|
|
|
|
key={option.value}
|
|
|
|
|
className={`dropdown-option${value === option.value ? ' selected' : ''}`}
|
|
|
|
|
onClick={() => handleSelectChange(option.value)}
|
|
|
|
|
>
|
|
|
|
|
{option.label}
|
2026-01-27 18:27:29 +05:30
|
|
|
</div>
|
2026-02-12 18:15:06 +05:30
|
|
|
))
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>,
|
|
|
|
|
document.body
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-01-27 18:27:29 +05:30
|
|
|
);
|
2026-02-12 18:15:06 +05:30
|
|
|
}
|
|
|
|
|
);
|
2026-01-27 18:27:29 +05:30
|
|
|
|
2026-02-12 18:15:06 +05:30
|
|
|
export default CellSelect;
|