@codecademy/gamut
73.6.273.6.3-alpha.2b3b95.0
dist/Video/index.js~
dist/Video/index.jsModified+62−9
Index: package/dist/Video/index.js
===================================================================
--- package/dist/Video/index.js
+++ package/dist/Video/index.js
@@ -1,11 +1,15 @@
+// @vidstack/react and react-player are peer deps of @codecademy/gamut.
+// If they can't be resolved, install them: yarn add @vidstack/react react-player
+
import * as React from 'react';
-import { useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { Box } from '../Box';
import { useIsMounted } from '../utils';
import { OverlayPlayButton, ReactVideoPlayer } from './lib/ReactPlayer';
import { VidstackPlayer } from './lib/VidstackPlayer';
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
+const DEFAULT_VIDEO_TITLE = 'Video player';
export const Video = props => {
const {
autoplay = false,
className,
@@ -20,18 +24,56 @@
showPlayerEmbed = true
} = props;
const [loading, setLoading] = useState(true);
const isMounted = useIsMounted();
+ const playerWrapperRef = useRef(null);
const config = {
youtube: {
- playerVars: {
- color: 'white'
- }
- },
- vimeo: {
- title: videoTitle
+ color: 'white'
}
};
+
+ // react-player v3 puts `title` on its custom element, not the inner provider
+ // iframe, so the iframe would otherwise have no accessible name (WCAG 4.1.2).
+ // Provider elements like <youtube-video> render that iframe inside an open
+ // shadow root, so search shadow roots too. `onShadowRoot` lets the observer
+ // below watch each shadow root it finds.
+ const labelProviderIframe = onShadowRoot => {
+ const label = root => {
+ root.querySelectorAll('iframe').forEach(iframe => {
+ if (!iframe.title) iframe.title = videoTitle || DEFAULT_VIDEO_TITLE;
+ });
+ root.querySelectorAll('*').forEach(el => {
+ if (el.shadowRoot) {
+ onShadowRoot?.(el.shadowRoot);
+ label(el.shadowRoot);
+ }
+ });
+ };
+ if (playerWrapperRef.current) label(playerWrapperRef.current);
+ };
+
+ // The provider inserts its iframe asynchronously, often before `onReady`
+ // fires, and may replace it later (e.g. on src change), so label it
+ // whenever it appears rather than waiting.
+ useEffect(() => {
+ const wrapper = playerWrapperRef.current;
+ if (!wrapper) return;
+ const observed = new WeakSet();
+ const observe = root => {
+ if (observed.has(root)) return;
+ observed.add(root);
+ observer.observe(root, {
+ childList: true,
+ subtree: true
+ });
+ };
+ const observer = new MutationObserver(() => labelProviderIframe(observe));
+ observe(wrapper);
+ labelProviderIframe(observe);
+ return () => observer.disconnect();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [videoTitle, videoUrl, isMounted]);
const isExternallyHostedVideoUrl = url => !!(url.match(/youtu(be\.com|\.be)/) || url.match(/vimeo\.com/));
const isExternallyHostedVideo = videoUrl => {
if (!videoUrl) return false;
if (typeof videoUrl === 'string') {
@@ -59,8 +101,9 @@
className: className,
overflow: "hidden",
position: "relative",
pt: '56.25%',
+ ref: playerWrapperRef,
width: "100%",
children: isMounted ? /*#__PURE__*/_jsx(ReactVideoPlayer, {
config: config,
controls: controls === undefined ? true : controls,
@@ -71,13 +114,14 @@
playIcon: /*#__PURE__*/_jsx(OverlayPlayButton, {
videoTitle: videoTitle
}),
playing: autoplay,
+ src: videoUrl,
title: videoTitle,
- url: videoUrl,
width: "100%",
onPlay: onPlay,
onReady: () => {
+ labelProviderIframe();
onReady?.();
setLoading(false);
}
}) : null
@@ -88,5 +132,14 @@
...props,
onLoad: () => setLoading(false)
})
});
-};
\ No newline at end of file
+};
+
+/*
+ Re-exported here (rather than from the main '@codecademy/gamut' barrel)
+ so Markdown's video/iframe overrides stay opt-in: importing them only
+ pulls in react-player if a consumer explicitly wants Video-in-markdown
+ rendering. See Markdown's videoOverride/iframeOverride props.
+*/
+export { Iframe } from '../Markdown/libs/overrides/Iframe';
+export { MarkdownVideo } from '../Markdown/libs/overrides/Video';
\ No newline at end of file