react-threat-map 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DECISIONS.md +22 -0
- package/README.md +36 -0
- package/dist/index.cjs +43 -10
- package/dist/index.js +43 -10
- package/package.json +1 -1
package/DECISIONS.md
CHANGED
|
@@ -394,6 +394,28 @@ their own render there. Every one of those call sites issues a single `setState`
|
|
|
394
394
|
practical cost is nil — but a future hook that sets two pieces of state in one async
|
|
395
395
|
callback would render twice on the floor and once on 18+.
|
|
396
396
|
|
|
397
|
+
### Known behavioural differences on the floor
|
|
398
|
+
|
|
399
|
+
Each of these is invisible to a suite running on 18, so each carries a guard that fails on
|
|
400
|
+
any version. Append to this list rather than fixing quietly — the entries are the only
|
|
401
|
+
record of what the static allowlist cannot reach.
|
|
402
|
+
|
|
403
|
+
**1. Numeric style values.** React appends `px` to numeric `style` values unless the property
|
|
404
|
+
is on its unitless allowlist, and `aspectRatio` only joined that list in React 18. The
|
|
405
|
+
wrapper's fallback ratio was emitted as a number, so on 16.14/17 it serialized to
|
|
406
|
+
`aspect-ratio: 2px` — invalid, dropped by the browser, wrapper collapsed to zero height,
|
|
407
|
+
absolutely-positioned canvases gated off, **map entirely invisible**. It affected every
|
|
408
|
+
projection, not just custom ones, and the whole no-explicit-`height` default path.
|
|
409
|
+
|
|
410
|
+
Found by a consumer on React 16.14, not by CI — precisely the gap this section predicted.
|
|
411
|
+
Fixed by `aspectRatioStyleFor` in `src/render/projection.ts`, which stringifies deliberately;
|
|
412
|
+
guarded by `tests/render/projection.test.ts`, which asserts the emitted **type** rather than
|
|
413
|
+
the rendered value, since the rendered value is correct on 18 either way.
|
|
414
|
+
|
|
415
|
+
The general rule this implies: **any numeric value handed to `style` must be checked against
|
|
416
|
+
React 17's unitless allowlist, or stringified.** `lineHeight` and `flexGrow` are on that list
|
|
417
|
+
and safe; `scale`, `translate`, `rotate` and `inset` are not.
|
|
418
|
+
|
|
397
419
|
---
|
|
398
420
|
|
|
399
421
|
## 8. Package entry points: per-condition types, plus a `typesVersions` fallback
|
package/README.md
CHANGED
|
@@ -32,6 +32,7 @@ import { ThreatMap } from 'react-threat-map';
|
|
|
32
32
|
## Contents
|
|
33
33
|
|
|
34
34
|
- [Install](#install)
|
|
35
|
+
- [Browser support](#browser-support)
|
|
35
36
|
- [Quick start](#quick-start)
|
|
36
37
|
- [Aggregation](#aggregation)
|
|
37
38
|
- [Customization](#customization)
|
|
@@ -62,6 +63,41 @@ React 16.14, 17, 18, and 19 are all supported.
|
|
|
62
63
|
|
|
63
64
|
---
|
|
64
65
|
|
|
66
|
+
## Browser support
|
|
67
|
+
|
|
68
|
+
| Browser | Minimum |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| Chrome / Edge | 88 |
|
|
71
|
+
| Firefox | 89 |
|
|
72
|
+
| Safari (macOS) | 15 |
|
|
73
|
+
| Safari (iOS) | 15 |
|
|
74
|
+
|
|
75
|
+
The floor is set by CSS [`aspect-ratio`][ar], which is how a `<ThreatMap>` given no
|
|
76
|
+
height derives one — the canvases are absolutely positioned, so nothing else in the
|
|
77
|
+
box has a height to contribute. Below that, the component falls back to a
|
|
78
|
+
`min-height` computed from the measured width, so it still renders; the map just
|
|
79
|
+
stops responding to a container whose height is set purely in CSS.
|
|
80
|
+
|
|
81
|
+
Everything else the library touches is older than that floor: Canvas 2D with
|
|
82
|
+
`Path2D` and the `lighter` composite operation, `ResizeObserver` (feature-detected,
|
|
83
|
+
with a `getBoundingClientRect` fallback), and `matchMedia` (both the modern
|
|
84
|
+
`addEventListener` and the `addListener` form Safari needed before 14). The
|
|
85
|
+
published bundle is ES2020 and ships no polyfills.
|
|
86
|
+
|
|
87
|
+
Two things degrade quietly rather than breaking, both on Safari below 16:
|
|
88
|
+
|
|
89
|
+
- The `resolution` media feature is missing, so the canvas is not re-sharpened when
|
|
90
|
+
a window moves to a display with a different pixel ratio. The ratio picked at
|
|
91
|
+
mount stays.
|
|
92
|
+
- `prefers-reduced-motion` still works — that query has been supported since Safari
|
|
93
|
+
10.1.
|
|
94
|
+
|
|
95
|
+
There is no IE11 support and none is planned; `Path2D` alone rules it out.
|
|
96
|
+
|
|
97
|
+
[ar]: https://developer.mozilla.org/en-US/docs/Web/CSS/aspect-ratio
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
65
101
|
## Quick start
|
|
66
102
|
|
|
67
103
|
The only required prop is `attacks`. The map sizes itself to its container, loads its own
|
package/dist/index.cjs
CHANGED
|
@@ -345,6 +345,9 @@ function defaultHeightFor(spec, width) {
|
|
|
345
345
|
function aspectRatioFor(spec) {
|
|
346
346
|
return 1 / ratioFor(spec);
|
|
347
347
|
}
|
|
348
|
+
function aspectRatioStyleFor(spec) {
|
|
349
|
+
return String(aspectRatioFor(spec));
|
|
350
|
+
}
|
|
348
351
|
var SPHERE = { type: "Sphere" };
|
|
349
352
|
function createProjection(spec, width, height) {
|
|
350
353
|
const projection = typeof spec === "string" ? (factories[spec] ?? factories.naturalEarth1)() : spec;
|
|
@@ -427,17 +430,24 @@ function usePixelRatio() {
|
|
|
427
430
|
const [ratio, setRatio] = react.useState(currentRatio);
|
|
428
431
|
react.useEffect(() => {
|
|
429
432
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
|
|
430
|
-
let
|
|
433
|
+
let unlisten;
|
|
431
434
|
const update = () => {
|
|
435
|
+
unlisten?.();
|
|
432
436
|
setRatio(currentRatio());
|
|
433
437
|
listen();
|
|
434
438
|
};
|
|
435
439
|
const listen = () => {
|
|
436
|
-
query = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
437
|
-
query.addEventListener
|
|
440
|
+
const query = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
441
|
+
if (typeof query.addEventListener === "function") {
|
|
442
|
+
query.addEventListener("change", update);
|
|
443
|
+
unlisten = () => query.removeEventListener("change", update);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
query.addListener(update);
|
|
447
|
+
unlisten = () => query.removeListener(update);
|
|
438
448
|
};
|
|
439
449
|
listen();
|
|
440
|
-
return () =>
|
|
450
|
+
return () => unlisten?.();
|
|
441
451
|
}, []);
|
|
442
452
|
return ratio;
|
|
443
453
|
}
|
|
@@ -449,11 +459,23 @@ function useReducedMotion() {
|
|
|
449
459
|
const query = window.matchMedia(QUERY);
|
|
450
460
|
setReduced(query.matches);
|
|
451
461
|
const update = (event) => setReduced(event.matches);
|
|
452
|
-
query.addEventListener
|
|
453
|
-
|
|
462
|
+
if (typeof query.addEventListener === "function") {
|
|
463
|
+
query.addEventListener("change", update);
|
|
464
|
+
return () => query.removeEventListener("change", update);
|
|
465
|
+
}
|
|
466
|
+
query.addListener(update);
|
|
467
|
+
return () => query.removeListener(update);
|
|
454
468
|
}, []);
|
|
455
469
|
return reduced;
|
|
456
470
|
}
|
|
471
|
+
function useSupportsAspectRatio() {
|
|
472
|
+
const [supported, setSupported] = react.useState(true);
|
|
473
|
+
react.useEffect(() => {
|
|
474
|
+
if (typeof CSS === "undefined" || typeof CSS.supports !== "function") return;
|
|
475
|
+
setSupported(CSS.supports("aspect-ratio", "1"));
|
|
476
|
+
}, []);
|
|
477
|
+
return supported;
|
|
478
|
+
}
|
|
457
479
|
function configEqual(a, b) {
|
|
458
480
|
const aKeys = Object.keys(a);
|
|
459
481
|
if (aKeys.length !== Object.keys(b).length) return false;
|
|
@@ -576,7 +598,7 @@ function BaseMapCanvas(props) {
|
|
|
576
598
|
ref: canvasRef,
|
|
577
599
|
width: Math.max(1, Math.round(width * pixelRatio)),
|
|
578
600
|
height: Math.max(1, Math.round(height * pixelRatio)),
|
|
579
|
-
style: { position: "absolute",
|
|
601
|
+
style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%" },
|
|
580
602
|
"aria-hidden": "true"
|
|
581
603
|
}
|
|
582
604
|
);
|
|
@@ -1221,7 +1243,9 @@ function ThreatCanvas(props) {
|
|
|
1221
1243
|
height: Math.max(1, Math.round(height * pixelRatio)),
|
|
1222
1244
|
style: {
|
|
1223
1245
|
position: "absolute",
|
|
1224
|
-
|
|
1246
|
+
// Longhand: the `inset` shorthand is Safari 14.1+.
|
|
1247
|
+
top: 0,
|
|
1248
|
+
left: 0,
|
|
1225
1249
|
width: "100%",
|
|
1226
1250
|
height: "100%",
|
|
1227
1251
|
// Without handlers the canvas must not eat pointer events aimed at
|
|
@@ -1271,6 +1295,7 @@ function ThreatMap(props) {
|
|
|
1271
1295
|
);
|
|
1272
1296
|
const measured = useElementSize(containerRef, widthProp === void 0 || heightProp === void 0);
|
|
1273
1297
|
const pixelRatio = usePixelRatio();
|
|
1298
|
+
const supportsAspectRatio = useSupportsAspectRatio();
|
|
1274
1299
|
const width = widthProp ?? measured?.width ?? 0;
|
|
1275
1300
|
const height = heightProp ?? (measured && measured.height > 0 ? measured.height : width > 0 ? defaultHeightFor(projectionProp, width) : 0);
|
|
1276
1301
|
const projection = react.useMemo(
|
|
@@ -1326,8 +1351,16 @@ function ThreatMap(props) {
|
|
|
1326
1351
|
// which gates off the canvases, which keeps it collapsed. `aspect-ratio`
|
|
1327
1352
|
// is inert the moment anything else determines the height — a CSS class,
|
|
1328
1353
|
// a flex parent, or the consumer's own `style` below — so it sets a
|
|
1329
|
-
// floor without taking the decision away from them.
|
|
1330
|
-
|
|
1354
|
+
// floor without taking the decision away from them. The value is a string
|
|
1355
|
+
// for React 16.14/17's sake; see aspectRatioStyleFor.
|
|
1356
|
+
//
|
|
1357
|
+
// Where `aspect-ratio` is not implemented at all (Safari/iOS <15, Chrome
|
|
1358
|
+
// <88, Firefox <89) the declaration is dropped and the wrapper collapses,
|
|
1359
|
+
// so fall back to a `min-height` derived from the width. `min-height`
|
|
1360
|
+
// rather than `height` keeps the "floor, not a decision" property, and
|
|
1361
|
+
// deriving it from the *width* rather than the measured height is what
|
|
1362
|
+
// stops it feeding back into its own measurement.
|
|
1363
|
+
...heightProp !== void 0 ? { height: heightProp } : supportsAspectRatio ? { aspectRatio: aspectRatioStyleFor(projectionProp) } : width > 0 ? { minHeight: defaultHeightFor(projectionProp, width) } : {},
|
|
1331
1364
|
...style
|
|
1332
1365
|
},
|
|
1333
1366
|
role: "img",
|
package/dist/index.js
CHANGED
|
@@ -344,6 +344,9 @@ function defaultHeightFor(spec, width) {
|
|
|
344
344
|
function aspectRatioFor(spec) {
|
|
345
345
|
return 1 / ratioFor(spec);
|
|
346
346
|
}
|
|
347
|
+
function aspectRatioStyleFor(spec) {
|
|
348
|
+
return String(aspectRatioFor(spec));
|
|
349
|
+
}
|
|
347
350
|
var SPHERE = { type: "Sphere" };
|
|
348
351
|
function createProjection(spec, width, height) {
|
|
349
352
|
const projection = typeof spec === "string" ? (factories[spec] ?? factories.naturalEarth1)() : spec;
|
|
@@ -426,17 +429,24 @@ function usePixelRatio() {
|
|
|
426
429
|
const [ratio, setRatio] = useState(currentRatio);
|
|
427
430
|
useEffect(() => {
|
|
428
431
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
|
|
429
|
-
let
|
|
432
|
+
let unlisten;
|
|
430
433
|
const update = () => {
|
|
434
|
+
unlisten?.();
|
|
431
435
|
setRatio(currentRatio());
|
|
432
436
|
listen();
|
|
433
437
|
};
|
|
434
438
|
const listen = () => {
|
|
435
|
-
query = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
436
|
-
query.addEventListener
|
|
439
|
+
const query = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
440
|
+
if (typeof query.addEventListener === "function") {
|
|
441
|
+
query.addEventListener("change", update);
|
|
442
|
+
unlisten = () => query.removeEventListener("change", update);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
query.addListener(update);
|
|
446
|
+
unlisten = () => query.removeListener(update);
|
|
437
447
|
};
|
|
438
448
|
listen();
|
|
439
|
-
return () =>
|
|
449
|
+
return () => unlisten?.();
|
|
440
450
|
}, []);
|
|
441
451
|
return ratio;
|
|
442
452
|
}
|
|
@@ -448,11 +458,23 @@ function useReducedMotion() {
|
|
|
448
458
|
const query = window.matchMedia(QUERY);
|
|
449
459
|
setReduced(query.matches);
|
|
450
460
|
const update = (event) => setReduced(event.matches);
|
|
451
|
-
query.addEventListener
|
|
452
|
-
|
|
461
|
+
if (typeof query.addEventListener === "function") {
|
|
462
|
+
query.addEventListener("change", update);
|
|
463
|
+
return () => query.removeEventListener("change", update);
|
|
464
|
+
}
|
|
465
|
+
query.addListener(update);
|
|
466
|
+
return () => query.removeListener(update);
|
|
453
467
|
}, []);
|
|
454
468
|
return reduced;
|
|
455
469
|
}
|
|
470
|
+
function useSupportsAspectRatio() {
|
|
471
|
+
const [supported, setSupported] = useState(true);
|
|
472
|
+
useEffect(() => {
|
|
473
|
+
if (typeof CSS === "undefined" || typeof CSS.supports !== "function") return;
|
|
474
|
+
setSupported(CSS.supports("aspect-ratio", "1"));
|
|
475
|
+
}, []);
|
|
476
|
+
return supported;
|
|
477
|
+
}
|
|
456
478
|
function configEqual(a, b) {
|
|
457
479
|
const aKeys = Object.keys(a);
|
|
458
480
|
if (aKeys.length !== Object.keys(b).length) return false;
|
|
@@ -575,7 +597,7 @@ function BaseMapCanvas(props) {
|
|
|
575
597
|
ref: canvasRef,
|
|
576
598
|
width: Math.max(1, Math.round(width * pixelRatio)),
|
|
577
599
|
height: Math.max(1, Math.round(height * pixelRatio)),
|
|
578
|
-
style: { position: "absolute",
|
|
600
|
+
style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%" },
|
|
579
601
|
"aria-hidden": "true"
|
|
580
602
|
}
|
|
581
603
|
);
|
|
@@ -1220,7 +1242,9 @@ function ThreatCanvas(props) {
|
|
|
1220
1242
|
height: Math.max(1, Math.round(height * pixelRatio)),
|
|
1221
1243
|
style: {
|
|
1222
1244
|
position: "absolute",
|
|
1223
|
-
|
|
1245
|
+
// Longhand: the `inset` shorthand is Safari 14.1+.
|
|
1246
|
+
top: 0,
|
|
1247
|
+
left: 0,
|
|
1224
1248
|
width: "100%",
|
|
1225
1249
|
height: "100%",
|
|
1226
1250
|
// Without handlers the canvas must not eat pointer events aimed at
|
|
@@ -1270,6 +1294,7 @@ function ThreatMap(props) {
|
|
|
1270
1294
|
);
|
|
1271
1295
|
const measured = useElementSize(containerRef, widthProp === void 0 || heightProp === void 0);
|
|
1272
1296
|
const pixelRatio = usePixelRatio();
|
|
1297
|
+
const supportsAspectRatio = useSupportsAspectRatio();
|
|
1273
1298
|
const width = widthProp ?? measured?.width ?? 0;
|
|
1274
1299
|
const height = heightProp ?? (measured && measured.height > 0 ? measured.height : width > 0 ? defaultHeightFor(projectionProp, width) : 0);
|
|
1275
1300
|
const projection = useMemo(
|
|
@@ -1325,8 +1350,16 @@ function ThreatMap(props) {
|
|
|
1325
1350
|
// which gates off the canvases, which keeps it collapsed. `aspect-ratio`
|
|
1326
1351
|
// is inert the moment anything else determines the height — a CSS class,
|
|
1327
1352
|
// a flex parent, or the consumer's own `style` below — so it sets a
|
|
1328
|
-
// floor without taking the decision away from them.
|
|
1329
|
-
|
|
1353
|
+
// floor without taking the decision away from them. The value is a string
|
|
1354
|
+
// for React 16.14/17's sake; see aspectRatioStyleFor.
|
|
1355
|
+
//
|
|
1356
|
+
// Where `aspect-ratio` is not implemented at all (Safari/iOS <15, Chrome
|
|
1357
|
+
// <88, Firefox <89) the declaration is dropped and the wrapper collapses,
|
|
1358
|
+
// so fall back to a `min-height` derived from the width. `min-height`
|
|
1359
|
+
// rather than `height` keeps the "floor, not a decision" property, and
|
|
1360
|
+
// deriving it from the *width* rather than the measured height is what
|
|
1361
|
+
// stops it feeding back into its own measurement.
|
|
1362
|
+
...heightProp !== void 0 ? { height: heightProp } : supportsAspectRatio ? { aspectRatio: aspectRatioStyleFor(projectionProp) } : width > 0 ? { minHeight: defaultHeightFor(projectionProp, width) } : {},
|
|
1330
1363
|
...style
|
|
1331
1364
|
},
|
|
1332
1365
|
role: "img",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-threat-map",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "A React component that renders animated cyberattack threats on a static world map, with intelligent per-region aggregation and first-class US state support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|