diff --git a/src/uk_bus_stops/static/app.js b/src/uk_bus_stops/static/app.js index 76f9605..4c8eb36 100644 --- a/src/uk_bus_stops/static/app.js +++ b/src/uk_bus_stops/static/app.js @@ -410,6 +410,18 @@ function clearHoveredStop() { hoveredMarkerHalo = null; } +/** Centre a selected stop in the portion of the map not covered by the mobile sheet. */ +function centreSelectedStop(stop) { + const zoom = Math.max(map.getZoom(), 17); + let verticalOffset = 0; + if (isMobile() && byId('sidebar').classList.contains('panel-open')) { + verticalOffset = Math.min(map.getSize().y, byId('sidebar').getBoundingClientRect().height) / 2; + } + const stopPoint = map.project(L.latLng(stop.lat, stop.lon), zoom); + const adjustedCenter = map.unproject(stopPoint.add(L.point(0, verticalOffset)), zoom); + map.setView(adjustedCenter, zoom); +} + function osmUrl(stop, edit = false) { return `https://www.openstreetmap.org/${edit ? 'edit?' : ''}${edit ? `${stop.type}=${stop.id}` : `${stop.type}/${stop.id}`}`; } @@ -462,15 +474,15 @@ async function selectStop(stop, updateUrl = true) { show('atco-missing', !stop.atco_code); byId('copy-status').textContent = ''; renderTags(stop.tags); + if (isMobile()) setPanel(true); moveMap( - () => map.setView([stop.lat, stop.lon], Math.max(map.getZoom(), 17)), + () => centreSelectedStop(stop), () => { if (selectedStop && selectedStop.type === stop.type && selectedStop.id === stop.id) { loadVisibleMapStops(); } } ); - if (isMobile()) setPanel(true); byId('route-list').replaceChildren(); show('route-loading'); diff --git a/tests/test_uk_bus_stops_playwright.py b/tests/test_uk_bus_stops_playwright.py index e7ad2c1..0374905 100644 --- a/tests/test_uk_bus_stops_playwright.py +++ b/tests/test_uk_bus_stops_playwright.py @@ -243,3 +243,38 @@ def test_mobile_layout_opens_results_sheet(chromium_browser: Any, flask_url: str playwright_api.expect(page.locator("#sidebar")).to_have_class(re.compile("panel-open")) playwright_api.expect(page.get_by_label("Location or ATCO code")).to_be_visible() page.close() + + +def test_mobile_selected_stop_stays_above_detail_sheet( + chromium_browser: Any, flask_url: str +) -> None: + """A shared stop is centred in the visible map area above the mobile sheet.""" + page = chromium_browser.new_page(viewport={"width": 390, "height": 844}) + stop = { + "type": "node", "id": 485403178, "lat": 51.439385, "lon": -2.601798, + "name": "West Street", "atco_code": "0100BRA10073", "indicator": None, + "bearing": "SW", "transport_type": "Bus stop", + "tags": {"highway": "bus_stop", "naptan:Bearing": "SW"}, + } + page.route("**/api/stop/node/485403178", lambda route: route.fulfill(json={"stop": stop})) + page.route("**/api/stop/node/485403178/routes", lambda route: route.fulfill(json={"routes": []})) + page.route("**/api/stops?*", lambda route: route.fulfill(json={ + "kind": "location", + "location": {"lat": stop["lat"], "lon": stop["lon"], "label": stop["name"]}, + "stops": [stop], + })) + page.route("**/api/stops/in-bounds?*", lambda route: route.fulfill(json={ + "kind": "map", "stops": [stop], + })) + page.goto(f"{flask_url}?node=485403178", wait_until="networkidle") + playwright_api.expect(page.locator("#stop-name")).to_have_text("West Street") + playwright_api.expect(page.locator(".selected-stop-marker")).to_be_visible() + page.wait_for_timeout(400) + position = page.evaluate("""() => { + const point = map.latLngToContainerPoint(selectedMarkerHalo.getLatLng()); + const mapRect = document.getElementById('map').getBoundingClientRect(); + const sheetRect = document.getElementById('sidebar').getBoundingClientRect(); + return {markerY: point.y, visibleBottom: sheetRect.top - mapRect.top}; + }""") + assert 0 < position["markerY"] < position["visibleBottom"] + page.close()