From 72674b91cdfade12c0f0fdbdefbfc76bbf1ef327 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 3 Feb 2026 15:14:34 -0800 Subject: [PATCH 1/6] Add test that landscape video will be used when no portrait --- .../cypress/component/ContentfulVisual.cy.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/contentful/cypress/component/ContentfulVisual.cy.tsx b/packages/contentful/cypress/component/ContentfulVisual.cy.tsx index 19198e9..01f3571 100644 --- a/packages/contentful/cypress/component/ContentfulVisual.cy.tsx +++ b/packages/contentful/cypress/component/ContentfulVisual.cy.tsx @@ -113,6 +113,21 @@ describe("contentful visual entry props", () => { cy.get("video").its("[0].currentSrc").should("contain", videoAsset.url); }); + it("uses landscape video when portrait video is not explicitly set", () => { + cy.mount( + , + ); + + // Portrait asset should use landscape video + cy.get("video").its("[0].currentSrc").should("contain", videoAsset.url); + }); + it("renders full visual entry", () => { cy.mount(); From 6f0f634d234b02d2587fe9ad2b68ddbe0020eae5 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 3 Feb 2026 16:40:17 -0800 Subject: [PATCH 2/6] Support reusuing video sources on multiple queries --- .../react/cypress/component/LazyVideo.cy.tsx | 33 +++++++++++++++++++ .../react/src/LazyVideo/LazyVideoClient.tsx | 32 ++++++++++++++---- .../react/src/LazyVideo/LazyVideoServer.tsx | 12 +++---- 3 files changed, 62 insertions(+), 15 deletions(-) diff --git a/packages/react/cypress/component/LazyVideo.cy.tsx b/packages/react/cypress/component/LazyVideo.cy.tsx index 7916405..302205c 100644 --- a/packages/react/cypress/component/LazyVideo.cy.tsx +++ b/packages/react/cypress/component/LazyVideo.cy.tsx @@ -70,6 +70,39 @@ describe("responsive video", () => { cy.viewport(500, 600); cy.get("video").its("[0].currentSrc").should("contain", "portrait"); }); + + it("supports the same asset with different media queries", () => { + cy.mount( + { + if (media?.includes("portrait")) return src.portrait; + else return src.landscape; + }} + alt="Same srcs" + />, + ); + + // Portrait should use landscape asset + cy.get("video source") + .should("have.attr", "src") + .and("contain", "landscape"); + + // Landscape should also use landscape asset. Using a test on the source + // element because currentSrc on video element wasn't changing even though + // video was empty. + cy.viewport(500, 250); + cy.wait(100); // Wait for resize to propagate + cy.get("video source") + .should("have.attr", "src") + .and("contain", "landscape"); + }); }); describe("Accessibility controls", () => { diff --git a/packages/react/src/LazyVideo/LazyVideoClient.tsx b/packages/react/src/LazyVideo/LazyVideoClient.tsx index db6f50c..a452980 100644 --- a/packages/react/src/LazyVideo/LazyVideoClient.tsx +++ b/packages/react/src/LazyVideo/LazyVideoClient.tsx @@ -2,7 +2,7 @@ "use client"; import { useInView } from "react-intersection-observer"; -import { useMediaQueries } from "@react-hook/media-query"; +import { MediaQueryMatches, useMediaQueries } from "@react-hook/media-query"; import { useEffect, useRef, @@ -20,6 +20,10 @@ type LazyVideoClientProps = Omit< "videoLoader" | "src" | "sourceMedia" > & { srcUrl?: string; + /** + * The key is the media query, the value is the URL to use when that media + * query matches. + */ mediaSrcs?: Record; }; @@ -179,9 +183,20 @@ function ResponsiveSource({ mediaSrcs, videoRef, }: ResponsiveVideoSourceProps): ReactNode { + // Make an object suitable for useMediaQueries that uses indexes from the + // mediaSrcs obj as it's keys so there won't be any issues with multiple + // media queries using the same asset. + const indexedQueries = Object.keys(mediaSrcs).reduce>( + (queries, mediaQuery, index) => { + queries[index] = mediaQuery; + return queries; + }, + {}, + ); + // Find the src url that is currently active - const { matches } = useMediaQueries(mediaSrcs); - const srcUrl = getFirstMatch(matches); + const { matches } = useMediaQueries(indexedQueries); + const srcUrl = getFirstMatch(mediaSrcs, matches); // Reload the video since the source changed useEffect(() => reloadVideoWhenSafe(videoRef), [matches]); @@ -191,10 +206,13 @@ function ResponsiveSource({ } // Get the URL with a media query match -function getFirstMatch(matches: Record): string | undefined { - for (const srcUrl in matches) { - if (matches[srcUrl]) { - return srcUrl; +function getFirstMatch( + mediaSrcs: Record, + matches: MediaQueryMatches>["matches"], +): string | undefined { + for (const index in matches) { + if (matches[index]) { + return Object.values(mediaSrcs)[index]; } } } diff --git a/packages/react/src/LazyVideo/LazyVideoServer.tsx b/packages/react/src/LazyVideo/LazyVideoServer.tsx index d746aae..f9c42d6 100644 --- a/packages/react/src/LazyVideo/LazyVideoServer.tsx +++ b/packages/react/src/LazyVideo/LazyVideoServer.tsx @@ -19,15 +19,11 @@ export default function LazyVideo(props: LazyVideoProps): ReactNode { // Prepare a hash of source URLs and their media query constraint in the // style expected by useMediaQueries. if (useResponsiveSource) { - const mediaSrcEntries = sourceMedia.map((media) => { + mediaSrcs = sourceMedia.reduce>((srcs, media) => { const url = videoLoader({ src, media }); - return [url, media]; - }); - // If the array ended up empty, abort - if (mediaSrcEntries.filter(([url]) => !!url).length == 0) return null; - - // Make the hash - mediaSrcs = Object.fromEntries(mediaSrcEntries); + if (url) srcs[media] = url; + return srcs; + }, {}); // Make a simple string src url } else { From 8801a41def0f22828475b0052be53e9c447ed654 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 3 Feb 2026 16:46:05 -0800 Subject: [PATCH 3/6] =?UTF-8?q?Don=E2=80=99t=20add=20video=20tag=20if=20no?= =?UTF-8?q?=20responsive=20sources?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/react/src/LazyVideo/LazyVideoServer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react/src/LazyVideo/LazyVideoServer.tsx b/packages/react/src/LazyVideo/LazyVideoServer.tsx index f9c42d6..acaa544 100644 --- a/packages/react/src/LazyVideo/LazyVideoServer.tsx +++ b/packages/react/src/LazyVideo/LazyVideoServer.tsx @@ -24,6 +24,7 @@ export default function LazyVideo(props: LazyVideoProps): ReactNode { if (url) srcs[media] = url; return srcs; }, {}); + if (!Object.values(mediaSrcs).length) return null; // Abort if no urls // Make a simple string src url } else { From 71ab6cb2eff59b85c59dcfae94cf1109e9c513fa Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 3 Feb 2026 16:47:26 -0800 Subject: [PATCH 4/6] Correct comment --- packages/react/src/LazyVideo/LazyVideoServer.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react/src/LazyVideo/LazyVideoServer.tsx b/packages/react/src/LazyVideo/LazyVideoServer.tsx index acaa544..69f30b2 100644 --- a/packages/react/src/LazyVideo/LazyVideoServer.tsx +++ b/packages/react/src/LazyVideo/LazyVideoServer.tsx @@ -16,8 +16,7 @@ export default function LazyVideo(props: LazyVideoProps): ReactNode { // Vars that will be conditionally populated let srcUrl, mediaSrcs; - // Prepare a hash of source URLs and their media query constraint in the - // style expected by useMediaQueries. + // Prepare a hash mapping media query strings to source URLs if (useResponsiveSource) { mediaSrcs = sourceMedia.reduce>((srcs, media) => { const url = videoLoader({ src, media }); From 8965772adab8756b0bc118d45030db818ac8b318 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 3 Feb 2026 16:49:38 -0800 Subject: [PATCH 5/6] Memoize indexedQueries --- .../react/src/LazyVideo/LazyVideoClient.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/react/src/LazyVideo/LazyVideoClient.tsx b/packages/react/src/LazyVideo/LazyVideoClient.tsx index a452980..c0e443d 100644 --- a/packages/react/src/LazyVideo/LazyVideoClient.tsx +++ b/packages/react/src/LazyVideo/LazyVideoClient.tsx @@ -10,6 +10,7 @@ import { type MutableRefObject, useState, type ReactNode, + useMemo, } from "react"; import type { LazyVideoProps } from "../types/lazyVideoTypes"; import { fillStyles, transparentGif } from "../lib/styles"; @@ -186,13 +187,15 @@ function ResponsiveSource({ // Make an object suitable for useMediaQueries that uses indexes from the // mediaSrcs obj as it's keys so there won't be any issues with multiple // media queries using the same asset. - const indexedQueries = Object.keys(mediaSrcs).reduce>( - (queries, mediaQuery, index) => { - queries[index] = mediaQuery; - return queries; - }, - {}, - ); + const indexedQueries = useMemo(() => { + return Object.keys(mediaSrcs).reduce>( + (queries, mediaQuery, index) => { + queries[index] = mediaQuery; + return queries; + }, + {}, + ); + }, [mediaSrcs]); // Find the src url that is currently active const { matches } = useMediaQueries(indexedQueries); From d44085636c6b88497191e05cf5eb5a77279d0d1d Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 3 Feb 2026 16:49:51 -0800 Subject: [PATCH 6/6] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/react/src/LazyVideo/LazyVideoClient.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/LazyVideo/LazyVideoClient.tsx b/packages/react/src/LazyVideo/LazyVideoClient.tsx index a452980..9a51e5c 100644 --- a/packages/react/src/LazyVideo/LazyVideoClient.tsx +++ b/packages/react/src/LazyVideo/LazyVideoClient.tsx @@ -184,7 +184,7 @@ function ResponsiveSource({ videoRef, }: ResponsiveVideoSourceProps): ReactNode { // Make an object suitable for useMediaQueries that uses indexes from the - // mediaSrcs obj as it's keys so there won't be any issues with multiple + // mediaSrcs obj as its keys so there won't be any issues with multiple // media queries using the same asset. const indexedQueries = Object.keys(mediaSrcs).reduce>( (queries, mediaQuery, index) => {