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

Proposal: getLeafTarget() method #624

Open
polywock opened this issue May 26, 2024 · 7 comments
Open

Proposal: getLeafTarget() method #624

polywock opened this issue May 26, 2024 · 7 comments
Assignees
Labels
follow-up: chrome Needs a response from a Chrome representative needs-triage: chrome Chrome needs to assess this issue for the first time supportive: firefox Supportive from Firefox supportive: safari Supportive from Safari

Comments

@polywock
Copy link

polywock commented May 26, 2024

Context

Normally, when an event is fired, event.target returns the element on which the event occurred. However, when the event originates inside a Shadow DOM, event.target only provides the first shadow host element, not the actual element inside the Shadow DOM where the event originated.

Current Workarounds and Their Limitations

To access the true event target, you can add event listeners directly to the Shadow DOMs. However, this approach has two significant drawbacks:

  1. Unreliablilty: Adding listeners directly to Shadow DOMs is unreliable on websites that liberally use stopPropagation().
  2. Complexity and Performance Issues: Modern websites, like Reddit, can have hundreds of Shadow DOMs with varying degrees of nesting. Efficiently managing these listeners can be challenging and may have performance implications. (@tophf highlighted this limitation).

Proposal

I propose a getLeafTarget(event: Event): Element function to solve this problem. It takes an event as an argument and returns the actual, or leaf, target of the event. With this function, a single event listener attached to the window object can replace the need for hundreds of event listeners attached to individual Shadow DOMs.

Example Use Case

Translate any button's text on mouse hover. Using getLeafTarget allows the extension to work on websites that use Shadow DOMs. To replicate this functionality without getLeafTarget will require much, much, more work.

window.addEventListener("mouseover", event => {
    const target = browser.dom.getLeafTarget(event);     
    if (target.tagName === "BUTTON") {
        target.textContent = translate(target.textContent);
    }
}, { capture: true });
@github-actions github-actions bot added needs-triage: chrome Chrome needs to assess this issue for the first time needs-triage: firefox Firefox needs to assess this issue for the first time needs-triage: safari Safari needs to assess this issue for the first time labels May 26, 2024
@tophf
Copy link

tophf commented May 26, 2024

this isn't a great workaround [...]

Particularly in case of [deeply] nested shadows, e.g. to intercept mouseover and mousemove event I have to recursively attach listeners to every hovered shadow and detach them on mouseleave to avoid having thousands of listeners.

I wonder if this is something that should be solved in the web platform? I mean maybe they didn't do it initially because custom elements were expected to be simple and self-contained, but now they are used as freely as standard elements and contain complex layouts, which makes event delegation desirable to reduce the amount of listeners.

@Rob--W
Copy link
Member

Rob--W commented Jun 6, 2024

Independently of extensions, by design shadow DOM comes in two flavors, closed and open shadow DOM.

If the shadow root was created with mode: "open", you can use event.composedPath() to get the information you're after, where the first element in the array may be the DOM element you're looking for.

Closed shadow DOM is supposed to represent a self-contained piece of DOM structure. In reality, web pages can of course use it for anything and embed complicated DOM trees.

What are the use cases of wanting to reach deep into closed shadow DOM?

@polywock
Copy link
Author

polywock commented Jun 6, 2024

What are the use cases of wanting to reach deep into closed shadow DOM?

Most are already served by openOrClosedShadowRoot, but getLeafEvent() offers a simpler approach.

Use case 1

If an extension implements shortcut keys using keyboard events, it's best practice to not trigger when the event target is an input element, textarea, etc. That way if the user is trying to type, the extension's shortcut won't be triggered.

Using getLeafEvent here instead of just event.target takes into account situations where the user is typing into elements that are inside shadow DOMs.

window.addEventListener("keyup", e => {
    const target = browser.dom.getLeafEvent(e.target)
    if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") return 
    
     if (e.code === "Space") {      
           activateShortcut() 
           e.stopPropagation(). 
           e.preventDefault()  
    }
}, {capture: true})

Use case 2

I wanted to show an overlay to interactively change the video's brightness.

  • To avoid the page's style from conflicting with mine, I've placed the overlay inside a shadow DOM. An IFRAME element wasn't a good fit because I needed a transparent background.
  • When the user hovered over the Reset button, I wanted the brightness to reset back to normal and overlay to close. But, I noticed the pointerover event wasn't being triggered on some websites who were stopping event propagation before it reached my listener.
  • Adding the listener to the window object and turning the shadow to "open" mode allowed me to use event.composedPath(), but that resulted in more issues as many dark mode extensions inject styling on all open shadow roots. I know of ways to work around that (CSS priority hacks), but I still wanted a closed shadow for better isolation from the page.
  • I decided to rely on using a recursive elementsFromPoint + openOrClosedShadowRoot approach. I was satisfied with this approach, but it required a lot of trial and error, and it's not the most elegant solution.
  • If getLeafTarget() existed, it would've taken me a tenth of the time with half the code.

Use case 3

Translating text while hovering over it. If the text is inside a closed shadow DOM, you won't be able to detect the text target unless you recursively add listeners to all shadow DOMs (as @tophf mentioned, this approach is complicated and can have memory implications). In addition, if event propagation was stopped, there will be no way for the extension to access the text target.

@fregante
Copy link

fregante commented Jun 8, 2024

What are the use cases of wanting to reach deep into closed shadow DOM?

I think this is a weird question in the context of extensions. The answer is the same as why you build extensions (and content scripts specifically): to extract, manipulate, extend content.

The page should be fully accessible to extensions regardless of the encapsulation web page authors used — unless it's protected in the name of security. I don’t think that's the case here.

@zombie
Copy link
Collaborator

zombie commented Jul 4, 2024

We would need to confirm with the web platform team how best to expose this, but in general we support solving this use case.

@Rob--W Rob--W added supportive: safari Supportive from Safari follow-up: chrome Needs a response from a Chrome representative and removed needs-triage: safari Safari needs to assess this issue for the first time labels Jul 4, 2024
@Rob--W
Copy link
Member

Rob--W commented Jul 4, 2024

@xeenon expressed being supportive for this capability in the meeting. @oliverdunk will follow up with Chrome.

@Rob--W
Copy link
Member

Rob--W commented Sep 28, 2024

Status as of TPAC 2024 (#659) is still for Oliver to follow up with Chrome to see the preferred approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
follow-up: chrome Needs a response from a Chrome representative needs-triage: chrome Chrome needs to assess this issue for the first time supportive: firefox Supportive from Firefox supportive: safari Supportive from Safari
Projects
None yet
Development

No branches or pull requests

6 participants