UNPKG

@ozen-ui/kit

Version:

React component library

199 lines (198 loc) 14.2 kB
import { __assign, __read, __rest, __spreadArray } from "tslib"; import './Autocomplete.css'; import React, { forwardRef, useEffect, useRef, useState } from 'react'; import { useControlled } from '../../hooks/useControlled'; import { useDeprecatedComponent } from '../../hooks/useDeprecated'; import { useMultiRef } from '../../hooks/useMultiRef'; import { useMutableRef } from '../../hooks/useMutableRef'; import { useThemeProps } from '../../hooks/useThemeProps'; import { isKeys } from '../../utils/isKeys'; import { DataList, DataListOption } from '../DataList'; import { Input } from '../Input'; import { cnAutocomplete } from './classNames'; import { AutocompleteLoading, AutocompleteNoOptions, AutocompleteRenderRight, } from './components'; import { AUTOCOMPLETE_DEFAULT_ALLOW_CUSTOM_VALUE, AUTOCOMPLETE_DEFAULT_AUTOFOCUS, AUTOCOMPLETE_DEFAULT_DISABLE_CLEAR_BUTTON, AUTOCOMPLETE_DEFAULT_DISABLE_CLOSE_ON_SELECT, AUTOCOMPLETE_DEFAULT_DISABLE_SHOW_CHEVRON, AUTOCOMPLETE_DEFAULT_DISABLE_SHOW_EMPTY_OPTIONS_LIST, AUTOCOMPLETE_DEFAULT_DISABLED, AUTOCOMPLETE_DEFAULT_FULLWIDTH, AUTOCOMPLETE_DEFAULT_REQUIRED, AUTOCOMPLETE_DEFAULT_SIZE, } from './constants'; import { withDefaultGetters } from './helper'; function AutocompleteRender(inProps, ref) { var props = useThemeProps({ props: inProps, name: 'Autocomplete', }); var _a = withDefaultGetters(props), _b = _a.disabled, disabled = _b === void 0 ? AUTOCOMPLETE_DEFAULT_DISABLED : _b, _c = _a.required, required = _c === void 0 ? AUTOCOMPLETE_DEFAULT_REQUIRED : _c, _d = _a.autoFocus, autoFocus = _d === void 0 ? AUTOCOMPLETE_DEFAULT_AUTOFOCUS : _d, _e = _a.fullWidth, fullWidth = _e === void 0 ? AUTOCOMPLETE_DEFAULT_FULLWIDTH : _e, _f = _a.size, size = _f === void 0 ? AUTOCOMPLETE_DEFAULT_SIZE : _f, _g = _a.allowCustomValue, allowCustomValue = _g === void 0 ? AUTOCOMPLETE_DEFAULT_ALLOW_CUSTOM_VALUE : _g, _h = _a.disableShowChevron, disableShowChevron = _h === void 0 ? AUTOCOMPLETE_DEFAULT_DISABLE_SHOW_CHEVRON : _h, _j = _a.disableClearButton, disableClearButton = _j === void 0 ? AUTOCOMPLETE_DEFAULT_DISABLE_CLEAR_BUTTON : _j, _k = _a.disableShowEmptyOptionsList, disableShowEmptyOptionsList = _k === void 0 ? AUTOCOMPLETE_DEFAULT_DISABLE_SHOW_EMPTY_OPTIONS_LIST : _k, _l = _a.disableCloseOnSelect, disableCloseOnSelect = _l === void 0 ? AUTOCOMPLETE_DEFAULT_DISABLE_CLOSE_ON_SELECT : _l, _m = _a.renderInput, renderInput = _m === void 0 ? function (props) { return React.createElement(Input, __assign({}, props, { ref: ref })); } : _m, searchFunctionProp = _a.searchFunction, renderOptionProp = _a.renderOption, inputValueProp = _a.inputValue, className = _a.className, valueProp = _a.value, options = _a.options, defaultValue = _a.defaultValue, error = _a.error, onChange = _a.onChange, onInputChange = _a.onInputChange, label = _a.label, placeholder = _a.placeholder, renderLeft = _a.renderLeft, renderRight = _a.renderRight, hint = _a.hint, getOptionKey = _a.getOptionKey, getOptionLabel = _a.getOptionLabel, getOptionDisabled = _a.getOptionDisabled, dataListProps = _a.dataListProps, onCloseProp = _a.onClose, onOpenProp = _a.onOpen, openProp = _a.open, defaultOpen = _a.defaultOpen, loading = _a.loading, inputProps = _a.inputProps, bodyProps = _a.bodyProps, onKeyDown = _a.onKeyDown, noOptionsText = _a.noOptionsText, clearText = _a.clearText, openText = _a.openText, loadingText = _a.loadingText, closeText = _a.closeText, other = __rest(_a, ["disabled", "required", "autoFocus", "fullWidth", "size", "allowCustomValue", "disableShowChevron", "disableClearButton", "disableShowEmptyOptionsList", "disableCloseOnSelect", "renderInput", "searchFunction", "renderOption", "inputValue", "className", "value", "options", "defaultValue", "error", "onChange", "onInputChange", "label", "placeholder", "renderLeft", "renderRight", "hint", "getOptionKey", "getOptionLabel", "getOptionDisabled", "dataListProps", "onClose", "onOpen", "open", "defaultOpen", "loading", "inputProps", "bodyProps", "onKeyDown", "noOptionsText", "clearText", "openText", "loadingText", "closeText"]); useDeprecatedComponent('Autocomplete'); var anchorRef = useRef(null); var lastInputValue = useRef(''); var inputBodyRef = useMultiRef([anchorRef, bodyProps === null || bodyProps === void 0 ? void 0 : bodyProps.ref]); var _o = __read(useControlled({ value: inputValueProp, defaultValue: '', name: 'Autocomplete', state: 'inputValue', }), 2), inputValue = _o[0], setInputValue = _o[1]; var _p = __read(useControlled({ value: valueProp, defaultValue: defaultValue, name: 'Autocomplete', state: 'value', }), 2), valueState = _p[0], setValueState = _p[1]; var _q = __read(useControlled({ value: openProp, defaultValue: defaultOpen, name: 'Autocomplete', state: 'open', }), 2), open = _q[0], setOpen = _q[1]; var searchFunctionDefault = function (options, searchValue) { return options === null || options === void 0 ? void 0 : options.filter(function (option) { return getOptionLabel(option).toLowerCase().includes(searchValue.toLowerCase()); }); }; var savedOnInputChange = useMutableRef(onInputChange); var savedGetOptionLabel = useMutableRef(getOptionLabel); var _r = __read(useState(__spreadArray([], __read(options), false)), 2), filteredOptions = _r[0], setFilteredOptions = _r[1]; var searchFunction = searchFunctionProp || searchFunctionDefault; var dataListValue = valueState ? getOptionKey(valueState) : ''; var hasOptions = !!(filteredOptions === null || filteredOptions === void 0 ? void 0 : filteredOptions.length); var showNoOptions = !hasOptions && !loading; var showLoading = !hasOptions && !!loading; var _s = __read(useState(undefined), 2), search = _s[0], setSearch = _s[1]; var changeInputValue = function (e, value) { var _a; setInputValue(value); (_a = savedOnInputChange.current) === null || _a === void 0 ? void 0 : _a.call(savedOnInputChange, e, value); }; var callOnChange = function (e, value) { var inputValue = value ? getOptionLabel(value) : ''; setValueState(value); onChange === null || onChange === void 0 ? void 0 : onChange(e, value); changeInputValue(null, inputValue); }; /** Эффект — фильтрация списка при изменении опций */ useEffect(function () { if (!open) return; setFilteredOptions(search ? search(options) : __spreadArray([], __read(options), false)); }, [search, options, open, searchFunctionProp]); /** Эффект — синхронизируем значение текстового поля со значением в списке */ useEffect(function () { var _a; if (allowCustomValue) return; lastInputValue.current = valueState ? (_a = savedGetOptionLabel.current) === null || _a === void 0 ? void 0 : _a.call(savedGetOptionLabel, valueState) : ''; if (lastInputValue.current !== inputValue) { changeInputValue(null, lastInputValue.current); } }, [valueState, allowCustomValue]); /** Эффект — автофокусировка в текстовом поле */ useEffect(function () { var _a; if (autoFocus) (_a = anchorRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, [autoFocus]); /** Ждём выполнения анимации на закрытие и делаем сброс функции фильтрации */ var handleExited = function () { var _a; setSearch(undefined); (_a = dataListProps === null || dataListProps === void 0 ? void 0 : dataListProps.onExited) === null || _a === void 0 ? void 0 : _a.call(dataListProps); }; /** Закрытие */ var handleClose = function () { setOpen(false); onCloseProp === null || onCloseProp === void 0 ? void 0 : onCloseProp(); }; /** Открытие */ var handleOpen = function () { setOpen(true); onOpenProp === null || onOpenProp === void 0 ? void 0 : onOpenProp(); }; /** Переключатель открытия и закрытия */ var handleToggle = function () { if (disabled) return; if (open) handleClose(); else handleOpen(); }; /** Очистка поля */ var handleClear = function (e) { callOnChange(e, null); setSearch(undefined); }; /** Актуализация значения текстового поля после его покидания */ var handleBlur = function (e) { var _a; (_a = inputProps === null || inputProps === void 0 ? void 0 : inputProps.onBlur) === null || _a === void 0 ? void 0 : _a.call(inputProps, e); if (allowCustomValue) return; if (inputValue !== lastInputValue.current) { changeInputValue(null, lastInputValue.current); } }; /** Открытие списка по клику на текстовом поле */ var handleClickOnInput = function (e) { var _a; handleToggle(); (_a = inputProps === null || inputProps === void 0 ? void 0 : inputProps.onClick) === null || _a === void 0 ? void 0 : _a.call(inputProps, e); }; /** Управление элементом контроля через клавиатуру */ var handleKeyDown = function (e) { if (isKeys(e, ['ArrowDown', 'ArrowUp']) && !open) { e.preventDefault(); handleToggle(); } onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(e); }; /** Событие ввода значения в текстовом поле */ var handleChangeInput = function (e) { var value = e.target.value; // Открываем список при вводе первого символа (при условии, что список еще не открыт) if (value.length && !open) handleOpen(); if (value) { changeInputValue(e, value); } else { callOnChange(e, null); } // Задаем функцию фильтрации setSearch(function () { return function (options) { return searchFunction(__spreadArray([], __read(options), false), value); }; }); }; /** Событие выбора значения из раскрывающегося списка */ var handleChangeDataList = function (e, _a) { var value = _a.value; var selectedOption = filteredOptions === null || filteredOptions === void 0 ? void 0 : filteredOptions.find(function (option) { return (getOptionKey === null || getOptionKey === void 0 ? void 0 : getOptionKey(option)) === value; }); callOnChange(e, selectedOption || null); // Закрываем список после выбора if (!disableCloseOnSelect) handleClose(); }; /** Отображение текстового поля */ var input = renderInput(__assign(__assign({ size: size, hint: hint, disabled: disabled, label: label, required: required, error: error, fullWidth: fullWidth, placeholder: placeholder, renderLeft: renderLeft }, other), { renderRight: (React.createElement(AutocompleteRenderRight, { open: open, size: size, disabled: disabled, clearText: clearText, closeText: closeText, openText: openText, hasValue: !!inputValue, onClear: handleClear, onOpen: handleToggle, renderRight: renderRight, disableShowChevron: disableShowChevron, disableClearButton: disableClearButton })), onChange: handleChangeInput, onKeyDown: handleKeyDown, value: inputValue || '', inputProps: __assign(__assign({ autoComplete: 'off' }, inputProps), { onBlur: handleBlur, onClick: handleClickOnInput }), bodyProps: __assign(__assign({ 'aria-expanded': open }, bodyProps), { ref: inputBodyRef }), className: cnAutocomplete({ size: size, hasChevron: !disableShowChevron }, [ className, ]), ref: ref })); /** Отображение опций */ var renderOptions = filteredOptions === null || filteredOptions === void 0 ? void 0 : filteredOptions.map(function (option) { var selected = valueState ? getOptionKey(valueState) === getOptionKey(option) : false; var renderOptionDefault = function (_a) { var option = _a.option; return (React.createElement(DataListOption, { key: getOptionKey === null || getOptionKey === void 0 ? void 0 : getOptionKey(option), label: getOptionLabel === null || getOptionLabel === void 0 ? void 0 : getOptionLabel(option), value: getOptionKey === null || getOptionKey === void 0 ? void 0 : getOptionKey(option), disabled: getOptionDisabled === null || getOptionDisabled === void 0 ? void 0 : getOptionDisabled(option) })); }; var renderOption = renderOptionProp || renderOptionDefault; return renderOption({ option: option, selected: selected }); }); return (React.createElement(React.Fragment, null, input, React.createElement(DataList, __assign({ size: size, equalAnchorWidth: true, offset: [0, 4], placement: "bottom-start" }, dataListProps, { open: open, onClose: handleClose, anchorRef: anchorRef, onExited: handleExited, selected: dataListValue, onSelect: handleChangeDataList, listProps: __assign({ role: 'listbox' }, dataListProps === null || dataListProps === void 0 ? void 0 : dataListProps.listProps) }), renderOptions, !disableShowEmptyOptionsList && (React.createElement(AutocompleteNoOptions, { showNoOptions: showNoOptions, noOptionsText: noOptionsText })), React.createElement(AutocompleteLoading, { showLoading: showLoading, loadingText: loadingText, size: size })))); } /** * @deprecated Компонент устарел. Для замены используйте компонент AutocompleteNext */ export var Autocomplete = forwardRef(AutocompleteRender); Autocomplete.displayName = 'Autocomplete';