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
5 changes: 5 additions & 0 deletions .changeset/early-planes-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/hydrogen-react': patch
---

Added React.forwardRef to Video and ExternalVideo components
12 changes: 12 additions & 0 deletions packages/hydrogen-react/src/ExternalVideo.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {vi, describe, expect, it} from 'vitest';

import {createRef} from 'react';
import {render, screen} from '@testing-library/react';
import {ExternalVideo} from './ExternalVideo.js';
import {getExternalVideoData} from './ExternalVideo.test.helpers.js';
Expand Down Expand Up @@ -111,4 +112,15 @@ describe('<ExternalVideo />', () => {
'https://www.youtube.com/embed/a2YSgfwXc9c?autoplay=true&color=red',
);
});

it('allows ref', () => {
const video = getExternalVideoData();
const ref = createRef<HTMLIFrameElement>();

render(<ExternalVideo data={video} ref={ref} data-testid={testId} />);

const videoEl = screen.getByTestId(testId);

expect(videoEl).toBe(ref.current);
});
});
88 changes: 48 additions & 40 deletions packages/hydrogen-react/src/ExternalVideo.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {ExternalVideo as ExternalVideoType} from './storefront-api-types.js';
import type {Entries, PartialDeep} from 'type-fest';
import {forwardRef, IframeHTMLAttributes} from 'react';

interface ExternalVideoBaseProps {
/**
Expand All @@ -13,58 +14,65 @@ interface ExternalVideoBaseProps {
options?: YouTube | Vimeo;
}

export type ExternalVideoProps = Omit<JSX.IntrinsicElements['iframe'], 'src'> &
export type ExternalVideoProps = Omit<
IframeHTMLAttributes<HTMLIFrameElement>,
'src'
> &
ExternalVideoBaseProps;

/**
* The `ExternalVideo` component renders an embedded video for the Storefront
* API's [ExternalVideo object](https://shopify.dev/api/storefront/reference/products/externalvideo).
*/
export function ExternalVideo(props: ExternalVideoProps): JSX.Element {
const {
data,
options,
id = data.id,
frameBorder = '0',
allow = 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture',
allowFullScreen = true,
loading = 'lazy',
...passthroughProps
} = props;

if (!data.embedUrl) {
throw new Error(`<ExternalVideo/> requires the 'embedUrl' property`);
}
export const ExternalVideo = forwardRef<HTMLIFrameElement, ExternalVideoProps>(
(props, ref): JSX.Element => {
const {
data,
options,
id = data.id,
frameBorder = '0',
allow = 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture',
allowFullScreen = true,
loading = 'lazy',
...passthroughProps
} = props;

let finalUrl: string = data.embedUrl;
if (!data.embedUrl) {
throw new Error(`<ExternalVideo/> requires the 'embedUrl' property`);
}

if (options) {
const urlObject = new URL(data.embedUrl);
for (const [key, value] of Object.entries(options) as Entries<
typeof options
>) {
if (typeof value === 'undefined') {
continue;
}
let finalUrl: string = data.embedUrl;

if (options) {
const urlObject = new URL(data.embedUrl);
for (const [key, value] of Object.entries(options) as Entries<
typeof options
>) {
if (typeof value === 'undefined') {
continue;
}

urlObject.searchParams.set(key, value.toString());
urlObject.searchParams.set(key, value.toString());
}
finalUrl = urlObject.toString();
}
finalUrl = urlObject.toString();
}

return (
<iframe
{...passthroughProps}
id={id ?? data.embedUrl}
title={data.alt ?? data.id ?? 'external video'}
frameBorder={frameBorder}
allow={allow}
allowFullScreen={allowFullScreen}
src={finalUrl}
loading={loading}
></iframe>
);
}
return (
<iframe
{...passthroughProps}
id={id ?? data.embedUrl}
title={data.alt ?? data.id ?? 'external video'}
frameBorder={frameBorder}
allow={allow}
allowFullScreen={allowFullScreen}
src={finalUrl}
loading={loading}
ref={ref}
></iframe>
);
},
);

interface YouTube {
autoplay?: 0 | 1;
Expand Down
59 changes: 58 additions & 1 deletion packages/hydrogen-react/src/MediaFile.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import {describe, it} from 'vitest';
import {describe, expect, it} from 'vitest';

import {createRef} from 'react';
import {render, screen} from '@testing-library/react';
import {MediaFile} from './MediaFile.js';

const testId = 'media-file';

describe(`<MediaFile/>`, () => {
it.skip(`typescript types`, () => {
// ensure className is valid
Expand All @@ -14,4 +18,57 @@ describe(`<MediaFile/>`, () => {
<MediaFile data={{id: 'test'}} mediaOptions={{image: {}, video: {}}} />;
<MediaFile data={{id: 'test'}} mediaOptions={{}} />;
});

it('allows ref on video', () => {
const ref = createRef<HTMLVideoElement>();

const data = {
__typename: 'Video' as const,
mediaContentType: 'VIDEO' as const,
sources: [],
};

render(
<MediaFile
data={data}
mediaOptions={{
video: {
ref,
},
}}
data-testid={testId}
/>,
);

const mediaFile = screen.getByTestId(testId);

expect(ref.current).toBe(mediaFile);
});

it('allows ref on external video', () => {
const ref = createRef<HTMLIFrameElement>();

const data = {
__typename: 'ExternalVideo' as const,
mediaContentType: 'EXTERNAL_VIDEO' as const,
embedUrl: 'https://www.youtube.com/embed/dQw4w9WgXcQ',
host: 'YOUTUBE' as const,
};

render(
<MediaFile
data={data}
mediaOptions={{
externalVideo: {
ref,
},
}}
data-testid={testId}
/>,
);

const mediaFile = screen.getByTestId(testId);

expect(ref.current).toBe(mediaFile);
});
});
11 changes: 11 additions & 0 deletions packages/hydrogen-react/src/Video.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {createRef} from 'react';
import {describe, expect, it} from 'vitest';
import {render, screen} from '@testing-library/react';
import {Video} from './Video.js';
Expand Down Expand Up @@ -58,4 +59,14 @@ describe('<Video />', () => {

expect(video).toHaveAttribute('class', 'testClass');
});

it('allows ref', () => {
const ref = createRef<HTMLVideoElement>();

render(<Video data={VIDEO_PROPS} ref={ref} data-testid="video" />);

const video = screen.getByTestId('video');

expect(video).toBe(ref.current);
});
});
12 changes: 7 additions & 5 deletions packages/hydrogen-react/src/Video.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {type HTMLAttributes} from 'react';
import {forwardRef, type HTMLAttributes} from 'react';
import {shopifyLoader} from './Image.js';
import type {Video as VideoType} from './storefront-api-types.js';
import type {PartialDeep} from 'type-fest';
Expand All @@ -17,9 +17,10 @@ export interface VideoProps {
/**
* The `Video` component renders a `video` for the Storefront API's [Video object](https://shopify.dev/api/storefront/reference/products/video).
*/
export function Video(
props: JSX.IntrinsicElements['video'] & VideoProps,
): JSX.Element {
export const Video = forwardRef<
HTMLVideoElement,
JSX.IntrinsicElements['video'] & VideoProps
>((props, ref): JSX.Element => {
const {
data,
previewImageOptions,
Expand Down Expand Up @@ -47,6 +48,7 @@ export function Video(
playsInline={playsInline}
controls={controls}
poster={posterUrl}
ref={ref}
>
{data.sources.map((source) => {
if (!(source?.url && source?.mimeType)) {
Expand All @@ -63,4 +65,4 @@ export function Video(
})}
</video>
);
}
});