#Implementing a Table of Contents (hash change scroll issue)

36 messages · Page 1 of 1 (latest)

keen moon
#

I am trying to implement a Table of Contents sidebar for an API documentation site (in one-page style) that contains hash links (e.g., to="#Auth"). When clicking on one of the Table of Contents links, having the browser scroll down to the anchor that matches the hash is working as expected. The part I'm running into some trouble with is when I try to update the hash as a result of the user scrolling down the content part of the site. When I try to update the hash in the URL, the browser scrolls to the anchor element, but I'd really like to avoid that behavior so the user can scroll at their own pace. I've tried using navigate, updating the location state using router.buildAndCommitLocation, and even just trying window.location.hash = '#Auth'. All of these result in the hash being updated in the address bar, but they also all take over scrolling. I believe the latter is happening because the history implementation is overwriting window.history to support the router's subscriptions.

I found this PR, which seems relevant to the issue I'm experiencing: https://github.com/TanStack/router/pull/1105#issuecomment-2019026150 (the code causing the scrolling seems to have moved here: https://github.com/TanStack/router/blob/35af575ab4c623556ecdb613ac1c85864f0c95d9/packages/react-router/src/Transitioner.tsx#L146)

If I'm understanding correctly, the recommendation was to try using the Scroll Restoration API. I wasn't able to get that to work either, unfortunately.

Here's a StackBlitz that hopefully minimally demonstrates the issue: https://stackblitz.com/edit/tanstack-router-785cuome?file=src%2Fmain.tsx I also tried using useElementScrollRestoration, but it didn't solve the issue either. (The intersection observer implementation here isn't without issue, but it should be fine to demonstrate the routing problem I have, I think).

Has anyone found a solution for something like this? Thanks!

keen moon
#

@zealous solar, I see you were involved in the original discussion. Would you be able to help me make sure I was understanding you correctly with what you were recommending in using the Scroll Restoration API?

wooden elbow
#

maybe it's time to revisit this PR then

#

it probably needs more configurability though. like opting out of scrolling per navigate call

keen moon
#

What do you think about modeling it after the resetScroll option? It could be a prop of Link, an option in commitLocation, and navigate.

wooden elbow
#

yes

#

just not sure how it can propagate to the Transitioner from there

keen moon
#

It could be a property on router, just like it is for resetScroll? That's how it's accessed in ScrollRestoration, at least.

#

The scrollIntoView condition would become something like:

if (typeof document !== 'undefined' && (document as any).querySelector) {
        if (
          router.scrollOnNextHashChange &&
          router.state.location.hash !== ''
        ) {
          const el = document.getElementById(router.state.location.hash)
          if (el) {
            el.scrollIntoView()
          }
        }
      }
wooden elbow
#

a bit ugly 😄

#

would rather add it to state then

#

not as global router option

#

similar to location masking

keen moon
#

Should router.resetNextScroll also move to state, in that case?

wooden elbow
#

one thing at a time 😉

#

have a look how location masking is handled in commitLocation

#

can you create a draft PR?

keen moon
#

Yes, will do.

wooden elbow
#

cool

#

default behavior would be the same as currently, which is scrolling is activated

#

and it can be disabled with this property

#

be aware that we need extensive tests for this 😉

#

e2e tests with playwright

keen moon
#

Ok thanks, I'll work on it.

wooden elbow
#

let me know if you need support

keen moon
#

I've got one question, regarding where to put hashChangeScrollIntoView after looking at the location masking handling in commitLocation.

It looks like when there's location masking, the HistoryState is where some fields are being written, and the others to ParsedLocation.

Did you envision properties like this being added to HistoryState temporarily and deleted when consumed? i.e.:

declare module '@tanstack/history' {
  interface HistoryState {
    __tempLocation?: HistoryLocation
    __tempKey?: string
    __tempHashChangeScrollIntoViewOptions: boolean | ScrollIntoViewOptions
  }
}

I was just a little hesitant to add here because the history state feels a little unrelated to the scrolling/transitional state. As for adding directly to ParsedLocation, I understand why the location masking options fit there, but I am a little unsure about transitional options. What do you think?

wooden elbow
#

isn't this also kind of a history thing?

#

what's the back / forward navigation behavior for this?

#

should hitting the back button scroll or not scroll to the element?

keen moon
keen moon
wooden elbow
#

we might need to discuss the name of the property as well

keen moon
#

Sure, I was just trying to get something working first. I'm happy to rename things, reorganize, etc.

wooden elbow
#

<@&1156977613093486602> ⬆️ ⬆️ check this out ⬆️ ⬆️

#

nice touch to not only add a boolean flag but allow ScrollIntoViewOptions

#

from a superficial glance, this looks like this is the right direction