From 4aab64d6c770d21e5b12dce2577d218de55083ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Wed, 30 Jun 2021 12:02:49 +0200 Subject: [PATCH 01/76] rewrite it with hooks --- .eslintrc.js | 3 + src/eventNames.js | 8 - src/index.js | 395 ++++++++++++++++++------------------- src/loadSdk.js | 8 +- test/util/createYouTube.js | 8 +- test/util/render.js | 36 ++-- 6 files changed, 223 insertions(+), 235 deletions(-) delete mode 100644 src/eventNames.js diff --git a/.eslintrc.js b/.eslintrc.js index 0d954cc6..5f421ca8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,8 @@ module.exports = { extends: 'airbnb', + parserOptions: { + ecmaVersion: 2021, + }, rules: { // I disagree 'react/jsx-filename-extension': 'off', diff --git a/src/eventNames.js b/src/eventNames.js deleted file mode 100644 index 0325380f..00000000 --- a/src/eventNames.js +++ /dev/null @@ -1,8 +0,0 @@ -export default [ - 'onReady', - 'onStateChange', - 'onPlaybackQualityChange', - 'onPlaybackRateChange', - 'onError', - 'onApiChange', -]; diff --git a/src/index.js b/src/index.js index 68eb6abb..fc504611 100644 --- a/src/index.js +++ b/src/index.js @@ -1,71 +1,103 @@ -import * as React from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import eventNames from './eventNames'; import loadSdk from './loadSdk'; -class YouTube extends React.Component { - constructor(props) { - super(props); - - this.onPlayerReady = this.onPlayerReady.bind(this); - this.onPlayerStateChange = this.onPlayerStateChange.bind(this); - this.refContainer = this.refContainer.bind(this); - } - - componentDidMount() { - this.createPlayer(); - } - - componentDidUpdate(prevProps) { - // eslint-disable-next-line react/destructuring-assignment - const changes = Object.keys(this.props).filter((name) => this.props[name] !== prevProps[name]); - - this.updateProps(changes); - } - - componentWillUnmount() { - if (this.playerInstance) { - this.playerInstance.destroy(); +const { + useCallback, + useEffect, + useRef, + useState, +} = React; + +function useEventHandler(player, event, handler) { + useEffect(() => { + if (handler) { + player?.addEventListener(event, handler); } - } - - onPlayerReady(event) { - const { - volume, - muted, - suggestedQuality, - playbackRate, - } = this.props; - - if (typeof volume !== 'undefined') { - event.target.setVolume(volume * 100); - } - if (typeof muted !== 'undefined') { - if (muted) { - event.target.mute(); - } else { - event.target.unMute(); + return () => { + if (handler) { + player?.removeEventListener(event, handler); } - } - if (typeof suggestedQuality !== 'undefined') { - event.target.setPlaybackQuality(suggestedQuality); - } - if (typeof playbackRate !== 'undefined') { - event.target.setPlaybackRate(playbackRate); - } + }; + }, [player, event, handler]); +} - this.resolvePlayer(event.target); - } +function YouTube({ + video, + id, + className, + style, + startSeconds, + endSeconds, + width, + height, + lang, + paused, + muted, + volume, + suggestedQuality, + playbackRate, + autoplay = false, + showCaptions = false, + controls = true, + disableKeyboard = false, + allowFullscreen = true, + annotations = true, + modestBranding = false, + playsInline = false, + showRelatedVideos = true, + showInfo = true, + onReady, + onError, + onStateChange, + onPlaybackQualityChange, + onPlaybackRateChange, + onCued = () => {}, + onBuffering = () => {}, + onPlaying = () => {}, + onPause = () => {}, + onEnd = () => {}, +}) { + const container = useRef(null); + const createPlayer = useRef(null); + const firstRender = useRef(false); + const [player, setPlayer] = useState(null); + + const playerVars = { + autoplay, + cc_load_policy: showCaptions ? 1 : 0, + controls: controls ? 1 : 0, + disablekb: disableKeyboard ? 1 : 0, + fs: allowFullscreen ? 1 : 0, + hl: lang, + iv_load_policy: annotations ? 1 : 3, + start: startSeconds, + end: endSeconds, + modestbranding: modestBranding ? 1 : 0, + playsinline: playsInline ? 1 : 0, + rel: showRelatedVideos ? 1 : 0, + showinfo: showInfo ? 1 : 0, + }; - onPlayerStateChange(event) { - const { - onCued, - onBuffering, - onPause, - onPlaying, - onEnd, - } = this.props; + // Stick the player initialisation in a ref so it has the most recent props values + // when it gets instantiated. + if (!player) { + // eslint-disable-next-line no-undef + createPlayer.current = () => new YT.Player(container.current, { + videoId: video, + width, + height, + playerVars, + events: { + onReady: (event) => { + firstRender.current = true; + setPlayer(event.target); + }, + }, + }); + } + const handlePlayerStateChange = useCallback((event) => { const State = YT.PlayerState; // eslint-disable-line no-undef switch (event.data) { case State.CUED: @@ -86,157 +118,111 @@ class YouTube extends React.Component { default: // Nothing } - } + }, [onCued, onBuffering, onPause, onPlaying, onEnd]); - /** - * @private - */ - getPlayerParameters() { - /* eslint-disable react/destructuring-assignment */ - return { - autoplay: this.props.autoplay, - cc_load_policy: this.props.showCaptions ? 1 : 0, - controls: this.props.controls ? 1 : 0, - disablekb: this.props.disableKeyboard ? 1 : 0, - fs: this.props.allowFullscreen ? 1 : 0, - hl: this.props.lang, - iv_load_policy: this.props.annotations ? 1 : 3, - start: this.props.startSeconds, - end: this.props.endSeconds, - modestbranding: this.props.modestBranding ? 1 : 0, - playsinline: this.props.playsInline ? 1 : 0, - rel: this.props.showRelatedVideos ? 1 : 0, - showinfo: this.props.showInfo ? 1 : 0, - }; - /* eslint-enable react/destructuring-assignment */ - } + // The effect that manages the player's lifetime. + useEffect(() => { + let instance = null; + let cancelled = false; - /** - * @private - */ - getInitialOptions() { - /* eslint-disable react/destructuring-assignment */ - return { - videoId: this.props.video, - width: this.props.width, - height: this.props.height, - playerVars: this.getPlayerParameters(), - events: { - onReady: this.onPlayerReady, - onStateChange: this.onPlayerStateChange, - }, + loadSdk().then(() => { + if (!cancelled) { + instance = createPlayer.current(); + } + }); + + return () => { + cancelled = true; + instance?.destroy(); }; - /* eslint-enable react/destructuring-assignment */ - } + }, []); + + useEventHandler(player, 'onStateChange', handlePlayerStateChange); + useEventHandler(player, 'onReady', onReady); + useEventHandler(player, 'onStateChange', onStateChange); + useEventHandler(player, 'onPlaybackQualityChange', onPlaybackQualityChange); + useEventHandler(player, 'onPlaybackRateChange', onPlaybackRateChange); + useEventHandler(player, 'onError', onError); + + useEffect(() => { + if (player) { + player.getIframe().width = width; + } + }, [player, width]); - /** - * @private - */ - updateProps(propNames) { - this.player.then((player) => { - propNames.forEach((name) => { - // eslint-disable-next-line react/destructuring-assignment - const value = this.props[name]; - switch (name) { - case 'muted': - if (value) { - player.mute(); - } else { - player.unMute(); - } - break; - case 'suggestedQuality': - player.setPlaybackQuality(value); - break; - case 'volume': - player.setVolume(value * 100); - break; - case 'paused': - if (value && player.getPlayerState() !== 2) { - player.pauseVideo(); - } else if (!value && player.getPlayerState() === 2) { - player.playVideo(); - } - break; - case 'id': - case 'className': - case 'width': - case 'height': - player.getIframe()[name] = value; // eslint-disable-line no-param-reassign - break; - case 'video': - if (!value) { - player.stopVideo(); - } else { - const { startSeconds, endSeconds, autoplay } = this.props; - const opts = { - videoId: value, - startSeconds: startSeconds || 0, - endSeconds, - }; - if (autoplay) { - player.loadVideoById(opts); - } else { - player.cueVideoById(opts); - } - } - break; - default: - // Nothing - } - }); - }); - } + useEffect(() => { + if (player) { + player.getIframe().height = height; + } + }, [player, height]); - /** - * @private - */ - createPlayer() { - const { volume } = this.props; - - this.player = loadSdk().then((YT) => new Promise((resolve) => { - this.resolvePlayer = resolve; - - const player = new YT.Player(this.container, this.getInitialOptions()); - // Store the instance directly so we can destroy it sync in - // `componentWillUnmount`. - this.playerInstance = player; - - eventNames.forEach((name) => { - player.addEventListener(name, (event) => { - // eslint-disable-next-line react/destructuring-assignment - const handler = this.props[name]; - if (handler) { - handler(event); - } - }); - }); - })); - - if (typeof volume === 'number') { - this.updateProps(['volume']); + useEffect(() => { + if (muted) { + player?.mute(); + } else { + player?.unMute(); } - } + }, [player, muted]); - /** - * @private - */ - refContainer(container) { - this.container = container; - } + useEffect(() => { + player?.setPlaybackQuality(suggestedQuality); + }, [player, suggestedQuality]); - render() { - const { id, className, style } = this.props; - - return ( -
- ); - } + useEffect(() => { + player?.setPlaybackRate(playbackRate); + }, [player, playbackRate]); + + useEffect(() => { + player?.setVolume(volume * 100); + }, [player, volume]); + + useEffect(() => { + if (!player) { + return; + } + if (paused && player.getPlayerState() !== 2) { + player.pauseVideo(); + } else if (!paused && player.getPlayerState() === 2) { + player.playVideo(); + } + }, [player, paused]); + + useEffect(() => { + if (!player) { + return; + } + + // Avoid calling a load() function when the player has just initialised, + // since it will already be up to date at that stage. + if (firstRender.current) { + firstRender.current = false; + return; + } + + if (!video) { + player.stopVideo(); + } else { + const opts = { + videoId: video, + startSeconds: startSeconds || 0, + endSeconds, + }; + if (autoplay) { + player.loadVideoById(opts); + } else { + player.cueVideoById(opts); + } + } + }, [player, video]); + + return ( +
+ ); } if (process.env.NODE_ENV !== 'production') { @@ -386,7 +372,6 @@ if (process.env.NODE_ENV !== 'production') { playbackRate: PropTypes.number, // Events - /* eslint-disable react/no-unused-prop-types */ /** * Sent when the YouTube player API has loaded. @@ -419,8 +404,6 @@ if (process.env.NODE_ENV !== 'production') { onStateChange: PropTypes.func, onPlaybackRateChange: PropTypes.func, onPlaybackQualityChange: PropTypes.func, - - /* eslint-enable react/no-unused-prop-types */ }; } diff --git a/src/loadSdk.js b/src/loadSdk.js index 53245388..9168c84e 100644 --- a/src/loadSdk.js +++ b/src/loadSdk.js @@ -5,9 +5,7 @@ function loadSdk() { return new Promise((resolve, reject) => { if (typeof window.YT === 'object' && typeof window.YT.ready === 'function') { // A YouTube SDK is already loaded, so reuse that - window.YT.ready(() => { - resolve(window.YT); - }); + window.YT.ready(resolve); return; } @@ -15,9 +13,7 @@ function loadSdk() { if (err) { reject(err); } else { - window.YT.ready(() => { - resolve(window.YT); - }); + window.YT.ready(resolve); } }); }); diff --git a/test/util/createYouTube.js b/test/util/createYouTube.js index 8dfce8cc..0bb45086 100644 --- a/test/util/createYouTube.js +++ b/test/util/createYouTube.js @@ -25,8 +25,9 @@ export default function createYouTube() { const playerMock = { addEventListener: createSpy().andCall((eventName, fn) => { - if (eventName === 'ready') fn(); + if (eventName === 'onReady') fn({ target: playerMock }); }), + removeEventListener: createSpy(), mute: createSpy(), unMute: createSpy(), setVolume: createSpy(), @@ -65,7 +66,10 @@ export default function createYouTube() { const YouTube = proxyquire('../../src/index.js', { './loadSdk': { - default: () => Promise.resolve(sdkMock), + default() { + global.YT = sdkMock; + return Promise.resolve(sdkMock); + }, }, }).default; diff --git a/test/util/render.js b/test/util/render.js index ea48ee4b..55ab0f89 100644 --- a/test/util/render.js +++ b/test/util/render.js @@ -5,15 +5,20 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { act } from 'react-dom/test-utils'; import env from 'min-react-env'; import createYouTube from './createYouTube'; Object.assign(global, env); -const render = (initialProps) => { +async function render(initialProps) { const { YouTube, sdkMock, playerMock } = createYouTube(); - let component; + let resolveReady; + const readyPromise = new Promise((resolve) => { + resolveReady = resolve; + }); + // Emulate changes to component.props using a container component's state class Container extends React.Component { constructor(ytProps) { @@ -25,10 +30,15 @@ const render = (initialProps) => { render() { const { props } = this.state; + const onReady = (event) => { + resolveReady(); + props.onReady?.(event); + }; + return ( { component = youtube; }} {...props} + onReady={onReady} /> ); } @@ -38,26 +48,26 @@ const render = (initialProps) => { const container = new Promise((resolve) => { ReactDOM.render(, div); }); + await readyPromise; + + async function rerender(newProps) { + const wrapper = await container; - function rerender(newProps) { - return container.then((wrapper) => new Promise((resolve) => { - wrapper.setState({ props: newProps }, () => { - Promise.resolve().then(resolve); - }); - })); + act(() => { + wrapper.setState({ props: newProps }); + }); } function unmount() { ReactDOM.unmountComponentAtNode(div); } - return component.player.then(() => ({ + return { sdkMock, playerMock, - component, rerender, unmount, - })); -}; + }; +} export default render; From 6cc2a908fe379a3dd180dc20d1afb26fa784e5a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Wed, 30 Jun 2021 12:07:47 +0200 Subject: [PATCH 02/76] use "real" es modules in node.js --- package.json | 10 ++++++++-- rollup.config.js | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 53d9b2c5..440f8471 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,14 @@ "youtube" ], "license": "MIT", - "main": "dist/react-youtube.js", - "module": "dist/react-youtube.es.js", + "main": "./dist/react-youtube.js", + "module": "./dist/react-youtube.mjs", + "exports": { + ".": { + "require": "./dist/react-youtube.js", + "import": "./dist/react-youtube.mjs" + } + }, "types": "index.d.ts", "peerDependencies": { "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" diff --git a/rollup.config.js b/rollup.config.js index 3d3889c8..46f9864a 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -5,8 +5,8 @@ const meta = require('./package.json'); export default { input: './src/index.js', output: [ - { format: 'cjs', file: meta.main, exports: 'named' }, - { format: 'es', file: meta.module }, + { format: 'cjs', file: meta.exports['.'].require, exports: 'named' }, + { format: 'esm', file: meta.exports['.'].import }, ], external: Object.keys(meta.dependencies) From d7d5312cb5f3fdf9684e28318f356e6b8a6b5f47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Wed, 30 Jun 2021 13:04:51 +0200 Subject: [PATCH 03/76] example sourcemaps --- example/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/package.json b/example/package.json index 6bc28907..18155eb3 100644 --- a/example/package.json +++ b/example/package.json @@ -6,7 +6,7 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "postinstall": "npm run build", - "build": "esbuild --bundle app.js --loader:.js=jsx > bundle.js", + "build": "esbuild --bundle app.js --loader:.js=jsx --sourcemap=inline --minify > bundle.js", "start": "serve ." }, "dependencies": { From f8e5432f8f0ff1f5904cb1a68e10f93f75c328ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Wed, 30 Jun 2021 13:05:20 +0200 Subject: [PATCH 04/76] generate source map with rollup --- rollup.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rollup.config.js b/rollup.config.js index 46f9864a..8600d435 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -5,8 +5,8 @@ const meta = require('./package.json'); export default { input: './src/index.js', output: [ - { format: 'cjs', file: meta.exports['.'].require, exports: 'named' }, - { format: 'esm', file: meta.exports['.'].import }, + { format: 'cjs', file: meta.exports['.'].require, exports: 'named', sourcemap: true }, + { format: 'esm', file: meta.exports['.'].import, sourcemap: true }, ], external: Object.keys(meta.dependencies) From fe06529659a938bab245c11449853658d43ffd53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Wed, 30 Jun 2021 13:09:23 +0200 Subject: [PATCH 05/76] lint --- rollup.config.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/rollup.config.js b/rollup.config.js index 8600d435..cb579e0e 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -5,8 +5,17 @@ const meta = require('./package.json'); export default { input: './src/index.js', output: [ - { format: 'cjs', file: meta.exports['.'].require, exports: 'named', sourcemap: true }, - { format: 'esm', file: meta.exports['.'].import, sourcemap: true }, + { + format: 'cjs', + file: meta.exports['.'].require, + exports: 'named', + sourcemap: true, + }, + { + format: 'esm', + file: meta.exports['.'].import, + sourcemap: true, + }, ], external: Object.keys(meta.dependencies) From 008648216f883201671d41d1ae53d4454f7b1b7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Wed, 30 Jun 2021 13:09:27 +0200 Subject: [PATCH 06/76] ci: use npm 7 --- .github/workflows/ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29c60aa5..d9029482 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: - name: Install Node.js uses: actions/setup-node@v2 with: - node-version: 14.x + node-version: 16.x - name: Install dependencies run: npm install - name: Check types @@ -26,7 +26,7 @@ jobs: - name: Install Node.js uses: actions/setup-node@v2 with: - node-version: 14.x + node-version: 16.x - name: Install dependencies run: npm install - name: Check code style @@ -48,6 +48,8 @@ jobs: uses: actions/setup-node@v2 with: node-version: ${{matrix.node-version}} + - name: Use npm 7 + runs: npm install -g npm@7 - name: Install dependencies run: npm install - name: Install React ${{matrix.react-version}} From 6d54d234588b7edf029f12160c8834a9205d9478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Wed, 30 Jun 2021 13:19:42 +0200 Subject: [PATCH 07/76] ci: test react 17 and 18 only --- .github/workflows/ci.yml | 4 ++-- package.json | 3 +-- src/loadSdk.js | 26 +++++++++++++++++--------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9029482..a16910e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,8 +38,8 @@ jobs: node-version: [12.x, 14.x, 16.x] react-version: [17.x] include: - - node-version: 12.x - react-version: 16.0.0 + - node-version: 16.x + react-version: alpha runs-on: ubuntu-latest steps: - name: Checkout sources diff --git a/package.json b/package.json index 440f8471..3cbdd135 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ "dependencies": { "@types/react": "^17.0.0", "@types/youtube": "0.0.46", - "load-script2": "^1.0.1", "prop-types": "^15.7.2" }, "devDependencies": { @@ -57,7 +56,7 @@ }, "types": "index.d.ts", "peerDependencies": { - "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "react": "^17.0.0" }, "repository": { "type": "git", diff --git a/src/loadSdk.js b/src/loadSdk.js index 9168c84e..1979e47c 100644 --- a/src/loadSdk.js +++ b/src/loadSdk.js @@ -1,5 +1,4 @@ -/* global window */ -import loadScript from 'load-script2'; +/* global window, document */ function loadSdk() { return new Promise((resolve, reject) => { @@ -9,13 +8,22 @@ function loadSdk() { return; } - loadScript('https://www.youtube.com/iframe_api', (err) => { - if (err) { - reject(err); - } else { - window.YT.ready(resolve); - } - }); + const script = document.createElement('script'); + script.async = true; + script.src = 'https://www.youtube.com/iframe_api'; + script.onload = () => { + script.onerror = null; + script.onload = null; + window.YT.ready(resolve); + }; + script.onerror = () => { + script.onerror = null; + script.onload = null; + reject(new Error('Could not load YouTube SDK')); + }; + + const node = document.head || document.getElementsByTagName('head')[0]; + node.appendChild(script); }); } From 3f09a8b562aba7d984b73c69101b45a780f71a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Wed, 30 Jun 2021 13:20:04 +0200 Subject: [PATCH 08/76] ci: fix typo --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a16910e5..6b620eb2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: with: node-version: ${{matrix.node-version}} - name: Use npm 7 - runs: npm install -g npm@7 + run: npm install -g npm@7 - name: Install dependencies run: npm install - name: Install React ${{matrix.react-version}} From ee1a0391f5d936ca0885e1c01884973bbd70792b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Wed, 30 Jun 2021 13:25:14 +0200 Subject: [PATCH 09/76] actually test with react 18 --- .github/workflows/ci.yml | 2 +- test/util/render.js | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b620eb2..7611d7ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,7 +55,7 @@ jobs: - name: Install React ${{matrix.react-version}} if: matrix.react-version != '17.x' run: | - npm install --save-dev \ + npm install --force --save-dev \ react@${{matrix.react-version}} \ react-dom@${{matrix.react-version}} \ react-test-renderer@${{matrix.react-version}} diff --git a/test/util/render.js b/test/util/render.js index 55ab0f89..6610845a 100644 --- a/test/util/render.js +++ b/test/util/render.js @@ -46,7 +46,11 @@ async function render(initialProps) { const div = env.document.createElement('div'); const container = new Promise((resolve) => { - ReactDOM.render(, div); + if (ReactDOM.version.startsWith('18')) { + ReactDOM.createRoot(div).render(); + } else { + ReactDOM.render(, div); + } }); await readyPromise; From c188a1fb67a4dda4c938ba7b135101606db81b9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Thu, 19 Aug 2021 09:24:24 +0200 Subject: [PATCH 10/76] target modern-er browsers --- .babelrc.js | 17 +++++------------ .browserslistrc | 30 ++++++++++++++++++++++++++++++ package.json | 4 ++-- 3 files changed, 37 insertions(+), 14 deletions(-) create mode 100644 .browserslistrc diff --git a/.babelrc.js b/.babelrc.js index 339063f7..ccf9b08a 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -1,19 +1,12 @@ module.exports = (api) => { - api.cache.never(); - - const envOptions = { - modules: false, - loose: true, - }; - - if (process.env.NODE_ENV === 'test') { - envOptions.modules = 'commonjs'; - envOptions.targets = { node: 'current' }; - } + const isTest = api.caller((caller) => caller.name === '@babel/register'); return { + targets: isTest ? { node: 'current' } : {}, presets: [ - ['@babel/env', envOptions], + ['@babel/env', { + modules: isTest ? 'commonjs' : false, + }], '@babel/react', ], }; diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 00000000..8f6c8a64 --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,30 @@ +and_chr 92 +and_chr 91 +and_ff 90 +and_ff 89 +and_qq 10.4 +and_uc 12.12 +android 92 +android 91 +baidu 7.12 +chrome 92 +chrome 91 +chrome 90 +edge 92 +edge 91 +firefox 90 +firefox 89 +firefox 78 +ios_saf 14.5-14.7 +ios_saf 14.0-14.4 +ios_saf 13.4-13.7 +kaios 2.5 +op_mini all +op_mob 77 +op_mob 76 +opera 77 +opera 76 +safari 14.1 +safari 14 +samsung 14.0 +samsung 13.0 diff --git a/package.json b/package.json index 3cbdd135..b9beaf0f 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "@babel/preset-react": "^7.8.3", "@babel/register": "^7.8.3", "@rollup/plugin-babel": "^5.0.4", - "cross-env": "^7.0.0", "eslint": "^8.2.0", "eslint-config-airbnb": "^19.0.0", "eslint-plugin-import": "^2.22.0", @@ -67,9 +66,10 @@ "docs": "prop-types-table src/index.js | md-insert README.md --header Props -i", "example": "npm run -w example build && npm run -w example start", "prepare": "npm run build", + "browserslist": "npx browserslist --mobile-to-desktop '> 0.5%, last 2 versions, Firefox ESR, not dead, not IE 11' > .browserslistrc", "test": "npm run test:types && npm run test:mocha && npm run test:lint", "test:lint": "eslint --cache --fix .", - "test:mocha": "cross-env NODE_ENV=test mocha --require @babel/register test/*.js", + "test:mocha": "mocha --require @babel/register test/*.js", "test:types": "tsd" }, "workspaces": { From f80fd0d530b8a42a20e12add34548472f484c48b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Mon, 30 Aug 2021 10:39:52 +0200 Subject: [PATCH 11/76] no need for document.head fallback --- src/loadSdk.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/loadSdk.js b/src/loadSdk.js index 1979e47c..94ca83ce 100644 --- a/src/loadSdk.js +++ b/src/loadSdk.js @@ -22,8 +22,7 @@ function loadSdk() { reject(new Error('Could not load YouTube SDK')); }; - const node = document.head || document.getElementsByTagName('head')[0]; - node.appendChild(script); + document.head.appendChild(script); }); } From da410231a5b4f922880421f2e84f01383cadb698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sat, 18 Dec 2021 14:38:12 +0100 Subject: [PATCH 12/76] jsdoc types --- src/index.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index fc504611..b369e5ee 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,4 @@ +// @ts-check import React from 'react'; import PropTypes from 'prop-types'; import loadSdk from './loadSdk'; @@ -9,6 +10,12 @@ const { useState, } = React; +/** + * @template {keyof YT.Events} K + * @param {YT.Player} player + * @param {K} event + * @param {YT.Events[K]} handler + */ function useEventHandler(player, event, handler) { useEffect(() => { if (handler) { @@ -22,6 +29,7 @@ function useEventHandler(player, event, handler) { }, [player, event, handler]); } +/** @param {import('../index').YouTubeProps} props */ function YouTube({ video, id, @@ -58,13 +66,16 @@ function YouTube({ onPause = () => {}, onEnd = () => {}, }) { + /** @type {React.RefObject} */ const container = useRef(null); + /** @type {React.MutableRefObject<() => YT.Player>} */ const createPlayer = useRef(null); const firstRender = useRef(false); - const [player, setPlayer] = useState(null); + const [player, setPlayer] = useState(/** @type {YT.Player | null} */ (null)); + /** @type {YT.PlayerVars} */ const playerVars = { - autoplay, + autoplay: autoplay ? 1 : 0, cc_load_policy: showCaptions ? 1 : 0, controls: controls ? 1 : 0, disablekb: disableKeyboard ? 1 : 0, From 4995d5e39f4dc28b767b9cb83a311405d836728e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sat, 18 Dec 2021 14:38:58 +0100 Subject: [PATCH 13/76] ci: run with react 18 RC --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 953f35a8..9f3f66ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: strategy: matrix: node-version: [12.x, 14.x, 16.x] - react-version: [17.x] + react-version: [17.x, 'rc'] include: - node-version: 16.x react-version: alpha From af17792e36da6d6378fec0dd1f638dc341fa1e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sat, 18 Dec 2021 14:41:57 +0100 Subject: [PATCH 14/76] ci: should do it like this oops --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f3f66ae..3d660236 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,10 +38,10 @@ jobs: strategy: matrix: node-version: [12.x, 14.x, 16.x] - react-version: [17.x, 'rc'] + react-version: [17.x] include: - node-version: 16.x - react-version: alpha + react-version: rc runs-on: ubuntu-latest steps: - name: Checkout sources From 5a750b35afbf7f6d8dd4aa4647255ae4b5a8c53a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sat, 18 Dec 2021 14:44:41 +0100 Subject: [PATCH 15/76] 1.0.0-alpha.0 --- CHANGELOG.md | 5 +++++ package.json | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f877df46..16f99586 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## 1.0.0-alpha.0 - 2021-12-01 + * Use hooks internally. + * Drop support for React 16. This version requires React 17 or 18. + * Target evergreen browsers. If you need to support older browsers, you need to transpile this dependency. + ## 0.7.2 - 2020-10-21 * Allow React 17 in peerDependency range. * Test with React 16 and React 17 on CI. diff --git a/package.json b/package.json index 35cecc62..b281e2a3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@u-wave/react-youtube", "description": "YouTube player component for React.", - "version": "0.7.2", + "version": "1.0.0-alpha.0", "author": "Renée Kooi ", "bugs": { "url": "https://github.com/u-wave/react-youtube/issues" @@ -55,7 +55,7 @@ }, "types": "index.d.ts", "peerDependencies": { - "react": "^17.0.0" + "react": "^17.0.0 || ^18.0.0-0" }, "repository": { "type": "git", From 7598ddc37377a7686240b0da9d6366329afa092e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Thu, 7 Apr 2022 12:34:05 +0200 Subject: [PATCH 16/76] fix types --- index.d.ts | 2 +- src/index.js | 4 ++-- test/test.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/index.d.ts b/index.d.ts index d5c15a0e..8bd8532b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -130,7 +130,7 @@ export interface YouTubeProps { * * https://developers.google.com/youtube/iframe_api_reference#Playback_quality */ - suggestedQuality?: string; + suggestedQuality?: YT.SuggestedVideoQuality; /** * Playback speed. * diff --git a/src/index.js b/src/index.js index b369e5ee..bf83c2b8 100644 --- a/src/index.js +++ b/src/index.js @@ -157,13 +157,13 @@ function YouTube({ useEffect(() => { if (player) { - player.getIframe().width = width; + player.getIframe().width = String(width); } }, [player, width]); useEffect(() => { if (player) { - player.getIframe().height = height; + player.getIframe().height = String(height); } }, [player, height]); diff --git a/test/test.js b/test/test.js index fd88cd70..d6806d0c 100644 --- a/test/test.js +++ b/test/test.js @@ -114,7 +114,7 @@ describe('YouTube', () => { }); expect(playerMock.getIframe().setWidth).toHaveBeenCalledWith('100%'); - expect(playerMock.getIframe().setHeight).toHaveBeenCalledWith(800); + expect(playerMock.getIframe().setHeight).toHaveBeenCalledWith('800'); }); it('should respect start/endSeconds', async () => { From 00f9654eacea8985edb4e21434fa4c2fdd5b9bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Tue, 19 Apr 2022 11:34:28 +0200 Subject: [PATCH 17/76] ci: use default npm version --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ea6dba5..75abe74c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,8 +47,6 @@ jobs: uses: actions/setup-node@v2 with: node-version: ${{matrix.node-version}} - - name: Use npm 7 - run: npm install -g npm@7 - name: Install dependencies run: npm install - name: Install React ${{matrix.react-version}} From f1e5670bb80e2c903cb771f8d6eacfc79e39e2f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Tue, 19 Apr 2022 11:46:26 +0200 Subject: [PATCH 18/76] use correct entry point for different react versions --- test/util/render.js | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/test/util/render.js b/test/util/render.js index 6610845a..51764308 100644 --- a/test/util/render.js +++ b/test/util/render.js @@ -45,12 +45,25 @@ async function render(initialProps) { } const div = env.document.createElement('div'); + let root; + if (ReactDOM.version.startsWith('18')) { + const { createRoot } = await import('react-dom/client'); + root = createRoot(div); + } else { + root = { + render(element) { + ReactDOM.render(element, div); + }, + unmount() { + ReactDOM.unmountComponentAtNode(div); + }, + }; + } + const container = new Promise((resolve) => { - if (ReactDOM.version.startsWith('18')) { - ReactDOM.createRoot(div).render(); - } else { - ReactDOM.render(, div); - } + act(() => { + root.render(); + }); }); await readyPromise; @@ -62,15 +75,13 @@ async function render(initialProps) { }); } - function unmount() { - ReactDOM.unmountComponentAtNode(div); - } - return { sdkMock, playerMock, rerender, - unmount, + unmount() { + root.unmount(); + }, }; } From 28fbcd4ecd927ef5e8d3876591ba50c979702c3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Tue, 19 Apr 2022 17:53:55 +0200 Subject: [PATCH 19/76] readme: remove --save --- README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index 9130c2d7..99b1bd88 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,14 @@ # @u-wave/react-youtube - YouTube player component for React. [Install][] - [Usage][] - [Demo][] - [Props][] ## Install - ``` -npm install --save @u-wave/react-youtube +npm install @u-wave/react-youtube ``` ## Usage - [Demo][] - [Demo source code][] ```js @@ -63,13 +60,11 @@ import YouTube from '@u-wave/react-youtube'; ## Related - - [react-dailymotion][] - A Dailymotion component with a similar declarative API. - [@u-wave/react-vimeo][] - A Vimeo component with a similar declarative API. - [react-youtube][] - A widely-used YouTube component. Its API matches the YouTube iframe API more closely, and it doesn't support prop-based volume/quality/playback changes. ## License - [MIT][] [Install]: #install From b2b03b62bff95b9a18a4ca0fda8b3f6e6c655783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Tue, 19 Apr 2022 18:03:20 +0200 Subject: [PATCH 20/76] tweaks --- src/index.js | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/index.js b/src/index.js index bf83c2b8..9f6b0644 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,5 @@ // @ts-check +/* global YT */ import React from 'react'; import PropTypes from 'prop-types'; import loadSdk from './loadSdk'; @@ -11,8 +12,10 @@ const { } = React; /** + * Attach an event listener to a YouTube player. + * * @template {keyof YT.Events} K - * @param {YT.Player} player + * @param {YT.Player|null} player * @param {K} event * @param {YT.Events[K]} handler */ @@ -73,27 +76,26 @@ function YouTube({ const firstRender = useRef(false); const [player, setPlayer] = useState(/** @type {YT.Player | null} */ (null)); - /** @type {YT.PlayerVars} */ - const playerVars = { - autoplay: autoplay ? 1 : 0, - cc_load_policy: showCaptions ? 1 : 0, - controls: controls ? 1 : 0, - disablekb: disableKeyboard ? 1 : 0, - fs: allowFullscreen ? 1 : 0, - hl: lang, - iv_load_policy: annotations ? 1 : 3, - start: startSeconds, - end: endSeconds, - modestbranding: modestBranding ? 1 : 0, - playsinline: playsInline ? 1 : 0, - rel: showRelatedVideos ? 1 : 0, - showinfo: showInfo ? 1 : 0, - }; - // Stick the player initialisation in a ref so it has the most recent props values // when it gets instantiated. if (!player) { - // eslint-disable-next-line no-undef + /** @type {YT.PlayerVars} */ + const playerVars = { + autoplay: autoplay ? 1 : 0, + cc_load_policy: showCaptions ? 1 : 0, + controls: controls ? 1 : 0, + disablekb: disableKeyboard ? 1 : 0, + fs: allowFullscreen ? 1 : 0, + hl: lang, + iv_load_policy: annotations ? 1 : 3, + start: startSeconds, + end: endSeconds, + modestbranding: modestBranding ? 1 : 0, + playsinline: playsInline ? 1 : 0, + rel: showRelatedVideos ? 1 : 0, + showinfo: showInfo ? 1 : 0, + }; + createPlayer.current = () => new YT.Player(container.current, { videoId: video, width, @@ -109,7 +111,7 @@ function YouTube({ } const handlePlayerStateChange = useCallback((event) => { - const State = YT.PlayerState; // eslint-disable-line no-undef + const State = YT.PlayerState; switch (event.data) { case State.CUED: onCued(event); From 5476e9e1c2d7c2bcdd7ba7e63e0ae6c122f047cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Wed, 20 Apr 2022 09:00:31 +0200 Subject: [PATCH 21/76] Remove defaultProps, handled by default arguments --- src/index.js | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/index.js b/src/index.js index cc6e8a3d..9dcea8b5 100644 --- a/src/index.js +++ b/src/index.js @@ -409,21 +409,4 @@ if (process.env.NODE_ENV !== 'production') { }; } -YouTube.defaultProps = { - autoplay: false, - showCaptions: false, - controls: true, - disableKeyboard: false, - allowFullscreen: true, - annotations: true, - modestBranding: false, - playsInline: false, - showRelatedVideos: true, - onCued: () => {}, - onBuffering: () => {}, - onPlaying: () => {}, - onPause: () => {}, - onEnd: () => {}, -}; - export default YouTube; From 13aef5a2b5c980d79db72ce2e1555b84b2e29100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Wed, 20 Apr 2022 09:02:38 +0200 Subject: [PATCH 22/76] 1.0.0-alpha.1 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fa805f0..11fb0cc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## 1.0.0-alpha.1 - 2022-04-20 + * Improve typings. + * Remove duplicate `defaultProps`. + ## 1.0.0-alpha.0 - 2021-12-01 * Use hooks internally. * Drop support for React 16. This version requires React 17 or 18. diff --git a/package.json b/package.json index 89e4684b..c7d74725 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@u-wave/react-youtube", "description": "YouTube player component for React.", - "version": "1.0.0-alpha.0", + "version": "1.0.0-alpha.1", "author": "Renée Kooi ", "bugs": { "url": "https://github.com/u-wave/react-youtube/issues" From bfecfb62b4a02db8fd18bf5236e4266ed1f7c4f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Wed, 20 Apr 2022 09:10:11 +0200 Subject: [PATCH 23/76] Accept react 17/18 typings --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c7d74725..5b7ab803 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://github.com/u-wave/react-youtube/issues" }, "dependencies": { - "@types/react": "^17.0.0", + "@types/react": "^17.0.0 || ^18.0.0", "@types/youtube": "0.0.46", "prop-types": "^15.7.2" }, From 62380ebdf5b8d25a1dd219105ad574e7ff4de4fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sun, 1 May 2022 15:21:58 +0200 Subject: [PATCH 24/76] Expose all functionality in a hook --- index.d.ts | 34 +++++++++++++++------------ src/index.js | 65 ++++++++++++++++++++++++++++++++-------------------- 2 files changed, 59 insertions(+), 40 deletions(-) diff --git a/index.d.ts b/index.d.ts index c5c6713e..bde02272 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,23 +1,11 @@ /// import * as React from 'react' -export interface YouTubeProps { +export interface YouTubeOptions { /** - * An 11-character string representing a YouTube video ID.. + * An 11-character string representing a YouTube video ID. */ video?: string; - /** - * DOM ID for the player element. - */ - id?: string; - /** - * CSS className for the player element. - */ - className?: string; - /** - * Inline style for container element. - */ - style?: React.CSSProperties; /** * Width of the player element. */ @@ -170,4 +158,20 @@ export interface YouTubeProps { onPlaybackQualityChange?: YT.PlayerEventHandler; } -export default class YouTube extends React.Component {} +export interface YouTubeProps extends YouTubeOptions { + /** + * DOM ID for the player element. + */ + id?: string; + /** + * CSS className for the player element. + */ + className?: string; + /** + * Inline style for container element. + */ + style?: React.CSSProperties; +} + +export function useYouTube(container: React.Ref, options: YouTubeOptions): YT.Player | null; +export default function YouTube(props: YouTubeProps): JSX.Element; diff --git a/src/index.js b/src/index.js index 9dcea8b5..d4ef9e0c 100644 --- a/src/index.js +++ b/src/index.js @@ -32,12 +32,12 @@ function useEventHandler(player, event, handler) { }, [player, event, handler]); } -/** @param {import('../index').YouTubeProps} props */ -function YouTube({ +/** +* @param {React.RefObject} container +* @param {import('../index').YouTubeOptions} options +*/ +function useYouTube(container, { video, - id, - className, - style, startSeconds, endSeconds, width, @@ -68,12 +68,12 @@ function YouTube({ onPause = () => {}, onEnd = () => {}, }) { - /** @type {React.RefObject} */ - const container = useRef(null); + // Storing the player in the very first hook makes it easier to + // find in React DevTools :) + const [player, setPlayer] = useState(/** @type {YT.Player | null} */ (null)); /** @type {React.MutableRefObject<() => YT.Player>} */ const createPlayer = useRef(null); const firstRender = useRef(false); - const [player, setPlayer] = useState(/** @type {YT.Player | null} */ (null)); // Stick the player initialisation in a ref so it has the most recent props values // when it gets instantiated. @@ -108,6 +108,23 @@ function YouTube({ }); } + // The effect that manages the player's lifetime. + useEffect(() => { + let instance = null; + let cancelled = false; + + loadSdk().then(() => { + if (!cancelled) { + instance = createPlayer.current(); + } + }); + + return () => { + cancelled = true; + instance?.destroy(); + }; + }, []); + const handlePlayerStateChange = useCallback((event) => { const State = YT.PlayerState; switch (event.data) { @@ -131,23 +148,6 @@ function YouTube({ } }, [onCued, onBuffering, onPause, onPlaying, onEnd]); - // The effect that manages the player's lifetime. - useEffect(() => { - let instance = null; - let cancelled = false; - - loadSdk().then(() => { - if (!cancelled) { - instance = createPlayer.current(); - } - }); - - return () => { - cancelled = true; - instance?.destroy(); - }; - }, []); - useEventHandler(player, 'onStateChange', handlePlayerStateChange); useEventHandler(player, 'onReady', onReady); useEventHandler(player, 'onStateChange', onStateChange); @@ -226,6 +226,20 @@ function YouTube({ } }, [player, video]); + return player; +} + +/** @param {import('../index').YouTubeProps} props */ +function YouTube({ + id, + className, + style, + ...options +}) { + /** @type {React.RefObject} */ + const container = useRef(null); + useYouTube(container, options); + return (
Date: Sun, 1 May 2022 15:22:30 +0200 Subject: [PATCH 25/76] use player.setSize() method --- src/index.js | 14 ++++---------- test/test.js | 3 +-- test/util/createYouTube.js | 12 +----------- 3 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/index.js b/src/index.js index d4ef9e0c..f703a4a1 100644 --- a/src/index.js +++ b/src/index.js @@ -156,16 +156,10 @@ function useYouTube(container, { useEventHandler(player, 'onError', onError); useEffect(() => { - if (player) { - player.getIframe().width = String(width); - } - }, [player, width]); - - useEffect(() => { - if (player) { - player.getIframe().height = String(height); - } - }, [player, height]); + // We pretend to be a bit smarter than the typescript definitions here, since + // YouTube teeeechnically supports strings like '100%' too. + player?.setSize(/** @type {number} */ (width), /** @type {number} */ (height)); + }, [player, width, height]); useEffect(() => { if (muted) { diff --git a/test/test.js b/test/test.js index d6806d0c..34a34494 100644 --- a/test/test.js +++ b/test/test.js @@ -113,8 +113,7 @@ describe('YouTube', () => { height: 800, }); - expect(playerMock.getIframe().setWidth).toHaveBeenCalledWith('100%'); - expect(playerMock.getIframe().setHeight).toHaveBeenCalledWith('800'); + expect(playerMock.setSize).toHaveBeenCalledWith('100%', 800); }); it('should respect start/endSeconds', async () => { diff --git a/test/util/createYouTube.js b/test/util/createYouTube.js index 0bb45086..d8806ffc 100644 --- a/test/util/createYouTube.js +++ b/test/util/createYouTube.js @@ -5,19 +5,8 @@ export default function createYouTube() { let isPaused = true; const iframeMock = { - setWidth: createSpy(), - setHeight: createSpy(), setId: createSpy(), setClassName: createSpy(), - set width(width) { - iframeMock.setWidth(width); - }, - set height(height) { - iframeMock.setHeight(height); - }, - set id(id) { - iframeMock.setId(id); - }, set className(className) { iframeMock.setClassName(className); }, @@ -48,6 +37,7 @@ export default function createYouTube() { getIframe() { return iframeMock; }, + setSize: createSpy(), }; const sdkMock = { From 73ee1c640c37b47080bdf7119714c6c6980df563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sun, 1 May 2022 15:29:55 +0200 Subject: [PATCH 26/76] example: do not minify identifiers so the internals are easier to inspect --- example/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/package.json b/example/package.json index 88ecccde..1b3e0233 100644 --- a/example/package.json +++ b/example/package.json @@ -5,7 +5,7 @@ "version": "0.0.0-example", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "build": "esbuild --bundle app.js --loader:.js=jsx --sourcemap=inline --minify > bundle.js", + "build": "esbuild --bundle app.js --loader:.js=jsx --sourcemap=inline --minify-whitespace --minify-syntax > bundle.js", "start": "serve ." }, "dependencies": { From 31e9e9a2c7b11fe49ee143adee3ff5d5573ec4fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sun, 1 May 2022 15:31:48 +0200 Subject: [PATCH 27/76] tweak firstRender check --- src/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index f703a4a1..3b3bd322 100644 --- a/src/index.js +++ b/src/index.js @@ -73,7 +73,7 @@ function useYouTube(container, { const [player, setPlayer] = useState(/** @type {YT.Player | null} */ (null)); /** @type {React.MutableRefObject<() => YT.Player>} */ const createPlayer = useRef(null); - const firstRender = useRef(false); + const firstRender = useRef(true); // Stick the player initialisation in a ref so it has the most recent props values // when it gets instantiated. @@ -101,7 +101,6 @@ function useYouTube(container, { playerVars, events: { onReady: (event) => { - firstRender.current = true; setPlayer(event.target); }, }, From df22e79f8d3c03efc8ff9834dffdc96efb158e29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sun, 1 May 2022 15:32:43 +0200 Subject: [PATCH 28/76] indent --- src/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index 3b3bd322..c3fb3392 100644 --- a/src/index.js +++ b/src/index.js @@ -33,9 +33,9 @@ function useEventHandler(player, event, handler) { } /** -* @param {React.RefObject} container -* @param {import('../index').YouTubeOptions} options -*/ + * @param {React.RefObject} container + * @param {import('../index').YouTubeOptions} options + */ function useYouTube(container, { video, startSeconds, From 4b3d539fee7ce6918e1763bc40bb78af1cdb68c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sun, 1 May 2022 15:38:48 +0200 Subject: [PATCH 29/76] setPlaybackQuality is removed --- README.md | 3 +-- example/app.js | 18 ------------------ index.d.ts | 6 ------ src/index.js | 11 ----------- test/test.js | 12 ------------ test/util/createYouTube.js | 1 - 6 files changed, 1 insertion(+), 50 deletions(-) diff --git a/README.md b/README.md index b61a71ca..ee7778d7 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,6 @@ import YouTube from '@u-wave/react-youtube'; | showRelatedVideos | bool | true | Whether to show related videos after the video is over.
https://developers.google.com/youtube/player_parameters#rel | | volume | number | | The playback volume, **as a number between 0 and 1**. | | muted | bool | | Whether the video's sound should be muted. | -| suggestedQuality | string | | The suggested playback quality.
https://developers.google.com/youtube/iframe_api_reference#Playback_quality | | playbackRate | number | | Playback speed.
https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate | | onReady | function | | Sent when the YouTube player API has loaded. | | onError | function | | Sent when the player triggers an error. | @@ -60,7 +59,7 @@ import YouTube from '@u-wave/react-youtube'; ## Related - [react-dailymotion][] - A Dailymotion component with a similar declarative API. - [@u-wave/react-vimeo][] - A Vimeo component with a similar declarative API. - - [react-youtube][] - A widely-used YouTube component. Its API matches the YouTube iframe API more closely, and it doesn't support prop-based volume/quality/playback changes. + - [react-youtube][] - A widely-used YouTube component. Its API matches the YouTube iframe API more closely, and it doesn't support prop-based volume/playback changes. ## License [MIT][] diff --git a/example/app.js b/example/app.js index 095898f9..e5a97f65 100644 --- a/example/app.js +++ b/example/app.js @@ -15,8 +15,6 @@ const videos = [ { id: null, name: '' }, ]; -const qualities = ['auto', '240', '380', '480', '720', '1080', '1440', '2160']; - const hashVideoRx = /^#!\/video\/(\d)$/; const hash = typeof window.location !== 'undefined' ? window.location.hash : ''; // eslint-disable-line no-undef @@ -26,7 +24,6 @@ const defaultVideo = hashVideoRx.test(hash) function App() { const [videoIndex, setVideoIndex] = useState(defaultVideo); - const [suggestedQuality, setSuggestedQuality] = useState('auto'); const [volume, setVolume] = useState(1); const [paused, setPaused] = useState(false); @@ -52,10 +49,6 @@ function App() { setVolume(parseFloat(event.target.value)); }, []); - const handleQuality = useCallback((event) => { - setSuggestedQuality(qualities[event.target.selectedIndex]); - }, []); - return (
@@ -99,16 +92,6 @@ function App() { step={0.01} onChange={handleVolume} /> -
- Quality -
-
{ - player?.setPlaybackQuality(suggestedQuality); - }, [player, suggestedQuality]); - useEffect(() => { player?.setPlaybackRate(playbackRate); }, [player, playbackRate]); @@ -367,12 +362,6 @@ if (process.env.NODE_ENV !== 'production') { */ muted: PropTypes.bool, - /** - * The suggested playback quality. - * - * https://developers.google.com/youtube/iframe_api_reference#Playback_quality - */ - suggestedQuality: PropTypes.string, /** * Playback speed. * diff --git a/test/test.js b/test/test.js index 34a34494..34916361 100644 --- a/test/test.js +++ b/test/test.js @@ -85,18 +85,6 @@ describe('YouTube', () => { expect(playerMock.mute).toHaveBeenCalled(); }); - it('should set the quality when the "suggestedQuality" prop changes', async () => { - const { playerMock, rerender } = await render({ - video: 'x2y5kyu', - suggestedQuality: 'default', - }); - - await rerender({ suggestedQuality: '720hd' }); - expect(playerMock.setPlaybackQuality).toHaveBeenCalledWith('720hd'); - await rerender({ suggestedQuality: 'highres' }); - expect(playerMock.setPlaybackQuality).toHaveBeenCalledWith('highres'); - }); - it('should set the iframe width/height using the width/height props', async () => { const { sdkMock, playerMock, rerender } = await render({ video: 'x2y5kyu', diff --git a/test/util/createYouTube.js b/test/util/createYouTube.js index d8806ffc..2847c6cb 100644 --- a/test/util/createYouTube.js +++ b/test/util/createYouTube.js @@ -20,7 +20,6 @@ export default function createYouTube() { mute: createSpy(), unMute: createSpy(), setVolume: createSpy(), - setPlaybackQuality: createSpy(), setPlaybackRate: createSpy(), loadVideoById: createSpy(), cueVideoById: createSpy(), From e2ab405869f29a54ee99c66097439db745228178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sun, 1 May 2022 15:39:00 +0200 Subject: [PATCH 30/76] showInfo is removed --- index.d.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/index.d.ts b/index.d.ts index f1705391..47147298 100644 --- a/index.d.ts +++ b/index.d.ts @@ -95,10 +95,6 @@ export interface YouTubeOptions { * https://developers.google.com/youtube/player_parameters#rel */ showRelatedVideos?: boolean; - /** - * @deprecated This property was removed from the YouTube API. - */ - showInfo?: boolean; /** * The playback volume, **as a number between 0 and 1**. From 57c4336aab6a42ec671409b36e76da3639876e74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sun, 1 May 2022 15:44:01 +0200 Subject: [PATCH 31/76] 1.0.0-alpha.2 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f49d18d6..ca7b9a9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## 1.0.0-alpha.2 - 2022-05-01 + * Expose all functionality as a `useYouTube` hook. + * Remove props no longer supported by YouTube: `showInfo`, `suggestedQuality`. + ## 1.0.0-alpha.1 - 2022-04-20 * Improve typings. * Remove duplicate `defaultProps`. diff --git a/package.json b/package.json index 5b7ab803..af6b1576 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@u-wave/react-youtube", "description": "YouTube player component for React.", - "version": "1.0.0-alpha.1", + "version": "1.0.0-alpha.2", "author": "Renée Kooi ", "bugs": { "url": "https://github.com/u-wave/react-youtube/issues" From 70970295973050c4185f2a42ecaa1aa2dc19bdf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sun, 1 May 2022 17:13:03 +0200 Subject: [PATCH 32/76] fix unmount order --- src/index.js | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/index.js b/src/index.js index 181aa303..127c4203 100644 --- a/src/index.js +++ b/src/index.js @@ -106,23 +106,6 @@ function useYouTube(container, { }); } - // The effect that manages the player's lifetime. - useEffect(() => { - let instance = null; - let cancelled = false; - - loadSdk().then(() => { - if (!cancelled) { - instance = createPlayer.current(); - } - }); - - return () => { - cancelled = true; - instance?.destroy(); - }; - }, []); - const handlePlayerStateChange = useCallback((event) => { const State = YT.PlayerState; switch (event.data) { @@ -214,6 +197,27 @@ function useYouTube(container, { } }, [player, video]); + // The effect that manages the player's lifetime. + // This must be done at the end to ensure that `.destroy()` runs last. + // Else, other hooks will attempt to do things like `.removeEventListener()` + // after the player is destroyed. + useEffect(() => { + /** @type {YT.Player|null} */ + let instance = null; + let cancelled = false; + + loadSdk().then(() => { + if (!cancelled) { + instance = createPlayer.current(); + } + }); + + return () => { + cancelled = true; + instance?.destroy(); + }; + }, []); + return player; } From 968ed15e3323cf7d0fd49ecc5eb8088b971514a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sun, 1 May 2022 17:23:06 +0200 Subject: [PATCH 33/76] load synchronously if possible --- src/index.js | 2 +- src/loadSdk.js | 18 ++++++++---------- test/util/createYouTube.js | 4 ++-- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/index.js b/src/index.js index 127c4203..50cd2f7f 100644 --- a/src/index.js +++ b/src/index.js @@ -206,7 +206,7 @@ function useYouTube(container, { let instance = null; let cancelled = false; - loadSdk().then(() => { + loadSdk(() => { if (!cancelled) { instance = createPlayer.current(); } diff --git a/src/loadSdk.js b/src/loadSdk.js index 94ca83ce..14e50b56 100644 --- a/src/loadSdk.js +++ b/src/loadSdk.js @@ -2,12 +2,6 @@ function loadSdk() { return new Promise((resolve, reject) => { - if (typeof window.YT === 'object' && typeof window.YT.ready === 'function') { - // A YouTube SDK is already loaded, so reuse that - window.YT.ready(resolve); - return; - } - const script = document.createElement('script'); script.async = true; script.src = 'https://www.youtube.com/iframe_api'; @@ -27,9 +21,13 @@ function loadSdk() { } let sdk = null; -export default function getSdk() { - if (!sdk) { - sdk = loadSdk(); +export default function getSdk(callback) { + if (typeof window.YT === 'object' && typeof window.YT.ready === 'function') { + // A YouTube SDK is already loaded, so reuse that + window.YT.ready(callback); + return; } - return sdk; + + sdk ??= loadSdk(); + sdk.then(callback); } diff --git a/test/util/createYouTube.js b/test/util/createYouTube.js index 2847c6cb..f8aa9844 100644 --- a/test/util/createYouTube.js +++ b/test/util/createYouTube.js @@ -55,9 +55,9 @@ export default function createYouTube() { const YouTube = proxyquire('../../src/index.js', { './loadSdk': { - default() { + default(callback) { global.YT = sdkMock; - return Promise.resolve(sdkMock); + setImmediate(() => callback(sdkMock)); }, }, }).default; From ae7bca944ba7f192c4e9814abf3543cbece43db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sun, 1 May 2022 17:26:50 +0200 Subject: [PATCH 34/76] 1.0.0-alpha.3 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca7b9a9f..1b053747 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## 1.0.0-alpha.3 - 2022-05-01 + * Fix unmount order. + * Start player synchronously if the SDK is already loaded. + ## 1.0.0-alpha.2 - 2022-05-01 * Expose all functionality as a `useYouTube` hook. * Remove props no longer supported by YouTube: `showInfo`, `suggestedQuality`. diff --git a/package.json b/package.json index af6b1576..214207d7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@u-wave/react-youtube", "description": "YouTube player component for React.", - "version": "1.0.0-alpha.2", + "version": "1.0.0-alpha.3", "author": "Renée Kooi ", "bugs": { "url": "https://github.com/u-wave/react-youtube/issues" From c8cd8c32ab9a06e4dc6723dd25c34d179d26ace0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sun, 1 May 2022 17:50:52 +0200 Subject: [PATCH 35/76] argument tweaks --- index.d.ts | 2 +- src/index.js | 105 ++++++++++++++++++++++++++++++--------------------- 2 files changed, 62 insertions(+), 45 deletions(-) diff --git a/index.d.ts b/index.d.ts index 47147298..0ac1328e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -158,7 +158,7 @@ export interface YouTubeProps extends YouTubeOptions { */ className?: string; /** - * Inline style for container element. + * Inline style for player element. */ style?: React.CSSProperties; } diff --git a/src/index.js b/src/index.js index 50cd2f7f..fdd29f0d 100644 --- a/src/index.js +++ b/src/index.js @@ -11,6 +11,13 @@ const { useState, } = React; +// Player state numbers are documented to be constants, so we can inline them. +const ENDED = 0; +const PLAYING = 1; +const PAUSED = 2; +const BUFFERING = 3; +const CUED = 5; + /** * Attach an event listener to a YouTube player. * @@ -33,20 +40,13 @@ function useEventHandler(player, event, handler) { } /** - * @param {React.RefObject} container * @param {import('../index').YouTubeOptions} options + * @return {YT.PlayerVars} */ -function useYouTube(container, { - video, +function getPlayerVars({ startSeconds, endSeconds, - width, - height, lang, - paused, - muted, - volume, - playbackRate, autoplay = false, showCaptions = false, controls = true, @@ -56,17 +56,51 @@ function useYouTube(container, { modestBranding = false, playsInline = false, showRelatedVideos = true, - onReady, - onError, - onStateChange, - onPlaybackQualityChange, - onPlaybackRateChange, - onCued = () => {}, - onBuffering = () => {}, - onPlaying = () => {}, - onPause = () => {}, - onEnd = () => {}, }) { + return { + autoplay: autoplay ? 1 : 0, + cc_load_policy: showCaptions ? 1 : 0, + controls: controls ? 1 : 0, + disablekb: disableKeyboard ? 1 : 0, + fs: allowFullscreen ? 1 : 0, + hl: lang, + iv_load_policy: annotations ? 1 : 3, + start: startSeconds, + end: endSeconds, + modestbranding: modestBranding ? 1 : 0, + playsinline: playsInline ? 1 : 0, + rel: showRelatedVideos ? 1 : 0, + }; +} + +/** + * @param {React.RefObject} container + * @param {import('../index').YouTubeOptions} options + */ +function useYouTube(container, options) { + const { + video, + startSeconds, + endSeconds, + width, + height, + paused, + muted, + volume, + playbackRate, + autoplay = false, + onReady, + onError, + onStateChange, + onPlaybackQualityChange, + onPlaybackRateChange, + onCued = () => {}, + onBuffering = () => {}, + onPlaying = () => {}, + onPause = () => {}, + onEnd = () => {}, + } = options; + // Storing the player in the very first hook makes it easier to // find in React DevTools :) const [player, setPlayer] = useState(/** @type {YT.Player | null} */ (null)); @@ -77,27 +111,11 @@ function useYouTube(container, { // Stick the player initialisation in a ref so it has the most recent props values // when it gets instantiated. if (!player) { - /** @type {YT.PlayerVars} */ - const playerVars = { - autoplay: autoplay ? 1 : 0, - cc_load_policy: showCaptions ? 1 : 0, - controls: controls ? 1 : 0, - disablekb: disableKeyboard ? 1 : 0, - fs: allowFullscreen ? 1 : 0, - hl: lang, - iv_load_policy: annotations ? 1 : 3, - start: startSeconds, - end: endSeconds, - modestbranding: modestBranding ? 1 : 0, - playsinline: playsInline ? 1 : 0, - rel: showRelatedVideos ? 1 : 0, - }; - createPlayer.current = () => new YT.Player(container.current, { videoId: video, width, height, - playerVars, + playerVars: getPlayerVars(options), events: { onReady: (event) => { setPlayer(event.target); @@ -107,21 +125,20 @@ function useYouTube(container, { } const handlePlayerStateChange = useCallback((event) => { - const State = YT.PlayerState; switch (event.data) { - case State.CUED: + case CUED: onCued(event); break; - case State.BUFFERING: + case BUFFERING: onBuffering(event); break; - case State.PAUSED: + case PAUSED: onPause(event); break; - case State.PLAYING: + case PLAYING: onPlaying(event); break; - case State.ENDED: + case ENDED: onEnd(event); break; default: @@ -245,7 +262,7 @@ function YouTube({ if (process.env.NODE_ENV !== 'production') { YouTube.propTypes = { /** - * An 11-character string representing a YouTube video ID.. + * An 11-character string representing a YouTube video ID. */ video: PropTypes.string, /** @@ -257,7 +274,7 @@ if (process.env.NODE_ENV !== 'production') { */ className: PropTypes.string, /** - * Inline style for container element. + * Inline style for the player element. */ style: PropTypes.object, // eslint-disable-line react/forbid-prop-types /** From 99b8365a4ec9eeb5d251332a40ac94a5aa3bd036 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 May 2022 09:34:43 +0200 Subject: [PATCH 36/76] build(deps-dev): bump mocha from 9.2.2 to 10.0.0 (#145) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 214207d7..281ddfb8 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "expect": "^1.20.2", "md-insert": "^1.0.1", "min-react-env": "^1.0.1", - "mocha": "^9.0.0", + "mocha": "^10.0.0", "prop-types-table": "^1.0.0", "proxyquire": "^2.1.3", "react": "^18.0.0", From 73fe5af6cdd717f135ba1402c665f982bfd85e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Mon, 2 May 2022 09:59:31 +0200 Subject: [PATCH 37/76] declare as a function component --- index.d.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 0ac1328e..f1736768 100644 --- a/index.d.ts +++ b/index.d.ts @@ -164,4 +164,6 @@ export interface YouTubeProps extends YouTubeOptions { } export function useYouTube(container: React.Ref, options: YouTubeOptions): YT.Player | null; -export default function YouTube(props: YouTubeProps): JSX.Element; + +declare const YouTube: React.FunctionComponent; +export default YouTube; From 5d85885e1395053ada1ed1298b9e71ec2b17d258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Mon, 2 May 2022 09:59:40 +0200 Subject: [PATCH 38/76] add default values to typescript doc --- index.d.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/index.d.ts b/index.d.ts index f1736768..eec6dda1 100644 --- a/index.d.ts +++ b/index.d.ts @@ -26,30 +26,40 @@ export interface YouTubeOptions { * Whether the video should start playing automatically. * * https://developers.google.com/youtube/player_parameters#autoplay + * + * @default false */ autoplay?: boolean; /** * Whether to show captions below the video. * * https://developers.google.com/youtube/player_parameters#cc_load_policy + * + * @default false */ showCaptions?: boolean; /** * Whether to show video controls. * * https://developers.google.com/youtube/player_parameters#controls + * + * @default true */ controls?: boolean; /** * Ignore keyboard controls. * * https://developers.google.com/youtube/player_parameters#disablekb + * + * @default false */ disableKeyboard?: boolean; /** * Whether to display the fullscreen button. * * https://developers.google.com/youtube/player_parameters#fs + * + * @default true */ allowFullscreen?: boolean; /** @@ -63,6 +73,8 @@ export interface YouTubeOptions { * Whether to show annotations on top of the video. * * https://developers.google.com/youtube/player_parameters#iv_load_policy + * + * @default true */ annotations?: boolean; /** @@ -81,18 +93,24 @@ export interface YouTubeOptions { * Remove most YouTube logos from the player. * * https://developers.google.com/youtube/player_parameters#modestbranding + * + * @default false */ modestBranding?: boolean; /** * Whether to play the video inline on iOS, instead of fullscreen. * * https://developers.google.com/youtube/player_parameters#playsinline + * + * @default false */ playsInline?: boolean; /** * Whether to show related videos after the video is over. * * https://developers.google.com/youtube/player_parameters#rel + * + * @default true */ showRelatedVideos?: boolean; From bc8501c34410537497e6eb79c63a0c32b5ce3ea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Mon, 2 May 2022 09:59:46 +0200 Subject: [PATCH 39/76] use `YT` global --- src/loadSdk.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/loadSdk.js b/src/loadSdk.js index 14e50b56..b0cbe7c9 100644 --- a/src/loadSdk.js +++ b/src/loadSdk.js @@ -1,4 +1,4 @@ -/* global window, document */ +/* global YT, document */ function loadSdk() { return new Promise((resolve, reject) => { @@ -8,7 +8,7 @@ function loadSdk() { script.onload = () => { script.onerror = null; script.onload = null; - window.YT.ready(resolve); + YT.ready(resolve); }; script.onerror = () => { script.onerror = null; @@ -21,10 +21,11 @@ function loadSdk() { } let sdk = null; +/** @param {(sdk: typeof YT) => void} callback */ export default function getSdk(callback) { - if (typeof window.YT === 'object' && typeof window.YT.ready === 'function') { + if (typeof YT === 'object' && typeof YT.ready === 'function') { // A YouTube SDK is already loaded, so reuse that - window.YT.ready(callback); + YT.ready(callback); return; } From 66c67f009a73f7c14422e0ca4336bc7fb595a069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Mon, 2 May 2022 10:10:32 +0200 Subject: [PATCH 40/76] document hook --- README.md | 78 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ee7778d7..11a9387a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # @u-wave/react-youtube YouTube player component for React. -[Install][] - [Usage][] - [Demo][] - [Props][] +[Install][] - [Usage][] - [Demo][] - [Component API][] - [Hook API][] ## Install ``` @@ -20,13 +20,72 @@ import YouTube from '@u-wave/react-youtube'; /> ``` -## Props + +## `` +The `` component renders an iframe and attaches the YouTube player to it. It supports all the +same options as the `useYouTube` hook, plus a few to configure the iframe. If you need to do more with +the iframe than this component provides, consider using the `useYouTube` hook directly. + +### Props | Name | Type | Default | Description | |:-----|:-----|:-----|:-----| -| video | string | | An 11-character string representing a YouTube video ID.. | | id | string | | DOM ID for the player element. | | className | string | | CSS className for the player element. | | style | object | | Inline style for container element. | +| video | string | | An 11-character string representing a YouTube video ID.. | +| width | number, string | | Width of the player element. | +| height | number, string | | Height of the player element. | +| paused | bool | | Pause the video. | +| autoplay | bool | false | Whether the video should start playing automatically.
https://developers.google.com/youtube/player_parameters#autoplay | +| showCaptions | bool | false | Whether to show captions below the video.
https://developers.google.com/youtube/player_parameters#cc_load_policy | +| controls | bool | true | Whether to show video controls.
https://developers.google.com/youtube/player_parameters#controls | +| disableKeyboard | bool | false | Ignore keyboard controls.
https://developers.google.com/youtube/player_parameters#disablekb | +| allowFullscreen | bool | true | Whether to display the fullscreen button.
https://developers.google.com/youtube/player_parameters#fs | +| lang | string | | The player's interface language. The parameter value is an ISO 639-1 two-letter language code or a fully specified locale.
https://developers.google.com/youtube/player_parameters#hl | +| annotations | bool | true | Whether to show annotations on top of the video.
https://developers.google.com/youtube/player_parameters#iv_load_policy | +| startSeconds | number | | Time in seconds at which to start playing the video.
https://developers.google.com/youtube/player_parameters#start | +| endSeconds | number | | Time in seconds at which to stop playing the video.
https://developers.google.com/youtube/player_parameters#end | +| modestBranding | bool | false | Remove most YouTube logos from the player.
https://developers.google.com/youtube/player_parameters#modestbranding | +| playsInline | bool | false | Whether to play the video inline on iOS, instead of fullscreen.
https://developers.google.com/youtube/player_parameters#playsinline | +| showRelatedVideos | bool | true | Whether to show related videos after the video is over.
https://developers.google.com/youtube/player_parameters#rel | +| volume | number | | The playback volume, **as a number between 0 and 1**. | +| muted | bool | | Whether the video's sound should be muted. | +| playbackRate | number | | Playback speed.
https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate | +| onReady | function | | Sent when the YouTube player API has loaded. | +| onError | function | | Sent when the player triggers an error. | +| onCued | function | | Sent when the video is cued and ready to play. | +| onBuffering | function | | Sent when the video is buffering. | +| onPlaying | function | | Sent when playback has been started or resumed. | +| onPause | function | | Sent when playback has been paused. | +| onEnd | function | | Sent when playback has stopped. | +| onStateChange | function | | | +| onPlaybackRateChange | function | | | +| onPlaybackQualityChange | function | | | + + +## `useYouTube(container, options)` +Create a YouTube player at `container`. `container` must be a ref object. + +Returns the `YT.Player` object, or `null` until the player is ready. + +```js +import { useYouTube } from '@u-wave/react-youtube'; + +function Player() { + const container = useRef(null); + const player = useYouTube(container, { + video: 'x2to0hs', + autoplay: true, + }); + console.log(player?.getVideoUrl()); + return
; +} +``` + +### Options +| Name | Type | Default | Description | +|:-----|:-----|:-----|:-----| +| video | string | | An 11-character string representing a YouTube video ID.. | | width | number, string | | Width of the player element. | | height | number, string | | Height of the player element. | | paused | bool | | Pause the video. | @@ -47,11 +106,11 @@ import YouTube from '@u-wave/react-youtube'; | playbackRate | number | | Playback speed.
https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate | | onReady | function | | Sent when the YouTube player API has loaded. | | onError | function | | Sent when the player triggers an error. | -| onCued | function | () => {} | Sent when the video is cued and ready to play. | -| onBuffering | function | () => {} | Sent when the video is buffering. | -| onPlaying | function | () => {} | Sent when playback has been started or resumed. | -| onPause | function | () => {} | Sent when playback has been paused. | -| onEnd | function | () => {} | Sent when playback has stopped. | +| onCued | function | | Sent when the video is cued and ready to play. | +| onBuffering | function | | Sent when the video is buffering. | +| onPlaying | function | | Sent when playback has been started or resumed. | +| onPause | function | | Sent when playback has been paused. | +| onEnd | function | | Sent when playback has stopped. | | onStateChange | function | | | | onPlaybackRateChange | function | | | | onPlaybackQualityChange | function | | | @@ -66,7 +125,8 @@ import YouTube from '@u-wave/react-youtube'; [Install]: #install [Usage]: #usage -[Props]: #props +[Component API]: #component +[Hook API]: #hook [Demo]: https://u-wave.net/react-youtube [Demo source code]: ./example [MIT]: ./LICENSE From 3068dac7ad84a58d62386ab34e55912673e75b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Mon, 2 May 2022 12:30:01 +0200 Subject: [PATCH 41/76] pass `mute` in playerVars --- src/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.js b/src/index.js index fdd29f0d..5ce66f5e 100644 --- a/src/index.js +++ b/src/index.js @@ -47,6 +47,7 @@ function getPlayerVars({ startSeconds, endSeconds, lang, + muted = false, autoplay = false, showCaptions = false, controls = true, @@ -70,6 +71,7 @@ function getPlayerVars({ modestbranding: modestBranding ? 1 : 0, playsinline: playsInline ? 1 : 0, rel: showRelatedVideos ? 1 : 0, + mute: muted ? 1 : 0, }; } From 0c47f9f514a374529a036244307a5c8099b7b570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Tue, 3 May 2022 09:45:01 +0200 Subject: [PATCH 42/76] work around unmount order fun times --- src/index.js | 56 +++++++++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/src/index.js b/src/index.js index 5ce66f5e..6b23f7fd 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,7 @@ const { useRef, useState, } = React; +const useLayoutEffect = typeof document !== 'undefined' ? React.useLayoutEffect : useEffect; // Player state numbers are documented to be constants, so we can inline them. const ENDED = 0; @@ -28,12 +29,14 @@ const CUED = 5; */ function useEventHandler(player, event, handler) { useEffect(() => { - if (handler) { - player?.addEventListener(event, handler); + if (handler && player) { + player.addEventListener(event, handler); } return () => { - if (handler) { - player?.removeEventListener(event, handler); + // If the iframe was already deleted, removing event + // listeners is unnecessary, and can actually cause a crash. + if (handler && player && player.getIframe()) { + player.removeEventListener(event, handler); } }; }, [player, event, handler]); @@ -126,6 +129,30 @@ function useYouTube(container, options) { }); } + useLayoutEffect(() => { + /** @type {YT.Player|null} */ + let instance = null; + let cancelled = false; + + loadSdk(() => { + if (!cancelled) { + instance = createPlayer.current(); + } + }); + + return () => { + cancelled = true; + // Destroying the player here means that some other hooks cannot access its methods anymore, + // so they do need to be careful in their unsubscribe effects. + // There isn't really a way around this aside from manually implementing parts of the + // `destroy()` method. + // It's tempting to just remove the iframe here just in time for React to move in, + // but we must use `.destroy()` to avoid memory leaks, since the YouTube SDK holds on + // to references to player objects globally. + instance?.destroy(); + }; + }, []); + const handlePlayerStateChange = useCallback((event) => { switch (event.data) { case CUED: @@ -216,27 +243,6 @@ function useYouTube(container, options) { } }, [player, video]); - // The effect that manages the player's lifetime. - // This must be done at the end to ensure that `.destroy()` runs last. - // Else, other hooks will attempt to do things like `.removeEventListener()` - // after the player is destroyed. - useEffect(() => { - /** @type {YT.Player|null} */ - let instance = null; - let cancelled = false; - - loadSdk(() => { - if (!cancelled) { - instance = createPlayer.current(); - } - }); - - return () => { - cancelled = true; - instance?.destroy(); - }; - }, []); - return player; } From 34dc48d8f019804276eee8ca645fc2a56dea7766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Tue, 3 May 2022 10:35:02 +0200 Subject: [PATCH 43/76] add origin/host configs --- README.md | 4 ++++ index.d.ts | 12 ++++++++++++ src/index.js | 10 ++++++++-- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 11a9387a..d6a5aec5 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ the iframe than this component provides, consider using the `useYouTube` hook di | video | string | | An 11-character string representing a YouTube video ID.. | | width | number, string | | Width of the player element. | | height | number, string | | Height of the player element. | +| host | string | https://www.youtube.com | YouTube host to use: 'https://www.youtube.com' or 'https://www.youtube-nocookie.com'. | +| origin | string | | The YouTube API will usually default this value correctly. It is exposed for completeness.
https://developers.google.com/youtube/player_parameters#origin | | paused | bool | | Pause the video. | | autoplay | bool | false | Whether the video should start playing automatically.
https://developers.google.com/youtube/player_parameters#autoplay | | showCaptions | bool | false | Whether to show captions below the video.
https://developers.google.com/youtube/player_parameters#cc_load_policy | @@ -88,6 +90,8 @@ function Player() { | video | string | | An 11-character string representing a YouTube video ID.. | | width | number, string | | Width of the player element. | | height | number, string | | Height of the player element. | +| host | string | https://www.youtube.com | YouTube host to use: 'https://www.youtube.com' or 'https://www.youtube-nocookie.com'. | +| origin | string | | The YouTube API will usually default this value correctly. It is exposed for completeness.
https://developers.google.com/youtube/player_parameters#origin | | paused | bool | | Pause the video. | | autoplay | bool | false | Whether the video should start playing automatically.
https://developers.google.com/youtube/player_parameters#autoplay | | showCaptions | bool | false | Whether to show captions below the video.
https://developers.google.com/youtube/player_parameters#cc_load_policy | diff --git a/index.d.ts b/index.d.ts index eec6dda1..8fcba274 100644 --- a/index.d.ts +++ b/index.d.ts @@ -15,6 +15,18 @@ export interface YouTubeOptions { */ height?: number | string; + /** + * YouTube host to use: 'https://www.youtube.com' or 'https://www.youtube-nocookie.com'. + * + * @default 'https://www.youtube.com' + */ + host?: string; + + /** + * The YouTube API will usually default this value correctly. It is exposed for completeness. + */ + origin?: string; + /** * Pause the video. */ diff --git a/src/index.js b/src/index.js index 6b23f7fd..1f1ecf8a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ // @ts-check -/* global YT */ +/* global YT, window */ import React from 'react'; import PropTypes from 'prop-types'; import loadSdk from './loadSdk'; @@ -60,6 +60,7 @@ function getPlayerVars({ modestBranding = false, playsInline = false, showRelatedVideos = true, + origin = typeof window.location === 'object' ? window.location.origin : undefined, }) { return { autoplay: autoplay ? 1 : 0, @@ -75,6 +76,7 @@ function getPlayerVars({ playsinline: playsInline ? 1 : 0, rel: showRelatedVideos ? 1 : 0, mute: muted ? 1 : 0, + origin, }; } @@ -120,6 +122,7 @@ function useYouTube(container, options) { videoId: video, width, height, + host: options.host, playerVars: getPlayerVars(options), events: { onReady: (event) => { @@ -303,7 +306,10 @@ if (process.env.NODE_ENV !== 'production') { /** * Pause the video. */ - paused: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types + paused: PropTypes.bool, + + host: PropTypes.string, + origin: PropTypes.string, // Player parameters From 06389ad847359ad00a5fbf87751342c6b05d526b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Tue, 3 May 2022 12:49:37 +0200 Subject: [PATCH 44/76] 1.0.0-alpha.4 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b053747..80a7d0a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## 1.0.0-alpha.4 - 2022-05-03 + * Add docs for the `useYouTube` hook. + * Add props for `origin` / `host` settings. + * Pass-through `muted` to the player initially, so `` works as expected. + ## 1.0.0-alpha.3 - 2022-05-01 * Fix unmount order. * Start player synchronously if the SDK is already loaded. diff --git a/package.json b/package.json index 281ddfb8..81424d61 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@u-wave/react-youtube", "description": "YouTube player component for React.", - "version": "1.0.0-alpha.3", + "version": "1.0.0-alpha.4", "author": "Renée Kooi ", "bugs": { "url": "https://github.com/u-wave/react-youtube/issues" From 69c173ef18a52d64dfe9aff52626a7f28ea28e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Tue, 3 May 2022 12:50:31 +0200 Subject: [PATCH 45/76] do not cache eslint runs --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 81424d61..df394fbb 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "prepare": "npm run build", "browserslist": "npx browserslist --mobile-to-desktop '> 0.5%, last 2 versions, Firefox ESR, not dead, not IE 11' > .browserslistrc", "test": "npm run tsd && npm run tests-only && npm run lint", - "lint": "eslint --cache --fix .", + "lint": "eslint --fix .", "tests-only": "mocha --require @babel/register test/*.js", "tsd": "tsd" }, From 63cb4014698fd8292e6b89d4febd9183a9408618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Wed, 4 May 2022 15:06:00 +0200 Subject: [PATCH 46/76] fix getSdk type --- src/loadSdk.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/loadSdk.js b/src/loadSdk.js index b0cbe7c9..1fc144fd 100644 --- a/src/loadSdk.js +++ b/src/loadSdk.js @@ -21,7 +21,7 @@ function loadSdk() { } let sdk = null; -/** @param {(sdk: typeof YT) => void} callback */ +/** @param {() => void} callback */ export default function getSdk(callback) { if (typeof YT === 'object' && typeof YT.ready === 'function') { // A YouTube SDK is already loaded, so reuse that From 33cb208499098d21d5f81ef43fee1b99320f57bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Tue, 1 Nov 2022 10:11:11 +0100 Subject: [PATCH 47/76] deps: update rollup --- package.json | 2 +- rollup.config.js => rollup.config.mjs | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) rename rollup.config.js => rollup.config.mjs (52%) diff --git a/package.json b/package.json index 3063aaaa..17e810c8 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "proxyquire": "^2.1.3", "react": "^18.0.0", "react-dom": "^18.0.0", - "rollup": "^2.0.2", + "rollup": "^3.2.5", "tsd": "^0.24.1" }, "homepage": "https://github.com/u-wave/react-youtube#readme", diff --git a/rollup.config.js b/rollup.config.mjs similarity index 52% rename from rollup.config.js rename to rollup.config.mjs index cb579e0e..81aaa4e6 100644 --- a/rollup.config.js +++ b/rollup.config.mjs @@ -1,25 +1,26 @@ +import fs from 'node:fs'; import babel from '@rollup/plugin-babel'; -const meta = require('./package.json'); +const pkg = JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')); export default { input: './src/index.js', output: [ { format: 'cjs', - file: meta.exports['.'].require, + file: pkg.exports['.'].require, exports: 'named', sourcemap: true, }, { format: 'esm', - file: meta.exports['.'].import, + file: pkg.exports['.'].import, sourcemap: true, }, ], - external: Object.keys(meta.dependencies) - .concat(Object.keys(meta.peerDependencies)), + external: Object.keys(pkg.dependencies) + .concat(Object.keys(pkg.peerDependencies)), plugins: [ babel({ babelHelpers: 'bundled', From b350ec25583f90d340accb58fbfd31e60df6cc3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sat, 12 Nov 2022 15:24:58 +0100 Subject: [PATCH 48/76] ci: tweak matrix --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75abe74c..2a36e6d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v3 - name: Install Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: lts/* - name: Install dependencies @@ -25,7 +25,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v3 - name: Install Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: lts/* - name: Install dependencies @@ -37,14 +37,14 @@ jobs: name: Tests strategy: matrix: - node-version: [14.x, 16.x, 17.x] - react-version: [17.x, 18.x] + node-version: [14.x, 16.x, 18.x] + react-version: [16.x, 17.x, 18.x] runs-on: ubuntu-latest steps: - name: Checkout sources uses: actions/checkout@v3 - name: Install Node.js ${{matrix.node-version}} - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{matrix.node-version}} - name: Install dependencies From 17dd127dd63f5f2c0bf4664e895ebe64bd6ac6aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sun, 13 Nov 2022 13:19:24 +0100 Subject: [PATCH 49/76] example: fix logo url --- example/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/index.html b/example/index.html index e7dd8331..5bcc24af 100644 --- a/example/index.html +++ b/example/index.html @@ -20,7 +20,7 @@