UNPKG

@servicenow/ui-renderer-snabbdom

Version:

Snabbdom renderer for UI Framework on Next Experience

313 lines (243 loc) 13.1 kB
/** * 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. */ "use strict"; 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