inertiajs / inertia

Inertia.js lets you quickly build modern single-page React, Vue and Svelte apps using classic server-side routing and controllers.
https://inertiajs.com
MIT License
6.3k stars 423 forks source link

Unintended browser redirection despite declining confirmation modal #1830

Closed jurrid closed 6 months ago

jurrid commented 6 months ago

Version:

Describe the problem:

The documentation provides an example of canceling events. When navigating away within the application, the confirm message is displayed. Declining to navigate away works perfectly in this scenario. However, when using the browser's back button, although the confirm message appears, declining it leads to the browser redirecting away, which is not the desired behavior.

Steps to Reproduce:

Utilize the examples provided at: https://inertiajs.com/events#cancelling-events

import { router } from '@inertiajs/vue3'

router.on('before', (event) => {
  if (!confirm('Are you sure you want to navigate away?')) {
    event.preventDefault()
  }
})
driesvints commented 6 months ago

Could you share the OS/Browser you're using?

jurrid commented 6 months ago

Sure;

driesvints commented 6 months ago

Wondering if this is related to https://github.com/inertiajs/inertia/issues/721 and if there's really any way at all we can control history navigation.

jurrid commented 6 months ago

When I read it, it seems highly related! I'll conduct some testing on my end to see if it's possible in plain HTML and Javascript. I've done it previously with Vue Router, but that's not utilized with Inertia.

jurrid commented 6 months ago

I have developed a sample application to illustrate the possibility of blocking the back button, along with some accompanying notes.

You can access the sample app at the following link: https://plankton-app-cm9rj.ondigitalocean.app

The code used on the create page:

window.addEventListener("load", (e) => {
    window.history.pushState({}, null, null);
});

window.addEventListener("popstate", (e) => {
    if (!confirm("Do you really want to leave?")) {
        e.preventDefault();
        window.history.pushState({}, null, null);
    } else {
        history.back();
    }
});

In Firefox, the functionality behaves as expected. However, in Chrome, interaction with the browser element is necessary to trigger the window.onpopstate function. Another peculiar behavior in Chrome is that after declining the confirmation and subsequently pressing the back button again, the browser does not prompt the confirmation dialog for the second time. To trigger the confirmation dialog for the second time, it is necessary to interact with the browser element once more.

Safari on macOS fails to trigger any functionality, making it a mystery as to what is happening.

Although I believe it is somehow possible, I may overlook the implications of window.history.pushState({}, null, null) for Inertia; that must be executed, along with the peculiar behavior observed in Safari and Chrome.

As of now, the quickest approach would be to adding a note into the documentation addressing this behavior.

driesvints commented 6 months ago

Thanks @jurrid. I'll leave this one open but we'd also appreciate a PR to the docs for confirm about this.

reinink commented 6 months ago

Hey I haven't read through everything here, but just thought I'd mention that the browser does not allow cancelling popstate events:

https://stackoverflow.com/questions/32432296/is-it-possible-to-e-preventdefault-in-window-onpopstate

https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event

jurrid commented 6 months ago

Hi @reinink ,

Thank you for taking the time to read this far. I understand that the event is non-cancelable. My assumption is that messing with the history state will have some effect.

Perhaps you could provide some insight into this matter: Filamentphp utilizes the window.onbeforeunload JavaScript function, as demonstrated on their demo application. When I incorporate this code directly into a plain HTML page, it functions properly(page refresh, back button, etc). However, when implemented within an Inertia page, it fails to work with the back button. Interestingly though, upon page refresh, the message is displayed.

onMounted(() => {
    window.onbeforeunload = function () {
        return "Data will be lost if you leave the page, are you sure?";
    };
});
RobertBoes commented 6 months ago

That is because an window.onbeforeunload event isn't triggered, it would only trigger on fresh page visits (as is the case with multi-page apps, such as Filament). For an SPA that even wouldn't be triggered, the page isn't unloaded.

reinink commented 6 months ago

Yup, exactly @RobertBoes. Basically browsers make cancelling forward/back navigation events really difficult as otherwise these APIs could be abused by malicious websites. From what I understand even window.onbeforeunload has really strict rules around it and should only be used to warn people about unsaved changes. See here: https://developer.chrome.com/docs/web-platform/page-lifecycle-api#the_beforeunload_event

jurrid commented 6 months ago

Alright, that makes sense. But what about when you hit the refresh button? Technically, the page isn't reloaded at that point and remains in SPA mode, or am I overlooking something?

reinink commented 6 months ago

Nah in that case it doesn't do an "SPA refresh", it does a full page reload, so it's effectively the same thing as visiting a different page.

jurrid commented 6 months ago

Alright, thanks everyone for the information!

driesvints commented 6 months ago

So is the general consensus here that this is simply technically not possible? If so I feel that a note the docs is required and this can be closed.

reinink commented 6 months ago

@driesvints I've added a note to the docs: https://github.com/inertiajs/inertiajs.com/commit/0cd3b20044f2aa0f9ffc983bf03aa26a24588607

Technically the popstate event is a native browser event and not an Inertia event, so in that sense kind of doesn't make sense to include in the Inertia event docs, but given their close connection it's probably helpful to include this note either way.