Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Terminal eats click events #5220

Closed
jtbandes opened this issue Nov 20, 2024 · 8 comments · Fixed by #5249
Closed

Terminal eats click events #5220

jtbandes opened this issue Nov 20, 2024 · 8 comments · Fixed by #5249
Labels
Milestone

Comments

@jtbandes
Copy link
Contributor

When adding mousedown, mouseup, and click listeners on the terminal container element, and then clicking on the terminal, I observe that the click is sometimes not delivered. (On macOS with a Magic Trackpad, I noticed that the click event almost always works when using tap to click and tapping with a single finger, but when actually pressing the trackpad to click it, the click event is usually not delivered.)

I suspect this is a clue: right-clicking on the logged event.target and clicking "Reveal in Elements panel" sometimes shows "Node cannot be found in the current page". Perhaps the browser is not delivering the click event because the mousedown/mouseup are not on matching elements, or the clicked element is removed before the click finishes.

image image
Screen.Recording.2024-11-19.at.4.49.26.PM.mov

Details

  • Browser and browser version: Chrome 131.0.6778.70
  • OS version: macOS 15.1
  • xterm.js version: 5.5.0

Note: same behavior seems to happen with 5.6.0 beta as well: https://codesandbox.io/p/sandbox/xtermjs-test-forked-qfng37

Steps to reproduce

https://codesandbox.io/p/sandbox/xtermjs-test-forked-c5s8g4
Or visit https://c5s8g4.csb.app/ and view the console

import "./styles.css";
import "@xterm/xterm/css/xterm.css";
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";

const container = document.getElementById("app");

const terminal = new Terminal({
  cursorStyle: "bar",
  allowProposedApi: true,
});

const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);

terminal.open(container);

for (let i = 0; i < 100; i++) {
  if (i > 0) {
    terminal.write("\r\n");
  }
  terminal.write(
    `line ${i}: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua`
  );
}

const resizeObserver = new ResizeObserver((_entries) => {
  fitAddon.fit();
});
resizeObserver.observe(container);

container.addEventListener("mousedown", (e) => {
  console.log("mousedown", e.target);
});
container.addEventListener("mouseup", (e) => {
  console.log("mouseup", e.target);
});
container.addEventListener("click", (e) => {
  console.log("click", e.target);
});
@Tyriar
Copy link
Member

Tyriar commented Nov 20, 2024

I think we do prevent bubbling sometimes as we handle mouse events in various ways. Seems a bit weird that click works sometimes and sometimes not though.

@jtbandes
Copy link
Contributor Author

Yes, even with a capture event listener, I saw the events were sometimes missing. I think this means xtermjs is not intentionally stopping the events, but they are actually never delivered.

@jtbandes
Copy link
Contributor Author

jtbandes commented Dec 6, 2024

@Tyriar @jerch I discovered that this is because of this refresh() call inside SelectionSerivce.handleMouseDown:

This ends up calling DomRenderer.renderRows() which uses replaceChildren():

rowElement.replaceChildren(
...this._rowFactory.createRow(

It turns out that when a child is replaced during a click, the browser does not fire a click event (tested in Chrome, Safari, and Firefox with the same results): https://codesandbox.io/p/sandbox/click-event-test-yytwry / https://yytwry.csb.app/

You can see that clicking the background (app) fires a click event, while clicking the child does not, if the child is replaced:

Screen.Recording.2024-12-06.at.2.51.43.PM.mov

However, I'm not sure what else to do if calling refresh() here is a problem? Perhaps we need to try changing the elements' text content without fully replacing them? (Do you know if there's a reason replaceChildren was used?)

@Tyriar
Copy link
Member

Tyriar commented Dec 12, 2024

replaceChildren is pretty critical to how it works, changing it would probably be a bunch of work. Perhaps in order to keep events we need to do the replace in a queueMicrotask or setTimeout(..., 0)?

@jerch
Copy link
Member

jerch commented Dec 12, 2024

replaceChildren is pretty critical to how it works, changing it would probably be a bunch of work. Perhaps in order to keep events we need to do the replace in a queueMicrotask or setTimeout(..., 0)?

Yeah, I think we cannot get rid of replaceChildren at all, as the line content changes shape based on markup and link handling here.

I wonder though, if we could let the event bubble through before the actual content change happens. 🤔

@jtbandes
Copy link
Contributor Author

I think that would mean waiting until the click event (or mousemove?) to render the new selection. That might work if you are willing to make that change.

Another idea - maybe some well placed pointer-events: none would help to change which element is the event target, so it’s not one of the elements being replaced?

@Tyriar
Copy link
Member

Tyriar commented Dec 12, 2024

Another idea - maybe some well placed pointer-events: none would help to change which element is the event target, so it’s not one of the elements being replaced?

This might work actually, I don't think we depend on any click events in .xterm-rows as that doesn't exist when the webgl addon is used.

@jtbandes
Copy link
Contributor Author

It seems to work 🙏 🎉 #5249

@Tyriar Tyriar added this to the 6.0.0 milestone Dec 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants