spotlight opens on shift-space, and stands down inside a text field

ctrl-space focuses the address bar in Brave, so it never reached the app.

shift-space is a real combination inside a text field — it types a space, and people hold shift through
one more often than they think while typing capitals. So the trigger is ignored when focus is in an
input, a textarea or anything contenteditable, and the space is typed as normal.

The cost is worth stating rather than discovering: focus usually IS in the chat composer, so opening
Spotlight from there means clicking out first. If that turns out to be the common case, a double-tap of
shift is the escape hatch — it conflicts with nothing anywhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 03:51:57 +00:00
co-authored by Claude Opus 5
parent 81f32a1faa
commit a2cca526f9
@@ -11,14 +11,35 @@ import { useSpotlightItems, type SpotlightItem } from './useSpotlightItems';
//
// ── Keyboard first, and the trigger ──
//
// `ctrl-space`. `alt-space` is the more macOS-shaped chord and is already load-bearing for the owner
// elsewhere, so it is deliberately not taken. `cmd-k` is not used either: it belongs to the browser's own
// surface on some platforms and to text fields on others.
// `shift-space`. The two obvious chords were both taken: `ctrl-space` focuses the address bar in Brave,
// and `alt-space` is load-bearing for the owner elsewhere. `cmd-k` belongs to the browser on some
// platforms and to text fields on others.
//
// **Shift-space is a real key combination inside a text field** — it types a space, and people hold shift
// through one more often than they think while typing capitals. So the trigger is IGNORED when focus is
// in an input, a textarea or anything contenteditable, and the space is typed as normal.
//
// The cost of that is worth stating: focus usually IS in the chat composer, so opening Spotlight from
// there means clicking out first. If that turns out to be the common case, a double-tap of shift is the
// escape hatch — it conflicts with nothing anywhere.
//
// Escape closes, arrows move, enter goes. The mouse works, but nothing here is designed around it.
const MAX_RESULTS = 8;
/**
* Is the user typing into something?
*
* `shift-space` types a space in any text field, so the trigger has to stand down there or it eats a
* keystroke people make by accident all day. `isContentEditable` covers the editor and any rich input a
* panel brings with it.
*/
function isTyping(target: EventTarget | null): boolean {
const el = target as HTMLElement | null;
if (!el || !el.tagName) return false;
return el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable === true;
}
/** Label with the matched characters picked out, so it is obvious WHY a result is here. */
const Highlighted = ({ label, hits }: { label: string; hits: number[] }) => {
if (!hits.length) return <>{label}</>;
@@ -115,7 +136,7 @@ export const Spotlight = () => {
// anywhere — including inside a panel that has its own key handling.
useEffect(() => {
const onKey = (ev: KeyboardEvent) => {
if (ev.ctrlKey && ev.code === 'Space') {
if (ev.shiftKey && ev.code === 'Space' && !isTyping(ev.target)) {
ev.preventDefault();
setOpen((wasOpen) => !wasOpen);
return;