T
TanStack10mo ago
foreign-sapphire

Implementing a Table of Contents (hash change scroll issue)

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!
19 Replies
foreign-sapphire
foreign-sapphireOP9mo ago
@Sean Cassiere, 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?
like-gold
like-gold9mo ago
maybe it's time to revisit this PR then it probably needs more configurability though. like opting out of scrolling per navigate call
foreign-sapphire
foreign-sapphireOP9mo ago
What do you think about modeling it after the resetScroll option? It could be a prop of Link, an option in commitLocation, and navigate.
like-gold
like-gold9mo ago
yes just not sure how it can propagate to the Transitioner from there
foreign-sapphire
foreign-sapphireOP9mo ago
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()
}
}
}
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()
}
}
}
like-gold
like-gold9mo ago
a bit ugly 😄 would rather add it to state then not as global router option similar to location masking
foreign-sapphire
foreign-sapphireOP9mo ago
Should router.resetNextScroll also move to state, in that case?
like-gold
like-gold9mo ago
one thing at a time 😉 have a look how location masking is handled in commitLocation can you create a draft PR?
foreign-sapphire
foreign-sapphireOP9mo ago
Yes, will do.
like-gold
like-gold9mo ago
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
foreign-sapphire
foreign-sapphireOP9mo ago
Ok thanks, I'll work on it.
like-gold
like-gold9mo ago
let me know if you need support
foreign-sapphire
foreign-sapphireOP9mo ago
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
}
}
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?
like-gold
like-gold9mo ago
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?
foreign-sapphire
foreign-sapphireOP9mo ago
Hmm, fair point. I just tested it here: https://tanstack.com/router/latest/docs/framework/react/guide/file-based-routing#using-the-watch-command and it seems like it does scroll back to the element. I need to add test coverage, but I've opened an initial draft here: https://github.com/TanStack/router/pull/2996
like-gold
like-gold9mo ago
we might need to discuss the name of the property as well
foreign-sapphire
foreign-sapphireOP9mo ago
Sure, I was just trying to get something working first. I'm happy to rename things, reorganize, etc.
like-gold
like-gold9mo ago
@Maintainer - Router ⬆️ ⬆️ 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
foreign-sapphire
foreign-sapphireOP9mo ago
Thank you!

Did you find this page helpful?