focus: restore body focus on overlay close, test shield, ESC closes results

Focus restoration:
* editor.js closeModal(): blur activeElement after the close so board
  shortcuts (arrows, bare keys) work immediately without clicking.
* detail.js closeDetail(): same blur, skipped when openedFromSearch
  (search input should keep focus in that case).
* search.js closeSearch(): blur after removing .open.
* calendar.js closeCalendar(): blur after closeDayPopup.
All four check activeElement !== body before blurring to avoid no-ops.

Test shield (loader.js):
* ensureProgressBar() now also creates #test-shield — a transparent
  fixed overlay (z-index one below the progress bar) covering the
  viewport below the bar. Blocks all real mouse interaction while
  tests run; synthetic events from the test suite still work because
  they bypass pointer-events. cursor:not-allowed as visual feedback.
  Removed by hideProgress() alongside the progress bar.

ESC closes test results (runner.js):
* The existing keydown listener now also handles Escape when the
  test-panel is visible. stopPropagation prevents the app's Escape
  chain from firing a second close underneath.

Tests (board.test.js):
* New describe "Board: Focus restoration after overlay close":
  - closing the editor returns focus to body
  - closing the detail view returns focus to body
  - closing search returns focus to body
  - arrow keys work after editor round-trip (enter → enter → escape)
    — the exact flow the user reported as broken. Dispatches
    ArrowDown → Enter (detail) → Enter (editor) → Escape (close) →
    asserts body focus → ArrowDown → asserts .card-focused exists.
* Fixed .not.toBe() → (x !== y).toBe(true) since the custom runner
  doesn't have a .not chain.

chore: bump version to 0.9.14

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.6 committed 2026-04-12 11:02:27 +02:00
1 parent b459159b0f
commit 88108ecc1b
8 files changed
+101 -5

No files matched your search

+60
View File
@@ -124,3 +124,63 @@ describe('Board: Card Create & Delete', () => {
}
});
});
describe('Board: Focus restoration after overlay close', () => {
afterEach(() => closeAll());
it('closing the editor returns focus to body', async () => {
click('#add-btn');
await sleep(200);
// Editor is open — focus should be inside the modal (title input)
expect(document.activeElement !== document.body).toBe(true);
click('#modal-cancel');
await sleep(200);
expect(document.activeElement).toBe(document.body);
});
it('closing the detail view returns focus to body', async () => {
const card = $('#board [data-id]');
if (!card) return;
click(card);
await sleep(300);
if (!$('#detail')?.classList.contains('open')) return;
click('#detail-close');
await sleep(200);
expect(document.activeElement).toBe(document.body);
});
it('closing search returns focus to body', async () => {
click('#search-btn');
await sleep(200);
if (!$('#search')?.classList.contains('open')) return;
expect(document.activeElement?.id).toBe('search-input');
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
await sleep(200);
expect(document.activeElement).toBe(document.body);
});
it('arrow keys work after editor round-trip (enter → enter → escape)', async () => {
const card = $('#board [data-id]');
if (!card) return;
// Focus a card
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
await sleep(100);
if (!document.querySelector('#board .card-focused')) return;
// Enter → detail
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
await sleep(300);
if (!$('#detail')?.classList.contains('open')) return;
// Enter → close detail, open editor
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
await sleep(300);
if (!$('#modal')?.classList.contains('open')) return;
// Escape → close editor
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
await sleep(200);
expect(document.activeElement).toBe(document.body);
// Arrows should work again
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
await sleep(100);
expect(document.querySelector('#board .card-focused')).toExist();
});
});