Keep a navigation out of a modal from popping history it does not own

This commit is contained in:
Coracle-Bot 2026-09-25 20:15:49 +00:00
parent da6962d995
commit 31b1f7f80d

View file

@ -1,5 +1,5 @@
import type {Component} from "svelte" 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 {goto, pushState, replaceState} from "$app/navigation"
import {page} from "$app/state" import {page} from "$app/state"
import type {DialogSize} from "@lib/components/Dialog.svelte" import type {DialogSize} from "@lib/components/Dialog.svelte"
@ -26,6 +26,8 @@ export const emitter = new Emitter()
const modals: Record<string, Modal> = {} const modals: Record<string, Modal> = {}
let dropping: Promise<boolean> | undefined
// Modal ids live in SvelteKit page state, so a `goto` that drops `state` closes them. // 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) 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<typeof goto>[1] & {keepModal?: boolean} export type NavigateOptions = Parameters<typeof goto>[1] & {keepModal?: boolean}
// A back with nothing left to pop fires no popstate, so the wait for one is bounded.
const popHistory = () => const popHistory = () =>
new Promise(resolve => { new Promise<boolean>(resolve => {
addEventListener("popstate", resolve, {once: true}) 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() history.back()
}) })
// SvelteKit reuses its navigation index for a replacing `goto`, so entries are given back instead. // SvelteKit reuses its navigation index for a replacing `goto`, so entries are given back instead.
const dropModalEntries = async () => { const dropModalEntries = () => {
let dropped = false dropping ??= call(async () => {
let dropped = false
while (getModalStack().length > 0) { while (getModalStack().length > 0 && (await popHistory())) {
await popHistory() dropped = true
}
dropped = true return dropped
} }).finally(() => {
dropping = undefined
})
return dropped return dropping
} }
export const navigate = async (path: string, {keepModal, ...options}: NavigateOptions = {}) => { export const navigate = async (path: string, {keepModal, ...options}: NavigateOptions = {}) => {