diff --git a/src/web/public/tile-grid.js b/src/web/public/tile-grid.js index 84bfc7c8..73b7a1e6 100644 --- a/src/web/public/tile-grid.js +++ b/src/web/public/tile-grid.js @@ -375,10 +375,8 @@ Object.assign(CodemanApp.prototype, { * replaces its set. */ openTilePicker(event) { - // As the split picker: the opening event must not reach the outside-click - // listener this call installs. + // The right-click: the browser's own context menu stays away. event?.preventDefault?.(); - event?.stopPropagation?.(); if (this._tilePicker) { this.closeTilePicker(); return; @@ -472,18 +470,17 @@ Object.assign(CodemanApp.prototype, { menu.style.top = `${rect.bottom + 4}px`; menu.style.right = `${window.innerWidth - rect.right}px`; } + // A click elsewhere or Escape closes it. A click on the Tiles button needs no + // exception: its own handler (toggleTileGrid) closes the picker first. const onOutside = (e) => { - if (menu.contains?.(e.target) || e.target?.closest?.('.btn-tile-grid')) return; + if (menu.contains?.(e.target)) return; this.closeTilePicker(); }; const onKey = (e) => { if (e.key === 'Escape') this.closeTilePicker(); }; this._tilePicker = { menu, onOutside, onKey }; - // Deferred a tick so the opening click (still bubbling) does not close it. - setTimeout(() => { - if (this._tilePicker?.menu === menu) document.addEventListener('click', onOutside); - }, 0); + document.addEventListener('click', onOutside); document.addEventListener('keydown', onKey); (boxes.find((b) => !b.disabled) || open).focus?.(); }, diff --git a/test/tile-grid-open-set.test.ts b/test/tile-grid-open-set.test.ts index f44769df..de4775dc 100644 --- a/test/tile-grid-open-set.test.ts +++ b/test/tile-grid-open-set.test.ts @@ -18,7 +18,14 @@ import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import vm from 'node:vm'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { FakeEl, body, bySelector, makeGridApp, resetGridHarness } from './mocks/tile-grid-vm.js'; +import { + FakeEl, + body, + bySelector, + documentAddEventListener, + makeGridApp, + resetGridHarness, +} from './mocks/tile-grid-vm.js'; type OpenSet = { source: string; ids: string[]; focusedId: string | null } | null; type Helpers = { tileGridOpenSet(p: Record): OpenSet; TILE_GRID_MAX: number }; @@ -137,6 +144,18 @@ describe('opening at once, in the app', () => { expect(app._tilesOwnTerminal()).toBe(true); }); + it('a click elsewhere closes the picker; a click inside it does not', () => { + const app = makeGridApp(IDS); + const before = documentAddEventListener.mock.calls.length; + app.openTilePicker({ preventDefault: vi.fn() }); + const calls = documentAddEventListener.mock.calls.slice(before) as Array<[string, (e: unknown) => void]>; + const onClick = calls.find(([type]) => type === 'click')![1]; + onClick({ target: picker()!.children[0] }); + expect(picker()).not.toBeNull(); + onClick({ target: new FakeEl() }); + expect(picker()).toBeNull(); + }); + it('a right-click opens the picker and keeps the browser menu away', () => { const app = makeGridApp(IDS); const ev = { preventDefault: vi.fn(), stopPropagation: vi.fn() };