10364 lines
290 KiB
JavaScript
10364 lines
290 KiB
JavaScript
import { nativeFocus, KEYBORG_FOCUSIN, KEYBORG_FOCUSOUT, createKeyborg, disposeKeyborg } from 'keyborg';
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
const TABSTER_ATTRIBUTE_NAME = "data-tabster";
|
|
const TABSTER_DUMMY_INPUT_ATTRIBUTE_NAME = "data-tabster-dummy";
|
|
const FOCUSABLE_SELECTOR = /*#__PURE__*/["a[href]", "button:not([disabled])", "input:not([disabled])", "select:not([disabled])", "textarea:not([disabled])", "*[tabindex]", "*[contenteditable]", "details > summary", "audio[controls]", "video[controls]"].join(", ");
|
|
const AsyncFocusSources = {
|
|
EscapeGroupper: 1,
|
|
Restorer: 2,
|
|
Deloser: 3
|
|
};
|
|
const ObservedElementAccessibilities = {
|
|
Any: 0,
|
|
Accessible: 1,
|
|
Focusable: 2
|
|
};
|
|
const RestoreFocusOrders = {
|
|
History: 0,
|
|
DeloserDefault: 1,
|
|
RootDefault: 2,
|
|
DeloserFirst: 3,
|
|
RootFirst: 4
|
|
};
|
|
const DeloserStrategies = {
|
|
/**
|
|
* If the focus is lost, the focus will be restored automatically using all available focus history.
|
|
* This is the default strategy.
|
|
*/
|
|
Auto: 0,
|
|
|
|
/**
|
|
* If the focus is lost from this Deloser instance, the focus will not be restored automatically.
|
|
* The application might listen to the event and restore the focus manually.
|
|
* But if it is lost from another Deloser instance, the history of this Deloser could be used finding
|
|
* the element to focus.
|
|
*/
|
|
Manual: 1
|
|
};
|
|
const Visibilities = {
|
|
Invisible: 0,
|
|
PartiallyVisible: 1,
|
|
Visible: 2
|
|
};
|
|
const RestorerTypes = {
|
|
Source: 0,
|
|
Target: 1
|
|
};
|
|
const MoverDirections = {
|
|
Both: 0,
|
|
Vertical: 1,
|
|
Horizontal: 2,
|
|
Grid: 3,
|
|
GridLinear: 4 // Two-dimentional movement depending on the visual placement. Allows linear movement.
|
|
|
|
};
|
|
const MoverKeys = {
|
|
ArrowUp: 1,
|
|
ArrowDown: 2,
|
|
ArrowLeft: 3,
|
|
ArrowRight: 4,
|
|
PageUp: 5,
|
|
PageDown: 6,
|
|
Home: 7,
|
|
End: 8
|
|
};
|
|
const GroupperTabbabilities = {
|
|
Unlimited: 0,
|
|
Limited: 1,
|
|
LimitedTrapFocus: 2 // The focus is limited as above, plus trapped when inside.
|
|
|
|
};
|
|
const GroupperMoveFocusActions = {
|
|
Enter: 1,
|
|
Escape: 2
|
|
};
|
|
const SysDummyInputsPositions = {
|
|
Auto: 0,
|
|
Inside: 1,
|
|
Outside: 2 // Tabster will always place dummy inputs outside of the container.
|
|
|
|
};
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
function getTabsterOnElement(tabster, element) {
|
|
var _a;
|
|
|
|
return (_a = tabster.storageEntry(element)) === null || _a === void 0 ? void 0 : _a.tabster;
|
|
}
|
|
function updateTabsterByAttribute(tabster, element, dispose) {
|
|
var _a, _b;
|
|
|
|
const newAttrValue = dispose || tabster._noop ? undefined : element.getAttribute(TABSTER_ATTRIBUTE_NAME);
|
|
let entry = tabster.storageEntry(element);
|
|
let newAttr;
|
|
|
|
if (newAttrValue) {
|
|
if (newAttrValue !== ((_a = entry === null || entry === void 0 ? void 0 : entry.attr) === null || _a === void 0 ? void 0 : _a.string)) {
|
|
try {
|
|
const newValue = JSON.parse(newAttrValue);
|
|
|
|
if (typeof newValue !== "object") {
|
|
throw new Error(`Value is not a JSON object, got '${newAttrValue}'.`);
|
|
}
|
|
|
|
newAttr = {
|
|
string: newAttrValue,
|
|
object: newValue
|
|
};
|
|
} catch (e) {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.error(`data-tabster attribute error: ${e}`, element);
|
|
}
|
|
}
|
|
} else {
|
|
return;
|
|
}
|
|
} else if (!entry) {
|
|
return;
|
|
}
|
|
|
|
if (!entry) {
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
entry = tabster.storageEntry(element, true);
|
|
}
|
|
|
|
if (!entry.tabster) {
|
|
entry.tabster = {};
|
|
}
|
|
|
|
const tabsterOnElement = entry.tabster || {};
|
|
const oldTabsterProps = ((_b = entry.attr) === null || _b === void 0 ? void 0 : _b.object) || {};
|
|
const newTabsterProps = (newAttr === null || newAttr === void 0 ? void 0 : newAttr.object) || {};
|
|
|
|
for (const key of Object.keys(oldTabsterProps)) {
|
|
if (!newTabsterProps[key]) {
|
|
if (key === "root") {
|
|
const root = tabsterOnElement[key];
|
|
|
|
if (root) {
|
|
tabster.root.onRoot(root, true);
|
|
}
|
|
}
|
|
|
|
switch (key) {
|
|
case "deloser":
|
|
case "root":
|
|
case "groupper":
|
|
case "modalizer":
|
|
case "restorer":
|
|
case "mover":
|
|
// eslint-disable-next-line no-case-declarations
|
|
const part = tabsterOnElement[key];
|
|
|
|
if (part) {
|
|
part.dispose();
|
|
delete tabsterOnElement[key];
|
|
}
|
|
|
|
break;
|
|
|
|
case "observed":
|
|
delete tabsterOnElement[key];
|
|
|
|
if (tabster.observedElement) {
|
|
tabster.observedElement.onObservedElementUpdate(element);
|
|
}
|
|
|
|
break;
|
|
|
|
case "focusable":
|
|
case "outline":
|
|
case "uncontrolled":
|
|
case "sys":
|
|
delete tabsterOnElement[key];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const key of Object.keys(newTabsterProps)) {
|
|
const sys = newTabsterProps.sys;
|
|
|
|
switch (key) {
|
|
case "deloser":
|
|
if (tabsterOnElement.deloser) {
|
|
tabsterOnElement.deloser.setProps(newTabsterProps.deloser);
|
|
} else {
|
|
if (tabster.deloser) {
|
|
tabsterOnElement.deloser = tabster.deloser.createDeloser(element, newTabsterProps.deloser);
|
|
} else if (process.env.NODE_ENV === 'development') {
|
|
console.error("Deloser API used before initialization, please call `getDeloser()`");
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case "root":
|
|
if (tabsterOnElement.root) {
|
|
tabsterOnElement.root.setProps(newTabsterProps.root);
|
|
} else {
|
|
tabsterOnElement.root = tabster.root.createRoot(element, newTabsterProps.root, sys);
|
|
}
|
|
|
|
tabster.root.onRoot(tabsterOnElement.root);
|
|
break;
|
|
|
|
case "modalizer":
|
|
if (tabsterOnElement.modalizer) {
|
|
tabsterOnElement.modalizer.setProps(newTabsterProps.modalizer);
|
|
} else {
|
|
if (tabster.modalizer) {
|
|
tabsterOnElement.modalizer = tabster.modalizer.createModalizer(element, newTabsterProps.modalizer, sys);
|
|
} else if (process.env.NODE_ENV === 'development') {
|
|
console.error("Modalizer API used before initialization, please call `getModalizer()`");
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case "restorer":
|
|
if (tabsterOnElement.restorer) {
|
|
tabsterOnElement.restorer.setProps(newTabsterProps.restorer);
|
|
} else {
|
|
if (tabster.restorer) {
|
|
if (newTabsterProps.restorer) {
|
|
tabsterOnElement.restorer = tabster.restorer.createRestorer(element, newTabsterProps.restorer);
|
|
}
|
|
} else if (process.env.NODE_ENV === 'development') {
|
|
console.error("Restorer API used before initialization, please call `getRestorer()`");
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case "focusable":
|
|
tabsterOnElement.focusable = newTabsterProps.focusable;
|
|
break;
|
|
|
|
case "groupper":
|
|
if (tabsterOnElement.groupper) {
|
|
tabsterOnElement.groupper.setProps(newTabsterProps.groupper);
|
|
} else {
|
|
if (tabster.groupper) {
|
|
tabsterOnElement.groupper = tabster.groupper.createGroupper(element, newTabsterProps.groupper, sys);
|
|
} else if (process.env.NODE_ENV === 'development') {
|
|
console.error("Groupper API used before initialization, please call `getGroupper()`");
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case "mover":
|
|
if (tabsterOnElement.mover) {
|
|
tabsterOnElement.mover.setProps(newTabsterProps.mover);
|
|
} else {
|
|
if (tabster.mover) {
|
|
tabsterOnElement.mover = tabster.mover.createMover(element, newTabsterProps.mover, sys);
|
|
} else if (process.env.NODE_ENV === 'development') {
|
|
console.error("Mover API used before initialization, please call `getMover()`");
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case "observed":
|
|
if (tabster.observedElement) {
|
|
tabsterOnElement.observed = newTabsterProps.observed;
|
|
tabster.observedElement.onObservedElementUpdate(element);
|
|
} else if (process.env.NODE_ENV === 'development') {
|
|
console.error("ObservedElement API used before initialization, please call `getObservedElement()`");
|
|
}
|
|
|
|
break;
|
|
|
|
case "uncontrolled":
|
|
tabsterOnElement.uncontrolled = newTabsterProps.uncontrolled;
|
|
break;
|
|
|
|
case "outline":
|
|
if (tabster.outline) {
|
|
tabsterOnElement.outline = newTabsterProps.outline;
|
|
} else if (process.env.NODE_ENV === 'development') {
|
|
console.error("Outline API used before initialization, please call `getOutline()`");
|
|
}
|
|
|
|
break;
|
|
|
|
case "sys":
|
|
tabsterOnElement.sys = newTabsterProps.sys;
|
|
break;
|
|
|
|
default:
|
|
console.error(`Unknown key '${key}' in data-tabster attribute value.`);
|
|
}
|
|
}
|
|
|
|
if (newAttr) {
|
|
entry.attr = newAttr;
|
|
} else {
|
|
if (Object.keys(tabsterOnElement).length === 0) {
|
|
delete entry.tabster;
|
|
delete entry.attr;
|
|
}
|
|
|
|
tabster.storageEntry(element, false);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
|
|
/**
|
|
* Events sent by Tabster.
|
|
*/
|
|
const TabsterFocusInEventName = "tabster:focusin";
|
|
const TabsterFocusOutEventName = "tabster:focusout"; // Event is dispatched when Tabster wants to move focus as the result of
|
|
// handling keyboard event. This allows to preventDefault() if you want to have
|
|
// some custom logic.
|
|
|
|
const TabsterMoveFocusEventName = "tabster:movefocus";
|
|
/**
|
|
* Events sent by Deloser.
|
|
*/
|
|
|
|
const DeloserFocusLostEventName = "tabster:deloser:focus-lost";
|
|
/**
|
|
* Events to be sent to Deloser by the application.
|
|
*/
|
|
|
|
const DeloserRestoreFocusEventName = "tabster:deloser:restore-focus";
|
|
/**
|
|
* Events sent by Modalizer.
|
|
*/
|
|
|
|
const ModalizerActiveEventName = "tabster:modalizer:active";
|
|
const ModalizerInactiveEventName = "tabster:modalizer:inactive";
|
|
const ModalizerFocusInEventName = "tabster:modalizer:focusin";
|
|
const ModalizerFocusOutEventName = "tabster:modalizer:focusout";
|
|
/**
|
|
* Events sent by Mover.
|
|
*/
|
|
|
|
const MoverStateEventName = "tabster:mover:state";
|
|
/**
|
|
* Events to be sent to Mover by the application.
|
|
*/
|
|
// Event that can be dispatched by the application to programmatically move
|
|
// focus inside Mover.
|
|
|
|
const MoverMoveFocusEventName = "tabster:mover:movefocus"; // Event that can be dispatched by the application to forget or modify
|
|
// memorized element in Mover with memorizeCurrent property.
|
|
|
|
const MoverMemorizedElementEventName = "tabster:mover:memorized-element";
|
|
/**
|
|
* Events sent by Groupper.
|
|
*/
|
|
|
|
/**
|
|
* Events to be sent to Groupper by the application.
|
|
*/
|
|
// Event that can be dispatched by the application to programmatically enter
|
|
// or escape Groupper.
|
|
|
|
const GroupperMoveFocusEventName = "tabster:groupper:movefocus";
|
|
/**
|
|
* Events sent by Restorer.
|
|
*/
|
|
|
|
const RestorerRestoreFocusEventName = "tabster:restorer:restore-focus";
|
|
/**
|
|
* Events sent by Root.
|
|
*/
|
|
|
|
const RootFocusEventName = "tabster:root:focus";
|
|
const RootBlurEventName = "tabster:root:blur"; // Node.js environments do not have CustomEvent and it is needed for the events to be
|
|
// evaluated. It doesn't matter if it works or not in Node.js environment.
|
|
// So, we just need to make sure that it doesn't throw undefined reference.
|
|
|
|
const CustomEvent_ = typeof CustomEvent !== "undefined" ? CustomEvent : function () {
|
|
/* no-op */
|
|
};
|
|
class TabsterCustomEvent extends CustomEvent_ {
|
|
constructor(type, detail) {
|
|
super(type, {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
composed: true,
|
|
detail
|
|
});
|
|
this.details = detail;
|
|
}
|
|
|
|
}
|
|
class TabsterFocusInEvent extends TabsterCustomEvent {
|
|
constructor(detail) {
|
|
super(TabsterFocusInEventName, detail);
|
|
}
|
|
|
|
}
|
|
class TabsterFocusOutEvent extends TabsterCustomEvent {
|
|
constructor(detail) {
|
|
super(TabsterFocusOutEventName, detail);
|
|
}
|
|
|
|
}
|
|
class TabsterMoveFocusEvent extends TabsterCustomEvent {
|
|
constructor(detail) {
|
|
super(TabsterMoveFocusEventName, detail);
|
|
}
|
|
|
|
}
|
|
class MoverStateEvent extends TabsterCustomEvent {
|
|
constructor(detail) {
|
|
super(MoverStateEventName, detail);
|
|
}
|
|
|
|
}
|
|
class MoverMoveFocusEvent extends TabsterCustomEvent {
|
|
constructor(detail) {
|
|
super(MoverMoveFocusEventName, detail);
|
|
}
|
|
|
|
}
|
|
class MoverMemorizedElementEvent extends TabsterCustomEvent {
|
|
constructor(detail) {
|
|
super(MoverMemorizedElementEventName, detail);
|
|
}
|
|
|
|
}
|
|
class GroupperMoveFocusEvent extends TabsterCustomEvent {
|
|
constructor(detail) {
|
|
super(GroupperMoveFocusEventName, detail);
|
|
}
|
|
|
|
}
|
|
class ModalizerActiveEvent extends TabsterCustomEvent {
|
|
constructor(detail) {
|
|
super(ModalizerActiveEventName, detail);
|
|
}
|
|
|
|
}
|
|
class ModalizerInactiveEvent extends TabsterCustomEvent {
|
|
constructor(detail) {
|
|
super(ModalizerInactiveEventName, detail);
|
|
}
|
|
|
|
}
|
|
class DeloserFocusLostEvent extends TabsterCustomEvent {
|
|
constructor(detail) {
|
|
super(DeloserFocusLostEventName, detail);
|
|
}
|
|
|
|
}
|
|
class DeloserRestoreFocusEvent extends TabsterCustomEvent {
|
|
constructor() {
|
|
super(DeloserRestoreFocusEventName);
|
|
}
|
|
|
|
}
|
|
class RestorerRestoreFocusEvent extends TabsterCustomEvent {
|
|
constructor() {
|
|
super(RestorerRestoreFocusEventName);
|
|
}
|
|
|
|
}
|
|
class RootFocusEvent extends TabsterCustomEvent {
|
|
constructor(detail) {
|
|
super(RootFocusEventName, detail);
|
|
}
|
|
|
|
}
|
|
class RootBlurEvent extends TabsterCustomEvent {
|
|
constructor(detail) {
|
|
super(RootBlurEventName, detail);
|
|
}
|
|
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
const _createMutationObserver = callback => new MutationObserver(callback);
|
|
|
|
const _createTreeWalker = (doc, root, whatToShow, filter) => doc.createTreeWalker(root, whatToShow, filter);
|
|
|
|
const _getParentNode = node => node ? node.parentNode : null;
|
|
|
|
const _getParentElement = element => element ? element.parentElement : null;
|
|
|
|
const _nodeContains = (parent, child) => !!(child && (parent === null || parent === void 0 ? void 0 : parent.contains(child)));
|
|
|
|
const _getActiveElement = doc => doc.activeElement;
|
|
|
|
const _querySelector = (element, selector) => element.querySelector(selector);
|
|
|
|
const _querySelectorAll = (element, selector) => Array.prototype.slice.call(element.querySelectorAll(selector), 0);
|
|
|
|
const _getElementById = (doc, id) => doc.getElementById(id);
|
|
|
|
const _getFirstChild = node => (node === null || node === void 0 ? void 0 : node.firstChild) || null;
|
|
|
|
const _getLastChild = node => (node === null || node === void 0 ? void 0 : node.lastChild) || null;
|
|
|
|
const _getNextSibling = node => (node === null || node === void 0 ? void 0 : node.nextSibling) || null;
|
|
|
|
const _getPreviousSibling = node => (node === null || node === void 0 ? void 0 : node.previousSibling) || null;
|
|
|
|
const _getFirstElementChild = element => (element === null || element === void 0 ? void 0 : element.firstElementChild) || null;
|
|
|
|
const _getLastElementChild = element => (element === null || element === void 0 ? void 0 : element.lastElementChild) || null;
|
|
|
|
const _getNextElementSibling = element => (element === null || element === void 0 ? void 0 : element.nextElementSibling) || null;
|
|
|
|
const _getPreviousElementSibling = element => (element === null || element === void 0 ? void 0 : element.previousElementSibling) || null;
|
|
|
|
const _appendChild = (parent, child) => parent.appendChild(child);
|
|
|
|
const _insertBefore = (parent, child, referenceChild) => parent.insertBefore(child, referenceChild);
|
|
|
|
const _getSelection = ref => {
|
|
var _a;
|
|
|
|
return ((_a = ref.ownerDocument) === null || _a === void 0 ? void 0 : _a.getSelection()) || null;
|
|
};
|
|
|
|
const _getElementsByName = (referenceElement, name) => referenceElement.ownerDocument.getElementsByName(name);
|
|
|
|
const dom = {
|
|
createMutationObserver: _createMutationObserver,
|
|
createTreeWalker: _createTreeWalker,
|
|
getParentNode: _getParentNode,
|
|
getParentElement: _getParentElement,
|
|
nodeContains: _nodeContains,
|
|
getActiveElement: _getActiveElement,
|
|
querySelector: _querySelector,
|
|
querySelectorAll: _querySelectorAll,
|
|
getElementById: _getElementById,
|
|
getFirstChild: _getFirstChild,
|
|
getLastChild: _getLastChild,
|
|
getNextSibling: _getNextSibling,
|
|
getPreviousSibling: _getPreviousSibling,
|
|
getFirstElementChild: _getFirstElementChild,
|
|
getLastElementChild: _getLastElementChild,
|
|
getNextElementSibling: _getNextElementSibling,
|
|
getPreviousElementSibling: _getPreviousElementSibling,
|
|
appendChild: _appendChild,
|
|
insertBefore: _insertBefore,
|
|
getSelection: _getSelection,
|
|
getElementsByName: _getElementsByName
|
|
};
|
|
function setDOMAPI(domapi) {
|
|
for (const key of Object.keys(domapi)) {
|
|
dom[key] = domapi[key];
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
|
|
let _isBrokenIE11;
|
|
|
|
const _DOMRect = typeof DOMRect !== "undefined" ? DOMRect : class {
|
|
constructor(x, y, width, height) {
|
|
this.left = x || 0;
|
|
this.top = y || 0;
|
|
this.right = (x || 0) + (width || 0);
|
|
this.bottom = (y || 0) + (height || 0);
|
|
}
|
|
|
|
};
|
|
|
|
let _uidCounter = 0;
|
|
|
|
try {
|
|
// IE11 only accepts `filter` argument as a function (not object with the `acceptNode`
|
|
// property as the docs define). Also `entityReferenceExpansion` argument is not
|
|
// optional. And it throws exception when the above arguments aren't there.
|
|
document.createTreeWalker(document, NodeFilter.SHOW_ELEMENT);
|
|
_isBrokenIE11 = false;
|
|
} catch (e) {
|
|
_isBrokenIE11 = true;
|
|
}
|
|
|
|
const _updateDummyInputsTimeout = 100;
|
|
function getInstanceContext(getWindow) {
|
|
const win = getWindow();
|
|
let ctx = win.__tabsterInstanceContext;
|
|
|
|
if (!ctx) {
|
|
ctx = {
|
|
elementByUId: {},
|
|
basics: {
|
|
Promise: win.Promise || undefined,
|
|
WeakRef: win.WeakRef || undefined
|
|
},
|
|
containerBoundingRectCache: {},
|
|
lastContainerBoundingRectCacheId: 0,
|
|
fakeWeakRefs: [],
|
|
fakeWeakRefsStarted: false
|
|
};
|
|
win.__tabsterInstanceContext = ctx;
|
|
}
|
|
|
|
return ctx;
|
|
}
|
|
function disposeInstanceContext(win) {
|
|
const ctx = win.__tabsterInstanceContext;
|
|
|
|
if (ctx) {
|
|
ctx.elementByUId = {};
|
|
delete ctx.WeakRef;
|
|
ctx.containerBoundingRectCache = {};
|
|
|
|
if (ctx.containerBoundingRectCacheTimer) {
|
|
win.clearTimeout(ctx.containerBoundingRectCacheTimer);
|
|
}
|
|
|
|
if (ctx.fakeWeakRefsTimer) {
|
|
win.clearTimeout(ctx.fakeWeakRefsTimer);
|
|
}
|
|
|
|
ctx.fakeWeakRefs = [];
|
|
delete win.__tabsterInstanceContext;
|
|
}
|
|
}
|
|
function createWeakMap(win) {
|
|
const ctx = win.__tabsterInstanceContext;
|
|
return new ((ctx === null || ctx === void 0 ? void 0 : ctx.basics.WeakMap) || WeakMap)();
|
|
}
|
|
function hasSubFocusable(element) {
|
|
return !!element.querySelector(FOCUSABLE_SELECTOR);
|
|
}
|
|
|
|
class FakeWeakRef {
|
|
constructor(target) {
|
|
this._target = target;
|
|
}
|
|
|
|
deref() {
|
|
return this._target;
|
|
}
|
|
|
|
static cleanup(fwr, forceRemove) {
|
|
if (!fwr._target) {
|
|
return true;
|
|
}
|
|
|
|
if (forceRemove || !documentContains(fwr._target.ownerDocument, fwr._target)) {
|
|
delete fwr._target;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
class WeakHTMLElement {
|
|
constructor(getWindow, element, data) {
|
|
const context = getInstanceContext(getWindow);
|
|
let ref;
|
|
|
|
if (context.WeakRef) {
|
|
ref = new context.WeakRef(element);
|
|
} else {
|
|
ref = new FakeWeakRef(element);
|
|
context.fakeWeakRefs.push(ref);
|
|
}
|
|
|
|
this._ref = ref;
|
|
this._data = data;
|
|
}
|
|
|
|
get() {
|
|
const ref = this._ref;
|
|
let element;
|
|
|
|
if (ref) {
|
|
element = ref.deref();
|
|
|
|
if (!element) {
|
|
delete this._ref;
|
|
}
|
|
}
|
|
|
|
return element;
|
|
}
|
|
|
|
getData() {
|
|
return this._data;
|
|
}
|
|
|
|
}
|
|
function cleanupFakeWeakRefs(getWindow, forceRemove) {
|
|
const context = getInstanceContext(getWindow);
|
|
context.fakeWeakRefs = context.fakeWeakRefs.filter(e => !FakeWeakRef.cleanup(e, forceRemove));
|
|
}
|
|
function startFakeWeakRefsCleanup(getWindow) {
|
|
const context = getInstanceContext(getWindow);
|
|
|
|
if (!context.fakeWeakRefsStarted) {
|
|
context.fakeWeakRefsStarted = true;
|
|
context.WeakRef = getWeakRef(context);
|
|
}
|
|
|
|
if (!context.fakeWeakRefsTimer) {
|
|
context.fakeWeakRefsTimer = getWindow().setTimeout(() => {
|
|
context.fakeWeakRefsTimer = undefined;
|
|
cleanupFakeWeakRefs(getWindow);
|
|
startFakeWeakRefsCleanup(getWindow);
|
|
}, 2 * 60 * 1000); // 2 minutes.
|
|
}
|
|
}
|
|
function stopFakeWeakRefsCleanupAndClearStorage(getWindow) {
|
|
const context = getInstanceContext(getWindow);
|
|
context.fakeWeakRefsStarted = false;
|
|
|
|
if (context.fakeWeakRefsTimer) {
|
|
getWindow().clearTimeout(context.fakeWeakRefsTimer);
|
|
context.fakeWeakRefsTimer = undefined;
|
|
context.fakeWeakRefs = [];
|
|
}
|
|
}
|
|
function createElementTreeWalker(doc, root, acceptNode) {
|
|
// IE11 will throw an exception when the TreeWalker root is not an Element.
|
|
if (root.nodeType !== Node.ELEMENT_NODE) {
|
|
return undefined;
|
|
} // TypeScript isn't aware of IE11 behaving badly.
|
|
|
|
|
|
const filter = _isBrokenIE11 ? acceptNode : {
|
|
acceptNode
|
|
};
|
|
return dom.createTreeWalker(doc, root, NodeFilter.SHOW_ELEMENT, filter, // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore: We still don't want to completely break IE11, so, entityReferenceExpansion argument is not optional.
|
|
false
|
|
/* Last argument is not optional for IE11! */
|
|
);
|
|
}
|
|
function getBoundingRect(getWindow, element) {
|
|
let cacheId = element.__tabsterCacheId;
|
|
const context = getInstanceContext(getWindow);
|
|
const cached = cacheId ? context.containerBoundingRectCache[cacheId] : undefined;
|
|
|
|
if (cached) {
|
|
return cached.rect;
|
|
}
|
|
|
|
const scrollingElement = element.ownerDocument && element.ownerDocument.documentElement;
|
|
|
|
if (!scrollingElement) {
|
|
return new _DOMRect();
|
|
} // A bounding rect of the top-level element contains the whole page regardless of the
|
|
// scrollbar. So, we improvise a little and limiting the final result...
|
|
|
|
|
|
let left = 0;
|
|
let top = 0;
|
|
let right = scrollingElement.clientWidth;
|
|
let bottom = scrollingElement.clientHeight;
|
|
|
|
if (element !== scrollingElement) {
|
|
const r = element.getBoundingClientRect();
|
|
left = Math.max(left, r.left);
|
|
top = Math.max(top, r.top);
|
|
right = Math.min(right, r.right);
|
|
bottom = Math.min(bottom, r.bottom);
|
|
}
|
|
|
|
const rect = new _DOMRect(left < right ? left : -1, top < bottom ? top : -1, left < right ? right - left : 0, top < bottom ? bottom - top : 0);
|
|
|
|
if (!cacheId) {
|
|
cacheId = "r-" + ++context.lastContainerBoundingRectCacheId;
|
|
element.__tabsterCacheId = cacheId;
|
|
}
|
|
|
|
context.containerBoundingRectCache[cacheId] = {
|
|
rect,
|
|
element
|
|
};
|
|
|
|
if (!context.containerBoundingRectCacheTimer) {
|
|
context.containerBoundingRectCacheTimer = window.setTimeout(() => {
|
|
context.containerBoundingRectCacheTimer = undefined;
|
|
|
|
for (const cId of Object.keys(context.containerBoundingRectCache)) {
|
|
delete context.containerBoundingRectCache[cId].element.__tabsterCacheId;
|
|
}
|
|
|
|
context.containerBoundingRectCache = {};
|
|
}, 50);
|
|
}
|
|
|
|
return rect;
|
|
}
|
|
function isElementVerticallyVisibleInContainer(getWindow, element, tolerance) {
|
|
const container = getScrollableContainer(element);
|
|
|
|
if (!container) {
|
|
return false;
|
|
}
|
|
|
|
const containerRect = getBoundingRect(getWindow, container);
|
|
const elementRect = element.getBoundingClientRect();
|
|
const intersectionTolerance = elementRect.height * (1 - tolerance);
|
|
const topIntersection = Math.max(0, containerRect.top - elementRect.top);
|
|
const bottomIntersection = Math.max(0, elementRect.bottom - containerRect.bottom);
|
|
const totalIntersection = topIntersection + bottomIntersection;
|
|
return totalIntersection === 0 || totalIntersection <= intersectionTolerance;
|
|
}
|
|
function scrollIntoView(getWindow, element, alignToTop) {
|
|
// Built-in DOM's scrollIntoView() is cool, but when we have nested containers,
|
|
// it scrolls all of them, not just the deepest one. So, trying to work it around.
|
|
const container = getScrollableContainer(element);
|
|
|
|
if (container) {
|
|
const containerRect = getBoundingRect(getWindow, container);
|
|
const elementRect = element.getBoundingClientRect();
|
|
|
|
if (alignToTop) {
|
|
container.scrollTop += elementRect.top - containerRect.top;
|
|
} else {
|
|
container.scrollTop += elementRect.bottom - containerRect.bottom;
|
|
}
|
|
}
|
|
}
|
|
function getScrollableContainer(element) {
|
|
const doc = element.ownerDocument;
|
|
|
|
if (doc) {
|
|
for (let el = dom.getParentElement(element); el; el = dom.getParentElement(el)) {
|
|
if (el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight) {
|
|
return el;
|
|
}
|
|
}
|
|
|
|
return doc.documentElement;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
function makeFocusIgnored(element) {
|
|
element.__shouldIgnoreFocus = true;
|
|
}
|
|
function shouldIgnoreFocus(element) {
|
|
return !!element.__shouldIgnoreFocus;
|
|
}
|
|
function getUId(wnd) {
|
|
const rnd = new Uint32Array(4);
|
|
|
|
if (wnd.crypto && wnd.crypto.getRandomValues) {
|
|
wnd.crypto.getRandomValues(rnd);
|
|
} else if (wnd.msCrypto && wnd.msCrypto.getRandomValues) {
|
|
wnd.msCrypto.getRandomValues(rnd);
|
|
} else {
|
|
for (let i = 0; i < rnd.length; i++) {
|
|
rnd[i] = 0xffffffff * Math.random();
|
|
}
|
|
}
|
|
|
|
const srnd = [];
|
|
|
|
for (let i = 0; i < rnd.length; i++) {
|
|
srnd.push(rnd[i].toString(36));
|
|
}
|
|
|
|
srnd.push("|");
|
|
srnd.push((++_uidCounter).toString(36));
|
|
srnd.push("|");
|
|
srnd.push(Date.now().toString(36));
|
|
return srnd.join("");
|
|
}
|
|
function getElementUId(getWindow, element) {
|
|
const context = getInstanceContext(getWindow);
|
|
let uid = element.__tabsterElementUID;
|
|
|
|
if (!uid) {
|
|
uid = element.__tabsterElementUID = getUId(getWindow());
|
|
}
|
|
|
|
if (!context.elementByUId[uid] && documentContains(element.ownerDocument, element)) {
|
|
context.elementByUId[uid] = new WeakHTMLElement(getWindow, element);
|
|
}
|
|
|
|
return uid;
|
|
}
|
|
function getWindowUId(win) {
|
|
let uid = win.__tabsterCrossOriginWindowUID;
|
|
|
|
if (!uid) {
|
|
uid = win.__tabsterCrossOriginWindowUID = getUId(win);
|
|
}
|
|
|
|
return uid;
|
|
}
|
|
function clearElementCache(getWindow, parent) {
|
|
const context = getInstanceContext(getWindow);
|
|
|
|
for (const key of Object.keys(context.elementByUId)) {
|
|
const wel = context.elementByUId[key];
|
|
const el = wel && wel.get();
|
|
|
|
if (el && parent) {
|
|
if (!dom.nodeContains(parent, el)) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
delete context.elementByUId[key];
|
|
}
|
|
} // IE11 doesn't have document.contains()...
|
|
|
|
function documentContains(doc, element) {
|
|
return dom.nodeContains(doc === null || doc === void 0 ? void 0 : doc.body, element);
|
|
}
|
|
function matchesSelector(element, selector) {
|
|
const matches = element.matches || element.matchesSelector || element.msMatchesSelector || element.webkitMatchesSelector;
|
|
return matches && matches.call(element, selector);
|
|
}
|
|
function getPromise(getWindow) {
|
|
const context = getInstanceContext(getWindow);
|
|
|
|
if (context.basics.Promise) {
|
|
return context.basics.Promise;
|
|
}
|
|
|
|
throw new Error("No Promise defined.");
|
|
}
|
|
function getWeakRef(context) {
|
|
return context.basics.WeakRef;
|
|
}
|
|
let _lastTabsterPartId = 0;
|
|
class TabsterPart {
|
|
constructor(tabster, element, props) {
|
|
const getWindow = tabster.getWindow;
|
|
this._tabster = tabster;
|
|
this._element = new WeakHTMLElement(getWindow, element);
|
|
this._props = { ...props
|
|
};
|
|
this.id = "i" + ++_lastTabsterPartId;
|
|
}
|
|
|
|
getElement() {
|
|
return this._element.get();
|
|
}
|
|
|
|
getProps() {
|
|
return this._props;
|
|
}
|
|
|
|
setProps(props) {
|
|
this._props = { ...props
|
|
};
|
|
}
|
|
|
|
}
|
|
/**
|
|
* Dummy HTML elements that are used as focus sentinels for the DOM enclosed within them
|
|
*/
|
|
|
|
class DummyInput {
|
|
constructor(getWindow, isOutside, props, element, fixedTarget) {
|
|
var _a;
|
|
|
|
this._focusIn = e => {
|
|
if (this._fixedTarget) {
|
|
const target = this._fixedTarget.get();
|
|
|
|
if (target) {
|
|
nativeFocus(target);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
const input = this.input;
|
|
|
|
if (this.onFocusIn && input) {
|
|
const relatedTarget = e.relatedTarget;
|
|
this.onFocusIn(this, this._isBackward(true, input, relatedTarget), relatedTarget);
|
|
}
|
|
};
|
|
|
|
this._focusOut = e => {
|
|
if (this._fixedTarget) {
|
|
return;
|
|
}
|
|
|
|
this.useDefaultAction = false;
|
|
const input = this.input;
|
|
|
|
if (this.onFocusOut && input) {
|
|
const relatedTarget = e.relatedTarget;
|
|
this.onFocusOut(this, this._isBackward(false, input, relatedTarget), relatedTarget);
|
|
}
|
|
};
|
|
|
|
const win = getWindow();
|
|
const input = win.document.createElement("i");
|
|
input.tabIndex = 0;
|
|
input.setAttribute("role", "none");
|
|
input.setAttribute(TABSTER_DUMMY_INPUT_ATTRIBUTE_NAME, "");
|
|
input.setAttribute("aria-hidden", "true");
|
|
const style = input.style;
|
|
style.position = "fixed";
|
|
style.width = style.height = "1px";
|
|
style.opacity = "0.001";
|
|
style.zIndex = "-1";
|
|
style.setProperty("content-visibility", "hidden");
|
|
makeFocusIgnored(input);
|
|
this.input = input;
|
|
this.isFirst = props.isFirst;
|
|
this.isOutside = isOutside;
|
|
this._isPhantom = (_a = props.isPhantom) !== null && _a !== void 0 ? _a : false;
|
|
this._fixedTarget = fixedTarget;
|
|
input.addEventListener("focusin", this._focusIn);
|
|
input.addEventListener("focusout", this._focusOut);
|
|
input.__tabsterDummyContainer = element;
|
|
|
|
if (this._isPhantom) {
|
|
this._disposeTimer = win.setTimeout(() => {
|
|
delete this._disposeTimer;
|
|
this.dispose();
|
|
}, 0);
|
|
|
|
this._clearDisposeTimeout = () => {
|
|
if (this._disposeTimer) {
|
|
win.clearTimeout(this._disposeTimer);
|
|
delete this._disposeTimer;
|
|
}
|
|
|
|
delete this._clearDisposeTimeout;
|
|
};
|
|
}
|
|
}
|
|
|
|
dispose() {
|
|
var _a;
|
|
|
|
if (this._clearDisposeTimeout) {
|
|
this._clearDisposeTimeout();
|
|
}
|
|
|
|
const input = this.input;
|
|
|
|
if (!input) {
|
|
return;
|
|
}
|
|
|
|
delete this._fixedTarget;
|
|
delete this.onFocusIn;
|
|
delete this.onFocusOut;
|
|
delete this.input;
|
|
input.removeEventListener("focusin", this._focusIn);
|
|
input.removeEventListener("focusout", this._focusOut);
|
|
delete input.__tabsterDummyContainer;
|
|
(_a = dom.getParentNode(input)) === null || _a === void 0 ? void 0 : _a.removeChild(input);
|
|
}
|
|
|
|
setTopLeft(top, left) {
|
|
var _a;
|
|
|
|
const style = (_a = this.input) === null || _a === void 0 ? void 0 : _a.style;
|
|
|
|
if (style) {
|
|
style.top = `${top}px`;
|
|
style.left = `${left}px`;
|
|
}
|
|
}
|
|
|
|
_isBackward(isIn, current, previous) {
|
|
return isIn && !previous ? !this.isFirst : !!(previous && current.compareDocumentPosition(previous) & Node.DOCUMENT_POSITION_FOLLOWING);
|
|
}
|
|
|
|
}
|
|
const DummyInputManagerPriorities = {
|
|
Root: 1,
|
|
Modalizer: 2,
|
|
Mover: 3,
|
|
Groupper: 4
|
|
};
|
|
class DummyInputManager {
|
|
constructor(tabster, element, priority, sys, outsideByDefault, callForDefaultAction) {
|
|
this._element = element;
|
|
this._instance = new DummyInputManagerCore(tabster, element, this, priority, sys, outsideByDefault, callForDefaultAction);
|
|
}
|
|
|
|
_setHandlers(onFocusIn, onFocusOut) {
|
|
this._onFocusIn = onFocusIn;
|
|
this._onFocusOut = onFocusOut;
|
|
}
|
|
|
|
moveOut(backwards) {
|
|
var _a;
|
|
|
|
(_a = this._instance) === null || _a === void 0 ? void 0 : _a.moveOut(backwards);
|
|
}
|
|
|
|
moveOutWithDefaultAction(backwards, relatedEvent) {
|
|
var _a;
|
|
|
|
(_a = this._instance) === null || _a === void 0 ? void 0 : _a.moveOutWithDefaultAction(backwards, relatedEvent);
|
|
}
|
|
|
|
getHandler(isIn) {
|
|
return isIn ? this._onFocusIn : this._onFocusOut;
|
|
}
|
|
|
|
setTabbable(tabbable) {
|
|
var _a;
|
|
|
|
(_a = this._instance) === null || _a === void 0 ? void 0 : _a.setTabbable(this, tabbable);
|
|
}
|
|
|
|
dispose() {
|
|
if (this._instance) {
|
|
this._instance.dispose(this);
|
|
|
|
delete this._instance;
|
|
}
|
|
|
|
delete this._onFocusIn;
|
|
delete this._onFocusOut;
|
|
}
|
|
|
|
static moveWithPhantomDummy(tabster, element, // The target element to move to or out of.
|
|
moveOutOfElement, // Whether to move out of the element or into it.
|
|
isBackward, // Are we tabbing of shift-tabbing?
|
|
relatedEvent // The event that triggered the move.
|
|
) {
|
|
// Phantom dummy is a hack to use browser's default action to move
|
|
// focus from a specific point in the application to the next/previous
|
|
// element. Default action is needed because next focusable element
|
|
// is not always available to focus directly (for example, next focusable
|
|
// is inside isolated iframe) or for uncontrolled areas we want to make
|
|
// sure that something that controls it takes care of the focusing.
|
|
// It works in a way that during the Tab key handling, we create a dummy
|
|
// input element, place it to the specific place in the DOM and focus it,
|
|
// then the default action of the Tab press will move focus from our dummy
|
|
// input. And we remove it from the DOM right after that.
|
|
const dummy = new DummyInput(tabster.getWindow, true, {
|
|
isPhantom: true,
|
|
isFirst: true
|
|
});
|
|
const input = dummy.input;
|
|
|
|
if (input) {
|
|
let parent;
|
|
let insertBefore; // Let's say we have a following DOM structure:
|
|
// <div>
|
|
// <button>Button1</button>
|
|
// <div id="uncontrolled" data-tabster={uncontrolled: {}}>
|
|
// <button>Button2</button>
|
|
// <button>Button3</button>
|
|
// </div>
|
|
// <button>Button4</button>
|
|
// </div>
|
|
//
|
|
// We pass the "uncontrolled" div as the element to move to or out of.
|
|
//
|
|
// When we pass moveOutOfElement=true and isBackward=false,
|
|
// the phantom dummy input will be inserted before Button4.
|
|
//
|
|
// When we pass moveOutOfElement=true and isBackward=true, there are
|
|
// two cases. If the uncontrolled element is focusable (has tabindex=0),
|
|
// the phantom dummy input will be inserted after Button1. If the
|
|
// uncontrolled element is not focusable, the phantom dummy input will be
|
|
// inserted before Button2.
|
|
//
|
|
// When we pass moveOutOfElement=false and isBackward=false, the
|
|
// phantom dummy input will be inserted after Button1.
|
|
//
|
|
// When we pass moveOutOfElement=false and isBackward=true, the phantom
|
|
// dummy input will be inserted before Button4.
|
|
//
|
|
// And we have a corner case for <body> and we make sure that the inserted
|
|
// dummy is inserted properly when there are existing permanent dummies.
|
|
|
|
if (element.tagName === "BODY") {
|
|
// We cannot insert elements outside of BODY.
|
|
parent = element;
|
|
insertBefore = moveOutOfElement && isBackward || !moveOutOfElement && !isBackward ? dom.getFirstElementChild(element) : null;
|
|
} else {
|
|
if (moveOutOfElement && (!isBackward || isBackward && !tabster.focusable.isFocusable(element, false, true, true))) {
|
|
parent = element;
|
|
insertBefore = isBackward ? element.firstElementChild : null;
|
|
} else {
|
|
parent = dom.getParentElement(element);
|
|
insertBefore = moveOutOfElement && isBackward || !moveOutOfElement && !isBackward ? element : dom.getNextElementSibling(element);
|
|
}
|
|
|
|
let potentialDummy;
|
|
let dummyFor;
|
|
|
|
do {
|
|
// This is a safety pillow for the cases when someone, combines
|
|
// groupper with uncontrolled on the same node. Which is technically
|
|
// not correct, but moving into the container element via its dummy
|
|
// input would produce a correct behaviour in uncontrolled mode.
|
|
potentialDummy = moveOutOfElement && isBackward || !moveOutOfElement && !isBackward ? dom.getPreviousElementSibling(insertBefore) : insertBefore;
|
|
dummyFor = getDummyInputContainer(potentialDummy);
|
|
|
|
if (dummyFor === element) {
|
|
insertBefore = moveOutOfElement && isBackward || !moveOutOfElement && !isBackward ? potentialDummy : dom.getNextElementSibling(potentialDummy);
|
|
} else {
|
|
dummyFor = null;
|
|
}
|
|
} while (dummyFor);
|
|
}
|
|
|
|
if (parent === null || parent === void 0 ? void 0 : parent.dispatchEvent(new TabsterMoveFocusEvent({
|
|
by: "root",
|
|
owner: parent,
|
|
next: null,
|
|
relatedEvent
|
|
}))) {
|
|
dom.insertBefore(parent, input, insertBefore);
|
|
nativeFocus(input);
|
|
}
|
|
}
|
|
}
|
|
|
|
static addPhantomDummyWithTarget(tabster, sourceElement, isBackward, targetElement) {
|
|
const dummy = new DummyInput(tabster.getWindow, true, {
|
|
isPhantom: true,
|
|
isFirst: true
|
|
}, undefined, new WeakHTMLElement(tabster.getWindow, targetElement));
|
|
const input = dummy.input;
|
|
|
|
if (input) {
|
|
let dummyParent;
|
|
let insertBefore;
|
|
|
|
if (hasSubFocusable(sourceElement) && !isBackward) {
|
|
dummyParent = sourceElement;
|
|
insertBefore = dom.getFirstElementChild(sourceElement);
|
|
} else {
|
|
dummyParent = dom.getParentElement(sourceElement);
|
|
insertBefore = isBackward ? sourceElement : dom.getNextElementSibling(sourceElement);
|
|
}
|
|
|
|
if (dummyParent) {
|
|
dom.insertBefore(dummyParent, input, insertBefore);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
function setDummyInputDebugValue(dummy, wrappers) {
|
|
var _a;
|
|
|
|
const what = {
|
|
1: "Root",
|
|
2: "Modalizer",
|
|
3: "Mover",
|
|
4: "Groupper"
|
|
};
|
|
(_a = dummy.input) === null || _a === void 0 ? void 0 : _a.setAttribute(TABSTER_DUMMY_INPUT_ATTRIBUTE_NAME, [`isFirst=${dummy.isFirst}`, `isOutside=${dummy.isOutside}`, ...wrappers.map(w => `(${what[w.priority]}, tabbable=${w.tabbable})`)].join(", "));
|
|
}
|
|
|
|
class DummyInputObserver {
|
|
constructor(win) {
|
|
this._updateQueue = new Set();
|
|
this._lastUpdateQueueTime = 0;
|
|
this._changedParents = new WeakSet();
|
|
this._dummyElements = [];
|
|
this._dummyCallbacks = new WeakMap();
|
|
|
|
this._domChanged = parent => {
|
|
var _a;
|
|
|
|
if (this._changedParents.has(parent)) {
|
|
return;
|
|
}
|
|
|
|
this._changedParents.add(parent);
|
|
|
|
if (this._updateDummyInputsTimer) {
|
|
return;
|
|
}
|
|
|
|
this._updateDummyInputsTimer = (_a = this._win) === null || _a === void 0 ? void 0 : _a.call(this).setTimeout(() => {
|
|
delete this._updateDummyInputsTimer;
|
|
|
|
for (const ref of this._dummyElements) {
|
|
const dummyElement = ref.get();
|
|
|
|
if (dummyElement) {
|
|
const callback = this._dummyCallbacks.get(dummyElement);
|
|
|
|
if (callback) {
|
|
const dummyParent = dom.getParentNode(dummyElement);
|
|
|
|
if (!dummyParent || this._changedParents.has(dummyParent)) {
|
|
callback();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
this._changedParents = new WeakSet();
|
|
}, _updateDummyInputsTimeout);
|
|
};
|
|
|
|
this._win = win;
|
|
}
|
|
|
|
add(dummy, callback) {
|
|
if (!this._dummyCallbacks.has(dummy) && this._win) {
|
|
this._dummyElements.push(new WeakHTMLElement(this._win, dummy));
|
|
|
|
this._dummyCallbacks.set(dummy, callback);
|
|
|
|
this.domChanged = this._domChanged;
|
|
}
|
|
}
|
|
|
|
remove(dummy) {
|
|
this._dummyElements = this._dummyElements.filter(ref => {
|
|
const element = ref.get();
|
|
return element && element !== dummy;
|
|
});
|
|
|
|
this._dummyCallbacks.delete(dummy);
|
|
|
|
if (this._dummyElements.length === 0) {
|
|
delete this.domChanged;
|
|
}
|
|
}
|
|
|
|
dispose() {
|
|
var _a;
|
|
|
|
const win = (_a = this._win) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
|
|
if (this._updateTimer) {
|
|
win === null || win === void 0 ? void 0 : win.clearTimeout(this._updateTimer);
|
|
delete this._updateTimer;
|
|
}
|
|
|
|
if (this._updateDummyInputsTimer) {
|
|
win === null || win === void 0 ? void 0 : win.clearTimeout(this._updateDummyInputsTimer);
|
|
delete this._updateDummyInputsTimer;
|
|
}
|
|
|
|
this._changedParents = new WeakSet();
|
|
this._dummyCallbacks = new WeakMap();
|
|
this._dummyElements = [];
|
|
|
|
this._updateQueue.clear();
|
|
|
|
delete this.domChanged;
|
|
delete this._win;
|
|
}
|
|
|
|
updatePositions(compute) {
|
|
if (!this._win) {
|
|
// As this is a public method, we make sure that it has no effect when
|
|
// called after dispose().
|
|
return;
|
|
}
|
|
|
|
this._updateQueue.add(compute);
|
|
|
|
this._lastUpdateQueueTime = Date.now();
|
|
|
|
this._scheduledUpdatePositions();
|
|
}
|
|
|
|
_scheduledUpdatePositions() {
|
|
var _a;
|
|
|
|
if (this._updateTimer) {
|
|
return;
|
|
}
|
|
|
|
this._updateTimer = (_a = this._win) === null || _a === void 0 ? void 0 : _a.call(this).setTimeout(() => {
|
|
delete this._updateTimer; // updatePositions() might be called quite a lot during the scrolling.
|
|
// So, instead of clearing the timeout and scheduling a new one, we
|
|
// check if enough time has passed since the last updatePositions() call
|
|
// and only schedule a new one if not.
|
|
// At maximum, we will update dummy inputs positions
|
|
// _updateDummyInputsTimeout * 2 after the last updatePositions() call.
|
|
|
|
if (this._lastUpdateQueueTime + _updateDummyInputsTimeout <= Date.now()) {
|
|
// A cache for current bulk of updates to reduce getComputedStyle() calls.
|
|
const scrollTopLeftCache = new Map();
|
|
const setTopLeftCallbacks = [];
|
|
|
|
for (const compute of this._updateQueue) {
|
|
setTopLeftCallbacks.push(compute(scrollTopLeftCache));
|
|
}
|
|
|
|
this._updateQueue.clear(); // We're splitting the computation of offsets and setting them to avoid extra
|
|
// reflows.
|
|
|
|
|
|
for (const setTopLeft of setTopLeftCallbacks) {
|
|
setTopLeft();
|
|
} // Explicitly clear to not hold references till the next garbage collection.
|
|
|
|
|
|
scrollTopLeftCache.clear();
|
|
} else {
|
|
this._scheduledUpdatePositions();
|
|
}
|
|
}, _updateDummyInputsTimeout);
|
|
}
|
|
|
|
}
|
|
/**
|
|
* Parent class that encapsulates the behaviour of dummy inputs (focus sentinels)
|
|
*/
|
|
|
|
class DummyInputManagerCore {
|
|
constructor(tabster, element, manager, priority, sys, outsideByDefault, callForDefaultAction) {
|
|
this._wrappers = [];
|
|
this._isOutside = false;
|
|
this._transformElements = new Set();
|
|
|
|
this._onFocusIn = (dummyInput, isBackward, relatedTarget) => {
|
|
this._onFocus(true, dummyInput, isBackward, relatedTarget);
|
|
};
|
|
|
|
this._onFocusOut = (dummyInput, isBackward, relatedTarget) => {
|
|
this._onFocus(false, dummyInput, isBackward, relatedTarget);
|
|
};
|
|
|
|
this.moveOut = backwards => {
|
|
var _a;
|
|
|
|
const first = this._firstDummy;
|
|
const last = this._lastDummy;
|
|
|
|
if (first && last) {
|
|
// For the sake of performance optimization, the dummy input
|
|
// position in the DOM updates asynchronously from the DOM change.
|
|
// Calling _ensurePosition() to make sure the position is correct.
|
|
this._ensurePosition();
|
|
|
|
const firstInput = first.input;
|
|
const lastInput = last.input;
|
|
const element = (_a = this._element) === null || _a === void 0 ? void 0 : _a.get();
|
|
|
|
if (firstInput && lastInput && element) {
|
|
let toFocus;
|
|
|
|
if (backwards) {
|
|
firstInput.tabIndex = 0;
|
|
toFocus = firstInput;
|
|
} else {
|
|
lastInput.tabIndex = 0;
|
|
toFocus = lastInput;
|
|
}
|
|
|
|
if (toFocus) {
|
|
nativeFocus(toFocus);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
/**
|
|
* Prepares to move focus out of the given element by focusing
|
|
* one of the dummy inputs and setting the `useDefaultAction` flag
|
|
* @param backwards focus moving to an element behind the given element
|
|
*/
|
|
|
|
|
|
this.moveOutWithDefaultAction = (backwards, relatedEvent) => {
|
|
var _a;
|
|
|
|
const first = this._firstDummy;
|
|
const last = this._lastDummy;
|
|
|
|
if (first && last) {
|
|
// For the sake of performance optimization, the dummy input
|
|
// position in the DOM updates asynchronously from the DOM change.
|
|
// Calling _ensurePosition() to make sure the position is correct.
|
|
this._ensurePosition();
|
|
|
|
const firstInput = first.input;
|
|
const lastInput = last.input;
|
|
const element = (_a = this._element) === null || _a === void 0 ? void 0 : _a.get();
|
|
|
|
if (firstInput && lastInput && element) {
|
|
let toFocus;
|
|
|
|
if (backwards) {
|
|
if (!first.isOutside && this._tabster.focusable.isFocusable(element, true, true, true)) {
|
|
toFocus = element;
|
|
} else {
|
|
first.useDefaultAction = true;
|
|
firstInput.tabIndex = 0;
|
|
toFocus = firstInput;
|
|
}
|
|
} else {
|
|
last.useDefaultAction = true;
|
|
lastInput.tabIndex = 0;
|
|
toFocus = lastInput;
|
|
}
|
|
|
|
if (toFocus && element.dispatchEvent(new TabsterMoveFocusEvent({
|
|
by: "root",
|
|
owner: element,
|
|
next: null,
|
|
relatedEvent
|
|
}))) {
|
|
nativeFocus(toFocus);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
this.setTabbable = (manager, tabbable) => {
|
|
var _a, _b;
|
|
|
|
for (const w of this._wrappers) {
|
|
if (w.manager === manager) {
|
|
w.tabbable = tabbable;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const wrapper = this._getCurrent();
|
|
|
|
if (wrapper) {
|
|
const tabIndex = wrapper.tabbable ? 0 : -1;
|
|
let input = (_a = this._firstDummy) === null || _a === void 0 ? void 0 : _a.input;
|
|
|
|
if (input) {
|
|
input.tabIndex = tabIndex;
|
|
}
|
|
|
|
input = (_b = this._lastDummy) === null || _b === void 0 ? void 0 : _b.input;
|
|
|
|
if (input) {
|
|
input.tabIndex = tabIndex;
|
|
}
|
|
}
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
this._firstDummy && setDummyInputDebugValue(this._firstDummy, this._wrappers);
|
|
this._lastDummy && setDummyInputDebugValue(this._lastDummy, this._wrappers);
|
|
}
|
|
};
|
|
/**
|
|
* Adds dummy inputs as the first and last child of the given element
|
|
* Called each time the children under the element is mutated
|
|
*/
|
|
|
|
|
|
this._addDummyInputs = () => {
|
|
if (this._addTimer) {
|
|
return;
|
|
}
|
|
|
|
this._addTimer = this._getWindow().setTimeout(() => {
|
|
delete this._addTimer;
|
|
|
|
this._ensurePosition();
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
this._firstDummy && setDummyInputDebugValue(this._firstDummy, this._wrappers);
|
|
this._lastDummy && setDummyInputDebugValue(this._lastDummy, this._wrappers);
|
|
}
|
|
|
|
this._addTransformOffsets();
|
|
}, 0);
|
|
};
|
|
|
|
this._addTransformOffsets = () => {
|
|
this._tabster._dummyObserver.updatePositions(this._computeTransformOffsets);
|
|
};
|
|
|
|
this._computeTransformOffsets = scrollTopLeftCache => {
|
|
var _a, _b;
|
|
|
|
const from = ((_a = this._firstDummy) === null || _a === void 0 ? void 0 : _a.input) || ((_b = this._lastDummy) === null || _b === void 0 ? void 0 : _b.input);
|
|
const transformElements = this._transformElements;
|
|
const newTransformElements = new Set();
|
|
let scrollTop = 0;
|
|
let scrollLeft = 0;
|
|
|
|
const win = this._getWindow();
|
|
|
|
for (let element = from; element && element.nodeType === Node.ELEMENT_NODE; element = dom.getParentElement(element)) {
|
|
let scrollTopLeft = scrollTopLeftCache.get(element); // getComputedStyle() and element.scrollLeft/Top() cause style recalculation,
|
|
// so we cache the result across all elements in the current bulk.
|
|
|
|
if (scrollTopLeft === undefined) {
|
|
const transform = win.getComputedStyle(element).transform;
|
|
|
|
if (transform && transform !== "none") {
|
|
scrollTopLeft = {
|
|
scrollTop: element.scrollTop,
|
|
scrollLeft: element.scrollLeft
|
|
};
|
|
}
|
|
|
|
scrollTopLeftCache.set(element, scrollTopLeft || null);
|
|
}
|
|
|
|
if (scrollTopLeft) {
|
|
newTransformElements.add(element);
|
|
|
|
if (!transformElements.has(element)) {
|
|
element.addEventListener("scroll", this._addTransformOffsets);
|
|
}
|
|
|
|
scrollTop += scrollTopLeft.scrollTop;
|
|
scrollLeft += scrollTopLeft.scrollLeft;
|
|
}
|
|
}
|
|
|
|
for (const el of transformElements) {
|
|
if (!newTransformElements.has(el)) {
|
|
el.removeEventListener("scroll", this._addTransformOffsets);
|
|
}
|
|
}
|
|
|
|
this._transformElements = newTransformElements;
|
|
return () => {
|
|
var _a, _b;
|
|
|
|
(_a = this._firstDummy) === null || _a === void 0 ? void 0 : _a.setTopLeft(scrollTop, scrollLeft);
|
|
(_b = this._lastDummy) === null || _b === void 0 ? void 0 : _b.setTopLeft(scrollTop, scrollLeft);
|
|
};
|
|
};
|
|
|
|
const el = element.get();
|
|
|
|
if (!el) {
|
|
throw new Error("No element");
|
|
}
|
|
|
|
this._tabster = tabster;
|
|
this._getWindow = tabster.getWindow;
|
|
this._callForDefaultAction = callForDefaultAction;
|
|
const instance = el.__tabsterDummy;
|
|
|
|
(instance || this)._wrappers.push({
|
|
manager,
|
|
priority,
|
|
tabbable: true
|
|
});
|
|
|
|
if (instance) {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
this._firstDummy && setDummyInputDebugValue(this._firstDummy, instance._wrappers);
|
|
this._lastDummy && setDummyInputDebugValue(this._lastDummy, instance._wrappers);
|
|
}
|
|
|
|
return instance;
|
|
}
|
|
|
|
el.__tabsterDummy = this; // Some elements allow only specific types of direct descendants and we need to
|
|
// put our dummy inputs inside or outside of the element accordingly.
|
|
|
|
const forcedDummyPosition = sys === null || sys === void 0 ? void 0 : sys.dummyInputsPosition;
|
|
const tagName = el.tagName;
|
|
this._isOutside = !forcedDummyPosition ? (outsideByDefault || tagName === "UL" || tagName === "OL" || tagName === "TABLE") && !(tagName === "LI" || tagName === "TD" || tagName === "TH") : forcedDummyPosition === SysDummyInputsPositions.Outside;
|
|
this._firstDummy = new DummyInput(this._getWindow, this._isOutside, {
|
|
isFirst: true
|
|
}, element);
|
|
this._lastDummy = new DummyInput(this._getWindow, this._isOutside, {
|
|
isFirst: false
|
|
}, element); // We will be checking dummy input parents to see if their child list have changed.
|
|
// So, it is enough to have just one of the inputs observed, because
|
|
// both dummy inputs always have the same parent.
|
|
|
|
const dummyElement = this._firstDummy.input;
|
|
dummyElement && tabster._dummyObserver.add(dummyElement, this._addDummyInputs);
|
|
this._firstDummy.onFocusIn = this._onFocusIn;
|
|
this._firstDummy.onFocusOut = this._onFocusOut;
|
|
this._lastDummy.onFocusIn = this._onFocusIn;
|
|
this._lastDummy.onFocusOut = this._onFocusOut;
|
|
this._element = element;
|
|
|
|
this._addDummyInputs();
|
|
}
|
|
|
|
dispose(manager, force) {
|
|
var _a, _b, _c, _d;
|
|
|
|
const wrappers = this._wrappers = this._wrappers.filter(w => w.manager !== manager && !force);
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
this._firstDummy && setDummyInputDebugValue(this._firstDummy, wrappers);
|
|
this._lastDummy && setDummyInputDebugValue(this._lastDummy, wrappers);
|
|
}
|
|
|
|
if (wrappers.length === 0) {
|
|
delete ((_a = this._element) === null || _a === void 0 ? void 0 : _a.get()).__tabsterDummy;
|
|
|
|
for (const el of this._transformElements) {
|
|
el.removeEventListener("scroll", this._addTransformOffsets);
|
|
}
|
|
|
|
this._transformElements.clear();
|
|
|
|
const win = this._getWindow();
|
|
|
|
if (this._addTimer) {
|
|
win.clearTimeout(this._addTimer);
|
|
delete this._addTimer;
|
|
}
|
|
|
|
const dummyElement = (_b = this._firstDummy) === null || _b === void 0 ? void 0 : _b.input;
|
|
dummyElement && this._tabster._dummyObserver.remove(dummyElement);
|
|
(_c = this._firstDummy) === null || _c === void 0 ? void 0 : _c.dispose();
|
|
(_d = this._lastDummy) === null || _d === void 0 ? void 0 : _d.dispose();
|
|
}
|
|
}
|
|
|
|
_onFocus(isIn, dummyInput, isBackward, relatedTarget) {
|
|
var _a;
|
|
|
|
const wrapper = this._getCurrent();
|
|
|
|
if (wrapper && (!dummyInput.useDefaultAction || this._callForDefaultAction)) {
|
|
(_a = wrapper.manager.getHandler(isIn)) === null || _a === void 0 ? void 0 : _a(dummyInput, isBackward, relatedTarget);
|
|
}
|
|
}
|
|
|
|
_getCurrent() {
|
|
this._wrappers.sort((a, b) => {
|
|
if (a.tabbable !== b.tabbable) {
|
|
return a.tabbable ? -1 : 1;
|
|
}
|
|
|
|
return a.priority - b.priority;
|
|
});
|
|
|
|
return this._wrappers[0];
|
|
}
|
|
|
|
_ensurePosition() {
|
|
var _a, _b, _c;
|
|
|
|
const element = (_a = this._element) === null || _a === void 0 ? void 0 : _a.get();
|
|
const firstDummyInput = (_b = this._firstDummy) === null || _b === void 0 ? void 0 : _b.input;
|
|
const lastDummyInput = (_c = this._lastDummy) === null || _c === void 0 ? void 0 : _c.input;
|
|
|
|
if (!element || !firstDummyInput || !lastDummyInput) {
|
|
return;
|
|
}
|
|
|
|
if (this._isOutside) {
|
|
const elementParent = dom.getParentNode(element);
|
|
|
|
if (elementParent) {
|
|
const nextSibling = dom.getNextSibling(element);
|
|
|
|
if (nextSibling !== lastDummyInput) {
|
|
dom.insertBefore(elementParent, lastDummyInput, nextSibling);
|
|
}
|
|
|
|
if (dom.getPreviousElementSibling(element) !== firstDummyInput) {
|
|
dom.insertBefore(elementParent, firstDummyInput, element);
|
|
}
|
|
}
|
|
} else {
|
|
if (dom.getLastElementChild(element) !== lastDummyInput) {
|
|
dom.appendChild(element, lastDummyInput);
|
|
}
|
|
|
|
const firstElementChild = dom.getFirstElementChild(element);
|
|
|
|
if (firstElementChild && firstElementChild !== firstDummyInput && firstElementChild.parentNode) {
|
|
dom.insertBefore(firstElementChild.parentNode, firstDummyInput, firstElementChild);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
function getLastChild$2(container) {
|
|
let lastChild = null;
|
|
|
|
for (let i = dom.getLastElementChild(container); i; i = dom.getLastElementChild(i)) {
|
|
lastChild = i;
|
|
}
|
|
|
|
return lastChild || undefined;
|
|
}
|
|
function getAdjacentElement(from, prev) {
|
|
let cur = from;
|
|
let adjacent = null;
|
|
|
|
while (cur && !adjacent) {
|
|
adjacent = prev ? dom.getPreviousElementSibling(cur) : dom.getNextElementSibling(cur);
|
|
cur = dom.getParentElement(cur);
|
|
}
|
|
|
|
return adjacent || undefined;
|
|
}
|
|
function augmentAttribute(tabster, element, name, value // Restore original value when undefined.
|
|
) {
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
const entry = tabster.storageEntry(element, true);
|
|
let ret = false;
|
|
|
|
if (!entry.aug) {
|
|
if (value === undefined) {
|
|
return ret;
|
|
}
|
|
|
|
entry.aug = {};
|
|
}
|
|
|
|
if (value === undefined) {
|
|
if (name in entry.aug) {
|
|
const origVal = entry.aug[name];
|
|
delete entry.aug[name];
|
|
|
|
if (origVal === null) {
|
|
element.removeAttribute(name);
|
|
} else {
|
|
element.setAttribute(name, origVal);
|
|
}
|
|
|
|
ret = true;
|
|
}
|
|
} else {
|
|
let origValue;
|
|
|
|
if (!(name in entry.aug)) {
|
|
origValue = element.getAttribute(name);
|
|
}
|
|
|
|
if (origValue !== undefined && origValue !== value) {
|
|
entry.aug[name] = origValue;
|
|
|
|
if (value === null) {
|
|
element.removeAttribute(name);
|
|
} else {
|
|
element.setAttribute(name, value);
|
|
}
|
|
|
|
ret = true;
|
|
}
|
|
}
|
|
|
|
if (value === undefined && Object.keys(entry.aug).length === 0) {
|
|
delete entry.aug;
|
|
tabster.storageEntry(element, false);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
function isDisplayNone(element) {
|
|
var _a, _b;
|
|
|
|
const elementDocument = element.ownerDocument;
|
|
const computedStyle = (_a = elementDocument.defaultView) === null || _a === void 0 ? void 0 : _a.getComputedStyle(element); // offsetParent is null for elements with display:none, display:fixed and for <body>.
|
|
|
|
if (element.offsetParent === null && elementDocument.body !== element && (computedStyle === null || computedStyle === void 0 ? void 0 : computedStyle.position) !== "fixed") {
|
|
return true;
|
|
} // For our purposes of looking for focusable elements, visibility:hidden has the same
|
|
// effect as display:none.
|
|
|
|
|
|
if ((computedStyle === null || computedStyle === void 0 ? void 0 : computedStyle.visibility) === "hidden") {
|
|
return true;
|
|
} // if an element has display: fixed, we need to check if it is also hidden with CSS,
|
|
// or within a parent hidden with CSS
|
|
|
|
|
|
if ((computedStyle === null || computedStyle === void 0 ? void 0 : computedStyle.position) === "fixed") {
|
|
if (computedStyle.display === "none") {
|
|
return true;
|
|
}
|
|
|
|
if (((_b = element.parentElement) === null || _b === void 0 ? void 0 : _b.offsetParent) === null && elementDocument.body !== element.parentElement) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
function isRadio(element) {
|
|
return element.tagName === "INPUT" && !!element.name && element.type === "radio";
|
|
}
|
|
function getRadioButtonGroup(element) {
|
|
if (!isRadio(element)) {
|
|
return;
|
|
}
|
|
|
|
const name = element.name;
|
|
let radioButtons = Array.from(dom.getElementsByName(element, name));
|
|
let checked;
|
|
radioButtons = radioButtons.filter(el => {
|
|
if (isRadio(el)) {
|
|
if (el.checked) {
|
|
checked = el;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
return {
|
|
name,
|
|
buttons: new Set(radioButtons),
|
|
checked
|
|
};
|
|
}
|
|
/**
|
|
* If the passed element is Tabster dummy input, returns the container element this dummy input belongs to.
|
|
* @param element Element to check for being dummy input.
|
|
* @returns Dummy input container element (if the passed element is a dummy input) or null.
|
|
*/
|
|
|
|
function getDummyInputContainer(element) {
|
|
var _a;
|
|
|
|
return ((_a = element === null || element === void 0 ? void 0 : element.__tabsterDummyContainer) === null || _a === void 0 ? void 0 : _a.get()) || null;
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
function getTabsterAttribute(props, plain) {
|
|
const attr = JSON.stringify(props);
|
|
|
|
if (plain === true) {
|
|
return attr;
|
|
}
|
|
|
|
return {
|
|
[TABSTER_ATTRIBUTE_NAME]: attr
|
|
};
|
|
}
|
|
/**
|
|
* Updates Tabster props object with new props.
|
|
* @param element an element to set data-tabster attribute on.
|
|
* @param props current Tabster props to update.
|
|
* @param newProps new Tabster props to add.
|
|
* When the value of a property in newProps is undefined, the property
|
|
* will be removed from the attribute.
|
|
*/
|
|
|
|
function mergeTabsterProps(props, newProps) {
|
|
for (const key of Object.keys(newProps)) {
|
|
const value = newProps[key];
|
|
|
|
if (value) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
props[key] = value;
|
|
} else {
|
|
delete props[key];
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Sets or updates Tabster attribute of the element.
|
|
* @param element an element to set data-tabster attribute on.
|
|
* @param newProps new Tabster props to set.
|
|
* @param update if true, newProps will be merged with the existing props.
|
|
* When true and the value of a property in newProps is undefined, the property
|
|
* will be removed from the attribute.
|
|
*/
|
|
|
|
function setTabsterAttribute(element, newProps, update) {
|
|
let props;
|
|
|
|
if (update) {
|
|
const attr = element.getAttribute(TABSTER_ATTRIBUTE_NAME);
|
|
|
|
if (attr) {
|
|
try {
|
|
props = JSON.parse(attr);
|
|
} catch (e) {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.error(`data-tabster attribute error: ${e}`, element);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!props) {
|
|
props = {};
|
|
}
|
|
|
|
mergeTabsterProps(props, newProps);
|
|
|
|
if (Object.keys(props).length > 0) {
|
|
element.setAttribute(TABSTER_ATTRIBUTE_NAME, getTabsterAttribute(props, true));
|
|
} else {
|
|
element.removeAttribute(TABSTER_ATTRIBUTE_NAME);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
|
|
function _setInformativeStyle$3(weakElement, remove, id) {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
const element = weakElement.get();
|
|
|
|
if (element) {
|
|
if (remove) {
|
|
element.style.removeProperty("--tabster-root");
|
|
} else {
|
|
element.style.setProperty("--tabster-root", id + ",");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class RootDummyManager extends DummyInputManager {
|
|
constructor(tabster, element, setFocused, sys) {
|
|
super(tabster, element, DummyInputManagerPriorities.Root, sys, undefined, true);
|
|
|
|
this._onDummyInputFocus = dummyInput => {
|
|
var _a;
|
|
|
|
if (dummyInput.useDefaultAction) {
|
|
// When we've reached the last focusable element, we want to let the browser
|
|
// to move the focus outside of the page. In order to do that we're synchronously
|
|
// calling focus() of the dummy input from the Tab key handler and allowing
|
|
// the default action to move the focus out.
|
|
this._setFocused(false);
|
|
} else {
|
|
// The only way a dummy input gets focused is during the keyboard navigation.
|
|
this._tabster.keyboardNavigation.setNavigatingWithKeyboard(true);
|
|
|
|
const element = this._element.get();
|
|
|
|
if (element) {
|
|
this._setFocused(true);
|
|
|
|
const toFocus = this._tabster.focusedElement.getFirstOrLastTabbable(dummyInput.isFirst, {
|
|
container: element,
|
|
ignoreAccessibility: true
|
|
});
|
|
|
|
if (toFocus) {
|
|
nativeFocus(toFocus);
|
|
return;
|
|
}
|
|
}
|
|
|
|
(_a = dummyInput.input) === null || _a === void 0 ? void 0 : _a.blur();
|
|
}
|
|
};
|
|
|
|
this._setHandlers(this._onDummyInputFocus);
|
|
|
|
this._tabster = tabster;
|
|
this._setFocused = setFocused;
|
|
}
|
|
|
|
}
|
|
|
|
class Root extends TabsterPart {
|
|
constructor(tabster, element, onDispose, props, sys) {
|
|
super(tabster, element, props);
|
|
this._isFocused = false;
|
|
|
|
this._setFocused = hasFocused => {
|
|
var _a;
|
|
|
|
if (this._setFocusedTimer) {
|
|
this._tabster.getWindow().clearTimeout(this._setFocusedTimer);
|
|
|
|
delete this._setFocusedTimer;
|
|
}
|
|
|
|
if (this._isFocused === hasFocused) {
|
|
return;
|
|
}
|
|
|
|
const element = this._element.get();
|
|
|
|
if (element) {
|
|
if (hasFocused) {
|
|
this._isFocused = true;
|
|
(_a = this._dummyManager) === null || _a === void 0 ? void 0 : _a.setTabbable(false);
|
|
element.dispatchEvent(new RootFocusEvent({
|
|
element
|
|
}));
|
|
} else {
|
|
this._setFocusedTimer = this._tabster.getWindow().setTimeout(() => {
|
|
var _a;
|
|
|
|
delete this._setFocusedTimer;
|
|
this._isFocused = false;
|
|
(_a = this._dummyManager) === null || _a === void 0 ? void 0 : _a.setTabbable(true);
|
|
element.dispatchEvent(new RootBlurEvent({
|
|
element
|
|
}));
|
|
}, 0);
|
|
}
|
|
}
|
|
};
|
|
|
|
this._onFocusIn = event => {
|
|
const getParent = this._tabster.getParent;
|
|
|
|
const rootElement = this._element.get();
|
|
|
|
let curElement = event.composedPath()[0];
|
|
|
|
do {
|
|
if (curElement === rootElement) {
|
|
this._setFocused(true);
|
|
|
|
return;
|
|
}
|
|
|
|
curElement = curElement && getParent(curElement);
|
|
} while (curElement);
|
|
};
|
|
|
|
this._onFocusOut = () => {
|
|
this._setFocused(false);
|
|
};
|
|
|
|
this._onDispose = onDispose;
|
|
const win = tabster.getWindow;
|
|
this.uid = getElementUId(win, element);
|
|
this._sys = sys;
|
|
|
|
if (tabster.controlTab || tabster.rootDummyInputs) {
|
|
this.addDummyInputs();
|
|
}
|
|
|
|
const w = win();
|
|
const doc = w.document;
|
|
doc.addEventListener(KEYBORG_FOCUSIN, this._onFocusIn);
|
|
doc.addEventListener(KEYBORG_FOCUSOUT, this._onFocusOut);
|
|
|
|
this._add();
|
|
}
|
|
|
|
addDummyInputs() {
|
|
if (!this._dummyManager) {
|
|
this._dummyManager = new RootDummyManager(this._tabster, this._element, this._setFocused, this._sys);
|
|
}
|
|
}
|
|
|
|
dispose() {
|
|
var _a;
|
|
|
|
this._onDispose(this);
|
|
|
|
const win = this._tabster.getWindow();
|
|
|
|
const doc = win.document;
|
|
doc.removeEventListener(KEYBORG_FOCUSIN, this._onFocusIn);
|
|
doc.removeEventListener(KEYBORG_FOCUSOUT, this._onFocusOut);
|
|
|
|
if (this._setFocusedTimer) {
|
|
win.clearTimeout(this._setFocusedTimer);
|
|
delete this._setFocusedTimer;
|
|
}
|
|
|
|
(_a = this._dummyManager) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
|
|
this._remove();
|
|
}
|
|
|
|
moveOutWithDefaultAction(isBackward, relatedEvent) {
|
|
const dummyManager = this._dummyManager;
|
|
|
|
if (dummyManager) {
|
|
dummyManager.moveOutWithDefaultAction(isBackward, relatedEvent);
|
|
} else {
|
|
const el = this.getElement();
|
|
|
|
if (el) {
|
|
RootDummyManager.moveWithPhantomDummy(this._tabster, el, true, isBackward, relatedEvent);
|
|
}
|
|
}
|
|
}
|
|
|
|
_add() {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
_setInformativeStyle$3(this._element, false, this.uid);
|
|
}
|
|
}
|
|
|
|
_remove() {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
_setInformativeStyle$3(this._element, true);
|
|
}
|
|
}
|
|
|
|
} // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
|
|
class RootAPI {
|
|
constructor(tabster, autoRoot) {
|
|
this._autoRootWaiting = false;
|
|
this._roots = {};
|
|
this._forceDummy = false;
|
|
this.rootById = {};
|
|
|
|
this._autoRootCreate = () => {
|
|
var _a;
|
|
|
|
const doc = this._win().document;
|
|
|
|
const body = doc.body;
|
|
|
|
if (body) {
|
|
this._autoRootUnwait(doc);
|
|
|
|
const props = this._autoRoot;
|
|
|
|
if (props) {
|
|
setTabsterAttribute(body, {
|
|
root: props
|
|
}, true);
|
|
updateTabsterByAttribute(this._tabster, body);
|
|
return (_a = getTabsterOnElement(this._tabster, body)) === null || _a === void 0 ? void 0 : _a.root;
|
|
}
|
|
} else if (!this._autoRootWaiting) {
|
|
this._autoRootWaiting = true;
|
|
doc.addEventListener("readystatechange", this._autoRootCreate);
|
|
}
|
|
|
|
return undefined;
|
|
};
|
|
|
|
this._onRootDispose = root => {
|
|
delete this._roots[root.id];
|
|
};
|
|
|
|
this._tabster = tabster;
|
|
this._win = tabster.getWindow;
|
|
this._autoRoot = autoRoot;
|
|
tabster.queueInit(() => {
|
|
if (this._autoRoot) {
|
|
this._autoRootCreate();
|
|
}
|
|
});
|
|
}
|
|
|
|
_autoRootUnwait(doc) {
|
|
doc.removeEventListener("readystatechange", this._autoRootCreate);
|
|
this._autoRootWaiting = false;
|
|
}
|
|
|
|
dispose() {
|
|
const win = this._win();
|
|
|
|
this._autoRootUnwait(win.document);
|
|
|
|
delete this._autoRoot;
|
|
Object.keys(this._roots).forEach(rootId => {
|
|
if (this._roots[rootId]) {
|
|
this._roots[rootId].dispose();
|
|
|
|
delete this._roots[rootId];
|
|
}
|
|
});
|
|
this.rootById = {};
|
|
}
|
|
|
|
createRoot(element, props, sys) {
|
|
if (process.env.NODE_ENV === 'development') ;
|
|
|
|
const newRoot = new Root(this._tabster, element, this._onRootDispose, props, sys);
|
|
this._roots[newRoot.id] = newRoot;
|
|
|
|
if (this._forceDummy) {
|
|
newRoot.addDummyInputs();
|
|
}
|
|
|
|
return newRoot;
|
|
}
|
|
|
|
addDummyInputs() {
|
|
this._forceDummy = true;
|
|
const roots = this._roots;
|
|
|
|
for (const id of Object.keys(roots)) {
|
|
roots[id].addDummyInputs();
|
|
}
|
|
}
|
|
|
|
static getRootByUId(getWindow, id) {
|
|
const tabster = getWindow().__tabsterInstance;
|
|
|
|
return tabster && tabster.root.rootById[id];
|
|
}
|
|
/**
|
|
* Fetches the tabster context for an element walking up its ancestors
|
|
*
|
|
* @param tabster Tabster instance
|
|
* @param element The element the tabster context should represent
|
|
* @param options Additional options
|
|
* @returns undefined if the element is not a child of a tabster root, otherwise all applicable tabster behaviours and configurations
|
|
*/
|
|
|
|
|
|
static getTabsterContext(tabster, element, options) {
|
|
if (options === void 0) {
|
|
options = {};
|
|
}
|
|
|
|
var _a, _b, _c, _d;
|
|
|
|
if (!element.ownerDocument) {
|
|
return undefined;
|
|
}
|
|
|
|
const {
|
|
checkRtl,
|
|
referenceElement
|
|
} = options;
|
|
const getParent = tabster.getParent; // Normally, the initialization starts on the next tick after the tabster
|
|
// instance creation. However, if the application starts using it before
|
|
// the next tick, we need to make sure the initialization is done.
|
|
|
|
tabster.drainInitQueue();
|
|
let root;
|
|
let modalizer;
|
|
let groupper;
|
|
let mover;
|
|
let excludedFromMover = false;
|
|
let groupperBeforeMover;
|
|
let modalizerInGroupper;
|
|
let dirRightToLeft;
|
|
let uncontrolled;
|
|
let curElement = referenceElement || element;
|
|
const ignoreKeydown = {};
|
|
|
|
while (curElement && (!root || checkRtl)) {
|
|
const tabsterOnElement = getTabsterOnElement(tabster, curElement);
|
|
|
|
if (checkRtl && dirRightToLeft === undefined) {
|
|
const dir = curElement.dir;
|
|
|
|
if (dir) {
|
|
dirRightToLeft = dir.toLowerCase() === "rtl";
|
|
}
|
|
}
|
|
|
|
if (!tabsterOnElement) {
|
|
curElement = getParent(curElement);
|
|
continue;
|
|
}
|
|
|
|
const tagName = curElement.tagName;
|
|
|
|
if (tabsterOnElement.uncontrolled || tagName === "IFRAME" || tagName === "WEBVIEW") {
|
|
uncontrolled = curElement;
|
|
}
|
|
|
|
if (!mover && ((_a = tabsterOnElement.focusable) === null || _a === void 0 ? void 0 : _a.excludeFromMover) && !groupper) {
|
|
excludedFromMover = true;
|
|
}
|
|
|
|
const curModalizer = tabsterOnElement.modalizer;
|
|
const curGroupper = tabsterOnElement.groupper;
|
|
const curMover = tabsterOnElement.mover;
|
|
|
|
if (!modalizer && curModalizer) {
|
|
modalizer = curModalizer;
|
|
}
|
|
|
|
if (!groupper && curGroupper && (!modalizer || curModalizer)) {
|
|
if (modalizer) {
|
|
// Modalizer dominates the groupper when they are on the same node and the groupper is active.
|
|
if (!curGroupper.isActive() && curGroupper.getProps().tabbability && modalizer.userId !== ((_b = tabster.modalizer) === null || _b === void 0 ? void 0 : _b.activeId)) {
|
|
modalizer = undefined;
|
|
groupper = curGroupper;
|
|
}
|
|
|
|
modalizerInGroupper = curGroupper;
|
|
} else {
|
|
groupper = curGroupper;
|
|
}
|
|
}
|
|
|
|
if (!mover && curMover && (!modalizer || curModalizer) && (!curGroupper || curElement !== element) && curElement.contains(element) // Mover makes sense only for really inside elements, not for virutal out of the DOM order children.
|
|
) {
|
|
mover = curMover;
|
|
groupperBeforeMover = !!groupper && groupper !== curGroupper;
|
|
}
|
|
|
|
if (tabsterOnElement.root) {
|
|
root = tabsterOnElement.root;
|
|
}
|
|
|
|
if ((_c = tabsterOnElement.focusable) === null || _c === void 0 ? void 0 : _c.ignoreKeydown) {
|
|
Object.assign(ignoreKeydown, tabsterOnElement.focusable.ignoreKeydown);
|
|
}
|
|
|
|
curElement = getParent(curElement);
|
|
} // No root element could be found, try to get an auto root
|
|
|
|
|
|
if (!root) {
|
|
const rootAPI = tabster.root;
|
|
const autoRoot = rootAPI._autoRoot;
|
|
|
|
if (autoRoot) {
|
|
if ((_d = element.ownerDocument) === null || _d === void 0 ? void 0 : _d.body) {
|
|
root = rootAPI._autoRootCreate();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (groupper && !mover) {
|
|
groupperBeforeMover = true;
|
|
}
|
|
|
|
if (process.env.NODE_ENV === 'development' && !root) {
|
|
if (modalizer || groupper || mover) {
|
|
console.error("Tabster Root is required for Mover, Groupper and Modalizer to work.");
|
|
}
|
|
}
|
|
|
|
const shouldIgnoreKeydown = event => !!ignoreKeydown[event.key];
|
|
|
|
return root ? {
|
|
root,
|
|
modalizer,
|
|
groupper,
|
|
mover,
|
|
groupperBeforeMover,
|
|
modalizerInGroupper,
|
|
rtl: checkRtl ? !!dirRightToLeft : undefined,
|
|
uncontrolled,
|
|
excludedFromMover,
|
|
ignoreKeydown: shouldIgnoreKeydown
|
|
} : undefined;
|
|
}
|
|
|
|
static getRoot(tabster, element) {
|
|
var _a;
|
|
|
|
const getParent = tabster.getParent;
|
|
|
|
for (let el = element; el; el = getParent(el)) {
|
|
const root = (_a = getTabsterOnElement(tabster, el)) === null || _a === void 0 ? void 0 : _a.root;
|
|
|
|
if (root) {
|
|
return root;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
onRoot(root, removed) {
|
|
if (removed) {
|
|
delete this.rootById[root.uid];
|
|
} else {
|
|
this.rootById[root.uid] = root;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
const _containerHistoryLength = 10;
|
|
class DeloserItemBase {}
|
|
class DeloserItem extends DeloserItemBase {
|
|
constructor(tabster, deloser) {
|
|
super();
|
|
this.uid = deloser.uid;
|
|
this._tabster = tabster;
|
|
this._deloser = deloser;
|
|
}
|
|
|
|
belongsTo(deloser) {
|
|
return deloser === this._deloser;
|
|
}
|
|
|
|
unshift(element) {
|
|
this._deloser.unshift(element);
|
|
}
|
|
|
|
async focusAvailable() {
|
|
const available = this._deloser.findAvailable();
|
|
|
|
const deloserElement = this._deloser.getElement();
|
|
|
|
if (available && deloserElement) {
|
|
if (!deloserElement.dispatchEvent(new TabsterMoveFocusEvent({
|
|
by: "deloser",
|
|
owner: deloserElement,
|
|
next: available
|
|
}))) {
|
|
// Default action is prevented, don't look further.
|
|
return null;
|
|
}
|
|
|
|
return this._tabster.focusedElement.focus(available);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async resetFocus() {
|
|
const getWindow = this._tabster.getWindow;
|
|
return getPromise(getWindow).resolve(this._deloser.resetFocus());
|
|
}
|
|
|
|
}
|
|
class DeloserHistoryByRootBase {
|
|
constructor(tabster, rootUId) {
|
|
this._history = [];
|
|
this._tabster = tabster;
|
|
this.rootUId = rootUId;
|
|
}
|
|
|
|
getLength() {
|
|
return this._history.length;
|
|
}
|
|
|
|
removeDeloser(deloser) {
|
|
this._history = this._history.filter(c => !c.belongsTo(deloser));
|
|
}
|
|
|
|
hasDeloser(deloser) {
|
|
return this._history.some(d => d.belongsTo(deloser));
|
|
}
|
|
|
|
}
|
|
|
|
class DeloserHistoryByRoot extends DeloserHistoryByRootBase {
|
|
unshiftToDeloser(deloser, element) {
|
|
let item;
|
|
|
|
for (let i = 0; i < this._history.length; i++) {
|
|
if (this._history[i].belongsTo(deloser)) {
|
|
item = this._history[i];
|
|
|
|
this._history.splice(i, 1);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!item) {
|
|
item = new DeloserItem(this._tabster, deloser);
|
|
}
|
|
|
|
item.unshift(element);
|
|
|
|
this._history.unshift(item);
|
|
|
|
this._history.splice(_containerHistoryLength, this._history.length - _containerHistoryLength);
|
|
}
|
|
|
|
async focusAvailable(from) {
|
|
let skip = !!from;
|
|
|
|
for (const i of this._history) {
|
|
if (from && i.belongsTo(from)) {
|
|
skip = false;
|
|
}
|
|
|
|
if (!skip) {
|
|
const result = await i.focusAvailable(); // Result is null when the default action is prevented by the application
|
|
// and we don't need to look further.
|
|
|
|
if (result || result === null) {
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async resetFocus(from) {
|
|
let skip = !!from;
|
|
const resetQueue = {};
|
|
|
|
for (const i of this._history) {
|
|
if (from && i.belongsTo(from)) {
|
|
skip = false;
|
|
}
|
|
|
|
if (!skip && !resetQueue[i.uid]) {
|
|
resetQueue[i.uid] = i;
|
|
}
|
|
} // Nothing is found, at least try to reset.
|
|
|
|
|
|
for (const id of Object.keys(resetQueue)) {
|
|
if (await resetQueue[id].resetFocus()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
class DeloserHistory {
|
|
constructor(tabster) {
|
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
this._history = [];
|
|
this._tabster = tabster;
|
|
}
|
|
|
|
dispose() {
|
|
this._history = [];
|
|
}
|
|
|
|
process(element) {
|
|
var _a;
|
|
|
|
const ctx = RootAPI.getTabsterContext(this._tabster, element);
|
|
const rootUId = ctx && ctx.root.uid;
|
|
const deloser = DeloserAPI.getDeloser(this._tabster, element);
|
|
|
|
if (!rootUId || !deloser) {
|
|
return undefined;
|
|
}
|
|
|
|
const historyByRoot = this.make(rootUId, () => new DeloserHistoryByRoot(this._tabster, rootUId));
|
|
|
|
if (!ctx || !ctx.modalizer || ((_a = ctx.modalizer) === null || _a === void 0 ? void 0 : _a.isActive())) {
|
|
historyByRoot.unshiftToDeloser(deloser, element);
|
|
}
|
|
|
|
return deloser;
|
|
}
|
|
|
|
make(rootUId, createInstance) {
|
|
let historyByRoot;
|
|
|
|
for (let i = 0; i < this._history.length; i++) {
|
|
const hbr = this._history[i];
|
|
|
|
if (hbr.rootUId === rootUId) {
|
|
historyByRoot = hbr;
|
|
|
|
this._history.splice(i, 1);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!historyByRoot) {
|
|
historyByRoot = createInstance();
|
|
}
|
|
|
|
this._history.unshift(historyByRoot);
|
|
|
|
this._history.splice(_containerHistoryLength, this._history.length - _containerHistoryLength);
|
|
|
|
return historyByRoot;
|
|
}
|
|
|
|
removeDeloser(deloser) {
|
|
this._history.forEach(i => {
|
|
i.removeDeloser(deloser);
|
|
});
|
|
|
|
this._history = this._history.filter(i => i.getLength() > 0);
|
|
}
|
|
|
|
async focusAvailable(from) {
|
|
let skip = !!from;
|
|
|
|
for (const h of this._history) {
|
|
if (from && h.hasDeloser(from)) {
|
|
skip = false;
|
|
}
|
|
|
|
if (!skip) {
|
|
const result = await h.focusAvailable(from); // Result is null when the default action is prevented by the application
|
|
// and we don't need to look further.
|
|
|
|
if (result || result === null) {
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async resetFocus(from) {
|
|
let skip = !!from;
|
|
|
|
for (const h of this._history) {
|
|
if (from && h.hasDeloser(from)) {
|
|
skip = false;
|
|
}
|
|
|
|
if (!skip && (await h.resetFocus(from))) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
function _setInformativeStyle$2(weakElement, remove, isActive, snapshotIndex) {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
const element = weakElement.get();
|
|
|
|
if (element) {
|
|
if (remove) {
|
|
element.style.removeProperty("--tabster-deloser");
|
|
} else {
|
|
element.style.setProperty("--tabster-deloser", (isActive ? "active" : "inactive") + "," + ("snapshot-" + snapshotIndex));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function buildElementSelector(element, withClass, withIndex) {
|
|
const selector = [];
|
|
const escapeRegExp = /(:|\.|\[|\]|,|=|@)/g;
|
|
const escapeReplaceValue = "\\$1";
|
|
const elementId = element.getAttribute("id");
|
|
|
|
if (elementId) {
|
|
selector.push("#" + elementId.replace(escapeRegExp, escapeReplaceValue));
|
|
}
|
|
|
|
if (withClass !== false && element.className) {
|
|
element.className.split(" ").forEach(cls => {
|
|
cls = cls.trim();
|
|
|
|
if (cls) {
|
|
selector.push("." + cls.replace(escapeRegExp, escapeReplaceValue));
|
|
}
|
|
});
|
|
}
|
|
|
|
let index = 0;
|
|
let el;
|
|
|
|
if (withIndex !== false && selector.length === 0) {
|
|
el = element;
|
|
|
|
while (el) {
|
|
index++;
|
|
el = el.previousElementSibling;
|
|
}
|
|
|
|
selector.unshift(":nth-child(" + index + ")");
|
|
}
|
|
|
|
selector.unshift(element.tagName.toLowerCase());
|
|
return selector.join("");
|
|
}
|
|
|
|
function buildSelector(element) {
|
|
if (!documentContains(element.ownerDocument, element)) {
|
|
return undefined;
|
|
}
|
|
|
|
const selector = [buildElementSelector(element)];
|
|
let node = dom.getParentNode(element);
|
|
|
|
while (node && node.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) {
|
|
// Stop at the shadow root as cross shadow selectors won't work.
|
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
const isBody = node.tagName === "BODY";
|
|
selector.unshift(buildElementSelector(node, false, !isBody));
|
|
|
|
if (isBody) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
node = dom.getParentNode(node);
|
|
}
|
|
|
|
return selector.join(" ");
|
|
}
|
|
|
|
class Deloser extends TabsterPart {
|
|
constructor(tabster, element, onDispose, props) {
|
|
super(tabster, element, props);
|
|
this._isActive = false;
|
|
this._history = [[]];
|
|
this._snapshotIndex = 0;
|
|
|
|
this.isActive = () => {
|
|
return this._isActive;
|
|
};
|
|
|
|
this.setSnapshot = index => {
|
|
this._snapshotIndex = index;
|
|
|
|
if (this._history.length > index + 1) {
|
|
this._history.splice(index + 1, this._history.length - index - 1);
|
|
}
|
|
|
|
if (!this._history[index]) {
|
|
this._history[index] = [];
|
|
}
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
_setInformativeStyle$2(this._element, false, this._isActive, this._snapshotIndex);
|
|
}
|
|
};
|
|
|
|
this.focusFirst = () => {
|
|
const e = this._element.get();
|
|
|
|
return !!e && this._tabster.focusedElement.focusFirst({
|
|
container: e
|
|
});
|
|
};
|
|
|
|
this.focusDefault = () => {
|
|
const e = this._element.get();
|
|
|
|
return !!e && this._tabster.focusedElement.focusDefault(e);
|
|
};
|
|
|
|
this.resetFocus = () => {
|
|
const e = this._element.get();
|
|
|
|
return !!e && this._tabster.focusedElement.resetFocus(e);
|
|
};
|
|
|
|
this.clearHistory = preserveExisting => {
|
|
const element = this._element.get();
|
|
|
|
if (!element) {
|
|
this._history[this._snapshotIndex] = [];
|
|
return;
|
|
}
|
|
|
|
this._history[this._snapshotIndex] = this._history[this._snapshotIndex].filter(we => {
|
|
const e = we.get();
|
|
return e && preserveExisting ? dom.nodeContains(element, e) : false;
|
|
});
|
|
};
|
|
|
|
this.uid = getElementUId(tabster.getWindow, element);
|
|
this.strategy = props.strategy || DeloserStrategies.Auto;
|
|
this._onDispose = onDispose;
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
_setInformativeStyle$2(this._element, false, this._isActive, this._snapshotIndex);
|
|
}
|
|
}
|
|
|
|
dispose() {
|
|
this._remove();
|
|
|
|
this._onDispose(this);
|
|
|
|
this._isActive = false;
|
|
this._snapshotIndex = 0;
|
|
this._props = {};
|
|
this._history = [];
|
|
}
|
|
|
|
setActive(active) {
|
|
this._isActive = active;
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
_setInformativeStyle$2(this._element, false, this._isActive, this._snapshotIndex);
|
|
}
|
|
}
|
|
|
|
getActions() {
|
|
return {
|
|
focusDefault: this.focusDefault,
|
|
focusFirst: this.focusFirst,
|
|
resetFocus: this.resetFocus,
|
|
clearHistory: this.clearHistory,
|
|
setSnapshot: this.setSnapshot,
|
|
isActive: this.isActive
|
|
};
|
|
}
|
|
|
|
unshift(element) {
|
|
let cur = this._history[this._snapshotIndex];
|
|
cur = this._history[this._snapshotIndex] = cur.filter(we => {
|
|
const e = we.get();
|
|
return e && e !== element;
|
|
});
|
|
cur.unshift(new WeakHTMLElement(this._tabster.getWindow, element, buildSelector(element)));
|
|
|
|
while (cur.length > _containerHistoryLength) {
|
|
cur.pop();
|
|
}
|
|
}
|
|
|
|
findAvailable() {
|
|
const element = this._element.get();
|
|
|
|
if (!element || !this._tabster.focusable.isVisible(element)) {
|
|
return null;
|
|
}
|
|
|
|
let restoreFocusOrder = this._props.restoreFocusOrder;
|
|
let available = null;
|
|
const ctx = RootAPI.getTabsterContext(this._tabster, element);
|
|
|
|
if (!ctx) {
|
|
return null;
|
|
}
|
|
|
|
const root = ctx.root;
|
|
const rootElement = root.getElement();
|
|
|
|
if (!rootElement) {
|
|
return null;
|
|
}
|
|
|
|
if (restoreFocusOrder === undefined) {
|
|
restoreFocusOrder = root.getProps().restoreFocusOrder;
|
|
}
|
|
|
|
if (restoreFocusOrder === RestoreFocusOrders.RootDefault) {
|
|
available = this._tabster.focusable.findDefault({
|
|
container: rootElement
|
|
});
|
|
}
|
|
|
|
if (!available && restoreFocusOrder === RestoreFocusOrders.RootFirst) {
|
|
available = this._findFirst(rootElement);
|
|
}
|
|
|
|
if (available) {
|
|
return available;
|
|
}
|
|
|
|
const availableInHistory = this._findInHistory();
|
|
|
|
if (availableInHistory && restoreFocusOrder === RestoreFocusOrders.History) {
|
|
return availableInHistory;
|
|
}
|
|
|
|
const availableDefault = this._tabster.focusable.findDefault({
|
|
container: element
|
|
});
|
|
|
|
if (availableDefault && restoreFocusOrder === RestoreFocusOrders.DeloserDefault) {
|
|
return availableDefault;
|
|
}
|
|
|
|
const availableFirst = this._findFirst(element);
|
|
|
|
if (availableFirst && restoreFocusOrder === RestoreFocusOrders.DeloserFirst) {
|
|
return availableFirst;
|
|
}
|
|
|
|
return availableDefault || availableInHistory || availableFirst || null;
|
|
}
|
|
|
|
customFocusLostHandler(element) {
|
|
return element.dispatchEvent(new DeloserFocusLostEvent(this.getActions()));
|
|
}
|
|
|
|
_findInHistory() {
|
|
const cur = this._history[this._snapshotIndex].slice(0);
|
|
|
|
this.clearHistory(true);
|
|
|
|
for (let i = 0; i < cur.length; i++) {
|
|
const we = cur[i];
|
|
const e = we.get();
|
|
|
|
const element = this._element.get();
|
|
|
|
if (e && element && dom.nodeContains(element, e)) {
|
|
if (this._tabster.focusable.isFocusable(e)) {
|
|
return e;
|
|
}
|
|
} else if (!this._props.noSelectorCheck) {
|
|
// Element is not in the DOM, try to locate the node by it's
|
|
// selector. This might return not exactly the right node,
|
|
// but it would be easily fixable by having more detailed selectors.
|
|
const selector = we.getData();
|
|
|
|
if (selector && element) {
|
|
let els;
|
|
|
|
try {
|
|
els = dom.querySelectorAll(element.ownerDocument, selector);
|
|
} catch (e) {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
// This should never happen, unless there is some bug in buildElementSelector().
|
|
console.error(`Failed to querySelectorAll('${selector}')`);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
for (let i = 0; i < els.length; i++) {
|
|
const el = els[i];
|
|
|
|
if (el && this._tabster.focusable.isFocusable(el)) {
|
|
return el;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
_findFirst(element) {
|
|
if (this._tabster.keyboardNavigation.isNavigatingWithKeyboard()) {
|
|
const first = this._tabster.focusable.findFirst({
|
|
container: element,
|
|
useActiveModalizer: true
|
|
});
|
|
|
|
if (first) {
|
|
return first;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
_remove() {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
_setInformativeStyle$2(this._element, true);
|
|
}
|
|
}
|
|
|
|
} // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
|
|
class DeloserAPI {
|
|
constructor(tabster, props) {
|
|
/**
|
|
* Tracks if focus is inside a deloser
|
|
*/
|
|
this._inDeloser = false;
|
|
this._isRestoringFocus = false;
|
|
this._isPaused = false;
|
|
|
|
this._onRestoreFocus = event => {
|
|
var _a;
|
|
|
|
const target = event.composedPath()[0];
|
|
|
|
if (target) {
|
|
const available = (_a = DeloserAPI.getDeloser(this._tabster, target)) === null || _a === void 0 ? void 0 : _a.findAvailable();
|
|
|
|
if (available) {
|
|
this._tabster.focusedElement.focus(available);
|
|
}
|
|
|
|
event.stopImmediatePropagation();
|
|
}
|
|
};
|
|
|
|
this._onFocus = e => {
|
|
if (this._restoreFocusTimer) {
|
|
this._win().clearTimeout(this._restoreFocusTimer);
|
|
|
|
this._restoreFocusTimer = undefined;
|
|
}
|
|
|
|
if (!e) {
|
|
this._scheduleRestoreFocus();
|
|
|
|
return;
|
|
}
|
|
|
|
const deloser = this._history.process(e);
|
|
|
|
if (deloser) {
|
|
this._activate(deloser);
|
|
} else {
|
|
this._deactivate();
|
|
}
|
|
};
|
|
|
|
this._onDeloserDispose = deloser => {
|
|
this._history.removeDeloser(deloser);
|
|
|
|
if (deloser.isActive()) {
|
|
this._scheduleRestoreFocus();
|
|
}
|
|
};
|
|
|
|
this._tabster = tabster;
|
|
this._win = tabster.getWindow;
|
|
this._history = new DeloserHistory(tabster);
|
|
tabster.queueInit(() => {
|
|
this._tabster.focusedElement.subscribe(this._onFocus);
|
|
|
|
const doc = this._win().document;
|
|
|
|
doc.addEventListener(DeloserRestoreFocusEventName, this._onRestoreFocus);
|
|
const activeElement = dom.getActiveElement(doc);
|
|
|
|
if (activeElement && activeElement !== doc.body) {
|
|
// Adding currently focused element to the deloser history.
|
|
this._onFocus(activeElement);
|
|
}
|
|
});
|
|
const autoDeloser = props === null || props === void 0 ? void 0 : props.autoDeloser;
|
|
|
|
if (autoDeloser) {
|
|
this._autoDeloser = autoDeloser;
|
|
}
|
|
}
|
|
|
|
dispose() {
|
|
const win = this._win();
|
|
|
|
if (this._restoreFocusTimer) {
|
|
win.clearTimeout(this._restoreFocusTimer);
|
|
this._restoreFocusTimer = undefined;
|
|
}
|
|
|
|
if (this._autoDeloserInstance) {
|
|
this._autoDeloserInstance.dispose();
|
|
|
|
delete this._autoDeloserInstance;
|
|
delete this._autoDeloser;
|
|
}
|
|
|
|
this._tabster.focusedElement.unsubscribe(this._onFocus);
|
|
|
|
win.document.removeEventListener(DeloserRestoreFocusEventName, this._onRestoreFocus);
|
|
|
|
this._history.dispose();
|
|
|
|
delete this._curDeloser;
|
|
}
|
|
|
|
createDeloser(element, props) {
|
|
var _a;
|
|
|
|
if (process.env.NODE_ENV === 'development') ;
|
|
|
|
const deloser = new Deloser(this._tabster, element, this._onDeloserDispose, props);
|
|
|
|
if (dom.nodeContains(element, (_a = this._tabster.focusedElement.getFocusedElement()) !== null && _a !== void 0 ? _a : null)) {
|
|
this._activate(deloser);
|
|
}
|
|
|
|
return deloser;
|
|
}
|
|
|
|
getActions(element) {
|
|
for (let e = element; e; e = dom.getParentElement(e)) {
|
|
const tabsterOnElement = getTabsterOnElement(this._tabster, e);
|
|
|
|
if (tabsterOnElement && tabsterOnElement.deloser) {
|
|
return tabsterOnElement.deloser.getActions();
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
pause() {
|
|
this._isPaused = true;
|
|
|
|
if (this._restoreFocusTimer) {
|
|
this._win().clearTimeout(this._restoreFocusTimer);
|
|
|
|
this._restoreFocusTimer = undefined;
|
|
}
|
|
}
|
|
|
|
resume(restore) {
|
|
this._isPaused = false;
|
|
|
|
if (restore) {
|
|
this._scheduleRestoreFocus();
|
|
}
|
|
}
|
|
/**
|
|
* Activates and sets the current deloser
|
|
*/
|
|
|
|
|
|
_activate(deloser) {
|
|
const curDeloser = this._curDeloser;
|
|
|
|
if (curDeloser !== deloser) {
|
|
this._inDeloser = true;
|
|
curDeloser === null || curDeloser === void 0 ? void 0 : curDeloser.setActive(false);
|
|
deloser.setActive(true);
|
|
this._curDeloser = deloser;
|
|
}
|
|
}
|
|
/**
|
|
* Called when focus should no longer be in a deloser
|
|
*/
|
|
|
|
|
|
_deactivate() {
|
|
var _a;
|
|
|
|
this._inDeloser = false;
|
|
(_a = this._curDeloser) === null || _a === void 0 ? void 0 : _a.setActive(false);
|
|
this._curDeloser = undefined;
|
|
}
|
|
|
|
_scheduleRestoreFocus(force) {
|
|
if (this._isPaused || this._isRestoringFocus) {
|
|
return;
|
|
}
|
|
|
|
const restoreFocus = async () => {
|
|
this._restoreFocusTimer = undefined;
|
|
|
|
const lastFocused = this._tabster.focusedElement.getLastFocusedElement();
|
|
|
|
if (!force && (this._isRestoringFocus || !this._inDeloser || lastFocused && !isDisplayNone(lastFocused))) {
|
|
return;
|
|
}
|
|
|
|
const curDeloser = this._curDeloser;
|
|
let isManual = false;
|
|
|
|
if (curDeloser) {
|
|
if (lastFocused && curDeloser.customFocusLostHandler(lastFocused)) {
|
|
return;
|
|
}
|
|
|
|
if (curDeloser.strategy === DeloserStrategies.Manual) {
|
|
isManual = true;
|
|
} else {
|
|
const curDeloserElement = curDeloser.getElement();
|
|
const el = curDeloser.findAvailable();
|
|
|
|
if (el && (!(curDeloserElement === null || curDeloserElement === void 0 ? void 0 : curDeloserElement.dispatchEvent(new TabsterMoveFocusEvent({
|
|
by: "deloser",
|
|
owner: curDeloserElement,
|
|
next: el
|
|
}))) || this._tabster.focusedElement.focus(el))) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
this._deactivate();
|
|
|
|
if (isManual) {
|
|
return;
|
|
}
|
|
|
|
this._isRestoringFocus = true; // focusAvailable returns null when the default action is prevented by the application, false
|
|
// when nothing was focused and true when something was focused.
|
|
|
|
if ((await this._history.focusAvailable(null)) === false) {
|
|
await this._history.resetFocus(null);
|
|
}
|
|
|
|
this._isRestoringFocus = false;
|
|
};
|
|
|
|
if (force) {
|
|
restoreFocus();
|
|
} else {
|
|
this._restoreFocusTimer = this._win().setTimeout(restoreFocus, 100);
|
|
}
|
|
}
|
|
|
|
static getDeloser(tabster, element) {
|
|
var _a;
|
|
|
|
let root;
|
|
|
|
for (let e = element; e; e = dom.getParentElement(e)) {
|
|
const tabsterOnElement = getTabsterOnElement(tabster, e);
|
|
|
|
if (tabsterOnElement) {
|
|
if (!root) {
|
|
root = tabsterOnElement.root;
|
|
}
|
|
|
|
const deloser = tabsterOnElement.deloser;
|
|
|
|
if (deloser) {
|
|
return deloser;
|
|
}
|
|
}
|
|
}
|
|
|
|
const deloserAPI = tabster.deloser && tabster.deloser;
|
|
|
|
if (deloserAPI) {
|
|
if (deloserAPI._autoDeloserInstance) {
|
|
return deloserAPI._autoDeloserInstance;
|
|
}
|
|
|
|
const autoDeloserProps = deloserAPI._autoDeloser;
|
|
|
|
if (root && !deloserAPI._autoDeloserInstance && autoDeloserProps) {
|
|
const body = (_a = element.ownerDocument) === null || _a === void 0 ? void 0 : _a.body;
|
|
|
|
if (body) {
|
|
deloserAPI._autoDeloserInstance = new Deloser(tabster, body, tabster.deloser._onDeloserDispose, autoDeloserProps);
|
|
}
|
|
}
|
|
|
|
return deloserAPI._autoDeloserInstance;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
static getHistory(instance) {
|
|
return instance._history;
|
|
}
|
|
|
|
static forceRestoreFocus(instance) {
|
|
instance._scheduleRestoreFocus(true);
|
|
}
|
|
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
class Subscribable {
|
|
constructor() {
|
|
this._callbacks = [];
|
|
}
|
|
|
|
dispose() {
|
|
this._callbacks = [];
|
|
delete this._val;
|
|
}
|
|
|
|
subscribe(callback) {
|
|
const callbacks = this._callbacks;
|
|
const index = callbacks.indexOf(callback);
|
|
|
|
if (index < 0) {
|
|
callbacks.push(callback);
|
|
}
|
|
}
|
|
|
|
subscribeFirst(callback) {
|
|
const callbacks = this._callbacks;
|
|
const index = callbacks.indexOf(callback);
|
|
|
|
if (index >= 0) {
|
|
callbacks.splice(index, 1);
|
|
}
|
|
|
|
callbacks.unshift(callback);
|
|
}
|
|
|
|
unsubscribe(callback) {
|
|
const index = this._callbacks.indexOf(callback);
|
|
|
|
if (index >= 0) {
|
|
this._callbacks.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
setVal(val, detail) {
|
|
if (this._val === val) {
|
|
return;
|
|
}
|
|
|
|
this._val = val;
|
|
|
|
this._callCallbacks(val, detail);
|
|
}
|
|
|
|
getVal() {
|
|
return this._val;
|
|
}
|
|
|
|
trigger(val, detail) {
|
|
this._callCallbacks(val, detail);
|
|
}
|
|
|
|
_callCallbacks(val, detail) {
|
|
this._callbacks.forEach(callback => callback(val, detail));
|
|
}
|
|
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
const _transactionTimeout = 1500;
|
|
const _pingTimeout = 3000;
|
|
const _targetIdUp = "up";
|
|
const CrossOriginTransactionTypes = {
|
|
Bootstrap: 1,
|
|
FocusElement: 2,
|
|
State: 3,
|
|
GetElement: 4,
|
|
RestoreFocusInDeloser: 5,
|
|
Ping: 6
|
|
};
|
|
|
|
class CrossOriginDeloserItem extends DeloserItemBase {
|
|
constructor(tabster, deloser, trasactions) {
|
|
super();
|
|
this._deloser = deloser;
|
|
this._transactions = trasactions;
|
|
}
|
|
|
|
belongsTo(deloser) {
|
|
return deloser.deloserUId === this._deloser.deloserUId;
|
|
}
|
|
|
|
async focusAvailable() {
|
|
const data = { ...this._deloser,
|
|
reset: false
|
|
};
|
|
return this._transactions.beginTransaction(RestoreFocusInDeloserTransaction, data).then(value => !!value);
|
|
}
|
|
|
|
async resetFocus() {
|
|
const data = { ...this._deloser,
|
|
reset: true
|
|
};
|
|
return this._transactions.beginTransaction(RestoreFocusInDeloserTransaction, data).then(value => !!value);
|
|
}
|
|
|
|
}
|
|
|
|
class CrossOriginDeloserHistoryByRoot extends DeloserHistoryByRootBase {
|
|
constructor(tabster, rootUId, transactions) {
|
|
super(tabster, rootUId);
|
|
this._transactions = transactions;
|
|
}
|
|
|
|
unshift(deloser) {
|
|
let item;
|
|
|
|
for (let i = 0; i < this._history.length; i++) {
|
|
if (this._history[i].belongsTo(deloser)) {
|
|
item = this._history[i];
|
|
|
|
this._history.splice(i, 1);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!item) {
|
|
item = new CrossOriginDeloserItem(this._tabster, deloser, this._transactions);
|
|
}
|
|
|
|
this._history.unshift(item);
|
|
|
|
this._history.splice(10, this._history.length - 10);
|
|
}
|
|
|
|
async focusAvailable() {
|
|
for (const i of this._history) {
|
|
if (await i.focusAvailable()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async resetFocus() {
|
|
for (const i of this._history) {
|
|
if (await i.resetFocus()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
class CrossOriginTransaction {
|
|
constructor(tabster, getOwner, knownTargets, value, timeout, sentTo, targetId, sendUp) {
|
|
this._inProgress = {};
|
|
this._isDone = false;
|
|
this._isSelfResponding = false;
|
|
this._sentCount = 0;
|
|
this.tabster = tabster;
|
|
this.owner = getOwner;
|
|
this.ownerId = getWindowUId(getOwner());
|
|
this.id = getUId(getOwner());
|
|
this.beginData = value;
|
|
this._knownTargets = knownTargets;
|
|
this._sentTo = sentTo || {
|
|
[this.ownerId]: true
|
|
};
|
|
this.targetId = targetId;
|
|
this.sendUp = sendUp;
|
|
this.timeout = timeout;
|
|
this._promise = new (getPromise(getOwner))((resolve, reject) => {
|
|
this._resolve = resolve;
|
|
this._reject = reject;
|
|
});
|
|
}
|
|
|
|
getTargets(knownTargets) {
|
|
return this.targetId === _targetIdUp ? this.sendUp ? {
|
|
[_targetIdUp]: {
|
|
send: this.sendUp
|
|
}
|
|
} : null : this.targetId ? knownTargets[this.targetId] ? {
|
|
[this.targetId]: {
|
|
send: knownTargets[this.targetId].send
|
|
}
|
|
} : null : Object.keys(knownTargets).length === 0 && this.sendUp ? {
|
|
[_targetIdUp]: {
|
|
send: this.sendUp
|
|
}
|
|
} : Object.keys(knownTargets).length > 0 ? knownTargets : null;
|
|
}
|
|
|
|
begin(selfResponse) {
|
|
const targets = this.getTargets(this._knownTargets);
|
|
const sentTo = { ...this._sentTo
|
|
};
|
|
|
|
if (targets) {
|
|
for (const id of Object.keys(targets)) {
|
|
sentTo[id] = true;
|
|
}
|
|
}
|
|
|
|
const data = {
|
|
transaction: this.id,
|
|
type: this.type,
|
|
isResponse: false,
|
|
timestamp: Date.now(),
|
|
owner: this.ownerId,
|
|
sentto: sentTo,
|
|
timeout: this.timeout,
|
|
beginData: this.beginData
|
|
};
|
|
|
|
if (this.targetId) {
|
|
data.target = this.targetId;
|
|
}
|
|
|
|
if (selfResponse) {
|
|
this._isSelfResponding = true;
|
|
selfResponse(data).then(value => {
|
|
this._isSelfResponding = false;
|
|
|
|
if (value !== undefined) {
|
|
if (!this.endData) {
|
|
this.endData = value;
|
|
}
|
|
}
|
|
|
|
if (this.endData || this._sentCount === 0) {
|
|
this.end();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (targets) {
|
|
for (const id of Object.keys(targets)) {
|
|
if (!(id in this._sentTo)) {
|
|
this._send(targets[id].send, id, data);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this._sentCount === 0 && !this._isSelfResponding) {
|
|
this.end();
|
|
}
|
|
|
|
return this._promise;
|
|
}
|
|
|
|
_send(send, targetId, data) {
|
|
if (this._inProgress[targetId] === undefined) {
|
|
this._inProgress[targetId] = true;
|
|
this._sentCount++;
|
|
send(data);
|
|
}
|
|
}
|
|
|
|
end(error) {
|
|
if (this._isDone) {
|
|
return;
|
|
}
|
|
|
|
this._isDone = true;
|
|
|
|
if (this.endData === undefined && error) {
|
|
if (this._reject) {
|
|
this._reject(error);
|
|
}
|
|
} else if (this._resolve) {
|
|
this._resolve(this.endData);
|
|
}
|
|
}
|
|
|
|
onResponse(data) {
|
|
const endData = data.endData;
|
|
|
|
if (endData !== undefined && !this.endData) {
|
|
this.endData = endData;
|
|
}
|
|
|
|
const inProgressId = data.target === _targetIdUp ? _targetIdUp : data.owner;
|
|
|
|
if (this._inProgress[inProgressId]) {
|
|
this._inProgress[inProgressId] = false;
|
|
this._sentCount--;
|
|
|
|
if (this.endData || this._sentCount === 0 && !this._isSelfResponding) {
|
|
this.end();
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
class BootstrapTransaction extends CrossOriginTransaction {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.type = CrossOriginTransactionTypes.Bootstrap;
|
|
}
|
|
|
|
static shouldForward() {
|
|
return false;
|
|
}
|
|
|
|
static async makeResponse(tabster) {
|
|
return {
|
|
isNavigatingWithKeyboard: tabster.keyboardNavigation.isNavigatingWithKeyboard()
|
|
};
|
|
}
|
|
|
|
}
|
|
|
|
class FocusElementTransaction extends CrossOriginTransaction {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.type = CrossOriginTransactionTypes.FocusElement;
|
|
}
|
|
|
|
static shouldSelfRespond() {
|
|
return true;
|
|
}
|
|
|
|
static shouldForward(tabster, data, getOwner) {
|
|
const el = GetElementTransaction.findElement(tabster, getOwner, data.beginData);
|
|
return !el || !tabster.focusable.isFocusable(el);
|
|
}
|
|
|
|
static async makeResponse(tabster, data, getOwner, ownerId, transactions, forwardResult) {
|
|
const el = GetElementTransaction.findElement(tabster, getOwner, data.beginData);
|
|
return !!el && tabster.focusedElement.focus(el, true) || !!(await forwardResult);
|
|
}
|
|
|
|
}
|
|
|
|
const CrossOriginStates = {
|
|
Focused: 1,
|
|
Blurred: 2,
|
|
Observed: 3,
|
|
DeadWindow: 4,
|
|
KeyboardNavigation: 5,
|
|
Outline: 6
|
|
};
|
|
|
|
class StateTransaction extends CrossOriginTransaction {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.type = CrossOriginTransactionTypes.State;
|
|
}
|
|
|
|
static shouldSelfRespond(tabster, data) {
|
|
return data.state !== CrossOriginStates.DeadWindow && data.state !== CrossOriginStates.KeyboardNavigation;
|
|
}
|
|
|
|
static async makeResponse(tabster, data, getOwner, ownerId, transactions, forwardResult, isSelfResponse) {
|
|
const timestamp = data.timestamp;
|
|
const beginData = data.beginData;
|
|
|
|
if (timestamp && beginData) {
|
|
switch (beginData.state) {
|
|
case CrossOriginStates.Focused:
|
|
return StateTransaction._makeFocusedResponse(tabster, timestamp, beginData, transactions, isSelfResponse);
|
|
|
|
case CrossOriginStates.Blurred:
|
|
return StateTransaction._makeBlurredResponse(tabster, timestamp, beginData, transactions.ctx);
|
|
|
|
case CrossOriginStates.Observed:
|
|
return StateTransaction._makeObservedResponse(tabster, beginData);
|
|
|
|
case CrossOriginStates.DeadWindow:
|
|
return StateTransaction._makeDeadWindowResponse(tabster, beginData, transactions, forwardResult);
|
|
|
|
case CrossOriginStates.KeyboardNavigation:
|
|
return StateTransaction._makeKeyboardNavigationResponse(tabster, transactions.ctx, beginData.isNavigatingWithKeyboard);
|
|
|
|
case CrossOriginStates.Outline:
|
|
return StateTransaction._makeOutlineResponse(tabster, transactions.ctx, beginData.outline);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static createElement(tabster, beginData) {
|
|
return beginData.uid ? new CrossOriginElement(tabster, beginData.uid, beginData.ownerUId, beginData.id, beginData.rootUId, beginData.observedName, beginData.observedDetails) : null;
|
|
}
|
|
|
|
static async _makeFocusedResponse(tabster, timestamp, beginData, transactions, isSelfResponse) {
|
|
const element = StateTransaction.createElement(tabster, beginData);
|
|
|
|
if (beginData && beginData.ownerUId && element) {
|
|
transactions.ctx.focusOwner = beginData.ownerUId;
|
|
transactions.ctx.focusOwnerTimestamp = timestamp;
|
|
|
|
if (!isSelfResponse && beginData.rootUId && beginData.deloserUId) {
|
|
const deloserAPI = tabster.deloser;
|
|
|
|
if (deloserAPI) {
|
|
const history = DeloserAPI.getHistory(deloserAPI);
|
|
const deloser = {
|
|
ownerUId: beginData.ownerUId,
|
|
deloserUId: beginData.deloserUId,
|
|
rootUId: beginData.rootUId
|
|
};
|
|
const historyItem = history.make(beginData.rootUId, () => new CrossOriginDeloserHistoryByRoot(tabster, deloser.rootUId, transactions));
|
|
historyItem.unshift(deloser);
|
|
}
|
|
}
|
|
|
|
CrossOriginFocusedElementState.setVal( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
tabster.crossOrigin.focusedElement, element, {
|
|
isFocusedProgrammatically: beginData.isFocusedProgrammatically
|
|
});
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static async _makeBlurredResponse(tabster, timestamp, beginData, context) {
|
|
if (beginData && (beginData.ownerUId === context.focusOwner || beginData.force) && (!context.focusOwnerTimestamp || context.focusOwnerTimestamp < timestamp)) {
|
|
CrossOriginFocusedElementState.setVal( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
tabster.crossOrigin.focusedElement, undefined, {});
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static async _makeObservedResponse(tabster, beginData) {
|
|
const name = beginData.observedName;
|
|
const element = StateTransaction.createElement(tabster, beginData);
|
|
|
|
if (name && element) {
|
|
CrossOriginObservedElementState.trigger( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
tabster.crossOrigin.observedElement, element, {
|
|
names: [name],
|
|
details: beginData.observedDetails
|
|
});
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static async _makeDeadWindowResponse(tabster, beginData, transactions, forwardResult) {
|
|
const deadUId = beginData && beginData.ownerUId;
|
|
|
|
if (deadUId) {
|
|
transactions.removeTarget(deadUId);
|
|
}
|
|
|
|
return forwardResult.then(() => {
|
|
if (deadUId === transactions.ctx.focusOwner) {
|
|
const deloserAPI = tabster.deloser;
|
|
|
|
if (deloserAPI) {
|
|
DeloserAPI.forceRestoreFocus(deloserAPI);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
static async _makeKeyboardNavigationResponse(tabster, context, isNavigatingWithKeyboard) {
|
|
if (isNavigatingWithKeyboard !== undefined && tabster.keyboardNavigation.isNavigatingWithKeyboard() !== isNavigatingWithKeyboard) {
|
|
context.ignoreKeyboardNavigationStateUpdate = true;
|
|
tabster.keyboardNavigation.setNavigatingWithKeyboard(isNavigatingWithKeyboard);
|
|
context.ignoreKeyboardNavigationStateUpdate = false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static async _makeOutlineResponse(tabster, context, props) {
|
|
if (context.origOutlineSetup) {
|
|
context.origOutlineSetup.call( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
tabster.outline, props);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
}
|
|
|
|
class GetElementTransaction extends CrossOriginTransaction {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.type = CrossOriginTransactionTypes.GetElement;
|
|
}
|
|
|
|
static shouldSelfRespond() {
|
|
return true;
|
|
}
|
|
|
|
static findElement(tabster, getOwner, data) {
|
|
let element;
|
|
|
|
if (data && (!data.ownerId || data.ownerId === getWindowUId(getOwner()))) {
|
|
if (data.id) {
|
|
element = dom.getElementById(getOwner().document, data.id);
|
|
|
|
if (element && data.rootId) {
|
|
const ctx = RootAPI.getTabsterContext(tabster, element);
|
|
|
|
if (!ctx || ctx.root.uid !== data.rootId) {
|
|
return null;
|
|
}
|
|
}
|
|
} else if (data.uid) {
|
|
const ref = getInstanceContext(getOwner).elementByUId[data.uid];
|
|
element = ref && ref.get();
|
|
} else if (data.observedName) {
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
element = tabster.observedElement.getElement(data.observedName, data.accessibility);
|
|
}
|
|
}
|
|
|
|
return element || null;
|
|
}
|
|
|
|
static getElementData(tabster, element, getOwner, context, ownerUId) {
|
|
const deloser = DeloserAPI.getDeloser(tabster, element);
|
|
const ctx = RootAPI.getTabsterContext(tabster, element);
|
|
const tabsterOnElement = getTabsterOnElement(tabster, element);
|
|
const observed = tabsterOnElement && tabsterOnElement.observed;
|
|
return {
|
|
uid: getElementUId(getOwner, element),
|
|
ownerUId,
|
|
id: element.id || undefined,
|
|
rootUId: ctx ? ctx.root.uid : undefined,
|
|
deloserUId: deloser ? getDeloserUID(getOwner, context, deloser) : undefined,
|
|
observedName: observed && observed.names && observed.names[0],
|
|
observedDetails: observed && observed.details
|
|
};
|
|
}
|
|
|
|
static async makeResponse(tabster, data, getOwner, ownerUId, transactions, forwardResult) {
|
|
const beginData = data.beginData;
|
|
let element;
|
|
let dataOut;
|
|
|
|
if (beginData === undefined) {
|
|
element = tabster.focusedElement.getFocusedElement();
|
|
} else if (beginData) {
|
|
element = GetElementTransaction.findElement(tabster, getOwner, beginData) || undefined;
|
|
}
|
|
|
|
if (!element && beginData) {
|
|
const name = beginData.observedName;
|
|
const timeout = data.timeout;
|
|
const accessibility = beginData.accessibility;
|
|
|
|
if (name && timeout) {
|
|
const e = await new (getPromise(getOwner))(resolve => {
|
|
let isWaitElementResolved = false;
|
|
let isForwardResolved = false;
|
|
let isResolved = false; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
|
|
tabster.observedElement.waitElement(name, timeout, accessibility).result.then(value => {
|
|
isWaitElementResolved = true;
|
|
|
|
if (!isResolved && (value || isForwardResolved)) {
|
|
isResolved = true;
|
|
resolve({
|
|
element: value
|
|
});
|
|
}
|
|
});
|
|
forwardResult.then(value => {
|
|
isForwardResolved = true;
|
|
|
|
if (!isResolved && (value || isWaitElementResolved)) {
|
|
isResolved = true;
|
|
resolve({
|
|
crossOrigin: value
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
if (e.element) {
|
|
element = e.element;
|
|
} else if (e.crossOrigin) {
|
|
dataOut = e.crossOrigin;
|
|
}
|
|
}
|
|
}
|
|
|
|
return element ? GetElementTransaction.getElementData(tabster, element, getOwner, transactions.ctx, ownerUId) : dataOut;
|
|
}
|
|
|
|
}
|
|
|
|
class RestoreFocusInDeloserTransaction extends CrossOriginTransaction {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.type = CrossOriginTransactionTypes.RestoreFocusInDeloser;
|
|
}
|
|
|
|
static async makeResponse(tabster, data, getOwner, ownerId, transactions, forwardResult) {
|
|
const forwardRet = await forwardResult;
|
|
const begin = !forwardRet && data.beginData;
|
|
const uid = begin && begin.deloserUId;
|
|
const deloser = uid && transactions.ctx.deloserByUId[uid];
|
|
const deloserAPI = tabster.deloser;
|
|
|
|
if (begin && deloser && deloserAPI) {
|
|
const history = DeloserAPI.getHistory(deloserAPI);
|
|
return begin.reset ? history.resetFocus(deloser) : history.focusAvailable(deloser);
|
|
}
|
|
|
|
return !!forwardRet;
|
|
}
|
|
|
|
}
|
|
|
|
class PingTransaction extends CrossOriginTransaction {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.type = CrossOriginTransactionTypes.Ping;
|
|
}
|
|
|
|
static shouldForward() {
|
|
return false;
|
|
}
|
|
|
|
static async makeResponse() {
|
|
return true;
|
|
}
|
|
|
|
}
|
|
|
|
class CrossOriginTransactions {
|
|
constructor(tabster, getOwner, context) {
|
|
this._knownTargets = {};
|
|
this._transactions = {};
|
|
this._isDefaultSendUp = false;
|
|
this.isSetUp = false;
|
|
|
|
this._onMessage = e => {
|
|
if (e.data.owner === this._ownerUId || !this._tabster) {
|
|
return;
|
|
} // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
const data = e.data;
|
|
let transactionId;
|
|
|
|
if (!data || !(transactionId = data.transaction) || !data.type || !data.timestamp || !data.owner || !data.sentto) {
|
|
return;
|
|
}
|
|
|
|
let knownTarget = this._knownTargets[data.owner];
|
|
|
|
if (!knownTarget && e.send && data.owner !== this._ownerUId) {
|
|
knownTarget = this._knownTargets[data.owner] = {
|
|
send: e.send
|
|
};
|
|
}
|
|
|
|
if (knownTarget) {
|
|
knownTarget.last = Date.now();
|
|
}
|
|
|
|
if (data.isResponse) {
|
|
const t = this._transactions[transactionId];
|
|
|
|
if (t && t.transaction && t.transaction.type === data.type) {
|
|
t.transaction.onResponse(data);
|
|
}
|
|
} else {
|
|
const Transaction = this._getTransactionClass(data.type);
|
|
|
|
const forwardResult = this.forwardTransaction(data);
|
|
|
|
if (Transaction && e.send) {
|
|
Transaction.makeResponse(this._tabster, data, this._owner, this._ownerUId, this, forwardResult, false).then(r => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const response = {
|
|
transaction: data.transaction,
|
|
type: data.type,
|
|
isResponse: true,
|
|
timestamp: Date.now(),
|
|
owner: this._ownerUId,
|
|
timeout: data.timeout,
|
|
sentto: {},
|
|
target: data.target === _targetIdUp ? _targetIdUp : data.owner,
|
|
endData: r
|
|
};
|
|
e.send(response);
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
this._onPageHide = () => {
|
|
this._dead();
|
|
};
|
|
|
|
this._onBrowserMessage = e => {
|
|
if (e.source === this._owner()) {
|
|
return;
|
|
} // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
const send = data => {
|
|
if (e.source && e.source.postMessage) {
|
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
e.source.postMessage(JSON.stringify(data), "*");
|
|
}
|
|
};
|
|
|
|
try {
|
|
this._onMessage({
|
|
data: JSON.parse(e.data),
|
|
send
|
|
});
|
|
} catch (e) {
|
|
/* Ignore */
|
|
}
|
|
};
|
|
|
|
this._tabster = tabster;
|
|
this._owner = getOwner;
|
|
this._ownerUId = getWindowUId(getOwner());
|
|
this.ctx = context;
|
|
}
|
|
|
|
setup(sendUp) {
|
|
if (this.isSetUp) {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.error("CrossOrigin is already set up.");
|
|
}
|
|
} else {
|
|
this.isSetUp = true;
|
|
this.setSendUp(sendUp);
|
|
|
|
this._owner().addEventListener("pagehide", this._onPageHide);
|
|
|
|
this._ping();
|
|
}
|
|
|
|
return this._onMessage;
|
|
}
|
|
|
|
setSendUp(sendUp) {
|
|
if (!this.isSetUp) {
|
|
throw new Error("CrossOrigin is not set up.");
|
|
}
|
|
|
|
this.sendUp = sendUp || undefined;
|
|
|
|
const owner = this._owner();
|
|
|
|
if (sendUp === undefined) {
|
|
if (!this._isDefaultSendUp) {
|
|
if (owner.document) {
|
|
this._isDefaultSendUp = true;
|
|
|
|
if (owner.parent && owner.parent !== owner && owner.parent.postMessage) {
|
|
this.sendUp = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
data) => {
|
|
owner.parent.postMessage(JSON.stringify(data), "*");
|
|
};
|
|
}
|
|
|
|
owner.addEventListener("message", this._onBrowserMessage);
|
|
}
|
|
}
|
|
} else if (this._isDefaultSendUp) {
|
|
owner.removeEventListener("message", this._onBrowserMessage);
|
|
this._isDefaultSendUp = false;
|
|
}
|
|
|
|
return this._onMessage;
|
|
}
|
|
|
|
async dispose() {
|
|
const owner = this._owner();
|
|
|
|
if (this._pingTimer) {
|
|
owner.clearTimeout(this._pingTimer);
|
|
this._pingTimer = undefined;
|
|
}
|
|
|
|
owner.removeEventListener("message", this._onBrowserMessage);
|
|
owner.removeEventListener("pagehide", this._onPageHide);
|
|
await this._dead();
|
|
delete this._deadPromise;
|
|
|
|
for (const id of Object.keys(this._transactions)) {
|
|
const t = this._transactions[id];
|
|
|
|
if (t.timer) {
|
|
owner.clearTimeout(t.timer);
|
|
delete t.timer;
|
|
}
|
|
|
|
t.transaction.end();
|
|
}
|
|
|
|
this._knownTargets = {};
|
|
delete this.sendUp;
|
|
}
|
|
|
|
beginTransaction(Transaction, value, timeout, sentTo, targetId, withReject) {
|
|
if (!this._owner) {
|
|
return getPromise(this._owner).reject();
|
|
}
|
|
|
|
const transaction = new Transaction(this._tabster, this._owner, this._knownTargets, value, timeout, sentTo, targetId, this.sendUp);
|
|
let selfResponse;
|
|
|
|
if (Transaction.shouldSelfRespond && Transaction.shouldSelfRespond(this._tabster, value, this._owner, this._ownerUId)) {
|
|
selfResponse = data => {
|
|
return Transaction.makeResponse(this._tabster, data, this._owner, this._ownerUId, this, getPromise(this._owner).resolve(undefined), true);
|
|
};
|
|
}
|
|
|
|
return this._beginTransaction(transaction, timeout, selfResponse, withReject);
|
|
}
|
|
|
|
removeTarget(uid) {
|
|
delete this._knownTargets[uid];
|
|
}
|
|
|
|
_beginTransaction(transaction, timeout, selfResponse, withReject) {
|
|
const owner = this._owner();
|
|
|
|
const wrapper = {
|
|
transaction,
|
|
timer: owner.setTimeout(() => {
|
|
delete wrapper.timer;
|
|
transaction.end("Cross origin transaction timed out.");
|
|
}, _transactionTimeout + (timeout || 0))
|
|
};
|
|
this._transactions[transaction.id] = wrapper;
|
|
const ret = transaction.begin(selfResponse);
|
|
ret.catch(() => {
|
|
/**/
|
|
}).finally(() => {
|
|
if (wrapper.timer) {
|
|
owner.clearTimeout(wrapper.timer);
|
|
}
|
|
|
|
delete this._transactions[transaction.id];
|
|
});
|
|
return ret.then(value => value, withReject ? undefined : () => undefined);
|
|
}
|
|
|
|
forwardTransaction( // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
data // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
) {
|
|
const owner = this._owner;
|
|
let targetId = data.target;
|
|
|
|
if (targetId === this._ownerUId) {
|
|
return getPromise(owner).resolve();
|
|
}
|
|
|
|
const Transaction = this._getTransactionClass(data.type);
|
|
|
|
if (Transaction) {
|
|
if (Transaction.shouldForward === undefined || Transaction.shouldForward(this._tabster, data, owner, this._ownerUId)) {
|
|
const sentTo = data.sentto;
|
|
|
|
if (targetId === _targetIdUp) {
|
|
targetId = undefined;
|
|
sentTo[this._ownerUId] = true;
|
|
}
|
|
|
|
delete sentTo[_targetIdUp];
|
|
return this._beginTransaction(new Transaction(this._tabster, owner, this._knownTargets, data.beginData, data.timeout, sentTo, targetId, this.sendUp), data.timeout);
|
|
} else {
|
|
return getPromise(owner).resolve();
|
|
}
|
|
}
|
|
|
|
return getPromise(owner).reject(`Unknown transaction type ${data.type}`);
|
|
}
|
|
|
|
_getTransactionClass(type // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
) {
|
|
switch (type) {
|
|
case CrossOriginTransactionTypes.Bootstrap:
|
|
return BootstrapTransaction;
|
|
|
|
case CrossOriginTransactionTypes.FocusElement:
|
|
return FocusElementTransaction;
|
|
|
|
case CrossOriginTransactionTypes.State:
|
|
return StateTransaction;
|
|
|
|
case CrossOriginTransactionTypes.GetElement:
|
|
return GetElementTransaction;
|
|
|
|
case CrossOriginTransactionTypes.RestoreFocusInDeloser:
|
|
return RestoreFocusInDeloserTransaction;
|
|
|
|
case CrossOriginTransactionTypes.Ping:
|
|
return PingTransaction;
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async _dead() {
|
|
if (!this._deadPromise && this.ctx.focusOwner === this._ownerUId) {
|
|
this._deadPromise = this.beginTransaction(StateTransaction, {
|
|
ownerUId: this._ownerUId,
|
|
state: CrossOriginStates.DeadWindow
|
|
});
|
|
}
|
|
|
|
if (this._deadPromise) {
|
|
await this._deadPromise;
|
|
}
|
|
}
|
|
|
|
async _ping() {
|
|
if (this._pingTimer) {
|
|
return;
|
|
}
|
|
|
|
let deadWindows;
|
|
const now = Date.now();
|
|
const targets = Object.keys(this._knownTargets).filter(uid => now - (this._knownTargets[uid].last || 0) > _pingTimeout);
|
|
|
|
if (this.sendUp) {
|
|
targets.push(_targetIdUp);
|
|
}
|
|
|
|
if (targets.length) {
|
|
await getPromise(this._owner).all(targets.map(uid => this.beginTransaction(PingTransaction, undefined, undefined, undefined, uid, true).then(() => true, () => {
|
|
if (uid !== _targetIdUp) {
|
|
if (!deadWindows) {
|
|
deadWindows = {};
|
|
}
|
|
|
|
deadWindows[uid] = true;
|
|
delete this._knownTargets[uid];
|
|
}
|
|
|
|
return false;
|
|
})));
|
|
}
|
|
|
|
if (deadWindows) {
|
|
const focused = await this.beginTransaction(GetElementTransaction, undefined);
|
|
|
|
if (!focused && this.ctx.focusOwner && this.ctx.focusOwner in deadWindows) {
|
|
await this.beginTransaction(StateTransaction, {
|
|
ownerUId: this._ownerUId,
|
|
state: CrossOriginStates.Blurred,
|
|
force: true
|
|
});
|
|
const deloserAPI = this._tabster.deloser;
|
|
|
|
if (deloserAPI) {
|
|
DeloserAPI.forceRestoreFocus(deloserAPI);
|
|
}
|
|
}
|
|
}
|
|
|
|
this._pingTimer = this._owner().setTimeout(() => {
|
|
this._pingTimer = undefined;
|
|
|
|
this._ping();
|
|
}, _pingTimeout);
|
|
}
|
|
|
|
}
|
|
|
|
class CrossOriginElement {
|
|
constructor(tabster, uid, ownerId, id, rootId, observedName, observedDetails) {
|
|
this._tabster = tabster;
|
|
this.uid = uid;
|
|
this.ownerId = ownerId;
|
|
this.id = id;
|
|
this.rootId = rootId;
|
|
this.observedName = observedName;
|
|
this.observedDetails = observedDetails;
|
|
}
|
|
|
|
focus(noFocusedProgrammaticallyFlag, noAccessibleCheck) {
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
return this._tabster.crossOrigin.focusedElement.focus(this, noFocusedProgrammaticallyFlag, noAccessibleCheck);
|
|
}
|
|
|
|
}
|
|
class CrossOriginFocusedElementState extends Subscribable {
|
|
constructor(transactions) {
|
|
super();
|
|
this._transactions = transactions;
|
|
}
|
|
|
|
async focus(element, noFocusedProgrammaticallyFlag, noAccessibleCheck) {
|
|
return this._focus({
|
|
uid: element.uid,
|
|
id: element.id,
|
|
rootId: element.rootId,
|
|
ownerId: element.ownerId,
|
|
observedName: element.observedName
|
|
}, noFocusedProgrammaticallyFlag, noAccessibleCheck);
|
|
}
|
|
|
|
async focusById(elementId, rootId, noFocusedProgrammaticallyFlag, noAccessibleCheck) {
|
|
return this._focus({
|
|
id: elementId,
|
|
rootId
|
|
}, noFocusedProgrammaticallyFlag, noAccessibleCheck);
|
|
}
|
|
|
|
async focusByObservedName(observedName, timeout, rootId, noFocusedProgrammaticallyFlag, noAccessibleCheck) {
|
|
return this._focus({
|
|
observedName,
|
|
rootId
|
|
}, noFocusedProgrammaticallyFlag, noAccessibleCheck, timeout);
|
|
}
|
|
|
|
async _focus(elementData, noFocusedProgrammaticallyFlag, noAccessibleCheck, timeout) {
|
|
return this._transactions.beginTransaction(FocusElementTransaction, { ...elementData,
|
|
noFocusedProgrammaticallyFlag,
|
|
noAccessibleCheck
|
|
}, timeout).then(value => !!value);
|
|
}
|
|
|
|
static setVal(instance, val, detail) {
|
|
instance.setVal(val, detail);
|
|
}
|
|
|
|
}
|
|
class CrossOriginObservedElementState extends Subscribable {
|
|
constructor(tabster, transactions) {
|
|
super();
|
|
this._lastRequestFocusId = 0;
|
|
this._tabster = tabster;
|
|
this._transactions = transactions;
|
|
}
|
|
|
|
async getElement(observedName, accessibility) {
|
|
return this.waitElement(observedName, 0, accessibility);
|
|
}
|
|
|
|
async waitElement(observedName, timeout, accessibility) {
|
|
return this._transactions.beginTransaction(GetElementTransaction, {
|
|
observedName,
|
|
accessibility
|
|
}, timeout).then(value => value ? StateTransaction.createElement(this._tabster, value) : null);
|
|
}
|
|
|
|
async requestFocus(observedName, timeout) {
|
|
const requestId = ++this._lastRequestFocusId;
|
|
return this.waitElement(observedName, timeout, ObservedElementAccessibilities.Focusable).then(element => this._lastRequestFocusId === requestId && element ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
this._tabster.crossOrigin.focusedElement.focus(element, true) : false);
|
|
}
|
|
|
|
static trigger(instance, element, details) {
|
|
instance.trigger(element, details);
|
|
}
|
|
|
|
}
|
|
class CrossOriginAPI {
|
|
constructor(tabster) {
|
|
this._init = () => {
|
|
const tabster = this._tabster;
|
|
tabster.keyboardNavigation.subscribe(this._onKeyboardNavigationStateChanged);
|
|
tabster.focusedElement.subscribe(this._onFocus); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
|
|
tabster.observedElement.subscribe(this._onObserved);
|
|
|
|
if (!this._ctx.origOutlineSetup) {
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
this._ctx.origOutlineSetup = tabster.outline.setup; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
|
|
tabster.outline.setup = this._outlineSetup;
|
|
}
|
|
|
|
this._transactions.beginTransaction(BootstrapTransaction, undefined, undefined, undefined, _targetIdUp).then(data => {
|
|
if (data && this._tabster.keyboardNavigation.isNavigatingWithKeyboard() !== data.isNavigatingWithKeyboard) {
|
|
this._ctx.ignoreKeyboardNavigationStateUpdate = true;
|
|
|
|
this._tabster.keyboardNavigation.setNavigatingWithKeyboard(data.isNavigatingWithKeyboard);
|
|
|
|
this._ctx.ignoreKeyboardNavigationStateUpdate = false;
|
|
}
|
|
});
|
|
};
|
|
|
|
this._onKeyboardNavigationStateChanged = value => {
|
|
if (!this._ctx.ignoreKeyboardNavigationStateUpdate) {
|
|
this._transactions.beginTransaction(StateTransaction, {
|
|
state: CrossOriginStates.KeyboardNavigation,
|
|
ownerUId: getWindowUId(this._win()),
|
|
isNavigatingWithKeyboard: value
|
|
});
|
|
}
|
|
};
|
|
|
|
this._onFocus = element => {
|
|
const win = this._win();
|
|
|
|
const ownerUId = getWindowUId(win);
|
|
|
|
if (this._blurTimer) {
|
|
win.clearTimeout(this._blurTimer);
|
|
this._blurTimer = undefined;
|
|
}
|
|
|
|
if (element) {
|
|
this._transactions.beginTransaction(StateTransaction, { ...GetElementTransaction.getElementData(this._tabster, element, this._win, this._ctx, ownerUId),
|
|
state: CrossOriginStates.Focused
|
|
});
|
|
} else {
|
|
this._blurTimer = win.setTimeout(() => {
|
|
this._blurTimer = undefined;
|
|
|
|
if (this._ctx.focusOwner && this._ctx.focusOwner === ownerUId) {
|
|
this._transactions.beginTransaction(GetElementTransaction, undefined).then(value => {
|
|
if (!value && this._ctx.focusOwner === ownerUId) {
|
|
this._transactions.beginTransaction(StateTransaction, {
|
|
ownerUId,
|
|
state: CrossOriginStates.Blurred,
|
|
force: false
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}, 0);
|
|
}
|
|
};
|
|
|
|
this._onObserved = (element, details) => {
|
|
var _a;
|
|
|
|
const d = GetElementTransaction.getElementData(this._tabster, element, this._win, this._ctx, getWindowUId(this._win()));
|
|
d.state = CrossOriginStates.Observed;
|
|
d.observedName = (_a = details.names) === null || _a === void 0 ? void 0 : _a[0];
|
|
d.observedDetails = details.details;
|
|
|
|
this._transactions.beginTransaction(StateTransaction, d);
|
|
};
|
|
|
|
this._outlineSetup = props => {
|
|
this._transactions.beginTransaction(StateTransaction, {
|
|
state: CrossOriginStates.Outline,
|
|
ownerUId: getWindowUId(this._win()),
|
|
outline: props
|
|
});
|
|
};
|
|
|
|
this._tabster = tabster;
|
|
this._win = tabster.getWindow;
|
|
this._ctx = {
|
|
ignoreKeyboardNavigationStateUpdate: false,
|
|
deloserByUId: {}
|
|
};
|
|
this._transactions = new CrossOriginTransactions(tabster, this._win, this._ctx);
|
|
this.focusedElement = new CrossOriginFocusedElementState(this._transactions);
|
|
this.observedElement = new CrossOriginObservedElementState(tabster, this._transactions);
|
|
}
|
|
|
|
setup(sendUp) {
|
|
if (this.isSetUp()) {
|
|
return this._transactions.setSendUp(sendUp);
|
|
} else {
|
|
this._tabster.queueInit(this._init);
|
|
|
|
return this._transactions.setup(sendUp);
|
|
}
|
|
}
|
|
|
|
isSetUp() {
|
|
return this._transactions.isSetUp;
|
|
}
|
|
|
|
dispose() {
|
|
var _a;
|
|
|
|
const tabster = this._tabster;
|
|
tabster.keyboardNavigation.unsubscribe(this._onKeyboardNavigationStateChanged);
|
|
tabster.focusedElement.unsubscribe(this._onFocus);
|
|
(_a = tabster.observedElement) === null || _a === void 0 ? void 0 : _a.unsubscribe(this._onObserved);
|
|
|
|
this._transactions.dispose();
|
|
|
|
this.focusedElement.dispose();
|
|
this.observedElement.dispose();
|
|
this._ctx.deloserByUId = {};
|
|
}
|
|
|
|
}
|
|
|
|
function getDeloserUID(getWindow, context, deloser) {
|
|
const deloserElement = deloser.getElement();
|
|
|
|
if (deloserElement) {
|
|
const uid = getElementUId(getWindow, deloserElement);
|
|
|
|
if (!context.deloserByUId[uid]) {
|
|
context.deloserByUId[uid] = deloser;
|
|
}
|
|
|
|
return uid;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
class FocusableAPI {
|
|
constructor(tabster) {
|
|
this._tabster = tabster;
|
|
}
|
|
|
|
dispose() {
|
|
/**/
|
|
}
|
|
|
|
getProps(element) {
|
|
const tabsterOnElement = getTabsterOnElement(this._tabster, element);
|
|
return tabsterOnElement && tabsterOnElement.focusable || {};
|
|
}
|
|
|
|
isFocusable(el, includeProgrammaticallyFocusable, noVisibleCheck, noAccessibleCheck) {
|
|
if (matchesSelector(el, FOCUSABLE_SELECTOR) && (includeProgrammaticallyFocusable || el.tabIndex !== -1)) {
|
|
return (noVisibleCheck || this.isVisible(el)) && (noAccessibleCheck || this.isAccessible(el));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
isVisible(el) {
|
|
if (!el.ownerDocument || el.nodeType !== Node.ELEMENT_NODE) {
|
|
return false;
|
|
}
|
|
|
|
if (isDisplayNone(el)) {
|
|
return false;
|
|
}
|
|
|
|
const rect = el.ownerDocument.body.getBoundingClientRect();
|
|
|
|
if (rect.width === 0 && rect.height === 0) {
|
|
// This might happen, for example, if our <body> is in hidden <iframe>.
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
isAccessible(el) {
|
|
var _a;
|
|
|
|
for (let e = el; e; e = dom.getParentElement(e)) {
|
|
const tabsterOnElement = getTabsterOnElement(this._tabster, e);
|
|
|
|
if (this._isHidden(e)) {
|
|
return false;
|
|
}
|
|
|
|
const ignoreDisabled = (_a = tabsterOnElement === null || tabsterOnElement === void 0 ? void 0 : tabsterOnElement.focusable) === null || _a === void 0 ? void 0 : _a.ignoreAriaDisabled;
|
|
|
|
if (!ignoreDisabled && this._isDisabled(e)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
_isDisabled(el) {
|
|
return el.hasAttribute("disabled");
|
|
}
|
|
|
|
_isHidden(el) {
|
|
var _a;
|
|
|
|
const attrVal = el.getAttribute("aria-hidden");
|
|
|
|
if (attrVal && attrVal.toLowerCase() === "true") {
|
|
if (!((_a = this._tabster.modalizer) === null || _a === void 0 ? void 0 : _a.isAugmented(el))) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
findFirst(options, out) {
|
|
return this.findElement({ ...options
|
|
}, out);
|
|
}
|
|
|
|
findLast(options, out) {
|
|
return this.findElement({
|
|
isBackward: true,
|
|
...options
|
|
}, out);
|
|
}
|
|
|
|
findNext(options, out) {
|
|
return this.findElement({ ...options
|
|
}, out);
|
|
}
|
|
|
|
findPrev(options, out) {
|
|
return this.findElement({ ...options,
|
|
isBackward: true
|
|
}, out);
|
|
}
|
|
|
|
findDefault(options, out) {
|
|
return this.findElement({ ...options,
|
|
acceptCondition: el => this.isFocusable(el, options.includeProgrammaticallyFocusable) && !!this.getProps(el).isDefault
|
|
}, out) || null;
|
|
}
|
|
|
|
findAll(options) {
|
|
return this._findElements(true, options) || [];
|
|
}
|
|
|
|
findElement(options, out) {
|
|
const found = this._findElements(false, options, out);
|
|
|
|
return found ? found[0] : found;
|
|
}
|
|
|
|
_findElements(isFindAll, options, out) {
|
|
var _a, _b, _c;
|
|
|
|
const {
|
|
container,
|
|
currentElement = null,
|
|
includeProgrammaticallyFocusable,
|
|
useActiveModalizer,
|
|
ignoreAccessibility,
|
|
modalizerId,
|
|
isBackward,
|
|
onElement
|
|
} = options;
|
|
|
|
if (!out) {
|
|
out = {};
|
|
}
|
|
|
|
const elements = [];
|
|
let {
|
|
acceptCondition
|
|
} = options;
|
|
const hasCustomCondition = !!acceptCondition;
|
|
|
|
if (!container) {
|
|
return null;
|
|
}
|
|
|
|
if (!acceptCondition) {
|
|
acceptCondition = el => this.isFocusable(el, includeProgrammaticallyFocusable, false, ignoreAccessibility);
|
|
}
|
|
|
|
const acceptElementState = {
|
|
container,
|
|
modalizerUserId: modalizerId === undefined && useActiveModalizer ? (_a = this._tabster.modalizer) === null || _a === void 0 ? void 0 : _a.activeId : modalizerId || ((_c = (_b = RootAPI.getTabsterContext(this._tabster, container)) === null || _b === void 0 ? void 0 : _b.modalizer) === null || _c === void 0 ? void 0 : _c.userId),
|
|
from: currentElement || container,
|
|
isBackward,
|
|
isFindAll,
|
|
acceptCondition,
|
|
hasCustomCondition,
|
|
includeProgrammaticallyFocusable,
|
|
ignoreAccessibility,
|
|
cachedGrouppers: {},
|
|
cachedRadioGroups: {}
|
|
};
|
|
const walker = createElementTreeWalker(container.ownerDocument, container, node => this._acceptElement(node, acceptElementState));
|
|
|
|
if (!walker) {
|
|
return null;
|
|
}
|
|
|
|
const prepareForNextElement = shouldContinueIfNotFound => {
|
|
var _a, _b;
|
|
|
|
const foundElement = (_a = acceptElementState.foundElement) !== null && _a !== void 0 ? _a : acceptElementState.foundBackward;
|
|
|
|
if (foundElement) {
|
|
elements.push(foundElement);
|
|
}
|
|
|
|
if (isFindAll) {
|
|
if (foundElement) {
|
|
acceptElementState.found = false;
|
|
delete acceptElementState.foundElement;
|
|
delete acceptElementState.foundBackward;
|
|
delete acceptElementState.fromCtx;
|
|
acceptElementState.from = foundElement;
|
|
|
|
if (onElement && !onElement(foundElement)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return !!(foundElement || shouldContinueIfNotFound);
|
|
} else {
|
|
if (foundElement && out) {
|
|
out.uncontrolled = (_b = RootAPI.getTabsterContext(this._tabster, foundElement)) === null || _b === void 0 ? void 0 : _b.uncontrolled;
|
|
}
|
|
|
|
return !!(shouldContinueIfNotFound && !foundElement);
|
|
}
|
|
};
|
|
|
|
if (!currentElement) {
|
|
out.outOfDOMOrder = true;
|
|
}
|
|
|
|
if (currentElement && dom.nodeContains(container, currentElement)) {
|
|
walker.currentNode = currentElement;
|
|
} else if (isBackward) {
|
|
const lastChild = getLastChild$2(container);
|
|
|
|
if (!lastChild) {
|
|
return null;
|
|
}
|
|
|
|
if (this._acceptElement(lastChild, acceptElementState) === NodeFilter.FILTER_ACCEPT && !prepareForNextElement(true)) {
|
|
if (acceptElementState.skippedFocusable) {
|
|
out.outOfDOMOrder = true;
|
|
}
|
|
|
|
return elements;
|
|
}
|
|
|
|
walker.currentNode = lastChild;
|
|
}
|
|
|
|
do {
|
|
if (isBackward) {
|
|
walker.previousNode();
|
|
} else {
|
|
walker.nextNode();
|
|
}
|
|
} while (prepareForNextElement());
|
|
|
|
if (acceptElementState.skippedFocusable) {
|
|
out.outOfDOMOrder = true;
|
|
}
|
|
|
|
return elements.length ? elements : null;
|
|
}
|
|
|
|
_acceptElement(element, state) {
|
|
var _a, _b, _c;
|
|
|
|
if (state.found) {
|
|
return NodeFilter.FILTER_ACCEPT;
|
|
}
|
|
|
|
const foundBackward = state.foundBackward;
|
|
|
|
if (foundBackward && (element === foundBackward || !dom.nodeContains(foundBackward, element))) {
|
|
state.found = true;
|
|
state.foundElement = foundBackward;
|
|
return NodeFilter.FILTER_ACCEPT;
|
|
}
|
|
|
|
const container = state.container;
|
|
|
|
if (element === container) {
|
|
return NodeFilter.FILTER_SKIP;
|
|
}
|
|
|
|
if (!dom.nodeContains(container, element)) {
|
|
return NodeFilter.FILTER_REJECT;
|
|
}
|
|
|
|
if (getDummyInputContainer(element)) {
|
|
return NodeFilter.FILTER_REJECT;
|
|
}
|
|
|
|
if (dom.nodeContains(state.rejectElementsFrom, element)) {
|
|
return NodeFilter.FILTER_REJECT;
|
|
}
|
|
|
|
const ctx = state.currentCtx = RootAPI.getTabsterContext(this._tabster, element); // Tabster is opt in, if it is not managed, don't try and get do anything special
|
|
|
|
if (!ctx) {
|
|
return NodeFilter.FILTER_SKIP;
|
|
}
|
|
|
|
if (shouldIgnoreFocus(element)) {
|
|
if (this.isFocusable(element, undefined, true, true)) {
|
|
state.skippedFocusable = true;
|
|
}
|
|
|
|
return NodeFilter.FILTER_SKIP;
|
|
} // We assume iframes are focusable because native tab behaviour would tab inside.
|
|
// But we do it only during the standard search when there is no custom accept
|
|
// element condition.
|
|
|
|
|
|
if (!state.hasCustomCondition && (element.tagName === "IFRAME" || element.tagName === "WEBVIEW")) {
|
|
if (((_a = ctx.modalizer) === null || _a === void 0 ? void 0 : _a.userId) === ((_b = this._tabster.modalizer) === null || _b === void 0 ? void 0 : _b.activeId)) {
|
|
state.found = true;
|
|
state.rejectElementsFrom = state.foundElement = element;
|
|
return NodeFilter.FILTER_ACCEPT;
|
|
} else {
|
|
return NodeFilter.FILTER_REJECT;
|
|
}
|
|
}
|
|
|
|
if (!state.ignoreAccessibility && !this.isAccessible(element)) {
|
|
if (this.isFocusable(element, false, true, true)) {
|
|
state.skippedFocusable = true;
|
|
}
|
|
|
|
return NodeFilter.FILTER_REJECT;
|
|
}
|
|
|
|
let result;
|
|
let fromCtx = state.fromCtx;
|
|
|
|
if (!fromCtx) {
|
|
fromCtx = state.fromCtx = RootAPI.getTabsterContext(this._tabster, state.from);
|
|
}
|
|
|
|
const fromMover = fromCtx === null || fromCtx === void 0 ? void 0 : fromCtx.mover;
|
|
let groupper = ctx.groupper;
|
|
let mover = ctx.mover;
|
|
result = (_c = this._tabster.modalizer) === null || _c === void 0 ? void 0 : _c.acceptElement(element, state);
|
|
|
|
if (result !== undefined) {
|
|
state.skippedFocusable = true;
|
|
}
|
|
|
|
if (result === undefined && (groupper || mover || fromMover)) {
|
|
const groupperElement = groupper === null || groupper === void 0 ? void 0 : groupper.getElement();
|
|
const fromMoverElement = fromMover === null || fromMover === void 0 ? void 0 : fromMover.getElement();
|
|
let moverElement = mover === null || mover === void 0 ? void 0 : mover.getElement();
|
|
|
|
if (moverElement && dom.nodeContains(fromMoverElement, moverElement) && dom.nodeContains(container, fromMoverElement) && (!groupperElement || !mover || dom.nodeContains(fromMoverElement, groupperElement))) {
|
|
mover = fromMover;
|
|
moverElement = fromMoverElement;
|
|
}
|
|
|
|
if (groupperElement && (groupperElement === container || !dom.nodeContains(container, groupperElement))) {
|
|
groupper = undefined;
|
|
}
|
|
|
|
if (moverElement && !dom.nodeContains(container, moverElement)) {
|
|
mover = undefined;
|
|
}
|
|
|
|
if (groupper && mover) {
|
|
if (moverElement && groupperElement && !dom.nodeContains(groupperElement, moverElement)) {
|
|
mover = undefined;
|
|
} else {
|
|
groupper = undefined;
|
|
}
|
|
}
|
|
|
|
if (groupper) {
|
|
result = groupper.acceptElement(element, state);
|
|
}
|
|
|
|
if (mover) {
|
|
result = mover.acceptElement(element, state);
|
|
}
|
|
}
|
|
|
|
if (result === undefined) {
|
|
result = state.acceptCondition(element) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
|
|
|
|
if (result === NodeFilter.FILTER_SKIP && this.isFocusable(element, false, true, true)) {
|
|
state.skippedFocusable = true;
|
|
}
|
|
}
|
|
|
|
if (result === NodeFilter.FILTER_ACCEPT && !state.found) {
|
|
if (!state.isFindAll && isRadio(element) && !element.checked) {
|
|
// We need to mimic the browser's behaviour to skip unchecked radio buttons.
|
|
const radioGroupName = element.name;
|
|
let radioGroup = state.cachedRadioGroups[radioGroupName];
|
|
|
|
if (!radioGroup) {
|
|
radioGroup = getRadioButtonGroup(element);
|
|
|
|
if (radioGroup) {
|
|
state.cachedRadioGroups[radioGroupName] = radioGroup;
|
|
}
|
|
}
|
|
|
|
if ((radioGroup === null || radioGroup === void 0 ? void 0 : radioGroup.checked) && radioGroup.checked !== element) {
|
|
// Currently found element is a radio button in a group that has another radio button checked.
|
|
return NodeFilter.FILTER_SKIP;
|
|
}
|
|
}
|
|
|
|
if (state.isBackward) {
|
|
// When TreeWalker goes backwards, it visits the container first,
|
|
// then it goes inside. So, if the container is accepted, we remember it,
|
|
// but allowing the TreeWalker to check inside.
|
|
state.foundBackward = element;
|
|
result = NodeFilter.FILTER_SKIP;
|
|
} else {
|
|
state.found = true;
|
|
state.foundElement = element;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
const Keys = {
|
|
Tab: "Tab",
|
|
Enter: "Enter",
|
|
Escape: "Escape",
|
|
Space: " ",
|
|
PageUp: "PageUp",
|
|
PageDown: "PageDown",
|
|
End: "End",
|
|
Home: "Home",
|
|
ArrowLeft: "ArrowLeft",
|
|
ArrowUp: "ArrowUp",
|
|
ArrowRight: "ArrowRight",
|
|
ArrowDown: "ArrowDown"
|
|
};
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
|
|
function getUncontrolledCompletelyContainer(tabster, element) {
|
|
var _a;
|
|
|
|
const getParent = tabster.getParent;
|
|
let el = element;
|
|
|
|
do {
|
|
const uncontrolledOnElement = (_a = getTabsterOnElement(tabster, el)) === null || _a === void 0 ? void 0 : _a.uncontrolled;
|
|
|
|
if (uncontrolledOnElement && tabster.uncontrolled.isUncontrolledCompletely(el, !!uncontrolledOnElement.completely)) {
|
|
return el;
|
|
}
|
|
|
|
el = getParent(el);
|
|
} while (el);
|
|
|
|
return undefined;
|
|
}
|
|
|
|
const AsyncFocusIntentPriorityBySource = {
|
|
[AsyncFocusSources.Restorer]: 0,
|
|
[AsyncFocusSources.Deloser]: 1,
|
|
[AsyncFocusSources.EscapeGroupper]: 2
|
|
};
|
|
class FocusedElementState extends Subscribable {
|
|
constructor(tabster, getWindow) {
|
|
super();
|
|
|
|
this._init = () => {
|
|
const win = this._win();
|
|
|
|
const doc = win.document; // Add these event listeners as capture - we want Tabster to run before user event handlers
|
|
|
|
doc.addEventListener(KEYBORG_FOCUSIN, this._onFocusIn, true);
|
|
doc.addEventListener(KEYBORG_FOCUSOUT, this._onFocusOut, true);
|
|
win.addEventListener("keydown", this._onKeyDown, true);
|
|
const activeElement = dom.getActiveElement(doc);
|
|
|
|
if (activeElement && activeElement !== doc.body) {
|
|
this._setFocusedElement(activeElement);
|
|
}
|
|
|
|
this.subscribe(this._onChanged);
|
|
};
|
|
|
|
this._onFocusIn = e => {
|
|
const target = e.composedPath()[0];
|
|
|
|
if (target) {
|
|
this._setFocusedElement(target, e.detail.relatedTarget, e.detail.isFocusedProgrammatically);
|
|
}
|
|
};
|
|
|
|
this._onFocusOut = e => {
|
|
var _a;
|
|
|
|
this._setFocusedElement(undefined, (_a = e.detail) === null || _a === void 0 ? void 0 : _a.originalEvent.relatedTarget);
|
|
}; // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
|
|
|
|
this._validateFocusedElement = element => {// TODO: Make sure this is not needed anymore and write tests.
|
|
};
|
|
|
|
this._onKeyDown = event => {
|
|
if (event.key !== Keys.Tab || event.ctrlKey) {
|
|
return;
|
|
}
|
|
|
|
const currentElement = this.getVal();
|
|
|
|
if (!currentElement || !currentElement.ownerDocument || currentElement.contentEditable === "true") {
|
|
return;
|
|
}
|
|
|
|
const tabster = this._tabster;
|
|
const controlTab = tabster.controlTab;
|
|
const ctx = RootAPI.getTabsterContext(tabster, currentElement);
|
|
|
|
if (!ctx || ctx.ignoreKeydown(event)) {
|
|
return;
|
|
}
|
|
|
|
const isBackward = event.shiftKey;
|
|
const next = FocusedElementState.findNextTabbable(tabster, ctx, undefined, currentElement, undefined, isBackward, true);
|
|
const rootElement = ctx.root.getElement();
|
|
|
|
if (!rootElement) {
|
|
return;
|
|
}
|
|
|
|
const nextElement = next === null || next === void 0 ? void 0 : next.element;
|
|
const uncontrolledCompletelyContainer = getUncontrolledCompletelyContainer(tabster, currentElement);
|
|
|
|
if (nextElement) {
|
|
const nextUncontrolled = next.uncontrolled;
|
|
|
|
if (ctx.uncontrolled || dom.nodeContains(nextUncontrolled, currentElement)) {
|
|
if (!next.outOfDOMOrder && nextUncontrolled === ctx.uncontrolled || uncontrolledCompletelyContainer && !dom.nodeContains(uncontrolledCompletelyContainer, nextElement)) {
|
|
// Nothing to do, everything will be done by the browser or something
|
|
// that controls the uncontrolled area.
|
|
return;
|
|
} // We are in uncontrolled area. We allow whatever controls it to move
|
|
// focus, but we add a phantom dummy to make sure the focus is moved
|
|
// to the correct place if the uncontrolled area allows default action.
|
|
// We only need that in the controlled mode, because in uncontrolled
|
|
// mode we have dummy inputs around everything that redirects focus.
|
|
|
|
|
|
DummyInputManager.addPhantomDummyWithTarget(tabster, currentElement, isBackward, nextElement);
|
|
return;
|
|
}
|
|
|
|
if (nextUncontrolled || nextElement.tagName === "IFRAME") {
|
|
// For iframes and uncontrolled areas we always want to use default action to
|
|
// move focus into.
|
|
if (rootElement.dispatchEvent(new TabsterMoveFocusEvent({
|
|
by: "root",
|
|
owner: rootElement,
|
|
next: nextElement,
|
|
relatedEvent: event
|
|
}))) {
|
|
DummyInputManager.moveWithPhantomDummy(this._tabster, nextUncontrolled !== null && nextUncontrolled !== void 0 ? nextUncontrolled : nextElement, false, isBackward, event);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (controlTab || (next === null || next === void 0 ? void 0 : next.outOfDOMOrder)) {
|
|
if (rootElement.dispatchEvent(new TabsterMoveFocusEvent({
|
|
by: "root",
|
|
owner: rootElement,
|
|
next: nextElement,
|
|
relatedEvent: event
|
|
}))) {
|
|
event.preventDefault();
|
|
event.stopImmediatePropagation();
|
|
nativeFocus(nextElement);
|
|
}
|
|
}
|
|
} else {
|
|
if (!uncontrolledCompletelyContainer && rootElement.dispatchEvent(new TabsterMoveFocusEvent({
|
|
by: "root",
|
|
owner: rootElement,
|
|
next: null,
|
|
relatedEvent: event
|
|
}))) {
|
|
ctx.root.moveOutWithDefaultAction(isBackward, event);
|
|
}
|
|
}
|
|
};
|
|
|
|
this._onChanged = (element, detail) => {
|
|
var _a, _b;
|
|
|
|
if (element) {
|
|
element.dispatchEvent(new TabsterFocusInEvent(detail));
|
|
} else {
|
|
const last = (_a = this._lastVal) === null || _a === void 0 ? void 0 : _a.get();
|
|
|
|
if (last) {
|
|
const d = { ...detail
|
|
};
|
|
const lastCtx = RootAPI.getTabsterContext(this._tabster, last);
|
|
const modalizerId = (_b = lastCtx === null || lastCtx === void 0 ? void 0 : lastCtx.modalizer) === null || _b === void 0 ? void 0 : _b.userId;
|
|
|
|
if (modalizerId) {
|
|
d.modalizerId = modalizerId;
|
|
}
|
|
|
|
last.dispatchEvent(new TabsterFocusOutEvent(d));
|
|
}
|
|
}
|
|
};
|
|
|
|
this._tabster = tabster;
|
|
this._win = getWindow;
|
|
tabster.queueInit(this._init);
|
|
}
|
|
|
|
dispose() {
|
|
super.dispose();
|
|
|
|
const win = this._win();
|
|
|
|
const doc = win.document;
|
|
doc.removeEventListener(KEYBORG_FOCUSIN, this._onFocusIn, true);
|
|
doc.removeEventListener(KEYBORG_FOCUSOUT, this._onFocusOut, true);
|
|
win.removeEventListener("keydown", this._onKeyDown, true);
|
|
this.unsubscribe(this._onChanged);
|
|
const asyncFocus = this._asyncFocus;
|
|
|
|
if (asyncFocus) {
|
|
win.clearTimeout(asyncFocus.timeout);
|
|
delete this._asyncFocus;
|
|
}
|
|
|
|
delete FocusedElementState._lastResetElement;
|
|
delete this._nextVal;
|
|
delete this._lastVal;
|
|
}
|
|
|
|
static forgetMemorized(instance, parent) {
|
|
var _a, _b;
|
|
|
|
let wel = FocusedElementState._lastResetElement;
|
|
let el = wel && wel.get();
|
|
|
|
if (el && dom.nodeContains(parent, el)) {
|
|
delete FocusedElementState._lastResetElement;
|
|
}
|
|
|
|
el = (_b = (_a = instance._nextVal) === null || _a === void 0 ? void 0 : _a.element) === null || _b === void 0 ? void 0 : _b.get();
|
|
|
|
if (el && dom.nodeContains(parent, el)) {
|
|
delete instance._nextVal;
|
|
}
|
|
|
|
wel = instance._lastVal;
|
|
el = wel && wel.get();
|
|
|
|
if (el && dom.nodeContains(parent, el)) {
|
|
delete instance._lastVal;
|
|
}
|
|
}
|
|
|
|
getFocusedElement() {
|
|
return this.getVal();
|
|
}
|
|
|
|
getLastFocusedElement() {
|
|
var _a;
|
|
|
|
let el = (_a = this._lastVal) === null || _a === void 0 ? void 0 : _a.get();
|
|
|
|
if (!el || el && !documentContains(el.ownerDocument, el)) {
|
|
this._lastVal = el = undefined;
|
|
}
|
|
|
|
return el;
|
|
}
|
|
|
|
focus(element, noFocusedProgrammaticallyFlag, noAccessibleCheck) {
|
|
if (!this._tabster.focusable.isFocusable(element, noFocusedProgrammaticallyFlag, false, noAccessibleCheck)) {
|
|
return false;
|
|
}
|
|
|
|
element.focus();
|
|
return true;
|
|
}
|
|
|
|
focusDefault(container) {
|
|
const el = this._tabster.focusable.findDefault({
|
|
container
|
|
});
|
|
|
|
if (el) {
|
|
this._tabster.focusedElement.focus(el);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
getFirstOrLastTabbable(isFirst, props) {
|
|
var _a;
|
|
|
|
const {
|
|
container,
|
|
ignoreAccessibility
|
|
} = props;
|
|
let toFocus;
|
|
|
|
if (container) {
|
|
const ctx = RootAPI.getTabsterContext(this._tabster, container);
|
|
|
|
if (ctx) {
|
|
toFocus = (_a = FocusedElementState.findNextTabbable(this._tabster, ctx, container, undefined, undefined, !isFirst, ignoreAccessibility)) === null || _a === void 0 ? void 0 : _a.element;
|
|
}
|
|
}
|
|
|
|
if (toFocus && !dom.nodeContains(container, toFocus)) {
|
|
toFocus = undefined;
|
|
}
|
|
|
|
return toFocus || undefined;
|
|
}
|
|
|
|
_focusFirstOrLast(isFirst, props) {
|
|
const toFocus = this.getFirstOrLastTabbable(isFirst, props);
|
|
|
|
if (toFocus) {
|
|
this.focus(toFocus, false, true);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
focusFirst(props) {
|
|
return this._focusFirstOrLast(true, props);
|
|
}
|
|
|
|
focusLast(props) {
|
|
return this._focusFirstOrLast(false, props);
|
|
}
|
|
|
|
resetFocus(container) {
|
|
if (!this._tabster.focusable.isVisible(container)) {
|
|
return false;
|
|
}
|
|
|
|
if (!this._tabster.focusable.isFocusable(container, true, true, true)) {
|
|
const prevTabIndex = container.getAttribute("tabindex");
|
|
const prevAriaHidden = container.getAttribute("aria-hidden");
|
|
container.tabIndex = -1;
|
|
container.setAttribute("aria-hidden", "true");
|
|
FocusedElementState._lastResetElement = new WeakHTMLElement(this._win, container);
|
|
this.focus(container, true, true);
|
|
|
|
this._setOrRemoveAttribute(container, "tabindex", prevTabIndex);
|
|
|
|
this._setOrRemoveAttribute(container, "aria-hidden", prevAriaHidden);
|
|
} else {
|
|
this.focus(container);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
requestAsyncFocus(source, callback, delay) {
|
|
const win = this._tabster.getWindow();
|
|
|
|
const currentAsyncFocus = this._asyncFocus;
|
|
|
|
if (currentAsyncFocus) {
|
|
if (AsyncFocusIntentPriorityBySource[source] > AsyncFocusIntentPriorityBySource[currentAsyncFocus.source]) {
|
|
// Previously registered intent has higher priority.
|
|
return;
|
|
} // New intent has higher priority.
|
|
|
|
|
|
win.clearTimeout(currentAsyncFocus.timeout);
|
|
}
|
|
|
|
this._asyncFocus = {
|
|
source,
|
|
callback,
|
|
timeout: win.setTimeout(() => {
|
|
this._asyncFocus = undefined;
|
|
callback();
|
|
}, delay)
|
|
};
|
|
}
|
|
|
|
cancelAsyncFocus(source) {
|
|
const asyncFocus = this._asyncFocus;
|
|
|
|
if ((asyncFocus === null || asyncFocus === void 0 ? void 0 : asyncFocus.source) === source) {
|
|
this._tabster.getWindow().clearTimeout(asyncFocus.timeout);
|
|
|
|
this._asyncFocus = undefined;
|
|
}
|
|
}
|
|
|
|
_setOrRemoveAttribute(element, name, value) {
|
|
if (value === null) {
|
|
element.removeAttribute(name);
|
|
} else {
|
|
element.setAttribute(name, value);
|
|
}
|
|
}
|
|
|
|
_setFocusedElement(element, relatedTarget, isFocusedProgrammatically) {
|
|
var _a, _b;
|
|
|
|
if (this._tabster._noop) {
|
|
return;
|
|
}
|
|
|
|
const detail = {
|
|
relatedTarget
|
|
};
|
|
|
|
if (element) {
|
|
const lastResetElement = (_a = FocusedElementState._lastResetElement) === null || _a === void 0 ? void 0 : _a.get();
|
|
FocusedElementState._lastResetElement = undefined;
|
|
|
|
if (lastResetElement === element || shouldIgnoreFocus(element)) {
|
|
return;
|
|
}
|
|
|
|
detail.isFocusedProgrammatically = isFocusedProgrammatically;
|
|
const ctx = RootAPI.getTabsterContext(this._tabster, element);
|
|
const modalizerId = (_b = ctx === null || ctx === void 0 ? void 0 : ctx.modalizer) === null || _b === void 0 ? void 0 : _b.userId;
|
|
|
|
if (modalizerId) {
|
|
detail.modalizerId = modalizerId;
|
|
}
|
|
}
|
|
|
|
const nextVal = this._nextVal = {
|
|
element: element ? new WeakHTMLElement(this._win, element) : undefined,
|
|
detail
|
|
};
|
|
|
|
if (element && element !== this._val) {
|
|
this._validateFocusedElement(element);
|
|
} // _validateFocusedElement() might cause the refocus which will trigger
|
|
// another call to this function. Making sure that the value is correct.
|
|
|
|
|
|
if (this._nextVal === nextVal) {
|
|
this.setVal(element, detail);
|
|
}
|
|
|
|
this._nextVal = undefined;
|
|
}
|
|
|
|
setVal(val, detail) {
|
|
super.setVal(val, detail);
|
|
|
|
if (val) {
|
|
this._lastVal = new WeakHTMLElement(this._win, val);
|
|
}
|
|
}
|
|
|
|
static findNextTabbable(tabster, ctx, container, currentElement, referenceElement, isBackward, ignoreAccessibility) {
|
|
const actualContainer = container || ctx.root.getElement();
|
|
|
|
if (!actualContainer) {
|
|
return null;
|
|
}
|
|
|
|
let next = null;
|
|
const isTabbingTimer = FocusedElementState._isTabbingTimer;
|
|
const win = tabster.getWindow();
|
|
|
|
if (isTabbingTimer) {
|
|
win.clearTimeout(isTabbingTimer);
|
|
}
|
|
|
|
FocusedElementState.isTabbing = true;
|
|
FocusedElementState._isTabbingTimer = win.setTimeout(() => {
|
|
delete FocusedElementState._isTabbingTimer;
|
|
FocusedElementState.isTabbing = false;
|
|
}, 0);
|
|
const modalizer = ctx.modalizer;
|
|
const groupper = ctx.groupper;
|
|
const mover = ctx.mover;
|
|
|
|
const callFindNext = what => {
|
|
next = what.findNextTabbable(currentElement, referenceElement, isBackward, ignoreAccessibility);
|
|
|
|
if (currentElement && !(next === null || next === void 0 ? void 0 : next.element)) {
|
|
const parentElement = what !== modalizer && dom.getParentElement(what.getElement());
|
|
|
|
if (parentElement) {
|
|
const parentCtx = RootAPI.getTabsterContext(tabster, currentElement, {
|
|
referenceElement: parentElement
|
|
});
|
|
|
|
if (parentCtx) {
|
|
const currentScopeElement = what.getElement();
|
|
const newCurrent = isBackward ? currentScopeElement : currentScopeElement && getLastChild$2(currentScopeElement) || currentScopeElement;
|
|
|
|
if (newCurrent) {
|
|
next = FocusedElementState.findNextTabbable(tabster, parentCtx, container, newCurrent, parentElement, isBackward, ignoreAccessibility);
|
|
|
|
if (next) {
|
|
next.outOfDOMOrder = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
if (groupper && mover) {
|
|
callFindNext(ctx.groupperBeforeMover ? groupper : mover);
|
|
} else if (groupper) {
|
|
callFindNext(groupper);
|
|
} else if (mover) {
|
|
callFindNext(mover);
|
|
} else if (modalizer) {
|
|
callFindNext(modalizer);
|
|
} else {
|
|
const findProps = {
|
|
container: actualContainer,
|
|
currentElement,
|
|
referenceElement,
|
|
ignoreAccessibility,
|
|
useActiveModalizer: true
|
|
};
|
|
const findPropsOut = {};
|
|
const nextElement = tabster.focusable[isBackward ? "findPrev" : "findNext"](findProps, findPropsOut);
|
|
next = {
|
|
element: nextElement,
|
|
outOfDOMOrder: findPropsOut.outOfDOMOrder,
|
|
uncontrolled: findPropsOut.uncontrolled
|
|
};
|
|
}
|
|
|
|
return next;
|
|
}
|
|
|
|
}
|
|
FocusedElementState.isTabbing = false;
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
|
|
class GroupperDummyManager extends DummyInputManager {
|
|
constructor(element, groupper, tabster, sys) {
|
|
super(tabster, element, DummyInputManagerPriorities.Groupper, sys, true);
|
|
|
|
this._setHandlers((dummyInput, isBackward, relatedTarget) => {
|
|
var _a, _b;
|
|
|
|
const container = element.get();
|
|
const input = dummyInput.input;
|
|
|
|
if (container && input) {
|
|
const ctx = RootAPI.getTabsterContext(tabster, input);
|
|
|
|
if (ctx) {
|
|
let next;
|
|
next = (_a = groupper.findNextTabbable(relatedTarget || undefined, undefined, isBackward, true)) === null || _a === void 0 ? void 0 : _a.element;
|
|
|
|
if (!next) {
|
|
next = (_b = FocusedElementState.findNextTabbable(tabster, ctx, undefined, dummyInput.isOutside ? input : getAdjacentElement(container, !isBackward), undefined, isBackward, true)) === null || _b === void 0 ? void 0 : _b.element;
|
|
}
|
|
|
|
if (next) {
|
|
nativeFocus(next);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
class Groupper extends TabsterPart {
|
|
constructor(tabster, element, onDispose, props, sys) {
|
|
super(tabster, element, props);
|
|
this._shouldTabInside = false;
|
|
this.makeTabbable(false);
|
|
this._onDispose = onDispose;
|
|
|
|
if (!tabster.controlTab) {
|
|
this.dummyManager = new GroupperDummyManager(this._element, this, tabster, sys);
|
|
}
|
|
}
|
|
|
|
dispose() {
|
|
var _a;
|
|
|
|
this._onDispose(this);
|
|
|
|
const element = this._element.get();
|
|
|
|
(_a = this.dummyManager) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
delete this.dummyManager;
|
|
|
|
if (element) {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
_setInformativeStyle$1(this._element, true);
|
|
}
|
|
}
|
|
|
|
delete this._first;
|
|
}
|
|
|
|
findNextTabbable(currentElement, referenceElement, isBackward, ignoreAccessibility) {
|
|
const groupperElement = this.getElement();
|
|
|
|
if (!groupperElement) {
|
|
return null;
|
|
}
|
|
|
|
const currentIsDummy = getDummyInputContainer(currentElement) === groupperElement;
|
|
|
|
if (!this._shouldTabInside && currentElement && dom.nodeContains(groupperElement, currentElement) && !currentIsDummy) {
|
|
return {
|
|
element: undefined,
|
|
outOfDOMOrder: true
|
|
};
|
|
}
|
|
|
|
const groupperFirstFocusable = this.getFirst(true);
|
|
|
|
if (!currentElement || !dom.nodeContains(groupperElement, currentElement) || currentIsDummy) {
|
|
return {
|
|
element: groupperFirstFocusable,
|
|
outOfDOMOrder: true
|
|
};
|
|
}
|
|
|
|
const tabster = this._tabster;
|
|
let next = null;
|
|
let outOfDOMOrder = false;
|
|
let uncontrolled;
|
|
|
|
if (this._shouldTabInside && groupperFirstFocusable) {
|
|
const findProps = {
|
|
container: groupperElement,
|
|
currentElement,
|
|
referenceElement,
|
|
ignoreAccessibility,
|
|
useActiveModalizer: true
|
|
};
|
|
const findPropsOut = {};
|
|
next = tabster.focusable[isBackward ? "findPrev" : "findNext"](findProps, findPropsOut);
|
|
outOfDOMOrder = !!findPropsOut.outOfDOMOrder;
|
|
|
|
if (!next && this._props.tabbability === GroupperTabbabilities.LimitedTrapFocus) {
|
|
next = tabster.focusable[isBackward ? "findLast" : "findFirst"]({
|
|
container: groupperElement,
|
|
ignoreAccessibility,
|
|
useActiveModalizer: true
|
|
}, findPropsOut);
|
|
outOfDOMOrder = true;
|
|
}
|
|
|
|
uncontrolled = findPropsOut.uncontrolled;
|
|
}
|
|
|
|
return {
|
|
element: next,
|
|
uncontrolled,
|
|
outOfDOMOrder
|
|
};
|
|
}
|
|
|
|
makeTabbable(isTabbable) {
|
|
this._shouldTabInside = isTabbable || !this._props.tabbability;
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
_setInformativeStyle$1(this._element, !this._shouldTabInside);
|
|
}
|
|
}
|
|
|
|
isActive(noIfFirstIsFocused) {
|
|
var _a;
|
|
|
|
const element = this.getElement() || null;
|
|
let isParentActive = true;
|
|
|
|
for (let e = dom.getParentElement(element); e; e = dom.getParentElement(e)) {
|
|
const g = (_a = getTabsterOnElement(this._tabster, e)) === null || _a === void 0 ? void 0 : _a.groupper;
|
|
|
|
if (g) {
|
|
if (!g._shouldTabInside) {
|
|
isParentActive = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
let ret = isParentActive ? this._props.tabbability ? this._shouldTabInside : false : undefined;
|
|
|
|
if (ret && noIfFirstIsFocused) {
|
|
const focused = this._tabster.focusedElement.getFocusedElement();
|
|
|
|
if (focused) {
|
|
ret = focused !== this.getFirst(true);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
getFirst(orContainer) {
|
|
var _a;
|
|
|
|
const groupperElement = this.getElement();
|
|
let first;
|
|
|
|
if (groupperElement) {
|
|
if (orContainer && this._tabster.focusable.isFocusable(groupperElement)) {
|
|
return groupperElement;
|
|
}
|
|
|
|
first = (_a = this._first) === null || _a === void 0 ? void 0 : _a.get();
|
|
|
|
if (!first) {
|
|
first = this._tabster.focusable.findFirst({
|
|
container: groupperElement,
|
|
useActiveModalizer: true
|
|
}) || undefined;
|
|
|
|
if (first) {
|
|
this.setFirst(first);
|
|
}
|
|
}
|
|
}
|
|
|
|
return first;
|
|
}
|
|
|
|
setFirst(element) {
|
|
if (element) {
|
|
this._first = new WeakHTMLElement(this._tabster.getWindow, element);
|
|
} else {
|
|
delete this._first;
|
|
}
|
|
}
|
|
|
|
acceptElement(element, state) {
|
|
const cachedGrouppers = state.cachedGrouppers;
|
|
const parentElement = dom.getParentElement(this.getElement());
|
|
const parentCtx = parentElement && RootAPI.getTabsterContext(this._tabster, parentElement);
|
|
const parentCtxGroupper = parentCtx === null || parentCtx === void 0 ? void 0 : parentCtx.groupper;
|
|
const parentGroupper = (parentCtx === null || parentCtx === void 0 ? void 0 : parentCtx.groupperBeforeMover) ? parentCtxGroupper : undefined;
|
|
let parentGroupperElement;
|
|
|
|
const getIsActive = groupper => {
|
|
let cached = cachedGrouppers[groupper.id];
|
|
let isActive;
|
|
|
|
if (cached) {
|
|
isActive = cached.isActive;
|
|
} else {
|
|
isActive = this.isActive(true);
|
|
cached = cachedGrouppers[groupper.id] = {
|
|
isActive
|
|
};
|
|
}
|
|
|
|
return isActive;
|
|
};
|
|
|
|
if (parentGroupper) {
|
|
parentGroupperElement = parentGroupper.getElement();
|
|
|
|
if (!getIsActive(parentGroupper) && parentGroupperElement && state.container !== parentGroupperElement && dom.nodeContains(state.container, parentGroupperElement)) {
|
|
// Do not fall into a child groupper of inactive parent groupper if it's in the scope of the search.
|
|
state.skippedFocusable = true;
|
|
return NodeFilter.FILTER_REJECT;
|
|
}
|
|
}
|
|
|
|
const isActive = getIsActive(this);
|
|
const groupperElement = this.getElement();
|
|
|
|
if (groupperElement) {
|
|
if (isActive !== true) {
|
|
if (groupperElement === element && parentCtxGroupper) {
|
|
if (!parentGroupperElement) {
|
|
parentGroupperElement = parentCtxGroupper.getElement();
|
|
}
|
|
|
|
if (parentGroupperElement && !getIsActive(parentCtxGroupper) && dom.nodeContains(state.container, parentGroupperElement) && parentGroupperElement !== state.container) {
|
|
state.skippedFocusable = true;
|
|
return NodeFilter.FILTER_REJECT;
|
|
}
|
|
}
|
|
|
|
if (groupperElement !== element && dom.nodeContains(groupperElement, element)) {
|
|
state.skippedFocusable = true;
|
|
return NodeFilter.FILTER_REJECT;
|
|
}
|
|
|
|
const cached = cachedGrouppers[this.id];
|
|
let first;
|
|
|
|
if ("first" in cached) {
|
|
first = cached.first;
|
|
} else {
|
|
first = cached.first = this.getFirst(true);
|
|
}
|
|
|
|
if (first && state.acceptCondition(first)) {
|
|
state.rejectElementsFrom = groupperElement;
|
|
state.skippedFocusable = true;
|
|
|
|
if (first !== state.from) {
|
|
state.found = true;
|
|
state.foundElement = first;
|
|
return NodeFilter.FILTER_ACCEPT;
|
|
} else {
|
|
return NodeFilter.FILTER_REJECT;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
} // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
|
|
class GroupperAPI {
|
|
constructor(tabster, getWindow) {
|
|
this._current = {};
|
|
this._grouppers = {};
|
|
|
|
this._init = () => {
|
|
const win = this._win(); // Making sure groupper's onFocus is called before modalizer's onFocus.
|
|
|
|
|
|
this._tabster.focusedElement.subscribeFirst(this._onFocus);
|
|
|
|
const doc = win.document;
|
|
const activeElement = dom.getActiveElement(doc);
|
|
|
|
if (activeElement) {
|
|
this._onFocus(activeElement);
|
|
}
|
|
|
|
doc.addEventListener("mousedown", this._onMouseDown, true);
|
|
win.addEventListener("keydown", this._onKeyDown, true);
|
|
win.addEventListener(GroupperMoveFocusEventName, this._onMoveFocus);
|
|
};
|
|
|
|
this._onGroupperDispose = groupper => {
|
|
delete this._grouppers[groupper.id];
|
|
};
|
|
|
|
this._onFocus = element => {
|
|
if (element) {
|
|
this._updateCurrent(element, true, true);
|
|
}
|
|
};
|
|
|
|
this._onMouseDown = e => {
|
|
if (e.target) {
|
|
this._updateCurrent(e.target, true);
|
|
}
|
|
};
|
|
|
|
this._onKeyDown = event => {
|
|
if (event.key !== Keys.Enter && event.key !== Keys.Escape) {
|
|
return;
|
|
} // Give a chance to other listeners to handle the event.
|
|
|
|
|
|
if (event.ctrlKey || event.altKey || event.shiftKey || event.metaKey) {
|
|
return;
|
|
}
|
|
|
|
const element = this._tabster.focusedElement.getFocusedElement();
|
|
|
|
if (element) {
|
|
this.handleKeyPress(element, event);
|
|
}
|
|
};
|
|
|
|
this._onMoveFocus = e => {
|
|
var _a;
|
|
|
|
const element = e.composedPath()[0];
|
|
const action = (_a = e.detail) === null || _a === void 0 ? void 0 : _a.action;
|
|
|
|
if (element && action !== undefined && !e.defaultPrevented) {
|
|
if (action === GroupperMoveFocusActions.Enter) {
|
|
this._enterGroupper(element);
|
|
} else {
|
|
this._escapeGroupper(element);
|
|
}
|
|
|
|
e.stopImmediatePropagation();
|
|
}
|
|
};
|
|
|
|
this._tabster = tabster;
|
|
this._win = getWindow;
|
|
tabster.queueInit(this._init);
|
|
}
|
|
|
|
dispose() {
|
|
const win = this._win();
|
|
|
|
this._tabster.focusedElement.cancelAsyncFocus(AsyncFocusSources.EscapeGroupper);
|
|
|
|
this._current = {};
|
|
|
|
if (this._updateTimer) {
|
|
win.clearTimeout(this._updateTimer);
|
|
delete this._updateTimer;
|
|
}
|
|
|
|
this._tabster.focusedElement.unsubscribe(this._onFocus);
|
|
|
|
win.document.removeEventListener("mousedown", this._onMouseDown, true);
|
|
win.removeEventListener("keydown", this._onKeyDown, true);
|
|
win.removeEventListener(GroupperMoveFocusEventName, this._onMoveFocus);
|
|
Object.keys(this._grouppers).forEach(groupperId => {
|
|
if (this._grouppers[groupperId]) {
|
|
this._grouppers[groupperId].dispose();
|
|
|
|
delete this._grouppers[groupperId];
|
|
}
|
|
});
|
|
}
|
|
|
|
createGroupper(element, props, sys) {
|
|
if (process.env.NODE_ENV === 'development') ;
|
|
|
|
const newGroupper = new Groupper(this._tabster, element, this._onGroupperDispose, props, sys);
|
|
this._grouppers[newGroupper.id] = newGroupper;
|
|
|
|
const focusedElement = this._tabster.focusedElement.getFocusedElement(); // Newly created groupper contains currently focused element, update the state on the next tick (to
|
|
// make sure all grouppers are processed).
|
|
|
|
|
|
if (focusedElement && dom.nodeContains(element, focusedElement) && !this._updateTimer) {
|
|
this._updateTimer = this._win().setTimeout(() => {
|
|
delete this._updateTimer; // Making sure the focused element hasn't changed.
|
|
|
|
if (focusedElement === this._tabster.focusedElement.getFocusedElement()) {
|
|
this._updateCurrent(focusedElement, true, true);
|
|
}
|
|
}, 0);
|
|
}
|
|
|
|
return newGroupper;
|
|
}
|
|
|
|
forgetCurrentGrouppers() {
|
|
this._current = {};
|
|
}
|
|
|
|
_updateCurrent(element, includeTarget, checkTarget) {
|
|
var _a;
|
|
|
|
if (this._updateTimer) {
|
|
this._win().clearTimeout(this._updateTimer);
|
|
|
|
delete this._updateTimer;
|
|
}
|
|
|
|
const newIds = {};
|
|
let isTarget = true;
|
|
|
|
for (let el = element; el; el = dom.getParentElement(el)) {
|
|
const groupper = (_a = getTabsterOnElement(this._tabster, el)) === null || _a === void 0 ? void 0 : _a.groupper;
|
|
|
|
if (groupper) {
|
|
newIds[groupper.id] = true;
|
|
|
|
if (isTarget && checkTarget && el !== element) {
|
|
isTarget = false;
|
|
}
|
|
|
|
if (includeTarget || !isTarget) {
|
|
this._current[groupper.id] = groupper;
|
|
const isTabbable = groupper.isActive() || element !== el && (!groupper.getProps().delegated || groupper.getFirst(false) !== element);
|
|
groupper.makeTabbable(isTabbable);
|
|
}
|
|
|
|
isTarget = false;
|
|
}
|
|
}
|
|
|
|
for (const id of Object.keys(this._current)) {
|
|
const groupper = this._current[id];
|
|
|
|
if (!(groupper.id in newIds)) {
|
|
groupper.makeTabbable(false);
|
|
groupper.setFirst(undefined);
|
|
delete this._current[id];
|
|
}
|
|
}
|
|
}
|
|
|
|
_enterGroupper(element, relatedEvent) {
|
|
const tabster = this._tabster;
|
|
const ctx = RootAPI.getTabsterContext(tabster, element);
|
|
const groupper = (ctx === null || ctx === void 0 ? void 0 : ctx.groupper) || (ctx === null || ctx === void 0 ? void 0 : ctx.modalizerInGroupper);
|
|
const groupperElement = groupper === null || groupper === void 0 ? void 0 : groupper.getElement();
|
|
|
|
if (groupper && groupperElement && (element === groupperElement || groupper.getProps().delegated && element === groupper.getFirst(false))) {
|
|
const next = tabster.focusable.findNext({
|
|
container: groupperElement,
|
|
currentElement: element,
|
|
useActiveModalizer: true
|
|
});
|
|
|
|
if (next && (!relatedEvent || relatedEvent && groupperElement.dispatchEvent(new TabsterMoveFocusEvent({
|
|
by: "groupper",
|
|
owner: groupperElement,
|
|
next,
|
|
relatedEvent
|
|
})))) {
|
|
if (relatedEvent) {
|
|
// When the application hasn't prevented default,
|
|
// we consider the event completely handled, hence we
|
|
// prevent the initial event's default action and stop
|
|
// propagation.
|
|
relatedEvent.preventDefault();
|
|
relatedEvent.stopImmediatePropagation();
|
|
}
|
|
|
|
next.focus();
|
|
return next;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
_escapeGroupper(element, relatedEvent, fromModalizer) {
|
|
var _a;
|
|
|
|
const tabster = this._tabster;
|
|
const ctx = RootAPI.getTabsterContext(tabster, element);
|
|
const modalizerInGroupper = ctx === null || ctx === void 0 ? void 0 : ctx.modalizerInGroupper;
|
|
let groupper = (ctx === null || ctx === void 0 ? void 0 : ctx.groupper) || modalizerInGroupper;
|
|
const groupperElement = groupper === null || groupper === void 0 ? void 0 : groupper.getElement();
|
|
|
|
if (groupper && groupperElement && dom.nodeContains(groupperElement, element)) {
|
|
let next;
|
|
|
|
if (element !== groupperElement || fromModalizer) {
|
|
next = groupper.getFirst(true);
|
|
} else {
|
|
const parentElement = dom.getParentElement(groupperElement);
|
|
const parentCtx = parentElement ? RootAPI.getTabsterContext(tabster, parentElement) : undefined;
|
|
groupper = parentCtx === null || parentCtx === void 0 ? void 0 : parentCtx.groupper;
|
|
next = groupper === null || groupper === void 0 ? void 0 : groupper.getFirst(true);
|
|
}
|
|
|
|
if (next && (!relatedEvent || relatedEvent && groupperElement.dispatchEvent(new TabsterMoveFocusEvent({
|
|
by: "groupper",
|
|
owner: groupperElement,
|
|
next,
|
|
relatedEvent
|
|
})))) {
|
|
if (groupper) {
|
|
groupper.makeTabbable(false);
|
|
|
|
if (modalizerInGroupper) {
|
|
(_a = tabster.modalizer) === null || _a === void 0 ? void 0 : _a.setActive(undefined);
|
|
}
|
|
} // This part happens asynchronously inside setTimeout,
|
|
// so no need to prevent default or stop propagation.
|
|
|
|
|
|
next.focus();
|
|
return next;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
moveFocus(element, action) {
|
|
return action === GroupperMoveFocusActions.Enter ? this._enterGroupper(element) : this._escapeGroupper(element);
|
|
}
|
|
|
|
handleKeyPress(element, event, fromModalizer) {
|
|
const tabster = this._tabster;
|
|
const ctx = RootAPI.getTabsterContext(tabster, element);
|
|
|
|
if (ctx && ((ctx === null || ctx === void 0 ? void 0 : ctx.groupper) || (ctx === null || ctx === void 0 ? void 0 : ctx.modalizerInGroupper))) {
|
|
tabster.focusedElement.cancelAsyncFocus(AsyncFocusSources.EscapeGroupper);
|
|
|
|
if (ctx.ignoreKeydown(event)) {
|
|
return;
|
|
}
|
|
|
|
if (event.key === Keys.Enter) {
|
|
this._enterGroupper(element, event);
|
|
} else if (event.key === Keys.Escape) {
|
|
// We will handle Esc asynchronously, if something in the application will
|
|
// move focus during the keypress handling, we will not interfere.
|
|
const focusedElement = tabster.focusedElement.getFocusedElement();
|
|
tabster.focusedElement.requestAsyncFocus(AsyncFocusSources.EscapeGroupper, () => {
|
|
if (focusedElement !== tabster.focusedElement.getFocusedElement() && ( // A part of Modalizer that has called this handler to escape the active groupper
|
|
// might have been removed from DOM, if the focus is on body, we still want to handle Esc.
|
|
fromModalizer && !focusedElement || !fromModalizer)) {
|
|
// Something else in the application has moved focus, we will not handle Esc.
|
|
return;
|
|
}
|
|
|
|
this._escapeGroupper(element, event, fromModalizer);
|
|
}, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
function _setInformativeStyle$1(weakElement, remove) {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
const element = weakElement.get();
|
|
|
|
if (element) {
|
|
if (remove) {
|
|
element.style.removeProperty("--tabster-groupper");
|
|
} else {
|
|
element.style.setProperty("--tabster-groupper", "unlimited");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
class KeyboardNavigationState extends Subscribable {
|
|
constructor(getWindow) {
|
|
super();
|
|
|
|
this._onChange = isNavigatingWithKeyboard => {
|
|
this.setVal(isNavigatingWithKeyboard, undefined);
|
|
};
|
|
|
|
this._keyborg = createKeyborg(getWindow());
|
|
|
|
this._keyborg.subscribe(this._onChange);
|
|
}
|
|
|
|
dispose() {
|
|
super.dispose();
|
|
|
|
if (this._keyborg) {
|
|
this._keyborg.unsubscribe(this._onChange);
|
|
|
|
disposeKeyborg(this._keyborg);
|
|
delete this._keyborg;
|
|
}
|
|
}
|
|
|
|
setNavigatingWithKeyboard(isNavigatingWithKeyboard) {
|
|
var _a;
|
|
|
|
(_a = this._keyborg) === null || _a === void 0 ? void 0 : _a.setVal(isNavigatingWithKeyboard);
|
|
}
|
|
|
|
isNavigatingWithKeyboard() {
|
|
var _a;
|
|
|
|
return !!((_a = this._keyborg) === null || _a === void 0 ? void 0 : _a.isNavigatingWithKeyboard());
|
|
}
|
|
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
let _wasFocusedCounter = 0;
|
|
const _ariaHidden = "aria-hidden";
|
|
|
|
function _setInformativeStyle(weakElement, remove, internalId, userId, isActive, wasFocused) {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
const element = weakElement.get();
|
|
|
|
if (element) {
|
|
if (remove) {
|
|
element.style.removeProperty("--tabster-modalizer");
|
|
} else {
|
|
element.style.setProperty("--tabster-modalizer", internalId + "," + userId + "," + (isActive ? "active" : "inactive") + "," + "," + (wasFocused ? `focused(${wasFocused})` : "not-focused"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Manages the dummy inputs for the Modalizer.
|
|
*/
|
|
|
|
|
|
class ModalizerDummyManager extends DummyInputManager {
|
|
constructor(element, tabster, sys) {
|
|
super(tabster, element, DummyInputManagerPriorities.Modalizer, sys);
|
|
|
|
this._setHandlers((dummyInput, isBackward) => {
|
|
var _a, _b;
|
|
|
|
const el = element.get();
|
|
const container = el && ((_a = RootAPI.getRoot(tabster, el)) === null || _a === void 0 ? void 0 : _a.getElement());
|
|
const input = dummyInput.input;
|
|
let toFocus;
|
|
|
|
if (container && input) {
|
|
const dummyContainer = getDummyInputContainer(input);
|
|
const ctx = RootAPI.getTabsterContext(tabster, dummyContainer || input);
|
|
|
|
if (ctx) {
|
|
toFocus = (_b = FocusedElementState.findNextTabbable(tabster, ctx, container, input, undefined, isBackward, true)) === null || _b === void 0 ? void 0 : _b.element;
|
|
}
|
|
|
|
if (toFocus) {
|
|
nativeFocus(toFocus);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
class Modalizer extends TabsterPart {
|
|
constructor(tabster, element, onDispose, props, sys, activeElements) {
|
|
super(tabster, element, props);
|
|
this._wasFocused = 0;
|
|
this.userId = props.id;
|
|
this._onDispose = onDispose;
|
|
this._activeElements = activeElements;
|
|
|
|
if (!tabster.controlTab) {
|
|
this.dummyManager = new ModalizerDummyManager(this._element, tabster, sys);
|
|
}
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
_setInformativeStyle(this._element, false, this.id, this.userId, this._isActive, this._wasFocused);
|
|
}
|
|
}
|
|
|
|
makeActive(isActive) {
|
|
if (this._isActive !== isActive) {
|
|
this._isActive = isActive;
|
|
const element = this.getElement();
|
|
|
|
if (element) {
|
|
const activeElements = this._activeElements;
|
|
const index = activeElements.map(e => e.get()).indexOf(element);
|
|
|
|
if (isActive) {
|
|
if (index < 0) {
|
|
activeElements.push(new WeakHTMLElement(this._tabster.getWindow, element));
|
|
}
|
|
} else {
|
|
if (index >= 0) {
|
|
activeElements.splice(index, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
_setInformativeStyle(this._element, false, this.id, this.userId, this._isActive, this._wasFocused);
|
|
}
|
|
|
|
this._dispatchEvent(isActive);
|
|
}
|
|
}
|
|
|
|
focused(noIncrement) {
|
|
if (!noIncrement) {
|
|
this._wasFocused = ++_wasFocusedCounter;
|
|
}
|
|
|
|
return this._wasFocused;
|
|
}
|
|
|
|
setProps(props) {
|
|
if (props.id) {
|
|
this.userId = props.id;
|
|
}
|
|
|
|
this._props = { ...props
|
|
};
|
|
}
|
|
|
|
dispose() {
|
|
var _a;
|
|
|
|
this.makeActive(false);
|
|
|
|
this._onDispose(this);
|
|
|
|
(_a = this.dummyManager) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
delete this.dummyManager;
|
|
this._activeElements = [];
|
|
|
|
this._remove();
|
|
}
|
|
|
|
isActive() {
|
|
return !!this._isActive;
|
|
}
|
|
|
|
contains(element) {
|
|
return dom.nodeContains(this.getElement(), element);
|
|
}
|
|
|
|
findNextTabbable(currentElement, referenceElement, isBackward, ignoreAccessibility) {
|
|
var _a, _b;
|
|
|
|
const modalizerElement = this.getElement();
|
|
|
|
if (!modalizerElement) {
|
|
return null;
|
|
}
|
|
|
|
const tabster = this._tabster;
|
|
let next = null;
|
|
let outOfDOMOrder = false;
|
|
let uncontrolled;
|
|
const container = currentElement && ((_a = RootAPI.getRoot(tabster, currentElement)) === null || _a === void 0 ? void 0 : _a.getElement());
|
|
|
|
if (container) {
|
|
const findProps = {
|
|
container,
|
|
currentElement,
|
|
referenceElement,
|
|
ignoreAccessibility,
|
|
useActiveModalizer: true
|
|
};
|
|
const findPropsOut = {};
|
|
next = tabster.focusable[isBackward ? "findPrev" : "findNext"](findProps, findPropsOut);
|
|
|
|
if (!next && this._props.isTrapped && ((_b = tabster.modalizer) === null || _b === void 0 ? void 0 : _b.activeId)) {
|
|
next = tabster.focusable[isBackward ? "findLast" : "findFirst"]({
|
|
container,
|
|
ignoreAccessibility,
|
|
useActiveModalizer: true
|
|
}, findPropsOut);
|
|
outOfDOMOrder = true;
|
|
} else {
|
|
outOfDOMOrder = !!findPropsOut.outOfDOMOrder;
|
|
}
|
|
|
|
uncontrolled = findPropsOut.uncontrolled;
|
|
}
|
|
|
|
return {
|
|
element: next,
|
|
uncontrolled,
|
|
outOfDOMOrder
|
|
};
|
|
}
|
|
|
|
_dispatchEvent(isActive, allElements) {
|
|
const element = this.getElement();
|
|
let defaultPrevented = false;
|
|
|
|
if (element) {
|
|
const elements = allElements ? this._activeElements.map(e => e.get()) : [element];
|
|
|
|
for (const el of elements) {
|
|
if (el) {
|
|
const eventDetail = {
|
|
id: this.userId,
|
|
element
|
|
};
|
|
const event = isActive ? new ModalizerActiveEvent(eventDetail) : new ModalizerInactiveEvent(eventDetail);
|
|
el.dispatchEvent(event);
|
|
|
|
if (event.defaultPrevented) {
|
|
defaultPrevented = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return defaultPrevented;
|
|
}
|
|
|
|
_remove() {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
_setInformativeStyle(this._element, true);
|
|
}
|
|
}
|
|
|
|
} // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
|
|
class ModalizerAPI {
|
|
constructor(tabster, // @deprecated use accessibleCheck.
|
|
alwaysAccessibleSelector, accessibleCheck) {
|
|
this._onModalizerDispose = modalizer => {
|
|
const id = modalizer.id;
|
|
const userId = modalizer.userId;
|
|
const part = this._parts[userId];
|
|
delete this._modalizers[id];
|
|
|
|
if (part) {
|
|
delete part[id];
|
|
|
|
if (Object.keys(part).length === 0) {
|
|
delete this._parts[userId];
|
|
|
|
if (this.activeId === userId) {
|
|
this.setActive(undefined);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
this._onKeyDown = event => {
|
|
var _a;
|
|
|
|
if (event.key !== Keys.Escape) {
|
|
return;
|
|
}
|
|
|
|
const tabster = this._tabster;
|
|
const element = tabster.focusedElement.getFocusedElement();
|
|
|
|
if (element) {
|
|
const ctx = RootAPI.getTabsterContext(tabster, element);
|
|
const modalizer = ctx === null || ctx === void 0 ? void 0 : ctx.modalizer;
|
|
|
|
if (ctx && !ctx.groupper && (modalizer === null || modalizer === void 0 ? void 0 : modalizer.isActive()) && !ctx.ignoreKeydown(event)) {
|
|
const activeId = modalizer.userId;
|
|
|
|
if (activeId) {
|
|
const part = this._parts[activeId];
|
|
|
|
if (part) {
|
|
const focusedSince = Object.keys(part).map(id => {
|
|
var _a;
|
|
|
|
const m = part[id];
|
|
const el = m.getElement();
|
|
let groupper;
|
|
|
|
if (el) {
|
|
groupper = (_a = getTabsterOnElement(this._tabster, el)) === null || _a === void 0 ? void 0 : _a.groupper;
|
|
}
|
|
|
|
return m && el && groupper ? {
|
|
el,
|
|
focusedSince: m.focused(true)
|
|
} : {
|
|
focusedSince: 0
|
|
};
|
|
}).filter(f => f.focusedSince > 0).sort((a, b) => a.focusedSince > b.focusedSince ? -1 : a.focusedSince < b.focusedSince ? 1 : 0);
|
|
|
|
if (focusedSince.length) {
|
|
const groupperElement = focusedSince[0].el;
|
|
|
|
if (groupperElement) {
|
|
(_a = tabster.groupper) === null || _a === void 0 ? void 0 : _a.handleKeyPress(groupperElement, event, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
/**
|
|
* Subscribes to the focus state and handles modalizer related focus events
|
|
* @param focusedElement - Element that is focused
|
|
* @param detail - Additional data about the focus event
|
|
*/
|
|
|
|
|
|
this._onFocus = (focusedElement, detail) => {
|
|
var _a, _b;
|
|
|
|
const ctx = focusedElement && RootAPI.getTabsterContext(this._tabster, focusedElement); // Modalizer behaviour is opt in, only apply to elements that have a tabster context
|
|
|
|
if (!ctx || !focusedElement) {
|
|
return;
|
|
}
|
|
|
|
const augmentedMap = this._augMap;
|
|
|
|
for (let e = focusedElement; e; e = dom.getParentElement(e)) {
|
|
// If the newly focused element is inside some of the hidden containers,
|
|
// remove aria-hidden from those synchronously for the screen readers
|
|
// to be able to read the element. The rest of aria-hiddens, will be removed
|
|
// acynchronously for the sake of performance.
|
|
if (augmentedMap.has(e)) {
|
|
augmentedMap.delete(e);
|
|
augmentAttribute(this._tabster, e, _ariaHidden);
|
|
}
|
|
}
|
|
|
|
const modalizer = ctx.modalizer; // An inactive groupper with the modalizer on the same node will not give the modalizer
|
|
// in the context, yet we still want to track that the modalizer's container was focused.
|
|
|
|
(_b = modalizer || ((_a = getTabsterOnElement(this._tabster, focusedElement)) === null || _a === void 0 ? void 0 : _a.modalizer)) === null || _b === void 0 ? void 0 : _b.focused();
|
|
|
|
if ((modalizer === null || modalizer === void 0 ? void 0 : modalizer.userId) === this.activeId) {
|
|
this.currentIsOthersAccessible = modalizer === null || modalizer === void 0 ? void 0 : modalizer.getProps().isOthersAccessible;
|
|
return;
|
|
} // Developers calling `element.focus()` should change/deactivate active modalizer
|
|
|
|
|
|
if (detail.isFocusedProgrammatically || this.currentIsOthersAccessible || (modalizer === null || modalizer === void 0 ? void 0 : modalizer.getProps().isAlwaysAccessible)) {
|
|
this.setActive(modalizer);
|
|
} else {
|
|
// Focused outside of the active modalizer, try pull focus back to current modalizer
|
|
const win = this._win();
|
|
|
|
win.clearTimeout(this._restoreModalizerFocusTimer); // TODO some rendering frameworks (i.e. React) might async rerender the DOM so we need to wait for a duration
|
|
// Figure out a better way of doing this rather than a 100ms timeout
|
|
|
|
this._restoreModalizerFocusTimer = win.setTimeout(() => this._restoreModalizerFocus(focusedElement), 100);
|
|
}
|
|
};
|
|
|
|
this._tabster = tabster;
|
|
this._win = tabster.getWindow;
|
|
this._modalizers = {};
|
|
this._parts = {};
|
|
this._augMap = new WeakMap();
|
|
this._aug = [];
|
|
this._alwaysAccessibleSelector = alwaysAccessibleSelector;
|
|
this._accessibleCheck = accessibleCheck;
|
|
this.activeElements = [];
|
|
|
|
if (!tabster.controlTab) {
|
|
tabster.root.addDummyInputs();
|
|
}
|
|
|
|
const win = this._win();
|
|
|
|
win.addEventListener("keydown", this._onKeyDown, true);
|
|
tabster.queueInit(() => {
|
|
this._tabster.focusedElement.subscribe(this._onFocus);
|
|
});
|
|
}
|
|
|
|
dispose() {
|
|
const win = this._win();
|
|
|
|
win.removeEventListener("keydown", this._onKeyDown, true); // Dispose all modalizers managed by the API
|
|
|
|
Object.keys(this._modalizers).forEach(modalizerId => {
|
|
if (this._modalizers[modalizerId]) {
|
|
this._modalizers[modalizerId].dispose();
|
|
|
|
delete this._modalizers[modalizerId];
|
|
}
|
|
});
|
|
win.clearTimeout(this._restoreModalizerFocusTimer);
|
|
win.clearTimeout(this._hiddenUpdateTimer);
|
|
this._parts = {};
|
|
delete this.activeId;
|
|
this.activeElements = [];
|
|
this._augMap = new WeakMap();
|
|
this._aug = [];
|
|
|
|
this._tabster.focusedElement.unsubscribe(this._onFocus);
|
|
}
|
|
|
|
createModalizer(element, props, sys) {
|
|
var _a;
|
|
|
|
if (process.env.NODE_ENV === 'development') ;
|
|
|
|
const modalizer = new Modalizer(this._tabster, element, this._onModalizerDispose, props, sys, this.activeElements);
|
|
const id = modalizer.id;
|
|
const userId = props.id;
|
|
this._modalizers[id] = modalizer;
|
|
let part = this._parts[userId];
|
|
|
|
if (!part) {
|
|
part = this._parts[userId] = {};
|
|
}
|
|
|
|
part[id] = modalizer; // Adding a modalizer which is already focused, activate it
|
|
|
|
if (dom.nodeContains(element, (_a = this._tabster.focusedElement.getFocusedElement()) !== null && _a !== void 0 ? _a : null)) {
|
|
if (userId !== this.activeId) {
|
|
this.setActive(modalizer);
|
|
} else {
|
|
modalizer.makeActive(true);
|
|
}
|
|
}
|
|
|
|
return modalizer;
|
|
}
|
|
|
|
isAugmented(element) {
|
|
return this._augMap.has(element);
|
|
}
|
|
|
|
hiddenUpdate() {
|
|
if (this._hiddenUpdateTimer) {
|
|
return;
|
|
}
|
|
|
|
this._hiddenUpdateTimer = this._win().setTimeout(() => {
|
|
delete this._hiddenUpdateTimer;
|
|
|
|
this._hiddenUpdate();
|
|
}, 250);
|
|
}
|
|
|
|
setActive(modalizer) {
|
|
const userId = modalizer === null || modalizer === void 0 ? void 0 : modalizer.userId;
|
|
const activeId = this.activeId;
|
|
|
|
if (activeId === userId) {
|
|
return;
|
|
}
|
|
|
|
this.activeId = userId;
|
|
|
|
if (activeId) {
|
|
const part = this._parts[activeId];
|
|
|
|
if (part) {
|
|
for (const id of Object.keys(part)) {
|
|
part[id].makeActive(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (userId) {
|
|
const part = this._parts[userId];
|
|
|
|
if (part) {
|
|
for (const id of Object.keys(part)) {
|
|
part[id].makeActive(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.currentIsOthersAccessible = modalizer === null || modalizer === void 0 ? void 0 : modalizer.getProps().isOthersAccessible;
|
|
this.hiddenUpdate();
|
|
}
|
|
|
|
focus(elementFromModalizer, noFocusFirst, noFocusDefault) {
|
|
const ctx = RootAPI.getTabsterContext(this._tabster, elementFromModalizer);
|
|
const modalizer = ctx === null || ctx === void 0 ? void 0 : ctx.modalizer;
|
|
|
|
if (modalizer) {
|
|
this.setActive(modalizer);
|
|
const props = modalizer.getProps();
|
|
const modalizerRoot = modalizer.getElement();
|
|
|
|
if (modalizerRoot) {
|
|
if (noFocusFirst === undefined) {
|
|
noFocusFirst = props.isNoFocusFirst;
|
|
}
|
|
|
|
if (!noFocusFirst && this._tabster.keyboardNavigation.isNavigatingWithKeyboard() && this._tabster.focusedElement.focusFirst({
|
|
container: modalizerRoot
|
|
})) {
|
|
return true;
|
|
}
|
|
|
|
if (noFocusDefault === undefined) {
|
|
noFocusDefault = props.isNoFocusDefault;
|
|
}
|
|
|
|
if (!noFocusDefault && this._tabster.focusedElement.focusDefault(modalizerRoot)) {
|
|
return true;
|
|
}
|
|
|
|
this._tabster.focusedElement.resetFocus(modalizerRoot);
|
|
}
|
|
} else if (process.env.NODE_ENV === 'development') {
|
|
console.error("Element is not in Modalizer.", elementFromModalizer);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
acceptElement(element, state) {
|
|
var _a;
|
|
|
|
const modalizerUserId = state.modalizerUserId;
|
|
const currentModalizer = (_a = state.currentCtx) === null || _a === void 0 ? void 0 : _a.modalizer;
|
|
|
|
if (modalizerUserId) {
|
|
for (const e of this.activeElements) {
|
|
const el = e.get();
|
|
|
|
if (el && (dom.nodeContains(element, el) || el === element)) {
|
|
// We have a part of currently active modalizer somewhere deeper in the DOM,
|
|
// skipping all other checks.
|
|
return NodeFilter.FILTER_SKIP;
|
|
}
|
|
}
|
|
}
|
|
|
|
const ret = modalizerUserId === (currentModalizer === null || currentModalizer === void 0 ? void 0 : currentModalizer.userId) || !modalizerUserId && (currentModalizer === null || currentModalizer === void 0 ? void 0 : currentModalizer.getProps().isAlwaysAccessible) ? undefined : NodeFilter.FILTER_SKIP;
|
|
|
|
if (ret !== undefined) {
|
|
state.skippedFocusable = true;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
_hiddenUpdate() {
|
|
var _a;
|
|
|
|
const tabster = this._tabster;
|
|
const body = tabster.getWindow().document.body;
|
|
const activeId = this.activeId;
|
|
const parts = this._parts;
|
|
const visibleElements = [];
|
|
const hiddenElements = [];
|
|
const alwaysAccessibleSelector = this._alwaysAccessibleSelector;
|
|
const alwaysAccessibleElements = alwaysAccessibleSelector ? Array.from(dom.querySelectorAll(body, alwaysAccessibleSelector)) : [];
|
|
const activeModalizerElements = [];
|
|
|
|
for (const userId of Object.keys(parts)) {
|
|
const modalizerParts = parts[userId];
|
|
|
|
for (const id of Object.keys(modalizerParts)) {
|
|
const modalizer = modalizerParts[id];
|
|
const el = modalizer.getElement();
|
|
const props = modalizer.getProps();
|
|
const isAlwaysAccessible = props.isAlwaysAccessible;
|
|
|
|
if (el) {
|
|
if (userId === activeId) {
|
|
activeModalizerElements.push(el);
|
|
|
|
if (!this.currentIsOthersAccessible) {
|
|
visibleElements.push(el);
|
|
}
|
|
} else if (isAlwaysAccessible) {
|
|
alwaysAccessibleElements.push(el);
|
|
} else {
|
|
hiddenElements.push(el);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const augmentedMap = this._augMap;
|
|
const allVisibleElements = visibleElements.length > 0 ? [...visibleElements, ...alwaysAccessibleElements] : undefined;
|
|
const newAugmented = [];
|
|
const newAugmentedMap = new WeakMap();
|
|
|
|
const toggle = (element, hide) => {
|
|
var _a;
|
|
|
|
const tagName = element.tagName;
|
|
|
|
if (tagName === "SCRIPT" || tagName === "STYLE") {
|
|
return;
|
|
}
|
|
|
|
let isAugmented = false;
|
|
|
|
if (augmentedMap.has(element)) {
|
|
if (hide) {
|
|
isAugmented = true;
|
|
} else {
|
|
augmentedMap.delete(element);
|
|
augmentAttribute(tabster, element, _ariaHidden);
|
|
}
|
|
} else if (hide && !((_a = this._accessibleCheck) === null || _a === void 0 ? void 0 : _a.call(this, element, activeModalizerElements)) && augmentAttribute(tabster, element, _ariaHidden, "true")) {
|
|
augmentedMap.set(element, true);
|
|
isAugmented = true;
|
|
}
|
|
|
|
if (isAugmented) {
|
|
newAugmented.push(new WeakHTMLElement(tabster.getWindow, element));
|
|
newAugmentedMap.set(element, true);
|
|
}
|
|
};
|
|
|
|
const walk = element => {
|
|
var _a;
|
|
|
|
for (let el = dom.getFirstElementChild(element); el; el = dom.getNextElementSibling(el)) {
|
|
let skip = false;
|
|
let containsModalizer = false;
|
|
let containedByModalizer = false;
|
|
|
|
if (allVisibleElements) {
|
|
const elParent = tabster.getParent(el);
|
|
|
|
for (const c of allVisibleElements) {
|
|
if (el === c) {
|
|
skip = true;
|
|
break;
|
|
}
|
|
|
|
if (dom.nodeContains(el, c)) {
|
|
containsModalizer = true;
|
|
break;
|
|
} else if (dom.nodeContains(c, elParent)) {
|
|
// tabster.getParent() could be provided by the application to
|
|
// handle, for example, virtual parents. Making sure, we are
|
|
// not setting aria-hidden on elements which are virtually
|
|
// inside modalizer.
|
|
containedByModalizer = true;
|
|
}
|
|
}
|
|
|
|
if (containsModalizer || ((_a = el.__tabsterElementFlags) === null || _a === void 0 ? void 0 : _a.noDirectAriaHidden)) {
|
|
walk(el);
|
|
} else if (!skip && !containedByModalizer) {
|
|
toggle(el, true);
|
|
}
|
|
} else {
|
|
toggle(el, false);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (!allVisibleElements) {
|
|
alwaysAccessibleElements.forEach(e => toggle(e, false));
|
|
}
|
|
|
|
hiddenElements.forEach(e => toggle(e, true));
|
|
|
|
if (body) {
|
|
walk(body);
|
|
}
|
|
|
|
(_a = this._aug) === null || _a === void 0 ? void 0 : _a.map(e => e.get()).forEach(e => {
|
|
if (e && !newAugmentedMap.get(e)) {
|
|
toggle(e, false);
|
|
}
|
|
});
|
|
this._aug = newAugmented;
|
|
this._augMap = newAugmentedMap;
|
|
}
|
|
/**
|
|
* Called when an element is focused outside of an active modalizer.
|
|
* Attempts to pull focus back into the active modalizer
|
|
* @param outsideElement - An element being focused outside of the modalizer
|
|
*/
|
|
|
|
|
|
_restoreModalizerFocus(outsideElement) {
|
|
const ownerDocument = outsideElement === null || outsideElement === void 0 ? void 0 : outsideElement.ownerDocument;
|
|
|
|
if (!outsideElement || !ownerDocument) {
|
|
return;
|
|
}
|
|
|
|
const ctx = RootAPI.getTabsterContext(this._tabster, outsideElement);
|
|
const modalizer = ctx === null || ctx === void 0 ? void 0 : ctx.modalizer;
|
|
const activeId = this.activeId;
|
|
|
|
if (!modalizer && !activeId || modalizer && activeId === modalizer.userId) {
|
|
return;
|
|
}
|
|
|
|
const container = ctx === null || ctx === void 0 ? void 0 : ctx.root.getElement();
|
|
|
|
if (container) {
|
|
let toFocus = this._tabster.focusable.findFirst({
|
|
container,
|
|
useActiveModalizer: true
|
|
});
|
|
|
|
if (toFocus) {
|
|
if (outsideElement.compareDocumentPosition(toFocus) & document.DOCUMENT_POSITION_PRECEDING) {
|
|
toFocus = this._tabster.focusable.findLast({
|
|
container,
|
|
useActiveModalizer: true
|
|
});
|
|
|
|
if (!toFocus) {
|
|
// This only might mean that findFirst/findLast are buggy and inconsistent.
|
|
throw new Error("Something went wrong.");
|
|
}
|
|
}
|
|
|
|
this._tabster.focusedElement.focus(toFocus);
|
|
|
|
return;
|
|
}
|
|
} // Current Modalizer doesn't seem to have focusable elements.
|
|
// Blurring the currently focused element which is outside of the current Modalizer.
|
|
|
|
|
|
outsideElement.blur();
|
|
}
|
|
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
|
|
const _inputSelector = /*#__PURE__*/["input", "textarea", "*[contenteditable]"].join(", ");
|
|
|
|
class MoverDummyManager extends DummyInputManager {
|
|
constructor(element, tabster, getMemorized, sys) {
|
|
super(tabster, element, DummyInputManagerPriorities.Mover, sys);
|
|
|
|
this._onFocusDummyInput = dummyInput => {
|
|
var _a, _b;
|
|
|
|
const container = this._element.get();
|
|
|
|
const input = dummyInput.input;
|
|
|
|
if (container && input) {
|
|
const ctx = RootAPI.getTabsterContext(this._tabster, container);
|
|
let toFocus;
|
|
|
|
if (ctx) {
|
|
toFocus = (_a = FocusedElementState.findNextTabbable(this._tabster, ctx, undefined, input, undefined, !dummyInput.isFirst, true)) === null || _a === void 0 ? void 0 : _a.element;
|
|
}
|
|
|
|
const memorized = (_b = this._getMemorized()) === null || _b === void 0 ? void 0 : _b.get();
|
|
|
|
if (memorized && this._tabster.focusable.isFocusable(memorized)) {
|
|
toFocus = memorized;
|
|
}
|
|
|
|
if (toFocus) {
|
|
nativeFocus(toFocus);
|
|
}
|
|
}
|
|
};
|
|
|
|
this._tabster = tabster;
|
|
this._getMemorized = getMemorized;
|
|
|
|
this._setHandlers(this._onFocusDummyInput);
|
|
}
|
|
|
|
} // TypeScript enums produce depressing JavaScript code, so, we're just using
|
|
// a few old style constants here.
|
|
|
|
|
|
const _moverUpdateAdd = 1;
|
|
const _moverUpdateAttr = 2;
|
|
const _moverUpdateRemove = 3;
|
|
class Mover extends TabsterPart {
|
|
constructor(tabster, element, onDispose, props, sys) {
|
|
var _a;
|
|
|
|
super(tabster, element, props);
|
|
this._visible = {};
|
|
|
|
this._onIntersection = entries => {
|
|
for (const entry of entries) {
|
|
const el = entry.target;
|
|
const id = getElementUId(this._win, el);
|
|
let newVisibility;
|
|
let fullyVisible = this._fullyVisible;
|
|
|
|
if (entry.intersectionRatio >= 0.25) {
|
|
newVisibility = entry.intersectionRatio >= 0.75 ? Visibilities.Visible : Visibilities.PartiallyVisible;
|
|
|
|
if (newVisibility === Visibilities.Visible) {
|
|
fullyVisible = id;
|
|
}
|
|
} else {
|
|
newVisibility = Visibilities.Invisible;
|
|
}
|
|
|
|
if (this._visible[id] !== newVisibility) {
|
|
if (newVisibility === undefined) {
|
|
delete this._visible[id];
|
|
|
|
if (fullyVisible === id) {
|
|
delete this._fullyVisible;
|
|
}
|
|
} else {
|
|
this._visible[id] = newVisibility;
|
|
this._fullyVisible = fullyVisible;
|
|
}
|
|
|
|
const state = this.getState(el);
|
|
|
|
if (state) {
|
|
el.dispatchEvent(new MoverStateEvent(state));
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
this._win = tabster.getWindow;
|
|
this.visibilityTolerance = (_a = props.visibilityTolerance) !== null && _a !== void 0 ? _a : 0.8;
|
|
|
|
if (this._props.trackState || this._props.visibilityAware) {
|
|
this._intersectionObserver = new IntersectionObserver(this._onIntersection, {
|
|
threshold: [0, 0.25, 0.5, 0.75, 1]
|
|
});
|
|
|
|
this._observeState();
|
|
}
|
|
|
|
this._onDispose = onDispose;
|
|
|
|
const getMemorized = () => props.memorizeCurrent ? this._current : undefined;
|
|
|
|
if (!tabster.controlTab) {
|
|
this.dummyManager = new MoverDummyManager(this._element, tabster, getMemorized, sys);
|
|
}
|
|
}
|
|
|
|
dispose() {
|
|
var _a;
|
|
|
|
this._onDispose(this);
|
|
|
|
if (this._intersectionObserver) {
|
|
this._intersectionObserver.disconnect();
|
|
|
|
delete this._intersectionObserver;
|
|
}
|
|
|
|
delete this._current;
|
|
delete this._fullyVisible;
|
|
delete this._allElements;
|
|
delete this._updateQueue;
|
|
|
|
if (this._unobserve) {
|
|
this._unobserve();
|
|
|
|
delete this._unobserve;
|
|
}
|
|
|
|
const win = this._win();
|
|
|
|
if (this._setCurrentTimer) {
|
|
win.clearTimeout(this._setCurrentTimer);
|
|
delete this._setCurrentTimer;
|
|
}
|
|
|
|
if (this._updateTimer) {
|
|
win.clearTimeout(this._updateTimer);
|
|
delete this._updateTimer;
|
|
}
|
|
|
|
(_a = this.dummyManager) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
delete this.dummyManager;
|
|
}
|
|
|
|
setCurrent(element) {
|
|
if (element) {
|
|
this._current = new WeakHTMLElement(this._win, element);
|
|
} else {
|
|
this._current = undefined;
|
|
}
|
|
|
|
if ((this._props.trackState || this._props.visibilityAware) && !this._setCurrentTimer) {
|
|
this._setCurrentTimer = this._win().setTimeout(() => {
|
|
var _a;
|
|
|
|
delete this._setCurrentTimer;
|
|
const changed = [];
|
|
|
|
if (this._current !== this._prevCurrent) {
|
|
changed.push(this._current);
|
|
changed.push(this._prevCurrent);
|
|
this._prevCurrent = this._current;
|
|
}
|
|
|
|
for (const weak of changed) {
|
|
const el = weak === null || weak === void 0 ? void 0 : weak.get();
|
|
|
|
if (el && ((_a = this._allElements) === null || _a === void 0 ? void 0 : _a.get(el)) === this) {
|
|
const props = this._props;
|
|
|
|
if (el && (props.visibilityAware !== undefined || props.trackState)) {
|
|
const state = this.getState(el);
|
|
|
|
if (state) {
|
|
el.dispatchEvent(new MoverStateEvent(state));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
getCurrent() {
|
|
var _a;
|
|
|
|
return ((_a = this._current) === null || _a === void 0 ? void 0 : _a.get()) || null;
|
|
}
|
|
|
|
findNextTabbable(currentElement, referenceElement, isBackward, ignoreAccessibility) {
|
|
const container = this.getElement();
|
|
const currentIsDummy = container && getDummyInputContainer(currentElement) === container;
|
|
|
|
if (!container) {
|
|
return null;
|
|
}
|
|
|
|
let next = null;
|
|
let outOfDOMOrder = false;
|
|
let uncontrolled;
|
|
|
|
if (this._props.tabbable || currentIsDummy || currentElement && !dom.nodeContains(container, currentElement)) {
|
|
const findProps = {
|
|
currentElement,
|
|
referenceElement,
|
|
container,
|
|
ignoreAccessibility,
|
|
useActiveModalizer: true
|
|
};
|
|
const findPropsOut = {};
|
|
next = this._tabster.focusable[isBackward ? "findPrev" : "findNext"](findProps, findPropsOut);
|
|
outOfDOMOrder = !!findPropsOut.outOfDOMOrder;
|
|
uncontrolled = findPropsOut.uncontrolled;
|
|
}
|
|
|
|
return {
|
|
element: next,
|
|
uncontrolled,
|
|
outOfDOMOrder
|
|
};
|
|
}
|
|
|
|
acceptElement(element, state) {
|
|
var _a, _b;
|
|
|
|
if (!FocusedElementState.isTabbing) {
|
|
return ((_a = state.currentCtx) === null || _a === void 0 ? void 0 : _a.excludedFromMover) ? NodeFilter.FILTER_REJECT : undefined;
|
|
}
|
|
|
|
const {
|
|
memorizeCurrent,
|
|
visibilityAware,
|
|
hasDefault = true
|
|
} = this._props;
|
|
const moverElement = this.getElement();
|
|
|
|
if (moverElement && (memorizeCurrent || visibilityAware || hasDefault) && (!dom.nodeContains(moverElement, state.from) || getDummyInputContainer(state.from) === moverElement)) {
|
|
let found;
|
|
|
|
if (memorizeCurrent) {
|
|
const current = (_b = this._current) === null || _b === void 0 ? void 0 : _b.get();
|
|
|
|
if (current && state.acceptCondition(current)) {
|
|
found = current;
|
|
}
|
|
}
|
|
|
|
if (!found && hasDefault) {
|
|
found = this._tabster.focusable.findDefault({
|
|
container: moverElement,
|
|
useActiveModalizer: true
|
|
});
|
|
}
|
|
|
|
if (!found && visibilityAware) {
|
|
found = this._tabster.focusable.findElement({
|
|
container: moverElement,
|
|
useActiveModalizer: true,
|
|
isBackward: state.isBackward,
|
|
acceptCondition: el => {
|
|
var _a;
|
|
|
|
const id = getElementUId(this._win, el);
|
|
const visibility = this._visible[id];
|
|
return moverElement !== el && !!((_a = this._allElements) === null || _a === void 0 ? void 0 : _a.get(el)) && state.acceptCondition(el) && (visibility === Visibilities.Visible || visibility === Visibilities.PartiallyVisible && (visibilityAware === Visibilities.PartiallyVisible || !this._fullyVisible));
|
|
}
|
|
});
|
|
}
|
|
|
|
if (found) {
|
|
state.found = true;
|
|
state.foundElement = found;
|
|
state.rejectElementsFrom = moverElement;
|
|
state.skippedFocusable = true;
|
|
return NodeFilter.FILTER_ACCEPT;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
_observeState() {
|
|
const element = this.getElement();
|
|
|
|
if (this._unobserve || !element || typeof MutationObserver === "undefined") {
|
|
return;
|
|
}
|
|
|
|
const win = this._win();
|
|
|
|
const allElements = this._allElements = new WeakMap();
|
|
const tabsterFocusable = this._tabster.focusable;
|
|
let updateQueue = this._updateQueue = [];
|
|
const observer = dom.createMutationObserver(mutations => {
|
|
for (const mutation of mutations) {
|
|
const target = mutation.target;
|
|
const removed = mutation.removedNodes;
|
|
const added = mutation.addedNodes;
|
|
|
|
if (mutation.type === "attributes") {
|
|
if (mutation.attributeName === "tabindex") {
|
|
updateQueue.push({
|
|
element: target,
|
|
type: _moverUpdateAttr
|
|
});
|
|
}
|
|
} else {
|
|
for (let i = 0; i < removed.length; i++) {
|
|
updateQueue.push({
|
|
element: removed[i],
|
|
type: _moverUpdateRemove
|
|
});
|
|
}
|
|
|
|
for (let i = 0; i < added.length; i++) {
|
|
updateQueue.push({
|
|
element: added[i],
|
|
type: _moverUpdateAdd
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
requestUpdate();
|
|
});
|
|
|
|
const setElement = (element, remove) => {
|
|
var _a, _b;
|
|
|
|
const current = allElements.get(element);
|
|
|
|
if (current && remove) {
|
|
(_a = this._intersectionObserver) === null || _a === void 0 ? void 0 : _a.unobserve(element);
|
|
allElements.delete(element);
|
|
}
|
|
|
|
if (!current && !remove) {
|
|
allElements.set(element, this);
|
|
(_b = this._intersectionObserver) === null || _b === void 0 ? void 0 : _b.observe(element);
|
|
}
|
|
};
|
|
|
|
const updateElement = element => {
|
|
const isFocusable = tabsterFocusable.isFocusable(element);
|
|
const current = allElements.get(element);
|
|
|
|
if (current) {
|
|
if (!isFocusable) {
|
|
setElement(element, true);
|
|
}
|
|
} else {
|
|
if (isFocusable) {
|
|
setElement(element);
|
|
}
|
|
}
|
|
};
|
|
|
|
const addNewElements = element => {
|
|
const {
|
|
mover
|
|
} = getMoverGroupper(element);
|
|
|
|
if (mover && mover !== this) {
|
|
if (mover.getElement() === element && tabsterFocusable.isFocusable(element)) {
|
|
setElement(element);
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
const walker = createElementTreeWalker(win.document, element, node => {
|
|
const {
|
|
mover,
|
|
groupper
|
|
} = getMoverGroupper(node);
|
|
|
|
if (mover && mover !== this) {
|
|
return NodeFilter.FILTER_REJECT;
|
|
}
|
|
|
|
const groupperFirstFocusable = groupper === null || groupper === void 0 ? void 0 : groupper.getFirst(true);
|
|
|
|
if (groupper && groupper.getElement() !== node && groupperFirstFocusable && groupperFirstFocusable !== node) {
|
|
return NodeFilter.FILTER_REJECT;
|
|
}
|
|
|
|
if (tabsterFocusable.isFocusable(node)) {
|
|
setElement(node);
|
|
}
|
|
|
|
return NodeFilter.FILTER_SKIP;
|
|
});
|
|
|
|
if (walker) {
|
|
walker.currentNode = element;
|
|
|
|
while (walker.nextNode()) {
|
|
/* Iterating for the sake of calling processNode() callback. */
|
|
}
|
|
}
|
|
};
|
|
|
|
const removeWalk = element => {
|
|
const current = allElements.get(element);
|
|
|
|
if (current) {
|
|
setElement(element, true);
|
|
}
|
|
|
|
for (let el = dom.getFirstElementChild(element); el; el = dom.getNextElementSibling(el)) {
|
|
removeWalk(el);
|
|
}
|
|
};
|
|
|
|
const requestUpdate = () => {
|
|
if (!this._updateTimer && updateQueue.length) {
|
|
this._updateTimer = win.setTimeout(() => {
|
|
delete this._updateTimer;
|
|
|
|
for (const {
|
|
element,
|
|
type
|
|
} of updateQueue) {
|
|
switch (type) {
|
|
case _moverUpdateAttr:
|
|
updateElement(element);
|
|
break;
|
|
|
|
case _moverUpdateAdd:
|
|
addNewElements(element);
|
|
break;
|
|
|
|
case _moverUpdateRemove:
|
|
removeWalk(element);
|
|
break;
|
|
}
|
|
}
|
|
|
|
updateQueue = this._updateQueue = [];
|
|
}, 0);
|
|
}
|
|
};
|
|
|
|
const getMoverGroupper = element => {
|
|
const ret = {};
|
|
|
|
for (let el = element; el; el = dom.getParentElement(el)) {
|
|
const toe = getTabsterOnElement(this._tabster, el);
|
|
|
|
if (toe) {
|
|
if (toe.groupper && !ret.groupper) {
|
|
ret.groupper = toe.groupper;
|
|
}
|
|
|
|
if (toe.mover) {
|
|
ret.mover = toe.mover;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
updateQueue.push({
|
|
element,
|
|
type: _moverUpdateAdd
|
|
});
|
|
requestUpdate();
|
|
observer.observe(element, {
|
|
childList: true,
|
|
subtree: true,
|
|
attributes: true,
|
|
attributeFilter: ["tabindex"]
|
|
});
|
|
|
|
this._unobserve = () => {
|
|
observer.disconnect();
|
|
};
|
|
}
|
|
|
|
getState(element) {
|
|
const id = getElementUId(this._win, element);
|
|
|
|
if (id in this._visible) {
|
|
const visibility = this._visible[id] || Visibilities.Invisible;
|
|
const isCurrent = this._current ? this._current.get() === element : undefined;
|
|
return {
|
|
isCurrent,
|
|
visibility
|
|
};
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
} // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
/**
|
|
* Calculates distance between two rectangles.
|
|
*
|
|
* @param ax1 first rectangle left
|
|
* @param ay1 first rectangle top
|
|
* @param ax2 first rectangle right
|
|
* @param ay2 first rectangle bottom
|
|
* @param bx1 second rectangle left
|
|
* @param by1 second rectangle top
|
|
* @param bx2 second rectangle right
|
|
* @param by2 second rectangle bottom
|
|
* @returns number, shortest distance between the rectangles.
|
|
*/
|
|
|
|
|
|
function getDistance(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2) {
|
|
const xDistance = ax2 < bx1 ? bx1 - ax2 : bx2 < ax1 ? ax1 - bx2 : 0;
|
|
const yDistance = ay2 < by1 ? by1 - ay2 : by2 < ay1 ? ay1 - by2 : 0;
|
|
return xDistance === 0 ? yDistance : yDistance === 0 ? xDistance : Math.sqrt(xDistance * xDistance + yDistance * yDistance);
|
|
}
|
|
|
|
class MoverAPI {
|
|
constructor(tabster, getWindow) {
|
|
this._init = () => {
|
|
const win = this._win();
|
|
|
|
win.addEventListener("keydown", this._onKeyDown, true);
|
|
win.addEventListener(MoverMoveFocusEventName, this._onMoveFocus);
|
|
win.addEventListener(MoverMemorizedElementEventName, this._onMemorizedElement);
|
|
|
|
this._tabster.focusedElement.subscribe(this._onFocus);
|
|
};
|
|
|
|
this._onMoverDispose = mover => {
|
|
delete this._movers[mover.id];
|
|
};
|
|
|
|
this._onFocus = element => {
|
|
var _a; // When something in the app gets focused, we are making sure that
|
|
// the relevant context Mover is aware of it.
|
|
// Looking for the relevant context Mover from the currently
|
|
// focused element parent, not from the element itself, because the
|
|
// Mover element itself cannot be its own current (but might be
|
|
// current for its parent Mover).
|
|
|
|
|
|
let currentFocusableElement = element;
|
|
let deepestFocusableElement = element;
|
|
|
|
for (let el = dom.getParentElement(element); el; el = dom.getParentElement(el)) {
|
|
// We go through all Movers up from the focused element and
|
|
// set their current element to the deepest focusable of that
|
|
// Mover.
|
|
const mover = (_a = getTabsterOnElement(this._tabster, el)) === null || _a === void 0 ? void 0 : _a.mover;
|
|
|
|
if (mover) {
|
|
mover.setCurrent(deepestFocusableElement);
|
|
currentFocusableElement = undefined;
|
|
}
|
|
|
|
if (!currentFocusableElement && this._tabster.focusable.isFocusable(el)) {
|
|
currentFocusableElement = deepestFocusableElement = el;
|
|
}
|
|
}
|
|
};
|
|
|
|
this._onKeyDown = async event => {
|
|
var _a;
|
|
|
|
if (this._ignoredInputTimer) {
|
|
this._win().clearTimeout(this._ignoredInputTimer);
|
|
|
|
delete this._ignoredInputTimer;
|
|
}
|
|
|
|
(_a = this._ignoredInputResolve) === null || _a === void 0 ? void 0 : _a.call(this, false); // Give a chance to other listeners to handle the event (for example,
|
|
// to scroll instead of moving focus).
|
|
|
|
if (event.ctrlKey || event.altKey || event.shiftKey || event.metaKey) {
|
|
return;
|
|
}
|
|
|
|
const key = event.key;
|
|
let moverKey;
|
|
|
|
if (key === Keys.ArrowDown) {
|
|
moverKey = MoverKeys.ArrowDown;
|
|
} else if (key === Keys.ArrowRight) {
|
|
moverKey = MoverKeys.ArrowRight;
|
|
} else if (key === Keys.ArrowUp) {
|
|
moverKey = MoverKeys.ArrowUp;
|
|
} else if (key === Keys.ArrowLeft) {
|
|
moverKey = MoverKeys.ArrowLeft;
|
|
} else if (key === Keys.PageDown) {
|
|
moverKey = MoverKeys.PageDown;
|
|
} else if (key === Keys.PageUp) {
|
|
moverKey = MoverKeys.PageUp;
|
|
} else if (key === Keys.Home) {
|
|
moverKey = MoverKeys.Home;
|
|
} else if (key === Keys.End) {
|
|
moverKey = MoverKeys.End;
|
|
}
|
|
|
|
if (!moverKey) {
|
|
return;
|
|
}
|
|
|
|
const focused = this._tabster.focusedElement.getFocusedElement();
|
|
|
|
if (!focused || (await this._isIgnoredInput(focused, key))) {
|
|
return;
|
|
}
|
|
|
|
this._moveFocus(focused, moverKey, event);
|
|
};
|
|
|
|
this._onMoveFocus = e => {
|
|
var _a;
|
|
|
|
const element = e.composedPath()[0];
|
|
const key = (_a = e.detail) === null || _a === void 0 ? void 0 : _a.key;
|
|
|
|
if (element && key !== undefined && !e.defaultPrevented) {
|
|
this._moveFocus(element, key);
|
|
|
|
e.stopImmediatePropagation();
|
|
}
|
|
};
|
|
|
|
this._onMemorizedElement = e => {
|
|
var _a;
|
|
|
|
const target = e.composedPath()[0];
|
|
let memorizedElement = (_a = e.detail) === null || _a === void 0 ? void 0 : _a.memorizedElement;
|
|
|
|
if (target) {
|
|
const ctx = RootAPI.getTabsterContext(this._tabster, target);
|
|
const mover = ctx === null || ctx === void 0 ? void 0 : ctx.mover;
|
|
|
|
if (mover) {
|
|
if (memorizedElement && !dom.nodeContains(mover.getElement(), memorizedElement)) {
|
|
memorizedElement = undefined;
|
|
}
|
|
|
|
mover.setCurrent(memorizedElement);
|
|
e.stopImmediatePropagation();
|
|
}
|
|
}
|
|
};
|
|
|
|
this._tabster = tabster;
|
|
this._win = getWindow;
|
|
this._movers = {};
|
|
tabster.queueInit(this._init);
|
|
}
|
|
|
|
dispose() {
|
|
var _a;
|
|
|
|
const win = this._win();
|
|
|
|
this._tabster.focusedElement.unsubscribe(this._onFocus);
|
|
|
|
(_a = this._ignoredInputResolve) === null || _a === void 0 ? void 0 : _a.call(this, false);
|
|
|
|
if (this._ignoredInputTimer) {
|
|
win.clearTimeout(this._ignoredInputTimer);
|
|
delete this._ignoredInputTimer;
|
|
}
|
|
|
|
win.removeEventListener("keydown", this._onKeyDown, true);
|
|
win.removeEventListener(MoverMoveFocusEventName, this._onMoveFocus);
|
|
win.removeEventListener(MoverMemorizedElementEventName, this._onMemorizedElement);
|
|
Object.keys(this._movers).forEach(moverId => {
|
|
if (this._movers[moverId]) {
|
|
this._movers[moverId].dispose();
|
|
|
|
delete this._movers[moverId];
|
|
}
|
|
});
|
|
}
|
|
|
|
createMover(element, props, sys) {
|
|
if (process.env.NODE_ENV === 'development') ;
|
|
|
|
const newMover = new Mover(this._tabster, element, this._onMoverDispose, props, sys);
|
|
this._movers[newMover.id] = newMover;
|
|
return newMover;
|
|
}
|
|
|
|
moveFocus(fromElement, key) {
|
|
return this._moveFocus(fromElement, key);
|
|
}
|
|
|
|
_moveFocus(fromElement, key, relatedEvent) {
|
|
var _a, _b;
|
|
|
|
const tabster = this._tabster;
|
|
const ctx = RootAPI.getTabsterContext(tabster, fromElement, {
|
|
checkRtl: true
|
|
});
|
|
|
|
if (!ctx || !ctx.mover || ctx.excludedFromMover || relatedEvent && ctx.ignoreKeydown(relatedEvent)) {
|
|
return null;
|
|
}
|
|
|
|
const mover = ctx.mover;
|
|
const container = mover.getElement();
|
|
|
|
if (ctx.groupperBeforeMover) {
|
|
const groupper = ctx.groupper;
|
|
|
|
if (groupper && !groupper.isActive(true)) {
|
|
// For the cases when we have Mover/Active Groupper/Inactive Groupper, we need to check
|
|
// the grouppers between the current element and the current mover.
|
|
for (let el = dom.getParentElement(groupper.getElement()); el && el !== container; el = dom.getParentElement(el)) {
|
|
if ((_b = (_a = getTabsterOnElement(tabster, el)) === null || _a === void 0 ? void 0 : _a.groupper) === null || _b === void 0 ? void 0 : _b.isActive(true)) {
|
|
return null;
|
|
}
|
|
}
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if (!container) {
|
|
return null;
|
|
}
|
|
|
|
const focusable = tabster.focusable;
|
|
const moverProps = mover.getProps();
|
|
const direction = moverProps.direction || MoverDirections.Both;
|
|
const isBoth = direction === MoverDirections.Both;
|
|
const isVertical = isBoth || direction === MoverDirections.Vertical;
|
|
const isHorizontal = isBoth || direction === MoverDirections.Horizontal;
|
|
const isGridLinear = direction === MoverDirections.GridLinear;
|
|
const isGrid = isGridLinear || direction === MoverDirections.Grid;
|
|
const isCyclic = moverProps.cyclic;
|
|
let next;
|
|
let scrollIntoViewArg;
|
|
let focusedElementRect;
|
|
let focusedElementX1 = 0;
|
|
let focusedElementX2 = 0;
|
|
|
|
if (isGrid) {
|
|
focusedElementRect = fromElement.getBoundingClientRect();
|
|
focusedElementX1 = Math.ceil(focusedElementRect.left);
|
|
focusedElementX2 = Math.floor(focusedElementRect.right);
|
|
}
|
|
|
|
if (ctx.rtl) {
|
|
if (key === MoverKeys.ArrowRight) {
|
|
key = MoverKeys.ArrowLeft;
|
|
} else if (key === MoverKeys.ArrowLeft) {
|
|
key = MoverKeys.ArrowRight;
|
|
}
|
|
}
|
|
|
|
if (key === MoverKeys.ArrowDown && isVertical || key === MoverKeys.ArrowRight && (isHorizontal || isGrid)) {
|
|
next = focusable.findNext({
|
|
currentElement: fromElement,
|
|
container,
|
|
useActiveModalizer: true
|
|
});
|
|
|
|
if (next && isGrid) {
|
|
const nextElementX1 = Math.ceil(next.getBoundingClientRect().left);
|
|
|
|
if (!isGridLinear && focusedElementX2 > nextElementX1) {
|
|
next = undefined;
|
|
}
|
|
} else if (!next && isCyclic) {
|
|
next = focusable.findFirst({
|
|
container,
|
|
useActiveModalizer: true
|
|
});
|
|
}
|
|
} else if (key === MoverKeys.ArrowUp && isVertical || key === MoverKeys.ArrowLeft && (isHorizontal || isGrid)) {
|
|
next = focusable.findPrev({
|
|
currentElement: fromElement,
|
|
container,
|
|
useActiveModalizer: true
|
|
});
|
|
|
|
if (next && isGrid) {
|
|
const nextElementX2 = Math.floor(next.getBoundingClientRect().right);
|
|
|
|
if (!isGridLinear && nextElementX2 > focusedElementX1) {
|
|
next = undefined;
|
|
}
|
|
} else if (!next && isCyclic) {
|
|
next = focusable.findLast({
|
|
container,
|
|
useActiveModalizer: true
|
|
});
|
|
}
|
|
} else if (key === MoverKeys.Home) {
|
|
if (isGrid) {
|
|
focusable.findElement({
|
|
container,
|
|
currentElement: fromElement,
|
|
useActiveModalizer: true,
|
|
isBackward: true,
|
|
acceptCondition: el => {
|
|
var _a;
|
|
|
|
if (!focusable.isFocusable(el)) {
|
|
return false;
|
|
}
|
|
|
|
const nextElementX1 = Math.ceil((_a = el.getBoundingClientRect().left) !== null && _a !== void 0 ? _a : 0);
|
|
|
|
if (el !== fromElement && focusedElementX1 <= nextElementX1) {
|
|
return true;
|
|
}
|
|
|
|
next = el;
|
|
return false;
|
|
}
|
|
});
|
|
} else {
|
|
next = focusable.findFirst({
|
|
container,
|
|
useActiveModalizer: true
|
|
});
|
|
}
|
|
} else if (key === MoverKeys.End) {
|
|
if (isGrid) {
|
|
focusable.findElement({
|
|
container,
|
|
currentElement: fromElement,
|
|
useActiveModalizer: true,
|
|
acceptCondition: el => {
|
|
var _a;
|
|
|
|
if (!focusable.isFocusable(el)) {
|
|
return false;
|
|
}
|
|
|
|
const nextElementX1 = Math.ceil((_a = el.getBoundingClientRect().left) !== null && _a !== void 0 ? _a : 0);
|
|
|
|
if (el !== fromElement && focusedElementX1 >= nextElementX1) {
|
|
return true;
|
|
}
|
|
|
|
next = el;
|
|
return false;
|
|
}
|
|
});
|
|
} else {
|
|
next = focusable.findLast({
|
|
container,
|
|
useActiveModalizer: true
|
|
});
|
|
}
|
|
} else if (key === MoverKeys.PageUp) {
|
|
focusable.findElement({
|
|
currentElement: fromElement,
|
|
container,
|
|
useActiveModalizer: true,
|
|
isBackward: true,
|
|
acceptCondition: el => {
|
|
if (!focusable.isFocusable(el)) {
|
|
return false;
|
|
}
|
|
|
|
if (isElementVerticallyVisibleInContainer(this._win, el, mover.visibilityTolerance)) {
|
|
next = el;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}); // will be on the first column move forward and preserve previous column
|
|
|
|
if (isGrid && next) {
|
|
const firstColumnX1 = Math.ceil(next.getBoundingClientRect().left);
|
|
focusable.findElement({
|
|
currentElement: next,
|
|
container,
|
|
useActiveModalizer: true,
|
|
acceptCondition: el => {
|
|
if (!focusable.isFocusable(el)) {
|
|
return false;
|
|
}
|
|
|
|
const nextElementX1 = Math.ceil(el.getBoundingClientRect().left);
|
|
|
|
if (focusedElementX1 < nextElementX1 || firstColumnX1 >= nextElementX1) {
|
|
return true;
|
|
}
|
|
|
|
next = el;
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
scrollIntoViewArg = false;
|
|
} else if (key === MoverKeys.PageDown) {
|
|
focusable.findElement({
|
|
currentElement: fromElement,
|
|
container,
|
|
useActiveModalizer: true,
|
|
acceptCondition: el => {
|
|
if (!focusable.isFocusable(el)) {
|
|
return false;
|
|
}
|
|
|
|
if (isElementVerticallyVisibleInContainer(this._win, el, mover.visibilityTolerance)) {
|
|
next = el;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}); // will be on the last column move backwards and preserve previous column
|
|
|
|
if (isGrid && next) {
|
|
const lastColumnX1 = Math.ceil(next.getBoundingClientRect().left);
|
|
focusable.findElement({
|
|
currentElement: next,
|
|
container,
|
|
useActiveModalizer: true,
|
|
isBackward: true,
|
|
acceptCondition: el => {
|
|
if (!focusable.isFocusable(el)) {
|
|
return false;
|
|
}
|
|
|
|
const nextElementX1 = Math.ceil(el.getBoundingClientRect().left);
|
|
|
|
if (focusedElementX1 > nextElementX1 || lastColumnX1 <= nextElementX1) {
|
|
return true;
|
|
}
|
|
|
|
next = el;
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
scrollIntoViewArg = true;
|
|
} else if (isGrid) {
|
|
const isBackward = key === MoverKeys.ArrowUp;
|
|
const ax1 = focusedElementX1; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
|
|
const ay1 = Math.ceil(focusedElementRect.top);
|
|
const ax2 = focusedElementX2; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
|
|
const ay2 = Math.floor(focusedElementRect.bottom);
|
|
let targetElement;
|
|
let lastDistance;
|
|
let lastIntersection = 0;
|
|
focusable.findAll({
|
|
container,
|
|
currentElement: fromElement,
|
|
isBackward,
|
|
onElement: el => {
|
|
// Find element which has maximal intersection with the focused element horizontally,
|
|
// or the closest one.
|
|
const rect = el.getBoundingClientRect();
|
|
const bx1 = Math.ceil(rect.left);
|
|
const by1 = Math.ceil(rect.top);
|
|
const bx2 = Math.floor(rect.right);
|
|
const by2 = Math.floor(rect.bottom);
|
|
|
|
if (isBackward && ay1 < by2 || !isBackward && ay2 > by1) {
|
|
// Only consider elements which are below/above curretly focused.
|
|
return true;
|
|
}
|
|
|
|
const xIntersectionWidth = Math.ceil(Math.min(ax2, bx2)) - Math.floor(Math.max(ax1, bx1));
|
|
const minWidth = Math.ceil(Math.min(ax2 - ax1, bx2 - bx1));
|
|
|
|
if (xIntersectionWidth > 0 && minWidth >= xIntersectionWidth) {
|
|
// Element intersects with the focused element on X axis.
|
|
const intersection = xIntersectionWidth / minWidth;
|
|
|
|
if (intersection > lastIntersection) {
|
|
targetElement = el;
|
|
lastIntersection = intersection;
|
|
}
|
|
} else if (lastIntersection === 0) {
|
|
// If we didn't have intersection, try just the closest one.
|
|
const distance = getDistance(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2);
|
|
|
|
if (lastDistance === undefined || distance < lastDistance) {
|
|
lastDistance = distance;
|
|
targetElement = el;
|
|
}
|
|
} else if (lastIntersection > 0) {
|
|
// Element doesn't intersect, but we had intersection already, stop search.
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
});
|
|
next = targetElement;
|
|
}
|
|
|
|
if (next && (!relatedEvent || relatedEvent && container.dispatchEvent(new TabsterMoveFocusEvent({
|
|
by: "mover",
|
|
owner: container,
|
|
next,
|
|
relatedEvent
|
|
})))) {
|
|
if (scrollIntoViewArg !== undefined) {
|
|
scrollIntoView(this._win, next, scrollIntoViewArg);
|
|
}
|
|
|
|
if (relatedEvent) {
|
|
relatedEvent.preventDefault();
|
|
relatedEvent.stopImmediatePropagation();
|
|
}
|
|
|
|
nativeFocus(next);
|
|
return next;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async _isIgnoredInput(element, key) {
|
|
if (element.getAttribute("aria-expanded") === "true" && element.hasAttribute("aria-activedescendant")) {
|
|
// It is likely a combobox with expanded options and arrow keys are
|
|
// controlled by it.
|
|
return true;
|
|
}
|
|
|
|
if (matchesSelector(element, _inputSelector)) {
|
|
let selectionStart = 0;
|
|
let selectionEnd = 0;
|
|
let textLength = 0;
|
|
let asyncRet;
|
|
|
|
if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") {
|
|
const type = element.type;
|
|
const value = element.value;
|
|
textLength = (value || "").length;
|
|
|
|
if (type === "email" || type === "number") {
|
|
// For these types Chromium doesn't provide selectionStart and selectionEnd.
|
|
// Hence the ugly workaround to find if the caret position is changed with
|
|
// the keypress.
|
|
// TODO: Have a look at range, week, time, time, date, datetime-local.
|
|
if (textLength) {
|
|
const selection = dom.getSelection(element);
|
|
|
|
if (selection) {
|
|
const initialLength = selection.toString().length;
|
|
const isBackward = key === Keys.ArrowLeft || key === Keys.ArrowUp;
|
|
selection.modify("extend", isBackward ? "backward" : "forward", "character");
|
|
|
|
if (initialLength !== selection.toString().length) {
|
|
// The caret is moved, so, we're not on the edge of the value.
|
|
// Restore original selection.
|
|
selection.modify("extend", isBackward ? "forward" : "backward", "character");
|
|
return true;
|
|
} else {
|
|
textLength = 0;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
const selStart = element.selectionStart;
|
|
|
|
if (selStart === null) {
|
|
// Do not ignore not text editable inputs like checkboxes and radios (but ignore hidden).
|
|
return type === "hidden";
|
|
}
|
|
|
|
selectionStart = selStart || 0;
|
|
selectionEnd = element.selectionEnd || 0;
|
|
}
|
|
} else if (element.contentEditable === "true") {
|
|
asyncRet = new (getPromise(this._win))(resolve => {
|
|
this._ignoredInputResolve = value => {
|
|
delete this._ignoredInputResolve;
|
|
resolve(value);
|
|
};
|
|
|
|
const win = this._win();
|
|
|
|
if (this._ignoredInputTimer) {
|
|
win.clearTimeout(this._ignoredInputTimer);
|
|
}
|
|
|
|
const {
|
|
anchorNode: prevAnchorNode,
|
|
focusNode: prevFocusNode,
|
|
anchorOffset: prevAnchorOffset,
|
|
focusOffset: prevFocusOffset
|
|
} = dom.getSelection(element) || {}; // Get selection gives incorrect value if we call it syncronously onKeyDown.
|
|
|
|
this._ignoredInputTimer = win.setTimeout(() => {
|
|
var _a, _b, _c;
|
|
|
|
delete this._ignoredInputTimer;
|
|
const {
|
|
anchorNode,
|
|
focusNode,
|
|
anchorOffset,
|
|
focusOffset
|
|
} = dom.getSelection(element) || {};
|
|
|
|
if (anchorNode !== prevAnchorNode || focusNode !== prevFocusNode || anchorOffset !== prevAnchorOffset || focusOffset !== prevFocusOffset) {
|
|
(_a = this._ignoredInputResolve) === null || _a === void 0 ? void 0 : _a.call(this, false);
|
|
return;
|
|
}
|
|
|
|
selectionStart = anchorOffset || 0;
|
|
selectionEnd = focusOffset || 0;
|
|
textLength = ((_b = element.textContent) === null || _b === void 0 ? void 0 : _b.length) || 0;
|
|
|
|
if (anchorNode && focusNode) {
|
|
if (dom.nodeContains(element, anchorNode) && dom.nodeContains(element, focusNode)) {
|
|
if (anchorNode !== element) {
|
|
let anchorFound = false;
|
|
|
|
const addOffsets = node => {
|
|
if (node === anchorNode) {
|
|
anchorFound = true;
|
|
} else if (node === focusNode) {
|
|
return true;
|
|
}
|
|
|
|
const nodeText = node.textContent;
|
|
|
|
if (nodeText && !dom.getFirstChild(node)) {
|
|
const len = nodeText.length;
|
|
|
|
if (anchorFound) {
|
|
if (focusNode !== anchorNode) {
|
|
selectionEnd += len;
|
|
}
|
|
} else {
|
|
selectionStart += len;
|
|
selectionEnd += len;
|
|
}
|
|
}
|
|
|
|
let stop = false;
|
|
|
|
for (let e = dom.getFirstChild(node); e && !stop; e = e.nextSibling) {
|
|
stop = addOffsets(e);
|
|
}
|
|
|
|
return stop;
|
|
};
|
|
|
|
addOffsets(element);
|
|
}
|
|
}
|
|
}
|
|
|
|
(_c = this._ignoredInputResolve) === null || _c === void 0 ? void 0 : _c.call(this, true);
|
|
}, 0);
|
|
});
|
|
}
|
|
|
|
if (asyncRet && !(await asyncRet)) {
|
|
return true;
|
|
}
|
|
|
|
if (selectionStart !== selectionEnd) {
|
|
return true;
|
|
}
|
|
|
|
if (selectionStart > 0 && (key === Keys.ArrowLeft || key === Keys.ArrowUp || key === Keys.Home)) {
|
|
return true;
|
|
}
|
|
|
|
if (selectionStart < textLength && (key === Keys.ArrowRight || key === Keys.ArrowDown || key === Keys.End)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
function observeMutations(doc, tabster, updateTabsterByAttribute, syncState) {
|
|
if (typeof MutationObserver === "undefined") {
|
|
return () => {
|
|
/* Noop */
|
|
};
|
|
}
|
|
|
|
const getWindow = tabster.getWindow;
|
|
let elementByUId;
|
|
|
|
const onMutation = mutations => {
|
|
var _a, _b, _c, _d, _e;
|
|
|
|
const removedNodes = new Set();
|
|
|
|
for (const mutation of mutations) {
|
|
const target = mutation.target;
|
|
const removed = mutation.removedNodes;
|
|
const added = mutation.addedNodes;
|
|
|
|
if (mutation.type === "attributes") {
|
|
if (mutation.attributeName === TABSTER_ATTRIBUTE_NAME) {
|
|
// removedNodes helps to make sure we are not recreating things
|
|
// for the removed elements.
|
|
// For some reason, if we do removeChild() and setAttribute() on the
|
|
// removed child in the same tick, both the child removal and the attribute
|
|
// change will be present in the mutation records. And the attribute change
|
|
// will follow the child removal.
|
|
// So, we remember the removed nodes and ignore attribute changes for them.
|
|
if (!removedNodes.has(target)) {
|
|
updateTabsterByAttribute(tabster, target);
|
|
}
|
|
}
|
|
} else {
|
|
for (let i = 0; i < removed.length; i++) {
|
|
const removedNode = removed[i];
|
|
removedNodes.add(removedNode);
|
|
updateTabsterElements(removedNode, true);
|
|
(_b = (_a = tabster._dummyObserver).domChanged) === null || _b === void 0 ? void 0 : _b.call(_a, target);
|
|
}
|
|
|
|
for (let i = 0; i < added.length; i++) {
|
|
updateTabsterElements(added[i]);
|
|
(_d = (_c = tabster._dummyObserver).domChanged) === null || _d === void 0 ? void 0 : _d.call(_c, target);
|
|
}
|
|
}
|
|
}
|
|
|
|
removedNodes.clear();
|
|
(_e = tabster.modalizer) === null || _e === void 0 ? void 0 : _e.hiddenUpdate();
|
|
};
|
|
|
|
function updateTabsterElements(node, removed) {
|
|
if (!elementByUId) {
|
|
elementByUId = getInstanceContext(getWindow).elementByUId;
|
|
}
|
|
|
|
processNode(node, removed);
|
|
const walker = createElementTreeWalker(doc, node, element => {
|
|
return processNode(element, removed);
|
|
});
|
|
|
|
if (walker) {
|
|
while (walker.nextNode()) {
|
|
/* Iterating for the sake of calling processNode() callback. */
|
|
}
|
|
}
|
|
}
|
|
|
|
function processNode(element, removed) {
|
|
var _a;
|
|
|
|
if (!element.getAttribute) {
|
|
// It might actually be a text node.
|
|
return NodeFilter.FILTER_SKIP;
|
|
}
|
|
|
|
const uid = element.__tabsterElementUID;
|
|
|
|
if (uid && elementByUId) {
|
|
if (removed) {
|
|
delete elementByUId[uid];
|
|
} else {
|
|
(_a = elementByUId[uid]) !== null && _a !== void 0 ? _a : elementByUId[uid] = new WeakHTMLElement(getWindow, element);
|
|
}
|
|
}
|
|
|
|
if (getTabsterOnElement(tabster, element) || element.hasAttribute(TABSTER_ATTRIBUTE_NAME)) {
|
|
updateTabsterByAttribute(tabster, element, removed);
|
|
}
|
|
|
|
return NodeFilter.FILTER_SKIP;
|
|
}
|
|
|
|
const observer = dom.createMutationObserver(onMutation);
|
|
|
|
if (syncState) {
|
|
updateTabsterElements(getWindow().document.body);
|
|
}
|
|
|
|
observer.observe(doc, {
|
|
childList: true,
|
|
subtree: true,
|
|
attributes: true,
|
|
attributeFilter: [TABSTER_ATTRIBUTE_NAME]
|
|
});
|
|
return () => {
|
|
observer.disconnect();
|
|
};
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
const _conditionCheckTimeout = 100;
|
|
class ObservedElementAPI extends Subscribable {
|
|
constructor(tabster) {
|
|
super();
|
|
this._waiting = {};
|
|
this._lastRequestFocusId = 0;
|
|
this._observedById = {};
|
|
this._observedByName = {};
|
|
this._currentRequestTimestamp = 0;
|
|
|
|
this._onFocus = e => {
|
|
if (e) {
|
|
const current = this._currentRequest;
|
|
|
|
if (current) {
|
|
const delta = Date.now() - this._currentRequestTimestamp;
|
|
|
|
const settleTime = 300;
|
|
|
|
if (delta >= settleTime) {
|
|
// Giving some time for the focus to settle before
|
|
// automatically cancelling the current request on focus change.
|
|
delete this._currentRequest;
|
|
current.cancel();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
this.onObservedElementUpdate = element => {
|
|
var _a;
|
|
|
|
const observed = (_a = getTabsterOnElement(this._tabster, element)) === null || _a === void 0 ? void 0 : _a.observed;
|
|
const uid = getElementUId(this._win, element);
|
|
let info = this._observedById[uid];
|
|
|
|
if (observed && documentContains(element.ownerDocument, element)) {
|
|
if (!info) {
|
|
info = this._observedById[uid] = {
|
|
element: new WeakHTMLElement(this._win, element)
|
|
};
|
|
}
|
|
|
|
observed.names.sort();
|
|
const observedNames = observed.names;
|
|
const prevNames = info.prevNames; // prevNames are already sorted
|
|
|
|
if (this._isObservedNamesUpdated(observedNames, prevNames)) {
|
|
if (prevNames) {
|
|
prevNames.forEach(prevName => {
|
|
const obn = this._observedByName[prevName];
|
|
|
|
if (obn && obn[uid]) {
|
|
if (Object.keys(obn).length > 1) {
|
|
delete obn[uid];
|
|
} else {
|
|
delete this._observedByName[prevName];
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
info.prevNames = observedNames;
|
|
}
|
|
|
|
observedNames.forEach(observedName => {
|
|
let obn = this._observedByName[observedName];
|
|
|
|
if (!obn) {
|
|
obn = this._observedByName[observedName] = {};
|
|
} // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
|
|
|
|
obn[uid] = info;
|
|
|
|
this._waitConditional(observedName);
|
|
});
|
|
} else if (info) {
|
|
const prevNames = info.prevNames;
|
|
|
|
if (prevNames) {
|
|
prevNames.forEach(prevName => {
|
|
const obn = this._observedByName[prevName];
|
|
|
|
if (obn && obn[uid]) {
|
|
if (Object.keys(obn).length > 1) {
|
|
delete obn[uid];
|
|
} else {
|
|
delete this._observedByName[prevName];
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
delete this._observedById[uid];
|
|
}
|
|
};
|
|
|
|
this._tabster = tabster;
|
|
this._win = tabster.getWindow;
|
|
tabster.queueInit(() => {
|
|
this._tabster.focusedElement.subscribe(this._onFocus);
|
|
});
|
|
}
|
|
|
|
dispose() {
|
|
this._tabster.focusedElement.unsubscribe(this._onFocus);
|
|
|
|
for (const key of Object.keys(this._waiting)) {
|
|
this._rejectWaiting(key);
|
|
}
|
|
|
|
this._observedById = {};
|
|
this._observedByName = {};
|
|
}
|
|
|
|
_rejectWaiting(key, shouldResolve) {
|
|
const w = this._waiting[key];
|
|
|
|
if (w) {
|
|
const win = this._win();
|
|
|
|
if (w.timer) {
|
|
win.clearTimeout(w.timer);
|
|
}
|
|
|
|
if (w.conditionTimer) {
|
|
win.clearTimeout(w.conditionTimer);
|
|
}
|
|
|
|
if (!shouldResolve && w.reject) {
|
|
w.reject();
|
|
} else if (shouldResolve && w.resolve) {
|
|
w.resolve(null);
|
|
}
|
|
|
|
delete this._waiting[key];
|
|
}
|
|
}
|
|
|
|
_isObservedNamesUpdated(cur, prev) {
|
|
if (!prev || cur.length !== prev.length) {
|
|
return true;
|
|
}
|
|
|
|
for (let i = 0; i < cur.length; ++i) {
|
|
if (cur[i] !== prev[i]) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
/**
|
|
* Returns existing element by observed name
|
|
*
|
|
* @param observedName An observed name
|
|
* @param accessibility Optionally, return only if the element is accessible or focusable
|
|
* @returns HTMLElement | null
|
|
*/
|
|
|
|
|
|
getElement(observedName, accessibility) {
|
|
const o = this._observedByName[observedName];
|
|
|
|
if (o) {
|
|
for (const uid of Object.keys(o)) {
|
|
let el = o[uid].element.get() || null;
|
|
|
|
if (el) {
|
|
if (accessibility === ObservedElementAccessibilities.Accessible && !this._tabster.focusable.isAccessible(el) || accessibility === ObservedElementAccessibilities.Focusable && !this._tabster.focusable.isFocusable(el, true)) {
|
|
el = null;
|
|
}
|
|
} else {
|
|
delete o[uid];
|
|
delete this._observedById[uid];
|
|
}
|
|
|
|
return el;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
/**
|
|
* Waits for the element to appear in the DOM and returns it.
|
|
*
|
|
* @param observedName An observed name
|
|
* @param timeout Wait no longer than this timeout
|
|
* @param accessibility Optionally, wait for the element to also become accessible or focusable before returning it
|
|
* @returns Promise<HTMLElement | null>
|
|
*/
|
|
|
|
|
|
waitElement(observedName, timeout, accessibility) {
|
|
const el = this.getElement(observedName, accessibility);
|
|
|
|
if (el) {
|
|
return {
|
|
result: getPromise(this._win).resolve(el),
|
|
cancel: () => {
|
|
/**/
|
|
}
|
|
};
|
|
}
|
|
|
|
let prefix;
|
|
|
|
if (accessibility === ObservedElementAccessibilities.Accessible) {
|
|
prefix = "a";
|
|
} else if (accessibility === ObservedElementAccessibilities.Focusable) {
|
|
prefix = "f";
|
|
} else {
|
|
prefix = "_";
|
|
}
|
|
|
|
const key = prefix + observedName;
|
|
let w = this._waiting[key];
|
|
|
|
if (w && w.request) {
|
|
return w.request;
|
|
}
|
|
|
|
w = this._waiting[key] = {
|
|
timer: this._win().setTimeout(() => {
|
|
if (w.conditionTimer) {
|
|
this._win().clearTimeout(w.conditionTimer);
|
|
}
|
|
|
|
delete this._waiting[key];
|
|
|
|
if (w.resolve) {
|
|
w.resolve(null);
|
|
}
|
|
}, timeout)
|
|
};
|
|
const promise = new (getPromise(this._win))((resolve, reject) => {
|
|
w.resolve = resolve;
|
|
w.reject = reject;
|
|
});
|
|
w.request = {
|
|
result: promise,
|
|
cancel: () => {
|
|
this._rejectWaiting(key, true);
|
|
}
|
|
};
|
|
|
|
if (accessibility && this.getElement(observedName)) {
|
|
// If the observed element is alread in DOM, but not accessible yet,
|
|
// we need to run the wait logic.
|
|
this._waitConditional(observedName);
|
|
}
|
|
|
|
return w.request;
|
|
}
|
|
|
|
requestFocus(observedName, timeout) {
|
|
const requestId = ++this._lastRequestFocusId;
|
|
const currentRequestFocus = this._currentRequest;
|
|
|
|
if (currentRequestFocus) {
|
|
currentRequestFocus.cancel();
|
|
}
|
|
|
|
const request = this.waitElement(observedName, timeout, ObservedElementAccessibilities.Focusable);
|
|
this._currentRequest = request;
|
|
this._currentRequestTimestamp = Date.now();
|
|
request.result.finally(() => {
|
|
if (this._currentRequest === request) {
|
|
delete this._currentRequest;
|
|
}
|
|
});
|
|
return {
|
|
result: request.result.then(element => this._lastRequestFocusId === requestId && element ? this._tabster.focusedElement.focus(element, true) : false),
|
|
cancel: () => {
|
|
request.cancel();
|
|
}
|
|
};
|
|
}
|
|
|
|
_waitConditional(observedName) {
|
|
const waitingElementKey = "_" + observedName;
|
|
const waitingAccessibleElementKey = "a" + observedName;
|
|
const waitingFocusableElementKey = "f" + observedName;
|
|
const waitingElement = this._waiting[waitingElementKey];
|
|
const waitingAccessibleElement = this._waiting[waitingAccessibleElementKey];
|
|
const waitingFocusableElement = this._waiting[waitingFocusableElementKey];
|
|
|
|
const win = this._win();
|
|
|
|
const resolve = (element, key, waiting, accessibility) => {
|
|
var _a;
|
|
|
|
const observed = (_a = getTabsterOnElement(this._tabster, element)) === null || _a === void 0 ? void 0 : _a.observed;
|
|
|
|
if (!observed || !observed.names.includes(observedName)) {
|
|
return;
|
|
}
|
|
|
|
if (waiting.timer) {
|
|
win.clearTimeout(waiting.timer);
|
|
}
|
|
|
|
delete this._waiting[key];
|
|
|
|
if (waiting.resolve) {
|
|
waiting.resolve(element);
|
|
}
|
|
|
|
this.trigger(element, {
|
|
names: [observedName],
|
|
details: observed.details,
|
|
accessibility
|
|
});
|
|
};
|
|
|
|
if (waitingElement) {
|
|
const element = this.getElement(observedName);
|
|
|
|
if (element && documentContains(element.ownerDocument, element)) {
|
|
resolve(element, waitingElementKey, waitingElement, ObservedElementAccessibilities.Any);
|
|
}
|
|
}
|
|
|
|
if (waitingAccessibleElement && !waitingAccessibleElement.conditionTimer) {
|
|
const resolveAccessible = () => {
|
|
const element = this.getElement(observedName);
|
|
|
|
if (element && documentContains(element.ownerDocument, element) && this._tabster.focusable.isAccessible(element)) {
|
|
resolve(element, waitingAccessibleElementKey, waitingAccessibleElement, ObservedElementAccessibilities.Accessible);
|
|
} else {
|
|
waitingAccessibleElement.conditionTimer = win.setTimeout(resolveAccessible, _conditionCheckTimeout);
|
|
}
|
|
};
|
|
|
|
resolveAccessible();
|
|
}
|
|
|
|
if (waitingFocusableElement && !waitingFocusableElement.conditionTimer) {
|
|
const resolveFocusable = () => {
|
|
const element = this.getElement(observedName);
|
|
|
|
if (element && documentContains(element.ownerDocument, element) && this._tabster.focusable.isFocusable(element, true)) {
|
|
resolve(element, waitingFocusableElementKey, waitingFocusableElement, ObservedElementAccessibilities.Focusable);
|
|
} else {
|
|
waitingFocusableElement.conditionTimer = win.setTimeout(resolveFocusable, _conditionCheckTimeout);
|
|
}
|
|
};
|
|
|
|
resolveFocusable();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
const defaultProps = {
|
|
areaClass: "tabster-focus-outline-area",
|
|
outlineClass: "tabster-focus-outline",
|
|
outlineColor: "#ff4500",
|
|
outlineWidth: 2,
|
|
zIndex: 2147483647
|
|
};
|
|
let _props = defaultProps;
|
|
|
|
class OutlinePosition {
|
|
constructor(left, top, right, bottom) {
|
|
this.left = left;
|
|
this.top = top;
|
|
this.right = right;
|
|
this.bottom = bottom;
|
|
}
|
|
|
|
equalsTo(other) {
|
|
return this.left === other.left && this.top === other.top && this.right === other.right && this.bottom === other.bottom;
|
|
}
|
|
|
|
clone() {
|
|
return new OutlinePosition(this.left, this.top, this.right, this.bottom);
|
|
}
|
|
|
|
}
|
|
|
|
class OutlineAPI {
|
|
constructor(tabster) {
|
|
this._isVisible = false;
|
|
this._allOutlineElements = [];
|
|
|
|
this._init = () => {
|
|
this._tabster.keyboardNavigation.subscribe(this._onKeyboardNavigationStateChanged);
|
|
|
|
this._tabster.focusedElement.subscribe(this._onFocus);
|
|
|
|
const win = this._win();
|
|
|
|
win.addEventListener("scroll", this._onScroll, true); // Capture!
|
|
|
|
if (this._fullScreenEventName) {
|
|
win.document.addEventListener(this._fullScreenEventName, this._onFullScreenChanged);
|
|
}
|
|
};
|
|
|
|
this._onFullScreenChanged = e => {
|
|
if (!this._fullScreenElementName || !e.target) {
|
|
return;
|
|
}
|
|
|
|
const target = e.target.body || e.target;
|
|
|
|
const outlineElements = this._getDOM(target);
|
|
|
|
if (target.ownerDocument && outlineElements) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const fsElement = target.ownerDocument[this._fullScreenElementName];
|
|
|
|
if (fsElement) {
|
|
fsElement.appendChild(outlineElements.container);
|
|
this._fullScreenElement = fsElement;
|
|
} else {
|
|
target.ownerDocument.body.appendChild(outlineElements.container);
|
|
this._fullScreenElement = undefined;
|
|
}
|
|
}
|
|
};
|
|
|
|
this._onKeyboardNavigationStateChanged = () => {
|
|
this._onFocus(this._tabster.focusedElement.getFocusedElement());
|
|
};
|
|
|
|
this._onFocus = e => {
|
|
if (!this._updateElement(e) && this._isVisible) {
|
|
this._setVisibility(false);
|
|
}
|
|
};
|
|
|
|
this._onScroll = e => {
|
|
if (!this._outlinedElement || !OutlineAPI._isParentChild(e.target, this._outlinedElement)) {
|
|
return;
|
|
}
|
|
|
|
this._curPos = undefined;
|
|
|
|
this._setOutlinePosition();
|
|
};
|
|
|
|
this._tabster = tabster;
|
|
this._win = tabster.getWindow;
|
|
tabster.queueInit(this._init);
|
|
|
|
if (typeof document !== "undefined") {
|
|
if ("onfullscreenchange" in document) {
|
|
this._fullScreenEventName = "fullscreenchange";
|
|
this._fullScreenElementName = "fullscreenElement";
|
|
} else if ("onwebkitfullscreenchange" in document) {
|
|
this._fullScreenEventName = "webkitfullscreenchange";
|
|
this._fullScreenElementName = "webkitFullscreenElement";
|
|
} else if ("onmozfullscreenchange" in document) {
|
|
this._fullScreenEventName = "mozfullscreenchange";
|
|
this._fullScreenElementName = "mozFullScreenElement";
|
|
} else if ("onmsfullscreenchange" in document) {
|
|
this._fullScreenEventName = "msfullscreenchange";
|
|
this._fullScreenElementName = "msFullscreenElement";
|
|
}
|
|
}
|
|
}
|
|
|
|
setup(props) {
|
|
_props = { ..._props,
|
|
...props
|
|
};
|
|
|
|
const win = this._win();
|
|
|
|
if (!win.__tabsterOutline) {
|
|
win.__tabsterOutline = {};
|
|
}
|
|
|
|
if (!win.__tabsterOutline.style) {
|
|
win.__tabsterOutline.style = appendStyles(win.document, _props);
|
|
}
|
|
|
|
if (!props || !props.areaClass) {
|
|
win.document.body.classList.add(defaultProps.areaClass);
|
|
} else {
|
|
win.document.body.classList.remove(defaultProps.areaClass);
|
|
}
|
|
}
|
|
|
|
dispose() {
|
|
const win = this._win();
|
|
|
|
if (this._updateTimer) {
|
|
win.clearTimeout(this._updateTimer);
|
|
this._updateTimer = undefined;
|
|
}
|
|
|
|
this._tabster.keyboardNavigation.unsubscribe(this._onKeyboardNavigationStateChanged);
|
|
|
|
this._tabster.focusedElement.unsubscribe(this._onFocus);
|
|
|
|
win.removeEventListener("scroll", this._onScroll, true);
|
|
|
|
if (this._fullScreenEventName) {
|
|
win.document.removeEventListener(this._fullScreenEventName, this._onFullScreenChanged);
|
|
}
|
|
|
|
this._allOutlineElements.forEach(outlineElements => this._removeDOM(outlineElements.container));
|
|
|
|
this._allOutlineElements = [];
|
|
delete this._outlinedElement;
|
|
delete this._curPos;
|
|
delete this._curOutlineElements;
|
|
delete this._fullScreenElement;
|
|
}
|
|
|
|
_shouldShowCustomOutline(element) {
|
|
const tabsterOnElement = getTabsterOnElement(this._tabster, element);
|
|
|
|
if (tabsterOnElement && tabsterOnElement.outline && tabsterOnElement.outline.isIgnored) {
|
|
return false;
|
|
}
|
|
|
|
for (let i = element; i; i = i.parentElement) {
|
|
if (i.classList && i.classList.contains(_props.areaClass)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
_updateElement(e) {
|
|
this._outlinedElement = undefined;
|
|
|
|
if (this._updateTimer) {
|
|
this._win().clearTimeout(this._updateTimer);
|
|
|
|
this._updateTimer = undefined;
|
|
}
|
|
|
|
this._curPos = undefined;
|
|
|
|
if (!this._tabster.keyboardNavigation.isNavigatingWithKeyboard()) {
|
|
return false;
|
|
}
|
|
|
|
if (e) {
|
|
// TODO: It's hard (and not necessary) to come up with every possible
|
|
// condition when there should be no outline, it's better to add an
|
|
// API to customize the ignores.
|
|
if (e.tagName === "INPUT") {
|
|
const inputType = e.type;
|
|
const outlinedInputTypes = {
|
|
button: true,
|
|
checkbox: true,
|
|
file: true,
|
|
image: true,
|
|
radio: true,
|
|
range: true,
|
|
reset: true,
|
|
submit: true
|
|
};
|
|
|
|
if (!(inputType in outlinedInputTypes)) {
|
|
return false;
|
|
}
|
|
} else if (e.tagName === "TEXTAREA" || e.contentEditable === "true" || e.tagName === "IFRAME") {
|
|
return false;
|
|
}
|
|
|
|
if (!this._shouldShowCustomOutline(e)) {
|
|
return false;
|
|
}
|
|
|
|
if (this._tabster.keyboardNavigation.isNavigatingWithKeyboard()) {
|
|
this._outlinedElement = e;
|
|
|
|
this._updateOutline();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
_updateOutline() {
|
|
this._setOutlinePosition();
|
|
|
|
if (this._updateTimer) {
|
|
this._win().clearTimeout(this._updateTimer);
|
|
|
|
this._updateTimer = undefined;
|
|
}
|
|
|
|
if (!this._outlinedElement) {
|
|
return;
|
|
}
|
|
|
|
this._updateTimer = this._win().setTimeout(() => {
|
|
this._updateTimer = undefined;
|
|
|
|
this._updateOutline();
|
|
}, 30);
|
|
}
|
|
|
|
_setVisibility(visible) {
|
|
this._isVisible = visible;
|
|
|
|
if (this._curOutlineElements) {
|
|
if (visible) {
|
|
this._curOutlineElements.container.classList.add(`${_props.outlineClass}_visible`);
|
|
} else {
|
|
this._curOutlineElements.container.classList.remove(`${_props.outlineClass}_visible`);
|
|
|
|
this._curPos = undefined;
|
|
}
|
|
}
|
|
}
|
|
|
|
_setOutlinePosition() {
|
|
if (!this._outlinedElement) {
|
|
return;
|
|
}
|
|
|
|
let boundingRect = getBoundingRect(this._win, this._outlinedElement);
|
|
const position = new OutlinePosition(boundingRect.left, boundingRect.top, boundingRect.right, boundingRect.bottom);
|
|
|
|
if (this._curPos && position.equalsTo(this._curPos)) {
|
|
return;
|
|
}
|
|
|
|
const outlineElements = this._getDOM(this._outlinedElement);
|
|
|
|
const win = this._outlinedElement.ownerDocument && this._outlinedElement.ownerDocument.defaultView;
|
|
|
|
if (!outlineElements || !win) {
|
|
return;
|
|
}
|
|
|
|
if (this._curOutlineElements !== outlineElements) {
|
|
this._setVisibility(false);
|
|
|
|
this._curOutlineElements = outlineElements;
|
|
}
|
|
|
|
this._curPos = position;
|
|
const p = position.clone();
|
|
let hasAbsolutePositionedParent = false;
|
|
let hasFixedPositionedParent = false;
|
|
const container = outlineElements.container;
|
|
const scrollingElement = container && container.ownerDocument && container.ownerDocument.scrollingElement;
|
|
|
|
if (!scrollingElement) {
|
|
return;
|
|
}
|
|
|
|
for (let parent = this._outlinedElement.parentElement; parent && parent.nodeType === Node.ELEMENT_NODE; parent = parent.parentElement) {
|
|
// The element might be partially visible within its scrollable parent,
|
|
// reduce the bounding rect if this is the case.
|
|
if (parent === this._fullScreenElement) {
|
|
break;
|
|
}
|
|
|
|
boundingRect = getBoundingRect(this._win, parent);
|
|
const win = parent.ownerDocument && parent.ownerDocument.defaultView;
|
|
|
|
if (!win) {
|
|
return;
|
|
}
|
|
|
|
const computedStyle = win.getComputedStyle(parent);
|
|
const position = computedStyle.position;
|
|
|
|
if (position === "absolute") {
|
|
hasAbsolutePositionedParent = true;
|
|
} else if (position === "fixed" || position === "sticky") {
|
|
hasFixedPositionedParent = true;
|
|
}
|
|
|
|
if (computedStyle.overflow === "visible") {
|
|
continue;
|
|
}
|
|
|
|
if (!hasAbsolutePositionedParent && !hasFixedPositionedParent || computedStyle.overflow === "hidden") {
|
|
if (boundingRect.left > p.left) {
|
|
p.left = boundingRect.left;
|
|
}
|
|
|
|
if (boundingRect.top > p.top) {
|
|
p.top = boundingRect.top;
|
|
}
|
|
|
|
if (boundingRect.right < p.right) {
|
|
p.right = boundingRect.right;
|
|
}
|
|
|
|
if (boundingRect.bottom < p.bottom) {
|
|
p.bottom = boundingRect.bottom;
|
|
}
|
|
}
|
|
}
|
|
|
|
const allRect = getBoundingRect(this._win, scrollingElement);
|
|
const allWidth = allRect.left + allRect.right;
|
|
const allHeight = allRect.top + allRect.bottom;
|
|
const ow = _props.outlineWidth;
|
|
p.left = p.left > ow ? p.left - ow : 0;
|
|
p.top = p.top > ow ? p.top - ow : 0;
|
|
p.right = p.right < allWidth - ow ? p.right + ow : allWidth;
|
|
p.bottom = p.bottom < allHeight - ow ? p.bottom + ow : allHeight;
|
|
const width = p.right - p.left;
|
|
const height = p.bottom - p.top;
|
|
|
|
if (width > ow * 2 && height > ow * 2) {
|
|
const leftBorderNode = outlineElements.left;
|
|
const topBorderNode = outlineElements.top;
|
|
const rightBorderNode = outlineElements.right;
|
|
const bottomBorderNode = outlineElements.bottom;
|
|
const sx = this._fullScreenElement || hasFixedPositionedParent ? 0 : win.pageXOffset;
|
|
const sy = this._fullScreenElement || hasFixedPositionedParent ? 0 : win.pageYOffset;
|
|
container.style.position = hasFixedPositionedParent ? "fixed" : "absolute";
|
|
container.style.background = _props.outlineColor;
|
|
leftBorderNode.style.width = rightBorderNode.style.width = topBorderNode.style.height = bottomBorderNode.style.height = _props.outlineWidth + "px";
|
|
leftBorderNode.style.left = topBorderNode.style.left = bottomBorderNode.style.left = p.left + sx + "px";
|
|
rightBorderNode.style.left = p.left + sx + width - ow + "px";
|
|
leftBorderNode.style.top = rightBorderNode.style.top = topBorderNode.style.top = p.top + sy + "px";
|
|
bottomBorderNode.style.top = p.top + sy + height - ow + "px";
|
|
leftBorderNode.style.height = rightBorderNode.style.height = height + "px";
|
|
topBorderNode.style.width = bottomBorderNode.style.width = width + "px";
|
|
|
|
this._setVisibility(true);
|
|
} else {
|
|
this._setVisibility(false);
|
|
}
|
|
}
|
|
|
|
_getDOM(contextElement) {
|
|
const doc = contextElement.ownerDocument;
|
|
const win = doc && doc.defaultView;
|
|
|
|
if (!doc || !win || !win.__tabsterOutline) {
|
|
return undefined;
|
|
}
|
|
|
|
if (!win.__tabsterOutline.style) {
|
|
win.__tabsterOutline.style = appendStyles(doc, _props);
|
|
}
|
|
|
|
if (!win.__tabsterOutline.elements) {
|
|
const outlineElements = {
|
|
container: doc.createElement("div"),
|
|
left: doc.createElement("div"),
|
|
top: doc.createElement("div"),
|
|
right: doc.createElement("div"),
|
|
bottom: doc.createElement("div")
|
|
};
|
|
outlineElements.container.className = _props.outlineClass;
|
|
outlineElements.left.className = `${_props.outlineClass}__left`;
|
|
outlineElements.top.className = `${_props.outlineClass}__top`;
|
|
outlineElements.right.className = `${_props.outlineClass}__right`;
|
|
outlineElements.bottom.className = `${_props.outlineClass}__bottom`;
|
|
outlineElements.container.appendChild(outlineElements.left);
|
|
outlineElements.container.appendChild(outlineElements.top);
|
|
outlineElements.container.appendChild(outlineElements.right);
|
|
outlineElements.container.appendChild(outlineElements.bottom);
|
|
doc.body.appendChild(outlineElements.container);
|
|
win.__tabsterOutline.elements = outlineElements; // TODO: Make a garbage collector to remove the references
|
|
// to the outlines which are nowhere in the DOM anymore.
|
|
|
|
this._allOutlineElements.push(outlineElements);
|
|
}
|
|
|
|
return win.__tabsterOutline.elements;
|
|
}
|
|
|
|
_removeDOM(contextElement) {
|
|
const win = contextElement.ownerDocument && contextElement.ownerDocument.defaultView;
|
|
const outline = win && win.__tabsterOutline;
|
|
|
|
if (!outline) {
|
|
return;
|
|
}
|
|
|
|
if (outline.style && outline.style.parentNode) {
|
|
outline.style.parentNode.removeChild(outline.style);
|
|
delete outline.style;
|
|
}
|
|
|
|
const outlineElements = outline && outline.elements;
|
|
|
|
if (outlineElements) {
|
|
if (outlineElements.container.parentNode) {
|
|
outlineElements.container.parentNode.removeChild(outlineElements.container);
|
|
}
|
|
|
|
delete outline.elements;
|
|
}
|
|
}
|
|
|
|
static _isParentChild(parent, child) {
|
|
return child === parent || // tslint:disable-next-line:no-bitwise
|
|
!!(parent.compareDocumentPosition(child) & document.DOCUMENT_POSITION_CONTAINED_BY);
|
|
}
|
|
|
|
}
|
|
|
|
function appendStyles(document, props) {
|
|
const style = document.createElement("style");
|
|
style.type = "text/css";
|
|
style.appendChild(document.createTextNode(getOutlineStyles(props)));
|
|
document.head.appendChild(style);
|
|
return style;
|
|
}
|
|
|
|
function getOutlineStyles(props) {
|
|
return `
|
|
.${props.areaClass} *, .${props.areaClass} *:focus {
|
|
outline: none !important;
|
|
}
|
|
|
|
.${props.outlineClass} {
|
|
display: none;
|
|
position: absolute;
|
|
width: 0;
|
|
height: 0;
|
|
left: 0;
|
|
top: 0;
|
|
z-index: ${props.zIndex};
|
|
}
|
|
|
|
.${props.outlineClass}.${props.outlineClass}_visible {
|
|
display: block;
|
|
}
|
|
|
|
.${props.outlineClass}__left,
|
|
.${props.outlineClass}__top,
|
|
.${props.outlineClass}__right,
|
|
.${props.outlineClass}__bottom {
|
|
position: absolute;
|
|
background: inherit;
|
|
}`;
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
|
|
/**
|
|
* Allows default or user focus behaviour on the DOM subtree
|
|
* i.e. Tabster will not control focus events within an uncontrolled area
|
|
*/
|
|
class UncontrolledAPI {
|
|
constructor(isUncontrolledCompletely) {
|
|
this._isUncontrolledCompletely = isUncontrolledCompletely;
|
|
}
|
|
|
|
isUncontrolledCompletely(element, completely) {
|
|
var _a;
|
|
|
|
const isUncontrolledCompletely = (_a = this._isUncontrolledCompletely) === null || _a === void 0 ? void 0 : _a.call(this, element, completely); // If isUncontrolledCompletely callback is not defined or returns undefined, then the default
|
|
// behaviour is to return the uncontrolled.completely value from the element.
|
|
|
|
return isUncontrolledCompletely === undefined ? completely : isUncontrolledCompletely;
|
|
}
|
|
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
const HISOTRY_DEPTH = 10;
|
|
|
|
class Restorer extends TabsterPart {
|
|
constructor(tabster, element, props) {
|
|
var _a;
|
|
|
|
super(tabster, element, props);
|
|
this._hasFocus = false;
|
|
|
|
this._onFocusOut = e => {
|
|
var _a;
|
|
|
|
const element = (_a = this._element) === null || _a === void 0 ? void 0 : _a.get();
|
|
|
|
if (element && e.relatedTarget === null) {
|
|
element.dispatchEvent(new RestorerRestoreFocusEvent());
|
|
}
|
|
|
|
if (element && !dom.nodeContains(element, e.relatedTarget)) {
|
|
this._hasFocus = false;
|
|
}
|
|
};
|
|
|
|
this._onFocusIn = () => {
|
|
this._hasFocus = true;
|
|
};
|
|
|
|
if (this._props.type === RestorerTypes.Source) {
|
|
const element = (_a = this._element) === null || _a === void 0 ? void 0 : _a.get();
|
|
element === null || element === void 0 ? void 0 : element.addEventListener("focusout", this._onFocusOut);
|
|
element === null || element === void 0 ? void 0 : element.addEventListener("focusin", this._onFocusIn); // set hasFocus when the instance is created, in case focus has already moved within it
|
|
|
|
this._hasFocus = dom.nodeContains(element, element && dom.getActiveElement(element.ownerDocument));
|
|
}
|
|
}
|
|
|
|
dispose() {
|
|
var _a;
|
|
|
|
if (this._props.type === RestorerTypes.Source) {
|
|
const element = (_a = this._element) === null || _a === void 0 ? void 0 : _a.get();
|
|
element === null || element === void 0 ? void 0 : element.removeEventListener("focusout", this._onFocusOut);
|
|
element === null || element === void 0 ? void 0 : element.removeEventListener("focusin", this._onFocusIn);
|
|
|
|
if (this._hasFocus) {
|
|
const doc = this._tabster.getWindow().document;
|
|
|
|
doc.body.dispatchEvent(new RestorerRestoreFocusEvent());
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
class RestorerAPI {
|
|
constructor(tabster) {
|
|
this._history = [];
|
|
|
|
this._onRestoreFocus = e => {
|
|
this._focusedElementState.cancelAsyncFocus(AsyncFocusSources.Restorer); // ShadowDOM will have shadowRoot as e.target.
|
|
|
|
|
|
const target = e.composedPath()[0];
|
|
|
|
if (target) {
|
|
this._focusedElementState.requestAsyncFocus(AsyncFocusSources.Restorer, () => this._restoreFocus(target), 0);
|
|
}
|
|
};
|
|
|
|
this._onFocusIn = element => {
|
|
var _a;
|
|
|
|
if (!element) {
|
|
return;
|
|
}
|
|
|
|
const tabsterAttribute = getTabsterOnElement(this._tabster, element);
|
|
|
|
if (((_a = tabsterAttribute === null || tabsterAttribute === void 0 ? void 0 : tabsterAttribute.restorer) === null || _a === void 0 ? void 0 : _a.getProps().type) !== RestorerTypes.Target) {
|
|
return;
|
|
}
|
|
|
|
this._addToHistory(element);
|
|
};
|
|
|
|
this._restoreFocus = source => {
|
|
var _a; // don't restore focus if focus isn't lost to body
|
|
|
|
|
|
const doc = this._getWindow().document;
|
|
|
|
if (dom.getActiveElement(doc) !== doc.body) {
|
|
return;
|
|
}
|
|
|
|
if ( // clicking on any empty space focuses body - this is can be a false positive
|
|
!this._keyboardNavState.isNavigatingWithKeyboard() && // Source no longer exists on DOM - always restore focus
|
|
dom.nodeContains(doc.body, source)) {
|
|
return;
|
|
}
|
|
|
|
let weakElement = this._history.pop();
|
|
|
|
while (weakElement && !dom.nodeContains(doc.body, dom.getParentElement(weakElement.get()))) {
|
|
weakElement = this._history.pop();
|
|
}
|
|
|
|
(_a = weakElement === null || weakElement === void 0 ? void 0 : weakElement.get()) === null || _a === void 0 ? void 0 : _a.focus();
|
|
};
|
|
|
|
this._tabster = tabster;
|
|
this._getWindow = tabster.getWindow;
|
|
|
|
this._getWindow().addEventListener(RestorerRestoreFocusEventName, this._onRestoreFocus);
|
|
|
|
this._keyboardNavState = tabster.keyboardNavigation;
|
|
this._focusedElementState = tabster.focusedElement;
|
|
|
|
this._focusedElementState.subscribe(this._onFocusIn);
|
|
}
|
|
|
|
dispose() {
|
|
const win = this._getWindow();
|
|
|
|
this._focusedElementState.unsubscribe(this._onFocusIn);
|
|
|
|
this._focusedElementState.cancelAsyncFocus(AsyncFocusSources.Restorer);
|
|
|
|
win.removeEventListener(RestorerRestoreFocusEventName, this._onRestoreFocus);
|
|
}
|
|
|
|
_addToHistory(element) {
|
|
var _a; // Don't duplicate the top of history
|
|
|
|
|
|
if (((_a = this._history[this._history.length - 1]) === null || _a === void 0 ? void 0 : _a.get()) === element) {
|
|
return;
|
|
}
|
|
|
|
if (this._history.length > HISOTRY_DEPTH) {
|
|
this._history.shift();
|
|
}
|
|
|
|
this._history.push(new WeakHTMLElement(this._getWindow, element));
|
|
}
|
|
|
|
createRestorer(element, props) {
|
|
const restorer = new Restorer(this._tabster, element, props); // Focus might already be on a restorer target when it gets created so the focusin will not do anything
|
|
|
|
if (props.type === RestorerTypes.Target && dom.getActiveElement(element.ownerDocument) === element) {
|
|
this._addToHistory(element);
|
|
}
|
|
|
|
return restorer;
|
|
}
|
|
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
function getActiveElement(doc) {
|
|
var _a;
|
|
|
|
let activeElement = doc.activeElement;
|
|
|
|
while ((_a = activeElement === null || activeElement === void 0 ? void 0 : activeElement.shadowRoot) === null || _a === void 0 ? void 0 : _a.activeElement) {
|
|
activeElement = activeElement.shadowRoot.activeElement;
|
|
}
|
|
|
|
return activeElement;
|
|
}
|
|
function nodeContains(node, otherNode) {
|
|
var _a, _b;
|
|
|
|
if (!node || !otherNode) {
|
|
return false;
|
|
}
|
|
|
|
let currentNode = otherNode;
|
|
|
|
while (currentNode) {
|
|
if (currentNode === node) {
|
|
return true;
|
|
}
|
|
|
|
if (typeof currentNode.assignedElements !== "function" && ((_a = currentNode.assignedSlot) === null || _a === void 0 ? void 0 : _a.parentNode)) {
|
|
// Element is slotted
|
|
currentNode = (_b = currentNode.assignedSlot) === null || _b === void 0 ? void 0 : _b.parentNode;
|
|
} else if (currentNode.nodeType === document.DOCUMENT_FRAGMENT_NODE) {
|
|
// Element is in shadow root
|
|
currentNode = currentNode.host;
|
|
} else {
|
|
currentNode = currentNode.parentNode;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
function getParentNode(node) {
|
|
if (!node) {
|
|
return null;
|
|
}
|
|
|
|
if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE && node.host) {
|
|
return node.host;
|
|
}
|
|
|
|
return node.parentNode;
|
|
}
|
|
function getParentElement(element) {
|
|
for (let parentNode = getParentNode(element); parentNode; parentNode = getParentNode(parentNode)) {
|
|
if (parentNode.nodeType === Node.ELEMENT_NODE) {
|
|
return parentNode;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
function getFirstChild(node) {
|
|
if (!node) {
|
|
return null;
|
|
}
|
|
|
|
if (node.shadowRoot) {
|
|
const child = getFirstChild(node.shadowRoot);
|
|
|
|
if (child) {
|
|
return child;
|
|
} // If the attached shadowRoot has no children, just try ordinary children,
|
|
// that might come after.
|
|
|
|
}
|
|
|
|
return node.firstChild;
|
|
}
|
|
function getLastChild$1(node) {
|
|
if (!node) {
|
|
return null;
|
|
}
|
|
|
|
if (!node.lastChild && node.shadowRoot) {
|
|
return getLastChild$1(node.shadowRoot);
|
|
}
|
|
|
|
return node.lastChild;
|
|
}
|
|
function getNextSibling(node) {
|
|
return (node === null || node === void 0 ? void 0 : node.nextSibling) || null;
|
|
}
|
|
function getPreviousSibling(node) {
|
|
var _a;
|
|
|
|
if (!node) {
|
|
return null;
|
|
}
|
|
|
|
let sibling = node.previousSibling;
|
|
|
|
if (!sibling && ((_a = node.parentElement) === null || _a === void 0 ? void 0 : _a.shadowRoot)) {
|
|
sibling = getLastChild$1(node.parentElement.shadowRoot);
|
|
}
|
|
|
|
return sibling;
|
|
}
|
|
function getFirstElementChild(element) {
|
|
let child = getFirstChild(element);
|
|
|
|
while (child && child.nodeType !== Node.ELEMENT_NODE) {
|
|
child = getNextSibling(child);
|
|
}
|
|
|
|
return child;
|
|
}
|
|
function getLastElementChild(element) {
|
|
let child = getLastChild$1(element);
|
|
|
|
while (child && child.nodeType !== Node.ELEMENT_NODE) {
|
|
child = getPreviousSibling(child);
|
|
}
|
|
|
|
return child;
|
|
}
|
|
function getNextElementSibling(element) {
|
|
let sibling = getNextSibling(element);
|
|
|
|
while (sibling && sibling.nodeType !== Node.ELEMENT_NODE) {
|
|
sibling = getNextSibling(sibling);
|
|
}
|
|
|
|
return sibling;
|
|
}
|
|
function getPreviousElementSibling(element) {
|
|
let sibling = getPreviousSibling(element);
|
|
|
|
while (sibling && sibling.nodeType !== Node.ELEMENT_NODE) {
|
|
sibling = getPreviousSibling(sibling);
|
|
}
|
|
|
|
return sibling;
|
|
}
|
|
function appendChild(parent, child) {
|
|
const shadowRoot = parent.shadowRoot;
|
|
return shadowRoot ? shadowRoot.appendChild(child) : parent.appendChild(child);
|
|
}
|
|
function insertBefore(parent, child, referenceChild) {
|
|
const shadowRoot = parent.shadowRoot;
|
|
return shadowRoot ? shadowRoot.insertBefore(child, referenceChild) : parent.insertBefore(child, referenceChild);
|
|
}
|
|
function getSelection(ref) {
|
|
var _a;
|
|
|
|
const win = (_a = ref.ownerDocument) === null || _a === void 0 ? void 0 : _a.defaultView;
|
|
|
|
if (!win) {
|
|
return null;
|
|
}
|
|
|
|
for (let el = ref; el; el = el.parentNode) {
|
|
if (el.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
const tmp = el; // ShadowRoot.getSelection() exists only in Chrome.
|
|
|
|
if (tmp.getSelection) {
|
|
return tmp.getSelection() || null;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return win.getSelection() || null;
|
|
}
|
|
function getElementsByName(referenceElement, name) {
|
|
for (let el = referenceElement; el; el = el.parentNode) {
|
|
if (el.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
// Shadow root doesn't have getElementsByName()...
|
|
return el.querySelectorAll(`[name=${name}]`);
|
|
}
|
|
}
|
|
|
|
return referenceElement.ownerDocument.getElementsByName(name);
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
|
|
function getLastChild(container) {
|
|
let lastChild = null;
|
|
|
|
for (let i = getLastElementChild(container); i; i = getLastElementChild(i)) {
|
|
lastChild = i;
|
|
}
|
|
|
|
return lastChild || undefined;
|
|
}
|
|
|
|
class ShadowTreeWalker {
|
|
constructor(doc, root, whatToShow, filter) {
|
|
this._walkerStack = [];
|
|
this._currentSetFor = new Set();
|
|
|
|
this._acceptNode = node => {
|
|
var _a;
|
|
|
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
const shadowRoot = node.shadowRoot;
|
|
|
|
if (shadowRoot) {
|
|
const walker = this._doc.createTreeWalker(shadowRoot, this.whatToShow, {
|
|
acceptNode: this._acceptNode
|
|
});
|
|
|
|
this._walkerStack.unshift(walker);
|
|
|
|
return NodeFilter.FILTER_ACCEPT;
|
|
} else {
|
|
if (typeof this.filter === "function") {
|
|
return this.filter(node);
|
|
} else if ((_a = this.filter) === null || _a === void 0 ? void 0 : _a.acceptNode) {
|
|
return this.filter.acceptNode(node);
|
|
} else if (this.filter === null) {
|
|
return NodeFilter.FILTER_ACCEPT;
|
|
}
|
|
}
|
|
}
|
|
|
|
return NodeFilter.FILTER_SKIP;
|
|
};
|
|
|
|
this._doc = doc;
|
|
this.root = root;
|
|
this.filter = filter !== null && filter !== void 0 ? filter : null;
|
|
this.whatToShow = whatToShow !== null && whatToShow !== void 0 ? whatToShow : NodeFilter.SHOW_ALL;
|
|
this._currentNode = root;
|
|
|
|
this._walkerStack.unshift(doc.createTreeWalker(root, whatToShow, this._acceptNode));
|
|
|
|
const shadowRoot = root.shadowRoot;
|
|
|
|
if (shadowRoot) {
|
|
const walker = this._doc.createTreeWalker(shadowRoot, this.whatToShow, {
|
|
acceptNode: this._acceptNode
|
|
});
|
|
|
|
this._walkerStack.unshift(walker);
|
|
}
|
|
}
|
|
|
|
get currentNode() {
|
|
return this._currentNode;
|
|
}
|
|
|
|
set currentNode(node) {
|
|
if (!nodeContains(this.root, node)) {
|
|
throw new Error("Cannot set currentNode to a node that is not contained by the root node.");
|
|
}
|
|
|
|
const walkers = [];
|
|
let curNode = node;
|
|
let currentWalkerCurrentNode = node;
|
|
this._currentNode = node;
|
|
|
|
while (curNode && curNode !== this.root) {
|
|
if (curNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
const shadowRoot = curNode;
|
|
|
|
const walker = this._doc.createTreeWalker(shadowRoot, this.whatToShow, {
|
|
acceptNode: this._acceptNode
|
|
});
|
|
|
|
walkers.push(walker);
|
|
walker.currentNode = currentWalkerCurrentNode;
|
|
|
|
this._currentSetFor.add(walker);
|
|
|
|
curNode = currentWalkerCurrentNode = shadowRoot.host;
|
|
} else {
|
|
curNode = curNode.parentNode;
|
|
}
|
|
}
|
|
|
|
const walker = this._doc.createTreeWalker(this.root, this.whatToShow, {
|
|
acceptNode: this._acceptNode
|
|
});
|
|
|
|
walkers.push(walker);
|
|
walker.currentNode = currentWalkerCurrentNode;
|
|
|
|
this._currentSetFor.add(walker);
|
|
|
|
this._walkerStack = walkers;
|
|
}
|
|
|
|
firstChild() {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
throw new Error("Method not implemented.");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
lastChild() {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
throw new Error("Method not implemented.");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
nextNode() {
|
|
var _a;
|
|
|
|
const nextNode = this._walkerStack[0].nextNode();
|
|
|
|
if (nextNode) {
|
|
const shadowRoot = nextNode.shadowRoot;
|
|
|
|
if (shadowRoot) {
|
|
let nodeResult;
|
|
|
|
if (typeof this.filter === "function") {
|
|
nodeResult = this.filter(nextNode);
|
|
} else if ((_a = this.filter) === null || _a === void 0 ? void 0 : _a.acceptNode) {
|
|
nodeResult = this.filter.acceptNode(nextNode);
|
|
}
|
|
|
|
if (nodeResult === NodeFilter.FILTER_ACCEPT) {
|
|
return nextNode;
|
|
} // _acceptNode should have added new walker for this shadow,
|
|
// go in recursively.
|
|
|
|
|
|
return this.nextNode();
|
|
}
|
|
|
|
return nextNode;
|
|
} else {
|
|
if (this._walkerStack.length > 1) {
|
|
this._walkerStack.shift();
|
|
|
|
return this.nextNode();
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
previousNode() {
|
|
var _a, _b;
|
|
|
|
const currentWalker = this._walkerStack[0];
|
|
|
|
if (currentWalker.currentNode === currentWalker.root) {
|
|
if (this._currentSetFor.has(currentWalker)) {
|
|
this._currentSetFor.delete(currentWalker);
|
|
|
|
if (this._walkerStack.length > 1) {
|
|
this._walkerStack.shift();
|
|
|
|
return this.previousNode();
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const lastChild = getLastChild(currentWalker.root);
|
|
|
|
if (lastChild) {
|
|
currentWalker.currentNode = lastChild;
|
|
let nodeResult;
|
|
|
|
if (typeof this.filter === "function") {
|
|
nodeResult = this.filter(lastChild);
|
|
} else if ((_a = this.filter) === null || _a === void 0 ? void 0 : _a.acceptNode) {
|
|
nodeResult = this.filter.acceptNode(lastChild);
|
|
}
|
|
|
|
if (nodeResult === NodeFilter.FILTER_ACCEPT) {
|
|
return lastChild;
|
|
}
|
|
}
|
|
}
|
|
|
|
const previousNode = currentWalker.previousNode();
|
|
|
|
if (previousNode) {
|
|
const shadowRoot = previousNode.shadowRoot;
|
|
|
|
if (shadowRoot) {
|
|
let nodeResult;
|
|
|
|
if (typeof this.filter === "function") {
|
|
nodeResult = this.filter(previousNode);
|
|
} else if ((_b = this.filter) === null || _b === void 0 ? void 0 : _b.acceptNode) {
|
|
nodeResult = this.filter.acceptNode(previousNode);
|
|
}
|
|
|
|
if (nodeResult === NodeFilter.FILTER_ACCEPT) {
|
|
return previousNode;
|
|
} // _acceptNode should have added new walker for this shadow,
|
|
// go in recursively.
|
|
|
|
|
|
return this.previousNode();
|
|
}
|
|
|
|
return previousNode;
|
|
} else {
|
|
if (this._walkerStack.length > 1) {
|
|
this._walkerStack.shift();
|
|
|
|
return this.previousNode();
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
nextSibling() {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
throw new Error("Method not implemented.");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
previousSibling() {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
throw new Error("Method not implemented.");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
parentNode() {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
throw new Error("Method not implemented.");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
}
|
|
function createShadowTreeWalker(doc, root, whatToShow, filter) {
|
|
return new ShadowTreeWalker(doc, root, whatToShow, filter);
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
class ShadowMutationObserver {
|
|
static _overrideAttachShadow(win) {
|
|
const origAttachShadow = win.Element.prototype.attachShadow;
|
|
|
|
if (origAttachShadow.__origAttachShadow) {
|
|
return;
|
|
}
|
|
|
|
Element.prototype.attachShadow = function (options) {
|
|
const shadowRoot = origAttachShadow.call(this, options);
|
|
|
|
for (const shadowObserver of ShadowMutationObserver._shadowObservers) {
|
|
shadowObserver._addSubObserver(shadowRoot);
|
|
}
|
|
|
|
return shadowRoot;
|
|
};
|
|
|
|
Element.prototype.attachShadow.__origAttachShadow = origAttachShadow;
|
|
}
|
|
|
|
constructor(callback) {
|
|
this._isObserving = false;
|
|
|
|
this._callbackWrapper = (mutations, observer) => {
|
|
for (const mutation of mutations) {
|
|
if (mutation.type === "childList") {
|
|
const removed = mutation.removedNodes;
|
|
const added = mutation.addedNodes;
|
|
|
|
for (let i = 0; i < removed.length; i++) {
|
|
this._walkShadows(removed[i], true);
|
|
}
|
|
|
|
for (let i = 0; i < added.length; i++) {
|
|
this._walkShadows(added[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
this._callback(mutations, observer);
|
|
};
|
|
|
|
this._callback = callback;
|
|
this._observer = new MutationObserver(this._callbackWrapper);
|
|
this._subObservers = new Map();
|
|
}
|
|
|
|
_addSubObserver(shadowRoot) {
|
|
if (!this._options || !this._callback || this._subObservers.has(shadowRoot)) {
|
|
return;
|
|
}
|
|
|
|
if (this._options.subtree && nodeContains(this._root, shadowRoot)) {
|
|
const subObserver = new MutationObserver(this._callbackWrapper);
|
|
|
|
this._subObservers.set(shadowRoot, subObserver);
|
|
|
|
if (this._isObserving) {
|
|
subObserver.observe(shadowRoot, this._options);
|
|
}
|
|
|
|
this._walkShadows(shadowRoot);
|
|
}
|
|
}
|
|
|
|
disconnect() {
|
|
this._isObserving = false;
|
|
delete this._options;
|
|
|
|
ShadowMutationObserver._shadowObservers.delete(this);
|
|
|
|
for (const subObserver of this._subObservers.values()) {
|
|
subObserver.disconnect();
|
|
}
|
|
|
|
this._subObservers.clear();
|
|
|
|
this._observer.disconnect();
|
|
}
|
|
|
|
observe(target, options) {
|
|
const doc = target.nodeType === Node.DOCUMENT_NODE ? target : target.ownerDocument;
|
|
const win = doc === null || doc === void 0 ? void 0 : doc.defaultView;
|
|
|
|
if (!doc || !win) {
|
|
return;
|
|
}
|
|
|
|
ShadowMutationObserver._overrideAttachShadow(win);
|
|
|
|
ShadowMutationObserver._shadowObservers.add(this);
|
|
|
|
this._root = target;
|
|
this._options = options;
|
|
this._isObserving = true;
|
|
|
|
this._observer.observe(target, options);
|
|
|
|
this._walkShadows(target);
|
|
}
|
|
|
|
_walkShadows(target, remove) {
|
|
const doc = target.nodeType === Node.DOCUMENT_NODE ? target : target.ownerDocument;
|
|
|
|
if (!doc) {
|
|
return;
|
|
}
|
|
|
|
if (target === doc) {
|
|
target = doc.body;
|
|
} else {
|
|
const shadowRoot = target.shadowRoot;
|
|
|
|
if (shadowRoot) {
|
|
this._addSubObserver(shadowRoot);
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
const walker = doc.createTreeWalker(target, NodeFilter.SHOW_ELEMENT, {
|
|
acceptNode: node => {
|
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
if (remove) {
|
|
const subObserver = this._subObservers.get(node);
|
|
|
|
if (subObserver) {
|
|
subObserver.disconnect();
|
|
|
|
this._subObservers.delete(node);
|
|
}
|
|
} else {
|
|
const shadowRoot = node.shadowRoot;
|
|
|
|
if (shadowRoot) {
|
|
this._addSubObserver(shadowRoot);
|
|
}
|
|
}
|
|
}
|
|
|
|
return NodeFilter.FILTER_SKIP;
|
|
}
|
|
});
|
|
walker.nextNode();
|
|
}
|
|
|
|
takeRecords() {
|
|
const records = this._observer.takeRecords();
|
|
|
|
for (const subObserver of this._subObservers.values()) {
|
|
records.push(...subObserver.takeRecords());
|
|
}
|
|
|
|
return records;
|
|
}
|
|
|
|
}
|
|
ShadowMutationObserver._shadowObservers = /*#__PURE__*/new Set();
|
|
function createShadowMutationObserver(callback) {
|
|
return new ShadowMutationObserver(callback);
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
function shadowQuerySelector(node, selector, all) {
|
|
// TODO: This is probably slow. Optimize to use each shadowRoot's querySelector/querySelectorAll
|
|
// instead of walking the tree.
|
|
const elements = [];
|
|
walk(node, selector);
|
|
return elements;
|
|
|
|
function walk(from, selector) {
|
|
let el = null;
|
|
const walker = document.createTreeWalker(from, NodeFilter.SHOW_ELEMENT, {
|
|
acceptNode: n => {
|
|
if (n.nodeType === Node.ELEMENT_NODE) {
|
|
if (n.matches(selector)) {
|
|
el = n;
|
|
elements.push(el);
|
|
return all ? NodeFilter.FILTER_SKIP : NodeFilter.FILTER_ACCEPT;
|
|
}
|
|
|
|
const shadowRoot = n.shadowRoot;
|
|
|
|
if (shadowRoot) {
|
|
walk(shadowRoot, selector);
|
|
return !all && elements.length ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
|
|
}
|
|
}
|
|
|
|
return NodeFilter.FILTER_SKIP;
|
|
}
|
|
});
|
|
walker.nextNode();
|
|
}
|
|
}
|
|
|
|
function querySelectorAll(node, selector) {
|
|
return shadowQuerySelector(node, selector, true);
|
|
}
|
|
function querySelector(node, selector) {
|
|
return shadowQuerySelector(node, selector, false)[0] || null;
|
|
}
|
|
function getElementById(doc, id) {
|
|
return querySelector(doc, "#" + id);
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
|
|
var shadowDOMAPI = /*#__PURE__*/Object.freeze({
|
|
__proto__: null,
|
|
createTreeWalker: createShadowTreeWalker,
|
|
createMutationObserver: createShadowMutationObserver,
|
|
appendChild: appendChild,
|
|
getActiveElement: getActiveElement,
|
|
getFirstChild: getFirstChild,
|
|
getFirstElementChild: getFirstElementChild,
|
|
getLastChild: getLastChild$1,
|
|
getLastElementChild: getLastElementChild,
|
|
getNextElementSibling: getNextElementSibling,
|
|
getNextSibling: getNextSibling,
|
|
getParentElement: getParentElement,
|
|
getParentNode: getParentNode,
|
|
getPreviousElementSibling: getPreviousElementSibling,
|
|
getPreviousSibling: getPreviousSibling,
|
|
getSelection: getSelection,
|
|
getElementsByName: getElementsByName,
|
|
insertBefore: insertBefore,
|
|
nodeContains: nodeContains,
|
|
getElementById: getElementById,
|
|
querySelector: querySelector,
|
|
querySelectorAll: querySelectorAll
|
|
});
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
|
|
class Tabster {
|
|
constructor(tabster) {
|
|
this.keyboardNavigation = tabster.keyboardNavigation;
|
|
this.focusedElement = tabster.focusedElement;
|
|
this.focusable = tabster.focusable;
|
|
this.root = tabster.root;
|
|
this.uncontrolled = tabster.uncontrolled;
|
|
this.core = tabster;
|
|
}
|
|
|
|
}
|
|
/**
|
|
* Extends Window to include an internal Tabster instance.
|
|
*/
|
|
|
|
|
|
class TabsterCore {
|
|
constructor(win, props) {
|
|
var _a, _b;
|
|
|
|
this._forgetMemorizedElements = [];
|
|
this._wrappers = new Set();
|
|
this._initQueue = [];
|
|
this._version = "8.0.1";
|
|
this._noop = false;
|
|
|
|
this.getWindow = () => {
|
|
if (!this._win) {
|
|
throw new Error("Using disposed Tabster.");
|
|
}
|
|
|
|
return this._win;
|
|
};
|
|
|
|
this._storage = createWeakMap(win);
|
|
this._win = win;
|
|
const getWindow = this.getWindow;
|
|
|
|
if (props === null || props === void 0 ? void 0 : props.DOMAPI) {
|
|
setDOMAPI({ ...props.DOMAPI
|
|
});
|
|
}
|
|
|
|
this.keyboardNavigation = new KeyboardNavigationState(getWindow);
|
|
this.focusedElement = new FocusedElementState(this, getWindow);
|
|
this.focusable = new FocusableAPI(this);
|
|
this.root = new RootAPI(this, props === null || props === void 0 ? void 0 : props.autoRoot);
|
|
this.uncontrolled = new UncontrolledAPI( // TODO: Remove checkUncontrolledTrappingFocus in the next major version.
|
|
(props === null || props === void 0 ? void 0 : props.checkUncontrolledCompletely) || (props === null || props === void 0 ? void 0 : props.checkUncontrolledTrappingFocus));
|
|
this.controlTab = (_a = props === null || props === void 0 ? void 0 : props.controlTab) !== null && _a !== void 0 ? _a : true;
|
|
this.rootDummyInputs = !!(props === null || props === void 0 ? void 0 : props.rootDummyInputs);
|
|
this._dummyObserver = new DummyInputObserver(getWindow);
|
|
this.getParent = (_b = props === null || props === void 0 ? void 0 : props.getParent) !== null && _b !== void 0 ? _b : dom.getParentNode;
|
|
this.internal = {
|
|
stopObserver: () => {
|
|
if (this._unobserve) {
|
|
this._unobserve();
|
|
|
|
delete this._unobserve;
|
|
}
|
|
},
|
|
resumeObserver: syncState => {
|
|
if (!this._unobserve) {
|
|
const doc = getWindow().document;
|
|
this._unobserve = observeMutations(doc, this, updateTabsterByAttribute, syncState);
|
|
}
|
|
}
|
|
};
|
|
startFakeWeakRefsCleanup(getWindow); // Gives a tick to the host app to initialize other tabster
|
|
// APIs before tabster starts observing attributes.
|
|
|
|
this.queueInit(() => {
|
|
this.internal.resumeObserver(true);
|
|
});
|
|
}
|
|
/**
|
|
* Merges external props with the current props. Not all
|
|
* props can/should be mergeable, so let's add more as we move on.
|
|
* @param props Tabster props
|
|
*/
|
|
|
|
|
|
_mergeProps(props) {
|
|
var _a;
|
|
|
|
if (!props) {
|
|
return;
|
|
}
|
|
|
|
this.getParent = (_a = props.getParent) !== null && _a !== void 0 ? _a : this.getParent;
|
|
}
|
|
|
|
createTabster(noRefCount, props) {
|
|
const wrapper = new Tabster(this);
|
|
|
|
if (!noRefCount) {
|
|
this._wrappers.add(wrapper);
|
|
}
|
|
|
|
this._mergeProps(props);
|
|
|
|
return wrapper;
|
|
}
|
|
|
|
disposeTabster(wrapper, allInstances) {
|
|
if (allInstances) {
|
|
this._wrappers.clear();
|
|
} else {
|
|
this._wrappers.delete(wrapper);
|
|
}
|
|
|
|
if (this._wrappers.size === 0) {
|
|
this.dispose();
|
|
}
|
|
}
|
|
|
|
dispose() {
|
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
|
|
this.internal.stopObserver();
|
|
const win = this._win;
|
|
win === null || win === void 0 ? void 0 : win.clearTimeout(this._initTimer);
|
|
delete this._initTimer;
|
|
this._initQueue = [];
|
|
this._forgetMemorizedElements = [];
|
|
|
|
if (win && this._forgetMemorizedTimer) {
|
|
win.clearTimeout(this._forgetMemorizedTimer);
|
|
delete this._forgetMemorizedTimer;
|
|
}
|
|
|
|
(_a = this.outline) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
(_b = this.crossOrigin) === null || _b === void 0 ? void 0 : _b.dispose();
|
|
(_c = this.deloser) === null || _c === void 0 ? void 0 : _c.dispose();
|
|
(_d = this.groupper) === null || _d === void 0 ? void 0 : _d.dispose();
|
|
(_e = this.mover) === null || _e === void 0 ? void 0 : _e.dispose();
|
|
(_f = this.modalizer) === null || _f === void 0 ? void 0 : _f.dispose();
|
|
(_g = this.observedElement) === null || _g === void 0 ? void 0 : _g.dispose();
|
|
(_h = this.restorer) === null || _h === void 0 ? void 0 : _h.dispose();
|
|
this.keyboardNavigation.dispose();
|
|
this.focusable.dispose();
|
|
this.focusedElement.dispose();
|
|
this.root.dispose();
|
|
|
|
this._dummyObserver.dispose();
|
|
|
|
stopFakeWeakRefsCleanupAndClearStorage(this.getWindow);
|
|
clearElementCache(this.getWindow);
|
|
this._storage = new WeakMap();
|
|
|
|
this._wrappers.clear();
|
|
|
|
if (win) {
|
|
disposeInstanceContext(win);
|
|
delete win.__tabsterInstance;
|
|
delete this._win;
|
|
}
|
|
}
|
|
|
|
storageEntry(element, addremove) {
|
|
const storage = this._storage;
|
|
let entry = storage.get(element);
|
|
|
|
if (entry) {
|
|
if (addremove === false && Object.keys(entry).length === 0) {
|
|
storage.delete(element);
|
|
}
|
|
} else if (addremove === true) {
|
|
entry = {};
|
|
storage.set(element, entry);
|
|
}
|
|
|
|
return entry;
|
|
}
|
|
|
|
forceCleanup() {
|
|
if (!this._win) {
|
|
return;
|
|
}
|
|
|
|
this._forgetMemorizedElements.push(this._win.document.body);
|
|
|
|
if (this._forgetMemorizedTimer) {
|
|
return;
|
|
}
|
|
|
|
this._forgetMemorizedTimer = this._win.setTimeout(() => {
|
|
delete this._forgetMemorizedTimer;
|
|
|
|
for (let el = this._forgetMemorizedElements.shift(); el; el = this._forgetMemorizedElements.shift()) {
|
|
clearElementCache(this.getWindow, el);
|
|
FocusedElementState.forgetMemorized(this.focusedElement, el);
|
|
}
|
|
}, 0);
|
|
cleanupFakeWeakRefs(this.getWindow, true);
|
|
}
|
|
|
|
queueInit(callback) {
|
|
var _a;
|
|
|
|
if (!this._win) {
|
|
return;
|
|
}
|
|
|
|
this._initQueue.push(callback);
|
|
|
|
if (!this._initTimer) {
|
|
this._initTimer = (_a = this._win) === null || _a === void 0 ? void 0 : _a.setTimeout(() => {
|
|
delete this._initTimer;
|
|
this.drainInitQueue();
|
|
}, 0);
|
|
}
|
|
}
|
|
|
|
drainInitQueue() {
|
|
if (!this._win) {
|
|
return;
|
|
}
|
|
|
|
const queue = this._initQueue; // Resetting the queue before calling the callbacks to avoid recursion.
|
|
|
|
this._initQueue = [];
|
|
queue.forEach(callback => callback());
|
|
}
|
|
|
|
}
|
|
|
|
function forceCleanup(tabster) {
|
|
// The only legit case for calling this method is when you've completely removed
|
|
// the application DOM and not going to add the new one for a while.
|
|
const tabsterCore = tabster.core;
|
|
tabsterCore.forceCleanup();
|
|
}
|
|
/**
|
|
* Creates an instance of Tabster, returns the current window instance if it already exists.
|
|
*/
|
|
|
|
function createTabster(win, props) {
|
|
let tabster = getCurrentTabster(win);
|
|
|
|
if (tabster) {
|
|
return tabster.createTabster(false, props);
|
|
}
|
|
|
|
tabster = new TabsterCore(win, props);
|
|
win.__tabsterInstance = tabster;
|
|
return tabster.createTabster();
|
|
}
|
|
/**
|
|
* Returns an instance of Tabster if it was created before or null.
|
|
*/
|
|
|
|
function getTabster(win) {
|
|
const tabster = getCurrentTabster(win);
|
|
return tabster ? tabster.createTabster(true) : null;
|
|
}
|
|
function getShadowDOMAPI() {
|
|
return shadowDOMAPI;
|
|
}
|
|
/**
|
|
* Creates a new groupper instance or returns an existing one
|
|
* @param tabster Tabster instance
|
|
*/
|
|
|
|
function getGroupper(tabster) {
|
|
const tabsterCore = tabster.core;
|
|
|
|
if (!tabsterCore.groupper) {
|
|
tabsterCore.groupper = new GroupperAPI(tabsterCore, tabsterCore.getWindow);
|
|
}
|
|
|
|
return tabsterCore.groupper;
|
|
}
|
|
/**
|
|
* Creates a new mover instance or returns an existing one
|
|
* @param tabster Tabster instance
|
|
*/
|
|
|
|
function getMover(tabster) {
|
|
const tabsterCore = tabster.core;
|
|
|
|
if (!tabsterCore.mover) {
|
|
tabsterCore.mover = new MoverAPI(tabsterCore, tabsterCore.getWindow);
|
|
}
|
|
|
|
return tabsterCore.mover;
|
|
}
|
|
function getOutline(tabster) {
|
|
const tabsterCore = tabster.core;
|
|
|
|
if (!tabsterCore.outline) {
|
|
tabsterCore.outline = new OutlineAPI(tabsterCore);
|
|
}
|
|
|
|
return tabsterCore.outline;
|
|
}
|
|
/**
|
|
* Creates a new new deloser instance or returns an existing one
|
|
* @param tabster Tabster instance
|
|
* @param props Deloser props
|
|
*/
|
|
|
|
function getDeloser(tabster, props) {
|
|
const tabsterCore = tabster.core;
|
|
|
|
if (!tabsterCore.deloser) {
|
|
tabsterCore.deloser = new DeloserAPI(tabsterCore, props);
|
|
}
|
|
|
|
return tabsterCore.deloser;
|
|
}
|
|
/**
|
|
* Creates a new modalizer instance or returns an existing one
|
|
* @param tabster Tabster instance
|
|
* @param alwaysAccessibleSelector When Modalizer is active, we put
|
|
* aria-hidden to everything else to hide it from screen readers. This CSS
|
|
* selector allows to exclude some elements from this behaviour. For example,
|
|
* this could be used to exclude aria-live region with the application-wide
|
|
* status announcements.
|
|
* @param accessibleCheck An optional callback that will be called when
|
|
* active Modalizer wants to hide an element that doesn't belong to it from
|
|
* the screen readers by setting aria-hidden. Similar to alwaysAccessibleSelector
|
|
* but allows to address the elements programmatically rather than with a selector.
|
|
* If the callback returns true, the element will not receive aria-hidden.
|
|
*/
|
|
|
|
function getModalizer(tabster, // @deprecated use accessibleCheck.
|
|
alwaysAccessibleSelector, accessibleCheck) {
|
|
const tabsterCore = tabster.core;
|
|
|
|
if (!tabsterCore.modalizer) {
|
|
tabsterCore.modalizer = new ModalizerAPI(tabsterCore, alwaysAccessibleSelector, accessibleCheck);
|
|
}
|
|
|
|
return tabsterCore.modalizer;
|
|
}
|
|
function getObservedElement(tabster) {
|
|
const tabsterCore = tabster.core;
|
|
|
|
if (!tabsterCore.observedElement) {
|
|
tabsterCore.observedElement = new ObservedElementAPI(tabsterCore);
|
|
}
|
|
|
|
return tabsterCore.observedElement;
|
|
}
|
|
function getCrossOrigin(tabster) {
|
|
const tabsterCore = tabster.core;
|
|
|
|
if (!tabsterCore.crossOrigin) {
|
|
getDeloser(tabster);
|
|
getModalizer(tabster);
|
|
getMover(tabster);
|
|
getGroupper(tabster);
|
|
getOutline(tabster);
|
|
getObservedElement(tabster);
|
|
tabsterCore.crossOrigin = new CrossOriginAPI(tabsterCore);
|
|
}
|
|
|
|
return tabsterCore.crossOrigin;
|
|
}
|
|
function getInternal(tabster) {
|
|
const tabsterCore = tabster.core;
|
|
return tabsterCore.internal;
|
|
}
|
|
function getRestorer(tabster) {
|
|
const tabsterCore = tabster.core;
|
|
|
|
if (!tabsterCore.restorer) {
|
|
tabsterCore.restorer = new RestorerAPI(tabsterCore);
|
|
}
|
|
|
|
return tabsterCore.restorer;
|
|
}
|
|
function disposeTabster(tabster, allInstances) {
|
|
tabster.core.disposeTabster(tabster, allInstances);
|
|
}
|
|
/**
|
|
* Returns an instance of Tabster if it already exists on the window .
|
|
* @param win window instance that could contain an Tabster instance.
|
|
*/
|
|
|
|
function getCurrentTabster(win) {
|
|
return win.__tabsterInstance;
|
|
}
|
|
/**
|
|
* Allows to make Tabster non operational. Intended for performance debugging (and other
|
|
* kinds of debugging), you can switch Tabster off without changing the application code
|
|
* that consumes it.
|
|
* @param tabster a reference created by createTabster().
|
|
* @param noop true if you want to make Tabster noop, false if you want to turn it back.
|
|
*/
|
|
|
|
function makeNoOp(tabster, noop) {
|
|
const core = tabster.core;
|
|
|
|
if (core._noop !== noop) {
|
|
core._noop = noop;
|
|
|
|
const processNode = element => {
|
|
if (!element.getAttribute) {
|
|
return NodeFilter.FILTER_SKIP;
|
|
}
|
|
|
|
if (getTabsterOnElement(core, element) || element.hasAttribute(TABSTER_ATTRIBUTE_NAME)) {
|
|
updateTabsterByAttribute(core, element);
|
|
}
|
|
|
|
return NodeFilter.FILTER_SKIP;
|
|
};
|
|
|
|
const doc = core.getWindow().document;
|
|
const body = doc.body;
|
|
processNode(body);
|
|
const walker = createElementTreeWalker(doc, body, processNode);
|
|
|
|
if (walker) {
|
|
while (walker.nextNode()) {
|
|
/* Iterating for the sake of calling processNode() callback. */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function isNoOp(tabster) {
|
|
return tabster._noop;
|
|
}
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
|
|
var Types = /*#__PURE__*/Object.freeze({
|
|
__proto__: null
|
|
});
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
|
|
var EventsTypes = /*#__PURE__*/Object.freeze({
|
|
__proto__: null
|
|
});
|
|
|
|
/*!
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
/** @deprecated This function is obsolete, use native element.dispatchEvent(new GroupperMoveFocusEvent(...)). */
|
|
|
|
function dispatchGroupperMoveFocusEvent(target, action) {
|
|
return target.dispatchEvent(new GroupperMoveFocusEvent({
|
|
action
|
|
}));
|
|
}
|
|
/** @deprecated This function is obsolete, use native element.dispatchEvent(new MoverMoveFocusEvent(...)). */
|
|
|
|
function dispatchMoverMoveFocusEvent(target, key) {
|
|
return target.dispatchEvent(new MoverMoveFocusEvent({
|
|
key
|
|
}));
|
|
}
|
|
/** @deprecated This function is obsolete, use native element.dispatchEvent(new MoverMemorizedElementEvent(...)). */
|
|
|
|
function dispatchMoverMemorizedElementEvent(target, memorizedElement) {
|
|
return target.dispatchEvent(new MoverMemorizedElementEvent({
|
|
memorizedElement
|
|
}));
|
|
}
|
|
|
|
export { AsyncFocusSources, DeloserFocusLostEvent, DeloserFocusLostEventName, DeloserRestoreFocusEvent, DeloserRestoreFocusEventName, DeloserStrategies, EventsTypes, FOCUSABLE_SELECTOR, GroupperMoveFocusActions, GroupperMoveFocusEvent, GroupperMoveFocusEventName, GroupperTabbabilities, ModalizerActiveEvent, ModalizerActiveEventName, ModalizerFocusInEventName, ModalizerFocusOutEventName, ModalizerInactiveEvent, ModalizerInactiveEventName, MoverDirections, MoverKeys, MoverMemorizedElementEvent, MoverMemorizedElementEventName, MoverMoveFocusEvent, MoverMoveFocusEventName, MoverStateEvent, MoverStateEventName, ObservedElementAccessibilities, RestoreFocusOrders, RestorerRestoreFocusEvent, RestorerRestoreFocusEventName, RestorerTypes, RootBlurEvent, RootBlurEventName, RootFocusEvent, RootFocusEventName, SysDummyInputsPositions, TABSTER_ATTRIBUTE_NAME, TABSTER_DUMMY_INPUT_ATTRIBUTE_NAME, TabsterCustomEvent, TabsterFocusInEvent, TabsterFocusInEventName, TabsterFocusOutEvent, TabsterFocusOutEventName, TabsterMoveFocusEvent, TabsterMoveFocusEventName, Types, Visibilities, createTabster, dispatchGroupperMoveFocusEvent, dispatchMoverMemorizedElementEvent, dispatchMoverMoveFocusEvent, disposeTabster, forceCleanup, getCrossOrigin, getDeloser, getDummyInputContainer, getGroupper, getInternal, getModalizer, getMover, getObservedElement, getOutline, getRestorer, getShadowDOMAPI, getTabster, getTabsterAttribute, isNoOp, makeNoOp, mergeTabsterProps, setTabsterAttribute };
|
|
//# sourceMappingURL=tabster.esm.js.map
|