@servicenow/ui-renderer-snabbdom
Version:
Snabbdom renderer for UI Framework on Next Experience
313 lines (243 loc) • 13.1 kB
JavaScript
/**
* Copyright (c) 2020 ServiceNow, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _accessibilityTargets = _interopRequireDefault(require("./accessibilityTargets"));
var _ariaRefAssignments = _interopRequireDefault(require("./ariaRefAssignments"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); }
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
function accessibilityDecorator(patchFn) {
return function patchWrapped(prev, next) {
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
_accessibilityTargets["default"].push([]);
_ariaRefAssignments["default"].push([]);
var vnode = patchFn(prev, next);
var accessibilityTargetItems = _accessibilityTargets["default"].pop();
var assignments = _ariaRefAssignments["default"].pop();
var ariaRefToAssignment = new Map();
for (var i = 0; i < assignments.length; i++) {
var ariaRefAssignment = assignments[i];
ariaRefToAssignment.set(ariaRefAssignment.ariaRef, ariaRefAssignment); // Since vnodes are handled in DFS order, we know that a parent of an ariaRef will also be before
// it in the assignments array. We can use this knowledge to efficiently figure out
// each ariaRef's parent ariaRef
if (i > 0) {
var possibleParentAriaRef = assignments[i - 1].ariaRef;
while (possibleParentAriaRef) {
if (possibleParentAriaRef.contains(ariaRefAssignment.element)) {
possibleParentAriaRef.setChildQuerySelector(ariaRefAssignment.ariaRef, buildQuerySelector(ariaRefToAssignment.get(possibleParentAriaRef).element, ariaRefAssignment.element));
ariaRefAssignment.ariaRef.setParentAriaRef(possibleParentAriaRef);
break;
}
possibleParentAriaRef = possibleParentAriaRef.getParentAriaRef();
}
}
ariaRefAssignment.ariaRef.setCurrent(ariaRefAssignment.element);
}
var shadowRootToAccessibilityTargetsItems = new Map();
var _iterator = _createForOfIteratorHelper(accessibilityTargetItems),
_step;
try {
var _loop = function _loop() {
var item = _step.value;
if (options.disconnect) {
if (item.currentClone && item.currentClone.parentNode) {
item.currentClone.parentNode.removeChild(item.currentClone);
item.currentClone = null;
}
if (item.callback) {
item.targetRef.unsubscribe({
element: item.element,
attribute: item.ariaAttribute
});
item.callback = null;
}
return "continue";
}
var callback = function callback(targetRef) {
if (!targetRef || !targetRef.isSet()) {
if (item.currentClone && item.currentClone.parentNode) {
item.currentClone.parentNode.removeChild(item.currentClone);
item.currentClone = null;
}
item.element.removeAttribute(item.ariaAttribute);
return;
}
if (item.element.getRootNode() === targetRef.getRootNode()) {
var id = targetRef.getAttribute('id');
item.element.setAttribute(item.ariaAttribute, id || item.id);
if (!id) targetRef.setId(item.id);
return;
} // Check if there are other ariaRefs within accessibilityTargetItems that are in the same shadow root
// If so, queue them, so they can be handled all at once after all their callbacks have been called
var shadowRoot = targetRef.getRootNode();
var targetItemsWithSharedShadow = accessibilityTargetItems.filter(function (t) {
return t.targetRef.isSet() && t.targetRef.getRootNode() === shadowRoot;
});
var targetItemsSharedShadowQueue = shadowRootToAccessibilityTargetsItems.get(shadowRoot);
if (!targetItemsSharedShadowQueue) {
targetItemsSharedShadowQueue = new Set();
shadowRootToAccessibilityTargetsItems.set(shadowRoot, targetItemsSharedShadowQueue);
}
targetItemsSharedShadowQueue.add(item);
if (targetItemsSharedShadowQueue.size === targetItemsWithSharedShadow.length) {
handleAccessibilityTargets(targetItemsSharedShadowQueue);
shadowRootToAccessibilityTargetsItems["delete"](shadowRoot);
}
}; // Subscribe for any changes to the aria ref so we can update the cloned node
item.callback = callback;
item.targetRef.subscribe({
element: item.element,
attribute: item.ariaAttribute
}, callback);
};
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var _ret = _loop();
if (_ret === "continue") continue;
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
return vnode;
};
}
function handleAccessibilityTargets(targetItemsShadowQueue) {
// Separate ariaRefs that do and do not have a parent.
// Those without a parent are the refs that should be cloned
var targetRefToTreeNode = new Map();
var targetItemRoots = [];
var targetItems = Array.from(targetItemsShadowQueue);
var _loop2 = function _loop2(i) {
var targetItem = targetItems[i];
var treeNode = targetRefToTreeNode.get(targetItem.targetRef);
if (!treeNode) {
treeNode = {
targetItems: [],
children: [],
targetRef: targetItem.targetRef,
parentTargetRef: null
};
targetRefToTreeNode.set(targetItem.targetRef, treeNode);
}
treeNode.targetItems.push(targetItem);
var parentAriaRef = targetItem.targetRef.getParentAriaRef();
while (parentAriaRef) {
if (targetItems.find(function (item) {
return item.targetRef === parentAriaRef;
})) {
var parentNode = targetRefToTreeNode.get(parentAriaRef);
if (!parentNode) {
parentNode = {
targetItems: [],
children: [],
targetRef: parentAriaRef,
parentTargetRef: null
};
targetRefToTreeNode.set(parentAriaRef, parentNode);
}
treeNode.parentTargetRef = parentAriaRef;
parentNode.children.push(targetItem);
break;
}
parentAriaRef = parentAriaRef.getParentAriaRef();
}
if (!parentAriaRef) targetItemRoots.push(targetItem);
};
for (var i = 0; i < targetItems.length; i++) {
_loop2(i);
} // Loop over targetItemRoots:
// For each one, if it hasn't been cloned yet, clone into DOM and then for each child:
// If it hasn't been handled yet, create the query selector from the child targetRef to the
// parent targetRef, then use that selector on the cloned item, then edit the returned element
// to have the correct id
var cloned = new Map();
var targetRefToQuerySelector = new Map();
for (var _i = 0; _i < targetItemRoots.length; _i++) {
var rootTargetItem = targetItemRoots[_i];
var firstItemWithTargetRef = cloned.get(rootTargetItem.targetRef);
rootTargetItem.element.setAttribute(rootTargetItem.ariaAttribute, firstItemWithTargetRef ? firstItemWithTargetRef.id : rootTargetItem.id);
if (!firstItemWithTargetRef) {
cloned.set(rootTargetItem.targetRef, rootTargetItem);
var clone = rootTargetItem.targetRef.cloneNode(true);
clone.id = rootTargetItem.id;
var container = document.createElement('div');
container.style = 'position: absolute; overflow: hidden; width: 1px; height: 1px; margin: 0; border: none; padding: 0; white-space: nowrap; clip: rect(0 0 0 0); clip-path: inset(50%);';
container.appendChild(clone);
if (rootTargetItem.currentClone && rootTargetItem.currentClone.parentNode) rootTargetItem.currentClone.parentNode.removeChild(rootTargetItem.currentClone);
rootTargetItem.element.getRootNode().appendChild(container);
rootTargetItem.currentClone = container;
}
var children = _toConsumableArray(targetRefToTreeNode.get(rootTargetItem.targetRef).children);
targetRefToQuerySelector.set(rootTargetItem.targetRef, '');
while (children.length) {
var childItem = children.shift();
var _firstItemWithTargetRef = cloned.get(childItem.targetRef);
var itemId = _firstItemWithTargetRef ? _firstItemWithTargetRef.id : childItem.id;
childItem.element.setAttribute(childItem.ariaAttribute, itemId);
if (!_firstItemWithTargetRef) {
cloned.set(childItem.targetRef, childItem);
var treeNode = targetRefToTreeNode.get(childItem.targetRef);
var selections = [];
var currentParent = void 0;
var currentChild = childItem.targetRef;
do {
currentParent = currentChild.getParentAriaRef();
var partialSelector = currentParent.getChildQuerySelector(currentChild);
selections.push(partialSelector);
currentChild = currentParent;
} while (currentParent !== rootTargetItem.targetRef);
var selector = selections.reverse().join(' > ');
var node = rootTargetItem.currentClone.querySelector(selector);
node.id = itemId;
children = children.concat(treeNode.children);
}
}
}
}
function buildQuerySelector(parent, child) {
var path = [];
var el = child;
while (el !== parent) {
var position = 0;
var current = el;
while (current) {
if (current.tagName === el.tagName) position += 1;
current = current.previousElementSibling;
}
path.push("".concat(el.tagName.toLowerCase(), ":nth-of-type(").concat(position, ")"));
el = el.parentNode;
}
return path.reverse().join(' > ');
}
var _default = accessibilityDecorator;
exports["default"] = _default;
//# sourceMappingURL=accessibilityDecorator.js.map