184 lines
5.1 KiB
TypeScript
Executable File
184 lines
5.1 KiB
TypeScript
Executable File
import React, {ReactElement, Ref} from 'react';
|
|
import {BaseFieldPropsWithDom} from '../input-field/base-field-props';
|
|
import {Item} from '../listbox/item';
|
|
import {useListbox} from '../listbox/use-listbox';
|
|
import {IconButton} from '../../buttons/icon-button';
|
|
import {TextField} from '../input-field/text-field/text-field';
|
|
import {Listbox} from '../listbox/listbox';
|
|
import {SvgIconProps} from '@common/icons/svg-icon';
|
|
import {useListboxKeyboardNavigation} from '@common/ui/forms/listbox/use-listbox-keyboard-navigation';
|
|
import {createEventHandler} from '@common/utils/dom/create-event-handler';
|
|
import {ListBoxChildren, ListboxProps} from '../listbox/types';
|
|
import {Popover} from '../../overlays/popover';
|
|
import {ComboboxEndAdornment} from '@common/ui/forms/combobox/combobox-end-adornment';
|
|
|
|
export {Item as Option};
|
|
|
|
export type ComboboxProps<T extends object> = Omit<
|
|
BaseFieldPropsWithDom<HTMLInputElement>,
|
|
'endAdornment'
|
|
> &
|
|
ListBoxChildren<T> &
|
|
ListboxProps & {
|
|
selectionMode?: 'single' | 'none';
|
|
isAsync?: boolean;
|
|
isLoading?: boolean;
|
|
openMenuOnFocus?: boolean;
|
|
endAdornmentIcon?: ReactElement<SvgIconProps>;
|
|
useOptionLabelAsInputValue?: boolean;
|
|
hideEndAdornment?: boolean;
|
|
onEndAdornmentClick?: () => void;
|
|
prependListbox?: boolean;
|
|
listboxClassName?: string;
|
|
};
|
|
|
|
function ComboBox<T extends object>(
|
|
props: ComboboxProps<T> & {selectionMode: 'single'},
|
|
ref: Ref<HTMLInputElement>,
|
|
) {
|
|
const {
|
|
children,
|
|
items,
|
|
isAsync,
|
|
isLoading,
|
|
openMenuOnFocus = true,
|
|
endAdornmentIcon,
|
|
onItemSelected,
|
|
maxItems,
|
|
clearInputOnItemSelection,
|
|
inputValue: userInputValue,
|
|
selectedValue,
|
|
onSelectionChange,
|
|
allowCustomValue = false,
|
|
onInputValueChange,
|
|
defaultInputValue,
|
|
selectionMode = 'single',
|
|
useOptionLabelAsInputValue,
|
|
showEmptyMessage,
|
|
floatingMaxHeight,
|
|
hideEndAdornment = false,
|
|
blurReferenceOnItemSelection,
|
|
isOpen: propsIsOpen,
|
|
onOpenChange: propsOnOpenChange,
|
|
prependListbox,
|
|
listboxClassName,
|
|
onEndAdornmentClick,
|
|
autoFocusFirstItem = true,
|
|
...textFieldProps
|
|
} = props;
|
|
|
|
const listbox = useListbox(
|
|
{
|
|
...props,
|
|
floatingMaxHeight,
|
|
blurReferenceOnItemSelection,
|
|
selectionMode,
|
|
role: 'listbox',
|
|
virtualFocus: true,
|
|
clearSelectionOnInputClear: true,
|
|
},
|
|
ref,
|
|
);
|
|
|
|
const {
|
|
reference,
|
|
listboxId,
|
|
onInputChange,
|
|
state: {
|
|
isOpen,
|
|
setIsOpen,
|
|
inputValue,
|
|
setInputValue,
|
|
selectValues,
|
|
selectedValues,
|
|
setActiveCollection,
|
|
},
|
|
collection,
|
|
} = listbox;
|
|
|
|
const textLabel = selectedValues[0]
|
|
? collection.get(selectedValues[0])?.textLabel
|
|
: undefined;
|
|
|
|
const {handleListboxSearchFieldKeydown} =
|
|
useListboxKeyboardNavigation(listbox);
|
|
|
|
const handleFocusAndClick = createEventHandler(
|
|
(e: React.FocusEvent<HTMLInputElement>) => {
|
|
if (openMenuOnFocus && !isOpen) {
|
|
setIsOpen(true);
|
|
}
|
|
e.target.select();
|
|
},
|
|
);
|
|
|
|
return (
|
|
<Listbox
|
|
prepend={prependListbox}
|
|
className={listboxClassName}
|
|
listbox={listbox}
|
|
mobileOverlay={Popover}
|
|
isLoading={isLoading}
|
|
onPointerDown={e => {
|
|
// prevent focus from leaving input when scrolling listbox via mouse
|
|
e.preventDefault();
|
|
}}
|
|
>
|
|
<TextField
|
|
inputRef={reference}
|
|
{...textFieldProps}
|
|
endAdornment={
|
|
!hideEndAdornment ? (
|
|
<IconButton
|
|
size="md"
|
|
tabIndex={-1}
|
|
disabled={textFieldProps.disabled}
|
|
className="pointer-events-auto"
|
|
onPointerDown={e => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (onEndAdornmentClick) {
|
|
onEndAdornmentClick();
|
|
} else {
|
|
setActiveCollection('all');
|
|
setIsOpen(!isOpen);
|
|
}
|
|
}}
|
|
>
|
|
<ComboboxEndAdornment
|
|
isLoading={isLoading}
|
|
icon={endAdornmentIcon}
|
|
/>
|
|
</IconButton>
|
|
) : null
|
|
}
|
|
aria-expanded={isOpen ? 'true' : 'false'}
|
|
aria-haspopup="listbox"
|
|
aria-controls={isOpen ? listboxId : undefined}
|
|
aria-autocomplete="list"
|
|
autoComplete="off"
|
|
autoCorrect="off"
|
|
spellCheck="false"
|
|
onChange={onInputChange}
|
|
value={useOptionLabelAsInputValue && textLabel ? textLabel : inputValue}
|
|
onBlur={e => {
|
|
if (allowCustomValue) {
|
|
selectValues(e.target.value);
|
|
} else if (!clearInputOnItemSelection) {
|
|
const val = selectedValues[0];
|
|
setInputValue(selectValues.length && val != null ? `${val}` : '');
|
|
}
|
|
}}
|
|
onFocus={handleFocusAndClick}
|
|
onClick={handleFocusAndClick}
|
|
onKeyDown={e => handleListboxSearchFieldKeydown(e)}
|
|
/>
|
|
</Listbox>
|
|
);
|
|
}
|
|
|
|
const ComboBoxForwardRef = React.forwardRef(ComboBox) as <T extends object>(
|
|
props: ComboboxProps<T> & {ref?: Ref<HTMLInputElement>},
|
|
) => ReactElement;
|
|
export {ComboBoxForwardRef as ComboBox};
|