Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions src/activeElement.ts

This file was deleted.

2 changes: 0 additions & 2 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export interface Config {
animationsEnabled: boolean;
fontSettings: Partial<TextProps>;
rendererOptions?: Partial<RendererMainSettings> | DomRendererMainSettings;
setActiveElement: (elm: ElementNode) => void;
focusStateKey: DollarString;
lockStyles?: boolean;
fontWeightAlias?: Record<string, number | string>;
Expand Down Expand Up @@ -83,7 +82,6 @@ export const Config: Config = {
bold: 700,
black: 900,
},
setActiveElement: () => {},
focusStateKey: '$focus',
lockStyles: true,
rendererOptions: {},
Expand Down
94 changes: 45 additions & 49 deletions src/core/focusManager.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
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';
Expand All @@ -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<KeyNameOrKeyCode, string>;

const keyMapEntries: KeyMapEntries = {
Expand Down Expand Up @@ -197,20 +204,18 @@ 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[] = [];
export const [focusPath, setFocusPath] = createSignal<ElementNode[]>([]);

const updateFocusPath = (
currentFocusedElm: ElementNode,
prevFocusedElm: ElementNode | undefined,
Expand Down Expand Up @@ -243,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);
Expand All @@ -258,11 +264,10 @@ const updateFocusPath = (
});

if (Config.focusDebug) {
addFocusDebug(focusPath, fp);
addFocusDebug(prevFp, fp);
}

focusPath = fp;
return fp;
_signalWrapper(() => setFocusPath(fp));
};

let lastGlobalKeyPressTime = 0;
Expand Down Expand Up @@ -299,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) {
Expand Down Expand Up @@ -344,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) {
Expand Down Expand Up @@ -446,52 +452,42 @@ const handleKeyEvents = (
}
};

interface FocusManagerOptions {
userKeyMap?: Partial<KeyMap>;
keyHoldOptions?: KeyHoldOptions;
ownerContext?: (cb: () => void) => void;
}

export const useFocusManager = ({
userKeyMap,
keyHoldOptions,
ownerContext = (cb) => {
cb();
},
}: FocusManagerOptions = {}) => {
export const useFocusManager = (
userKeyMap?: Partial<KeyMap>,
keyHoldOptions?: KeyHoldOptions,
) => {
if (userKeyMap) {
flattenKeyMap(userKeyMap, keyMapEntries);
}

if (keyHoldOptions?.userKeyHoldMap) {
flattenKeyMap(keyHoldOptions.userKeyHoldMap, keyHoldMapEntries);
}

// 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);

// Owner context is for frameworks that need effects
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);
}
});
};
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
49 changes: 5 additions & 44 deletions src/primitives/useFocusManager.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,8 @@
import {
createEffect,
on,
createSignal,
onCleanup,
getOwner,
runWithOwner,
} from 'solid-js';
import { Config } from '../core/index.js';
import type { ElementNode } from '../core/index.js';
import {
useFocusManager as useFocusManagerCore,
export {
focusPath,
useFocusManager,
activeElement,
setActiveElement,
type KeyMap,
type KeyHoldOptions,
} from '../core/focusManager.js';
import { activeElement, setActiveElement } from '../activeElement.js';

const [focusPath, setFocusPath] = createSignal<ElementNode[]>([]);
export { focusPath };

export const useFocusManager = (
userKeyMap?: Partial<KeyMap>,
keyHoldOptions?: KeyHoldOptions,
) => {
const owner = getOwner();
const ownerContext = runWithOwner.bind(this, owner);
Config.setActiveElement = (activeElm) =>
ownerContext(() => setActiveElement(activeElm));

const { cleanup, focusPath: focusPathCore } = useFocusManagerCore({
userKeyMap,
keyHoldOptions,
ownerContext,
});

createEffect(
on(
activeElement,
() => {
setFocusPath([...focusPathCore()]);
},
{ defer: true },
),
);

onCleanup(cleanup);
};
4 changes: 1 addition & 3 deletions src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SolidNode>(nodeOpts);

Expand All @@ -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', () => {
Expand Down
Loading