From 8ec42184e50ea04923f1d272b6e4012c03a269a2 Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Wed, 20 May 2026 20:52:30 -0400 Subject: [PATCH 1/2] clean up of activeElement --- src/activeElement.ts | 5 ----- src/core/config.ts | 2 -- src/core/focusManager.ts | 24 ++++++++++++++++-------- src/index.ts | 2 +- src/primitives/useFocusManager.ts | 5 +---- src/render.ts | 4 +--- 6 files changed, 19 insertions(+), 23 deletions(-) delete mode 100644 src/activeElement.ts diff --git a/src/activeElement.ts b/src/activeElement.ts deleted file mode 100644 index 5962948..0000000 --- a/src/activeElement.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createSignal } from 'solid-js'; -import { type ElementNode } from './core/index.js'; -export const [activeElement, setActiveElement] = createSignal< - ElementNode | undefined ->(undefined); diff --git a/src/core/config.ts b/src/core/config.ts index 506912f..14eb471 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -48,7 +48,6 @@ export interface Config { animationsEnabled: boolean; fontSettings: Partial; rendererOptions?: Partial | DomRendererMainSettings; - setActiveElement: (elm: ElementNode) => void; focusStateKey: DollarString; lockStyles?: boolean; fontWeightAlias?: Record; @@ -83,7 +82,6 @@ export const Config: Config = { bold: 700, black: 900, }, - setActiveElement: () => {}, focusStateKey: '$focus', lockStyles: true, rendererOptions: {}, diff --git a/src/core/focusManager.ts b/src/core/focusManager.ts index 98dddca..4d0e1cb 100644 --- a/src/core/focusManager.ts +++ b/src/core/focusManager.ts @@ -1,3 +1,4 @@ +import { createSignal } from 'solid-js'; import { Config, isDev } from './config.js'; import { IRendererNode } from './dom-renderer/domRendererTypes.js'; export type * from './focusKeyTypes.js'; @@ -9,6 +10,12 @@ import type { } from './focusKeyTypes.js'; import { isFunction } from './utils.js'; +export const [activeElement, setActiveElementSignal] = createSignal< + ElementNode | undefined +>(undefined); + +let _signalWrapper: (cb: () => void) => void = (cb) => cb(); + type KeyMapEntries = Record; const keyMapEntries: KeyMapEntries = { @@ -197,17 +204,14 @@ export const printFocusHistory = (count: number): void => { // --------------------------------------------------------------------------- -let activeElement: ElementNode | undefined; export const setActiveElement = (elm: ElementNode) => { - if (elm === activeElement) return; - const prev = activeElement; - updateFocusPath(elm, activeElement); - activeElement = elm; + const prev = activeElement(); + if (elm === prev) return; + updateFocusPath(elm, prev); recordFocusHistory(elm, prev); // Reset key attribution so programmatic focus changes show '—' for key fields _pendingHistoryKey = { keyPressed: undefined, mappedKey: undefined }; - // Callback for libraries to use signals / refs - Config.setActiveElement(elm); + _signalWrapper(() => setActiveElementSignal(elm)); }; let focusPath: ElementNode[] = []; @@ -467,10 +471,14 @@ export const useFocusManager = ({ flattenKeyMap(keyHoldOptions.userKeyHoldMap, keyHoldMapEntries); } + // Route signal updates from programmatic .setFocus() and post-mutation + // through the same owner context as key events so effect subscribers have + // a parent for cleanup. + _signalWrapper = ownerContext; + const delay = keyHoldOptions?.holdThreshold || DEFAULT_KEY_HOLD_THRESHOLD; const runKeyEvent = handleKeyEvents.bind(null, delay); - // Owner context is for frameworks that need effects const keyPressHandler = (event: KeyboardEvent) => ownerContext(() => { runKeyEvent(event, undefined); diff --git a/src/index.ts b/src/index.ts index 3a2c717..7d5d5fc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ export type * from '@solidtv/solid/jsx-runtime'; export * from './core/index.js'; export type * from './core/index.js'; export type { KeyHandler, KeyMap } from './core/focusManager.js'; -export * from './activeElement.js'; +export { activeElement, setActiveElement } from './core/focusManager.js'; export * from './utils.js'; export * from './render.js'; export * from './types.js'; diff --git a/src/primitives/useFocusManager.ts b/src/primitives/useFocusManager.ts index 8274a50..62becdf 100644 --- a/src/primitives/useFocusManager.ts +++ b/src/primitives/useFocusManager.ts @@ -6,14 +6,13 @@ import { getOwner, runWithOwner, } from 'solid-js'; -import { Config } from '../core/index.js'; import type { ElementNode } from '../core/index.js'; import { + activeElement, useFocusManager as useFocusManagerCore, type KeyMap, type KeyHoldOptions, } from '../core/focusManager.js'; -import { activeElement, setActiveElement } from '../activeElement.js'; const [focusPath, setFocusPath] = createSignal([]); export { focusPath }; @@ -24,8 +23,6 @@ export const useFocusManager = ( ) => { const owner = getOwner(); const ownerContext = runWithOwner.bind(this, owner); - Config.setActiveElement = (activeElm) => - ownerContext(() => setActiveElement(activeElm)); const { cleanup, focusPath: focusPathCore } = useFocusManagerCore({ userKeyMap, diff --git a/src/render.ts b/src/render.ts index 6cafad3..4f412d5 100644 --- a/src/render.ts +++ b/src/render.ts @@ -18,7 +18,7 @@ import { type Component, } from 'solid-js'; import type { SolidNode } from './types.js'; -import { activeElement, setActiveElement } from './activeElement.js'; +import { activeElement } from './core/focusManager.js'; const solidRenderer = solidCreateRenderer(nodeOpts); @@ -37,8 +37,6 @@ export function createRenderer( const options = rendererOptions || Config.rendererOptions; renderer = startLightningRenderer(options!, node || 'app'); - //Prevent this from happening automatically - Config.setActiveElement = setActiveElement; rootNode.lng = renderer.root!; rootNode.rendered = true; renderer.on('idle', () => { From a1814a2fef6cd892954f9aa0b2e488a1885b8d00 Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Wed, 20 May 2026 20:59:03 -0400 Subject: [PATCH 2/2] refactor(focus): merge useFocusManager primitive into core/focusManager - Replace the mutable focusPath array with a real solid signal; updateFocusPath now sets it through the existing _signalWrapper, eliminating the mirror effect the primitive used to maintain. - Collapse the two-layer useFocusManager (core + primitive) into a single function in core that captures its calling owner via getOwner/runWithOwner, attaches its own document listeners, and self-registers cleanup via onCleanup. - Reduce primitives/useFocusManager.ts to a thin re-export shim so existing imports (announcer, etc.) keep working. One source of truth for activeElement and focusPath, one useFocusManager, no options-object/core-wrapper indirection. Co-Authored-By: Claude Opus 4.7 --- src/core/focusManager.ts | 78 +++++++++++++------------------ src/primitives/useFocusManager.ts | 44 ++--------------- 2 files changed, 37 insertions(+), 85 deletions(-) diff --git a/src/core/focusManager.ts b/src/core/focusManager.ts index 4d0e1cb..0a207bb 100644 --- a/src/core/focusManager.ts +++ b/src/core/focusManager.ts @@ -1,4 +1,4 @@ -import { createSignal } from 'solid-js'; +import { createSignal, getOwner, onCleanup, runWithOwner } from 'solid-js'; import { Config, isDev } from './config.js'; import { IRendererNode } from './dom-renderer/domRendererTypes.js'; export type * from './focusKeyTypes.js'; @@ -214,7 +214,8 @@ export const setActiveElement = (elm: ElementNode) => { _signalWrapper(() => setActiveElementSignal(elm)); }; -let focusPath: ElementNode[] = []; +export const [focusPath, setFocusPath] = createSignal([]); + const updateFocusPath = ( currentFocusedElm: ElementNode, prevFocusedElm: ElementNode | undefined, @@ -247,7 +248,8 @@ const updateFocusPath = ( current = current.parent; } - focusPath.forEach((elm) => { + const prevFp = focusPath(); + prevFp.forEach((elm) => { if (!fpSet.has(elm)) { elm.states.remove(Config.focusStateKey); elm.onBlur?.call(elm, currentFocusedElm, prevFocusedElm!, elm); @@ -262,11 +264,10 @@ const updateFocusPath = ( }); if (Config.focusDebug) { - addFocusDebug(focusPath, fp); + addFocusDebug(prevFp, fp); } - focusPath = fp; - return fp; + _signalWrapper(() => setFocusPath(fp)); }; let lastGlobalKeyPressTime = 0; @@ -303,17 +304,18 @@ const propagateKeyPress = ( _pendingHistoryKey = { keyPressed: key, mappedKey: mappedEvent }; } - const numItems = focusPath.length; + const fp = focusPath(); + const numItems = fp.length; if (numItems === 0) return false; let handlerAvailable: ElementNode | undefined; - const finalFocusElm = focusPath[0]!; + const finalFocusElm = fp[0]!; const keyBase = mappedEvent || e.key; const captureEvent = `onCapture${keyBase}${isUp ? 'Release' : ''}`; const captureKey = isUp ? 'onCaptureKeyRelease' : 'onCaptureKey'; for (let i = numItems - 1; i >= 0; i--) { - const elm = focusPath[i]!; + const elm = fp[i]!; // Check throttle for capture phase if (elm.throttleInput) { @@ -348,7 +350,7 @@ const propagateKeyPress = ( } for (let i = 0; i < numItems; i++) { - const elm = focusPath[i]!; + const elm = fp[i]!; // Check throttle for bubbling phase if (elm.throttleInput) { @@ -450,56 +452,42 @@ const handleKeyEvents = ( } }; -interface FocusManagerOptions { - userKeyMap?: Partial; - keyHoldOptions?: KeyHoldOptions; - ownerContext?: (cb: () => void) => void; -} - -export const useFocusManager = ({ - userKeyMap, - keyHoldOptions, - ownerContext = (cb) => { - cb(); - }, -}: FocusManagerOptions = {}) => { +export const useFocusManager = ( + userKeyMap?: Partial, + keyHoldOptions?: KeyHoldOptions, +) => { if (userKeyMap) { flattenKeyMap(userKeyMap, keyMapEntries); } - if (keyHoldOptions?.userKeyHoldMap) { flattenKeyMap(keyHoldOptions.userKeyHoldMap, keyHoldMapEntries); } - // Route signal updates from programmatic .setFocus() and post-mutation - // through the same owner context as key events so effect subscribers have - // a parent for cleanup. + // Capture the calling owner so signal updates and key-event reactions + // can run inside it — needed for programmatic .setFocus(), post-mutation + // focus, and any effect subscribers that rely on onCleanup. + const owner = getOwner(); + const ownerContext = (cb: () => void) => { + runWithOwner(owner, cb); + }; _signalWrapper = ownerContext; const delay = keyHoldOptions?.holdThreshold || DEFAULT_KEY_HOLD_THRESHOLD; const runKeyEvent = handleKeyEvents.bind(null, delay); const keyPressHandler = (event: KeyboardEvent) => - ownerContext(() => { - runKeyEvent(event, undefined); - }); - + ownerContext(() => runKeyEvent(event, undefined)); const keyUpHandler = (event: KeyboardEvent) => - ownerContext(() => { - runKeyEvent(undefined, event); - }); + ownerContext(() => runKeyEvent(undefined, event)); - document.addEventListener('keyup', keyUpHandler); document.addEventListener('keydown', keyPressHandler); + document.addEventListener('keyup', keyUpHandler); - return { - cleanup: () => { - document.removeEventListener('keydown', keyPressHandler); - document.removeEventListener('keyup', keyUpHandler); - for (const [_, timeout] of Object.entries(keyHoldTimeouts)) { - if (timeout && timeout !== true) clearTimeout(timeout); - } - }, - focusPath: () => focusPath, - }; + onCleanup(() => { + document.removeEventListener('keydown', keyPressHandler); + document.removeEventListener('keyup', keyUpHandler); + for (const timeout of Object.values(keyHoldTimeouts)) { + if (timeout && timeout !== true) clearTimeout(timeout); + } + }); }; diff --git a/src/primitives/useFocusManager.ts b/src/primitives/useFocusManager.ts index 62becdf..5505fd8 100644 --- a/src/primitives/useFocusManager.ts +++ b/src/primitives/useFocusManager.ts @@ -1,44 +1,8 @@ -import { - createEffect, - on, - createSignal, - onCleanup, - getOwner, - runWithOwner, -} from 'solid-js'; -import type { ElementNode } from '../core/index.js'; -import { +export { + focusPath, + useFocusManager, activeElement, - useFocusManager as useFocusManagerCore, + setActiveElement, type KeyMap, type KeyHoldOptions, } from '../core/focusManager.js'; - -const [focusPath, setFocusPath] = createSignal([]); -export { focusPath }; - -export const useFocusManager = ( - userKeyMap?: Partial, - keyHoldOptions?: KeyHoldOptions, -) => { - const owner = getOwner(); - const ownerContext = runWithOwner.bind(this, owner); - - const { cleanup, focusPath: focusPathCore } = useFocusManagerCore({ - userKeyMap, - keyHoldOptions, - ownerContext, - }); - - createEffect( - on( - activeElement, - () => { - setFocusPath([...focusPathCore()]); - }, - { defer: true }, - ), - ); - - onCleanup(cleanup); -};