From 7408a22477b422ba5015c0ba5fdfd0001649c79e Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Mon, 18 May 2026 12:15:58 +0200 Subject: [PATCH 1/3] perf: drop deprecated event.details alias on KeyborgFocusInEvent `event.details` was kept as an `@deprecated` alias of `event.detail` for Tabster and other early consumers. The known consumer chain (Tabster's src/State/FocusedElement.ts and elsewhere) reads `e.detail` exclusively, and the field has been marked deprecated for multiple releases. Dropping the field plus the per-dispatch assignment trims a few bytes from every focus-in event handler call site. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/FocusEvent.mts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/FocusEvent.mts b/src/FocusEvent.mts index cacb6fafe..7da1e048f 100644 --- a/src/FocusEvent.mts +++ b/src/FocusEvent.mts @@ -64,12 +64,7 @@ export interface KeyborgFocusInEventDetails { originalEvent?: FocusEvent; } -export interface KeyborgFocusInEvent extends CustomEvent { - /** - * @deprecated - used `event.detail` - */ - details?: KeyborgFocusInEventDetails; -} +export type KeyborgFocusInEvent = CustomEvent; export interface KeyborgFocusOutEventDetails { originalEvent: FocusEvent; @@ -255,9 +250,6 @@ export function setupFocusEvent(win: Window): void { detail: details, }); - // Tabster (and other users) can still use the legacy details field - keeping for backwards compat - event.details = details; - if (_canOverrideNativeFocus || data[LAST_FOCUSED_PROGRAMMATICALLY]) { details.isFocusedProgrammatically = target === data[LAST_FOCUSED_PROGRAMMATICALLY]?.deref(); From ef3a8b41a3e5269c263ecb21171bd2fcba6baf94 Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Mon, 18 May 2026 12:17:11 +0200 Subject: [PATCH 2/3] perf: drop KeyborgProps (triggerKeys/dismissKeys) and dismiss timer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KeyborgProps was the only argument to `createKeyborg`/`createKeyborgCore`, gating two features: * `triggerKeys` — a custom set of keys that should opt the window into keyboard-navigation mode in place of "any non-Tab key on a non-editable element". Never used by any known consumer; the default predicate is already the right behaviour for screen-reader and Tab-driven flows. * `dismissKeys` — a key set that scheduled a 500 ms timer to dismiss keyboard mode if focus did not move (typically wired to Escape). This hand-rolled latch duplicated work consumers already do at the application level. Drop the props interface, the `shouldDismiss`/`scheduleDismiss`/ `dismissTimer`/`_dismissTimeout` machinery, and the `triggerKeys` membership check in `shouldTrigger`. `onKeyDown` collapses to a single guard: enter keyboard mode if not already in it and the key should trigger. `KeyborgProps` was never re-exported from `src/index.mts`, so the public surface stays compatible aside from the (still-typed) extra `createKeyborg` parameter going away. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Keyborg.mts | 78 ++++--------------------------------------------- 1 file changed, 5 insertions(+), 73 deletions(-) diff --git a/src/Keyborg.mts b/src/Keyborg.mts index 76d33586f..76c6a5bfc 100644 --- a/src/Keyborg.mts +++ b/src/Keyborg.mts @@ -18,23 +18,8 @@ interface WindowWithKeyborg extends Window { }; } -// When a key from dismissKeys is pressed and the focus is not moved during -// _dismissTimeout ms, keyboard navigation mode is dismissed. -const _dismissTimeout = 500; - let _lastId = 0; -export interface KeyborgProps { - // Keys to be used to trigger keyboard navigation mode. By default, any key - // will trigger it. Could be limited to, for example, just Tab (or Tab and - // arrow keys). - triggerKeys?: number[]; - // Keys to be used to dismiss keyboard navigation mode using keyboard (in - // addition to mouse clicks which dismiss it). For example, Esc could be used - // to dismiss. - dismissKeys?: number[]; -} - export type KeyborgCallback = (isNavigatingWithKeyboard: boolean) => void; /** @@ -83,25 +68,10 @@ interface KeyborgInternal extends Keyborg { dispose(): void; } -function createKeyborgCore( - targetWindow: WindowWithKeyborg, - props?: KeyborgProps, -): KeyborgCoreHandle { +function createKeyborgCore(targetWindow: WindowWithKeyborg): KeyborgCoreHandle { let currentTargetWindow: WindowWithKeyborg | undefined = targetWindow; let isNavigating = false; let isMouseOrTouchUsedTimer: number | undefined; - let dismissTimer: number | undefined; - let triggerKeys: Set | undefined; - let dismissKeys: Set | undefined; - - if (props) { - if (props.triggerKeys?.length) { - triggerKeys = new Set(props.triggerKeys); - } - if (props.dismissKeys?.length) { - dismissKeys = new Set(props.dismissKeys); - } - } const broadcast = (): void => { const refs = currentTargetWindow?.__keyborg?.refs; @@ -128,42 +98,12 @@ function createKeyborgCore( } const active = currentTargetWindow?.document .activeElement as HTMLElement | null; - const isTriggerKey = !triggerKeys || triggerKeys.has(e.keyCode); const isEditable = active && (active.tagName === "INPUT" || active.tagName === "TEXTAREA" || active.isContentEditable); - return isTriggerKey && !isEditable; - }; - - const shouldDismiss = (e: KeyboardEvent): boolean => { - return !!dismissKeys?.has(e.keyCode); - }; - - const scheduleDismiss = (): void => { - const targetWindow = currentTargetWindow; - if (!targetWindow) { - return; - } - if (dismissTimer) { - targetWindow.clearTimeout(dismissTimer); - dismissTimer = undefined; - } - const previousActiveElement = targetWindow.document.activeElement; - dismissTimer = targetWindow.setTimeout(() => { - dismissTimer = undefined; - const currentActiveElement = targetWindow.document.activeElement; - if ( - previousActiveElement && - currentActiveElement && - previousActiveElement === currentActiveElement - ) { - // Esc was pressed, currently focused element hasn't changed. - // Just dismiss the keyboard navigation mode. - setNavigating(false); - } - }, _dismissTimeout); + return !isEditable; }; const onFocusIn = (e: KeyborgFocusInEvent): void => { @@ -216,11 +156,7 @@ function createKeyborgCore( }; const onKeyDown = (e: KeyboardEvent): void => { - if (isNavigating) { - if (shouldDismiss(e)) { - scheduleDismiss(); - } - } else if (shouldTrigger(e)) { + if (!isNavigating && shouldTrigger(e)) { setNavigating(true); } }; @@ -243,10 +179,6 @@ function createKeyborgCore( currentTargetWindow.clearTimeout(isMouseOrTouchUsedTimer); isMouseOrTouchUsedTimer = undefined; } - if (dismissTimer) { - currentTargetWindow.clearTimeout(dismissTimer); - dismissTimer = undefined; - } disposeFocusEvent(currentTargetWindow); const targetDocument = currentTargetWindow.document; removeEventListener( @@ -281,7 +213,7 @@ function createKeyborgCore( }; } -export function createKeyborg(win: Window, props?: KeyborgProps): Keyborg { +export function createKeyborg(win: Window): Keyborg { const kwin = win as WindowWithKeyborg; const id = "k" + ++_lastId; let localWin: WindowWithKeyborg | undefined = kwin; @@ -295,7 +227,7 @@ export function createKeyborg(win: Window, props?: KeyborgProps): Keyborg { if (existing) { core = existing.core; } else { - core = createKeyborgCore(kwin, props); + core = createKeyborgCore(kwin); } const instance: KeyborgInternal = { From 640081641ebd8bf84b5e14ccc140ccf34f91629e Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Mon, 18 May 2026 12:17:51 +0200 Subject: [PATCH 3/3] perf: drop canOverrideNativeFocus runtime probe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `canOverrideNativeFocus` helper detected whether reassigning `HTMLElement.prototype.focus` was respected by the browser. The check was guarding against pre-IE9 behaviour where prototype overrides were ignored. Every browser keyborg currently supports honours the override, so the `_canOverrideNativeFocus` flag is effectively always true after the first call. Replace the conditional `details.isFocusedProgrammatically` write with an unconditional one — semantically identical when the override works, which is the only case we still ship for — and drop the probe entirely. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/FocusEvent.mts | 34 +++------------------------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/src/FocusEvent.mts b/src/FocusEvent.mts index 7da1e048f..455f3eb45 100644 --- a/src/FocusEvent.mts +++ b/src/FocusEvent.mts @@ -37,27 +37,6 @@ interface WindowWithKeyborgFocusEvent extends Window { __keyborgData?: KeyborgFocusEventData; } -function canOverrideNativeFocus(win: Window): boolean { - const HTMLElement = (win as WindowWithKeyborgFocusEvent).HTMLElement; - const origFocus = HTMLElement.prototype.focus; - - let isCustomFocusCalled = false; - - HTMLElement.prototype.focus = function focus(): void { - isCustomFocusCalled = true; - }; - - const btn = win.document.createElement("button"); - - btn.focus(); - - HTMLElement.prototype.focus = origFocus; - - return isCustomFocusCalled; -} - -let _canOverrideNativeFocus = false; - export interface KeyborgFocusInEventDetails { relatedTarget?: HTMLElement; isFocusedProgrammatically?: boolean; @@ -92,11 +71,6 @@ export function setupFocusEvent(win: Window): void { const kwin = win as WindowWithKeyborgFocusEvent; const doc = kwin.document; const proto = kwin.HTMLElement.prototype; - - if (!_canOverrideNativeFocus) { - _canOverrideNativeFocus = canOverrideNativeFocus(kwin); - } - const origFocus = proto.focus; if ((origFocus as KeyborgFocus).__keyborgNativeFocus) { @@ -250,12 +224,10 @@ export function setupFocusEvent(win: Window): void { detail: details, }); - if (_canOverrideNativeFocus || data[LAST_FOCUSED_PROGRAMMATICALLY]) { - details.isFocusedProgrammatically = - target === data[LAST_FOCUSED_PROGRAMMATICALLY]?.deref(); + details.isFocusedProgrammatically = + target === data[LAST_FOCUSED_PROGRAMMATICALLY]?.deref(); - data[LAST_FOCUSED_PROGRAMMATICALLY] = undefined; - } + data[LAST_FOCUSED_PROGRAMMATICALLY] = undefined; target.dispatchEvent(event); };