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 {