fix(maps): handing a place to a maps app leaves no blank window behind (#2218)

In an installed app, opening a maps target in a new context left an empty
window: the platform switches to the maps application before that context
paints anything, and coming back lands on a blank page the user has to
dismiss before TREK is usable again. The handover now happens from the page
the user is already on when TREK runs as an installed app, which is where
there is no tab strip to close a stray window from. A browser tab still opens
a tab, because there a link should.
This commit is contained in:
Maurice
2026-09-03 15:51:13 +02:00
parent 07e4a558e6
commit 8b291a9a7e
2 changed files with 63 additions and 3 deletions
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { getNavigationTargets, showsAppleMaps } from './placeNavigation'
import { getNavigationTargets, openNavigationTarget, showsAppleMaps } from './placeNavigation'
import type { Place } from '../../types'
// FE-PLANNER-NAV-001 to FE-PLANNER-NAV-008
@@ -105,3 +105,42 @@ describe('getNavigationTargets', () => {
expect(targets.find(t => t.id === 'waze')!.url).toBe('https://waze.com/ul?ll=48.2038,16.3616&navigate=yes')
})
})
describe('openNavigationTarget', () => {
const target = { id: 'google', label: 'Google Maps', url: 'https://maps.google.com/?q=1,2' } as const
afterEach(() => { vi.unstubAllGlobals(); vi.restoreAllMocks() })
function stubDisplayMode(installed: boolean, iosStandalone = false) {
const open = vi.fn()
const assigned: string[] = []
vi.stubGlobal('window', {
matchMedia: (q: string) => ({ matches: installed && q.includes('display-mode') }),
navigator: { standalone: iosStandalone },
open,
get location() { return { get href() { return '' }, set href(v: string) { assigned.push(v) } } },
})
return { open, assigned }
}
it('FE-PLANNER-NAV-101: a browser tab keeps opening a tab', () => {
const { open, assigned } = stubDisplayMode(false)
openNavigationTarget(target)
expect(open).toHaveBeenCalledWith(target.url, '_blank', 'noopener,noreferrer')
expect(assigned).toEqual([])
})
it('FE-PLANNER-NAV-102: an installed app hands over from the current context, leaving no blank window (#2218)', () => {
const { open, assigned } = stubDisplayMode(true)
openNavigationTarget(target)
expect(open).not.toHaveBeenCalled()
expect(assigned).toEqual([target.url])
})
it('FE-PLANNER-NAV-103: an iOS home-screen app is recognised without the display-mode query', () => {
const { open, assigned } = stubDisplayMode(false, true)
openNavigationTarget(target)
expect(open).not.toHaveBeenCalled()
expect(assigned).toEqual([target.url])
})
})
@@ -95,7 +95,28 @@ export function getNavigationTargets(
return targets
}
/** Opens a target the way every external link in the planner is opened. */
/**
* Hands the place over to a maps application.
*
* In a browser tab this opens a tab, which is what a link should do. In an
* installed app it must not: the maps application takes over from the new
* context before it paints, so what the user comes back to is an empty window
* they have to dismiss before TREK is usable again (#2218). Navigating the
* current context instead means the handover happens from the page the user is
* already on, and returning lands them back where they were, because the app
* shell was never replaced by the time the platform switched away.
*/
export function openNavigationTarget(target: NavigationTarget): void {
window.open(target.url, '_blank', 'noopener,noreferrer')
if (isInstalledApp()) window.location.href = target.url
else window.open(target.url, '_blank', 'noopener,noreferrer')
}
/** True in a display mode that has no tab strip to close a stray window from. */
function isInstalledApp(): boolean {
if (typeof window === 'undefined') return false
const standalone = ['standalone', 'fullscreen', 'minimal-ui'].some(
mode => window.matchMedia?.(`(display-mode: ${mode})`).matches,
)
// iOS predates the display-mode query for home-screen apps.
return standalone || (window.navigator as { standalone?: boolean }).standalone === true
}