Skip to content

feat(web): make Back step out of what was opened instead of leaving the app - #739

Merged
Medformatik merged 1 commit into
mainfrom
ccr-b0188873-9mloyb
Nov 2, 2026
Merged

Medformatik merged 1 commit into
mainfrom
ccr-b0188873-9mloyb

Conversation

@Medformatik

@Medformatik Medformatik commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

What changed and why

In the installed PWA (and the native app), the system Back gesture closed OpenMapX instead of closing what the user had just opened. DeepLinkManager mirrored all state into the URL with replaceState only, so the history stack never grew past one entry.

Now every surface gets a history entry, and Back closes the most recent one:

Opened Back does
Place card, category/search results, directions, saved list, data-source item, measure/travel-time tool Closes it. The map stays where it is, and whatever remains open keeps its state (list, scroll position, loaded place).
Dialog, menu, popover (all of them, via wrappers) Closes it, like Escape.
Drawer menu, full-screen search, photo gallery, map-click card Closes it.
Bottom sheet expanded to full height Lowers it to the middle detent. The next Back closes its content.
Active turn-by-turn or transit guidance Ends the trip, like the End button.
Nothing left Leaves the app, as before.

How it works

  • lib/backNavigation.ts is the only code that writes history. navigationPath() (in deepLink.ts) lists the open surfaces and leaves out the camera, base layer and overlays, so panning or toggling a layer still rewrites the current entry.
  • Opening something deeper pushes an entry. Closing rewrites the current entry, so a later Back cannot reopen it. Switching between siblings (another place in the same list, another category chip) also rewrites.
  • App code never calls history.back(). Next.js treats any popstate as a traversal and cancels a route navigation in flight, so a drawer link that closes the drawer as it navigates would silently stay on the map. Instead, a surface closed by its own control leaves its entry behind. A Back that lands on an entry showing nothing new keeps going, so Back always visibly does something. The next surface to open reuses such an entry, so history doesn't pile up.
  • useBackDismiss(open, onBack) gives a transient surface its own entry. components/ui/{Dialog,Menu,Popover} wrap MUI and close on Back the way Escape closes them. All ~60 importers now use them, a Biome noRestrictedImports rule keeps new code on them, and integrations get @/integration-api/components/Dialog.
  • Text searches are now encoded as search= in the URL, so a results list survives Back (and sharing).
  • Native shell: useHardwareBack sends Android's hardware Back to WebView.goBack() while the page has history; otherwise Back goes to the system. iOS already uses the WebView's swipe-back gesture.

Trade-offs

  • History traversal no longer restores camera, base layer, globe or overlays (two existing tests asserted that, and I updated them). A loaded or shared link still restores everything.
  • On desktop, browser Back now steps through panels and dialogs as well.
  • Forward (desktop only) can land on a leftover entry that changes nothing.

Verification

  • Unit tests: historyStep, navigationPath, push/rewrite/reuse rules, layers, step-over of leftover entries (including returning from another page), and the router-navigation case (apps/web/src/lib/backNavigation.test.ts). Also Back in DeepLinkManager (closes the card in place without re-searching the list), the Dialog wrapper, the bottom sheet, and the mobile useHardwareBack/App wiring.
  • pnpm exec vitest run --project web: 5723 passed. 8 failures in useNavJunctions.test.tsx and CategoryResultMarkers.test.tsx also fail on an unmodified checkout in this environment.
  • pnpm -C apps/mobile run test: 1220 passed. tsc --noEmit passes for web, tsconfig.integrations.json, tsconfig.sw.json and mobile.
  • biome check . --diagnostic-level=error, Prettier on the changed docs and pnpm check-translations pass. pnpm check-dead-code reports only findings in files this PR doesn't touch.
  • Browser check with Playwright on next dev, 390×844 touch viewport, no API backend. Back closed: the drawer; the Settings dialog opened from the drawer, then the drawer underneath; the directions panel; the full-screen search; the layer popover. Closing directions with its own button didn't leave a Back that reopens it. A drawer link to /settings/offline navigated normally, and Back from there returned to the map. The page didn't reload between steps.
  • Not verified: Android hardware Back in a real PWA install or the native app, iOS swipe-back, and flows that need the API (place cards from real search results, active guidance, expanding a sheet by drag).

Release and upgrade impact

No migration or operator action. Contributors must import Dialog, Menu and Popover from @/components/ui/... (integrations: @/integration-api/components/Dialog); lint enforces this.

…he app

The map page mirrored all state into the URL with replaceState only, so the
history stack never grew and the system Back gesture closed the installed PWA
(and the native app). Every surface now gets a history entry and Back closes
the last one opened.

- lib/backNavigation owns all history writes. Opening a panel, place, list item
  or map tool pushes an entry (navigationPath ignores camera, base layer and
  overlays, which keep rewriting the current entry). Closing rewrites the
  entry; sibling switches rewrite it too.
- Back restores only the open surfaces: the map stays where the user left it,
  and stepping out of something closes it in place instead of rebuilding the
  rest from the URL.
- App code never calls history.back(): to the Next.js router that popstate
  would cancel a page navigation in flight (a drawer link that also closes the
  drawer). Closed surfaces leave their entry; a Back landing on an entry that
  shows nothing new keeps going, and the next surface reuses such an entry.
- useBackDismiss gives transient surfaces an entry: Dialog/Menu/Popover via
  Back-aware wrappers in components/ui (enforced by lint, exposed to
  integrations), the drawer menu, full-screen search, photo gallery, map-click
  card, an expanded bottom sheet (Back lowers it) and active guidance (Back
  ends the trip like the End button).
- Text searches are encoded as `search=` so they survive Back and sharing.
- The native shell sends Android's hardware Back into the WebView history.
@Medformatik
Medformatik enabled auto-merge (squash) November 2, 2026 10:03
@Medformatik
Medformatik merged commit 08ccd20 into main Nov 2, 2026
40 of 44 checks passed
@Medformatik
Medformatik deleted the ccr-b0188873-9mloyb branch November 2, 2026 10:14
@github-actions github-actions Bot locked and limited conversation to collaborators Nov 2, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant