first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
import React, {ReactElement, useEffect, useRef, useState} from 'react';
import {SvgIconProps} from '@common/icons/svg-icon';
import {useTrans} from '@common/i18n/use-trans';
import {useListboxContext} from '@common/ui/forms/listbox/listbox-context';
import {ProgressCircle} from '@common/ui/progress/progress-circle';
import {KeyboardArrowDownIcon} from '@common/icons/material/KeyboardArrowDown';
interface Props {
isLoading?: boolean;
icon?: ReactElement<SvgIconProps>;
}
export function ComboboxEndAdornment({isLoading, icon}: Props) {
const timeout = useRef<any>(null);
const {trans} = useTrans();
const [showLoading, setShowLoading] = useState(false);
const {
state: {isOpen, inputValue},
} = useListboxContext();
const lastInputValue = useRef(inputValue);
useEffect(() => {
if (isLoading && !showLoading) {
if (timeout.current === null) {
timeout.current = setTimeout(() => {
setShowLoading(true);
}, 500);
}
// If user is typing, clear the timer and restart since it is a new request
if (inputValue !== lastInputValue.current) {
clearTimeout(timeout.current);
timeout.current = setTimeout(() => {
setShowLoading(true);
}, 500);
}
} else if (!isLoading) {
// If loading is no longer happening, clear any timers and hide the loading circle
setShowLoading(false);
clearTimeout(timeout.current);
timeout.current = null;
}
lastInputValue.current = inputValue;
}, [isLoading, showLoading, inputValue]);
// loading circle should only be displayed if menu is open, if menuTrigger is "manual", or first time load (to stop circle from showing up when user selects an option)
const showLoadingIndicator = showLoading && (isOpen || isLoading);
if (showLoadingIndicator) {
return (
<ProgressCircle
aria-label={trans({message: 'Loading'})}
size="sm"
isIndeterminate
/>
);
}
return icon || <KeyboardArrowDownIcon />;
}

View File

@@ -0,0 +1,183 @@
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};

View File

@@ -0,0 +1,32 @@
import {useController} from 'react-hook-form';
import {mergeProps} from '@react-aria/utils';
import React from 'react';
import {ComboBox, ComboboxProps} from './combobox';
type Props<T extends object> = ComboboxProps<T> & {
name: string;
selectionMode?: 'single';
};
export function FormComboBox<T extends object>({children, ...props}: Props<T>) {
const {
field: {onChange, onBlur, value = '', ref},
fieldState: {invalid, error},
} = useController({
name: props.name,
});
const formProps: Partial<ComboboxProps<T>> = {
onSelectionChange: onChange,
onBlur,
selectedValue: value,
defaultInputValue: value,
invalid,
errorMessage: error?.message,
};
return (
<ComboBox ref={ref} {...mergeProps(formProps, props)}>
{children}
</ComboBox>
);
}