From 6e4138cac6e129e3aa54c82aa70a93a5d224e3bd Mon Sep 17 00:00:00 2001 From: sharathkannan <138484653+sharath-kannan@users.noreply.github.com> Date: Wed, 4 Dec 2024 15:37:39 +0530 Subject: [PATCH] Feat(MWPW-146367):Added accessibility player controls (NON MPC) (#3053) * updated feature with accessibility code * video accessiblity added for carousels * added opt-out functionality * fixed linting errors * fixed unit test cases * fixed adobe tv issue * hover and focus added * controls positioned for rtl * hide-controls hash params added * how to block controls position bug fix * dark mode|bug fixes * code enhancement * pause-play bug mouse click bug fix * marquee dark mode|positioning fix * code enhancement * handled marquee backward compatiblity * Added placeholder for labels|indexed video aria-labels * aria-label added for hover play videos * async awaited decorateVideo in video.js and other linting errors * video indexes added * random video index and unit test cases updated * daa-ll is synced along with aria-label * code enhancement * nala test fix|code coverege * nala test bug fix * nala test fix * right-left positioning is done for screens > 600px and a img fix * getFedsconfig moved to feds file|url fetched from fedRoot function * linting fix * icons adapted to the figma * carousel and how-to fix with other minor fixes * playpause wrapper adjusted for window * icon offset bug fix * indentation of string literal * figma match * figma focus match --- libs/blocks/adobetv/adobetv.css | 1 + libs/blocks/aside/aside.css | 1 + libs/blocks/aside/aside.js | 4 +- libs/blocks/brick/brick.css | 9 + libs/blocks/carousel/carousel.css | 2 +- libs/blocks/carousel/carousel.js | 8 +- libs/blocks/figure/figure.js | 10 +- libs/blocks/global-footer/global-footer.js | 3 +- .../features/profile/dropdown.js | 3 +- .../features/search/gnav-search.js | 2 +- .../global-navigation/global-navigation.js | 6 +- .../global-navigation/utilities/utilities.js | 20 +-- libs/blocks/hero-marquee/hero-marquee.js | 2 +- libs/blocks/how-to/how-to.js | 2 +- libs/blocks/marquee/marquee.js | 2 +- libs/blocks/video/video.css | 149 +++++++++++++++++ libs/features/webapp-prompt/webapp-prompt.js | 2 +- libs/utils/decorate.js | 158 +++++++++++++++++- libs/utils/federated.js | 18 ++ nala/blocks/marquee/marquee.test.js | 10 +- test/blocks/adobetv/adobetv.test.js | 6 +- test/blocks/figure/figure.test.js | 3 +- .../utilities/utilities.test.js | 2 +- test/blocks/how-to/how-to.test.js | 2 +- test/blocks/how-to/mocks/body.html | 2 +- test/blocks/video/mocks/body.html | 9 +- test/blocks/video/video.test.js | 135 ++++++++++++--- 27 files changed, 493 insertions(+), 78 deletions(-) diff --git a/libs/blocks/adobetv/adobetv.css b/libs/blocks/adobetv/adobetv.css index 7907e053ca..28322d4585 100644 --- a/libs/blocks/adobetv/adobetv.css +++ b/libs/blocks/adobetv/adobetv.css @@ -1,4 +1,5 @@ @import url('../../styles/iframe.css'); +@import url('../video/video.css'); a[href*='.mp4'].hide-video { visibility: hidden !important; diff --git a/libs/blocks/aside/aside.css b/libs/blocks/aside/aside.css index 7feb805ada..54615ee978 100644 --- a/libs/blocks/aside/aside.css +++ b/libs/blocks/aside/aside.css @@ -298,6 +298,7 @@ } .aside.rounded-corners .foreground .image img, +.aside.rounded-corners .foreground .image:not(:has(.video-container)) .pause-play-wrapper, .aside.rounded-corners .foreground .image video { border-radius: 16px; } diff --git a/libs/blocks/aside/aside.js b/libs/blocks/aside/aside.js index 7519967de3..92ea2500d4 100644 --- a/libs/blocks/aside/aside.js +++ b/libs/blocks/aside/aside.js @@ -197,8 +197,8 @@ function decorateLayout(el) { } const foregroundImage = foreground.querySelector(':scope > div:not(.text) img')?.closest('div'); const bgImage = el.querySelector(':scope > div:not(.text):not(.foreground) img')?.closest('div'); - const foregroundMedia = foreground.querySelector(':scope > div:not(.text) video, :scope > div:not(.text) a:is([href*=".mp4"], [href*="tv.adobe.com"]), :scope > div:not(.text) iframe[src*="tv.adobe.com"]')?.closest('div'); - + const foregroundMedia = foreground.querySelector(':scope > div:not(.text) :is(.video-container, video, a[href*=".mp4"], a[href*="tv.adobe.com"]), :scope > div:not(.text) iframe[src*="tv.adobe.com"]') + ?.closest('div:not(.video-container)'); const bgMedia = el.querySelector(':scope > div:not(.text):not(.foreground) video, :scope > div:not(.text):not(.foreground) a:is([href*=".mp4"], [href*="tv.adobe.com"])')?.closest('div'); const image = foregroundImage ?? bgImage; const asideMedia = foregroundMedia ?? bgMedia ?? image; diff --git a/libs/blocks/brick/brick.css b/libs/blocks/brick/brick.css index 43cd31584d..4ed08eb815 100644 --- a/libs/blocks/brick/brick.css +++ b/libs/blocks/brick/brick.css @@ -98,6 +98,10 @@ margin: 0; } +.brick .foreground div > .video-container { + margin: 0; +} + .brick .foreground div > * { margin-top: var(--spacing-xs); } @@ -342,6 +346,11 @@ position: absolute; } + .brick.split.row .foreground .brick-media .video-container img, + .brick.split.row .foreground .brick-media .video-container video { + width: 100%; + } + .brick .foreground .brick-media video, .brick.split.row .foreground .brick-media video { object-fit: fill; diff --git a/libs/blocks/carousel/carousel.css b/libs/blocks/carousel/carousel.css index 41e70a1ef2..08bffdf704 100644 --- a/libs/blocks/carousel/carousel.css +++ b/libs/blocks/carousel/carousel.css @@ -397,7 +397,7 @@ html[dir="rtl"] .carousel-slides .section.carousel-slide { overflow: hidden; } -.carousel .carousel-slide > div p > video { +.carousel .carousel-slide > div p :is(.video-holder, video) { width: 100%; height: auto; } diff --git a/libs/blocks/carousel/carousel.js b/libs/blocks/carousel/carousel.js index 1bd73281f6..4bb57f7180 100644 --- a/libs/blocks/carousel/carousel.js +++ b/libs/blocks/carousel/carousel.js @@ -168,7 +168,7 @@ function moveSlides(event, carouselElements, jumpToIndex) { referenceSlide.classList.remove('reference-slide'); referenceSlide.style.order = null; activeSlide.classList.remove('active'); - activeSlide.querySelectorAll('a').forEach((focusableElement) => { focusableElement.setAttribute('tabindex', -1); }); + activeSlide.querySelectorAll('a, video').forEach((focusableElement) => focusableElement.setAttribute('tabindex', -1)); activeSlideIndicator.classList.remove('active'); activeSlideIndicator.setAttribute('tabindex', -1); @@ -230,10 +230,12 @@ function moveSlides(event, carouselElements, jumpToIndex) { if (index < show) { tabIndex = 0; } - slide.querySelectorAll('a').forEach((focusableElement) => { focusableElement.setAttribute('tabindex', tabIndex); }); + slide.querySelectorAll('a,:not(.video-container, .pause-play-wrapper) > video') + .forEach((focusableElement) => { focusableElement.setAttribute('tabindex', tabIndex); }); }); } else { - activeSlide.querySelectorAll('a').forEach((focusableElement) => { focusableElement.setAttribute('tabindex', 0); }); + activeSlide.querySelectorAll('a,:not(.video-container, .pause-play-wrapper) > video') + .forEach((focusableElement) => { focusableElement.setAttribute('tabindex', 0); }); } activeSlideIndicator.classList.add('active'); activeSlideIndicator.setAttribute('tabindex', 0); diff --git a/libs/blocks/figure/figure.js b/libs/blocks/figure/figure.js index cd0d80cd88..40c9e40c28 100644 --- a/libs/blocks/figure/figure.js +++ b/libs/blocks/figure/figure.js @@ -1,4 +1,4 @@ -import { applyHoverPlay, decorateAnchorVideo } from '../../utils/decorate.js'; +import { applyHoverPlay, decorateAnchorVideo, applyAccessibilityEvents, decoratePausePlayWrapper, isVideoAccessible } from '../../utils/decorate.js'; import { createTag } from '../../utils/utils.js'; function buildCaption(pEl) { @@ -31,7 +31,11 @@ function decorateVideo(clone, figEl) { ); } applyHoverPlay(videoTag); - figEl.prepend(videoTag); + if (!videoTag.controls && isVideoAccessible(anchorTag)) { + applyAccessibilityEvents(videoTag); + decoratePausePlayWrapper(videoTag, 'autoplay'); + } + figEl.prepend(clone.querySelector('.video-container, .pause-play-wrapper, video')); } } @@ -68,7 +72,7 @@ export function buildFigure(blockEl) { const link = clone.querySelector('a'); if (link) { const img = figEl.querySelector('picture') || figEl.querySelector('video'); - if (img) { + if (img && !link.classList.contains('pause-play-wrapper')) { // wrap picture or video in A tag link.textContent = ''; link.append(img); diff --git a/libs/blocks/global-footer/global-footer.js b/libs/blocks/global-footer/global-footer.js index 6c74620291..21e18208cd 100644 --- a/libs/blocks/global-footer/global-footer.js +++ b/libs/blocks/global-footer/global-footer.js @@ -9,7 +9,6 @@ import { } from '../../utils/utils.js'; import { - getFedsPlaceholderConfig, getExperienceName, getAnalyticsValue, loadDecorateMenu, @@ -23,7 +22,7 @@ import { isDarkMode, } from '../global-navigation/utilities/utilities.js'; -import { getFederatedUrl } from '../../utils/federated.js'; +import { getFederatedUrl, getFedsPlaceholderConfig } from '../../utils/federated.js'; import { replaceKey } from '../../features/placeholders.js'; diff --git a/libs/blocks/global-navigation/features/profile/dropdown.js b/libs/blocks/global-navigation/features/profile/dropdown.js index 836f9cd834..d874963e82 100644 --- a/libs/blocks/global-navigation/features/profile/dropdown.js +++ b/libs/blocks/global-navigation/features/profile/dropdown.js @@ -1,6 +1,7 @@ import { getConfig } from '../../../../utils/utils.js'; -import { toFragment, getFedsPlaceholderConfig, trigger, closeAllDropdowns, logErrorFor } from '../../utilities/utilities.js'; +import { toFragment, trigger, closeAllDropdowns, logErrorFor } from '../../utilities/utilities.js'; import { replaceKeyArray } from '../../../../features/placeholders.js'; +import { getFedsPlaceholderConfig } from '../../../../utils/federated.js'; const getLanguage = (ietfLocale) => { if (!ietfLocale.length) return 'en'; diff --git a/libs/blocks/global-navigation/features/search/gnav-search.js b/libs/blocks/global-navigation/features/search/gnav-search.js index ca6df552b8..76532c5d31 100644 --- a/libs/blocks/global-navigation/features/search/gnav-search.js +++ b/libs/blocks/global-navigation/features/search/gnav-search.js @@ -1,6 +1,5 @@ import { toFragment, - getFedsPlaceholderConfig, isDesktop, setCurtainState, trigger, @@ -10,6 +9,7 @@ import { import { replaceKeyArray } from '../../../../features/placeholders.js'; import { getConfig } from '../../../../utils/utils.js'; import { debounce } from '../../../../utils/action.js'; +import { getFedsPlaceholderConfig } from '../../../../utils/federated.js'; const CONFIG = { suggestions: { diff --git a/libs/blocks/global-navigation/global-navigation.js b/libs/blocks/global-navigation/global-navigation.js index 55815c5994..9d13051e86 100644 --- a/libs/blocks/global-navigation/global-navigation.js +++ b/libs/blocks/global-navigation/global-navigation.js @@ -14,7 +14,6 @@ import { getActiveLink, getAnalyticsValue, getExperienceName, - getFedsPlaceholderConfig, hasActiveLink, isActiveLink, icons, @@ -40,6 +39,7 @@ import { setDisableAEDState, getDisableAEDState, } from './utilities/utilities.js'; +import { getFedsPlaceholderConfig } from '../../utils/federated.js'; import { replaceKey, replaceKeyArray } from '../../features/placeholders.js'; @@ -679,7 +679,7 @@ class Gnav { return this.loadDelayed().then(() => { this.blocks.search.instance = new this.Search(this.blocks.search.config); - }).catch(() => {}); + }).catch(() => { }); }; isToggleExpanded = () => this.elements.mobileToggle?.getAttribute('aria-expanded') === 'true'; @@ -773,7 +773,7 @@ class Gnav { if (allSvgImgs.length === 2) return allSvgImgs[1]; const images = blockLinks.filter((blockLink) => imgRegex.test(blockLink.href) - || imgRegex.test(blockLink.textContent)); + || imgRegex.test(blockLink.textContent)); if (images.length === 2) return getBrandImage(images[1], isBrandImage); } const svgImg = rawBlock.querySelector('picture img[src$=".svg"]'); diff --git a/libs/blocks/global-navigation/utilities/utilities.js b/libs/blocks/global-navigation/utilities/utilities.js index 370ae1f2ca..f880d14ae1 100644 --- a/libs/blocks/global-navigation/utilities/utilities.js +++ b/libs/blocks/global-navigation/utilities/utilities.js @@ -2,7 +2,7 @@ import { getConfig, getMetadata, loadStyle, loadLana, decorateLinks, localizeLink, } from '../../../utils/utils.js'; -import { getFederatedContentRoot, getFederatedUrl } from '../../../utils/federated.js'; +import { getFederatedContentRoot, getFederatedUrl, getFedsPlaceholderConfig } from '../../../utils/federated.js'; import { processTrackingLabels } from '../../../martech/attributes.js'; import { replaceText } from '../../../features/placeholders.js'; @@ -108,24 +108,6 @@ export const federatePictureSources = ({ section, forceFederate } = {}) => { }); }; -let fedsPlaceholderConfig; -export const getFedsPlaceholderConfig = ({ useCache = true } = {}) => { - if (useCache && fedsPlaceholderConfig) return fedsPlaceholderConfig; - - const { locale, placeholders } = getConfig(); - const libOrigin = getFederatedContentRoot(); - - fedsPlaceholderConfig = { - locale: { - ...locale, - contentRoot: `${libOrigin}${locale.prefix}/federal/globalnav`, - }, - placeholders, - }; - - return fedsPlaceholderConfig; -}; - export function getAnalyticsValue(str, index) { if (typeof str !== 'string' || !str.length) return str; diff --git a/libs/blocks/hero-marquee/hero-marquee.js b/libs/blocks/hero-marquee/hero-marquee.js index c9f409757d..f747559e29 100644 --- a/libs/blocks/hero-marquee/hero-marquee.js +++ b/libs/blocks/hero-marquee/hero-marquee.js @@ -187,7 +187,7 @@ export default async function init(el) { foreground.classList.add('foreground', `cols-${fRows.length}`); let copy = fRows[0]; const anyTag = foreground.querySelector('p, h1, h2, h3, h4, h5, h6'); - const asset = foreground.querySelector('div > picture, div > video, div > a[href*=".mp4"], div > a.image-link'); + const asset = foreground.querySelector('div > picture, :is(.video-container, .pause-play-wrapper), div > video, div > a[href*=".mp4"], div > a.image-link'); const allRows = foreground.querySelectorAll('div > div'); copy = anyTag.closest('div'); copy.classList.add('copy'); diff --git a/libs/blocks/how-to/how-to.js b/libs/blocks/how-to/how-to.js index ce7cf146df..8080319fd0 100644 --- a/libs/blocks/how-to/how-to.js +++ b/libs/blocks/how-to/how-to.js @@ -47,7 +47,7 @@ const setJsonLd = (heading, description, mainImage, stepsLd) => { }; const getImage = (el) => el.querySelector('picture') || el.querySelector('a[href$=".svg"'); -const getVideo = (el) => el.querySelector('video') || el.querySelector('.milo-video'); +const getVideo = (el) => el.querySelector('.video-container, .pause-play-wrapper, video, .milo-video'); const getHowToInfo = (el) => { const infoDiv = el.querySelector(':scope > div > div'); diff --git a/libs/blocks/marquee/marquee.js b/libs/blocks/marquee/marquee.js index d747586fd3..21c3e8c8a5 100644 --- a/libs/blocks/marquee/marquee.js +++ b/libs/blocks/marquee/marquee.js @@ -88,7 +88,7 @@ function decorateSplit(el, foreground, media) { let mediaCreditInner; const txtContent = media?.lastChild?.textContent?.trim(); - if (txtContent?.match(/^http.*\.mp4/) || media?.lastChild?.tagName === 'VIDEO') return; + if (txtContent?.match(/^http.*\.mp4/) || media?.lastChild?.tagName === 'VIDEO' || media.querySelector('.video-holder video')) return; if (txtContent) { mediaCreditInner = createTag('p', { class: 'body-s' }, txtContent); } else if (media.lastElementChild?.tagName !== 'PICTURE') { diff --git a/libs/blocks/video/video.css b/libs/blocks/video/video.css index f6ec58c065..6538e0596a 100644 --- a/libs/blocks/video/video.css +++ b/libs/blocks/video/video.css @@ -5,4 +5,153 @@ a[href*='.mp4'].hide-video { video { max-width: 100%; height: auto; + object-fit: cover; +} + +:is(.marquee, .aside.split) .pause-play-wrapper img.accessibility-control { + min-height: 40px; +} + +.video-container { + display: flex; + position: relative; + height: 100%; + width: fit-content; + margin: auto; +} + +:is(.aside, .marquee, .quiz-marquee) .video-container { + width: auto; +} + +.brick-media .video-container { + width: 100%; + height: 100%; + border-radius: inherit; +} + +.pause-play-wrapper { + display: flex; + width: fit-content; + margin: auto; +} + +.video-container .pause-play-wrapper { + position: absolute; + bottom: 2%; + right: 2%; + margin: 0; + justify-content: center; + align-items: center; + border-radius: 50%; + z-index: 2; + padding: 3px; + cursor: pointer; +} + +.video-container .pause-play-wrapper .offset-filler { + display: inherit; + justify-content: center; + align-items: center; + width: 40px; + height: 40px; + border-radius: inherit; + background: var(--color-gray-800); +} + +:is(.marquee:not(.light), .dark) .video-container .pause-play-wrapper { + padding: 1px; +} + +:is(.marquee:not(.light), .dark) .video-container .pause-play-wrapper:focus-visible { + background: #000; +} + +:is(.marquee:not(.light), .dark) .video-container .pause-play-wrapper .offset-filler { + border: 2px solid #fff; +} + +.video-container .pause-play-wrapper:focus-visible { + background: #fff; +} + +.video-container .pause-play-wrapper .offset-filler:hover { + background: #000; +} + +.video-container .pause-play-wrapper:focus-visible { + outline: var(--color-accent-focus-ring) solid 2px; +} + +.video-container .pause-play-wrapper .offset-filler.is-playing .play-icon, +.video-container .pause-play-wrapper .offset-filler:not(.is-playing) .pause-icon { + display: none; +} + +:is(.editorial-card, .hero-marquee, .marquee):not(:has(.video-container)) .pause-play-wrapper, +.editorial-card .video-container { + width: auto; + height: 100%; +} + +.brick .brick-media:not(:has(.video-container)) .pause-play-wrapper { + border-radius: 0; + border-top-right-radius: inherit; + border-bottom-right-radius: inherit; + width: auto; + height: 100%; + margin: 0; +} + +[dir="rtl"] .brick .brick-media:not(:has(.video-container)) .pause-play-wrapper { + border-top-left-radius: inherit; + border-bottom-left-radius: inherit; +} + +.hero-marquee .background .video-container { + position: inherit; +} + +:is(.video-container .pause-play-wrapper, .aside.split.split-left .split-image) img.accessibility-control { + width: auto; +} + +.video-container .pause-play-wrapper img.hidden { + display: none; +} + +.marquee .background .video-container { + display: contents; +} + +.how-to .how-to-media .video-container { + height: fit-content; +} + +@media (min-width: 600px) { + .media:not(.media-reverse-mobile, .media-reversed) .video-container .pause-play-wrapper, + :is(.marquee.row-reversed .asset, .marquee-anchors, .hero-marquee.asset-left) .video-container .pause-play-wrapper, + :is(.aside:not(.split), .aside.split.split-right) .video-container .pause-play-wrapper { + left: 2%; + } + + :is(.section[class*="-up"] .media .foreground .image:first-child, .aside .foreground .image:nth-last-child(1)) + .video-container .pause-play-wrapper { + left: auto; + right: 2%; + } + + [dir="rtl"] :is(.marquee.split:not(.row-reversed), .media:is(.media-reverse-mobile, .media-reversed), + .hero-marquee:not(.asset-left) :is([class*="foreground"]), .aside.split:not(.split-right), + .aside .foreground.container .image:nth-last-child(1), .brick.media-right, .how-to) .video-container .pause-play-wrapper { + left: 2%; + right: auto; + } +} + +@media (min-width: 600px) and (max-width: 1199px) { + .hero-marquee.asset-left .video-container.video-holder .pause-play-wrapper { + left: auto; + right: 2%; + } } diff --git a/libs/features/webapp-prompt/webapp-prompt.js b/libs/features/webapp-prompt/webapp-prompt.js index ea9bc072d0..894d010d0b 100644 --- a/libs/features/webapp-prompt/webapp-prompt.js +++ b/libs/features/webapp-prompt/webapp-prompt.js @@ -1,5 +1,4 @@ import { - getFedsPlaceholderConfig, getUserProfile, icons, lanaLog, @@ -7,6 +6,7 @@ import { } from '../../blocks/global-navigation/utilities/utilities.js'; import { getConfig, decorateSVG } from '../../utils/utils.js'; import { replaceKey, replaceText } from '../placeholders.js'; +import { getFedsPlaceholderConfig } from '../../utils/federated.js'; export const DISMISSAL_CONFIG = { animationCount: 2, diff --git a/libs/utils/decorate.js b/libs/utils/decorate.js index 36e23dd055..580d2fcc1f 100644 --- a/libs/utils/decorate.js +++ b/libs/utils/decorate.js @@ -1,6 +1,17 @@ import { createTag, loadStyle, getConfig, createIntersectionObserver } from './utils.js'; +import { getFederatedContentRoot, getFedsPlaceholderConfig } from './federated.js'; const { miloLibs, codeRoot } = getConfig(); +const HIDE_CONTROLS = '_hide-controls'; +let firstVideo = null; +let videoLabels = { + playMotion: 'Play', + pauseMotion: 'Pause', + pauseIcon: 'Pause icon', + playIcon: 'Play icon', + hasFetched: false, +}; +let videoCounter = 0; export function decorateButtons(el, size) { const buttons = el.querySelectorAll('em a, strong a, p > a strong'); @@ -209,7 +220,7 @@ export function getImgSrc(pic) { return source?.srcset ? `poster='${source.srcset}'` : ''; } -function getVideoAttrs(hash, dataset) { +export function getVideoAttrs(hash, dataset) { const isAutoplay = hash?.includes('autoplay'); const isAutoplayOnce = hash?.includes('autoplay1'); const playOnHover = hash?.includes('hoverplay'); @@ -234,12 +245,80 @@ function getVideoAttrs(hash, dataset) { return `${globalAttrs} controls`; } +export function syncPausePlayIcon(video) { + if (!video.getAttributeNames().includes('data-hoverplay')) { + const offsetFiller = video.closest('.video-holder').querySelector('.offset-filler'); + const anchorTag = video.closest('.video-holder').querySelector('a'); + offsetFiller?.classList.toggle('is-playing'); + const isPlaying = offsetFiller?.classList.contains('is-playing'); + const indexOfVideo = (anchorTag.getAttribute('video-index') === '1' && videoCounter === 1) ? '' : anchorTag.getAttribute('video-index'); + const changedLabel = `${isPlaying ? videoLabels?.pauseMotion : videoLabels?.playMotion}`; + const oldLabel = `${!isPlaying ? videoLabels?.pauseMotion : videoLabels?.playMotion}`; + const ariaLabel = `${changedLabel} ${indexOfVideo}`.trim(); + anchorTag?.setAttribute('aria-label', `${ariaLabel} `); + anchorTag?.setAttribute('aria-pressed', isPlaying ? 'true' : 'false'); + const daaLL = anchorTag.getAttribute('daa-ll'); + if (daaLL) anchorTag.setAttribute('daa-ll', daaLL.replace(oldLabel, changedLabel)); + } +} + +export function addAccessibilityControl(videoString, videoAttrs, indexOfVideo, tabIndex = 0) { + if (videoAttrs.includes('controls')) return videoString; + const fedRoot = getFederatedContentRoot(); + if (videoAttrs.includes('hoverplay')) { + return `${videoString}`; + } + return ` +
Follow these easy steps to compress a large PDF file online:
- +