UNPKG

openkeynav

Version:

OpenKeyNav: A JavaScript plugin for enhancing keyboard navigation and accessibility on web pages.

1,261 lines (1,162 loc) 141 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.OpenKeyNav = factory()); })(this, (function () { 'use strict'; function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } var OpenKeyNav$1 = {}; var version = {}; Object.defineProperty(version, "__esModule", { value: true }); version.version = void 0; version.version = "0.1.229"; var signals = {}; Object.defineProperty(signals, "__esModule", { value: true }); signals.derived = derived; signals.effect = effect; signals.signal = signal; var subscriber = null; function signal(value) { var subscriptions = new Set(); return { get value() { if (subscriber) { subscriptions.add(subscriber); } return value; }, set value(updated) { value = updated; subscriptions.forEach(function (fn) { return fn(); }); } }; } function effect(fn) { subscriber = fn; fn(); subscriber = null; } function derived(fn) { var derived = signal(); effect(function () { derived.value = fn(); }); return derived; } var toolbar = {}; var keyButton = {}; Object.defineProperty(keyButton, "__esModule", { value: true }); keyButton.keyButton = void 0; keyButton.keyButton = function keyButton(keyCodes, text, reverseOrder) { // let styledKeyCode = `<span class="keyButton">${keyCode}</span>`; var styledKeyCodes = keyCodes.map(function (keyCode) { return "<span class=\"keyButton\">".concat(keyCode, "</span>"); }).join(""); if (!text) { return "".concat(styledKeyCodes); } if (reverseOrder) { return "\n <span class=\"keyButtonContainer\"> \n <span>\n ".concat(styledKeyCodes, "\n </span>\n <span class=\"keyButtonLabel\">").concat(text, "</span> \n </span>\n "); } return "\n <span class=\"keyButtonContainer\"> \n <span class=\"keyButtonLabel\">".concat(text, "</span> \n <span>\n ").concat(styledKeyCodes, "\n </span>\n </span>\n "); }; var keypress = {}; var clicking = {}; var hasRequiredClicking; function requireClicking() { if (hasRequiredClicking) return clicking; hasRequiredClicking = 1; Object.defineProperty(clicking, "__esModule", { value: true }); clicking.placeCursorAndScrollToCursor = clicking.handleTargetClickInteraction = void 0; _interopRequireDefault(requireOpenKeyNav()); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } clicking.handleTargetClickInteraction = function handleTargetClickInteraction(openKeyNav, target, e) { var doc = target.ownerDocument; var win = doc.defaultView || doc.parentWindow; var target_tagName = target.tagName.toLowerCase(); if (target_tagName === 'input' || target_tagName === 'textarea' || target.contentEditable === 'true' || target.contentEditable === 'plaintext-only' || target.hasAttribute('tabindex') && target.tabIndex > -1) { placeCursorAndScrollToCursor(openKeyNav, target); } else { if (e.shiftKey && target.tagName.toLowerCase() === 'a' && target.href) { win.open(target.href, '_blank'); } else { openKeyNav.focus(target); // Ensure the target element is focused before dispatching the click event if (!openKeyNav.config.modesConfig.click.modifier) { var clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, view: win }); target.dispatchEvent(clickEvent); } } } openKeyNav.removeOverlays(); openKeyNav.clearMoveAttributes(); }; var placeCursorAndScrollToCursor = clicking.placeCursorAndScrollToCursor = function placeCursorAndScrollToCursor(openKeyNav, target) { var targetTagName = target.tagName.toLowerCase(); setTimeout(function () { openKeyNav.focus(target); if (targetTagName === 'input' && ['text', 'search', 'url', 'tel', 'email', 'password'].indexOf(target.type) > -1 || targetTagName === 'textarea') { // Move the cursor to the end for input and textarea elements var valueLength = target.value.length; target.selectionStart = valueLength; target.selectionEnd = valueLength; // Scroll the element itself into view if it's not fully visible target.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }); } else if (target.contentEditable === 'true' || target.contentEditable === 'plaintext-only') { // Move the caret to the end for contenteditable elements var range = document.createRange(); var sel = window.getSelection(); range.selectNodeContents(target); range.collapse(false); // false to move to the end sel.removeAllRanges(); sel.addRange(range); // Attempt to ensure the caret is visible, considering the element might be larger than the viewport var rect = range.getBoundingClientRect(); if (rect.bottom > window.innerHeight || rect.top < 0) { target.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }); } } // For elements with tabIndex > -1, focusing them should scroll them into view, // but additional logic might be needed based on specific requirements. }, 0); }; return clicking; } var dragAndDrop = {}; Object.defineProperty(dragAndDrop, "__esModule", { value: true }); dragAndDrop.simulateDragAndDrop = dragAndDrop.endDrag = dragAndDrop.beginDrag = void 0; function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray$1(r)) || e) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, 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 o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; } function _unsupportedIterableToArray$1(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray$1(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray$1(r, a) : void 0; } } function _arrayLikeToArray$1(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } dragAndDrop.simulateDragAndDrop = function simulateDragAndDrop(openKeyNav, sourceElement, targetElement) { var handleStickyMove = function handleStickyMove() { function findMatchingElementByHTML(htmlString) { function removeComments(htmlString) { return htmlString.replace(/<!--[\s\S]*?-->/g, ''); } // Remove comments from the HTML string var cleanedHTMLString = removeComments(htmlString); // Get all elements in the document var allElements = document.querySelectorAll('*'); var _iterator = _createForOfIteratorHelper(allElements), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var element = _step.value; // Remove comments from the element's HTML var cleanedElementHTML = removeComments(element.innerHTML); // Compare the cleaned HTML of each element with the cleaned HTML string if (cleanedElementHTML === cleanedHTMLString) { return element; } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } return null; } if (!openKeyNav.config.modesConfig.move.modifier) { return false; } openKeyNav.config.typedLabel.value = ''; openKeyNav.config.modesConfig.move.selectedDropZone = false; // if the selected element (openKeyNav.config.modesConfig.move.selectedMoveable) is no longer in the DOM, // try to find an element in the DOM with matching HTML // that complies with the move inclusion criteria // and doesn't go against the exclusion criteria // set that element as the selected element // and then move the openKeyNav-label-selected label to it if (!document.contains(openKeyNav.config.modesConfig.move.selectedMoveable)) { var matchingElement = findMatchingElementByHTML(openKeyNav.config.modesConfig.move.selectedMoveableHTML); var selectedConfig = openKeyNav.config.modesConfig.move.config[openKeyNav.config.modesConfig.move.selectedConfig]; var passesInclusionCriteria = matchingElement && matchingElement.matches(selectedConfig.fromElements) || matchingElement.matches(selectedConfig.fromContainer + ' > *'); var passesExclusionCriteria = matchingElement && !matchingElement.matches(selectedConfig.fromExclude); if (passesInclusionCriteria && passesExclusionCriteria) { console.log('Matching element found:', matchingElement); openKeyNav.config.modesConfig.move.selectedMoveable = matchingElement; openKeyNav.config.modesConfig.move.selectedMoveableHTML = matchingElement.innerHTML; openKeyNav.updateOverlayPosition(matchingElement, openKeyNav.config.modesConfig.move.selectedLabel); beginDrag(openKeyNav); } else { console.log('No matching element found.'); openKeyNav.removeOverlays(true); openKeyNav.clearMoveAttributes(); } } }; endDrag(openKeyNav, targetElement); handleStickyMove(); // // Sequence the event dispatches with delays // (() => { return new Promise((resolve) => {resolve()})})() // .then(() => dispatchEvent(sourceElement, mouseDownEvent, 1)) // .then(() => dispatchEvent(sourceElement, dragStartEvent, 1)) // .then(() => dispatchEvent(targetElement, dragEnterEvent, 1)) // .then(() => dispatchEvent(targetElement, dragOverEvent, 1)) // .then(() => dispatchEvent(targetElement, dropEvent, 1)) // .then(() => dispatchEvent(sourceElement, dragEndEvent, 1)) // .then(() => dispatchEvent(targetElement, mouseUpEvent, 1)) // .then(() => handleStickyMove()); }; var endDrag = dragAndDrop.endDrag = function endDrag(openKeyNav, targetElement) { var dataTransfer = new DataTransfer(); // Create a DataTransfer object to carry the drag data. var clientX = 0; var clientY = 0; var sourceElement = openKeyNav.config.modesConfig.move.selectedMoveable; if (typeof TouchEvent === 'undefined') { openKeyNav.setupTouchEvent(); } if (!sourceElement) { sourceElement = document.body; } if (!targetElement) { targetElement = document.body; } var rectTarget = targetElement.getBoundingClientRect(); if (targetElement != document) { clientX = rectTarget.left + rectTarget.width / 2; clientY = rectTarget.top + rectTarget.height / 2; } // Create mousemove event to simulate dragging var mouseMoveEvent = new MouseEvent('mousemove', { bubbles: true, cancelable: true, clientX: clientX, clientY: clientY }); // Create touchmove event to simulate dragging var touchMoveEvent = new TouchEvent('touchmove', { bubbles: true, cancelable: true, touches: [new Touch({ identifier: Date.now(), target: targetElement, clientX: clientX, clientY: clientY })] }); // Create dragenter event var dragEnterEvent = new DragEvent('dragenter', { bubbles: true, cancelable: true, clientX: clientX, clientY: clientY, dataTransfer: dataTransfer }); Object.defineProperty(dragEnterEvent, 'dataTransfer', { value: dataTransfer }); // Create dragover event var dragOverEvent = new DragEvent('dragover', { bubbles: true, cancelable: true, clientX: clientX, clientY: clientY, dataTransfer: dataTransfer }); Object.defineProperty(dragOverEvent, 'dataTransfer', { value: dataTransfer }); // Create drop event var dropEvent = new DragEvent('drop', { bubbles: true, cancelable: true, clientX: clientX, clientY: clientY, dataTransfer: dataTransfer }); Object.defineProperty(dropEvent, 'dataTransfer', { value: dataTransfer }); // Create dragend event var dragEndEvent = new DragEvent('dragend', { bubbles: true, cancelable: true, clientX: clientX, clientY: clientY, dataTransfer: dataTransfer }); Object.defineProperty(dragEndEvent, 'dataTransfer', { value: dataTransfer }); // Create mouseup event to drop var mouseUpEvent = new MouseEvent('mouseup', { bubbles: true, cancelable: true, clientX: clientX, clientY: clientY }); // Create touchend event to drop var touchEndEvent = new TouchEvent('touchend', { bubbles: true, cancelable: true, changedTouches: [new Touch({ identifier: Date.now(), target: targetElement, clientX: clientX, clientY: clientY })] }); // Dispatch the events try { document.dispatchEvent(mouseMoveEvent); } catch (error) { console.log(error); } try { document.dispatchEvent(touchMoveEvent); } catch (error) { console.log(error); } try { targetElement.dispatchEvent(dragEnterEvent); } catch (error) { console.log(error); } try { targetElement.dispatchEvent(dragOverEvent); } catch (error) { console.log(error); } try { if (targetElement != document) { targetElement.dispatchEvent(dropEvent); } } catch (error) { console.log(error); } try { sourceElement.dispatchEvent(dragEndEvent); } catch (error) { console.log(error); } targetElement.dispatchEvent(mouseUpEvent); targetElement.dispatchEvent(touchEndEvent); }; var beginDrag = dragAndDrop.beginDrag = function beginDrag(openKeyNav) { var sourceElement = openKeyNav.config.modesConfig.move.selectedMoveable; var rectSource = sourceElement.getBoundingClientRect(); var dataTransfer = new DataTransfer(); // Create a DataTransfer object to carry the drag data. if (typeof TouchEvent === 'undefined') { openKeyNav.setupTouchEvent(); } // Create and dispatch mousedown event var mouseDownEvent = new MouseEvent('mousedown', { bubbles: true, cancelable: true, clientX: rectSource.left + rectSource.width / 2, clientY: rectSource.top + rectSource.height / 2 }); sourceElement.dispatchEvent(mouseDownEvent); // Create and dispatch touchstart event (if needed) var touchStartEvent = new TouchEvent('touchstart', { bubbles: true, cancelable: true, touches: [new Touch({ identifier: Date.now(), target: sourceElement, clientX: rectSource.left + rectSource.width / 2, clientY: rectSource.top + rectSource.height / 2 })] }); sourceElement.dispatchEvent(touchStartEvent); // Simulate mouse movement to trigger Dragula's drag start logic var mouseMoveEvent = new MouseEvent('mousemove', { bubbles: true, cancelable: true, clientX: rectSource.left + rectSource.width / 2 + 10, // Move mouse 10 pixels to the right clientY: rectSource.top + rectSource.height / 2 + 10 // Move mouse 10 pixels down }); document.dispatchEvent(mouseMoveEvent); // Create and dispatch dragstart event var dragStartEvent = new DragEvent('dragstart', { bubbles: true, cancelable: true, clientX: rectSource.left + rectSource.width / 2 + 10, clientY: rectSource.top + rectSource.height / 2 + 10, dataTransfer: dataTransfer }); // Use Object.defineProperty to attach the dataTransfer object to the event. Object.defineProperty(dragStartEvent, 'dataTransfer', { value: dataTransfer }); sourceElement.dispatchEvent(dragStartEvent); }; var _escape$1 = {}; Object.defineProperty(_escape$1, "__esModule", { value: true }); _escape$1.handleEscape = void 0; var _dragAndDrop = dragAndDrop; _escape$1.handleEscape = function handleEscape(openKeyNav, e) { var returnFalse = false; if (openKeyNav.config.modes.clicking.value || openKeyNav.config.modes.moving.value || openKeyNav.config.modes.menu.value) { e.preventDefault(); e.stopPropagation(); (0, _dragAndDrop.endDrag)(openKeyNav); openKeyNav.removeOverlays(); openKeyNav.clearMoveAttributes(); returnFalse = true; } if (openKeyNav.isTextInputActive()) { document.activeElement.blur(); // Removes focus from the active text input } if (returnFalse) { return false; } else { if (document.activeElement != document.body) { document.activeElement.blur(); } } }; var focus = {}; Object.defineProperty(focus, "__esModule", { value: true }); focus.focusOnScrollables = focus.focusOnHeadings = void 0; focus.focusOnHeadings = function focusOnHeadings(openKeyNav, headings, e) { openKeyNav.config.headings.list = Array.from(document.querySelectorAll(headings)) // Get all headings in the view .filter(function (el) { // Skip if the element is visually hidden var style = getComputedStyle(el); if (style.display === 'none' || style.visibility === 'hidden') return false; // debug mode: debug mode: do isAnyCornerVisible check by default and disable the check if debug.screenReaderVisible is true if (!openKeyNav.config.debug.screenReaderVisible) { // Skip if the element's top left corner is covered by another element if (!openKeyNav.isAnyCornerVisible(el)) { return false; } } return true; }); if (openKeyNav.config.headings.list.length == 0) { return true; } // handle moving to the next / previous heading if (e.shiftKey) { // shift key is pressed, so move backwards. If at the beginning, go to the end. if (openKeyNav.config.headings.currentHeadingIndex > 0) { openKeyNav.config.headings.currentHeadingIndex--; } else { openKeyNav.config.headings.currentHeadingIndex = openKeyNav.config.headings.list.length - 1; } } else { // Move to the next heading. If at the end, go to the beginning. if (openKeyNav.config.headings.currentHeadingIndex < openKeyNav.config.headings.list.length - 1) { openKeyNav.config.headings.currentHeadingIndex++; } else { openKeyNav.config.headings.currentHeadingIndex = 0; } } var nextHeading = openKeyNav.config.headings.list[openKeyNav.config.headings.currentHeadingIndex]; if (!nextHeading.hasAttribute('tabindex')) { nextHeading.setAttribute('tabindex', '-1'); // Make the heading focusable nextHeading.setAttribute('data-openkeynav-tabIndexed', true); } openKeyNav.focus(nextHeading); // Set focus on the next heading // Listen for the blur event to remove the tabindex attribute nextHeading.addEventListener('blur', function handler() { if (nextHeading.hasAttribute('data-openkeynav-tabIndexed')) { nextHeading.removeAttribute('tabindex'); // Remove the tabindex attribute nextHeading.removeAttribute('data-openkeynav-tabIndexed'); } nextHeading.removeEventListener('blur', handler); // Clean up the event listener }); }; focus.focusOnScrollables = function focusOnScrollables(openKeyNav, e) { openKeyNav.config.scrollables.list = openKeyNav.getScrollableElements(); // Populate or refresh the list of scrollable elements if (openKeyNav.config.scrollables.list.length == 0) { return; // If no scrollable elements, exit the function } // /* { // Navigate through scrollable elements if (e.shiftKey) { // Move backwards openKeyNav.config.currentScrollableIndex = openKeyNav.config.currentScrollableIndex > 0 ? openKeyNav.config.currentScrollableIndex - 1 : openKeyNav.config.scrollables.list.length - 1; } else { // Move forwards openKeyNav.config.currentScrollableIndex = openKeyNav.config.currentScrollableIndex < openKeyNav.config.scrollables.list.length - 1 ? openKeyNav.config.currentScrollableIndex + 1 : 0; } } //*/ // Focus the current scrollable element var currentScrollable = openKeyNav.config.scrollables.list[openKeyNav.config.currentScrollableIndex]; if (!currentScrollable.hasAttribute('tabindex')) { currentScrollable.setAttribute('tabindex', '-1'); // Make the element focusable currentScrollable.setAttribute('data-openkeynav-tabIndexed', true); } openKeyNav.focus(currentScrollable); // Set focus on the element // Clean up: remove tabindex and blur listener when focus is lost currentScrollable.addEventListener('blur', function handler() { if (currentScrollable.hasAttribute('data-openkeynav-tabIndexed')) { currentScrollable.removeAttribute('tabindex'); // Remove the tabindex attribute currentScrollable.removeAttribute('data-openkeynav-tabIndexed'); } currentScrollable.removeEventListener('blur', handler); }); }; var isTabbable = {}; Object.defineProperty(isTabbable, "__esModule", { value: true }); isTabbable.isTabbable = void 0; var isHiddenByOverflow = function isHiddenByOverflow(element) { var parent = element.parentNode; // Use the ownerDocument to get the correct document context var doc = element.ownerDocument; var body = doc.body; while (parent && parent !== body) { // Use the specific document body of the element // if (parent instanceof HTMLElement) { var parentStyle = getComputedStyle(parent); if (['scroll', 'auto'].includes(parentStyle.overflow) || ['scroll', 'auto'].includes(parentStyle.overflowX) || ['scroll', 'auto'].includes(parentStyle.overflowY)) { var parentRect = parent.getBoundingClientRect(); var rect = element.getBoundingClientRect(); if (rect.bottom < parentRect.top || rect.top > parentRect.bottom || rect.right < parentRect.left || rect.left > parentRect.right) { return true; // Element is hidden by parent's overflow } } // } parent = parent.parentNode; } return false; // No parent hides the element by overflow }; var inViewport = function inViewport(el) { // check if the element's top left corner is within the window's viewport var rect = el.getBoundingClientRect(); var isInViewport = rect.top < window.innerHeight && rect.left < window.innerWidth && rect.bottom > 0 && rect.right > 0; return isInViewport; }; isTabbable.isTabbable = function isTabbable(el, openKeyNav) { var clickableElements = ['a', 'button', 'textarea', 'select', 'input', 'iframe', 'summary', '[onclick]']; var interactiveRoles = ['button', 'link', 'menuitem', 'option', 'tab', 'treeitem', 'checkbox', 'radio']; var isTypicallyClickableElement = function isTypicallyClickableElement(el) { // Check if the element is a known clickable element if (el.matches(clickableElements.join())) { return true; } // Check if the element has an interactive ARIA role var role = el.getAttribute('role'); if (role && interactiveRoles.includes(role)) { return true; } return false; }; // Ensure el is an Element before accessing styles if (!(el instanceof Element)) { // console.log(`!(el instanceof Element)`, el); //debug return false; } // Skip if the element is set to not display (not the same as having zero size) var style = getComputedStyle(el); if (style.display === 'none') { // console.log(`style.display === 'none'`, el); //debug return false; } // Skip if the element is hidden by a parent's overflow if (isHiddenByOverflow(el)) { // console.log(`isHiddenByOverflow(el)`, el); //debug return false; } // Skip if the element is within a <details> that is not open, but allow if it's a <summary> or a clickable element inside a <summary> // aka it's hidden by the collapsed detail if (el.matches('details:not([open]) *') && !el.matches('details:not([open]) > summary, details:not([open]) > summary *')) { // console.log(`hidden details element`, el); //debug return false; } // always include if tabindex > -1 // include this after checking if the element is hidden by a parent's overflow, which most screen readers respect // (elements should not be tabbable by keyboard if they are visibly hidden, // so include visibly hidden items that are explicitly tabbable to help with accessibility bug discovery) // do not move this earlier in the heuristic var tabIndex = el.getAttribute('tabindex'); if (tabIndex && parseInt(tabIndex, 10) > -1) { // console.log(`tabindex > -1`, el); //debug return true; } // Skip if the element is visually hidden (not the same as having zero size or set to not display) if (style.visibility === 'hidden') { // console.log(`style.visibility === 'hidden'`, el); //debug return false; } // console.log("isTabbable() -> openKeyNav", openKeyNav); // Skip if the element has no size (another way to visually hide something) if (!openKeyNav.isNonzeroSize(el)) { // console.log(`!openKeyNav.isNonzeroSize(el)`, el); //debug return false; } // Skip if the element's top left corner is not within the window's viewport if (!inViewport(el)) { // console.log(`!inViewport(el)`, el); //debug return false; } // do isAnyCornerVisible check by default and disable the check if debug.screenReaderVisible is true if (!openKeyNav.config.debug.screenReaderVisible) { // Skip if the element's top left corner is covered by another element if (!openKeyNav.isAnyCornerVisible(el)) { // console.log(`!openKeyNav.isAnyCornerVisible(el)`, el); //debug return false; } } // Skip if <summary> is not the first <summary> element of a <details> if (el.tagName.toLowerCase() === 'summary') { var details = el.parentElement; if (details && details.tagName.toLowerCase() === 'details' && details.querySelector('summary') !== el) { // console.log(`<summary> is not the first <summary> element of a <details>`, el); //debug return false; } } // lastly, elements that are inaccessible due to not being tabbable if (tabIndex && parseInt(tabIndex, 10) == -1) { if (isTypicallyClickableElement(el)) { // if (openKeyNav.config.modes.clicking.value) { openKeyNav.flagAsInaccessible(el, "\n <h2>Inaccessible Element</h2>\n <h3>Problem: </h3>\n <p>This element is not keyboard-focusable.</p>\n <h3>Solution: </h3>\n <p>Since this element has a tabindex attribute set to -1, it cannot be keyboard focusable.</p>\n <p>It must have a tabindex set to a value &gt; -1, ideally 0.</p>\n <p>You can ignore this warning if this element is not meant to be clickable.</p>\n ", "keyboard"); // } } // return false; // let's keep it, since we are flagging it } // Skip if the element is an <a> without an href (unless it has an ARIA role that makes it tabbable) var role = el.getAttribute('role'); switch (el.tagName.toLowerCase()) { case 'a': // console.log(el); //debug if (!el.hasAttribute('href') || el.getAttribute('href') === '') { if (!interactiveRoles.includes(role)) { // if (openKeyNav.config.modes.clicking.value) { openKeyNav.flagAsInaccessible(el, "\n <h2>Inaccessible Button</h2>\n <h3>Problem: </h3>\n <p>This clickable button is not keyboard-focusable.</p>\n <p>As a result, only mouse users can click on it.</p>\n <p>This usability disparity can create an accessibility barrier.</p>\n <h3>Solution: </h3>\n <p>Since it is an anchor tag (&lt;a&gt;), it needs a non-empty <em>href</em> attribute.</p>\n <p>Alternatively, it needs an ARIA <em>role</em> attribute set to something like 'button' or 'link' AND a tabindex attribute set to a value &gt; -1, ideally 0.</p>\n ", "keyboard"); // return false; // } } } break; case 'button': case 'textarea': case 'select': case 'input': case 'iframe': case 'summary': break; default: if (!!role && !interactiveRoles.includes(role)) { if (openKeyNav.config.modesConfig.click.clickEventElements.has(el)) { openKeyNav.flagAsInaccessible(el, "\n <!--\n !el(a,button,textarea,select,input,iframe,summary)\n !el[role('button', 'link', 'menuitem', 'option', 'tab', 'treeitem', 'checkbox', 'radio')]\n fromClickEvents\n -->\n <h2>Possibly Inaccessible Clickable Element</h2>\n <h3>Problem: </h3>\n <p>This element has a mouse click event handler attached to it, but it is not keyboard-focusable.</p>\n <p>As a result, only mouse users can click on it.</p>\n <p>This usability disparity can create an accessibility barrier.</p>\n <h3>Solution Options: </h3>\n <ol>\n <li>\n <p>If clicking this element takes the user to a different location, convert this element to an anchor link (&lt;a&gt;) with a non-empty <em>href</em> attribute.</p>\n </li>\n <li>\n <p>Otherwise if clicking this element triggers an action on the page, convert this element to a &lt;button&gt; without a <em>disabled</em> attribute.</p>\n <p>Alternatively, it needs an ARIA <em>role</em> attribute set to something like 'button' or 'link' AND a tabindex attribute set to a value &gt; -1, ideally 0.</p>\n </li>\n <li>\n <p>Otherwise, if clicking this element does not do anything, then consider removing the click event handler attached to this element.</p>\n </li>\n </ol>\n ", "keyboard"); } // return false; // } } break; } // it must be a valid tabbable element return true; }; var keylabels = {}; var scrolling = {}; Object.defineProperty(scrolling, "__esModule", { value: true }); scrolling.disableScrolling = void 0; scrolling.disableScrolling = function disableScrolling(openKeyNav) { // Prevent scrolling on the webpage var disableScrollingForEl = function disableScrollingForEl(el) { el.addEventListener('scroll', openKeyNav.preventScroll, { passive: false }); el.addEventListener('wheel', openKeyNav.preventScroll, { passive: false }); el.addEventListener('touchmove', openKeyNav.preventScroll, { passive: false }); }; var disableScrollingForScrollableElements = function disableScrollingForScrollableElements() { disableScrollingForEl(window); openKeyNav.getScrollableElements().forEach(function (el) { disableScrollingForEl(el); }); }; disableScrollingForScrollableElements(); }; Object.defineProperty(keylabels, "__esModule", { value: true }); keylabels.showMoveableFromOverlays = keylabels.showClickableOverlays = keylabels.generateValidKeyChars = keylabels.generateLabels = keylabels.filterRemainingOverlays = void 0; var _escape = _escape$1; var _isTabbable = isTabbable; var _scrolling = scrolling; function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _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 _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); } function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } var generateLabels = keylabels.generateLabels = function generateLabels(openKeyNav, count) { var labels = []; var chars = generateValidKeyChars(openKeyNav); var maxLength = Math.pow(chars.length, 2); var useThirdChar = count > maxLength; if (useThirdChar) { maxLength = Math.pow(chars.length, 3); } for (var i = 0; i < count && labels.length < maxLength; i++) { var firstChar = chars[i % chars.length]; var secondChar = chars[Math.floor(i / chars.length) % chars.length] || ''; var thirdChar = useThirdChar ? chars[Math.floor(i / Math.pow(chars.length, 2)) % chars.length] : ''; labels.push(firstChar + secondChar + thirdChar); } // Attempt to shorten labels that are uniquely identifiable by their first character var labelCounts = {}; labels.forEach(function (label) { var firstChar = label[0]; labelCounts[firstChar] = (labelCounts[firstChar] || 0) + 1; }); labels = labels.map(function (label) { var firstChar = label[0]; if (labelCounts[firstChar] === 1 && !label.includes('.')) { // Check for uniqueness and ensure not shortened if it's a prefix return firstChar; } return label; }); // alert(labels) // now we have all the labels we will use. // Shuffle them for variable rewards. ++addiction // return shuffle(labels); return labels; // unshuffled }; keylabels.showClickableOverlays = function showClickableOverlays(openKeyNav) { (0, _scrolling.disableScrolling)(openKeyNav); setTimeout(function () { var clickables = _getAllCandidateElements(openKeyNav, document).filter(function (el) { return (0, _isTabbable.isTabbable)(el, openKeyNav); }); // console.log(clickables); var labels = generateLabels(openKeyNav, clickables.length); clickables.forEach(function (element, index) { element.setAttribute('data-openkeynav-label', labels[index]); }); clickables.forEach(function (element, index) { openKeyNav.createOverlay(element, labels[index]); }); }, 0); // Use timeout to ensure the operation completes }; keylabels.showMoveableFromOverlays = function showMoveableFromOverlays(openKeyNav) { // alert("showMoveableFromOverlays()"); // return; // Combine all unique 'from' classes from moveConfig to query the document var moveables = []; // direct selectors of from elements var fromElementSelectors = _toConsumableArray(new Set(openKeyNav.config.modesConfig.move.config.filter(function (config) { return config.fromElements; }).map(function (config) { return config.fromElements; }))); if (!!fromElementSelectors.length) { document.querySelectorAll(fromElementSelectors.join(', ')).forEach(function (element) { var config = openKeyNav.config.modesConfig.move.config.find(function (c) { return element.matches(c.fromElements); }); if (config) { var configKey = openKeyNav.config.modesConfig.move.config.indexOf(config); if (openKeyNav.isNonzeroSize(element) && (!config.fromExclude || !element.matches(config.fromExclude))) { element.setAttribute('data-openkeynav-moveconfig', configKey); // Store the moveConfig key moveables.push(element); } } }); } // containers of from elements var fromContainerSelectors = _toConsumableArray(new Set(openKeyNav.config.modesConfig.move.config.filter(function (config) { return config.fromContainer; }).map(function (config) { return config.fromContainer; }))); if (!!fromContainerSelectors.length) { var fromContainers = document.querySelectorAll(fromContainerSelectors.join(', ')); // Collect all direct children of each fromContainer as moveable elements fromContainers.forEach(function (container) { var config = openKeyNav.config.modesConfig.move.config.find(function (c) { return container.matches(c.fromContainer); }); if (config) { var configKey = openKeyNav.config.modesConfig.move.config.indexOf(config); var children = Array.from(container.children); children.forEach(function (child) { if (openKeyNav.isNonzeroSize(child) && (!config.fromExclude || !child.matches(config.fromExclude))) { child.setAttribute('data-openkeynav-moveconfig', configKey); // Store the moveConfig key moveables.push(child); } }); } }); } // Resolve elements using provided callbacks if available openKeyNav.config.modesConfig.move.config.forEach(function (config) { if (config.resolveFromElements) { var resolvedElements = config.resolveFromElements(); resolvedElements.forEach(function (element) { var configKey = openKeyNav.config.modesConfig.move.config.indexOf(config); if (openKeyNav.isNonzeroSize(element) && (!config.fromExclude || !element.matches(config.fromExclude))) { element.setAttribute('data-openkeynav-moveconfig', configKey); // Store the moveConfig key moveables.push(element); } }); } }); // filter out moveables that would not be clickable moveables = moveables.filter(function (el) { return (0, _isTabbable.isTabbable)(el, openKeyNav); }); var labels = generateLabels(openKeyNav, moveables.length); moveables.forEach(function (element, index) { element.setAttribute('data-openkeynav-label', labels[index]); }); moveables.forEach(function (element, index) { openKeyNav.createOverlay(element, labels[index]); element.setAttribute('data-openkeynav-draggable', 'true'); }); }; keylabels.filterRemainingOverlays = function filterRemainingOverlays(openKeyNav, e) { // Filter overlays, removing non-matching ones document.querySelectorAll('.openKeyNav-label').forEach(function (overlay) { var label = overlay.textContent; // If the current typedLabel no longer matches the beginning of this element's label, remove both the overlay and clean up the target element if (!label.startsWith(openKeyNav.config.typedLabel.value)) { var targetElement = document.querySelector("[data-openkeynav-label=\"".concat(label, "\"]")); targetElement && targetElement.removeAttribute('data-openkeynav-label'); // Clean up the target element's attribute overlay.remove(); // Remove the overlay } }); if (document.querySelectorAll('.openKeyNav-label').length == 0) { // there are no overlays left. clean up and unblock. (0, _escape.handleEscape)(openKeyNav, e); return true; } }; var generateValidKeyChars = keylabels.generateValidKeyChars = function generateValidKeyChars(openKeyNav) { var chars = 'abcdefghijklmnopqrstuvwxyz'; // let chars = '1234567890'; // let chars = 'abcdefghijklmnopqrstuvwxyz1234567890'; // not a good idea because 1 and l can be confused // Remove letters from chars that are present in openKeyNav.config.keys // maybe this isn't necessary when in click mode (mode paradigm is common in screen readers) // Object.values(openKeyNav.config.keys).forEach(key => { // chars = chars.replace(key, ''); // }); // remove the secondary escape key code chars = chars.replace(openKeyNav.config.keys.escape, ''); return chars; }; var _getAllCandidateElements = function getAllCandidateElements(openKeyNav, doc) { var allElements = Array.from(doc.querySelectorAll("a," + // can be made non-tabbable by removing the href attribute or setting tabindex="-1". "button:not([disabled])," + // are not tabbable when disabled. "textarea:not([disabled])," + // are not tabbable when disabled. "select:not([disabled])," + // are not tabbable when disabled. "input:not([disabled])," + // are not tabbable when disabled. // "label," + // are not normally tabbable unless they contain tabbable content. "iframe," + // are tabbable by default. "details > summary," + // The summary element inside a details element can be tabbable "[role=button]," + // can be made non-tabbable by adding tabindex="-1". "[role=link]," + // can be made non-tabbable by adding tabindex="-1". "[role=menuitem]," + // can be made non-tabbable by adding tabindex="-1". "[role=option]," + // can be made non-tabbable by adding tabindex="-1". "[role=tab]," + // can be made non-tabbable by adding tabindex="-1". "[role=treeitem]," + // can be made non-tabbable by adding tabindex="-1". "[role=checkbox]," + // can be made non-tabbable by adding tabindex="-1". "[role=radio]," + // can be made non-tabbable by adding tabindex="-1". "[aria-checked]," + // not inherently tabbable or non-tabbable. "[contenteditable=true]," + // elements with contenteditable="true" are tabbable. "[contenteditable=plaintext-only]," + // elements with contenteditable="plaintext-only" are tabbable. "[tabindex]," + // elements with a tabindex attribute can be made tabbable or non-tabbable depending on the value of tabindex. "[onclick]" // elements with an onclick attribute are not inherently tabbable or non-tabbable. )); var iframes = doc.querySelectorAll('iframe'); iframes.forEach(function (iframe) { try { var iframeDoc = iframe.contentDocument || iframe.contentWindow.document; var iframeElements = _getAllCandidateElements(openKeyNav, iframeDoc); allElements = allElements.concat(Array.from(iframeElements)); // Add elements from each iframe } catch (error) { console.log('Access denied to iframe content:', error); } }); // Merge with clickEventElements var mergedSet = new Set([].concat(_toConsumableArray(allElements), _toConsumableArray(openKeyNav.config.modesConfig.click.clickEventElements))); return Array.from(mergedSet); // return allElements; }; var lifecycle = {}; Object.defineProperty(lifecycle, "__esModule", { value: true }); lifecycle.enable = lifecycle.disable = void 0; lifecycle.enable = function enable() {}; lifecycle.disable = function disable() {}; var hasRequiredKeypress; function requireKeypress() { if (hasRequiredKeypress) return keypress; hasRequiredKeypress = 1; Object.defineProperty(keypress, "__esModule", { value: true }); keypress.modiferKeyString = keypress.handleKeyPress = void 0; var _clicking = requireClicking(); var _dragAndDrop = dragAndDrop; var _escape = _escape$1; var _focus = focus; var _isTabbable = isTabbable; var _keylabels = keylabels; var _keyButton = keyButton; function getMetaKeyName() { var userAgent = window.navigator.userAgent.toLowerCase(); if (userAgent.indexOf('mac') >= 0) return 'Cmd'; if (userAgent.indexOf('win') >= 0) return 'Win'; if (userAgent.indexOf('linux') >= 0) return 'Super'; // fallback return 'Meta'; } var modiferKeyString = keypress.modiferKeyString = function modiferKeyString(openKeyNav) { switch (openKeyNav.config.keys.modifierKey) { case 'shiftKey': return 'Shift'; case 'altKey': return 'Alt'; case 'metaKey': return getMetaKeyName(); default: return openKeyNav.config.keys.modifierKey; } }; keypress.handleKeyPress = function handleKeyPress(openKeyNav, e) { var isTextInputActive = openKeyNav.isTextInputActive(); // enable / disable openKeyNav if (e[openKeyNav.config.keys.modifierKey] && openKeyNav.config.keys.menu.toLowerCase() == e.key.toLowerCase()) { if (isTextInputActive) { if (!e[openKeyNav.config.keys.inputEscape]) { return true; } } if (!openKeyNav.meta.enabled.value) { // if openKeyNav disabled openKeyNav.enable(); var message = "openKeyNav enabled. Press ".concat((0, _keyButton.keyButton)([modiferKeyString(openKeyNav), openKeyNav.config.keys.menu]), " to disable."); openKeyNav.emitNotification(message); return true; } else { (0, _escape.handleEscape)(openKeyNav, e); openKeyNav.disable(); var _message = "openKeyNav disabled. Press ".concat((0, _keyButton.keyButton)([modiferKeyString(openKeyNav), openKeyNav.config.keys.menu]), " to enable."); openKeyNav.emitNotification(_message); return true; } } // first check for modifier keys and escape switch (e.key) { case 'Shift': // exit this event listener if it's the shift key press case 'Control': // exit this event listener if it's the control key press case 'Alt': // exit this event listener if it's the alt key press case 'Meta': // exit this event listener if it's the meta key (Command/Windows) press case ' ': // exit this event listener if it's the space bar key press // Prevent default action and stop the function // e.preventDefault(); return true; // handle escape first case 'Escape': // escaping // alert("Escape"); (0, _escape.handleEscape)(openKeyNav, e); break; } // check if currently in any openkeynav modes if (openKeyNav.config.modes.clicking.value) { return handleClickMode(openKeyNav, e); } if (openKeyNav.config.modes.moving.value) { return handleMoveMode(openKeyNav, e); } if (openKeyNav.config.modes.menu.value) { handleMenuMode(); } if (isTextInputActive) { if (!e[openKeyNav.config.keys.inputEscape]) { return true; } } if (!openKeyNav.meta.enabled.value) { return true; } // escape and toggles switch (e.key) { case openKeyNav.config.keys.escape: // escaping // alert("Escape"); (0, _escape.handleEscape)(openKeyNav, e); return true; // case openKeyNav.config.keys.toggleCursor: // toggle Cursor // // toggle class openKeyNav-noCursor for body // document.body.classList.toggle('openKeyNav-noCursor'); // return true; // break; } // modes switch (e.key) { case openKeyNav.config.keys.click: // possibly attempting to initiate click mode case openKeyNav.config.keys.click.toUpperCase(): e.preventDefault(); openKeyNav.config.modes.clicking.value = true; if (e.key == openKeyNav.config.keys.click.toUpperCase()) { openKeyNav.config.modesConfig.click.modifier = true; } (0, _keylabels.showClickableOverlays)(openKeyNav); return true; // possibly attempting to initiate moving mode case openKeyNav.config.keys.move: case openKeyNav.config.keys.move.toUpperCase(): // Toggle move mode e.preventDefault(); openKeyNav.config.modes.moving.value = true; // Assuming you add a 'move' flag to your modes object if (e.key == openKeyNav.config.keys.move.toUpperCase()) { openKeyNav.config.modesConfig.move.modifier = true; } (0, _keylabels.showMoveableFromOverlays)(openKeyNav); // This will be a new function similar to showClickableOverlays return true; case openKeyNav.config.keys.menu: case openKeyNav.config.keys.menu.toUpperCase(): openKeyNav.config.modes.menu.value = true; if (e.key == openKeyNav.config.keys.menu.toUpperCase()) { openKeyNav.config.modesConfig.menu.modifier = true; } return true; } // focus / navigation (can be modified by shift, so always check for lowercase) switch (e.key.toLowerCase()) { // Check if the pressed key is for headings case openKeyNav.config.keys.heading.toLowerCase(): /* const OpenKeyNav = { currentHeadingIndex: 0, keys: {