Skip to content

Commit

Permalink
Add forwardRef to ExternalVideo and Video (#1415)
Browse files Browse the repository at this point in the history
Co-authored-by: Anders Søgaard <9662430+andershagbard@users.noreply.github.com>
Co-authored-by: Benjamin Sehl <ben@sehl.ca>
  • Loading branch information
3 people authored Jan 22, 2024
1 parent 0241b7d commit dc8f90d
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 46 deletions.
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>
);
}
});

0 comments on commit dc8f90d

Please sign in to comment.