WICG / webcomponents

Web Components specifications
Other
4.38k stars 371 forks source link

There is no way to navigate to a fragment inside a shadow tree #924

Open acarlstein opened 3 years ago

acarlstein commented 3 years ago

Example

Let's assume you have this Web Component:

let template = `
    <div>
        <a href="#message">Make the message red</a>
        <h2 id="message">Message in a bottle</h2>
    </div>
    <style>
        #message:target {
            color: red;
        }
    </style>
`;

customElements.define('my-component', class myElement extends HTMLElement {
    constructor() {
        super();
        let shadowRoot = this.attachShadow({ mode: 'open' });
        shadowRoot.innerHTML = template;
    }
});

Problem

This code will fail because the event :target is missing in the root of the shadow.

Also, since it isn't in the specifications, there are not signs that other browsers will fix this issue.

Expectations

I was expecting to see the header turn to red when clicking on the link. This works outside the web component but fails to do so inside the web component.

rniwa commented 3 years ago

This was previously discussed in https://github.com/WICG/webcomponents/issues/66.

acarlstein commented 3 years ago

@rniwa , the discussion on #66 is about navigate to a fragment inside a shadow tree. This is not my case. My case is a pure CSS trick that doesn't work inside the Web Component when it should.

For example, by clicking a link, I could change the color of a header: Before: image After: image

Here is the code:

<style>
  #message:target { color: red; }
</style>
 <body>
    <a href="#message">Make the message red</a>
    <h2 id="message">Message in a bottle</h2>
 </body>

Another example, I could create a toolbar in which the function selection remains active: image

However, since target isn't part of the root of shadow-root, this feature isn't available. This pure CSS trick doesn't work inside the Web Component.

annevk commented 3 years ago

It's not a pure CSS trick, you are literally navigating to a fragment and that fragment's corresponding ID cannot be found as its inside a shadow tree.

annevk commented 3 years ago

Solving this would need something like delegatesIds = [ list of IDs ].

acarlstein commented 3 years ago

It's not a pure CSS trick, you are literally navigating to a fragment and that fragment's corresponding ID cannot be found as its inside a shadow tree.

@annevk , I called it "trick" to make it easy to understand; however, the fact is that the code below works fine outside the Web Component:

<style>
  #message:target {
    color: red;
  }
</style>
<div>
  <a href="#message">Make the message red</a>
  <h2 id="message">Message in a bottle</h2>
</div>

The header changes colors when the link is click due :target. However, when moved this code into a component itself it fails to do so.

It works fine until its move into a Web Component fails because the target isn't there in the shadow-root.

I understand that Web Components should be encapsulated and outside links shouldn't be allowed to navigate to the fragment; however, the selector :target shouldn't be affected.

This piece of the CSS code, #message:target is affected. So, at least there should be an alternative than having to write extra JS code to obtain the same effect.

rniwa commented 3 years ago

@rniwa , the discussion on #66 is about navigate to a fragment inside a shadow tree. This is not my case. My case is a pure CSS trick that doesn't work inside the Web Component when it should.

No, you're misunderstanding the cause of the problem. The fragment navigation is precisely what's missing & what's causing :target to never apply inside a shadow tree.

Please go read the definition of :target before leaving any new comments.

acarlstein commented 3 years ago

@rniwa , the discussion on #66 is about navigate to a fragment inside a shadow tree. This is not my case. My case is a pure CSS trick that doesn't work inside the Web Component when it should.

No, you're misunderstanding the cause of the problem. The fragment navigation is precisely what's missing & what's causing :target to never apply inside a shadow tree.

Please go read the definition of :target before leaving any new comments.

Thank you. That's is what I'm trying to say which means there is a bug because that piece is missing. If you search for the documentation on the CSS pseudo-class, there is nothing that says "It doesn't work in Web Components by the way"

So my point is, if there is a decision to not include a fragment navigation, at least put something in place so the code I shared doesn't break.

rniwa commented 3 years ago

That's is what I'm trying to say which means there is a bug because that piece is missing. If you search for the documentation on the CSS pseudo-class, there is nothing that says "It doesn't work in Web Components by the way"

I don't think this is a bug per se. It's more of a missing feature. This is very much the intended behavior. In general, id references work within each tree, not across shadow boundaries. For example, document.getElementsById(id) will only find elements with the specified id in the document tree, not any of the shadow trees connected to it.

Jamesernator commented 3 years ago

Solving this would need something like delegatesIds = [ list of IDs ].

The downside of something like this is that the parent tree might not be aware of all ids being exposed via all shadow roots, this would make it easy to potentially have conflicting ids.

An alternative would be to just allow associating an id to a given shadow ::part e.g.:

<style>
  /* Ideally works, although using it .querySelector wouldn't select anything
     as it's in a shadow root, so this should arguably be a pseudo-element */
  :target {
    color: red;
  }
</style>

<a href="#myId">Works</a>

<my-element partids="myId:myPart">
  <template shadowroot="closed">
    <div part="myPart">Foo bar!</div>
    <div part="fooPart">Bazz</div>
  </template>
</my-element>

The one problem with parts though is that many elements can have the same part name, although this might not be a big deal as we can already have multiple elements with the same id attribute, if we were being consistent with that we'd just ignore all but the first.

Alternatively again this could even be a part of the url similar to scroll-to-text-fragment, e.g. <a href="#myId:~:part=myPart">, although this would be trickier with CSS (maybe?).

rniwa commented 3 years ago

FWIW, for this kind of API, it's very important that there is an opt-in from both component author & user. We can't pollute the global or outer tree's fragment ID from within shadow trees or let users of a component make random parts of a component addressable via a fragment ID in order to maintain the encapsulation we have at shadow boundaries.

calebdwilliams commented 3 years ago

Feels like if anything the host should have to map the exported id back to another name.

<c-e importnodes="foo as bar">
  <template shadowroot="open">
    <div id="foo" exportnode></div>
  </template>
</c-e>
acarlstein commented 3 years ago

That's is what I'm trying to say which means there is a bug because that piece is missing. If you search for the documentation on the CSS pseudo-class, there is nothing that says "It doesn't work in Web Components by the way"

I don't think this is a bug per se. It's more of a missing feature. This is very much the intended behavior. In general, id references work within each tree, not across shadow boundaries. For example, document.getElementsById(id) will only find elements with the specified id in the document tree, not any of the shadow trees connected to it.

I will present it in a different way...

There no need to cross boundaries. You use the example document.getElementById(id), did you? Then, how about this.shadowRoot.getElementById(id)? Make the :target work inside the shadow boundaries by itself, as a separate :target.

I should be able to copy any HTML and CSS code and have it run inside the shadow without surprises. If there is something that shouldn't cross boundaries, then have two, one for document and one for shadow, but both doing what the documentation says it does.

claviska commented 3 years ago

Make the :target work inside the shadow boundaries by itself, as a separate :target.

I don't think this is possible because it's a breaking change, but I agree with your sentiment:

I should be able to copy any HTML and CSS code and have it run inside the shadow without surprises.

Like @rniwa said earlier, it's less a bug and more a missing feature that would probably need to be implemented as a new selector, e.g. :local-target or :host-target.

keithamus commented 1 year ago

WCCG had their spring F2F in which this was discussed. You can read the full notes of the discussion (https://github.com/WICG/webcomponents/issues/978#issuecomment-1516897276) in which this was discussed, heading entitled "ARIA Mixin & Cross Root ARIA" - where this issue was specifically discussed.

In the meeting, present members of WCCG reached a consensus to discuss further in breakout sessions. I'd like to call out that https://github.com/WICG/webcomponents/issues/1005 is the tracking issue for that breakout, in which this will likely be discussed.