diff --git a/map/static/map/editor.js b/map/static/map/editor.js index 8cddd7b..040db3e 100644 --- a/map/static/map/editor.js +++ b/map/static/map/editor.js @@ -395,25 +395,18 @@ var EditorTool = (function() { // ---- Toolbar (edit count / undo / redo) --------------------------------------- - // The top bar centers the search box between its two flex side sections - // (equal-width while there's room, but content-floored on narrow - // windows), so "centered" for top notifications means the SEARCH BOX's - // center, not the viewport's -- align to it directly. - function alignToolbar() { - var searchBox = document.getElementById("searchBox"); - if (!searchBox || !toolbar) { - return; - } - var box = searchBox.getBoundingClientRect(); - if (box.width > 0) { - toolbar.style.left = (box.left + box.width / 2) + "px"; - } - } + // No JS centring here any more. This used to measure the search box and set + // an inline `left`, because the top bar was a row of floating islands over + // the map and "centred" could not be expressed in CSS. The app bar and the + // map's overlay layer are now both window-width, so plain CSS centring lines + // the toolbar up with the search field for free -- and the inline `left` it + // used to write was in VIEWPORT coordinates while the element's containing + // block is the overlay layer, which is exactly what threw the toolbar off + // centre once that layer stopped being the whole viewport. function updateToolbar() { var any = actions.length > 0 || redoStack.length > 0; toolbar.style.display = any ? "flex" : "none"; - alignToolbar(); editCountEl.textContent = actions.length + " edit" + (actions.length === 1 ? "" : "s"); undoBtn.disabled = applyInFlight || actions.length === 0; redoBtn.disabled = applyInFlight || redoStack.length === 0; @@ -1193,7 +1186,6 @@ var EditorTool = (function() { e.returnValue = ""; } }); - window.addEventListener("resize", alignToolbar); }); return { diff --git a/map/static/map/map.css b/map/static/map/map.css index 083362e..c624f23 100644 --- a/map/static/map/map.css +++ b/map/static/map/map.css @@ -31,6 +31,7 @@ --dock-left-inset: var(--dock-left-width); --dock-right-inset: 0px; + /* One shared feel for anything that moves, and the lifted shadow the genuinely floating things (hints, popovers, tooltip) share. Docked surfaces get a border instead -- they are attached, not hovering. */ @@ -386,22 +387,40 @@ body.dockResizing { the part of it you can actually SEE, so this layer -- unlike #map -- insets by whatever the docks are covering. It is the only thing that needs to know the dock widths, and it costs the map nothing to tell it. */ +/* The layer the still-floating things live in: the hint bars, the selection + action bar, the active-filter banner. + + It spans the FULL window, exactly like #map, and never changes size for any + reason but a window resize. That is what makes its centre the window's + centre -- the same thing the app bar centres its search field on -- so a + hint always lines up with the search field above it, whatever the docks are + doing. Two earlier versions insetted it by the docks so hints would centre + on the *visible* map: that put them 109px out of line with the search field, + and, once a dock made the layer narrower than a wide bar, the auto margins + collapsed and the bar drifted further still. + + Trade-off: a bar wider than the gap between the docks runs underneath one. + The docks paint above this layer (z-index 20 vs 15), so it is clipped rather + than overlapping, and at any normal window width there is room to spare. + + Children are centred by `justify-self`, NOT by `left`/`right`/margins. That + is not a style preference: an absolutely positioned child that is + display:none while this layer's width changes can come back with a stale + USED value for a percentage or resolved `left` -- measured as declared + `left: 0px` but used `left: 800px`, which put the editor toolbar a quarter + of the window off-centre until something forced it to reflow. Grid alignment + never computes a `left` at all, so there is nothing to go stale. Children + still place themselves vertically with top/bottom. */ #mapOverlays { position: fixed; - top: var(--appbar-height); - left: var(--dock-left-inset); - right: var(--dock-right-inset); - bottom: 0; + inset: var(--appbar-height) 0 0 0; + display: grid; pointer-events: none; z-index: 15; - transition: left 0.12s var(--panel-ease), right 0.12s var(--panel-ease); -} - -@media (prefers-reduced-motion: reduce) { - #mapOverlays { transition: none; } } #mapOverlays > * { + justify-self: center; pointer-events: auto; } @@ -1344,8 +1363,6 @@ button:focus-visible, select:focus-visible, input[type="checkbox"]:focus-visible #selectionPanel { position: absolute; bottom: 22px; - left: 50%; - transform: translateX(-50%); z-index: 600; display: flex; align-items: center; @@ -1375,8 +1392,6 @@ button:focus-visible, select:focus-visible, input[type="checkbox"]:focus-visible #editorToolbar { position: absolute; top: 58px; - left: 50%; - transform: translateX(-50%); z-index: 600; display: flex; align-items: center; @@ -1401,8 +1416,6 @@ button:focus-visible, select:focus-visible, input[type="checkbox"]:focus-visible #editorHint { position: absolute; bottom: 64px; - left: 50%; - transform: translateX(-50%); z-index: 1401; padding: 7px 14px; background: var(--surface-2); @@ -1503,8 +1516,6 @@ button:focus-visible, select:focus-visible, input[type="checkbox"]:focus-visible #activeFilterBanner { position: absolute; top: 68px; /* Just below the floating search pill. */ - left: 50%; - transform: translateX(-50%); z-index: 500; display: flex; align-items: center; @@ -2605,8 +2616,6 @@ button:focus-visible, select:focus-visible, input[type="checkbox"]:focus-visible #networkHint { position: absolute; bottom: 22px; - left: 50%; - transform: translateX(-50%); z-index: 600; padding: 8px 16px; background: var(--surface-2); @@ -2958,8 +2967,13 @@ button:focus-visible, select:focus-visible, input[type="checkbox"]:focus-visible } /* Below this the pills lose their labels -- the icons are unambiguous next to - each other, and the search field is worth more than the words. */ -@media (max-width: 980px) { + each other, and the search field is worth more than the words. + The threshold is set by geometry, not taste: the app bar centres its search + field by giving both side sections an equal share of the leftover space, and + that only holds while each side's content FITS its share. With labels the + right-hand cluster needs ~260px, and it stops fitting at about this width -- + below which the search field would start drifting off-centre. */ +@media (max-width: 1150px) { #mainSearchWrap { flex: 1 1 auto; } @@ -2991,14 +3005,6 @@ button:focus-visible, select:focus-visible, input[type="checkbox"]:focus-visible width: min(var(--dock-right-width), 78vw); } - /* At this size a dock covers most of the map, so insetting the hints inside - what is left would squeeze them to nothing -- let them use the whole - window and sit under the dock instead. */ - #mapOverlays { - left: 0; - right: 0; - } - /* Dragging a width is a pointer-and-space affordance; neither applies here. */ .dockResizeHandle { display: none; @@ -3017,18 +3023,14 @@ button:focus-visible, select:focus-visible, input[type="checkbox"]:focus-visible } } -/* Leaflet's controls live inside #map, which now runs under the tool dock -- - push the right-hand ones (zoom, attribution) clear of it. */ +/* Leaflet's controls live inside #map, which runs under the docks -- push them + clear. Each side insets by its own dock (unlike #mapOverlays above): these + hug an edge rather than centring on anything, so there is nothing to keep + symmetric. Untransitioned for the same reason as #mapOverlays. */ .leaflet-right { right: var(--dock-right-inset); - transition: right 0.12s var(--panel-ease); } .leaflet-left { left: var(--dock-left-inset); - transition: left 0.12s var(--panel-ease); -} - -@media (prefers-reduced-motion: reduce) { - .leaflet-right, .leaflet-left { transition: none; } } diff --git a/tools/ui_behaviour.py b/tools/ui_behaviour.py index 94f464f..8a182bb 100644 --- a/tools/ui_behaviour.py +++ b/tools/ui_behaviour.py @@ -249,6 +249,68 @@ def holdsStill(label, action): check("map never moves: dragging the dock's width", not bad, "%d/%d frames" % (len(bad), len(rows))) + # ---- Everything centred agrees on one centre ------------------------------ + # + # The app bar centres its search field on the window; the floating hints, + # the selection bar and the dialogs must line up with it. They have drifted + # apart twice: once when the map's overlay layer insetted by each dock + # separately (109px out), and once from a leftover JS hack that measured the + # search box and wrote an inline `left` in VIEWPORT coordinates onto an + # element whose containing block was the overlay layer (a quarter of the + # window out). Both were invisible to every other check. + page.evaluate("""() => { + const show = (i, how) => { const e = document.getElementById(i); if (e) e.style.display = how; }; + show('activeFilterBanner', 'flex'); show('selectionPanel', 'flex'); + show('editorToolbar', 'flex'); show('editorHint', 'block'); show('networkHint', 'block'); + }""") + page.wait_for_timeout(400) + CENTRES = """() => { + const c = el => { if (!el) return null; const r = el.getBoundingClientRect(); + return r.width ? Math.round(r.left + r.width / 2) : null; }; + const id = s => document.getElementById(s); + return { window: Math.round(window.innerWidth / 2), + searchPill: c(id('searchBox')), + filterBanner: c(id('activeFilterBanner')), + selectionBar: c(id('selectionPanel')), + editorToolbar: c(id('editorToolbar')), + editorHint: c(id('editorHint')), + networkHint: c(id('networkHint')) }; + }""" + + def centred(label): + c = page.evaluate(CENTRES) + mid = c.pop("window") + off = ["%s %+d" % (k, v - mid) for k, v in c.items() + if v is not None and abs(v - mid) > 1] + check("centred on the window: " + label, not off, ", ".join(off)) + + centred("dock open") + page.evaluate("NetworkTool.open()") + page.wait_for_timeout(700) + centred("tool dock open") + page.evaluate("NetworkTool.close()") + page.evaluate("document.getElementById('menuButton').click()") + page.wait_for_timeout(700) + centred("dock hidden") + page.evaluate("document.getElementById('menuButton').click()") + page.wait_for_timeout(500) + # The altitude rail's 4px track has its own centring inside the 64px rail. + track = page.evaluate("""() => { + const rail = document.getElementById('altitudePanel').getBoundingClientRect(); + const bg = document.querySelector('.altitudeTrackBg').getBoundingClientRect(); + return [Math.round(bg.width), Math.round((bg.left + bg.width/2) - (rail.left + rail.width/2))]; + }""") + check("altitude track is 4px and centred in the rail", + track[0] == 4 and abs(track[1]) <= 1, track) + + # These were forced visible; leave them hidden or they sit over the map and + # swallow the clicks the checks below need. + page.evaluate("""() => { + ['activeFilterBanner','selectionPanel','editorToolbar','editorHint','networkHint'] + .forEach(i => { const e = document.getElementById(i); if (e) e.style.display = 'none'; }); + }""") + page.wait_for_timeout(300) + # ---- Search -------------------------------------------------------------- page.click("#mainSearchInput") page.fill("#mainSearchInput", "constructor")