diff --git a/src/components/AutoCompleteCanary/AutoComplete.tsx b/src/components/AutoCompleteCanary/AutoComplete.tsx new file mode 100644 index 000000000..5f7a20c76 --- /dev/null +++ b/src/components/AutoCompleteCanary/AutoComplete.tsx @@ -0,0 +1,43 @@ +import React from 'react'; + +import { AutoCompleteTypeText } from './AutoCompleteTypeText'; +import { AutoCompleteTypeTextArray } from './AutoCompleteTypeTextArray'; +import { + AutoCompleteComponent, + AutoCompleteGroupDefault, + AutoCompleteItemDefault, + AutoCompleteProps, + AutoCompleteTypeComponent, +} from './types'; + +const typeMap: Record< + string, + AutoCompleteTypeComponent | AutoCompleteTypeComponent<'textarray'> +> = { + text: AutoCompleteTypeText, + textarray: AutoCompleteTypeTextArray, +}; + +const AutoCompleteRender = < + TYPE extends string, + ITEM = AutoCompleteItemDefault, + GROUP = AutoCompleteGroupDefault, +>( + props: AutoCompleteProps, + ref: React.Ref, +) => { + const Component = typeMap[props.type || 'text'] || typeMap.text; + + return ( + )} + /> + ); +}; + +export const AutoComplete = React.forwardRef( + AutoCompleteRender, +) as AutoCompleteComponent; + +export * from './types'; diff --git a/src/components/AutoCompleteCanary/AutoCompleteTypeText/AutoCompleteTypeText.tsx b/src/components/AutoCompleteCanary/AutoCompleteTypeText/AutoCompleteTypeText.tsx new file mode 100644 index 000000000..2fb79d03f --- /dev/null +++ b/src/components/AutoCompleteCanary/AutoCompleteTypeText/AutoCompleteTypeText.tsx @@ -0,0 +1,134 @@ +import React, { forwardRef, useRef } from 'react'; + +import { SelectDropdown } from '##/components/SelectComponents/SelectDropdown'; +import { + defaultPropForm, + defaultPropSize, + defaultPropView, +} from '##/components/SelectComponentsDeprecated/types'; +import { TextFieldTypeText } from '##/components/TextFieldCanary'; +import { useForkRef } from '##/hooks/useForkRef'; + +import { withDefaultGetters } from '../helpers'; +import { AutoCompleteTypeComponent } from '../types'; +import { useAutoComplete } from '../useAutoComplete'; +import { useRenderItemDefault } from '../useRenderItemDefault'; + +export const AutoCompleteTypeText: AutoCompleteTypeComponent = + forwardRef((props, componentRef) => { + const { + items = [], + groups = [], + disabled = false, + getItemLabel, + getItemKey, + isLoading, + getItemGroupKey, + getGroupKey, + getGroupLabel, + inputRef, + onFocus, + dropdownRef: dropdownRefProp, + dropdownClassName, + dropdownForm = 'default', + value, + form = defaultPropForm, + view = defaultPropView, + size = defaultPropSize, + onChange, + style, + renderItem, + searchFunction, + id, + name, + className, + virtualScroll, + onScrollToBottom, + onDropdownOpen, + dropdownOpen, + ignoreOutsideClicksRefs, + ...otherProps + } = withDefaultGetters(props); + + const dropdownRef = useRef(null); + const controlRef = useRef(null); + + const { + getOptionProps, + isOpen, + getKeyProps, + visibleItems, + handleInputFocus, + inputRef: inputControlRef, + optionsRefs, + handleChange, + } = useAutoComplete({ + items, + groups, + onChange, + dropdownRef, + disabled, + searchValue: value || '', + getItemLabel, + getItemKey, + getItemGroupKey, + getGroupKey, + onFocus, + searchFunction, + isLoading, + onDropdownOpen, + dropdownOpen, + ignoreOutsideClicksRefs: [controlRef, ...(ignoreOutsideClicksRefs || [])], + }); + + const renderItemDefault = useRenderItemDefault( + getItemLabel, + disabled, + size, + dropdownForm, + ); + + return ( + <> + + + + ); + }); diff --git a/src/components/AutoCompleteCanary/AutoCompleteTypeText/index.ts b/src/components/AutoCompleteCanary/AutoCompleteTypeText/index.ts new file mode 100644 index 000000000..5b7caad7b --- /dev/null +++ b/src/components/AutoCompleteCanary/AutoCompleteTypeText/index.ts @@ -0,0 +1 @@ +export * from './AutoCompleteTypeText'; diff --git a/src/components/AutoCompleteCanary/AutoCompleteTypeTextArray/AutoCompleteTypeTextArray.tsx b/src/components/AutoCompleteCanary/AutoCompleteTypeTextArray/AutoCompleteTypeTextArray.tsx new file mode 100644 index 000000000..6cb92615b --- /dev/null +++ b/src/components/AutoCompleteCanary/AutoCompleteTypeTextArray/AutoCompleteTypeTextArray.tsx @@ -0,0 +1,139 @@ +import React, { forwardRef, useRef } from 'react'; + +import { SelectDropdown } from '##/components/SelectComponents/SelectDropdown'; +import { + defaultPropForm, + defaultPropSize, + defaultPropView, +} from '##/components/SelectComponentsDeprecated/types'; +import { TextFieldTypeTextArray } from '##/components/TextFieldCanary'; +import { useForkRef } from '##/hooks/useForkRef'; + +import { withDefaultGetters } from '../helpers'; +import { AutoCompleteTypeComponent } from '../types'; +import { useAutoComplete } from '../useAutoComplete'; +import { useRenderItemDefault } from '../useRenderItemDefault'; + +export const AutoCompleteTypeTextArray: AutoCompleteTypeComponent<'textarray'> = + forwardRef((props, componentRef) => { + const withDefaultGettersProps = withDefaultGetters(props); + const { + items = [], + groups = [], + disabled = false, + getItemLabel, + getItemKey, + isLoading, + getItemGroupKey, + getGroupKey, + getGroupLabel, + inputRef, + onFocus, + dropdownRef: dropdownRefProp, + dropdownClassName, + dropdownForm = 'default', + value, + form = defaultPropForm, + view = defaultPropView, + size = defaultPropSize, + onChange, + style, + renderItem, + searchFunction, + id, + name, + className, + virtualScroll, + onScrollToBottom, + onDropdownOpen, + dropdownOpen, + ignoreOutsideClicksRefs, + onInputChange, + inputValue, + ...otherProps + } = withDefaultGettersProps; + + const dropdownRef = useRef(null); + const controlRef = useRef(null); + + const { + getOptionProps, + isOpen, + getKeyProps, + visibleItems, + handleInputFocus, + inputRef: inputControlRef, + optionsRefs, + handleChange, + highlightedIndex, + } = useAutoComplete({ + items, + groups, + onChange: onInputChange, + dropdownRef, + disabled, + searchValue: inputValue || '', + getItemLabel, + getItemKey, + getItemGroupKey, + getGroupKey, + onFocus, + searchFunction, + isLoading, + onDropdownOpen, + dropdownOpen, + ignoreOutsideClicksRefs: [controlRef, ...(ignoreOutsideClicksRefs || [])], + }); + + const renderItemDefault = useRenderItemDefault( + getItemLabel, + disabled, + size, + dropdownForm, + ); + + return ( + <> + + + + ); + }); diff --git a/src/components/AutoCompleteCanary/AutoCompleteTypeTextArray/index.ts b/src/components/AutoCompleteCanary/AutoCompleteTypeTextArray/index.ts new file mode 100644 index 000000000..d54ecc3e6 --- /dev/null +++ b/src/components/AutoCompleteCanary/AutoCompleteTypeTextArray/index.ts @@ -0,0 +1 @@ +export * from './AutoCompleteTypeTextArray'; diff --git a/src/components/AutoCompleteCanary/__mocks__/data.mock.ts b/src/components/AutoCompleteCanary/__mocks__/data.mock.ts new file mode 100644 index 000000000..42387fac0 --- /dev/null +++ b/src/components/AutoCompleteCanary/__mocks__/data.mock.ts @@ -0,0 +1,122 @@ +export const basicItems = ['Первый', 'Второй', 'Третий', 'Четвертый', 'Пятый']; + +export type Item = { + label: string; + id: string | number; + groupId?: string | number; +}; + +export type Group = { + label: string; + id: string | number; +}; + +const postfixes = ['@gmail.com', '@yandex.ru', '@mail.ru']; + +export const getMailItems = (value: string | null | undefined): Item[] => { + if (postfixes.find((el) => value?.includes('@'))) { + return []; + } + return postfixes.map((el, index) => ({ + id: index, + label: `${value}${el}`, + })); +}; + +export const items: Item[] = [ + { + label: 'Чёрный', + groupId: 1, + id: 1, + }, + { + label: 'Белый', + groupId: 1, + id: 2, + }, + { + label: 'Синий', + groupId: 1, + id: 3, + }, + { + label: 'Красный', + groupId: 1, + id: 4, + }, + { + label: 'Сине-зелёный', + groupId: 2, + id: 5, + }, + { + label: 'Красно-коричневый', + groupId: 2, + id: 6, + }, + { + label: 'Жёлтый', + groupId: 1, + id: 7, + }, + { + label: 'В полосочку', + groupId: 3, + id: 8, + }, + { + label: 'В горошек', + groupId: 3, + id: 9, + }, + { + label: 'Оранжевый', + groupId: 1, + id: 10, + }, + { + label: 'Серо-бурый', + groupId: 2, + id: 11, + }, + { + label: 'Сиренево-синий', + groupId: 2, + id: 12, + }, + { + label: 'Розовый', + groupId: 1, + id: 13, + }, + { + label: 'В клетку', + groupId: 3, + id: 14, + }, + { + label: 'Коричневый', + groupId: 1, + id: 15, + }, + { + label: 'Жёлто-красный', + groupId: 2, + id: 16, + }, +]; + +export const groups: Group[] = [ + { + id: 1, + label: 'Первая группа', + }, + { + id: 2, + label: 'Вторая группа', + }, + { + id: 3, + label: 'Третья группа', + }, +]; diff --git a/src/components/AutoCompleteCanary/__stand__/AutoComplete.dev.stand.mdx b/src/components/AutoCompleteCanary/__stand__/AutoComplete.dev.stand.mdx new file mode 100644 index 000000000..d34245dc7 --- /dev/null +++ b/src/components/AutoCompleteCanary/__stand__/AutoComplete.dev.stand.mdx @@ -0,0 +1,1379 @@ +import { AutoCompleteExampleItems } from './examples/AutoCompleteExampleItems/AutoCompleteExampleItems'; +import { AutoCompleteExampleDisabled } from './examples/AutoCompleteExampleDisabled/AutoCompleteExampleDisabled'; +import { + AutoCompleteExampleForm, + AutoCompleteExampleDropdownForm, +} from './examples/AutoCompleteExampleForm/AutoCompleteExampleForm'; +import { AutoCompleteExampleGroups } from './examples/AutoCompleteExampleGroups/AutoCompleteExampleGroups'; +import { AutoCompleteExampleLabel } from './examples/AutoCompleteExampleLabel/AutoCompleteExampleLabel'; +import { AutoCompleteExamplePlaceholder } from './examples/AutoCompleteExamplePlaceholder/AutoCompleteExamplePlaceholder'; +import { AutoCompleteExampleRenderItem } from './examples/AutoCompleteExampleRenderItem/AutoCompleteExampleRenderItem'; +import { AutoCompleteExampleSize } from './examples/AutoCompleteExampleSize/AutoCompleteExampleSize'; +import { AutoCompleteExampleStatus } from './examples/AutoCompleteExampleStatus/AutoCompleteExampleStatus'; +import { + AutoCompleteExampleType, + AutoCompleteExampleTypeEmail, + AutoCompleteExampleTypeEmailArray, +} from './examples/AutoCompleteExampleType/AutoCompleteExampleType'; +import { AutoCompleteExampleView } from './examples/AutoCompleteExampleView/AutoCompleteExampleView'; +import { AutoCompleteExampleVirtualScroll } from './examples/AutoCompleteExampleVirtualScroll/AutoCompleteExampleVirtualScroll'; +import { AutoCompleteExampleDropdownOpen } from './examples/AutoCompleteExampleDropdownOpen/AutoCompleteExampleDropdownOpen'; +import { + AutoCompleteExampleIsLoading, + AutoCompleteExampleOnScrollBottom, +} from './examples/AutoCompleteExampleIsLoading/AutoCompleteExampleIsLoading'; +import { MdxMenu, MdxTabs } from '@consta/stand'; + +```tsx +import { AutoComplete } from '@consta/uikit/AutoCompleteCanary'; +``` + + + +- [Подсказки при наборе](#подсказки-при-наборе) + - [Объект с подсказками](#объект-с-подсказками) + - [Группы подсказок](#группы-подсказок) + - [Поиск по подсказкам](#поиск-по-подсказкам) +- [Подсказка в поле (плейсхолдер)](#подсказка-в-поле-плейсхолдер) +- [Тип поля](#тип-поля) +- [Заголовок и подпись](#заголовок-и-подпись) +- [Статус](#статус-поля) +- [Размер](#размер) +- [Форма](#форма) + - [Форма поля](#форма-поля) + - [Форма списка](#форма-выпадающего-списка) +- [Внешний вид](#внешний-вид) +- [Виртуализация списка](#виртуализация-списка) +- [Индикация загрузки](#индикация-загрузки) +- [Неактивный выпадающий список](#неактивный-выпадающий-список) +- [Управление состоянием выподающего списка](#управление-состоянием-выподающего-списка) +- [Кастомизация](#кастомизация) + +- [Список свойств](#свойства) + + + +## Подсказки при наборе + +Компонент выглядит, как обычное поле ввода. Если в этом поле что-то напечатать, +компонент найдёт все подсказки, в названии которых есть напечатанные символы и +покажет их в выпадающем списке. + +Подсказки нужно собрать заранее и передать компоненту в свойстве `items`. + +### Объект с подсказками + +Элементы выпадающего списка, то есть подсказки, можно описать в объекте `items` типа [DefaultItem](#свойства). Что внутри: + +- `label` — название элемента списка, +- `id` — уникальный ключ элемента, +- `groupId` — идентификатор [группы](#группы-подсказок), в которую входит этот элемент (если список разбит на группы). + +Вы можете создать [кастомный тип данных для подсказок](#кастомный-элемент-списка) и использовать его. + +### Группы подсказок + +Чтобы разбить элементы списка на группы, создайте объект с группами и передайте +в свойстве `groups` типа [DefaultGroup](#свойства). Что внутри: + +- `label` — название группы, +- `id` — уникальный ключ. + + + +```tsx +import React, { useState } from 'react'; + +import { AutoComplete } from '@consta/uikit/AutoCompleteCanary'; + +type Item = { + label: string; + id: string | number; + groupId?: string | number; +}; + +type Group = { + label: string; + id: string | number; +}; + +const items: Item[] = [ + { + label: 'Чёрный', + groupId: 1, + id: 1, + }, + { + label: 'Белый', + groupId: 1, + id: 2, + }, + { + label: 'Синий', + groupId: 1, + id: 3, + }, + { + label: 'Красный', + groupId: 1, + id: 4, + }, + { + label: 'Сине-зелёный', + groupId: 2, + id: 5, + }, + { + label: 'Красно-коричневый', + groupId: 2, + id: 6, + }, + ... +]; + +const groups: Group[] = [ + { + id: 1, + label: 'Первая группа', + }, + { + id: 2, + label: 'Вторая группа', + }, + { + id: 3, + label: 'Третья группа', + }, +]; + +const AutoCompleteExampleGroups = () => { + const [value, setValue] = useState(null); + return ( + + ); +}; +``` + + + + + +### Поиск по подсказкам + +По умолчанию компонент ищет по `label` подсказки. Вы можете поменять параметры поиска с помощью `searchFunction`. + +## Подсказка в поле (плейсхолдер) + +Подсказка, которая видна, когда ни один вариант не выбран, задаётся в свойстве `placeholder`. +Сформулируйте подсказку так, чтобы стало понятно, что по клику на поле раскроется список вариантов, один из которых нужно выбрать. + +Если не знаете, что написать в подсказке, используйте универсальное **Выберите**. + + + +```tsx +import React, { useState } from 'react'; + +import { AutoComplete } from '@consta/uikit/AutoCompleteCanary'; + +type Item = { + label: string; + id: number; +}; + +const items: Item[] = [ + { + label: 'Первый', + id: 1, + }, + { + label: 'Второй', + id: 2, + }, + { + label: 'Третий', + id: 3, + }, +]; + +const AutoCompleteExamplePlaceholder = () => { + const [value, setValue] = useState(null); + return ( + + ); +}; +``` + + + + + +## Тип поля + +Свойство `type` определяет тип поля и тег, который будет для него использоваться. Варианты: + +- `text` — поле для ввода текста в одну строку,``. + +- `textarray` — текстовое поле массива строк. + +Здесь описаны основные типы, но вы можете использовать любые `input type`, +например, ``. + + + +```tsx +import React, { useCallback, useState } from 'react'; + +import { + AutoComplete, + AutoCompletePropOnChange, +} from '@consta/uikit/AutoCompleteCanary'; + +type Item = { + label: string; + id: number; +}; + +const items: Item[] = [ + { + label: 'Первый', + id: 1, + }, + { + label: 'Второй', + id: 2, + }, + { + label: 'Третий', + id: 3, + }, +]; +export const AutoCompleteExampleType = () => { + const [value, setValue] = useState(null); + const [inputValue, setInputValue] = useState(null); + + const handleChange: AutoCompletePropOnChange<'textarray'> = useCallback( + (value) => { + setValue(value); + setInputValue(null); + }, + [], + ); + + return ( + + ); +}; +``` + + + + + + + +```tsx +import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; + +import { AutoComplete } from '@consta/uikit/AutoCompleteCanary'; + +const postfixes = ['@gmail.com', '@yandex.ru', '@mail.ru']; + +export const getMailItems = (value: string | null | undefined): Item[] => { + if (postfixes.find((el) => value?.includes('@'))) { + return []; + } + return postfixes.map((el, index) => ({ + id: index, + label: `${value}${el}`, + })); +}; + +const AutoCompleteExampleTypeEmail = () => { + const [value, setValue] = useState(null); + return ( + + ); +}; +``` + + + + + + + +```tsx +import { + AutoComplete, + AutoCompletePropOnChange, +} from '@consta/uikit/AutoCompleteCanary'; + +const postfixes = ['@gmail.com', '@yandex.ru', '@mail.ru']; + +export const AutoCompleteExampleTypeEmailArray = () => { + const [value, setValue] = useState(null); + const [inputValue, setInputValue] = useState(null); + + const handleChange: AutoCompletePropOnChange<'textarray'> = useCallback( + (value) => { + setValue(value); + setInputValue(null); + }, + [], + ); + + return ( + + ); +}; +``` + + + + + +## Заголовок и подпись + +Вы можете добавить к компоненту заголовок и подпись, используя [FieldWrapper](##LIBS.LIB.STAND/lib:uikit/stand:components-fieldcomponents-canary/hash:fieldwrapper) + +Заголовок можно добавить с помощью свойства `label`. +За его расположение отвечает свойство `labelPosition`. +Варианты: `'top'` (над полем) или `'left'` (слева от поля), по умолчанию `'top'`. + +Подпись можно добавить с помощью свойства `caption`. + + + +```tsx +import React, { useState } from 'react'; + +import { AutoComplete } from '@consta/uikit/AutoCompleteCanary'; + +const items: string[] = ['Первый', 'Второй', 'Третий']; + +export const AutoCompleteExampleLabel = () => { + const [value, setValue] = useState(null); + return ( + + item} + value={value} + items={items} + onChange={setValue} + /> + + ); +}; +``` + + + + + +## Статус поля + +С помощью статуса можно выделить поле: показать, где ошибка, или, наоборот, +отметить правильно заполненные поля. + +За статус отвечает свойство `status`. Если оно не указано, поле будет обычным. + + + +```tsx +import React, { useState } from 'react'; + +import { AutoComplete } from '@consta/uikit/AutoCompleteCanary'; + +type Item = { + label: string; + id: number; +}; + +const items: Item[] = [ + { + label: 'Первый', + id: 1, + }, + { + label: 'Второй', + id: 2, + }, + { + label: 'Третий', + id: 3, + }, +]; +const AutoCompleteExampleStatus = () => { + const [value, setValue] = useState(null); + return ( + <> + + + + + ); +}; +``` + + + + + +## Размер + +За размер компонента отвечает свойство `size`. Варианты: `xs`, `s` , `m` , `l` , по умолчанию `m`. + +Компонент занимает всю ширину родительского блока. + + + +```tsx +import React, { useState } from 'react'; + +import { AutoComplete } from '@consta/uikit/AutoCompleteCanary'; + +type Item = { + label: string; + id: number; +}; + +const items: Item[] = [ + { + label: 'Первый', + id: 1, + }, + { + label: 'Второй', + id: 2, + }, + { + label: 'Третий', + id: 3, + }, +]; + +const AutoCompleteExampleSize = () => { + const [value, setValue] = useState(null); + return ( + <> + + + + + + ); +}; +``` + + + + + +## Форма + +### Форма поля + +За форму селекта отвечает свойство `form`. Варианты: +`default`, `brick`, `round`, `clearRound`, `roundClear`, `clearDefault`, `defaultClear`, `defaultBrick`, `brickDefault`, `brickClear`, `clearBrick`, `clearClear`. По умолчанию `default`. + + + +```tsx +import React, { useState } from 'react'; + +import { AutoComplete } from '@consta/uikit/AutoCompleteCanary'; + +type Item = { + label: string; + id: number; +}; + +const items: Item[] = [ + { + label: 'Первый', + id: 1, + }, + { + label: 'Второй', + id: 2, + }, + { + label: 'Третий', + id: 3, + }, +]; + +const AutoCompleteExampleForm = () => { + const [value, setValue] = useState(null); + return ( + + + ); +}; +``` + + + + + +### Форма выпадающего списка + +За форму выпадающего списка с подсказками отвечает свойство `dropdownForm`. +Варианты: `default` (по умолчанию), `brick`, `round`. + + + +```tsx + + +``` + + + + + +## Внешний вид + +За вид компонента отвечает свойство `view`. Варианты: `default`, `clear`, по умолчанию `default`. + +Вид `view="clear"` используется, когда нужно создать кастомное поле ввода или обернуть его в контейнер. В этом случае поля с разными статусами и разной формы будут выглядеть одинаково. + +> Для создания hover эффекта для view="`clear`", управляйте внешним видом контейнера, в котором находится AutoComplete. + + + +```tsx +import React, { useState } from 'react'; + +import { AutoComplete } from '@consta/uikit/AutoCompleteCanary'; + +type Item = { + label: string; + id: number; +}; + +const items: Item[] = [ + { + label: 'Первый', + id: 1, + }, + { + label: 'Второй', + id: 2, + }, + { + label: 'Третий', + id: 3, + }, +]; +const AutoCompleteExampleView = () => { + const [value, setValue] = useState(null); + return ( + + + ); +}; +``` + + + + + +## Виртуализация списка + +Если список элементов очень большой, то стоит использовать виртуализацию скрола. + + + +```tsx +import { Example } from '@consta/stand'; +import React, { useState } from 'react'; + +import { + AutoComplete, + AutoCompletePropSearchFunction, +} from '@consta/uikit/AutoCompleteCanary'; + +type Item = { + label: string; + id: number; + groupId: number; +}; + +type Group = { + label: string; + id: number; +}; + +const items: Item[] = new Array(100000).fill(null).map((_, i) => ({ + label: `Опция ${i + 1}`, + groupId: Math.floor(i / 100) + 1, + id: i, +})); + +const groups: Group[] = new Array(items.length / 100) + .fill(null) + .map((_, i) => ({ + id: i + 1, + label: `Группа ${i + 1}`, + })); + +export const AutoCompleteExampleVirtualScroll = () => { + const [value, setValue] = useState(null); + + const searchFunction: AutoCompletePropSearchFunction = useDebounce( + (item, searchValue) => { + if (!searchValue) { + return true; + } + return ( + item.label + .toLocaleLowerCase() + .indexOf(searchValue.toLocaleLowerCase()) !== -1 + ); + }, + 300, + ); + + return ( + + ); +}; +``` + + + + + +## Индикация загрузки + +Для отображения индикации загрузки укажите параметр `isLoading` + +Пример ниже показывает, как можно загрузить данные после раскрытия списка. +Этот функционаол стоит использовать когда список сильно большой + + + +```tsx +type Item = { + label: string; + id: number; +}; + +const useMockLoadData = ( + searchValue = '', + timeout = 2000, +): [Item[], boolean, () => void] => { + const [isLoading, setIsLoading] = useFlag(); + const [isStart, setIsStart] = useFlag(); + const [lenght, setLenght] = useState(0); + const isLoadingOffDebounce = useDebounce(setIsLoading.off, 2000); + + const items = useMemo(() => { + if (searchValue) { + return new Array(10000) + .fill(null) + .map((_, i) => ({ + label: `Опция ${i + 1}`, + id: i, + })) + .filter( + (item) => + item.label + .toLocaleLowerCase() + .indexOf(searchValue.toLocaleLowerCase()) !== -1, + ); + } + return new Array(lenght).fill(null).map((_, i) => ({ + label: `Опция ${i + 1}`, + id: i, + })); + }, [lenght, searchValue]); + + useEffect(() => { + if (isLoading) { + setIsLoading.on(); + isLoadingOffDebounce(); + } + }, [searchValue]); + + useEffect(() => { + if (isStart) { + setIsLoading.on(); + } + + const timeoutNumber = setTimeout(() => { + if (isStart) { + setLenght((state) => state + 100); + setIsLoading.off(); + setIsStart.off(); + } + }, timeout); + + return () => { + clearTimeout(timeoutNumber); + }; + }, [isStart]); + + useEffect(() => { + return () => { + setIsLoading.off(); + setIsStart.off(); + }; + }, []); + + return [searchValue && isLoading ? [] : items, isLoading, setIsStart.on]; +}; + +export const AutoCompleteExampleIsLoading = () => { + const [value, setValue] = useState(); + const [data, isLoading, onFocus] = useMockLoadData(); + + return ( + { + if (!searchValue) { + return true; + } + return ( + item.label + .toLocaleLowerCase() + .indexOf(searchValue.toLocaleLowerCase()) !== -1 + ); + }} + /> + ); +}; +``` + + + + + +Пример ниже показывает как можно дозагружать данные при скролле до конца списка. + + + +```tsx +const useMockLoadData = ( + searchValue = '', + timeout = 2000, +): [Item[], boolean, () => void] => { + const [isLoading, setIsLoading] = useFlag(); + const [isStart, setIsStart] = useFlag(); + const [lenght, setLenght] = useState(0); + const isLoadingOffDebounce = useDebounce(setIsLoading.off, 2000); + + const items = useMemo(() => { + if (searchValue) { + return new Array(10000) + .fill(null) + .map((_, i) => ({ + label: `Опция ${i + 1}`, + id: i, + })) + .filter( + (item) => + item.label + .toLocaleLowerCase() + .indexOf(searchValue.toLocaleLowerCase()) !== -1, + ); + } + return new Array(lenght).fill(null).map((_, i) => ({ + label: `Опция ${i + 1}`, + id: i, + })); + }, [lenght, searchValue]); + + useEffect(() => { + if (isLoading) { + setIsLoading.on(); + isLoadingOffDebounce(); + } + }, [searchValue]); + + useEffect(() => { + if (isStart) { + setIsLoading.on(); + } + + const timeoutNumber = setTimeout(() => { + if (isStart) { + setLenght((state) => state + 100); + setIsLoading.off(); + setIsStart.off(); + } + }, timeout); + + return () => { + clearTimeout(timeoutNumber); + }; + }, [isStart]); + + useEffect(() => { + return () => { + setIsLoading.off(); + setIsStart.off(); + }; + }, []); + + return [searchValue && isLoading ? [] : items, isLoading, setIsStart.on]; +}; + +export const AutoCompleteExampleOnScrollBottom = () => { + const [value, setValue] = useState(null); + const [data, isLoading, onFocus] = useMockLoadData(value || ''); + + return ( + true} + /> + ); +}; +``` + + + + + +## Неактивный выпадающий список + +Чтобы сделать весь комбобокс неактивным, добавьте `disabled`. + + + +```tsx +import React, { useState } from 'react'; + +import { AutoComplete } from '@consta/uikit/AutoCompleteCanary'; + +type Item = { + label: string; + id: number; +}; + +const items: Item[] = [ + { + label: 'Первый', + id: 1, + }, + { + label: 'Второй', + id: 2, + }, + { + label: 'Третий', + id: 3, + }, +]; + +const AutoCompleteExampleDisabled = () => { + const [value, setValue] = useState(null); + return ( + + ); +}; +``` + + + + + +## Управление состоянием выподающего списка + +Для управлением состояния выподающего списка используйте следующие свойства: + +- `dropdownOpen` - флаг открыт/закрыт +- `onDropdownOpen` - колбек отрабатывающий каждый раз при открытии/закрытии +- `ignoreClicksInsideRefs` - список ссылок на элементы по которым игнорируется клики + + + +```tsx +type Item = { + label: string; + id: number | string; +}; + +const items: Item[] = [ + { + label: 'Первый', + id: 1, + }, + { + label: 'Второй', + id: 2, + }, + { + label: 'Третий', + id: 3, + }, +]; + +const Icon = withAnimateSwitcherHOC({ + startIcon: IconArrowDown, + startDirection: 0, + endDirection: 180, +}); + +export const AutoCompleteExampleDropdownOpen = () => { + const [value, setValue] = useState(); + const [open, setOpen] = useFlag(); + const buttonRef = useRef(null); + const inputRef = useRef(null); + const onDropdownOpen = useCallback((open: boolean) => { + setOpen.set(open); + if (open) { + inputRef.current?.focus(); + } + }, []); + + return ( + + +