474 lines
14 KiB
JavaScript
474 lines
14 KiB
JavaScript
|
// src/WeakRefInstance.ts
|
||
|
var _canUseWeakRef = typeof WeakRef !== "undefined";
|
||
|
var WeakRefInstance = class {
|
||
|
constructor(instance) {
|
||
|
if (_canUseWeakRef && typeof instance === "object") {
|
||
|
this._weakRef = new WeakRef(instance);
|
||
|
} else {
|
||
|
this._instance = instance;
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef/deref}
|
||
|
*/
|
||
|
deref() {
|
||
|
var _a, _b;
|
||
|
let instance;
|
||
|
if (this._weakRef) {
|
||
|
instance = (_a = this._weakRef) == null ? void 0 : _a.deref();
|
||
|
if (!instance) {
|
||
|
delete this._weakRef;
|
||
|
}
|
||
|
} else {
|
||
|
instance = this._instance;
|
||
|
if ((_b = instance == null ? void 0 : instance.isDisposed) == null ? void 0 : _b.call(instance)) {
|
||
|
delete this._instance;
|
||
|
}
|
||
|
}
|
||
|
return instance;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// src/FocusEvent.ts
|
||
|
var KEYBORG_FOCUSIN = "keyborg:focusin";
|
||
|
var KEYBORG_FOCUSOUT = "keyborg:focusout";
|
||
|
function canOverrideNativeFocus(win) {
|
||
|
const HTMLElement = win.HTMLElement;
|
||
|
const origFocus = HTMLElement.prototype.focus;
|
||
|
let isCustomFocusCalled = false;
|
||
|
HTMLElement.prototype.focus = function focus() {
|
||
|
isCustomFocusCalled = true;
|
||
|
};
|
||
|
const btn = win.document.createElement("button");
|
||
|
btn.focus();
|
||
|
HTMLElement.prototype.focus = origFocus;
|
||
|
return isCustomFocusCalled;
|
||
|
}
|
||
|
var _canOverrideNativeFocus = false;
|
||
|
function nativeFocus(element) {
|
||
|
const focus = element.focus;
|
||
|
if (focus.__keyborgNativeFocus) {
|
||
|
focus.__keyborgNativeFocus.call(element);
|
||
|
} else {
|
||
|
element.focus();
|
||
|
}
|
||
|
}
|
||
|
function setupFocusEvent(win) {
|
||
|
const kwin = win;
|
||
|
if (!_canOverrideNativeFocus) {
|
||
|
_canOverrideNativeFocus = canOverrideNativeFocus(kwin);
|
||
|
}
|
||
|
const origFocus = kwin.HTMLElement.prototype.focus;
|
||
|
if (origFocus.__keyborgNativeFocus) {
|
||
|
return;
|
||
|
}
|
||
|
kwin.HTMLElement.prototype.focus = focus;
|
||
|
const shadowTargets = /* @__PURE__ */ new Set();
|
||
|
const focusOutHandler = (e) => {
|
||
|
const target = e.target;
|
||
|
if (!target) {
|
||
|
return;
|
||
|
}
|
||
|
const event = new CustomEvent(KEYBORG_FOCUSOUT, {
|
||
|
cancelable: true,
|
||
|
bubbles: true,
|
||
|
// Allows the event to bubble past an open shadow root
|
||
|
composed: true,
|
||
|
detail: {
|
||
|
originalEvent: e
|
||
|
}
|
||
|
});
|
||
|
target.dispatchEvent(event);
|
||
|
};
|
||
|
const focusInHandler = (e) => {
|
||
|
const target = e.target;
|
||
|
if (!target) {
|
||
|
return;
|
||
|
}
|
||
|
let node = e.composedPath()[0];
|
||
|
const currentShadows = /* @__PURE__ */ new Set();
|
||
|
while (node) {
|
||
|
if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
||
|
currentShadows.add(node);
|
||
|
node = node.host;
|
||
|
} else {
|
||
|
node = node.parentNode;
|
||
|
}
|
||
|
}
|
||
|
for (const shadowRootWeakRef of shadowTargets) {
|
||
|
const shadowRoot = shadowRootWeakRef.deref();
|
||
|
if (!shadowRoot || !currentShadows.has(shadowRoot)) {
|
||
|
shadowTargets.delete(shadowRootWeakRef);
|
||
|
if (shadowRoot) {
|
||
|
shadowRoot.removeEventListener("focusin", focusInHandler, true);
|
||
|
shadowRoot.removeEventListener("focusout", focusOutHandler, true);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
onFocusIn(target, e.relatedTarget || void 0);
|
||
|
};
|
||
|
const onFocusIn = (target, relatedTarget, originalEvent) => {
|
||
|
var _a;
|
||
|
const shadowRoot = target.shadowRoot;
|
||
|
if (shadowRoot) {
|
||
|
for (const shadowRootWeakRef of shadowTargets) {
|
||
|
if (shadowRootWeakRef.deref() === shadowRoot) {
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
shadowRoot.addEventListener("focusin", focusInHandler, true);
|
||
|
shadowRoot.addEventListener("focusout", focusOutHandler, true);
|
||
|
shadowTargets.add(new WeakRefInstance(shadowRoot));
|
||
|
return;
|
||
|
}
|
||
|
const details = {
|
||
|
relatedTarget,
|
||
|
originalEvent
|
||
|
};
|
||
|
const event = new CustomEvent(KEYBORG_FOCUSIN, {
|
||
|
cancelable: true,
|
||
|
bubbles: true,
|
||
|
// Allows the event to bubble past an open shadow root
|
||
|
composed: true,
|
||
|
detail: details
|
||
|
});
|
||
|
event.details = details;
|
||
|
if (_canOverrideNativeFocus || data.lastFocusedProgrammatically) {
|
||
|
details.isFocusedProgrammatically = target === ((_a = data.lastFocusedProgrammatically) == null ? void 0 : _a.deref());
|
||
|
data.lastFocusedProgrammatically = void 0;
|
||
|
}
|
||
|
target.dispatchEvent(event);
|
||
|
};
|
||
|
const data = kwin.__keyborgData = {
|
||
|
focusInHandler,
|
||
|
focusOutHandler,
|
||
|
shadowTargets
|
||
|
};
|
||
|
kwin.document.addEventListener(
|
||
|
"focusin",
|
||
|
kwin.__keyborgData.focusInHandler,
|
||
|
true
|
||
|
);
|
||
|
kwin.document.addEventListener(
|
||
|
"focusout",
|
||
|
kwin.__keyborgData.focusOutHandler,
|
||
|
true
|
||
|
);
|
||
|
function focus() {
|
||
|
const keyborgNativeFocusEvent = kwin.__keyborgData;
|
||
|
if (keyborgNativeFocusEvent) {
|
||
|
keyborgNativeFocusEvent.lastFocusedProgrammatically = new WeakRefInstance(
|
||
|
this
|
||
|
);
|
||
|
}
|
||
|
return origFocus.apply(this, arguments);
|
||
|
}
|
||
|
let activeElement = kwin.document.activeElement;
|
||
|
while (activeElement && activeElement.shadowRoot) {
|
||
|
onFocusIn(activeElement);
|
||
|
activeElement = activeElement.shadowRoot.activeElement;
|
||
|
}
|
||
|
focus.__keyborgNativeFocus = origFocus;
|
||
|
}
|
||
|
function disposeFocusEvent(win) {
|
||
|
const kwin = win;
|
||
|
const proto = kwin.HTMLElement.prototype;
|
||
|
const origFocus = proto.focus.__keyborgNativeFocus;
|
||
|
const keyborgNativeFocusEvent = kwin.__keyborgData;
|
||
|
if (keyborgNativeFocusEvent) {
|
||
|
kwin.document.removeEventListener(
|
||
|
"focusin",
|
||
|
keyborgNativeFocusEvent.focusInHandler,
|
||
|
true
|
||
|
);
|
||
|
kwin.document.removeEventListener(
|
||
|
"focusout",
|
||
|
keyborgNativeFocusEvent.focusOutHandler,
|
||
|
true
|
||
|
);
|
||
|
for (const shadowRootWeakRef of keyborgNativeFocusEvent.shadowTargets) {
|
||
|
const shadowRoot = shadowRootWeakRef.deref();
|
||
|
if (shadowRoot) {
|
||
|
shadowRoot.removeEventListener(
|
||
|
"focusin",
|
||
|
keyborgNativeFocusEvent.focusInHandler,
|
||
|
true
|
||
|
);
|
||
|
shadowRoot.removeEventListener(
|
||
|
"focusout",
|
||
|
keyborgNativeFocusEvent.focusOutHandler,
|
||
|
true
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
keyborgNativeFocusEvent.shadowTargets.clear();
|
||
|
delete kwin.__keyborgData;
|
||
|
}
|
||
|
if (origFocus) {
|
||
|
proto.focus = origFocus;
|
||
|
}
|
||
|
}
|
||
|
function getLastFocusedProgrammatically(win) {
|
||
|
var _a;
|
||
|
const keyborgNativeFocusEvent = win.__keyborgData;
|
||
|
return keyborgNativeFocusEvent ? ((_a = keyborgNativeFocusEvent.lastFocusedProgrammatically) == null ? void 0 : _a.deref()) || null : void 0;
|
||
|
}
|
||
|
|
||
|
// src/Keyborg.ts
|
||
|
var _dismissTimeout = 500;
|
||
|
var _lastId = 0;
|
||
|
var KeyborgCore = class {
|
||
|
constructor(win, props) {
|
||
|
this._isNavigatingWithKeyboard_DO_NOT_USE = false;
|
||
|
this._onFocusIn = (e) => {
|
||
|
if (this._isMouseOrTouchUsedTimer) {
|
||
|
return;
|
||
|
}
|
||
|
if (this.isNavigatingWithKeyboard) {
|
||
|
return;
|
||
|
}
|
||
|
const details = e.detail;
|
||
|
if (!details.relatedTarget) {
|
||
|
return;
|
||
|
}
|
||
|
if (details.isFocusedProgrammatically || details.isFocusedProgrammatically === void 0) {
|
||
|
return;
|
||
|
}
|
||
|
this.isNavigatingWithKeyboard = true;
|
||
|
};
|
||
|
this._onMouseDown = (e) => {
|
||
|
if (e.buttons === 0 || e.clientX === 0 && e.clientY === 0 && e.screenX === 0 && e.screenY === 0) {
|
||
|
return;
|
||
|
}
|
||
|
this._onMouseOrTouch();
|
||
|
};
|
||
|
this._onMouseOrTouch = () => {
|
||
|
const win = this._win;
|
||
|
if (win) {
|
||
|
if (this._isMouseOrTouchUsedTimer) {
|
||
|
win.clearTimeout(this._isMouseOrTouchUsedTimer);
|
||
|
}
|
||
|
this._isMouseOrTouchUsedTimer = win.setTimeout(() => {
|
||
|
delete this._isMouseOrTouchUsedTimer;
|
||
|
}, 1e3);
|
||
|
}
|
||
|
this.isNavigatingWithKeyboard = false;
|
||
|
};
|
||
|
this._onKeyDown = (e) => {
|
||
|
const isNavigatingWithKeyboard = this.isNavigatingWithKeyboard;
|
||
|
if (isNavigatingWithKeyboard) {
|
||
|
if (this._shouldDismissKeyboardNavigation(e)) {
|
||
|
this._scheduleDismiss();
|
||
|
}
|
||
|
} else {
|
||
|
if (this._shouldTriggerKeyboardNavigation(e)) {
|
||
|
this.isNavigatingWithKeyboard = true;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
this.id = "c" + ++_lastId;
|
||
|
this._win = win;
|
||
|
const doc = win.document;
|
||
|
if (props) {
|
||
|
const triggerKeys = props.triggerKeys;
|
||
|
const dismissKeys = props.dismissKeys;
|
||
|
if (triggerKeys == null ? void 0 : triggerKeys.length) {
|
||
|
this._triggerKeys = new Set(triggerKeys);
|
||
|
}
|
||
|
if (dismissKeys == null ? void 0 : dismissKeys.length) {
|
||
|
this._dismissKeys = new Set(dismissKeys);
|
||
|
}
|
||
|
}
|
||
|
doc.addEventListener(KEYBORG_FOCUSIN, this._onFocusIn, true);
|
||
|
doc.addEventListener("mousedown", this._onMouseDown, true);
|
||
|
win.addEventListener("keydown", this._onKeyDown, true);
|
||
|
doc.addEventListener("touchstart", this._onMouseOrTouch, true);
|
||
|
doc.addEventListener("touchend", this._onMouseOrTouch, true);
|
||
|
doc.addEventListener("touchcancel", this._onMouseOrTouch, true);
|
||
|
setupFocusEvent(win);
|
||
|
}
|
||
|
get isNavigatingWithKeyboard() {
|
||
|
return this._isNavigatingWithKeyboard_DO_NOT_USE;
|
||
|
}
|
||
|
set isNavigatingWithKeyboard(val) {
|
||
|
if (this._isNavigatingWithKeyboard_DO_NOT_USE !== val) {
|
||
|
this._isNavigatingWithKeyboard_DO_NOT_USE = val;
|
||
|
this.update();
|
||
|
}
|
||
|
}
|
||
|
dispose() {
|
||
|
const win = this._win;
|
||
|
if (win) {
|
||
|
if (this._isMouseOrTouchUsedTimer) {
|
||
|
win.clearTimeout(this._isMouseOrTouchUsedTimer);
|
||
|
this._isMouseOrTouchUsedTimer = void 0;
|
||
|
}
|
||
|
if (this._dismissTimer) {
|
||
|
win.clearTimeout(this._dismissTimer);
|
||
|
this._dismissTimer = void 0;
|
||
|
}
|
||
|
disposeFocusEvent(win);
|
||
|
const doc = win.document;
|
||
|
doc.removeEventListener(KEYBORG_FOCUSIN, this._onFocusIn, true);
|
||
|
doc.removeEventListener("mousedown", this._onMouseDown, true);
|
||
|
win.removeEventListener("keydown", this._onKeyDown, true);
|
||
|
doc.removeEventListener("touchstart", this._onMouseOrTouch, true);
|
||
|
doc.removeEventListener("touchend", this._onMouseOrTouch, true);
|
||
|
doc.removeEventListener("touchcancel", this._onMouseOrTouch, true);
|
||
|
delete this._win;
|
||
|
}
|
||
|
}
|
||
|
isDisposed() {
|
||
|
return !!this._win;
|
||
|
}
|
||
|
/**
|
||
|
* Updates all keyborg instances with the keyboard navigation state
|
||
|
*/
|
||
|
update() {
|
||
|
var _a, _b;
|
||
|
const keyborgs = (_b = (_a = this._win) == null ? void 0 : _a.__keyborg) == null ? void 0 : _b.refs;
|
||
|
if (keyborgs) {
|
||
|
for (const id of Object.keys(keyborgs)) {
|
||
|
Keyborg.update(keyborgs[id], this.isNavigatingWithKeyboard);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* @returns whether the keyboard event should trigger keyboard navigation mode
|
||
|
*/
|
||
|
_shouldTriggerKeyboardNavigation(e) {
|
||
|
var _a;
|
||
|
if (e.key === "Tab") {
|
||
|
return true;
|
||
|
}
|
||
|
const activeElement = (_a = this._win) == null ? void 0 : _a.document.activeElement;
|
||
|
const isTriggerKey = !this._triggerKeys || this._triggerKeys.has(e.keyCode);
|
||
|
const isEditable = activeElement && (activeElement.tagName === "INPUT" || activeElement.tagName === "TEXTAREA" || activeElement.isContentEditable);
|
||
|
return isTriggerKey && !isEditable;
|
||
|
}
|
||
|
/**
|
||
|
* @returns whether the keyboard event should dismiss keyboard navigation mode
|
||
|
*/
|
||
|
_shouldDismissKeyboardNavigation(e) {
|
||
|
var _a;
|
||
|
return (_a = this._dismissKeys) == null ? void 0 : _a.has(e.keyCode);
|
||
|
}
|
||
|
_scheduleDismiss() {
|
||
|
const win = this._win;
|
||
|
if (win) {
|
||
|
if (this._dismissTimer) {
|
||
|
win.clearTimeout(this._dismissTimer);
|
||
|
this._dismissTimer = void 0;
|
||
|
}
|
||
|
const was = win.document.activeElement;
|
||
|
this._dismissTimer = win.setTimeout(() => {
|
||
|
this._dismissTimer = void 0;
|
||
|
const cur = win.document.activeElement;
|
||
|
if (was && cur && was === cur) {
|
||
|
this.isNavigatingWithKeyboard = false;
|
||
|
}
|
||
|
}, _dismissTimeout);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
var Keyborg = class _Keyborg {
|
||
|
constructor(win, props) {
|
||
|
this._cb = [];
|
||
|
this._id = "k" + ++_lastId;
|
||
|
this._win = win;
|
||
|
const current = win.__keyborg;
|
||
|
if (current) {
|
||
|
this._core = current.core;
|
||
|
current.refs[this._id] = this;
|
||
|
} else {
|
||
|
this._core = new KeyborgCore(win, props);
|
||
|
win.__keyborg = {
|
||
|
core: this._core,
|
||
|
refs: { [this._id]: this }
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
static create(win, props) {
|
||
|
return new _Keyborg(win, props);
|
||
|
}
|
||
|
static dispose(instance) {
|
||
|
instance.dispose();
|
||
|
}
|
||
|
/**
|
||
|
* Updates all subscribed callbacks with the keyboard navigation state
|
||
|
*/
|
||
|
static update(instance, isNavigatingWithKeyboard) {
|
||
|
instance._cb.forEach((callback) => callback(isNavigatingWithKeyboard));
|
||
|
}
|
||
|
dispose() {
|
||
|
var _a;
|
||
|
const current = (_a = this._win) == null ? void 0 : _a.__keyborg;
|
||
|
if (current == null ? void 0 : current.refs[this._id]) {
|
||
|
delete current.refs[this._id];
|
||
|
if (Object.keys(current.refs).length === 0) {
|
||
|
current.core.dispose();
|
||
|
delete this._win.__keyborg;
|
||
|
}
|
||
|
} else if (process.env.NODE_ENV !== "production") {
|
||
|
console.error(
|
||
|
`Keyborg instance ${this._id} is being disposed incorrectly.`
|
||
|
);
|
||
|
}
|
||
|
this._cb = [];
|
||
|
delete this._core;
|
||
|
delete this._win;
|
||
|
}
|
||
|
/**
|
||
|
* @returns Whether the user is navigating with keyboard
|
||
|
*/
|
||
|
isNavigatingWithKeyboard() {
|
||
|
var _a;
|
||
|
return !!((_a = this._core) == null ? void 0 : _a.isNavigatingWithKeyboard);
|
||
|
}
|
||
|
/**
|
||
|
* @param callback - Called when the keyboard navigation state changes
|
||
|
*/
|
||
|
subscribe(callback) {
|
||
|
this._cb.push(callback);
|
||
|
}
|
||
|
/**
|
||
|
* @param callback - Registered with subscribe
|
||
|
*/
|
||
|
unsubscribe(callback) {
|
||
|
const index = this._cb.indexOf(callback);
|
||
|
if (index >= 0) {
|
||
|
this._cb.splice(index, 1);
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Manually set the keyboard navigtion state
|
||
|
*/
|
||
|
setVal(isNavigatingWithKeyboard) {
|
||
|
if (this._core) {
|
||
|
this._core.isNavigatingWithKeyboard = isNavigatingWithKeyboard;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
function createKeyborg(win, props) {
|
||
|
return Keyborg.create(win, props);
|
||
|
}
|
||
|
function disposeKeyborg(instance) {
|
||
|
Keyborg.dispose(instance);
|
||
|
}
|
||
|
|
||
|
// src/index.ts
|
||
|
var version = "2.6.0";
|
||
|
export {
|
||
|
KEYBORG_FOCUSIN,
|
||
|
KEYBORG_FOCUSOUT,
|
||
|
createKeyborg,
|
||
|
disposeKeyborg,
|
||
|
getLastFocusedProgrammatically,
|
||
|
nativeFocus,
|
||
|
version
|
||
|
};
|
||
|
/*!
|
||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||
|
* Licensed under the MIT License.
|
||
|
*/
|
||
|
//# sourceMappingURL=index.js.map
|