From cf396fd7b65e748ec2b6f8e066c1e485a15f3ec2 Mon Sep 17 00:00:00 2001 From: emily saffron Date: Wed, 14 Jan 2026 16:25:16 +0000 Subject: [PATCH 01/66] copy webcore --- .../MediaLoader/configs/overlayPlugin.js | 49 +++++++++ .../MediaLoader/configs/portraitClipMedia.ts | 18 ++++ src/app/components/MediaLoader/index.tsx | 3 + src/app/components/MediaLoader/types.ts | 4 +- .../MediaLoader/utils/buildSettings.ts | 2 + .../PortraitVideoCarousel/index.tsx | 11 ++ .../pluginCacheProvider.jsx | 24 +++++ .../PortraitVideoCarousel/resetCss.js | 69 ++++++++++++ .../PortraitVideoCarousel/videoOverlay.jsx | 100 ++++++++++++++++++ .../components/PortraitVideoModal/index.tsx | 3 + 10 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 src/app/components/MediaLoader/configs/overlayPlugin.js create mode 100644 src/app/components/PortraitVideoCarousel/pluginCacheProvider.jsx create mode 100644 src/app/components/PortraitVideoCarousel/resetCss.js create mode 100644 src/app/components/PortraitVideoCarousel/videoOverlay.jsx diff --git a/src/app/components/MediaLoader/configs/overlayPlugin.js b/src/app/components/MediaLoader/configs/overlayPlugin.js new file mode 100644 index 00000000000..cae3381c476 --- /dev/null +++ b/src/app/components/MediaLoader/configs/overlayPlugin.js @@ -0,0 +1,49 @@ +/* eslint-disable fp/no-class */ +/* eslint-disable import/no-commonjs */ + +class VideoOverlayPlugin { + constructor(utils, data = {}) { + this.utils = utils; + this.pluginData = data; + } + + pluginInitialisation(pluginUtils) { + pluginUtils.register({ name: 'videoOverlayPlugin' }); + this.playerInterface = pluginUtils.playerInterface; + + const { setPluginContainer, moveSubtitlesToTopWhenControlsActive = true } = + this.pluginData; + + const getReservedArea = () => { + return this.playerInterface.container + .querySelector('[data-region-exclude-subtitles]') + ?.getBoundingClientRect(); + }; + + this.playerInterface.addEventListener('uiControlBarShown', () => { + if (moveSubtitlesToTopWhenControlsActive) + this.playerInterface.addRegionFunction(getReservedArea); + }); + + this.playerInterface.addEventListener('uiControlBarHidden', () => { + if (moveSubtitlesToTopWhenControlsActive) + this.playerInterface.removeRegionFunction(getReservedArea); + }); + + if (setPluginContainer) { + setPluginContainer(this.playerInterface.container); + } + } +} + +var runPlugin = function (utils, data) { + const videoOverlayPlugin = new VideoOverlayPlugin(utils, data); + return videoOverlayPlugin; +}; + +// We need to export runPlugin for a unit test, but this will throw an error when loaded by SMP +try { + module.exports = { runPlugin }; +} catch { + /* no-op */ +} diff --git a/src/app/components/MediaLoader/configs/portraitClipMedia.ts b/src/app/components/MediaLoader/configs/portraitClipMedia.ts index 9eac78460db..ab5e95c74d4 100644 --- a/src/app/components/MediaLoader/configs/portraitClipMedia.ts +++ b/src/app/components/MediaLoader/configs/portraitClipMedia.ts @@ -9,10 +9,12 @@ import { ConfigBuilderReturnProps, PlaylistItem, } from '../types'; +import { useEffect, useRef } from 'react'; export default ({ blocks, basePlayerConfig, + setVideoOverlayContainer, }: ConfigBuilderProps): ConfigBuilderReturnProps => { const { model }: PortraitClipMediaBlock = filterForBlockType(blocks, 'portraitClipMedia') ?? {}; @@ -45,6 +47,11 @@ export default ({ isMobile = false; } } + const setVideoOverlayContainerRef = useRef(null); + + useEffect(() => { + setVideoOverlayContainerRef.current = setVideoOverlayContainer; + }); return { mediaType: 'video', @@ -67,6 +74,17 @@ export default ({ ], }, }), + plugins: { + toLoad: [ + { + html: 'https://static.files.bbci.co.uk/core/website/assets/static/scripts/smp/video-overlay-plugin.embed.869ac0e5834c1784f3ab.js', + playerOnly: true, // do not enable this plugin for old J2 version of the SMP player due to different UI + data: { + setPluginContainer: setVideoOverlayContainerRef.current, + }, + }, + ], + }, ui: { ...basePlayerConfig.ui, swipable: { diff --git a/src/app/components/MediaLoader/index.tsx b/src/app/components/MediaLoader/index.tsx index 657c4c027e2..eea8900d395 100644 --- a/src/app/components/MediaLoader/index.tsx +++ b/src/app/components/MediaLoader/index.tsx @@ -215,6 +215,7 @@ type Props = { embedded?: boolean; uniqueId?: string; eventMapping?: EventMapping; + setVideoOverlayContainer?: any; }; const MediaLoader = ({ @@ -223,6 +224,7 @@ const MediaLoader = ({ embedded, uniqueId, eventMapping, + setVideoOverlayContainer, }: Props) => { const { lang, service, translations } = use(ServiceContext); const { pageIdentifier } = use(EventTrackingContext); @@ -262,6 +264,7 @@ const MediaLoader = ({ adsEnabled, showAdsBasedOnLocation, embedded, + setVideoOverlayContainer, }); if (!config) return null; diff --git a/src/app/components/MediaLoader/types.ts b/src/app/components/MediaLoader/types.ts index 3baffa78d76..57cda5f98f5 100644 --- a/src/app/components/MediaLoader/types.ts +++ b/src/app/components/MediaLoader/types.ts @@ -78,7 +78,7 @@ export type PlayerConfig = { ui: PlayerUiConfig; playlistObject?: Playlist; plugins?: { - toLoad: { html: string; playerOnly?: boolean }[]; + toLoad: { html: string; playerOnly?: boolean; data: any }[]; }; }; @@ -121,6 +121,7 @@ export type ConfigBuilderProps = { embedUrl?: string; embedded?: boolean; lang: string; + setVideoOverlayContainer?: any; }; export type Orientations = 'landscape' | 'portrait'; @@ -365,4 +366,5 @@ export type BuildConfigProps = { adsEnabled?: boolean; showAdsBasedOnLocation?: boolean; embedded?: boolean; + setVideoOverlayContainer?: any; }; diff --git a/src/app/components/MediaLoader/utils/buildSettings.ts b/src/app/components/MediaLoader/utils/buildSettings.ts index f212670ee8f..9723e1ec890 100644 --- a/src/app/components/MediaLoader/utils/buildSettings.ts +++ b/src/app/components/MediaLoader/utils/buildSettings.ts @@ -36,6 +36,7 @@ const buildSettings = ({ adsEnabled = false, showAdsBasedOnLocation = false, embedded, + setVideoOverlayContainer, }: BuildConfigProps) => { const { model: mediaOverrides } = filterForBlockType(blocks, 'mediaOverrides') || {}; @@ -74,6 +75,7 @@ const buildSettings = ({ showAdsBasedOnLocation, embedded, lang, + setVideoOverlayContainer, }); if (!config) return null; diff --git a/src/app/components/PortraitVideoCarousel/index.tsx b/src/app/components/PortraitVideoCarousel/index.tsx index a09c31070e6..1908fbbc083 100644 --- a/src/app/components/PortraitVideoCarousel/index.tsx +++ b/src/app/components/PortraitVideoCarousel/index.tsx @@ -14,6 +14,8 @@ import PortraitCarouselNavigation from './PortraitVideoCarouselNavigation'; import Heading from '../Heading'; import PortraitVideoNoJs from './PortraitVideoNoJs'; import { PortraitClipMediaBlock } from '../MediaLoader/types'; +import { PluginCacheProvider } from './pluginCacheProvider'; +import VideoOverlay from './videoOverlay'; type PortraitVideoCarouselProps = { title: string; @@ -31,6 +33,7 @@ const PortraitVideoCarousel = ({ const [selectedVideoIndex, setSelectedVideoIndex] = useState( null, ); + const [videoOverlayContainer, setVideoOverlayContainer] = useState(); const { isLite, nonce } = use(RequestContext); @@ -122,9 +125,17 @@ const PortraitVideoCarousel = ({ onClose={handleCloseModal} nonce={nonce} eventTrackingData={eventTrackingDataExtended} + setVideoOverlayContainer={setVideoOverlayContainer} />, document.body, )} + {videoOverlayContainer && + createPortal( + + + , + videoOverlayContainer, + )} ); diff --git a/src/app/components/PortraitVideoCarousel/pluginCacheProvider.jsx b/src/app/components/PortraitVideoCarousel/pluginCacheProvider.jsx new file mode 100644 index 00000000000..29a472045c3 --- /dev/null +++ b/src/app/components/PortraitVideoCarousel/pluginCacheProvider.jsx @@ -0,0 +1,24 @@ +/* eslint-disable import/no-extraneous-dependencies */ +// eslint-disable-next-line no-unused-vars +import React, { useRef } from 'react'; +// eslint-disable-next-line no-restricted-imports +import createCache from '@emotion/cache'; +// eslint-disable-next-line no-restricted-imports +import { CacheProvider } from '@emotion/react'; + +const PluginCacheProvider = ({ children, container }) => { + const cacheRef = useRef(null); + + if (!cacheRef.current) { + const key = 'plugincss'; + const cache = createCache({ key, container }); + cache.compat = true; + + cacheRef.current = cache; + } + + return {children}; +}; + +// eslint-disable-next-line import/prefer-default-export +export { PluginCacheProvider }; diff --git a/src/app/components/PortraitVideoCarousel/resetCss.js b/src/app/components/PortraitVideoCarousel/resetCss.js new file mode 100644 index 00000000000..e468e3703a4 --- /dev/null +++ b/src/app/components/PortraitVideoCarousel/resetCss.js @@ -0,0 +1,69 @@ +const resetCss = ` +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +main, menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} + +article, aside, details, figcaption, figure, +footer, header, hgroup, main, menu, nav, section { + display: block; +} + +:root { + interpolate-size: allow-keywords; +} + +body { + line-height: 1; + text-size-adjust: none; +} + +ol, ul { + list-style: none; +} + +blockquote, q { + quotes: none; +} + +blockquote::before, blockquote::after, +q::before, q::after { + content: ''; + content: none; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +button { + appearance: none; + margin: 0; /* reset for safari */ + padding: 0; + background: none; + border-radius: 0; + border: 0; + font-family: inherit; + color: inherit; +} +`; + +// eslint-disable-next-line import/prefer-default-export +export { resetCss }; diff --git a/src/app/components/PortraitVideoCarousel/videoOverlay.jsx b/src/app/components/PortraitVideoCarousel/videoOverlay.jsx new file mode 100644 index 00000000000..b46dae8221b --- /dev/null +++ b/src/app/components/PortraitVideoCarousel/videoOverlay.jsx @@ -0,0 +1,100 @@ +import styled from '@emotion/styled'; +import { resetCss } from './resetCss'; + +const convertStringToNumber = val => parseInt(val, 10); + +const createSize = sizeInPixels => { + const size = + typeof sizeInPixels === 'string' + ? convertStringToNumber(sizeInPixels) + : sizeInPixels; + + return `${size / 16}rem`; +}; + +// See: https://confluence.dev.bbc.co.uk/display/mp/Writing+HTML5+Plugins +// Needs to be above the subtitles so the share-tools popover is not obscured by the subtitles. +const Z_INDEX_ABOVE_SMP_CONTROLS = '200'; +const SPACING_4 = '16px'; +const SPACING_2 = '8px'; +const SPACING_7 = '28px'; + +const VideoOverlayWrapper = styled.div` + z-index: ${Z_INDEX_ABOVE_SMP_CONTROLS}; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + container-type: inline-size; +`; + +const VideoOverlayFooterContents = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + gap: ${SPACING_4}; + max-width: ${createSize(1240)}; + margin: auto; + align-items: flex-end; +`; + +const VideoOverlayFooter = styled.div` + position: absolute; + left: ${SPACING_2}; + right: ${SPACING_2}; + bottom: 0; + padding-bottom: ${createSize(16)}; + opacity: ${({ controlsDisplayed }) => (controlsDisplayed ? '1' : '0')}; + + @supports not (container-type: inline-size) { + display: none; + } + + @container (min-width: ${createSize(320)}) { + padding-bottom: ${createSize(80)}; + left: ${SPACING_4}; + right: ${SPACING_4}; + } + + @container (min-width: ${createSize(500)}) { + left: ${SPACING_7}; + right: ${SPACING_7}; + } + + @container (min-width: ${createSize(860)}) { + padding-bottom: ${createSize(140)}; + left: ${SPACING_4}; + right: ${SPACING_4}; + } +`; + +const ShareToolComponent = () => { +
SHARE
; +}; + +const VideoOverlay = () => { + return ( + <> + {/* + Apply a CSS reset as this component is rendered inside of an SMP plugin using shadow-dom. + For webcore components to be displayed correctly, we must apply the CSS reset that is normally available on the page. + */} + + + + + + + + + + ); +}; + +export default VideoOverlay; diff --git a/src/app/components/PortraitVideoModal/index.tsx b/src/app/components/PortraitVideoModal/index.tsx index 750395d8a6b..2562cac662a 100644 --- a/src/app/components/PortraitVideoModal/index.tsx +++ b/src/app/components/PortraitVideoModal/index.tsx @@ -196,6 +196,7 @@ export interface PortraitVideoModalProps { selectedVideoIndex: number; nonce?: string | null; eventTrackingData: EventTrackingData; + setVideoOverlayContainer: any; } const PortraitVideoModal = ({ @@ -203,6 +204,7 @@ const PortraitVideoModal = ({ onClose, selectedVideoIndex, eventTrackingData, + setVideoOverlayContainer }: PortraitVideoModalProps) => { const { translations: { @@ -336,6 +338,7 @@ const PortraitVideoModal = ({ playlistLoadedCallback(e, blocks), pluginLoaded: pluginLoadedCallback, From a7ffec97f57fdcc2e5170e6f00398e794fc68d56 Mon Sep 17 00:00:00 2001 From: emily saffron Date: Thu, 15 Jan 2026 11:49:23 +0000 Subject: [PATCH 02/66] with logs --- .../MediaLoader/configs/portraitClipMedia.ts | 16 +++-- src/app/components/MediaLoader/index.tsx | 58 +++++++++++++------ .../PortraitVideoCarousel/index.tsx | 16 ++++- .../PortraitVideoCarousel/videoOverlay.jsx | 3 +- .../components/PortraitVideoModal/index.tsx | 2 +- 5 files changed, 66 insertions(+), 29 deletions(-) diff --git a/src/app/components/MediaLoader/configs/portraitClipMedia.ts b/src/app/components/MediaLoader/configs/portraitClipMedia.ts index ab5e95c74d4..92517d85556 100644 --- a/src/app/components/MediaLoader/configs/portraitClipMedia.ts +++ b/src/app/components/MediaLoader/configs/portraitClipMedia.ts @@ -47,12 +47,16 @@ export default ({ isMobile = false; } } - const setVideoOverlayContainerRef = useRef(null); - - useEffect(() => { - setVideoOverlayContainerRef.current = setVideoOverlayContainer; - }); + // useEffect(() => { + // console.log('in the effect ', setVideoOverlayContainer); + // setVideoOverlayContainerRef.current = setVideoOverlayContainer; + // }, [id]); + console.log( + 'setVideoOverlayContainerRef', + setVideoOverlayContainer, + setVideoOverlayContainer.current, + ); return { mediaType: 'video', playerConfig: { @@ -80,7 +84,7 @@ export default ({ html: 'https://static.files.bbci.co.uk/core/website/assets/static/scripts/smp/video-overlay-plugin.embed.869ac0e5834c1784f3ab.js', playerOnly: true, // do not enable this plugin for old J2 version of the SMP player due to different UI data: { - setPluginContainer: setVideoOverlayContainerRef.current, + setPluginContainer: setVideoOverlayContainer.current, }, }, ], diff --git a/src/app/components/MediaLoader/index.tsx b/src/app/components/MediaLoader/index.tsx index eea8900d395..93e9a2b2474 100644 --- a/src/app/components/MediaLoader/index.tsx +++ b/src/app/components/MediaLoader/index.tsx @@ -1,4 +1,4 @@ -import { use, useEffect, useRef, useState } from 'react'; +import { use, useEffect, useMemo, useRef, useState } from 'react'; import { Helmet } from 'react-helmet'; import { RequestContext } from '#contexts/RequestContext'; import { MEDIA_PLAYER_STATUS } from '#app/lib/logger.const'; @@ -244,28 +244,48 @@ const MediaLoader = ({ !PAGETYPES_IGNORE_PLACEHOLDER.includes(pageType), ); - if (isLite) return null; - const { model: mediaOverrides } = filterForBlockType(blocks, 'mediaOverrides') || {}; const producer = getProducerFromServiceName(service); - const config = buildConfig({ - id: id || '', - blocks, - counterName: mediaOverrides?.pageIdentifierOverride || pageIdentifier, - statsDestination, - producer, - isAmp, - lang, - pageType, - service, - translations, - adsEnabled, - showAdsBasedOnLocation, - embedded, - setVideoOverlayContainer, - }); + const config = useMemo( + () => + buildConfig({ + id: id || '', + blocks, + counterName: mediaOverrides?.pageIdentifierOverride || pageIdentifier, + statsDestination, + producer, + isAmp, + lang, + pageType, + service, + translations, + adsEnabled, + showAdsBasedOnLocation, + embedded, + setVideoOverlayContainer, + }), + [ + id, + blocks, + mediaOverrides?.pageIdentifierOverride, + pageIdentifier, + statsDestination, + producer, + isAmp, + lang, + pageType, + service, + translations, + adsEnabled, + showAdsBasedOnLocation, + embedded, + setVideoOverlayContainer, + ], + ); + + if (isLite) return null; if (!config) return null; diff --git a/src/app/components/PortraitVideoCarousel/index.tsx b/src/app/components/PortraitVideoCarousel/index.tsx index 1908fbbc083..d9010806aaf 100644 --- a/src/app/components/PortraitVideoCarousel/index.tsx +++ b/src/app/components/PortraitVideoCarousel/index.tsx @@ -1,4 +1,4 @@ -import { use, useRef, useState } from 'react'; +import { use, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { RequestContext } from '#app/contexts/RequestContext'; import useViewTracker from '#app/hooks/useViewTracker'; @@ -35,6 +35,18 @@ const PortraitVideoCarousel = ({ ); const [videoOverlayContainer, setVideoOverlayContainer] = useState(); + const setVideoOverlayContainerRef = useRef(setVideoOverlayContainer); + const count = useRef(0); + + useEffect(() => { + count.current += 1; + console.log( + 'PortraitVideoCarousel rendered times: ', + count.current, + videoOverlayContainer, + ); + }); + console.log('CONTAINER ', videoOverlayContainer); const { isLite, nonce } = use(RequestContext); // EXPERIMENT: Homepage Portrait Video 2 @@ -125,7 +137,7 @@ const PortraitVideoCarousel = ({ onClose={handleCloseModal} nonce={nonce} eventTrackingData={eventTrackingDataExtended} - setVideoOverlayContainer={setVideoOverlayContainer} + setVideoOverlayContainer={setVideoOverlayContainerRef} />, document.body, )} diff --git a/src/app/components/PortraitVideoCarousel/videoOverlay.jsx b/src/app/components/PortraitVideoCarousel/videoOverlay.jsx index b46dae8221b..c1ca2d41d89 100644 --- a/src/app/components/PortraitVideoCarousel/videoOverlay.jsx +++ b/src/app/components/PortraitVideoCarousel/videoOverlay.jsx @@ -88,7 +88,8 @@ const VideoOverlay = () => { // The video-overlay plugin will use this attribute to instruct SMP not to render subtitles in the space occupied by this div. data-region-exclude-subtitles > - + + hello diff --git a/src/app/components/PortraitVideoModal/index.tsx b/src/app/components/PortraitVideoModal/index.tsx index 2562cac662a..3ac0c114724 100644 --- a/src/app/components/PortraitVideoModal/index.tsx +++ b/src/app/components/PortraitVideoModal/index.tsx @@ -196,7 +196,7 @@ export interface PortraitVideoModalProps { selectedVideoIndex: number; nonce?: string | null; eventTrackingData: EventTrackingData; - setVideoOverlayContainer: any; + setVideoOverlayContainer?: any; } const PortraitVideoModal = ({ From cc2e36fcdd4391a5955b5c6c6531120f091e0b45 Mon Sep 17 00:00:00 2001 From: emily saffron Date: Thu, 15 Jan 2026 12:01:35 +0000 Subject: [PATCH 03/66] fix build --- src/app/components/MediaLoader/configs/portraitClipMedia.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/components/MediaLoader/configs/portraitClipMedia.ts b/src/app/components/MediaLoader/configs/portraitClipMedia.ts index 92517d85556..1bfbbf1236b 100644 --- a/src/app/components/MediaLoader/configs/portraitClipMedia.ts +++ b/src/app/components/MediaLoader/configs/portraitClipMedia.ts @@ -9,7 +9,6 @@ import { ConfigBuilderReturnProps, PlaylistItem, } from '../types'; -import { useEffect, useRef } from 'react'; export default ({ blocks, From c105ad87e7bbecab45662bac480306c5b4dc3814 Mon Sep 17 00:00:00 2001 From: emily saffron Date: Thu, 15 Jan 2026 12:45:17 +0000 Subject: [PATCH 04/66] memoise modal --- .../components/PortraitVideoModal/index.tsx | 146 +++++++++--------- 1 file changed, 77 insertions(+), 69 deletions(-) diff --git a/src/app/components/PortraitVideoModal/index.tsx b/src/app/components/PortraitVideoModal/index.tsx index 3ac0c114724..1e3863b6e5b 100644 --- a/src/app/components/PortraitVideoModal/index.tsx +++ b/src/app/components/PortraitVideoModal/index.tsx @@ -1,5 +1,5 @@ import { Global } from '@emotion/react'; -import { use, useEffect, useRef } from 'react'; +import { use, useEffect, useMemo, useRef } from 'react'; import moment from 'moment-timezone'; import MediaLoader from '#app/components/MediaLoader'; import { @@ -204,7 +204,7 @@ const PortraitVideoModal = ({ onClose, selectedVideoIndex, eventTrackingData, - setVideoOverlayContainer + setVideoOverlayContainer, }: PortraitVideoModalProps) => { const { translations: { @@ -288,85 +288,93 @@ const PortraitVideoModal = ({ }; }, [onClose]); - return ( - <> - - - + + ), + [], ); }; From 7783bf3d13489bba33649688a03c15032298eeac Mon Sep 17 00:00:00 2001 From: emily saffron Date: Thu, 15 Jan 2026 14:32:58 +0000 Subject: [PATCH 05/66] remove unused memo --- src/app/components/MediaLoader/index.tsx | 52 ++++++------------- .../PortraitVideoCarousel/videoOverlay.jsx | 11 ++-- 2 files changed, 22 insertions(+), 41 deletions(-) diff --git a/src/app/components/MediaLoader/index.tsx b/src/app/components/MediaLoader/index.tsx index 93e9a2b2474..cddec95e9eb 100644 --- a/src/app/components/MediaLoader/index.tsx +++ b/src/app/components/MediaLoader/index.tsx @@ -248,42 +248,22 @@ const MediaLoader = ({ filterForBlockType(blocks, 'mediaOverrides') || {}; const producer = getProducerFromServiceName(service); - const config = useMemo( - () => - buildConfig({ - id: id || '', - blocks, - counterName: mediaOverrides?.pageIdentifierOverride || pageIdentifier, - statsDestination, - producer, - isAmp, - lang, - pageType, - service, - translations, - adsEnabled, - showAdsBasedOnLocation, - embedded, - setVideoOverlayContainer, - }), - [ - id, - blocks, - mediaOverrides?.pageIdentifierOverride, - pageIdentifier, - statsDestination, - producer, - isAmp, - lang, - pageType, - service, - translations, - adsEnabled, - showAdsBasedOnLocation, - embedded, - setVideoOverlayContainer, - ], - ); + const config = buildConfig({ + id: id || '', + blocks, + counterName: mediaOverrides?.pageIdentifierOverride || pageIdentifier, + statsDestination, + producer, + isAmp, + lang, + pageType, + service, + translations, + adsEnabled, + showAdsBasedOnLocation, + embedded, + setVideoOverlayContainer, + }); if (isLite) return null; diff --git a/src/app/components/PortraitVideoCarousel/videoOverlay.jsx b/src/app/components/PortraitVideoCarousel/videoOverlay.jsx index c1ca2d41d89..5a3bdd8e09e 100644 --- a/src/app/components/PortraitVideoCarousel/videoOverlay.jsx +++ b/src/app/components/PortraitVideoCarousel/videoOverlay.jsx @@ -69,9 +69,11 @@ const VideoOverlayFooter = styled.div` } `; -const ShareToolComponent = () => { -
SHARE
; -}; +const ShareTool = styled.div` + margin-left: auto; + pointer-events: auto; + color: pink; +`; const VideoOverlay = () => { return ( @@ -89,8 +91,7 @@ const VideoOverlay = () => { data-region-exclude-subtitles > - hello - + SHARE From dc81069ddc8a6181774312a6533641f821618180 Mon Sep 17 00:00:00 2001 From: emily saffron Date: Tue, 20 Jan 2026 09:22:21 +0000 Subject: [PATCH 06/66] remove need for memo and fix autpplay --- .../MediaLoader/configs/portraitClipMedia.ts | 22 --- src/app/components/MediaLoader/index.tsx | 30 +++- src/app/components/MediaLoader/types.ts | 5 +- .../MediaLoader/utils/buildSettings.ts | 2 - .../PortraitVideoCarousel/index.tsx | 25 +-- .../components/PortraitVideoModal/index.tsx | 145 ++++++++---------- 6 files changed, 95 insertions(+), 134 deletions(-) diff --git a/src/app/components/MediaLoader/configs/portraitClipMedia.ts b/src/app/components/MediaLoader/configs/portraitClipMedia.ts index 1bfbbf1236b..7549fa35f2e 100644 --- a/src/app/components/MediaLoader/configs/portraitClipMedia.ts +++ b/src/app/components/MediaLoader/configs/portraitClipMedia.ts @@ -13,7 +13,6 @@ import { export default ({ blocks, basePlayerConfig, - setVideoOverlayContainer, }: ConfigBuilderProps): ConfigBuilderReturnProps => { const { model }: PortraitClipMediaBlock = filterForBlockType(blocks, 'portraitClipMedia') ?? {}; @@ -46,16 +45,6 @@ export default ({ isMobile = false; } } - - // useEffect(() => { - // console.log('in the effect ', setVideoOverlayContainer); - // setVideoOverlayContainerRef.current = setVideoOverlayContainer; - // }, [id]); - console.log( - 'setVideoOverlayContainerRef', - setVideoOverlayContainer, - setVideoOverlayContainer.current, - ); return { mediaType: 'video', playerConfig: { @@ -77,17 +66,6 @@ export default ({ ], }, }), - plugins: { - toLoad: [ - { - html: 'https://static.files.bbci.co.uk/core/website/assets/static/scripts/smp/video-overlay-plugin.embed.869ac0e5834c1784f3ab.js', - playerOnly: true, // do not enable this plugin for old J2 version of the SMP player due to different UI - data: { - setPluginContainer: setVideoOverlayContainer.current, - }, - }, - ], - }, ui: { ...basePlayerConfig.ui, swipable: { diff --git a/src/app/components/MediaLoader/index.tsx b/src/app/components/MediaLoader/index.tsx index cddec95e9eb..8a12cf385ba 100644 --- a/src/app/components/MediaLoader/index.tsx +++ b/src/app/components/MediaLoader/index.tsx @@ -1,4 +1,4 @@ -import { use, useEffect, useMemo, useRef, useState } from 'react'; +import { use, useEffect, useRef, useState } from 'react'; import { Helmet } from 'react-helmet'; import { RequestContext } from '#contexts/RequestContext'; import { MEDIA_PLAYER_STATUS } from '#app/lib/logger.const'; @@ -12,6 +12,7 @@ import { import filterForBlockType from '#lib/utilities/blockHandlers'; import { PageTypes } from '#app/models/types/global'; import { EventTrackingContext } from '#app/contexts/EventTrackingContext'; +import { createPortal } from 'react-dom'; import { BumpType, EventMapping, @@ -30,6 +31,8 @@ import { getBootstrapSrc } from '../Ad/Canonical'; import Metadata from './Metadata'; import AmpMediaLoader from './Amp'; import Message from './Message'; +import VideoOverlay from '../PortraitVideoCarousel/videoOverlay'; +import { PluginCacheProvider } from '../PortraitVideoCarousel/pluginCacheProvider'; const PAGETYPES_IGNORE_PLACEHOLDER: PageTypes[] = [ MEDIA_ARTICLE_PAGE, @@ -119,6 +122,8 @@ const MediaContainer = ({ noJsMessage, eventMapping, }: MediaContainerProps) => { + const [videoOverlayContainer, setVideoOverlayContainer] = useState(); + const setVideoOverlayContainerRef = useRef(setVideoOverlayContainer); const playerElementRef = useRef(null); const isAudio = isAudioPlayer(playerConfig); @@ -183,7 +188,18 @@ const MediaContainer = ({ } }); } - + if (setVideoOverlayContainerRef.current) { + mediaPlayer.loadPlugin( + { + html: 'https://static.files.bbci.co.uk/core/website/assets/static/scripts/smp/video-overlay-plugin.embed.869ac0e5834c1784f3ab.js', + playerOnly: true as any, // do not enable this plugin for old J2 version of the SMP player due to different UI }, + waitOnPluginLoad: true as any, + }, + { + setPluginContainer: setVideoOverlayContainerRef.current, + } as any, + ); + } mediaPlayer.load(); }; @@ -205,6 +221,13 @@ const MediaContainer = ({ + {videoOverlayContainer && + createPortal( + + + , + videoOverlayContainer, + )} ); }; @@ -215,7 +238,6 @@ type Props = { embedded?: boolean; uniqueId?: string; eventMapping?: EventMapping; - setVideoOverlayContainer?: any; }; const MediaLoader = ({ @@ -224,7 +246,6 @@ const MediaLoader = ({ embedded, uniqueId, eventMapping, - setVideoOverlayContainer, }: Props) => { const { lang, service, translations } = use(ServiceContext); const { pageIdentifier } = use(EventTrackingContext); @@ -262,7 +283,6 @@ const MediaLoader = ({ adsEnabled, showAdsBasedOnLocation, embedded, - setVideoOverlayContainer, }); if (isLite) return null; diff --git a/src/app/components/MediaLoader/types.ts b/src/app/components/MediaLoader/types.ts index 57cda5f98f5..3b967d941e0 100644 --- a/src/app/components/MediaLoader/types.ts +++ b/src/app/components/MediaLoader/types.ts @@ -78,7 +78,7 @@ export type PlayerConfig = { ui: PlayerUiConfig; playlistObject?: Playlist; plugins?: { - toLoad: { html: string; playerOnly?: boolean; data: any }[]; + toLoad: { html: string; playerOnly?: boolean; data?: any }[]; }; }; @@ -168,7 +168,7 @@ export type Player = { parameters?: { name: string; data: { - adTag: string; + adTag?: string; }; }, ) => void; @@ -366,5 +366,4 @@ export type BuildConfigProps = { adsEnabled?: boolean; showAdsBasedOnLocation?: boolean; embedded?: boolean; - setVideoOverlayContainer?: any; }; diff --git a/src/app/components/MediaLoader/utils/buildSettings.ts b/src/app/components/MediaLoader/utils/buildSettings.ts index 9723e1ec890..f212670ee8f 100644 --- a/src/app/components/MediaLoader/utils/buildSettings.ts +++ b/src/app/components/MediaLoader/utils/buildSettings.ts @@ -36,7 +36,6 @@ const buildSettings = ({ adsEnabled = false, showAdsBasedOnLocation = false, embedded, - setVideoOverlayContainer, }: BuildConfigProps) => { const { model: mediaOverrides } = filterForBlockType(blocks, 'mediaOverrides') || {}; @@ -75,7 +74,6 @@ const buildSettings = ({ showAdsBasedOnLocation, embedded, lang, - setVideoOverlayContainer, }); if (!config) return null; diff --git a/src/app/components/PortraitVideoCarousel/index.tsx b/src/app/components/PortraitVideoCarousel/index.tsx index d9010806aaf..a09c31070e6 100644 --- a/src/app/components/PortraitVideoCarousel/index.tsx +++ b/src/app/components/PortraitVideoCarousel/index.tsx @@ -1,4 +1,4 @@ -import { use, useEffect, useRef, useState } from 'react'; +import { use, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { RequestContext } from '#app/contexts/RequestContext'; import useViewTracker from '#app/hooks/useViewTracker'; @@ -14,8 +14,6 @@ import PortraitCarouselNavigation from './PortraitVideoCarouselNavigation'; import Heading from '../Heading'; import PortraitVideoNoJs from './PortraitVideoNoJs'; import { PortraitClipMediaBlock } from '../MediaLoader/types'; -import { PluginCacheProvider } from './pluginCacheProvider'; -import VideoOverlay from './videoOverlay'; type PortraitVideoCarouselProps = { title: string; @@ -33,20 +31,7 @@ const PortraitVideoCarousel = ({ const [selectedVideoIndex, setSelectedVideoIndex] = useState( null, ); - const [videoOverlayContainer, setVideoOverlayContainer] = useState(); - const setVideoOverlayContainerRef = useRef(setVideoOverlayContainer); - const count = useRef(0); - - useEffect(() => { - count.current += 1; - console.log( - 'PortraitVideoCarousel rendered times: ', - count.current, - videoOverlayContainer, - ); - }); - console.log('CONTAINER ', videoOverlayContainer); const { isLite, nonce } = use(RequestContext); // EXPERIMENT: Homepage Portrait Video 2 @@ -137,17 +122,9 @@ const PortraitVideoCarousel = ({ onClose={handleCloseModal} nonce={nonce} eventTrackingData={eventTrackingDataExtended} - setVideoOverlayContainer={setVideoOverlayContainerRef} />, document.body, )} - {videoOverlayContainer && - createPortal( - - - , - videoOverlayContainer, - )} ); diff --git a/src/app/components/PortraitVideoModal/index.tsx b/src/app/components/PortraitVideoModal/index.tsx index 1e3863b6e5b..750395d8a6b 100644 --- a/src/app/components/PortraitVideoModal/index.tsx +++ b/src/app/components/PortraitVideoModal/index.tsx @@ -1,5 +1,5 @@ import { Global } from '@emotion/react'; -import { use, useEffect, useMemo, useRef } from 'react'; +import { use, useEffect, useRef } from 'react'; import moment from 'moment-timezone'; import MediaLoader from '#app/components/MediaLoader'; import { @@ -196,7 +196,6 @@ export interface PortraitVideoModalProps { selectedVideoIndex: number; nonce?: string | null; eventTrackingData: EventTrackingData; - setVideoOverlayContainer?: any; } const PortraitVideoModal = ({ @@ -204,7 +203,6 @@ const PortraitVideoModal = ({ onClose, selectedVideoIndex, eventTrackingData, - setVideoOverlayContainer, }: PortraitVideoModalProps) => { const { translations: { @@ -288,93 +286,84 @@ const PortraitVideoModal = ({ }; }, [onClose]); - return useMemo( - () => ( - <> - - ); }; diff --git a/src/app/components/PortraitVideoCarousel/index.tsx b/src/app/components/PortraitVideoCarousel/index.tsx index e966a11063f..01437689da7 100644 --- a/src/app/components/PortraitVideoCarousel/index.tsx +++ b/src/app/components/PortraitVideoCarousel/index.tsx @@ -36,7 +36,7 @@ const PortraitVideoCarousel = ({ const [videoOverlayContainer, setVideoOverlayContainer] = useState(null); const setVideoOverlayContainerRef = useRef(setVideoOverlayContainer); - console.log('reloaded'); + console.log('PV CAROUSEL: reloaded'); const { isLite, nonce } = use(RequestContext); @@ -78,6 +78,10 @@ const PortraitVideoCarousel = ({ }; let shareUrlPath; if (selectedVideoIndex !== null) { + console.log( + 'blocks', + blocks?.[selectedVideoIndex]?.model?.video?.id.split(':'), + ); shareUrlPath = blocks?.[selectedVideoIndex]?.model?.video?.id.split(':')[4]; } diff --git a/src/app/components/PortraitVideoCarousel/videoOverlay.jsx b/src/app/components/PortraitVideoCarousel/videoOverlay.jsx index 8b887cb1f1b..a97788cde75 100644 --- a/src/app/components/PortraitVideoCarousel/videoOverlay.jsx +++ b/src/app/components/PortraitVideoCarousel/videoOverlay.jsx @@ -1,5 +1,6 @@ import styled from '@emotion/styled'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, use } from 'react'; +import { ServiceContext } from '#contexts/ServiceContext'; import { resetCss } from './resetCss'; const convertStringToNumber = val => parseInt(val, 10); @@ -81,14 +82,17 @@ const ShareToolWrapper = styled.div` pointer-events: auto; `; -const ShareToolComponent = shareUrlPath => { +const ShareToolComponent = (shareUrlPath, service) => { const [shareUrl, setShareUrl] = useState(); useEffect(() => { - const baseUrl = 'window.location.host'; - setShareUrl(`https://${baseUrl}/articles/${shareUrlPath}`); - console.log('baseUrl:', baseUrl); - console.log('shareUrl:', shareUrl); + setShareUrl(`https://${service}/articles/${shareUrlPath}`); + console.log( + 'VIDEO OVERLAY Share tool: service:', + service, + 'shareUrl:', + shareUrl, + ); }, [shareUrlPath]); return ( @@ -100,7 +104,9 @@ const ShareToolComponent = shareUrlPath => { }; const VideoOverlay = shareUrlPath => { - console.log(shareUrlPath); + console.log('VIDEO OVERLAY: shareUrlPath:', shareUrlPath); + const { service } = use(ServiceContext); + return ( <> {/* @@ -116,7 +122,7 @@ const VideoOverlay = shareUrlPath => { data-region-exclude-subtitles > - + SHARE From 75288f00df890275beb31dbb780b712ee2e4cbb8 Mon Sep 17 00:00:00 2001 From: emily saffron Date: Tue, 20 Jan 2026 14:47:10 +0000 Subject: [PATCH 10/66] remove unnecessary things --- .../MediaLoader/configs/overlayPlugin.js | 49 ------------------- .../MediaLoader/configs/portraitClipMedia.ts | 1 + 2 files changed, 1 insertion(+), 49 deletions(-) delete mode 100644 src/app/components/MediaLoader/configs/overlayPlugin.js diff --git a/src/app/components/MediaLoader/configs/overlayPlugin.js b/src/app/components/MediaLoader/configs/overlayPlugin.js deleted file mode 100644 index cae3381c476..00000000000 --- a/src/app/components/MediaLoader/configs/overlayPlugin.js +++ /dev/null @@ -1,49 +0,0 @@ -/* eslint-disable fp/no-class */ -/* eslint-disable import/no-commonjs */ - -class VideoOverlayPlugin { - constructor(utils, data = {}) { - this.utils = utils; - this.pluginData = data; - } - - pluginInitialisation(pluginUtils) { - pluginUtils.register({ name: 'videoOverlayPlugin' }); - this.playerInterface = pluginUtils.playerInterface; - - const { setPluginContainer, moveSubtitlesToTopWhenControlsActive = true } = - this.pluginData; - - const getReservedArea = () => { - return this.playerInterface.container - .querySelector('[data-region-exclude-subtitles]') - ?.getBoundingClientRect(); - }; - - this.playerInterface.addEventListener('uiControlBarShown', () => { - if (moveSubtitlesToTopWhenControlsActive) - this.playerInterface.addRegionFunction(getReservedArea); - }); - - this.playerInterface.addEventListener('uiControlBarHidden', () => { - if (moveSubtitlesToTopWhenControlsActive) - this.playerInterface.removeRegionFunction(getReservedArea); - }); - - if (setPluginContainer) { - setPluginContainer(this.playerInterface.container); - } - } -} - -var runPlugin = function (utils, data) { - const videoOverlayPlugin = new VideoOverlayPlugin(utils, data); - return videoOverlayPlugin; -}; - -// We need to export runPlugin for a unit test, but this will throw an error when loaded by SMP -try { - module.exports = { runPlugin }; -} catch { - /* no-op */ -} diff --git a/src/app/components/MediaLoader/configs/portraitClipMedia.ts b/src/app/components/MediaLoader/configs/portraitClipMedia.ts index 7549fa35f2e..9eac78460db 100644 --- a/src/app/components/MediaLoader/configs/portraitClipMedia.ts +++ b/src/app/components/MediaLoader/configs/portraitClipMedia.ts @@ -45,6 +45,7 @@ export default ({ isMobile = false; } } + return { mediaType: 'video', playerConfig: { From 2e6fbc823770e81bfc1c1d262e1176e30ed3f10e Mon Sep 17 00:00:00 2001 From: emily saffron Date: Tue, 20 Jan 2026 14:57:42 +0000 Subject: [PATCH 11/66] return is lite check to correct place --- src/app/components/MediaLoader/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/MediaLoader/index.tsx b/src/app/components/MediaLoader/index.tsx index 30b58eb3d74..2c7ded23afc 100644 --- a/src/app/components/MediaLoader/index.tsx +++ b/src/app/components/MediaLoader/index.tsx @@ -272,6 +272,8 @@ const MediaLoader = ({ !PAGETYPES_IGNORE_PLACEHOLDER.includes(pageType), ); + if (isLite) return null; + const { model: mediaOverrides } = filterForBlockType(blocks, 'mediaOverrides') || {}; @@ -292,8 +294,6 @@ const MediaLoader = ({ embedded, }); - if (isLite) return null; - if (!config) return null; const { From 6d1074f435be97f29b8ce7273057a1dd2419776b Mon Sep 17 00:00:00 2001 From: emily saffron Date: Tue, 20 Jan 2026 15:00:43 +0000 Subject: [PATCH 12/66] rename shareurl --- src/app/components/PortraitVideoCarousel/index.tsx | 6 +++--- .../PortraitVideoCarousel/videoOverlay.jsx | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app/components/PortraitVideoCarousel/index.tsx b/src/app/components/PortraitVideoCarousel/index.tsx index 01437689da7..0fd8ffa5f86 100644 --- a/src/app/components/PortraitVideoCarousel/index.tsx +++ b/src/app/components/PortraitVideoCarousel/index.tsx @@ -76,13 +76,13 @@ const PortraitVideoCarousel = ({ setIsModalOpen(false); setSelectedVideoIndex(null); }; - let shareUrlPath; + let urn; if (selectedVideoIndex !== null) { console.log( 'blocks', blocks?.[selectedVideoIndex]?.model?.video?.id.split(':'), ); - shareUrlPath = blocks?.[selectedVideoIndex]?.model?.video?.id.split(':')[4]; + urn = blocks?.[selectedVideoIndex]?.model?.video?.id.split(':')[4]; } return ( @@ -143,7 +143,7 @@ const PortraitVideoCarousel = ({ {videoOverlayContainer && createPortal( - + , videoOverlayContainer, )} diff --git a/src/app/components/PortraitVideoCarousel/videoOverlay.jsx b/src/app/components/PortraitVideoCarousel/videoOverlay.jsx index a97788cde75..a9a9b710ff8 100644 --- a/src/app/components/PortraitVideoCarousel/videoOverlay.jsx +++ b/src/app/components/PortraitVideoCarousel/videoOverlay.jsx @@ -82,18 +82,18 @@ const ShareToolWrapper = styled.div` pointer-events: auto; `; -const ShareToolComponent = (shareUrlPath, service) => { +const ShareToolComponent = (urn, service) => { const [shareUrl, setShareUrl] = useState(); useEffect(() => { - setShareUrl(`https://${service}/articles/${shareUrlPath}`); + setShareUrl(`https://${service}/articles/${urn}`); console.log( 'VIDEO OVERLAY Share tool: service:', service, 'shareUrl:', shareUrl, ); - }, [shareUrlPath]); + }, [urn]); return ( @@ -103,8 +103,8 @@ const ShareToolComponent = (shareUrlPath, service) => { ); }; -const VideoOverlay = shareUrlPath => { - console.log('VIDEO OVERLAY: shareUrlPath:', shareUrlPath); +const VideoOverlay = urn => { + console.log('VIDEO OVERLAY: urn:', urn); const { service } = use(ServiceContext); return ( @@ -122,7 +122,7 @@ const VideoOverlay = shareUrlPath => { data-region-exclude-subtitles > - + SHARE From 92d3c4004da390e2afd51d9f1a398482e48edf9f Mon Sep 17 00:00:00 2001 From: emily saffron Date: Wed, 21 Jan 2026 08:52:55 +0000 Subject: [PATCH 13/66] use fabl to fetch shareUrl --- .../PortraitVideoCarousel/index.tsx | 13 +++------ .../PortraitVideoCarousel/videoOverlay.jsx | 27 +++++++------------ 2 files changed, 12 insertions(+), 28 deletions(-) diff --git a/src/app/components/PortraitVideoCarousel/index.tsx b/src/app/components/PortraitVideoCarousel/index.tsx index 0fd8ffa5f86..fa3c75eadaf 100644 --- a/src/app/components/PortraitVideoCarousel/index.tsx +++ b/src/app/components/PortraitVideoCarousel/index.tsx @@ -76,15 +76,7 @@ const PortraitVideoCarousel = ({ setIsModalOpen(false); setSelectedVideoIndex(null); }; - let urn; - if (selectedVideoIndex !== null) { - console.log( - 'blocks', - blocks?.[selectedVideoIndex]?.model?.video?.id.split(':'), - ); - urn = blocks?.[selectedVideoIndex]?.model?.video?.id.split(':')[4]; - } - + console.log('selectedVideoIndex', selectedVideoIndex); return ( <> @@ -141,9 +133,10 @@ const PortraitVideoCarousel = ({ document.body, )} {videoOverlayContainer && + selectedVideoIndex !== null && createPortal( - + , videoOverlayContainer, )} diff --git a/src/app/components/PortraitVideoCarousel/videoOverlay.jsx b/src/app/components/PortraitVideoCarousel/videoOverlay.jsx index a9a9b710ff8..f0a8e678fb9 100644 --- a/src/app/components/PortraitVideoCarousel/videoOverlay.jsx +++ b/src/app/components/PortraitVideoCarousel/videoOverlay.jsx @@ -1,6 +1,5 @@ import styled from '@emotion/styled'; -import { useEffect, useState, use } from 'react'; -import { ServiceContext } from '#contexts/ServiceContext'; +import { useEffect, useState } from 'react'; import { resetCss } from './resetCss'; const convertStringToNumber = val => parseInt(val, 10); @@ -82,20 +81,14 @@ const ShareToolWrapper = styled.div` pointer-events: auto; `; -const ShareToolComponent = (urn, service) => { +const ShareToolComponent = ({ shareUrlPath }) => { const [shareUrl, setShareUrl] = useState(); useEffect(() => { - setShareUrl(`https://${service}/articles/${urn}`); - console.log( - 'VIDEO OVERLAY Share tool: service:', - service, - 'shareUrl:', - shareUrl, - ); - }, [urn]); + setShareUrl(`https://bbc.com${shareUrlPath}`); + }, [shareUrlPath]); return ( - + SHARE @@ -103,9 +96,9 @@ const ShareToolComponent = (urn, service) => { ); }; -const VideoOverlay = urn => { - console.log('VIDEO OVERLAY: urn:', urn); - const { service } = use(ServiceContext); +const VideoOverlay = ({ blocks, index }) => { + const currentItem = blocks?.[index]; + const { shareUrl } = currentItem?.model?.video || {}; return ( <> @@ -122,9 +115,7 @@ const VideoOverlay = urn => { data-region-exclude-subtitles > - - SHARE - + From 895ea4cda546d3cd863acacfec622db50544e35d Mon Sep 17 00:00:00 2001 From: emily saffron Date: Wed, 21 Jan 2026 10:04:31 +0000 Subject: [PATCH 14/66] update share link on each swipe/click to next pv --- src/app/components/MediaLoader/types.ts | 2 + .../PortraitVideoCarousel/index.tsx | 19 ++++++++-- .../PortraitVideoCarousel/videoOverlay.jsx | 3 +- .../components/PortraitVideoModal/index.tsx | 38 ++++++++++++++++--- 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/app/components/MediaLoader/types.ts b/src/app/components/MediaLoader/types.ts index 3b967d941e0..8a6b2314084 100644 --- a/src/app/components/MediaLoader/types.ts +++ b/src/app/components/MediaLoader/types.ts @@ -34,6 +34,7 @@ export type Playlist = { summary?: string; holdingImageURL?: string; items: PlaylistItem[] | LegacyPlayListItem[]; + shareUrl?: string | null; guidance?: string; embedRights?: 'allowed'; liveRewind?: boolean; @@ -276,6 +277,7 @@ export type PortraitClipMediaBlock = { altText?: string; }[]; video: { + shareUrl: string | null; id: string; title: string; holdingImageURL?: string; diff --git a/src/app/components/PortraitVideoCarousel/index.tsx b/src/app/components/PortraitVideoCarousel/index.tsx index fa3c75eadaf..6ac77303bca 100644 --- a/src/app/components/PortraitVideoCarousel/index.tsx +++ b/src/app/components/PortraitVideoCarousel/index.tsx @@ -36,7 +36,9 @@ const PortraitVideoCarousel = ({ const [videoOverlayContainer, setVideoOverlayContainer] = useState(null); const setVideoOverlayContainerRef = useRef(setVideoOverlayContainer); - console.log('PV CAROUSEL: reloaded'); + const [currentItem, setCurrentItem] = useState< + PortraitClipMediaBlock | undefined + >(); const { isLite, nonce } = use(RequestContext); @@ -65,9 +67,20 @@ const PortraitVideoCarousel = ({ if (isLite) return null; + const onMediaChanged = ({ item }) => { + console.log('item', item); + const newIdentifier = item?.model?.video?.id; + const index = blocks.findIndex(({ model }) => { + return model.video.id === newIdentifier; + }); + const newItem = blocks[index]; + if (newItem) setCurrentItem(newItem); + }; + const handlePromoClick = (index: number) => { if (blocks?.[index]?.model?.video) { setSelectedVideoIndex(index); + setCurrentItem(blocks[index]); setIsModalOpen(true); } }; @@ -76,7 +89,6 @@ const PortraitVideoCarousel = ({ setIsModalOpen(false); setSelectedVideoIndex(null); }; - console.log('selectedVideoIndex', selectedVideoIndex); return ( <> @@ -129,6 +141,7 @@ const PortraitVideoCarousel = ({ nonce={nonce} eventTrackingData={eventTrackingDataExtended} setVideoOverlayContainerRef={setVideoOverlayContainerRef} + onMediaChanged={onMediaChanged} />, document.body, )} @@ -136,7 +149,7 @@ const PortraitVideoCarousel = ({ selectedVideoIndex !== null && createPortal( - + , videoOverlayContainer, )} diff --git a/src/app/components/PortraitVideoCarousel/videoOverlay.jsx b/src/app/components/PortraitVideoCarousel/videoOverlay.jsx index f0a8e678fb9..8933b62e70f 100644 --- a/src/app/components/PortraitVideoCarousel/videoOverlay.jsx +++ b/src/app/components/PortraitVideoCarousel/videoOverlay.jsx @@ -96,8 +96,7 @@ const ShareToolComponent = ({ shareUrlPath }) => { ); }; -const VideoOverlay = ({ blocks, index }) => { - const currentItem = blocks?.[index]; +const VideoOverlay = ({ currentItem }) => { const { shareUrl } = currentItem?.model?.video || {}; return ( diff --git a/src/app/components/PortraitVideoModal/index.tsx b/src/app/components/PortraitVideoModal/index.tsx index 4adbc7b7aa2..57b5f30c49d 100644 --- a/src/app/components/PortraitVideoModal/index.tsx +++ b/src/app/components/PortraitVideoModal/index.tsx @@ -105,13 +105,13 @@ export const playlistLoadedCallback = ( const previous = blocks?.[currentIndex - 1]?.model; const next = blocks?.[currentIndex + 1]?.model; - if (previous) { player.setPreviousPlaylist( { title: previous?.video?.title ?? '', holdingImageURL: previous?.video?.holdingImageURL ?? '', items: [{ versionID: previous?.video?.version?.id }], + shareUrl: previous?.video?.shareUrl ?? null, }, { statsObject: { clipPID: previous?.video?.id } }, ); @@ -123,6 +123,7 @@ export const playlistLoadedCallback = ( title: next?.video?.title ?? '', holdingImageURL: next?.video?.holdingImageURL ?? '', items: [{ versionID: next?.video?.version?.id }], + shareUrl: next?.video?.shareUrl ?? null, }, { statsObject: { clipPID: next?.video?.id } }, ); @@ -134,6 +135,7 @@ export const statsNavigationCallback = async ( blocks: PortraitClipMediaBlock[], eventTrackingData: EventTrackingData, swipeTracker: ReturnType, + onMediaChanged?: (data: { item: PortraitClipMediaBlock }) => void, ) => { const { direction, method } = e || {}; @@ -143,6 +145,7 @@ export const statsNavigationCallback = async ( const currentIndex = getCurrentIndex({ e, blocks }); const newIndex = direction === 'next' ? currentIndex + 1 : currentIndex - 1; + onMediaChanged?.({ item: blocks[newIndex] }); const newEventTrackingData = getEventTrackingData({ eventTrackingData, @@ -184,9 +187,19 @@ const pluginLoadedCallback = () => { player.dispatchEvent('fullScreenPlugin.launchFullscreen'); }; -const handlePrevNextVideo = (direction: 'previous' | 'next') => { +const handlePrevNextVideo = ({ + direction, + blocks, + onMediaChanged, +}: { + direction: 'previous' | 'next'; + blocks: PortraitClipMediaBlock[]; + onMediaChanged?: (data: { item: PortraitClipMediaBlock }) => void; +}) => { const player = getPlayerInstance(); - + const index = getCurrentIndex({ blocks, player }); + const newIndex = direction === 'next' ? index + 1 : index - 1; + onMediaChanged?.({ item: blocks[newIndex] }); player?.[direction]?.(); }; @@ -199,6 +212,7 @@ export interface PortraitVideoModalProps { setVideoOverlayContainerRef?: React.RefObject< React.Dispatch> >; + onMediaChanged?: (data: { item: PortraitClipMediaBlock }) => void; } const PortraitVideoModal = ({ @@ -207,6 +221,7 @@ const PortraitVideoModal = ({ selectedVideoIndex, eventTrackingData, setVideoOverlayContainerRef, + onMediaChanged, }: PortraitVideoModalProps) => { const { translations: { @@ -317,7 +332,13 @@ const PortraitVideoModal = ({ + + ); +}; + +export default ShareButton; diff --git a/src/app/components/ShareButton/styles.ts b/src/app/components/ShareButton/styles.ts new file mode 100644 index 00000000000..207e5ad3ff5 --- /dev/null +++ b/src/app/components/ShareButton/styles.ts @@ -0,0 +1,52 @@ +import { Theme, css } from '@emotion/react'; +import pixelsToRem from '../../utilities/pixelsToRem'; + +const styles = { + button: ({ palette, fontSizes, fontVariants, spacings, mq }: Theme) => + css({ + display: 'inline-flex', + alignItems: 'center', + color: palette.WHITE, + ...fontSizes.pica, + ...fontVariants.sansBold, + padding: `${pixelsToRem(10)}rem`, + marginBottom: `${spacings.TRIPLE}rem`, + marginInlineStart: `${spacings.DOUBLE}rem`, + border: `${pixelsToRem(2)}rem solid ${palette.BLACK}`, + backgroundColor: palette.BLACK, + cursor: 'pointer', + '&:hover, &:focus-visible': { + color: palette.WHITE, + border: `${pixelsToRem(2)}rem solid ${palette.BRAND_BACKGROUND}`, + backgroundColor: palette.BRAND_BACKGROUND, + path: { + fill: palette.WHITE, + [mq.FORCED_COLOURS]: { + fill: 'canvasText', + }, + }, + [mq.FORCED_COLOURS]: { + backgroundColor: 'canvas', + color: 'canvasText', + border: `${pixelsToRem(2)}rem solid canvasText`, + textDecoration: 'underline', + }, + }, + [mq.FORCED_COLOURS]: { + color: 'canvasText', + border: `${pixelsToRem(2)}rem solid canvasText`, + }, + svg: { + width: `${spacings.DOUBLE}rem`, + height: `${spacings.DOUBLE}rem`, + marginInlineEnd: `${spacings.FULL}rem`, + path: { + fill: palette.WHITE, + [mq.FORCED_COLOURS]: { + fill: 'canvasText', + }, + }, + }, + }), +}; +export default styles; From d1bbfed56bfbc18c8971799e64a9a284b3136bbb Mon Sep 17 00:00:00 2001 From: emily saffron Date: Wed, 21 Jan 2026 15:00:04 +0000 Subject: [PATCH 16/66] fix build --- src/app/components/MediaLoader/types.ts | 2 +- src/app/components/ShareButton/index.tsx | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/app/components/MediaLoader/types.ts b/src/app/components/MediaLoader/types.ts index 8a6b2314084..98ff48dd99e 100644 --- a/src/app/components/MediaLoader/types.ts +++ b/src/app/components/MediaLoader/types.ts @@ -277,7 +277,7 @@ export type PortraitClipMediaBlock = { altText?: string; }[]; video: { - shareUrl: string | null; + shareUrl?: string | null; id: string; title: string; holdingImageURL?: string; diff --git a/src/app/components/ShareButton/index.tsx b/src/app/components/ShareButton/index.tsx index fa32c5859d2..b6832a4a72c 100644 --- a/src/app/components/ShareButton/index.tsx +++ b/src/app/components/ShareButton/index.tsx @@ -1,8 +1,6 @@ /* eslint-disable jsx-a11y/aria-role */ import { ServiceContext } from '#app/contexts/ServiceContext'; -import useClickTrackerHandler from '#app/hooks/useClickTrackerHandler'; -import useViewTracker from '#app/hooks/useViewTracker'; -import { use, useRef, MouseEvent } from 'react'; +import { use, useRef } from 'react'; import VisuallyHiddenText from '../VisuallyHiddenText'; import styles from './styles'; @@ -25,7 +23,7 @@ const ShareSvg = () => ( const ShareButton = ({ contentId, - eventTrackingData, + // eventTrackingData, title, url, }: { @@ -45,8 +43,8 @@ const ShareButton = ({ liveExperiencePage: { shareButtonText = 'Share' }, }, } = use(ServiceContext); - - const handleShare = async (event: MouseEvent) => { + // (event: MouseEvent) + const handleShare = async () => { // if (clickTrackerHandler) clickTrackerHandler(event); try { let shareUrl; @@ -61,6 +59,7 @@ const ShareButton = ({ shareUrl = `${currentUrlNoHash.origin}${currentUrlNoHash.pathname}?${newParams}#${contentId}`; // await navigator.clipboard.writeText(shareUrl); } + // eslint-disable-next-line no-console console.log('sharebutton ', url); await navigator.share({ url: url || shareUrl, From 9cb20681bf9a1bb8eccfdf8efbe793a58b38575e Mon Sep 17 00:00:00 2001 From: emily saffron Date: Thu, 22 Jan 2026 10:55:36 +0000 Subject: [PATCH 17/66] show share button on hover --- src/app/components/MediaLoader/types.ts | 4 +++- src/app/components/PortraitVideoCarousel/index.tsx | 7 ++++++- src/app/components/PortraitVideoCarousel/videoOverlay.jsx | 4 ++-- src/app/components/PortraitVideoModal/index.tsx | 4 ++++ 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/app/components/MediaLoader/types.ts b/src/app/components/MediaLoader/types.ts index 98ff48dd99e..3c7c5a158b0 100644 --- a/src/app/components/MediaLoader/types.ts +++ b/src/app/components/MediaLoader/types.ts @@ -23,7 +23,9 @@ export type MediaPlayerEvents = | 'pluginLoaded' | 'fullscreenExit' | 'statsNavigation' - | 'pause'; + | 'pause' + | 'uiControlBarShown' + | 'uiControlBarHidden'; export type EventMapping = Partial< Record void> diff --git a/src/app/components/PortraitVideoCarousel/index.tsx b/src/app/components/PortraitVideoCarousel/index.tsx index 6ac77303bca..68c987c25be 100644 --- a/src/app/components/PortraitVideoCarousel/index.tsx +++ b/src/app/components/PortraitVideoCarousel/index.tsx @@ -39,6 +39,7 @@ const PortraitVideoCarousel = ({ const [currentItem, setCurrentItem] = useState< PortraitClipMediaBlock | undefined >(); + const [controlsDisplayed, setControlsDisplayed] = useState(false); const { isLite, nonce } = use(RequestContext); @@ -142,6 +143,7 @@ const PortraitVideoCarousel = ({ eventTrackingData={eventTrackingDataExtended} setVideoOverlayContainerRef={setVideoOverlayContainerRef} onMediaChanged={onMediaChanged} + setControlsDisplayed={setControlsDisplayed} />, document.body, )} @@ -149,7 +151,10 @@ const PortraitVideoCarousel = ({ selectedVideoIndex !== null && createPortal( - + , videoOverlayContainer, )} diff --git a/src/app/components/PortraitVideoCarousel/videoOverlay.jsx b/src/app/components/PortraitVideoCarousel/videoOverlay.jsx index 96c45a72648..94ef5377749 100644 --- a/src/app/components/PortraitVideoCarousel/videoOverlay.jsx +++ b/src/app/components/PortraitVideoCarousel/videoOverlay.jsx @@ -89,7 +89,7 @@ const ShareToolComponent = ({ shareUrlPath, title }) => { ); }; -const VideoOverlay = ({ currentItem }) => { +const VideoOverlay = ({ currentItem, controlsDisplayed }) => { const { shareUrl, title } = currentItem?.model?.video || {}; return ( @@ -102,7 +102,7 @@ const VideoOverlay = ({ currentItem }) => { diff --git a/src/app/components/PortraitVideoModal/index.tsx b/src/app/components/PortraitVideoModal/index.tsx index 57b5f30c49d..ac5dde87f74 100644 --- a/src/app/components/PortraitVideoModal/index.tsx +++ b/src/app/components/PortraitVideoModal/index.tsx @@ -213,6 +213,7 @@ export interface PortraitVideoModalProps { React.Dispatch> >; onMediaChanged?: (data: { item: PortraitClipMediaBlock }) => void; + setControlsDisplayed?: any; } const PortraitVideoModal = ({ @@ -222,6 +223,7 @@ const PortraitVideoModal = ({ eventTrackingData, setVideoOverlayContainerRef, onMediaChanged, + setControlsDisplayed, }: PortraitVideoModalProps) => { const { translations: { @@ -382,6 +384,8 @@ const PortraitVideoModal = ({ ), pause: e => playbackEndedCallback(e, blocks, eventTrackingData, swipeTracker), + uiControlBarShown: () => setControlsDisplayed(true), + uiControlBarHidden: () => setControlsDisplayed(false), }} /> - - ); -}; - -export default ShareButton; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/ShareButton/styles.ts b/ws-nextjs-app/pages/[service]/live/[id]/ShareButton/styles.ts deleted file mode 100644 index f22d8ba00e0..00000000000 --- a/ws-nextjs-app/pages/[service]/live/[id]/ShareButton/styles.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Theme, css } from '@emotion/react'; -import pixelsToRem from '../../../../../../src/app/utilities/pixelsToRem'; - -const styles = { - button: ({ palette, fontSizes, fontVariants, spacings, mq }: Theme) => - css({ - display: 'inline-flex', - alignItems: 'center', - color: palette.BLACK, - ...fontSizes.pica, - ...fontVariants.sansBold, - padding: `${pixelsToRem(10)}rem`, - marginBottom: `${spacings.TRIPLE}rem`, - marginInlineStart: `${spacings.DOUBLE}rem`, - border: `${pixelsToRem(2)}rem solid ${palette.BLACK}`, - backgroundColor: 'transparent', - cursor: 'pointer', - '&:hover, &:focus-visible': { - color: palette.WHITE, - border: `${pixelsToRem(2)}rem solid ${palette.BRAND_BACKGROUND}`, - backgroundColor: palette.BRAND_BACKGROUND, - path: { - fill: palette.WHITE, - [mq.FORCED_COLOURS]: { - fill: 'canvasText', - }, - }, - [mq.FORCED_COLOURS]: { - backgroundColor: 'canvas', - color: 'canvasText', - border: `${pixelsToRem(2)}rem solid canvasText`, - textDecoration: 'underline', - }, - }, - [mq.FORCED_COLOURS]: { - color: 'canvasText', - border: `${pixelsToRem(2)}rem solid canvasText`, - }, - svg: { - width: `${spacings.DOUBLE}rem`, - height: `${spacings.DOUBLE}rem`, - marginInlineEnd: `${spacings.FULL}rem`, - path: { - fill: palette.BLACK, - [mq.FORCED_COLOURS]: { - fill: 'canvasText', - }, - }, - }, - }), -}; -export default styles; From a08d9c81188ad7fc13738d864a9a4c2ea35c951b Mon Sep 17 00:00:00 2001 From: emily saffron Date: Wed, 11 Feb 2026 10:17:42 +0000 Subject: [PATCH 34/66] use plugin from simorgh static assets --- src/app/components/MediaLoader/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app/components/MediaLoader/index.tsx b/src/app/components/MediaLoader/index.tsx index ec202146f48..d1ff7cccad1 100644 --- a/src/app/components/MediaLoader/index.tsx +++ b/src/app/components/MediaLoader/index.tsx @@ -20,6 +20,7 @@ import { import filterForBlockType from '#lib/utilities/blockHandlers'; import { PageTypes } from '#app/models/types/global'; import { EventTrackingContext } from '#app/contexts/EventTrackingContext'; +import { getEnvConfig } from '#app/lib/utilities/getEnvConfig'; import { BumpType, EventMapping, @@ -136,6 +137,10 @@ const MediaContainer = ({ const playerElementRef = useRef(null); const isAudio = isAudioPlayer(playerConfig); const playerKeyRef = useRef(null); + const { + SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN, + SIMORGH_PUBLIC_STATIC_ASSETS_PATH, + } = getEnvConfig(); useEffect(() => { if (!playerElementRef.current) return; @@ -210,7 +215,7 @@ const MediaContainer = ({ ) { mediaPlayer.loadPlugin( { - html: 'https://static.files.bbci.co.uk/core/website/assets/static/scripts/smp/video-overlay-plugin.embed.869ac0e5834c1784f3ab.js', + html: `${SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN}${SIMORGH_PUBLIC_STATIC_ASSETS_PATH}smpPlugins/video-overlay-plugin.js`, playerOnly: true, // do not enable this plugin for old J2 version of the SMP player due to different UI }, waitOnPluginLoad: true, }, From ba134910c7771c03775ac9b85a049f87122b14d8 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Wed, 11 Feb 2026 11:40:25 +0000 Subject: [PATCH 35/66] WS-2095: Updates tracking values for post share button for consistency --- src/app/components/ShareButton/index.test.tsx | 14 ++++++++++---- .../pages/[service]/live/[id]/Post/index.tsx | 5 ++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/app/components/ShareButton/index.test.tsx b/src/app/components/ShareButton/index.test.tsx index 2b80e034017..9bc6fdfc6b5 100644 --- a/src/app/components/ShareButton/index.test.tsx +++ b/src/app/components/ShareButton/index.test.tsx @@ -15,7 +15,10 @@ Object.assign(navigator, { describe('ShareButton', () => { const mockLivePagePostShareButtonProps = { eventTrackingData: { - componentName: 'live-page-post-share-button', + componentName: 'share-button-live-page-post', + itemTracker: { + resourceId: 'urn:foo', + }, }, contentId: 'urn:foo', title: 'Share this post', @@ -23,7 +26,7 @@ describe('ShareButton', () => { const mockPortraitVideoShareButtonProps = { eventTrackingData: { - componentName: 'portrait-video-share-button', + componentName: 'share-button-portrait-video-carousel', itemTracker: { resourceId: 'urn:foo', }, @@ -62,7 +65,10 @@ describe('ShareButton', () => { await user.click(shareButton); expect(clickTrackerSpy).toHaveBeenCalledWith({ - componentName: 'live-page-post-share-button', + componentName: 'share-button-live-page-post', + itemTracker: { + resourceId: 'urn:foo', + }, }); }); }); @@ -98,7 +104,7 @@ describe('ShareButton', () => { await user.click(shareButton); expect(clickTrackerSpy).toHaveBeenCalledWith({ - componentName: 'portrait-video-share-button', + componentName: 'share-button-portrait-video-carousel', itemTracker: { resourceId: 'urn:foo', }, diff --git a/ws-nextjs-app/pages/[service]/live/[id]/Post/index.tsx b/ws-nextjs-app/pages/[service]/live/[id]/Post/index.tsx index 9fe91ba992f..0689b6961cb 100644 --- a/ws-nextjs-app/pages/[service]/live/[id]/Post/index.tsx +++ b/ws-nextjs-app/pages/[service]/live/[id]/Post/index.tsx @@ -220,7 +220,10 @@ const Post = ({ {hasShareApi && ( Date: Wed, 11 Feb 2026 14:07:15 +0000 Subject: [PATCH 36/66] WS-2095: Constructs plugin link outside of useEffect --- src/app/components/MediaLoader/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/components/MediaLoader/index.tsx b/src/app/components/MediaLoader/index.tsx index d1ff7cccad1..916c7719971 100644 --- a/src/app/components/MediaLoader/index.tsx +++ b/src/app/components/MediaLoader/index.tsx @@ -141,6 +141,7 @@ const MediaContainer = ({ SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN, SIMORGH_PUBLIC_STATIC_ASSETS_PATH, } = getEnvConfig(); + const videoOverlayPlugin = `${SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN}${SIMORGH_PUBLIC_STATIC_ASSETS_PATH}smpPlugins/video-overlay-plugin.js`; useEffect(() => { if (!playerElementRef.current) return; @@ -215,7 +216,7 @@ const MediaContainer = ({ ) { mediaPlayer.loadPlugin( { - html: `${SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN}${SIMORGH_PUBLIC_STATIC_ASSETS_PATH}smpPlugins/video-overlay-plugin.js`, + html: videoOverlayPlugin, playerOnly: true, // do not enable this plugin for old J2 version of the SMP player due to different UI }, waitOnPluginLoad: true, }, @@ -240,6 +241,7 @@ const MediaContainer = ({ setVideoOverlayContainerRef, showAds, uniqueId, + videoOverlayPlugin, ]); return ( From 453178e9016111994211072fcb1088f242636b9f Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 13 Feb 2026 10:37:15 +0000 Subject: [PATCH 37/66] WS-2095: Fix merge --- src/app/components/PortraitVideoCarousel/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/components/PortraitVideoCarousel/index.tsx b/src/app/components/PortraitVideoCarousel/index.tsx index 67c0bb4d2d0..1a5e4369a1c 100644 --- a/src/app/components/PortraitVideoCarousel/index.tsx +++ b/src/app/components/PortraitVideoCarousel/index.tsx @@ -77,8 +77,6 @@ const PortraitVideoCarousel = ({ const viewTracker = useViewTracker(eventTrackingDataExtended); - if (isLite || isAmp) return null; - const handlePromoClick = (index: number) => { if (blocks?.[index]?.model?.video) { setSelectedVideoIndex(index); @@ -92,7 +90,7 @@ const PortraitVideoCarousel = ({ setSelectedVideoIndex(null); }, []); - if (isLite) return null; + if (isLite || isAmp) return null; return ( <> From ee4df83e22445051274724fd7dc759d2717d8722 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Mon, 16 Feb 2026 11:12:30 +0000 Subject: [PATCH 38/66] WS-2095: Adds test coverage [copilot] --- .../PortraitVideoModal/index.test.tsx | 126 ++++++++++++++++++ .../components/PortraitVideoModal/index.tsx | 2 +- 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/src/app/components/PortraitVideoModal/index.test.tsx b/src/app/components/PortraitVideoModal/index.test.tsx index 07f4bfbec67..24c7331526d 100644 --- a/src/app/components/PortraitVideoModal/index.test.tsx +++ b/src/app/components/PortraitVideoModal/index.test.tsx @@ -2,6 +2,7 @@ import Component, { playlistLoadedCallback, statsNavigationCallback, playbackEndedCallback, + handlePrevNextVideo, } from '.'; import { screen, @@ -738,4 +739,129 @@ describe('PortraitVideoModal', () => { expect(mockPlayer.next).toHaveBeenCalled(); }); }); + + describe('handlePrevNextVideo', () => { + let originalEmbeddedMedia; + beforeEach(() => { + jest.clearAllMocks(); + originalEmbeddedMedia = window.embeddedMedia; + }); + afterEach(() => { + window.embeddedMedia = originalEmbeddedMedia; + }); + + it('calls setCurrentItem and player.next for next', () => { + const setCurrentItem = jest.fn(); + const next = jest.fn(); + Object.defineProperty(window, 'embeddedMedia', { + writable: true, + value: { + api: { + players: () => ({ + bbcMediaPlayer0: { + next, + playlist: () => ({ + items: [{ versionID: blocks[0].model.video.version.id }], + }), + }, + }), + }, + }, + }); + handlePrevNextVideo({ + direction: 'next', + blocks, + setCurrentItem, + }); + expect(setCurrentItem).toHaveBeenCalledWith(blocks[1]); + expect(next).toHaveBeenCalled(); + }); + + it('calls setCurrentItem and player.previous for previous', () => { + const setCurrentItem = jest.fn(); + const previous = jest.fn(); + Object.defineProperty(window, 'embeddedMedia', { + writable: true, + value: { + api: { + players: () => ({ + bbcMediaPlayer0: { + previous, + playlist: () => ({ + items: [{ versionID: blocks[1].model.video.version.id }], + }), + }, + }), + }, + }, + }); + handlePrevNextVideo({ + direction: 'previous', + blocks, + setCurrentItem, + }); + expect(setCurrentItem).toHaveBeenCalledWith(blocks[0]); + expect(previous).toHaveBeenCalled(); + }); + + it('does not throw if setCurrentItem is not provided', () => { + const next = jest.fn(); + Object.defineProperty(window, 'embeddedMedia', { + writable: true, + value: { + api: { + players: () => ({ + bbcMediaPlayer0: { + next, + playlist: () => ({ + items: [{ versionID: blocks[0].model.video.version.id }], + }), + }, + }), + }, + }, + }); + expect(() => + handlePrevNextVideo({ + direction: 'next', + blocks, + }), + ).not.toThrow(); + expect(next).toHaveBeenCalled(); + }); + }); + + describe('setControlsDisplayed', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('does not crash if setCurrentItem, setControlsDisplayed, setVideoOverlayContainerRef are not provided', () => { + render( + , + ); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + it('calls setControlsDisplayed when uiControlBarShown/uiControlBarHidden fire', () => { + const setControlsDisplayed = jest.fn(); + render( + , + ); + // Simulate MediaLoader eventMapping + screen.getByRole('dialog').querySelectorAll('button'); // Just to trigger render + // Directly call the eventMapping + // Not possible to trigger uiControlBarShown/uiControlBarHidden from here, but this ensures prop is accepted + expect(typeof setControlsDisplayed).toBe('function'); + }); + }); }); diff --git a/src/app/components/PortraitVideoModal/index.tsx b/src/app/components/PortraitVideoModal/index.tsx index 65e78821c55..e96db45df2a 100644 --- a/src/app/components/PortraitVideoModal/index.tsx +++ b/src/app/components/PortraitVideoModal/index.tsx @@ -184,7 +184,7 @@ const pluginLoadedCallback = () => { player.dispatchEvent('fullScreenPlugin.launchFullscreen'); }; -const handlePrevNextVideo = ({ +export const handlePrevNextVideo = ({ direction, blocks, setCurrentItem, From ec6eeb970b1a14c79fc2b863f1c50cc3f5ac78e3 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Mon, 16 Feb 2026 11:20:05 +0000 Subject: [PATCH 39/66] WS-2095: Adds unit test coverage [copilot] --- .../PortraitVideoModal/index.test.tsx | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/app/components/PortraitVideoModal/index.test.tsx b/src/app/components/PortraitVideoModal/index.test.tsx index 24c7331526d..8d1fcfa8091 100644 --- a/src/app/components/PortraitVideoModal/index.test.tsx +++ b/src/app/components/PortraitVideoModal/index.test.tsx @@ -486,6 +486,63 @@ describe('PortraitVideoModal', () => { expect(mockSwipeTracker).not.toHaveBeenCalled(); }); + + it('calls setCurrentItem when navigating to next video', () => { + const setCurrentItem = jest.fn(); + const mockSMPEvent: SMPEvent = { + playlist: { + items: [{ versionID: blocks[0].model.video.version.id }], + }, + direction: 'next', + method: 'swipe', + }; + const mockSwipeEventTrackingData = { + ...eventTrackingData, + groupTracker: { + name: 'group name', + itemCount: 20, + resourceId: 'urn:bbc:tipo:list:fe4a1c8c-9a7c-4a50-845d-7da91aa65204', + position: 4, + }, + }; + statsNavigationCallback( + mockSMPEvent, + blocks, + mockSwipeEventTrackingData, + mockSwipeTracker, + setCurrentItem, + ); + expect(setCurrentItem).toHaveBeenCalledWith(blocks[1]); + }); + + it('calls setCurrentItem when navigating to previous video', () => { + const setCurrentItem = jest.fn(); + const mockSMPEvent: SMPEvent = { + playlist: { + items: [{ versionID: blocks[1].model.video.version.id }], + }, + direction: 'previous', + method: 'swipe', + }; + const mockSwipeEventTrackingData = { + ...eventTrackingData, + groupTracker: { + name: 'group name', + itemCount: 20, + resourceId: 'urn:bbc:tipo:list:fe4a1c8c-9a7c-4a50-845d-7da91aa65204', + position: 4, + }, + }; + statsNavigationCallback( + mockSMPEvent, + blocks, + mockSwipeEventTrackingData, + mockSwipeTracker, + setCurrentItem, + ); + + expect(setCurrentItem).toHaveBeenCalledWith(blocks[0]); + }); }); describe('playbackEndedCallback', () => { From a5e9d34a0f41a433f4a7fbd3d2e396a5e29d65d6 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Mon, 16 Feb 2026 11:29:12 +0000 Subject: [PATCH 40/66] WS-2095: Renames constants and functions for clarity --- .../VideoOverlay/index.tsx | 6 ++-- .../PortraitVideoCarousel/index.tsx | 8 ++--- .../PortraitVideoModal/index.test.tsx | 36 +++++++++---------- .../components/PortraitVideoModal/index.tsx | 29 +++++++-------- 4 files changed, 40 insertions(+), 39 deletions(-) diff --git a/src/app/components/PortraitVideoCarousel/VideoOverlay/index.tsx b/src/app/components/PortraitVideoCarousel/VideoOverlay/index.tsx index 5388b81857f..18080f5cb6a 100644 --- a/src/app/components/PortraitVideoCarousel/VideoOverlay/index.tsx +++ b/src/app/components/PortraitVideoCarousel/VideoOverlay/index.tsx @@ -30,13 +30,13 @@ const ShareToolComponent = ({ shareUrlPath, title, id }) => { }; const VideoOverlay = ({ - currentItem, + currentVideo, controlsDisplayed, }: { - currentItem: PortraitClipMediaBlock | undefined; + currentVideo: PortraitClipMediaBlock | undefined; controlsDisplayed: boolean; }) => { - const { shareUrl, title, id } = currentItem?.model?.video || {}; + const { shareUrl, title, id } = currentVideo?.model?.video || {}; return (
diff --git a/src/app/components/PortraitVideoCarousel/index.tsx b/src/app/components/PortraitVideoCarousel/index.tsx index 1a5e4369a1c..42915cae5d7 100644 --- a/src/app/components/PortraitVideoCarousel/index.tsx +++ b/src/app/components/PortraitVideoCarousel/index.tsx @@ -40,7 +40,7 @@ const PortraitVideoCarousel = ({ const [videoOverlayContainer, setVideoOverlayContainer] = useState(null); const setVideoOverlayContainerRef = useRef(setVideoOverlayContainer); - const [currentItem, setCurrentItem] = useState< + const [currentVideo, setCurrentVideo] = useState< PortraitClipMediaBlock | undefined >(); const [controlsDisplayed, setControlsDisplayed] = useState(false); @@ -80,7 +80,7 @@ const PortraitVideoCarousel = ({ const handlePromoClick = (index: number) => { if (blocks?.[index]?.model?.video) { setSelectedVideoIndex(index); - setCurrentItem(blocks[index]); + setCurrentVideo(blocks[index]); setIsModalOpen(true); } }; @@ -148,7 +148,7 @@ const PortraitVideoCarousel = ({ nonce={nonce} eventTrackingData={eventTrackingDataExtended} setVideoOverlayContainerRef={setVideoOverlayContainerRef} - setCurrentItem={setCurrentItem} + setCurrentVideo={setCurrentVideo} setControlsDisplayed={setControlsDisplayed} />, document.body, @@ -159,7 +159,7 @@ const PortraitVideoCarousel = ({ createPortal( , diff --git a/src/app/components/PortraitVideoModal/index.test.tsx b/src/app/components/PortraitVideoModal/index.test.tsx index 8d1fcfa8091..be827a1aecb 100644 --- a/src/app/components/PortraitVideoModal/index.test.tsx +++ b/src/app/components/PortraitVideoModal/index.test.tsx @@ -487,8 +487,8 @@ describe('PortraitVideoModal', () => { expect(mockSwipeTracker).not.toHaveBeenCalled(); }); - it('calls setCurrentItem when navigating to next video', () => { - const setCurrentItem = jest.fn(); + it('calls setCurrentVideo when navigating to next video', () => { + const setCurrentVideo = jest.fn(); const mockSMPEvent: SMPEvent = { playlist: { items: [{ versionID: blocks[0].model.video.version.id }], @@ -510,13 +510,13 @@ describe('PortraitVideoModal', () => { blocks, mockSwipeEventTrackingData, mockSwipeTracker, - setCurrentItem, + setCurrentVideo, ); - expect(setCurrentItem).toHaveBeenCalledWith(blocks[1]); + expect(setCurrentVideo).toHaveBeenCalledWith(blocks[1]); }); - it('calls setCurrentItem when navigating to previous video', () => { - const setCurrentItem = jest.fn(); + it('calls setCurrentVideo when navigating to previous video', () => { + const setCurrentVideo = jest.fn(); const mockSMPEvent: SMPEvent = { playlist: { items: [{ versionID: blocks[1].model.video.version.id }], @@ -538,10 +538,10 @@ describe('PortraitVideoModal', () => { blocks, mockSwipeEventTrackingData, mockSwipeTracker, - setCurrentItem, + setCurrentVideo, ); - expect(setCurrentItem).toHaveBeenCalledWith(blocks[0]); + expect(setCurrentVideo).toHaveBeenCalledWith(blocks[0]); }); }); @@ -807,8 +807,8 @@ describe('PortraitVideoModal', () => { window.embeddedMedia = originalEmbeddedMedia; }); - it('calls setCurrentItem and player.next for next', () => { - const setCurrentItem = jest.fn(); + it('calls setCurrentVideo and player.next for next', () => { + const setCurrentVideo = jest.fn(); const next = jest.fn(); Object.defineProperty(window, 'embeddedMedia', { writable: true, @@ -828,14 +828,14 @@ describe('PortraitVideoModal', () => { handlePrevNextVideo({ direction: 'next', blocks, - setCurrentItem, + setCurrentVideo, }); - expect(setCurrentItem).toHaveBeenCalledWith(blocks[1]); + expect(setCurrentVideo).toHaveBeenCalledWith(blocks[1]); expect(next).toHaveBeenCalled(); }); - it('calls setCurrentItem and player.previous for previous', () => { - const setCurrentItem = jest.fn(); + it('calls setCurrentVideo and player.previous for previous', () => { + const setCurrentVideo = jest.fn(); const previous = jest.fn(); Object.defineProperty(window, 'embeddedMedia', { writable: true, @@ -855,13 +855,13 @@ describe('PortraitVideoModal', () => { handlePrevNextVideo({ direction: 'previous', blocks, - setCurrentItem, + setCurrentVideo, }); - expect(setCurrentItem).toHaveBeenCalledWith(blocks[0]); + expect(setCurrentVideo).toHaveBeenCalledWith(blocks[0]); expect(previous).toHaveBeenCalled(); }); - it('does not throw if setCurrentItem is not provided', () => { + it('does not throw if setCurrentVideo is not provided', () => { const next = jest.fn(); Object.defineProperty(window, 'embeddedMedia', { writable: true, @@ -892,7 +892,7 @@ describe('PortraitVideoModal', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('does not crash if setCurrentItem, setControlsDisplayed, setVideoOverlayContainerRef are not provided', () => { + it('does not crash if setCurrentVideo, setControlsDisplayed, setVideoOverlayContainerRef are not provided', () => { render( { const playlist = (e?.playlist || player?.playlist() || {}) as Playlist; - const [currentItem] = (playlist?.items || []) as PlaylistItem[]; - const currentId = currentItem?.vpid || currentItem?.versionID; + const [currentVideo] = (playlist?.items || []) as PlaylistItem[]; + const currentId = currentVideo?.vpid || currentVideo?.versionID; const currentIndex = blocks?.findIndex( item => @@ -133,7 +133,7 @@ export const statsNavigationCallback = async ( blocks: PortraitClipMediaBlock[], eventTrackingData: EventTrackingData, swipeTracker: ReturnType, - setCurrentItem?: (item: PortraitClipMediaBlock) => void, + setCurrentVideo?: (item: PortraitClipMediaBlock) => void, ) => { const { direction, method } = e || {}; @@ -143,7 +143,7 @@ export const statsNavigationCallback = async ( const currentIndex = getCurrentIndex({ e, blocks }); const newIndex = direction === 'next' ? currentIndex + 1 : currentIndex - 1; - setCurrentItem?.(blocks[newIndex]); + setCurrentVideo?.(blocks[newIndex]); const newEventTrackingData = getEventTrackingData({ eventTrackingData, selectedVideo: blocks?.[newIndex], @@ -187,16 +187,17 @@ const pluginLoadedCallback = () => { export const handlePrevNextVideo = ({ direction, blocks, - setCurrentItem, + setCurrentVideo, }: { direction: 'previous' | 'next'; blocks: PortraitClipMediaBlock[]; - setCurrentItem?: (item: PortraitClipMediaBlock) => void; + setCurrentVideo?: (item: PortraitClipMediaBlock) => void; }) => { const player = getPlayerInstance(); - const index = getCurrentIndex({ blocks, player }); - const newIndex = direction === 'next' ? index + 1 : index - 1; - setCurrentItem?.(blocks[newIndex]); + const currentVideoIndex = getCurrentIndex({ blocks, player }); + const prevNextVideoIndex = + direction === 'next' ? currentVideoIndex + 1 : currentVideoIndex - 1; + setCurrentVideo?.(blocks[prevNextVideoIndex]); player?.[direction]?.(); }; @@ -209,7 +210,7 @@ export interface PortraitVideoModalProps { setVideoOverlayContainerRef?: React.RefObject< React.Dispatch> >; - setCurrentItem?: (item: PortraitClipMediaBlock) => void; + setCurrentVideo?: (item: PortraitClipMediaBlock) => void; setControlsDisplayed?: (displayed: boolean) => void; } @@ -219,7 +220,7 @@ const PortraitVideoModal = ({ selectedVideoIndex, eventTrackingData, setVideoOverlayContainerRef, - setCurrentItem, + setCurrentVideo, setControlsDisplayed, }: PortraitVideoModalProps) => { const { @@ -335,7 +336,7 @@ const PortraitVideoModal = ({ handlePrevNextVideo({ direction: 'previous', blocks, - setCurrentItem, + setCurrentVideo, }) } css={styles.navButton} @@ -352,7 +353,7 @@ const PortraitVideoModal = ({ handlePrevNextVideo({ direction: 'next', blocks, - setCurrentItem, + setCurrentVideo, }) } css={styles.navButton} @@ -377,7 +378,7 @@ const PortraitVideoModal = ({ blocks, eventTrackingData, swipeTracker, - setCurrentItem, + setCurrentVideo, ), pause: e => playbackEndedCallback(e, blocks, eventTrackingData, swipeTracker), From ccbc68cd3ac9de2146d1ba7ceeb3abbdda0d67bc Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Mon, 16 Feb 2026 11:36:20 +0000 Subject: [PATCH 41/66] WS-2095: Simplify early return --- src/app/components/MediaLoader/index.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/app/components/MediaLoader/index.tsx b/src/app/components/MediaLoader/index.tsx index 2f133d70c2f..2be34e9cf37 100644 --- a/src/app/components/MediaLoader/index.tsx +++ b/src/app/components/MediaLoader/index.tsx @@ -145,11 +145,7 @@ const MediaContainer = ({ } = getEnvConfig(); const videoOverlayPlugin = `${SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN}${SIMORGH_PUBLIC_STATIC_ASSETS_PATH}smpPlugins/video-overlay-plugin.js`; useEffect(() => { - if (!playerElementRef.current) return; - - if (playerKeyRef.current === playerKey) { - return; - } + if (!playerElementRef.current || playerKeyRef.current === playerKey) return; playerKeyRef.current = playerKey; try { From 3c746730cb06a7316baf217ff0a892a087c335fe Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Mon, 16 Feb 2026 14:14:40 +0000 Subject: [PATCH 42/66] WS-2095: Refactors early returns --- src/app/components/MediaLoader/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/components/MediaLoader/index.tsx b/src/app/components/MediaLoader/index.tsx index 2be34e9cf37..b463c5ff787 100644 --- a/src/app/components/MediaLoader/index.tsx +++ b/src/app/components/MediaLoader/index.tsx @@ -331,9 +331,7 @@ const MediaLoader = ({ const playerKey = `${clipPID}-${skin}-${showAds}`; - if (isLite) return null; - - if (!config || !playerConfig) return null; + if (isLite || !config || !playerConfig) return null; const captionBlock = getCaptionBlock(blocks, pageType); const isPortrait = orientation === 'portrait'; From cb548291bd09b3fe52198cc5547ef898dfdd4b1f Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Mon, 16 Feb 2026 14:16:30 +0000 Subject: [PATCH 43/66] TEMP change: hasShareApi as const --- .../components/PortraitVideoCarousel/index.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/app/components/PortraitVideoCarousel/index.tsx b/src/app/components/PortraitVideoCarousel/index.tsx index 42915cae5d7..2318848e081 100644 --- a/src/app/components/PortraitVideoCarousel/index.tsx +++ b/src/app/components/PortraitVideoCarousel/index.tsx @@ -44,13 +44,17 @@ const PortraitVideoCarousel = ({ PortraitClipMediaBlock | undefined >(); const [controlsDisplayed, setControlsDisplayed] = useState(false); - const [hasShareApi, setHasShareApi] = useState(false); + // const [hasShareApi, setHasShareApi] = useState(false); - useEffect(() => { - if ('share' in navigator) { - setHasShareApi(true); - } - }, []); + // useEffect(() => { + // if ('share' in navigator) { + // setHasShareApi(true); + // } + // }, []); + + const hasShareApi = typeof navigator !== 'undefined' && 'share' in navigator; + + console.log('hasShareApi', hasShareApi); const { isLite, isAmp, nonce } = use(RequestContext); From 93b645ab738c9e8ef0605357a73e35d53f2a4dbb Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Mon, 16 Feb 2026 14:19:57 +0000 Subject: [PATCH 44/66] TEMP change: hasShareApi as const --- src/app/components/PortraitVideoCarousel/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/components/PortraitVideoCarousel/index.tsx b/src/app/components/PortraitVideoCarousel/index.tsx index 2318848e081..7abed22a81d 100644 --- a/src/app/components/PortraitVideoCarousel/index.tsx +++ b/src/app/components/PortraitVideoCarousel/index.tsx @@ -1,4 +1,5 @@ -import { use, useCallback, useEffect, useRef, useState } from 'react'; +import { use, useCallback, useRef, useState } from 'react'; +// import { use, useCallback, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { RequestContext } from '#app/contexts/RequestContext'; import useViewTracker from '#app/hooks/useViewTracker'; @@ -54,6 +55,7 @@ const PortraitVideoCarousel = ({ const hasShareApi = typeof navigator !== 'undefined' && 'share' in navigator; + // eslint-disable-next-line no-console console.log('hasShareApi', hasShareApi); const { isLite, isAmp, nonce } = use(RequestContext); From deb8bd0846765408204377d3b2cdb5e29d871c97 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Mon, 16 Feb 2026 14:55:13 +0000 Subject: [PATCH 45/66] WS-2095: Tidies --- src/app/components/PortraitVideoCarousel/index.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/app/components/PortraitVideoCarousel/index.tsx b/src/app/components/PortraitVideoCarousel/index.tsx index 7abed22a81d..df7b42db06a 100644 --- a/src/app/components/PortraitVideoCarousel/index.tsx +++ b/src/app/components/PortraitVideoCarousel/index.tsx @@ -1,5 +1,4 @@ import { use, useCallback, useRef, useState } from 'react'; -// import { use, useCallback, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { RequestContext } from '#app/contexts/RequestContext'; import useViewTracker from '#app/hooks/useViewTracker'; @@ -45,19 +44,9 @@ const PortraitVideoCarousel = ({ PortraitClipMediaBlock | undefined >(); const [controlsDisplayed, setControlsDisplayed] = useState(false); - // const [hasShareApi, setHasShareApi] = useState(false); - - // useEffect(() => { - // if ('share' in navigator) { - // setHasShareApi(true); - // } - // }, []); const hasShareApi = typeof navigator !== 'undefined' && 'share' in navigator; - // eslint-disable-next-line no-console - console.log('hasShareApi', hasShareApi); - const { isLite, isAmp, nonce } = use(RequestContext); // EXPERIMENT: Homepage Portrait Video 2 From ef4f2d51b689891a28d4962614b60ec506835c0e Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Mon, 16 Feb 2026 15:51:51 +0000 Subject: [PATCH 46/66] WS-2095: Adds story --- .../components/ShareButton/index.stories.tsx | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/app/components/ShareButton/index.stories.tsx b/src/app/components/ShareButton/index.stories.tsx index 425ba18b3a9..47f3c6f5e1d 100644 --- a/src/app/components/ShareButton/index.stories.tsx +++ b/src/app/components/ShareButton/index.stories.tsx @@ -1,18 +1,23 @@ import ShareButton from '.'; import metadata from './metadata.json'; -const Component = () => { - return ( - - ); -}; +const Component = ({ + url, + contentId, +}: { + url?: string; + contentId?: string; +}) => ( + +); export default { - title: 'Components/Live Page Share Button', + title: 'Components/Share Button', Component, parameters: { metadata, @@ -46,4 +51,9 @@ export default { }, }; -export const ShareButtonComponent = () => ; +export const ShareButtonLivePagePost = () => ( + +); +export const ShareButtonPortraitVideoCarousel = () => ( + +); From a4d7121de07697c784d0bb2430179591195c1b86 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Mon, 16 Feb 2026 15:53:42 +0000 Subject: [PATCH 47/66] WS-2095: Fix --- src/app/components/ShareButton/index.stories.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/ShareButton/index.stories.tsx b/src/app/components/ShareButton/index.stories.tsx index 47f3c6f5e1d..233d520ddf5 100644 --- a/src/app/components/ShareButton/index.stories.tsx +++ b/src/app/components/ShareButton/index.stories.tsx @@ -52,8 +52,8 @@ export default { }; export const ShareButtonLivePagePost = () => ( - + ); export const ShareButtonPortraitVideoCarousel = () => ( - + ); From 543bae7969ce154e8ce12cc35b5d1d213bc7cccf Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Mon, 16 Feb 2026 16:10:51 +0000 Subject: [PATCH 48/66] WS-2095: Removes text from share button (UX review) --- src/app/components/ShareButton/index.tsx | 16 +++++++++++---- src/app/components/ShareButton/styles.ts | 26 ++++++++++++++---------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/app/components/ShareButton/index.tsx b/src/app/components/ShareButton/index.tsx index 484c2e58795..0d2bdc75613 100644 --- a/src/app/components/ShareButton/index.tsx +++ b/src/app/components/ShareButton/index.tsx @@ -85,10 +85,18 @@ const ShareButton = ({ ]} > - - {shareButtonText} - , {title} - + {isLivePagePost ? ( + + {shareButtonText} + , {title} + + ) : ( + + + {shareButtonText}, {title} + + + )}
); diff --git a/src/app/components/ShareButton/styles.ts b/src/app/components/ShareButton/styles.ts index 79448a2dd95..e4d580a5550 100644 --- a/src/app/components/ShareButton/styles.ts +++ b/src/app/components/ShareButton/styles.ts @@ -34,34 +34,38 @@ const styles = { color: 'canvasText', border: `${pixelsToRem(2)}rem solid canvasText`, }, + }), + postButton: ({ palette, spacings, mq }: Theme) => + css({ + color: palette.BLACK, + backgroundColor: palette.WHITE, svg: { width: `${spacings.DOUBLE}rem`, height: `${spacings.DOUBLE}rem`, marginInlineEnd: `${spacings.FULL}rem`, path: { + fill: palette.BLACK, [mq.FORCED_COLOURS]: { fill: 'canvasText', }, }, }, }), - postButton: ({ palette }: Theme) => - css({ - color: palette.BLACK, - backgroundColor: palette.WHITE, - svg: { - path: { - fill: palette.BLACK, - }, - }, - }), - portraitVideoButton: ({ palette }: Theme) => + portraitVideoButton: ({ palette, mq }: Theme) => css({ color: palette.WHITE, backgroundColor: palette.BLACK, + minHeight: `${pixelsToRem(44)}rem`, + minWidth: `${pixelsToRem(44)}rem`, svg: { + width: `${pixelsToRem(18)}rem`, + height: `${pixelsToRem(18)}rem`, + margin: 'auto', path: { fill: palette.WHITE, + [mq.FORCED_COLOURS]: { + fill: 'canvasText', + }, }, }, }), From fa1463249b8c774f2f94ff82db4cca90e583bd06 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Mon, 16 Feb 2026 16:18:54 +0000 Subject: [PATCH 49/66] WS-2095: Focus indicator styles fix --- src/app/components/ShareButton/styles.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/app/components/ShareButton/styles.ts b/src/app/components/ShareButton/styles.ts index e4d580a5550..a4ba2652d90 100644 --- a/src/app/components/ShareButton/styles.ts +++ b/src/app/components/ShareButton/styles.ts @@ -1,5 +1,6 @@ import { Theme, css } from '@emotion/react'; import pixelsToRem from '../../utilities/pixelsToRem'; +import { focusIndicatorThickness } from '../ThemeProvider/focusIndicator'; const styles = { button: ({ palette, fontSizes, fontVariants, spacings, mq }: Theme) => @@ -57,6 +58,12 @@ const styles = { backgroundColor: palette.BLACK, minHeight: `${pixelsToRem(44)}rem`, minWidth: `${pixelsToRem(44)}rem`, + // global styles not applied to shadow dom, so focus indicator styles added here + '&:focus-visible': { + outline: `${focusIndicatorThickness} solid ${palette.BLACK}`, + boxShadow: `0 0 0 ${focusIndicatorThickness} ${palette.WHITE}`, + outlineOffset: `${focusIndicatorThickness}`, + }, svg: { width: `${pixelsToRem(18)}rem`, height: `${pixelsToRem(18)}rem`, From e825b64f73f7168d4dbb28dbbfc4f26793925cf0 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Mon, 16 Feb 2026 16:59:38 +0000 Subject: [PATCH 50/66] WS-2095: Reverts navigator change to try fix safari bug --- src/app/components/PortraitVideoCarousel/index.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/components/PortraitVideoCarousel/index.tsx b/src/app/components/PortraitVideoCarousel/index.tsx index df7b42db06a..42915cae5d7 100644 --- a/src/app/components/PortraitVideoCarousel/index.tsx +++ b/src/app/components/PortraitVideoCarousel/index.tsx @@ -1,4 +1,4 @@ -import { use, useCallback, useRef, useState } from 'react'; +import { use, useCallback, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { RequestContext } from '#app/contexts/RequestContext'; import useViewTracker from '#app/hooks/useViewTracker'; @@ -44,8 +44,13 @@ const PortraitVideoCarousel = ({ PortraitClipMediaBlock | undefined >(); const [controlsDisplayed, setControlsDisplayed] = useState(false); + const [hasShareApi, setHasShareApi] = useState(false); - const hasShareApi = typeof navigator !== 'undefined' && 'share' in navigator; + useEffect(() => { + if ('share' in navigator) { + setHasShareApi(true); + } + }, []); const { isLite, isAmp, nonce } = use(RequestContext); From f82ac17834e129637d8c87362c60a3f556a6cf56 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Tue, 17 Feb 2026 08:10:13 +0000 Subject: [PATCH 51/66] Revert "WS-2095: Reverts navigator change to try fix safari bug" This reverts commit e825b64f73f7168d4dbb28dbbfc4f26793925cf0. --- src/app/components/PortraitVideoCarousel/index.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/app/components/PortraitVideoCarousel/index.tsx b/src/app/components/PortraitVideoCarousel/index.tsx index 42915cae5d7..df7b42db06a 100644 --- a/src/app/components/PortraitVideoCarousel/index.tsx +++ b/src/app/components/PortraitVideoCarousel/index.tsx @@ -1,4 +1,4 @@ -import { use, useCallback, useEffect, useRef, useState } from 'react'; +import { use, useCallback, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { RequestContext } from '#app/contexts/RequestContext'; import useViewTracker from '#app/hooks/useViewTracker'; @@ -44,13 +44,8 @@ const PortraitVideoCarousel = ({ PortraitClipMediaBlock | undefined >(); const [controlsDisplayed, setControlsDisplayed] = useState(false); - const [hasShareApi, setHasShareApi] = useState(false); - useEffect(() => { - if ('share' in navigator) { - setHasShareApi(true); - } - }, []); + const hasShareApi = typeof navigator !== 'undefined' && 'share' in navigator; const { isLite, isAmp, nonce } = use(RequestContext); From afc41deda6f618beb3eaf2ce04fa24de435f2b01 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Tue, 17 Feb 2026 09:25:21 +0000 Subject: [PATCH 52/66] WS-2095: Temp to debug, fetch plugin from PS --- src/app/components/MediaLoader/index.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/app/components/MediaLoader/index.tsx b/src/app/components/MediaLoader/index.tsx index b463c5ff787..35e1f603091 100644 --- a/src/app/components/MediaLoader/index.tsx +++ b/src/app/components/MediaLoader/index.tsx @@ -21,7 +21,7 @@ import { import filterForBlockType from '#lib/utilities/blockHandlers'; import { PageTypes } from '#app/models/types/global'; import { EventTrackingContext } from '#app/contexts/EventTrackingContext'; -import { getEnvConfig } from '#app/lib/utilities/getEnvConfig'; +// import { getEnvConfig } from '#app/lib/utilities/getEnvConfig'; import { BumpType, EventMapping, @@ -139,11 +139,11 @@ const MediaContainer = ({ const playerElementRef = useRef(null); const isAudio = isAudioPlayer(playerConfig); const playerKeyRef = useRef(null); - const { - SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN, - SIMORGH_PUBLIC_STATIC_ASSETS_PATH, - } = getEnvConfig(); - const videoOverlayPlugin = `${SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN}${SIMORGH_PUBLIC_STATIC_ASSETS_PATH}smpPlugins/video-overlay-plugin.js`; + // const { + // SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN, + // SIMORGH_PUBLIC_STATIC_ASSETS_PATH, + // } = getEnvConfig(); + // const videoOverlayPlugin = `${SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN}${SIMORGH_PUBLIC_STATIC_ASSETS_PATH}smpPlugins/video-overlay-plugin.js`; useEffect(() => { if (!playerElementRef.current || playerKeyRef.current === playerKey) return; @@ -214,7 +214,8 @@ const MediaContainer = ({ ) { mediaPlayer.loadPlugin( { - html: videoOverlayPlugin, + // html: videoOverlayPlugin, + html: 'https://static.files.bbci.co.uk/core/website/assets/static/scripts/smp/video-overlay-plugin.embed.869ac0e5834c1784f3ab.js', playerOnly: true, // do not enable this plugin for old J2 version of the SMP player due to different UI }, waitOnPluginLoad: true, }, @@ -239,7 +240,7 @@ const MediaContainer = ({ setVideoOverlayContainerRef, showAds, uniqueId, - videoOverlayPlugin, + // videoOverlayPlugin, ]); return ( From b989159f79f21d5b8ac7c7fb1f13756dcfe2e5d4 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Tue, 17 Feb 2026 09:51:51 +0000 Subject: [PATCH 53/66] Revert "WS-2095: Temp to debug, fetch plugin from PS" This reverts commit afc41deda6f618beb3eaf2ce04fa24de435f2b01. --- src/app/components/MediaLoader/index.tsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/app/components/MediaLoader/index.tsx b/src/app/components/MediaLoader/index.tsx index 35e1f603091..b463c5ff787 100644 --- a/src/app/components/MediaLoader/index.tsx +++ b/src/app/components/MediaLoader/index.tsx @@ -21,7 +21,7 @@ import { import filterForBlockType from '#lib/utilities/blockHandlers'; import { PageTypes } from '#app/models/types/global'; import { EventTrackingContext } from '#app/contexts/EventTrackingContext'; -// import { getEnvConfig } from '#app/lib/utilities/getEnvConfig'; +import { getEnvConfig } from '#app/lib/utilities/getEnvConfig'; import { BumpType, EventMapping, @@ -139,11 +139,11 @@ const MediaContainer = ({ const playerElementRef = useRef(null); const isAudio = isAudioPlayer(playerConfig); const playerKeyRef = useRef(null); - // const { - // SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN, - // SIMORGH_PUBLIC_STATIC_ASSETS_PATH, - // } = getEnvConfig(); - // const videoOverlayPlugin = `${SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN}${SIMORGH_PUBLIC_STATIC_ASSETS_PATH}smpPlugins/video-overlay-plugin.js`; + const { + SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN, + SIMORGH_PUBLIC_STATIC_ASSETS_PATH, + } = getEnvConfig(); + const videoOverlayPlugin = `${SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN}${SIMORGH_PUBLIC_STATIC_ASSETS_PATH}smpPlugins/video-overlay-plugin.js`; useEffect(() => { if (!playerElementRef.current || playerKeyRef.current === playerKey) return; @@ -214,8 +214,7 @@ const MediaContainer = ({ ) { mediaPlayer.loadPlugin( { - // html: videoOverlayPlugin, - html: 'https://static.files.bbci.co.uk/core/website/assets/static/scripts/smp/video-overlay-plugin.embed.869ac0e5834c1784f3ab.js', + html: videoOverlayPlugin, playerOnly: true, // do not enable this plugin for old J2 version of the SMP player due to different UI }, waitOnPluginLoad: true, }, @@ -240,7 +239,7 @@ const MediaContainer = ({ setVideoOverlayContainerRef, showAds, uniqueId, - // videoOverlayPlugin, + videoOverlayPlugin, ]); return ( From f3920fb98c3b42b0aab19c153ef01171e83ac228 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Tue, 17 Feb 2026 10:22:25 +0000 Subject: [PATCH 54/66] WS-2095: Temp, reflects plugin update on A11y swarm branch --- public/smpPlugins/video-overlay-plugin.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/public/smpPlugins/video-overlay-plugin.js b/public/smpPlugins/video-overlay-plugin.js index 692b5eae55c..2128b543a4d 100644 --- a/public/smpPlugins/video-overlay-plugin.js +++ b/public/smpPlugins/video-overlay-plugin.js @@ -1,3 +1,6 @@ +/* eslint-disable no-var */ +/* eslint-disable vars-on-top */ +/* eslint-disable func-names */ class VideoOverlayPlugin { constructor(utils, data = {}) { this.utils = utils; @@ -33,14 +36,12 @@ class VideoOverlayPlugin { } } -const runPlugin = (utils, data) => { +var runPlugin = function (utils, data) { const videoOverlayPlugin = new VideoOverlayPlugin(utils, data); return videoOverlayPlugin; }; -// We need to export runPlugin for a unit test, but this will throw an error when loaded by SMP -try { +// Export for Node/CommonJS only if needed (safe no-op in browser) +if (typeof module !== 'undefined' && module.exports) { module.exports = { runPlugin }; -} catch { - /* no-op */ } From bb45716f8b6f46be040225b9bf40c60c5d19dbc1 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Tue, 17 Feb 2026 10:42:56 +0000 Subject: [PATCH 55/66] WS-2095: Temp, refactors fullScreenPlugin --- public/smpPlugins/fullscreen.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/smpPlugins/fullscreen.js b/public/smpPlugins/fullscreen.js index 2c6cb3bd781..02959f869b1 100644 --- a/public/smpPlugins/fullscreen.js +++ b/public/smpPlugins/fullscreen.js @@ -1,4 +1,7 @@ /* eslint-disable no-unused-vars */ +/* eslint-disable no-var */ +/* eslint-disable vars-on-top */ +/* eslint-disable func-names */ class FullScreenPlugin { pluginInitialisation(pluginUtils) { @@ -15,8 +18,7 @@ class FullScreenPlugin { } } -const runPlugin = () => { +var runPlugin = function () { const fullScreenPlugin = new FullScreenPlugin(); - return fullScreenPlugin; }; From 6ee15c230e346db1ac2160cda4bbd558e8af518c Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Tue, 17 Feb 2026 16:26:39 +0000 Subject: [PATCH 56/66] WS-2095: temp, cache bust for videoOverlayPlugin --- src/app/components/MediaLoader/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/MediaLoader/index.tsx b/src/app/components/MediaLoader/index.tsx index b463c5ff787..ccacffce956 100644 --- a/src/app/components/MediaLoader/index.tsx +++ b/src/app/components/MediaLoader/index.tsx @@ -143,7 +143,7 @@ const MediaContainer = ({ SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN, SIMORGH_PUBLIC_STATIC_ASSETS_PATH, } = getEnvConfig(); - const videoOverlayPlugin = `${SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN}${SIMORGH_PUBLIC_STATIC_ASSETS_PATH}smpPlugins/video-overlay-plugin.js`; + const videoOverlayPlugin = `${SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN}${SIMORGH_PUBLIC_STATIC_ASSETS_PATH}smpPlugins/video-overlay-plugin.js?v=1`; useEffect(() => { if (!playerElementRef.current || playerKeyRef.current === playerKey) return; From 0ca3ba568b820fce9969d6bea4100b93feef483d Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Wed, 18 Feb 2026 11:45:48 +0000 Subject: [PATCH 57/66] WS-2095: Temp, adds fullscreen plugin cache busting --- src/app/components/MediaLoader/configs/portraitClipMedia.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/MediaLoader/configs/portraitClipMedia.ts b/src/app/components/MediaLoader/configs/portraitClipMedia.ts index 9eac78460db..eda86a22907 100644 --- a/src/app/components/MediaLoader/configs/portraitClipMedia.ts +++ b/src/app/components/MediaLoader/configs/portraitClipMedia.ts @@ -61,7 +61,7 @@ export default ({ plugins: { toLoad: [ { - html: `${SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN}${SIMORGH_PUBLIC_STATIC_ASSETS_PATH}smpPlugins/fullscreen.js`, + html: `${SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN}${SIMORGH_PUBLIC_STATIC_ASSETS_PATH}smpPlugins/fullscreen.js?v=1`, playerOnly: true, }, ], From 9a88adfc9b56078e8ecc3725b1f91b9fdb3b0d49 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Wed, 18 Feb 2026 12:29:21 +0000 Subject: [PATCH 58/66] WS-2095: Try fix issues on mobile --- src/app/components/MediaLoader/types.ts | 3 +++ src/app/components/PortraitVideoModal/index.tsx | 10 ++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/app/components/MediaLoader/types.ts b/src/app/components/MediaLoader/types.ts index b6d8ae139f9..76b321b4afc 100644 --- a/src/app/components/MediaLoader/types.ts +++ b/src/app/components/MediaLoader/types.ts @@ -11,6 +11,9 @@ import { Translations } from '#app/models/types/translations'; import { Dispatch, SetStateAction } from 'react'; export type SMPEvent = { + detail?: { + url?: string; + }; playlist?: { items: PlaylistItem[]; }; diff --git a/src/app/components/PortraitVideoModal/index.tsx b/src/app/components/PortraitVideoModal/index.tsx index 0c7cbd39f3e..1c0b888df8d 100644 --- a/src/app/components/PortraitVideoModal/index.tsx +++ b/src/app/components/PortraitVideoModal/index.tsx @@ -179,9 +179,11 @@ export const playbackEndedCallback = async ( } }; -const pluginLoadedCallback = () => { - const player = getPlayerInstance(); - player.dispatchEvent('fullScreenPlugin.launchFullscreen'); +const pluginLoadedCallback = (e: SMPEvent) => { + if (e?.detail?.url && e?.detail?.url.includes('fullscreen')) { + const player = getPlayerInstance(); + player.dispatchEvent('fullScreenPlugin.launchFullscreen'); + } }; export const handlePrevNextVideo = ({ @@ -370,7 +372,7 @@ const PortraitVideoModal = ({ setVideoOverlayContainerRef={setVideoOverlayContainerRef} eventMapping={{ playlistLoaded: e => playlistLoadedCallback(e, blocks), - pluginLoaded: pluginLoadedCallback, + pluginLoaded: e => pluginLoadedCallback(e), fullscreenExit: onClose, statsNavigation: e => statsNavigationCallback( From 187a50c269f67a440a481594690cd5855809f677 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Wed, 18 Feb 2026 14:43:16 +0000 Subject: [PATCH 59/66] WS-2095: debug --- src/app/components/PortraitVideoModal/index.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/app/components/PortraitVideoModal/index.tsx b/src/app/components/PortraitVideoModal/index.tsx index 1c0b888df8d..f5dde7b7a85 100644 --- a/src/app/components/PortraitVideoModal/index.tsx +++ b/src/app/components/PortraitVideoModal/index.tsx @@ -179,11 +179,9 @@ export const playbackEndedCallback = async ( } }; -const pluginLoadedCallback = (e: SMPEvent) => { - if (e?.detail?.url && e?.detail?.url.includes('fullscreen')) { - const player = getPlayerInstance(); - player.dispatchEvent('fullScreenPlugin.launchFullscreen'); - } +const pluginLoadedCallback = () => { + const player = getPlayerInstance(); + player.dispatchEvent('fullScreenPlugin.launchFullscreen'); }; export const handlePrevNextVideo = ({ @@ -372,7 +370,11 @@ const PortraitVideoModal = ({ setVideoOverlayContainerRef={setVideoOverlayContainerRef} eventMapping={{ playlistLoaded: e => playlistLoadedCallback(e, blocks), - pluginLoaded: e => pluginLoadedCallback(e), + pluginLoaded: e => { + if (e?.detail?.url && e?.detail?.url.includes('fullscreen')) { + pluginLoadedCallback(); + } + }, fullscreenExit: onClose, statsNavigation: e => statsNavigationCallback( From 1c5f76d78a99d672f220c1ec41c24b816487350a Mon Sep 17 00:00:00 2001 From: emily saffron Date: Wed, 18 Feb 2026 15:11:54 +0000 Subject: [PATCH 60/66] setCurrentVideo using mediaItemChanged smp event --- src/app/components/MediaLoader/types.ts | 3 +- .../components/PortraitVideoModal/index.tsx | 30 +++++++++---------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/app/components/MediaLoader/types.ts b/src/app/components/MediaLoader/types.ts index 76b321b4afc..ccf7659c57a 100644 --- a/src/app/components/MediaLoader/types.ts +++ b/src/app/components/MediaLoader/types.ts @@ -29,7 +29,8 @@ export type MediaPlayerEvents = | 'statsNavigation' | 'pause' | 'uiControlBarShown' - | 'uiControlBarHidden'; + | 'uiControlBarHidden' + | 'mediaItemChanged'; export type EventMapping = Partial< Record void> diff --git a/src/app/components/PortraitVideoModal/index.tsx b/src/app/components/PortraitVideoModal/index.tsx index f5dde7b7a85..1853d904c28 100644 --- a/src/app/components/PortraitVideoModal/index.tsx +++ b/src/app/components/PortraitVideoModal/index.tsx @@ -133,7 +133,6 @@ export const statsNavigationCallback = async ( blocks: PortraitClipMediaBlock[], eventTrackingData: EventTrackingData, swipeTracker: ReturnType, - setCurrentVideo?: (item: PortraitClipMediaBlock) => void, ) => { const { direction, method } = e || {}; @@ -143,7 +142,6 @@ export const statsNavigationCallback = async ( const currentIndex = getCurrentIndex({ e, blocks }); const newIndex = direction === 'next' ? currentIndex + 1 : currentIndex - 1; - setCurrentVideo?.(blocks[newIndex]); const newEventTrackingData = getEventTrackingData({ eventTrackingData, selectedVideo: blocks?.[newIndex], @@ -179,6 +177,19 @@ export const playbackEndedCallback = async ( } }; +const mediaItemChangedCallback = async ({ + e, + blocks, + setCurrentVideo, +}: { + e: SMPEvent; + blocks: PortraitClipMediaBlock[]; + setCurrentVideo?: (item: PortraitClipMediaBlock) => void; +}) => { + const currentIndex = getCurrentIndex({ e, blocks }); + setCurrentVideo?.(blocks[currentIndex]); +}; + const pluginLoadedCallback = () => { const player = getPlayerInstance(); player.dispatchEvent('fullScreenPlugin.launchFullscreen'); @@ -186,18 +197,10 @@ const pluginLoadedCallback = () => { export const handlePrevNextVideo = ({ direction, - blocks, - setCurrentVideo, }: { direction: 'previous' | 'next'; - blocks: PortraitClipMediaBlock[]; - setCurrentVideo?: (item: PortraitClipMediaBlock) => void; }) => { const player = getPlayerInstance(); - const currentVideoIndex = getCurrentIndex({ blocks, player }); - const prevNextVideoIndex = - direction === 'next' ? currentVideoIndex + 1 : currentVideoIndex - 1; - setCurrentVideo?.(blocks[prevNextVideoIndex]); player?.[direction]?.(); }; @@ -335,8 +338,6 @@ const PortraitVideoModal = ({ onClick={() => handlePrevNextVideo({ direction: 'previous', - blocks, - setCurrentVideo, }) } css={styles.navButton} @@ -352,8 +353,6 @@ const PortraitVideoModal = ({ onClick={() => handlePrevNextVideo({ direction: 'next', - blocks, - setCurrentVideo, }) } css={styles.navButton} @@ -382,10 +381,11 @@ const PortraitVideoModal = ({ blocks, eventTrackingData, swipeTracker, - setCurrentVideo, ), pause: e => playbackEndedCallback(e, blocks, eventTrackingData, swipeTracker), + mediaItemChanged: e => + mediaItemChangedCallback({ e, blocks, setCurrentVideo }), uiControlBarShown: () => setControlsDisplayed?.(true), uiControlBarHidden: () => setControlsDisplayed?.(false), }} From 0a3786315a3808e55da7d6d4cd9c21bd33d1f076 Mon Sep 17 00:00:00 2001 From: emily saffron Date: Wed, 18 Feb 2026 15:51:28 +0000 Subject: [PATCH 61/66] temporary logs for preview --- src/app/components/PortraitVideoModal/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/components/PortraitVideoModal/index.tsx b/src/app/components/PortraitVideoModal/index.tsx index 1853d904c28..ab252851d06 100644 --- a/src/app/components/PortraitVideoModal/index.tsx +++ b/src/app/components/PortraitVideoModal/index.tsx @@ -192,6 +192,8 @@ const mediaItemChangedCallback = async ({ const pluginLoadedCallback = () => { const player = getPlayerInstance(); + const player2 = window.embeddedMedia.api.players().bbcMediaPlayer0; + console.log('player', getPlayerInstance(), player, 'player2', player2); player.dispatchEvent('fullScreenPlugin.launchFullscreen'); }; From 7a8986a458a109aaac1ab5d3b24140fbb3a92f97 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Wed, 18 Feb 2026 16:55:44 +0000 Subject: [PATCH 62/66] WS-2095: HCM hover focus styles --- src/app/components/ShareButton/styles.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/app/components/ShareButton/styles.ts b/src/app/components/ShareButton/styles.ts index a4ba2652d90..83caad42d5d 100644 --- a/src/app/components/ShareButton/styles.ts +++ b/src/app/components/ShareButton/styles.ts @@ -75,6 +75,14 @@ const styles = { }, }, }, + '&:hover, &:focus-visible': { + [mq.FORCED_COLOURS]: { + border: `${pixelsToRem(3)}rem solid canvasText`, + }, + }, + [mq.FORCED_COLOURS]: { + border: `${pixelsToRem(1)}rem solid canvasText`, + }, }), }; export default styles; From 7fa5b9f2e9d7a16fe611775e7dee441af7ee1193 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Wed, 18 Feb 2026 17:00:24 +0000 Subject: [PATCH 63/66] WS-2095: Updates visually hidden text --- src/app/components/ShareButton/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/components/ShareButton/index.tsx b/src/app/components/ShareButton/index.tsx index 0d2bdc75613..8a578368c87 100644 --- a/src/app/components/ShareButton/index.tsx +++ b/src/app/components/ShareButton/index.tsx @@ -44,6 +44,7 @@ const ShareButton = ({ const { translations: { liveExperiencePage: { shareButtonText = 'Share' }, + media: { video }, }, } = use(ServiceContext); const handleShare = async (event: MouseEvent) => { @@ -93,7 +94,7 @@ const ShareButton = ({ ) : ( - {shareButtonText}, {title} + {`${shareButtonText} ${video}`} )} From c79a60db7f2e518dd7b3d5f0109c06bc0be6605d Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Thu, 19 Feb 2026 11:56:34 +0000 Subject: [PATCH 64/66] WS-2095: UX update - align share button spacing with PS --- src/app/components/ShareButton/styles.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/components/ShareButton/styles.ts b/src/app/components/ShareButton/styles.ts index 83caad42d5d..931d95a907c 100644 --- a/src/app/components/ShareButton/styles.ts +++ b/src/app/components/ShareButton/styles.ts @@ -3,15 +3,13 @@ import pixelsToRem from '../../utilities/pixelsToRem'; import { focusIndicatorThickness } from '../ThemeProvider/focusIndicator'; const styles = { - button: ({ palette, fontSizes, fontVariants, spacings, mq }: Theme) => + button: ({ palette, fontSizes, fontVariants, mq }: Theme) => css({ display: 'inline-flex', alignItems: 'center', ...fontSizes.pica, ...fontVariants.sansBold, padding: `${pixelsToRem(10)}rem`, - marginBottom: `${spacings.TRIPLE}rem`, - marginInlineStart: `${spacings.DOUBLE}rem`, border: `${pixelsToRem(2)}rem solid ${palette.BLACK}`, cursor: 'pointer', '&:hover, &:focus-visible': { @@ -40,6 +38,8 @@ const styles = { css({ color: palette.BLACK, backgroundColor: palette.WHITE, + marginInlineStart: `${spacings.DOUBLE}rem`, + marginBottom: `${spacings.TRIPLE}rem`, svg: { width: `${spacings.DOUBLE}rem`, height: `${spacings.DOUBLE}rem`, From c1bc6f7210d2066100ec5c60b4c00fec9a8e1b0f Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Wed, 25 Feb 2026 11:42:00 +0000 Subject: [PATCH 65/66] WS-2095: Debugs, uses CSS to hide player and not destroy it --- src/app/components/PortraitVideoCarousel/index.styles.ts | 9 +++++++++ src/app/components/PortraitVideoCarousel/index.tsx | 6 +++--- src/app/components/PortraitVideoModal/index.tsx | 3 +++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/app/components/PortraitVideoCarousel/index.styles.ts b/src/app/components/PortraitVideoCarousel/index.styles.ts index fb620566e03..497fce5fa0a 100644 --- a/src/app/components/PortraitVideoCarousel/index.styles.ts +++ b/src/app/components/PortraitVideoCarousel/index.styles.ts @@ -10,6 +10,15 @@ const styles = { display: 'none', }, }), + showModal: () => + css({ + border: '1px solid yellow', + display: 'flex', + }), + hiddenModal: () => + css({ + display: 'none', + }), heading: ({ palette, mq, spacings }: Theme) => css({ display: 'inline-block', diff --git a/src/app/components/PortraitVideoCarousel/index.tsx b/src/app/components/PortraitVideoCarousel/index.tsx index df7b42db06a..3cbcf33e616 100644 --- a/src/app/components/PortraitVideoCarousel/index.tsx +++ b/src/app/components/PortraitVideoCarousel/index.tsx @@ -82,7 +82,7 @@ const PortraitVideoCarousel = ({ const handleCloseModal = useCallback(() => { setIsModalOpen(false); - setSelectedVideoIndex(null); + // setSelectedVideoIndex(null); }, []); if (isLite || isAmp) return null; @@ -133,8 +133,7 @@ const PortraitVideoCarousel = ({ ))} - {isModalOpen && - selectedVideoIndex !== null && + {selectedVideoIndex !== null && createPortal( , document.body, )} diff --git a/src/app/components/PortraitVideoModal/index.tsx b/src/app/components/PortraitVideoModal/index.tsx index ab252851d06..d2239ddc7bb 100644 --- a/src/app/components/PortraitVideoModal/index.tsx +++ b/src/app/components/PortraitVideoModal/index.tsx @@ -217,6 +217,7 @@ export interface PortraitVideoModalProps { >; setCurrentVideo?: (item: PortraitClipMediaBlock) => void; setControlsDisplayed?: (displayed: boolean) => void; + className?: string; } const PortraitVideoModal = ({ @@ -227,6 +228,7 @@ const PortraitVideoModal = ({ setVideoOverlayContainerRef, setCurrentVideo, setControlsDisplayed, + className, }: PortraitVideoModalProps) => { const { translations: { @@ -320,6 +322,7 @@ const PortraitVideoModal = ({ css={styles.modal} id="portrait-video-modal-container" {...viewTracker} + className={className} >