diff --git a/CHANGELOG.md b/CHANGELOG.md index 49d103c0..a4048ddd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Lines marked **Upgrade step** need you to act after updating. ### Fixed +- Browser Back now leaves a form's Settings page. Before, every tab you opened added a history step, so Back only went through the tabs. A link to a Settings tab still opens that tab. (#287) - A new long text question now shows in your form and in the editor preview, also before you set any options for it. Before, it failed to show. (#286) - Ticking "Remember me" on the login page now keeps you logged in after your browser session ends. Before, it was ignored. (#227) - The counter under the text editor shows the length of your text as soon as a text opens. Before, it showed a larger number until your first edit. (#228) diff --git a/resources/js/Pages/Forms/Settings.test.ts b/resources/js/Pages/Forms/Settings.test.ts index 30ad825a..6854834b 100644 --- a/resources/js/Pages/Forms/Settings.test.ts +++ b/resources/js/Pages/Forms/Settings.test.ts @@ -9,7 +9,7 @@ import { trackFormSave } from "@/utils/useFreshForm"; vi.mock("@inertiajs/vue3", async (importOriginal) => ({ ...(await importOriginal()), - router: { on: vi.fn() }, + router: { on: vi.fn(), page: { url: "" } }, })); vi.mock("@/api/forms", async (importOriginal) => ({ ...(await importOriginal()), @@ -110,6 +110,57 @@ describe("settings page from history", () => { expect(callGetForm).not.toHaveBeenCalled(); }); + it("adds no history entry when you open Settings or switch tabs", async () => { + const length = history.length; + history.replaceState( + { component: "x", url: "/forms/1/settings" }, + "", + "/forms/1/settings", + ); + + mountSettings(); + await wrapper!.findAll("button[role=tab]")[1].trigger("click"); + await wrapper!.findAll("button[role=tab]")[3].trigger("click"); + + expect(history.length).toBe(length); + expect(location.hash).toBe("#appearance"); + expect(history.state).toEqual({ + component: "x", + url: "/forms/1/settings#appearance", + }); + }); + + it("leaves the address alone when Settings opens, so Inertia saves its page once", async () => { + const state = { component: "x", url: "/forms/1/settings" }; + history.replaceState(state, "", "/forms/1/settings"); + + mountSettings(); + + expect(location.href).toBe("http://localhost:3000/forms/1/settings"); + expect(history.state).toEqual(state); + }); + + it("tells Inertia the tab, so a Back to this page opens the same tab", async () => { + history.replaceState(null, "", "/forms/1/settings"); + + mountSettings(); + await wrapper!.findAll("button[role=tab]")[3].trigger("click"); + + expect((router as unknown as { page: { url: string } }).page.url).toBe( + "/forms/1/settings#appearance", + ); + }); + + it("opens the tab named in the address", async () => { + history.replaceState(null, "", "/forms/1/settings#completion-page"); + + mountSettings(); + + expect( + wrapper!.findAll("button[role=tab]")[2].attributes("aria-selected"), + ).toBe("true"); + }); + it("keeps the tab you switch to while the form loads again", async () => { let landed = () => {}; trackFormSave(new Promise((resolve) => (landed = resolve))); diff --git a/resources/js/Pages/Forms/Settings.vue b/resources/js/Pages/Forms/Settings.vue index e6e6fbc0..724aa714 100644 --- a/resources/js/Pages/Forms/Settings.vue +++ b/resources/js/Pages/Forms/Settings.vue @@ -74,6 +74,7 @@ import Privacy from "@/components/Factory/Settings/Privacy.vue"; import TemplateExport from "@/components/Factory/Settings/TemplateExport.vue"; import { useForm } from "@/stores"; import { useFreshForm } from "@/utils/useFreshForm"; +import { router } from "@inertiajs/vue3"; import { onBeforeUnmount, ref } from "vue"; const props = defineProps<{ @@ -107,20 +108,21 @@ const navigation: Array<{ ]; const tabChanged = (index: any) => { - const item = navigation[index]; - location.hash = item.slug; + // replace the entry: a new one per tab would trap browser Back on Settings. + // Inertia saves router.page.url and history.state.url when you leave and on a + // Back that reloads, so both follow the hash + const url = `${location.pathname}${location.search}#${navigation[index].slug}`; + // shortcut: page is protected in Inertia 1.x's types, no public way to set its url + (router as unknown as { page: { url: string } }).page.url = url; + history.replaceState({ ...history.state, url }, "", url); selectedTabIndex.value = index; }; +// don't write the address here: before Inertia saves its first page, that would add a 2nd entry const foundIndex = navigation.findIndex( (item) => item.slug === location.hash.replace("#", ""), ); - -if (foundIndex !== -1) { - tabChanged(foundIndex); -} else { - tabChanged(0); -} +selectedTabIndex.value = Math.max(foundIndex, 0); // the panels copy the form when they load const freshForms = ref(0);