From 31b1f7f80dd64e881520446529885c6ba1dac705 Mon Sep 17 00:00:00 2001 From: Coracle-Bot Date: Fri, 25 Sep 2026 20:15:49 +0000 Subject: [PATCH] Keep a navigation out of a modal from popping history it does not own --- src/app/modal.ts | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/app/modal.ts b/src/app/modal.ts index 92279436..a5d50eae 100644 --- a/src/app/modal.ts +++ b/src/app/modal.ts @@ -1,5 +1,5 @@ import type {Component} from "svelte" -import {randomId, last, Emitter} from "@welshman/lib" +import {call, ms, randomId, last, Emitter} from "@welshman/lib" import {goto, pushState, replaceState} from "$app/navigation" import {page} from "$app/state" import type {DialogSize} from "@lib/components/Dialog.svelte" @@ -26,6 +26,8 @@ export const emitter = new Emitter() const modals: Record = {} +let dropping: Promise | undefined + // Modal ids live in SvelteKit page state, so a `goto` that drops `state` closes them. export const getModalStack = () => (page.state.modals ?? []).map(id => modals[id]).filter(Boolean) @@ -33,24 +35,37 @@ export const getModal = () => last(getModalStack()) export type NavigateOptions = Parameters[1] & {keepModal?: boolean} +// A back with nothing left to pop fires no popstate, so the wait for one is bounded. const popHistory = () => - new Promise(resolve => { - addEventListener("popstate", resolve, {once: true}) + new Promise(resolve => { + const finish = (popped: boolean) => { + clearTimeout(timeout) + removeEventListener("popstate", onPopState) + resolve(popped) + } + const onPopState = () => finish(true) + const timeout = setTimeout(() => finish(false), ms(1)) + + addEventListener("popstate", onPopState) history.back() }) // SvelteKit reuses its navigation index for a replacing `goto`, so entries are given back instead. -const dropModalEntries = async () => { - let dropped = false +const dropModalEntries = () => { + dropping ??= call(async () => { + let dropped = false - while (getModalStack().length > 0) { - await popHistory() + while (getModalStack().length > 0 && (await popHistory())) { + dropped = true + } - dropped = true - } + return dropped + }).finally(() => { + dropping = undefined + }) - return dropped + return dropping } export const navigate = async (path: string, {keepModal, ...options}: NavigateOptions = {}) => {