From 14d6f2ecc9cd54ed9589e61a3b8fb3ca82aaf75d Mon Sep 17 00:00:00 2001 From: Maurice Date: Mon, 10 Aug 2026 09:18:48 +0200 Subject: [PATCH] Keep the hover card's corners round without its tail --- client/src/components/Map/MapViewGL.test.tsx | 18 +++++++++ client/src/components/Map/mapPopupCss.test.ts | 37 +++++++++++++++++++ client/src/index.css | 17 +++++++-- 3 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 client/src/components/Map/mapPopupCss.test.ts diff --git a/client/src/components/Map/MapViewGL.test.tsx b/client/src/components/Map/MapViewGL.test.tsx index a8262dc4d..550e14a9b 100644 --- a/client/src/components/Map/MapViewGL.test.tsx +++ b/client/src/components/Map/MapViewGL.test.tsx @@ -942,6 +942,24 @@ describe('MapViewGL', () => { expect(glMarkers.created).toHaveLength(before) }) + it.each([ + ['mapbox-gl', () => mapboxgl.Popup], + ['maplibre-gl', () => maplibregl.Popup], + ] as const)('FE-COMP-MAPVIEWGL-068: the %s hover card clears its marker without a tail', async (provider, popupOf) => { + loadOnAttach() + const { unmount } = render() + await act(async () => {}) + + // The tail is hidden in index.css, and it used to contribute 10px of the + // gap itself — the offset has to make those up or the card sits on top of + // the marker it describes. + const options = (popupOf() as unknown as { mock: { calls: [Record][] } }).mock.calls[0]?.[0] + expect(options).toMatchObject({ className: 'trek-map-popup', offset: 26, closeButton: false }) + // Leaves the map listeners attached otherwise, and the next case reads them + // back out of the same mock. + unmount() + }) + it('FE-COMP-MAPVIEWGL-027: clicking a GPX track selects its place unless a marker or cluster is on top', async () => { loadOnAttach() const onMarkerClick = vi.fn() diff --git a/client/src/components/Map/mapPopupCss.test.ts b/client/src/components/Map/mapPopupCss.test.ts new file mode 100644 index 000000000..80cd7a4f6 --- /dev/null +++ b/client/src/components/Map/mapPopupCss.test.ts @@ -0,0 +1,37 @@ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { describe, it, expect } from 'vitest' + +// The GL map's hover card is styled from index.css while its geometry comes +// from the vendor stylesheet, so the two only agree by convention. These read +// the real files rather than a rendered component: jsdom applies no vendor CSS, +// which is exactly the interaction that can break here. +describe('GL hover popup css', () => { + // Vitest runs with the client package as its root, so cwd is stable here. + const css = readFileSync(resolve(process.cwd(), 'src/index.css'), 'utf8') + const block = (selector: string): string => { + const at = css.indexOf(selector) + expect(at, `${selector} missing from index.css`).toBeGreaterThan(-1) + return css.slice(at, css.indexOf('}', at) + 1) + } + + it('FE-COMP-MAPPOPUPCSS-001: the tail is off, not merely painted white', () => { + // Colouring it left a visible notch under the card in every theme. + expect(block('.trek-map-popup .maplibregl-popup-tip')).toMatch(/display:\s*none/) + }) + + it('FE-COMP-MAPPOPUPCSS-002: the corner radius outweighs the vendor anchor rules', () => { + // maplibre-gl.css squares one corner per anchor so the tail meets the card + // flush. With the tail gone that is just a squared corner, and the vendor + // rule carries the same specificity as a plain `.trek-map-popup .content` + // — whichever sheet loads last would win. The `[class]` guard settles it. + const radius = block('.trek-map-popup[class] .maplibregl-popup-content') + expect(radius).toMatch(/border-radius:\s*10px/) + }) + + it('FE-COMP-MAPPOPUPCSS-003: leaflet click popups keep their tail', () => { + // Those are anchored, dismissable popups where the tail earns its place — + // the fix above is scoped to the GL hover card on purpose. + expect(block('.leaflet-popup-tip')).not.toMatch(/display:\s*none/) + }) +}) diff --git a/client/src/index.css b/client/src/index.css index 9f6c4ce3b..23b477fc2 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -46,14 +46,23 @@ body { height: 100%; overflow: auto; overscroll-behavior: none; -webkit-overflow background: #fff; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.16); } -/* No speech-bubble tail on the hover card. It is a label that follows the - cursor onto whatever it is over, not a popup anchored to one spot, and the - tail read as a stray notch hanging off the bottom edge. The popup's own - offset carries the gap to the marker instead. */ +/* No speech-bubble tail on the hover card. The card stands off its marker by + 26px and reads as a label, not as a callout pinned to a point — the tail + hung off the bottom edge as a stray notch. The popup's offset carries the + gap on its own. */ .trek-map-popup .mapboxgl-popup-tip, .trek-map-popup .maplibregl-popup-tip { display: none; } +/* Both engines square off one corner per anchor so the tail meets the card + flush (.maplibregl-popup-anchor-bottom-left .maplibregl-popup-content and + its three siblings). Same specificity as the rule above, so which one wins + comes down to stylesheet order — and with no tail there, a squared corner is + just a squared corner. `[class]` buys the extra weight to settle it. */ +.trek-map-popup[class] .mapboxgl-popup-content, +.trek-map-popup[class] .maplibregl-popup-content { + border-radius: 10px; +} .atlas-tooltip { background: rgba(10, 10, 20, 0.6) !important;