
⌘K es un componente React de menú de comando que también se puede utilizar como un Combobox accesible. Usted representa elementos, los filtra y los clasifica automáticamente. ⌘K admite una API totalmente compuesta ¿Cómo? , por lo que puede envolver elementos en otros componentes o incluso como JSX estático.
Demostración y ejemplos: 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 >
)
}O en un diálogo:
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 >
)
} Todas las piezas se reenvían a los accesorios, incluida ref , a un elemento apropiado. Cada parte tiene un atributo de datos específico (comenzando con cmdk- ) que puede usarse para el estilo.
[cmdk-root] Renderizar esto para mostrar el menú de comando en línea, o usar el diálogo para renderizar en un contexto elevado. Se puede controlar con los accesorios value y onValueChange .
Nota
Los valores siempre se recortan con el método 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 >
) Puede proporcionar una función filter personalizada que se llama para clasificar cada elemento. Tenga en cuenta que el valor será recortado.
< Command
filter = { ( value , search ) => {
if ( value . includes ( search ) ) return 1
return 0
} }
/ > También se puede proporcionar un tercer argumento, keywords , a la función de filtro. Las palabras clave actúan como alias para el valor del elemento, y también pueden afectar el rango del artículo. Las palabras clave se recortan.
< Command
filter = { ( value , search , keywords ) => {
const extendValue = value + ' ' + keywords . join ( ' ' )
if ( extendValue . includes ( search ) ) return 1
return 0
} }
/ >O deshabilitar el filtrado y la clasificación por completo:
< Command shouldFilter = { false } >
< Command . List >
{ filteredItems . map ( ( item ) => {
return (
< Command . Item key = { item } value = { item } >
{ item }
< / Command . Item >
)
} ) }
< / Command . List >
< / Command > Puede hacer que las teclas de flecha envuelvan alrededor de la lista (cuando llegue al final, se remonta al primer elemento) configurando el accesorio loop :
< Command loop / >[cmdk-dialog] [cmdk-overlay] Los accesorios se reenvían al comando. Compone el componente de diálogo de Radix UI. La superposición siempre se representa. Consulte la documentación de Radix para obtener más información. Se puede controlar con los accesorios open y onOpenChange .
const [ open , setOpen ] = React . useState ( false )
return (
< Command . Dialog open = { open } onOpenChange = { setOpen } >
...
< / Command . Dialog >
) Puede proporcionar un accesorio container que acepte un elemento HTML que se reenvía al componente del portal de diálogo de Radix UI para especificar en qué elemento debe el diálogo en el portal (predeterminado a body ). Consulte la documentación de Radix para obtener más información.
const containerElement = React . useRef ( null )
return (
< >
< Command . Dialog container = { containerElement . current } / >
< div ref = { containerElement } / >
< / >
)[cmdk-input] Todos los accesorios se reenvían al elemento input subyacente. Se puede controlar con los accesorios value y onValueChange .
const [ search , setSearch ] = React . useState ( '' )
return < Command . Input value = { search } onValueChange = { setSearch } / >[cmdk-list] Contiene elementos y grupos. Animate altura utilizando la variable CSS --cmdk-list-height .
[ cmdk-list ] {
min-height : 300 px ;
height : var ( --cmdk-list-height );
max-height : 500 px ;
transition : height 100 ms ease;
}Para desplazarse a la vista anteriormente cerca de los bordes de la ventana gráfica, use el toque de desplazamiento:
[ cmdk-list ] {
scroll-padding-block-start : 8 px ;
scroll-padding-block-end : 8 px ;
}[cmdk-item] [data-disabled?] [data-selected?] Elemento que se activa en el puntero enter. Debe proporcionar un value único para cada elemento, pero se inferirá automáticamente del .textContent .
< Command . Item
onSelect = { ( value ) => console . log ( 'Selected' , value ) }
// Value is implicity "apple" because of the provided text content
>
Apple
< / Command . Item > También puede proporcionar un accesorio keywords para ayudar con el filtrado. Las palabras clave se recortan.
< 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 > Puede obligar a un elemento a renderizar siempre, independientemente del filtrado, pasando el accesorio forceMount .
[cmdk-group] [hidden?] Agrupe elementos junto con el heading dado ( [cmdk-group-heading] ).
< Command . Group heading = "Fruit" >
< Command . Item > Apple < / Command . Item >
< / Command . Group > Los grupos no desmontarán del DOM, sino que el atributo hidden se aplica para ocultarlo de la vista. Esto puede ser relevante en su estilo.
Puede obligar a un grupo a siempre renderizar, independientemente del filtrado, pasando el apoyo forceMount .
[cmdk-separator] Visible cuando la consulta de búsqueda está vacía o alwaysRender es verdadera, oculta de otra manera.
[cmdk-empty]Renderiza automáticamente cuando no hay resultados para la consulta de búsqueda.
[cmdk-loading] Debe renderizar esto condicionalmente con progress al cargar elementos asíncronos.
const [ loading , setLoading ] = React . useState ( false )
return < Command . List > { loading && < Command . Loading > Hang on… < / Command . Loading > } < / Command . List >useCommandState(state => state.selectedField) Gancho que compone useSyncExternalStore . Pase una función que devuelve una porción del estado del menú de comando para volver a renderizar cuando cambia ese corte. Este gancho se proporciona para casos de uso avanzados y no debe usarse comúnmente.
Un buen caso de uso sería representar un estado vacío más detallado, como así:
const search = useCommandState ( ( state ) => state . search )
return < Command . Empty > No results found for " { search } ". < / Command . Empty > Fragmentos de código para casos de uso comunes.
A menudo, seleccionar un elemento debe navegar más profundo, con un conjunto más refinado de elementos. Por ejemplo, seleccionar "Cambiar tema ..." Debería mostrar nuevos elementos "Tema oscuro" y "Tema Light". Llamamos a estos conjuntos de elementos "páginas", y pueden implementarse con un estado simple:
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 >
)Si sus artículos tienen sub-ítems anidados que solo desea revelar cuando busca, renderizar según el estado de búsqueda:
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 >
)Renderiza los artículos a medida que estén disponibles. El filtrado y la clasificación ocurrirán automáticamente.
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 >
)Recomendamos usar el componente Radix UI Popover. ⌘K se basa en el componente de diálogo Radix UI, por lo que esto reducirá un poco el tamaño de su paquete debido a las dependencias compartidas.
$ pnpm install @radix-ui/react-popover Render Command dentro del contenido de Popover:
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 >
)Puede encontrar hojas de estilo global para que se encuentren como punto de partida para el estilo. Consulte el sitio web/estilos/cmdk para ver ejemplos.
¿Accesible? Sí. El etiquetado, los atributos de aria y el orden de DOM probados con voz y cromo devitools. El diálogo compone una implementación de diálogo accesible.
Virtualización? Sin embargo, un buen rendimiento hasta 2,000-3,000 artículos. Lea a continuación para traer el suyo.
Filtrar/clasificar elementos manualmente? Sí. Pase shouldFilter={false} para comandar. Mejor uso de memoria y rendimiento. Trae tu propia virtualización de esta manera.
Reaccionar 18 seguro? Sí, requerido. Utiliza React 18 Hooks como useId y useSyncExternalStore .
¿Sin estilos? Sí, use los selectores CSS enumerados.
¿Deseginimiento de hidratación? No, probablemente un error en su código. Asegúrese de que open Prop to Command.Dialog sea false en el servidor.
Reaccionar el modo estricto seguro? Sí. Abra un problema si nota un problema.
¿Comportamiento extraño/incorrecto? Asegúrese de que su Command.Item tenga un value key y único.
¿Modo concurrente seguro? Tal vez, pero el modo concurrente aún no es real. Utiliza enfoques riesgosos como el pedido manual de DOM.
React Servidor Componente? No, es un componente del cliente.
Escuchar ⌘k automáticamente? No, hazlo tú mismo para tener un control total sobre el contexto de KeyBind.
Reaccionar nativo? No, y no hay planes para apoyarlo. Si crea una versión nativa de React, háganoslo saber y vincularemos su repositorio aquí.
Escrito en 2019 por PACO (@PaCoCoursey) para ver si era posible una API ComboBox compuesta. Utilizado para el menú de comando Vercely y autocompletado por Rauno (@RaunoFreiberg) en 2020. Reescritado independientemente en 2022 con un enfoque más simple y más performador. Ideas y ayuda de shu (@shuding_).
Los descendientes de uso se extrajeron de la versión 2019.
Primero, instale dependencias y navegadores de dramaturgos:
pnpm install
pnpm playwright installLuego asegúrese de construir la biblioteca:
pnpm buildLuego ejecute las pruebas usando su compilación local contra motores de navegador reales:
pnpm test