Enable keyboard nav of slash commands with arrow keys on mount (#4543)

This commit is contained in:
Timothy Carambat 2025-10-15 10:45:14 -07:00 committed by GitHub
parent 797920a25f
commit be82f91fc3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 71 additions and 0 deletions

View File

@ -186,6 +186,8 @@ function PresetItem({ preset, onUse, onEdit, onPublish }) {
return (
<button
type="button"
data-slash-command={preset.command}
onClick={onUse}
className="border-none w-full hover:cursor-pointer hover:bg-theme-action-menu-item-hover px-2 py-2 rounded-xl flex flex-row justify-start items-center relative"
>

View File

@ -6,6 +6,8 @@ export default function EndAgentSession({ setShowing, sendCommand }) {
return (
<button
type="button"
data-slash-command="/exit"
onClick={() => {
setShowing(false);
sendCommand({ text: "/exit", autoSubmit: true });

View File

@ -5,6 +5,7 @@ import ResetCommand from "./reset";
import EndAgentSession from "./endAgentSession";
import SlashPresets from "./SlashPresets";
import { useTranslation } from "react-i18next";
import { useSlashCommandKeyboardNavigation } from "@/hooks/useSlashCommandKeyboardNavigation";
export default function SlashCommandsButton({ showing, setShowSlashCommand }) {
const { t } = useTranslation();
@ -34,6 +35,8 @@ export default function SlashCommandsButton({ showing, setShowSlashCommand }) {
export function SlashCommands({ showing, setShowing, sendCommand, promptRef }) {
const cmdRef = useRef(null);
useSlashCommandKeyboardNavigation({ showing });
useEffect(() => {
function listenForOutsideClick() {
if (!showing || !cmdRef.current) return false;

View File

@ -8,6 +8,8 @@ export default function ResetCommand({ setShowing, sendCommand }) {
return (
<button
type="button"
data-slash-command="/reset"
onClick={() => {
setShowing(false);
sendCommand({ text: "/reset", autoSubmit: true });

View File

@ -0,0 +1,62 @@
import { useEffect, useRef } from "react";
/**
* Handles keyboard navigation for the slash commands menu is presented in the UI.
* @param {boolean} showing - Whether the slash commands menu is showing
* @returns {void}
*/
export function useSlashCommandKeyboardNavigation({ showing }) {
const focusedCommandRef = useRef(null);
const availableCommands = useRef([]);
useEffect(() => {
const commands = document.querySelectorAll("[data-slash-command]");
availableCommands.current = Array.from(commands).map(
(cmd) => cmd.dataset.slashCommand
);
}, [showing]);
useEffect(() => {
if (!showing) return;
document.addEventListener("keydown", handleKeyboardNavigation);
return () =>
document.removeEventListener("keydown", handleKeyboardNavigation);
}, [showing]);
useEffect(() => {
// Reset the focused command when the slash commands menu is closed or opened
focusedCommandRef.current = null;
}, [showing]);
function handleKeyboardNavigation(event) {
event.preventDefault();
if (!availableCommands.current.length) return;
let currentIndex = availableCommands.current.indexOf(
focusedCommandRef.current
);
// If the enter key is pressed, click the focused command if it exists
// This will also trigger the onClick event of the focused command
// to cleanup everything on hide
if (event.key === "Enter" && !!focusedCommandRef.current) {
document
.querySelector(`[data-slash-command="${focusedCommandRef.current}"]`)
?.click();
return;
}
// If the current index is -1, set it to the last command, otherwise inc/dec by 1
if (currentIndex === -1)
currentIndex = availableCommands.current.length - 1;
else currentIndex += event.key === "ArrowUp" ? -1 : 1;
// Wrap around the array both ways if index is out of bounds
if (currentIndex < 0) currentIndex = availableCommands.current.length - 1;
else if (currentIndex >= availableCommands.current.length) currentIndex = 0;
focusedCommandRef.current = availableCommands.current[currentIndex];
document
.querySelector(`[data-slash-command="${focusedCommandRef.current}"]`)
?.focus();
}
}