Keep replacement dialogs open when handing off modal history
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest'
|
||||||
|
import { effectScope, nextTick, ref } from 'vue'
|
||||||
|
import { useModalHistory } from '../useModalHistory'
|
||||||
|
|
||||||
|
describe('modal history transitions', () => {
|
||||||
|
it('replaces a setup dialog with its funding dialog without backing out of it', async () => {
|
||||||
|
window.history.replaceState({ position: 5 }, '')
|
||||||
|
const back = vi.spyOn(window.history, 'back').mockImplementation(() => {})
|
||||||
|
const push = vi.spyOn(window.history, 'pushState')
|
||||||
|
const first = ref(false), second = ref(false)
|
||||||
|
const closeFirst = vi.fn(() => { first.value = false })
|
||||||
|
const closeSecond = vi.fn(() => { second.value = false })
|
||||||
|
const scope = effectScope()
|
||||||
|
scope.run(() => { useModalHistory(first, closeFirst); useModalHistory(second, closeSecond) })
|
||||||
|
first.value = true
|
||||||
|
await nextTick()
|
||||||
|
expect(push).toHaveBeenCalledTimes(1)
|
||||||
|
first.value = false
|
||||||
|
second.value = true
|
||||||
|
await nextTick()
|
||||||
|
await Promise.resolve()
|
||||||
|
expect(back).not.toHaveBeenCalled()
|
||||||
|
expect(push).toHaveBeenCalledTimes(1)
|
||||||
|
expect(window.history.state).toEqual({ position: 5, __archyModal: 1 })
|
||||||
|
window.history.replaceState({ position: 5 }, '')
|
||||||
|
window.dispatchEvent(new PopStateEvent('popstate', { state: { position: 5 } }))
|
||||||
|
await nextTick()
|
||||||
|
expect(closeSecond).toHaveBeenCalledOnce()
|
||||||
|
expect(closeFirst).not.toHaveBeenCalled()
|
||||||
|
expect(back).not.toHaveBeenCalled()
|
||||||
|
scope.stop()
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -24,6 +24,9 @@ const stack: Entry[] = []
|
|||||||
// gone, so the close-side cleanup must not call history.back() again.
|
// gone, so the close-side cleanup must not call history.back() again.
|
||||||
let poppedClose = false
|
let poppedClose = false
|
||||||
let listening = false
|
let listening = false
|
||||||
|
// Vue can replace one modal with another in the same update. Reuse that
|
||||||
|
// history entry rather than scheduling a Back that would close the new modal.
|
||||||
|
let pendingCleanup: { depth: number } | null = null
|
||||||
|
|
||||||
function modalDepth(state: unknown): number {
|
function modalDepth(state: unknown): number {
|
||||||
return (state as { __archyModal?: number } | null)?.__archyModal ?? 0
|
return (state as { __archyModal?: number } | null)?.__archyModal ?? 0
|
||||||
@@ -58,10 +61,11 @@ export function useModalHistory(show: Ref<boolean>, close: () => void) {
|
|||||||
try {
|
try {
|
||||||
// Preserve vue-router's own keys in state — clobbering them breaks
|
// Preserve vue-router's own keys in state — clobbering them breaks
|
||||||
// its scroll restoration and position tracking.
|
// its scroll restoration and position tracking.
|
||||||
window.history.pushState(
|
const replacement = pendingCleanup !== null && modalDepth(window.history.state) === pendingCleanup.depth
|
||||||
{ ...(window.history.state ?? {}), __archyModal: stack.length },
|
pendingCleanup = null
|
||||||
'',
|
const state = { ...(window.history.state ?? {}), __archyModal: stack.length }
|
||||||
)
|
if (replacement) window.history.replaceState(state, '')
|
||||||
|
else window.history.pushState(state, '')
|
||||||
} catch { /* history can throw in exotic embeds — modal still works */ }
|
} catch { /* history can throw in exotic embeds — modal still works */ }
|
||||||
} else {
|
} else {
|
||||||
const wasTop = stack[stack.length - 1] === entry
|
const wasTop = stack[stack.length - 1] === entry
|
||||||
@@ -75,8 +79,16 @@ export function useModalHistory(show: Ref<boolean>, close: () => void) {
|
|||||||
// only if it is still the current one (a route change after opening
|
// only if it is still the current one (a route change after opening
|
||||||
// moves history past it — backing out then would eat a real entry).
|
// moves history past it — backing out then would eat a real entry).
|
||||||
if (wasTop && modalDepth(window.history.state) > stack.length) {
|
if (wasTop && modalDepth(window.history.state) > stack.length) {
|
||||||
|
const cleanup = { depth: modalDepth(window.history.state) }
|
||||||
|
pendingCleanup = cleanup
|
||||||
|
queueMicrotask(() => {
|
||||||
|
if (pendingCleanup !== cleanup) return
|
||||||
|
pendingCleanup = null
|
||||||
|
if (modalDepth(window.history.state) === cleanup.depth && cleanup.depth > stack.length) {
|
||||||
try { window.history.back() } catch { /* same guard as above */ }
|
try { window.history.back() } catch { /* same guard as above */ }
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user