Fix world map country selection handling and preserve map during re-fetch

This commit is contained in:
2026-04-05 22:44:50 +02:00
parent 15d53a8e96
commit 6e2abe9d97
2 changed files with 35 additions and 15 deletions

View File

@@ -208,10 +208,16 @@ export function WorldMap({
[onSelectCountry, selectedCountry], [onSelectCountry, selectedCountry],
); );
/** SVG-level click handler — paths never receive click when pointer capture
* is active on the SVG, so we resolve the target via the data-cc attribute. */
const handleSvgClick = useCallback((event: React.MouseEvent<SVGSVGElement>) => {
const target = (event.target as Element).closest('[data-cc]');
const cc = target?.getAttribute('data-cc') ?? null;
if (cc) handleCountrySelect(cc);
}, [handleCountrySelect]);
const handlePointerDown = useCallback((event: React.PointerEvent<SVGSVGElement>) => { const handlePointerDown = useCallback((event: React.PointerEvent<SVGSVGElement>) => {
if (event.button !== 0) return; if (event.button !== 0) return;
event.currentTarget.setPointerCapture(event.pointerId);
dragStateRef.current = { dragStateRef.current = {
active: true, active: true,
startX: event.clientX, startX: event.clientX,
@@ -231,6 +237,7 @@ export function WorldMap({
if (!drag.moved && Math.hypot(dx, dy) > PAN_THRESHOLD) { if (!drag.moved && Math.hypot(dx, dy) > PAN_THRESHOLD) {
drag.moved = true; drag.moved = true;
clickSuppressedRef.current = true; clickSuppressedRef.current = true;
event.currentTarget.setPointerCapture(event.pointerId);
} }
setCenter([drag.startCenter[0] + dx, drag.startCenter[1] + dy]); setCenter([drag.startCenter[0] + dx, drag.startCenter[1] + dy]);
@@ -332,6 +339,7 @@ export function WorldMap({
onPointerUp={handlePointerUp} onPointerUp={handlePointerUp}
onPointerLeave={handlePointerUp} onPointerLeave={handlePointerUp}
onWheel={handleWheel} onWheel={handleWheel}
onClick={handleSvgClick}
> >
<g transform={`translate(${center[0]} ${center[1]}) scale(${zoom})`}> <g transform={`translate(${center[0]} ${center[1]}) scale(${zoom})`}>
{countryFeatures.map((featureItem) => { {countryFeatures.map((featureItem) => {
@@ -350,6 +358,7 @@ export function WorldMap({
<g key={String(rawId)}> <g key={String(rawId)}>
<path <path
d={pathString} d={pathString}
data-cc={cc ?? undefined}
role={cc ? "button" : undefined} role={cc ? "button" : undefined}
tabIndex={cc ? 0 : undefined} tabIndex={cc ? 0 : undefined}
aria-label={ aria-label={
@@ -373,9 +382,7 @@ export function WorldMap({
} as React.CSSProperties } as React.CSSProperties
} }
onClick={(): void => { onClick={(): void => {
if (cc) { if (cc) handleCountrySelect(cc);
handleCountrySelect(cc);
}
}} }}
onKeyDown={(event): void => { onKeyDown={(event): void => {
if (cc && (event.key === "Enter" || event.key === " ")) { if (cc && (event.key === "Enter" || event.key === " ")) {

View File

@@ -113,6 +113,13 @@ export function MapPage(): React.JSX.Element {
const { countries, countryNames, bans, total, loading, error, refresh } = const { countries, countryNames, bans, total, loading, error, refresh } =
useMapData(range, originFilter, source, selectedCountry ?? undefined); useMapData(range, originFilter, source, selectedCountry ?? undefined);
// True after the first successful data load — keeps the map mounted
// during subsequent re-fetches so country selection gives instant feedback.
const [hasLoadedOnce, setHasLoadedOnce] = useState(false);
useEffect(() => {
if (!loading && !error) setHasLoadedOnce(true);
}, [loading, error]);
const { const {
thresholds: mapThresholds, thresholds: mapThresholds,
error: mapThresholdError, error: mapThresholdError,
@@ -195,7 +202,8 @@ export function MapPage(): React.JSX.Element {
</MessageBar> </MessageBar>
)} )}
{loading && !error && ( {/* Initial load spinner — only shown before the first data arrives. */}
{loading && !error && !hasLoadedOnce && (
<div style={{ display: "flex", justifyContent: "center", padding: tokens.spacingVerticalXL }}> <div style={{ display: "flex", justifyContent: "center", padding: tokens.spacingVerticalXL }}>
<Spinner label="Loading map data…" /> <Spinner label="Loading map data…" />
</div> </div>
@@ -203,8 +211,10 @@ export function MapPage(): React.JSX.Element {
{/* ---------------------------------------------------------------- */} {/* ---------------------------------------------------------------- */}
{/* World map */} {/* World map */}
{/* Keep the map mounted after first load so clicking a country gives */}
{/* immediate visual feedback before the filtered data arrives. */}
{/* ---------------------------------------------------------------- */} {/* ---------------------------------------------------------------- */}
{!loading && !error && ( {!error && hasLoadedOnce && (
<WorldMap <WorldMap
countries={countries} countries={countries}
countryNames={countryNames} countryNames={countryNames}
@@ -242,19 +252,22 @@ export function MapPage(): React.JSX.Element {
{/* ---------------------------------------------------------------- */} {/* ---------------------------------------------------------------- */}
{/* Summary line */} {/* Summary line */}
{/* ---------------------------------------------------------------- */} {/* ---------------------------------------------------------------- */}
{!loading && !error && ( {!error && hasLoadedOnce && (
<Text size={300} style={{ color: tokens.colorNeutralForeground3 }}> <div style={{ display: "flex", alignItems: "center", gap: tokens.spacingHorizontalS }}>
{String(total)} total ban{total !== 1 ? "s" : ""} in the selected period <Text size={300} style={{ color: tokens.colorNeutralForeground3 }}>
{" · "} {String(total)} total ban{total !== 1 ? "s" : ""} in the selected period
{String(Object.keys(countries).length)} countr{Object.keys(countries).length !== 1 ? "ies" : "y"} affected {" · "}
</Text> {String(Object.keys(countries).length)} countr{Object.keys(countries).length !== 1 ? "ies" : "y"} affected
</Text>
{loading && <Spinner size="tiny" />}
</div>
)} )}
{/* ---------------------------------------------------------------- */} {/* ---------------------------------------------------------------- */}
{/* Companion bans table */} {/* Companion bans table */}
{/* ---------------------------------------------------------------- */} {/* ---------------------------------------------------------------- */}
{!loading && !error && ( {!error && hasLoadedOnce && (
<div className={styles.tableWrapper}> <div className={styles.tableWrapper} style={{ opacity: loading ? 0.5 : 1, transition: "opacity 150ms" }}>
<Table size="small" aria-label="Bans list"> <Table size="small" aria-label="Bans list">
<TableHeader> <TableHeader>
<TableRow> <TableRow>