feat: multi-feature update
This commit is contained in:
+93
-51
@@ -17,6 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import * as React from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Check, ChevronsUpDown } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -150,10 +151,101 @@ export function ComboboxInput({
|
||||
item?.scrollIntoView({ block: 'nearest' })
|
||||
}, [highlightedIndex])
|
||||
|
||||
const [dropdownPos, setDropdownPos] = React.useState<{
|
||||
top: number
|
||||
left: number
|
||||
width: number
|
||||
} | null>(null)
|
||||
|
||||
const updateDropdownPos = React.useCallback(() => {
|
||||
if (!containerRef.current) return
|
||||
const rect = containerRef.current.getBoundingClientRect()
|
||||
setDropdownPos({
|
||||
top: rect.bottom + 4,
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Update dropdown position when open
|
||||
React.useEffect(() => {
|
||||
if (!open) {
|
||||
setDropdownPos(null)
|
||||
return
|
||||
}
|
||||
updateDropdownPos()
|
||||
const handleScroll = () => updateDropdownPos()
|
||||
window.addEventListener('scroll', handleScroll, true)
|
||||
window.addEventListener('resize', handleScroll)
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll, true)
|
||||
window.removeEventListener('resize', handleScroll)
|
||||
}
|
||||
}, [open, updateDropdownPos])
|
||||
|
||||
const showDropdown =
|
||||
open &&
|
||||
(filteredOptions.length > 0 || (allowCustomValue && searchValue.trim()))
|
||||
|
||||
const dropdownContent = showDropdown && dropdownPos ? (
|
||||
<div
|
||||
className='bg-popover text-popover-foreground fixed z-[100] rounded-md border shadow-md'
|
||||
style={{
|
||||
top: dropdownPos.top,
|
||||
left: dropdownPos.left,
|
||||
width: dropdownPos.width,
|
||||
}}
|
||||
>
|
||||
{filteredOptions.length > 0 ? (
|
||||
<ul
|
||||
ref={listRef}
|
||||
role='listbox'
|
||||
className='max-h-[200px] overflow-y-auto p-1'
|
||||
>
|
||||
{filteredOptions.map((option, index) => (
|
||||
<li
|
||||
key={option.value}
|
||||
role='option'
|
||||
aria-selected={value === option.value}
|
||||
data-highlighted={index === highlightedIndex}
|
||||
className={cn(
|
||||
'relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm select-none',
|
||||
index === highlightedIndex &&
|
||||
'bg-accent text-accent-foreground',
|
||||
value === option.value && 'font-medium'
|
||||
)}
|
||||
onMouseEnter={() => setHighlightedIndex(index)}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault() // Prevent blur
|
||||
handleSelect(option.value)
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
'size-4 shrink-0',
|
||||
value === option.value ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
{option.icon && <span>{option.icon}</span>}
|
||||
<span className='truncate'>{option.label}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className='px-2 py-6 text-center text-sm'>
|
||||
{t(emptyText)}
|
||||
{allowCustomValue && searchValue.trim() && (
|
||||
<div className='text-muted-foreground mt-1 text-xs'>
|
||||
{t('Press Enter to use "{{value}}"', {
|
||||
value: searchValue.trim(),
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className='relative'>
|
||||
<Input
|
||||
@@ -184,57 +276,7 @@ export function ComboboxInput({
|
||||
/>
|
||||
<ChevronsUpDown className='pointer-events-none absolute top-1/2 right-3 size-4 shrink-0 -translate-y-1/2 opacity-50' />
|
||||
|
||||
{showDropdown && (
|
||||
<div className='bg-popover text-popover-foreground absolute top-full z-100 mt-1 w-full rounded-md border shadow-md'>
|
||||
{filteredOptions.length > 0 ? (
|
||||
<ul
|
||||
ref={listRef}
|
||||
role='listbox'
|
||||
className='max-h-[200px] overflow-y-auto p-1'
|
||||
>
|
||||
{filteredOptions.map((option, index) => (
|
||||
<li
|
||||
key={option.value}
|
||||
role='option'
|
||||
aria-selected={value === option.value}
|
||||
data-highlighted={index === highlightedIndex}
|
||||
className={cn(
|
||||
'relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm select-none',
|
||||
index === highlightedIndex &&
|
||||
'bg-accent text-accent-foreground',
|
||||
value === option.value && 'font-medium'
|
||||
)}
|
||||
onMouseEnter={() => setHighlightedIndex(index)}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault() // Prevent blur
|
||||
handleSelect(option.value)
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
'size-4 shrink-0',
|
||||
value === option.value ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
{option.icon && <span>{option.icon}</span>}
|
||||
<span className='truncate'>{option.label}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className='px-2 py-6 text-center text-sm'>
|
||||
{t(emptyText)}
|
||||
{allowCustomValue && searchValue.trim() && (
|
||||
<div className='text-muted-foreground mt-1 text-xs'>
|
||||
{t('Press Enter to use "{{value}}"', {
|
||||
value: searchValue.trim(),
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{dropdownContent && createPortal(dropdownContent, document.body)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user