diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 9a3a0263..86b19b26 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -29,11 +29,14 @@ jobs: - name: Installing dependencies run: npm ci - - name: Running tests - run: npm run test + - name: Running typecheck + run: npm run typecheck - name: Running lint run: npm run lint - - name: Running typecheck - run: npm run typecheck + - name: Running tests + run: npm run test + + - name: Running build + run: npm run build diff --git a/lerna.json b/lerna.json index 559ee9d7..63c8d365 100644 --- a/lerna.json +++ b/lerna.json @@ -2,5 +2,6 @@ "packages": [ "packages/**/!(dist|playground)*" ], + "npmClient": "npm", "version": "independent" } diff --git a/package.json b/package.json index d5047bc8..dfa0b6f3 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "frontend-frameworks-monorepo", "version": "1.0.0", "engines": { - "node": ">=14.0.0 <19" + "node": ">=16.0.0" }, "scripts": { "postinstall": "husky install", @@ -10,8 +10,8 @@ "build": "lerna run build", "quickstart": "lerna bootstrap && lerna run build", "test": "lerna run test", - "lint": "lerna run lint", "typecheck": "lerna run typecheck", + "lint": "lerna run lint", "build:docs": "node ./scripts/buildDocs.js", "start:docs": "live-server --open=public/docs", "update:urlgen": "npm install @cloudinary/url-gen@latest --prefix packages/html && npm install @cloudinary/url-gen@latest --prefix packages/react && npm install @cloudinary/url-gen@latest --prefix packages/vue && npm install @cloudinary/url-gen@latest --prefix packages/angular && npm install @cloudinary/url-gen@latest --prefix packages/angular/projects/cloudinary-library" diff --git a/packages/angular/package.json b/packages/angular/package.json index 658a3659..2b356ba8 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -5,6 +5,7 @@ "repository": "https://github.com/cloudinary/frontend-frameworks", "sideEffects": false, "scripts": { + "postinstall": "npm run prepare-version", "ng": "ng", "start": "ng serve", "build": "npm run prepare-version && npm run build --prefix ../html && ng build cloudinary-library --prod", diff --git a/packages/react/.eslintignore b/packages/react/.eslintignore index a804767d..a9423fac 100644 --- a/packages/react/.eslintignore +++ b/packages/react/.eslintignore @@ -2,4 +2,5 @@ build/ dist/ node_modules/ .snapshots/ -*.min.js \ No newline at end of file +*.min.js +.eslintrc.js diff --git a/packages/react/.eslintrc b/packages/react/.eslintrc deleted file mode 100644 index 2e8b1056..00000000 --- a/packages/react/.eslintrc +++ /dev/null @@ -1,34 +0,0 @@ -{ - "parser": "@typescript-eslint/parser", - "extends": [ - "standard", - "standard-react", - "plugin:@typescript-eslint/eslint-recommended" - ], - "env": { - "node": true, - "jest": true - }, - "parserOptions": { - "ecmaVersion": 2020, - "ecmaFeatures": { - "legacyDecorators": true, - "jsx": true - } - }, - "settings": { - "react": { - "version": "16" - } - }, - "rules": { - "space-before-function-paren": 0, - "react/prop-types": 0, - "react/jsx-handler-names": 0, - "react/jsx-fragments": 0, - "react/no-unused-prop-types": 0, - "import/export": 0, - "no-unused-vars": "off", - "semi": "off" - } -} diff --git a/packages/react/.eslintrc.js b/packages/react/.eslintrc.js new file mode 100644 index 00000000..52af8148 --- /dev/null +++ b/packages/react/.eslintrc.js @@ -0,0 +1,34 @@ +module.exports = { + parser: '@typescript-eslint/parser', + extends: ['standard', 'standard-react', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:storybook/recommended'], + env: { + node: true + }, + plugins: ['react', '@typescript-eslint'], + parserOptions: { + ecmaVersion: 2020, + ecmaFeatures: { + legacyDecorators: true, + jsx: true + }, + project: './tsconfig.json', + }, + settings: { + react: { + version: '16' + } + }, + rules: { + 'space-before-function-paren': 0, + 'react/prop-types': 0, + 'react/jsx-handler-names': 0, + 'react/jsx-fragments': 0, + 'react/no-unused-prop-types': 0, + 'import/export': 0, + 'no-unused-vars': 'off', + semi: 'off', + 'no-use-before-define': 'off', + '@typescript-eslint/no-use-before-define': ['error'], + '@typescript-eslint/switch-exhaustiveness-check': 'error' + } +}; diff --git a/packages/react/.gitignore b/packages/react/.gitignore index 6ebff662..056860f9 100644 --- a/packages/react/.gitignore +++ b/packages/react/.gitignore @@ -1,8 +1,9 @@ node_modules .idea dist -#example/src playground/node_modules coverage src/react-app-env.d.ts *.tgz + +*storybook.log diff --git a/packages/react/.mocharc.js b/packages/react/.mocharc.js new file mode 100644 index 00000000..185bb0b2 --- /dev/null +++ b/packages/react/.mocharc.js @@ -0,0 +1,17 @@ +process.env.TS_NODE_COMPILER_OPTIONS = JSON.stringify({ + allowJs: true, + module: 'commonjs', +}); + +process.env.TS_NODE_PREFER_TS_EXTS = 'true'; + +process.env.NODE_ENV = 'test'; +process.env.TZ = 'UTC'; + +module.exports = { + recursive: true, + parallel: true, + jobs: 3, + timeout: 20000, + require: ['tsconfig-paths/register', 'ts-node/register/transpile-only', 'jsdom-global/register'], +}; diff --git a/packages/react/.mocharc.json b/packages/react/.mocharc.json new file mode 100644 index 00000000..53bbad8d --- /dev/null +++ b/packages/react/.mocharc.json @@ -0,0 +1,8 @@ +{ + "extensions": ["ts", "tsx"], + "spec": ["**/*.test.*"], + "node-option": [ + "experimental-specifier-resolution=node", + "loader=ts-node/esm" + ] +} diff --git a/packages/react/.prettierrc b/packages/react/.prettierrc index a9646d44..8c684ca3 100644 --- a/packages/react/.prettierrc +++ b/packages/react/.prettierrc @@ -1,10 +1,12 @@ { "singleQuote": true, - "jsxSingleQuote": true, - "semi": false, + "jsxSingleQuote": false, + "semi": true, "tabWidth": 2, "bracketSpacing": true, "jsxBracketSameLine": false, + "printWidth": 120, "arrowParens": "always", - "trailingComma": "none" + "trailingComma": "none", + "proseWrap": "always" } diff --git a/packages/react/.storybook/main.ts b/packages/react/.storybook/main.ts new file mode 100644 index 00000000..8fceb88d --- /dev/null +++ b/packages/react/.storybook/main.ts @@ -0,0 +1,21 @@ +import type { StorybookConfig } from '@storybook/react-webpack5'; + +// const { build } = require("@storybook/core-server"); +// const options = {}; +// build(options).then(() => console.log("done")); + +const config: StorybookConfig = { + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + addons: [ + '@storybook/addon-webpack5-compiler-swc', + // '@storybook/addon-onboarding', + '@storybook/addon-essentials', + '@chromatic-com/storybook', + '@storybook/addon-interactions' + ], + framework: { + name: '@storybook/react-webpack5', + options: {} + } +}; +export default config; diff --git a/packages/react/.storybook/preview.ts b/packages/react/.storybook/preview.ts new file mode 100644 index 00000000..bd3c0615 --- /dev/null +++ b/packages/react/.storybook/preview.ts @@ -0,0 +1,14 @@ +import type { Preview } from '@storybook/react'; + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i + } + } + } +}; + +export default preview; diff --git a/packages/react/__tests__/AdvancedImage.test.tsx b/packages/react/__tests__/AdvancedImage.test.tsx deleted file mode 100644 index d0ae7cc4..00000000 --- a/packages/react/__tests__/AdvancedImage.test.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { AdvancedImage } from '../src' -import { CloudinaryImage } from '@cloudinary/url-gen/assets/CloudinaryImage'; -import { mount } from 'enzyme'; -import React from 'react'; - -const cloudinaryImage = new CloudinaryImage('sample', { cloudName: 'demo' }, { analytics: false }); - -describe('AdvancedImage', () => { - it('is truthy', () => { - expect(AdvancedImage).toBeTruthy() - }); - it('should create an img tag', async function() { - const component = await mount(); - expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/sample"'); - }); - - it('should add style to img component', async function() { - const component = await mount(); - expect(component.find('img').prop('style')).toStrictEqual({ opacity: '0.5' }); - }); - - it('should resolve with a cancel on unmount', function(done) { - const component = mount( - { - return new Promise((resolve) => { - htmlPluginState.cleanupCallbacks.push(() => { - resolve('canceled'); - }); - }).then((res) => { - expect(res).toBe('canceled'); - done(); - }); - }]} - />); - - component.unmount(); - }); - - it('componentDidUpdate should trigger plugin rerun', function() { - const mock = jest.fn(); - const component = mount(); - - // plugins called once - expect(mock).toHaveBeenCalledTimes(1); - - // trigger componentDidUpdate - component.setProps(''); - - expect(mock).toHaveBeenCalledTimes(2); - }); -}); diff --git a/packages/react/__tests__/AdvancedVideo.test.tsx b/packages/react/__tests__/AdvancedVideo.test.tsx deleted file mode 100644 index c51730aa..00000000 --- a/packages/react/__tests__/AdvancedVideo.test.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import { AdvancedVideo } from '../src'; -import { CloudinaryImage, CloudinaryVideo } from '@cloudinary/url-gen'; -import { mount } from 'enzyme'; -import React from 'react'; -import { auto, theora, vp9 } from '@cloudinary/url-gen/qualifiers/videoCodec'; -import { videoCodec } from '@cloudinary/url-gen/actions/transcode'; - -const cloudinaryImage = new CloudinaryImage('sample', { cloudName: 'demo' }, { analytics: false }); -const cloudinaryVideo = new CloudinaryVideo('sample', { cloudName: 'demo' }, { analytics: false }); -const cloudinaryVideoWithExtension = new CloudinaryVideo('sample.mp4', { cloudName: 'demo' }, { analytics: false }); -const cloudinaryVideoWithAnalytics = new CloudinaryVideo('sample', { cloudName: 'demo' }, { analytics: true }); - -describe('AdvancedVideo', () => { - it('should render video with default sources', function (done) { - const component = mount(); - setTimeout(() => { - expect(component.html()).toContain( - ''); - done(); - }, 0);// one tick - }); - - it('should generate url sources with correct placement of extension and url analytics', function (done) { - const component = mount(); - setTimeout(() => { - expect(component.html()).toContain( - 'https://res.cloudinary.com/demo/video/upload/sample.webm?_a='); - expect(component.html()).toContain( - 'https://res.cloudinary.com/demo/video/upload/sample.ogv?_a='); - expect(component.html()).toContain( - 'https://res.cloudinary.com/demo/video/upload/sample.mp4?_a='); - done(); - }, 0);// one tick - }); - - it('should render video with input sources', function (done) { - const sources = [ - { - type: 'mp4', - codecs: ['vp8', 'vorbis'], - transcode: videoCodec(auto()) - }, - { - type: 'webm', - codecs: ['avc1.4D401E', 'mp4a.40.2'], - transcode: videoCodec(vp9()) - }]; - - const component = mount(); - - setTimeout(() => { - expect(component.html()).toContain( - ''); - done(); - }, 0);// one tick - }); - - it('should render video with input sources when using useFetchFormat', function (done) { - const sources = [ - { - type: 'mp4', - codecs: ['vp8', 'vorbis'], - transcode: videoCodec(auto()) - }, - { - type: 'webm', - codecs: ['avc1.4D401E', 'mp4a.40.2'], - transcode: videoCodec(vp9()) - }, - { - type: 'ogv', - codecs: ['theora'], - transcode: videoCodec(theora()) - }]; - - const component = mount(); - - setTimeout(() => { - expect(component.html()).toContain( - ''); - done(); - }, 0);// one tick - }); - - it('should pass video attributes', function (done) { - const component = mount(); - - setTimeout(() => { - expect(component.html()).toContain('loop=""'); - expect(component.html()).not.toContain('playsinline'); - expect(component.html()).toContain('muted=""'); - expect(component.html()).toContain('autoplay=""'); - expect(component.html()).toContain('controls=""'); - done(); - }, 1000);// one tick - }); - - it('should contain poster', function (done) { - const component = mount(); - - setTimeout(() => { - expect(component.html()).toContain('poster="www.example.com"'); - done(); - }, 0);// one tick - }); - - it('should contain poster when "auto" is passed as cldPoster', function (done) { - const component = mount(); - - setTimeout(() => { - expect(component.html()).toContain('poster="https://res.cloudinary.com/demo/video/upload/q_auto/f_jpg/so_auto/sample"'); - done(); - }, 0);// one tick - }); - - it('should contain poster when cloudinary image is passed as cldPoster', function (done) { - const component = mount(); - - setTimeout(() => { - expect(component.html()).toContain('poster="https://res.cloudinary.com/demo/image/upload/sample"'); - done(); - }, 0);// one tick - }); - - it('should simulate onPlay event', function (done) { - const mockCallBack = jest.fn(); - - const component = mount(); - setTimeout(() => { - component.find('video').simulate('play'); - expect(mockCallBack.mock.calls.length).toEqual(1); - done(); - }, 0);// one tick - }); - - it('should simulate onLoadStart event', function (done) { - const mockCallBack = jest.fn(); - - const component = mount(); - setTimeout(() => { - component.find('video').simulate('loadstart'); - expect(mockCallBack.mock.calls.length).toEqual(1); - done(); - }, 0);// one tick - }); - - it('should simulate onEnded event', function (done) { - const mockCallBack = jest.fn(); - - const component = mount(); - setTimeout(() => { - component.find('video').simulate('ended'); - expect(mockCallBack.mock.calls.length).toEqual(1); - done(); - }, 0);// one tick - }); - - it('should simulate onError event', function (done) { - const mockCallBack = jest.fn(); - - const component = mount(); - setTimeout(() => { - component.find('video').simulate('error'); - expect(mockCallBack.mock.calls.length).toEqual(1); - done(); - }, 0);// one tick - }); - - it('should simulate onPlaying event', function (done) { - const mockCallBack = jest.fn(); - - const component = mount(); - setTimeout(() => { - component.find('video').simulate('playing'); - expect(mockCallBack.mock.calls.length).toEqual(1); - done(); - }, 0);// one tick - }); - - it('Should support forwarding innerRef to underlying video element', () => { - const myRef = React.createRef(); - mount(); - const video: any = myRef.current; - - ['play', 'pause', 'canPlayType', 'addTextTrack'].forEach((func) => { - expect(typeof video[func]).toBe('function'); - }); - }); -}); diff --git a/packages/react/__tests__/accessibility.test.tsx b/packages/react/__tests__/accessibility.test.tsx deleted file mode 100644 index 059c286a..00000000 --- a/packages/react/__tests__/accessibility.test.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { AdvancedImage, accessibility } from '../src'; -import { CloudinaryImage } from '@cloudinary/url-gen/assets/CloudinaryImage'; -import { mount } from 'enzyme'; -import React from 'react'; - -const cloudinaryImage = new CloudinaryImage('sample', { cloudName: 'demo' }, { analytics: false }); - -describe('accessibility', () => { - it('should apply default', function (done) { - const component = mount(); - setTimeout(() => { - expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/co_black,e_colorize:70/sample"'); - done(); - }, 0);// one tick - }); - - it('should apply darkmode', function () { - const component = mount(); - setTimeout(() => { - expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/co_black,e_colorize:70/sample"'); - }, 0);// one tick - }); - - it('should apply brightmode', function (done) { - const component = mount(); - setTimeout(() => { - expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/co_white,e_colorize:40/sample"'); - done(); - }, 0);// one tick - }); - - it('should apply monochrome', function (done) { - const component = mount(); - setTimeout(() => { - expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/e_grayscale/sample"'); - done(); - }, 0);// one tick - }); - - it('should apply colorblind', function (done) { - const component = mount(); - setTimeout(() => { - expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/e_assist_colorblind/sample"'); - done(); - }, 0);// one tick - }); - - it('should default if supplied with incorrect mode', function (done) { - const component = mount(); - setTimeout(() => { - expect(component.html()) - .toBe(''); - done(); - }, 0);// one tick - }); -}); diff --git a/packages/react/__tests__/analytics-ssr.test.tsx b/packages/react/__tests__/analytics-ssr.test.tsx deleted file mode 100644 index 1d200454..00000000 --- a/packages/react/__tests__/analytics-ssr.test.tsx +++ /dev/null @@ -1,35 +0,0 @@ -/** - * @jest-environment node - */ -import { AdvancedImage } from '../src' -import { CloudinaryImage } from '@cloudinary/url-gen/assets/CloudinaryImage' -import React from 'react' -import { SDKAnalyticsConstants } from '../src/internal/SDKAnalyticsConstants' -// @ts-ignore -import { version } from '../package.json' -import { renderToString } from 'react-dom/server' - -const cloudinaryImage = new CloudinaryImage('sample', { cloudName: 'demo' }) - -describe('ssr analytics', () => { - beforeEach(() => { - SDKAnalyticsConstants.sdkSemver = '1.0.0' - SDKAnalyticsConstants.techVersion = '10.2.5' - }) - - afterEach(() => { - SDKAnalyticsConstants.sdkSemver = version - SDKAnalyticsConstants.techVersion = React.version - }) - it('creates an img with analytics', function (done) { - const ElementImageHtml = renderToString( - - ) - setTimeout(() => { - expect(ElementImageHtml).toContain( - 'https://res.cloudinary.com/demo/image/upload/sample?_a=DAJAABDSZAA0' - ) - done() - }, 0) // one tick - }) -}) diff --git a/packages/react/__tests__/analytics.test.tsx b/packages/react/__tests__/analytics.test.tsx deleted file mode 100644 index 96f468d6..00000000 --- a/packages/react/__tests__/analytics.test.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { AdvancedImage } from '../src'; -import { CloudinaryImage } from '@cloudinary/url-gen/assets/CloudinaryImage'; -import { mount } from 'enzyme'; -import React from 'react'; -import { SDKAnalyticsConstants } from '../src/internal/SDKAnalyticsConstants'; -// @ts-ignore -import { version } from '../package.json'; - -const cloudinaryImage = new CloudinaryImage('sample', { cloudName: 'demo' }); - -describe('analytics', () => { - beforeEach(() => { - SDKAnalyticsConstants.sdkSemver = '1.0.0'; - SDKAnalyticsConstants.techVersion = '10.2.5'; - }); - - afterEach(() => { - SDKAnalyticsConstants.sdkSemver = version; - SDKAnalyticsConstants.techVersion = React.version; - }); - it('creates an img with analytics', function (done) { - const component = mount(); - setTimeout(() => { - expect(component.html()).toEqual(''); - done(); - }, 0);// one tick - }); -}); diff --git a/packages/react/__tests__/lazyload.test.tsx b/packages/react/__tests__/lazyload.test.tsx deleted file mode 100644 index a56582ab..00000000 --- a/packages/react/__tests__/lazyload.test.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { AdvancedImage, lazyload } from '../src' -import { CloudinaryImage } from '@cloudinary/url-gen/assets/CloudinaryImage'; -import { mount } from 'enzyme'; -import React from 'react'; -import { testWithMockedIntersectionObserver } from '../../../testUtils/testWithMockedIntersectionObserver' - -const cloudinaryImage = new CloudinaryImage('sample', { cloudName: 'demo' }, { analytics: false }); - -describe('lazy-load', () => { - it('should not have src pre-scroll', async function() { - const component = await mount( -
-
- -
- ); - // no src pre scroll - expect(component.html()).toBe('
'); - }); - - it('should have src when in view', function(done) { - const elm = document.createElement('img'); - testWithMockedIntersectionObserver((mockIntersectionEvent: ({}) => void) => { - const component = mount(); - mockIntersectionEvent([{ isIntersecting: true, target: component.getDOMNode() }]); - setTimeout(() => { - expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/sample"'); - done(); - }, 0);// one tick - }); - }); - - it('should set lazyload root margin and threshold', function(done) { - const elm = document.createElement('img'); - testWithMockedIntersectionObserver((mockIntersectionEvent: ({}) => void) => { - // @ts-ignore - const component = mount(); - mockIntersectionEvent([{ isIntersecting: true, target: component.getDOMNode() }]); - setTimeout(() => { - expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/sample"'); - done(); - }, 0);// one tick - }); - }); -}); diff --git a/packages/react/__tests__/package.tests.tsx b/packages/react/__tests__/package.tests.tsx deleted file mode 100644 index 7a176391..00000000 --- a/packages/react/__tests__/package.tests.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import {execSync} from "child_process"; -import fs from 'fs'; -import * as path from "path"; - -describe('tests for the package output', () => { - it('ensures that the pacakge contains the right files', () => { - execSync('npm run build'); - const dirContents = fs.readdirSync(path.resolve(__dirname, '../dist')); - - expect(dirContents.includes('index.js')).toBe(true); - expect(dirContents.includes('index.umd.js')).toBe(true); - expect(dirContents.includes('index.cjs.js')).toBe(true); - expect(dirContents.includes('index.d.ts')).toBe(true); - expect(dirContents.includes('package.json')).toBe(true); - }); -}); diff --git a/packages/react/__tests__/placeholder.test.tsx b/packages/react/__tests__/placeholder.test.tsx deleted file mode 100644 index e9a90c84..00000000 --- a/packages/react/__tests__/placeholder.test.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { AdvancedImage, placeholder } from '../src' -import { CloudinaryImage } from '@cloudinary/url-gen/assets/CloudinaryImage'; -import { PLACEHOLDER_IMAGE_OPTIONS } from '../../html/src/utils/internalConstants'; -import { mount } from 'enzyme'; -import React from 'react'; -import { sepia } from '@cloudinary/url-gen/actions/effect'; - -describe('placeholder', () => { - let cloudinaryImage: CloudinaryImage; - - const mockImage = { - src: null, - onload: () => {}, - onerror: () => {} - }; - beforeEach(() => { - // @ts-ignore - window.Image = function() { return mockImage }; - cloudinaryImage = new CloudinaryImage('sample', { cloudName: 'demo' }, { analytics: false }); - }); - it('should apply default', function (done) { - const component = mount(); - setTimeout(() => { - expect(component.html()).toContain(`src="https://res.cloudinary.com/demo/image/upload/${PLACEHOLDER_IMAGE_OPTIONS.vectorize}/sample"`); - done(); - }, 0);// one tick - }); - - it("should apply 'vectorize'", function () { - const component = mount(); - setTimeout(() => { - expect(component.html()).toContain(`src="https://res.cloudinary.com/demo/image/upload/${PLACEHOLDER_IMAGE_OPTIONS.vectorize}/sample"`); - }, 0);// one tick - }); - - it('should apply pixelate', function (done) { - const component = mount(); - setTimeout(() => { - expect(component.html()).toContain(`src="https://res.cloudinary.com/demo/image/upload/${PLACEHOLDER_IMAGE_OPTIONS.pixelate}/sample"`); - done(); - }, 0);// one tick - }); - - it('should apply blur', function (done) { - const component = mount(); - setTimeout(() => { - expect(component.html()).toContain(`src="https://res.cloudinary.com/demo/image/upload/${PLACEHOLDER_IMAGE_OPTIONS.blur}/sample"`); - done(); - }, 0);// one tick - }); - - it('should apply predominant-color', function (done) { - const component = mount(); - setTimeout(() => { - expect(component.html()).toContain(`src="https://res.cloudinary.com/demo/image/upload/${PLACEHOLDER_IMAGE_OPTIONS['predominant-color']}/sample"`); - done(); - }, 0);// one tick - }); - - it('should default if supplied with incorrect mode', function (done) { - const component = mount(); - setTimeout(() => { - expect(component.html()).toContain(`src="https://res.cloudinary.com/demo/image/upload/${PLACEHOLDER_IMAGE_OPTIONS.vectorize}/sample"`); - done(); - }, 0);// one tick - }); - - it('should append placeholder transformation', function (done) { - cloudinaryImage.effect(sepia()); - const component = mount(); - setTimeout(() => { - expect(component.html()).toContain(`src="https://res.cloudinary.com/demo/image/upload/e_sepia/${PLACEHOLDER_IMAGE_OPTIONS.vectorize}/sample"`); - done(); - }, 0);// one tick - }); - - /* - This test is built with two setTimouts since the placeholder plugin makes use of two promises. - The placeholder image loads first. Once it loads, the promise is resolved and the - larger image will load. Once the larger image loads, promised and plugin is resolved. - */ - it('should not fail error', function (done) { - const component = mount(); - setTimeout(() => { - // @ts-ignore - component.getDOMNode().onload(); // simulate element onload - setTimeout(() => { - mockImage.onerror(); // simulate image onerror - expect(mockImage.src).toBe('https://res.cloudinary.com/demo/image/upload/sample'); - done(); - }) - }, 5);// one tick - }); -}); diff --git a/packages/react/__tests__/responsive.test.tsx b/packages/react/__tests__/responsive.test.tsx deleted file mode 100644 index 9396c6b0..00000000 --- a/packages/react/__tests__/responsive.test.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { AdvancedImage, responsive } from '../src' -import { CloudinaryImage } from '@cloudinary/url-gen/assets/CloudinaryImage'; -import { mount } from 'enzyme'; -import React from 'react'; -import { ResponsiveHelper } from './testUtils/responsiveHelperWrapper'; -import { crop } from '@cloudinary/url-gen/actions/resize'; -import { dispatchResize } from './testUtils/dispatchResize'; -import FakeTimers from '@sinonjs/fake-timers' - -const cloudinaryImage = new CloudinaryImage('sample', { cloudName: 'demo' }, { analytics: false }); - -describe('responsive', () => { - let clock:any; - beforeEach(() => { - clock = FakeTimers.install() - }); - afterEach(() => { - clock.uninstall() - }); - - it('should apply initial container width (default 250)', async function () { - const component = mount( - - - ); - - window.dispatchEvent(new Event('resize')); - clock.tick(100); // timeout for debounce - - const el = component.find('#wrapper').getDOMNode(); - expect(el.clientWidth).toBe(250); - }); - - - it('Should respect single step and ignore default width of 250 (When Step < Width)', async function () { - const component = mount( - - - ); - - clock.tick(100); // timeout for debounce - - // Output is exactly 300 due to internal rounding: ROUND_UP(CONTAINER / STEP) * STEP - // When STEP < CONTAINER, output is always a multiplication of STEP - expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/c_scale,w_300/sample"'); - }); - - - it('Should respect single step and ignore default width of 250 (When Step > Width)', async function () { - const component = mount( - - - ); - - clock.tick(100); // timeout for debounce - - // Output is exactly 251 due to internal rounding: ROUND_UP(CONTAINER / STEP) * STEP - // When STEP > CONTAINER, output is always STEP. - expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/c_scale,w_251/sample"'); - }); - - it('Should respect steps and ignore default width of 250', async function () { - const component = mount( - - - ); - - clock.tick(100); // timeout for debounce - - // Output is closest number to parentElement, never exceeding the width of the max step ) - expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/c_scale,w_30/sample"'); - }); - - it('should update container width on window resize', function () { - const component = mount( - - - ); - - const el = dispatchResize(component, 100); - clock.tick(100); // timeout for debounce - expect(el.clientWidth).toBe(100); - }); - - it('should step by the 100th', async function () { - const component = mount( - - - ); - - await clock.tickAsync(0); // one tick - window.dispatchEvent(new Event('resize')); - await clock.tickAsync(100); // timeout for debounce - expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/c_scale,w_300/sample"'); - }); - - it('should step by breakpoints', async function () { - const component = mount( - - - ); - - await clock.tickAsync(0); // one tick - window.dispatchEvent(new Event('resize')); - await clock.tickAsync(100); // timeout for debounce - - expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/c_scale,w_800/sample"'); - - // simulate resize to 975 - await clock.tickAsync(0); // one tick - dispatchResize(component, 975); - await clock.tickAsync(100); // timeout for debounce - - expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/c_scale,w_1000/sample"'); - }); - - it('should not resize to larger than provided breakpoints', async function () { - const component = mount( - - - ); - - await clock.tickAsync(0); // one tick - dispatchResize(component, 4000); - await clock.tickAsync(100); // timeout for debounce - - expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/c_scale,w_3000/sample"'); - }); - - it('should handle unordered breakpoints', async function () { - const component = mount( - - - ); - - await clock.tickAsync(0); // one tick - dispatchResize(component, 5000); - await clock.tickAsync(100); // timeout for debounce - - expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/c_scale,w_3000/sample"'); - }); - - it('should append to existing transformation', async function () { - cloudinaryImage.resize(crop('500')); - - const component = mount( - - - ); - - await clock.tickAsync(0); // one tick - window.dispatchEvent(new Event('resize')); - await clock.tickAsync(100); // timeout for debounce - - expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/c_crop,w_500/c_scale,w_250/sample"'); - }); -}); diff --git a/packages/react/__tests__/ssr.test.tsx b/packages/react/__tests__/ssr.test.tsx deleted file mode 100644 index e954b408..00000000 --- a/packages/react/__tests__/ssr.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/** - * @jest-environment node - */ -import { AdvancedImage, placeholder, responsive, accessibility, lazyload } from '../src' -import { CloudinaryImage } from '@cloudinary/url-gen/assets/CloudinaryImage'; -import React from 'react'; -import { renderToString } from 'react-dom/server' - -const cloudinaryImage = new CloudinaryImage('sample', { cloudName: 'demo' }, { analytics: false }); - -describe('ssr', () => { - it('should render accessibility transformation with accessibility', function (done) { - const ElementImageHtml = renderToString(); - setTimeout(() => { - expect(ElementImageHtml).toContain('https://res.cloudinary.com/demo/image/upload/co_black,e_colorize:70/sample'); - done(); - }, 0);// one tick - }); - - it('should render the placeholder image in SSR', function (done) { - const ElementImageHtml = renderToString(); - setTimeout(() => { - expect(ElementImageHtml).toContain('https://res.cloudinary.com/demo/image/upload/e_vectorize/q_auto/f_svg/sample'); - done(); - }, 0);// one tick - }); - - it('should render original image when responsive', function (done) { - const ElementImageHtml = renderToString(); - setTimeout(() => { - expect(ElementImageHtml).toContain('https://res.cloudinary.com/demo/image/upload/sample'); - done(); - }, 0);// one tick - }); - - it('should render original image when lazy loaded', function (done) { - const ElementImageHtml = renderToString(); - setTimeout(() => { - expect(ElementImageHtml).toContain('https://res.cloudinary.com/demo/image/upload/sample'); - done(); - }, 0);// one tick - }); -}); diff --git a/packages/react/__tests__/testUtils/dispatchResize.tsx b/packages/react/__tests__/testUtils/dispatchResize.tsx deleted file mode 100644 index 04406f24..00000000 --- a/packages/react/__tests__/testUtils/dispatchResize.tsx +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Util function used to dispatch a resize event used in the responsive tests - * @param component - * @param resizeValue - */ -export function dispatchResize(component: any, resizeValue:number){ - const el = component.find('#wrapper').getDOMNode(); - Object.defineProperty(el, 'clientWidth', {value: resizeValue, configurable: true}); - window.dispatchEvent(new Event('resize')); - - return el; -} diff --git a/packages/react/__tests__/testUtils/responsiveHelperWrapper.tsx b/packages/react/__tests__/testUtils/responsiveHelperWrapper.tsx deleted file mode 100644 index e86ad38c..00000000 --- a/packages/react/__tests__/testUtils/responsiveHelperWrapper.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React, {useEffect, useRef, useState} from 'react'; - -/** - * Simulates clientWidth - * Initializes clientWidth with 250 - */ -export function ResponsiveHelper(props: { children: React.ReactNode }) { - const ref = useRef(null); - const [refElement, setRefElement] = useState(false) - useEffect(() => { - const parentElement = ref.current as unknown as HTMLElement; - Object.defineProperty(parentElement, 'clientWidth', {value: 250, configurable: true}); - // @ts-ignore - setRefElement(ref.current); - }, []); - return
- {refElement && props.children} -
-} diff --git a/packages/react/babel.config.js b/packages/react/babel.config.js deleted file mode 100644 index 597a9815..00000000 --- a/packages/react/babel.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - presets: [ - '@babel/preset-env', - '@babel/preset-react', - '@babel/preset-typescript' - ] -}; diff --git a/packages/react/esbuild.mjs b/packages/react/esbuild.mjs new file mode 100644 index 00000000..3021173b --- /dev/null +++ b/packages/react/esbuild.mjs @@ -0,0 +1,11 @@ +import esbuild from 'esbuild' + +await esbuild.build({ + outfile: `./dist/index.js`, + entryPoints: ['src/index.ts'], + tsconfig: './tsconfig.json', + bundle: true, + platform: 'neutral', + format: 'esm', + packages: 'external', +}) diff --git a/packages/react/index.html b/packages/react/index.html new file mode 100644 index 00000000..ec421f7f --- /dev/null +++ b/packages/react/index.html @@ -0,0 +1,12 @@ + + + + + + + + +

Welcome to Cloudinary React SDK

+
+ + diff --git a/packages/react/jest.config.json b/packages/react/jest.config.json index 700a234c..e69de29b 100644 --- a/packages/react/jest.config.json +++ b/packages/react/jest.config.json @@ -1,50 +0,0 @@ -{ - "preset": "ts-jest", - "setupFiles": ["./setup.jest.ts"], - "bail": true, - "collectCoverageFrom": [ - "/src/**/*.ts" - ], - "transform": { - "^.+\\.(js|ts|tsx)$": "ts-jest" - }, - "testEnvironment": "jsdom", - "moduleFileExtensions": [ - "ts", - "tsx", - "js", - "jsx", - "json", - "node" - ], - "modulePaths": [ - "node_modules", - "/src" - ], - "testPathIgnorePatterns": [ - "./__tests__/testUtils" - ], - "moduleNameMapper": { - "^@cloudinary/html": "/../html/src" - }, - "snapshotSerializers": ["enzyme-to-json/serializer"], - "coverageThreshold": { - "global": { - "branches": 95, - "functions": 95, - "lines": 95, - "statements": 95 - } - }, - "globals": { - "ts-jest": { - "diagnostics": false - } - }, - "transformIgnorePatterns": [ - "/node_modules\/(?!@cloudinary/(html|url-gen|transformation-builder-sdk))(.*)" - ], - "setupFilesAfterEnv": [ - "./src/setupTests.js" - ] -} diff --git a/packages/react/package.json b/packages/react/package.json index 2045b622..c2dcf8e1 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,57 +1,52 @@ { "name": "@cloudinary/react", - "version": "1.13.1", + "version": "2.0.0-alpha-1", "description": "Cloudinary React SDK", "author": "cloudinary", "license": "MIT", "repository": "https://github.com/cloudinary/frontend-frameworks", - "main": "dist/index.umd.js", - "module": "dist/index.js", - "types": "dist/index.d.ts", + "main": "dist/index.js", "files": [ "package.json", "dist" ], "sideEffects": false, "engines": { - "node": ">=10" + "node": ">=18" }, "scripts": { - "build": "npm run build --prefix ../html && tsc && rollup -c && cp package.json ./dist", - "test": "jest --config jest.config.json", - "test-coverage": "jest --coverage" + "build": "node ./esbuild.mjs && tsc", + "lint": "eslint ./src", + "typecheck": "tsc --noEmit --skipLibCheck --project ./tsconfig.json", + "test": "mocha src/**/*.test.*", + "test-coverage": "exit 1", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" }, "peerDependencies": { + "@cloudinary/url-gen": "^1.16.0", "react": "^16.3.0 || ^17.0.0 || ^18.0.0" }, "devDependencies": { - "@babel/preset-env": "^7.12.10", - "@babel/preset-typescript": "^7.12.7", - "@cloudinary/html": "^1.0.1", - "@cloudinary/url-gen": "^1.21.0", - "@rollup/plugin-commonjs": "^21.0.1", - "@rollup/plugin-json": "^4.1.0", - "@rollup/plugin-node-resolve": "^13.1.3", - "@rollup/plugin-replace": "^3.0.1", - "@rollup/plugin-typescript": "^8.3.0", - "@sinonjs/fake-timers": "^6.0.1", - "@testing-library/jest-dom": "^4.2.4", - "@types/enzyme": "^3.10.8", - "@types/enzyme-adapter-react-16": "^1.0.6", - "@types/jest": "^27.5.2", + "@chromatic-com/storybook": "^3.2.2", + "@storybook/addon-essentials": "^8.4.7", + "@storybook/addon-interactions": "^8.4.7", + "@storybook/addon-onboarding": "^8.4.7", + "@storybook/addon-webpack5-compiler-swc": "^1.0.5", + "@storybook/blocks": "^8.4.7", + "@storybook/core-server": "^8.4.7", + "@storybook/react": "^8.4.7", + "@storybook/react-webpack5": "^8.4.7", + "@storybook/test": "^8.4.7", + "@types/mocha": "^10.0.10", "@types/node": "^12.12.38", "@types/react": "^16.9.27", "@types/react-dom": "^16.9.7", - "@types/sinonjs__fake-timers": "^6.0.2", - "@typescript-eslint/eslint-plugin": "^2.26.0", - "@typescript-eslint/parser": "^2.26.0", - "babel-eslint": "^10.0.3", - "cheerio": "1.0.0-rc.12", - "cross-env": "^7.0.2", - "enzyme": "^3.11.0", - "enzyme-adapter-react-16": "^1.15.5", - "enzyme-to-json": "^3.6.1", - "eslint": "^6.8.0", + "@typescript-eslint/eslint-plugin": "^8.18.0", + "@typescript-eslint/parser": "^8.18.0", + "assert": "^2.1.0", + "esbuild": "^0.24.0", + "eslint": "^8.57.1", "eslint-config-prettier": "^6.7.0", "eslint-config-standard": "^14.1.0", "eslint-config-standard-react": "^9.2.0", @@ -61,18 +56,16 @@ "eslint-plugin-promise": "^4.2.1", "eslint-plugin-react": "^7.17.0", "eslint-plugin-standard": "^4.0.1", + "eslint-plugin-storybook": "^0.11.1", "gh-pages": "^2.2.0", - "jest": "27.5.1", - "npm-run-all": "^4.1.5", - "prettier": "^2.0.4", + "jsdom": "^21.1.0", + "jsdom-global": "^3.0.2", + "mocha": "^10.8.2", + "prettier": "^3.4.1", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-scripts": "^3.4.1", - "rollup": "^2.66.1", - "ts-jest": "^27.1.5", - "typescript": "^3.7.5" - }, - "dependencies": { - "@cloudinary/html": "^1.13.1" + "storybook": "^8.4.7", + "ts-node": "^10.9.2", + "typescript": "^5.7.2" } } diff --git a/packages/react/setup.jest.ts b/packages/react/setup.jest.ts index bcc44a6d..e69de29b 100644 --- a/packages/react/setup.jest.ts +++ b/packages/react/setup.jest.ts @@ -1,7 +0,0 @@ -// const { TextDecoder, TextEncoder, ReadableStream } = require('node:util'); -// -// globalThis.TextDecoder = TextDecoder; -// globalThis.TextEncoder = TextEncoder; -// globalThis.ReadableStream = ReadableStream; - -export {}; diff --git a/packages/react/src/AdvancedImage.tsx b/packages/react/src/AdvancedImage.tsx deleted file mode 100644 index 434745ec..00000000 --- a/packages/react/src/AdvancedImage.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import React from 'react'; -import { CloudinaryImage } from '@cloudinary/url-gen/assets/CloudinaryImage'; - -import { - HtmlImageLayer, - Plugins, - isBrowser, - serverSideSrc, - cancelCurrentlyRunningPlugins -} from '@cloudinary/html' -import { SDKAnalyticsConstants } from './internal/SDKAnalyticsConstants'; - -interface ImgProps { - cldImg: CloudinaryImage; - plugins?: Plugins; - [x: string]: any; -} - -/** - * @mixin ReactSDK - * @description The Cloudinary React SDK contains components like \ to easily render your media assets from Cloudinary. - * The SDK also comes with support for optional JS plugins that make the components smart, with features like lazy loading, placeholder, accessibility & responsiveness. - * - * @example - * - * Please note that the order of the plugins is important. See {@link https://cloudinary.com/documentation/sdks/js/frontend-frameworks/index.html#plugin-order|Plugin Order} for more details. - * - * // Example - * import {CloudinaryImage} from "@cloudinary/url-gen/assets/CloudinaryImage"; - * import { - * AdvancedImage, - * accessibility, - * responsive, - * lazyload, - * placeholder - * } from '@cloudinary/react'; - * - * const App = () => { - * - * const myCld = new Cloudinary({ cloudName: 'demo'}); - * let img = myCld().image('sample'); - * - * return ( - *
- *
- * - *
- * ) - * }; - * - * - * - * - * - */ - -/** - * @memberOf ReactSDK - * @type {Component} - * @description The Cloudinary image component. - * @prop {CloudinaryImage} cldImg Generated by @cloudinary/url-gen - * @prop {Plugins} plugins Advanced image component plugins accessibility(), responsive(), lazyload(), placeholder() - */ -class AdvancedImage extends React.Component { - imageRef: React.RefObject; - htmlLayerInstance: HtmlImageLayer; - - constructor(props: ImgProps) { - super(props); - this.imageRef = React.createRef(); - } - - /** - * On mount, creates a new HTMLLayer instance and initializes with ref to img element, - * user generated cloudinaryImage and the plugins to be used. - */ - componentDidMount() { - this.htmlLayerInstance = new HtmlImageLayer( - this.imageRef.current, - this.props.cldImg, - this.props.plugins, - SDKAnalyticsConstants - ) - } - - /** - * On update, we cancel running plugins and update image instance with the state of user - * cloudinaryImage and the state of plugins. - */ - componentDidUpdate() { - cancelCurrentlyRunningPlugins(this.htmlLayerInstance.htmlPluginState); - // call html layer to update the dom again with plugins and reset toBeCanceled - this.htmlLayerInstance.update(this.props.cldImg, this.props.plugins, SDKAnalyticsConstants) - } - - /** - * On unmount, we cancel the currently running plugins, and destroy the html layer instance - */ - componentWillUnmount() { - // Safely cancel running events on unmount. - cancelCurrentlyRunningPlugins(this.htmlLayerInstance.htmlPluginState); - this.htmlLayerInstance.unmount(); - } - - render() { - const { - cldImg, - plugins, - ...otherProps // Assume any other props are for the base element - } = this.props; - if (isBrowser()) { // On client side render - return - } else { // on server side render - const src = serverSideSrc( - this.props.plugins, - this.props.cldImg, - SDKAnalyticsConstants - ); - return - } - } -} - -export { AdvancedImage }; diff --git a/packages/react/src/AdvancedVideo.tsx b/packages/react/src/AdvancedVideo.tsx deleted file mode 100644 index c8ef4ddf..00000000 --- a/packages/react/src/AdvancedVideo.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import React, { Component, createRef, EventHandler, HTMLAttributes, MutableRefObject, SyntheticEvent } from 'react'; -import { CloudinaryImage, CloudinaryVideo } from '@cloudinary/url-gen'; - -import { - HtmlVideoLayer, - Plugins, - VideoPoster, - VideoSources, - cancelCurrentlyRunningPlugins -} from '@cloudinary/html'; - -type ReactEventHandler = EventHandler>; - -interface VideoProps extends HTMLAttributes{ - cldVid: CloudinaryVideo, - cldPoster?: VideoPoster, - plugins?: Plugins, - sources?: VideoSources, - innerRef?: ((instance: any) => void) | MutableRefObject | null - useFetchFormat?: boolean, - - // supported video attributes - controls?: boolean - loop?: boolean, - muted?: boolean, - poster?: string, - preload?: string, - autoPlay?: boolean, - playsInline?: boolean - - // supported video events - onPlay?: ReactEventHandler, - onLoadStart?: ReactEventHandler, - onPlaying?: ReactEventHandler, - onError?: ReactEventHandler, - onEnded?: ReactEventHandler -} - -const VIDEO_ATTRIBUTES_KEYS: string[] = ['controls', 'loop', 'muted', 'poster', 'preload', 'autoplay', 'playsinline']; - -/** - * @memberOf ReactSDK - * @type {Component} - * @description The Cloudinary video component. - * @prop {CloudinaryVideo} transformation Generated by @cloudinary/url-gen - * @prop {Plugins} plugins Advanced image component plugins lazyload() - * @prop videoAttributes Optional attributes include controls, loop, muted, poster, preload, autoplay - * @prop videoEvents Optional video events include play, loadstart, playing, error, ended - * @prop {VideoSources} sources Optional sources to generate - * @example - * - * Using custom defined resources. - * - * const vid = new CloudinaryVideo('dog', {cloudName: 'demo'}); - * const videoEl = useRef(); - * const sources = [ - * { - * type: 'mp4', - * codecs: ['vp8', 'vorbis'], - * transcode: videoCodec(auto()) - * }, - * { - * type: 'webm', - * codecs: ['avc1.4D401E', 'mp4a.40.2'], - * videoCodec: videoCodec(auto()) - * }]; - * - * return - */ -class AdvancedVideo extends Component { - videoRef: MutableRefObject - htmlVideoLayerInstance: HtmlVideoLayer; - - constructor(props: VideoProps) { - super(props); - this.videoRef = createRef(); - this.attachRef = this.attachRef.bind(this); - } - - /** - * On mount, creates a new HTMLVideoLayer instance and initializes with ref to video element, - * user generated cloudinaryVideo and the plugins to be used. - */ - componentDidMount() { - this.htmlVideoLayerInstance = new HtmlVideoLayer( - this.videoRef && this.videoRef.current, - this.props.cldVid, - this.props.sources, - this.props.plugins, - this.getVideoAttributes(), - this.props.cldPoster, - { - useFetchFormat: this.props.useFetchFormat - } - ) - } - - /** - * On update, we cancel running plugins and update the video instance if the src - * was changed. - */ - componentDidUpdate() { - cancelCurrentlyRunningPlugins(this.htmlVideoLayerInstance.htmlPluginState); - // call html layer to update the dom again with plugins and reset toBeCanceled - this.htmlVideoLayerInstance.update( - this.props.cldVid, - this.props.sources, - this.props.plugins, - this.getVideoAttributes(), - this.props.cldPoster - ) - } - - /** - * On unmount, we cancel the currently running plugins. - */ - componentWillUnmount() { - // safely cancel running events on unmount - cancelCurrentlyRunningPlugins(this.htmlVideoLayerInstance.htmlPluginState) - } - - /** - * Returns video attributes. - */ - getVideoAttributes() { - const result = {}; - VIDEO_ATTRIBUTES_KEYS.forEach((key: string) => { - if (key in this.props) { - result[key] = this.props[key]; - } - }); - - return result; - } - - /** - * Attach both this.videoRef and props.innerRef as ref to the given element. - * @param element - the element to attach a ref to - */ - attachRef(element: HTMLVideoElement) { - this.videoRef.current = element; - const { innerRef } = this.props; - - if (innerRef) { - if (innerRef instanceof Function) { - innerRef(element); - } else { - innerRef.current = element; - } - } - }; - - render() { - const { - cldVid, - cldPoster, - plugins, - sources, - innerRef, - useFetchFormat, - ...videoAttrs // Assume any other props are for the base element - } = this.props; - - return