From 37ff8b1f68d37fdf9378a892d6cbfe0e63a55356 Mon Sep 17 00:00:00 2001 From: Etienne Lachance-Perreault Date: Mon, 25 May 2026 14:18:27 -0400 Subject: [PATCH 1/2] test --- src/primitives/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/primitives/index.ts b/src/primitives/index.ts index d93e94d..7bc9f5b 100644 --- a/src/primitives/index.ts +++ b/src/primitives/index.ts @@ -33,3 +33,4 @@ export { createBlurredImage } from './utils/createBlurredImage.js'; export type * from './types.js'; export type { KeyHandler } from '../core/focusManager.js'; +export type { SpeechType } from './announcer/speech.js'; From 8f1d26b69c6be9ccdf285f50e57c1a2154000d3c Mon Sep 17 00:00:00 2001 From: Etienne Lachance-Perreault Date: Wed, 17 Jun 2026 13:35:48 -0400 Subject: [PATCH 2/2] add fallback picture --- src/core/dom-renderer/domRenderer.ts | 78 ++++++++++++++++++++++++---- src/core/elementNode.ts | 24 +++++---- 2 files changed, 81 insertions(+), 21 deletions(-) diff --git a/src/core/dom-renderer/domRenderer.ts b/src/core/dom-renderer/domRenderer.ts index cbdf5dd..cf311a9 100644 --- a/src/core/dom-renderer/domRenderer.ts +++ b/src/core/dom-renderer/domRenderer.ts @@ -519,9 +519,14 @@ function updateNodeStyles(node: DOMNode | DOMText) { } hasDivBgTint = true; } - } else if (gradient) { - // use gradient as a mask when no tint is applied - maskStyle += `mask-image: ${gradient};`; + } else { + if (gradient) { + // use gradient as a mask when no tint is applied + maskStyle += `mask-image: ${gradient};`; + } + if (props.placeholderColor !== 0) { + bgStyle += `background-color: ${colorToRgba(props.placeholderColor)};`; + } } const imgStyleParts = [ @@ -852,6 +857,22 @@ function updateNodeStyles(node: DOMNode | DOMText) { node.imgEl.addEventListener('error', () => { node.imageLoading = false; + + const failedSrc = + node.imgEl?.dataset.pendingSrc || node.lazyImagePendingSrc || ''; + + const fallback = node.props.fallbackImage; + if ( + fallback && + node.imgEl && + node.imgEl.dataset.rawSrc !== fallback + ) { + node.imgEl.dataset.pendingSrc = fallback; + node.imgEl.dataset.rawSrc = fallback; + node.imgEl.src = fallback; + return; + } + node.showBackgroundLayer(); if (node.imgEl) { node.imgEl.removeAttribute('src'); @@ -859,9 +880,6 @@ function updateNodeStyles(node: DOMNode | DOMText) { node.imgEl.removeAttribute('data-rawSrc'); } - const failedSrc = - node.imgEl?.dataset.pendingSrc || node.lazyImagePendingSrc || ''; - const payload: lng.NodeTextureFailedPayload = { type: 'texture', error: new Error(`Failed to load image: ${failedSrc}`), @@ -972,15 +990,28 @@ function updateNodeStyles(node: DOMNode | DOMText) { node.imgEl.addEventListener('error', () => { node.imageLoading = false; + + const failedSrc = + node.imgEl?.dataset.pendingSrc || node.lazyImagePendingSrc || ''; + + const fallback = node.props.fallbackImage; + if ( + fallback && + node.imgEl && + node.imgEl.dataset.rawSrc !== fallback + ) { + node.imgEl.dataset.pendingSrc = fallback; + node.imgEl.dataset.rawSrc = fallback; + node.imgEl.src = fallback; + return; + } + if (node.imgEl) { node.imgEl.removeAttribute('src'); node.imgEl.style.display = 'none'; node.imgEl.removeAttribute('data-rawSrc'); } - const failedSrc = - node.imgEl?.dataset.pendingSrc || node.lazyImagePendingSrc || ''; - const payload: lng.NodeTextureFailedPayload = { type: 'texture', error: new Error(`Failed to load image: ${failedSrc}`), @@ -1137,7 +1168,6 @@ function updateDOMTextSize(node: DOMText, emitLoaded = true): void { w: node.w, h: node.h, }, - trimmedHeight: node.h, }; node.emit('loaded', payload); node.loaded = true; @@ -1244,6 +1274,7 @@ function resolveNodeDefaults( w: props.w ?? 0, h: props.h ?? 0, alpha: props.alpha ?? 1, + ignoreParentAlpha: props.ignoreParentAlpha ?? false, autosize: props.autosize ?? false, boundsMargin: props.boundsMargin ?? null, clipping: props.clipping ?? false, @@ -1279,6 +1310,8 @@ function resolveNodeDefaults( pivotY: props.pivotY ?? props.pivot ?? 0.5, rotation: props.rotation ?? 0, rtt: props.rtt ?? false, + placeholderColor: props.placeholderColor ?? 0, + fallbackImage: props.fallbackImage ?? null, data: {}, imageType: props.imageType, }; @@ -1807,6 +1840,31 @@ export class DOMNode extends EventEmitter implements IRendererNode { this.markChildrenBoundsDirty(); } + get ignoreParentAlpha(): boolean { + return this.props.ignoreParentAlpha; + } + set ignoreParentAlpha(v: boolean) { + this.props.ignoreParentAlpha = v; + updateNodeStyles(this); + } + + get placeholderColor(): number { + return this.props.placeholderColor; + } + set placeholderColor(v: number) { + this.props.placeholderColor = v; + updateNodeStyles(this); + } + + get fallbackImage(): string | null { + return this.props.fallbackImage ?? null; + } + + set fallbackImage(v: string | null) { + if (this.props.fallbackImage === v) return; + this.props.fallbackImage = v; + } + get absX(): number { const parent = this.props.parent; return ( diff --git a/src/core/elementNode.ts b/src/core/elementNode.ts index 2fbb6e0..1960540 100644 --- a/src/core/elementNode.ts +++ b/src/core/elementNode.ts @@ -33,12 +33,8 @@ import { isFunction, spliceItem, } from './utils.js'; -import { - Config, - isDev, - SHADERS_ENABLED, - isDomRendererActive, -} from './config.js'; +import { isDev, SHADERS_ENABLED } from './env.js'; +import { Config, isDomRendererActive } from './config.js'; import type { RendererMain, INode, @@ -55,7 +51,7 @@ import { assertTruthy } from '@solidtv/renderer/utils'; import { NodeType, TextNode } from './nodeTypes.js'; import { ForwardFocusHandler, - setActiveElement, + setActiveElementCore, FocusNode, } from './focusManager.js'; import { initClickInspector } from './clickInspector.js'; @@ -130,7 +126,7 @@ function runPostMutation() { // Phase 3: focus. setFocus() may have evaluated forwardFocus pre-render // (when no children existed yet); deferredFocusElement re-runs setFocus - // here once the subtree has rendered, then setActiveElement is applied. + // here once the subtree has rendered, then setActiveElementCore is applied. if (deferredFocusElement !== null) { const el = deferredFocusElement; deferredFocusElement = null; @@ -138,7 +134,7 @@ function runPostMutation() { } else if (nextActiveElement !== null) { const element = nextActiveElement; nextActiveElement = null; - setActiveElement(element); + setActiveElementCore(element); } } @@ -271,6 +267,7 @@ const LightningRendererNonAnimatingProps = [ 'fontStretch', 'fontStyle', 'group', + 'ignoreParentAlpha', 'imageType', 'letterSpacing', 'maxHeight', @@ -278,6 +275,8 @@ const LightningRendererNonAnimatingProps = [ 'maxWidth', 'offsetY', 'overflowSuffix', + 'placeholderColor', + 'fallbackImage', 'preventCleanup', 'rtt', 'scrollable', @@ -332,7 +331,7 @@ export interface ElementNode extends RendererNode, FocusNode { _animationQueueSettings?: AnimationSettings; _animationRunning?: boolean; _animationSettings?: AnimationSettings; - _autofocus?: boolean; + _autofocus?: any; _containsFlexGrow?: boolean | null; _hasRenderedChildren?: boolean; _effects?: Record; @@ -793,6 +792,9 @@ export class ElementNode { shader: undefined, clipping: undefined, text: undefined, + ignoreParentAlpha: undefined, + placeholderColor: undefined, + fallbackImage: undefined, }; this.children = []; @@ -1316,7 +1318,7 @@ export class ElementNode { * @param val - A value to determine if the element should autofocus. * A truthy value enables autofocus, otherwise disables it. */ - set autofocus(val: boolean | undefined) { + set autofocus(val: any) { this._autofocus = val; // Defer setFocus so children render first (forwardFocus needs them). // The post-mutation focus phase calls setFocus on this element.