Giter VIP home page Giter VIP logo

cmdk's Introduction

⌘K cmdk minzip package size cmdk package version

⌘K is a command menu React component that can also be used as an accessible combobox. You render items, it filters and sorts them automatically. ⌘K supports a fully composable API How?, so you can wrap items in other components or even as static JSX.

Demo and examples: cmdk.paco.me

Install

pnpm install cmdk

Use

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>
  )
}

Or in a dialog:

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>
  )
}

Parts and styling

All parts forward props, including ref, to an appropriate element. Each part has a specific data-attribute (starting with cmdk-) that can be used for styling.

Command [cmdk-root]

Render this to show the command menu inline, or use Dialog to render in a elevated context. Can be controlled with the value and onValueChange props.

Note

Values are always trimmed with the trim() method.

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>
)

You can provide a custom filter function that is called to rank each item. Note that the value will be trimmed.

<Command
  filter={(value, search) => {
    if (value.includes(search)) return 1
    return 0
  }}
/>

A third argument, keywords, can also be provided to the filter function. Keywords act as aliases for the item value, and can also affect the rank of the item. Keywords are trimmed.

<Command
  filter={(value, search, keywords) => {
    const extendValue = value + ' ' + keywords.join(' ')
    if (extendValue.includes(search)) return 1
    return 0
  }}
/>

Or disable filtering and sorting entirely:

<Command shouldFilter={false}>
  <Command.List>
    {filteredItems.map((item) => {
      return (
        <Command.Item key={item} value={item}>
          {item}
        </Command.Item>
      )
    })}
  </Command.List>
</Command>

You can make the arrow keys wrap around the list (when you reach the end, it goes back to the first item) by setting the loop prop:

<Command loop />

Dialog [cmdk-dialog] [cmdk-overlay]

Props are forwarded to Command. Composes Radix UI's Dialog component. The overlay is always rendered. See the Radix Documentation for more information. Can be controlled with the open and onOpenChange props.

const [open, setOpen] = React.useState(false)

return (
  <Command.Dialog open={open} onOpenChange={setOpen}>
    ...
  </Command.Dialog>
)

You can provide a container prop that accepts an HTML element that is forwarded to Radix UI's Dialog Portal component to specify which element the Dialog should portal into (defaults to body). See the Radix Documentation for more information.

const containerElement = React.useRef(null)

return (
  <>
    <Command.Dialog container={containerElement.current} />
    <div ref={containerElement} />
  </>
)

Input [cmdk-input]

All props are forwarded to the underlying input element. Can be controlled with the value and onValueChange props.

const [search, setSearch] = React.useState('')

return <Command.Input value={search} onValueChange={setSearch} />

List [cmdk-list]

Contains items and groups. Animate height using the --cmdk-list-height CSS variable.

[cmdk-list] {
  min-height: 300px;
  height: var(--cmdk-list-height);
  max-height: 500px;
  transition: height 100ms ease;
}

To scroll item into view earlier near the edges of the viewport, use scroll-padding:

[cmdk-list] {
  scroll-padding-block-start: 8px;
  scroll-padding-block-end: 8px;
}

Item [cmdk-item] [data-disabled?] [data-selected?]

Item that becomes active on pointer enter. You should provide a unique value for each item, but it will be automatically inferred from the .textContent.

<Command.Item
  onSelect={(value) => console.log('Selected', value)}
  // Value is implicity "apple" because of the provided text content
>
  Apple
</Command.Item>

You can also provide a keywords prop to help with filtering. Keywords are trimmed.

<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>

You can force an item to always render, regardless of filtering, by passing the forceMount prop.

Group [cmdk-group] [hidden?]

Groups items together with the given heading ([cmdk-group-heading]).

<Command.Group heading="Fruit">
  <Command.Item>Apple</Command.Item>
</Command.Group>

Groups will not unmount from the DOM, rather the hidden attribute is applied to hide it from view. This may be relevant in your styling.

You can force a group to always render, regardless of filtering, by passing the forceMount prop.

Separator [cmdk-separator]

Visible when the search query is empty or alwaysRender is true, hidden otherwise.

Empty [cmdk-empty]

Automatically renders when there are no results for the search query.

Loading [cmdk-loading]

You should conditionally render this with progress while loading asynchronous items.

const [loading, setLoading] = React.useState(false)

return <Command.List>{loading && <Command.Loading>Hang on…</Command.Loading>}</Command.List>

useCommandState(state => state.selectedField)

Hook that composes useSyncExternalStore. Pass a function that returns a slice of the command menu state to re-render when that slice changes. This hook is provided for advanced use cases and should not be commonly used.

A good use case would be to render a more detailed empty state, like so:

const search = useCommandState((state) => state.search)
return <Command.Empty>No results found for "{search}".</Command.Empty>

Examples

Code snippets for common use cases.

Nested items

Often selecting one item should navigate deeper, with a more refined set of items. For example selecting "Change theme…" should show new items "Dark theme" and "Light theme". We call these sets of items "pages", and they can be implemented with simple state:

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>
)

Show sub-items when searching

If your items have nested sub-items that you only want to reveal when searching, render based on the search state:

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>
)

Asynchronous results

Render the items as they become available. Filtering and sorting will happen automatically.

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>
)

Use inside Popover

We recommend using the Radix UI popover component. ⌘K relies on the Radix UI Dialog component, so this will reduce your bundle size a bit due to shared dependencies.

$ pnpm install @radix-ui/react-popover

Render Command inside of the popover content:

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>
)

Drop in stylesheets

You can find global stylesheets to drop in as a starting point for styling. See website/styles/cmdk for examples.

FAQ

Accessible? Yes. Labeling, aria attributes, and DOM ordering tested with Voice Over and Chrome DevTools. Dialog composes an accessible Dialog implementation.

Virtualization? No. Good performance up to 2,000-3,000 items, though. Read below to bring your own.

Filter/sort items manually? Yes. Pass shouldFilter={false} to Command. Better memory usage and performance. Bring your own virtualization this way.

React 18 safe? Yes, required. Uses React 18 hooks like useId and useSyncExternalStore.

Unstyled? Yes, use the listed CSS selectors.

Hydration mismatch? No, likely a bug in your code. Ensure the open prop to Command.Dialog is false on the server.

React strict mode safe? Yes. Open an issue if you notice an issue.

Weird/wrong behavior? Make sure your Command.Item has a key and unique value.

Concurrent mode safe? Maybe, but concurrent mode is not yet real. Uses risky approaches like manual DOM ordering.

React server component? No, it's a client component.

Listen for ⌘K automatically? No, do it yourself to have full control over keybind context.

React Native? No, and no plans to support it. If you build a React Native version, let us know and we'll link your repository here.

History

Written in 2019 by Paco (@pacocoursey) to see if a composable combobox API was possible. Used for the Vercel command menu and autocomplete by Rauno (@raunofreiberg) in 2020. Re-written independently in 2022 with a simpler and more performant approach. Ideas and help from Shu (@shuding_).

use-descendants was extracted from the 2019 version.

Testing

First, install dependencies and Playwright browsers:

pnpm install
pnpm playwright install

Then ensure you've built the library:

pnpm build

Then run the tests using your local build against real browser engines:

pnpm test

cmdk's People

Contributors

0xcadams avatar afercia avatar alexcarpenter avatar andarist avatar andipaetzold avatar arihanv avatar bkrausz avatar caumeira avatar cristicretu avatar evanob avatar flaviouk avatar glocore avatar hajimism avatar itaikeren avatar itsjoeoui avatar jeromefitz avatar joaom00 avatar jrysana avatar kilian avatar mildtomato avatar pacocoursey avatar pagebakers avatar pondorasti avatar prestonbourne avatar raunofreiberg avatar revogabe avatar simek avatar thiskevinwang avatar wits avatar zingerj avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

cmdk's Issues

[Convention] Aria attributes should not be used to style components

Hi!

We've had issues when trying to style a CmdK Select Item: we have to use the aria-selected attribute to style the items, even though it goes against our principles/conventions (aria-attributes should not be used to style elements).

Meanwhile, Radix has implemented a data-highlighted custom attribute to fix that issue. Could you do the same, please? I'm willing to dig into the code and make a PR, if necessary.

Add asChild for Item

Add asChild with raddix-ui like functionality: Change the component to the HTML tag or custom component of the only child. This will merge the original component props with the props of the supplied element/component and change the underlying DOM node.

Automatically select the contents of the input field when ⌘K dialog opens

In my use case, ⌘K is shown in dialog mode. If I open ⌘K, enter a search string into the input field, then close ⌘K and then re-open ⌘K again, the search string entered before is still there. In general, this is quite useful, as I can easily repeat or amend my previous search.

Usually, however, I rather want to enter a new search. Currently, after ⌘K is re-opened, the input field gets focus with the cursor placed at the end of the previously entered search string. Hence, replacing that search string with a new search string requires a few keystrokes. In my opinion, it would be more convenient if the previously entered search string was automatically selected when ⌘K is re-opened. This way, if I want to enter a new search string, I can just type right away, but I am also still able to use the old search string, if I want to.

Therefore, I am looking for a way to configure ⌘K such that when ⌘K opens, the content of the search input field gets automatically selected (if the input field has no content, of course nothing gets selected). Unfortunately I am not very familiar with React. It tried accessing the input field within a function called by onOpenChange. However, as it seems, when that function is called the input field is not yet part of the DOM and therefore cannot be accessed. Furthermore, I found that Radix UI's Dialog component has an onOpenAutoFocus event handler. Can it be used with ⌘K? Or is there any other simple way to automatically select the contents of the input field when ⌘K opens?

Items added asynchronously cause the active item to be reset

My use case is an infinitely scrolling list, that loads more items as the user scrolls or arrows down. Whenever the items in the list is changed, the active index appears to be reset to zero.

Here's a simple reproduction (it doesn't actually load more on scroll, it simply loads more on an interval). If you start using the down arrow to move down the list, you'll see your position gets lost as new items are concatenated onto the end of the list.

https://codesandbox.io/s/nifty-wood-4u4cnx?file=/src/App.tsx

This is less of an issue when scrolling with the mouse, because the pointer sets a new active item on whatever it hovers over. So the primary thing I'm hoping to solve is preserving the active item when using arrow keys.

Thanks very much for this library! If you need a PR, I may be able to contribute—I recognize this might be non-trivial.

Creating a combobox - `value` prop resets

Hi there,

I am trying to create a Combobox in combination with the Radix UI popover component, where I want to have a specific item/value pre-selected. I could use the value prop on the Command component but unfortunately, as soon as I open the Combobox, the value resets to the first item again.

Is there any other way to have an item pre-selected, as the expected behaviour for a Combobox component. It seems quite difficult to create a Combobox using cmdk.

For example, I am using the example from the docs. Apple is the pre-selected value but this gets reset to Orange as soon as I close and re-open the menu.

Better example code / tutorial?

I'm unable to get a simple example running with any of the example code on the website in a React Codesandbox. Is there any fully working example that can be provided?

import "./styles.css";
import React from "react";
import { Command } from "cmdk";

export default function App() {
  const loading = true;
  const setOpen = () => {};
  const open = true;

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>

      <Command.Dialog open={open} onOpenChange={setOpen}>
        <Command.Input />

        <Command.List>
          {loading && <Command.Loading>Hang on…</Command.Loading>}

          <Command.Empty>No results found.</Command.Empty>

          <Command.Group heading="Fruits">
            <Command.Item>Apple</Command.Item>
            <Command.Item>Orange</Command.Item>
            <Command.Separator />
            <Command.Item>Pear</Command.Item>
            <Command.Item>Blueberry</Command.Item>
          </Command.Group>

          <Command.Item>Fish</Command.Item>
        </Command.List>
      </Command.Dialog>
    </div>
  );
}

https://codesandbox.io/s/wizardly-currying-x68myq

Wraparound when using arrow keys

With the input focused and using the arrow keys to navigate, it would be nice if focus wrapped around after reaching the end of the list (and conversely, pressing "up" from the input should focus the last item in the filtered list)

ResizeObserver loop limit exceeded Sentry flood

Hello, lately we have switched from Algolia Docsearch UI to custom one build upon the CMDK menu on http://docs.expo.dev.

After few days on production I have spotted that we started to receive around 1k Sentry events a day related to the ResizeObserver logic used inside CMDK ("ResizeObserver loop limit exceeded"):

I have tried to reproduce this problem locally, but without a success. From the reports it looks like this mostly affect desktop users using Chrome or Edge, on macOS and Windows.

You can find our implementation here:

Any hint on how to reproduce or fix that problem would be appreciated.

Create New

It would be really cool if there were no result, just as it is possible to render Command.Empty there was also a Command.Create that could render a selected Item with the message Create new "${seach}".

I tried to do this, however, the Command.Item is rendered null when there are no filtered results.

if (!render) return null

Como seria legal que fosse

Proposal 1

<Command>
  <Command.Input />
  <Command.List>
    <Command.Item>Apple</Command.Item>
    <Command.Item create>Create New</Command.Item>
  </Command.List>
</Command>

Command.Item create would only be displayed when the filter count was 0 (like Command.Empty).

Proposal 2

<Command>
  <Command.Input />
  <Command.List>
    <Command.Item>Apple</Command.Item>
    <Command.Create>Create New</Command.Create>
  </Command.List>
</Command>

Command.Create would only be displayed when the filter count was 0 (like Command.Empty).

App freezes when Items inside List changes based on async query

I'm combining React Query and Meilisearch with cmdk and the app freezes if I type something on the Command.Input that doesn't give any results on the query. The bug doesn't happen if I change the Command.Item for a common div tag.
Any ideas on what could I be doing wrong?

Source code:

export const Search = () => {
  const [open, setOpen] = React.useState(false)
  const [search, setSearch] = React.useState('')

  const { data: results, isLoading } = useQuery(
    ['docs', search],
    async (context) => {
      const client = new MeiliSearch({
        host: 'http://127.0.0.1:7700',
        apiKey: 'myMasterKey'
      })
      // An index is where the documents are stored.
      const index = client.index('movies')

      const queryResults = await index.search(context.queryKey[1], { limit: 5 })
      return queryResults
    }
  )

  // Toggle the menu when ⌘K is pressed
  React.useEffect(() => {
    const down = (e: KeyboardEvent) => {
      if (e.key === 'k' && e.metaKey) {
        setOpen((open) => !open)
      }
    }

    document.addEventListener('keydown', down)
    return () => document.removeEventListener('keydown', down)
  }, [])

  return (
    <>
      <button onClick={() => setOpen(true)}>
        Search docs
      </button>
      <Command.Dialog
        shouldFilter={false}
        open={open}
        onOpenChange={setOpen}
        label="Global Command Menu"
      >
        <Command.Input
          value={search}
          onValueChange={setSearch}
        />
        <Command.List>
          {isLoading && <Command.Loading>Fetching words…</Command.Loading>}
          {results?.hits.map((hit) => (
            <Command.Item key={hit.id} value={hit.title}>
              <h4>{hit.title}</h4>
              <span>{hit.overview}</span>
            </Command.Item>
          ))}
          <Command.Empty>No results found.</Command.Empty>
        </Command.List>
      </Command.Dialog>
    </>
  )
}

Sometimes cannot go down with down arrow

Hey all, thanks for making this. I was playing around with the demo on the website and found two bugs that I'll lump in one here.
First if I go to the Framer example and type "baa", then delete the last "a", the only result showing is not highlighted which seems like a bug to me. However I can highlight it by going down with the down arrow.

Then if I delete another "a", I see two results, but I cannot go up/down with the arrows anymore.

Here's a video where I'm pressing up/down arrow at the end.

Screen.Recording.2022-08-09.at.11.51.09.mov

Run `playwright test` on every PR push

We should set up a GitHub action to test changes made in every PR via the existing Playwright setup. Would complement nicely with the Vercel deployment.

Minify output better

The library size can be minified by mangling object properties like .filtered. I think https://github.com/vercel/swr uses some tricks for this, like exporting things from barrel files or using TS enums.

Also, we could consider splitting cmdk and cmdk/dialog so that the ~fairly large Radix Dialog dependency isn't an upfront cost. Though I'd imagine most people want to use this library in the context of a dialog.

Option to not change selected item via mouse events

I'm building a raycast clone for linux as a learning project, using cmdk and wails.io. It worked really well for me so far, thanks a lot for releasing this !

One pain point I have is the keyboard/mouse interaction during the selection. When I scroll a list using a keyboard, the mouse is often stealing the focus.

Screen.Recording.2022-09-07.at.19.01.45.mov

I love how raycast solve the issue: the mouse only provide an hover effect, and only select on click.

Screen.Recording.2022-09-07.at.18.59.07.mov

Would you be interested in adding this behaviour to the app ? I can work on the pr.

Ignore Safari pointer events on keyboard navigation

Safari will fire pointer events when navigating via the keyboard, annoyingly causing the active index to jump back up to where the cursor is resting. Instead, these should be ignored when caused via keyboard navigation.

Could be solved by #49

Consider prettier

Hi, would you be interested in formatting with prettier & enforcing in CI? Can create a PR

[Convention] Refactoring Project structure

Is there value in refactoring the src directory into a more standard project structure since it's approaching 1000 lines of code

Something along the lines of

src/
├── types
├── index
├── hooks/
│   ├── types
│   └── useLazyRef
├── store/
│   ├── types
│   ├── index
│   └── contexts
└── constants/
    └── index

I'd be willing to begin breaking this down to smaller tasks and migrating to if there's an interest

How do I force dark mode?

My app doesn't support light mode. How can I prevent the styles being dynamic?

I was thinking there would be some prop for theme override.

How to make SubItem render other items

I am trying to make my SubItem render something like this:

{searchItems.map(({ icon, label }) => {
    return (
        <SubItem key={label}>
            <div>
                <img src={icon} />
                <p>{label}</p>
            </div>
        </SubItem>;
    )
})}

But it won´t work like this.

It will only work when it looks like this:

{searchItems.map(({ icon, label }) => {
    return 
        <SubItem key={label}>
            {label}
        </SubItem>;
})}

Then it shows up correctly. If I were to do {icon} {label} it doesn't work either.

It seems like it only accepts one thing, and I am trying to get it to look like a normal Command.Item with an icon on the left and text next to it without a shortcut.

feat: separate value from text content when filtering

I have a use case where I want my value to be different than the label presented to the end user. When typing in the search field today, the user would have to know the underlying value to be able to see something return.... typing what is visible text content wouldn't yield the expected filtered results.

My ask is that we change the behavior so that the search field only looks at the text content rendered (we have to pass this anyway for something to show up). The only way an average user would know what the value is is if they opened their dev tools, which is not expected.

Custom filter doesn't apply until search term exists

I'm trying to provide a custom filter to filter out items based on the URL before the user even enters a search term, and it's not working because the current implementation renders all items by default (ignoring filtering) until the search term is provided. I believe this line is where that functionality is written.

I'd like to see an implementation that allows the custom filter to be applied even before there's a search term.

Using cmdk with Remix

Issue

I am trying to integrate this cmdk tool with a remix application, but I am facing some styling issues:

  • The menu always appears at the bottom of the page instead of a pop over on top of whatever is currently on the page
  • The cmdk menu does not match the styling showed in the demo
    Is there any documentation that can help to integrate the css more appropriately, thanks

References

Here is a link to the stackblitz I have been working in: https://stackblitz.com/edit/remix-run-remix-sjsqzh?file=app/root.tsx

Enable className on the `Overlay` and `Root`

Can we expose the Overlay and Root elements and manually implement them, or be able to pass in a className for custom styling? This will make styling simpler with frameworks like tailwind

cmdk/cmdk/src/index.tsx

Lines 779 to 786 in 605ae13

<RadixDialog.Root open={open} onOpenChange={onOpenChange}>
<RadixDialog.Portal container={container}>
<RadixDialog.Overlay cmdk-overlay="" />
<RadixDialog.Content aria-label={props.label} cmdk-dialog="">
<Command ref={forwardedRef} {...etc} />
</RadixDialog.Content>
</RadixDialog.Portal>
</RadixDialog.Root>

Use cmdk for Chrome Extension

I am trying to add cmdk for the react-based chrome extension.
But Command.Dialog appears at the end of the page without any styling.

For example, how it looks on github.com:
209028806-dd553769-fdd4-4f55-95ec-0f343226f8bf

Do you have any idea what went wrong?

Expose getters / setters for selected index?

I wanted to be able to control the selected index from outside the component if possible. Motivation is dealing with sub pages and parent pages. If I go into a sub page and then back out hitting Escape, I wanted the parent page Item to have focus in the List.

Looks like internally there are a couple of helper methods like updateSelectedByChange and updateSelectedToIndex that I wanted to get access to or be able to pass a prop to the Command component indicating initial Item to be selected/focused.

Thanks for putting this library out, seems to be working well for me so far!

Webpack Issue

I was trying to install it in my project and got this error.
Anyone knows how should I resolve it ?
image

`ERROR in ./node_modules/cmdk/dist/index.mjs 1:1916
Module parse failed: Unexpected token (1:1916)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders

importas C from"@radix-ui/react-dialog";importas t from"react";import le from"command-score";var ue='[cmdk-list-sizer=""]',M='[cmdk-group=""]',N='[cmdk-group-items=""]',de='[cmdk-group-heading=""]',ee='[cmdk-item=""]',Z=${ee}:not([aria-disabled="true"]),z="cmdk-item-select",S="data-value",fe=(n,i)=>le(n,i),te=t.createContext(void 0),k=()=>t.useContext(te),re=t.createContext(void 0),U=()=>t.useContext(re),ne=t.createContext(void 0),oe=t.forwardRef((n,i)=>{let r=t.useRef(null),o=x(()=>({search:"",value:"",filtered:{count:0,items:new Map,groups:new Set}})),u=x(()=>new Set),l=x(()=>new Map),g=x(()=>new Map),f=x(()=>new Set),d=ie(n),{label:b,children:E,value:v,onValueChange:w,filter:O,shouldFilter:ae,...D}=n,F=t.useId(),p=t.useId(),A=t.useId(),y=ye();L(()=>{if(v!==void 0){let e=v.trim().toLowerCase();o.current.value=e,y(6,W),h.emit()}},[v]);let h=t.useMemo(()=>({subscribe:e=>(f.current.add(e),()=>f.current.delete(e)),snapshot:()=>o.current,setState:(e,c,a)=>{var s,m,R;if(!Object.is(o.current[e],c)){if(o.current[e]=c,e==="search")j(),G(),y(1,V);else if(e==="value")if(((s=d.current)==null?void 0:s.value)!==void 0){(R=(m=d.current).onValueChange)==null||R.call(m,c);return}else a||y(5,W);h.emit()}},emit:()=>{f.current.forEach(e=>e())}}),[]),K=t.useMemo(()=>({value:(e,c)=>{c!==g.current.get(e)&&(g.current.set(e,c),o.current.filtered.items.set(e,B(c)),y(2,()=>{G(),h.emit()}))},item:(e,c)=>(u.current.add(e),c&&(l.current.has(c)?l.current.get(c).add(e):l.current.set(c,new Set([e]))),y(3,()=>{j(),G(),o.current.value||V(),h.emit()}),()=>{g.current.delete(e),u.current.delete(e),o.current.filtered.items.delete(e),y(4,()=>{j(),V(),h.emit()})}),group:e=>(l.current.has(e)||l.current.set(e,new Set),()=>{g.current.delete(e),l.current.delete(e)}),filter:()=>d.current.shouldFilter,label:b||n["aria-label"],listId:F,inputId:A,labelId:p}),[]);function B(e){var a;let c=((a=d.current)==null?void 0:a.filter)??fe;return e?c(e,o.current.search):0}function G(){if(!r.current||!o.current.search||d.current.shouldFilter===!1)return;let e=o.current.filtered.items,c=[];o.current.filtered.groups.forEach(s=>{let m=l.current.get(s),R=0;m.forEach(P=>{let ce=e.get(P);R=Math.max(ce,R)}),c.push([s,R])});let a=r.current.querySelector(ue);I().sort((s,m)=>{let R=s.getAttribute(S),P=m.getAttribute(S);return(e.get(P)??0)-(e.get(R)??0)}).forEach(s=>{let m=s.closest(N);m?m.appendChild(s.parentElement===m?s:s.closest(${N} > *)):a.appendChild(s.parentElement===a?s:s.closest(${N} > *))}),c.sort((s,m)=>m[1]-s[1]).forEach(s=>{let m=r.current.querySelector(${M}[${S}="${s[0]}"]);m==null||m.parentElement.appendChild(m)})}function V(){let e=I().find(a=>!a.ari`

Consider adding `aria-activedescendant` to support VoiceOver

By default, cmdk doesn't seem to be accessible to VoiceOver (Mac). I'm guessing this is just an edge-case / difference between different screen readers. I figured out how to fix the issue in my project, but I think it would make sense for this to be built into the library itself.

To get it to work well with VoiceOver, I had to add aria-activedescendant to the command input. I've added screen recordings for a "before" and "after" to the bottom of this issue.

// CommandItem component
const id = useId();
return <Command.Item id={id} {...etc} />

// CommandInput component
const value = useCommandState((state) => state.value);
const [id, setId] = React.useState<string | undefined>(undefined);

useEffect(() => {
  const nextId = document.querySelector(`[cmdk-item=""][data-value="${value}"]`)?.id;
  setId(nextId);
}, [value]);

return <Command.Input aria-activedescendant={id} {...etc} />

Thanks for the great library!


Before the change

before.mp4

After the change

after.mp4

Not working on NextJS

Hi @raunofreiberg, for some reason, I’m unable to get it to work on NextJS (using latest next and react, react-dom). I’ve included a basic reproduction here: cmdk-nextjs.

As you can see below, nothing happens. I must be missing something basic, but have no idea what I’m missing.

CleanShot 2022-08-17 at 08 47 58

Error when using useCommandState

I have just added useCommandState to my cmdk component and before anything is even typed this error is shown. I am using the latest version of next.js (v12.3.1), React (v18.2.0) and cmdk (v0.1.20). Do you have any idea why this might be occurring? Is there anything else that I need to add in order for this to work?

Usage

const search = useCommandState((state) => state.search)

Error

TypeError: Cannot read properties of undefined (reading 'subscribe')

Unexpected matching behavior

Reproduction steps:

  1. On https://cmdk.paco.me/ make sure you have the raycast example selected
  2. Type "eee"
  3. See "Manage Extensions" still being visible even though it does not contain the string "eee"
  4. Type "eeee"
  5. See the list is now empty

I would expect input to match sequentially e.g. if there is no "eee" in the label, it should not match. It looks like right now each character is matched individually-but-uniquely, since "manage extensions" contains three non-sequential "e"s.

NextJS Support

Does this library officially support NextJS, or have any plans to? When trying to integrate it to a larger existing NextJS codebase, I received the following error:

Unhandled Runtime Error
TypeError: react__WEBPACK_IMPORTED_MODULE_0__.useId is not a function

Call Stack
eval
node_modules/cmdk/dist/index.mjs (1:719)

However, when trying to make a minimal reproduction, I failed to get it to load at all, with Next complaining of a missing webpack loader -- not sure what loader is required in this instance.

See codesandbox here: https://codesandbox.io/s/charming-darkness-gw9ezm

TL;DR - Library doesn't seem to work with NextJS right now.

Support Grid View

Would love to use this library while rendering items in a Grid and being able to use left/right/up/down keyboard arrow to navigate across Grid Items. I appreciate the Grid view is not the most common presentation for Command K, but I see now reason why it shouldn't be! Having this as an option could open up lots of cool and more rich UIs.

Raycast for example is a very similar UI and supports Grid:
CleanShot 2022-10-29 at 09 06 53@2x

Bugs when rendering the `<Command.List>` in a `<Portal>`

This isn't necessarily a bug—as the documentation never states this is supported—but much of CMDK's functionality breaks down when the <Command.List> is rendered in a Radix <Portal> (e.g. when building a combobox component) and thus is not a child of the <Command> HTML component, which causes all of the ref.current.querySelectorAll to fail:

// cmdk/src/index.tsx#L404
function getSelectedItem() {
  return ref.current.querySelector(`${ITEM_SELECTOR}[aria-selected="true"]`)
}
function getValidItems() {
  return Array.from(ref.current.querySelectorAll(VALID_ITEM_SELECTOR))
}

It would be nice to be able to pass in a listRef property that points to the <Command.List> component like so:

const listRef = useRef()
return (
  <Command listRef={listRef}>
    <Command.Input />
    <Portal.Root>
      <Command.List ref={listRef}>
        <Command.Item>a</Command.Item>
        <Command.Item>b</Command.Item>
        <Command.Item>c</Command.Item>
      </Command.List>
    </Portal.Root>
  </Command>
)

CMDK would then be able to use that listRef (if it exists) in place of its built-in ref whenever possible.

If this seems like a viable solution @raunofreiberg @pacocoursey, let me know and I can throw up a PR.

It may also be informative to have some examples of implementing a <Combobox> using this library. The README describes "⌘K is a command menu React component that can also be used as an accessible combobox." For example, how exactly did Rauno use the 2019 version for Vercel's combobox? Adding an example snippet or implementation to the docs will hopefully prevent a bunch of naive, junior developers such as myself from creating issues like this one. :)

[Accessibility] `aria-selected=false` never happens

cmdk/cmdk/src/index.tsx

Lines 613 to 615 in abe6b24

aria-disabled={disabled || undefined}
aria-selected={selected || undefined}
data-selected={selected || undefined}

Since we're using || if selected or disabled returns false, the entire aria label will be undefined.

We would prefer aria-label=false to be the case so that someone relying on the labels see's that it isnt selected but can be. Right now the only possible state is aria-selected=true

We can fix this by migrating from the || operator to the nullish ?? operator

List selection doesn't reset when using external filtering

When filtering the list externally, the selected list item doesn't reset to 0 when the list changes. In the screencap below, I'm using Fuse.js to filter a list and mapping over the results.

Screen.Recording.2022-11-04.at.11.08.35.PM.mov

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.