Skip to content
Closed
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
44 changes: 4 additions & 40 deletions src/FocusEvent.mts
Original file line number Diff line number Diff line change
Expand Up @@ -37,39 +37,13 @@ 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;
originalEvent?: FocusEvent;
}

export interface KeyborgFocusInEvent extends CustomEvent<KeyborgFocusInEventDetails> {
/**
* @deprecated - used `event.detail`
*/
details?: KeyborgFocusInEventDetails;
}
export type KeyborgFocusInEvent = CustomEvent<KeyborgFocusInEventDetails>;

export interface KeyborgFocusOutEventDetails {
originalEvent: FocusEvent;
Expand Down Expand Up @@ -97,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) {
Expand Down Expand Up @@ -255,15 +224,10 @@ 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;
details.isFocusedProgrammatically =
target === data[LAST_FOCUSED_PROGRAMMATICALLY]?.deref();

if (_canOverrideNativeFocus || data[LAST_FOCUSED_PROGRAMMATICALLY]) {
details.isFocusedProgrammatically =
target === data[LAST_FOCUSED_PROGRAMMATICALLY]?.deref();

data[LAST_FOCUSED_PROGRAMMATICALLY] = undefined;
}
data[LAST_FOCUSED_PROGRAMMATICALLY] = undefined;

target.dispatchEvent(event);
};
Expand Down
78 changes: 5 additions & 73 deletions src/Keyborg.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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<number> | undefined;
let dismissKeys: Set<number> | 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;
Expand All @@ -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 => {
Expand Down Expand Up @@ -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);
}
};
Expand All @@ -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(
Expand Down Expand Up @@ -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;
Expand All @@ -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 = {
Expand Down