fix(tabs): no drag while a rail search is active (#580)

From the owner's review of #580 rather than the bot's report: "Drag
while searching: I'd turn it off. A drop is saved for every device
(session order or the tab layout), and where it lands relative to the
rows the search is hiding is something you only see after clearing it."
The PR head left drag on.

Both rail drags now refuse while the search narrows the list: the
grouped rail's pointer drag in _onTabLayoutPointerDown(), and the flat
manual rail's HTML5 drag in its dragstart listener. The flat rail is
refused in the listener, not by flipping `draggable`, because a
keystroke in the box does not re-render the rows, so a cleared search
drags again with the same rows. The sidebar filter box, the header strip
and the keyboard moves (Ctrl+Shift+{ }, the row menu) are unchanged.

Tests: a grouped-rail press during a search starts no drag and one after
clearing does; a flat-rail dragstart during a search is refused and one
after clearing goes through. CLAUDE.md and the Dashboard wiki row say
so.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-10 04:10:05 +02:00
parent 4a94eb9ba7
commit 183efacc93
4 changed files with 68 additions and 2 deletions
+55
View File
@@ -682,6 +682,61 @@ describe('Escape in the search box', () => {
});
});
describe('no drag while a rail search is active', () => {
it('will not start a grouped-rail drag, and starts one again once the search is cleared', () => {
const app = makeApp();
app._fullRenderSessionTabs();
const container = document.getElementById('sessionTabs')!;
expect(container.classList.contains('session-tabs--grouped')).toBe(true);
const press = () => ({
button: 0,
pointerType: 'mouse',
pointerId: 1,
clientX: 10,
clientY: 10,
target: container.querySelector('[data-id="review"]'),
});
app.setTabRailSearch('review');
app._onTabLayoutPointerDown(press(), container);
expect(app._tabLayoutDrag ?? null).toBeNull();
app.clearTabRailSearch();
app._onTabLayoutPointerDown(press(), container);
try {
expect(app._tabLayoutDrag?.source).toEqual({ type: 'ref', ref: { kind: 'session', id: 'review' } });
} finally {
app._cancelTabLayoutPointerDrag(container);
}
});
it('refuses the flat rail drag while searching, though the rows stay bound for after', () => {
const app = makeApp({ tabLayout: null });
app._fullRenderSessionTabs();
const row = () => document.querySelector<HTMLElement>('#sessionTabs [data-id="roadmap"]')!;
expect(row().getAttribute('draggable')).toBe('true');
const dragstart = () => {
const event = new window.Event('dragstart', { bubbles: true, cancelable: true });
Object.defineProperty(event, 'dataTransfer', { value: { effectAllowed: '', setData: () => {} } });
return event;
};
app.setTabRailSearch('road');
const refused = dragstart();
row().dispatchEvent(refused);
expect(refused.defaultPrevented).toBe(true);
expect(app.draggedTabId ?? null).toBeNull();
// A keystroke does not re-render, so the cleared rail drags with the same rows.
app.clearTabRailSearch();
const allowed = dragstart();
row().dispatchEvent(allowed);
expect(allowed.defaultPrevented).toBe(false);
expect(app.draggedTabId).toBe('roadmap');
row().dispatchEvent(new window.Event('dragend', { bubbles: true }));
});
});
describe('connector lines follow the rows a search moves', () => {
it('redraws on a rail keystroke that hides or reveals rows, and only then', () => {
const app = makeApp({ tabLayout: null });