w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.44k stars 656 forks source link

[selectors] New pseudo-class that matches the current URL fragment #6942

Open domenic opened 2 years ago

domenic commented 2 years ago

Original issue: https://github.com/WICG/app-history/issues/162; /cc @jakearchibald @stephband.

Currently the :target pseudo-class is defined like so:

The :target pseudo-class matches the document’s target elements.

HTML then defines this based on a target element pointer, which is updated at specific points when the scroll to the fragment algorithm is run during navigation/history traversal.

This approach of updating a pointer at specific points in time is not equivalent to ":target always matches the first document-tree element with an ID corresponding to the document's URL's fragment component". This is true in several regards:

  1. You can update a document's URL's fragment via history.pushState(null, "", "#foo"). (Or by using cursed magics.) This does not trigger "scroll to the fragment" and so does not update the target element.

  2. Inserting a new element does not trigger "scroll to the fragment". So if the current URL fragment is #foo, but no element currently has ID foo, then adding such an element will not cause that element to match :target. Adding such an element can sometimes work, if you do so early in the document lifecycle before "scroll to a fragment" has completed. But usually it does not.

  3. There are various legacy-seeming string processing tricks going on that make the fragment/ID matching non-exact. E.g. special processing for #top (not sure if this impacts :target), and how the algorithm first tries exact match and then tries to do an extra round of percent-decoding (so, #%66oo will cause an element with ID foo to match, unless there is another element with ID %66oo somewhere in the DOM).

  4. Similarly, there is a presumably-legacy affordance for <a> elements with name="" attributes.

@stephband has expressed that he much prefers the "always matches the first document-tree element with an ID corresponding to the document's URL's fragment component" model, mentioning specifically (1)-(2). ((3) and (4) were just things I noticed reviewing the algorithms, which maybe are worth considering while we're here.) And this use case makes sense to me.

I strongly suspect changing :target's behavior is not web-compatible, and further I think there are probably people who want the currently-specified target behavior. E.g. if you use :target to highlight an element after the user scrolls to it via a fragment hyperlink, with some sort of fading-out flash, you might not want that fading-out flash effect for newly-inserted elements, even if they have an ID that happens to match the current URL fragment.

But introducing a new pseudo-class, with the always-match semantics, might be worthwhile. My strawperson names are :current-url-fragment or :location-hash.

It's not clear to me how much of a problem this is. So far only @stephband has expressed this to be an issue for their applications. Lots of developers "liked" my corresponding tweet but nobody else showed up to the original issue on WICG/app-history or replied with comments. But I wanted to log this feature suggestion here since it does make sense to me.

stephband commented 2 years ago

Here's a test page that demonstrates the :target behaviour detailed by @domenic above:

https://stephen.band/target/

nornagon commented 2 years ago

Just to add a voice here, at present :target is more or less useless for apps that dynamically render and update their content. Not only does :target match nothing at pageload (since as you mentioned, the element isn't present in the document at that time), it also can't be updated to match anything.

Perhaps an alternative would be to add a JS API to allow a page to set the current :target? Perhaps including scrolling to that element.

fantasai commented 2 years ago

Afaict, the Selectors spec has always defined the :target element in terms of the currently-effective fragment ID. (Selectors does not have a concept of history, so it is always evaluated against the current state of the document.)

So I would push back against this issue and say, are we really sure that it's not Web-compatible to do the right thing here? Because that seems like the best way forward. I'd rather not introduce a new :target-no-really pseudo-element that everyone has to learn, alongside learning the fact that :target itself is broken.

tabatkins commented 2 years ago

Related: #5619

kizu commented 1 year ago

Just want to bump this — this issue is the one that constantly comes up in my practice, in sometimes it doesn't have any solution or workaround.

Worst case: when a page has an anchor, and its content is dynamic in any way where the element won't exist initially. This would mean that we would be unable to share a link to this page where the :target would work.

The scrolling to the element can be worked around in various ways (most straightforward way — to have a mutation observer that waits for the element with the id matching the anchor to appear), but there is just no way to update the :target without adding a new history entry.

And the workarounds to have when navigating via pushState are far from convenient (replace the content, navigate to the anchor, so the :target would trigger, replaceState with the new URL, replacing the entry with the anchor).

I don't care much if the solution would be adding a new pseudo-class (though I agree that :target should just work, and :target-no-really is just weird), adding something like an opt-in to the pushState/replaceState where it would apply the target to the given anchor (this way the initial load issue could be potentially worked around via calling replaceState), or maybe just adding a method that invalidates the old :target and applies the new one.

But this is a real issue that comes up constantly, and I really hope we would have a solution for this one day.

jerrygreen commented 7 months ago

Until someone gives me a use-case where history.back() and history.forward() should respect :target, while history.pushState() and history.replaceState() shouldn’t, and it’s okay, – until then I advocate for all of them to handle the :target the same way. The current browsers behavior in this regard, is awful. It’s so inconsistent, so surprising. If current spec says otherwise (that this is normal behavior), – then the spec is awful too. Prove me wrong.

The current approach almost says to me: «don’t touch browser's native :target, make your own target implementation with javascript and classes, leave this pseudo-class alone, it’s messed up, and please make your own history object just like React does»… Heck, should I maybe make my own browser too, in such a case? Gosh…

I might believe that changing :target to what it’s really named for, without involving some new :target-no-really might break some functionality on some 0.000002% websites. Might as well turn out to break exactly 0 websites. I suppose, this largely related to websites which interpret hash in links as normal way to navigate pages. Like github.com#issues or github.com#users#jerrygreen. Not that I’m aware of a lot of such websites. With 15 years of experience in web development, I might only say that in most cases it’s just related to an element on page, but in one quirky use-case we indeed used it differently: as a second layer of navigation primarily used for modals – it wasn’t related to elements on page, so changing the current :target behavior COULD potentially break some things a bit, like name of the page equals to id of one of the elements on accident, and developers use native history.pushState or history.replaceState, then browser will scroll to such an object… I guess? Really a quirky rare case for a rare developer choice.

Shortly saying, use-cases where changing :target might break something is too narrow and too non-harmful to take into consideration really, IMO. Of course, the web is trying to always be as much backwards-compatible as possible. But I don’t think that implementing yet another :target-no-really would make more sense than just fixing :target to what it’s really intended to be. The web is enough of a mess already.

✅ I solemnly claim to sign petition to fix :target instead of making another similar pseudo-class