From 6c3d3ec16d9bd660acd42bb9c802148d80a13261 Mon Sep 17 00:00:00 2001 From: SimpleTest Date: Fri, 24 Jul 2026 12:54:41 +0300 Subject: [PATCH] fix(ui): make map fallback interactive --- assets/css/app.css | 37 +++++ assets/js/hooks.js | 28 +++- e2e/tests/public.spec.ts | 14 +- .../controllers/page_html/home.html.heex | 7 +- priv/gettext/default.pot | 129 ++++++++++-------- priv/gettext/en/LC_MESSAGES/default.po | 129 ++++++++++-------- priv/gettext/ru/LC_MESSAGES/default.po | 129 ++++++++++-------- priv/gettext/uk/LC_MESSAGES/default.po | 129 ++++++++++-------- 8 files changed, 367 insertions(+), 235 deletions(-) diff --git a/assets/css/app.css b/assets/css/app.css index 3f65c65..f5d96cd 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -444,6 +444,7 @@ html { } .home-demo-map-fallback-point { + appearance: none; display: flex; align-items: center; gap: 0.75rem; @@ -452,14 +453,41 @@ html { border: 1px solid color-mix(in oklab, var(--color-base-content) 12%, transparent); border-radius: 1rem; background: color-mix(in oklab, var(--color-base-100) 96%, transparent); + color: var(--color-base-content); + cursor: pointer; + font: inherit; + text-align: left; box-shadow: 0 0.75rem 2rem rgb(23 37 31 / 0.11); backdrop-filter: blur(0.5rem); + transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease; } .home-demo-map-fallback-point:nth-child(2) { justify-self: end; } +.home-demo-map-fallback-point:hover { + border-color: color-mix(in oklab, var(--color-success) 55%, var(--color-base-300)); + transform: translateY(-1px); +} + +.home-demo-map-fallback-point:focus-visible { + outline: 3px solid color-mix(in oklab, var(--color-success) 48%, transparent); + outline-offset: 3px; +} + +.home-demo-map-fallback-point.is-selected { + border-color: var(--color-success); + box-shadow: + 0 0 0 2px color-mix(in oklab, var(--color-success) 18%, transparent), + 0 0.75rem 2rem rgb(23 37 31 / 0.13); +} + +.home-demo-map-fallback-point.is-selected .home-demo-map-fallback-marker { + background: var(--color-primary); + color: var(--color-primary-content); +} + .home-demo-map-fallback-marker { display: grid; width: 2.25rem; @@ -497,6 +525,15 @@ html { font-size: 0.6875rem; } +.home-demo-map-fallback-selection { + position: relative; + z-index: 1; + margin-top: 0.75rem; + color: color-mix(in oklab, var(--color-base-content) 72%, transparent); + font-size: 0.75rem; + font-weight: 700; +} + @keyframes home-map-spin { to { transform: translate(-50%, -50%) rotate(360deg); diff --git a/assets/js/hooks.js b/assets/js/hooks.js index cfa51ac..c1059cf 100644 --- a/assets/js/hooks.js +++ b/assets/js/hooks.js @@ -205,7 +205,10 @@ const createAidMap = element => { state.markReady() if (state.statusBadge) { - state.statusBadge.textContent = state.statusBadge.dataset.staticLabel + state.statusBadge.textContent = + state.element.dataset.demoMap === "true" + ? state.statusBadge.dataset.fallbackLabel + : state.statusBadge.dataset.staticLabel } if (state.element.dataset.demoMap !== "true") { @@ -232,9 +235,18 @@ const createAidMap = element => { const points = document.createElement("div") points.className = "home-demo-map-fallback-points" + const selection = document.createElement("p") + selection.className = "home-demo-map-fallback-selection" + selection.dataset.mapFallbackSelection = "true" + selection.setAttribute("aria-live", "polite") + selection.textContent = state.element.dataset.mapFallbackInteractionLabel + markerPoints(state.element).forEach((point, index) => { - const item = document.createElement("div") + const item = document.createElement("button") + item.type = "button" item.className = "home-demo-map-fallback-point" + item.dataset.mapFallbackPoint = "true" + item.setAttribute("aria-pressed", "false") const marker = document.createElement("span") marker.className = "home-demo-map-fallback-marker" @@ -252,10 +264,20 @@ const createAidMap = element => { copy.append(title, location) item.append(marker, copy) + item.addEventListener("click", () => { + points.querySelectorAll("[data-map-fallback-point]").forEach(candidate => { + const selected = candidate === item + candidate.classList.toggle("is-selected", selected) + candidate.setAttribute("aria-pressed", String(selected)) + }) + + selection.textContent = + `${state.element.dataset.mapFallbackSelectedLabel} ${point.title || ""}`.trim() + }) points.append(item) }) - fallback.append(points, mapRetryButton(state.element, state.retry)) + fallback.append(points, selection, mapRetryButton(state.element, state.retry)) state.element.replaceChildren(fallback) } diff --git a/e2e/tests/public.spec.ts b/e2e/tests/public.spec.ts index 9f9d1d1..1442f26 100644 --- a/e2e/tests/public.spec.ts +++ b/e2e/tests/public.spec.ts @@ -23,7 +23,7 @@ test("public safety pages load and private discovery requires authentication", a assertNoBrowserFailures(); }); -test("public demo labels the non-WebGL fallback honestly and offers retry", async ({ +test("public demo provides an interactive non-WebGL fallback and offers retry", async ({ page, }) => { const assertNoBrowserFailures = captureBrowserFailures(page); @@ -55,14 +55,22 @@ test("public demo labels the non-WebGL fallback honestly and offers retry", asyn await map.scrollIntoViewIfNeeded(); await expect(map.locator("[data-map-fallback=true]")).toBeVisible(); - await expect(status).toHaveText("Static preview"); + await expect(status).toHaveText("Interactive fallback"); await expect(retry).toBeVisible(); await expect(map).toHaveAttribute("data-map-unavailable-reason", "webgl-context-unavailable"); await expect.poll(() => mapWarnings.length).toBe(1); + const firstExample = map.locator("[data-map-fallback-point]").first(); + await expect(firstExample).toHaveAttribute("aria-pressed", "false"); + await firstExample.click(); + await expect(firstExample).toHaveAttribute("aria-pressed", "true"); + await expect(map.locator("[data-map-fallback-selection]")).toContainText( + "Selected example: Medicine pickup", + ); + await retry.click(); await expect.poll(() => mapWarnings.length).toBe(2); - await expect(status).toHaveText("Static preview"); + await expect(status).toHaveText("Interactive fallback"); await expect(retry).toBeVisible(); assertNoBrowserFailures(); diff --git a/lib/who_need_help_web/controllers/page_html/home.html.heex b/lib/who_need_help_web/controllers/page_html/home.html.heex index 98aedbc..ffdf465 100644 --- a/lib/who_need_help_web/controllers/page_html/home.html.heex +++ b/lib/who_need_help_web/controllers/page_html/home.html.heex @@ -148,9 +148,13 @@ data-markers={demo_markers} data-map-unavailable-label={ gettext( - "The interactive demo could not start in this browser. You can retry or use the example steps beside it." + "WebGL is unavailable in this browser, so the full map cannot start. Select an example below or retry the map." ) } + data-map-fallback-interaction-label={ + gettext("Select an example to preview it without exposing a real address.") + } + data-map-fallback-selected-label={gettext("Selected example:")} data-map-retry-label={gettext("Try map again")} role="region" aria-label={gettext("Demo map with synthetic approximate help requests")} @@ -161,6 +165,7 @@