From b48edbb055c591d0c63c25420fb3078f3a9fe609 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 13 Jan 2026 21:13:32 -0500 Subject: [PATCH] Refactor inline styles to CSS rules --- css/tailwind.source.css | 74 ++++++++++- docs/moderation/fixtures/index.html | 18 ++- js/channelProfile.js | 52 +++++++- js/index.js | 101 ++++++++------- js/ui/components/SimilarContentCard.js | 67 +++++++--- js/ui/components/VideoModal.js | 33 ++++- js/ui/overlay/popoverEngine.js | 159 +++++++++++++++++++++--- js/ui/profileModalController.js | 72 ++--------- js/utils/qrcode.js | 116 +++++++++++++++-- tests/e2e/popover.spec.ts | 18 ++- tests/position-floating-panel.test.mjs | 33 ++--- tests/profile-modal-controller.test.mjs | 22 ---- tests/ui/popoverEngine.test.mjs | 30 +++-- tests/ui/similar-content-card.test.mjs | 8 +- 14 files changed, 579 insertions(+), 224 deletions(-) diff --git a/css/tailwind.source.css b/css/tailwind.source.css index 81d31079..54a56e3e 100644 --- a/css/tailwind.source.css +++ b/css/tailwind.source.css @@ -448,6 +448,11 @@ .popover-panel, .popover__panel { @apply focus-ring pointer-events-none z-overlay-popover min-w-menu max-w-popover-safe scale-95 rounded-overlay border border-border border-opacity-40 bg-overlay-panel px-overlay-panel-inline py-overlay-panel-block text-sm text-text opacity-0 shadow-overlay-panel transition-opacity transition-transform duration-base ease-out; + position: var(--popover-strategy, fixed); + left: var(--popover-left, 0px); + top: var(--popover-top, 0px); + max-width: var(--popover-max-width, none); + max-height: var(--popover-max-height, none); } .popover-panel[data-state="open"], @@ -460,6 +465,13 @@ @apply pointer-events-none scale-95 opacity-0; } + [data-popover-arrow="true"] { + left: var(--popover-arrow-left, auto); + right: var(--popover-arrow-right, auto); + top: var(--popover-arrow-top, auto); + bottom: var(--popover-arrow-bottom, auto); + } + .p-overlay-panel { @apply px-overlay-panel-inline py-overlay-panel-block; } @@ -808,6 +820,10 @@ @apply inline-flex flex-col items-start gap-sm rounded-xl border px-3 py-1.5 text-xs font-semibold normal-case tracking-tight shadow-sm; } + .moderation-badge--interactive { + pointer-events: auto; + } + .moderation-badge__actions { @apply inline-flex flex-nowrap items-center gap-sm; } @@ -2104,6 +2120,9 @@ @apply relative block shrink-0 overflow-hidden rounded-lg bg-surface-alt; width: 168px; aspect-ratio: 16 / 9; + background-image: var(--similar-card-thumb-url, none); + background-position: center; + background-size: cover; } @media (max-width: theme("screens.sm")) { @@ -2126,6 +2145,7 @@ .player-modal__similar-card-title { @apply text-sm font-semibold leading-snug text-text transition-colors duration-base ease-out; + min-width: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; @@ -2160,6 +2180,7 @@ .player-modal__similar-card-meta { @apply flex flex-wrap items-center gap-1 text-2xs text-muted-strong; + min-width: 0; } .player-modal__similar-card-timestamp { @@ -2442,6 +2463,8 @@ inset-block-start: 0; block-size: 100%; max-inline-size: 100%; + inline-size: var(--video-modal-reaction-fill, 0%); + width: var(--video-modal-reaction-fill, 0%); background-image: linear-gradient( 90deg, var(--color-accent) 0%, @@ -3338,6 +3361,7 @@ .qr-code__table { border: 0; border-collapse: collapse; + margin: var(--qr-code-table-margin, 0); } .qr-code__cell { @@ -3354,6 +3378,22 @@ background-color: var(--qr-code-color-dark); } + .qr-code__container[data-qr-code-mode="image"] .qr-code__canvas { + display: none; + } + + .qr-code__container[data-qr-code-mode="image"] .qr-code__image { + display: block; + } + + .qr-code__container[data-qr-code-mode="canvas"] .qr-code__canvas { + display: block; + } + + .qr-code__container[data-qr-code-mode="canvas"] .qr-code__image { + display: none; + } + .fade-in { opacity: 0; animation: interface-fade-in 280ms ease-out forwards; @@ -3568,18 +3608,26 @@ z-index: var(--z-modal-content); } - #modalContainer > #profileModal { + #modalContainer > #profileModal, + #profileModal[data-modal-stack="top"] { z-index: var(--z-modal-top-root); } - #modalContainer > #profileModal > .bv-modal-backdrop { + #modalContainer > #profileModal > .bv-modal-backdrop, + #profileModal[data-modal-stack="top"] > .bv-modal-backdrop { z-index: var(--z-modal-top-overlay); } - #modalContainer > #profileModal .bv-modal__panel { + #modalContainer > #profileModal .bv-modal__panel, + #profileModal[data-modal-stack="top"] .bv-modal__panel { z-index: var(--z-modal-top-content); } + .bv-modal[data-nested-modal-active="true"] { + pointer-events: none; + visibility: hidden; + } + #modalContainer > #loginModal { z-index: var(--z-modal-peak-root); } @@ -4389,6 +4437,26 @@ scrollbar-gutter: stable both-edges; } + .sidebar-dropup-panel__inner.is-measuring { + position: absolute; + width: max-content; + max-width: none; + visibility: hidden; + pointer-events: none; + left: -9999px; + right: auto; + top: auto; + bottom: auto; + } + + .sidebar-dropup-measure { + position: absolute; + visibility: hidden; + pointer-events: none; + height: 0; + overflow: hidden; + } + .sidebar-dropup-panel__inner > * { width: 100%; min-width: 0; diff --git a/docs/moderation/fixtures/index.html b/docs/moderation/fixtures/index.html index 967da4f3..372e3936 100644 --- a/docs/moderation/fixtures/index.html +++ b/docs/moderation/fixtures/index.html @@ -34,6 +34,16 @@ color: #cbd5f5; margin-bottom: 1rem; } + .fixture-toggle { + margin-top: 0.75rem; + font-size: 0.875rem; + color: #cbd5f5; + } + .fixture-toggle__label { + display: inline-flex; + align-items: center; + gap: 0.5rem; + } @@ -581,14 +591,10 @@ if (fixture.blockToggle && fixture.blockToggle.pubkey) { const toggleWrapper = document.createElement("div"); - toggleWrapper.style.marginTop = "0.75rem"; - toggleWrapper.style.fontSize = "0.875rem"; - toggleWrapper.style.color = "#cbd5f5"; + toggleWrapper.classList.add("fixture-toggle"); const label = document.createElement("label"); - label.style.display = "inline-flex"; - label.style.alignItems = "center"; - label.style.gap = "0.5rem"; + label.classList.add("fixture-toggle__label"); const input = document.createElement("input"); input.type = "checkbox"; diff --git a/js/channelProfile.js b/js/channelProfile.js index 0d33fb1f..d6221adf 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -90,6 +90,53 @@ const channelModerationBadgeState = { }; const channelVideoCardsById = new Map(); +let channelBannerStyleCounter = 0; +const channelBannerStyleRegistry = new WeakMap(); + +function getChannelBannerStyleState(doc) { + if (!doc || typeof doc.createElement !== "function") { + return null; + } + const existing = channelBannerStyleRegistry.get(doc); + if (existing) { + return existing; + } + let styleNode = doc.getElementById?.("channelBannerStyles") || null; + if (!(styleNode instanceof HTMLStyleElement)) { + styleNode = doc.createElement("style"); + styleNode.id = "channelBannerStyles"; + doc.head?.appendChild(styleNode); + } + const state = { + styleNode, + rules: new Map(), + }; + channelBannerStyleRegistry.set(doc, state); + return state; +} + +function updateChannelBannerBackground(el, url) { + const doc = el?.ownerDocument || (typeof document !== "undefined" ? document : null); + const state = getChannelBannerStyleState(doc); + if (!state || !el) { + return; + } + + if (!el.dataset.channelBannerStyleId) { + channelBannerStyleCounter += 1; + el.dataset.channelBannerStyleId = `channel-banner-${channelBannerStyleCounter}`; + } + const selector = `[data-channel-banner-style-id="${el.dataset.channelBannerStyleId}"]`; + + if (url) { + const escaped = url.replace(/(["\\])/g, "\\$1"); + state.rules.set(selector, `${selector} { background-image: url("${escaped}"); }`); + } else { + state.rules.set(selector, `${selector} { background-image: none; }`); + } + + state.styleNode.textContent = [...state.rules.values()].join("\n"); +} export function clearChannelVideoCardRegistry() { channelVideoCardsById.clear(); @@ -1432,10 +1479,7 @@ function setBannerVisual(el, url, { referrerPolicy } = {}) { el.src = resolvedUrl; } } else { - const value = resolvedUrl ? `url("${resolvedUrl}")` : ""; - if (el.style.backgroundImage !== value) { - el.style.backgroundImage = value; - } + updateChannelBannerBackground(el, resolvedUrl || ""); } if (el.dataset.bannerSrc !== resolvedUrl) { diff --git a/js/index.js b/js/index.js index f74e90b6..9284bd82 100644 --- a/js/index.js +++ b/js/index.js @@ -885,22 +885,67 @@ async function bootstrapInterface() { }; }; + const sidebarDropupStyleState = (() => { + let styleNode = null; + let rootWidthRule = ""; + const measureRules = new Map(); + + const ensureStyleNode = () => { + if (styleNode && styleNode.isConnected) { + return styleNode; + } + styleNode = document.getElementById("sidebarDropupStyles"); + if (!(styleNode instanceof HTMLStyleElement)) { + styleNode = document.createElement("style"); + styleNode.id = "sidebarDropupStyles"; + document.head.appendChild(styleNode); + } + return styleNode; + }; + + const updateStyles = () => { + const node = ensureStyleNode(); + const rules = [rootWidthRule, ...measureRules.values()].filter(Boolean); + node.textContent = rules.join("\n"); + }; + + return { + setRootWidth: (value) => { + rootWidthRule = `:root { --sidebar-dropup-content-width: ${value}; }`; + updateStyles(); + }, + setMeasureWidth: (id, value) => { + measureRules.set( + id, + `.sidebar-dropup-measure[data-measure-id="${id}"] { width: ${value}; }`, + ); + updateStyles(); + }, + clearMeasure: (id) => { + measureRules.delete(id); + updateStyles(); + }, + }; + })(); + + let sidebarDropupMeasureId = 0; + const resolveCssLengthToPixels = (value, container) => { if (!(container instanceof HTMLElement) || typeof value !== "string") { return 0; } const measurementNode = container.ownerDocument.createElement("div"); - measurementNode.style.position = "absolute"; - measurementNode.style.visibility = "hidden"; - measurementNode.style.pointerEvents = "none"; - measurementNode.style.height = "0"; - measurementNode.style.width = value; - measurementNode.style.overflow = "hidden"; + sidebarDropupMeasureId += 1; + const measureId = `sidebar-dropup-measure-${sidebarDropupMeasureId}`; + measurementNode.classList.add("sidebar-dropup-measure"); + measurementNode.dataset.measureId = measureId; + sidebarDropupStyleState.setMeasureWidth(measureId, value); container.appendChild(measurementNode); const pixels = measurementNode.getBoundingClientRect().width; container.removeChild(measurementNode); + sidebarDropupStyleState.clearMeasure(measureId); return Number.isFinite(pixels) ? pixels : 0; }; @@ -910,46 +955,13 @@ async function bootstrapInterface() { const panel = panelInner?.closest(".sidebar-dropup-panel"); if (!(panelInner instanceof HTMLElement) || !(panel instanceof HTMLElement)) { - document.documentElement.style.setProperty( - "--sidebar-dropup-content-width", - "0px", - ); + sidebarDropupStyleState.setRootWidth("0px"); return; } - const originalStyles = { - position: panelInner.style.position, - width: panelInner.style.width, - maxWidth: panelInner.style.maxWidth, - visibility: panelInner.style.visibility, - pointerEvents: panelInner.style.pointerEvents, - left: panelInner.style.left, - right: panelInner.style.right, - top: panelInner.style.top, - bottom: panelInner.style.bottom, - }; - - panelInner.style.position = "absolute"; - panelInner.style.width = "max-content"; - panelInner.style.maxWidth = "none"; - panelInner.style.visibility = "hidden"; - panelInner.style.pointerEvents = "none"; - panelInner.style.left = "-9999px"; - panelInner.style.right = "auto"; - panelInner.style.top = "auto"; - panelInner.style.bottom = "auto"; - + panelInner.classList.add("is-measuring"); const measuredInnerWidth = Math.ceil(panelInner.scrollWidth); - - panelInner.style.position = originalStyles.position; - panelInner.style.width = originalStyles.width; - panelInner.style.maxWidth = originalStyles.maxWidth; - panelInner.style.visibility = originalStyles.visibility; - panelInner.style.pointerEvents = originalStyles.pointerEvents; - panelInner.style.left = originalStyles.left; - panelInner.style.right = originalStyles.right; - panelInner.style.top = originalStyles.top; - panelInner.style.bottom = originalStyles.bottom; + panelInner.classList.remove("is-measuring"); const panelStyles = window.getComputedStyle(panel); const paddingInlineStart = Number.parseFloat(panelStyles.paddingInlineStart) || 0; @@ -965,10 +977,7 @@ async function bootstrapInterface() { measuredInnerWidth + paddingInlineStart + paddingInlineEnd + scrollReserve, ); - document.documentElement.style.setProperty( - "--sidebar-dropup-content-width", - `${Math.ceil(totalWidth)}px`, - ); + sidebarDropupStyleState.setRootWidth(`${Math.ceil(totalWidth)}px`); }; const debouncedSidebarDropupResize = debounce(updateSidebarDropupContentWidth, 150); diff --git a/js/ui/components/SimilarContentCard.js b/js/ui/components/SimilarContentCard.js index dcb2a9ca..f844e4bd 100644 --- a/js/ui/components/SimilarContentCard.js +++ b/js/ui/components/SimilarContentCard.js @@ -6,6 +6,48 @@ import { buildModerationBadgeText } from "../moderationCopy.js"; const SVG_NAMESPACE = "http://www.w3.org/2000/svg"; const DEFAULT_PROFILE_AVATAR = "assets/svg/default-profile.svg"; +let similarCardIdCounter = 0; +const similarCardStyleRegistry = new WeakMap(); + +function getSimilarCardStyleState(doc) { + if (!doc || typeof doc.createElement !== "function") { + return null; + } + const existing = similarCardStyleRegistry.get(doc); + if (existing) { + return existing; + } + let styleNode = doc.getElementById?.("similarContentCardStyles") || null; + if (!(styleNode instanceof HTMLStyleElement)) { + styleNode = doc.createElement("style"); + styleNode.id = "similarContentCardStyles"; + doc.head?.appendChild(styleNode); + } + const state = { + styleNode, + rules: new Map(), + }; + similarCardStyleRegistry.set(doc, state); + return state; +} + +function updateSimilarCardBackdrop(doc, cardId, url) { + const state = getSimilarCardStyleState(doc); + if (!state || !cardId) { + return; + } + + if (url) { + state.rules.set( + cardId, + `[data-similar-card-id="${cardId}"] { --similar-card-thumb-url: url("${url}"); }`, + ); + } else { + state.rules.delete(cardId); + } + + state.styleNode.textContent = [...state.rules.values()].join("\n"); +} export class SimilarContentCard { constructor({ @@ -60,6 +102,7 @@ export class SimilarContentCard { this.callbacks = { onPlay: null }; this.root = null; + this.cardStyleId = ""; this.mediaLinkEl = null; this.thumbnailEl = null; this.contentEl = null; @@ -330,6 +373,9 @@ export class SimilarContentCard { if (this.video.id) { root.dataset.videoId = this.video.id; } + similarCardIdCounter += 1; + this.cardStyleId = `similar-card-${similarCardIdCounter}`; + root.dataset.similarCardId = this.cardStyleId; const dsMode = this.designSystem?.getMode?.(); if (dsMode) { root.setAttribute("data-ds", dsMode); @@ -378,14 +424,11 @@ export class SimilarContentCard { } const badge = this.document.createElement("div"); - badge.className = "moderation-badge opacity-95"; + badge.className = "moderation-badge moderation-badge--interactive opacity-95"; badge.dataset.variant = "warning"; badge.dataset.moderationBadge = "true"; badge.dataset.moderationState = "trusted-mute"; - // Position it centrally over the thumbnail like in VideoCard - badge.style.pointerEvents = "auto"; - const label = this.document.createElement("span"); label.className = "moderation-badge__label inline-flex items-center gap-xs"; @@ -556,14 +599,12 @@ export class SimilarContentCard { buildContentSection() { const content = this.document.createElement("div"); content.classList.add("player-modal__similar-card-content"); - content.style.minWidth = "0"; const titleLink = this.document.createElement("a"); titleLink.classList.add("player-modal__similar-card-title"); titleLink.href = this.shareUrl; titleLink.textContent = this.video.title || "Untitled"; titleLink.title = this.video.title || "Untitled"; - titleLink.style.minWidth = "0"; const authorStack = this.buildAuthorStack(); const metaRow = this.buildMetaRow(); @@ -588,11 +629,9 @@ export class SimilarContentCard { buildAuthorStack() { const wrapper = this.document.createElement("div"); wrapper.classList.add("player-modal__similar-card-author"); - wrapper.style.minWidth = "0"; const textWrapper = this.document.createElement("span"); textWrapper.classList.add("player-modal__similar-card-author-meta"); - textWrapper.style.minWidth = "0"; const nameEl = this.document.createElement("span"); nameEl.classList.add("author-name", "player-modal__similar-card-author-name"); @@ -653,7 +692,6 @@ export class SimilarContentCard { buildMetaRow() { const row = this.document.createElement("div"); row.classList.add("player-modal__similar-card-meta"); - row.style.minWidth = "0"; const timeEl = this.document.createElement("time"); timeEl.classList.add("player-modal__similar-card-timestamp"); @@ -870,12 +908,10 @@ export class SimilarContentCard { } setCardBackdropImage(src) { - if (!this.root || !this.root.style) { + if (!this.root || !this.cardStyleId) { return; } - const style = this.root.style; - const normalizeSource = (raw) => { if (typeof raw !== "string") { return ""; @@ -919,12 +955,9 @@ export class SimilarContentCard { const sanitized = normalizeSource(src); if (sanitized) { const escaped = sanitized.replace(/(["\\])/g, "\\$1"); - style.setProperty( - "--similar-card-thumb-url", - `url("${escaped}")` - ); + updateSimilarCardBackdrop(this.document, this.cardStyleId, escaped); } else { - style.removeProperty("--similar-card-thumb-url"); + updateSimilarCardBackdrop(this.document, this.cardStyleId, ""); } } } diff --git a/js/ui/components/VideoModal.js b/js/ui/components/VideoModal.js index 4a80168b..955748a8 100644 --- a/js/ui/components/VideoModal.js +++ b/js/ui/components/VideoModal.js @@ -153,6 +153,7 @@ export class VideoModal { "+": null, "-": null, }; + this.reactionMeterStyleNode = null; this.reactionState = { counts: { "+": 0, "-": 0 }, total: 0, @@ -3116,9 +3117,32 @@ export class VideoModal { this.reactionMeterAssistive.textContent = assistiveLabel; } - if (this.reactionMeterFill) { - this.reactionMeterFill.style.inlineSize = `${likeRatio}%`; - this.reactionMeterFill.style.width = `${likeRatio}%`; + this.updateReactionMeterFill(likeRatio); + } + + updateReactionMeterFill(likeRatio) { + const doc = this.playerModal?.ownerDocument || this.document; + if (!doc) { + return; + } + + const clamped = Math.max(0, Math.min(100, Math.round(likeRatio))); + const widthValue = `${clamped}%`; + if (!this.reactionMeterStyleNode || !this.reactionMeterStyleNode.isConnected) { + const existing = doc.getElementById?.("videoModalReactionStyles"); + if (existing instanceof HTMLStyleElement) { + this.reactionMeterStyleNode = existing; + } else { + const styleNode = doc.createElement("style"); + styleNode.id = "videoModalReactionStyles"; + doc.head?.appendChild(styleNode); + this.reactionMeterStyleNode = styleNode; + } + } + + if (this.reactionMeterStyleNode) { + this.reactionMeterStyleNode.textContent = + `#playerModal { --video-modal-reaction-fill: ${widthValue}; }`; } } @@ -4021,9 +4045,6 @@ export class VideoModal { this.videoTitle.textContent = titleText; // Ensure element is visible this.videoTitle.hidden = false; - if (this.videoTitle.style) { - this.videoTitle.style.display = ""; - } } else { // Log for debugging if element cannot be found this.logger?.log?.( diff --git a/js/ui/overlay/popoverEngine.js b/js/ui/overlay/popoverEngine.js index 7eb50c65..ef0e9ea2 100644 --- a/js/ui/overlay/popoverEngine.js +++ b/js/ui/overlay/popoverEngine.js @@ -20,6 +20,7 @@ const DEFAULT_MAX_WIDTH_TOKEN = "--popover-inline-safe-max"; const MENU_TYPEAHEAD_RESET_MS = 500; let popoverPanelIdCounter = 0; +let popoverArrowIdCounter = 0; let activePopoverInstance = null; @@ -115,22 +116,39 @@ function resolveArrow(option, panel) { return null; } -function applyArrowStyles({ arrowElement, placement, middlewareData }) { +function ensureArrowId(arrowElement) { + if (!arrowElement) { + return ""; + } + if (arrowElement.id) { + return arrowElement.id; + } + popoverArrowIdCounter += 1; + const arrowId = `popover-arrow-${popoverArrowIdCounter}`; + arrowElement.id = arrowId; + return arrowId; +} + +function buildArrowStyleState({ arrowElement, placement, middlewareData }) { + const state = { + basePlacement: "", + values: { + left: "auto", + right: "auto", + top: "auto", + bottom: "auto", + }, + }; + if (!arrowElement || typeof placement !== "string") { - return; + return state; } const basePlacement = placement.split("-")[0]; const arrowData = middlewareData?.arrow || null; - arrowElement.style.left = ""; - arrowElement.style.right = ""; - arrowElement.style.top = ""; - arrowElement.style.bottom = ""; - if (!arrowData || !basePlacement) { - delete arrowElement.dataset.popoverArrowSide; - return; + return state; } const staticSideMap = { @@ -141,11 +159,11 @@ function applyArrowStyles({ arrowElement, placement, middlewareData }) { }; if (Number.isFinite(arrowData.x)) { - arrowElement.style.left = `${Math.round(arrowData.x)}px`; + state.values.left = `${Math.round(arrowData.x)}px`; } if (Number.isFinite(arrowData.y)) { - arrowElement.style.top = `${Math.round(arrowData.y)}px`; + state.values.top = `${Math.round(arrowData.y)}px`; } const staticSide = staticSideMap[basePlacement] || null; @@ -161,10 +179,11 @@ function applyArrowStyles({ arrowElement, placement, middlewareData }) { basePlacement === "top" || basePlacement === "bottom" ? arrowHeight : arrowWidth; - arrowElement.style[staticSide] = `${-Math.round(offsetValue)}px`; + state.values[staticSide] = `${-Math.round(offsetValue)}px`; } - arrowElement.dataset.popoverArrowSide = basePlacement; + state.basePlacement = basePlacement; + return state; } function containsTarget(root, target) { @@ -238,6 +257,67 @@ export function createPopover(trigger, render, options = {}) { typeaheadBuffer: "", typeaheadTimeout: null, }; + let popoverStyleNode = null; + let arrowStyleState = { + left: "auto", + right: "auto", + top: "auto", + bottom: "auto", + }; + let panelStyleState = { + left: 0, + top: 0, + strategy, + maxWidth: "none", + maxHeight: "none", + }; + let arrowId = ""; + + function ensurePopoverStyleNode(panelId) { + if (popoverStyleNode && popoverStyleNode.isConnected) { + return popoverStyleNode; + } + const styleId = `popover-style-${panelId}`; + const existing = documentRef?.getElementById?.(styleId) || null; + if (existing instanceof HTMLStyleElement) { + popoverStyleNode = existing; + return popoverStyleNode; + } + if (!documentRef?.createElement) { + return null; + } + const styleNode = documentRef.createElement("style"); + styleNode.id = styleId; + documentRef.head?.appendChild(styleNode); + popoverStyleNode = styleNode; + return popoverStyleNode; + } + + function syncPopoverStyles() { + if (!panel || !panel.id) { + return; + } + const styleNode = ensurePopoverStyleNode(panel.id); + if (!styleNode) { + return; + } + const panelRule = `#${panel.id}{` + + `--popover-left:${Math.round(panelStyleState.left)}px;` + + `--popover-top:${Math.round(panelStyleState.top)}px;` + + `--popover-strategy:${panelStyleState.strategy || strategy};` + + `--popover-max-width:${panelStyleState.maxWidth || "none"};` + + `--popover-max-height:${panelStyleState.maxHeight || "none"};` + + `}`; + const arrowRule = arrowId + ? `#${arrowId}{` + + `--popover-arrow-left:${arrowStyleState.left};` + + `--popover-arrow-right:${arrowStyleState.right};` + + `--popover-arrow-top:${arrowStyleState.top};` + + `--popover-arrow-bottom:${arrowStyleState.bottom};` + + `}` + : ""; + styleNode.textContent = [panelRule, arrowRule].filter(Boolean).join("\n"); + } function getView() { return documentRef?.defaultView || globalThis; @@ -610,13 +690,21 @@ export function createPopover(trigger, render, options = {}) { if (maxWidthToken) { const widthValue = resolveTokenValue(maxWidthToken, documentRef); - panel.style.maxWidth = widthValue || ""; + panelStyleState = { + ...panelStyleState, + maxWidth: widthValue || "none", + }; } if (maxHeightToken) { const heightValue = resolveTokenValue(maxHeightToken, documentRef); - panel.style.maxHeight = heightValue || ""; + panelStyleState = { + ...panelStyleState, + maxHeight: heightValue || "none", + }; } + + syncPopoverStyles(); } function ensurePanel() { @@ -672,6 +760,12 @@ export function createPopover(trigger, render, options = {}) { applyPanelTokens(); + if (arrowElement) { + arrowElement.dataset.popoverArrow = "true"; + arrowId = ensureArrowId(arrowElement); + syncPopoverStyles(); + } + return panel; } @@ -720,18 +814,39 @@ export function createPopover(trigger, render, options = {}) { ? resolvedPlacement : placement; - panel.style.position = resolvedStrategy || strategy; - panel.style.left = `${Math.round(nextX)}px`; - panel.style.top = `${Math.round(nextY)}px`; + panelStyleState = { + ...panelStyleState, + left: nextX, + top: nextY, + strategy: resolvedStrategy || strategy, + }; panel.dataset.popoverPlacement = finalPlacement; if (arrowElement) { - applyArrowStyles({ + const arrowState = buildArrowStyleState({ arrowElement, placement: finalPlacement, middlewareData, }); + arrowStyleState = { ...arrowState.values }; + if (arrowState.basePlacement) { + arrowElement.dataset.popoverArrowSide = arrowState.basePlacement; + } else { + delete arrowElement.dataset.popoverArrowSide; + } + arrowElement.dataset.popoverArrow = "true"; + arrowId = ensureArrowId(arrowElement); + } else { + arrowStyleState = { + left: "auto", + right: "auto", + top: "auto", + bottom: "auto", + }; + arrowId = ""; } + + syncPopoverStyles(); } catch (error) { logger.user.error("[popover] positioning failed", error); } @@ -908,8 +1023,14 @@ export function createPopover(trigger, render, options = {}) { portal.parentNode.removeChild(portal); } + if (popoverStyleNode?.parentNode) { + popoverStyleNode.parentNode.removeChild(popoverStyleNode); + } + panel = null; arrowElement = null; + popoverStyleNode = null; + arrowId = ""; if (anchor) { anchor.removeAttribute("aria-controls"); diff --git a/js/ui/profileModalController.js b/js/ui/profileModalController.js index aa6e962e..cfa9f271 100644 --- a/js/ui/profileModalController.js +++ b/js/ui/profileModalController.js @@ -1135,8 +1135,7 @@ export class ProfileModalController { this.focusTrapSuspended = false; this.focusTrapSuspendCount = 0; this.focusTrapAriaHiddenBeforeSuspend = null; - this.focusTrapPointerEventsBeforeSuspend = null; - this.focusTrapVisibilityBeforeSuspend = null; + this.focusTrapNestedModalActiveBeforeSuspend = null; this.profileSwitcherSelectionPubkey = null; this.previouslyFocusedElement = null; this.largeLayoutQuery = null; @@ -8332,25 +8331,16 @@ export class ProfileModalController { this.focusTrapAriaHiddenBeforeSuspend = modalRoot.getAttribute( "aria-hidden", ); - this.focusTrapPointerEventsBeforeSuspend = - typeof modalRoot.style.pointerEvents === "string" && - modalRoot.style.pointerEvents - ? modalRoot.style.pointerEvents - : null; - this.focusTrapVisibilityBeforeSuspend = - typeof modalRoot.style.visibility === "string" && - modalRoot.style.visibility - ? modalRoot.style.visibility + this.focusTrapNestedModalActiveBeforeSuspend = + typeof modalRoot.dataset.nestedModalActive === "string" + ? modalRoot.dataset.nestedModalActive : null; modalRoot.dataset.nestedModalActive = "true"; modalRoot.setAttribute("aria-hidden", "true"); - modalRoot.style.setProperty("pointer-events", "none"); - modalRoot.style.setProperty("visibility", "hidden"); } else { this.focusTrapAriaHiddenBeforeSuspend = null; - this.focusTrapPointerEventsBeforeSuspend = null; - this.focusTrapVisibilityBeforeSuspend = null; + this.focusTrapNestedModalActiveBeforeSuspend = null; } const panel = this.getModalPanelElement(); @@ -8391,30 +8381,11 @@ export class ProfileModalController { } } - if ( - this.focusTrapPointerEventsBeforeSuspend === null || - this.focusTrapPointerEventsBeforeSuspend === undefined || - this.focusTrapPointerEventsBeforeSuspend === "" - ) { - modalRoot.style.removeProperty("pointer-events"); + if (this.focusTrapNestedModalActiveBeforeSuspend === null) { + delete modalRoot.dataset.nestedModalActive; } else { - modalRoot.style.setProperty( - "pointer-events", - this.focusTrapPointerEventsBeforeSuspend, - ); - } - - if ( - this.focusTrapVisibilityBeforeSuspend === null || - this.focusTrapVisibilityBeforeSuspend === undefined || - this.focusTrapVisibilityBeforeSuspend === "" - ) { - modalRoot.style.removeProperty("visibility"); - } else { - modalRoot.style.setProperty( - "visibility", - this.focusTrapVisibilityBeforeSuspend, - ); + modalRoot.dataset.nestedModalActive = + this.focusTrapNestedModalActiveBeforeSuspend; } } @@ -8424,8 +8395,7 @@ export class ProfileModalController { } this.focusTrapAriaHiddenBeforeSuspend = null; - this.focusTrapPointerEventsBeforeSuspend = null; - this.focusTrapVisibilityBeforeSuspend = null; + this.focusTrapNestedModalActiveBeforeSuspend = null; this.updateFocusTrap(); @@ -8503,21 +8473,7 @@ export class ProfileModalController { : null; if (modalRoot) { - modalRoot.style.setProperty("z-index", "var(--z-modal-top-root)"); - } - - if (this.profileModalBackdrop instanceof HTMLElement) { - this.profileModalBackdrop.style.setProperty( - "z-index", - "var(--z-modal-top-overlay)", - ); - } - - if (this.profileModalPanel instanceof HTMLElement) { - this.profileModalPanel.style.setProperty( - "z-index", - "var(--z-modal-top-content)", - ); + modalRoot.dataset.modalStack = "top"; } } @@ -8685,8 +8641,7 @@ export class ProfileModalController { modalElement.classList.add("hidden"); modalElement.setAttribute("aria-hidden", "true"); delete modalElement.dataset.nestedModalActive; - modalElement.style.removeProperty("pointer-events"); - modalElement.style.removeProperty("visibility"); + delete modalElement.dataset.modalStack; this.setGlobalModalState("profile", false); this.setMobileView("menu", { skipFocusTrap: true }); @@ -8712,8 +8667,7 @@ export class ProfileModalController { this.focusTrapSuspendCount = 0; this.focusTrapSuspended = false; this.focusTrapAriaHiddenBeforeSuspend = null; - this.focusTrapPointerEventsBeforeSuspend = null; - this.focusTrapVisibilityBeforeSuspend = null; + this.focusTrapNestedModalActiveBeforeSuspend = null; if ( this.boundProfileHistoryVisibility && diff --git a/js/utils/qrcode.js b/js/utils/qrcode.js index 001aa787..20bb2932 100644 --- a/js/utils/qrcode.js +++ b/js/utils/qrcode.js @@ -235,6 +235,88 @@ if (hasDocument) { })(); var useSVG = document.documentElement.tagName.toLowerCase() === "svg"; + var qrCodeStyleRegistry = new WeakMap(); + var qrCodeStyleCounter = 0; + + function ensureQrCodeStyleState(doc) { + if (!doc || typeof doc.createElement !== "function") { + return null; + } + var existing = qrCodeStyleRegistry.get(doc); + if (existing) { + return existing; + } + var styleNode = doc.getElementById("qrCodeStyles"); + if (!(styleNode instanceof HTMLStyleElement)) { + styleNode = doc.createElement("style"); + styleNode.id = "qrCodeStyles"; + doc.head && doc.head.appendChild(styleNode); + } + var state = { + styleNode: styleNode, + values: new Map() + }; + qrCodeStyleRegistry.set(doc, state); + return state; + } + + function buildQrCodeRule(selector, values) { + var declarations = []; + if (values.colorDark) { + declarations.push("--qr-code-color-dark: " + values.colorDark + ";"); + } + if (values.colorLight) { + declarations.push("--qr-code-color-light: " + values.colorLight + ";"); + } + if (values.cellWidth) { + declarations.push("--qr-code-cell-width: " + values.cellWidth + ";"); + } + if (values.cellHeight) { + declarations.push("--qr-code-cell-height: " + values.cellHeight + ";"); + } + if (values.tableMargin) { + declarations.push("--qr-code-table-margin: " + values.tableMargin + ";"); + } + return selector + " { " + declarations.join(" ") + " }"; + } + + function updateQrCodeContainerStyles(el, values) { + if (!el) { + return; + } + var doc = el.ownerDocument || document; + var state = ensureQrCodeStyleState(doc); + if (!state) { + return; + } + if (!el.dataset.qrCodeStyleId) { + qrCodeStyleCounter += 1; + el.dataset.qrCodeStyleId = "qr-code-" + qrCodeStyleCounter; + } + var selector = "[data-qr-code-style-id=\"" + el.dataset.qrCodeStyleId + "\"]"; + var currentValues = state.values.get(selector) || {}; + var nextValues = Object.assign({}, currentValues, values || {}); + state.values.set(selector, nextValues); + var rules = []; + state.values.forEach(function (entryValues, entrySelector) { + rules.push(buildQrCodeRule(entrySelector, entryValues)); + }); + state.styleNode.textContent = rules.join("\n"); + } + + function setQrCodeMode(el, mode) { + if (!el) { + return; + } + if (el.classList) { + el.classList.add("qr-code__container"); + } + if (mode) { + el.dataset.qrCodeMode = mode; + } else { + delete el.dataset.qrCodeMode; + } + } // Drawing in DOM by using Table tag var Drawing = useSVG ? svgDrawer : !_isSupportCanvas() ? (function () { @@ -256,10 +338,13 @@ if (hasDocument) { var nHeight = Math.floor(_htOption.height / nCount); var aHTML = ['']; - _el.style.setProperty("--qr-code-color-dark", _htOption.colorDark); - _el.style.setProperty("--qr-code-color-light", _htOption.colorLight); - _el.style.setProperty("--qr-code-cell-width", nWidth + "px"); - _el.style.setProperty("--qr-code-cell-height", nHeight + "px"); + updateQrCodeContainerStyles(_el, { + colorDark: _htOption.colorDark, + colorLight: _htOption.colorLight, + cellWidth: nWidth + "px", + cellHeight: nHeight + "px", + tableMargin: "0px" + }); for (var row = 0; row < nCount; row++) { aHTML.push(''); @@ -283,7 +368,9 @@ if (hasDocument) { var nTopMarginTable = (_htOption.height - elTable.offsetHeight) / 2; if (nLeftMarginTable > 0 && nTopMarginTable > 0) { - elTable.style.margin = nTopMarginTable + "px " + nLeftMarginTable + "px"; + updateQrCodeContainerStyles(_el, { + tableMargin: nTopMarginTable + "px " + nLeftMarginTable + "px" + }); } }; @@ -298,8 +385,7 @@ if (hasDocument) { })() : (function () { // Drawing in Canvas function _onMakeImage() { this._elImage.src = this._elCanvas.toDataURL("image/png"); - this._elImage.style.display = "block"; - this._elCanvas.style.display = "none"; + setQrCodeMode(this._el, "image"); } // Android 2.1 bug workaround @@ -398,13 +484,22 @@ if (hasDocument) { this._elCanvas.height = htOption.height; el.appendChild(this._elCanvas); this._el = el; + if (this._el && this._el.classList) { + this._el.classList.add("qr-code__container"); + } this._oContext = this._elCanvas.getContext("2d"); this._bIsPainted = false; this._elImage = document.createElement("img"); this._elImage.alt = "Scan me!"; - this._elImage.style.display = "none"; + if (this._elImage.classList) { + this._elImage.classList.add("qr-code__image"); + } + if (this._elCanvas && this._elCanvas.classList) { + this._elCanvas.classList.add("qr-code__canvas"); + } this._el.appendChild(this._elImage); this._bSupportDataURI = null; + setQrCodeMode(this._el, "canvas"); }; /** @@ -423,7 +518,7 @@ if (hasDocument) { var nRoundedWidth = Math.round(nWidth); var nRoundedHeight = Math.round(nHeight); - _elImage.style.display = "none"; + setQrCodeMode(this._el, "canvas"); this.clear(); for (var row = 0; row < nCount; row++) { @@ -604,6 +699,9 @@ if (hasDocument) { this._android = _getAndroid(); this._el = el; + if (this._el && this._el.classList) { + this._el.classList.add("qr-code__container"); + } this._oQRCode = null; this._oDrawing = new Drawing(this._el, this._htOption); diff --git a/tests/e2e/popover.spec.ts b/tests/e2e/popover.spec.ts index 6da5be3a..653281c3 100644 --- a/tests/e2e/popover.spec.ts +++ b/tests/e2e/popover.spec.ts @@ -16,6 +16,10 @@ test.describe("popover layout scenarios", () => { .getComputedStyle(document.documentElement) .getPropertyValue("--popover-inline-safe-max") .trim(); + const popoverMaxWidth = window + .getComputedStyle(node) + .getPropertyValue("--popover-max-width") + .trim(); return { rect: { @@ -29,7 +33,7 @@ test.describe("popover layout scenarios", () => { viewport: { width: viewportWidth, height: viewportHeight }, placement: node.dataset.popoverPlacement || "", state: node.dataset.popoverState || "", - inlineMaxWidth: node.style.maxWidth, + popoverMaxWidth, tokenMaxWidth: tokenValue, }; }); @@ -51,6 +55,10 @@ test.describe("popover layout scenarios", () => { .getComputedStyle(document.documentElement) .getPropertyValue("--popover-inline-safe-max") .trim(); + const popoverMaxWidth = window + .getComputedStyle(node) + .getPropertyValue("--popover-max-width") + .trim(); return { rect: { @@ -72,7 +80,7 @@ test.describe("popover layout scenarios", () => { viewport: { width: viewportWidth, height: viewportHeight }, placement: node.dataset.popoverPlacement || "", state: node.dataset.popoverState || "", - inlineMaxWidth: node.style.maxWidth, + popoverMaxWidth, tokenMaxWidth: tokenValue, }; }, triggerHandle); @@ -102,7 +110,7 @@ test.describe("popover layout scenarios", () => { assertWithinViewport(metrics); expect(metrics.state).toBe("open"); expect(metrics.placement.startsWith("top")).toBeTruthy(); - expect(metrics.inlineMaxWidth).toBe(metrics.tokenMaxWidth); + expect(metrics.popoverMaxWidth).toBe(metrics.tokenMaxWidth); expect(metrics.tokenMaxWidth.length).toBeGreaterThan(0); }); @@ -119,7 +127,7 @@ test.describe("popover layout scenarios", () => { assertWithinViewport(metrics); expect(metrics.state).toBe("open"); expect(metrics.placement.includes("bottom")).toBeTruthy(); - expect(metrics.inlineMaxWidth).toBe(metrics.tokenMaxWidth); + expect(metrics.popoverMaxWidth).toBe(metrics.tokenMaxWidth); expect(metrics.tokenMaxWidth.length).toBeGreaterThan(0); }); @@ -137,7 +145,7 @@ test.describe("popover layout scenarios", () => { assertWithinViewport(metrics); expect(metrics.state).toBe("open"); - expect(metrics.inlineMaxWidth).toBe(metrics.tokenMaxWidth); + expect(metrics.popoverMaxWidth).toBe(metrics.tokenMaxWidth); expect(metrics.tokenMaxWidth.length).toBeGreaterThan(0); }); }); diff --git a/tests/position-floating-panel.test.mjs b/tests/position-floating-panel.test.mjs index 8e56345c..bdc76666 100644 --- a/tests/position-floating-panel.test.mjs +++ b/tests/position-floating-panel.test.mjs @@ -81,14 +81,15 @@ beforeEach(async () => { global.PointerEvent = windowRef.PointerEvent || windowRef.Event; } windowRef.scrollTo = () => {}; - documentRef.documentElement.style.setProperty( - "--popover-inline-safe-max", - "calc(100vw - var(--space-xl))", - ); - documentRef.documentElement.style.setProperty( - "--overlay-panel-padding-block", - "240px", - ); + const rootStyle = documentRef.createElement("style"); + rootStyle.id = "testRootVars"; + rootStyle.textContent = ` + :root { + --popover-inline-safe-max: calc(100vw - var(--space-xl)); + --overlay-panel-padding-block: 240px; + } + `; + documentRef.head.appendChild(rootStyle); setMockedPosition(); @@ -205,16 +206,20 @@ test("flips placement when bottom placement would collide with viewport", async const panel = documentRef.querySelector('[data-test-panel="collision"]'); assert.ok(panel, "panel should render"); + const panelStyles = windowRef.getComputedStyle(panel); assert.equal(panel.dataset.popoverState, "open"); assert.equal(panel.dataset.state, "open"); assert.equal(panel.dataset.popoverPlacement, "top-start"); - assert.equal(panel.style.position, "fixed"); - assert.equal(panel.style.left, "96px"); - assert.equal(panel.style.top, "81px"); - assert.equal(panel.style.maxWidth, "calc(100vw - var(--space-xl))"); + assert.equal(panelStyles.getPropertyValue("--popover-strategy").trim(), "fixed"); + assert.equal(panelStyles.getPropertyValue("--popover-left").trim(), "96px"); + assert.equal(panelStyles.getPropertyValue("--popover-top").trim(), "81px"); + assert.equal( + panelStyles.getPropertyValue("--popover-max-width").trim(), + "calc(100vw - var(--space-xl))", + ); - const top = Number.parseInt(panel.style.top, 10); - const left = Number.parseInt(panel.style.left, 10); + const top = Number.parseInt(panelStyles.getPropertyValue("--popover-top"), 10); + const left = Number.parseInt(panelStyles.getPropertyValue("--popover-left"), 10); assert.ok(Number.isFinite(top)); assert.ok(Number.isFinite(left)); assert.ok(top >= 0, "top should stay within viewport"); diff --git a/tests/profile-modal-controller.test.mjs b/tests/profile-modal-controller.test.mjs index 33222082..aea54f44 100644 --- a/tests/profile-modal-controller.test.mjs +++ b/tests/profile-modal-controller.test.mjs @@ -458,17 +458,6 @@ for (const _ of [0]) { profileModalRoot?.querySelector('.bv-modal__panel') || profileModalRoot; assert.ok(panel?.hasAttribute('inert')); - assert.equal( - profileModalRoot?.style.visibility, - 'hidden', - 'profile modal should hide visually while login modal is active', - ); - assert.equal( - profileModalRoot?.style.pointerEvents, - 'none', - 'profile modal should not intercept pointer events while login modal is active', - ); - assert.strictEqual( container.lastElementChild, loginModal, @@ -491,16 +480,6 @@ for (const _ of [0]) { 'profile modal should restore aria-hidden', ); assert.equal(panel?.hasAttribute('inert'), false); - assert.equal( - profileModalRoot?.style.visibility ?? '', - '', - 'profile modal visibility should restore after login', - ); - assert.equal( - profileModalRoot?.style.pointerEvents ?? '', - '', - 'profile modal pointer events should restore after login', - ); } finally { cleanup(); } @@ -1292,4 +1271,3 @@ test('history pane lazily initializes the watch history renderer', async () => { assert.equal(controller.profileHistoryRenderer, null); assert.equal(controller.boundProfileHistoryVisibility, null); }); - diff --git a/tests/ui/popoverEngine.test.mjs b/tests/ui/popoverEngine.test.mjs index 8dae962a..6d700eab 100644 --- a/tests/ui/popoverEngine.test.mjs +++ b/tests/ui/popoverEngine.test.mjs @@ -176,9 +176,10 @@ test("opens a popover in the overlay root and positions the panel", async () => assert.equal(panel.dataset.popoverState, "open"); assert.equal(panel.dataset.state, "open"); assert.equal(panel.dataset.popoverPlacement, "bottom-start"); - assert.equal(panel.style.position, "fixed"); - assert.equal(panel.style.left, "173px"); - assert.equal(panel.style.top, "248px"); + const panelStyles = documentRef.defaultView.getComputedStyle(panel); + assert.equal(panelStyles.getPropertyValue("--popover-strategy").trim(), "fixed"); + assert.equal(panelStyles.getPropertyValue("--popover-left").trim(), "173px"); + assert.equal(panelStyles.getPropertyValue("--popover-top").trim(), "248px"); assert.equal(panel.getAttribute("role"), "menu"); assert.equal(documentRef.activeElement, panel); assert.equal(trigger.getAttribute("aria-expanded"), "true"); @@ -238,12 +239,15 @@ test("positions bottom-end panels flush with the trigger's right edge", async () const panel = documentRef.getElementById("popover-bottom-end"); assert.ok(panel); + const panelStyles = documentRef.defaultView.getComputedStyle(panel); assert.equal(panel.dataset.popoverPlacement, "bottom-end"); - assert.equal(panel.style.left, "120px"); - assert.equal(panel.style.top, "265px"); + assert.equal(panelStyles.getPropertyValue("--popover-left").trim(), "120px"); + assert.equal(panelStyles.getPropertyValue("--popover-top").trim(), "265px"); const triggerRight = Math.round(trigger.getBoundingClientRect().right); - const panelRight = Number.parseInt(panel.style.left, 10) + panel.offsetWidth; + const panelRight = + Number.parseInt(panelStyles.getPropertyValue("--popover-left"), 10) + + panel.offsetWidth; assert.equal(panelRight, triggerRight); popover.destroy(); @@ -687,8 +691,14 @@ test("applies token-based sizing and arrow positioning", async () => { height: 40, }); - documentRef.documentElement.style.setProperty("--popover-inline-safe-max", "320px"); - documentRef.documentElement.style.setProperty("--overlay-panel-padding-block", "240px"); + const rootStyle = documentRef.createElement("style"); + rootStyle.textContent = ` + :root { + --popover-inline-safe-max: 320px; + --overlay-panel-padding-block: 240px; + } + `; + documentRef.head.appendChild(rootStyle); const popover = createPopover( trigger, @@ -742,8 +752,8 @@ test("applies token-based sizing and arrow positioning", async () => { const arrow = panel.querySelector(".arrow"); const panelStyles = getComputedStyle(panel); - assert.equal(panelStyles.maxWidth, "320px"); - assert.equal(panelStyles.maxHeight, "240px"); + assert.equal(panelStyles.getPropertyValue("--popover-max-width").trim(), "320px"); + assert.equal(panelStyles.getPropertyValue("--popover-max-height").trim(), "240px"); assert.equal(arrow.dataset.popoverArrowSide, "bottom"); const arrowStyles = getComputedStyle(arrow); assert.notEqual(arrowStyles.top, ""); diff --git a/tests/ui/similar-content-card.test.mjs b/tests/ui/similar-content-card.test.mjs index d4cb9ba0..e202c6f3 100644 --- a/tests/ui/similar-content-card.test.mjs +++ b/tests/ui/similar-content-card.test.mjs @@ -34,7 +34,7 @@ test("cached thumbnails reuse existing src without lazy-loading", (t) => { const cachedSrc = "https://cdn.example.com/thumb.jpg"; const cache = new Map([["video-123", cachedSrc]]); - const { card } = renderCard(t, { + const { card, window } = renderCard(t, { thumbnailCache: cache, fallbackThumbnailSrc: "https://cdn.example.com/fallback.jpg", }); @@ -46,7 +46,7 @@ test("cached thumbnails reuse existing src without lazy-loading", (t) => { assert.equal(img.src, cachedSrc); assert.equal(img.dataset.lazy, undefined); assert.equal( - root.style.getPropertyValue("--similar-card-thumb-url"), + window.getComputedStyle(root).getPropertyValue("--similar-card-thumb-url").trim(), `url("${cachedSrc}")`, ); }); @@ -75,7 +75,7 @@ test("uncached thumbnails use fallback, cache on load, and retain blur state", ( assert.equal(img.dataset.lazy, remoteThumb); assert.equal(img.dataset.thumbnailState, "blurred"); assert.equal( - root.style.getPropertyValue("--similar-card-thumb-url"), + window.getComputedStyle(root).getPropertyValue("--similar-card-thumb-url").trim(), `url("${fallbackSrc}")`, ); @@ -84,7 +84,7 @@ test("uncached thumbnails use fallback, cache on load, and retain blur state", ( assert.equal(cache.get("video-456"), remoteThumb); assert.equal( - root.style.getPropertyValue("--similar-card-thumb-url"), + window.getComputedStyle(root).getPropertyValue("--similar-card-thumb-url").trim(), `url("${remoteThumb}")`, ); assert.equal(img.dataset.thumbnailState, "blurred");