fix(tiles): no tile onto an empty slot, header focus on click, arrow chords skip text fields

The owner's answers on moving tiles:

- "dont move the tile": an empty slot no longer takes a tile. A slot is
  always the last cell, so a move there shifted every tile after it.
  Each drop target now says what it accepts (_acceptTabDrops'
  `accepts`): a tile takes any session but its own, an empty slot only a
  session not tiled yet. A refused drag is still held (dropEffect none,
  no highlight), and dropSessionOnSlot refuses a tiled session too, its
  tab included. A session not tiled yet still joins on a slot.
- A cancelled drag changes nothing, focus included (best practice): the
  header focuses its tile on click, never on press, so a drag that ends
  with Escape or outside leaves focus and the idle alert alone. The body
  keeps press-to-focus, so focus still moves before a press reaches
  xterm. The rename input stops its own clicks.
- A tiled tab dropped on the zoomed tile stays refused (confirmed).
- The Alt+Shift+Arrow focus chords skip a text field too (best
  practice), as the move chords already did: shifted arrows select
  there. Toggle and zoom are not text-editing keys and are unchanged.

Docs: the wiki, the spec's as-built bullet (with the owner's answers),
the invariants and CLAUDE.md say so.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-08 01:28:20 +02:00
parent adeb22d7b2
commit aecada8c56
9 changed files with 217 additions and 84 deletions
+22
View File
@@ -116,6 +116,28 @@ describe('when a chord applies', () => {
expect(app.tileShortcutFor(chord(RIGHT))).toBe('focus-tile-right');
});
it('the focus chords leave a text field its keys (shifted arrows select there); a terminal still gets them', () => {
const app = makeGridApp(IDS);
app.openTileGrid(IDS);
for (const tagName of ['INPUT', 'TEXTAREA']) {
const target = { tagName, closest: () => null, classList: { contains: () => false } };
expect(app.tileShortcutFor(chord({ ...RIGHT, target }))).toBeNull();
}
expect(
app.tileShortcutFor(chord({ ...RIGHT, target: { isContentEditable: true, closest: () => null } }))
).toBeNull();
// xterm's own input is a textarea too, and the chord is the grid's there.
const xterm = {
tagName: 'TEXTAREA',
closest: () => null,
classList: { contains: (c: string) => c === 'xterm-helper-textarea' },
};
expect(app.tileShortcutFor(chord({ ...RIGHT, target: xterm }))).toBe('focus-tile-right');
// Not an arrow chord: the toggle and zoom still apply from a field.
const input = { tagName: 'INPUT', closest: () => null, classList: { contains: () => false } };
expect(app.tileShortcutFor(chord({ ...TOGGLE, target: input }))).toBe('toggle-tile-grid');
});
it('plain typing and unrelated chords never match', () => {
const app = makeGridApp(IDS);
app.openTileGrid(IDS);