lifeto-shop/src/components/inventory/movetarget.tsx

176 lines
4.8 KiB
TypeScript
Raw Normal View History

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}
role="option"
id={id}
aria-selected={active}
{...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-05-13 21:02:59 +00:00
{...getItemProps({
key: item.path,
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
}