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

Add support for anchor links #157

Open
vigenere23 opened this issue Oct 27, 2020 · 9 comments
Open

Add support for anchor links #157

vigenere23 opened this issue Oct 27, 2020 · 9 comments

Comments

@vigenere23
Copy link
Contributor

vigenere23 commented Oct 27, 2020

Anchor links are the trailing # part in URL that usually corresponds to an ID on the page. The default and standard browser behavior is to automatically scroll to the element that corresponds to that id on page load. But since our app is an SPA, the hash starts at the first # symbol and thus preventing such behavior (ex: #/home#about will try to scroll to element with id /home#about instead of about).

Here is a way to achieve such a thing (would need to be inserted at the right place):

const elementId = window.location.hash.split('#').pop()

    if (elementId) {
      const element = document.getElementById(elementId)
      if (element) {
        element.scrollIntoView(true) // `true` is for aligning top of element to top of screen
      }
    }

We would also need to add a suffix to the parsed path regex pattern (like (#.*)?$) to match the anchor link ending.

On my side, when adding the suffix to the path and adding the code snippet inside a routeLoaded() event handler in my project, it works without any glitch (even with restoreScrollState activated).

Known problem and possible solution

Standard Same-page anchor links may be problematic. Since the URL is often just the hash without the rest (like #about), the browser's default behavior will the to replace the entire hash of the URL with this, thus breaking it. A possible solution would be to listen to location hash change and if the URL looks broken (aka with a # not followed by a /), then go back to previous route but with the anchor link appended (instead or replaced like the default behavior).

@ItalyPaleAle
Copy link
Owner

This is an interesting issue and I'd be willing to support looking for # in the querystring (so a # after a #) to determine the anchor to navigate to.

i agree that this would break links that want to point to an anchor. We could fix that for when people leverage the use:link action. There would be no way to fix it for "regular" links.

What do you think?

@vigenere23
Copy link
Contributor Author

vigenere23 commented Oct 29, 2020

Yes, sounds good! But I'm wondering if the following algorithm would be able to handle "same-page anchor-links":

  1. always keep a reference to the last hash
  2. on hashchange event, do:
    1. if hash is an anchor link (aka there's no / after the #):
      1. (needed?) prevent default behavior (would a event.preventDefault() work for this event?)
      2. change the window.location.hash to lastHash + anchorLink
      3. return (to pass to the next iteration of hashchange event handler). Should we return true, false or nothing?
    2. else, proceed normally but with added anchor link support:
      1. split hash into pageHash and anchorLink
      2. navigate to pageHash (programatically)
      3. scroll to element with id anchorLink (if present)

@vigenere23
Copy link
Contributor Author

vigenere23 commented Oct 30, 2020

wow... it actually works! + the default behavior still seems to work... sometimes. It still does create a race condition when using the restoreScrollState attribute, but I'm sure we can find a way to prevent this easily (just like the previousScrollState check in the afterUpdate()?)

Here's my prototype for now:
loc.subscribe(async (newLoc) => {
    // Find if the URL matches a same-page anchor link
    if (newLoc.hash.match(/^#[^\/]\w*/)) {
        if (!lastLoc) {
        // Could be another behavior, but this one seems right to me
            window.location.hash = '#'
            return true // don't know if `true` is needed here
        }

        // Find the "page" (aka fake "location") of last URL
        // Basically removes the anchor link part
        const lastLocPage = lastLoc.hash.split('#')[1]
        // The new URL should be the last page + the added anchor link part
        window.location.hash = lastLocPage + newLoc.hash
        return true // don't know if `true` is needed here
    }

    // Proceed like the rest of code
    lastLoc = newLoc

    // ... rest of function

We would then insert the automatic scrolling (stated in the description) algorithm somewhere.

To make this work we still need to manually (for now) add the regex to match the possible anchor link (something like (#[^\/]*)?) to the path pattern.

@brunnerh
Copy link

Note that jumping to an element via a fragment does not only scroll the element into view, it also changes the tab order context to be that element. So the next time you press tab it will focus the first focusable element within or after the target element.

This is helpful for accessibility features like "skip to content" links.

image

I tried reproducing this behavior in JS but so far it turned out to be tricky. If you just try to focus() the target element, nothing will happen if the target element itself is not focusable.

@SeanMcP
Copy link

SeanMcP commented May 19, 2021

@brunnerh You'll need to set the tab index of the target element to 0 before calling focus.

@brunnerh
Copy link

@brunnerh You'll need to set the tab index of the target element to 0 before calling focus.

Unfortunately that is usually not what you want. Non-interactive elements that are just sections of a page are not supposed to be in the tab order of the document. Maybe one could set the tab index, focus the element and remove the tab index again, but unless that behaves correctly across browsers that is not a viable solution.

@SeanMcP
Copy link

SeanMcP commented May 24, 2021

@brunnerh one could set the tab index, focus the element and remove the tab index again

Yep! That's a pretty common practice when doing keyboard navigation work in JS.

@seanthingee
Copy link

I'm also looking into a solution—specifically to support the skip-to-main link. Right now clicking/activating skip-to-main link changes the url, which I don't enjoy, but more problematic is that it will load the default page if you aren't viewing it already.

I see that @vigenere23 has a solution posted, and reports it works, but considering it about 7 months later is this the most current state of thinking for the solution?

@monlasan
Copy link

monlasan commented Nov 2, 2022

Good morning/evening ... has a solution been found ? I switched from PageJS to svelte-spa-router because I need hash based routing. Now my documentation anchors links dont work in SSrouter.. Also, If you paste a url in the browser it doesnt work at all

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants