react-threat-map 0.2.3 → 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/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
@@ -430,17 +430,24 @@ function usePixelRatio() {
430
430
  const [ratio, setRatio] = react.useState(currentRatio);
431
431
  react.useEffect(() => {
432
432
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
433
- let query;
433
+ let unlisten;
434
434
  const update = () => {
435
+ unlisten?.();
435
436
  setRatio(currentRatio());
436
437
  listen();
437
438
  };
438
439
  const listen = () => {
439
- query = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
440
- query.addEventListener("change", update, { once: true });
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);
441
448
  };
442
449
  listen();
443
- return () => query?.removeEventListener("change", update);
450
+ return () => unlisten?.();
444
451
  }, []);
445
452
  return ratio;
446
453
  }
@@ -452,11 +459,23 @@ function useReducedMotion() {
452
459
  const query = window.matchMedia(QUERY);
453
460
  setReduced(query.matches);
454
461
  const update = (event) => setReduced(event.matches);
455
- query.addEventListener("change", update);
456
- return () => query.removeEventListener("change", update);
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);
457
468
  }, []);
458
469
  return reduced;
459
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
+ }
460
479
  function configEqual(a, b) {
461
480
  const aKeys = Object.keys(a);
462
481
  if (aKeys.length !== Object.keys(b).length) return false;
@@ -579,7 +598,7 @@ function BaseMapCanvas(props) {
579
598
  ref: canvasRef,
580
599
  width: Math.max(1, Math.round(width * pixelRatio)),
581
600
  height: Math.max(1, Math.round(height * pixelRatio)),
582
- style: { position: "absolute", inset: 0, width: "100%", height: "100%" },
601
+ style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%" },
583
602
  "aria-hidden": "true"
584
603
  }
585
604
  );
@@ -1224,7 +1243,9 @@ function ThreatCanvas(props) {
1224
1243
  height: Math.max(1, Math.round(height * pixelRatio)),
1225
1244
  style: {
1226
1245
  position: "absolute",
1227
- inset: 0,
1246
+ // Longhand: the `inset` shorthand is Safari 14.1+.
1247
+ top: 0,
1248
+ left: 0,
1228
1249
  width: "100%",
1229
1250
  height: "100%",
1230
1251
  // Without handlers the canvas must not eat pointer events aimed at
@@ -1274,6 +1295,7 @@ function ThreatMap(props) {
1274
1295
  );
1275
1296
  const measured = useElementSize(containerRef, widthProp === void 0 || heightProp === void 0);
1276
1297
  const pixelRatio = usePixelRatio();
1298
+ const supportsAspectRatio = useSupportsAspectRatio();
1277
1299
  const width = widthProp ?? measured?.width ?? 0;
1278
1300
  const height = heightProp ?? (measured && measured.height > 0 ? measured.height : width > 0 ? defaultHeightFor(projectionProp, width) : 0);
1279
1301
  const projection = react.useMemo(
@@ -1331,7 +1353,14 @@ function ThreatMap(props) {
1331
1353
  // a flex parent, or the consumer's own `style` below — so it sets a
1332
1354
  // floor without taking the decision away from them. The value is a string
1333
1355
  // for React 16.14/17's sake; see aspectRatioStyleFor.
1334
- ...heightProp !== void 0 ? { height: heightProp } : { aspectRatio: aspectRatioStyleFor(projectionProp) },
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) } : {},
1335
1364
  ...style
1336
1365
  },
1337
1366
  role: "img",
package/dist/index.js CHANGED
@@ -429,17 +429,24 @@ function usePixelRatio() {
429
429
  const [ratio, setRatio] = useState(currentRatio);
430
430
  useEffect(() => {
431
431
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
432
- let query;
432
+ let unlisten;
433
433
  const update = () => {
434
+ unlisten?.();
434
435
  setRatio(currentRatio());
435
436
  listen();
436
437
  };
437
438
  const listen = () => {
438
- query = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
439
- query.addEventListener("change", update, { once: true });
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);
440
447
  };
441
448
  listen();
442
- return () => query?.removeEventListener("change", update);
449
+ return () => unlisten?.();
443
450
  }, []);
444
451
  return ratio;
445
452
  }
@@ -451,11 +458,23 @@ function useReducedMotion() {
451
458
  const query = window.matchMedia(QUERY);
452
459
  setReduced(query.matches);
453
460
  const update = (event) => setReduced(event.matches);
454
- query.addEventListener("change", update);
455
- return () => query.removeEventListener("change", update);
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);
456
467
  }, []);
457
468
  return reduced;
458
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
+ }
459
478
  function configEqual(a, b) {
460
479
  const aKeys = Object.keys(a);
461
480
  if (aKeys.length !== Object.keys(b).length) return false;
@@ -578,7 +597,7 @@ function BaseMapCanvas(props) {
578
597
  ref: canvasRef,
579
598
  width: Math.max(1, Math.round(width * pixelRatio)),
580
599
  height: Math.max(1, Math.round(height * pixelRatio)),
581
- style: { position: "absolute", inset: 0, width: "100%", height: "100%" },
600
+ style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%" },
582
601
  "aria-hidden": "true"
583
602
  }
584
603
  );
@@ -1223,7 +1242,9 @@ function ThreatCanvas(props) {
1223
1242
  height: Math.max(1, Math.round(height * pixelRatio)),
1224
1243
  style: {
1225
1244
  position: "absolute",
1226
- inset: 0,
1245
+ // Longhand: the `inset` shorthand is Safari 14.1+.
1246
+ top: 0,
1247
+ left: 0,
1227
1248
  width: "100%",
1228
1249
  height: "100%",
1229
1250
  // Without handlers the canvas must not eat pointer events aimed at
@@ -1273,6 +1294,7 @@ function ThreatMap(props) {
1273
1294
  );
1274
1295
  const measured = useElementSize(containerRef, widthProp === void 0 || heightProp === void 0);
1275
1296
  const pixelRatio = usePixelRatio();
1297
+ const supportsAspectRatio = useSupportsAspectRatio();
1276
1298
  const width = widthProp ?? measured?.width ?? 0;
1277
1299
  const height = heightProp ?? (measured && measured.height > 0 ? measured.height : width > 0 ? defaultHeightFor(projectionProp, width) : 0);
1278
1300
  const projection = useMemo(
@@ -1330,7 +1352,14 @@ function ThreatMap(props) {
1330
1352
  // a flex parent, or the consumer's own `style` below — so it sets a
1331
1353
  // floor without taking the decision away from them. The value is a string
1332
1354
  // for React 16.14/17's sake; see aspectRatioStyleFor.
1333
- ...heightProp !== void 0 ? { height: heightProp } : { aspectRatio: aspectRatioStyleFor(projectionProp) },
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) } : {},
1334
1363
  ...style
1335
1364
  },
1336
1365
  role: "img",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-threat-map",
3
- "version": "0.2.3",
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",