Public: anyone can read this; the crew can change it.
The map's zoom tools draw §5's bar and btn, and .map-tools, .map-tool leave the markup #4328
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4315-the-map-s-zoom-tools-draw-5-s-bar-and-bt → main 1 commit fork
2 files
changed +36
−6
src/web/pages/map.ts +13 −6
| @@ -876,10 +876,10 @@ export function drawMap( | ||
| 876 | 876 | )} |
| 877 | 877 | ${placed.nodes.map((n) => node(base, n, g, emphasis, issueHref))} |
| 878 | 878 | </svg> |
| 879 | <div class="map-tools js-only" role="group" aria-label="Zoom"> | |
| 879 | <div class="bar js-only" role="group" aria-label="Zoom"> | |
| 880 | 880 | <button |
| 881 | 881 | type="button" |
| 882 | class="map-tool" | |
| 882 | class="btn btn-icon" data-size="sm" | |
| 883 | 883 | data-map="out" |
| 884 | 884 | aria-label="Zoom out" |
| 885 | 885 | > |
| @@ -887,13 +887,19 @@ export function drawMap( | ||
| 887 | 887 | </button> |
| 888 | 888 | <button |
| 889 | 889 | type="button" |
| 890 | class="map-tool" | |
| 890 | class="btn" data-size="sm" | |
| 891 | 891 | data-map="fit" |
| 892 | 892 | title="Fit the map to view. Drag to pan; ⌘ or Ctrl and scroll to zoom." |
| 893 | 893 | > |
| 894 | 894 | fit |
| 895 | 895 | </button> |
| 896 | <button type="button" class="map-tool" data-map="in" aria-label="Zoom in"> | |
| 896 | <button | |
| 897 | type="button" | |
| 898 | class="btn btn-icon" | |
| 899 | data-size="sm" | |
| 900 | data-map="in" | |
| 901 | aria-label="Zoom in" | |
| 902 | > | |
| 897 | 903 | + |
| 898 | 904 | </button> |
| 899 | 905 | </div> |
| @@ -1092,6 +1098,7 @@ export const mapCss = /* css */ ` | ||
| 1092 | 1098 | .map-svg { display: block; width: 100%; height: auto; } |
| 1093 | 1099 | .map-canvas.is-view .map-svg { height: 100%; } |
| 1094 | 1100 | .map-svg text { font-family: var(--font); pointer-events: none; } |
| 1101 | .map-canvas > [role="group"] { position: absolute; top: 8px; right: 8px; gap: 4px; } | |
| 1095 | 1102 | .map-tools { position: absolute; top: 8px; right: 8px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--card); box-shadow: var(--shadow-1); overflow: hidden; } |
| 1096 | 1103 | .map-tool { min-width: 26px; height: 24px; padding: 0 7px; border: 0; background: transparent; color: var(--muted-fg); font-size: 12.5px; font-weight: 550; line-height: 1; cursor: pointer; } |
| 1097 | 1104 | .map-tool + .map-tool { border-left: 1px solid var(--border); } |
| @@ -1242,7 +1249,7 @@ export const mapViewJs = /* js */ ` | ||
| 1242 | 1249 | var pointers = {}, drag = null; |
| 1243 | 1250 | document.addEventListener('pointerdown', function (e) { |
| 1244 | 1251 | var canvas = canvasOf(e); |
| 1245 | if (!canvas || !views[canvas.id] || e.target.closest('.map-tools')) return; | |
| 1252 | if (!canvas || !views[canvas.id] || e.target.closest('[role="group"]')) return; | |
| 1246 | 1253 | if (e.pointerType === 'mouse' && e.button !== 0) return; |
| 1247 | 1254 | pointers[e.pointerId] = { x: e.clientX, y: e.clientY }; |
| 1248 | 1255 | var ids = Object.keys(pointers); |
| @@ -1301,7 +1308,7 @@ export const mapViewJs = /* js */ ` | ||
| 1301 | 1308 | }, { passive: false }); |
| 1302 | 1309 | document.addEventListener('dblclick', function (e) { |
| 1303 | 1310 | var canvas = canvasOf(e); |
| 1304 | if (!canvas || !views[canvas.id] || e.target.closest('.map-tools')) return; | |
| 1311 | if (!canvas || !views[canvas.id] || e.target.closest('[role="group"]')) return; | |
| 1305 | 1312 | e.preventDefault(); |
| 1306 | 1313 | var p = at(canvas, e); |
| 1307 | 1314 | zoom(canvas, 1.6, p[0], p[1]); |
test/map.test.ts +23
| @@ -383,6 +383,29 @@ describe("the map, per app and across the forge", () => { | ||
| 383 | 383 | expect(page).toContain("not linked"); |
| 384 | 384 | }); |
| 385 | 385 | |
| 386 | test("the zoom tools are §5's bar and btn, found by data-map, not by the old names", async () => { | |
| 387 | const cookie = await signIn("alice"); | |
| 388 | const page = await ( | |
| 389 | await fetch(`${base}/alice/engine/map`, { headers: { cookie } }) | |
| 390 | ).text(); | |
| 391 | // The stylesheet still holds the old rules (#4239 removes them); the markup must not. | |
| 392 | const markup = page.replace(/<style[\s\S]*?<\/style>/g, "").replace(/<script[\s\S]*?<\/script>/g, ""); | |
| 393 | expect(markup).not.toMatch(/class="[^"]*\bmap-tools?\b/); | |
| 394 | const group = markup.match(/<div[^>]*role="group"[^>]*aria-label="Zoom"[^>]*>/)?.[0] ?? ""; | |
| 395 | expect(group).toMatch(/class="bar js-only"/); | |
| 396 | for (const what of ["out", "fit", "in"]) { | |
| 397 | expect(markup).toMatch( | |
| 398 | new RegExp(`<button[^>]*class="btn[^"]*"[^>]*data-map="${what}"`), | |
| 399 | ); | |
| 400 | } | |
| 401 | expect(markup).toContain('aria-label="Zoom out"'); | |
| 402 | expect(markup).toContain('aria-label="Zoom in"'); | |
| 403 | expect(markup).toContain("Fit the map to view."); | |
| 404 | expect(markup).toMatch(/data-map="fit"[^>]*>\s*fit\s*</); | |
| 405 | expect(markup).toMatch(/data-map="out"[^>]*>\s*−\s*</); | |
| 406 | expect(markup).toMatch(/data-map="in"[^>]*>\s*\+\s*</); | |
| 407 | }); | |
| 408 | ||
| 386 | 409 | test("a person sees a lane for each repository they can read, idle lanes folded, at one scale", async () => { |
| 387 | 410 | const cookie = await signIn("alice"); |
| 388 | 411 | const page = await ( |
new above
offline