From 8b291a9a7e464fd5d939591309eddfaffc30aff0 Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 3 Sep 2026 15:51:13 +0200 Subject: [PATCH] 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. --- .../Planner/placeNavigation.test.ts | 41 ++++++++++++++++++- .../src/components/Planner/placeNavigation.ts | 25 ++++++++++- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/client/src/components/Planner/placeNavigation.test.ts b/client/src/components/Planner/placeNavigation.test.ts index d2ea8ddc0..c99dc81de 100644 --- a/client/src/components/Planner/placeNavigation.test.ts +++ b/client/src/components/Planner/placeNavigation.test.ts @@ -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]) + }) +}) diff --git a/client/src/components/Planner/placeNavigation.ts b/client/src/components/Planner/placeNavigation.ts index 725e5ca86..61af1fb31 100644 --- a/client/src/components/Planner/placeNavigation.ts +++ b/client/src/components/Planner/placeNavigation.ts @@ -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 }