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(); 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..eba9b11 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, @@ -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"; @@ -20,6 +21,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 +184,22 @@ function ResponsiveSource({ mediaSrcs, videoRef, }: ResponsiveVideoSourceProps): ReactNode { + // Make an object suitable for useMediaQueries that uses indexes from the + // mediaSrcs obj as its keys so there won't be any issues with multiple + // media queries using the same asset. + 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(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 +209,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..69f30b2 100644 --- a/packages/react/src/LazyVideo/LazyVideoServer.tsx +++ b/packages/react/src/LazyVideo/LazyVideoServer.tsx @@ -16,18 +16,14 @@ 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) { - 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; + }, {}); + if (!Object.values(mediaSrcs).length) return null; // Abort if no urls // Make a simple string src url } else {