Skip to content
Merged
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
202 changes: 112 additions & 90 deletions src/core/focusManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,60 +273,34 @@ const updateFocusPath = (
let lastGlobalKeyPressTime = 0;
let lastInputKey: string | number | undefined;

const propagateKeyPress = (
const isElementThrottled = (
elm: ElementNode,
sameKey: boolean,
currentTime: number,
): boolean =>
elm.throttleInput !== undefined &&
sameKey &&
elm._lastAnyKeyPressTime !== undefined &&
currentTime - elm._lastAnyKeyPressTime < elm.throttleInput;

// Walk focus path root→leaf. Returns true if a capture handler claimed the
// event (or an element on the path is currently rate-limited).
const runCapturePhase = (
fp: ElementNode[],
e: KeyboardEvent,
mappedEvent?: string,
isHold: boolean = false,
isUp: boolean = false,
mappedEvent: string | undefined,
isUp: boolean,
sameKey: boolean,
currentTime: number,
): boolean => {
const currentTime = performance.now();
const key = e.key || e.keyCode;
const sameKey = lastInputKey === key;
lastInputKey = key;

if (!isUp && Config.throttleInput) {
if (
sameKey &&
currentTime - lastGlobalKeyPressTime < Config.throttleInput
) {
if (isDev && Config.keyDebug) {
console.log(
`Keypress throttled by global Config.throttleInput: ${Config.throttleInput}ms`,
);
}
return false;
}
lastGlobalKeyPressTime = currentTime;
}

// Record the key for focus-history attribution (keyup presses don't trigger focus changes)
if (!isUp) {
_pendingHistoryKey = { keyPressed: key, mappedKey: mappedEvent };
}

const fp = focusPath();
const numItems = fp.length;
if (numItems === 0) return false;

let handlerAvailable: ElementNode | undefined;
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--) {
for (let i = fp.length - 1; i >= 0; i--) {
const elm = fp[i]!;

// Check throttle for capture phase
if (elm.throttleInput) {
if (
sameKey &&
elm._lastAnyKeyPressTime !== undefined &&
currentTime - elm._lastAnyKeyPressTime < elm.throttleInput
) {
return true;
}
}
if (isElementThrottled(elm, sameKey, currentTime)) return true;

const captureHandler = elm[captureEvent] || elm[captureKey];
if (
Expand All @@ -337,73 +311,121 @@ const propagateKeyPress = (
return true;
}
}
return false;
};

let eventHandlerKey: string | undefined;
let fallbackHandlerKey: 'onKeyHold' | 'onKeyPress' | undefined;

if (mappedEvent) {
eventHandlerKey = isUp ? `on${mappedEvent}Release` : `on${mappedEvent}`;
}

if (!isUp) {
fallbackHandlerKey = isHold ? 'onKeyHold' : 'onKeyPress';
}

for (let i = 0; i < numItems; i++) {
// Walk focus path leaf→root. Returns whether the event was handled and the
// last element that had *any* matching handler (for the no-handler debug log).
const runBubblePhase = (
fp: ElementNode[],
e: KeyboardEvent,
mappedEvent: string | undefined,
isHold: boolean,
isUp: boolean,
sameKey: boolean,
currentTime: number,
): { handled: boolean; lastHandlerSeen: ElementNode | undefined } => {
const finalFocusElm = fp[0]!;
const eventHandlerKey = mappedEvent
? isUp
? `on${mappedEvent}Release`
: `on${mappedEvent}`
: undefined;
const fallbackHandlerKey: 'onKeyHold' | 'onKeyPress' | undefined = isUp
? undefined
: isHold
? 'onKeyHold'
: 'onKeyPress';

let lastHandlerSeen: ElementNode | undefined;

for (let i = 0; i < fp.length; i++) {
const elm = fp[i]!;

// Check throttle for bubbling phase
if (elm.throttleInput) {
if (
sameKey &&
elm._lastAnyKeyPressTime !== undefined &&
currentTime - elm._lastAnyKeyPressTime < elm.throttleInput
) {
return true;
}
if (isElementThrottled(elm, sameKey, currentTime)) {
return { handled: true, lastHandlerSeen };
}

let handled = false;

if (eventHandlerKey) {
const eventHandler = elm[eventHandlerKey];
if (isFunction(eventHandler)) {
handlerAvailable = elm;
if (eventHandler.call(elm, e, elm, finalFocusElm) === true) {
handled = true;
}
lastHandlerSeen = elm;
handled = eventHandler.call(elm, e, elm, finalFocusElm) === true;
}
}

// Check for the fallback handler if its key is defined and not already handled by specific key handler
if (!handled && fallbackHandlerKey) {
const fallbackHandler = elm[fallbackHandlerKey];
if (isFunction(fallbackHandler)) {
handlerAvailable = elm;
if (
fallbackHandler.call(elm, e, mappedEvent, elm, finalFocusElm) === true
) {
handled = true;
}
lastHandlerSeen = elm;
handled =
fallbackHandler.call(elm, e, mappedEvent, elm, finalFocusElm) ===
true;
}
}

if (handled) {
elm._lastAnyKeyPressTime = currentTime;
return true;
return { handled: true, lastHandlerSeen };
}
}
return { handled: false, lastHandlerSeen };
};

const propagateKeyPress = (
e: KeyboardEvent,
mappedEvent?: string,
isHold: boolean = false,
isUp: boolean = false,
): boolean => {
const currentTime = performance.now();
const key = e.key || e.keyCode;
const sameKey = lastInputKey === key;
lastInputKey = key;

if (!isUp && Config.throttleInput) {
if (
sameKey &&
currentTime - lastGlobalKeyPressTime < Config.throttleInput
) {
if (isDev && Config.keyDebug) {
console.log(
`Keypress throttled by global Config.throttleInput: ${Config.throttleInput}ms`,
);
}
return false;
}
lastGlobalKeyPressTime = currentTime;
}

// Keyup events don't trigger focus changes, so don't record their key.
if (!isUp) {
_pendingHistoryKey = { keyPressed: key, mappedKey: mappedEvent };
}

const fp = focusPath();
if (fp.length === 0) return false;

if (runCapturePhase(fp, e, mappedEvent, isUp, sameKey, currentTime)) {
return true;
}

const { handled, lastHandlerSeen } = runBubblePhase(
fp,
e,
mappedEvent,
isHold,
isUp,
sameKey,
currentTime,
);
if (handled) return true;

if (isDev && Config.keyDebug && !isUp) {
if (handlerAvailable) {
console.log(
`Keypress bubbled, key="${e.key}", mappedEvent=${mappedEvent}, isHold=${isHold}, isUp=${isUp}`,
handlerAvailable,
);
const detail = `key="${e.key}", mappedEvent=${mappedEvent}, isHold=${isHold}, isUp=${isUp}`;
if (lastHandlerSeen) {
console.log(`Keypress bubbled, ${detail}`, lastHandlerSeen);
} else {
console.log(
`No event handler available for keypress: key="${e.key}", mappedEvent=${mappedEvent}, isHold=${isHold}, isUp=${isUp}`,
);
console.log(`No event handler available for keypress: ${detail}`);
}
}

Expand Down
Loading