fix(ext/popup): don't eat '/' and other keystrokes while typing in inputs

Bug: item-list's global "/" shortcut (focus search) and "+" shortcut
(new item) fired even when focus was inside any input/textarea other
than the list's own search field. This ate forward-slashes typed into
the setup wizard's host-url field and the add/edit form's notes area,
and would have done the same for any printable shortcut in a future
text field.

Root cause: the handler was attached to `document`, stays attached
when the user opens an item (and its click-handler navigated without
removing the listener), and only excluded the search field by id.

Fix:
- Add isEditableTarget() helper — returns true for
  INPUT/TEXTAREA/SELECT and contenteditable elements. Global shortcut
  handlers bail early when this fires, passing the keystroke through
  to the field.
- Apply the same guard in item-detail.ts (previously only guarded
  against INPUT, missing TEXTAREA + contenteditable).
- Remove handleListKeydown on row-click so it doesn't linger on
  detail/edit views even before the route-transition keydown
  listeners install.
- Escape in the list view still works from inside an editable
  field — only the printable-character interceptions are gated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-22 19:43:43 -04:00
parent 69bb58c977
commit 357455d979
2 changed files with 32 additions and 1 deletions

View File

@@ -207,7 +207,13 @@ function renderLogin(app: HTMLElement, item: Item): void {
// --- Keyboard shortcuts --- // --- Keyboard shortcuts ---
const handler = async (e: KeyboardEvent) => { const handler = async (e: KeyboardEvent) => {
if ((e.target as HTMLElement).tagName === 'INPUT') return; // Bail if the user is typing into any editable field — don't steal
// printable keystrokes meant for an input/textarea/contenteditable element.
const t = e.target;
if (t instanceof HTMLElement) {
const tag = t.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || t.isContentEditable) return;
}
switch (e.key) { switch (e.key) {
case 'Escape': case 'Escape':

View File

@@ -97,6 +97,7 @@ export function renderItemList(app: HTMLElement): void {
rows.forEach(row => { rows.forEach(row => {
row.addEventListener('click', async () => { row.addEventListener('click', async () => {
const id = (row as HTMLElement).dataset.id!; const id = (row as HTMLElement).dataset.id!;
document.removeEventListener('keydown', handleListKeydown);
await openItem(id); await openItem(id);
}); });
}); });
@@ -141,11 +142,35 @@ function getFilteredEntries(): Array<[ItemId, ManifestEntry]> {
return filtered; return filtered;
} }
/// True if the event target is an editable field (input/textarea/contenteditable).
/// Global shortcut handlers should bail when the user is typing into a field —
/// otherwise printable characters like "/" and "+" get eaten by the shortcut
/// routing and never reach the input.
function isEditableTarget(target: EventTarget | null): boolean {
if (!(target instanceof HTMLElement)) return false;
const tag = target.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
if (target.isContentEditable) return true;
return false;
}
function handleListKeydown(e: KeyboardEvent): void { function handleListKeydown(e: KeyboardEvent): void {
const state = getState(); const state = getState();
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
const isSearch = target.id === 'search-input'; const isSearch = target.id === 'search-input';
// If the user is typing into any input/textarea (other than the list's own
// search field, which we want to focus on "/" even from outside it), let the
// keystroke through. The "/" shortcut below is specifically "jump to search
// from the list," not "steal printable characters while typing."
if (isEditableTarget(target) && !isSearch) {
if (e.key === 'Escape') {
document.removeEventListener('keydown', handleListKeydown);
window.close();
}
return;
}
if (e.key === '/' && !isSearch) { if (e.key === '/' && !isSearch) {
e.preventDefault(); e.preventDefault();
(document.getElementById('search-input') as HTMLInputElement | null)?.focus(); (document.getElementById('search-input') as HTMLInputElement | null)?.focus();