import { Combobox, ScrollArea, useVirtualizedCombobox } from '@mantine/core'; import { useVirtualizer } from '@tanstack/react-virtual'; import React, { useCallback, useEffect, useImperativeHandle, useRef } from 'react'; import type { BoxerItem, BoxerProps, BoxerRef } from './Boxer.types'; import { BoxerProvider, useBoxerStore } from './Boxer.store'; import BoxerTarget from './BoxerTarget'; import useBoxerOptions from './hooks/useBoxerOptions'; const BoxerInner = React.forwardRef((_, ref) => { // Component Refs const parentRef = useRef(null); const inputRef = useRef(null); const valueRef = useRef(null); const bufferRef = useRef(null); // Component store State const { boxerData, clearable, comboBoxProps, dropDownProps, error, fetchData, fetchMoreOnBottomReached, input, isFetching, label, mah, multiSelect, onBufferChange, onChange, opened, openOnClear, placeholder, scrollAreaProps, search, selectedOptionIndex, selectFirst, setInput, setOpened, setSearch, setSelectedOptionIndex, showAll, value, } = useBoxerStore((state) => ({ boxerData: state.boxerData, clearable: state.clearable, comboBoxProps: state.comboBoxProps, dropDownProps: state.dropDownProps, error: state.error, fetchData: state.fetchData, fetchMoreOnBottomReached: state.fetchMoreOnBottomReached, input: state.input, isFetching: state.isFetching, label: state.label, mah: state.mah, multiSelect: state.multiSelect, onBufferChange: state.onBufferChange, onChange: state.onChange, opened: state.opened, openOnClear: state.openOnClear, placeholder: state.placeholder, scrollAreaProps: state.scrollAreaProps, search: state.search, selectedOptionIndex: state.selectedOptionIndex, selectFirst: state.selectFirst, setInput: state.setInput, setOpened: state.setOpened, setSearch: state.setSearch, setSelectedOptionIndex: state.setSelectedOptionIndex, showAll: state.showAll, value: state.value, })); // Virtualization setup const count = boxerData.length; const virtualizer = useVirtualizer({ count, estimateSize: () => 36, getScrollElement: () => parentRef.current, }); const virtualItems = virtualizer.getVirtualItems(); // Component Callback Functions const onOptionSubmit = useCallback( (indexOrId: number | string) => { const index = typeof indexOrId === 'string' ? parseInt(indexOrId, 10) : indexOrId; const option = boxerData[index]; if (!option) { return; } if (multiSelect) { // Handle multi-select const currentValues = Array.isArray(value) ? value : []; const isSelected = currentValues.includes(option.value); const newValues = isSelected ? currentValues.filter((v: any) => v !== option.value) : [...currentValues, option.value]; onChange?.(newValues); // Update buffer for multi-select const newBuffer = boxerData.filter((item) => newValues.includes(item.value)); onBufferChange?.(newBuffer); } else { // Handle single select onChange?.(option.value); setSearch(''); setInput(option.label); valueRef.current = option.value; setOpened(false); } }, [boxerData, multiSelect, value, onChange, onBufferChange, setSearch, setInput, setOpened] ); const onClear = useCallback(() => { if (showAll && selectFirst) { onOptionSubmit(0); } else { if (multiSelect) { onChange?.([] as any); } else { onChange?.(null as any); } setSearch(''); setInput(''); inputRef.current?.focus(); } if (openOnClear) { setOpened(true); } }, [showAll, selectFirst, multiSelect, onChange, setSearch, setInput, openOnClear, setOpened, onOptionSubmit]); // Component Hooks const combobox = useVirtualizedCombobox({ getOptionId: (index) => String(index), onDropdownClose: () => { setOpened(false); }, onDropdownOpen: () => { if (!value || (multiSelect && (!Array.isArray(value) || value.length === 0))) { setSearch(''); setInput(''); } combobox.selectFirstOption(); }, onSelectedOptionSubmit: onOptionSubmit, opened, selectedOptionIndex, setSelectedOptionIndex: (index) => { setSelectedOptionIndex(index); if (index !== -1) { virtualizer.scrollToIndex(index); } }, totalOptionsCount: boxerData.length, }); // Component variables const { options } = useBoxerOptions({ boxerData, multiSelect, onOptionSubmit, value, }); // Component useEffects useEffect(() => { // Fetch initial data fetchData('', true); }, []); useEffect(() => { // Handle search changes const delayDebounceFn = setTimeout(() => { if (search !== undefined && opened) { fetchData(search, true); } }, 300); return () => clearTimeout(delayDebounceFn); }, [search, opened]); useEffect(() => { // Sync input with value if (multiSelect) { const labels = boxerData .filter((item) => Array.isArray(value) && value.includes(item.value)) .map((item) => item.label) .join(', '); // When dropdown is closed, show selected labels. When open, allow searching if (!opened && input !== labels) { setInput(labels); setSearch(''); } } else { const label = boxerData.find((item) => item.value === value)?.label; // Only sync if we need to update the input to match the value if (input !== label && (search ?? '') === '' && valueRef.current !== value && value) { setInput(label ?? ''); } else if (!value && !valueRef.current && (search ?? '') === '') { setSearch(''); setInput(''); } } // Handle buffer change if (multiSelect) { const buffer = boxerData.filter((item: BoxerItem) => Array.isArray(value) && value.includes(item.value)) ?? []; if (JSON.stringify(bufferRef.current) !== JSON.stringify(buffer)) { onBufferChange?.(buffer); bufferRef.current = buffer; } } else { const buffer = boxerData?.find((item: BoxerItem) => item.value === value) ?? null; if (bufferRef.current?.value !== buffer?.value) { onBufferChange?.(buffer); bufferRef.current = buffer; } } }, [value, boxerData, input, search, multiSelect, opened, onBufferChange, setInput, setSearch]); useEffect(() => { // Select first option automatically if (selectFirst && (boxerData?.length ?? 0) > 0 && !multiSelect) { if (!value) { onOptionSubmit?.(0); } } }, [selectFirst, boxerData, multiSelect]); // Expose ref methods useImperativeHandle(ref, () => ({ clear: () => { onClear(); }, close: () => { setOpened(false); combobox.closeDropdown(); }, focus: () => { inputRef.current?.focus(); }, getValue: () => { return value; }, open: () => { setOpened(true); combobox.openDropdown(); }, setValue: (newValue: any) => { onChange?.(newValue); }, })); return ( { if (!value && !multiSelect) { setSearch(''); setInput(''); combobox.closeDropdown(); setOpened(false); } }} onClear={onClear} onSearch={(event) => { setSearch(event.currentTarget.value); setInput(event.currentTarget.value); setOpened(true); }} placeholder={placeholder} ref={inputRef} search={input} /> { if (inputRef.current) { inputRef.current.value = ''; inputRef.current?.focus(); } }} p={2} {...dropDownProps} > {opened && options.length > 0 ? ( { fetchMoreOnBottomReached(event.currentTarget as HTMLDivElement); }, style: { border: '1px solid gray', borderRadius: 4 }, }} viewportRef={parentRef} >
{virtualItems.map((virtualRow) => (
{options[virtualRow.index]}
))}
) : ( Nothing found )}
); }); BoxerInner.displayName = 'BoxerInner'; const Boxer = React.forwardRef((props, ref) => { return ( ); }); Boxer.displayName = 'Boxer'; export { Boxer }; export default Boxer;