/
sdds
/
plasma
Обзор
Документация
Войти
/
sdds
/
plasma
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
dev
website/sdds-dfa-docs/docs/components/Select.mdx
1 031 строка
39 KB
Dima Shugaev
feat(): refactoring Select
28 апр 2026, 13:41
28 апр 2026, 13:41
e7b69a5
Код
Авторство
О чём код?
--- id: select title: Select --- import { PropsTable, Description } from '@site/src/components'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Select <Description name="Select" /> <PropsTable name="Select" /> ## Использование Обязательным параметром является только `items`. Для controlled-варианта передавайте `value` и `onChange`, для интеграции с формой используйте `name` и при необходимости `defaultValue`. Множественный выбор включается через `multiselect`. В single-режиме `value` имеет тип `string`, в multiselect-режиме `string[]`. В single-режиме можно использовать `mode="radio"`, если нужно запретить снятие выбора повторным кликом. В multiselect-режиме доступны `selectAllOptions` и `isTargetAmount`. Select поддерживает два вида target-элемента: `textfield-like` по умолчанию и `button-like`. Для кастомизации доступны: - `renderValue` для текста выбранного значения; - `renderTarget` для полного переопределения target; - `renderItem` и `renderSelectionIcon` для отображения элементов списка; - `beforeList`, `afterList` и `emptyStateDescription` для содержимого выпадающего списка. Внутри `items` можно передавать вложенные элементы. Базовый формат: ```tsx type Items = Array<{ /** * Значение item. */ value: string; /** * Метка-подпись к item. */ label: string; /** * Сторона открытия вложенного списка относительно текущего элемента. * @default right */ placement?: SelectPlacement | Array<SelectPlacementBasic>; /** * Список дочерних items. */ items?: Array<ItemOption>; /** * Item не активен. */ disabled?: boolean; /** * Слот для контента слева. */ contentLeft?: ReactNode; /** * Слот для контента справа. */ contentRight?: ReactNode; /** * Classname для item. */ className?: string; /** * Максимальная высота дочернего выпадающего списка. */ listMaxHeight?: CSSProperties['height']; }>; ``` ## Примеры <Tabs> <TabItem value="textfield" label="Textfield" default> В режиме `textfield-like` target внешне мимикрирует под компонент <b>TextField</b> и наследует все его свойства. ```tsx live import React from 'react'; import { Select } from '@salutejs/sdds-dfa'; export function App() { const [singleValue, setSingleValue] = useState(''); const [multipleValue, setMultipleValue] = useState([]); const items = [ { value: 'north_america', label: 'Северная Америка', }, { value: 'south_america', label: 'Южная Америка', items: [ { value: 'brazil', label: 'Бразилия', }, { value: 'argentina', label: 'Аргентина', }, ], }, ]; return ( <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', maxWidth: '350px', height: '300px'}}> <Select items={items} value={singleValue} onChange={setSingleValue} label="Single" placeholder="Placeholder" helperText="Helper text" /> <Select multiselect items={items} value={multipleValue} onChange={setMultipleValue} label="Multiselect" placeholder="Placeholder" helperText="Helper text" /> </div> ); } ``` </TabItem> <TabItem value="button" label="Button"> В режиме `button-like` target внешне мимикрирует под компонент <b>Button</b>. Свойства из `TextField`, например `contentLeft`, `contentRight`, `helperText`, `labelPlacement`, `keepPlaceholder`, `chipType` и `chipClickArea` и тд., в этом режиме не применяются. ```tsx live import React from 'react'; import { Select } from '@salutejs/sdds-dfa'; export function App() { const [singleValue, setSingleValue] = useState(''); const [multipleValue, setMultipleValue] = useState([]); const items = [ { value: 'north_america', label: 'Северная Америка', }, { value: 'south_america', label: 'Южная Америка', items: [ { value: 'brazil', label: 'Бразилия', }, { value: 'argentina', label: 'Аргентина', }, ], }, ]; return ( <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', maxWidth: '350px', height: '300px' }}> <Select items={items} label="Single" target="button-like" value={singleValue} onChange={setSingleValue} /> <Select multiselect items={items} label="Multiple" target="button-like" value={multipleValue} onChange={setMultipleValue} /> </div> ); } ``` </TabItem> <TabItem value="predefined" label="Predefined"> Есть возможность задать значения по дефолту (главное, чтобы они находились в `items`). Также можно управлять состоянием снаружи. ```tsx live import React from 'react'; import { Select, Button } from '@salutejs/sdds-dfa'; export function App() { const [multipleValue, setMultipleValue] = useState(['brazil', 'north_america']); const items = [ { value: 'north_america', label: 'Северная Америка', }, { value: 'south_america', label: 'Южная Америка', items: [ { value: 'brazil', label: 'Бразилия', }, { value: 'argentina', label: 'Аргентина', }, ], }, ]; return ( <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', maxWidth: '350px', height: '300px' }}> <Button onClick={() => setMultipleValue([])}>Очистить</Button> <Select multiselect items={items} placeholder="Placeholder" value={multipleValue} onChange={setMultipleValue} /> </div> ); } ``` </TabItem> <TabItem value="renderValue" label="Render value"> Пропс `renderValue` переопределяет текст выбранного значения внутри target. Коллбэк получает один аргумент: выбранный `item`. ```tsx live import React from 'react'; import { Select } from '@salutejs/sdds-dfa'; export function App() { const [singleValue, setSingleValue] = useState(''); const items = [ { value: 'north_america', label: 'Северная Америка', }, { value: 'south_america', label: 'Южная Америка', items: [ { value: 'brazil', label: 'Бразилия', }, { value: 'argentina', label: 'Аргентина', }, ], }, ]; const renderValue = (item) => `${item.value} - ${item.label}`; return ( <div style={{ display: 'flex', flexDirection: 'column', maxWidth: '350px', height: '300px' }}> <Select items={items} label="Label" placeholder="Placeholder" value={singleValue} onChange={setSingleValue} renderValue={renderValue} /> </div> ); } ``` </TabItem> <TabItem value="renderItem" label="Render item"> Пропс `renderItem` отвечает за кастомный рендер элемента списка и получает весь объект `item`. В примере использован другой наш компонент - Cell. ```tsx live import React from 'react'; import { Select, Cell } from '@salutejs/sdds-dfa'; export function App() { const [multipleValue, setMultipleValue] = useState([]); const items = [ { value: 'north_america', label: 'Северная Америка', }, { value: 'south_america', label: 'Южная Америка', }, ]; const renderItem = ({ value, label }) => ( <Cell view="default" title={label} label="Top left" contentRight={<Cell view="default" title="Bottom right" label="Top right" />} /> ) return ( <div style={{ display: 'flex', flexDirection: 'column', maxWidth: '350px', height: '300px' }}> <Select placeholder="Placeholder" items={items} value={multipleValue} onChange={setMultipleValue} multiselect renderItem={renderItem} /> </div> ); } ``` </TabItem> <TabItem value="renderSelectionIcon" label="Render icon"> `renderSelectionIcon` кастомизирует иконку выбранного состояния элемента. Коллбэк получает `true`, `false` или `'indeterminate'`. ```tsx live import React from 'react'; import { Select } from '@salutejs/sdds-dfa'; export function App() { const [value, setValue] = useState('brazil'); const items = [ { value: 'north_america', label: 'Северная Америка', }, { value: 'south_america', label: 'Южная Америка', items: [ { value: 'brazil', label: 'Бразилия', }, { value: 'argentina', label: 'Аргентина', }, ], }, ]; const renderSelectionIcon = (selected: boolean | 'indeterminate') => { if (selected === true) { return <div style={{ width: '10px', height: '10px', borderRadius: '100%', background: 'red' }} />; } if (selected === 'indeterminate') { return <div style={{ width: '10px', height: '10px', background: 'blue' }} />; } return null; }; return ( <div style={{ display: 'flex', flexDirection: 'column', maxWidth: '350px', height: '300px' }}> <Select items={items} value={value} onChange={setValue} renderSelectionIcon={renderSelectionIcon} /> </div> ); } ``` </TabItem> <TabItem value="portal" label="Portal"> Иногда возникает необходимость вынесения выпадающего списка на уровни выше в DOM. К примеру, когда у родительского блока имеется скролл, и список будет обрезаться, чего в большинстве случаев хотелось бы избежать.\ Для такой реализации имеется пропс `portal`, который принимает либо `ref` либо `id` html-тега.\ Также нужно прокинуть проп `listWidth`, чтобы явно задать ширину списку. Если этого не сделать, то будет взята ширина 100% от блока, на который ведет ссылка портала. ```tsx live import React, { useRef } from 'react'; import { Select } from '@salutejs/sdds-dfa'; export function App() { const [value, setValue] = useState(''); const items = [ { value: 'north_america', label: 'Северная Америка', }, { value: 'south_america', label: 'Южная Америка', items: [ { value: 'brazil', label: 'Бразилия', }, { value: 'argentina', label: 'Аргентина', }, ], }, ]; const ref = useRef(null) return ( <div style={{ position: "relative", height: "300px" }} ref={ref}> <div style={{width: '300px'}}> <Select items={items} label="Label" placeholder="Placeholder" value={value} onChange={setValue} portal={ref} /> </div> </div> ); } ``` </TabItem> <TabItem value="uncontrolled" label="Uncontrolled"> `value` и `onChange` опциональны. Если нужен uncontrolled-вариант без интеграции с формой, достаточно передать `items`. ```tsx live import React from 'react'; import { Select } from '@salutejs/sdds-dfa'; export function App() { const items = [ { value: 'north_america', label: 'Северная Америка', }, { value: 'south_america', label: 'Южная Америка', items: [ { value: 'brazil', label: 'Бразилия', }, { value: 'argentina', label: 'Аргентина', }, ], }, ]; return ( <div style={{ display: 'flex', flexDirection: 'column', maxWidth: '350px', height: '300px' }}> <Select items={items} label="Label" placeholder="Placeholder" /> </div> ); } ``` </TabItem> <TabItem value="virtual" label="Virtual"> Свойство `virtual` позволяет виртуализировать выпадающий список. Для настройки высоты списка можно использовать свойство `listMaxHeight`. Работает только в одноуровневых списках. ```tsx live import React, { useState } from 'react'; import { Select } from '@salutejs/sdds-dfa'; export function App() { const items = Array(5000).fill(1).map((_, i) => ({ value: i.toString(), label: i.toString() })); return ( <div style={{ display: 'flex', flexDirection: 'column', maxWidth: '350px', height: '300px' }}> <Select items={items} virtual listMaxHeight="200px" placeholder="Placeholder" label="Label" helperText="Helper text" /> </div> ); } ``` </TabItem> <TabItem value="infinite" label="Infinite Loading"> Пример с бесконечной загрузкой элементов в списке. ```tsx live import React, { useState } from 'react'; import { Select } from '@salutejs/sdds-dfa'; export function App() { function getRandomData() { return Array(10) .fill(1) .map(() => { const n = Math.floor(Math.random() * 90000000) + 10000000; return { value: n.toString(), label: n.toString() }; }); }; const [items, setItems] = useState(getRandomData()); const [isInfiniteLoading, setIsInfiniteLoading] = useState(false); const getData = async () => { return new Promise((resolve) => { setTimeout(() => { resolve(getRandomData()); }, 1500); }); }; const handleScroll = async (e) => { if (isInfiniteLoading) return; if (e.currentTarget.scrollTop + e.currentTarget.offsetHeight + 10 > e.currentTarget.scrollHeight) { setIsInfiniteLoading(true); const res = await getData(); setItems([...items, ...res]); setIsInfiniteLoading(false); } }; return ( <div style={{ display: 'flex', flexDirection: 'column', maxWidth: '350px', height: '300px' }}> <Select items={items} listMaxHeight="200px" placeholder="Placeholder" label="Label" helperText="Helper text" onScroll={handleScroll} afterList={isInfiniteLoading ? <center>Загружаю...</center> : undefined} /> </div> ); } ``` </TabItem> <TabItem value="selectAll" label="Выбрать всё"> Работа с кнопкой "Выбрать всё" осуществляется через свойство `selectAllOptions` только в режиме `multiselect`: ```tsx type SelectAllProps = { checked?: boolean; indeterminate?: boolean; label?: string; onClick?: () => void; sticky?: boolean; }; ``` Вся логика выбора элементов и взаимодействия с компонентом лежит на стороне пользователя. ```tsx live import React, { useState } from 'react'; import { Select } from '@salutejs/sdds-dfa'; export function App() { const flatItems = [ { value: 'north_america', label: 'Северная Америка', }, { value: 'rio_de_janeiro', label: 'Рио-де-Жанейро', }, { value: 'sao_paulo', label: 'Сан-Паулу', }, { value: 'buenos_aires', label: 'Буэнос-Айрес', }, { value: 'cordoba', label: 'Кордова', }, { value: 'bogota', label: 'Богота', }, { value: 'medellin', label: 'Медельин', }, ]; const [value, setValue] = useState([]); const [checked, setChecked] = useState(false); const [indeterminate, setIndeterminate] = useState(false); const handleClick = () => { if (checked && !indeterminate) { setValue([]); } else { setValue(flatItems.map((item) => item.value)); } }; React.useEffect(() => { if (value.length === 0) { setChecked(false); setIndeterminate(false); } else if (value.length === flatItems.length) { setChecked(true); setIndeterminate(false); } else { setChecked(true); setIndeterminate(true); } }, [value]); return ( <div style={{ display: 'flex', flexDirection: 'column', maxWidth: '350px', height: '300px' }}> <Select value={value} onChange={setValue} label="Label" placeholder="Placeholder" items={flatItems} multiselect selectAllOptions={{ checked, indeterminate, onClick: handleClick, }} listMaxHeight="200px" /> </div> ); } ``` </TabItem> <TabItem value="treeView" label="Tree View"> Включение отображения выпадающего списка в виде дерева осуществляется через свойство `treeView`. Для настройки стороны стрелочки открытия/закрытия используется свойство `arrowPlacement`. ```tsx live import React, { useState } from 'react'; import { Select } from '@salutejs/sdds-dfa'; export function App() { const items = [ { value: 'north_america', label: 'Северная Америка', }, { value: 'south_america', label: 'Южная Америка', items: [ { value: 'brazil', label: 'Бразилия', }, { value: 'argentina', label: 'Аргентина', }, ], }, ]; return ( <div style={{ display: 'flex', flexDirection: 'column', maxWidth: '350px', height: '300px' }}> <Select label="Label" placeholder="Placeholder" items={items} treeView arrowPlacement="right" /> </div> ); } ``` </TabItem> </Tabs> ## Взаимодействие с disabled-элементами Изнутри компонента взаимодействие с disabled-элементами **невозможно**. Ниже представлены примеры с `selected` и `unselected` disabled-элементом. ```tsx live import React, { useState } from 'react'; import { Select } from '@salutejs/sdds-dfa'; export function App() { const items = [ { value: 'north_america', label: 'Северная Америка', }, { value: 'south_america', label: 'Южная Америка', items: [ { value: 'brazil', label: 'Бразилия', disabled: true, }, { value: 'argentina', label: 'Аргентина', }, ], }, ]; const [value, setValue] = useState(['brazil']); return ( <div style={{ display: 'flex', flexDirection: 'column', maxWidth: '350px', height: '300px' }}> <Select multiselect label="Label" placeholder="Placeholder" items={items} /> <Select multiselect label="Label" placeholder="Placeholder" items={items} value={value} onChange={setValue} isTargetAmount /> </div> ); } ``` ## Использование с React Hook Form и нативной формой :::caution Использование атрибута `name` Используйте свойство `name` только когда это необходимо. Оно влияет на выходной тип в `onChange`. ::: <Tabs> <TabItem value="default" label="Default" default> Работа с `react-hook-form` через `register`. ```tsx live import React from 'react'; import { useForm, SubmitHandler } from 'react-hook-form'; import { Select, Button } from '@salutejs/sdds-dfa'; export function App() { type Inputs = { select: string, selectMulti: string[] } const { register, handleSubmit } = useForm<Inputs>({ defaultValues: { select: 'north_america', selectMulti: ['brazil'] } }); const onSubmit: SubmitHandler<Inputs> = (data) => { console.log(data); }; const items = [ { value: 'north_america', label: 'Северная Америка', }, { value: 'south_america', label: 'Южная Америка', items: [ { value: 'brazil', label: 'Бразилия', }, { value: 'argentina', label: 'Аргентина', }, ], }, ]; return ( <form onSubmit={handleSubmit(onSubmit)} style={{ display: 'flex', flexDirection: 'column', gap: '10px', maxWidth: '350px', height: '300px' }}> <Select placeholder="Placeholder" items={items} {...register('select')} /> <Select placeholder="Placeholder" items={items} {...register('selectMulti')} multiselect /> <Button type="submit">Отправить</Button> </form> ); } ``` </TabItem> <TabItem value="controller" label="Controller"> Работа с `react-hook-form` через `controller`. ```tsx live import React from 'react'; import { useForm, Controller, SubmitHandler } from 'react-hook-form'; import { Select, Button } from '@salutejs/sdds-dfa'; export function App() { type Inputs = { select: string, selectMulti: string[] } const { control, handleSubmit } = useForm<Inputs>({ defaultValues: { select: 'north_america', selectMulti: ['brazil'] } }); const onSubmit: SubmitHandler<Inputs> = (data) => { console.log(data); }; const items = [ { value: 'north_america', label: 'Северная Америка', }, { value: 'south_america', label: 'Южная Америка', items: [ { value: 'brazil', label: 'Бразилия', }, { value: 'argentina', label: 'Аргентина', }, ], }, ]; return ( <form onSubmit={handleSubmit(onSubmit)} style={{ display: 'flex', flexDirection: 'column', gap: '10px', maxWidth: '350px', height: '300px' }}> <Controller control={control} name="select" render={({ field: { onChange, value, ref } }) => ( <Select items={items} ref={ref} value={value} onChange={onChange} /> )} /> <Controller control={control} name="selectMulti" render={({ field: { onChange, value, ref } }) => ( <Select items={items} ref={ref} value={value} onChange={onChange} multiselect /> )} /> <Button type="submit">Отправить</Button> </form> ); } ``` </TabItem> <TabItem value="native" label="Native Form"> Работа с нативной формой. ```tsx live import React from 'react'; import { Select, Button } from '@salutejs/sdds-dfa'; export function App() { const onSubmit = (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); const formData = new FormData(event.currentTarget); for (const [name, value] of formData) { console.log(name, value); } }; const items = [ { value: 'north_america', label: 'Северная Америка', }, { value: 'south_america', label: 'Южная Америка', items: [ { value: 'brazil', label: 'Бразилия', }, { value: 'argentina', label: 'Аргентина', }, ], }, ]; return ( <form onSubmit={onSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '10px', maxWidth: '350px', height: '300px' }}> <Select name="select" defaultValue="brazil" items={items} /> <Select name="selectMulti" defaultValue={['brazil']} items={items} multiselect /> <Button type="submit">Отправить</Button> </form> ); } ``` </TabItem> </Tabs> ## Поиск совпадений с функцией подсветки символов :::tip `Combobox` - уже имеет механизм поиска и фильтрацию из коробки. ::: Данный пример показывает реализацию подсветки (<b>highlighting</b>) символов в найденных элементах в режиме `treeView`. ```tsx live import React, { useState } from 'react'; import { Select, TextField } from '@salutejs/sdds-dfa'; import { IconCloseCircleOutline } from '@salutejs/plasma-icons'; export function App() { const items = [ { value: 'beijing', label: 'Пекин', }, { value: 'shanghai', label: 'Шанхай', }, { value: 'tokyo', label: 'Токио', }, { value: 'osaka', label: 'Осака', }, { value: 'delhi', label: 'Дели', }, { value: 'mumbai', label: 'Мумбаи', }, { value: 'seoul', label: 'Сеул', }, { value: 'busan', label: 'Пусан', }, { value: 'bangkok', label: 'Бангкок', }, { value: 'phuket', label: 'Пхукет', }, ]; const [textValue, setTextValue] = useState(''); function highlightText(label: string, highlight: string): React.ReactNode { if (!highlight.trim()) return label; const re = new RegExp('(' + highlight + ')', 'ig'); const parts = label.split(re); return ( <> {parts.map((part, i) => part.toLowerCase() === highlight.toLowerCase() ? ( <mark key={i} style={{ backgroundColor: '#ffeb3b' }}> {part} </mark> ) : ( <span key={i}>{part}</span> ), )} </> ); } const renderItem = (item) => { if (!textValue) return item.label; return highlightText(item.label, textValue); }; return ( <div style={{ display: 'flex', height: '400px' }}> <div style={{ width: '350px' }}> <Select multiselect variant="tight" placeholder="Placeholder" items={items} listMaxHeight="300px" renderItem={renderItem} onToggle={(open) => { if (open) { setTextValue(""); } }} beforeList={<TextField placeholder="Введите 'Токио'" size="s" value={textValue} onChange={(e) => setTextValue(e.target.value)} contentRight={textValue && <IconCloseCircleOutline onClick={() => setTextValue("")} color="gray" size="xs" />} />} /> </div> </div> ); } ``` ## Клавиатурная навигация Данный компонент соответствует требования W3C: [Select](https://www.w3.org/WAI/ARIA/apg/patterns/Select/) и частично [TreeView](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/). - `Tab` - закрывает дропдаун. Перемещает фокус на следующий элемент на странице; - `Enter` - открывает/закрывает дропдаун. Если на элементе - выбирает его. Если у элемента есть дочерний дропдаун - открывает его, выбор элемента не происходит; - `Space` - открывает/закрывает дропдаун. Если на элементе - выбирает его и все дочерние элементы. - `Home` - открывает дропдаун и перемещает фокус на первый элемент; - `End` - открывает дропдаун и перемещает фокус на последний элемент; - `PageUp` - перемещает фокус на 10 элементов выше либо в начало списка; - `PageDown` - перемещает фокус на 10 элементов ниже либо в конце списка; - `ArrowUp` - открывает дропдаун и перемещает фокус на первый элемент. Перемещает фокус на один элемент выше; - `ArrowDown` - открывает дропдаун и перемещает фокус на первый элемент. Перемещает фокус на один элемент ниже; - `ArrowRight` - если фокус на элементе вложенного списка - открывает его и перемещает фокус на первый элемент; Если фокус на таргете - переходит в режим выбора чипа. - `ArrowLeft` - закрывает текущий список и перемещает фокус на предыдущий; Если фокус на таргете - переходит в режим выбора чипа. - `Backspace` - только если фокус на чипе - снимает выбор с текущего значения;