@nok-integration/storefront-react 0.19.44 → 0.19.46

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/dist/index.d.ts CHANGED
@@ -5971,18 +5971,28 @@ export declare type ShopTone = 'dark' | 'light';
5971
5971
  /**
5972
5972
  * Full-bleed background video: muted, looping, inline, and never showing a control.
5973
5973
  *
5974
- * The four attributes below are not stylistic. `muted` and `playsInline` together are the
5975
- * only combination iOS will start without a gesture — a video that is unmuted, or that is
5974
+ * The attributes below are not stylistic. `muted` and `playsInline` together are the only
5975
+ * combination iOS will start without a gesture — a video that is unmuted, or that is
5976
5976
  * allowed to go fullscreen, simply does not play, and the panel shows its poster forever.
5977
5977
  * `loop` is the brief. `controls` is absent because this is artwork behind copy, not a
5978
5978
  * player: a tap belongs to the CTA stretched over the panel, and a play button in the
5979
5979
  * middle of a category slide would sit directly on that hit area.
5980
5980
  *
5981
- * **Playback follows visibility.** The shop entry is five full-screen panels, so a naive
5982
- * page decodes five simultaneous streams for four panels nobody is looking at. Each clip
5983
- * starts when its panel comes into view and pauses when it leaves, which keeps one video
5984
- * decoding at a time. Fetching is deferred the same way: `preload="none"` until the
5985
- * observer fires, so a fan who never scrolls past the hero pays for the hero alone.
5981
+ * **There is no `autoplay` attribute, and its absence is what makes `preload` mean
5982
+ * anything.** A `<video autoplay preload="none">` is a contradiction the browser resolves
5983
+ * in favour of autoplay: WebKit ignores the `preload` hint and begins fetching at once,
5984
+ * because it has been told the clip is going to play regardless. On the shop entry that is
5985
+ * five full-screen clips, several megabytes, all pulling against each other on a phone from
5986
+ * the moment the screen opens — which is slowest for the one panel a fan is actually
5987
+ * looking at. Playback is driven from the observer below instead, so the clips still start
5988
+ * by themselves and still loop, and `preload` is honoured.
5989
+ *
5990
+ * **Playback follows visibility; fetching runs one screen ahead of it.** Each clip plays
5991
+ * when its panel comes into view and pauses when it leaves, which keeps one video decoding
5992
+ * at a time. Downloading cannot wait that long: a clip that only starts arriving once its
5993
+ * panel is on screen shows its poster for as long as the file takes. So `preload` flips to
5994
+ * `auto` a screen out — the panel being approached is buffered before it is reached, and a
5995
+ * fan who never scrolls past the hero pays for the hero and its neighbour, not for all five.
5986
5996
  *
5987
5997
  * **Reduced motion is honoured by not playing at all.** Autoplaying background video is
5988
5998
  * exactly what the preference is about, and there is a good still to fall back to — the
package/dist/index.mjs CHANGED
@@ -5431,6 +5431,7 @@ function ShopImage({ src, alt, fill: fill2 = false, width, height, sizes, classN
5431
5431
  fetchPriority: priority ? "high" : void 0
5432
5432
  });
5433
5433
  }
5434
+ const WARM_MARGIN = "100%";
5434
5435
  function ShopVideo({ src, poster, fill: fill2 = false, className, style, priority = false }) {
5435
5436
  const assetUrl = useAssetUrl();
5436
5437
  const ref = useRef(null);
@@ -5438,6 +5439,9 @@ function ShopVideo({ src, poster, fill: fill2 = false, className, style, priorit
5438
5439
  const el = ref.current;
5439
5440
  if (!el || typeof IntersectionObserver === "undefined") return;
5440
5441
  const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)");
5442
+ const fetchNow = () => {
5443
+ if (el.preload === "none") el.preload = "auto";
5444
+ };
5441
5445
  const play = () => {
5442
5446
  if (reduced?.matches) {
5443
5447
  el.pause();
@@ -5446,10 +5450,18 @@ function ShopVideo({ src, poster, fill: fill2 = false, className, style, priorit
5446
5450
  void el.play().catch(() => {
5447
5451
  });
5448
5452
  };
5449
- const observer = new IntersectionObserver((entries) => {
5453
+ const warm = new IntersectionObserver((entries) => {
5454
+ if (!entries.some((entry) => entry.isIntersecting)) return;
5455
+ if (!reduced?.matches) fetchNow();
5456
+ warm.disconnect();
5457
+ }, {
5458
+ rootMargin: WARM_MARGIN
5459
+ });
5460
+ warm.observe(el);
5461
+ const playback = new IntersectionObserver((entries) => {
5450
5462
  for (const entry of entries) {
5451
5463
  if (entry.isIntersecting) {
5452
- if (el.preload === "none") el.preload = "auto";
5464
+ fetchNow();
5453
5465
  play();
5454
5466
  } else {
5455
5467
  el.pause();
@@ -5458,10 +5470,11 @@ function ShopVideo({ src, poster, fill: fill2 = false, className, style, priorit
5458
5470
  }, {
5459
5471
  threshold: 0.25
5460
5472
  });
5461
- observer.observe(el);
5473
+ playback.observe(el);
5462
5474
  reduced?.addEventListener("change", play);
5463
5475
  return () => {
5464
- observer.disconnect();
5476
+ warm.disconnect();
5477
+ playback.disconnect();
5465
5478
  reduced?.removeEventListener("change", play);
5466
5479
  };
5467
5480
  }, []);
@@ -5480,7 +5493,6 @@ function ShopVideo({ src, poster, fill: fill2 = false, className, style, priorit
5480
5493
  ...fillStyle,
5481
5494
  ...style
5482
5495
  },
5483
- autoPlay: true,
5484
5496
  loop: true,
5485
5497
  muted: true,
5486
5498
  playsInline: true,
@@ -11004,7 +11016,13 @@ function useDocumentSnap(ref, enabled = true) {
11004
11016
  const previousPadding = root2.style.scrollPaddingTop;
11005
11017
  root2.style.scrollSnapType = "y mandatory";
11006
11018
  root2.style.scrollPaddingTop = "0px";
11019
+ const top2 = () => window.scrollTo(0, 0);
11020
+ top2();
11021
+ const frame2 = typeof window.requestAnimationFrame === "function" ? window.requestAnimationFrame(top2) : null;
11007
11022
  return () => {
11023
+ if (frame2 !== null && typeof window.cancelAnimationFrame === "function") {
11024
+ window.cancelAnimationFrame(frame2);
11025
+ }
11008
11026
  root2.style.scrollSnapType = previousType;
11009
11027
  root2.style.scrollPaddingTop = previousPadding;
11010
11028
  };
@@ -5431,6 +5431,7 @@ function ShopImage({ src, alt, fill: fill2 = false, width, height, sizes, classN
5431
5431
  fetchPriority: priority ? "high" : void 0
5432
5432
  });
5433
5433
  }
5434
+ const WARM_MARGIN = "100%";
5434
5435
  function ShopVideo({ src, poster, fill: fill2 = false, className, style, priority = false }) {
5435
5436
  const assetUrl = useAssetUrl();
5436
5437
  const ref = useRef(null);
@@ -5438,6 +5439,9 @@ function ShopVideo({ src, poster, fill: fill2 = false, className, style, priorit
5438
5439
  const el = ref.current;
5439
5440
  if (!el || typeof IntersectionObserver === "undefined") return;
5440
5441
  const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)");
5442
+ const fetchNow = () => {
5443
+ if (el.preload === "none") el.preload = "auto";
5444
+ };
5441
5445
  const play = () => {
5442
5446
  if (reduced?.matches) {
5443
5447
  el.pause();
@@ -5446,10 +5450,18 @@ function ShopVideo({ src, poster, fill: fill2 = false, className, style, priorit
5446
5450
  void el.play().catch(() => {
5447
5451
  });
5448
5452
  };
5449
- const observer = new IntersectionObserver((entries) => {
5453
+ const warm = new IntersectionObserver((entries) => {
5454
+ if (!entries.some((entry) => entry.isIntersecting)) return;
5455
+ if (!reduced?.matches) fetchNow();
5456
+ warm.disconnect();
5457
+ }, {
5458
+ rootMargin: WARM_MARGIN
5459
+ });
5460
+ warm.observe(el);
5461
+ const playback = new IntersectionObserver((entries) => {
5450
5462
  for (const entry of entries) {
5451
5463
  if (entry.isIntersecting) {
5452
- if (el.preload === "none") el.preload = "auto";
5464
+ fetchNow();
5453
5465
  play();
5454
5466
  } else {
5455
5467
  el.pause();
@@ -5458,10 +5470,11 @@ function ShopVideo({ src, poster, fill: fill2 = false, className, style, priorit
5458
5470
  }, {
5459
5471
  threshold: 0.25
5460
5472
  });
5461
- observer.observe(el);
5473
+ playback.observe(el);
5462
5474
  reduced?.addEventListener("change", play);
5463
5475
  return () => {
5464
- observer.disconnect();
5476
+ warm.disconnect();
5477
+ playback.disconnect();
5465
5478
  reduced?.removeEventListener("change", play);
5466
5479
  };
5467
5480
  }, []);
@@ -5480,7 +5493,6 @@ function ShopVideo({ src, poster, fill: fill2 = false, className, style, priorit
5480
5493
  ...fillStyle,
5481
5494
  ...style
5482
5495
  },
5483
- autoPlay: true,
5484
5496
  loop: true,
5485
5497
  muted: true,
5486
5498
  playsInline: true,
@@ -11004,7 +11016,13 @@ function useDocumentSnap(ref, enabled = true) {
11004
11016
  const previousPadding = root2.style.scrollPaddingTop;
11005
11017
  root2.style.scrollSnapType = "y mandatory";
11006
11018
  root2.style.scrollPaddingTop = "0px";
11019
+ const top2 = () => window.scrollTo(0, 0);
11020
+ top2();
11021
+ const frame2 = typeof window.requestAnimationFrame === "function" ? window.requestAnimationFrame(top2) : null;
11007
11022
  return () => {
11023
+ if (frame2 !== null && typeof window.cancelAnimationFrame === "function") {
11024
+ window.cancelAnimationFrame(frame2);
11025
+ }
11008
11026
  root2.style.scrollSnapType = previousType;
11009
11027
  root2.style.scrollPaddingTop = previousPadding;
11010
11028
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nok-integration/storefront-react",
3
- "version": "0.19.44",
3
+ "version": "0.19.46",
4
4
  "description": "Mountable React storefront components: catalogue, product detail and cart, mounted directly into a host DOM tree. No iframe.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",