
⌘K - это компонент реагирования команд, который также может использоваться в качестве доступного ComboBox. Вы рендерируете предметы, он фильтрует и сортирует их автоматически. ⌘K поддерживает полностью композиционный API как? Таким образом, вы можете обернуть предметы в другие компоненты или даже в качестве статического JSX.
Демо и примеры: cmdk.paco.me
pnpm install cmdk import { Command } from 'cmdk'
const CommandMenu = ( ) => {
return (
< Command label = "Command Menu" >
< Command . Input / >
< Command . List >
< Command . Empty > No results found. < / Command . Empty >
< Command . Group heading = "Letters" >
< Command . Item > a < / Command . Item >
< Command . Item > b < / Command . Item >
< Command . Separator / >
< Command . Item > c < / Command . Item >
< / Command . Group >
< Command . Item > Apple < / Command . Item >
< / Command . List >
< / Command >
)
}Или в диалоге:
import { Command } from 'cmdk'
const CommandMenu = ( ) => {
const [ open , setOpen ] = React . useState ( false )
// Toggle the menu when ⌘K is pressed
React . useEffect ( ( ) => {
const down = ( e ) => {
if ( e . key === 'k' && ( e . metaKey || e . ctrlKey ) ) {
e . preventDefault ( )
setOpen ( ( open ) => ! open )
}
}
document . addEventListener ( 'keydown' , down )
return ( ) => document . removeEventListener ( 'keydown' , down )
} , [ ] )
return (
< Command . Dialog open = { open } onOpenChange = { setOpen } label = "Global Command Menu" >
< Command . Input / >
< Command . List >
< Command . Empty > No results found. < / Command . Empty >
< Command . Group heading = "Letters" >
< Command . Item > a < / Command . Item >
< Command . Item > b < / Command . Item >
< Command . Separator / >
< Command . Item > c < / Command . Item >
< / Command . Group >
< Command . Item > Apple < / Command . Item >
< / Command . List >
< / Command . Dialog >
)
} Все детали вперед, в том числе ref , к соответствующему элементу. Каждая часть имеет конкретный атрибут данных (начиная с cmdk- ), которые можно использовать для стиля.
[cmdk-root] Отправить это, чтобы показать встроенный меню команд, или использовать диалог для отображения в повышенном контексте. Можно контролировать со value и реквизитами onValueChange .
Примечание
Значения всегда обрезаны методом TRIM ().
const [ value , setValue ] = React . useState ( 'apple' )
return (
< Command value = { value } onValueChange = { setValue } >
< Command . Input / >
< Command . List >
< Command . Item > Orange < / Command . Item >
< Command . Item > Apple < / Command . Item >
< / Command . List >
< / Command >
) Вы можете предоставить пользовательскую функцию filter , вызванную для ранжирования каждого элемента. Обратите внимание, что значение будет обрезано.
< Command
filter = { ( value , search ) => {
if ( value . includes ( search ) ) return 1
return 0
} }
/ > Третий аргумент, keywords , также может быть предоставлен функции фильтра. Ключевые слова действуют как псевдонимы для значения элемента, а также могут повлиять на ранг элемента. Ключевые слова обрезаны.
< Command
filter = { ( value , search , keywords ) => {
const extendValue = value + ' ' + keywords . join ( ' ' )
if ( extendValue . includes ( search ) ) return 1
return 0
} }
/ >Или полностью отключить фильтрацию и сортировку:
< Command shouldFilter = { false } >
< Command . List >
{ filteredItems . map ( ( item ) => {
return (
< Command . Item key = { item } value = { item } >
{ item }
< / Command . Item >
)
} ) }
< / Command . List >
< / Command > Вы можете заставить клавиши со стрелками обернуться вокруг списка (когда вы достигнете конца, он возвращается к первому элементу), установив loop Prop:
< Command loop / >[cmdk-dialog] [cmdk-overlay] Реквизит отправляется в команду. Составляет диалоговый компонент Radix UI. Наложение всегда отображается. Смотрите документацию Radix для получения дополнительной информации. Можно контролировать с помощью open и onOpenChange реквизита.
const [ open , setOpen ] = React . useState ( false )
return (
< Command . Dialog open = { open } onOpenChange = { setOpen } >
...
< / Command . Dialog >
) Вы можете предоставить container опору, которая принимает элемент HTML, который направляется в диалоговый компонент Radix UI, чтобы указать, какой элемент диалог должен портал (по умолчанию в body ). Смотрите документацию Radix для получения дополнительной информации.
const containerElement = React . useRef ( null )
return (
< >
< Command . Dialog container = { containerElement . current } / >
< div ref = { containerElement } / >
< / >
)[cmdk-input] Все реквизиты направляются в базовый input элемент. Можно контролировать со value и реквизитами onValueChange .
const [ search , setSearch ] = React . useState ( '' )
return < Command . Input value = { search } onValueChange = { setSearch } / >[cmdk-list] Содержит предметы и группы. Аниматическая высота с использованием переменной CSS- --cmdk-list-height .
[ cmdk-list ] {
min-height : 300 px ;
height : var ( --cmdk-list-height );
max-height : 500 px ;
transition : height 100 ms ease;
}Чтобы прокрутить элемент, который можно увидеть ранее рядом с краями просмотра, используйте прокрутки:
[ cmdk-list ] {
scroll-padding-block-start : 8 px ;
scroll-padding-block-end : 8 px ;
}[cmdk-item] [data-disabled?] [data-selected?] Элемент, который становится активным на указателе. Вы должны предоставить уникальное value для каждого элемента, но он будет автоматически выведен из .textContent .
< Command . Item
onSelect = { ( value ) => console . log ( 'Selected' , value ) }
// Value is implicity "apple" because of the provided text content
>
Apple
< / Command . Item > Вы также можете предоставить keywords поддержку, чтобы помочь в фильтрации. Ключевые слова обрезаны.
< Command . Item keywords = { [ 'fruit' , 'apple' ] } > Apple < / Command . Item > < Command . Item
onSelect = { ( value ) => console . log ( 'Selected' , value ) }
// Value is implicity "apple" because of the provided text content
>
Apple
< / Command . Item > Вы можете заставить предмет всегда отображаться, независимо от фильтрации, пройдя forceMount .
[cmdk-group] [hidden?] Группировки элементы вместе с данным heading ( [cmdk-group-heading] ).
< Command . Group heading = "Fruit" >
< Command . Item > Apple < / Command . Item >
< / Command . Group > Группы не будут отступать от DOM, скорее hidden атрибут применяется, чтобы скрыть его от взгляда. Это может быть актуально в вашем стиле.
Вы можете заставить группу всегда отображаться, независимо от фильтрации, проходя forceMount .
[cmdk-separator] Видимо, когда поисковый запрос пуст или alwaysRender верно, иначе скрыт.
[cmdk-empty]Автоматически рендерирует, когда нет результатов для поискового запроса.
[cmdk-loading] Вы должны условно отобразить это с progress при загрузке асинхронных элементов.
const [ loading , setLoading ] = React . useState ( false )
return < Command . List > { loading && < Command . Loading > Hang on… < / Command . Loading > } < / Command . List >useCommandState(state => state.selectedField) Крюк, который составляет, useSyncExternalStore . Пропустите функцию, которая возвращает ломтик состояния меню команд, чтобы повторно рендеринг при изменении этого среза. Этот крюк предоставляется для расширенного вариантов использования и не должен использоваться.
Хорошим вариантом использования будет то, чтобы сделать более подробное пустое состояние, например, так:
const search = useCommandState ( ( state ) => state . search )
return < Command . Empty > No results found for " { search } ". < / Command . Empty > Кодовые фрагменты для общих вариантов использования.
Часто выбор одного предмета должен перемещаться глубже, с более изысканным набором элементов. Например, выбрать «Изменение темы…» должен показать новые элементы «Темная тема» и «Светлая тема». Мы называем эти наборы элементов «страницы», и они могут быть реализованы с помощью простого состояния:
const ref = React . useRef ( null )
const [ open , setOpen ] = React . useState ( false )
const [ search , setSearch ] = React . useState ( '' )
const [ pages , setPages ] = React . useState ( [ ] )
const page = pages [ pages . length - 1 ]
return (
< Command
onKeyDown = { ( e ) => {
// Escape goes to previous page
// Backspace goes to previous page when search is empty
if ( e . key === 'Escape' || ( e . key === 'Backspace' && ! search ) ) {
e . preventDefault ( )
setPages ( ( pages ) => pages . slice ( 0 , - 1 ) )
}
} }
>
< Command . Input value = { search } onValueChange = { setSearch } / >
< Command . List >
{ ! page && (
< >
< Command . Item onSelect = { ( ) => setPages ( [ ... pages , 'projects' ] ) } > Search projects… < / Command . Item >
< Command . Item onSelect = { ( ) => setPages ( [ ... pages , 'teams' ] ) } > Join a team… < / Command . Item >
< / >
) }
{ page === 'projects' && (
< >
< Command . Item > Project A < / Command . Item >
< Command . Item > Project B < / Command . Item >
< / >
) }
{ page === 'teams' && (
< >
< Command . Item > Team 1 < / Command . Item >
< Command . Item > Team 2 < / Command . Item >
< / >
) }
< / Command . List >
< / Command >
)Если ваши предметы имеют вложенные подразделения, которые вы хотите раскрыть только при поиске, рендеринг на основе состояния поиска:
const SubItem = ( props ) => {
const search = useCommandState ( ( state ) => state . search )
if ( ! search ) return null
return < Command . Item { ... props } / >
}
return (
< Command >
< Command . Input / >
< Command . List >
< Command . Item > Change theme… < / Command . Item >
< SubItem > Change theme to dark < / SubItem >
< SubItem > Change theme to light < / SubItem >
< / Command . List >
< / Command >
)Отправить предметы по мере того, как они становятся доступными. Фильтрация и сортировка произойдет автоматически.
const [ loading , setLoading ] = React . useState ( false )
const [ items , setItems ] = React . useState ( [ ] )
React . useEffect ( ( ) => {
async function getItems ( ) {
setLoading ( true )
const res = await api . get ( '/dictionary' )
setItems ( res )
setLoading ( false )
}
getItems ( )
} , [ ] )
return (
< Command >
< Command . Input / >
< Command . List >
{ loading && < Command . Loading > Fetching words… < / Command . Loading > }
{ items . map ( ( item ) => {
return (
< Command . Item key = { `word- ${ item } ` } value = { item } >
{ item }
< / Command . Item >
)
} ) }
< / Command . List >
< / Command >
)Мы рекомендуем использовать компонент Radix UI Popover. ⌘K полагается на компонент диалога пользовательского интерфейса Radix, так что это немного уменьшит размер пакета из -за общих зависимостей.
$ pnpm install @radix-ui/react-popover Рендеринг Command внутри попверного контента:
import * as Popover from '@radix-ui/react-popover'
return (
< Popover . Root >
< Popover . Trigger > Toggle popover < / Popover . Trigger >
< Popover . Content >
< Command >
< Command . Input / >
< Command . List >
< Command . Item > Apple < / Command . Item >
< / Command . List >
< / Command >
< / Popover . Content >
< / Popover . Root >
)Вы можете найти глобальные таблицы стилей, которые можно зайти в качестве отправной точки для стиля. Смотрите веб -сайт/стили/cmdk для примеров.
Доступный? Да. Маркировка, атрибуты ARIA и заказ DOM протестированы с помощью голоса и Chrome Devtools. Диалог составляет доступную реализацию диалога.
Виртуализация? Нет. Хорошая производительность до 2000-3000 предметов, хотя. Читайте ниже, чтобы принести свой собственный.
Фильтруя/сортировку предметов вручную? Да. Pass shouldFilter={false} Команду. Лучшее использование памяти и производительность. Принесите свою собственную виртуализацию таким образом.
Отреагировать 18 безопасно? Да, обязательно. Использует React 18 крючков, таких как useId и useSyncExternalStore .
Без неотложного? Да, используйте перечисленные селекторы CSS.
Несоответствие гидратации? Нет, вероятно, ошибка в вашем коде. Убедитесь, что open Prop to Command.Dialog является false на сервере.
Реагировать на строгий режим в безопасности? Да. Откройте проблему, если вы заметите проблему.
Странное/неправильное поведение? Убедитесь, что ваша Command.Item имеет key и уникальное value .
Одновременный режим в безопасности? Может быть, но одновременный режим еще не настоящий. Использует рискованные подходы, такие как ручной заказ DOM.
React Server Component? Нет, это клиентский компонент.
Слушайте ⌘k автоматически? Нет, сделайте это самостоятельно, чтобы иметь полный контроль над контекстом Keybind.
Отреагировать родным? Нет, и нет планов поддержать это. Если вы создадите нативную версию React, дайте нам знать, и мы свяжем ваш репозиторий здесь.
Написано в 2019 году Paco (@pacocoursy), чтобы увидеть, был ли возможен композиционный API Combobox. Используется для меню команды Vercel и автозаполнения Рауно (@raunofreiberg) в 2020 году. Переписывается независимо в 2022 году с более простым и более эффективным подходом. Идеи и помощь от Shu (@shuding_).
Последователи использования были извлечены из версии 2019 года.
Во -первых, установите зависимости и пилятельные браузеры:
pnpm install
pnpm playwright installЗатем убедитесь, что вы построили библиотеку:
pnpm buildЗатем запустите тесты, используя локальную сборку против реальных двигателей браузера:
pnpm test