From 2db173c51291aa30b6d1f31eafdda17f37366df8 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Tue, 18 Nov 2025 06:12:44 +0000 Subject: [PATCH] fix: clear text selection when clicking outside canvas Previously, when users highlighted text and clicked outside the terminal canvas (e.g., on the page background), the selection would persist indefinitely. This was confusing as there was no visual feedback that the selection was still active. This fix adds a document-level click event listener that detects clicks outside the canvas and automatically clears the selection. The listener is properly cleaned up in the dispose() method to prevent memory leaks. Changes: - Added boundClickHandler to track document click listener - Implemented click-outside detection using canvas.contains() - Added proper cleanup in dispose() method - Added test case for click-outside behavior The fix maintains the existing behavior where clicking ON the canvas starts a new selection (which already cleared the previous selection). --- lib/selection-manager.ts | 22 ++++++++++++++++++++++ lib/terminal.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/lib/selection-manager.ts b/lib/selection-manager.ts index 14af597..0b21365 100644 --- a/lib/selection-manager.ts +++ b/lib/selection-manager.ts @@ -51,6 +51,7 @@ export class SelectionManager { // Store bound event handlers for cleanup private boundMouseUpHandler: ((e: MouseEvent) => void) | null = null; private boundContextMenuHandler: ((e: MouseEvent) => void) | null = null; + private boundClickHandler: ((e: MouseEvent) => void) | null = null; constructor( terminal: Terminal, @@ -291,6 +292,12 @@ export class SelectionManager { this.boundContextMenuHandler = null; } + // Clean up document click listener + if (this.boundClickHandler) { + document.removeEventListener('click', this.boundClickHandler); + this.boundClickHandler = null; + } + // Canvas event listeners will be cleaned up when canvas is removed from DOM } @@ -444,6 +451,21 @@ export class SelectionManager { }; canvas.addEventListener('contextmenu', this.boundContextMenuHandler); + + // Click outside canvas - clear selection + // This allows users to deselect by clicking anywhere outside the terminal + this.boundClickHandler = (e: MouseEvent) => { + // Check if the click is outside the canvas + const target = e.target as Node; + if (!canvas.contains(target)) { + // Clicked outside the canvas - clear selection + if (this.hasSelection()) { + this.clearSelection(); + } + } + }; + + document.addEventListener('click', this.boundClickHandler); } /** diff --git a/lib/terminal.test.ts b/lib/terminal.test.ts index a465ae3..0a44e97 100644 --- a/lib/terminal.test.ts +++ b/lib/terminal.test.ts @@ -666,6 +666,29 @@ describe('select()', () => { expect(fired).toBe(true); term.dispose(); }); + + test('should clear selection when clicking outside canvas', async () => { + const term = new Terminal({ cols: 80, rows: 24 }); + // Using shared container from beforeEach + if (!container) return; + await term.open(container); + + // Create a selection + term.select(0, 0, 10); + expect(term.hasSelection()).toBe(true); + + // Simulate click outside the canvas (on document body) + const clickEvent = new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window, + }); + document.body.dispatchEvent(clickEvent); + + // Selection should be cleared + expect(term.hasSelection()).toBe(false); + term.dispose(); + }); }); });