mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
refactor(tiles): the picker's outside-click close loses its left-click leftovers
The Tiles picker opens on right-click now. Three pieces only served the left-click picker: stopPropagation on the opening event, the tick of delay before the outside-click listener went in (so the opening click could not close it), and the exception for clicks on the Tiles button. A right-click fires no click event, and a left click on the button runs toggleTileGrid, which closes the picker before the click reaches the document. The preventDefault that keeps the browser menu away stays. The outside-click close had no test: tile-grid-open-set now checks that a click inside the picker leaves it open and one elsewhere closes it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -375,10 +375,8 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
* replaces its set.
|
* replaces its set.
|
||||||
*/
|
*/
|
||||||
openTilePicker(event) {
|
openTilePicker(event) {
|
||||||
// As the split picker: the opening event must not reach the outside-click
|
// The right-click: the browser's own context menu stays away.
|
||||||
// listener this call installs.
|
|
||||||
event?.preventDefault?.();
|
event?.preventDefault?.();
|
||||||
event?.stopPropagation?.();
|
|
||||||
if (this._tilePicker) {
|
if (this._tilePicker) {
|
||||||
this.closeTilePicker();
|
this.closeTilePicker();
|
||||||
return;
|
return;
|
||||||
@@ -472,18 +470,17 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
menu.style.top = `${rect.bottom + 4}px`;
|
menu.style.top = `${rect.bottom + 4}px`;
|
||||||
menu.style.right = `${window.innerWidth - rect.right}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) => {
|
const onOutside = (e) => {
|
||||||
if (menu.contains?.(e.target) || e.target?.closest?.('.btn-tile-grid')) return;
|
if (menu.contains?.(e.target)) return;
|
||||||
this.closeTilePicker();
|
this.closeTilePicker();
|
||||||
};
|
};
|
||||||
const onKey = (e) => {
|
const onKey = (e) => {
|
||||||
if (e.key === 'Escape') this.closeTilePicker();
|
if (e.key === 'Escape') this.closeTilePicker();
|
||||||
};
|
};
|
||||||
this._tilePicker = { menu, onOutside, onKey };
|
this._tilePicker = { menu, onOutside, onKey };
|
||||||
// Deferred a tick so the opening click (still bubbling) does not close it.
|
document.addEventListener('click', onOutside);
|
||||||
setTimeout(() => {
|
|
||||||
if (this._tilePicker?.menu === menu) document.addEventListener('click', onOutside);
|
|
||||||
}, 0);
|
|
||||||
document.addEventListener('keydown', onKey);
|
document.addEventListener('keydown', onKey);
|
||||||
(boxes.find((b) => !b.disabled) || open).focus?.();
|
(boxes.find((b) => !b.disabled) || open).focus?.();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,7 +18,14 @@ import { readFileSync } from 'node:fs';
|
|||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import vm from 'node:vm';
|
import vm from 'node:vm';
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
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 OpenSet = { source: string; ids: string[]; focusedId: string | null } | null;
|
||||||
type Helpers = { tileGridOpenSet(p: Record<string, unknown>): OpenSet; TILE_GRID_MAX: number };
|
type Helpers = { tileGridOpenSet(p: Record<string, unknown>): OpenSet; TILE_GRID_MAX: number };
|
||||||
@@ -137,6 +144,18 @@ describe('opening at once, in the app', () => {
|
|||||||
expect(app._tilesOwnTerminal()).toBe(true);
|
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', () => {
|
it('a right-click opens the picker and keeps the browser menu away', () => {
|
||||||
const app = makeGridApp(IDS);
|
const app = makeGridApp(IDS);
|
||||||
const ev = { preventDefault: vi.fn(), stopPropagation: vi.fn() };
|
const ev = { preventDefault: vi.fn(), stopPropagation: vi.fn() };
|
||||||
|
|||||||
Reference in New Issue
Block a user