From 80ed2d65999ea2c8118b5803252b7ec2ee2e7f87 Mon Sep 17 00:00:00 2001 From: Samer Gassouma Date: Sun, 23 Aug 2026 16:13:31 +0100 Subject: [PATCH] fix(spotlight): allow mouse click to select select-type arg options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Command Spotlight rendered select-type argument options as rows with onMouseEnter only, so the selection logic (which lived solely in the Enter key handler) was unreachable from a click. Clicking an option did nothing. Extract the arg-selection step into a selectArg(value) callback in Spotlight.tsx and thread it through to SpotlightResults → ArgModeResults via a new onSelectArg prop. Each option row now fires onSelectArg(opt.value) on click, mirroring the existing Enter behavior (advance to next arg or run the command on the last arg). Adds a regression test in SpotlightResults.test.tsx. Fixes #420 --- src/admin/spotlight/Spotlight.tsx | 30 +++++++---- src/admin/spotlight/SpotlightResults.tsx | 6 +++ .../__tests__/SpotlightResults.test.tsx | 52 ++++++++++++++++++- 3 files changed, 77 insertions(+), 11 deletions(-) diff --git a/src/admin/spotlight/Spotlight.tsx b/src/admin/spotlight/Spotlight.tsx index 7d3db1966..91cb1f551 100644 --- a/src/admin/spotlight/Spotlight.tsx +++ b/src/admin/spotlight/Spotlight.tsx @@ -178,16 +178,7 @@ export function Spotlight({ isClosing = false }: SpotlightProps): ReactNode { else if (filtered.length > 0) value = filtered[0]!.value } - const isLastArg = argMode.argIndex >= args.length - 1 - - if (isLastArg) { - // All args filled — run the command - const fullArgs = { ...argMode.values, [currentArg.id]: value } - dispatch({ type: 'EXIT_ARG_MODE' }) - void runCommandWithArgs(argMode.command, fullArgs) - } else { - dispatch({ type: 'SAVE_ARG_AND_ADVANCE', argId: currentArg.id, value }) - } + selectArg(value) break } @@ -302,6 +293,24 @@ export function Spotlight({ isClosing = false }: SpotlightProps): ReactNode { void runCommand(cmd) } + // ─── Arg selection (shared by keyboard Enter + mouse click) ──────────────── + // Saves the given arg value and either advances to the next arg or, when the + // last arg is filled, exits arg mode and runs the command. + const selectArg = (value: string) => { + if (!dispatch || !runCommandWithArgs || !argMode) return + const args = argMode.command.args ?? [] + const currentArg = args[argMode.argIndex] + if (!currentArg) return + const isLastArg = argMode.argIndex >= args.length - 1 + if (isLastArg) { + const fullArgs = { ...argMode.values, [currentArg.id]: value } + dispatch({ type: 'EXIT_ARG_MODE' }) + void runCommandWithArgs(argMode.command, fullArgs) + } else { + dispatch({ type: 'SAVE_ARG_AND_ADVANCE', argId: currentArg.id, value }) + } + } + // Derive input placeholder const placeholder = (() => { if (argMode) { @@ -391,6 +400,7 @@ export function Spotlight({ isClosing = false }: SpotlightProps): ReactNode { listboxId={listboxId} highlightedIndex={highlightedIndex} onHighlightChange={handleHighlightChange} + onSelectArg={selectArg} onRun={handleRun} activeScopeId={activeScopeId} /> diff --git a/src/admin/spotlight/SpotlightResults.tsx b/src/admin/spotlight/SpotlightResults.tsx index 3d2842c20..539a6badd 100644 --- a/src/admin/spotlight/SpotlightResults.tsx +++ b/src/admin/spotlight/SpotlightResults.tsx @@ -92,11 +92,13 @@ function ArgModeResults({ query, highlightedIndex, onHighlightChange, + onSelectArg, }: { argMode: ArgModeState query: string highlightedIndex: number onHighlightChange: (index: number) => void + onSelectArg: (value: string) => void }): ReactNode { const args = argMode.command.args ?? [] const currentArg = args[argMode.argIndex] @@ -156,6 +158,7 @@ function ArgModeResults({ role="option" aria-selected={idx === highlightedIndex} onMouseEnter={() => onHighlightChange(idx)} + onClick={() => onSelectArg(opt.value)} > @@ -205,6 +208,7 @@ interface SpotlightResultsProps { listboxId: string highlightedIndex: number onHighlightChange: (index: number) => void + onSelectArg: (value: string) => void onRun: (command: Command) => void activeScopeId: string } @@ -213,6 +217,7 @@ export function SpotlightResults({ listboxId, highlightedIndex, onHighlightChange, + onSelectArg, onRun, activeScopeId, }: SpotlightResultsProps): ReactNode { @@ -326,6 +331,7 @@ export function SpotlightResults({ query={query} highlightedIndex={highlightedIndex} onHighlightChange={onHighlightChange} + onSelectArg={onSelectArg} /> ) } diff --git a/src/admin/spotlight/__tests__/SpotlightResults.test.tsx b/src/admin/spotlight/__tests__/SpotlightResults.test.tsx index 021634fb9..0eacf106d 100644 --- a/src/admin/spotlight/__tests__/SpotlightResults.test.tsx +++ b/src/admin/spotlight/__tests__/SpotlightResults.test.tsx @@ -1,8 +1,9 @@ import { afterEach, describe, expect, it, mock } from 'bun:test' import React from 'react' -import { cleanup, render, waitFor } from '@testing-library/react' +import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import { SpotlightInternalContext, type SpotlightInternalContextValue } from '../spotlightContext' import { SpotlightResults } from '../SpotlightResults' +import type { Command } from '../types' import type { SpotlightOpenState } from '../state' afterEach(() => { @@ -66,4 +67,53 @@ describe('SpotlightResults', () => { HTMLElement.prototype.scrollIntoView = originalScrollIntoView } }) + + it('selects a select-type arg value when its option row is clicked', () => { + const onSelectArg = mock(() => {}) + const command: Command = { + id: 'test.cmd', + title: 'Test command', + group: 'plugins', + args: [ + { + id: 'tone', + label: 'Tone', + type: 'select', + options: [ + { value: 'formal', label: 'Formal' }, + { value: 'casual', label: 'Casual' }, + ], + }, + ], + } + + const context: SpotlightInternalContextValue = { + state: { + ...makeOpenState(0), + argMode: { command, argIndex: 0, values: {} }, + }, + dispatch: () => {}, + commandContext: null, + runCommand: async () => {}, + runCommandWithArgs: async () => {}, + } + + render( + + {}} + onSelectArg={onSelectArg} + onRun={() => {}} + activeScopeId="root" + /> + , + ) + + fireEvent.click(screen.getByText('Casual')) + + expect(onSelectArg).toHaveBeenCalledTimes(1) + expect(onSelectArg).toHaveBeenCalledWith('casual') + }) })