diff --git a/.nx/version-plans/version-plan-1791216778502.md b/.nx/version-plans/version-plan-1791216778502.md new file mode 100644 index 00000000000..645bed50cfb --- /dev/null +++ b/.nx/version-plans/version-plan-1791216778502.md @@ -0,0 +1,8 @@ +--- +gamut: major +--- + +Move Video out of barrel and update + +- `Video` is now imported from `@codecademy/gamut/Video` instead of `@codecademy/gamut`. +- `@vidstack/react` and `react-player` are now peer dependencies. If you use `Video` (or the Markdown `Iframe`/`MarkdownVideo` overrides), install them: `yarn add @vidstack/react@~1.12.12 react-player@^3.4.0`. Gamut supports `@vidstack/react` 1.12.x only; note that its npm `latest` tag still points to 0.6.x. diff --git a/package.json b/package.json index ebed4bad7d2..5602fb5a022 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "dependencies": { "@emotion/react": "11.14.0", "@emotion/styled": "11.14.1", - "@vidstack/react": "^1.12.12", + "@vidstack/react": "~1.12.12", "core-js": "3.7.0", "lodash": "^4.17.23", "react": "^19.0.0", diff --git a/packages/code-connect/Molecules/Video.figma.tsx b/packages/code-connect/Molecules/Video.figma.tsx index 70cbc52079f..8ed08e90ea6 100644 --- a/packages/code-connect/Molecules/Video.figma.tsx +++ b/packages/code-connect/Molecules/Video.figma.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Video } from '@codecademy/gamut'; +import { Video } from '@codecademy/gamut/Video'; import figma from '@figma/code-connect'; /** diff --git a/packages/gamut/Video/package.json b/packages/gamut/Video/package.json new file mode 100644 index 00000000000..4a30c50a67d --- /dev/null +++ b/packages/gamut/Video/package.json @@ -0,0 +1,5 @@ +{ + "main": "../dist/Video/index.js", + "module": "../dist/Video/index.js", + "types": "../dist/Video/index.d.ts" +} diff --git a/packages/gamut/__tests__/__snapshots__/gamut.test.ts.snap b/packages/gamut/__tests__/__snapshots__/gamut.test.ts.snap index 14d8d3f58a3..30593dae536 100644 --- a/packages/gamut/__tests__/__snapshots__/gamut.test.ts.snap +++ b/packages/gamut/__tests__/__snapshots__/gamut.test.ts.snap @@ -132,6 +132,5 @@ exports[`Gamut Exported Keys 1`] = ` "useIsMounted", "useLocalQuery", "useSubmitState", - "Video", ] `; diff --git a/packages/gamut/jest.config.ts b/packages/gamut/jest.config.ts index a418f1b531f..668e9fffd52 100644 --- a/packages/gamut/jest.config.ts +++ b/packages/gamut/jest.config.ts @@ -9,5 +9,7 @@ export default base('gamut', { setupFiles: ['/../../script/jest/base-setup.js'], setupFilesAfterEnv: ['/../../script/jest/rtl-setup.js'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], - transformIgnorePatterns: ['node_modules/(?!(@vidstack/react|@formatjs)/)'], + transformIgnorePatterns: [ + 'node_modules/(?!(@vidstack/react|@formatjs|react-player)/)', + ], }); diff --git a/packages/gamut/package.json b/packages/gamut/package.json index f5b6c64eac9..039f70662a4 100644 --- a/packages/gamut/package.json +++ b/packages/gamut/package.json @@ -13,7 +13,6 @@ "@formatjs/intl-locale": "5.3.1", "@react-aria/interactions": "3.25.0", "@types/marked": "^4.0.8", - "@vidstack/react": "^1.12.12", "classnames": "^2.2.5", "framer-motion": "^12.0.0", "html-to-react": "^1.6.0", @@ -24,16 +23,20 @@ "react-aria-components": "1.7.1", "react-focus-on": "3.10.0", "react-hook-form": "^7.71.2", - "react-player": "^2.16.0", "react-select": "^5.2.2", "react-truncate-markup": "^5.1.2", "react-use": "^17.6.1", "sanitize-markdown": "^2.6.7" }, + "devDependencies": { + "@vidstack/react": "~1.12.12", + "react-player": "^3.4.0" + }, "files": [ "dist", "bin", - "agent-tools" + "agent-tools", + "Video" ], "license": "MIT", "main": "./dist/index.js", @@ -44,9 +47,11 @@ "peerDependencies": { "@emotion/react": "^11.4.0", "@emotion/styled": "^11.3.0", + "@vidstack/react": "~1.12.12", "css-loader": ">=4.0.0", "react": "^17.0.2 || ^18.3.0 || ^19.0.0", - "react-dom": "^17.0.2 || ^18.3.0 || ^19.0.0" + "react-dom": "^17.0.2 || ^18.3.0 || ^19.0.0", + "react-player": "^3.4.0" }, "peerDependenciesMeta": { "css-loader": { diff --git a/packages/gamut/src/Markdown/__tests__/Markdown.test.tsx b/packages/gamut/src/Markdown/__tests__/Markdown.test.tsx index a2f7f91f9c9..f89d8f48e4b 100644 --- a/packages/gamut/src/Markdown/__tests__/Markdown.test.tsx +++ b/packages/gamut/src/Markdown/__tests__/Markdown.test.tsx @@ -5,18 +5,23 @@ import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; +import { Iframe, MarkdownVideo } from '../../Video'; import { Markdown } from '../index'; const mockTitle = 'a fake youtube'; jest.mock('react-player', () => ({ __esModule: true, - default: () => \n\n`, + }); + screen.getByTitle('Doug Workshop Intro'); + screen.getByTitle('Example clip'); + }); + + it('Passes the iframe title through to the Video component when opted in', () => { + renderView({ + iframeOverride: { component: Iframe }, + text: ``, + }); + screen.getByTitle('Doug Workshop Intro'); + expect(screen.queryByTitle(mockTitle)).not.toBeInTheDocument(); + }); + + it('Does not wrap a single-line video override in a paragraph', () => { + renderView({ + text: ``, + videoOverride: { component: MarkdownVideo }, + }); + expect(document.querySelector('p div')).toBeNull(); + }); + + it('Passes undefined width and height to Video when the video has none', () => { + // React only warns about NaN styles once per value, so check the props directly + const element = MarkdownVideo({ + src: 'https://example.com/clip.mp4', + } as Parameters[0]) as React.ReactElement<{ + width?: number; + height?: number; + }>; + expect(element.props.width).toBeUndefined(); + expect(element.props.height).toBeUndefined(); + }); + + it('Passes the video title through to the Video component when opted in', () => { + renderView({ + text: ``, + videoOverride: { component: MarkdownVideo }, + }); + screen.getByTitle('Example clip'); + expect(screen.queryByTitle(mockTitle)).not.toBeInTheDocument(); + }); + + it('Renders video tags using the Video component if they have an src when opted in', () => { + renderView({ + text: videoMarkdown, + videoOverride: { component: MarkdownVideo }, + }); screen.getByTitle(mockTitle); }); - it('Renders YouTube iframes using the Video component', () => { - renderView({ text: youtubeMarkdown }); + + it('Renders video tags using the Video component if they have a source when opted in', () => { + renderView({ + text: videoSourceMarkdown, + videoOverride: { component: MarkdownVideo }, + }); screen.getByTitle(mockTitle); }); + it('Renders bare video tags when not opted in', () => { + renderView({ text: videoMarkdown }); + expect(document.querySelectorAll('video').length).toEqual(1); + expect(screen.queryByTitle(mockTitle)).not.toBeInTheDocument(); + }); it('Wraps the markdown in a div by default (block)', () => { renderView({ text: basicMarkdown }); expect( diff --git a/packages/gamut/src/Markdown/index.tsx b/packages/gamut/src/Markdown/index.tsx index bd17f489d4b..6aea782ab35 100644 --- a/packages/gamut/src/Markdown/index.tsx +++ b/packages/gamut/src/Markdown/index.tsx @@ -12,18 +12,17 @@ import { createInputOverride, createTagOverride, createVideoOverride, + MarkdownOverrideSetting, MarkdownOverrideSettings, standardOverrides, } from './libs/overrides'; import { MarkdownCheckbox } from './libs/overrides/Checkbox'; import { Details } from './libs/overrides/Details'; -import { Iframe } from './libs/overrides/Iframe'; import { MarkdownAnchor, 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'; import { markdownStyles } from './styles'; @@ -36,6 +35,20 @@ const preprocessingInstructions = createPreprocessingInstructions(); const isValidNode = () => true; +/** + * Puts each