From d9c3b5ba793d58252bc5a5553537c3adea678f85 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Gangumalla Date: Tue, 7 Apr 2026 14:24:14 +0530 Subject: [PATCH 1/6] Added `hoverable` config property Component's hoverable configuration property will alter holder node `interactive` flag Signed-off-by: Suresh Kumar Gangumalla --- index.d.ts | 10 +++++++++- src/engines/L3/element.test.js | 24 ++++++++++++++++++++++++ src/lib/codegenerator/generator.js | 4 +++- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index ef8a22f3..ef2782e3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -687,6 +687,14 @@ declare module '@lightningjs/blits' { * Watchers for changes to state variables, props or computed properties */ watch?: W & ComponentContext + /** + * Whether the component's holder node is interactive for mouse hit-testing. + * When set to `false`, the renderer skips this component's node during + * `getNodeFromPosition()`, allowing pointer events to pass through to + * the node underneath (e.g. a tile behind a focus ring overlay). + * Defaults to `true`. + */ + hoverable?: boolean } export interface RouterHooks { @@ -1353,7 +1361,7 @@ declare module '@lightningjs/blits' { M extends Methods, C extends Computed, W extends Watch>(name: string, config: ComponentConfig) : ComponentFactory - /** + /** * Blits Launch */ Launch(App: ComponentFactory, target: HTMLElement | String, settings?: Settings) : void diff --git a/src/engines/L3/element.test.js b/src/engines/L3/element.test.js index 09e8e59b..df8a598c 100644 --- a/src/engines/L3/element.test.js +++ b/src/engines/L3/element.test.js @@ -1255,3 +1255,27 @@ test('Element - Set maxheight property', (assert) => { assert.equal(el.props.props['contain'], 'height', 'maxheight should set contain to height') assert.end() }) + +test('Element - holder true sets interactive to true', (assert) => { + assert.capture(renderer, 'createNode', () => new EventEmitter()) + const el = element({ parent: { node: { w: 1920, h: 1080 } } }, {}) + el.populate({ parent: { node: new EventEmitter() }, holder: true }) + assert.equal( + el.props.props['interactive'], + true, + 'interactive should be true when holder is true' + ) + assert.end() +}) + +test('Element - holder false sets interactive to false', (assert) => { + assert.capture(renderer, 'createNode', () => new EventEmitter()) + const el = element({ parent: { node: { w: 1920, h: 1080 } } }, {}) + el.populate({ parent: { node: new EventEmitter() }, holder: false }) + assert.equal( + el.props.props['interactive'], + false, + 'interactive should be false when holder is false (hoverable: false)' + ) + assert.end() +}) diff --git a/src/lib/codegenerator/generator.js b/src/lib/codegenerator/generator.js index f8ab03bc..3b3755c9 100644 --- a/src/lib/codegenerator/generator.js +++ b/src/lib/codegenerator/generator.js @@ -187,7 +187,9 @@ const generateElementCode = function ( } if (options.holder) { - renderCode.push(`elementConfigs[${counter}]['holder'] = true`) + renderCode.push( + `elementConfigs[${counter}]['holder'] = cmps[${counter}] === undefined || cmps[${counter}][Symbol.for('config')].hoverable !== false` + ) } Object.keys(templateObject).forEach((key) => { From baba0ec5a80836559c57a193f1bb8ccc8ef1a826 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Gangumalla Date: Wed, 15 Apr 2026 18:10:11 +0530 Subject: [PATCH 2/6] Marking component as non-interactive based on hover/unhover if component does not registered hover/unhover hooks in component configuation then that component subjected to non interactive by setting `interactive` on holder node to false. Signed-off-by: Suresh Kumar Gangumalla --- index.d.ts | 9 +------- src/engines/L3/element.js | 11 +++++++++- src/engines/L3/element.test.js | 33 +++++++++++++++++++++++------- src/lib/codegenerator/generator.js | 4 +--- src/lib/hooks.js | 4 ++++ 5 files changed, 42 insertions(+), 19 deletions(-) diff --git a/index.d.ts b/index.d.ts index ef2782e3..e9ddc41e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -687,14 +687,7 @@ declare module '@lightningjs/blits' { * Watchers for changes to state variables, props or computed properties */ watch?: W & ComponentContext - /** - * Whether the component's holder node is interactive for mouse hit-testing. - * When set to `false`, the renderer skips this component's node during - * `getNodeFromPosition()`, allowing pointer events to pass through to - * the node underneath (e.g. a tile behind a focus ring overlay). - * Defaults to `true`. - */ - hoverable?: boolean + } export interface RouterHooks { diff --git a/src/engines/L3/element.js b/src/engines/L3/element.js index 1f2ef3e3..3ed0348a 100644 --- a/src/engines/L3/element.js +++ b/src/engines/L3/element.js @@ -23,6 +23,7 @@ import { Log } from '../../lib/log.js' import symbols from '../../lib/symbols.js' import Settings from '../../settings.js' import shaders from '../../lib/shaders/shaders.js' +import { hasHook } from '../../lib/hooks.js' const holderComponentMap = new WeakMap() @@ -528,7 +529,15 @@ const propsTransformer = { } }, set holder(v) { - this.props['interactive'] = v + const identifier = + this.element && this.element.component + ? this.element.component[symbols.identifier] + : undefined + if (identifier !== undefined) { + this.props['interactive'] = hasHook('hover', identifier) || hasHook('unhover', identifier) + } else { + this.props['interactive'] = v + } }, } diff --git a/src/engines/L3/element.test.js b/src/engines/L3/element.test.js index df8a598c..3f917341 100644 --- a/src/engines/L3/element.test.js +++ b/src/engines/L3/element.test.js @@ -23,6 +23,7 @@ import { initLog } from '../../lib/log.js' import symbols from '../../lib/symbols.js' import sinon from 'sinon' import shaders from '../../lib/shaders/shaders.js' // Changed from './shaderLoader.js' +import { registerHooks } from '../../lib/hooks.js' initLog() @@ -1256,26 +1257,44 @@ test('Element - Set maxheight property', (assert) => { assert.end() }) -test('Element - holder true sets interactive to true', (assert) => { +test('Element - holder true sets interactive to true when component has hover hook', (assert) => { assert.capture(renderer, 'createNode', () => new EventEmitter()) - const el = element({ parent: { node: { w: 1920, h: 1080 } } }, {}) + const component = { [symbols.identifier]: 'test-hover-1' } + registerHooks({ hover: () => {} }, component[symbols.identifier]) + const el = element({ parent: { node: { w: 1920, h: 1080 } } }, component) el.populate({ parent: { node: new EventEmitter() }, holder: true }) assert.equal( el.props.props['interactive'], true, - 'interactive should be true when holder is true' + 'interactive should be true when component has hover hook' ) assert.end() }) -test('Element - holder false sets interactive to false', (assert) => { +test('Element - holder true sets interactive to true when component has unhover hook', (assert) => { assert.capture(renderer, 'createNode', () => new EventEmitter()) - const el = element({ parent: { node: { w: 1920, h: 1080 } } }, {}) - el.populate({ parent: { node: new EventEmitter() }, holder: false }) + const component = { [symbols.identifier]: 'test-unhover-1' } + registerHooks({ unhover: () => {} }, component[symbols.identifier]) + const el = element({ parent: { node: { w: 1920, h: 1080 } } }, component) + el.populate({ parent: { node: new EventEmitter() }, holder: true }) + assert.equal( + el.props.props['interactive'], + true, + 'interactive should be true when component has unhover hook' + ) + assert.end() +}) + +test('Element - holder true sets interactive to false when component has no hover/unhover hooks', (assert) => { + assert.capture(renderer, 'createNode', () => new EventEmitter()) + const component = { [symbols.identifier]: 'test-no-hooks-1' } + registerHooks({}, component[symbols.identifier]) + const el = element({ parent: { node: { w: 1920, h: 1080 } } }, component) + el.populate({ parent: { node: new EventEmitter() }, holder: true }) assert.equal( el.props.props['interactive'], false, - 'interactive should be false when holder is false (hoverable: false)' + 'interactive should be false when component has no hover/unhover hooks' ) assert.end() }) diff --git a/src/lib/codegenerator/generator.js b/src/lib/codegenerator/generator.js index 3b3755c9..f8ab03bc 100644 --- a/src/lib/codegenerator/generator.js +++ b/src/lib/codegenerator/generator.js @@ -187,9 +187,7 @@ const generateElementCode = function ( } if (options.holder) { - renderCode.push( - `elementConfigs[${counter}]['holder'] = cmps[${counter}] === undefined || cmps[${counter}][Symbol.for('config')].hoverable !== false` - ) + renderCode.push(`elementConfigs[${counter}]['holder'] = true`) } Object.keys(templateObject).forEach((key) => { diff --git a/src/lib/hooks.js b/src/lib/hooks.js index f0a18c80..1f8e439e 100644 --- a/src/lib/hooks.js +++ b/src/lib/hooks.js @@ -57,3 +57,7 @@ export const registerHooks = (hooks = {}, identifier) => { if (typeof hooks[hook] === 'function') cbs[identifier][hook] = hooks[hook] }) } + +export const hasHook = (hook, identifier) => { + return !!(cbs[identifier] && cbs[identifier][hook]) +} From 6fdcd57149b24aadd8ac7f407ea6170c7e49d4cc Mon Sep 17 00:00:00 2001 From: Suresh Kumar Gangumalla Date: Thu, 16 Apr 2026 11:29:20 +0530 Subject: [PATCH 3/6] Addressed review comments Signed-off-by: Suresh Kumar Gangumalla --- src/engines/L3/element.js | 11 ++--------- src/lib/hooks.js | 2 +- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/engines/L3/element.js b/src/engines/L3/element.js index 3ed0348a..be3f2fbf 100644 --- a/src/engines/L3/element.js +++ b/src/engines/L3/element.js @@ -529,15 +529,8 @@ const propsTransformer = { } }, set holder(v) { - const identifier = - this.element && this.element.component - ? this.element.component[symbols.identifier] - : undefined - if (identifier !== undefined) { - this.props['interactive'] = hasHook('hover', identifier) || hasHook('unhover', identifier) - } else { - this.props['interactive'] = v - } + const identifier = this.element.component[symbols.identifier] + this.props['interactive'] = hasHook('hover', identifier) || hasHook('unhover', identifier) }, } diff --git a/src/lib/hooks.js b/src/lib/hooks.js index 1f8e439e..5bb45cf0 100644 --- a/src/lib/hooks.js +++ b/src/lib/hooks.js @@ -59,5 +59,5 @@ export const registerHooks = (hooks = {}, identifier) => { } export const hasHook = (hook, identifier) => { - return !!(cbs[identifier] && cbs[identifier][hook]) + return cbs[identifier] !== undefined && hook in cbs[identifier] } From a7bbec4a0b9698c5cea2206cf21bd2d79d7f0e24 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Gangumalla Date: Thu, 23 Apr 2026 17:12:49 +0530 Subject: [PATCH 4/6] Detecting $isHovered from template Detecting $isHovered from template and checking whether component holder node should be interactive or not based on $isHovered or hover/unhover hooks. Signed-off-by: Suresh Kumar Gangumalla --- src/application.js | 1 + src/component.js | 10 ++++++++++ src/engines/L3/element.js | 4 +--- src/lib/codegenerator/generator.js | 7 +++++-- src/lib/precompiler/precompiler.js | 2 +- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/application.js b/src/application.js index a10651d7..d6cf74f2 100644 --- a/src/application.js +++ b/src/application.js @@ -165,6 +165,7 @@ const Application = (config) => { if (node === null) { currentComponent = undefined + currentNode = null Hover.clear() return } diff --git a/src/component.js b/src/component.js index 1999ff2a..022b7c31 100644 --- a/src/component.js +++ b/src/component.js @@ -499,6 +499,16 @@ const Component = (name = required('name'), config = required('config')) => { // To determine whether dynamic component is actual Blits component or not factory[symbols.isComponent] = true + // Getter to determine if the component needs interactive hover support + Object.defineProperty(factory, 'needsInteractive', { + get() { + return ( + (config.hooks && !!(config.hooks.hover || config.hooks.unhover)) || + !!(config.code && config.code.usesHoverState) + ) + }, + }) + return factory } diff --git a/src/engines/L3/element.js b/src/engines/L3/element.js index be3f2fbf..1f2ef3e3 100644 --- a/src/engines/L3/element.js +++ b/src/engines/L3/element.js @@ -23,7 +23,6 @@ import { Log } from '../../lib/log.js' import symbols from '../../lib/symbols.js' import Settings from '../../settings.js' import shaders from '../../lib/shaders/shaders.js' -import { hasHook } from '../../lib/hooks.js' const holderComponentMap = new WeakMap() @@ -529,8 +528,7 @@ const propsTransformer = { } }, set holder(v) { - const identifier = this.element.component[symbols.identifier] - this.props['interactive'] = hasHook('hover', identifier) || hasHook('unhover', identifier) + this.props['interactive'] = v }, } diff --git a/src/lib/codegenerator/generator.js b/src/lib/codegenerator/generator.js index f8ab03bc..d54eb37c 100644 --- a/src/lib/codegenerator/generator.js +++ b/src/lib/codegenerator/generator.js @@ -87,6 +87,8 @@ export default function (templateObject = { children: [] }, devMode = false) { }, skips} `) + const renderCodeString = ctx.renderCode.join('\n') + return { render: new Function( 'parent', @@ -96,7 +98,7 @@ export default function (templateObject = { children: [] }, devMode = false) { 'effect', 'getRaw', 'Log', - ctx.renderCode.join('\n') + renderCodeString ), effects: ctx.effectsCode.map( (code) => @@ -112,6 +114,7 @@ export default function (templateObject = { children: [] }, devMode = false) { ) ), context: ctx.context, + usesHoverState: renderCodeString.includes('.$isHovered'), } } @@ -187,7 +190,7 @@ const generateElementCode = function ( } if (options.holder) { - renderCode.push(`elementConfigs[${counter}]['holder'] = true`) + renderCode.push(`elementConfigs[${counter}]['holder'] = cmps[${counter}].needsInteractive`) } Object.keys(templateObject).forEach((key) => { diff --git a/src/lib/precompiler/precompiler.js b/src/lib/precompiler/precompiler.js index 7552b20f..d11d511c 100644 --- a/src/lib/precompiler/precompiler.js +++ b/src/lib/precompiler/precompiler.js @@ -55,7 +55,7 @@ export default (source, filePath, mode) => { // Insert the code in the component using the 'code' key, replacing the template key const replacement = `/* eslint-disable no-unused-vars */ \ncode: { render: ${code.render.toString()}, effects: [${code.effects.map( (fn) => fn.toString() - )}], context: {}}` + )}], context: {}, usesHoverState: ${code.usesHoverState}}` // MagicString tracks all changes automatically, no offset needed s.overwrite(templateStartIndex, templateEndIndex, replacement) From cde97bf91acee32dba2702d7450178b7eb8f6eac Mon Sep 17 00:00:00 2001 From: Suresh Kumar Gangumalla Date: Fri, 24 Apr 2026 17:35:02 +0530 Subject: [PATCH 5/6] Handled $isHovered and this.$isHovered from template and from JS layer detecting whether any component holding reference to $isHovered then marking that component's holder node as interactive otherwise non-interactive For router page components, holder node will be created by router not code-generator like regular children component's inside a page component. Router page components are two types, lazy import and normal direct import. We can't alter (even we have alter also renderer doesn't remove/add it from hierarchy pattern) any node interactivity once it created so for normal import we can invoke needsInteractity function in component's factory which will return whether component's holder node need to be marked interactive or not based on folliwng conditions 1. Existance of hover/unhover hook in component config 2. Existance of $isHovered in template which will be inspected at pre-compile or run time compilation 3. Existance of this.$isHovered in JS layer of component This step is challening, in users opted for pre-compilation, it will be like finding existance of this.$isHovered in a source code If not, we will loop through all JS layer functions and stringy then look for this.$isHovered. So, finally dynamic imported router page component's holder node always interactive. Signed-off-by: Suresh Kumar Gangumalla --- index.d.ts | 4 +++- src/engines/L3/element.test.js | 31 ++++++------------------- src/lib/codegenerator/generator.test.js | 24 +++++++++---------- src/lib/precompiler/precompiler.js | 23 +++++++++++++++++- src/router/router.js | 7 ++++-- 5 files changed, 49 insertions(+), 40 deletions(-) diff --git a/index.d.ts b/index.d.ts index e9ddc41e..ffe605d3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -905,7 +905,9 @@ declare module '@lightningjs/blits' { } } - type ComponentFactory = () => void + type ComponentFactory = (() => void) & { + needsInteractive?: boolean + } // Launch Related diff --git a/src/engines/L3/element.test.js b/src/engines/L3/element.test.js index 3f917341..ef39ffaa 100644 --- a/src/engines/L3/element.test.js +++ b/src/engines/L3/element.test.js @@ -23,7 +23,6 @@ import { initLog } from '../../lib/log.js' import symbols from '../../lib/symbols.js' import sinon from 'sinon' import shaders from '../../lib/shaders/shaders.js' // Changed from './shaderLoader.js' -import { registerHooks } from '../../lib/hooks.js' initLog() @@ -1257,44 +1256,28 @@ test('Element - Set maxheight property', (assert) => { assert.end() }) -test('Element - holder true sets interactive to true when component has hover hook', (assert) => { +test('Element - holder sets interactive to the passed value (true)', (assert) => { assert.capture(renderer, 'createNode', () => new EventEmitter()) - const component = { [symbols.identifier]: 'test-hover-1' } - registerHooks({ hover: () => {} }, component[symbols.identifier]) + const component = {} const el = element({ parent: { node: { w: 1920, h: 1080 } } }, component) el.populate({ parent: { node: new EventEmitter() }, holder: true }) assert.equal( el.props.props['interactive'], true, - 'interactive should be true when component has hover hook' + 'interactive should be true when holder value is true' ) assert.end() }) -test('Element - holder true sets interactive to true when component has unhover hook', (assert) => { +test('Element - holder sets interactive to the passed value (false)', (assert) => { assert.capture(renderer, 'createNode', () => new EventEmitter()) - const component = { [symbols.identifier]: 'test-unhover-1' } - registerHooks({ unhover: () => {} }, component[symbols.identifier]) + const component = {} const el = element({ parent: { node: { w: 1920, h: 1080 } } }, component) - el.populate({ parent: { node: new EventEmitter() }, holder: true }) - assert.equal( - el.props.props['interactive'], - true, - 'interactive should be true when component has unhover hook' - ) - assert.end() -}) - -test('Element - holder true sets interactive to false when component has no hover/unhover hooks', (assert) => { - assert.capture(renderer, 'createNode', () => new EventEmitter()) - const component = { [symbols.identifier]: 'test-no-hooks-1' } - registerHooks({}, component[symbols.identifier]) - const el = element({ parent: { node: { w: 1920, h: 1080 } } }, component) - el.populate({ parent: { node: new EventEmitter() }, holder: true }) + el.populate({ parent: { node: new EventEmitter() }, holder: false }) assert.equal( el.props.props['interactive'], false, - 'interactive should be false when component has no hover/unhover hooks' + 'interactive should be false when holder value is false' ) assert.end() }) diff --git a/src/lib/codegenerator/generator.test.js b/src/lib/codegenerator/generator.test.js index e5cbfecf..23dfac2f 100644 --- a/src/lib/codegenerator/generator.test.js +++ b/src/lib/codegenerator/generator.test.js @@ -1542,7 +1542,7 @@ test('Generate code for a template with custom components', (assert) => { elementConfigs[1] = {} elms[1] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component) - elementConfigs[1]['holder'] = true + elementConfigs[1]['holder'] = cmps[1].needsInteractive skips[1] = [] if (typeof cmps[1] !== 'undefined' && cmps[1][Symbol.for('config')].props !== undefined) { let props = cmps[1][Symbol.for('config')].props @@ -1591,7 +1591,7 @@ test('Generate code for a template with custom components', (assert) => { elementConfigs[3] = {} elms[3] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component) - elementConfigs[3]['holder'] = true + elementConfigs[3]['holder'] = cmps[3].needsInteractive skips[3] = [] if (typeof cmps[3] !== 'undefined' && cmps[3][Symbol.for('config')].props !== undefined) { let props = cmps[3][Symbol.for('config')].props @@ -1730,7 +1730,7 @@ test('Generate code for a template with an unregistered custom component', (asse parent = elms[0] elementConfigs[1] = {} elms[1] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component) - elementConfigs[1]['holder'] = true + elementConfigs[1]['holder'] = cmps[1].needsInteractive skips[1] = [] if (typeof cmps[1] !== 'undefined' && cmps[1][Symbol.for('config')].props !== undefined) { let props = cmps[1][Symbol.for('config')].props @@ -1775,7 +1775,7 @@ test('Generate code for a template with an unregistered custom component', (asse parent = elms[0] elementConfigs[3] = {} elms[3] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component) - elementConfigs[3]['holder'] = true + elementConfigs[3]['holder'] = cmps[3].needsInteractive skips[3] = [] if (typeof cmps[3] !== 'undefined' && cmps[3][Symbol.for('config')].props !== undefined) { let props = cmps[3][Symbol.for('config')].props @@ -1913,7 +1913,7 @@ test('Generate code for a template with custom components with arguments', (asse parent = elms[0] elementConfigs[1] = {} elms[1] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component) - elementConfigs[1]['holder'] = true + elementConfigs[1]['holder'] = cmps[1].needsInteractive elementConfigs[1]['x'] = 10 skips[1] = [] if(typeof cmps[1] !== 'undefined' && cmps[1][Symbol.for('config')].props !== undefined) { @@ -1960,7 +1960,7 @@ test('Generate code for a template with custom components with arguments', (asse parent = elms[0] elementConfigs[3] = {} elms[3] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component) - elementConfigs[3]['holder'] = true + elementConfigs[3]['holder'] = cmps[3].needsInteractive elementConfigs[3]['x'] = 100 elementConfigs[3]['img'] = component.img skips[3] = [] @@ -2107,7 +2107,7 @@ test('Generate code for a template with custom components with argument value as elms[1] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component) - elementConfigs[1]['holder'] = true + elementConfigs[1]['holder'] = cmps[1].needsInteractive elementConfigs[1]['x'] = 1000 elementConfigs[1]['y'] = 100 elementConfigs[1]['h'] = parent.node.h * (45 / 100) @@ -2262,7 +2262,7 @@ test('Generate code for a template with custom components with reactive props', parent = elms[0] elementConfigs[1] = {} elms[1] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component) - elementConfigs[1]['holder'] = true + elementConfigs[1]['holder'] = cmps[1].needsInteractive elementConfigs[1]['x'] = 10 elementConfigs[1]['img'] = component.image skips[1] = [] @@ -2316,7 +2316,7 @@ test('Generate code for a template with custom components with reactive props', parent = elms[0] elementConfigs[3] = {} elms[3] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component) - elementConfigs[3]['holder'] = true + elementConfigs[3]['holder'] = cmps[3].needsInteractive elementConfigs[3]['x'] = 100 elementConfigs[3]['img'] = component.image skips[3] = [] @@ -2592,7 +2592,7 @@ test('Generate code for a template with slot content', (assert) => { elementConfigs[1] = {} elms[1] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component) - elementConfigs[1]['holder'] = true + elementConfigs[1]['holder'] = cmps[1].needsInteractive elementConfigs[1]['w'] = 1920 elementConfigs[1]['h'] = 1080 skips[1] = [] @@ -2781,7 +2781,7 @@ test('Generate code for a template with slot content, using a named slot', (asse elementConfigs[1] = {} elms[1] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component) - elementConfigs[1]['holder'] = true + elementConfigs[1]['holder'] = cmps[1].needsInteractive elementConfigs[1]['w'] = 1920 elementConfigs[1]['h'] = 1080 skips[1] = [] @@ -4411,7 +4411,7 @@ test('Generate code for a template with a simple for-loop on a Component with a if(elms[1][scope.key] === undefined) { elms[1][scope.key] = this.element({parent: parent || 'root'}, inSlot === true ? slotComponent : component) } - elementConfigs[1]['holder'] = true + elementConfigs[1]['holder'] = cmps[1].needsInteractive skips[1] = [] if(typeof cmps[1] !== 'undefined' && cmps[1][Symbol.for('config')].props !== undefined) { // attributes that are a prop should be removed from element config (even if it's a know element prop) diff --git a/src/lib/precompiler/precompiler.js b/src/lib/precompiler/precompiler.js index d11d511c..1605bf21 100644 --- a/src/lib/precompiler/precompiler.js +++ b/src/lib/precompiler/precompiler.js @@ -31,6 +31,11 @@ export default (source, filePath, mode) => { // Use MagicString to track changes const s = new MagicString(source) + // Find component definition start positions for scoping JS-layer checks + const componentStarts = [...source.matchAll(/(?:Blits\.)?(?:Component|Application)\s*\(/g)] + .map((m) => m.index) + .sort((a, b) => a - b) + for (const template of templates) { if (template[2]) { const templateContent = template[2] @@ -52,10 +57,26 @@ export default (source, filePath, mode) => { // Generate the code const code = generator.call({ components: {} }, parsed, mode === 'development') + // Determine the component scope containing this template + let scopeStart = 0 + let scopeEnd = source.length + for (let i = 0; i < componentStarts.length; i++) { + if (componentStarts[i] <= templateStartIndex) { + scopeStart = componentStarts[i] + scopeEnd = i + 1 < componentStarts.length ? componentStarts[i + 1] : source.length + } + } + const componentScope = source.slice(scopeStart, scopeEnd) + + // Derive usesHoverState from both template ($$isHovered) and JS layer (this.$isHovered) + const usesHoverState = + code.usesHoverState || + componentScope.replace(templateContent, '').includes('this.$isHovered') + // Insert the code in the component using the 'code' key, replacing the template key const replacement = `/* eslint-disable no-unused-vars */ \ncode: { render: ${code.render.toString()}, effects: [${code.effects.map( (fn) => fn.toString() - )}], context: {}, usesHoverState: ${code.usesHoverState}}` + )}], context: {}, usesHoverState: ${usesHoverState}}` // MagicString tracks all changes automatically, no offset needed s.overwrite(templateStartIndex, templateEndIndex, replacement) diff --git a/src/router/router.js b/src/router/router.js index 436ef4ce..9d2393be 100644 --- a/src/router/router.js +++ b/src/router/router.js @@ -42,7 +42,7 @@ import Settings from '../settings.js' * @property {Object} hooks - The hooks of the route * @property {Object} transition - The transition of the route * @property {Object} announce - The announce of the route - * @property {(options: Object, parentEl: BlitsElement, parentComponent: BlitsComponent, rootComponent?: BlitsComponent) => BlitsComponent} component - The component factory of the route + * @property {BlitsComponentFactory} component - The component factory of the route (or a lazy import function) * * @typedef {Object} Hash * @property {string} path - The path of the hash @@ -427,8 +427,11 @@ export const navigate = async function () { if (!view) { // create a holder element for the new view + // for lazy imports needsInteractive is undefined, default to true + const needsInteractive = + route.component.needsInteractive !== undefined ? route.component.needsInteractive : true holder = stage.element({ parent: this[symbols.children][0] }) - holder.populate({}) + holder.populate({ holder: needsInteractive }) holder.set('w', '100%') holder.set('h', '100%') From 0e08352b82db60b5d61ff93e302a1c4d1d88ee4d Mon Sep 17 00:00:00 2001 From: Ugur Aslan Date: Tue, 28 Apr 2026 12:46:30 +0200 Subject: [PATCH 6/6] fix: improve precompiler hover state detection --- src/lib/precompiler/precompiler.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/lib/precompiler/precompiler.js b/src/lib/precompiler/precompiler.js index 1605bf21..1aed01a2 100644 --- a/src/lib/precompiler/precompiler.js +++ b/src/lib/precompiler/precompiler.js @@ -32,7 +32,7 @@ export default (source, filePath, mode) => { const s = new MagicString(source) // Find component definition start positions for scoping JS-layer checks - const componentStarts = [...source.matchAll(/(?:Blits\.)?(?:Component|Application)\s*\(/g)] + const componentStarts = [...source.matchAll(/Blits\.(Component|Application)\s*\(/g)] .map((m) => m.index) .sort((a, b) => a - b) @@ -69,9 +69,11 @@ export default (source, filePath, mode) => { const componentScope = source.slice(scopeStart, scopeEnd) // Derive usesHoverState from both template ($$isHovered) and JS layer (this.$isHovered) - const usesHoverState = - code.usesHoverState || - componentScope.replace(templateContent, '').includes('this.$isHovered') + // Strip comments before checking to avoid false positives from commented-out code + const strippedScope = componentScope + .replace(/\/\/[^\n]*/g, '') + .replace(/\/\*[\s\S]*?\*\//g, '') + const usesHoverState = code.usesHoverState || strippedScope.includes('this.$isHovered') // Insert the code in the component using the 'code' key, replacing the template key const replacement = `/* eslint-disable no-unused-vars */ \ncode: { render: ${code.render.toString()}, effects: [${code.effects.map(