Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Video + Markdown): update react player + put videos in markdown #2941

Merged
merged 20 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from 18 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
2 changes: 1 addition & 1 deletion packages/gamut/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"react-aria-tabpanel": "^4.4.0",
"react-focus-on": "^3.5.1",
"react-hook-form": "^7.21.2",
"react-player": "^2.3.1",
"react-player": "^2.16.0",
"react-select": "^5.2.2",
"react-truncate-markup": "^5.1.2",
"react-use": "^15.3.8",
Expand Down
71 changes: 54 additions & 17 deletions packages/gamut/src/Markdown/__tests__/Markdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import * as React from 'react';

import { Markdown } from '../index';

const mockTitle = 'a fake youtube';

jest.mock('react-player', () => ({
__esModule: true,
default: () => <iframe title={mockTitle} />,
}));

const basicMarkdown = `
# Heading 1

Expand Down Expand Up @@ -36,12 +43,35 @@ const youtubeMarkdown = `
<iframe src="https://www.youtube.com/embed/KvgrQIK1yPY" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
`;

const vimeoMarkdown = `
<iframe src="https://player.vimeo.com/video/188237476?badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
`;

const videoMarkdown = `
<video src="/example.webm" title="video" />
`;

const videoSourceMarkdown = `
<video controls poster="/images/spaceghost.gif">
<source src="movie.mp4" type="video/mp4">
<source src="movie.ogg" type="video/ogg">
</video>
`;

const checkboxMarkdown = `
- [ ] checkbox
- [x] default checked checkbox
- [ ] third checkbox
`;

const table = `
| Tables | Are | Cool |
|----------|:-------------:|------:|
| col 1 is | left-aligned | $1600 |
| col 2 is | centered | $12 |
| col 3 is | right-aligned | $1 |
`;

const renderView = setupRtl(Markdown);

describe('<Markdown />', () => {
Expand All @@ -60,27 +90,13 @@ describe('<Markdown />', () => {
});

it('Renders custom tables in markdown', () => {
const table = `
| Tables | Are | Cool |
|----------|:-------------:|------:|
| col 1 is | left-aligned | $1600 |
| col 2 is | centered | $12 |
| col 3 is | right-aligned | $1 |
`;
renderView({ text: table });
expect(document.querySelectorAll('div.tableWrapper table').length).toEqual(
1
);
});

it('Skips rendering custom tables in markdown when skipProcessing.table is true', () => {
const table = `
| Tables | Are | Cool |
|----------|:-------------:|------:|
| col 1 is | left-aligned | $1600 |
| col 2 is | centered | $12 |
| col 3 is | right-aligned | $1 |
`;
renderView({
skipDefaultOverrides: { table: true },
text: table,
Expand All @@ -91,9 +107,28 @@ describe('<Markdown />', () => {
);
});

it('Wraps youtube iframes in a flexible container', () => {
it('Renders YouTube iframes using the Video component', () => {
renderView({ text: youtubeMarkdown });
screen.getByTitle(mockTitle);
});

it('Renders Vimeo iframes using the Video component', () => {
renderView({ text: vimeoMarkdown });
screen.getByTitle(mockTitle);
});

it('Renders video tags using the Video component if they have an src', () => {
renderView({ text: videoMarkdown });
screen.getByTitle(mockTitle);
});
it('Renders video tags using the Video component if they have an src', () => {
renderView({ text: videoSourceMarkdown });
expect(screen.queryByTitle(mockTitle)).toBeNull();
});

it('Renders YouTube iframes using the Video component', () => {
renderView({ text: youtubeMarkdown });
screen.getByTestId('yt-iframe');
screen.getByTitle(mockTitle);
});

it('Wraps the markdown in a div by default (block)', () => {
Expand Down Expand Up @@ -288,6 +323,7 @@ var test = true;
isCodeBlock?: boolean;
isWebBrowser?: boolean;
};

const renderedProps: jest.Mock<RenderedProps> = jest.fn();

beforeEach(() => {
Expand All @@ -296,9 +332,10 @@ var test = true;

<TestComponent name="my name" isCodeBlock="true" isWebBrowser />
`;

const TestComponent = (props: any) => {
renderedProps(props);
return <strong {...props}>attr-testing-component</strong>;
return <strong>attr-testing-component</strong>;
};

const overrides = {
Expand Down
6 changes: 6 additions & 0 deletions packages/gamut/src/Markdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
MarkdownAnchorProps,
} from './libs/overrides/MarkdownAnchor';
import { Table } from './libs/overrides/Table';
import { MarkdownVideo } from './libs/overrides/Video';
import { createPreprocessingInstructions } from './libs/preprocessing';
import { defaultSanitizationConfig } from './libs/sanitizationConfig';
// eslint-disable-next-line gamut/no-css-standalone
Expand All @@ -40,6 +41,7 @@ export type SkipDefaultOverridesSettings = {
details?: boolean;
iframe?: boolean;
table?: boolean;
video?: boolean;
};

export type MarkdownProps = {
Expand Down Expand Up @@ -114,6 +116,10 @@ export class Markdown extends PureComponent<MarkdownProps> {
component: Table,
allowedAttributes: ['style'],
}),
!skipDefaultOverrides.video &&
createTagOverride('video', {
component: MarkdownVideo,
}),
!skipDefaultOverrides.details &&
createTagOverride('details', {
component: Details,
Expand Down
26 changes: 9 additions & 17 deletions packages/gamut/src/Markdown/libs/overrides/Iframe/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/* eslint-disable jsx-a11y/iframe-has-title */
import { FunctionComponent, HTMLAttributes } from 'react';

// eslint-disable-next-line gamut/no-css-standalone
import styles from './styles.module.scss';
import { Video } from '../../../../Video';

export interface IframeProps extends HTMLAttributes<HTMLIFrameElement> {
allow?: string;
src?: string;
title?: string;
width?: number;
Expand All @@ -19,23 +19,15 @@ export const Iframe: FunctionComponent<IframeProps> = (props) => {
props.src &&
[YOUTUBE_PATTERN, VIMEO_PATTERN].some((pattern) => pattern.test(props.src!))
) {
const { width = 16, height = 9 } = props;
const ratioPadding = (
(Math.round(height) / Math.round(width)) *
100
).toFixed(2);
const wrapperStyles = {
paddingBottom: `${ratioPadding}%`,
};
return (
<div
className={styles.youtubeVideoWrapper}
data-testid="yt-iframe"
style={wrapperStyles}
>
<iframe {...props} />
</div>
<Video
height={props?.height}
width={props?.width}
videoUrl={props?.src}
videoTitle={props?.title}
/>
);
}

return <iframe {...props} />;
};

This file was deleted.

30 changes: 30 additions & 0 deletions packages/gamut/src/Markdown/libs/overrides/Video/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* eslint-disable jsx-a11y/iframe-has-title */
import { DetailedHTMLProps, VideoHTMLAttributes } from 'react';

import { Video } from '../../../../Video';
// eslint-disable-next-line gamut/no-css-standalone

export type MarkdownVideoProps = DetailedHTMLProps<
VideoHTMLAttributes<HTMLVideoElement>,
HTMLVideoElement
>;

export const MarkdownVideo: React.FC<MarkdownVideoProps> = (props) => {
if (props?.src) {
// Sanitize the props to pass to the Video component
const videoProps = {
autoplay: props?.autoPlay,
controls: props?.controls,
height: Number(props?.height),
loop: props?.loop,
muted: props?.muted,
videoTitle: props?.title,
videoUrl: props?.src,
width: Number(props?.width),
};

return <Video {...videoProps} />;
}
// eslint-disable-next-line jsx-a11y/media-has-caption
return <video {...props} />;
};
12 changes: 11 additions & 1 deletion packages/gamut/src/Markdown/libs/sanitizationConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,17 @@ export const defaultSanitizationConfig = {
source: ['src', 'type'],
img: ['src', 'alt', 'height', 'width', 'title', 'aria-label', 'style'],
input: ['checked', 'type'],
video: ['width', 'height', 'align', 'style', 'controls'],
video: [
'align',
'autoPlay',
'controls',
'height',
'loop',
'muted',
'src',
'style',
'width',
],
iframe: [
'src',
'width',
Expand Down
45 changes: 31 additions & 14 deletions packages/gamut/src/Video/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,47 +27,64 @@ const OverlayPlayButton = ({ videoTitle }: { videoTitle?: string }) => {
export type ReactPlayerWithWrapper = ReactPlayer & { wrapper: HTMLElement };

export type VideoProps = {
videoUrl: string;
videoTitle?: string;
placeholderImage?: string | boolean;
autoplay?: boolean;
className?: string;
controls?: boolean;
height?: number;
loop?: boolean;
muted?: boolean;
className?: string;
onReady?: (player: ReactPlayerWithWrapper) => void;
onPlay?: () => void;
onReady?: (player: ReactPlayerWithWrapper) => void;
placeholderImage?: string | boolean;
videoTitle?: string;
videoUrl: string;
width?: number;
};

export const Video: React.FC<VideoProps> = ({
videoUrl,
videoTitle,
placeholderImage,
autoplay,
className,
controls,
height,
loop,
muted,
className,
onReady,
onPlay,
onReady,
placeholderImage,
videoTitle,
videoUrl,
width,
}) => {
const [loading, setLoading] = useState(true);
const isMounted = useIsMounted();

const config = {
youtube: {
playerVars: { color: 'white' },
},
vimeo: {
title: videoTitle,
},
};

return (
<div
className={cx(styles.videoWrapper, loading && styles.loading, className)}
>
{isMounted ? (
<ReactPlayer
url={videoUrl}
light={placeholderImage}
title={videoTitle}
playing={autoplay}
className={styles.iframe}
config={config}
controls={controls === undefined ? true : controls}
height={height}
light={placeholderImage}
loop={loop}
muted={muted}
playIcon={<OverlayPlayButton videoTitle={videoTitle} />}
playing={autoplay}
title={videoTitle}
url={videoUrl}
width={width}
onReady={(player: ReactPlayerWithWrapper) => {
onReady?.(player);
setLoading(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ import { Video } from '@codecademy/gamut';
export const VideoWithPlaceholder = () => {
return (
<Video
placeholder="https://i.ytimg.com/vi/1y_kfWUCFDQ/maxresdefault.jpg"
videoUrl="https://www.youtube.com/watch?v=Yl8yy5tpVIM"
videoTitle="Workout with Rick Sanchez"
videoUrl="https://player.vimeo.com/video/188237476"
videoTitle="A Dream Within a Dream"
placeholderImage="https://placekitten.com/400/300"
autoplay
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ given a video URL`,
Tip: Some videos might have settings where you cannot share/embed them, make sure to check the video URL before making the Video go live.

<Canvas>
<Story name="Video">
<Story name="Youtube Video">
{(args) => {
return <Video {...args} />;
}}
</Story>
</Canvas>

## Here is an example with a Viemo URL
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Viemo lol

## Here is an example with a Vimeo URL

<Canvas>
<Story name="Vimeo video URL">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,4 +347,18 @@ Use the `printf()` function.

### Iframes

<iframe src="https://player.vimeo.com/video/145702525?byline=0&portrait=0&badge=0" width="640" height="360" frameborder="0" allow="autoplay; fullscreen" allowfullscreen></iframe>
Vimeo and Youtube video iframes will be rendered by our Video component, otherwise they'll render as stated.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: what does render as stated mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i meant just renders the original code, i'll rephrase


<iframe src="https://player.vimeo.com/video/188237476?badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479" frameborder="0" allow="autoplay; fullscreen; picture-in-picture; clipboard-write" style="position:absolute;top:0;left:0;width:100%;height:100%;" title="Studio Ghibli in Real Life"></iframe>

<br/>

<iframe width="1094" height="842" src="https://www.youtube.com/embed/zhDwjnYZiCo" title="Ghibli Coffee Shop ☕️ Music to put you in a better mood 🌿 lofi hip hop - lofi songs | study / relax" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>


### Video

`video`s with an `src` will be rendered by our Video component, otherwise they'll render as stated.

<video src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm" title="video" />

Loading
Loading