pacocoursey / cmdk

Fast, unstyled command menu React component.
https://cmdk.paco.me
MIT License
9.83k stars 282 forks source link

How to tell between Enter and Cmd+Enter #310

Open donovan-fintool opened 3 weeks ago

donovan-fintool commented 3 weeks ago

I have a use case where I want to perform different actions on enter and on cmd+enter. In my use case, I'm using cmdk as a multi select, so users can check / uncheck each item in the list, then hit cmd+enter to submit their selections.

onSelect triggers on both on enter and on cmd+enter, which causes issues for me. My workaround is to create a different React state that tracks if the cmd key is currently being pressed, which is kind of hacky and a lot of boilerplate.

My question is - is there a better way of doing it? If not, could we pass an event to onSelect so I can handle it myself?

Thanks!

import { Command } from 'cmdk';
import { useEffect, useState } from "react";

export const Demo = () => {
    const [isCmdKeyPressed, setIsCmdKeyPressed] = useState(false);

    const handleEnter = () => {
        // do something
    }

    const handleCmdEnter = () => {
        // do something else
    }

    useEffect(() => {
        const handleKeyDown = (event: KeyboardEvent) => {
            if (event.metaKey || event.ctrlKey) {
                setIsCmdKeyPressed(true);
            }

            if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
                handleCmdEnter();
            }
        };

        const handleKeyUp = (event: KeyboardEvent) => {
            if (event.key === 'Meta' || event.key === 'Control') {
                setIsCmdKeyPressed(false);
            }
        };

        document.addEventListener('keydown', handleKeyDown);
        document.addEventListener('keyup', handleKeyUp);

        return () => {
            document.removeEventListener('keydown', handleKeyDown);
            document.removeEventListener('keyup', handleKeyUp);
        };
    }, []);

    return (
        <Command label="Command Menu">
            <Command.Input />
            <Command.List>
                <Command.Empty>No results found.</Command.Empty>
                <Command.Item
                    onSelect={() => {
                        if (isCmdKeyPressed) {
                            // detected cmd+enter, don't do anything
                            return;
                        }

                        handleEnter();
                    }}
                >
                    Apple
                </Command.Item>
            </Command.List>
        </Command>
    );
};
EdoAPP commented 2 weeks ago

I had a similar situation where I wanted to perform a different action depending if an item was selected with "Enter". After looking at the implementation, I ended up with this solution:

<Command
  onKeyDown={(e) => {
    // You can also add another condition if (e.key === 'Enter' && e.metaKey) to tackle cmd + enter
    if (e.key === 'Enter') {
      // Prevents "onSelect" from being called"
      e.preventDefault();
      // item that is currently on focused (selected) either by mouse hover or keyboard navigation
      const item = getSelectedItem(listInnerRef);

      // you can do 

      if (item) {
      // this will dispatch an event that the cmdk library will listen to (then onSelect) will be executed
        const event = new Event(SELECT_EVENT);
        item.dispatchEvent(event);
      }
    }
  }}
>

Then your utility functions

export const ITEM_SELECTOR = `[cmdk-item=""]`;
export const SELECT_EVENT = `cmdk-item-select`;

export function getSelectedItem(listInnerRef: React.RefObject<HTMLDivElement>) {
  return listInnerRef.current?.querySelector(`${ITEM_SELECTOR}[aria-selected="true"]`);
}

listInnerRef is a reference to your list/group of items.

donovan-fintool commented 2 weeks ago

I had a similar situation where I wanted to perform a different action depending if an item was selected with "Enter". After looking at the implementation, I ended up with this solution:

<Command
  onKeyDown={(e) => {
    // You can also add another condition if (e.key === 'Enter' && e.metaKey) to tackle cmd + enter
    if (e.key === 'Enter') {
      // Prevents "onSelect" from being called"
      e.preventDefault();
      // item that is currently on focused (selected) either by mouse hover or keyboard navigation
      const item = getSelectedItem(listInnerRef);

      // you can do 

      if (item) {
      // this will dispatch an event that the cmdk library will listen to (then onSelect) will be executed
        const event = new Event(SELECT_EVENT);
        item.dispatchEvent(event);
      }
    }
  }}
>

Then your utility functions

export const ITEM_SELECTOR = `[cmdk-item=""]`;
export const SELECT_EVENT = `cmdk-item-select`;

export function getSelectedItem(listInnerRef: React.RefObject<HTMLDivElement>) {
  return listInnerRef.current?.querySelector(`${ITEM_SELECTOR}[aria-selected="true"]`);
}

listInnerRef is a reference to your list/group of items.

Amazing, that's much cleaner than my method. Thanks!