Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/contentful/cypress/component/ContentfulVisual.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ContentfulVisual
expand
src={{
...visualEntry,
portraitVideo: null,
}}
/>,
);

// Portrait asset should use landscape video
cy.get("video").its("[0].currentSrc").should("contain", videoAsset.url);
});

it("renders full visual entry", () => {
cy.mount(<ContentfulVisual src={visualEntry} />);

Expand Down
33 changes: 33 additions & 0 deletions packages/react/cypress/component/LazyVideo.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<LazyVideo
src={{
portrait:
"https://github.com/BKWLD/react-visual/raw/refs/heads/main/packages/react/cypress/fixtures/500x250.mp4#landscape",
landscape:
"https://github.com/BKWLD/react-visual/raw/refs/heads/main/packages/react/cypress/fixtures/500x250.mp4#landscape",
}}
sourceMedia={["(orientation:landscape)", "(orientation:portrait)"]}
videoLoader={({ src, media }) => {
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", () => {
Expand Down
35 changes: 28 additions & 7 deletions packages/react/src/LazyVideo/LazyVideoClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
"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,
useCallback,
type MutableRefObject,
useState,
type ReactNode,
useMemo,
} from "react";
import type { LazyVideoProps } from "../types/lazyVideoTypes";
import { fillStyles, transparentGif } from "../lib/styles";
Expand All @@ -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<string, string>;
};

Expand Down Expand Up @@ -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<Record<number, string>>(
(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<typeof indexedQueries>(indexedQueries);
const srcUrl = getFirstMatch(mediaSrcs, matches);

// Reload the video since the source changed
useEffect(() => reloadVideoWhenSafe(videoRef), [matches]);
Expand All @@ -191,10 +209,13 @@ function ResponsiveSource({
}

// Get the URL with a media query match
function getFirstMatch(matches: Record<string, boolean>): string | undefined {
for (const srcUrl in matches) {
if (matches[srcUrl]) {
return srcUrl;
function getFirstMatch(
mediaSrcs: Record<string, string>,
matches: MediaQueryMatches<Record<number, string>>["matches"],
): string | undefined {
for (const index in matches) {
if (matches[index]) {
return Object.values(mediaSrcs)[index];
}
}
}
Expand Down
16 changes: 6 additions & 10 deletions packages/react/src/LazyVideo/LazyVideoServer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, string>>((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 {
Expand Down
Loading