Files
mtdb_movie/common/resources/client/ui/forms/combobox/combobox.tsx
maher 703f50a09d
Some checks failed
Build / run (push) Has been cancelled
first commit
2025-10-29 11:42:25 +01:00

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};