WICG / navigation-api

The new navigation API provides a new interface for navigations and session history, with a focus on single-page application navigations.
https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigation-api
484 stars 30 forks source link

Making SPA-navs-with-hashes work #237

Closed domenic closed 2 years ago

domenic commented 2 years ago

Scenario: you are on /a. You want to do a SPA navigation /b#hash, by which you mean, load the contents for /b, replace the DOM with them appropriately, and then scroll to the element with id="hash".

This needs to be handled manually by SPA routers today, because they intercept the entire navigation process. See e.g. this issue: https://github.com/remix-run/react-router/issues/394. The code in, e.g., https://github.com/rafgraph/react-router-hash-link is extremely complicated, but I think the basic idea is: let the router do its normal thing, then call element.scrollIntoView() with element based on location.hash.

The navigation API in its current form does not help. It makes it easy to avoid intercepting navigations from /a to /a#hash. But navigations from /a to /b#hash will need to be intercepted with transitionWhile(), and once you do that, you lose any browser-native processing of following the fragment link.

We could fix this. I think we should fix it at the same time as fixing #231, which is also about how scrolling should be handled on push/replace navigations. #231 envisioned resetting the scroll position after the push/replace "finishes", in the sense of navigation.transition.finished, after all the DOM for /b is supposed to be loaded. Instead we should probably either reset the scroll position, or scroll to the location indicated by the hash.

The main question is how to combine our three scroll-related configurable pieces of functionality. We have:

One extreme is just scroll: "after-transition" | "manual", unifying scrollRestoration with this new option. If you want to configure scroll restoration different than scroll resetting or scrolling-to-fragment, you need to investigate navigateEvent.navigationType and/or navigateEvent.url.

Another is to keep with my argument in https://github.com/WICG/navigation-api/issues/231#issuecomment-1138930254 for splitting up push/replace and traverse/reload, something like scrollRestoration for the former and initialScroll for the latter. Or maybe scrollReset is still a fine name for the latter; just like focusReset "resets" the focus to either the top, or to an explicitly-autofocused element, so scrollReset "resets" the scroll position to either the top, or to an explicitly-fragment-indicated element.

And yet another is to allow configuring scroll-to-fragment behavior differently from no-fragment pushes, so e.g. scrollRestoration + scrollReset + scrollFragmentHandling.

I'm starting to prefer just unifying them, though.

atscott commented 2 years ago

@domenic Thanks for creating an issue for this. I was recently investigating what we could do in the Angular router to resolve some of our scrolling issues. One of those options could be to include something that interacts with navigation API in a way that would eliminate a lot of the manual work we do for this. We have a whole RouterScroller and supporting BrowserViewportScroller that controls scrolling to the top of the page or an anchor on a "push" as well as restoring scroll position on a "traverse".

A short investigation into some other routers indicated this manual handling is something other Router authors have encountered as well:

https://sourcegraph.com/github.com/vuejs/router/-/blob/src/scrollBehavior.ts?L81-140 / https://sourcegraph.com/github.com/vuejs/router/-/blob/src/router.ts?L669-679 https://github.com/rafgraph/react-router-hash-link/blob/main/src/HashLink.jsx (https://github.com/remix-run/react-router/issues/394#issuecomment-220221604)

While there is certainly value in some of the customizability these options provide, it would be great if there was a browser primitive that could handle the most common use-cases.