2025-06-20 05:41:10 +00:00
|
|
|
import {
|
|
|
|
|
autoUpdate,
|
|
|
|
|
FloatingFocusManager,
|
|
|
|
|
FloatingPortal,
|
|
|
|
|
flip,
|
|
|
|
|
size,
|
|
|
|
|
useDismiss,
|
|
|
|
|
useFloating,
|
|
|
|
|
useInteractions,
|
|
|
|
|
useListNavigation,
|
|
|
|
|
useRole,
|
|
|
|
|
} from '@floating-ui/react'
|
|
|
|
|
import Fuse from 'fuse.js'
|
|
|
|
|
import { useAtom, useAtomValue } from 'jotai'
|
|
|
|
|
import { forwardRef, useId, useMemo, useRef, useState } from 'react'
|
|
|
|
|
import { charactersAtom, selectedTargetInventoryAtom } from '@/state/atoms'
|
2024-08-12 01:13:42 +00:00
|
|
|
|
2025-05-25 05:17:41 +00:00
|
|
|
interface AccountInventorySelectorItemProps {
|
2025-06-20 05:41:10 +00:00
|
|
|
children: React.ReactNode
|
|
|
|
|
active: boolean
|
2025-05-13 21:02:59 +00:00
|
|
|
}
|
|
|
|
|
|
2025-05-25 05:17:41 +00:00
|
|
|
const AccountInventorySelectorItem = forwardRef<
|
2025-05-13 21:02:59 +00:00
|
|
|
HTMLDivElement,
|
2025-05-25 05:17:41 +00:00
|
|
|
AccountInventorySelectorItemProps & React.HTMLProps<HTMLDivElement>
|
2025-05-13 21:02:59 +00:00
|
|
|
>(({ children, active, ...rest }, ref) => {
|
2025-06-20 05:41:10 +00:00
|
|
|
const id = useId()
|
2025-05-13 21:02:59 +00:00
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={ref}
|
2025-06-20 06:18:37 +00:00
|
|
|
// biome-ignore lint/a11y/useSemanticElements: Custom autocomplete component needs role="option"
|
2025-05-13 21:02:59 +00:00
|
|
|
role="option"
|
|
|
|
|
id={id}
|
|
|
|
|
aria-selected={active}
|
2025-06-20 06:18:37 +00:00
|
|
|
tabIndex={-1}
|
2025-05-13 21:02:59 +00:00
|
|
|
{...rest}
|
|
|
|
|
style={{
|
2025-06-20 05:41:10 +00:00
|
|
|
background: active ? 'lightblue' : 'none',
|
2025-05-13 21:02:59 +00:00
|
|
|
padding: 4,
|
2025-06-20 05:41:10 +00:00
|
|
|
cursor: 'default',
|
2025-05-13 21:02:59 +00:00
|
|
|
...rest.style,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</div>
|
2025-06-20 05:41:10 +00:00
|
|
|
)
|
|
|
|
|
})
|
2025-05-25 05:17:41 +00:00
|
|
|
export const InventoryTargetSelector = () => {
|
2025-06-20 05:41:10 +00:00
|
|
|
const [open, setOpen] = useState(false)
|
|
|
|
|
const [inputValue, setInputValue] = useState('')
|
|
|
|
|
const [activeIndex, setActiveIndex] = useState<number | null>(null)
|
2025-05-13 21:02:59 +00:00
|
|
|
|
2025-06-20 05:41:10 +00:00
|
|
|
const listRef = useRef<Array<HTMLElement | null>>([])
|
2025-05-13 21:02:59 +00:00
|
|
|
|
|
|
|
|
const { refs, floatingStyles, context } = useFloating<HTMLInputElement>({
|
|
|
|
|
whileElementsMounted: autoUpdate,
|
|
|
|
|
open,
|
|
|
|
|
onOpenChange: setOpen,
|
|
|
|
|
middleware: [
|
|
|
|
|
flip({ padding: 10 }),
|
|
|
|
|
size({
|
|
|
|
|
apply({ rects, availableHeight, elements }) {
|
|
|
|
|
Object.assign(elements.floating.style, {
|
|
|
|
|
width: `${rects.reference.width}px`,
|
|
|
|
|
maxHeight: `${availableHeight}px`,
|
2025-06-20 05:41:10 +00:00
|
|
|
})
|
2025-05-13 21:02:59 +00:00
|
|
|
},
|
|
|
|
|
padding: 10,
|
|
|
|
|
}),
|
|
|
|
|
],
|
2025-06-20 05:41:10 +00:00
|
|
|
})
|
2025-05-13 21:02:59 +00:00
|
|
|
|
2025-06-20 05:41:10 +00:00
|
|
|
const role = useRole(context, { role: 'listbox' })
|
|
|
|
|
const dismiss = useDismiss(context)
|
2025-05-13 21:02:59 +00:00
|
|
|
const listNav = useListNavigation(context, {
|
|
|
|
|
listRef,
|
|
|
|
|
activeIndex,
|
|
|
|
|
onNavigate: setActiveIndex,
|
|
|
|
|
virtual: true,
|
|
|
|
|
loop: true,
|
2025-06-20 05:41:10 +00:00
|
|
|
})
|
2025-05-13 21:02:59 +00:00
|
|
|
|
2025-06-20 05:41:10 +00:00
|
|
|
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
|
|
|
|
|
role,
|
|
|
|
|
dismiss,
|
|
|
|
|
listNav,
|
|
|
|
|
])
|
2025-05-13 21:02:59 +00:00
|
|
|
|
|
|
|
|
function onChange(event: React.ChangeEvent<HTMLInputElement>) {
|
2025-06-20 05:41:10 +00:00
|
|
|
const value = event.target.value
|
|
|
|
|
setInputValue(value)
|
2025-05-13 21:02:59 +00:00
|
|
|
setSelectedTargetInventory(undefined)
|
|
|
|
|
|
|
|
|
|
if (value) {
|
2025-06-20 05:41:10 +00:00
|
|
|
setOpen(true)
|
|
|
|
|
setActiveIndex(0)
|
2025-05-13 21:02:59 +00:00
|
|
|
} else {
|
2025-06-20 05:41:10 +00:00
|
|
|
setOpen(false)
|
2025-05-13 21:02:59 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const { data: subaccounts } = useAtomValue(charactersAtom)
|
|
|
|
|
|
|
|
|
|
const [selectedTargetInventory, setSelectedTargetInventory] = useAtom(selectedTargetInventoryAtom)
|
|
|
|
|
|
2025-06-20 05:41:10 +00:00
|
|
|
const searcher = useMemo(() => {
|
|
|
|
|
return new Fuse(subaccounts?.flatMap(x => [x.bank, x.character]) || [], {
|
|
|
|
|
keys: ['path', 'name'],
|
|
|
|
|
findAllMatches: true,
|
|
|
|
|
threshold: 0.8,
|
|
|
|
|
useExtendedSearch: true,
|
|
|
|
|
})
|
2025-05-13 21:02:59 +00:00
|
|
|
}, [subaccounts])
|
|
|
|
|
|
2025-06-20 05:41:10 +00:00
|
|
|
const items = searcher.search(inputValue || '!-', { limit: 10 }).map(x => x.item)
|
2025-05-13 21:02:59 +00:00
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<input
|
|
|
|
|
className="border border-black-1 bg-gray-100 placeholder-gray-600"
|
|
|
|
|
{...getReferenceProps({
|
|
|
|
|
ref: refs.setReference,
|
|
|
|
|
onChange,
|
|
|
|
|
value: selectedTargetInventory !== undefined ? selectedTargetInventory.name : inputValue,
|
2025-06-20 05:41:10 +00:00
|
|
|
placeholder: 'Target Inventory',
|
|
|
|
|
'aria-autocomplete': 'list',
|
2025-05-25 05:17:41 +00:00
|
|
|
onFocus() {
|
2025-06-20 05:41:10 +00:00
|
|
|
setOpen(true)
|
2025-05-25 05:17:41 +00:00
|
|
|
},
|
2025-05-13 21:02:59 +00:00
|
|
|
onKeyDown(event) {
|
2025-06-20 05:41:10 +00:00
|
|
|
if (event.key === 'Enter' && activeIndex != null && items[activeIndex]) {
|
2025-05-13 21:02:59 +00:00
|
|
|
setSelectedTargetInventory(items[activeIndex])
|
2025-06-20 05:41:10 +00:00
|
|
|
setInputValue(items[activeIndex].name)
|
|
|
|
|
setActiveIndex(null)
|
|
|
|
|
setOpen(false)
|
2025-05-13 21:02:59 +00:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
})}
|
|
|
|
|
/>
|
|
|
|
|
{open && (
|
|
|
|
|
<FloatingPortal>
|
2025-06-20 05:41:10 +00:00
|
|
|
<FloatingFocusManager context={context} initialFocus={-1} visuallyHiddenDismiss>
|
2025-05-13 21:02:59 +00:00
|
|
|
<div
|
|
|
|
|
{...getFloatingProps({
|
|
|
|
|
ref: refs.setFloating,
|
|
|
|
|
style: {
|
|
|
|
|
...floatingStyles,
|
2025-06-20 05:41:10 +00:00
|
|
|
background: '#eee',
|
|
|
|
|
color: 'black',
|
|
|
|
|
overflowY: 'auto',
|
2025-05-13 21:02:59 +00:00
|
|
|
},
|
|
|
|
|
})}
|
|
|
|
|
>
|
|
|
|
|
{items.map((item, index) => (
|
2025-05-25 05:17:41 +00:00
|
|
|
<AccountInventorySelectorItem
|
2025-06-20 06:18:37 +00:00
|
|
|
key={item.path}
|
2025-05-13 21:02:59 +00:00
|
|
|
{...getItemProps({
|
|
|
|
|
ref(node) {
|
2025-06-20 05:41:10 +00:00
|
|
|
listRef.current[index] = node
|
2025-05-13 21:02:59 +00:00
|
|
|
},
|
|
|
|
|
onClick() {
|
2025-06-20 05:41:10 +00:00
|
|
|
setInputValue(item.name)
|
|
|
|
|
setSelectedTargetInventory(item)
|
|
|
|
|
setOpen(false)
|
|
|
|
|
refs.domReference.current?.focus()
|
2025-05-13 21:02:59 +00:00
|
|
|
},
|
|
|
|
|
})}
|
|
|
|
|
active={activeIndex === index}
|
|
|
|
|
>
|
|
|
|
|
{item.name}
|
2025-05-25 05:17:41 +00:00
|
|
|
</AccountInventorySelectorItem>
|
2025-05-13 21:02:59 +00:00
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</FloatingFocusManager>
|
|
|
|
|
</FloatingPortal>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
2025-06-20 05:41:10 +00:00
|
|
|
)
|
2025-05-13 21:02:59 +00:00
|
|
|
}
|