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
481 stars 30 forks source link

How to handle ephemeral states? #261

Open clshortfuse opened 1 year ago

clshortfuse commented 1 year ago

With the current replaceState/pushState mechanics, replaceState doesn't modify the next entry in the stack. This can create an awkward setup when trying to add "back" support for things like dialogs:

  1. User presses DELETE EMAIL button. [index=0]
  2. Dialog pop up appears. pushState is called. [index=1]
  3. User performs back action (eg: older Android button, or gesture). [index=0]
  4. Dialog is closed due to popState.

At this point there is still an [index=1] state that never goes away, while the user is normally on the [index=0]. It never fixes until we push another stack to write a new [index=1] (eg: clicks /help link).

There's nothing in the language that really lets us specific a state that cannot be returned to and should be skipped over (an ephemeral state). Another example is the same setup as above, but from the open dialog, they click a link inside the dialog ([index=2]). When they return back from that link, we may not want that state to be the "opened dialog" state. We want it passed over.

We also apply the same logic for closing pop-up menus, mimicking the Android feel. We'll likely do this for pop-up "bottom-sheets" as well. I wonder if it's possible to add something to either flag a state as "unnavigable" or even deleting states (though I can see that getting abused).


Side note: Looking forward to this API for intercepting ShadowDOM deep <a> anchor elements. :)

domenic commented 1 year ago

I think you are looking for https://github.com/WICG/close-watcher !

markcellus commented 1 year ago

@domenic that seems like a great proposal. Although it only seems to address the back button on Android (or other mobile devices presumably?). If it doesn't address the back button on the desktop browsers, how would it be a suitable solution?


btw @clshortfuse this a really great issue. I was thinking about this the other day and was going to file something similar, so thanks for filing it. :heart:

bathos commented 1 year ago

This seems similar to use cases described in https://github.com/WICG/navigation-api/issues/9 for deleting forward entries.

clshortfuse commented 1 year ago

I think the use case of "Android back" is correctly targeted with Close Target and #9 .

Though Android back is strictly for "close" not for pages. I wonder wondering if transient/ephemeral states could be something we can read and/or write. For example, when browsers perform their heuristics to avoid back-press hijacking (like multiple pushStates are created), instead of it being based on the browsers, we can say those extra pushStates ---because they had no user-interaction before being created--- can be non-navigable states.

Authors can choose when links are non-navigable explicit but, the browser can force it. For example, instead of back, sending you to a deleted or expired URL (which #9 alludes to as maybe a 404), it can just be skipped over.

  1. /search/?email=foo [index=0]
  2. /emails/5 [index=1] (pushed)
  3. /emails/5?action=delete [index=2] (non-navigable push for dialog)
  4. /emails/5 [index=1] (popped back, from dialog)
  5. /emails/5 [index=1] (replace state with non-navigable self)
  6. /emails/ [index=2] (push)

In this use case, a search screen may send you to an email. That email may be deleted. Author scripts user to go to /email page (instead of back) on delete, as constant destination. If from /emails, user presses back, they can be send back to the /search page. But from peeking of history, it still shows you were on /email/5, which is better than rewriting history, IMO.

Of course, this can still be rewritten as:

  1. /search/?email=foo [index=0]
  2. /emails/5 [index=1] (pushed)
  3. /emails/5?action=delete [index=1] (replace state and track with CloseTarget)
  4. /emails/5 [index=1] (replaceState)
  5. /emails/ [index=1] (replaceState)

But it'll be gone from history that emails/5 was ever visited, as well as the delete action.

clshortfuse commented 1 year ago

Sorry for the double comment, but I remembered my comment from the old discussion from 2016 when we discussed back-hijacking.

For example, let's say a PWA starts up and brings up a dialog. The dialog could be a "changelog" or a "What's new" or something like to that effect. The user may want to press Back on their Android device because that's how they generally close dialogs. But, because there was no user gesture, and that Back gesture was the first one, instead of closing the dialog, it would actually close the entire application (the default action on Back presses on PWA).

Another example would be an app logging in to a server on launch after it's already cached authentication from a previous session. A spinner shows and then, it can't connect to the server. From there, a dialog popup would appear showing the connection error and possible reason. Again, pressing back here would close out the app.

It's kinda why I would think PWAs in display:standalone would need some sort of exception. For normal web browsers, you can tap forward again, but standalone PWAs don't have user interface.

Originally posted by @clshortfuse in https://github.com/WICG/interventions/issues/21#issuecomment-448665891

Context here is a PWA/Home Screen App can be allowed to dictate non-navigable and work around the top issues. It's a bit more explicit than just letting the browser "work its magic". I like to push a state once a PWA opens to then intercept and throw up a "Are you sure you want to exit?" for certain apps.

domenic commented 1 year ago

So the point I was making by pointing out the close watcher proposal, was that in our experience, such "ephemeral states" are created mainly for the purpose of intercepting the Android back button. We don't see people wanting to create them for the desktop back button. On desktop, the traditional mechanism for closing an ephemeral dialog is the Esc key, instead of the browser back button. Thus, the close watcher proposal unifies those two into the "close signal" concept, which varies per platform, and has zero interaction with the history list.

On the other hand, sometimes there are dialogs which do want to participate in the history stack. The Twitter https://twitter.com/compose/tweet dialog is an example of this. But this one is very much not ephemeral:

So this example is really more of a first class history entry, that happens to be manifested in dialog UI format.

@markcellus and @clshortfuse, are you suggesting there's something in between these cases, where e.g. in your application the browser back button is used to close pop-up menus and bottom-sheets and "what's new" popups, even on desktop? And yet traversing forward should not bring those things back up? It's been our opinion so far that this intermediate class is not a great UX or something we want to support. But we'd be interested in discussing, if you disagree.

And yeah, as #9 alludes to, cases like deleted emails might call for a whole history entry deletion API. But we don't want to build that if people are mostly going to be using it to get around the Android back button issue; we think it's the wrong solution for those cases.

I like to push a state once a PWA opens to then intercept and throw up a "Are you sure you want to exit?" for certain apps.

This seems more related to the ability to cancel a navigation, which can be done with navigateEvent.preventDefault().

markcellus commented 1 year ago

in our experience, such "ephemeral states" are created mainly for the purpose of intercepting the Android back button. We don't see people wanting to create them for the desktop back button

are you suggesting there's something in between these cases, where e.g. in your application the browser back button is used to close pop-up menus and bottom-sheets and "what's new" popups, even on desktop? And yet traversing forward should not bring those things back up?

Yeah, see the original OP's example. After clicking a link in a modal (that doesn't have its own URL), the natural thing for a user to do is to click the back button—not press Esc—to see the modal again. I haven't taken a deep look at the close-watcher proposal and would hate to derail this thread to dive into its specifics. But if the close-watcher proposal assumes this use case would be restricted to just a mobile/Android device, it doesn't seem like a viable alternative for the original issue.


Side note: The term "close watcher" confused me at first, because I wouldn't expect for the "watching" to be closely coupled to the closing of an ephemeral HTML element, like a modal. Or for its support to be super specific to the user's device. At least for me, given this scenario, I care more about watching the navigation history--not whether there is a modal closing. But I haven't looked too deeply at the proposal, so this may already be addressed somewhere on the close-watcher repo.

domenic commented 1 year ago

Can you point to a public site that behaves like the OP's example? All sites I'm aware of on desktop use the Esc key to close dialogs, not the browser back button. From the browser's point of view, we'd prefer to encourage sites to follow that path, so as not to confuse users about how the desktop back button works. That's why, for example <dialog> responds to Esc and not the browser back button.

clshortfuse commented 1 year ago

Apologies for not replying sooner.

While I do feel we can be close to derailing the topic with Android back discussion, I think could serve useful to pinpoint the overlaps in order to check for gaps.

Bluntly, I have a concern related to full-screen dialogs:

https://m3.material.io/components/dialogs/guidelines

Basically, depending on screen metrics a dialog can be full-screen or windowed (modal). I would much rather not depend on checking viewport when deciding how to deal with handling interaction (eg: Browser Back, ESC, Back Gesture).

That means I will likely still support browser back as closing a dialog, even on desktop. It is rather strange for a nav back request to occur during a dialog in a native context, so there's no real user expectation I'm working against by implementing closing of a dialog with browser back on desktop.

The point about ESC being mapped as Back Gesture is interesting. I'm old (by today's tech standards) and have users who used our applications in DOS with keyboard only, then in Windows with mouse and, today, multiplatform via Web supporting even touch. ESC is more closely related to what I could say is navigation "UP". Essentially it would go up the tree, not back in flat stack. Even in our Web Apps, we still use this paradigm. If you are even lost in navigation, enough escape presses will take you back up to the home screen (app, not OS).

It sounds like it does properly overlap with Android back gesture, but there are quirks. For example, dialogs and menus do close with Back Gesture and ESC. The functionality is the same. But some popups like Bottom Sheets do not. If you think of a chat pop-up or Compose Message in Gmail, that pop-up will close if you press escape, but only if it has focus. If an large-screened Android user (eg: 10" tablet) were to use a Back Gesture, you would want it to navigate back, like if a desktop user pressed back, not close the bottom-sheet that is at a corner/side of the page.

It somewhat lends to ESC being focus-dependent. Desktop users are generally very comfortable with this, but mobile devices not so much. In fact, Safari doesn't even style focus for mobile devices, and intentionally so.

That leads me to opinion that Back Gesture should be a special key input. Maybe HTMLDialogElement can emit close event as a default action and we can still intercept it (preventDefault). On a related note, I have my concerns with ESC with tooltips. And I plan on migrating my tooltips to HTMLPopupElement and wouldn't want a Back Gesture to close those.

So, I'm a bit wary of conflating the two (nav and back gesture). I have a recent use case of scripting a web-based LetsEncrypt certificate generator. I should link to the ToS instead of hard-copy it. I haven't fully decided if the entry will be a dialog itself (eg: full-screen dialog) or if I will throw up a dialog that will include a link to the ToS. In one case the dialog is not ephemeral. In the second, it is. (And now I just thought of a popup dialog inside that full-screen dialog. 😵 )

I don't mind whatever I have to script, as long as I can fine tune it. If the back gesture is, uh, "listenable" then I can workaround any quirks or anything the group feels is non-standard.