fix(nav,map): iOS PWA bottom-nav safe area + map controls clearance and zoom-to-fit - #4499
Conversation
…le nav (#4497) Three iOS-PWA bugs on the bottom bar, reported on an iPhone 14 Pro Max standalone install. A. Safe-area padding was zeroed — a regression from #4484. The `padding-bottom: 0 !important` added to `.sidebar` (to beat the two landscape-rail blocks that set padding with !important) also beat SourceNav's own `padding-bottom: env(safe-area-inset-bottom)`, docking the bar flush to the screen edge. In a standalone PWA that puts the tab row inside the home-indicator swipe zone, so taps ran the system gesture instead of hitting the tab. Now `env(safe-area-inset-bottom) !important`, which satisfies both constraints. Nothing else shifts: App.css already reserves `--app-nav-bar-height + safe-area-inset-bottom`, and .save-bar / .nodes-split-view offset by that same sum. B. Horizontal scroll could chain out to Safari's back-swipe / app-switcher edge gesture and eject the user from the PWA. Adds `touch-action: pan-x` and `overscroll-behavior-x: contain`. Partial by nature — the iOS home-indicator and app-switcher gestures are OS-level and CSS cannot suppress them; A is what actually lifts the targets clear of the gesture zone, B only stops the scroll chaining. C. Nothing indicated the bar scrolls, so tabs were missed entirely. Adds edge fades driven by the measured scroll position, so a bar whose tabs all fit shows no fade. Uses `mask-image` rather than an ::after overlay because a pseudo-element inside a scroll container scrolls away with the content. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9NzRtqE8eSMS8tvAeodUB
…-all (#4495, #4496) Both touch `.map-controls`; the issues ask for the placement to be coordinated, so they land together. #4495 — the controls panel was partly covered by the Leaflet attribution on mobile. Raising z-index cannot fix it: the attribution lives in Leaflet's own `.leaflet-bottom` (z-index 1000, measured), while this panel must stay at 997 to yield to the Sources drawer (999). 997 < 1000, so wherever they overlap the attribution wins. Bounding the panel's height means they never overlap at all, with the body scrolling when the feature list is long (the base rule is `overflow: hidden`, so without that the excess would just be clipped). Verified at 390x780: panel bottom 515, attribution top 707, overlap false. Deliberately not the icon-row redesign the reporter floated — the position fix addresses the complaint without a layout rethink, and leaves the existing drag behaviour and muscle memory intact. #4496 — adds a zoom-to-fit-all-nodes button. It sits in the controls header so it stays reachable while the panel is collapsed; one-tap re-framing is the whole point, and click-to-expand-first would spoil it. `.map-controls` is a SIBLING of MapContainer with no access to the map instance, so this follows the file's existing shape (TracerouteBoundsController): a controller inside the map reacting to a prop. The prop is a monotonically increasing counter, not a boolean, so repeated taps re-fit even when the node set hasn't changed. Fits to the OFFSET marker positions, matching the pins the user actually sees, per the #4016/#4155 single-position rule — fitting to raw centres could leave a visible pin just outside the viewport. Single-node case centres instead of fitBounds, which would snap to max zoom on zero-area bounds. Verified live: zoomed in until 0 of 206 markers were on screen, one tap returned all 176 positioned nodes to view with padding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9NzRtqE8eSMS8tvAeodUB
|
Claude finished @Yeraze's task —— View job PR Review: fix(nav,map): iOS PWA bottom-nav safe area + map controls clearance and zoom-to-fit
This is a well-engineered PR that correctly identifies and fixes three distinct problems. The implementation quality is high throughout — good rationale in comments, measured over assumed for geometry checks, and genuine regression tests. A few observations worth considering: Correctness & Logic
The counter approach is correct and deliberately documented (
map.setView(positions[0], Math.max(map.getZoom(), 14), { animate: true });
// eslint-disable-next-line react-hooks/exhaustive-deps -- keyed on nodePositions like measurePoints below
[nodePositions, nodesWithPosition.map(n => n.nodeNum).join(',')],The disable comment is accurate — this mirrors the established CSS / CascadeSafe-area regression fix ( The fix is correct.
This is the right geometric solution given the z-index constraint (997 vs 1000 for the attribution). The hardcoded
Excellent choice over One subtle point: applying Performance
const observer =
typeof ResizeObserver !== 'undefined' ? new ResizeObserver(measure) : null;
observer?.observe(el);The
The memo is keyed on Test CoverageCSS-source tests ( The test strategy of reading CSS source directly is well-reasoned and clearly documented — jsdom implements no layout/cascade, so stylesheet assertions are the correct tool for these contract tests. The
function mobileOverrideRule(selector: string): string {
const block = nodesCss.slice(nodesCss.lastIndexOf('Map Controls (mobile override)'));
...
}Using Overflow cue tests ( The 1px slack logic in Minor Observations
SummaryThe implementation is solid: the safe-area regression is correctly fixed, the geometric approach to the map-controls/attribution overlap is sound, and the zoom-to-fit counter pattern is clean and testable. The cascade-order discipline (post-base-rule media overrides) is consistently applied and documented. No blocking issues found. Approved for merge with confidence. The one minor item to validate on real hardware: the |
Closes #4497, #4495, #4496.
Two commits: #4497 is a nav bug; #4495/#4496 both touch
.map-controlsand the issues ask for their placement to be coordinated, so they land together.#4497 — iOS PWA bottom nav
Reported on an iPhone 14 Pro Max standalone install.
A. Safe-area padding was zeroed — a regression I introduced in #4484
The
padding-bottom: 0 !importantI added to.sidebar(to beat the two landscape-rail blocks that also set padding with!important) also beat SourceNav's ownpadding-bottom: env(safe-area-inset-bottom). The bar docked flush to the screen edge, putting the tab row inside the iOS home-indicator swipe zone — so taps near it ran the system gesture instead of hitting a tab.I flagged at the time that the
!importantwas load-bearing for the landscape case; I didn't check what else it outranked.Now
env(safe-area-inset-bottom) !important, which satisfies both constraints. Nothing else shifts — App.css already reserves--app-nav-bar-height + safe-area-inset-bottom, and.save-bar/.nodes-split-viewoffset by that same sum.B. Horizontal scroll could eject the user from the PWA
Adds
touch-action: pan-xandoverscroll-behavior-x: containso a drag on the bar can't chain out to Safari's back-swipe / app-switcher edge gesture.Partial by nature, stated plainly: the iOS home-indicator and app-switcher gestures are OS-level and CSS cannot suppress them. Fix A is what actually lifts the tap targets clear of the gesture zone; B only stops the scroll chaining. Worth confirming on real hardware — a true standalone PWA can't be reproduced in desktop Chrome.
C. No cue that the bar scrolls
Edge fades driven by the measured scroll position, so a bar whose tabs all fit shows no fade at all. Uses
mask-imagerather than an::afteroverlay, because a pseudo-element inside a scroll container scrolls away with the content.#4495 — map controls covered by the attribution
z-index cannot fix this, and the measurement proves it: the attribution lives in Leaflet's own
.leaflet-bottomatz-index: 1000, while.map-controlsmust stay at997to yield to the Sources drawer (999). 997 < 1000, so wherever they overlap the attribution wins.Bounding the panel's height means they never overlap at all, with the body scrolling when the feature list is long (the base rule is
overflow: hidden, so the excess would otherwise just be clipped).Verified at 390×780: panel bottom 515, attribution top 707,
overlap: false.Deliberately not the icon-row redesign the reporter floated — the position fix addresses the complaint without a layout rethink, and keeps the existing drag behaviour and muscle memory. That redesign remains open as its own idea.
#4496 — zoom to fit all nodes
A
target-icon button in the controls header. It sits in the header so it stays reachable while the panel is collapsed: one-tap re-framing is the point, and click-to-expand-first would spoil it..map-controlsis a sibling ofMapContainerwith no access to the map instance, so this follows the shape already in the file (TracerouteBoundsController): a controller inside the map reacting to a prop. The prop is a monotonically increasing counter, not a boolean, so repeated taps re-fit even when the node set hasn't changed.Fits to the offset marker positions, matching the pins the user actually sees, per the #4016/#4155 single-position rule — fitting raw centres could leave a visible pin just outside the viewport. The single-node case centres rather than calling
fitBounds, which snaps to max zoom on zero-area bounds.Verified live: zoomed in until 0 of 206 markers were on screen; one tap returned 176 / 176 positioned nodes to view with padding.
Test plan
success: true, 12987 passed, 0 failed.npx tsc --noEmitclean;npm run lint:cino FAIL lines.Note for reviewers
One of the new tests initially anchored its CSS lookup on a phrase that also appears in an earlier cross-reference comment, so it sliced in the base
.map-controlsrule instead of the mobile override and failed. Fixed tolastIndexOf. Mentioning it becausesrc/styles/nodes.csshas a documented history of exactly this cascade-order confusion (#3532).Generated by Claude Code