-
Notifications
You must be signed in to change notification settings - Fork 26
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
transitionWhile doesn't quite work for scroll restoration #230
Comments
This is similar to the issue that lead me to defer url changes until (the equivalent of) transition completion in our current use of History. When the URL updates on the leading edge, every relative link in the DOM becomes a minesweeper cell for the duration of the transition. Usually too quick to matter, but not guaranteed to be, and especially a hazard for the click-everything-at-least-twice-but-not-quite-fast-enough-for-double-click senior cohort (RIP, grandpa!). |
Ohh, I think that's different enough to be its own issue #232 |
This feels like a general symptom of #232. Basically, the current API assumes that everything transitions to the new history entry the moment you call
I don't think this is correct. The following code should be used for sync renders: navigation.addEventListener("navigate", event => {
event.transitionWhile(Promise.resolve());
updateDOM(data);
}); This changes the URL, and captures the state, before updating the DOM. Whereas, if there's an immediately-fulfilled promise involved, then the usual flow of navigation.addEventListener("navigate", event => {
event.transitionWhile(async () => {
await doRouting();
await updateDOM();
});
}); where
If possible I'd like to avoid decoupling state capture and changing the history entry, since those are pretty coupled in current implementations and spec architecture. |
Some stuff from internal discussion: Having to change how you call this API whether it's a sync navigation or not seems like a gotcha, and restricts how you can write your routing logic. Having to add a seemingly redundant If
Allowing interactivity should be the norm, as it is with MPA. Otherwise we're getting into sync XHR territory 😄 |
I’d never considered the option of disabling interactivity during a transition at all admittedly. Is that a common practice currently? My first thought is that it seems like it could add a kinda “user-hostile” risk to situations where things don’t go as planned — like, if there’s a bug or network issue leading to a very long transition, I’d expect to be able to continue having the option to click another link personally and if this interaction produced no response, it could give the impression that the app itself had “frozen”. Is this what you mean by sync XHR territory @jakearchibald? |
Exactly. Sync XHR blocks JS, therefore blocks all interactivity until the fetch completes. |
If we move to a model where a callable is passed to event.transitionWhile({
focusReset: 'manual',
async handler() {
// …
},
}); This means that options that impact the behaviour of the callback can appear before the callback in the source. (it's a pet hate of mine that things like |
@jakearchibald iiuc that’s already the (intended) model (from an ES POV) because of the intention to define new |
So what's going on here is a bit subtle which makes the fix a bit subtle too. Basically the scroll position is captured when the
This is most obviously seen in the case of multiple So indeed, to work around the current situation you need to find a way to delay your DOM-update code until after This also means that the initial ideas floated here, of having
But, if we have it take a handler which only runs after
I'm unsure on the exact migration path here. It still feels like "transition while this promise is ongoing" is reasonable, but I admit I haven't seen it too much in the demos we've built, and maybe it's just a footgun by encouraging you to run promise-producing code before commit. The options I can think of are:
(I don't think we'd particularly want to introduce a new method that takes a (function, dictionary) since as @jakearchibald mentions it's a bit annoying.) Thoughts welcome on which path would be best. |
4 seems fine. I like the idea of 6 because I'm still worried that 'transition' is a naming clash with animated transitions. The only naming suggestion I have is |
Should we allow event.intercept(async () => {
// ...
}); as an overload? Or just event.intercept({
async handler() {
// ...
}
}); ? The overload makes a lot of short code snippets in the README and tests look nicer, but I'm not sure that it would matter in the real world... |
I guess event.intercept({ async handler() {
// ...
} }); isn't so bad. |
Accidentally merged what was supposed to be a PR to main; reopening. New question: should we rename |
If we did that we'd probably also have to rename the two |
I still don't like transition as a name, but I understand if you don't want to change it. I could make a Twitter poll and see how devs feel about either naming? |
The overload seems ok. |
Playing around with the Navigation API and I found myself writing Sample codeexport default class Router {
constructor() {
this.onNavigationAttempt = this.onNavigationAttempt.bind(this);
window.navigation.addEventListener('navigate', this.onNavigationAttempt);
}
public shouldInterceptNavigation(e: NavigateEvent): boolean {
return e.canTransition && !e.hashChange;
}
protected onNavigationAttempt(e: NavigateEvent): void {
if (!this.shouldInterceptNavigation(e)) {
return;
}
this.interceptNavigation(e);
}
protected interceptNavigation(e: NavigateEvent): void {
e.transitionWhile(
this.asyncStuff(e).then(() => {
this.onNavigationIntercepted(e);
})
);
}
protected onNavigationIntercepted(e: NavigateEvent): void {
//
}
} |
A regular navigation happens roughly like this:
transitionWhile
doesn't really fit in this model.If an unsettled promise is passed to
transitionWhile
(due to pending network activity), it goes like this:transitionWhile
called - capture & store scroll positiontransitionWhile
resolvesThis isn't ideal, as the user may have scrolled during the fetch, but that change is discarded.
If a fulfilled promise is passed to
transitionWhile
(due to the page data being available synchronously), it goes like this:transitionWhile
called - capture & store scroll positionThis behaviour is broken, as the new DOM may drastically alter the scroll position. Also, the developer may choose to reset the scroll position after altering the DOM, if the page is conceptually new.
It feels like the API needs to change so it knows when to capture the scroll position.
An alternative design:
Here,
transitionWhile
takes a callable. It captures scroll and focus state before calling the callable. This solves the synchronous issue.The developer can call
captureState
to recapture state at a later time, ideally just before updating the DOM.Scroll positions are restored when the promise returned by the callable fulfills.
The text was updated successfully, but these errors were encountered: