UNPKG

@spaced-out/ui-design-system

Version:
288 lines (270 loc) 8.63 kB
// @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> &nbsp; {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> ); }, );