@spaced-out/ui-design-system
Version:
Sense UI components library
288 lines (270 loc) • 8.63 kB
Flow
// @flow strict
import * as React from 'react';
import classify from '../../utils/classify';
import {Dropdown} from '../Dropdown';
import type {IconType} from '../Icon';
import type {InputOnChangeParamsType} from '../Input';
import {Input} from '../Input';
import type {MenuOption, MenuProps} from '../Menu';
import {BodySmall, FormLabelSmall} from '../Text';
import css from './Combobox.module.css';
type InputClassNames = $ReadOnly<{
box?: string,
iconLeft?: string,
iconRight?: string,
wrapper?: string,
}>;
type DropdownClassNames = $ReadOnly<{
wrapper?: string,
box?: string,
iconRight?: string,
}>;
type ClassNames = $ReadOnly<{
wrapper?: string,
box?: string,
input?: InputClassNames,
dropdown?: DropdownClassNames,
}>;
export type ComboboxProps = {
/* Combobox props */
classNames?: ClassNames,
disabled?: boolean,
type?: 'text' | 'tel',
label?: string | React.Node,
size?: 'medium' | 'small',
onContainerClick?: ?(SyntheticEvent<HTMLElement>) => mixed,
locked?: boolean,
error?: boolean,
errorText?: string,
helperText?: string | React.Node,
required?: boolean,
readOnly?: boolean,
boxRef?: (?HTMLElement) => mixed,
value: {dropdown?: string, input?: string},
onChange: ({
input: string,
dropdown: string,
inputChange?: InputOnChangeParamsType,
dropdownOption?: MenuOption,
}) => mixed,
/* Input props */
inputPlaceholder?: string,
onInputFocus?: (e: SyntheticInputEvent<HTMLInputElement>) => mixed,
onInputBlur?: (e: SyntheticInputEvent<HTMLInputElement>) => mixed,
onInputKeyDown?: (e: SyntheticKeyboardEvent<HTMLInputElement>) => mixed,
onInputContainerClick?: ?(SyntheticEvent<HTMLElement>) => mixed,
inputName?: string,
iconLeftName?: string,
iconLeftType?: IconType,
iconRightName?: string,
iconRightType?: IconType,
onIconRightClick?: ?(SyntheticEvent<HTMLElement>) => mixed,
inputBoxRef?: (?HTMLElement) => mixed,
minLength?: string,
maxLength?: string,
pattern?: string,
min?: string,
max?: string,
/* Dropdown props */
menu?: MenuProps,
onMenuOpen?: () => mixed,
onMenuClose?: () => mixed,
scrollMenuToBottom?: boolean,
onDropdownFocus?: (e: SyntheticInputEvent<HTMLInputElement>) => mixed,
onDropdownBlur?: (e: SyntheticInputEvent<HTMLInputElement>) => mixed,
onDropdownKeyDown?: (e: SyntheticKeyboardEvent<HTMLInputElement>) => mixed,
onDropdownContainerClick?: ?(SyntheticEvent<HTMLElement>) => mixed,
dropdownName?: string,
dropdownPlaceholder?: string,
dropdownBoxRef?: (?HTMLElement) => mixed,
...
};
export const Combobox: React$AbstractComponent<ComboboxProps, HTMLDivElement> =
React.forwardRef<ComboboxProps, HTMLDivElement>(
(props: ComboboxProps, ref) => {
const {
value,
onChange,
classNames,
disabled,
type = 'text',
label,
inputPlaceholder,
size = 'medium',
onContainerClick,
onInputFocus,
onInputBlur,
onInputKeyDown,
onInputContainerClick,
inputName,
locked,
error,
errorText,
helperText,
required,
iconLeftName,
iconLeftType,
iconRightName,
iconRightType,
onIconRightClick,
readOnly,
boxRef,
inputBoxRef,
minLength,
maxLength,
pattern,
min,
max,
menu,
onMenuOpen,
onMenuClose,
scrollMenuToBottom,
onDropdownFocus,
onDropdownBlur,
onDropdownKeyDown,
onDropdownContainerClick,
dropdownName,
dropdownPlaceholder,
dropdownBoxRef,
} = props;
const inputRef = React.useRef();
const dropdownRef = React.useRef();
const {input: inputValue, dropdown: dropdownInputText} = value;
const handleInputChange = (evt, isEnter) => {
onChange({
dropdown: dropdownInputText || '',
input: evt.target.value,
inputChange: {evt, isEnter},
});
};
const handleDropdownChange = (option) => {
onChange({
dropdown: option.label || '',
input: inputValue || '',
dropdownOption: option,
});
};
/* Handling locked functionality at Combobox level */
React.useEffect(() => {
if (locked) {
inputRef.current && (inputRef.current.disabled = true);
dropdownRef.current && (dropdownRef.current.disabled = true);
} else {
inputRef.current && (inputRef.current.disabled = false);
dropdownRef.current && (dropdownRef.current.disabled = false);
}
}, [locked]);
return (
<div ref={ref} className={classify(css.wrapper, classNames?.wrapper)}>
{Boolean(label) && (
<div className={css.info}>
<div className={css.infoContent}>
<FormLabelSmall color="secondary">{label ?? ''}</FormLabelSmall>
{required && <FormLabelSmall color="danger">*</FormLabelSmall>}
</div>
</div>
)}
<div
className={classify(
css.box,
{
[css.withError]: error ?? false,
},
classNames?.box,
)}
onClick={!(disabled || locked) ? onContainerClick : null}
ref={boxRef}
>
<Dropdown
ref={dropdownRef}
menu={menu}
boxRef={dropdownBoxRef}
placeholder={dropdownPlaceholder}
readOnly={readOnly}
classNames={{
wrapper: classify(
css.dropdownWrapper,
classNames?.dropdown?.wrapper,
),
box: classify(
css.dropdownBox,
{
[css.disabled]: disabled ?? null,
[css.locked]: locked ?? null,
},
classNames?.dropdown?.box,
),
iconRight: css.chevron,
}}
disabled={disabled}
size={size}
onMenuOpen={onMenuOpen}
onMenuClose={onMenuClose}
onChange={handleDropdownChange}
scrollMenuToBottom={scrollMenuToBottom}
dropdownInputText={dropdownInputText}
onFocus={onDropdownFocus}
onBlur={onDropdownBlur}
onKeyDown={onDropdownKeyDown}
onContainerClick={onDropdownContainerClick}
name={dropdownName}
/>
<Input
ref={inputRef}
boxRef={inputBoxRef}
name={inputName}
placeholder={inputPlaceholder}
onFocus={onInputFocus}
onBlur={onInputBlur}
onChange={handleInputChange}
onKeyDown={onInputKeyDown}
onContainerClick={onInputContainerClick}
value={inputValue}
type={type}
error={error}
disabled={disabled}
readOnly={readOnly}
classNames={{
wrapper: classify(css.inputWrapper, classNames?.input?.wrapper),
box: classify(
css.inputBox,
{
[css.disabled]: disabled ?? null,
[css.locked]: locked ?? null,
},
classNames?.input?.box,
),
iconLeft: classNames?.input?.iconLeft,
iconRight: classNames?.input?.iconRight,
}}
size={size}
iconLeftName={iconLeftName}
iconLeftType={iconLeftType}
iconRightName={iconRightName}
iconRightType={iconRightType}
onIconRightClick={onIconRightClick}
minLength={minLength}
maxLength={maxLength}
pattern={pattern}
min={min}
max={max}
/>
</div>
{(Boolean(helperText) || error) && (
<div className={css.info}>
{error && errorText ? (
<BodySmall color="danger">{errorText}</BodySmall>
) : typeof helperText === 'string' ? (
<BodySmall color={disabled ? 'disabled' : 'secondary'}>
{helperText}
</BodySmall>
) : (
helperText
)}
</div>
)}
</div>
);
},
);