From 99e12af36a50bf96fa2cdceee39b109d3a19d1af Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Tue, 14 Jan 2025 12:02:52 -0600 Subject: [PATCH 01/38] updated SkipTo.js to version 5.7.2 --- content/shared/js/skipto.js | 4155 ++++++++++++++++++++++++++++------- 1 file changed, 3408 insertions(+), 747 deletions(-) diff --git a/content/shared/js/skipto.js b/content/shared/js/skipto.js index d65ab3c766..96ce0d8960 100644 --- a/content/shared/js/skipto.js +++ b/content/shared/js/skipto.js @@ -1,6 +1,6 @@ /* ======================================================================== - * Version: 5.3.2 - * Copyright (c) 2022, 2023, 2024 Jon Gunderson; Licensed BSD + * Version: 5.7 + * Copyright (c) 2022, 2023, 2024, 2025 Jon Gunderson; Licensed BSD * Copyright (c) 2021 PayPal Accessibility Team and University of Illinois; Licensed BSD * All rights reserved. * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -18,6 +18,125 @@ (function () { 'use strict'; + /* colorThemes */ + + const colorThemes = { + 'default': { + + fontFamily: 'inherit', + fontSize: 'inherit', + positionLeft: '46%', + smallBreakPoint: '576', + mediumBreakPoint: '992', + buttonTextColor: '#13294b', + buttonBackgroundColor: '#dddddd', + focusBorderColor: '#c5050c', + menuTextColor: '#13294b', + menuBackgroundColor: '#dddddd', + menuitemFocusTextColor: '#dddddd', + menuitemFocusBackgroundColor: '#13294b', + menuTextDarkColor: '#ffffff', + menuBackgroundDarkColor: '#000000', + menuitemFocusTextDarkColor: '#ffffff', + menuitemFocusBackgroundDarkColor: '#013c93', + focusBorderDarkColor: '#ffffff', + buttonTextDarkColor: '#ffffff', + buttonBackgroundDarkColor: '#013c93', + zIndex: '2000000', + zHighlight: '1999900', + displayOption: 'fixed' + }, + 'aria': { + hostnameSelector: 'w3.org', + pathnameSelector: 'ARIA/apg', + fontFamily: 'sans-serif', + fontSize: '10pt', + positionLeft: '7%', + menuTextColor: '#000', + menuBackgroundColor: '#def', + menuitemFocusTextColor: '#fff', + menuitemFocusBackgroundColor: '#005a9c', + focusBorderColor: '#005a9c', + buttonTextColor: '#005a9c', + buttonBackgroundColor: '#ddd', + }, + 'illinois': { + hostnameSelector: 'illinois.edu', + menuTextColor: '#00132c', + menuBackgroundColor: '#cad9ef', + menuitemFocusTextColor: '#eeeeee', + menuitemFocusBackgroundColor: '#00132c', + focusBorderColor: '#ff552e', + buttonTextColor: '#444444', + buttonBackgroundColor: '#dddede', + highlightTarget: 'disabled' + }, + 'openweba11y': { + hostnameSelector: 'openweba11y.com', + buttonTextColor: '#13294B', + buttonBackgroundColor: '#dddddd', + focusBorderColor: '#C5050C', + menuTextColor: '#13294B', + menuBackgroundColor: '#dddddd', + menuitemFocusTextColor: '#dddddd', + menuitemFocusBackgroundColor: '#13294B', + fontSize: '90%' + }, + 'skipto': { + hostnameSelector: 'skipto-landmarks-headings.github.io', + positionLeft: '25%', + fontSize: '14px', + menuTextColor: '#00132c', + menuBackgroundColor: '#cad9ef', + menuitemFocusTextColor: '#eeeeee', + menuitemFocusBackgroundColor: '#00132c', + focusBorderColor: '#ff552e', + buttonTextColor: '#444444', + buttonBackgroundColor: '#dddede', + }, + 'uic': { + hostnameSelector: 'uic.edu', + menuTextColor: '#001e62', + menuBackgroundColor: '#f8f8f8', + menuitemFocusTextColor: '#ffffff', + menuitemFocusBackgroundColor: '#001e62', + focusBorderColor: '#d50032', + buttonTextColor: '#ffffff', + buttonBackgroundColor: '#001e62', + }, + 'uillinois': { + hostnameSelector: 'uillinois.edu', + menuTextColor: '#001e62', + menuBackgroundColor: '#e8e9ea', + menuitemFocusTextColor: '#f8f8f8', + menuitemFocusBackgroundColor: '#13294b', + focusBorderColor: '#dd3403', + buttonTextColor: '#e8e9ea', + buttonBackgroundColor: '#13294b', + highlightTarget: 'disabled' + }, + 'uis': { + hostnameSelector: 'uis.edu', + menuTextColor: '#036', + menuBackgroundColor: '#fff', + menuitemFocusTextColor: '#fff', + menuitemFocusBackgroundColor: '#036', + focusBorderColor: '#dd3444', + buttonTextColor: '#fff', + buttonBackgroundColor: '#036', + }, + 'walmart': { + hostnameSelector: 'walmart.com', + buttonTextColor: '#ffffff', + buttonBackgroundColor: '#00419a', + focusBorderColor: '#ffc220', + menuTextColor: '#ffffff', + menuBackgroundColor: '#0071dc', + menuitemFocusTextColor: '#00419a', + menuitemFocusBackgroundColor: '#ffffff', + } + }; + /* * debug.js * @@ -111,15 +230,25 @@ /* style.js */ /* Constants */ - const debug$6 = new DebugLogging('style', false); - debug$6.flag = false; + const debug$c = new DebugLogging('style', false); + debug$c.flag = false; + + const skipToMenuStyleID = 'id-skip-to-menu-style'; + const skipToHighlightStyleID = 'id-skip-to-highlight-style'; + + const cssMenuTemplate = document.createElement('template'); + cssMenuTemplate.textContent = ` +:root { + color-scheme: light dark; +} - const styleTemplate = document.createElement('template'); - styleTemplate.innerHTML = ` - + +$skipToId-overlay .overlay-border.hasInfoBottom { + border-radius: 3px 3px 3px 0; +} + +$skipToId-overlay .overlay-border.hasInfoTop { + border-radius: 0 3px 3px 3px; +} + +$skipToId-overlay .overlay-info { + position: relative; + text-align: left; + left: -2px; + padding: 1px 4px; + border: 2px solid light-dark($focusBorderColor, $focusBorderDarkColor); + background-color: light-dark($menuBackgroundColor, $menuBackgroundDarkColor); + color: light-dark($menuTextColor, $menuTextDarkColor); + z-index: $zHighlight; + overflow: hidden; + text-overflow: ellipsis; + pointer-events:none; +} + +$skipToId-overlay .overlay-info.hasInfoTop { + border-radius: 3px 3px 0 0; +} + +$skipToId-overlay .overlay-info.hasInfoBottom { + border-radius: 0 0 3px 3px; +} + +@media (forced-colors: active) { + + $skipToId-overlay { + border-color: ButtonBorder; + } + + $skipToId-overlay .overlay-border { + border-color: ButtonBorder; + } + + $skipToId-overlay .overlay-border.skip-to-hidden { + background-color: ButtonFace; + color: ButtonText; + } + + $skipToId-overlay .overlay-info { + border-color: ButtonBorder; + background-color: ButtonFace; + color: ButtonText; + } + +} + `; /* @@ -405,12 +699,11 @@ $skipToId [role="menuitem"].hover .label { * * @desc Returns * - * @param {Object} colorThemes - Javascript object with keyed color themes * @param {String} colorTheme - A string identifying a color theme * * @returns {Object} see @desc */ - function getTheme(colorThemes, colorTheme) { + function getTheme(colorTheme) { if (typeof colorThemes[colorTheme] === 'object') { return colorThemes[colorTheme]; } @@ -429,7 +722,6 @@ $skipToId [role="menuitem"].hover .label { let hostnameFlag = false; let pathnameFlag = false; - if (hostnameSelector) { if (hostname.indexOf(hostnameSelector) >= 0) { if (!hostnameMatch || @@ -489,7 +781,7 @@ $skipToId [role="menuitem"].hover .label { * * @returns */ - function updateStyle(stylePlaceholder, configValue, themeValue, defaultValue) { + function updateStyle(cssContent, stylePlaceholder, configValue, themeValue, defaultValue) { let value = defaultValue; if (typeof configValue === 'string' && configValue) { value = configValue; @@ -499,7 +791,6 @@ $skipToId [role="menuitem"].hover .label { } } - let cssContent = styleTemplate.innerHTML; let index1 = cssContent.indexOf(stylePlaceholder); let index2 = index1 + stylePlaceholder.length; while (index1 >= 0 && index2 < cssContent.length) { @@ -507,73 +798,139 @@ $skipToId [role="menuitem"].hover .label { index1 = cssContent.indexOf(stylePlaceholder, index2); index2 = index1 + stylePlaceholder.length; } - styleTemplate.innerHTML = cssContent; + return cssContent; } /* * @function addCSSColors * - * @desc Updates the styling information in the attached - * stylesheet to use the configured or default colors + * @desc Updates the styling for the menu and highlight information + * and returns the updated strings + * + * @param {String} cssMenu - CSS template for the button and menu + * @param {String} cssHighlight - CSS template for the highlighting + * @param {Object} config - SkipTo.js configuration information object + * @param {Boolean} useURLTheme - When true use the theme associated with the URL * - * @param {Object} colorThemes - Object with theme information - * @param {Object} config - Configuration information object + * @returns. see @desc */ - function addCSSColors (colorThemes, config) { - const theme = getTheme(colorThemes, config.colorTheme); - const defaultTheme = getTheme(colorThemes, 'default'); + function addCSSColors (cssMenu, cssHighlight, config, useURLTheme=false) { + const theme = useURLTheme ? getTheme(config.colorTheme) : {}; + const defaultTheme = getTheme('default'); // Check for display option in theme - if ((typeof theme.displayOption === 'string') && - ('fixed popup static'.indexOf(theme.displayOption.toLowerCase())>= 0)) { - config.displayOption = theme.displayOption; - } - - updateStyle('$fontFamily', config.fontFamily, theme.fontFamily, defaultTheme.fontFamily); - updateStyle('$fontSize', config.fontSize, theme.fontSize, defaultTheme.fontSize); - - updateStyle('$positionLeft', config.positionLeft, theme.positionLeft, defaultTheme.positionLeft); - updateStyle('$smallBreakPoint', config.smallBreakPoint, theme.smallBreakPoint, defaultTheme.smallBreakPoint); - updateStyle('$mediumBreakPoint', config.mediumBreakPoint, theme.mediumBreakPoint, defaultTheme.mediumBreakPoint); - - updateStyle('$menuTextColor', config.menuTextColor, theme.menuTextColor, defaultTheme.menuTextColor); - updateStyle('$menuBackgroundColor', config.menuBackgroundColor, theme.menuBackgroundColor, defaultTheme.menuBackgroundColor); + if ((typeof config.displayOption === 'string') && + (['popup-border', 'fixed', 'popup', 'static'].includes(config.displayOption.toLowerCase()) < 0)) { - updateStyle('$menuitemFocusTextColor', config.menuitemFocusTextColor, theme.menuitemFocusTextColor, defaultTheme.menuitemFocusTextColor); - updateStyle('$menuitemFocusBackgroundColor', config.menuitemFocusBackgroundColor, theme.menuitemFocusBackgroundColor, defaultTheme.menuitemFocusBackgroundColor); - - updateStyle('$focusBorderColor', config.focusBorderColor, theme.focusBorderColor, defaultTheme.focusBorderColor); + if ((typeof theme.displayOption === 'string') && + (['popup-border', 'fixed', 'popup', 'static'].includes(theme.displayOption.toLowerCase())>= 0)) { + config.displayOption = theme.displayOption; + } + else { + config.displayOption = defaultTheme.displayOption; + } + } - updateStyle('$buttonTextColor', config.buttonTextColor, theme.buttonTextColor, defaultTheme.buttonTextColor); - updateStyle('$buttonBackgroundColor', config.buttonBackgroundColor, theme.buttonBackgroundColor, defaultTheme.buttonBackgroundColor); + cssMenu = updateStyle(cssMenu, '$fontFamily', config.fontFamily, theme.fontFamily, defaultTheme.fontFamily); + cssMenu = updateStyle(cssMenu, '$fontSize', config.fontSize, theme.fontSize, defaultTheme.fontSize); + + cssMenu = updateStyle(cssMenu, '$positionLeft', config.positionLeft, theme.positionLeft, defaultTheme.positionLeft); + cssMenu = updateStyle(cssMenu, '$smallBreakPoint', config.smallBreakPoint, theme.smallBreakPoint, defaultTheme.smallBreakPoint); + cssMenu = updateStyle(cssMenu, '$mediumBreakPoint', config.mediumBreakPoint, theme.mediumBreakPoint, defaultTheme.mediumBreakPoint); + + cssMenu = updateStyle(cssMenu, '$menuTextColor', config.menuTextColor, theme.menuTextColor, defaultTheme.menuTextColor); + cssMenu = updateStyle(cssMenu, '$menuTextDarkColor', config.menuTextDarkColor, theme.menuTextDarkColor, defaultTheme.menuTextDarkColor); + cssMenu = updateStyle(cssMenu, '$menuBackgroundColor', config.menuBackgroundColor, theme.menuBackgroundColor, defaultTheme.menuBackgroundColor); + cssMenu = updateStyle(cssMenu, '$menuBackgroundDarkColor', config.menuBackgroundDarkColor, theme.menuBackgroundDarkColor, defaultTheme.menuBackgroundDarkColor); + + cssMenu = updateStyle(cssMenu, '$menuitemFocusTextColor', config.menuitemFocusTextColor, theme.menuitemFocusTextColor, defaultTheme.menuitemFocusTextColor); + cssMenu = updateStyle(cssMenu, '$menuitemFocusTextDarkColor', config.menuitemFocusTextDarkColor, theme.menuitemFocusTextDarkColor, defaultTheme.menuitemFocusTextDarkColor); + cssMenu = updateStyle(cssMenu, '$menuitemFocusBackgroundColor', config.menuitemFocusBackgroundColor, theme.menuitemFocusBackgroundColor, defaultTheme.menuitemFocusBackgroundColor); + cssMenu = updateStyle(cssMenu, '$menuitemFocusBackgroundDarkColor', config.menuitemFocusBackgroundDarkColor, theme.menuitemFocusBackgroundDarkColor, defaultTheme.menuitemFocusBackgroundDarkColor); + + cssMenu = updateStyle(cssMenu, '$focusBorderColor', config.focusBorderColor, theme.focusBorderColor, defaultTheme.focusBorderColor); + cssMenu = updateStyle(cssMenu, '$focusBorderDarkColor', config.focusBorderDarkColor, theme.focusBorderDarkColor, defaultTheme.focusBorderDarkColor); + + cssMenu = updateStyle(cssMenu, '$buttonTextColor', config.buttonTextColor, theme.buttonTextColor, defaultTheme.buttonTextColor); + cssMenu = updateStyle(cssMenu, '$buttonTextDarkColor', config.buttonTextDarkColor, theme.buttonTextDarkColor, defaultTheme.buttonTextDarkColor); + cssMenu = updateStyle(cssMenu, '$buttonBackgroundColor', config.buttonBackgroundColor, theme.buttonBackgroundColor, defaultTheme.buttonBackgroundColor); + cssMenu = updateStyle(cssMenu, '$buttonBackgroundDarkColor', config.buttonBackgroundDarkColor, theme.buttonBackgroundDarkColor, defaultTheme.buttonBackgroundDarkColor); + + cssMenu = updateStyle(cssMenu, '$zIndex', config.zIndex, theme.zIndex, defaultTheme.zIndex); + + cssHighlight = updateStyle(cssHighlight, '$zHighlight', config.zHighlight, theme.zHighlight, defaultTheme.zHighlight); + cssHighlight = updateStyle(cssHighlight, '$buttonBackgroundColor', config.buttonBackgroundColor, theme.buttonBackgroundColor, defaultTheme.buttonBackgroundColor); + cssHighlight = updateStyle(cssHighlight, '$buttonBackgroundDarkColor', config.buttonBackgroundDarkColor, theme.buttonBackgroundDarkColor, defaultTheme.buttonBackgroundDarkColor); + cssHighlight = updateStyle(cssHighlight, '$focusBorderColor', config.focusBorderColor, theme.focusBorderColor, defaultTheme.focusBorderColor); + cssHighlight = updateStyle(cssHighlight, '$focusBorderDarkColor', config.focusBorderDarkColor, theme.focusBorderDarkColor, defaultTheme.focusBorderDarkColor); + cssHighlight = updateStyle(cssHighlight, '$menuTextColor', config.menuitemFocusTextColor, theme.menuitemFocusTextColor, defaultTheme.menuitemFocusTextColor); + cssHighlight = updateStyle(cssHighlight, '$menuTextDarkColor', config.menuitemFocusTextDarkColor, theme.menuitemFocusTextDarkColor, defaultTheme.menuitemFocusTextDarkColor); + cssHighlight = updateStyle(cssHighlight, '$menuBackgroundColor', config.menuitemFocusBackgroundColor, theme.menuitemFocusBackgroundColor, defaultTheme.menuitemFocusBackgroundColor); + cssHighlight = updateStyle(cssHighlight, '$menuBackgroundDarkColor', config.menuitemFocusBackgroundDarkColor, theme.menuitemFocusBackgroundDarkColor, defaultTheme.menuitemFocusBackgroundDarkColor); + cssHighlight = updateStyle(cssHighlight, '$menuitemFocusTextColor', config.menuitemFocusTextColor, theme.menuitemFocusTextColor, defaultTheme.menuitemFocusTextColor); + cssHighlight = updateStyle(cssHighlight, '$menuitemFocusTextDarkColor', config.menuitemFocusTextDarkColor, theme.menuitemFocusTextDarkColor, defaultTheme.menuitemFocusTextDarkColor); + cssHighlight = updateStyle(cssHighlight, '$menuitemFocusBackgroundColor', config.menuitemFocusBackgroundColor, theme.menuitemFocusBackgroundColor, defaultTheme.menuitemFocusBackgroundColor); + cssHighlight = updateStyle(cssHighlight, '$menuitemFocusBackgroundDarkColor', config.menuitemFocusBackgroundDarkColor, theme.menuitemFocusBackgroundDarkColor, defaultTheme.menuitemFocusBackgroundDarkColor); + cssHighlight = updateStyle(cssHighlight, '$hiddenHeadingColor', config.hiddenHeadingColor, theme.hiddenHeadingColor, defaultTheme.hiddenHeadingColor); + cssHighlight = updateStyle(cssHighlight, '$hiddenHeadingDarkColor', config.hiddenHeadingDarkColor, theme.hiddenHeadingDarkColor, defaultTheme.hiddenHeadingDarkColor); + cssHighlight = updateStyle(cssHighlight, '$hiddenHeadingBackgroundColor', config.hiddenHeadingBackgroundColor, theme.hiddenHeadingBackgroundColor, defaultTheme.hiddenHeadingBackgroundColor); + cssHighlight = updateStyle(cssHighlight, '$hiddenHeadingBackgroundDarkColor', config.hiddenHeadingBackgroundDarkColor, theme.hiddenHeadingBackgroundDarkColor, defaultTheme.hiddenHeadingBackgroundDarkColor); + + // Special case for theme configuration used in Illinois theme + if (typeof theme.highlightTarget === 'string') { + config.highlightTarget = theme.highlightTarget; + } - updateStyle('$zIndex', config.zIndex, theme.zIndex, defaultTheme.zIndex); + return [cssMenu, cssHighlight]; } /* - * @function enderStyleElement + * @function renderStyleElement * * @desc Updates the style sheet template and then attaches it to the document * - * @param {Object} colorThemes - Object with theme information + * @param {Object} attachNode - DOM element node to attach button and menu container element * @param {Object} config - Configuration information object * @param {String} skipYToStyleId - Id used for the skipto container element + * @param {Boolean} useURLTheme - When true use the theme associated with the URL */ - function renderStyleElement (colorThemes, config, skipToId) { - styleTemplate.innerHTML = styleTemplate.innerHTML.replaceAll('$skipToId', '#' + skipToId); - addCSSColors(colorThemes, config); - const styleNode = styleTemplate.content.cloneNode(true); - styleNode.id = `${skipToId}-style`; - const headNode = document.getElementsByTagName('head')[0]; - headNode.appendChild(styleNode); + function renderStyleElement (attachNode, config, skipToId, useURLTheme=false) { + let cssMenu = cssMenuTemplate.textContent.slice(0); + cssMenu = cssMenu.replaceAll('$skipToId', '#' + skipToId); + + let cssHighlight = cssHighlightTemplate.textContent.slice(0); + cssHighlight = cssHighlight.replaceAll('$skipToId', '#' + skipToId); + + [cssMenu, cssHighlight] = addCSSColors(cssMenu, cssHighlight, config, useURLTheme); + + + let styleNode = attachNode.querySelector(`#${skipToMenuStyleID}`); + if (!styleNode) { + styleNode = document.createElement('style'); + attachNode.appendChild(styleNode); + styleNode.setAttribute('id', `${skipToMenuStyleID}`); + } + styleNode.textContent = cssMenu; + + const headNode = document.querySelector('head'); + if (headNode) { + let highlightStyleNode = headNode.querySelector(`#${skipToHighlightStyleID}`); + if (!highlightStyleNode) { + highlightStyleNode = document.createElement('style'); + headNode.appendChild(highlightStyleNode); + highlightStyleNode.setAttribute('id', `${skipToHighlightStyleID}`); + } + highlightStyleNode.textContent = cssHighlight; + } + } /* utils.js */ /* Constants */ - const debug$5 = new DebugLogging('Utils', false); - debug$5.flag = false; + const debug$b = new DebugLogging('Utils', false); + debug$b.flag = false; /* @@ -656,136 +1013,862 @@ $skipToId [role="menuitem"].hover .label { return !isDisplayNone(element); } - /* - * namefrom.js - */ + /* shortcutInfoDialog.js */ - /* constants */ + /* Constants */ + const debug$a = new DebugLogging('[shortcutsInfoDialog]', false); + debug$a.flag = false; - const debug$4 = new DebugLogging('nameFrom', false); - debug$4.flag = false; + const defaultStyleOptions$1 = { + fontFamily: 'sans-serif', + fontSize: '12pt', + focusBorderColor: '#c5050c', + focusBorderDarkColor: '#ffffff', - // - // LOW-LEVEL HELPER FUNCTIONS (NOT EXPORTED) + // Dialog styling defaults + dialogTextColor: '#000000', + dialogTextDarkColor: '#ffffff', + dialogBackgroundColor: '#ffffff', + dialogBackgroundDarkColor: '#000000', + dialogBackgroundTitleColor: '#eeeeee', + dialogBackgroundTitleDarkColor: '#013c93', - /* - * @function isDisplayNone - * - * @desc Returns true if the element or parent element has set the CSS - * display property to none or has the hidden attribute, - * otherwise false - * - * @param {Object} node - a DOM node - * - * @returns {Boolean} see @desc - */ + }; - function isDisplayNone (node) { - if (!node) { - return false; + const MORE_PAGE_INFO_URL='https://skipto-landmarks-headings.github.io/page-script-5/page.html'; + const MORE_SHORTCUT_INFO_URL='https://skipto-landmarks-headings.github.io/page-script-5/shortcuts.html'; + + const styleTemplate$1 = document.createElement('template'); + styleTemplate$1.textContent = ` +/* infoDialog.css */ + +dialog#skip-to-info-dialog { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%,-50%); + font-family: $fontFamily; + font-size: $fontSize; + max-width: 70%; + margin: 0; + padding: 0; + background-color: light-dark($dialogBackgroundColor, $dialogBackgroundDarkColor); + color: light-dark($dialogTextColor, $dialogTextDarkColor); + border-width: 2px; + border-style: solid; + border-color: light-dark($focusBorderColor, $focusBorderDarkColor); + border-radius: 5px; + z-index: 2000001; + +} + +dialog#skip-to-info-dialog .header { + margin: 0; + margin-bottom: 0.5em; + padding: 4px; + border-width: 0; + border-bottom-width: 1px; + border-style: solid; + border-color: light-dark($focusBorderColor, $focusBorderDarkColor); + border-top-left-radius: 5px; + border-top-right-radius: 5px; + font-weight: bold; + background-color: light-dark($dialogBackgroundTitleColor, $dialogBackgroundTitleDarkColor); + color: light-dark($dialogTextColor, $dialogTextDarkColor); + position: relative; + font-size: 100%; +} + +dialog#skip-to-info-dialog .header h2 { + margin: 0; + padding: 0; + font-size: 1em; +} + +dialog#skip-to-info-dialog .header button { + position: absolute; + top: -0.25em; + right: 0; + border: none; + background: transparent; + font-weight: bold; + color: light-dark(black, white); +} + +dialog#skip-to-info-dialog .content { + margin-left: 2em; + margin-right: 2em; + margin-top: 0; + margin-bottom: 2em; +} + +dialog#skip-to-info-dialog .content .desc { + max-width: 20em; +} + +dialog#skip-to-info-dialog .content .happy { + margin-top: 0.5em; + text-align: center; + font-family: fantasy, cursive; + font-size: 1.25em; + font-weight: bold; + font-style: italic; + letter-spacing: 0.05em; +} + + +dialog#skip-to-info-dialog .content .version, +dialog#skip-to-info-dialog .content .copyright { + margin-top: 0.5em; + text-align: center; + font-weight: bold; + font-size: 90%; +} + +dialog#skip-to-info-dialog .content table { + width: auto; +} + +dialog#skip-to-info-dialog .content caption { + margin: 0; + padding: 0; + margin-top: 1em; + text-align: left; + font-weight: bold; + font-size: 110%; +} + +dialog#skip-to-info-dialog .content th { + margin: 0; + padding: 0; + padding-top: 0.125em; + padding-buttom: 0.125em; + text-align: left; + font-weight: bold; + font-size: 100%; +} + +dialog#skip-to-info-dialog .content th { + border-bottom-width: 1px; + border-bottom-style: solid; + border-bottom-color: light-dark(#999999, #777777); +} + +dialog#skip-to-info-dialog .content th.shortcut { + width: 2.5em; +} + +dialog#skip-to-info-dialog .content td { + margin: 0; + padding: 0; + padding-top: 0.125em; + padding-buttom: 0.125em; + text-align: left; + font-size: 100%; +} + + +dialog#skip-to-info-dialog .content table tr:nth-child(even) { + background-color: light-dark(#eeeeee, #111111); +} + +dialog#skip-to-info-dialog .buttons { + float: right; + margin-right: 0.5em; + margin-bottom: 0.5em; +} + +dialog#skip-to-info-dialog button { + margin: 6px; +} + +dialog#skip-to-info-dialog .buttons button { + min-width: 5em; +} + +button:focus { + outline: 2px solid currentColor; + outline-offset: 2px; +} + +button:hover { + cursor: pointer; +} +`; + + class SkipToContentInfoDialog extends HTMLElement { + constructor () { + + super(); + this.attachShadow({ mode: 'open' }); + + // Get references + + this.infoDialog = document.createElement('dialog'); + this.infoDialog.id = 'skip-to-info-dialog'; + this.shadowRoot.appendChild(this.infoDialog); + + const headerElem = document.createElement('div'); + headerElem.className = 'header'; + this.infoDialog.appendChild(headerElem); + + this.h2Elem = document.createElement('h2'); + this.h2Elem.textContent = 'Keyboard Shortcuts'; + headerElem.appendChild(this.h2Elem); + + this.closeButton1 = document.createElement('button'); + this.closeButton1.textContent = '✕'; + headerElem.appendChild(this.closeButton1); + this.closeButton1.addEventListener('click', this.onCloseButtonClick.bind(this)); + this.closeButton1.addEventListener('keydown', this.onKeyDown.bind(this)); + + this.contentElem = document.createElement('div'); + this.contentElem.className = 'content'; + this.infoDialog.appendChild(this.contentElem); + + const buttonsElem = document.createElement('div'); + buttonsElem.className = 'buttons'; + this.infoDialog.appendChild(buttonsElem); + + this.moreInfoButton = document.createElement('button'); + this.moreInfoButton.textContent = 'More Information'; + buttonsElem.appendChild(this.moreInfoButton); + this.moreInfoButton.addEventListener('click', this.onMoreInfoClick.bind(this)); + + this.closeButton2 = document.createElement('button'); + this.closeButton2.textContent = 'Close'; + buttonsElem.appendChild(this.closeButton2); + this.closeButton2.addEventListener('click', this.onCloseButtonClick.bind(this)); + this.closeButton2.addEventListener('keydown', this.onKeyDown.bind(this)); + + this.moreInfoURL = ''; + } - if (node.nodeType === Node.TEXT_NODE) { - node = node.parentNode; + onCloseButtonClick () { + this.infoDialog.close(); } - if (node.nodeType === Node.ELEMENT_NODE) { + openDialog () { + this.infoDialog.showModal(); + this.closeButton2.focus(); + } - if (node.hasAttribute('hidden')) { - return true; + onMoreInfoClick () { + if (this.moreInfoURL) { + window.open(this.moreInfoURL, '_blank').focus(); } + } - // aria-hidden attribute with the value "true" is an same as - // setting the hidden attribute for name calcuation - if (node.hasAttribute('aria-hidden')) { - if (node.getAttribute('aria-hidden').toLowerCase() === 'true') { - return true; + configureStyle(config={}) { + + function updateOption(style, option, configOption, defaultOption) { + if (configOption) { + return style.replaceAll(option, configOption); + } + else { + return style.replaceAll(option, defaultOption); } } - const style = window.getComputedStyle(node, null); + // make a copy of the template + let style = styleTemplate$1.textContent.slice(0); - const display = style.getPropertyValue("display"); + style = updateOption(style, + '$fontFamily', + config.fontFamily, + defaultStyleOptions$1.fontFamily); - if (display) { - return display === 'none'; - } - } - return false; - } + style = updateOption(style, + '$fontSize', + config.fontSize, + defaultStyleOptions$1.fontSize); - /* - * @function isVisibilityHidden - * - * @desc Returns true if the node (or it's parrent) has the CSS visibility - * property set to "hidden" or "collapse", otherwise false - * - * @param {Object} node - DOM node - * - * @return see @desc - */ + style = updateOption(style, + '$focusBorderColor', + config.focusBorderColor, + defaultStyleOptions$1.focusBorderColor); - function isVisibilityHidden(node) { + style = updateOption(style, + '$focusBorderDarkColor', + config.focusBorderDarkColor, + defaultStyleOptions$1.focusBorderDarkColor); - if (!node) { - return false; - } + style = updateOption(style, + '$dialogTextColor', + config.dialogTextColor, + defaultStyleOptions$1.dialogTextColor); - if (node.nodeType === Node.TEXT_NODE) { - node = node.parentNode; - } + style = updateOption(style, + '$dialogextDarkColor', + config.dialogextDarkColor, + defaultStyleOptions$1.dialogextDarkColor); - if (node.nodeType === Node.ELEMENT_NODE) { - const style = window.getComputedStyle(node, null); + style = updateOption(style, + '$dialogBackgroundColor', + config.dialogBackgroundColor, + defaultStyleOptions$1.dialogBackgroundColor); - const visibility = style.getPropertyValue("visibility"); - if (visibility) { - return (visibility === 'hidden') || (visibility === 'collapse'); + style = updateOption(style, + '$dialogBackgroundDarkColor', + config.dialogBackgroundDarkColor, + defaultStyleOptions$1.dialogBackgroundDarkColor); + + style = updateOption(style, + '$dialogBackgroundTitleColor', + config.dialogBackgroundTitleColor, + defaultStyleOptions$1.dialogBackgroundTitleColor); + + style = updateOption(style, + '$dialogBackgroundTitleDarkColor', + config.dialogBackgroundTitleDarkColor, + defaultStyleOptions$1.dialogBackgroundTitleDarkColor); + + + let styleNode = this.shadowRoot.querySelector('style'); + + if (styleNode) { + styleNode.remove(); } + + styleNode = document.createElement('style'); + styleNode.textContent = style; + this.shadowRoot.appendChild(styleNode); + } - return false; - } - /* - * @function isAriaHiddenFalse - * - * @desc Returns true if the node has the aria-hidden property set to - * "false", otherwise false. - * NOTE: This function is important in the accessible namce - * calculation, since content hidden with a CSS technique - * can be included in the accessible name calculation when - * aria-hidden is set to false - * - * @param {Object} node - DOM node - * - * @return see @desc - */ - function isAriaHIddenFalse(node) { + updateShortcutContent (config) { - if (!node) { - return false; - } + while (this.contentElem.lastElementChild) { + this.contentElem.removeChild(this.contentElem.lastElementChild); + } - if (node.nodeType === Node.TEXT_NODE) { - node = node.parentNode; - } + this.moreInfoURL = MORE_SHORTCUT_INFO_URL; - if (node.nodeType === Node.ELEMENT_NODE) { - return (node.hasAttribute('aria-hidden') && - (node.getAttribute('aria-hidden').toLowerCase() === 'false')); - } + this.h2Elem.textContent = config.shortcutsInfoLabel; + this.closeButton1.setAttribute('aria-label', config.closeLabel); + this.closeButton2.textContent = config.closeLabel; + this.moreInfoButton.textContent = config.moreInfoLabel; - return false; - } + function addRow(tbodyElem, shortcut, desc) { - /* - * @function includeContentInName - * - * @desc Checks the CSS display and hidden properties, and - * the aria-hidden property to see if the content - * should be included in the accessible name + const trElem = document.createElement('tr'); + tbodyElem.appendChild(trElem); + + const tdElem1 = document.createElement('td'); + tdElem1.className = 'shortcut'; + tdElem1.textContent = shortcut; + trElem.appendChild(tdElem1); + + const tdElem2 = document.createElement('td'); + tdElem2.className = 'desc'; + tdElem2.textContent = desc; + trElem.appendChild(tdElem2); + } + + // Regions + + const tableElem1 = document.createElement('table'); + this.contentElem.appendChild(tableElem1); + + const captionElem1 = document.createElement('caption'); + captionElem1.textContent = config.landmarkGroupLabel; + tableElem1.appendChild(captionElem1); + + const theadElem1 = document.createElement('thead'); + tableElem1.appendChild(theadElem1); + + const trElem1 = document.createElement('tr'); + theadElem1.appendChild(trElem1); + + const thElem1 = document.createElement('th'); + thElem1.className = 'shortcut'; + thElem1.textContent = config.msgKey; + trElem1.appendChild(thElem1); + + const thElem2 = document.createElement('th'); + thElem2.className = 'desc'; + thElem2.textContent = config.msgDescription; + trElem1.appendChild(thElem2); + + const tbodyElem1 = document.createElement('tbody'); + tableElem1.appendChild(tbodyElem1); + + addRow(tbodyElem1, config.shortcutRegionNext, config.msgNextRegion); + addRow(tbodyElem1, config.shortcutRegionPrevious, config.msgPreviousRegion); + addRow(tbodyElem1, config.shortcutRegionMain, config.msgMainRegions); + addRow(tbodyElem1, config.shortcutRegionNavigation, config.msgNavigationRegions); + addRow(tbodyElem1, config.shortcutRegionComplementary, config.msgComplementaryRegions); + + // Headings + + const tableElem2 = document.createElement('table'); + this.contentElem.appendChild(tableElem2); + + const captionElem2 = document.createElement('caption'); + captionElem2.textContent = config.headingGroupLabel; + tableElem2.appendChild(captionElem2); + + const theadElem2 = document.createElement('thead'); + tableElem2.appendChild(theadElem2); + + const trElem2 = document.createElement('tr'); + theadElem2.appendChild(trElem2); + + const thElem3 = document.createElement('th'); + thElem3.className = 'shortcut'; + thElem3.textContent = config.msgKey; + trElem2.appendChild(thElem3); + + const thElem4 = document.createElement('th'); + thElem4.className = 'desc'; + thElem4.textContent = config.msgDescription; + trElem2.appendChild(thElem4); + + const tbodyElem2 = document.createElement('tbody'); + tableElem2.appendChild(tbodyElem2); + + addRow(tbodyElem2, config.shortcutHeadingNext, config.msgNextHeading); + addRow(tbodyElem2, config.shortcutHeadingPrevious, config.msgPreviousHeading); + addRow(tbodyElem2, config.shortcutHeadingH1, config.msgH1Headings); + addRow(tbodyElem2, config.shortcutHeadingH2, config.msgH2Headings); + addRow(tbodyElem2, config.shortcutHeadingH3, config.msgH3Headings); + addRow(tbodyElem2, config.shortcutHeadingH4, config.msgH4Headings); + addRow(tbodyElem2, config.shortcutHeadingH5, config.msgH5Headings); + addRow(tbodyElem2, config.shortcutHeadingH6, config.msgH6Headings); + + } + + updateAboutContent (config) { + + while (this.contentElem.lastElementChild) { + this.contentElem.removeChild(this.contentElem.lastElementChild); + } + + this.moreInfoURL = MORE_PAGE_INFO_URL; + + this.h2Elem.textContent = config.aboutInfoLabel; + this.closeButton1.setAttribute('aria-label', config.closeLabel); + this.closeButton2.textContent = config.closeLabel; + this.moreInfoButton.textContent = config.moreInfoLabel; + + let divElem = document.createElement('div'); + divElem.className = 'desc'; + divElem.textContent = config.aboutDesc; + this.contentElem.appendChild(divElem); + + divElem = document.createElement('div'); + divElem.className = 'happy'; + divElem.textContent = config.aboutHappy; + this.contentElem.appendChild(divElem); + + divElem = document.createElement('div'); + divElem.className = 'version'; + divElem.textContent = config.aboutVersion; + this.contentElem.appendChild(divElem); + + divElem = document.createElement('div'); + divElem.className = 'copyright'; + divElem.textContent = config.aboutCopyright; + this.contentElem.appendChild(divElem); + + } + + onKeyDown (event) { + + if ((event.key === "Tab") && + !event.altKey && + !event.ctlKey && + !event.metaKey) { + + if (event.shiftKey && + (event.currentTarget === this.closeButton1)) { + this.closeButton2.focus(); + event.preventDefault(); + event.stopPropagation(); + } + + if (!event.shiftKey && + (event.currentTarget === this.closeButton2)) { + this.closeButton1.focus(); + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + + /* shortcutsMessage.js */ + + /* Constants */ + const debug$9 = new DebugLogging('[shortcutsMessage]', false); + debug$9.flag = false; + + const defaultStyleOptions = { + fontFamily: 'sans-serif', + fontSize: '12pt', + focusBorderColor: '#c5050c', + focusBorderDarkColor: '#ffffff', + + // Dialog styling defaults + dialogTextColor: '#000000', + dialogTextDarkColor: '#ffffff', + dialogBackgroundColor: '#ffffff', + dialogBackgroundDarkColor: '#000000', + dialogBackgroundTitleColor: '#eeeeee', + dialogBackgroundTitleDarkColor: '#013c93', + + }; + + const styleTemplate = document.createElement('template'); + styleTemplate.textContent = ` +/* shortcutsMessage.css */ +:root { + color-scheme: light dark; +} + +div#skip-to-message { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%,-50%); + + font-family: $fontFamily; + font-size: $fontSize; + max-width: 70%; + margin: 0; + padding: 0; + background-color: light-dark($dialogBackgroundColor, $dialogBackgroundDarkColor); + border: 2px solid light-dark($focusBorderColor, $focusBorderDarkColor); + border-radius: 5px; + color: light-dark($dialogTextColor, $dialogTextDarkColor); + z-index: 2000001; + opacity: 1; +} + +div#skip-to-message .header { + margin: 0; + padding: 4px; + border-bottom: 1px solid light-dark($focusBorderColor, $focusBorderDarkColor); + border-top-left-radius: 5px; + border-top-right-radius: 5px; + font-weight: bold; + background-color: light-dark($dialogBackgroundTitleColor, $dialogBackgroundTitleDarkColor); + color light-dark($dialogTextColor, $dialogTextDarkColor); + font-size: 100%; +} + +div#skip-to-message .content { + margin-left: 2em; + margin-right: 2em; + margin-top: 2em; + margin-bottom: 2em; + background-color: light-dark($dialogBackgroundColor, $dialogBackgroundDarkColor); + color: light-dark($dialogTextColor, $dialogTextDarkColor); + font-size: 110%; + text-algin: center; +} + +div#skip-to-message.hidden { + display: none; +} + +div#skip-to-message.show { + display: block; + opacity: 1; +} + +div#skip-to-message.fade { + opacity: 0; + transition: visibility 0s 1s, opacity 1s linear; +} + +@media (forced-colors: active) { + + div#skip-to-message { + background-color: Canvas; + color CanvasText; + border-color: AccentColor; + } + + div#skip-to-message .header { + background-color: Canvas; + color CanvasText; + } + + div#skip-to-message .content { + background-color: Canvas; + color: CanvasText; + } +} + +`; + + class ShortcutsMessage extends HTMLElement { + constructor () { + + super(); + this.attachShadow({ mode: 'open' }); + + // Get references + + this.messageDialog = document.createElement('div'); + this.messageDialog.id = 'skip-to-message'; + this.messageDialog.classList.add('hidden'); + this.shadowRoot.appendChild(this.messageDialog); + + const headerElem = document.createElement('div'); + headerElem.className = 'header'; + headerElem.textContent = 'SkipTo.js Message'; + this.messageDialog.appendChild(headerElem); + + this.contentElem = document.createElement('div'); + this.contentElem.className = 'content'; + this.messageDialog.appendChild(this.contentElem); + + this.timeoutShowID = false; + this.timeoutFadeID = false; + + } + + configureStyle(config={}) { + + function updateOption(style, option, configOption, defaultOption) { + if (configOption) { + return style.replaceAll(option, configOption); + } + else { + return style.replaceAll(option, defaultOption); + } + } + + // make a copy of the template + let style = styleTemplate.textContent.slice(0); + + style = updateOption(style, + '$fontFamily', + config.fontFamily, + defaultStyleOptions.fontFamily); + + style = updateOption(style, + '$fontSize', + config.fontSize, + defaultStyleOptions.fontSize); + + style = updateOption(style, + '$focusBorderColor', + config.focusBorderColor, + defaultStyleOptions.focusBorderColor); + + style = updateOption(style, + '$focusBorderDarkColor', + config.focusBorderDarkColor, + defaultStyleOptions.focusBorderDarkColor); + + + style = updateOption(style, + '$dialogTextColor', + config.dialogTextColor, + defaultStyleOptions.dialogTextColor); + + style = updateOption(style, + '$dialogTextDarkColor', + config.dialogTextDarkColor, + defaultStyleOptions.dialogTextDarkColor); + + style = updateOption(style, + '$dialogBackgroundColor', + config.dialogBackgroundColor, + defaultStyleOptions.dialogBackgroundColor); + + style = updateOption(style, + '$dialogBackgroundDarkColor', + config.dialogBackgroundDarkColor, + defaultStyleOptions.dialogBackgroundDarkColor); + + style = updateOption(style, + '$dialogBackgroundTitleColor', + config.dialogBackgroundTitleColor, + defaultStyleOptions.dialogBackgroundTitleColor); + + style = updateOption(style, + '$dialogBackgroundTitleDarkColor', + config.dialogBackgroundTitleDarkColor, + defaultStyleOptions.dialogBackgroundTitleDarkColor); + + let styleNode = this.shadowRoot.querySelector('style'); + + if (styleNode) { + styleNode.remove(); + } + + styleNode = document.createElement('style'); + styleNode.textContent = style; + this.shadowRoot.appendChild(styleNode); + + } + + close() { + this.messageDialog.classList.remove('show'); + this.messageDialog.classList.remove('fade'); + this.messageDialog.classList.add('hidden'); + } + + open(message) { + clearInterval(this.timeoutFadeID); + clearInterval(this.timeoutShowID); + this.messageDialog.classList.remove('hidden'); + this.messageDialog.classList.remove('fade'); + this.messageDialog.classList.add('show'); + this.contentElem.textContent = message; + + const msg = this; + + this.timeoutFadeID = setTimeout( () => { + msg.messageDialog.classList.add('fade'); + msg.messageDialog.classList.remove('show'); + }, 3000); + + this.timeoutShowID = setTimeout( () => { + msg.close(); + }, 4000); + + } + + } + + /* + * namefrom.js + */ + + /* constants */ + + const debug$8 = new DebugLogging('nameFrom', false); + debug$8.flag = false; + + // + // LOW-LEVEL HELPER FUNCTIONS (NOT EXPORTED) + + /* + * @function isDisplayNone + * + * @desc Returns true if the element or parent element has set the CSS + * display property to none or has the hidden attribute, + * otherwise false + * + * @param {Object} node - a DOM node + * + * @returns {Boolean} see @desc + */ + + function isDisplayNone (node) { + + if (!node) { + return false; + } + + if (node.nodeType === Node.TEXT_NODE) { + node = node.parentNode; + } + + if (node.nodeType === Node.ELEMENT_NODE) { + + if (node.hasAttribute('hidden')) { + return true; + } + + // aria-hidden attribute with the value "true" is an same as + // setting the hidden attribute for name calcuation + if (node.hasAttribute('aria-hidden')) { + if (node.getAttribute('aria-hidden').toLowerCase() === 'true') { + return true; + } + } + + const style = window.getComputedStyle(node, null); + + const display = style.getPropertyValue("display"); + + if (display) { + return display === 'none'; + } + } + return false; + } + + /* + * @function isVisibilityHidden + * + * @desc Returns true if the node (or it's parrent) has the CSS visibility + * property set to "hidden" or "collapse", otherwise false + * + * @param {Object} node - DOM node + * + * @return see @desc + */ + + function isVisibilityHidden(node) { + + if (!node) { + return false; + } + + if (node.nodeType === Node.TEXT_NODE) { + node = node.parentNode; + } + + if (node.nodeType === Node.ELEMENT_NODE) { + const style = window.getComputedStyle(node, null); + + const visibility = style.getPropertyValue("visibility"); + if (visibility) { + return (visibility === 'hidden') || (visibility === 'collapse'); + } + } + return false; + } + + /* + * @function isAriaHiddenFalse + * + * @desc Returns true if the node has the aria-hidden property set to + * "false", otherwise false. + * NOTE: This function is important in the accessible namce + * calculation, since content hidden with a CSS technique + * can be included in the accessible name calculation when + * aria-hidden is set to false + * + * @param {Object} node - DOM node + * + * @return see @desc + */ + + function isAriaHIddenFalse(node) { + + if (!node) { + return false; + } + + if (node.nodeType === Node.TEXT_NODE) { + node = node.parentNode; + } + + if (node.nodeType === Node.ELEMENT_NODE) { + return (node.hasAttribute('aria-hidden') && + (node.getAttribute('aria-hidden').toLowerCase() === 'false')); + } + + return false; + } + + /* + * @function includeContentInName + * + * @desc Checks the CSS display and hidden properties, and + * the aria-hidden property to see if the content + * should be included in the accessible name * calculation. Returns true if it should be * included, otherwise false * @@ -932,8 +2015,8 @@ $skipToId [role="menuitem"].hover .label { /* accName.js */ /* Constants */ - const debug$3 = new DebugLogging('accName', false); - debug$3.flag = false; + const debug$7 = new DebugLogging('accName', false); + debug$7.flag = false; /** * @fuction getAccessibleName @@ -1011,8 +2094,8 @@ $skipToId [role="menuitem"].hover .label { /* landmarksHeadings.js */ /* Constants */ - const debug$2 = new DebugLogging('landmarksHeadings', false); - debug$2.flag = false; + const debug$6 = new DebugLogging('landmarksHeadings', false); + debug$6.flag = false; const skipableElements = [ 'base', @@ -1052,6 +2135,14 @@ $skipToId [role="menuitem"].hover .label { 'section' ]; + const headingTags = [ + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6' + ]; let idIndex = 0; @@ -1153,16 +2244,16 @@ $skipToId [role="menuitem"].hover .label { } /* - * @function checkForLandmark + * @function checkForLandmarkRole * - * @desc Re=trns the lamdnark name if a landmark, otherwise an - * empty string + * @desc Returns the type of landmark region, + * otherwise an empty string * * @param {Object} element - DOM element node * * @returns {String} see @desc */ - function checkForLandmark (element) { + function checkForLandmarkRole (element) { if (element.hasAttribute('role')) { const role = element.getAttribute('role').toLowerCase(); if (allowedLandmarkSelectors.indexOf(role) >= 0) { @@ -1250,6 +2341,12 @@ $skipToId [role="menuitem"].hover .label { return targetNode; } } + else { + targetNode = transverseDOMForSkipToId(node); + if (targetNode) { + return targetNode; + } + } } else { targetNode = transverseDOMForSkipToId(node); if (targetNode) { @@ -1268,12 +2365,12 @@ $skipToId [role="menuitem"].hover .label { /** * @function findVisibleElement * - * @desc Returns the first isible decsendant DOM node that matches a set of element tag names + * @desc Returns the first visible descendant DOM node that matches a set of element tag names * * @param {node} startingNode - dom node to start search for element * @param {Array} tagNames - Array of tag names * - * @returns (node} Returns first descendmt element, if not found returns false + * @returns (node} Returns first descendant element, if not found returns false */ function findVisibleElement (startingNode, tagNames) { @@ -1281,6 +2378,7 @@ $skipToId [role="menuitem"].hover .label { var targetNode = null; for (let node = startingNode.firstChild; node !== null; node = node.nextSibling ) { if (node.nodeType === Node.ELEMENT_NODE) { + if (!isSkipableElement(node)) { // check for slotted content if (isSlotElement(node)) { @@ -1312,6 +2410,12 @@ $skipToId [role="menuitem"].hover .label { return targetNode; } } + else { + targetNode = transverseDOMForVisibleElement(node, targetTagName); + if (targetNode) { + return targetNode; + } + } } else { const tagName = node.tagName.toLowerCase(); if (tagName === targetTagName){ @@ -1351,20 +2455,37 @@ $skipToId [role="menuitem"].hover .label { */ function skipToElement(menuitem) { - let focusNode = false; - let scrollNode = false; let elem; - const searchSelectors = ['input', 'button', 'a']; - const navigationSelectors = ['a', 'input', 'button']; - const landmarkSelectors = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'section', 'article', 'p', 'li', 'a']; - const isLandmark = menuitem.classList.contains('landmark'); const isSearch = menuitem.classList.contains('skip-to-search'); const isNav = menuitem.classList.contains('skip-to-nav'); elem = queryDOMForSkipToId(menuitem.getAttribute('data-id')); + setItemFocus(elem, isLandmark, isSearch, isNav); + + } + + /* + * @function setItemFocus + * + * @desc Sets focus on the appropriate element + * + * @param {Object} elem - A target element + * @param {Boolean} isLandmark - True if item is a landmark, otherwise false + * @param {Boolean} isSearch - True if item is a search landmark, otherwise false + * @param {Boolean} isNav - True if item is a navigation landmark, otherwise false + */ + function setItemFocus(elem, isLandmark, isSearch, isNav) { + + let focusNode = false; + let scrollNode = false; + + const searchSelectors = ['input', 'button', 'a']; + const navigationSelectors = ['a', 'input', 'button']; + const landmarkSelectors = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'section', 'article', 'p', 'li', 'a']; + if (elem) { if (isSearch) { focusNode = findVisibleElement(elem, searchSelectors); @@ -1399,6 +2520,8 @@ $skipToId [role="menuitem"].hover .label { elem.scrollIntoView({block: 'center'}); } } + + } /* @@ -1435,6 +2558,9 @@ $skipToId [role="menuitem"].hover .label { function isMain (element) { const tagName = element.tagName.toLowerCase(); const role = element.hasAttribute('role') ? element.getAttribute('role').toLowerCase() : ''; + if ((role === 'presentation') || (role === 'none')) { + return false; + } return (tagName === 'main') || (role === 'main'); } @@ -1451,29 +2577,69 @@ $skipToId [role="menuitem"].hover .label { * @returns {Array} @see @desc */ function queryDOMForLandmarksAndHeadings (landmarkTargets, headingTargets, skiptoId) { + let headingInfo = []; let landmarkInfo = []; let targetLandmarks = getLandmarkTargets(landmarkTargets.toLowerCase()); let targetHeadings = getHeadingTargets(headingTargets.toLowerCase()); - let onlyInMain = headingTargets.includes('main'); + let onlyInMain = headingTargets.includes('main') || headingTargets.includes('main-only'); function transverseDOM(startingNode, doc, parentDoc=null, inMain = false) { - for (let node = startingNode.firstChild; node !== null; node = node.nextSibling ) { - if (node.nodeType === Node.ELEMENT_NODE) { - const tagName = node.tagName.toLowerCase(); - if ((targetLandmarks.indexOf(checkForLandmark(node)) >= 0) && - (node.id !== skiptoId)) { - landmarkInfo.push({ node: node, name: getAccessibleName(doc, node)}); + + function checkForLandmark(doc, node) { + const landmark = checkForLandmarkRole(node); + if (landmark && (node.id !== skiptoId)) { + const accName = getAccessibleName(doc, node); + node.setAttribute('data-skip-to-info', `landmark ${landmark}`); + node.setAttribute('data-skip-to-acc-name', accName); + + if ((targetLandmarks.indexOf(landmark) >= 0) ) { + landmarkInfo.push({ + node: node, + name: accName + }); } + } + } + + function checkForHeading(doc, node, inMain) { + const isHeadingRole = node.role ? node.role.toLowerCase() === 'heading' : false; + const hasAriaLevel = parseInt(node.ariaLevel) > 0; + const tagName = (isHeadingRole && hasAriaLevel) ? + `h${node.ariaLevel}` : + node.tagName.toLowerCase(); + const level = (isHeadingRole && hasAriaLevel) ? + node.ariaLevel : + headingTags.includes(tagName) ? + tagName.substring(1) : + ''; + if (headingTags.includes(tagName) || + (isHeadingRole && hasAriaLevel)) { + const accName = getAccessibleName(doc, node, true); + node.setAttribute('data-skip-to-info', `heading ${tagName}`); + node.setAttribute('data-skip-to-acc-name', accName); if (targetHeadings.indexOf(tagName) >= 0) { if (!onlyInMain || inMain) { - headingInfo.push({ node: node, name: getAccessibleName(doc, node, true)}); + headingInfo.push({ + node: node, + tagName: tagName, + level: level, + name: accName, + inMain: inMain + }); } } + } + } - if (isMain(node)) { - inMain = true; - } + for (let node = startingNode.firstChild; node !== null; node = node.nextSibling ) { + if (node.nodeType === Node.ELEMENT_NODE) { + + debug$6.flag && debug$6.log(`[transverseDOM][node]: ${node.tagName} isSlot:${isSlotElement(node)} isCustom:${isCustomElement(node)}`); + + checkForLandmark(doc, node); + checkForHeading(doc, node, inMain); + inMain = isMain(node) || inMain; if (!isSkipableElement(node)) { // check for slotted content @@ -1489,16 +2655,8 @@ $skipToId [role="menuitem"].hover .label { for (let i = 0; i < assignedNodes.length; i += 1) { const assignedNode = assignedNodes[i]; if (assignedNode.nodeType === Node.ELEMENT_NODE) { - const tagName = assignedNodes[i].tagName.toLowerCase(); - if (targetLandmarks.indexOf(checkForLandmark(assignedNode)) >= 0) { - landmarkInfo.push({ node: assignedNode, name: getAccessibleName(nameDoc, assignedNode)}); - } - - if (targetHeadings.indexOf(tagName) >= 0) { - if (!onlyInMain || inMain) { - headingInfo.push({ node: assignedNode, name: getAccessibleName(nameDoc, assignedNode, true)}); - } - } + checkForLandmark(nameDoc, assignedNode); + checkForHeading(nameDoc, assignedNode, inMain); if (slotContent) { transverseDOM(assignedNode, parentDoc, null, inMain); } else { @@ -1512,6 +2670,9 @@ $skipToId [role="menuitem"].hover .label { if (node.shadowRoot) { transverseDOM(node.shadowRoot, node.shadowRoot, doc, inMain); } + else { + transverseDOM(node, doc, parentDoc, inMain); + } } else { transverseDOM(node, doc, parentDoc, inMain); } @@ -1541,7 +2702,6 @@ $skipToId [role="menuitem"].hover .label { console.warn(`[skipTo.js]: no landmarks found on page`); } - return [landmarkInfo, headingInfo]; } @@ -1569,7 +2729,7 @@ $skipToId [role="menuitem"].hover .label { // If targets undefined, use default settings if (typeof headingTargets !== 'string') { console.warn(`[skipto.js]: Error in heading configuration`); - headingTargets = 'h1 h2'; + headingTargets = 'main-only h1 h2'; } const [landmarks, headings] = queryDOMForLandmarksAndHeadings(landmarkTargets, headingTargets, skiptoId); @@ -1588,30 +2748,33 @@ $skipToId [role="menuitem"].hover .label { * @returns see @desc */ function getHeadings (config, headings) { - let dataId, level; + let dataId; let headingElementsArr = []; for (let i = 0, len = headings.length; i < len; i += 1) { let heading = headings[i]; + let role = heading.node.getAttribute('role'); - if ((typeof role === 'string') && (role === 'presentation')) continue; - if (isVisible(heading.node) && isNotEmptyString(heading.node.innerHTML)) { + if ((typeof role === 'string') && + ((role === 'presentation') || role === 'none') + ) continue; + if (isVisible(heading.node) && isNotEmptyString(heading.node.textContent)) { if (heading.node.hasAttribute('data-skip-to-id')) { dataId = heading.node.getAttribute('data-skip-to-id'); } else { dataId = getSkipToIdIndex(); heading.node.setAttribute('data-skip-to-id', dataId); } - level = heading.node.tagName.substring(1); const headingItem = {}; headingItem.dataId = dataId.toString(); headingItem.class = 'heading'; headingItem.name = heading.name; headingItem.ariaLabel = headingItem.name + ', '; - headingItem.ariaLabel += config.headingLevelLabel + ' ' + level; - headingItem.tagName = heading.node.tagName.toLowerCase(); + headingItem.ariaLabel += config.headingLevelLabel + ' ' + heading.level; + headingItem.tagName = heading.tagName; headingItem.role = 'heading'; - headingItem.level = level; + headingItem.level = heading.level; + headingItem.inMain = heading.inMain; headingElementsArr.push(headingItem); incSkipToIdIndex(); } @@ -1672,14 +2835,14 @@ $skipToId [role="menuitem"].hover .label { /* * @function getLandmarkTargets * - * @desc Analyzes a configuration string for landamrk and tag names + * @desc Analyzes a configuration string for landmark and tag names * NOTE: This function is included to maximize compatibility - * with confiuguration strings that use CSS selectors + * with configuration strings that use CSS selectors * in previous versions of SkipTo * - * @param {String} targets - String with landamrk and/or tag names + * @param {String} targets - String with landmark and/or tag names * - * @returns {Array} A normailized array of landmark names based on target configuration + * @returns {Array} A normalized array of landmark names based on target configuration */ function getLandmarkTargets (targets) { let targetLandmarks = []; @@ -1690,7 +2853,8 @@ $skipToId [role="menuitem"].hover .label { if (targets.includes('search')) { targetLandmarks.push('search'); } - if (targets.includes('nav')) { + if (targets.includes('nav') || + targets.includes('navigation')) { targetLandmarks.push('navigation'); } if (targets.includes('complementary') || @@ -1727,6 +2891,7 @@ $skipToId [role="menuitem"].hover .label { * @returns {Array} see @desc */ function getLandmarks(config, landmarks) { + let allElements = []; let mainElements = []; let searchElements = []; let navElements = []; @@ -1742,7 +2907,9 @@ $skipToId [role="menuitem"].hover .label { } let role = landmark.node.getAttribute('role'); let tagName = landmark.node.tagName.toLowerCase(); - if ((typeof role === 'string') && (role === 'presentation')) continue; + if ((typeof role === 'string') && + ((role === 'presentation') || (role === 'none')) + ) continue; if (isVisible(landmark.node)) { if (!role) role = tagName; // normalize tagNames @@ -1772,10 +2939,6 @@ $skipToId [role="menuitem"].hover .label { tagName = 'search'; break; } - // if using ID for selectQuery give tagName as main - if (['aside', 'footer', 'form', 'header', 'main', 'nav', 'section', 'search'].indexOf(tagName) < 0) { - tagName = 'main'; - } if (landmark.node.hasAttribute('aria-roledescription')) { tagName = landmark.node.getAttribute('aria-roledescription').trim().replace(' ', '-'); } @@ -1794,6 +2957,8 @@ $skipToId [role="menuitem"].hover .label { landmarkItem.nestingLevel = 0; incSkipToIdIndex(); + allElements.push(landmarkItem); + // For sorting landmarks into groups switch (tagName) { case 'main': @@ -1823,262 +2988,1054 @@ $skipToId [role="menuitem"].hover .label { } } } + if (config.landmarks.includes('doc-order')) { + return allElements; + } return [].concat(mainElements, searchElements, navElements, asideElements, regionElements, footerElements, otherElements); } - /* skiptoMenuButton.js */ + /* highlight.js */ /* Constants */ - const debug$1 = new DebugLogging('SkipToButton', false); - debug$1.flag = false; + const debug$5 = new DebugLogging('highlight', false); + debug$5.flag = false; - /** - * @class SkiptoMenuButton + const minWidth = 68; + const minHeight = 27; + const offset = 6; + const borderWidth = 2; + + const OVERLAY_ID = 'id-skip-to-overlay'; + + /* + * @function getOverlayElement * - * @desc Constructor for creating a button to open a menu of headings and landmarks on - * a web page + * @desc Returns DOM node for the overlay element * - * @param {Object} attachNode - DOM eleemnt node to attach button and menu container element - * - * @returns {Object} DOM element node that is the contatiner for the button and the menu + * @returns {Object} see @desc */ - class SkiptoMenuButton { - - constructor (attachNode, config, id) { - this.config = config; - this.skiptoId = id; - this.containerNode = document.createElement(config.containerElement); - if (config.containerElement === 'nav') { - this.containerNode.setAttribute('aria-label', config.buttonLabel); - } + function getOverlayElement() { - this.containerNode.id = id; + let overlayElem = document.getElementById(OVERLAY_ID); - if (isNotEmptyString(config.customClass)) { - this.containerNode.classList.add(config.customClass); - } + if (!overlayElem) { + overlayElem = document.createElement('div'); + overlayElem.style.display = 'none'; + overlayElem.id = OVERLAY_ID; + document.body.appendChild(overlayElem); - let displayOption = config.displayOption; - if (typeof displayOption === 'string') { - displayOption = displayOption.trim().toLowerCase(); - if (displayOption.length) { - switch (config.displayOption) { - case 'fixed': - this.containerNode.classList.add('fixed'); - break; - case 'onfocus': // Legacy option - case 'popup': - this.containerNode.classList.add('popup'); - break; - } - } - } + const overlayElemChild = document.createElement('div'); + overlayElemChild.className = 'overlay-border'; + overlayElem.appendChild(overlayElemChild); + } - // Create button + const infoElem = overlayElem.querySelector('.overlay-info'); - const [buttonVisibleLabel, buttonAriaLabel] = this.getBrowserSpecificShortcut(config); + if (infoElem === null) { + const overlayInfoChild = document.createElement('div'); + overlayInfoChild.className = 'overlay-info'; + overlayElem.appendChild(overlayInfoChild); + } - this.buttonNode = document.createElement('button'); - this.buttonNode.setAttribute('aria-label', buttonAriaLabel); - this.buttonNode.addEventListener('keydown', this.handleButtonKeydown.bind(this)); - this.buttonNode.addEventListener('click', this.handleButtonClick.bind(this)); - this.containerNode.appendChild(this.buttonNode); + return overlayElem; + } - this.buttonTextNode = document.createElement('span'); - this.buttonTextNode.classList.add('skipto-text'); - this.buttonTextNode.textContent = buttonVisibleLabel; - this.buttonNode.appendChild(this.buttonTextNode); + /* + * @function isElementInViewport + * + * @desc Returns true if element is already visible in view port, + * otheriwse false + * + * @param {Object} element : DOM node of element to highlight + * + * @returns see @desc + */ - const smallButtonNode = document.createElement('span'); - smallButtonNode.classList.add('skipto-small'); - smallButtonNode.textContent = config.smallButtonLabel; - this.buttonNode.appendChild(smallButtonNode); + function isElementInViewport(element) { + var rect = element.getBoundingClientRect(); + return ( + rect.top >= window.screenY && + rect.left >= window.screenX && + rect.bottom <= ((window.screenY + window.innerHeight) || + (window.screenY + document.documentElement.clientHeight)) && + rect.right <= ((window.screenX + window.innerWidth) || + (window.screenX + document.documentElement.clientWidth)) + ); + } - const mediumButtonNode = document.createElement('span'); - mediumButtonNode.classList.add('skipto-medium'); - mediumButtonNode.textContent = config.buttonLabel; - this.buttonNode.appendChild(mediumButtonNode); + /* + * @function isElementStartInViewport + * + * @desc Returns true if start of the element is already visible in view port, + * otherwise false + * + * @param {Object} element : DOM node of element to highlight + * + * @returns see @desc + */ - // Create menu container + function isElementStartInViewport(element) { + var rect = element.getBoundingClientRect(); + return ( + rect.top >= window.screenY && + rect.top <= ((window.screenY + window.innerHeight) || + (window.screenY + document.documentElement.clientHeight)) && + rect.left >= window.screenX && + rect.left <= ((window.screenX + window.innerWidth) || + (window.screenX + document.documentElement.clientWidth)) + ); + } - this.menuNode = document.createElement('div'); - this.menuNode.id = 'id-skip-to-menu'; - this.menuNode.setAttribute('role', 'menu'); - this.menuNode.setAttribute('aria-label', config.menuLabel); - this.menuNode.setAttribute('aria-busy', 'true'); - this.containerNode.appendChild(this.menuNode); - const landmarkGroupLabelNode = document.createElement('div'); - landmarkGroupLabelNode.id = 'id-skip-to-menu-landmark-group-label'; - landmarkGroupLabelNode.setAttribute('role', 'separator'); - landmarkGroupLabelNode.textContent = this.config.landmarkGroupLabel; - this.menuNode.appendChild(landmarkGroupLabelNode); + /* + * @function isElementHeightLarge + * + * @desc Returns true if element client height is larger than clientHeight, + * otheriwse false + * + * @param {Object} element : DOM node of element to highlight + * + * @returns see @desc + */ - this.landmarkGroupNode = document.createElement('div'); - this.landmarkGroupNode.setAttribute('role', 'group'); - this.landmarkGroupNode.setAttribute('aria-labelledby', landmarkGroupLabelNode.id); - this.landmarkGroupNode.id = '#id-skip-to-menu-landmark-group'; - this.menuNode.appendChild(this.landmarkGroupNode); + function isElementInHeightLarge(element) { + var rect = element.getBoundingClientRect(); + return (1.2 * rect.height) > (window.innerHeight || document.documentElement.clientHeight); + } - const headingGroupLabelNode = document.createElement('div'); - headingGroupLabelNode.id = 'id-skip-to-menu-heading-group-label'; - headingGroupLabelNode.setAttribute('role', 'separator'); - headingGroupLabelNode.textContent = this.config.headingGroupLabel; - this.menuNode.appendChild(headingGroupLabelNode); + /* + * @function highlightElement + * + * @desc Highlights the element with the id on a page when highlighting + * is enabled (NOTE: Highlight is enabled by default) + * + * @param {Object} elem : DOM node of element to highlight + * @param {String} highlightTarget : value of highlight target + * @param {String} info : Information about target + * @param {Boolean} force : If true override isRduced + */ + function highlightElement(elem, highlightTarget, info='', force=false) { + const mediaQuery = window.matchMedia(`(prefers-reduced-motion: reduce)`); + const isReduced = !mediaQuery || mediaQuery.matches; - this.headingGroupNode = document.createElement('div'); - this.headingGroupNode.setAttribute('role', 'group'); - this.headingGroupNode.setAttribute('aria-labelledby', headingGroupLabelNode.id); - this.headingGroupNode.id = '#id-skip-to-menu-heading-group'; - this.menuNode.appendChild(this.headingGroupNode); + if (elem && highlightTarget) { - this.containerNode.addEventListener('focusin', this.handleFocusin.bind(this)); - this.containerNode.addEventListener('focusout', this.handleFocusout.bind(this)); - window.addEventListener('pointerdown', this.handleBackgroundPointerdown.bind(this), true); + const overlayElem = getOverlayElement(); + const scrollElement = updateOverlayElement(overlayElem, elem, info); - if (this.usesAltKey || this.usesOptionKey) { - document.addEventListener( - 'keydown', - this.handleDocumentKeydown.bind(this) - ); + if (isElementInHeightLarge(elem)) { + if (!isElementStartInViewport(elem) && (!isReduced || force)) { + scrollElement.scrollIntoView({ behavior: highlightTarget, block: 'start', inline: 'nearest' }); + } + } + else { + if (!isElementInViewport(elem) && (!isReduced || force)) { + scrollElement.scrollIntoView({ behavior: highlightTarget, block: 'center', inline: 'nearest' }); } + } + } + } + + /* + * @function removeHighlight + * + * @desc Hides the highlight element on the page + */ + function removeHighlight() { + const overlayElement = getOverlayElement(); + if (overlayElement) { + overlayElement.style.display = 'none'; + } + } - attachNode.insertBefore(this.containerNode, attachNode.firstElementChild); + /* + * @function updateOverlayElement + * + * @desc Create an overlay element and set its position on the page. + * + * @param {Object} overlayElem - DOM element for overlay + * @param {Object} element - DOM element node to highlight + * @param {String} info - Description of the element + * + */ - this.focusMenuitem = null; + function updateOverlayElement (overlayElem, element, info) { - return this.containerNode; + const childElem = overlayElem.firstElementChild; + const infoElem = overlayElem.querySelector('.overlay-info'); - } - - /* - * @method getBrowserSpecificShortcut - * - * @desc Identifies the operating system and updates labels for - * shortcut key to use either the "alt" or the "option" - * label - * - * @param {Object} - SkipTp configure object - * - * @return {Array} - An array of two strings used for the button label - */ - getBrowserSpecificShortcut (config) { - const platform = navigator.platform.toLowerCase(); - const userAgent = navigator.userAgent.toLowerCase(); + let rect = element.getBoundingClientRect(); - const hasWin = platform.indexOf('win') >= 0; - const hasMac = platform.indexOf('mac') >= 0; - const hasLinux = platform.indexOf('linux') >= 0 || platform.indexOf('bsd') >= 0; - const hasAndroid = userAgent.indexOf('android') >= 0; + let isHidden = false; - this.usesAltKey = hasWin || (hasLinux && !hasAndroid); - this.usesOptionKey = hasMac; - let label = config.buttonLabel; - let ariaLabel = config.buttonLabel; - let buttonShortcut; + const rectLeft = rect.left > offset ? + Math.round(rect.left - offset + window.scrollX) : + Math.round(rect.left + window.scrollX); - // Check to make sure a shortcut key is defined - if (config.altShortcut && config.optionShortcut) { - if (this.usesAltKey || this.usesOptionKey) { - buttonShortcut = config.buttonShortcut.replace( - '$key', - config.altShortcut - ); - } - if (this.usesAltKey) { - buttonShortcut = buttonShortcut.replace( - '$modifier', - config.altLabel - ); - label = label + buttonShortcut; - ariaLabel = config.altButtonAriaLabel.replace('$key', config.altShortcut); - } + let left = rectLeft; - if (this.usesOptionKey) { - buttonShortcut = buttonShortcut.replace( - '$modifier', - config.optionLabel - ); - label = label + buttonShortcut; - ariaLabel = config.optionButtonAriaLabel.replace('$key', config.altShortcut); - } - } - return [label, ariaLabel]; - } + const rectWidth = rect.left > offset ? + Math.max(rect.width + offset * 2, minWidth) : + Math.max(rect.width, minWidth); - /* - * @method getFirstChar - * - * @desc Gets the first character in a menuitem to use as a shortcut key - * - * @param {Object} menuitem - DOM element node - * - * @returns {String} see @desc - */ - getFirstChar(menuitem) { - const label = menuitem.querySelector('.label'); - if (label && isNotEmptyString(label.textContent)) { - return label.textContent.trim()[0].toLowerCase(); - } - return ''; - } + let width = rectWidth; - /* - * @method getHeadingLevelFromAttribute - * - * @desc Returns the the heading level of the menu item - * - * @param {Object} menuitem - DOM element node - * - * @returns {String} see @desc - */ - getHeadingLevelFromAttribute(menuitem) { - if (menuitem.hasAttribute('data-level')) { - return menuitem.getAttribute('data-level'); - } - return ''; - } + const rectTop = rect.top > offset ? + Math.round(rect.top - offset + window.scrollY) : + Math.round(rect.top + window.scrollY); - /* - * @method updateKeyboardShortCuts - * - * @desc Updates the keyboard short cuts for the curent menu items - */ - updateKeyboardShortCuts () { - let mi; - this.firstChars = []; - this.headingLevels = []; + let top = rectTop; - for(let i = 0; i < this.menuitemNodes.length; i += 1) { - mi = this.menuitemNodes[i]; - this.firstChars.push(this.getFirstChar(mi)); - this.headingLevels.push(this.getHeadingLevelFromAttribute(mi)); - } - } + const rectHeight = rect.top > offset ? + Math.max(rect.height + offset * 2, minHeight) : + Math.max(rect.height, minHeight); - /* - * @method updateMenuitems - * - * @desc Updates the menu information with the current menu items - * used for menu navigation commands - */ - updateMenuitems () { - let menuitemNodes = this.menuNode.querySelectorAll('[role=menuitem'); + let height = rectHeight; - this.menuitemNodes = []; - for(let i = 0; i < menuitemNodes.length; i += 1) { - this.menuitemNodes.push(menuitemNodes[i]); + if ((rect.height < 3) || (rect.width < 3)) { + isHidden = true; + } + + if ((rectTop < 0) || (rectLeft < 0)) { + isHidden = true; + if (element.parentNode) { + const parentRect = element.parentNode.getBoundingClientRect(); + + if ((parentRect.top > 0) && (parentRect.left > 0)) { + top = parentRect.top > offset ? + Math.round(parentRect.top - offset + window.scrollY) : + Math.round(parentRect.top + window.scrollY); + left = parentRect.left > offset ? + Math.round(parentRect.left - offset + window.scrollX) : + Math.round(parentRect.left + window.scrollX); + } + else { + left = offset; + top = offset; } + } + else { + left = offset; + top = offset; + } + } - this.firstMenuitem = this.menuitemNodes[0]; - this.lastMenuitem = this.menuitemNodes[this.menuitemNodes.length-1]; - this.lastMenuitem.classList.add('last'); - this.updateKeyboardShortCuts(); + overlayElem.style.left = left + 'px'; + overlayElem.style.top = top + 'px'; + + if (isHidden) { + childElem.textContent = 'Heading is hidden'; + childElem.classList.add('skip-to-hidden'); + overlayElem.style.width = 'auto'; + overlayElem.style.height = 'auto'; + childElem.style.width = 'auto'; + childElem.style.height = 'auto'; + height = childElem.getBoundingClientRect().height; + width = childElem.getBoundingClientRect().width; + if (rect.top > offset) { + height += offset + 2; + width += offset + 2; } + } + else { + childElem.textContent = ''; + childElem.classList.remove('skip-to-hidden'); + overlayElem.style.width = width + 'px'; + overlayElem.style.height = height + 'px'; + childElem.style.width = (width - 2 * borderWidth) + 'px'; + childElem.style.height = (height - 2 * borderWidth) + 'px'; + } - /* + overlayElem.style.display = 'block'; + + if (info) { + infoElem.style.display = 'inline-block'; + infoElem.textContent = info; + if (top >= infoElem.getBoundingClientRect().height) { + childElem.classList.remove('hasInfoBottom'); + infoElem.classList.remove('hasInfoBottom'); + childElem.classList.add('hasInfoTop'); + infoElem.classList.add('hasInfoTop'); + if (!isHidden) { + infoElem.style.top = (-1 * (height + infoElem.getBoundingClientRect().height - 2 * borderWidth)) + 'px'; + } + else { + infoElem.style.top = (-1 * (infoElem.getBoundingClientRect().height + childElem.getBoundingClientRect().height)) + 'px'; + } + } + else { + childElem.classList.remove('hasInfoTop'); + infoElem.classList.remove('hasInfoTop'); + childElem.classList.add('hasInfoBottom'); + infoElem.classList.add('hasInfoBottom'); + infoElem.style.top = -2 + 'px'; + } + return infoElem; + } + else { + childElem.classList.remove('hasInfo'); + infoElem.style.display = 'none'; + return overlayElem; + } + } + + /* shortcuts.js */ + + /* Constants */ + const debug$4 = new DebugLogging('shortcuts', false); + debug$4.flag = false; + + + /** + * @function monitorKeyboardFocus + * + * @desc Removes highlighting when keyboard focus changes + */ + function monitorKeyboardFocus () { + document.addEventListener('focusin', () => { + removeHighlight(); + }); + } + + /** + * @function navigateContent + * + * @desc Returns DOM node associated with the id, if id not found returns null + * + * @param {String} target - Feature to navigate (e.g. heading, landmark) + * @param {String} direction - 'next' or 'previous' + * @param {boolean} useFirst - if item not found use first + * @param {boolean} nameRequired - if true, item must have accessible name + */ + + function navigateContent (target, direction, msgHeadingLevel, useFirst=false, nameRequired=false) { + + const lastFocusElem = getFocusElement(); + let elem = lastFocusElem; + let lastElem; + let count = 0; + + // Note: The counter is used as a safety mechanism for any endless loops + + do { + lastElem = elem; + elem = queryDOMForSkipToNavigation(target, direction, elem, useFirst, nameRequired); + debug$4.flag && debug$4.log(`[navigateContent][elem]: ${elem} (${lastElem === elem})`); + if (elem) { + elem.tabIndex = elem.tabIndex >= 0 ? elem.tabIndex : -1; + elem.focus(); + } + count += 1; + } + while (elem && (count < 100) && (lastElem !== elem) && (lastFocusElem === getFocusElement())); + + // Set highlight + if (elem) { + + let info = elem.hasAttribute('data-skip-to-info') ? + elem.getAttribute('data-skip-to-info').replace('heading', '').replace('landmark', '').trim() : + 'unknown'; + + if (elem.getAttribute('data-skip-to-info').includes('heading')) { + info = msgHeadingLevel.replace('#', info.substring(1)); + } + + if (elem.hasAttribute('data-skip-to-acc-name')) { + const name = elem.getAttribute('data-skip-to-acc-name').trim(); + if (name) { + info += `: ${name}`; + } + } + + highlightElement(elem, 'instant', info, true); // force highlight since navigation + } + + return elem; + } + + /** + * @function queryDOMForSkipToNavigation + * + * @desc Returns DOM node associated with the id, if id not found returns null + * + * @param {String} target - Feature to navigate (e.g. heading, landmark) + * @param {String} direction - 'next' or 'previous' + * @param {Object} elem - Element the search needs to pass, if null used focused element + * @param {boolean} useFirst - if true, if item not found use first + * @param {boolean} nameRequired - if true, accessible name is required to include in navigation + * + * @returns {Object} @desc + */ + function queryDOMForSkipToNavigation (target, direction, elem, useFirst=false, nameRequired=false) { + + let lastNode = false; + let firstNode = false; + let passFound = false; + + const passElem = elem ? elem : getFocusElement(); + + function transverseDOMForElement(startingNode) { + + function checkForTarget (node) { + + if (node.hasAttribute('data-skip-to-info') && + node.getAttribute('data-skip-to-info').includes(target) && + ( !nameRequired || (nameRequired && + node.hasAttribute('data-skip-to-acc-name') && + node.getAttribute('data-skip-to-acc-name').trim().length > 0))) { + + if (target.includes('heading')) + + debug$4.flag && debug$4.log(`[checkForTarget][${node.tagName}]: ${node.textContent.trim().substring(0, 10)} (vis:${isVisible(node)} pf:${passFound})`); + + if (!firstNode && + isVisible(node)) { + debug$4.flag && debug$4.log(`[checkForTarget][firstNode]`); + firstNode = node; + } + + if ((node !== passElem) && + isVisible(node)) { + debug$4.flag && debug$4.log(`[checkForTarget][lastNode]`); + lastNode = node; + } + + if (passFound && + (direction === 'next') && + isVisible(node)) { + debug$4.flag && debug$4.log(`[checkForTarget][found]`); + return node; + } + } + + if (node === passElem) { + passFound = true; + debug$4.flag && debug$4.log(`[checkForTarget][passFound]: ${node.tagName}`); + if (direction === 'previous') { + return lastNode; + } + } + + return false; + } + + let targetNode = null; + for (let node = startingNode.firstChild; node !== null; node = node.nextSibling ) { + if (node.nodeType === Node.ELEMENT_NODE) { + + targetNode = checkForTarget(node); + if (targetNode) { + return targetNode; + } + + if (!isSkipableElement(node)) { + // check for slotted content + if (isSlotElement(node)) { + // if no slotted elements, check for default slotted content + const assignedNodes = node.assignedNodes().length ? + node.assignedNodes() : + node.assignedNodes({ flatten: true }); + for (let i = 0; i < assignedNodes.length; i += 1) { + const assignedNode = assignedNodes[i]; + if (assignedNode.nodeType === Node.ELEMENT_NODE) { + + targetNode = checkForTarget(assignedNode); + if (targetNode) { + return targetNode; + } + + targetNode = transverseDOMForElement(assignedNode); + if (targetNode) { + return targetNode; + } + } + } + } else { + // check for custom elements + if (isCustomElement(node)) { + if (node.shadowRoot) { + targetNode = transverseDOMForElement(node.shadowRoot); + if (targetNode) { + return targetNode; + } + } + else { + targetNode = transverseDOMForElement(node); + if (targetNode) { + return targetNode; + } + } + } else { + targetNode = transverseDOMForElement(node); + if (targetNode) { + return targetNode; + } + } + } + } + } // end if + } // end for + return false; + } // end function + + passFound = passElem === document.body; + let node = transverseDOMForElement(document.body); + + if (!node && useFirst && firstNode) { + node = firstNode; + } + + return node; + } + + /** + * @function getFocusElement + * + * @desc Returns DOM element node that has focus, if no DOM node + * has focus returns null + * + * @returns {Object} @desc + */ + function getFocusElement() { + + let elem = document.activeElement; + + while (elem.shadowRoot && elem.shadowRoot.activeElement) { + elem = elem.shadowRoot.activeElement; + } + return elem; + } + + /* keyboardHelper.js */ + + /* Constants */ + const debug$3 = new DebugLogging('[kbdHelpers]', false); + debug$3.flag = false; + + /* + * @method isInteractiveElement + * + * @desc Returns true if the element can use key presses, otherwise false + * + * @param {object} elem - DOM node element + * + * @returns {Boolean} see @desc + */ + + function elementTakesText (elem) { + + const enabledInputTypes = [ + 'button', + 'checkbox', + 'color', + 'image', + 'radio', + 'range', + 'reset', + 'submit' + ]; + + const tagName = elem.tagName ? elem.tagName.toLowerCase() : ''; + const type = tagName === 'input' ? + (elem.type ? elem.type.toLowerCase() : 'text') : + ''; + + debug$3.flag && debug$3.log(`[elementTakesText][type]: ${type} (${enabledInputTypes.includes(type)})`); + + return (tagName === 'select') || + (tagName === 'textarea') || + ((tagName === 'input') && + !enabledInputTypes.includes(type)) || + inContentEditable(elem); + } + + /* + * @function inContentEditable + * + * @desc Returns false if node is not in a content editable element, + * otherwise true if it does + * + * @param {Object} elem - DOM node + * + * @returns {Boolean} see @desc + */ + function inContentEditable (elem) { + let n = elem; + while (n.hasAttribute) { + if (n.hasAttribute('contenteditable') && + (n.getAttribute('contenteditable').toLowerCase().trim() !== 'false')) { + return true; + } + n = n.parentNode; + } + return false; + } + + /* + * @function noModifierPressed + * + * @desc Returns true if no modifier key is pressed, other false + * + * @param {Object} event - Event object + * + * @returns {Boolean} see @desc + */ + + function noModifierPressed (event) { + return !event.altKey && + !event.ctrlKey && + !event.shiftKey && + !event.metaKey; + } + + /* + * @function onlyShiftPressed + * + * @desc Returns true if only the shift modifier key is pressed, other false + * + * @param {Object} event - Event object + * + * @returns {Boolean} see @desc + */ + + function onlyShiftPressed (event) { + return !event.altKey && + !event.ctrlKey && + event.shiftKey && + !event.metaKey; + } + + /* + * @function onlyAltPressed + * + * @desc Returns true if only the alt modifier key is pressed, other false + * + * @param {Object} event - Event object + * + * @returns {Boolean} see @desc + */ + + function onlyAltPressed (event) { + return event.altKey && + !event.ctrlKey && + !event.shiftKey && + !event.metaKey; + } + + /* + * @function onlyOptionPressed + * + * @desc Returns true if only the option modifier key is pressed, other false + * + * @param {Object} event - Event object + * + * @returns {Boolean} see @desc + */ + + function onlyOptionPressed (event) { + return event.altKey && + !event.ctrlKey && + !event.shiftKey && + !event.metaKey; + } + + /* skiptoMenuButton.js */ + + /* Constants */ + const debug$2 = new DebugLogging('SkipToButton', false); + debug$2.flag = false; + + /** + * @class SkiptoMenuButton + * + * @desc Constructor for creating a button to open a menu of headings and landmarks on + * a web page + * + * @param {Object} skipToContentElem - The skip-to-content objecy + * + * @returns {Object} DOM element node that is the container for the button and the menu + */ + class SkiptoMenuButton { + + constructor (skipToContentElem) { + this.skipToContentElem = skipToContentElem; + this.config = skipToContentElem.config; + this.skiptoId = skipToContentElem.skipToId; + + // check for 'nav' element, if not use 'div' element + const ce = this.config.containerElement.toLowerCase().trim() === 'nav' ? 'nav' : 'div'; + + this.containerNode = document.createElement(ce); + skipToContentElem.shadowRoot.appendChild(this.containerNode); + + this.containerNode.id = this.skiptoId; + if (ce === 'nav') { + this.containerNode.setAttribute('aria-label', this.config.buttonLabel); + } + if (isNotEmptyString(this.config.customClass)) { + this.containerNode.classList.add(this.config.customClass); + } + + this.setDisplayOption(this.config.displayOption); + + // Create button + + const [buttonVisibleLabel, buttonAriaLabel] = this.getBrowserSpecificShortcut(this.config); + + this.buttonNode = document.createElement('button'); + this.buttonNode.setAttribute('aria-haspopup', 'menu'); + this.buttonNode.setAttribute('aria-expanded', 'false'); + this.buttonNode.setAttribute('aria-label', buttonAriaLabel); + this.buttonNode.setAttribute('aria-controls', 'id-skip-to-menu'); + this.buttonNode.addEventListener('keydown', this.handleButtonKeydown.bind(this)); + this.buttonNode.addEventListener('click', this.handleButtonClick.bind(this)); + this.containerNode.appendChild(this.buttonNode); + + this.textButtonNode = document.createElement('span'); + this.buttonNode.appendChild(this.textButtonNode); + this.textButtonNode.classList.add('skipto-text'); + this.textButtonNode.textContent = buttonVisibleLabel; + + this.smallButtonNode = document.createElement('span'); + this.buttonNode.appendChild(this.smallButtonNode); + this.smallButtonNode.classList.add('skipto-small'); + this.smallButtonNode.textContent = this.config.smallButtonLabel; + + this.mediumButtonNode = document.createElement('span'); + this.buttonNode.appendChild(this.mediumButtonNode); + this.mediumButtonNode.classList.add('skipto-medium'); + this.mediumButtonNode.textContent = this.config.buttonLabel; + + // Create menu container + this.menuitemNodes = []; + + this.menuNode = document.createElement('div'); + this.menuNode.setAttribute('id', 'id-skip-to-menu'); + this.menuNode.setAttribute('role', 'menu'); + this.menuNode.setAttribute('aria-label', this.config.menuLabel); + this.containerNode.appendChild(this.menuNode); + + this.landmarkGroupLabelNode = document.createElement('div'); + this.landmarkGroupLabelNode.setAttribute('id', 'id-skip-to-menu-landmark-group-label'); + this.landmarkGroupLabelNode.setAttribute('role', 'separator'); + this.landmarkGroupLabelNode.textContent = this.addNumberToGroupLabel(this.config.landmarkGroupLabel); + this.menuNode.appendChild(this.landmarkGroupLabelNode); + + this.landmarkGroupNode = document.createElement('div'); + this.landmarkGroupNode.setAttribute('id', 'id-skip-to-menu-landmark-group'); + this.landmarkGroupNode.setAttribute('role', 'group'); + this.landmarkGroupNode.className = 'overflow'; + this.landmarkGroupNode.setAttribute('aria-labelledby', 'id-skip-to-menu-landmark-group-label'); + this.menuNode.appendChild(this.landmarkGroupNode); + + this.headingGroupLabelNode = document.createElement('div'); + this.headingGroupLabelNode.setAttribute('id', 'id-skip-to-menu-heading-group-label'); + this.headingGroupLabelNode.setAttribute('role', 'separator'); + this.headingGroupLabelNode.textContent = this.addNumberToGroupLabel(this.config.headingGroupLabel); + this.menuNode.appendChild(this.headingGroupLabelNode); + + this.headingGroupNode = document.createElement('div'); + this.headingGroupNode.setAttribute('id', 'id-skip-to-menu-heading-group'); + this.headingGroupNode.setAttribute('role', 'group'); + this.headingGroupNode.className = 'overflow'; + this.headingGroupNode.setAttribute('aria-labelledby', 'id-skip-to-menu-heading-group-label'); + this.menuNode.appendChild(this.headingGroupNode); + + this.shortcutsGroupLabelNode = document.createElement('div'); + this.shortcutsGroupLabelNode.setAttribute('id', 'id-skip-to-menu-shortcuts-group-label'); + this.shortcutsGroupLabelNode.setAttribute('role', 'separator'); + if (this.config.shortcuts === 'enabled') { + this.shortcutsGroupLabelNode.textContent = this.config.shortcutsGroupEnabledLabel; + } + else { + this.shortcutsGroupLabelNode.textContent = this.config.shortcutsGroupDisabledLabel; + } + this.menuNode.appendChild(this.shortcutsGroupLabelNode); + + this.shortcutsGroupNode = document.createElement('div'); + this.shortcutsGroupNode.setAttribute('id', 'id-skip-to-menu-shortcuts-group'); + this.shortcutsGroupNode.setAttribute('role', 'group'); + this.shortcutsGroupNode.setAttribute('aria-labelledby', 'id-skip-to-menu-shortcutse-group-label'); + this.menuNode.appendChild(this.shortcutsGroupNode); + + if (this.config.aboutSupported === 'true') { + this.renderAboutToMenu(this.menuNode, this.config); + } + + this.infoDialog = document.querySelector("skip-to-content-info-dialog"); + + if (!this.infoDialog) { + window.customElements.define("skip-to-content-info-dialog", SkipToContentInfoDialog); + this.infoDialog = document.createElement('skip-to-content-info-dialog'); + this.infoDialog.configureStyle(this.config); + document.body.appendChild(this.infoDialog); + } + + this.shortcutsMessage = document.querySelector("skip-to-shortcuts-message"); + + if (!this.shortcutsMessage) { + window.customElements.define("skip-to-shortcuts-message", ShortcutsMessage); + this.shortcutsMessage = document.createElement('skip-to-shortcuts-message'); + this.shortcutsMessage.configureStyle(this.config); + document.body.appendChild(this.shortcutsMessage); + } + + this.containerNode.addEventListener('focusin', this.handleFocusin.bind(this)); + this.containerNode.addEventListener('focusout', this.handleFocusout.bind(this)); + this.containerNode.addEventListener('pointerdown', this.handleContinerPointerdown.bind(this), true); + document.documentElement.addEventListener('pointerdown', this.handleBodyPointerdown.bind(this), true); + + if (this.usesAltKey || this.usesOptionKey) { + document.addEventListener( + 'keydown', + this.handleDocumentKeydown.bind(this) + ); + } + + skipToContentElem.shadowRoot.appendChild(this.containerNode); + + this.focusMenuitem = null; + } + + /* + * @get highlightTarget + * + * @desc Returns normalized value for the highlightTarget option + */ + get highlightTarget () { + let value = this.config.highlightTarget.trim().toLowerCase(); + + if ('enabled smooth'.includes(value)) { + return 'smooth'; + } + + if (value === 'instant') { + return 'instant'; + } + + return ''; + } + + /* + * @method focusButton + * + * @desc Sets keyboard focus on the menu button + */ + focusButton() { + this.buttonNode.focus(); + this.skipToContentElem.setAttribute('focus', 'button'); + } + + /* + * @method addNumberToGroupLabel + * + * @desc Updates group label with the number of items in group, + * The '#' character in the string is replaced with the number + * if number is not provided, just remove number + * + * @param {String} label - Label to include number, + * @param {Number} num - Number to add to label + * + * @return {String} see @desc + */ + addNumberToGroupLabel(label, num=0) { + if (num > 0) { + return `${label} (${num})`; + } + return label; + } + + /* + * @method updateLabels + * + * @desc Updates labels, important for configuration changes in browser + * add-ons and extensions + */ + updateLabels(config) { + if (this.containerNode.hasAttribute('aria-label')) { + this.containerNode.setAttribute('aria-label', config.buttonLabel); + } + + const [buttonVisibleLabel, buttonAriaLabel] = this.getBrowserSpecificShortcut(config); + this.buttonNode.setAttribute('aria-label', buttonAriaLabel); + + this.textButtonNode.textContent = buttonVisibleLabel; + this.smallButtonNode.textContent = config.smallButtonLabel; + this.mediumButtonNode.textContent = config.buttonLabel; + + this.menuNode.setAttribute('aria-label', config.menuLabel); + this.landmarkGroupLabelNode.textContent = this.addNumberToGroupLabel(config.landmarkGroupLabel); + this.headingGroupLabelNode.textContent = this.addNumberToGroupLabel(config.headingGroupLabel); + } + + /* + * @method getBrowserSpecificShortcut + * + * @desc Identifies the operating system and updates labels for + * shortcut key to use either the "alt" or the "option" + * label + * + * @param {Object} - SkipTp configure object + * + * @return {Array} - An array of two strings used for the button label + */ + getBrowserSpecificShortcut (config) { + const platform = navigator.platform.toLowerCase(); + const userAgent = navigator.userAgent.toLowerCase(); + + const hasWin = platform.indexOf('win') >= 0; + const hasMac = platform.indexOf('mac') >= 0; + const hasLinux = platform.indexOf('linux') >= 0 || platform.indexOf('bsd') >= 0; + const hasAndroid = userAgent.indexOf('android') >= 0; + + this.usesAltKey = hasWin || (hasLinux && !hasAndroid); + this.usesOptionKey = hasMac; + + let label = config.buttonLabel; + let ariaLabel = config.buttonLabel; + let buttonShortcut; + + // Check to make sure a shortcut key is defined + if (config.altShortcut && config.optionShortcut) { + if (this.usesAltKey || this.usesOptionKey) { + buttonShortcut = config.buttonShortcut.replace( + '$key', + config.altShortcut + ); + } + if (this.usesAltKey) { + buttonShortcut = buttonShortcut.replace( + '$modifier', + config.altLabel + ); + label = label + buttonShortcut; + ariaLabel = config.buttonAriaLabel.replace('$key', config.altShortcut); + ariaLabel = ariaLabel.replace('$buttonLabel', config.buttonLabel); + ariaLabel = ariaLabel.replace('$modifierLabel', config.altLabel); + ariaLabel = ariaLabel.replace('$shortcutLabel', config.shortcutLabel); + } + + if (this.usesOptionKey) { + buttonShortcut = buttonShortcut.replace( + '$modifier', + config.optionLabel + ); + label = label + buttonShortcut; + ariaLabel = config.buttonAriaLabel.replace('$key', config.altShortcut); + ariaLabel = ariaLabel.replace('$buttonLabel', config.buttonLabel); + ariaLabel = ariaLabel.replace('$modifierLabel', config.optionLabel); + ariaLabel = ariaLabel.replace('$shortcutLabel', config.shortcutLabel); + } + } + return [label, ariaLabel]; + } + + /* + * @method getFirstChar + * + * @desc Gets the first character in a menuitem to use as a shortcut key + * + * @param {Object} menuitem - DOM element node + * + * @returns {String} see @desc + */ + getFirstChar(menuitem) { + const label = menuitem.querySelector('.label'); + if (label && isNotEmptyString(label.textContent)) { + return label.textContent.trim()[0].toLowerCase(); + } + return ''; + } + + /* + * @method getHeadingLevelFromAttribute + * + * @desc Returns the the heading level of the menu item + * + * @param {Object} menuitem - DOM element node + * + * @returns {String} see @desc + */ + getHeadingLevelFromAttribute(menuitem) { + if (menuitem.hasAttribute('data-level')) { + return menuitem.getAttribute('data-level'); + } + return ''; + } + + /* + * @method updateKeyboardShortCuts + * + * @desc Updates the keyboard short cuts for the curent menu items + */ + updateKeyboardShortCuts () { + let mi; + this.firstChars = []; + this.headingLevels = []; + + for(let i = 0; i < this.menuitemNodes.length; i += 1) { + mi = this.menuitemNodes[i]; + this.firstChars.push(this.getFirstChar(mi)); + this.headingLevels.push(this.getHeadingLevelFromAttribute(mi)); + } + } + + /* + * @method updateMenuitems + * + * @desc Updates the menu information with the current menu items + * used for menu navigation commands and adds event handlers + */ + updateMenuitems () { + let menuitemNodes = this.menuNode.querySelectorAll('[role=menuitem'); + + this.menuitemNodes = []; + for(let i = 0; i < menuitemNodes.length; i += 1) { + const menuitemNode = menuitemNodes[i]; + menuitemNode.addEventListener('keydown', this.handleMenuitemKeydown.bind(this)); + menuitemNode.addEventListener('click', this.handleMenuitemClick.bind(this)); + menuitemNode.addEventListener('pointerenter', this.handleMenuitemPointerenter.bind(this)); + menuitemNode.addEventListener('pointerleave', this.handleMenuitemPointerleave.bind(this)); + menuitemNode.addEventListener('pointerover', this.handleMenuitemPointerover.bind(this)); + this.menuitemNodes.push(menuitemNode); + } + + this.firstMenuitem = this.menuitemNodes[0]; + this.lastMenuitem = this.menuitemNodes[this.menuitemNodes.length-1]; + this.lastMenuitem.classList.add('last'); + this.updateKeyboardShortCuts(); + } + + /* + * @method renderAboutToMenu + * + * @desc Render the about menuitem + * + * @param {Object} menuNode - DOM element node for the menu + */ + renderAboutToMenu (menuNode, config) { + + const separatorNode = document.createElement('div'); + separatorNode.setAttribute('role', 'separator'); + + const menuitemNode = document.createElement('div'); + menuitemNode.setAttribute('role', 'menuitem'); + menuitemNode.setAttribute('data-about-info', ''); + menuitemNode.className = 'skip-to-nav skip-to-nesting-level-0'; + menuitemNode.tabIndex = -1; + + const labelNode = document.createElement('span'); + labelNode.classList.add('label'); + labelNode.textContent = config.aboutInfoLabel; + menuitemNode.appendChild(labelNode); + + menuNode.appendChild(separatorNode); + menuNode.appendChild(menuitemNode); + } + + /* * @method renderMenuitemToGroup * * @desc Renders a menuitem using an information object about the menuitem @@ -2101,11 +4058,7 @@ $skipToId [role="menuitem"].hover .label { menuitemNode.setAttribute('aria-label', mi.ariaLabel); } - // add event handlers - menuitemNode.addEventListener('keydown', this.handleMenuitemKeydown.bind(this)); - menuitemNode.addEventListener('click', this.handleMenuitemClick.bind(this)); - menuitemNode.addEventListener('pointerenter', this.handleMenuitemPointerenter.bind(this)); - menuitemNode.addEventListener('pointerleave', this.handleMenuitemPointerleave.bind(this)); + // add to group groupNode.appendChild(menuitemNode); // add heading level and label @@ -2158,7 +4111,11 @@ $skipToId [role="menuitem"].hover .label { * @param {String} msgNoItesmFound - Message to render if there are no menu items */ renderMenuitemsToGroup(groupNode, menuitems, msgNoItemsFound) { - groupNode.innerHTML = ''; + // remove all child nodes + while (groupNode.firstChild) { + groupNode.removeChild(groupNode.firstChild); + } + this.lastNestingLevel = 0; if (menuitems.length === 0) { @@ -2181,7 +4138,7 @@ $skipToId [role="menuitem"].hover .label { * * @desc */ - renderMenu() { + renderMenu(config, skipToId) { // remove landmark menu items while (this.landmarkGroupNode.lastElementChild) { this.landmarkGroupNode.removeChild(this.landmarkGroupNode.lastElementChild); @@ -2192,13 +4149,95 @@ $skipToId [role="menuitem"].hover .label { } // Create landmarks group - const [landmarkElements, headingElements] = getLandmarksAndHeadings(this.config, this.skiptoId); + const [landmarkElements, headingElements] = getLandmarksAndHeadings(config, skipToId); - this.renderMenuitemsToGroup(this.landmarkGroupNode, landmarkElements, this.config.msgNoLandmarksFound); - this.renderMenuitemsToGroup(this.headingGroupNode, headingElements, this.config.msgNoHeadingsFound); + this.renderMenuitemsToGroup(this.landmarkGroupNode, landmarkElements, config.msgNoLandmarksFound); + this.renderMenuitemsToGroup(this.headingGroupNode, headingElements, config.msgNoHeadingsFound); + debug$2.flag && debug$2.log(`[shortcutsSupported]: ${config.shortcutsSupported}`); + this.renderMenuitemsToShortcutsGroup(this.shortcutsGroupLabelNode, this.shortcutsGroupNode); // Update list of menuitems this.updateMenuitems(); + + // Are all headings in the main region + const allInMain = headingElements.length > 0 ? + headingElements.reduce( (flag, item) => { + return flag && item.inMain; + }, true) : + false; + + this.landmarkGroupLabelNode.textContent = this.addNumberToGroupLabel(config.landmarkGroupLabel, landmarkElements.length); + if (config.headings.includes('main') && allInMain) { + this.headingGroupLabelNode.textContent = this.addNumberToGroupLabel(config.headingMainGroupLabel, headingElements.length); + } + else { + this.headingGroupLabelNode.textContent = this.addNumberToGroupLabel(config.headingGroupLabel, headingElements.length); + } + } + + /* + * @method renderMenuitemsToShortcutsGroup + * + * @desc Updates separator and menuitems related to page navigation + * + * @param {Object} groupLabelNode - DOM element node for the label for page navigation group + * @param {Object} groupLabelNode - DOM element node for the page navigation group + */ + renderMenuitemsToShortcutsGroup (groupLabelNode, groupNode) { + + // remove page navigation menu items + while (groupNode.lastElementChild) { + groupNode.removeChild(groupNode.lastElementChild); + } + + if (this.config.shortcutsSupported === 'true') { + groupNode.classList.remove('shortcuts-disabled'); + groupLabelNode.classList.remove('shortcuts-disabled'); + + const shortcutsToggleNode = document.createElement('div'); + shortcutsToggleNode.setAttribute('role', 'menuitem'); + shortcutsToggleNode.className = 'shortcuts skip-to-nav skip-to-nesting-level-0'; + shortcutsToggleNode.setAttribute('tabindex', '-1'); + groupNode.appendChild(shortcutsToggleNode); + + const shortcutsToggleLabelNode = document.createElement('span'); + shortcutsToggleLabelNode.className = 'label'; + shortcutsToggleNode.appendChild(shortcutsToggleLabelNode); + + if (this.config.shortcuts === 'enabled') { + groupLabelNode.textContent = this.config.shortcutsGroupEnabledLabel; + shortcutsToggleNode.setAttribute('data-shortcuts-toggle', 'disable'); + shortcutsToggleLabelNode.textContent = this.config.shortcutsToggleDisableLabel; + } + else { + groupLabelNode.textContent = this.config.shortcutsGroupDisabledLabel; + shortcutsToggleNode.setAttribute('data-shortcuts-toggle', 'enable'); + shortcutsToggleLabelNode.textContent = this.config.shortcutsToggleEnableLabel; + } + groupNode.appendChild(shortcutsToggleNode); + + + const shortcutsInfoNode = document.createElement('div'); + shortcutsInfoNode.setAttribute('role', 'menuitem'); + shortcutsInfoNode.className = 'shortcuts skip-to-nav skip-to-nesting-level-0'; + shortcutsInfoNode.setAttribute('tabindex', '-1'); + shortcutsInfoNode.setAttribute('data-shortcuts-info', ''); + groupNode.appendChild(shortcutsInfoNode); + + const shortcutsInfoLabelNode = document.createElement('span'); + shortcutsInfoLabelNode.className = 'label'; + shortcutsInfoLabelNode.textContent = this.config.shortcutsInfoLabel; + shortcutsInfoNode.appendChild(shortcutsInfoLabelNode); + + + } + else { + groupNode.classList.add('shortcuts-disabled'); + groupLabelNode.classList.add('shortcuts-disabled'); + } + + + } // @@ -2214,10 +4253,18 @@ $skipToId [role="menuitem"].hover .label { */ setFocusToMenuitem(menuitem) { if (menuitem) { - this.removeHoverClass(); + this.removeHoverClass(menuitem); menuitem.classList.add('hover'); menuitem.focus(); + this.skipToContentElem.setAttribute('focus', 'menu'); this.focusMenuitem = menuitem; + if (menuitem.hasAttribute('data-id')) { + const elem = queryDOMForSkipToId(menuitem.getAttribute('data-id')); + highlightElement(elem, this.highlightTarget); + } + else { + removeHighlight(); + } } } @@ -2342,14 +4389,19 @@ $skipToId [role="menuitem"].hover .label { /* * @method openPopup * - * @desc Opens the memu of landmark regions and headings + * @desc Opens the menu of landmark regions and headings */ openPopup() { + debug$2.flag && debug$2.log(`[openPopup]`); this.menuNode.setAttribute('aria-busy', 'true'); - const h = (80 * window.innerHeight) / 100; - this.menuNode.style.maxHeight = h + 'px'; - this.renderMenu(); + // Compute height of menu to not exceed about 80% of screen height + const h = (30 * window.innerHeight) / 100; + this.landmarkGroupNode.style.maxHeight = h + 'px'; + this.headingGroupNode.style.maxHeight = h + 'px'; + this.renderMenu(this.config, this.skipToId); this.menuNode.style.display = 'block'; + + // make sure menu is on screen and not clipped in the right edge of the window const buttonRect = this.buttonNode.getBoundingClientRect(); const menuRect = this.menuNode.getBoundingClientRect(); const diff = window.innerWidth - buttonRect.left - menuRect.width - 8; @@ -2360,8 +4412,11 @@ $skipToId [role="menuitem"].hover .label { this.menuNode.style.left = diff + 'px'; } } + this.menuNode.removeAttribute('aria-busy'); this.buttonNode.setAttribute('aria-expanded', 'true'); + // use custom element attribute to set focus to the menu + this.skipToContentElem.setAttribute('focus', 'menu'); } /* @@ -2370,9 +4425,11 @@ $skipToId [role="menuitem"].hover .label { * @desc Closes the memu of landmark regions and headings */ closePopup() { + debug$2.flag && debug$2.log(`[closePopup]`); if (this.isOpen()) { this.buttonNode.setAttribute('aria-expanded', 'false'); this.menuNode.style.display = 'none'; + removeHighlight(); } } @@ -2392,20 +4449,124 @@ $skipToId [role="menuitem"].hover .label { * * @desc Removes hover class for menuitems */ - removeHoverClass() { + removeHoverClass(target=null) { this.menuitemNodes.forEach( node => { - node.classList.remove('hover'); + if (node !== target) { + node.classList.remove('hover'); + } }); } + /* + * @method getMenuitem + * + * @desc Returns menuitem dom node if pointer is over it + * + * @param {Number} x: client x coordinator of pointer + * @param {Number} y: client y coordinator of pointer + * + * @return {object} see @desc + */ + getMenuitem(x, y) { + for (let i = 0; i < this.menuitemNodes.length; i += 1) { + const node = this.menuitemNodes[i]; + const rect = node.getBoundingClientRect(); + + if ((rect.left <= x) && + (rect.right >= x) && + (rect.top <= y) && + (rect.bottom >= y)) { + return node; + } + } + return false; + } + + /* + * @method isOverButton + * + * @desc Returns true if pointer over button + * + * @param {Number} x: client x coordinator of pointer + * @param {Number} y: client y coordinator of pointer + * + * @return {object} see @desc + */ + isOverButton(x, y) { + const node = this.buttonNode; + const rect = node.getBoundingClientRect(); + + return (rect.left <= x) && + (rect.right >= x) && + (rect.top <= y) && + (rect.bottom >= y); + } + + /* + * @method isOverMenu + * + * @desc Returns true if pointer over the menu + * + * @param {Number} x: client x coordinator of pointer + * @param {Number} y: client y coordinator of pointer + * + * @return {object} see @desc + */ + isOverMenu(x, y) { + const node = this.menuNode; + const rect = node.getBoundingClientRect(); + + return (rect.left <= x) && + (rect.right >= x) && + (rect.top <= y) && + (rect.bottom >= y); + } + + /* + * @method setDisplayOption + * + * @desc Set display option for button visibility wehn it does not + * have focus + * + * @param {String} value - String with configuration information + */ + setDisplayOption(value) { + + if (typeof value === 'string') { + value = value.trim().toLowerCase(); + if (value.length && this.containerNode) { + + this.containerNode.classList.remove('static'); + this.containerNode.classList.remove('popup'); + this.containerNode.classList.remove('show-border'); + + switch (value) { + case 'static': + this.containerNode.classList.add('static'); + break; + case 'onfocus': // Legacy option + case 'popup': + this.containerNode.classList.add('popup'); + break; + case 'popup-border': + this.containerNode.classList.add('popup'); + this.containerNode.classList.add('show-border'); + break; + } + } + } + } + // Menu event handlers handleFocusin() { this.containerNode.classList.add('focus'); + this.skipToContentElem.setAttribute('focus', 'button'); } handleFocusout() { this.containerNode.classList.remove('focus'); + this.skipToContentElem.setAttribute('focus', 'none'); } handleButtonKeydown(event) { @@ -2424,6 +4585,7 @@ $skipToId [role="menuitem"].hover .label { case 'Escape': this.closePopup(); this.buttonNode.focus(); + this.skipToContentElem.setAttribute('focus', 'button'); flag = true; break; case 'Up': @@ -2439,61 +4601,159 @@ $skipToId [role="menuitem"].hover .label { } } - handleButtonClick(event) { - if (this.isOpen()) { - this.closePopup(); - this.buttonNode.focus(); - } else { - this.openPopup(); - this.setFocusToFirstMenuitem(); - } - event.stopPropagation(); - event.preventDefault(); - } + handleButtonClick(event) { + debug$2.flag && debug$2.log(`[handleButtonClick]`); + if (this.isOpen()) { + this.closePopup(); + this.buttonNode.focus(); + this.skipToContentElem.setAttribute('focus', 'button'); + } else { + this.openPopup(); + this.setFocusToFirstMenuitem(); + } + event.stopPropagation(); + event.preventDefault(); + } + + handleDocumentKeydown (event) { + + this.shortcutsMessage.close(); + + let flag = false; + let elem; + const focusElem = getFocusElement(); + debug$2.flag && debug$2.log(`[handleDocumentKeydown][elementTakesText][${event.target.tagName}]: ${elementTakesText(focusElem)}`); + if (!elementTakesText(focusElem)) { + + const altPressed = this.usesAltKey && onlyAltPressed(event); + const optionPressed = this.usesOptionKey && onlyOptionPressed(event); + + if ((optionPressed && this.config.optionShortcut === event.key) || + (altPressed && this.config.altShortcut === event.key) || + ((optionPressed || altPressed) && (48 === event.keyCode)) + ) { + this.openPopup(); + this.setFocusToFirstMenuitem(); + flag = true; + } + + // Check for navigation keys + if ((this.config.shortcuts === 'enabled') && + (onlyShiftPressed(event) || noModifierPressed(event))) { + + switch (event.key) { + // ignore and space characters + case ' ': + case '': + break; + + case this.config.shortcutRegionNext: + elem = navigateContent('landmark', 'next', this.config.msgHeadingLevel); + if (!elem) { + this.shortcutsMessage.open(this.config.msgNoMoreRegions); + } + flag = true; + break; + + case this.config.shortcutRegionPrevious: + elem = navigateContent('landmark', 'previous', this.config.msgHeadingLevel); + if (!elem) { + this.shortcutsMessage.open(this.config.msgNoMoreRegions); + } + flag = true; + break; + + case this.config.shortcutRegionComplementary: + elem = navigateContent('complementary', 'next', this.config.msgHeadingLevel, true); + if (!elem) { + this.shortcutsMessage.open(this.config.msgNoMoreRegions.replace('%r', 'complementary')); + } + flag = true; + break; + + case this.config.shortcutRegionMain: + elem = navigateContent('main', 'next', this.config.msgHeadingLevel, true); + if (!elem) { + this.shortcutsMessage.open(this.config.msgNoMoreRegions.replace('%r', 'main')); + } + flag = true; + break; + + case this.config.shortcutRegionNavigation: + elem = navigateContent('navigation', 'next', this.config.msgHeadingLevel, true); + if (!elem) { + this.shortcutsMessage.open(this.config.msgNoMoreRegions.replace('%r', 'navigation')); + } + flag = true; + break; + + case this.config.shortcutHeadingNext: + elem = navigateContent('heading', 'next', this.config.msgHeadingLevel, false, true); + if (!elem) { + this.shortcutsMessage.open(this.config.msgNoMoreHeadings); + } + flag = true; + break; + + case this.config.shortcutHeadingPrevious: + elem = navigateContent('heading', 'previous', this.config.msgHeadingLevel, false, true); + if (!elem) { + this.shortcutsMessage.open(this.config.msgNoMoreHeadings); + } + flag = true; + break; + + case this.config.shortcutHeadingH1: + elem = navigateContent('h1', 'next', this.config.msgHeadingLevel, true, true); + if (!elem) { + this.shortcutsMessage.open(this.config.msgNoHeadingsLevelFound.replace('%h', '1')); + } + flag = true; + break; - handleDocumentKeydown (event) { + case this.config.shortcutHeadingH2: + elem = navigateContent('h2', 'next', this.config.msgHeadingLevel, true, true); + if (!elem) { + this.shortcutsMessage.open(this.config.msgNoHeadingsLevelFound.replace('%h', '2')); + } + flag = true; + break; - const enabledInputTypes = [ - 'button', - 'checkbox', - 'color', - 'file', - 'image', - 'radio', - 'range', - 'reset', - 'submit' - ]; + case this.config.shortcutHeadingH3: + elem = navigateContent('h3', 'next', this.config.msgHeadingLevel, true, true); + if (!elem) { + this.shortcutsMessage.open(this.config.msgNoHeadingsLevelFound.replace('%h', '3')); + } + flag = true; + break; - const target = event.target; - const tagName = target.tagName ? target.tagName.toLowerCase() : ''; - const type = tagName === 'input' ? target.type.toLowerCase() : ''; - - if ((tagName !== 'textarea') && - ((tagName !== 'input') || - ((tagName === 'input') && enabledInputTypes.includes(type)) - )) { - - const altPressed = - this.usesAltKey && - event.altKey && - !event.ctrlKey && - !event.shiftKey && - !event.metaKey; - - const optionPressed = - this.usesOptionKey && - event.altKey && - !event.ctrlKey && - !event.shiftKey && - !event.metaKey; + case this.config.shortcutHeadingH4: + elem = navigateContent('h4', 'next', this.config.msgHeadingLevel, true, true); + if (!elem) { + this.shortcutsMessage.open(this.config.msgNoHeadingsLevelFound.replace('%h', '4')); + } + flag = true; + break; - if ((optionPressed && this.config.optionShortcut === event.key) || - (altPressed && this.config.altShortcut === event.key) || - ((optionPressed || altPressed) && (48 === event.keyCode)) - ) { - this.openPopup(); - this.setFocusToFirstMenuitem(); + case this.config.shortcutHeadingH5: + elem = navigateContent('h5', 'next', this.config.msgHeadingLevel, true, true); + if (!elem) { + this.shortcutsMessage.open(this.config.msgNoHeadingsLevelFound.replace('%h', '5')); + } + flag = true; + break; + + case this.config.shortcutHeadingH6: + elem = navigateContent('h6', 'next', this.config.msgHeadingLevel, true, true); + if (!elem) { + this.shortcutsMessage.open(this.config.msgNoHeadingsLevelFound.replace('%h', '6')); + } + flag = true; + break; + } + } + + if (flag) { event.stopPropagation(); event.preventDefault(); } @@ -2501,16 +4761,41 @@ $skipToId [role="menuitem"].hover .label { } handleMenuitemAction(tgt) { - switch (tgt.getAttribute('data-id')) { - case '': - // this means there were no headings or landmarks in the list - break; + if (tgt.hasAttribute('data-id')) { + switch (tgt.getAttribute('data-id')) { + case '': + // this means there were no headings or landmarks in the list + break; - default: - this.closePopup(); - skipToElement(tgt); - break; + default: + this.closePopup(); + skipToElement(tgt); + break; + } + } + + if (tgt.hasAttribute('data-shortcuts-toggle')) { + if (tgt.getAttribute('data-shortcuts-toggle') === 'enable') { + this.skipToContentElem.setAttribute('shortcuts', 'enable'); + } + else { + this.skipToContentElem.setAttribute('shortcuts', 'disable'); + } + this.closePopup(); + } + + if (tgt.hasAttribute('data-shortcuts-info')) { + this.infoDialog.updateShortcutContent(this.skipToContentElem.config); + this.infoDialog.openDialog(); + this.closePopup(); + } + + if (tgt.hasAttribute('data-about-info')) { + this.infoDialog.updateAboutContent(this.skipToContentElem.config); + this.infoDialog.openDialog(); + this.closePopup(); } + } handleMenuitemKeydown(event) { @@ -2530,8 +4815,9 @@ $skipToId [role="menuitem"].hover .label { flag = true; } if (event.key === 'Tab') { - this.buttonNode.focus(); this.closePopup(); + this.buttonNode.focus(); + this.skipToContentElem.setAttribute('focus', 'button'); flag = true; } } else { @@ -2545,13 +4831,18 @@ $skipToId [role="menuitem"].hover .label { case 'Escape': this.closePopup(); this.buttonNode.focus(); + this.skipToContentElem.setAttribute('focus', 'button'); flag = true; break; + case 'Left': + case 'ArrowLeft': case 'Up': case 'ArrowUp': this.setFocusToPreviousMenuitem(tgt); flag = true; break; + case 'ArrowRight': + case 'Right': case 'ArrowDown': case 'Down': this.setFocusToNextMenuitem(tgt); @@ -2591,53 +4882,161 @@ $skipToId [role="menuitem"].hover .label { } handleMenuitemPointerenter(event) { + debug$2.flag && debug$2.log(`[enter]`); let tgt = event.currentTarget; - this.removeHoverClass(); tgt.classList.add('hover'); + if (tgt.hasAttribute('data-id')) { + const elem = queryDOMForSkipToId(tgt.getAttribute('data-id')); + highlightElement(elem, this.highlightTarget); + } + else { + removeHighlight(); + } + event.stopPropagation(); + event.preventDefault(); + } + + handleMenuitemPointerover(event) { + debug$2.flag && debug$2.log(`[over]`); + let tgt = event.currentTarget; + if (tgt.hasAttribute('data-id')) { + const elem = queryDOMForSkipToId(tgt.getAttribute('data-id')); + highlightElement(elem, this.highlightTarget); + } + else { + removeHighlight(); + } + event.stopPropagation(); + event.preventDefault(); } handleMenuitemPointerleave(event) { + debug$2.flag && debug$2.log(`[leave]`); let tgt = event.currentTarget; tgt.classList.remove('hover'); + event.stopPropagation(); + event.preventDefault(); } - handleBackgroundPointerdown(event) { - if (!this.containerNode.contains(event.target)) { - if (this.isOpen()) { - this.closePopup(); - this.buttonNode.focus(); + handleContinerPointerdown(event) { + debug$2.flag && debug$2.log(`[down]: target: ${event.pointerId}`); + + if (this.isOverButton(event.clientX, event.clientY)) { + this.containerNode.releasePointerCapture(event.pointerId); + } + else { + this.containerNode.setPointerCapture(event.pointerId); + this.containerNode.addEventListener('pointermove', this.handleContinerPointermove.bind(this)); + this.containerNode.addEventListener('pointerup', this.handleContinerPointerup.bind(this)); + + if (this.containerNode.contains(event.target)) { + if (this.isOpen()) { + if (!this.isOverMenu(event.clientX, event.clientY)) { + debug$2.flag && debug$2.log(`[down][close]`); + this.closePopup(); + this.buttonNode.focus(); + this.skipToContentElem.setAttribute('focus', 'button'); + } + } + else { + debug$2.flag && debug$2.log(`[down][open]`); + this.openPopup(); + this.setFocusToFirstMenuitem(); + } + + } + } + + event.stopPropagation(); + event.preventDefault(); + } + + handleContinerPointermove(event) { + const mi = this.getMenuitem(event.clientX, event.clientY); + if (mi) { + this.removeHoverClass(mi); + mi.classList.add('hover'); + if (mi.hasAttribute('data-id')) { + const elem = queryDOMForSkipToId(mi.getAttribute('data-id')); + highlightElement(elem, this.highlightTarget); + } + else { + removeHighlight(); + } + } + + event.stopPropagation(); + event.preventDefault(); + } + + handleContinerPointerup(event) { + + this.containerNode.releasePointerCapture(event.pointerId); + this.containerNode.removeEventListener('pointermove', this.handleContinerPointermove); + this.containerNode.removeEventListener('pointerup', this.handleContinerPointerup); + + const mi = this.getMenuitem(event.clientX, event.clientY); + const omb = this.isOverButton(event.clientX, event.clientY); + debug$2.flag && debug$2.log(`[up] isOverButton: ${omb} getMenuitem: ${mi} id: ${event.pointerId}`); + + if (mi) { + this.handleMenuitemAction(mi); + } + else { + if (!omb) { + debug$2.flag && debug$2.log(`[up] not over button `); + if (this.isOpen()) { + debug$2.flag && debug$2.log(`[up] close `); + this.closePopup(); + this.buttonNode.focus(); + this.skipToContentElem.setAttribute('focus', 'button'); + } } } + + event.stopPropagation(); + event.preventDefault(); + } + + handleBodyPointerdown(event) { + debug$2.flag && debug$2.log(`[handleBodyPointerdown]: target: ${event.pointerId}`); + + if (!this.isOverButton(event.clientX, event.clientY) && + !this.isOverMenu(event.clientX, event.clientY)) { + this.closePopup(); + } } } + /* skiptoContent.js */ + /* constants */ - const debug = new DebugLogging('skipto', false); - debug.flag = true; + const debug$1 = new DebugLogging('skiptoContent', false); + debug$1.flag = false; - (function() { - const SkipTo = { - skipToId: 'id-skip-to', - domNode: null, - buttonNode: null, - menuNode: null, - menuitemNodes: [], - firstMenuitem: false, - lastMenuitem: false, - firstChars: [], - headingLevels: [], - skipToIdIndex: 1, + class SkipToContent572 extends HTMLElement { + + constructor() { + // Always call super first in constructor + super(); + this.attachShadow({ mode: 'open' }); + this.skipToId = 'id-skip-to'; + this.version = "5.7.2"; + this.buttonSkipTo = false; + this.initialized = false; + // Default configuration values - config: { + this.config = { // Feature switches enableHeadingLevelShortcuts: true, + focusOption: 'none', // used by extensions only + // Customization of button and menu altShortcut: '0', // default shortcut key is the number zero - optionShortcut: 'º', // default shortcut key character associated with option+0 on mac - attachElement: 'body', - displayOption: 'static', // options: static (default), popup, fixed + optionShortcut: 'º', // default shortcut key character associated with option+0 on mac + displayOption: '', // options: static, popup, fixed (default) // container element, use containerClass for custom styling containerElement: 'nav', containerRole: '', @@ -2648,14 +5047,75 @@ $skipToId [role="menuitem"].hover .label { smallButtonLabel: 'SkipTo', altLabel: 'Alt', optionLabel: 'Option', + shortcutLabel: 'shortcut', buttonShortcut: ' ($modifier+$key)', - altButtonAriaLabel: 'Skip To Content, shortcut Alt plus $key', - optionButtonAriaLabel: 'Skip To Content, shortcut Option plus $key', + buttonAriaLabel: '$buttonLabel, $shortcutLabel $modifierLabel + $key', + + // Page navigation flag and keys + shortcutsSupported: 'true', // options: true or false + shortcuts: 'enabled', // options: disabled and enabled + shortcutHeadingNext: 'h', + shortcutHeadingPrevious: 'H', + shortcutHeadingH1: '1', + shortcutHeadingH2: '2', + shortcutHeadingH3: '3', + shortcutHeadingH4: '4', + shortcutHeadingH5: '5', + shortcutHeadingH6: '6', + + shortcutRegionNext: 'r', + shortcutRegionPrevious: 'R', + shortcutRegionMain: 'm', + shortcutRegionNavigation: 'n', + shortcutRegionComplementary: 'c', + + shortcutsGroupEnabledLabel: 'Shortcuts: Enabled', + shortcutsGroupDisabledLabel: 'Shortcuts: Disabled', + shortcutsToggleEnableLabel: 'Enable shortcuts', + shortcutsToggleDisableLabel: 'Disable shortcuts', + shortcutsInfoLabel: 'Shortcut Information', + + aboutSupported: 'true', + aboutInfoLabel: `About SkipTo.js`, + aboutHappy: `Happy Skipping!`, + aboutVersion: `Version ${this.version}`, + aboutCopyright: 'BSD License, Copyright 2021-2025', + aboutDesc: 'SkipTo.js is a free and open source utility to support authors in implementing the WCAG 4.2.1 Bypass Block requirement on their websites.', + + closeLabel: 'Close', + moreInfoLabel: 'More Information', + msgKey: 'Key', + msgDescription: 'Description', + + msgNextRegion: 'Next region', + msgPreviousRegion: 'Previous region', + msgNextHeading: 'Next heading', + msgPreviousHeading: 'Previous heading', + + msgMainRegions: 'Main regions', + msgNavigationRegions: 'Navigation regions', + msgComplementaryRegions: 'Complementary regions', + + msgHeadingLevel: 'Level #', + msgH1Headings: 'Level 1 headings', + msgH2Headings: 'Level 2 headings', + msgH3Headings: 'Level 3 headings', + msgH4Headings: 'Level 4 headings', + msgH5Headings: 'Level 5 headings', + msgH6Headings: 'Level 6 headings', + + // Messages for navigation + + msgNoMoreRegions: 'No more regions', + msgNoRegionsFound: 'No %r regions found', + msgNoMoreHeadings: 'No more headings', + msgNoHeadingsLevelFound: 'No level %h headings found', // Menu labels and messages menuLabel: 'Landmarks and Headings', landmarkGroupLabel: 'Landmark Regions', headingGroupLabel: 'Headings', + headingMainGroupLabel: 'Headings in Main Region', headingLevelLabel: 'Heading level', mainLabel: 'main', searchLabel: 'search', @@ -2670,7 +5130,25 @@ $skipToId [role="menuitem"].hover .label { // Selectors for landmark and headings sections landmarks: 'main search navigation complementary', - headings: 'main h1 h2', + headings: 'main-only h1 h2', + + // Highlight options + highlightTarget: 'instant', // options: 'instant' (default), 'smooth' and 'auto' + + // Hidden heading when highlighting + msgHidden: 'Heading is hidden', + hiddenHeadingColor: '#000000', + hiddenHeadingDarkColor: '#000000', + hiddenHeadingBackgroundColor: '#ffcc00', + hiddenHeadingBackgroundDarkColor: '#ffcc00', + + //Dialog styling + dialogTextColor: '#000000', + dialogTextDarkColor: '#ffffff', + dialogBackgroundColor: '#ffffff', + dialogBackgroundDarkColor: '#000000', + dialogBackgroundTitleColor: '#eeeeee', + dialogBackgroundTitleDarkColor: '#013c93', // Place holders for configuration colorTheme: '', @@ -2686,236 +5164,419 @@ $skipToId [role="menuitem"].hover .label { focusBorderColor: '', buttonTextColor: '', buttonBackgroundColor: '', + menuTextDarkColor: '', + menuBackgroundDarkColor: '', + menuitemFocusTextDarkColor: '', + menuitemFocusBackgroundDarkColor: '', + focusBorderDarkColor: '', + buttonTextDarkColor: '', + buttonBackgroundDarkColor: '', zIndex: '', - }, - colorThemes: { - 'default': { - fontFamily: 'inherit', - fontSize: 'inherit', - positionLeft: '46%', - smallBreakPoint: '576', - mediumBreakPoint: '992', - menuTextColor: '#1a1a1a', - menuBackgroundColor: '#dcdcdc', - menuitemFocusTextColor: '#eeeeee', - menuitemFocusBackgroundColor: '#1a1a1a', - focusBorderColor: '#1a1a1a', - buttonTextColor: '#1a1a1a', - buttonBackgroundColor: '#eeeeee', - zIndex: '100000', - }, - 'aria': { - hostnameSelector: 'w3.org', - pathnameSelector: 'ARIA/apg', - fontFamily: 'sans-serif', - fontSize: '10pt', - positionLeft: '7%', - menuTextColor: '#000', - menuBackgroundColor: '#def', - menuitemFocusTextColor: '#fff', - menuitemFocusBackgroundColor: '#005a9c', - focusBorderColor: '#005a9c', - buttonTextColor: '#005a9c', - buttonBackgroundColor: '#ddd', - }, - 'illinois': { - hostnameSelector: 'illinois.edu', - menuTextColor: '#00132c', - menuBackgroundColor: '#cad9ef', - menuitemFocusTextColor: '#eeeeee', - menuitemFocusBackgroundColor: '#00132c', - focusBorderColor: '#ff552e', - buttonTextColor: '#444444', - buttonBackgroundColor: '#dddede', - }, - 'skipto': { - hostnameSelector: 'skipto-landmarks-headings.github.io', - fontSize: '14px', - menuTextColor: '#00132c', - menuBackgroundColor: '#cad9ef', - menuitemFocusTextColor: '#eeeeee', - menuitemFocusBackgroundColor: '#00132c', - focusBorderColor: '#ff552e', - buttonTextColor: '#444444', - buttonBackgroundColor: '#dddede', - }, - 'uic': { - hostnameSelector: 'uic.edu', - menuTextColor: '#001e62', - menuBackgroundColor: '#f8f8f8', - menuitemFocusTextColor: '#ffffff', - menuitemFocusBackgroundColor: '#001e62', - focusBorderColor: '#d50032', - buttonTextColor: '#ffffff', - buttonBackgroundColor: '#001e62', - }, - 'uillinois': { - hostnameSelector: 'uillinois.edu', - menuTextColor: '#001e62', - menuBackgroundColor: '#e8e9ea', - menuitemFocusTextColor: '#f8f8f8', - menuitemFocusBackgroundColor: '#13294b', - focusBorderColor: '#dd3403', - buttonTextColor: '#e8e9ea', - buttonBackgroundColor: '#13294b', - }, - 'uis': { - hostnameSelector: 'uis.edu', - menuTextColor: '#036', - menuBackgroundColor: '#fff', - menuitemFocusTextColor: '#fff', - menuitemFocusBackgroundColor: '#036', - focusBorderColor: '#dd3444', - buttonTextColor: '#fff', - buttonBackgroundColor: '#036', - }, - 'openweba11y': { - hostnameSelector: 'openweba11y.com', - buttonTextColor: '#13294B', - buttonBackgroundColor: '#dddddd', - focusBorderColor: '#C5050C', - menuTextColor: '#13294B', - menuBackgroundColor: '#dddddd', - menuitemFocusTextColor: '#dddddd', - menuitemFocusBackgroundColor: '#13294B', - fontSize: '90%' - } - }, + zHighlight: '' + }; + } - /* - * @method init - * - * @desc Initializes the skipto button and menu with default and user - * defined options - * - * @param {object} config - Reference to configuration object - * can be undefined - */ - init: function(globalConfig) { - let node; + static get observedAttributes() { + return [ + "data-skipto", + "setfocus", + "type", + "shortcuts", + "about" + ]; + } - // Check if skipto is already loaded - if (document.skipToHasBeenLoaded) { - console.warn('[skipTo.js] Skipto is already loaded!'); - return; + attributeChangedCallback(name, oldValue, newValue) { + + if (name === 'data-skipto') { + this.config = this.setupConfigFromDataAttribute(this.config, newValue); + } + + if (name === 'type') { + if (newValue === 'extension') { + this.config.shortcuts = 'enabled'; + } + } + + if (name === 'shortcuts') { + if (newValue.trim().toLowerCase() === 'enable') { + this.config.shortcuts = 'enabled'; + } + else { + this.config.shortcuts = 'disabled'; + } + } + + if (name === 'about') { + if (newValue.trim().toLowerCase() === 'true') { + this.config.aboutSupported = 'true'; + } + else { + this.config.aboutSupported = 'false'; } + } + + if (name === 'setfocus') { + switch(newValue) { + case 'button': + this.buttonSkipTo.closePopup(); + this.buttonSkipTo.buttonNode.focus(); + break; - document.skipToHasBeenLoaded = true; + case 'menu': + this.buttonSkipTo.openPopup(); + this.buttonSkipTo.setFocusToFirstMenuitem(); + break; - let attachElement = document.body; + case 'none': + this.buttonSkipTo.closePopup(); + document.body.focus(); + break; + } + } + } + /* + * @method init + * + * @desc Initializes the skipto button and menu with default and user + * defined options + * + * @param {object} globalConfig - Reference to configuration object + * can be undefined + */ + init(globalConfig=false) { + if (!this.initialized) { + this.initialized = true; if (globalConfig) { this.config = this.setupConfigFromGlobal(this.config, globalConfig); } - this.config = this.setupConfigFromDataAttribute(this.config); - - if (typeof this.config.attachElement === 'string') { - node = document.querySelector(this.config.attachElement); - if (node && node.nodeType === Node.ELEMENT_NODE) { - attachElement = node; - } + // Check for data-skipto attribute values for configuration + const configElem = document.querySelector('[data-skipto]'); + if (configElem) { + const params = configElem.getAttribute('data-skipto'); + this.config = this.setupConfigFromDataAttribute(this.config, params); } + // Add skipto style sheet to document - renderStyleElement(this.colorThemes, this.config, this.skipToId); + renderStyleElement(this.shadowRoot, this.config, this.skipToId, globalConfig); + this.buttonSkipTo = new SkiptoMenuButton(this); + + // Add landmark and heading info to DOM elements for keyboard navigation + // if using bookmarklet or extension + if (!globalConfig) { + getLandmarksAndHeadings(this.config, this.skipToId); + monitorKeyboardFocus(); + } + + } - new SkiptoMenuButton(attachElement, this.config, this.skipToId); - }, + this.setAttribute('focus', 'none'); + } - /* - * @method setupConfigFromGlobal - * - * @desc Get configuration information from author configuration to change - * default settings - * - * @param {object} config - Javascript object with default configuration information - * @param {object} globalConfig - Javascript object with configuration information oin a global variable - */ - setupConfigFromGlobal: function(config, globalConfig) { - let authorConfig = {}; - // Support version 4.1 configuration object structure - // If found use it - if ((typeof globalConfig.settings === 'object') && - (typeof globalConfig.settings.skipTo === 'object')) { - authorConfig = globalConfig.settings.skipTo; + /* + * @method setupConfigFromGlobal + * + * @desc Get configuration information from author configuration to change + * default settings + * + * @param {object} config - Javascript object with default configuration information + * @param {object} globalConfig - Javascript object with configuration information oin a global variable + */ + setupConfigFromGlobal(config, globalConfig) { + let authorConfig = {}; + // Support version 4.1 configuration object structure + // If found use it + if ((typeof globalConfig.settings === 'object') && + (typeof globalConfig.settings.skipTo === 'object')) { + authorConfig = globalConfig.settings.skipTo; + } + else { + // Version 5.0 removes the requirement for the "settings" and "skipto" properties + // to reduce the complexity of configuring skipto + if (typeof globalConfig === 'object') { + authorConfig = globalConfig; } - else { - // Version 5.0 removes the requirement for the "settings" and "skipto" properties - // to reduce the complexity of configuring skipto - if (typeof globalConfig === 'object') { - authorConfig = globalConfig; - } + } + + for (const name in authorConfig) { + //overwrite values of our local config, based on the external config + if ((typeof config[name] !== 'undefined') && + ((typeof authorConfig[name] === 'string') && + (authorConfig[name].length > 0 ) || + typeof authorConfig[name] === 'boolean') + ) { + config[name] = authorConfig[name]; + } else { + console.warn('[SkipTo]: Unsupported or deprecated configuration option in global configuration object: ' + name); } + } - for (const name in authorConfig) { - //overwrite values of our local config, based on the external config - if ((typeof config[name] !== 'undefined') && - ((typeof authorConfig[name] === 'string') && - (authorConfig[name].length > 0 ) || - typeof authorConfig[name] === 'boolean') - ) { - config[name] = authorConfig[name]; - } else { - console.warn('[SkipTo]: Unsupported or deprecated configuration option in global configuration object: ' + name); + return config; + } + + /* + * @method setupConfigFromDataAttribute + * + * @desc Update configuration information from author configuration to change + * default settings + * + * @param {Object} config - Object with SkipTo.js configuration information + * @param {String} params - String with configuration information + */ + setupConfigFromDataAttribute(config, params) { + let dataConfig = {}; + + if (params) { + const values = params.split(';'); + values.forEach( v => { + const index = v.indexOf(':'); + let prop = v.substring(0,index); + let value = v.substring(index+1); + if (prop) { + prop = prop.trim(); + } + if (value) { + value = value.trim(); + } + if (prop && value) { + dataConfig[prop] = value; } + }); + } + + for (const name in dataConfig) { + //overwrite values of our local config, based on the external config + if ((typeof config[name] !== 'undefined') && + ((typeof dataConfig[name] === 'string') && + (dataConfig[name].length > 0 ) || + typeof dataConfig[name] === 'boolean') + ) { + config[name] = dataConfig[name]; + } else { + console.warn('[SkipTo]: Unsupported or deprecated configuration option in data-skipto attribute: ' + name); } + } + + renderStyleElement(this.shadowRoot, config, this.skipToId); + if (this.buttonSkipTo) { + this.buttonSkipTo.updateLabels(config); + this.buttonSkipTo.setDisplayOption(config['displayOption']); + } + + const infoDialog = document.querySelector('skip-to-shortcuts-info-dialog'); + if (infoDialog) { + infoDialog.configureStyle(config); + } + + const shortcutsMessage = document.querySelector('skip-to-shortcuts-message'); + if (shortcutsMessage) { + shortcutsMessage.configureStyle(config); + } - return config; - }, + return config; + } /* - * @method setupConfigFromDataAttribute - * - * @desc Get configuration information from author configuration to change - * default settings - * - * @param {object} config - Javascript object with default configuration information - */ - setupConfigFromDataAttribute: function(config) { - let dataConfig = {}; + * @method supportShortcuts + * + * @desc Set suuportShortcuts configuration property + * + * @param {Boolean} value - If true support keyboard shortcuts, otherwise disable + */ + supportShortcuts(value) { + if (value) { + this.config.shortcutsSupported = 'true'; + this.config.shortcuts = 'enabled'; + } + else { + this.config.shortcutsSupported = 'false'; + this.config.shortcuts = 'disabled'; + } + } + } + + /* skipto.js */ - // Check for data-skipto attribute values for configuration - const configElem = document.querySelector('[data-skipto]'); - if (configElem) { - const dataSkiptoValue = configElem.getAttribute('data-skipto'); - if (dataSkiptoValue) { - const values = dataSkiptoValue.split(';'); - values.forEach( v => { - let [prop, value] = v.split(':'); - if (prop) { - prop = prop.trim(); - } - if (value) { - value = value.trim(); - } - if (prop && value) { - dataConfig[prop] = value; + /* constants */ + const debug = new DebugLogging('skipto', false); + debug.flag = false; + + (function() { + + const SkipToPageElmName = 'skip-to-content'; + const SkipToBookmarkletElmName = 'skip-to-content-bookmarklet'; + const SkipToExtensionElmName = 'skip-to-content-extension'; + + const SkipToExtensionID = `id-skip-to-extension`; + const SkipToBookmarkletID = `id-skip-to-bookmarklet`; + + /* + * @function removeLegacySkipToJS + * + * @desc Removes legacy and duplicate versions of SkipTo.js + */ + function removeLegacySkipToJS() { + + function removeElementsWithId(id) { + let node = document.getElementById(id); + // do more than once in case of duplicates + while (node) { + console.warn(`[SkipTo.js]: Removing legacy 5.x component: ${id}`); + node.remove (); + node = document.getElementById(id); + } + } + + // Remove 5.x legacy code + removeElementsWithId('id-skip-to'); + removeElementsWithId('id-skip-to-css'); + removeElementsWithId('id-skip-to-highlight'); + + // Remove 4.x + const nodes = document.querySelectorAll('div.skip-to'); + debug.flag && debug.log(`[removeLegacySkipToJS]: ${nodes.length}`); + for(let i = 0; i < nodes.length; i += 1) { + nodes[i].remove(); + console.warn(`[SkipTo.js]: Removing legacy 4.x component`); + } + } + + /* + * @function removePageSkipTo + * + * @desc Removes duplicate versions of SkipTo.js + */ + function removePageSkipTo() { + const nodes = document.querySelectorAll(SkipToPageElmName); + debug.flag && debug.log(`[removePageSkipTo]: ${nodes.length}`); + for (let i = 0; i < nodes.length; i += 1) { + nodes[i].remove(); + console.warn(`[SkipTo.js]: Removing ${nodes[i].tagName}`); + } + } + + /* + * @function removeBookmarkletSkipTo + * + * @desc Removes duplicate versions of SkipTo.js + */ + function removeBookmarkletSkipTo() { + const nodes = document.querySelectorAll(SkipToBookmarkletElmName); + debug.flag && debug.log(`[removeBookmarkletSkipTo]: ${nodes.length}`); + for (let i = 0; i < nodes.length; i += 1) { + nodes[i].remove(); + console.warn(`[SkipTo.js]: Removing ${nodes[i].tagName}`); + } + } + + + /* + *. @function getSkipToContentElement + * + * @desc Creates and add a skip-to-content element in the page + * + * @returns Returns dom node of new element or false if the page + * has a legacy SkipTo.js + */ + function getSkipToContentElement(type="pagescript") { + + removeLegacySkipToJS(); + + const isExtensionLoaded = document.querySelector(SkipToExtensionElmName); + const isBookmarkletLoaded = document.querySelector(SkipToBookmarkletElmName); + const isPageLoaded = document.querySelector(SkipToPageElmName); + + let skipToContentElem = false; + + switch (type) { + case 'bookmarklet': + if (!isExtensionLoaded) { + if (!isBookmarkletLoaded) { + removePageSkipTo(); + window.customElements.define(SkipToBookmarkletElmName, SkipToContent572); + skipToContentElem = document.createElement(SkipToBookmarkletElmName); + skipToContentElem.setAttribute('version', skipToContentElem.version); + skipToContentElem.setAttribute('type', type); + // always attach SkipToContent element to body + if (document.body) { + document.body.insertBefore(skipToContentElem, document.body.firstElementChild); } - }); + } } - } + break; - for (const name in dataConfig) { - //overwrite values of our local config, based on the external config - if ((typeof config[name] !== 'undefined') && - ((typeof dataConfig[name] === 'string') && - (dataConfig[name].length > 0 ) || - typeof dataConfig[name] === 'boolean') - ) { - config[name] = dataConfig[name]; - } else { - console.warn('[SkipTo]: Unsupported or deprecated configuration option in data-skipto attribute: ' + name); + case 'extension': + if (!isExtensionLoaded) { + removePageSkipTo(); + removeBookmarkletSkipTo(); + window.customElements.define(SkipToExtensionElmName, SkipToContent572); + skipToContentElem = document.createElement(SkipToExtensionElmName); + skipToContentElem.setAttribute('version', skipToContentElem.version); + skipToContentElem.setAttribute('type', type); + skipToContentElem.setAttribute('about', 'false'); + // always attach SkipToContent element to body + if (document.body) { + document.body.insertBefore(skipToContentElem, document.body.firstElementChild); + } } - } - return config; + break; + default: + if (!isPageLoaded && !isBookmarkletLoaded && !isExtensionLoaded) { + window.customElements.define(SkipToPageElmName, SkipToContent572); + skipToContentElem = document.createElement(SkipToPageElmName); + skipToContentElem.setAttribute('version', skipToContentElem.version); + skipToContentElem.setAttribute('type', type); + // always attach SkipToContent element to body + if (document.body) { + document.body.insertBefore(skipToContentElem, document.body.firstElementChild); + } + } + break; } - }; + return skipToContentElem; + } - // Initialize skipto menu button with onload event - window.addEventListener('load', function() { - SkipTo.init(window.SkipToConfig); - }); + // Check for SkipTo.js bookmarklet script, if it is initialize it immediately + if (document.getElementById(SkipToBookmarkletID)) { + debug.flag && debug.log(`[bookmarklet]`); + const skipToContentBookmarkletElem = getSkipToContentElement('bookmarklet'); + if (skipToContentBookmarkletElem) { + skipToContentBookmarkletElem.init(); + skipToContentBookmarkletElem.buttonSkipTo.openPopup(); + skipToContentBookmarkletElem.buttonSkipTo.setFocusToFirstMenuitem(); + } + } + else { + // Check for SkipTo.js extension script, if it is initialize it immediately + if (document.getElementById(SkipToExtensionID)) { + debug.flag && debug.log(`[extension]`); + const skipToContentExtensionElem = getSkipToContentElement('extension'); + if (skipToContentExtensionElem) { + skipToContentExtensionElem.init(); + window.addEventListener('load', function() { + debug.flag && debug.log(`[onload][extension][elem]: ${skipToContentExtensionElem}`); + removeLegacySkipToJS(); + removePageSkipTo(); + }); + } + } + else { + // Initialize SkipTo.js menu button with onload event + window.addEventListener('load', function() { + debug.flag && debug.log(`[onload][script]`); + const skipToContentPageElem = getSkipToContentElement(); + if (skipToContentPageElem) { + skipToContentPageElem.supportShortcuts(false); + debug.flag && debug.log(`[onload][script][elem]: ${skipToContentPageElem}`); + const initInfo = window.SkipToConfig ? window.SkipToConfig : {}; + skipToContentPageElem.init(initInfo); + } + }); + } + } })(); })(); From 420afb722f089c122482c2cc9e1c729ab4bd7f59 Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Tue, 21 Jan 2025 11:35:56 -0600 Subject: [PATCH 02/38] updated to skipto.js 5.7.3 --- content/shared/js/skipto.js | 117 +++++++++++++++++++++++++++++++++--- 1 file changed, 110 insertions(+), 7 deletions(-) diff --git a/content/shared/js/skipto.js b/content/shared/js/skipto.js index 96ce0d8960..1d0c3dc542 100644 --- a/content/shared/js/skipto.js +++ b/content/shared/js/skipto.js @@ -596,6 +596,101 @@ $skipToId [role="menuitem"].shortcuts-disabled { const cssHighlightTemplate = document.createElement('template'); cssHighlightTemplate.textContent = ` +$skipToId-overlay { + margin: 0; + padding: 0; + position: absolute; + border-radius: 3px; + border: 4px solid $buttonBackgroundColor; + box-sizing: border-box; + pointer-events:none; +} + +$skipToId-overlay .overlay-border { + margin: 0; + padding: 0; + position: relative; + top: -2px; + left: -2px; + border-radius: 3px 3px 3px 3px; + border: 2px solid $focusBorderColor; + z-index: $zHighlight; + box-sizing: border-box; + pointer-events:none; +} + +@keyframes fadeIn { + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +$skipToId-overlay .overlay-border.skip-to-hidden { + background-color: $hiddenHeadingBackgroundColor; + color: $hiddenHeadingColor; + font-style: italic; + font-weight: bold; + font-size: 0.9em; + text-align: center; + padding: .25em; + animation: fadeIn 1.5s; +} + +$skipToId-overlay .overlay-border.hasInfoBottom { + border-radius: 3px 3px 3px 0; +} + +$skipToId-overlay .overlay-border.hasInfoTop { + border-radius: 0 3px 3px 3px; +} + +$skipToId-overlay .overlay-info { + position: relative; + text-align: left; + left: -2px; + padding: 1px 4px; + border: 2px solid $focusBorderColor; + background-color: $menuBackgroundColor; + color: $menuTextColor; + z-index: $zHighlight; + overflow: hidden; + text-overflow: ellipsis; + pointer-events:none; +} + +$skipToId-overlay .overlay-info.hasInfoTop { + border-radius: 3px 3px 0 0; +} + +$skipToId-overlay .overlay-info.hasInfoBottom { + border-radius: 0 0 3px 3px; +} + +@media (forced-colors: active) { + + $skipToId-overlay { + border-color: ButtonBorder; + } + + $skipToId-overlay .overlay-border { + border-color: ButtonBorder; + } + + $skipToId-overlay .overlay-border.skip-to-hidden { + background-color: ButtonFace; + color: ButtonText; + } + + $skipToId-overlay .overlay-info { + border-color: ButtonBorder; + background-color: ButtonFace; + color: ButtonText; + } + +} +`; + + const cssHighlightTemplateLightDark = document.createElement('template'); + cssHighlightTemplateLightDark.textContent = ` :root { color-scheme: light dark; } @@ -691,9 +786,9 @@ $skipToId-overlay .overlay-info.hasInfoBottom { } } - `; + /* * @function getTheme * @@ -899,7 +994,14 @@ $skipToId-overlay .overlay-info.hasInfoBottom { let cssMenu = cssMenuTemplate.textContent.slice(0); cssMenu = cssMenu.replaceAll('$skipToId', '#' + skipToId); - let cssHighlight = cssHighlightTemplate.textContent.slice(0); + debug$c.log(`[lightDarkSupported]: ${config.lightDarkSupported} ${typeof config.lightDarkSupported} ${config.lightDarkSupported === 'true'}`); + + let cssHighlight = config.lightDarkSupported === 'true' ? + cssHighlightTemplateLightDark.textContent.slice(0) : + cssHighlightTemplate.textContent.slice(0); + + debug$c.log(`[cssHighlight]: ${cssHighlight}`); + cssHighlight = cssHighlight.replaceAll('$skipToId', '#' + skipToId); [cssMenu, cssHighlight] = addCSSColors(cssMenu, cssHighlight, config, useURLTheme); @@ -5015,14 +5117,14 @@ div#skip-to-message.fade { debug$1.flag = false; - class SkipToContent572 extends HTMLElement { + class SkipToContent573 extends HTMLElement { constructor() { // Always call super first in constructor super(); this.attachShadow({ mode: 'open' }); this.skipToId = 'id-skip-to'; - this.version = "5.7.2"; + this.version = "5.7.3"; this.buttonSkipTo = false; this.initialized = false; @@ -5030,6 +5132,7 @@ div#skip-to-message.fade { this.config = { // Feature switches enableHeadingLevelShortcuts: true, + lightDarkSupported: 'false', focusOption: 'none', // used by extensions only @@ -5495,7 +5598,7 @@ div#skip-to-message.fade { if (!isExtensionLoaded) { if (!isBookmarkletLoaded) { removePageSkipTo(); - window.customElements.define(SkipToBookmarkletElmName, SkipToContent572); + window.customElements.define(SkipToBookmarkletElmName, SkipToContent573); skipToContentElem = document.createElement(SkipToBookmarkletElmName); skipToContentElem.setAttribute('version', skipToContentElem.version); skipToContentElem.setAttribute('type', type); @@ -5511,7 +5614,7 @@ div#skip-to-message.fade { if (!isExtensionLoaded) { removePageSkipTo(); removeBookmarkletSkipTo(); - window.customElements.define(SkipToExtensionElmName, SkipToContent572); + window.customElements.define(SkipToExtensionElmName, SkipToContent573); skipToContentElem = document.createElement(SkipToExtensionElmName); skipToContentElem.setAttribute('version', skipToContentElem.version); skipToContentElem.setAttribute('type', type); @@ -5525,7 +5628,7 @@ div#skip-to-message.fade { default: if (!isPageLoaded && !isBookmarkletLoaded && !isExtensionLoaded) { - window.customElements.define(SkipToPageElmName, SkipToContent572); + window.customElements.define(SkipToPageElmName, SkipToContent573); skipToContentElem = document.createElement(SkipToPageElmName); skipToContentElem.setAttribute('version', skipToContentElem.version); skipToContentElem.setAttribute('type', type); From 9f4f0dace94510abeb008865e99c2db10e2b1cc8 Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Fri, 14 Feb 2025 19:15:23 -0700 Subject: [PATCH 03/38] updated to latest version --- content/shared/js/skipto.js | 1518 ++++++++++++++++++----------------- 1 file changed, 768 insertions(+), 750 deletions(-) diff --git a/content/shared/js/skipto.js b/content/shared/js/skipto.js index 1d0c3dc542..ddadced748 100644 --- a/content/shared/js/skipto.js +++ b/content/shared/js/skipto.js @@ -227,49 +227,90 @@ } + /* constants.js */ + + // Element IDs + + const SKIP_TO_ID = 'id-skip-to-ver-5'; + const SKIP_TO_MENU_STYLE_ID = 'id-skip-to-menu-style'; + + const SCRIPT_EXTENSION_ID = `id-skip-to-extension`; + const SCRIPT_BOOKMARKLET_ID = `id-skip-to-bookmarklet`; + + const MENU_ID = 'id-skip-to-menu'; + + const MENU_LANDMARK_GROUP_ID = 'id-skip-to-landmark-group'; + const MENU_LANDMARK_GROUP_LABEL_ID = 'id-skip-to-landmark-group-label'; + + const MENU_HEADINGS_GROUP_ID = 'id-skip-to-heading-group'; + const MENU_HEADINGS_GROUP_LABEL_ID = 'id-skip-to-heading-group-label'; + + const MENU_SHORTCUTS_GROUP_ID = 'id-skip-to-shortcuts-group'; + const MENU_SHORTCUTS_GROUP_LABEL_ID = 'id-skip-to-shortcuts-group-label'; + + const MESSAGE_ID = 'id-skip-to-message'; + + const HIGHLIGHT_ID = 'id-skip-to-highlight-overlay'; + + // Custom element names + + const PAGE_SCRIPT_ELEMENT_NAME = 'skip-to-content'; + const BOOKMARKLET_ELEMENT_NAME = 'skip-to-content-bookmarklet'; + const EXTENSION_ELEMENT_NAME = 'skip-to-content-extension'; + + const INFO_DIALOG_ELEMENT_NAME = 'skip-to-content-info-dialog-574'; + const MESSAGE_ELEMENT_NAME = 'skip-to-content-message-element-574'; + const HIGHLIGHT_ELEMENT_NAME = 'skip-to-content-highlight-element-574'; + + // Attributes + + const ATTR_SKIP_TO_DATA = 'data-skipto'; + + // URLs to more information + + const MORE_PAGE_SCRIPT_INFO_URL ='https://skipto-landmarks-headings.github.io/page-script-5/'; + const MORE_SHORTCUT_INFO_URL ='https://skipto-landmarks-headings.github.io/page-script-5/shortcuts.html'; + /* style.js */ /* Constants */ const debug$c = new DebugLogging('style', false); debug$c.flag = false; - const skipToMenuStyleID = 'id-skip-to-menu-style'; - const skipToHighlightStyleID = 'id-skip-to-highlight-style'; - const cssMenuTemplate = document.createElement('template'); cssMenuTemplate.textContent = ` :root { color-scheme: light dark; } -$skipToId.popup { +#${SKIP_TO_ID}.popup { top: -36px; transition: top 0.35s ease; } -$skipToId.popup.show-border { +#${SKIP_TO_ID}.popup.show-border { top: -28px; transition: top 0.35s ease; } -$skipToId button .skipto-text { +#${SKIP_TO_ID} button .skipto-text { padding: 6px 8px 6px 8px; display: inline-block; } -$skipToId button .skipto-small { +#${SKIP_TO_ID} button .skipto-small { padding: 6px 8px 6px 8px; display: none; } -$skipToId button .skipto-medium { +#${SKIP_TO_ID} button .skipto-medium { padding: 6px 8px 6px 8px; display: none; } -$skipToId, -$skipToId.popup.focus, -$skipToId.popup:hover { +#${SKIP_TO_ID}, +#${SKIP_TO_ID}.popup.focus, +#${SKIP_TO_ID}.popup:hover { position: fixed; top: 0; left: $positionLeft; @@ -284,7 +325,7 @@ $skipToId.popup:hover { touch-action: none; } -$skipToId button { +#${SKIP_TO_ID} button { position: sticky; margin: 0; padding: 0; @@ -302,59 +343,59 @@ $skipToId button { } @media screen and (max-width: $smallBreakPointpx) { - $skipToId:not(.popup) button .skipto-small { + #${SKIP_TO_ID}:not(.popup) button .skipto-small { transition: top 0.35s ease; display: inline-block; } - $skipToId:not(.popup) button .skipto-text, - $skipToId:not(.popup) button .skipto-medium { + #${SKIP_TO_ID}:not(.popup) button .skipto-text, + #${SKIP_TO_ID}:not(.popup) button .skipto-medium { transition: top 0.35s ease; display: none; } - $skipToId:not(.popup).focus button .skipto-text { + #${SKIP_TO_ID}:not(.popup).focus button .skipto-text { transition: top 0.35s ease; display: inline-block; } - $skipToId:not(.popup).focus button .skipto-small, - $skipToId:not(.popup).focus button .skipto-medium { + #${SKIP_TO_ID}:not(.popup).focus button .skipto-small, + #${SKIP_TO_ID}:not(.popup).focus button .skipto-medium { transition: top 0.35s ease; display: none; } } @media screen and (min-width: $smallBreakPointpx) and (max-width: $mediumBreakPointpx) { - $skipToId:not(.popup) button .skipto-medium { + #${SKIP_TO_ID}:not(.popup) button .skipto-medium { transition: top 0.35s ease; display: inline-block; } - $skipToId:not(.popup) button .skipto-text, - $skipToId:not(.popup) button .skipto-small { + #${SKIP_TO_ID}:not(.popup) button .skipto-text, + #${SKIP_TO_ID}:not(.popup) button .skipto-small { transition: top 0.35s ease; display: none; } - $skipToId:not(.popup).focus button .skipto-text { + #${SKIP_TO_ID}:not(.popup).focus button .skipto-text { transition: top 0.35s ease; display: inline-block; } - $skipToId:not(.popup).focus button .skipto-small, - $skipToId:not(.popup).focus button .skipto-medium { + #${SKIP_TO_ID}:not(.popup).focus button .skipto-small, + #${SKIP_TO_ID}:not(.popup).focus button .skipto-medium { transition: top 0.35s ease; display: none; } } -$skipToId.static { +#${SKIP_TO_ID}.static { position: absolute !important; } -$skipToId [role="menu"] { +#${SKIP_TO_ID} [role="menu"] { position: absolute; min-width: 16em; display: none; @@ -369,22 +410,22 @@ $skipToId [role="menu"] { touch-action: none; } -$skipToId [role="group"] { +#${SKIP_TO_ID} [role="group"] { display: grid; grid-auto-rows: min-content; grid-row-gap: 1px; } -$skipToId [role="group"].overflow { +#${SKIP_TO_ID} [role="group"].overflow { overflow-x: hidden; overflow-y: scroll; } -$skipToId [role="separator"]:first-child { +#${SKIP_TO_ID} [role="separator"]:first-child { border-radius: 5px 5px 0 0; } -$skipToId [role="menuitem"] { +#${SKIP_TO_ID} [role="menuitem"] { padding: 3px; width: auto; border-width: 0px; @@ -399,8 +440,8 @@ $skipToId [role="menuitem"] { z-index: $zIndex !important; } -$skipToId [role="menuitem"] .level, -$skipToId [role="menuitem"] .label { +#${SKIP_TO_ID} [role="menuitem"] .level, +#${SKIP_TO_ID} [role="menuitem"] .label { font-size: 100%; font-weight: normal; color: light-dark($menuTextColor, $menuTextDarkColor); @@ -412,12 +453,12 @@ $skipToId [role="menuitem"] .label { border: none; } -$skipToId [role="menuitem"] .level { +#${SKIP_TO_ID} [role="menuitem"] .level { text-align: right; padding-right: 4px; } -$skipToId [role="menuitem"] .label { +#${SKIP_TO_ID} [role="menuitem"] .label { text-align: left; margin: 0; padding: 0; @@ -425,49 +466,49 @@ $skipToId [role="menuitem"] .label { text-overflow: ellipsis; } -$skipToId [role="menuitem"] .level:first-letter, -$skipToId [role="menuitem"] .label:first-letter { +#${SKIP_TO_ID} [role="menuitem"] .level:first-letter, +#${SKIP_TO_ID} [role="menuitem"] .label:first-letter { text-decoration: underline; text-transform: uppercase; } -$skipToId [role="menuitem"].skip-to-h1 .level { grid-column: 1; } -$skipToId [role="menuitem"].skip-to-h2 .level { grid-column: 2; } -$skipToId [role="menuitem"].skip-to-h3 .level { grid-column: 3; } -$skipToId [role="menuitem"].skip-to-h4 .level { grid-column: 4; } -$skipToId [role="menuitem"].skip-to-h5 .level { grid-column: 5; } -$skipToId [role="menuitem"].skip-to-h6 .level { grid-column: 6;} - -$skipToId [role="menuitem"].skip-to-h1 .label { grid-column: 2 / 8; } -$skipToId [role="menuitem"].skip-to-h2 .label { grid-column: 3 / 8; } -$skipToId [role="menuitem"].skip-to-h3 .label { grid-column: 4 / 8; } -$skipToId [role="menuitem"].skip-to-h4 .label { grid-column: 5 / 8; } -$skipToId [role="menuitem"].skip-to-h5 .label { grid-column: 6 / 8; } -$skipToId [role="menuitem"].skip-to-h6 .label { grid-column: 7 / 8;} - -$skipToId [role="menuitem"].skip-to-h1.no-level .label { grid-column: 1 / 8; } -$skipToId [role="menuitem"].skip-to-h2.no-level .label { grid-column: 2 / 8; } -$skipToId [role="menuitem"].skip-to-h3.no-level .label { grid-column: 3 / 8; } -$skipToId [role="menuitem"].skip-to-h4.no-level .label { grid-column: 4 / 8; } -$skipToId [role="menuitem"].skip-to-h5.no-level .label { grid-column: 5 / 8; } -$skipToId [role="menuitem"].skip-to-h6.no-level .label { grid-column: 6 / 8; } - -$skipToId [role="menuitem"].skip-to-nesting-level-1 .nesting { grid-column: 1; } -$skipToId [role="menuitem"].skip-to-nesting-level-2 .nesting { grid-column: 2; } -$skipToId [role="menuitem"].skip-to-nesting-level-3 .nesting { grid-column: 3; } - -$skipToId [role="menuitem"].skip-to-nesting-level-0 .label { grid-column: 1 / 8; } -$skipToId [role="menuitem"].skip-to-nesting-level-1 .label { grid-column: 2 / 8; } -$skipToId [role="menuitem"].skip-to-nesting-level-2 .label { grid-column: 3 / 8; } -$skipToId [role="menuitem"].skip-to-nesting-level-3 .label { grid-column: 4 / 8; } - -$skipToId [role="menuitem"].no-items .label, -$skipToId [role="menuitem"].action .label { +#${SKIP_TO_ID} [role="menuitem"].skip-to-h1 .level { grid-column: 1; } +#${SKIP_TO_ID} [role="menuitem"].skip-to-h2 .level { grid-column: 2; } +#${SKIP_TO_ID} [role="menuitem"].skip-to-h3 .level { grid-column: 3; } +#${SKIP_TO_ID} [role="menuitem"].skip-to-h4 .level { grid-column: 4; } +#${SKIP_TO_ID} [role="menuitem"].skip-to-h5 .level { grid-column: 5; } +#${SKIP_TO_ID} [role="menuitem"].skip-to-h6 .level { grid-column: 6;} + +#${SKIP_TO_ID} [role="menuitem"].skip-to-h1 .label { grid-column: 2 / 8; } +#${SKIP_TO_ID} [role="menuitem"].skip-to-h2 .label { grid-column: 3 / 8; } +#${SKIP_TO_ID} [role="menuitem"].skip-to-h3 .label { grid-column: 4 / 8; } +#${SKIP_TO_ID} [role="menuitem"].skip-to-h4 .label { grid-column: 5 / 8; } +#${SKIP_TO_ID} [role="menuitem"].skip-to-h5 .label { grid-column: 6 / 8; } +#${SKIP_TO_ID} [role="menuitem"].skip-to-h6 .label { grid-column: 7 / 8;} + +#${SKIP_TO_ID} [role="menuitem"].skip-to-h1.no-level .label { grid-column: 1 / 8; } +#${SKIP_TO_ID} [role="menuitem"].skip-to-h2.no-level .label { grid-column: 2 / 8; } +#${SKIP_TO_ID} [role="menuitem"].skip-to-h3.no-level .label { grid-column: 3 / 8; } +#${SKIP_TO_ID} [role="menuitem"].skip-to-h4.no-level .label { grid-column: 4 / 8; } +#${SKIP_TO_ID} [role="menuitem"].skip-to-h5.no-level .label { grid-column: 5 / 8; } +#${SKIP_TO_ID} [role="menuitem"].skip-to-h6.no-level .label { grid-column: 6 / 8; } + +#${SKIP_TO_ID} [role="menuitem"].skip-to-nesting-level-1 .nesting { grid-column: 1; } +#${SKIP_TO_ID} [role="menuitem"].skip-to-nesting-level-2 .nesting { grid-column: 2; } +#${SKIP_TO_ID} [role="menuitem"].skip-to-nesting-level-3 .nesting { grid-column: 3; } + +#${SKIP_TO_ID} [role="menuitem"].skip-to-nesting-level-0 .label { grid-column: 1 / 8; } +#${SKIP_TO_ID} [role="menuitem"].skip-to-nesting-level-1 .label { grid-column: 2 / 8; } +#${SKIP_TO_ID} [role="menuitem"].skip-to-nesting-level-2 .label { grid-column: 3 / 8; } +#${SKIP_TO_ID} [role="menuitem"].skip-to-nesting-level-3 .label { grid-column: 4 / 8; } + +#${SKIP_TO_ID} [role="menuitem"].no-items .label, +#${SKIP_TO_ID} [role="menuitem"].action .label { grid-column: 1 / 8; } -$skipToId [role="separator"] { +#${SKIP_TO_ID} [role="separator"] { margin: 1px 0px 1px 0px; padding: 3px; display: block; @@ -481,27 +522,27 @@ $skipToId [role="separator"] { z-index: $zIndex !important; } -$skipToId [role="separator"] .mofn { +#${SKIP_TO_ID} [role="separator"] .mofn { font-weight: normal; font-size: 85%; } -$skipToId [role="separator"]:first-child { +#${SKIP_TO_ID} [role="separator"]:first-child { border-radius: 5px 5px 0 0; } -$skipToId [role="menuitem"].last { +#${SKIP_TO_ID} [role="menuitem"].last { border-radius: 0 0 5px 5px; } /* focus styling */ -$skipToId.focus { +#${SKIP_TO_ID}.focus { display: block; } -$skipToId button:focus, -$skipToId button:hover { +#${SKIP_TO_ID} button:focus, +#${SKIP_TO_ID} button:hover { background-color: light-dark($menuBackgroundColor, $menuBackgroundDarkColor); color: light-dark($menuTextColor, $menuTextDarkColor); outline: none; @@ -510,16 +551,16 @@ $skipToId button:hover { } -$skipToId button:focus .skipto-text, -$skipToId button:hover .skipto-text, -$skipToId button:focus .skipto-small, -$skipToId button:hover .skipto-small, -$skipToId button:focus .skipto-medium, -$skipToId button:hover .skipto-medium { +#${SKIP_TO_ID} button:focus .skipto-text, +#${SKIP_TO_ID} button:hover .skipto-text, +#${SKIP_TO_ID} button:focus .skipto-small, +#${SKIP_TO_ID} button:hover .skipto-small, +#${SKIP_TO_ID} button:focus .skipto-medium, +#${SKIP_TO_ID} button:hover .skipto-medium { padding: 6px 7px 5px 7px; } -$skipToId [role="menuitem"]:focus { +#${SKIP_TO_ID} [role="menuitem"]:focus { padding: 1px; border-width: 2px; border-style: solid; @@ -527,265 +568,71 @@ $skipToId [role="menuitem"]:focus { outline: none; } -$skipToId [role="menuitem"].hover, -$skipToId [role="menuitem"].hover .level, -$skipToId [role="menuitem"].hover .label { +#${SKIP_TO_ID} [role="menuitem"].hover, +#${SKIP_TO_ID} [role="menuitem"].hover .level, +#${SKIP_TO_ID} [role="menuitem"].hover .label { background-color: light-dark($menuitemFocusBackgroundColor, $menuitemFocusBackgroundDarkColor); color: light-dark($menuitemFocusTextColor, $menuitemFocusTextDarkColor); } -$skipToId [role="separator"].shortcuts-disabled, -$skipToId [role="menuitem"].shortcuts-disabled { +#${SKIP_TO_ID} [role="separator"].shortcuts-disabled, +#${SKIP_TO_ID} [role="menuitem"].shortcuts-disabled { display: none; } @media (forced-colors: active) { - $skipToId button { + #${SKIP_TO_ID} button { border-color: ButtonBorder; color: ButtonText; background-color: ButtonFace; } - $skipToId [role="menu"] { + #${SKIP_TO_ID} [role="menu"] { background-color: ButtonFace; border-color: ButtonText; } - $skipToId [role="menuitem"] { + #${SKIP_TO_ID} [role="menuitem"] { color: ButtonText; background-color: ButtonFace; } - $skipToId [role="menuitem"] .level, - $skipToId [role="menuitem"] .label { + #${SKIP_TO_ID} [role="menuitem"] .level, + #${SKIP_TO_ID} [role="menuitem"] .label { color: ButtonText; background-color: ButtonFace; } - $skipToId [role="separator"] { + #${SKIP_TO_ID} [role="separator"] { border-bottom-color: ButtonBorder; background-color: ButtonFace; color: ButtonText; z-index: $zIndex !important; } - $skipToId button:focus, - $skipToId button:hover { + #${SKIP_TO_ID} button:focus, + #${SKIP_TO_ID} button:hover { background-color: ButtonFace; color: ButtonText; border-color: ButtonBorder; } - $skipToId [role="menuitem"]:focus { + #${SKIP_TO_ID} [role="menuitem"]:focus { background-color: ButtonText; color: ButtonFace; border-color: ButtonBorder; } - $skipToId [role="menuitem"].hover, - $skipToId [role="menuitem"].hover .level, - $skipToId [role="menuitem"].hover .label { + #${SKIP_TO_ID} [role="menuitem"].hover, + #${SKIP_TO_ID} [role="menuitem"].hover .level, + #${SKIP_TO_ID} [role="menuitem"].hover .label { background-color: ButtonText; color: ButtonFace; } } -`; - - const cssHighlightTemplate = document.createElement('template'); - cssHighlightTemplate.textContent = ` -$skipToId-overlay { - margin: 0; - padding: 0; - position: absolute; - border-radius: 3px; - border: 4px solid $buttonBackgroundColor; - box-sizing: border-box; - pointer-events:none; -} - -$skipToId-overlay .overlay-border { - margin: 0; - padding: 0; - position: relative; - top: -2px; - left: -2px; - border-radius: 3px 3px 3px 3px; - border: 2px solid $focusBorderColor; - z-index: $zHighlight; - box-sizing: border-box; - pointer-events:none; -} - -@keyframes fadeIn { - 0% { opacity: 0; } - 100% { opacity: 1; } -} - -$skipToId-overlay .overlay-border.skip-to-hidden { - background-color: $hiddenHeadingBackgroundColor; - color: $hiddenHeadingColor; - font-style: italic; - font-weight: bold; - font-size: 0.9em; - text-align: center; - padding: .25em; - animation: fadeIn 1.5s; -} - -$skipToId-overlay .overlay-border.hasInfoBottom { - border-radius: 3px 3px 3px 0; -} - -$skipToId-overlay .overlay-border.hasInfoTop { - border-radius: 0 3px 3px 3px; -} - -$skipToId-overlay .overlay-info { - position: relative; - text-align: left; - left: -2px; - padding: 1px 4px; - border: 2px solid $focusBorderColor; - background-color: $menuBackgroundColor; - color: $menuTextColor; - z-index: $zHighlight; - overflow: hidden; - text-overflow: ellipsis; - pointer-events:none; -} - -$skipToId-overlay .overlay-info.hasInfoTop { - border-radius: 3px 3px 0 0; -} - -$skipToId-overlay .overlay-info.hasInfoBottom { - border-radius: 0 0 3px 3px; -} - -@media (forced-colors: active) { - - $skipToId-overlay { - border-color: ButtonBorder; - } - - $skipToId-overlay .overlay-border { - border-color: ButtonBorder; - } - - $skipToId-overlay .overlay-border.skip-to-hidden { - background-color: ButtonFace; - color: ButtonText; - } - - $skipToId-overlay .overlay-info { - border-color: ButtonBorder; - background-color: ButtonFace; - color: ButtonText; - } - -} -`; - - const cssHighlightTemplateLightDark = document.createElement('template'); - cssHighlightTemplateLightDark.textContent = ` -:root { - color-scheme: light dark; -} - -$skipToId-overlay { - margin: 0; - padding: 0; - position: absolute; - border-radius: 3px; - border: 4px solid light-dark($buttonBackgroundColor, $buttonBackgroundDarkColor); - box-sizing: border-box; - pointer-events:none; -} - -$skipToId-overlay .overlay-border { - margin: 0; - padding: 0; - position: relative; - top: -2px; - left: -2px; - border-radius: 3px 3px 3px 3px; - border: 2px solid light-dark($focusBorderColor, $focusBorderDarkColor); - z-index: $zHighlight; - box-sizing: border-box; - pointer-events:none; -} - -@keyframes fadeIn { - 0% { opacity: 0; } - 100% { opacity: 1; } -} - -$skipToId-overlay .overlay-border.skip-to-hidden { - background-color: light-dark($hiddenHeadingBackgroundColor, $hiddenHeadingBackgroundDarkColor); - color: light-dark($hiddenHeadingColor, $hiddenHeadingDarkColor); - font-style: italic; - font-weight: bold; - font-size: 0.9em; - text-align: center; - padding: .25em; - animation: fadeIn 1.5s; -} - -$skipToId-overlay .overlay-border.hasInfoBottom { - border-radius: 3px 3px 3px 0; -} - -$skipToId-overlay .overlay-border.hasInfoTop { - border-radius: 0 3px 3px 3px; -} - -$skipToId-overlay .overlay-info { - position: relative; - text-align: left; - left: -2px; - padding: 1px 4px; - border: 2px solid light-dark($focusBorderColor, $focusBorderDarkColor); - background-color: light-dark($menuBackgroundColor, $menuBackgroundDarkColor); - color: light-dark($menuTextColor, $menuTextDarkColor); - z-index: $zHighlight; - overflow: hidden; - text-overflow: ellipsis; - pointer-events:none; -} - -$skipToId-overlay .overlay-info.hasInfoTop { - border-radius: 3px 3px 0 0; -} - -$skipToId-overlay .overlay-info.hasInfoBottom { - border-radius: 0 0 3px 3px; -} - -@media (forced-colors: active) { - - $skipToId-overlay { - border-color: ButtonBorder; - } - - $skipToId-overlay .overlay-border { - border-color: ButtonBorder; - } - - $skipToId-overlay .overlay-border.skip-to-hidden { - background-color: ButtonFace; - color: ButtonText; - } - - $skipToId-overlay .overlay-info { - border-color: ButtonBorder; - background-color: ButtonFace; - color: ButtonText; - } - -} `; @@ -903,13 +750,12 @@ $skipToId-overlay .overlay-info.hasInfoBottom { * and returns the updated strings * * @param {String} cssMenu - CSS template for the button and menu - * @param {String} cssHighlight - CSS template for the highlighting * @param {Object} config - SkipTo.js configuration information object * @param {Boolean} useURLTheme - When true use the theme associated with the URL * * @returns. see @desc */ - function addCSSColors (cssMenu, cssHighlight, config, useURLTheme=false) { + function addCSSColors (cssMenu, config, useURLTheme=false) { const theme = useURLTheme ? getTheme(config.colorTheme) : {}; const defaultTheme = getTheme('default'); @@ -953,30 +799,12 @@ $skipToId-overlay .overlay-info.hasInfoBottom { cssMenu = updateStyle(cssMenu, '$zIndex', config.zIndex, theme.zIndex, defaultTheme.zIndex); - cssHighlight = updateStyle(cssHighlight, '$zHighlight', config.zHighlight, theme.zHighlight, defaultTheme.zHighlight); - cssHighlight = updateStyle(cssHighlight, '$buttonBackgroundColor', config.buttonBackgroundColor, theme.buttonBackgroundColor, defaultTheme.buttonBackgroundColor); - cssHighlight = updateStyle(cssHighlight, '$buttonBackgroundDarkColor', config.buttonBackgroundDarkColor, theme.buttonBackgroundDarkColor, defaultTheme.buttonBackgroundDarkColor); - cssHighlight = updateStyle(cssHighlight, '$focusBorderColor', config.focusBorderColor, theme.focusBorderColor, defaultTheme.focusBorderColor); - cssHighlight = updateStyle(cssHighlight, '$focusBorderDarkColor', config.focusBorderDarkColor, theme.focusBorderDarkColor, defaultTheme.focusBorderDarkColor); - cssHighlight = updateStyle(cssHighlight, '$menuTextColor', config.menuitemFocusTextColor, theme.menuitemFocusTextColor, defaultTheme.menuitemFocusTextColor); - cssHighlight = updateStyle(cssHighlight, '$menuTextDarkColor', config.menuitemFocusTextDarkColor, theme.menuitemFocusTextDarkColor, defaultTheme.menuitemFocusTextDarkColor); - cssHighlight = updateStyle(cssHighlight, '$menuBackgroundColor', config.menuitemFocusBackgroundColor, theme.menuitemFocusBackgroundColor, defaultTheme.menuitemFocusBackgroundColor); - cssHighlight = updateStyle(cssHighlight, '$menuBackgroundDarkColor', config.menuitemFocusBackgroundDarkColor, theme.menuitemFocusBackgroundDarkColor, defaultTheme.menuitemFocusBackgroundDarkColor); - cssHighlight = updateStyle(cssHighlight, '$menuitemFocusTextColor', config.menuitemFocusTextColor, theme.menuitemFocusTextColor, defaultTheme.menuitemFocusTextColor); - cssHighlight = updateStyle(cssHighlight, '$menuitemFocusTextDarkColor', config.menuitemFocusTextDarkColor, theme.menuitemFocusTextDarkColor, defaultTheme.menuitemFocusTextDarkColor); - cssHighlight = updateStyle(cssHighlight, '$menuitemFocusBackgroundColor', config.menuitemFocusBackgroundColor, theme.menuitemFocusBackgroundColor, defaultTheme.menuitemFocusBackgroundColor); - cssHighlight = updateStyle(cssHighlight, '$menuitemFocusBackgroundDarkColor', config.menuitemFocusBackgroundDarkColor, theme.menuitemFocusBackgroundDarkColor, defaultTheme.menuitemFocusBackgroundDarkColor); - cssHighlight = updateStyle(cssHighlight, '$hiddenHeadingColor', config.hiddenHeadingColor, theme.hiddenHeadingColor, defaultTheme.hiddenHeadingColor); - cssHighlight = updateStyle(cssHighlight, '$hiddenHeadingDarkColor', config.hiddenHeadingDarkColor, theme.hiddenHeadingDarkColor, defaultTheme.hiddenHeadingDarkColor); - cssHighlight = updateStyle(cssHighlight, '$hiddenHeadingBackgroundColor', config.hiddenHeadingBackgroundColor, theme.hiddenHeadingBackgroundColor, defaultTheme.hiddenHeadingBackgroundColor); - cssHighlight = updateStyle(cssHighlight, '$hiddenHeadingBackgroundDarkColor', config.hiddenHeadingBackgroundDarkColor, theme.hiddenHeadingBackgroundDarkColor, defaultTheme.hiddenHeadingBackgroundDarkColor); - // Special case for theme configuration used in Illinois theme if (typeof theme.highlightTarget === 'string') { config.highlightTarget = theme.highlightTarget; } - return [cssMenu, cssHighlight]; + return cssMenu; } @@ -992,40 +820,17 @@ $skipToId-overlay .overlay-info.hasInfoBottom { */ function renderStyleElement (attachNode, config, skipToId, useURLTheme=false) { let cssMenu = cssMenuTemplate.textContent.slice(0); - cssMenu = cssMenu.replaceAll('$skipToId', '#' + skipToId); - - debug$c.log(`[lightDarkSupported]: ${config.lightDarkSupported} ${typeof config.lightDarkSupported} ${config.lightDarkSupported === 'true'}`); - - let cssHighlight = config.lightDarkSupported === 'true' ? - cssHighlightTemplateLightDark.textContent.slice(0) : - cssHighlightTemplate.textContent.slice(0); - - debug$c.log(`[cssHighlight]: ${cssHighlight}`); - cssHighlight = cssHighlight.replaceAll('$skipToId', '#' + skipToId); + cssMenu = addCSSColors(cssMenu, config, useURLTheme); - [cssMenu, cssHighlight] = addCSSColors(cssMenu, cssHighlight, config, useURLTheme); - - - let styleNode = attachNode.querySelector(`#${skipToMenuStyleID}`); + let styleNode = attachNode.querySelector(`#${SKIP_TO_MENU_STYLE_ID}`); if (!styleNode) { styleNode = document.createElement('style'); attachNode.appendChild(styleNode); - styleNode.setAttribute('id', `${skipToMenuStyleID}`); + styleNode.setAttribute('id', `${SKIP_TO_MENU_STYLE_ID}`); } styleNode.textContent = cssMenu; - const headNode = document.querySelector('head'); - if (headNode) { - let highlightStyleNode = headNode.querySelector(`#${skipToHighlightStyleID}`); - if (!highlightStyleNode) { - highlightStyleNode = document.createElement('style'); - headNode.appendChild(highlightStyleNode); - highlightStyleNode.setAttribute('id', `${skipToHighlightStyleID}`); - } - highlightStyleNode.textContent = cssHighlight; - } - } /* utils.js */ @@ -1121,25 +926,8 @@ $skipToId-overlay .overlay-info.hasInfoBottom { const debug$a = new DebugLogging('[shortcutsInfoDialog]', false); debug$a.flag = false; - const defaultStyleOptions$1 = { - fontFamily: 'sans-serif', - fontSize: '12pt', - focusBorderColor: '#c5050c', - focusBorderDarkColor: '#ffffff', + const defaultStyleOptions$2 = colorThemes['default']; - // Dialog styling defaults - dialogTextColor: '#000000', - dialogTextDarkColor: '#ffffff', - dialogBackgroundColor: '#ffffff', - dialogBackgroundDarkColor: '#000000', - dialogBackgroundTitleColor: '#eeeeee', - dialogBackgroundTitleDarkColor: '#013c93', - - }; - - - const MORE_PAGE_INFO_URL='https://skipto-landmarks-headings.github.io/page-script-5/page.html'; - const MORE_SHORTCUT_INFO_URL='https://skipto-landmarks-headings.github.io/page-script-5/shortcuts.html'; const styleTemplate$1 = document.createElement('template'); styleTemplate$1.textContent = ` @@ -1299,6 +1087,11 @@ button:hover { } `; + /* + * + * + */ + class SkipToContentInfoDialog extends HTMLElement { constructor () { @@ -1380,52 +1173,52 @@ button:hover { style = updateOption(style, '$fontFamily', config.fontFamily, - defaultStyleOptions$1.fontFamily); + defaultStyleOptions$2.fontFamily); style = updateOption(style, '$fontSize', config.fontSize, - defaultStyleOptions$1.fontSize); + defaultStyleOptions$2.fontSize); style = updateOption(style, '$focusBorderColor', config.focusBorderColor, - defaultStyleOptions$1.focusBorderColor); + defaultStyleOptions$2.focusBorderColor); style = updateOption(style, '$focusBorderDarkColor', config.focusBorderDarkColor, - defaultStyleOptions$1.focusBorderDarkColor); + defaultStyleOptions$2.focusBorderDarkColor); style = updateOption(style, '$dialogTextColor', config.dialogTextColor, - defaultStyleOptions$1.dialogTextColor); + defaultStyleOptions$2.dialogTextColor); style = updateOption(style, '$dialogextDarkColor', config.dialogextDarkColor, - defaultStyleOptions$1.dialogextDarkColor); + defaultStyleOptions$2.dialogextDarkColor); style = updateOption(style, '$dialogBackgroundColor', config.dialogBackgroundColor, - defaultStyleOptions$1.dialogBackgroundColor); + defaultStyleOptions$2.dialogBackgroundColor); style = updateOption(style, '$dialogBackgroundDarkColor', config.dialogBackgroundDarkColor, - defaultStyleOptions$1.dialogBackgroundDarkColor); + defaultStyleOptions$2.dialogBackgroundDarkColor); style = updateOption(style, '$dialogBackgroundTitleColor', config.dialogBackgroundTitleColor, - defaultStyleOptions$1.dialogBackgroundTitleColor); + defaultStyleOptions$2.dialogBackgroundTitleColor); style = updateOption(style, '$dialogBackgroundTitleDarkColor', config.dialogBackgroundTitleDarkColor, - defaultStyleOptions$1.dialogBackgroundTitleDarkColor); + defaultStyleOptions$2.dialogBackgroundTitleDarkColor); let styleNode = this.shadowRoot.querySelector('style'); @@ -1549,7 +1342,7 @@ button:hover { this.contentElem.removeChild(this.contentElem.lastElementChild); } - this.moreInfoURL = MORE_PAGE_INFO_URL; + this.moreInfoURL = MORE_PAGE_SCRIPT_INFO_URL; this.h2Elem.textContent = config.aboutInfoLabel; this.closeButton1.setAttribute('aria-label', config.closeLabel); @@ -1602,27 +1395,503 @@ button:hover { } } + /* highlight.js */ + + /* Constants */ + const debug$9 = new DebugLogging('highlight', false); + debug$9.flag = false; + + const minWidth = 68; + const minHeight = 27; + const offset = 6; + const borderWidth = 2; + + const defaultStyleOptions$1 = colorThemes['default']; + + const styleHighlightTemplate = document.createElement('template'); + styleHighlightTemplate.textContent = ` +:root { + color-scheme: light dark; +} + +#${HIGHLIGHT_ID} { + margin: 0; + padding: 0; + position: absolute; + border-radius: 3px; + border: 4px solid light-dark($buttonBackgroundColor, $buttonBackgroundDarkColor); + box-sizing: border-box; + pointer-events:none; +} + +#${HIGHLIGHT_ID} .overlay-border { + margin: 0; + padding: 0; + position: relative; + top: -2px; + left: -2px; + border-radius: 3px 3px 3px 3px; + border: 2px solid light-dark($focusBorderColor, $focusBorderDarkColor); + z-index: $zHighlight; + box-sizing: border-box; + pointer-events:none; +} + +@keyframes fadeIn { + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +#${HIGHLIGHT_ID} .overlay-border.skip-to-hidden { + background-color: light-dark($hiddenHeadingBackgroundColor, $hiddenHeadingBackgroundDarkColor); + color: light-dark($hiddenHeadingColor, $hiddenHeadingDarkColor); + font-family: $fontFamily; + font-size: $fontSize; + font-style: italic; + font-weight: bold; + text-align: center; + padding: .25em; + animation: fadeIn 1.5s; +} + +#${HIGHLIGHT_ID} .overlay-border.hasInfoBottom { + border-radius: 3px 3px 3px 0; +} + +#${HIGHLIGHT_ID} .overlay-border.hasInfoTop { + border-radius: 0 3px 3px 3px; +} + +#${HIGHLIGHT_ID} .overlay-info { + position: relative; + text-align: left; + left: -2px; + padding: 1px 4px; + font-size: $fontSize; + font-family: $fontFamily; + border: 2px solid light-dark($focusBorderColor, $focusBorderDarkColor); + background-color: light-dark($menuBackgroundColor, $menuBackgroundDarkColor); + color: light-dark($menuTextColor, $menuTextDarkColor); + z-index: $zHighlight; + overflow: hidden; + text-overflow: ellipsis; + pointer-events:none; +} + +#${HIGHLIGHT_ID} .overlay-info.hasInfoTop { + border-radius: 3px 3px 0 0; +} + +#${HIGHLIGHT_ID} .overlay-info.hasInfoBottom { + border-radius: 0 0 3px 3px; +} + +@media (forced-colors: active) { + + #${HIGHLIGHT_ID} { + border-color: ButtonBorder; + } + + #${HIGHLIGHT_ID} .overlay-border { + border-color: ButtonBorder; + } + + #${HIGHLIGHT_ID} .overlay-border.skip-to-hidden { + background-color: ButtonFace; + color: ButtonText; + } + + #${HIGHLIGHT_ID} .overlay-info { + border-color: ButtonBorder; + background-color: ButtonFace; + color: ButtonText; + } + +} +`; + + /* + * @class HighlightElement + * + */ + + class HighlightElement extends HTMLElement { + + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + + // Get references + + this.overlayElem = document.createElement('div'); + this.overlayElem.id = HIGHLIGHT_ID; + this.shadowRoot.appendChild(this.overlayElem); + this.overlayElem.style.display = 'none'; + + this.borderElem = document.createElement('div'); + this.borderElem.className = 'overlay-border'; + this.overlayElem.appendChild(this.borderElem); + + this.infoElem = document.createElement('div'); + this.infoElem.className = 'overlay-info'; + this.overlayElem.appendChild(this.infoElem); + + this.configureStyle(); + + } + + /* + * @method configureStyle + * + * @desc Updates stylesheet for styling the highlight information + * + * @param {Object} config : color and font information + */ + + + configureStyle(config={}) { + + function updateOption(style, option, configOption, defaultOption) { + if (configOption) { + return style.replaceAll(option, configOption); + } + else { + return style.replaceAll(option, defaultOption); + } + } + + // make a copy of the template + let style = styleHighlightTemplate.textContent.slice(0); + + style = updateOption(style, + '$fontFamily', + config.fontFamily, + defaultStyleOptions$1.fontFamily); + + style = updateOption(style, + '$fontSize', + config.fontSize, + defaultStyleOptions$1.fontSize); + + style = updateOption(style, + '$buttonBackgroundColor', + config.buttonBackgroundColor, + defaultStyleOptions$1.buttonBackgroundColor); + + style = updateOption(style, + '$buttonBackgroundDarkColor', + config.buttonBackgroundDarkColor, + defaultStyleOptions$1.buttonBackgroundDarkColor); + + style = updateOption(style, + '$focusBorderColor', + config.focusBorderColor, + defaultStyleOptions$1.focusBorderColor); + + style = updateOption(style, + '$focusBorderDarkColor', + config.focusBorderDarkColor, + defaultStyleOptions$1.focusBorderDarkColor); + + style = updateOption(style, + '$menuBackgroundColor', + config.menuBackgroundColor, + defaultStyleOptions$1.menuBackgroundColor); + + style = updateOption(style, + '$menuBackgroundDarkColor', + config.menuBackgroundDarkColor, + defaultStyleOptions$1.menuBackgroundDarkColor); + + style = updateOption(style, + '$menuTextColor', + config.menuTextColor, + defaultStyleOptions$1.menuTextColor); + + style = updateOption(style, + '$menuTextDarkColor', + config.menuTextDarkColor, + defaultStyleOptions$1.menuTextDarkColor); + + style = updateOption(style, + '$hiddenHeadingColor', + config.hiddenHeadingColor, + defaultStyleOptions$1.hiddenHeadingColor); + + style = updateOption(style, + '$hiddenHeadingDarkColor', + config.hiddenHeadingDarkColor, + defaultStyleOptions$1.hiddenHeadingDarkColor); + + style = updateOption(style, + '$hiddenHeadingBackgroundColor', + config.hiddenHeadingBackgroundColor, + defaultStyleOptions$1.hiddenHeadingBackgroundColor); + + style = updateOption(style, + '$hiddenHeadingBackgroundDarkColor', + config.hiddenHeadingBackgroundDarkColor, + defaultStyleOptions$1.hiddenHeadingBackgroundDarkColor); + + style = updateOption(style, + '$zHighlight', + config.zHighlight, + defaultStyleOptions$1.zHighlight); + + + let styleNode = this.shadowRoot.querySelector('style'); + + if (styleNode) { + styleNode.remove(); + } + + styleNode = document.createElement('style'); + styleNode.textContent = style; + this.shadowRoot.appendChild(styleNode); + + } + + /* + * @method isElementInViewport + * + * @desc Returns true if element is already visible in view port, + * otheriwse false + * + * @param {Object} element : DOM node of element to highlight + * + * @returns see @desc + */ + + isElementInViewport(element) { + const rect = element.getBoundingClientRect(); + return ( + rect.top >= window.screenY && + rect.left >= window.screenX && + rect.bottom <= ((window.screenY + window.innerHeight) || + (window.screenY + document.documentElement.clientHeight)) && + rect.right <= ((window.screenX + window.innerWidth) || + (window.screenX + document.documentElement.clientWidth))); + } + + + /* + * @method isElementStartInViewport + * + * @desc Returns true if start of the element is already visible in view port, + * otherwise false + * + * @param {Object} element : DOM node of element to highlight + * + * @returns see @desc + */ + + isElementStartInViewport(element) { + const rect = element.getBoundingClientRect(); + return ( + rect.top >= window.screenY && + rect.top <= ((window.screenY + window.innerHeight) || + (window.screenY + document.documentElement.clientHeight)) && + rect.left >= window.screenX && + rect.left <= ((window.screenX + window.innerWidth) || + (window.screenX + document.documentElement.clientWidth))); + } + + + /* + * @method isElementHeightLarge + * + * @desc Returns true if element client height is larger than clientHeight, + * otheriwse false + * + * @param {Object} element : DOM node of element to highlight + * + * @returns see @desc + */ + + isElementInHeightLarge(element) { + var rect = element.getBoundingClientRect(); + return (1.2 * rect.height) > (window.innerHeight || document.documentElement.clientHeight); + } + + /* + * @method highlight + * + * @desc Highlights the element with the id on a page when highlighting + * is enabled (NOTE: Highlight is enabled by default) + * + * @param {Object} elem : DOM node of element to highlight + * @param {String} highlightTarget : value of highlight target + * @param {String} info : Information about target + * @param {Boolean} force : If true override isRduced + */ + + highlight(elem, highlightTarget, info='', force=false) { + const mediaQuery = window.matchMedia(`(prefers-reduced-motion: reduce)`); + const isReduced = !mediaQuery || mediaQuery.matches; + + if (elem && highlightTarget) { + + const scrollElement = this.updateHighlightElement(elem, info); + + if (this.isElementInHeightLarge(elem)) { + if (!this.isElementStartInViewport(elem) && (!isReduced || force)) { + scrollElement.scrollIntoView({ behavior: highlightTarget, block: 'start', inline: 'nearest' }); + } + } + else { + if (!this.isElementInViewport(elem) && (!isReduced || force)) { + scrollElement.scrollIntoView({ behavior: highlightTarget, block: 'center', inline: 'nearest' }); + } + } + } + } + + /* + * @method removeHighlight + * + * @desc Hides the highlight element on the page + */ + removeHighlight() { + if (this.overlayElement) { + this.overlayElement.style.display = 'none'; + } + } + + /* + * @method updateHighlightElement + * + * @desc Create an overlay element and set its position on the page. + * + * @param {Object} element - DOM element node to highlight + * @param {String} info - Description of the element + * + */ + + updateHighlightElement (element, info) { + + let rect = element.getBoundingClientRect(); + + let isHidden = false; + + const rectLeft = rect.left > offset ? + Math.round(rect.left - offset + window.scrollX) : + Math.round(rect.left + window.scrollX); + + let left = rectLeft; + + const rectWidth = rect.left > offset ? + Math.max(rect.width + offset * 2, minWidth) : + Math.max(rect.width, minWidth); + + let width = rectWidth; + + const rectTop = rect.top > offset ? + Math.round(rect.top - offset + window.scrollY) : + Math.round(rect.top + window.scrollY); + + let top = rectTop; + + const rectHeight = rect.top > offset ? + Math.max(rect.height + offset * 2, minHeight) : + Math.max(rect.height, minHeight); + + let height = rectHeight; + + if ((rect.height < 3) || (rect.width < 3)) { + isHidden = true; + } + + if ((rectTop < 0) || (rectLeft < 0)) { + isHidden = true; + if (element.parentNode) { + const parentRect = element.parentNode.getBoundingClientRect(); + + if ((parentRect.top > 0) && (parentRect.left > 0)) { + top = parentRect.top > offset ? + Math.round(parentRect.top - offset + window.scrollY) : + Math.round(parentRect.top + window.scrollY); + left = parentRect.left > offset ? + Math.round(parentRect.left - offset + window.scrollX) : + Math.round(parentRect.left + window.scrollX); + } + else { + left = offset; + top = offset; + } + } + else { + left = offset; + top = offset; + } + } + + this.overlayElem.style.left = left + 'px'; + this.overlayElem.style.top = top + 'px'; + + if (isHidden) { + this.borderElem.textContent = 'Heading is hidden'; + this.borderElem.classList.add('skip-to-hidden'); + this.overlayElem.style.width = 'auto'; + this.overlayElem.style.height = 'auto'; + this.borderElem.style.width = 'auto'; + this.borderElem.style.height = 'auto'; + height = this.borderElem.getBoundingClientRect().height; + width = this.borderElem.getBoundingClientRect().width; + if (rect.top > offset) { + height += offset + 2; + width += offset + 2; + } + } + else { + this.borderElem.textContent = ''; + this.borderElem.classList.remove('skip-to-hidden'); + this.overlayElem.style.width = width + 'px'; + this.overlayElem.style.height = height + 'px'; + this.borderElem.style.width = (width - 2 * borderWidth) + 'px'; + this.borderElem.style.height = (height - 2 * borderWidth) + 'px'; + } + + this.overlayElem.style.display = 'block'; + + if (info) { + this.infoElem.style.display = 'inline-block'; + this.infoElem.textContent = info; + if (top >= this.infoElem.getBoundingClientRect().height) { + this.borderElem.classList.remove('hasInfoBottom'); + this.infoElem.classList.remove('hasInfoBottom'); + this.borderElem.classList.add('hasInfoTop'); + this.infoElem.classList.add('hasInfoTop'); + if (!isHidden) { + this.infoElem.style.top = (-1 * (height + this.infoElem.getBoundingClientRect().height - 2 * borderWidth)) + 'px'; + } + else { + this.infoElem.style.top = (-1 * (this.infoElem.getBoundingClientRect().height + this.borderElem.getBoundingClientRect().height)) + 'px'; + } + } + else { + this.borderElem.classList.remove('hasInfoTop'); + this.infoElem.classList.remove('hasInfoTop'); + this.borderElem.classList.add('hasInfoBottom'); + this.infoElem.classList.add('hasInfoBottom'); + this.infoElem.style.top = -2 + 'px'; + } + return this.infoElem; + } + else { + this.borderElem.classList.remove('hasInfo'); + this.infoElem.style.display = 'none'; + return this.overlayElem; + } + } + } + /* shortcutsMessage.js */ /* Constants */ - const debug$9 = new DebugLogging('[shortcutsMessage]', false); - debug$9.flag = false; - - const defaultStyleOptions = { - fontFamily: 'sans-serif', - fontSize: '12pt', - focusBorderColor: '#c5050c', - focusBorderDarkColor: '#ffffff', - - // Dialog styling defaults - dialogTextColor: '#000000', - dialogTextDarkColor: '#ffffff', - dialogBackgroundColor: '#ffffff', - dialogBackgroundDarkColor: '#000000', - dialogBackgroundTitleColor: '#eeeeee', - dialogBackgroundTitleDarkColor: '#013c93', + const debug$8 = new DebugLogging('[shortcutsMessage]', false); + debug$8.flag = false; - }; + const defaultStyleOptions = colorThemes['default']; const styleTemplate = document.createElement('template'); styleTemplate.textContent = ` @@ -1631,7 +1900,7 @@ button:hover { color-scheme: light dark; } -div#skip-to-message { +#${MESSAGE_ID} { position: fixed; top: 50%; left: 50%; @@ -1650,7 +1919,7 @@ div#skip-to-message { opacity: 1; } -div#skip-to-message .header { +#${MESSAGE_ID} .header { margin: 0; padding: 4px; border-bottom: 1px solid light-dark($focusBorderColor, $focusBorderDarkColor); @@ -1662,7 +1931,7 @@ div#skip-to-message .header { font-size: 100%; } -div#skip-to-message .content { +#${MESSAGE_ID} .content { margin-left: 2em; margin-right: 2em; margin-top: 2em; @@ -1673,34 +1942,34 @@ div#skip-to-message .content { text-algin: center; } -div#skip-to-message.hidden { +#${MESSAGE_ID}.hidden { display: none; } -div#skip-to-message.show { +#${MESSAGE_ID}.show { display: block; opacity: 1; } -div#skip-to-message.fade { +#${MESSAGE_ID}.fade { opacity: 0; transition: visibility 0s 1s, opacity 1s linear; } @media (forced-colors: active) { - div#skip-to-message { + #${MESSAGE_ID} { background-color: Canvas; color CanvasText; border-color: AccentColor; } - div#skip-to-message .header { + #${MESSAGE_ID} .header { background-color: Canvas; color CanvasText; } - div#skip-to-message .content { + #${MESSAGE_ID} .content { background-color: Canvas; color: CanvasText; } @@ -1717,7 +1986,7 @@ div#skip-to-message.fade { // Get references this.messageDialog = document.createElement('div'); - this.messageDialog.id = 'skip-to-message'; + this.messageDialog.id = MESSAGE_ID; this.messageDialog.classList.add('hidden'); this.shadowRoot.appendChild(this.messageDialog); @@ -1847,8 +2116,8 @@ div#skip-to-message.fade { /* constants */ - const debug$8 = new DebugLogging('nameFrom', false); - debug$8.flag = false; + const debug$7 = new DebugLogging('nameFrom', false); + debug$7.flag = false; // // LOW-LEVEL HELPER FUNCTIONS (NOT EXPORTED) @@ -2117,8 +2386,8 @@ div#skip-to-message.fade { /* accName.js */ /* Constants */ - const debug$7 = new DebugLogging('accName', false); - debug$7.flag = false; + const debug$6 = new DebugLogging('accName', false); + debug$6.flag = false; /** * @fuction getAccessibleName @@ -2196,8 +2465,8 @@ div#skip-to-message.fade { /* landmarksHeadings.js */ /* Constants */ - const debug$6 = new DebugLogging('landmarksHeadings', false); - debug$6.flag = false; + const debug$5 = new DebugLogging('landmarksHeadings', false); + debug$5.flag = false; const skipableElements = [ 'base', @@ -2737,7 +3006,7 @@ div#skip-to-message.fade { for (let node = startingNode.firstChild; node !== null; node = node.nextSibling ) { if (node.nodeType === Node.ELEMENT_NODE) { - debug$6.flag && debug$6.log(`[transverseDOM][node]: ${node.tagName} isSlot:${isSlotElement(node)} isCustom:${isCustomElement(node)}`); + debug$5.flag && debug$5.log(`[transverseDOM][node]: ${node.tagName} isSlot:${isSlotElement(node)} isCustom:${isCustomElement(node)}`); checkForLandmark(doc, node); checkForHeading(doc, node, inMain); @@ -3096,293 +3365,6 @@ div#skip-to-message.fade { return [].concat(mainElements, searchElements, navElements, asideElements, regionElements, footerElements, otherElements); } - /* highlight.js */ - - /* Constants */ - const debug$5 = new DebugLogging('highlight', false); - debug$5.flag = false; - - const minWidth = 68; - const minHeight = 27; - const offset = 6; - const borderWidth = 2; - - const OVERLAY_ID = 'id-skip-to-overlay'; - - /* - * @function getOverlayElement - * - * @desc Returns DOM node for the overlay element - * - * @returns {Object} see @desc - */ - - function getOverlayElement() { - - let overlayElem = document.getElementById(OVERLAY_ID); - - if (!overlayElem) { - overlayElem = document.createElement('div'); - overlayElem.style.display = 'none'; - overlayElem.id = OVERLAY_ID; - document.body.appendChild(overlayElem); - - const overlayElemChild = document.createElement('div'); - overlayElemChild.className = 'overlay-border'; - overlayElem.appendChild(overlayElemChild); - } - - const infoElem = overlayElem.querySelector('.overlay-info'); - - if (infoElem === null) { - const overlayInfoChild = document.createElement('div'); - overlayInfoChild.className = 'overlay-info'; - overlayElem.appendChild(overlayInfoChild); - } - - return overlayElem; - } - - /* - * @function isElementInViewport - * - * @desc Returns true if element is already visible in view port, - * otheriwse false - * - * @param {Object} element : DOM node of element to highlight - * - * @returns see @desc - */ - - function isElementInViewport(element) { - var rect = element.getBoundingClientRect(); - return ( - rect.top >= window.screenY && - rect.left >= window.screenX && - rect.bottom <= ((window.screenY + window.innerHeight) || - (window.screenY + document.documentElement.clientHeight)) && - rect.right <= ((window.screenX + window.innerWidth) || - (window.screenX + document.documentElement.clientWidth)) - ); - } - - /* - * @function isElementStartInViewport - * - * @desc Returns true if start of the element is already visible in view port, - * otherwise false - * - * @param {Object} element : DOM node of element to highlight - * - * @returns see @desc - */ - - function isElementStartInViewport(element) { - var rect = element.getBoundingClientRect(); - return ( - rect.top >= window.screenY && - rect.top <= ((window.screenY + window.innerHeight) || - (window.screenY + document.documentElement.clientHeight)) && - rect.left >= window.screenX && - rect.left <= ((window.screenX + window.innerWidth) || - (window.screenX + document.documentElement.clientWidth)) - ); - } - - - /* - * @function isElementHeightLarge - * - * @desc Returns true if element client height is larger than clientHeight, - * otheriwse false - * - * @param {Object} element : DOM node of element to highlight - * - * @returns see @desc - */ - - function isElementInHeightLarge(element) { - var rect = element.getBoundingClientRect(); - return (1.2 * rect.height) > (window.innerHeight || document.documentElement.clientHeight); - } - - /* - * @function highlightElement - * - * @desc Highlights the element with the id on a page when highlighting - * is enabled (NOTE: Highlight is enabled by default) - * - * @param {Object} elem : DOM node of element to highlight - * @param {String} highlightTarget : value of highlight target - * @param {String} info : Information about target - * @param {Boolean} force : If true override isRduced - */ - function highlightElement(elem, highlightTarget, info='', force=false) { - const mediaQuery = window.matchMedia(`(prefers-reduced-motion: reduce)`); - const isReduced = !mediaQuery || mediaQuery.matches; - - if (elem && highlightTarget) { - - const overlayElem = getOverlayElement(); - const scrollElement = updateOverlayElement(overlayElem, elem, info); - - if (isElementInHeightLarge(elem)) { - if (!isElementStartInViewport(elem) && (!isReduced || force)) { - scrollElement.scrollIntoView({ behavior: highlightTarget, block: 'start', inline: 'nearest' }); - } - } - else { - if (!isElementInViewport(elem) && (!isReduced || force)) { - scrollElement.scrollIntoView({ behavior: highlightTarget, block: 'center', inline: 'nearest' }); - } - } - } - } - - /* - * @function removeHighlight - * - * @desc Hides the highlight element on the page - */ - function removeHighlight() { - const overlayElement = getOverlayElement(); - if (overlayElement) { - overlayElement.style.display = 'none'; - } - } - - /* - * @function updateOverlayElement - * - * @desc Create an overlay element and set its position on the page. - * - * @param {Object} overlayElem - DOM element for overlay - * @param {Object} element - DOM element node to highlight - * @param {String} info - Description of the element - * - */ - - function updateOverlayElement (overlayElem, element, info) { - - const childElem = overlayElem.firstElementChild; - const infoElem = overlayElem.querySelector('.overlay-info'); - - let rect = element.getBoundingClientRect(); - - let isHidden = false; - - - const rectLeft = rect.left > offset ? - Math.round(rect.left - offset + window.scrollX) : - Math.round(rect.left + window.scrollX); - - let left = rectLeft; - - const rectWidth = rect.left > offset ? - Math.max(rect.width + offset * 2, minWidth) : - Math.max(rect.width, minWidth); - - let width = rectWidth; - - const rectTop = rect.top > offset ? - Math.round(rect.top - offset + window.scrollY) : - Math.round(rect.top + window.scrollY); - - let top = rectTop; - - const rectHeight = rect.top > offset ? - Math.max(rect.height + offset * 2, minHeight) : - Math.max(rect.height, minHeight); - - let height = rectHeight; - - if ((rect.height < 3) || (rect.width < 3)) { - isHidden = true; - } - - if ((rectTop < 0) || (rectLeft < 0)) { - isHidden = true; - if (element.parentNode) { - const parentRect = element.parentNode.getBoundingClientRect(); - - if ((parentRect.top > 0) && (parentRect.left > 0)) { - top = parentRect.top > offset ? - Math.round(parentRect.top - offset + window.scrollY) : - Math.round(parentRect.top + window.scrollY); - left = parentRect.left > offset ? - Math.round(parentRect.left - offset + window.scrollX) : - Math.round(parentRect.left + window.scrollX); - } - else { - left = offset; - top = offset; - } - } - else { - left = offset; - top = offset; - } - } - - overlayElem.style.left = left + 'px'; - overlayElem.style.top = top + 'px'; - - if (isHidden) { - childElem.textContent = 'Heading is hidden'; - childElem.classList.add('skip-to-hidden'); - overlayElem.style.width = 'auto'; - overlayElem.style.height = 'auto'; - childElem.style.width = 'auto'; - childElem.style.height = 'auto'; - height = childElem.getBoundingClientRect().height; - width = childElem.getBoundingClientRect().width; - if (rect.top > offset) { - height += offset + 2; - width += offset + 2; - } - } - else { - childElem.textContent = ''; - childElem.classList.remove('skip-to-hidden'); - overlayElem.style.width = width + 'px'; - overlayElem.style.height = height + 'px'; - childElem.style.width = (width - 2 * borderWidth) + 'px'; - childElem.style.height = (height - 2 * borderWidth) + 'px'; - } - - overlayElem.style.display = 'block'; - - if (info) { - infoElem.style.display = 'inline-block'; - infoElem.textContent = info; - if (top >= infoElem.getBoundingClientRect().height) { - childElem.classList.remove('hasInfoBottom'); - infoElem.classList.remove('hasInfoBottom'); - childElem.classList.add('hasInfoTop'); - infoElem.classList.add('hasInfoTop'); - if (!isHidden) { - infoElem.style.top = (-1 * (height + infoElem.getBoundingClientRect().height - 2 * borderWidth)) + 'px'; - } - else { - infoElem.style.top = (-1 * (infoElem.getBoundingClientRect().height + childElem.getBoundingClientRect().height)) + 'px'; - } - } - else { - childElem.classList.remove('hasInfoTop'); - infoElem.classList.remove('hasInfoTop'); - childElem.classList.add('hasInfoBottom'); - infoElem.classList.add('hasInfoBottom'); - infoElem.style.top = -2 + 'px'; - } - return infoElem; - } - else { - childElem.classList.remove('hasInfo'); - infoElem.style.display = 'none'; - return overlayElem; - } - } - /* shortcuts.js */ /* Constants */ @@ -3397,7 +3379,10 @@ div#skip-to-message.fade { */ function monitorKeyboardFocus () { document.addEventListener('focusin', () => { - removeHighlight(); + const highlightElem = document.querySelector(HIGHLIGHT_ELEMENT_NAME); + if (highlightElem) { + highlightElem.removeHighlight(); + } }); } @@ -3451,7 +3436,11 @@ div#skip-to-message.fade { } } - highlightElement(elem, 'instant', info, true); // force highlight since navigation + const highlightElem = document.querySelector(HIGHLIGHT_ELEMENT_NAME); + if (highlightElem) { + highlightElem.highlight(elem, 'instant', info, true); // force highlight + } + } return elem; @@ -3764,7 +3753,6 @@ div#skip-to-message.fade { constructor (skipToContentElem) { this.skipToContentElem = skipToContentElem; this.config = skipToContentElem.config; - this.skiptoId = skipToContentElem.skipToId; // check for 'nav' element, if not use 'div' element const ce = this.config.containerElement.toLowerCase().trim() === 'nav' ? 'nav' : 'div'; @@ -3772,7 +3760,7 @@ div#skip-to-message.fade { this.containerNode = document.createElement(ce); skipToContentElem.shadowRoot.appendChild(this.containerNode); - this.containerNode.id = this.skiptoId; + this.containerNode.id = SKIP_TO_ID; if (ce === 'nav') { this.containerNode.setAttribute('aria-label', this.config.buttonLabel); } @@ -3790,7 +3778,7 @@ div#skip-to-message.fade { this.buttonNode.setAttribute('aria-haspopup', 'menu'); this.buttonNode.setAttribute('aria-expanded', 'false'); this.buttonNode.setAttribute('aria-label', buttonAriaLabel); - this.buttonNode.setAttribute('aria-controls', 'id-skip-to-menu'); + this.buttonNode.setAttribute('aria-controls', MENU_ID); this.buttonNode.addEventListener('keydown', this.handleButtonKeydown.bind(this)); this.buttonNode.addEventListener('click', this.handleButtonClick.bind(this)); this.containerNode.appendChild(this.buttonNode); @@ -3814,39 +3802,39 @@ div#skip-to-message.fade { this.menuitemNodes = []; this.menuNode = document.createElement('div'); - this.menuNode.setAttribute('id', 'id-skip-to-menu'); + this.menuNode.setAttribute('id', MENU_ID); this.menuNode.setAttribute('role', 'menu'); this.menuNode.setAttribute('aria-label', this.config.menuLabel); this.containerNode.appendChild(this.menuNode); this.landmarkGroupLabelNode = document.createElement('div'); - this.landmarkGroupLabelNode.setAttribute('id', 'id-skip-to-menu-landmark-group-label'); + this.landmarkGroupLabelNode.setAttribute('id', MENU_LANDMARK_GROUP_LABEL_ID); this.landmarkGroupLabelNode.setAttribute('role', 'separator'); this.landmarkGroupLabelNode.textContent = this.addNumberToGroupLabel(this.config.landmarkGroupLabel); this.menuNode.appendChild(this.landmarkGroupLabelNode); this.landmarkGroupNode = document.createElement('div'); - this.landmarkGroupNode.setAttribute('id', 'id-skip-to-menu-landmark-group'); + this.landmarkGroupNode.setAttribute('id', MENU_LANDMARK_GROUP_ID); this.landmarkGroupNode.setAttribute('role', 'group'); this.landmarkGroupNode.className = 'overflow'; - this.landmarkGroupNode.setAttribute('aria-labelledby', 'id-skip-to-menu-landmark-group-label'); + this.landmarkGroupNode.setAttribute('aria-labelledby', MENU_LANDMARK_GROUP_LABEL_ID); this.menuNode.appendChild(this.landmarkGroupNode); this.headingGroupLabelNode = document.createElement('div'); - this.headingGroupLabelNode.setAttribute('id', 'id-skip-to-menu-heading-group-label'); + this.headingGroupLabelNode.setAttribute('id', MENU_HEADINGS_GROUP_ID); this.headingGroupLabelNode.setAttribute('role', 'separator'); this.headingGroupLabelNode.textContent = this.addNumberToGroupLabel(this.config.headingGroupLabel); this.menuNode.appendChild(this.headingGroupLabelNode); this.headingGroupNode = document.createElement('div'); - this.headingGroupNode.setAttribute('id', 'id-skip-to-menu-heading-group'); + this.headingGroupNode.setAttribute('id', MENU_HEADINGS_GROUP_ID); this.headingGroupNode.setAttribute('role', 'group'); this.headingGroupNode.className = 'overflow'; - this.headingGroupNode.setAttribute('aria-labelledby', 'id-skip-to-menu-heading-group-label'); + this.headingGroupNode.setAttribute('aria-labelledby', MENU_HEADINGS_GROUP_LABEL_ID); this.menuNode.appendChild(this.headingGroupNode); this.shortcutsGroupLabelNode = document.createElement('div'); - this.shortcutsGroupLabelNode.setAttribute('id', 'id-skip-to-menu-shortcuts-group-label'); + this.shortcutsGroupLabelNode.setAttribute('id', MENU_SHORTCUTS_GROUP_LABEL_ID); this.shortcutsGroupLabelNode.setAttribute('role', 'separator'); if (this.config.shortcuts === 'enabled') { this.shortcutsGroupLabelNode.textContent = this.config.shortcutsGroupEnabledLabel; @@ -3857,29 +3845,44 @@ div#skip-to-message.fade { this.menuNode.appendChild(this.shortcutsGroupLabelNode); this.shortcutsGroupNode = document.createElement('div'); - this.shortcutsGroupNode.setAttribute('id', 'id-skip-to-menu-shortcuts-group'); + this.shortcutsGroupNode.setAttribute('id', MENU_SHORTCUTS_GROUP_ID); this.shortcutsGroupNode.setAttribute('role', 'group'); - this.shortcutsGroupNode.setAttribute('aria-labelledby', 'id-skip-to-menu-shortcutse-group-label'); + this.shortcutsGroupNode.setAttribute('aria-labelledby', MENU_SHORTCUTS_GROUP_LABEL_ID); this.menuNode.appendChild(this.shortcutsGroupNode); if (this.config.aboutSupported === 'true') { this.renderAboutToMenu(this.menuNode, this.config); } - this.infoDialog = document.querySelector("skip-to-content-info-dialog"); + // Information dialog + + this.infoDialog = document.querySelector(INFO_DIALOG_ELEMENT_NAME); if (!this.infoDialog) { - window.customElements.define("skip-to-content-info-dialog", SkipToContentInfoDialog); - this.infoDialog = document.createElement('skip-to-content-info-dialog'); + window.customElements.define(INFO_DIALOG_ELEMENT_NAME, SkipToContentInfoDialog); + this.infoDialog = document.createElement(INFO_DIALOG_ELEMENT_NAME); this.infoDialog.configureStyle(this.config); document.body.appendChild(this.infoDialog); } - this.shortcutsMessage = document.querySelector("skip-to-shortcuts-message"); + // Highlight element + + this.highlightElem = document.querySelector(HIGHLIGHT_ELEMENT_NAME); + + if (!this.highlightElem) { + window.customElements.define(HIGHLIGHT_ELEMENT_NAME, HighlightElement); + this.highlightElem = document.createElement(HIGHLIGHT_ELEMENT_NAME); + this.highlightElem.configureStyle(this.config); + document.body.appendChild(this.highlightElem); + } + + // Shortcut messages + + this.shortcutsMessage = document.querySelector(MESSAGE_ELEMENT_NAME); if (!this.shortcutsMessage) { - window.customElements.define("skip-to-shortcuts-message", ShortcutsMessage); - this.shortcutsMessage = document.createElement('skip-to-shortcuts-message'); + window.customElements.define(MESSAGE_ELEMENT_NAME, ShortcutsMessage); + this.shortcutsMessage = document.createElement(MESSAGE_ELEMENT_NAME); this.shortcutsMessage.configureStyle(this.config); document.body.appendChild(this.shortcutsMessage); } @@ -4362,10 +4365,10 @@ div#skip-to-message.fade { this.focusMenuitem = menuitem; if (menuitem.hasAttribute('data-id')) { const elem = queryDOMForSkipToId(menuitem.getAttribute('data-id')); - highlightElement(elem, this.highlightTarget); + this.highlightElem.highlight(elem, this.highlightTarget); } else { - removeHighlight(); + this.highlightElem.removeHighlight(); } } } @@ -4531,7 +4534,7 @@ div#skip-to-message.fade { if (this.isOpen()) { this.buttonNode.setAttribute('aria-expanded', 'false'); this.menuNode.style.display = 'none'; - removeHighlight(); + this.highlightElem.removeHighlight(); } } @@ -4989,10 +4992,10 @@ div#skip-to-message.fade { tgt.classList.add('hover'); if (tgt.hasAttribute('data-id')) { const elem = queryDOMForSkipToId(tgt.getAttribute('data-id')); - highlightElement(elem, this.highlightTarget); + this.highlightElem.highlight(elem, this.highlightTarget); } else { - removeHighlight(); + this.highlightElem.removeHighlight(); } event.stopPropagation(); event.preventDefault(); @@ -5003,10 +5006,10 @@ div#skip-to-message.fade { let tgt = event.currentTarget; if (tgt.hasAttribute('data-id')) { const elem = queryDOMForSkipToId(tgt.getAttribute('data-id')); - highlightElement(elem, this.highlightTarget); + this.highlightElem.highlight(elem, this.highlightTarget); } else { - removeHighlight(); + this.highlightElem.removeHighlight(); } event.stopPropagation(); event.preventDefault(); @@ -5060,10 +5063,10 @@ div#skip-to-message.fade { mi.classList.add('hover'); if (mi.hasAttribute('data-id')) { const elem = queryDOMForSkipToId(mi.getAttribute('data-id')); - highlightElement(elem, this.highlightTarget); + this.highlightElem.highlight(elem, this.highlightTarget); } else { - removeHighlight(); + this.highlightElem.removeHighlight(); } } @@ -5116,15 +5119,17 @@ div#skip-to-message.fade { const debug$1 = new DebugLogging('skiptoContent', false); debug$1.flag = false; + /* @class SkipToContent574 + * + */ - class SkipToContent573 extends HTMLElement { + class SkipToContent574 extends HTMLElement { constructor() { // Always call super first in constructor super(); this.attachShadow({ mode: 'open' }); - this.skipToId = 'id-skip-to'; - this.version = "5.7.3"; + this.version = "5.7.4"; this.buttonSkipTo = false; this.initialized = false; @@ -5281,7 +5286,7 @@ div#skip-to-message.fade { static get observedAttributes() { return [ - "data-skipto", + ATTR_SKIP_TO_DATA, "setfocus", "type", "shortcuts", @@ -5291,7 +5296,7 @@ div#skip-to-message.fade { attributeChangedCallback(name, oldValue, newValue) { - if (name === 'data-skipto') { + if (name === ATTR_SKIP_TO_DATA) { this.config = this.setupConfigFromDataAttribute(this.config, newValue); } @@ -5356,9 +5361,9 @@ div#skip-to-message.fade { } // Check for data-skipto attribute values for configuration - const configElem = document.querySelector('[data-skipto]'); + const configElem = document.querySelector(`[${ATTR_SKIP_TO_DATA}]`); if (configElem) { - const params = configElem.getAttribute('data-skipto'); + const params = configElem.getAttribute(ATTR_SKIP_TO_DATA); this.config = this.setupConfigFromDataAttribute(this.config, params); } @@ -5463,21 +5468,30 @@ div#skip-to-message.fade { } renderStyleElement(this.shadowRoot, config, this.skipToId); + if (this.buttonSkipTo) { this.buttonSkipTo.updateLabels(config); this.buttonSkipTo.setDisplayOption(config['displayOption']); } - const infoDialog = document.querySelector('skip-to-shortcuts-info-dialog'); + const infoDialog = document.querySelector(INFO_DIALOG_ELEMENT_NAME); + debug$1.flag && debug$1.log(`[infoDialog]: ${infoDialog}`); if (infoDialog) { infoDialog.configureStyle(config); } - const shortcutsMessage = document.querySelector('skip-to-shortcuts-message'); + const shortcutsMessage = document.querySelector(MESSAGE_ELEMENT_NAME); + debug$1.flag && debug$1.log(`[shortcutMessage]: ${shortcutsMessage}`); if (shortcutsMessage) { shortcutsMessage.configureStyle(config); } + const highlight = document.querySelector(HIGHLIGHT_ELEMENT_NAME); + debug$1.flag && debug$1.log(`[highlight]: ${highlight}`); + if (highlight) { + highlight.configureStyle(config); + } + return config; } @@ -5508,13 +5522,6 @@ div#skip-to-message.fade { (function() { - const SkipToPageElmName = 'skip-to-content'; - const SkipToBookmarkletElmName = 'skip-to-content-bookmarklet'; - const SkipToExtensionElmName = 'skip-to-content-extension'; - - const SkipToExtensionID = `id-skip-to-extension`; - const SkipToBookmarkletID = `id-skip-to-bookmarklet`; - /* * @function removeLegacySkipToJS * @@ -5532,11 +5539,23 @@ div#skip-to-message.fade { } } + function removeElementsWithName(name) { + let nodes = document.getElementsByTagName(name); + // do more than once in case of duplicates + for(let i = 0; i < nodes.length; i += 1) { + const node = nodes[i]; + console.warn(`[SkipTo.js]: Removing legacy 5.x component: ${name}`); + node.remove(); + } + } + // Remove 5.x legacy code removeElementsWithId('id-skip-to'); removeElementsWithId('id-skip-to-css'); removeElementsWithId('id-skip-to-highlight'); + removeElementsWithName('skip-to-shortcuts-message'); + // Remove 4.x const nodes = document.querySelectorAll('div.skip-to'); debug.flag && debug.log(`[removeLegacySkipToJS]: ${nodes.length}`); @@ -5552,7 +5571,7 @@ div#skip-to-message.fade { * @desc Removes duplicate versions of SkipTo.js */ function removePageSkipTo() { - const nodes = document.querySelectorAll(SkipToPageElmName); + const nodes = document.querySelectorAll(PAGE_SCRIPT_ELEMENT_NAME); debug.flag && debug.log(`[removePageSkipTo]: ${nodes.length}`); for (let i = 0; i < nodes.length; i += 1) { nodes[i].remove(); @@ -5566,7 +5585,7 @@ div#skip-to-message.fade { * @desc Removes duplicate versions of SkipTo.js */ function removeBookmarkletSkipTo() { - const nodes = document.querySelectorAll(SkipToBookmarkletElmName); + const nodes = document.querySelectorAll(BOOKMARKLET_ELEMENT_NAME); debug.flag && debug.log(`[removeBookmarkletSkipTo]: ${nodes.length}`); for (let i = 0; i < nodes.length; i += 1) { nodes[i].remove(); @@ -5587,9 +5606,9 @@ div#skip-to-message.fade { removeLegacySkipToJS(); - const isExtensionLoaded = document.querySelector(SkipToExtensionElmName); - const isBookmarkletLoaded = document.querySelector(SkipToBookmarkletElmName); - const isPageLoaded = document.querySelector(SkipToPageElmName); + const isExtensionLoaded = document.querySelector(EXTENSION_ELEMENT_NAME); + const isBookmarkletLoaded = document.querySelector(BOOKMARKLET_ELEMENT_NAME); + const isPageLoaded = document.querySelector(PAGE_SCRIPT_ELEMENT_NAME); let skipToContentElem = false; @@ -5598,8 +5617,8 @@ div#skip-to-message.fade { if (!isExtensionLoaded) { if (!isBookmarkletLoaded) { removePageSkipTo(); - window.customElements.define(SkipToBookmarkletElmName, SkipToContent573); - skipToContentElem = document.createElement(SkipToBookmarkletElmName); + window.customElements.define(BOOKMARKLET_ELEMENT_NAME, SkipToContent574); + skipToContentElem = document.createElement(BOOKMARKLET_ELEMENT_NAME); skipToContentElem.setAttribute('version', skipToContentElem.version); skipToContentElem.setAttribute('type', type); // always attach SkipToContent element to body @@ -5614,11 +5633,10 @@ div#skip-to-message.fade { if (!isExtensionLoaded) { removePageSkipTo(); removeBookmarkletSkipTo(); - window.customElements.define(SkipToExtensionElmName, SkipToContent573); - skipToContentElem = document.createElement(SkipToExtensionElmName); + window.customElements.define(EXTENSION_ELEMENT_NAME, SkipToContent574); + skipToContentElem = document.createElement(EXTENSION_ELEMENT_NAME); skipToContentElem.setAttribute('version', skipToContentElem.version); skipToContentElem.setAttribute('type', type); - skipToContentElem.setAttribute('about', 'false'); // always attach SkipToContent element to body if (document.body) { document.body.insertBefore(skipToContentElem, document.body.firstElementChild); @@ -5628,8 +5646,8 @@ div#skip-to-message.fade { default: if (!isPageLoaded && !isBookmarkletLoaded && !isExtensionLoaded) { - window.customElements.define(SkipToPageElmName, SkipToContent573); - skipToContentElem = document.createElement(SkipToPageElmName); + window.customElements.define(PAGE_SCRIPT_ELEMENT_NAME, SkipToContent574); + skipToContentElem = document.createElement(PAGE_SCRIPT_ELEMENT_NAME); skipToContentElem.setAttribute('version', skipToContentElem.version); skipToContentElem.setAttribute('type', type); // always attach SkipToContent element to body @@ -5643,7 +5661,7 @@ div#skip-to-message.fade { } // Check for SkipTo.js bookmarklet script, if it is initialize it immediately - if (document.getElementById(SkipToBookmarkletID)) { + if (document.getElementById(SCRIPT_BOOKMARKLET_ID)) { debug.flag && debug.log(`[bookmarklet]`); const skipToContentBookmarkletElem = getSkipToContentElement('bookmarklet'); if (skipToContentBookmarkletElem) { @@ -5654,7 +5672,7 @@ div#skip-to-message.fade { } else { // Check for SkipTo.js extension script, if it is initialize it immediately - if (document.getElementById(SkipToExtensionID)) { + if (document.getElementById(SCRIPT_EXTENSION_ID)) { debug.flag && debug.log(`[extension]`); const skipToContentExtensionElem = getSkipToContentElement('extension'); if (skipToContentExtensionElem) { From 665d18e2a5ad39ed4ae53f4369de9e38029c73fb Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Tue, 1 Jul 2025 11:22:22 -0500 Subject: [PATCH 04/38] fixed bug in label reference --- content/shared/js/skipto.js | 691 +++++++++++++++++++++++------------- 1 file changed, 445 insertions(+), 246 deletions(-) diff --git a/content/shared/js/skipto.js b/content/shared/js/skipto.js index ddadced748..19227abd37 100644 --- a/content/shared/js/skipto.js +++ b/content/shared/js/skipto.js @@ -44,7 +44,10 @@ buttonBackgroundDarkColor: '#013c93', zIndex: '2000000', zHighlight: '1999900', - displayOption: 'fixed' + displayOption: 'fixed', + highlightTarget: 'instant', + highlightBorderSize: 'small', + highlightBorderStyle: 'solid' }, 'aria': { hostnameSelector: 'w3.org', @@ -258,9 +261,9 @@ const BOOKMARKLET_ELEMENT_NAME = 'skip-to-content-bookmarklet'; const EXTENSION_ELEMENT_NAME = 'skip-to-content-extension'; - const INFO_DIALOG_ELEMENT_NAME = 'skip-to-content-info-dialog-574'; - const MESSAGE_ELEMENT_NAME = 'skip-to-content-message-element-574'; - const HIGHLIGHT_ELEMENT_NAME = 'skip-to-content-highlight-element-574'; + const INFO_DIALOG_ELEMENT_NAME = 'skip-to-content-info-dialog-575'; + const MESSAGE_ELEMENT_NAME = 'skip-to-content-message-element-575'; + const HIGHLIGHT_ELEMENT_NAME = 'skip-to-content-highlight-element-575'; // Attributes @@ -926,7 +929,7 @@ const debug$a = new DebugLogging('[shortcutsInfoDialog]', false); debug$a.flag = false; - const defaultStyleOptions$2 = colorThemes['default']; + const defaultStyleOptions$3 = colorThemes['default']; const styleTemplate$1 = document.createElement('template'); @@ -1033,7 +1036,7 @@ dialog#skip-to-info-dialog .content th { margin: 0; padding: 0; padding-top: 0.125em; - padding-buttom: 0.125em; + padding-bottom: 0.125em; text-align: left; font-weight: bold; font-size: 100%; @@ -1053,7 +1056,7 @@ dialog#skip-to-info-dialog .content td { margin: 0; padding: 0; padding-top: 0.125em; - padding-buttom: 0.125em; + padding-bottom: 0.125em; text-align: left; font-size: 100%; } @@ -1173,52 +1176,52 @@ button:hover { style = updateOption(style, '$fontFamily', config.fontFamily, - defaultStyleOptions$2.fontFamily); + defaultStyleOptions$3.fontFamily); style = updateOption(style, '$fontSize', config.fontSize, - defaultStyleOptions$2.fontSize); + defaultStyleOptions$3.fontSize); style = updateOption(style, '$focusBorderColor', config.focusBorderColor, - defaultStyleOptions$2.focusBorderColor); + defaultStyleOptions$3.focusBorderColor); style = updateOption(style, '$focusBorderDarkColor', config.focusBorderDarkColor, - defaultStyleOptions$2.focusBorderDarkColor); + defaultStyleOptions$3.focusBorderDarkColor); style = updateOption(style, '$dialogTextColor', config.dialogTextColor, - defaultStyleOptions$2.dialogTextColor); + defaultStyleOptions$3.dialogTextColor); style = updateOption(style, '$dialogextDarkColor', config.dialogextDarkColor, - defaultStyleOptions$2.dialogextDarkColor); + defaultStyleOptions$3.dialogextDarkColor); style = updateOption(style, '$dialogBackgroundColor', config.dialogBackgroundColor, - defaultStyleOptions$2.dialogBackgroundColor); + defaultStyleOptions$3.dialogBackgroundColor); style = updateOption(style, '$dialogBackgroundDarkColor', config.dialogBackgroundDarkColor, - defaultStyleOptions$2.dialogBackgroundDarkColor); + defaultStyleOptions$3.dialogBackgroundDarkColor); style = updateOption(style, '$dialogBackgroundTitleColor', config.dialogBackgroundTitleColor, - defaultStyleOptions$2.dialogBackgroundTitleColor); + defaultStyleOptions$3.dialogBackgroundTitleColor); style = updateOption(style, '$dialogBackgroundTitleDarkColor', config.dialogBackgroundTitleDarkColor, - defaultStyleOptions$2.dialogBackgroundTitleDarkColor); + defaultStyleOptions$3.dialogBackgroundTitleDarkColor); let styleNode = this.shadowRoot.querySelector('style'); @@ -1403,10 +1406,8 @@ button:hover { const minWidth = 68; const minHeight = 27; - const offset = 6; - const borderWidth = 2; - const defaultStyleOptions$1 = colorThemes['default']; + const defaultStyleOptions$2 = colorThemes['default']; const styleHighlightTemplate = document.createElement('template'); styleHighlightTemplate.textContent = ` @@ -1418,31 +1419,45 @@ button:hover { margin: 0; padding: 0; position: absolute; - border-radius: 3px; - border: 4px solid light-dark($buttonBackgroundColor, $buttonBackgroundDarkColor); + border-radius: $highlightOffsetpx; + border: $shadowBorderWidthpx solid light-dark($menuBackgroundColor, $menuBackgroundDarkColor); box-sizing: border-box; pointer-events:none; + z-index: $zHighlight; +} + +#${HIGHLIGHT_ID}.hasInfoBottom, +#${HIGHLIGHT_ID} .overlay-border.hasInfoBottom { + border-radius: $highlightOffsetpx $highlightOffsetpx $highlightOffsetpx 0; +} + +#${HIGHLIGHT_ID}.hasInfoTop, +#${HIGHLIGHT_ID} .overlay-border.hasInfoTop { + border-radius: 0 $highlightOffsetpx $highlightOffsetpx $highlightOffsetpx; } #${HIGHLIGHT_ID} .overlay-border { margin: 0; padding: 0; position: relative; - top: -2px; - left: -2px; - border-radius: 3px 3px 3px 3px; - border: 2px solid light-dark($focusBorderColor, $focusBorderDarkColor); + border-radius: $highlightOffsetpx; + border: $overlayBorderWidthpx $highlightBorderStyle light-dark($focusBorderColor, $focusBorderDarkColor); z-index: $zHighlight; box-sizing: border-box; pointer-events:none; + background: transparent; } + @keyframes fadeIn { 0% { opacity: 0; } 100% { opacity: 1; } } -#${HIGHLIGHT_ID} .overlay-border.skip-to-hidden { +#hidden-elem-msg { + position: absolute; + margin: 0; + padding: .25em; background-color: light-dark($hiddenHeadingBackgroundColor, $hiddenHeadingBackgroundDarkColor); color: light-dark($hiddenHeadingColor, $hiddenHeadingDarkColor); font-family: $fontFamily; @@ -1450,26 +1465,18 @@ button:hover { font-style: italic; font-weight: bold; text-align: center; - padding: .25em; animation: fadeIn 1.5s; -} - -#${HIGHLIGHT_ID} .overlay-border.hasInfoBottom { - border-radius: 3px 3px 3px 0; -} - -#${HIGHLIGHT_ID} .overlay-border.hasInfoTop { - border-radius: 0 3px 3px 3px; + z-index: $zHighlight; } #${HIGHLIGHT_ID} .overlay-info { + margin: 0; + padding: 2px; position: relative; text-align: left; - left: -2px; - padding: 1px 4px; font-size: $fontSize; font-family: $fontFamily; - border: 2px solid light-dark($focusBorderColor, $focusBorderDarkColor); + border: $infoBorderWidthpx solid light-dark($menuBackgroundColor, $menuBackgroundDarkColor); background-color: light-dark($menuBackgroundColor, $menuBackgroundDarkColor); color: light-dark($menuTextColor, $menuTextDarkColor); z-index: $zHighlight; @@ -1479,11 +1486,11 @@ button:hover { } #${HIGHLIGHT_ID} .overlay-info.hasInfoTop { - border-radius: 3px 3px 0 0; + border-radius: $highlightOffsetpx $highlightOffsetpx 0 0; } #${HIGHLIGHT_ID} .overlay-info.hasInfoBottom { - border-radius: 0 0 3px 3px; + border-radius: 0 0 $highlightOffsetpx $highlightOffsetpx; } @media (forced-colors: active) { @@ -1536,6 +1543,17 @@ button:hover { this.infoElem.className = 'overlay-info'; this.overlayElem.appendChild(this.infoElem); + this.hiddenElem = document.createElement('div'); + this.hiddenElem.id = 'hidden-elem-msg'; + this.shadowRoot.appendChild(this.hiddenElem); + this.hiddenElem.style.display = 'none'; + + this.borderWidth = 0; + this.borderContrast = 0; + this.offset = 0; + + this.msgHeadingIsHidden = ''; + this.configureStyle(); } @@ -1548,7 +1566,6 @@ button:hover { * @param {Object} config : color and font information */ - configureStyle(config={}) { function updateOption(style, option, configOption, defaultOption) { @@ -1560,84 +1577,164 @@ button:hover { } } + // Get i18n Messages + + this.msgHeadingIsHidden = typeof config.msgHeadingIsHidden === 'string' ? + config.msgHeadingIsHidden : + 'Heading is hidden'; + + this.msgRegionIsHidden = typeof config.msgRegionIsHidden === 'string' ? + config.msgRegionIsHidden : + 'Region is hidden'; + + this.msgElementIsHidden = typeof config.msgElementIsHidden === 'string' ? + config.msgElemenIsHidden : + 'Element is hidden'; + + // make a copy of the template let style = styleHighlightTemplate.textContent.slice(0); style = updateOption(style, '$fontFamily', config.fontFamily, - defaultStyleOptions$1.fontFamily); - - style = updateOption(style, - '$fontSize', - config.fontSize, - defaultStyleOptions$1.fontSize); + defaultStyleOptions$2.fontFamily); style = updateOption(style, '$buttonBackgroundColor', config.buttonBackgroundColor, - defaultStyleOptions$1.buttonBackgroundColor); + defaultStyleOptions$2.buttonBackgroundColor); style = updateOption(style, '$buttonBackgroundDarkColor', config.buttonBackgroundDarkColor, - defaultStyleOptions$1.buttonBackgroundDarkColor); + defaultStyleOptions$2.buttonBackgroundDarkColor); style = updateOption(style, '$focusBorderColor', config.focusBorderColor, - defaultStyleOptions$1.focusBorderColor); + defaultStyleOptions$2.focusBorderColor); style = updateOption(style, '$focusBorderDarkColor', config.focusBorderDarkColor, - defaultStyleOptions$1.focusBorderDarkColor); + defaultStyleOptions$2.focusBorderDarkColor); style = updateOption(style, '$menuBackgroundColor', config.menuBackgroundColor, - defaultStyleOptions$1.menuBackgroundColor); + defaultStyleOptions$2.menuBackgroundColor); style = updateOption(style, '$menuBackgroundDarkColor', config.menuBackgroundDarkColor, - defaultStyleOptions$1.menuBackgroundDarkColor); + defaultStyleOptions$2.menuBackgroundDarkColor); style = updateOption(style, '$menuTextColor', config.menuTextColor, - defaultStyleOptions$1.menuTextColor); + defaultStyleOptions$2.menuTextColor); style = updateOption(style, '$menuTextDarkColor', config.menuTextDarkColor, - defaultStyleOptions$1.menuTextDarkColor); + defaultStyleOptions$2.menuTextDarkColor); style = updateOption(style, '$hiddenHeadingColor', config.hiddenHeadingColor, - defaultStyleOptions$1.hiddenHeadingColor); + defaultStyleOptions$2.hiddenHeadingColor); style = updateOption(style, '$hiddenHeadingDarkColor', config.hiddenHeadingDarkColor, - defaultStyleOptions$1.hiddenHeadingDarkColor); + defaultStyleOptions$2.hiddenHeadingDarkColor); style = updateOption(style, '$hiddenHeadingBackgroundColor', config.hiddenHeadingBackgroundColor, - defaultStyleOptions$1.hiddenHeadingBackgroundColor); + defaultStyleOptions$2.hiddenHeadingBackgroundColor); style = updateOption(style, '$hiddenHeadingBackgroundDarkColor', config.hiddenHeadingBackgroundDarkColor, - defaultStyleOptions$1.hiddenHeadingBackgroundDarkColor); + defaultStyleOptions$2.hiddenHeadingBackgroundDarkColor); style = updateOption(style, '$zHighlight', config.zHighlight, - defaultStyleOptions$1.zHighlight); + defaultStyleOptions$2.zHighlight); + + style = updateOption(style, + '$highlightBorderStyle', + config.highlightBorderStyle, + defaultStyleOptions$2.highlightBorderStyle); + + const highlightBorderSize = config.highlightBorderSize ? + config.highlightBorderSize : + defaultStyleOptions$2.highlightBorderSize; + + switch (highlightBorderSize) { + case 'small': + this.borderWidth = 2; + this.borderContrast = 1; + this.offset = 4; + this.fontSize = '12pt'; + break; + + case 'medium': + this.borderWidth = 3; + this.borderContrast = 2; + this.offset = 4; + this.fontSize = '13pt'; + break; + + case 'large': + this.borderWidth = 4; + this.borderContrast = 3; + this.offset = 6; + this.fontSize = '14pt'; + break; + + case 'x-large': + this.borderWidth = 6; + this.borderContrast = 3; + this.offset = 8; + this.fontSize = '16pt'; + break; + default: + this.borderWidth = 2; + this.borderContrast = 1; + this.offset = 4; + this.fontSize = '12pt'; + break; + } + + style = updateOption(style, + '$fontSize', + this.fontSize, + defaultStyleOptions$2.fontSize); + + style = updateOption(style, + '$highlightOffset', + this.offset, + this.offset); + + style = updateOption(style, + '$overlayBorderWidth', + this.borderWidth, + this.borderWidth); + + style = updateOption(style, + '$shadowBorderWidth', + this.borderWidth + 2 * this.borderContrast, + this.borderWidth + 2 * this.borderContrast); + + style = updateOption(style, + '$infoBorderWidth', + this.borderWidth, + this.borderWidth); let styleNode = this.shadowRoot.querySelector('style'); @@ -1651,19 +1748,217 @@ button:hover { } + /* + * @method highlight + * + * @desc Highlights the element on the page when highlighting + * is enabled (NOTE: Highlight is enabled by default) + * + * @param {Object} elem : DOM node of element to highlight + * @param {String} highlightTarget : value of highlight target + * @param {String} info : Information about target + * @param {Boolean} force : If true override isRduced + */ + + highlight(elem, highlightTarget, info='', force=false) { + let scrollElement; + const mediaQuery = window.matchMedia(`(prefers-reduced-motion: reduce)`); + const isReduced = !mediaQuery || mediaQuery.matches; + + if (elem && highlightTarget) { + + const rect = elem.getBoundingClientRect(); + + // If target element is hidden create a visible element + debug$9.flag && debug$9.log(`[ info]: ${info}`); + debug$9.flag && debug$9.log(`[ rect]: Left: ${rect.left} Top: ${rect.top} Width: ${rect.width} height: ${rect.height}`); + debug$9.flag && debug$9.log(`[isHidden]: ${this.isElementHidden(elem)}`); + + if (this.isElementHidden(elem)) { + // If element is hidden make hidden element message visible + // and use for highlighing + this.hiddenElem.textContent = this.getHiddenMessage(elem); + this.hiddenElem.style.display = 'block'; + + const left = rect.left > 0 ? rect.left + window.scrollX : this.offset; + const top = rect.top > 0 ? rect.top + window.scrollY : this.offset; + + this.hiddenElem.style.left = left + 'px'; + this.hiddenElem.style.top = top + 'px'; + scrollElement = this.updateHighlightElement(this.hiddenElem, + info, + 0, + this.borderWidth, + this.borderContrast); + } + else { + this.hiddenElem.style.display = 'none'; + scrollElement = this.updateHighlightElement(elem, + info, + this.offset, + this.borderWidth, + this.borderContrast); + } + + if (this.isElementInHeightLarge(elem)) { + if (!this.isElementStartInViewport(elem) && (!isReduced || force)) { + scrollElement.scrollIntoView({ behavior: highlightTarget, block: 'start', inline: 'nearest' }); + } + } + else { + if (!this.isElementInViewport(elem) && (!isReduced || force)) { + scrollElement.scrollIntoView({ behavior: highlightTarget, block: 'center', inline: 'nearest' }); + } + } + } + } + + /* + * @method updateHighlightElement + * + * @desc Create an overlay element and set its position on the page. + * + * @param {Object} elem - DOM element node to highlight + * @param {String} info - Description of the element + * @param {Number} offset - Number of pixels for offset + * @param {Number} borderWidth - Number of pixels for border width + * @param {Number} borderContrast - Number of pixels to provide border contrast + * + */ + + updateHighlightElement (elem, info, offset, borderWidth, borderContrast) { + + const adjRect = this.getAdjustedRect(elem, offset, borderWidth, borderContrast); + + const borderElemOffset = -1 * (this.borderWidth + this.borderContrast); + + this.overlayElem.style.left = adjRect.left + 'px'; + this.overlayElem.style.top = adjRect.top + 'px'; + this.borderElem.style.left = borderElemOffset + 'px'; + this.borderElem.style.top = borderElemOffset + 'px'; + + this.overlayElem.style.width = adjRect.width + 'px'; + this.overlayElem.style.height = adjRect.height + 'px'; + this.borderElem.style.width = (adjRect.width - (2 * borderContrast)) + 'px'; + this.borderElem.style.height = (adjRect.height - (2 * borderContrast)) + 'px'; + + this.overlayElem.style.display = 'block'; + + if (info) { + + this.infoElem.style.display = 'inline-block'; + this.infoElem.textContent = info; + + const infoElemOffsetLeft = -1 * (borderWidth + 2 * borderContrast); + this.infoElem.style.left = infoElemOffsetLeft + 'px'; + + const infoElemRect = this.infoElem.getBoundingClientRect(); + + // Is info displayed above or below the highlighted element + if (adjRect.top >= infoElemRect.height) { + // Info is displayed above the highlighted element (e.g. most of the time) + this.overlayElem.classList.remove('hasInfoBottom'); + this.borderElem.classList.remove('hasInfoBottom'); + this.infoElem.classList.remove('hasInfoBottom'); + this.overlayElem.classList.add('hasInfoTop'); + this.borderElem.classList.add('hasInfoTop'); + this.infoElem.classList.add('hasInfoTop'); + this.infoElem.style.top = (-1 * (adjRect.height + + infoElemRect.height + + borderWidth)) + 'px'; + } + else { + // Info is displayed below the highlighted element when it is at the top of + // the window + + const infoElemOffsetTop = -1 * (borderWidth + borderContrast); + + this.overlayElem.classList.remove('hasInfoTop'); + this.borderElem.classList.remove('hasInfoTop'); + this.infoElem.classList.remove('hasInfoTop'); + this.overlayElem.classList.add('hasInfoBottom'); + this.borderElem.classList.add('hasInfoBottom'); + this.infoElem.classList.add('hasInfoBottom'); + this.infoElem.style.top = infoElemOffsetTop + 'px'; + } + return this.infoElem; + } + else { + this.overlayElem.classList.remove('hasInfoTop'); + this.overlayElem.classList.remove('hasInfoBottom'); + this.borderElem.classList.remove('hasInfoTop'); + this.borderElem.classList.remove('hasInfoBottom'); + this.infoElem.style.display = 'none'; + return this.overlayElem; + } + } + + + /* + * @method getAdjustedRect + * + * @desc Returns a object with dimensions adjusted for highlighting element + * + * @param {Object} elem - DOM node of element to be highlighted + * @param {Number} offset - Number of pixels for offset + * @param {Number} borderWidth - Number of pixels for border width + * @param {Number} borderContrast - Number of pixels to provide border contrast + * + * @returns see @desc + */ + + getAdjustedRect(elem, offset, borderWidth, borderContrast) { + + const rect = elem.getBoundingClientRect(); + + const adjRect = { + left: 0, + top: 0, + width: 0, + height: 0 + }; + + const offsetBorder = offset + borderWidth + 2 * borderContrast; + + adjRect.left = rect.left > offset ? + Math.round(rect.left + (-1 * offsetBorder) + window.scrollX) : + Math.round(rect.left + window.scrollX); + + adjRect.width = rect.left > offset ? + Math.max(rect.width + (2 * offsetBorder), minWidth) : + Math.max(rect.width, minWidth); + + + adjRect.top = rect.top > offset ? + Math.round(rect.top + (-1 * offsetBorder) + window.scrollY) : + Math.round(rect.top + window.scrollY); + + adjRect.height = rect.top > offset ? + Math.max(rect.height + (2 * offsetBorder), minHeight) : + Math.max(rect.height, minHeight); + + if ((adjRect.top < 0) || (adjRect.left < 0)) { + // Element is near top or left side of screen + adjRect.left = this.offset; + adjRect.top = this.offset; + } + + return adjRect; + } + /* * @method isElementInViewport * * @desc Returns true if element is already visible in view port, * otheriwse false * - * @param {Object} element : DOM node of element to highlight + * @param {Object} elem : DOM node of element to highlight * * @returns see @desc */ - isElementInViewport(element) { - const rect = element.getBoundingClientRect(); + isElementInViewport(elem) { + const rect = elem.getBoundingClientRect(); return ( rect.top >= window.screenY && rect.left >= window.screenX && @@ -1673,20 +1968,19 @@ button:hover { (window.screenX + document.documentElement.clientWidth))); } - /* * @method isElementStartInViewport * * @desc Returns true if start of the element is already visible in view port, * otherwise false * - * @param {Object} element : DOM node of element to highlight + * @param {Object} elem : DOM node of element to highlight * * @returns see @desc */ - isElementStartInViewport(element) { - const rect = element.getBoundingClientRect(); + isElementStartInViewport(elem) { + const rect = elem.getBoundingClientRect(); return ( rect.top >= window.screenY && rect.top <= ((window.screenY + window.innerHeight) || @@ -1703,186 +1997,70 @@ button:hover { * @desc Returns true if element client height is larger than clientHeight, * otheriwse false * - * @param {Object} element : DOM node of element to highlight + * @param {Object} elem : DOM node of element to highlight * * @returns see @desc */ - isElementInHeightLarge(element) { - var rect = element.getBoundingClientRect(); + isElementInHeightLarge(elem) { + var rect = elem.getBoundingClientRect(); return (1.2 * rect.height) > (window.innerHeight || document.documentElement.clientHeight); } /* - * @method highlight + * @method isElementHidden * - * @desc Highlights the element with the id on a page when highlighting - * is enabled (NOTE: Highlight is enabled by default) + * @desc Returns true if the element is hidden on the + * graphical rendering * - * @param {Object} elem : DOM node of element to highlight - * @param {String} highlightTarget : value of highlight target - * @param {String} info : Information about target - * @param {Boolean} force : If true override isRduced - */ - - highlight(elem, highlightTarget, info='', force=false) { - const mediaQuery = window.matchMedia(`(prefers-reduced-motion: reduce)`); - const isReduced = !mediaQuery || mediaQuery.matches; - - if (elem && highlightTarget) { - - const scrollElement = this.updateHighlightElement(elem, info); - - if (this.isElementInHeightLarge(elem)) { - if (!this.isElementStartInViewport(elem) && (!isReduced || force)) { - scrollElement.scrollIntoView({ behavior: highlightTarget, block: 'start', inline: 'nearest' }); - } - } - else { - if (!this.isElementInViewport(elem) && (!isReduced || force)) { - scrollElement.scrollIntoView({ behavior: highlightTarget, block: 'center', inline: 'nearest' }); - } - } - } - } - - /* - * @method removeHighlight + * @param {Object} elem : DOM node * - * @desc Hides the highlight element on the page + * @returns see @desc */ - removeHighlight() { - if (this.overlayElement) { - this.overlayElement.style.display = 'none'; - } + isElementHidden(elem) { + const rect = elem.getBoundingClientRect(); + return (rect.height < 3) || + (rect.width < 3) || + ((rect.left + rect.width) < (rect.width / 2)) || + ((rect.top + rect.height) < (rect.height / 2)); } /* - * @method updateHighlightElement + * @method getHiddenMessage * - * @desc Create an overlay element and set its position on the page. + * @desc Returns string describing the hidden element * - * @param {Object} element - DOM element node to highlight - * @param {String} info - Description of the element + * @param {Object} elem : DOM node * + * @returns see @desc */ + getHiddenMessage(elem) { + if (elem.hasAttribute('data-skip-to-info')) { + const info = elem.getAttribute('data-skip-to-info'); - updateHighlightElement (element, info) { - - let rect = element.getBoundingClientRect(); - - let isHidden = false; - - const rectLeft = rect.left > offset ? - Math.round(rect.left - offset + window.scrollX) : - Math.round(rect.left + window.scrollX); - - let left = rectLeft; - - const rectWidth = rect.left > offset ? - Math.max(rect.width + offset * 2, minWidth) : - Math.max(rect.width, minWidth); - - let width = rectWidth; - - const rectTop = rect.top > offset ? - Math.round(rect.top - offset + window.scrollY) : - Math.round(rect.top + window.scrollY); - - let top = rectTop; - - const rectHeight = rect.top > offset ? - Math.max(rect.height + offset * 2, minHeight) : - Math.max(rect.height, minHeight); - - let height = rectHeight; - - if ((rect.height < 3) || (rect.width < 3)) { - isHidden = true; - } - - if ((rectTop < 0) || (rectLeft < 0)) { - isHidden = true; - if (element.parentNode) { - const parentRect = element.parentNode.getBoundingClientRect(); - - if ((parentRect.top > 0) && (parentRect.left > 0)) { - top = parentRect.top > offset ? - Math.round(parentRect.top - offset + window.scrollY) : - Math.round(parentRect.top + window.scrollY); - left = parentRect.left > offset ? - Math.round(parentRect.left - offset + window.scrollX) : - Math.round(parentRect.left + window.scrollX); - } - else { - left = offset; - top = offset; - } - } - else { - left = offset; - top = offset; + if (info.includes('heading')) { + return this.msgHeadingIsHidden; } - } - this.overlayElem.style.left = left + 'px'; - this.overlayElem.style.top = top + 'px'; - - if (isHidden) { - this.borderElem.textContent = 'Heading is hidden'; - this.borderElem.classList.add('skip-to-hidden'); - this.overlayElem.style.width = 'auto'; - this.overlayElem.style.height = 'auto'; - this.borderElem.style.width = 'auto'; - this.borderElem.style.height = 'auto'; - height = this.borderElem.getBoundingClientRect().height; - width = this.borderElem.getBoundingClientRect().width; - if (rect.top > offset) { - height += offset + 2; - width += offset + 2; + if (info.includes('landmark')) { + return this.msgRegionIsHidden; } } - else { - this.borderElem.textContent = ''; - this.borderElem.classList.remove('skip-to-hidden'); - this.overlayElem.style.width = width + 'px'; - this.overlayElem.style.height = height + 'px'; - this.borderElem.style.width = (width - 2 * borderWidth) + 'px'; - this.borderElem.style.height = (height - 2 * borderWidth) + 'px'; - } - this.overlayElem.style.display = 'block'; + return this.msgElementIsHidden; + } - if (info) { - this.infoElem.style.display = 'inline-block'; - this.infoElem.textContent = info; - if (top >= this.infoElem.getBoundingClientRect().height) { - this.borderElem.classList.remove('hasInfoBottom'); - this.infoElem.classList.remove('hasInfoBottom'); - this.borderElem.classList.add('hasInfoTop'); - this.infoElem.classList.add('hasInfoTop'); - if (!isHidden) { - this.infoElem.style.top = (-1 * (height + this.infoElem.getBoundingClientRect().height - 2 * borderWidth)) + 'px'; - } - else { - this.infoElem.style.top = (-1 * (this.infoElem.getBoundingClientRect().height + this.borderElem.getBoundingClientRect().height)) + 'px'; - } - } - else { - this.borderElem.classList.remove('hasInfoTop'); - this.infoElem.classList.remove('hasInfoTop'); - this.borderElem.classList.add('hasInfoBottom'); - this.infoElem.classList.add('hasInfoBottom'); - this.infoElem.style.top = -2 + 'px'; - } - return this.infoElem; - } - else { - this.borderElem.classList.remove('hasInfo'); - this.infoElem.style.display = 'none'; - return this.overlayElem; + /* + * @method removeHighlight + * + * @desc Hides the highlight element on the page + */ + removeHighlight() { + if (this.overlayElem) { + this.overlayElem.style.display = 'none'; } } + } /* shortcutsMessage.js */ @@ -1891,7 +2069,7 @@ button:hover { const debug$8 = new DebugLogging('[shortcutsMessage]', false); debug$8.flag = false; - const defaultStyleOptions = colorThemes['default']; + const defaultStyleOptions$1 = colorThemes['default']; const styleTemplate = document.createElement('template'); styleTemplate.textContent = ` @@ -2021,53 +2199,53 @@ button:hover { style = updateOption(style, '$fontFamily', config.fontFamily, - defaultStyleOptions.fontFamily); + defaultStyleOptions$1.fontFamily); style = updateOption(style, '$fontSize', config.fontSize, - defaultStyleOptions.fontSize); + defaultStyleOptions$1.fontSize); style = updateOption(style, '$focusBorderColor', config.focusBorderColor, - defaultStyleOptions.focusBorderColor); + defaultStyleOptions$1.focusBorderColor); style = updateOption(style, '$focusBorderDarkColor', config.focusBorderDarkColor, - defaultStyleOptions.focusBorderDarkColor); + defaultStyleOptions$1.focusBorderDarkColor); style = updateOption(style, '$dialogTextColor', config.dialogTextColor, - defaultStyleOptions.dialogTextColor); + defaultStyleOptions$1.dialogTextColor); style = updateOption(style, '$dialogTextDarkColor', config.dialogTextDarkColor, - defaultStyleOptions.dialogTextDarkColor); + defaultStyleOptions$1.dialogTextDarkColor); style = updateOption(style, '$dialogBackgroundColor', config.dialogBackgroundColor, - defaultStyleOptions.dialogBackgroundColor); + defaultStyleOptions$1.dialogBackgroundColor); style = updateOption(style, '$dialogBackgroundDarkColor', config.dialogBackgroundDarkColor, - defaultStyleOptions.dialogBackgroundDarkColor); + defaultStyleOptions$1.dialogBackgroundDarkColor); style = updateOption(style, '$dialogBackgroundTitleColor', config.dialogBackgroundTitleColor, - defaultStyleOptions.dialogBackgroundTitleColor); + defaultStyleOptions$1.dialogBackgroundTitleColor); style = updateOption(style, '$dialogBackgroundTitleDarkColor', config.dialogBackgroundTitleDarkColor, - defaultStyleOptions.dialogBackgroundTitleDarkColor); + defaultStyleOptions$1.dialogBackgroundTitleDarkColor); let styleNode = this.shadowRoot.querySelector('style'); @@ -3268,6 +3446,7 @@ button:hover { let navElements = []; let asideElements = []; let footerElements = []; + let headerElements = []; let regionElements = []; let otherElements = []; let dataId = ''; @@ -3347,12 +3526,21 @@ button:hover { case 'footer': footerElements.push(landmarkItem); break; + case 'header': + headerElements.push(landmarkItem); + break; case 'section': // Regions must have accessible name to be included if (landmarkItem.hasName) { regionElements.push(landmarkItem); } break; + case 'form': + // Forms must have accessible name to be included + if (landmarkItem.hasName) { + otherElements.push(landmarkItem); + } + break; default: otherElements.push(landmarkItem); break; @@ -3362,7 +3550,7 @@ button:hover { if (config.landmarks.includes('doc-order')) { return allElements; } - return [].concat(mainElements, searchElements, navElements, asideElements, regionElements, footerElements, otherElements); + return [].concat(mainElements, searchElements, navElements, asideElements, regionElements, footerElements, headerElements, otherElements); } /* shortcuts.js */ @@ -3808,26 +3996,26 @@ button:hover { this.containerNode.appendChild(this.menuNode); this.landmarkGroupLabelNode = document.createElement('div'); - this.landmarkGroupLabelNode.setAttribute('id', MENU_LANDMARK_GROUP_LABEL_ID); + this.landmarkGroupLabelNode.id = MENU_LANDMARK_GROUP_LABEL_ID; this.landmarkGroupLabelNode.setAttribute('role', 'separator'); this.landmarkGroupLabelNode.textContent = this.addNumberToGroupLabel(this.config.landmarkGroupLabel); this.menuNode.appendChild(this.landmarkGroupLabelNode); this.landmarkGroupNode = document.createElement('div'); - this.landmarkGroupNode.setAttribute('id', MENU_LANDMARK_GROUP_ID); + this.landmarkGroupNode.id = MENU_LANDMARK_GROUP_ID; this.landmarkGroupNode.setAttribute('role', 'group'); this.landmarkGroupNode.className = 'overflow'; this.landmarkGroupNode.setAttribute('aria-labelledby', MENU_LANDMARK_GROUP_LABEL_ID); this.menuNode.appendChild(this.landmarkGroupNode); this.headingGroupLabelNode = document.createElement('div'); - this.headingGroupLabelNode.setAttribute('id', MENU_HEADINGS_GROUP_ID); + this.headingGroupLabelNode.id = MENU_HEADINGS_GROUP_LABEL_ID; this.headingGroupLabelNode.setAttribute('role', 'separator'); this.headingGroupLabelNode.textContent = this.addNumberToGroupLabel(this.config.headingGroupLabel); this.menuNode.appendChild(this.headingGroupLabelNode); this.headingGroupNode = document.createElement('div'); - this.headingGroupNode.setAttribute('id', MENU_HEADINGS_GROUP_ID); + this.headingGroupNode.id = MENU_HEADINGS_GROUP_ID; this.headingGroupNode.setAttribute('role', 'group'); this.headingGroupNode.className = 'overflow'; this.headingGroupNode.setAttribute('aria-labelledby', MENU_HEADINGS_GROUP_LABEL_ID); @@ -5119,17 +5307,19 @@ button:hover { const debug$1 = new DebugLogging('skiptoContent', false); debug$1.flag = false; - /* @class SkipToContent574 + const defaultStyleOptions = colorThemes['default']; + + /* @class SkipToContent575 * */ - class SkipToContent574 extends HTMLElement { + class SkipToContent575 extends HTMLElement { constructor() { // Always call super first in constructor super(); this.attachShadow({ mode: 'open' }); - this.version = "5.7.4"; + this.version = "5.7.5"; this.buttonSkipTo = false; this.initialized = false; @@ -5195,10 +5385,15 @@ button:hover { msgKey: 'Key', msgDescription: 'Description', - msgNextRegion: 'Next region', + msgElementHidden: 'Element is hidden', + + msgNextRegion: 'Next region', msgPreviousRegion: 'Previous region', - msgNextHeading: 'Next heading', + msgRegionIsHidden: 'Region is hidden', + + msgNextHeading: 'Next heading', msgPreviousHeading: 'Previous heading', + msgHeadingIsHidden: 'Heading is hidden', msgMainRegions: 'Main regions', msgNavigationRegions: 'Navigation regions', @@ -5241,10 +5436,14 @@ button:hover { headings: 'main-only h1 h2', // Highlight options - highlightTarget: 'instant', // options: 'instant' (default), 'smooth' and 'auto' + highlightTarget: defaultStyleOptions.highlightTarget, + // options: 'instant' (default), 'smooth' and 'auto' + highlightBorderSize: defaultStyleOptions.highlightBorderSize, + // options: 'small' (default), 'medium', 'large', 'x-large' + highlightBorderStyle: defaultStyleOptions.highlightBorderStyle, + // options: 'solid' (default), 'dotted', 'dashed' // Hidden heading when highlighting - msgHidden: 'Heading is hidden', hiddenHeadingColor: '#000000', hiddenHeadingDarkColor: '#000000', hiddenHeadingBackgroundColor: '#ffcc00', @@ -5617,7 +5816,7 @@ button:hover { if (!isExtensionLoaded) { if (!isBookmarkletLoaded) { removePageSkipTo(); - window.customElements.define(BOOKMARKLET_ELEMENT_NAME, SkipToContent574); + window.customElements.define(BOOKMARKLET_ELEMENT_NAME, SkipToContent575); skipToContentElem = document.createElement(BOOKMARKLET_ELEMENT_NAME); skipToContentElem.setAttribute('version', skipToContentElem.version); skipToContentElem.setAttribute('type', type); @@ -5633,7 +5832,7 @@ button:hover { if (!isExtensionLoaded) { removePageSkipTo(); removeBookmarkletSkipTo(); - window.customElements.define(EXTENSION_ELEMENT_NAME, SkipToContent574); + window.customElements.define(EXTENSION_ELEMENT_NAME, SkipToContent575); skipToContentElem = document.createElement(EXTENSION_ELEMENT_NAME); skipToContentElem.setAttribute('version', skipToContentElem.version); skipToContentElem.setAttribute('type', type); @@ -5646,7 +5845,7 @@ button:hover { default: if (!isPageLoaded && !isBookmarkletLoaded && !isExtensionLoaded) { - window.customElements.define(PAGE_SCRIPT_ELEMENT_NAME, SkipToContent574); + window.customElements.define(PAGE_SCRIPT_ELEMENT_NAME, SkipToContent575); skipToContentElem = document.createElement(PAGE_SCRIPT_ELEMENT_NAME); skipToContentElem.setAttribute('version', skipToContentElem.version); skipToContentElem.setAttribute('type', type); From 559c21f76eb96d4d4c88a4fa7809660643720bb4 Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Tue, 1 Jul 2025 13:26:34 -0500 Subject: [PATCH 05/38] updated configuration for headings anywhere on page --- content/shared/js/skipto.js | 4053 ++++++++++++++++++----------------- 1 file changed, 2027 insertions(+), 2026 deletions(-) diff --git a/content/shared/js/skipto.js b/content/shared/js/skipto.js index 19227abd37..b92d0832a2 100644 --- a/content/shared/js/skipto.js +++ b/content/shared/js/skipto.js @@ -1,279 +1,280 @@ -/* ======================================================================== - * Version: 5.7 - * Copyright (c) 2022, 2023, 2024, 2025 Jon Gunderson; Licensed BSD - * Copyright (c) 2021 PayPal Accessibility Team and University of Illinois; Licensed BSD - * All rights reserved. - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of PayPal or any of its subsidiaries or affiliates, nor the name of the University of Illinois, nor the names of any other contributors contributors may be used to endorse or promote products derived from this software without specific prior written permission. - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * CDN: https://skipto-landmarks-headings.github.io/page-script-5/dist/skipto.min.js - * Documentation: https://skipto-landmarks-headings.github.io/page-script-5 - * Code: https://github.com/skipto-landmarks-headings/page-script-5 - * Report Issues: https://github.com/skipto-landmarks-headings/page-script-5/issues - * ======================================================================== */ - -(function () { - 'use strict'; - - /* colorThemes */ - - const colorThemes = { - 'default': { - - fontFamily: 'inherit', - fontSize: 'inherit', - positionLeft: '46%', - smallBreakPoint: '576', - mediumBreakPoint: '992', - buttonTextColor: '#13294b', - buttonBackgroundColor: '#dddddd', - focusBorderColor: '#c5050c', - menuTextColor: '#13294b', - menuBackgroundColor: '#dddddd', - menuitemFocusTextColor: '#dddddd', - menuitemFocusBackgroundColor: '#13294b', - menuTextDarkColor: '#ffffff', - menuBackgroundDarkColor: '#000000', - menuitemFocusTextDarkColor: '#ffffff', - menuitemFocusBackgroundDarkColor: '#013c93', - focusBorderDarkColor: '#ffffff', - buttonTextDarkColor: '#ffffff', - buttonBackgroundDarkColor: '#013c93', - zIndex: '2000000', - zHighlight: '1999900', - displayOption: 'fixed', - highlightTarget: 'instant', - highlightBorderSize: 'small', - highlightBorderStyle: 'solid' - }, - 'aria': { - hostnameSelector: 'w3.org', - pathnameSelector: 'ARIA/apg', - fontFamily: 'sans-serif', - fontSize: '10pt', - positionLeft: '7%', - menuTextColor: '#000', - menuBackgroundColor: '#def', - menuitemFocusTextColor: '#fff', - menuitemFocusBackgroundColor: '#005a9c', - focusBorderColor: '#005a9c', - buttonTextColor: '#005a9c', - buttonBackgroundColor: '#ddd', - }, - 'illinois': { - hostnameSelector: 'illinois.edu', - menuTextColor: '#00132c', - menuBackgroundColor: '#cad9ef', - menuitemFocusTextColor: '#eeeeee', - menuitemFocusBackgroundColor: '#00132c', - focusBorderColor: '#ff552e', - buttonTextColor: '#444444', - buttonBackgroundColor: '#dddede', - highlightTarget: 'disabled' - }, - 'openweba11y': { - hostnameSelector: 'openweba11y.com', - buttonTextColor: '#13294B', - buttonBackgroundColor: '#dddddd', - focusBorderColor: '#C5050C', - menuTextColor: '#13294B', - menuBackgroundColor: '#dddddd', - menuitemFocusTextColor: '#dddddd', - menuitemFocusBackgroundColor: '#13294B', - fontSize: '90%' - }, - 'skipto': { - hostnameSelector: 'skipto-landmarks-headings.github.io', - positionLeft: '25%', - fontSize: '14px', - menuTextColor: '#00132c', - menuBackgroundColor: '#cad9ef', - menuitemFocusTextColor: '#eeeeee', - menuitemFocusBackgroundColor: '#00132c', - focusBorderColor: '#ff552e', - buttonTextColor: '#444444', - buttonBackgroundColor: '#dddede', - }, - 'uic': { - hostnameSelector: 'uic.edu', - menuTextColor: '#001e62', - menuBackgroundColor: '#f8f8f8', - menuitemFocusTextColor: '#ffffff', - menuitemFocusBackgroundColor: '#001e62', - focusBorderColor: '#d50032', - buttonTextColor: '#ffffff', - buttonBackgroundColor: '#001e62', - }, - 'uillinois': { - hostnameSelector: 'uillinois.edu', - menuTextColor: '#001e62', - menuBackgroundColor: '#e8e9ea', - menuitemFocusTextColor: '#f8f8f8', - menuitemFocusBackgroundColor: '#13294b', - focusBorderColor: '#dd3403', - buttonTextColor: '#e8e9ea', - buttonBackgroundColor: '#13294b', - highlightTarget: 'disabled' - }, - 'uis': { - hostnameSelector: 'uis.edu', - menuTextColor: '#036', - menuBackgroundColor: '#fff', - menuitemFocusTextColor: '#fff', - menuitemFocusBackgroundColor: '#036', - focusBorderColor: '#dd3444', - buttonTextColor: '#fff', - buttonBackgroundColor: '#036', - }, - 'walmart': { - hostnameSelector: 'walmart.com', - buttonTextColor: '#ffffff', - buttonBackgroundColor: '#00419a', - focusBorderColor: '#ffc220', - menuTextColor: '#ffffff', - menuBackgroundColor: '#0071dc', - menuitemFocusTextColor: '#00419a', - menuitemFocusBackgroundColor: '#ffffff', - } - }; - - /* - * debug.js - * - * Usage - * import DebugLogging from './debug.js'; - * const debug = new DebugLogging('myLabel', true); // e.g. 'myModule' - * ... - * if (debug.flag) debug.log('myMessage'); - * - * Notes - * new DebugLogging() - calling the constructor with no arguments results - * in debug.flag set to false and debug.label set to 'debug'; - * constructor accepts 0, 1 or 2 arguments in any order - * @param flag [optional] {boolean} - sets debug.flag - * @param label [optional] {string} - sets debug.label - * Properties - * debug.flag {boolean} allows you to switch debug logging on or off; - * default value is false - * debug.label {string} rendered as a prefix to each log message; - * default value is 'debug' - * Methods - * debug.log calls console.log with label prefix and message - * @param message {object} - console.log calls toString() - * @param spaceAbove [optional] {boolean} - * - * debug.tag outputs tagName and textContent of DOM element - * @param node {DOM node reference} - usually an HTMLElement - * @param spaceAbove [optional] {boolean} - * - * debug.separator outputs only debug.label and a series of hyphens - * @param spaceAbove [optional] {boolean} - */ - - class DebugLogging { - constructor (...args) { - // Default values for cases where fewer than two arguments are provided - this._flag = false; - this._label = 'debug'; - - // The constructor may be called with zero, one or two arguments. If two - // arguments, they can be in any order: one is assumed to be the boolean - // value for '_flag' and the other one the string value for '_label'. - for (const [index, arg] of args.entries()) { - if (index < 2) { - switch (typeof arg) { - case 'boolean': - this._flag = arg; - break; - case 'string': - this._label = arg; - break; - } - } - } - } - - get flag () { return this._flag; } - - set flag (value) { - if (typeof value === 'boolean') { - this._flag = value; - } - } - - get label () { return this._label; } - - set label (value) { - if (typeof value === 'string') { - this._label = value; - } - } - - log (message, spaceAbove) { - const newline = spaceAbove ? '\n' : ''; - console.log(`${newline}[${this._label}] ${message}`); - } - - tag (node, spaceAbove) { - if (node && node.tagName) { - const text = node.textContent.trim().replace(/\s+/g, ' '); - this.log(`[${node.tagName}]: ${text.substring(0, 40)}`, spaceAbove); - } - } - - separator (spaceAbove) { - this.log('-----------------------------', spaceAbove); - } - - } - - /* constants.js */ - - // Element IDs - - const SKIP_TO_ID = 'id-skip-to-ver-5'; - const SKIP_TO_MENU_STYLE_ID = 'id-skip-to-menu-style'; - - const SCRIPT_EXTENSION_ID = `id-skip-to-extension`; - const SCRIPT_BOOKMARKLET_ID = `id-skip-to-bookmarklet`; - - const MENU_ID = 'id-skip-to-menu'; - - const MENU_LANDMARK_GROUP_ID = 'id-skip-to-landmark-group'; - const MENU_LANDMARK_GROUP_LABEL_ID = 'id-skip-to-landmark-group-label'; - - const MENU_HEADINGS_GROUP_ID = 'id-skip-to-heading-group'; - const MENU_HEADINGS_GROUP_LABEL_ID = 'id-skip-to-heading-group-label'; - - const MENU_SHORTCUTS_GROUP_ID = 'id-skip-to-shortcuts-group'; - const MENU_SHORTCUTS_GROUP_LABEL_ID = 'id-skip-to-shortcuts-group-label'; - - const MESSAGE_ID = 'id-skip-to-message'; - - const HIGHLIGHT_ID = 'id-skip-to-highlight-overlay'; - - // Custom element names - - const PAGE_SCRIPT_ELEMENT_NAME = 'skip-to-content'; - const BOOKMARKLET_ELEMENT_NAME = 'skip-to-content-bookmarklet'; - const EXTENSION_ELEMENT_NAME = 'skip-to-content-extension'; - - const INFO_DIALOG_ELEMENT_NAME = 'skip-to-content-info-dialog-575'; - const MESSAGE_ELEMENT_NAME = 'skip-to-content-message-element-575'; - const HIGHLIGHT_ELEMENT_NAME = 'skip-to-content-highlight-element-575'; - - // Attributes - - const ATTR_SKIP_TO_DATA = 'data-skipto'; - - // URLs to more information - - const MORE_PAGE_SCRIPT_INFO_URL ='https://skipto-landmarks-headings.github.io/page-script-5/'; - const MORE_SHORTCUT_INFO_URL ='https://skipto-landmarks-headings.github.io/page-script-5/shortcuts.html'; - +/* ======================================================================== + * Version: 5.7 + * Copyright (c) 2022, 2023, 2024, 2025 Jon Gunderson; Licensed BSD + * Copyright (c) 2021 PayPal Accessibility Team and University of Illinois; Licensed BSD + * All rights reserved. + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of PayPal or any of its subsidiaries or affiliates, nor the name of the University of Illinois, nor the names of any other contributors contributors may be used to endorse or promote products derived from this software without specific prior written permission. + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * CDN: https://skipto-landmarks-headings.github.io/page-script-5/dist/skipto.min.js + * Documentation: https://skipto-landmarks-headings.github.io/page-script-5 + * Code: https://github.com/skipto-landmarks-headings/page-script-5 + * Report Issues: https://github.com/skipto-landmarks-headings/page-script-5/issues + * ======================================================================== */ + +(function () { + 'use strict'; + + /* colorThemes */ + + const colorThemes = { + 'default': { + + fontFamily: 'inherit', + fontSize: 'inherit', + positionLeft: '46%', + smallBreakPoint: '576', + mediumBreakPoint: '992', + buttonTextColor: '#13294b', + buttonBackgroundColor: '#dddddd', + focusBorderColor: '#c5050c', + menuTextColor: '#13294b', + menuBackgroundColor: '#dddddd', + menuitemFocusTextColor: '#dddddd', + menuitemFocusBackgroundColor: '#13294b', + menuTextDarkColor: '#ffffff', + menuBackgroundDarkColor: '#000000', + menuitemFocusTextDarkColor: '#ffffff', + menuitemFocusBackgroundDarkColor: '#013c93', + focusBorderDarkColor: '#ffffff', + buttonTextDarkColor: '#ffffff', + buttonBackgroundDarkColor: '#013c93', + zIndex: '2000000', + zHighlight: '1999900', + displayOption: 'fixed', + highlightTarget: 'instant', + highlightBorderSize: 'small', + highlightBorderStyle: 'solid' + }, + 'aria': { + hostnameSelector: 'w3.org', + pathnameSelector: 'ARIA/apg', + headings: 'h1 h2', + fontFamily: 'sans-serif', + fontSize: '10pt', + positionLeft: '7%', + menuTextColor: '#000', + menuBackgroundColor: '#def', + menuitemFocusTextColor: '#fff', + menuitemFocusBackgroundColor: '#005a9c', + focusBorderColor: '#005a9c', + buttonTextColor: '#005a9c', + buttonBackgroundColor: '#ddd', + }, + 'illinois': { + hostnameSelector: 'illinois.edu', + menuTextColor: '#00132c', + menuBackgroundColor: '#cad9ef', + menuitemFocusTextColor: '#eeeeee', + menuitemFocusBackgroundColor: '#00132c', + focusBorderColor: '#ff552e', + buttonTextColor: '#444444', + buttonBackgroundColor: '#dddede', + highlightTarget: 'disabled' + }, + 'openweba11y': { + hostnameSelector: 'openweba11y.com', + buttonTextColor: '#13294B', + buttonBackgroundColor: '#dddddd', + focusBorderColor: '#C5050C', + menuTextColor: '#13294B', + menuBackgroundColor: '#dddddd', + menuitemFocusTextColor: '#dddddd', + menuitemFocusBackgroundColor: '#13294B', + fontSize: '90%' + }, + 'skipto': { + hostnameSelector: 'skipto-landmarks-headings.github.io', + positionLeft: '25%', + fontSize: '14px', + menuTextColor: '#00132c', + menuBackgroundColor: '#cad9ef', + menuitemFocusTextColor: '#eeeeee', + menuitemFocusBackgroundColor: '#00132c', + focusBorderColor: '#ff552e', + buttonTextColor: '#444444', + buttonBackgroundColor: '#dddede', + }, + 'uic': { + hostnameSelector: 'uic.edu', + menuTextColor: '#001e62', + menuBackgroundColor: '#f8f8f8', + menuitemFocusTextColor: '#ffffff', + menuitemFocusBackgroundColor: '#001e62', + focusBorderColor: '#d50032', + buttonTextColor: '#ffffff', + buttonBackgroundColor: '#001e62', + }, + 'uillinois': { + hostnameSelector: 'uillinois.edu', + menuTextColor: '#001e62', + menuBackgroundColor: '#e8e9ea', + menuitemFocusTextColor: '#f8f8f8', + menuitemFocusBackgroundColor: '#13294b', + focusBorderColor: '#dd3403', + buttonTextColor: '#e8e9ea', + buttonBackgroundColor: '#13294b', + highlightTarget: 'disabled' + }, + 'uis': { + hostnameSelector: 'uis.edu', + menuTextColor: '#036', + menuBackgroundColor: '#fff', + menuitemFocusTextColor: '#fff', + menuitemFocusBackgroundColor: '#036', + focusBorderColor: '#dd3444', + buttonTextColor: '#fff', + buttonBackgroundColor: '#036', + }, + 'walmart': { + hostnameSelector: 'walmart.com', + buttonTextColor: '#ffffff', + buttonBackgroundColor: '#00419a', + focusBorderColor: '#ffc220', + menuTextColor: '#ffffff', + menuBackgroundColor: '#0071dc', + menuitemFocusTextColor: '#00419a', + menuitemFocusBackgroundColor: '#ffffff', + } + }; + + /* + * debug.js + * + * Usage + * import DebugLogging from './debug.js'; + * const debug = new DebugLogging('myLabel', true); // e.g. 'myModule' + * ... + * if (debug.flag) debug.log('myMessage'); + * + * Notes + * new DebugLogging() - calling the constructor with no arguments results + * in debug.flag set to false and debug.label set to 'debug'; + * constructor accepts 0, 1 or 2 arguments in any order + * @param flag [optional] {boolean} - sets debug.flag + * @param label [optional] {string} - sets debug.label + * Properties + * debug.flag {boolean} allows you to switch debug logging on or off; + * default value is false + * debug.label {string} rendered as a prefix to each log message; + * default value is 'debug' + * Methods + * debug.log calls console.log with label prefix and message + * @param message {object} - console.log calls toString() + * @param spaceAbove [optional] {boolean} + * + * debug.tag outputs tagName and textContent of DOM element + * @param node {DOM node reference} - usually an HTMLElement + * @param spaceAbove [optional] {boolean} + * + * debug.separator outputs only debug.label and a series of hyphens + * @param spaceAbove [optional] {boolean} + */ + + class DebugLogging { + constructor (...args) { + // Default values for cases where fewer than two arguments are provided + this._flag = false; + this._label = 'debug'; + + // The constructor may be called with zero, one or two arguments. If two + // arguments, they can be in any order: one is assumed to be the boolean + // value for '_flag' and the other one the string value for '_label'. + for (const [index, arg] of args.entries()) { + if (index < 2) { + switch (typeof arg) { + case 'boolean': + this._flag = arg; + break; + case 'string': + this._label = arg; + break; + } + } + } + } + + get flag () { return this._flag; } + + set flag (value) { + if (typeof value === 'boolean') { + this._flag = value; + } + } + + get label () { return this._label; } + + set label (value) { + if (typeof value === 'string') { + this._label = value; + } + } + + log (message, spaceAbove) { + const newline = spaceAbove ? '\n' : ''; + console.log(`${newline}[${this._label}] ${message}`); + } + + tag (node, spaceAbove) { + if (node && node.tagName) { + const text = node.textContent.trim().replace(/\s+/g, ' '); + this.log(`[${node.tagName}]: ${text.substring(0, 40)}`, spaceAbove); + } + } + + separator (spaceAbove) { + this.log('-----------------------------', spaceAbove); + } + + } + + /* constants.js */ + + // Element IDs + + const SKIP_TO_ID = 'id-skip-to-ver-5'; + const SKIP_TO_MENU_STYLE_ID = 'id-skip-to-menu-style'; + + const SCRIPT_EXTENSION_ID = `id-skip-to-extension`; + const SCRIPT_BOOKMARKLET_ID = `id-skip-to-bookmarklet`; + + const MENU_ID = 'id-skip-to-menu'; + + const MENU_LANDMARK_GROUP_ID = 'id-skip-to-landmark-group'; + const MENU_LANDMARK_GROUP_LABEL_ID = 'id-skip-to-landmark-group-label'; + + const MENU_HEADINGS_GROUP_ID = 'id-skip-to-heading-group'; + const MENU_HEADINGS_GROUP_LABEL_ID = 'id-skip-to-heading-group-label'; + + const MENU_SHORTCUTS_GROUP_ID = 'id-skip-to-shortcuts-group'; + const MENU_SHORTCUTS_GROUP_LABEL_ID = 'id-skip-to-shortcuts-group-label'; + + const MESSAGE_ID = 'id-skip-to-message'; + + const HIGHLIGHT_ID = 'id-skip-to-highlight-overlay'; + + // Custom element names + + const PAGE_SCRIPT_ELEMENT_NAME = 'skip-to-content'; + const BOOKMARKLET_ELEMENT_NAME = 'skip-to-content-bookmarklet'; + const EXTENSION_ELEMENT_NAME = 'skip-to-content-extension'; + + const INFO_DIALOG_ELEMENT_NAME = 'skip-to-content-info-dialog-575'; + const MESSAGE_ELEMENT_NAME = 'skip-to-content-message-element-575'; + const HIGHLIGHT_ELEMENT_NAME = 'skip-to-content-highlight-element-575'; + + // Attributes + + const ATTR_SKIP_TO_DATA = 'data-skipto'; + + // URLs to more information + + const MORE_PAGE_SCRIPT_INFO_URL ='https://skipto-landmarks-headings.github.io/page-script-5/'; + const MORE_SHORTCUT_INFO_URL ='https://skipto-landmarks-headings.github.io/page-script-5/shortcuts.html'; + /* style.js */ /* Constants */ @@ -834,737 +835,330 @@ } styleNode.textContent = cssMenu; - } - - /* utils.js */ - - /* Constants */ - const debug$b = new DebugLogging('Utils', false); - debug$b.flag = false; - - - /* - * @function getAttributeValue - * - * @desc Return attribute value if present on element, - * otherwise return empty string. - * - * @returns {String} see @desc - */ - function getAttributeValue (element, attribute) { - let value = element.getAttribute(attribute); - return (value === null) ? '' : normalize(value); - } - - /* - * @function normalize - * - * @desc Trim leading and trailing whitespace and condense all - * internal sequences of whitespace to a single space. Adapted from - * Mozilla documentation on String.prototype.trim polyfill. Handles - * BOM and NBSP characters. - * - * @return {String} see @desc - */ - function normalize (s) { - let rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; - return s.replace(rtrim, '').replace(/\s+/g, ' '); - } - - /** - * @fuction isNotEmptyString - * - * @desc Returns true if the string has content, otherwise false - * - * @param {Boolean} see @desc - */ - function isNotEmptyString (str) { - return (typeof str === 'string') && str.length && str.trim() && str !== " "; - } - - /** - * @fuction isVisible - * - * @desc Returns true if the element is visible in the graphical rendering - * - * @param {node} elem - DOM element node of a labelable element - */ - function isVisible (element) { - - function isDisplayNone(el) { - if (!el || (el.nodeType !== Node.ELEMENT_NODE)) { - return false; - } - - if (el.hasAttribute('hidden')) { - return true; - } - - const style = window.getComputedStyle(el, null); - const display = style.getPropertyValue("display"); - if (display === 'none') { - return true; - } - - // check ancestors for display none - if (el.parentNode) { - return isDisplayNone(el.parentNode); - } - - return false; - } - - const computedStyle = window.getComputedStyle(element); - let visibility = computedStyle.getPropertyValue('visibility'); - if ((visibility === 'hidden') || (visibility === 'collapse')) { - return false; - } - - return !isDisplayNone(element); - } - - /* shortcutInfoDialog.js */ - - /* Constants */ - const debug$a = new DebugLogging('[shortcutsInfoDialog]', false); - debug$a.flag = false; - - const defaultStyleOptions$3 = colorThemes['default']; - - - const styleTemplate$1 = document.createElement('template'); - styleTemplate$1.textContent = ` -/* infoDialog.css */ - -dialog#skip-to-info-dialog { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%,-50%); - font-family: $fontFamily; - font-size: $fontSize; - max-width: 70%; - margin: 0; - padding: 0; - background-color: light-dark($dialogBackgroundColor, $dialogBackgroundDarkColor); - color: light-dark($dialogTextColor, $dialogTextDarkColor); - border-width: 2px; - border-style: solid; - border-color: light-dark($focusBorderColor, $focusBorderDarkColor); - border-radius: 5px; - z-index: 2000001; - -} - -dialog#skip-to-info-dialog .header { - margin: 0; - margin-bottom: 0.5em; - padding: 4px; - border-width: 0; - border-bottom-width: 1px; - border-style: solid; - border-color: light-dark($focusBorderColor, $focusBorderDarkColor); - border-top-left-radius: 5px; - border-top-right-radius: 5px; - font-weight: bold; - background-color: light-dark($dialogBackgroundTitleColor, $dialogBackgroundTitleDarkColor); - color: light-dark($dialogTextColor, $dialogTextDarkColor); - position: relative; - font-size: 100%; -} - -dialog#skip-to-info-dialog .header h2 { - margin: 0; - padding: 0; - font-size: 1em; -} - -dialog#skip-to-info-dialog .header button { - position: absolute; - top: -0.25em; - right: 0; - border: none; - background: transparent; - font-weight: bold; - color: light-dark(black, white); -} - -dialog#skip-to-info-dialog .content { - margin-left: 2em; - margin-right: 2em; - margin-top: 0; - margin-bottom: 2em; -} - -dialog#skip-to-info-dialog .content .desc { - max-width: 20em; -} - -dialog#skip-to-info-dialog .content .happy { - margin-top: 0.5em; - text-align: center; - font-family: fantasy, cursive; - font-size: 1.25em; - font-weight: bold; - font-style: italic; - letter-spacing: 0.05em; -} - - -dialog#skip-to-info-dialog .content .version, -dialog#skip-to-info-dialog .content .copyright { - margin-top: 0.5em; - text-align: center; - font-weight: bold; - font-size: 90%; -} - -dialog#skip-to-info-dialog .content table { - width: auto; -} - -dialog#skip-to-info-dialog .content caption { - margin: 0; - padding: 0; - margin-top: 1em; - text-align: left; - font-weight: bold; - font-size: 110%; -} - -dialog#skip-to-info-dialog .content th { - margin: 0; - padding: 0; - padding-top: 0.125em; - padding-bottom: 0.125em; - text-align: left; - font-weight: bold; - font-size: 100%; -} - -dialog#skip-to-info-dialog .content th { - border-bottom-width: 1px; - border-bottom-style: solid; - border-bottom-color: light-dark(#999999, #777777); -} - -dialog#skip-to-info-dialog .content th.shortcut { - width: 2.5em; -} - -dialog#skip-to-info-dialog .content td { - margin: 0; - padding: 0; - padding-top: 0.125em; - padding-bottom: 0.125em; - text-align: left; - font-size: 100%; -} - - -dialog#skip-to-info-dialog .content table tr:nth-child(even) { - background-color: light-dark(#eeeeee, #111111); -} - -dialog#skip-to-info-dialog .buttons { - float: right; - margin-right: 0.5em; - margin-bottom: 0.5em; -} - -dialog#skip-to-info-dialog button { - margin: 6px; -} - -dialog#skip-to-info-dialog .buttons button { - min-width: 5em; -} - -button:focus { - outline: 2px solid currentColor; - outline-offset: 2px; -} - -button:hover { - cursor: pointer; -} -`; - - /* - * - * - */ - - class SkipToContentInfoDialog extends HTMLElement { - constructor () { - - super(); - this.attachShadow({ mode: 'open' }); - - // Get references - - this.infoDialog = document.createElement('dialog'); - this.infoDialog.id = 'skip-to-info-dialog'; - this.shadowRoot.appendChild(this.infoDialog); - - const headerElem = document.createElement('div'); - headerElem.className = 'header'; - this.infoDialog.appendChild(headerElem); - - this.h2Elem = document.createElement('h2'); - this.h2Elem.textContent = 'Keyboard Shortcuts'; - headerElem.appendChild(this.h2Elem); - - this.closeButton1 = document.createElement('button'); - this.closeButton1.textContent = '✕'; - headerElem.appendChild(this.closeButton1); - this.closeButton1.addEventListener('click', this.onCloseButtonClick.bind(this)); - this.closeButton1.addEventListener('keydown', this.onKeyDown.bind(this)); - - this.contentElem = document.createElement('div'); - this.contentElem.className = 'content'; - this.infoDialog.appendChild(this.contentElem); - - const buttonsElem = document.createElement('div'); - buttonsElem.className = 'buttons'; - this.infoDialog.appendChild(buttonsElem); - - this.moreInfoButton = document.createElement('button'); - this.moreInfoButton.textContent = 'More Information'; - buttonsElem.appendChild(this.moreInfoButton); - this.moreInfoButton.addEventListener('click', this.onMoreInfoClick.bind(this)); - - this.closeButton2 = document.createElement('button'); - this.closeButton2.textContent = 'Close'; - buttonsElem.appendChild(this.closeButton2); - this.closeButton2.addEventListener('click', this.onCloseButtonClick.bind(this)); - this.closeButton2.addEventListener('keydown', this.onKeyDown.bind(this)); - - this.moreInfoURL = ''; - - } - - onCloseButtonClick () { - this.infoDialog.close(); - } - - openDialog () { - this.infoDialog.showModal(); - this.closeButton2.focus(); - } - - onMoreInfoClick () { - if (this.moreInfoURL) { - window.open(this.moreInfoURL, '_blank').focus(); - } - } - - configureStyle(config={}) { - - function updateOption(style, option, configOption, defaultOption) { - if (configOption) { - return style.replaceAll(option, configOption); - } - else { - return style.replaceAll(option, defaultOption); - } - } - - // make a copy of the template - let style = styleTemplate$1.textContent.slice(0); - - style = updateOption(style, - '$fontFamily', - config.fontFamily, - defaultStyleOptions$3.fontFamily); - - style = updateOption(style, - '$fontSize', - config.fontSize, - defaultStyleOptions$3.fontSize); - - style = updateOption(style, - '$focusBorderColor', - config.focusBorderColor, - defaultStyleOptions$3.focusBorderColor); - - style = updateOption(style, - '$focusBorderDarkColor', - config.focusBorderDarkColor, - defaultStyleOptions$3.focusBorderDarkColor); - - style = updateOption(style, - '$dialogTextColor', - config.dialogTextColor, - defaultStyleOptions$3.dialogTextColor); - - style = updateOption(style, - '$dialogextDarkColor', - config.dialogextDarkColor, - defaultStyleOptions$3.dialogextDarkColor); - - style = updateOption(style, - '$dialogBackgroundColor', - config.dialogBackgroundColor, - defaultStyleOptions$3.dialogBackgroundColor); - - style = updateOption(style, - '$dialogBackgroundDarkColor', - config.dialogBackgroundDarkColor, - defaultStyleOptions$3.dialogBackgroundDarkColor); - - style = updateOption(style, - '$dialogBackgroundTitleColor', - config.dialogBackgroundTitleColor, - defaultStyleOptions$3.dialogBackgroundTitleColor); - - style = updateOption(style, - '$dialogBackgroundTitleDarkColor', - config.dialogBackgroundTitleDarkColor, - defaultStyleOptions$3.dialogBackgroundTitleDarkColor); - - - let styleNode = this.shadowRoot.querySelector('style'); - - if (styleNode) { - styleNode.remove(); - } - - styleNode = document.createElement('style'); - styleNode.textContent = style; - this.shadowRoot.appendChild(styleNode); - - } - - - updateShortcutContent (config) { - - while (this.contentElem.lastElementChild) { - this.contentElem.removeChild(this.contentElem.lastElementChild); - } - - this.moreInfoURL = MORE_SHORTCUT_INFO_URL; - - this.h2Elem.textContent = config.shortcutsInfoLabel; - this.closeButton1.setAttribute('aria-label', config.closeLabel); - this.closeButton2.textContent = config.closeLabel; - this.moreInfoButton.textContent = config.moreInfoLabel; - - function addRow(tbodyElem, shortcut, desc) { - - const trElem = document.createElement('tr'); - tbodyElem.appendChild(trElem); - - const tdElem1 = document.createElement('td'); - tdElem1.className = 'shortcut'; - tdElem1.textContent = shortcut; - trElem.appendChild(tdElem1); - - const tdElem2 = document.createElement('td'); - tdElem2.className = 'desc'; - tdElem2.textContent = desc; - trElem.appendChild(tdElem2); - } - - // Regions - - const tableElem1 = document.createElement('table'); - this.contentElem.appendChild(tableElem1); - - const captionElem1 = document.createElement('caption'); - captionElem1.textContent = config.landmarkGroupLabel; - tableElem1.appendChild(captionElem1); - - const theadElem1 = document.createElement('thead'); - tableElem1.appendChild(theadElem1); - - const trElem1 = document.createElement('tr'); - theadElem1.appendChild(trElem1); - - const thElem1 = document.createElement('th'); - thElem1.className = 'shortcut'; - thElem1.textContent = config.msgKey; - trElem1.appendChild(thElem1); - - const thElem2 = document.createElement('th'); - thElem2.className = 'desc'; - thElem2.textContent = config.msgDescription; - trElem1.appendChild(thElem2); - - const tbodyElem1 = document.createElement('tbody'); - tableElem1.appendChild(tbodyElem1); - - addRow(tbodyElem1, config.shortcutRegionNext, config.msgNextRegion); - addRow(tbodyElem1, config.shortcutRegionPrevious, config.msgPreviousRegion); - addRow(tbodyElem1, config.shortcutRegionMain, config.msgMainRegions); - addRow(tbodyElem1, config.shortcutRegionNavigation, config.msgNavigationRegions); - addRow(tbodyElem1, config.shortcutRegionComplementary, config.msgComplementaryRegions); - - // Headings - - const tableElem2 = document.createElement('table'); - this.contentElem.appendChild(tableElem2); - - const captionElem2 = document.createElement('caption'); - captionElem2.textContent = config.headingGroupLabel; - tableElem2.appendChild(captionElem2); - - const theadElem2 = document.createElement('thead'); - tableElem2.appendChild(theadElem2); - - const trElem2 = document.createElement('tr'); - theadElem2.appendChild(trElem2); - - const thElem3 = document.createElement('th'); - thElem3.className = 'shortcut'; - thElem3.textContent = config.msgKey; - trElem2.appendChild(thElem3); - - const thElem4 = document.createElement('th'); - thElem4.className = 'desc'; - thElem4.textContent = config.msgDescription; - trElem2.appendChild(thElem4); - - const tbodyElem2 = document.createElement('tbody'); - tableElem2.appendChild(tbodyElem2); - - addRow(tbodyElem2, config.shortcutHeadingNext, config.msgNextHeading); - addRow(tbodyElem2, config.shortcutHeadingPrevious, config.msgPreviousHeading); - addRow(tbodyElem2, config.shortcutHeadingH1, config.msgH1Headings); - addRow(tbodyElem2, config.shortcutHeadingH2, config.msgH2Headings); - addRow(tbodyElem2, config.shortcutHeadingH3, config.msgH3Headings); - addRow(tbodyElem2, config.shortcutHeadingH4, config.msgH4Headings); - addRow(tbodyElem2, config.shortcutHeadingH5, config.msgH5Headings); - addRow(tbodyElem2, config.shortcutHeadingH6, config.msgH6Headings); - - } - - updateAboutContent (config) { - - while (this.contentElem.lastElementChild) { - this.contentElem.removeChild(this.contentElem.lastElementChild); - } - - this.moreInfoURL = MORE_PAGE_SCRIPT_INFO_URL; - - this.h2Elem.textContent = config.aboutInfoLabel; - this.closeButton1.setAttribute('aria-label', config.closeLabel); - this.closeButton2.textContent = config.closeLabel; - this.moreInfoButton.textContent = config.moreInfoLabel; - - let divElem = document.createElement('div'); - divElem.className = 'desc'; - divElem.textContent = config.aboutDesc; - this.contentElem.appendChild(divElem); - - divElem = document.createElement('div'); - divElem.className = 'happy'; - divElem.textContent = config.aboutHappy; - this.contentElem.appendChild(divElem); - - divElem = document.createElement('div'); - divElem.className = 'version'; - divElem.textContent = config.aboutVersion; - this.contentElem.appendChild(divElem); - - divElem = document.createElement('div'); - divElem.className = 'copyright'; - divElem.textContent = config.aboutCopyright; - this.contentElem.appendChild(divElem); - - } - - onKeyDown (event) { - - if ((event.key === "Tab") && - !event.altKey && - !event.ctlKey && - !event.metaKey) { - - if (event.shiftKey && - (event.currentTarget === this.closeButton1)) { - this.closeButton2.focus(); - event.preventDefault(); - event.stopPropagation(); - } - - if (!event.shiftKey && - (event.currentTarget === this.closeButton2)) { - this.closeButton1.focus(); - event.preventDefault(); - event.stopPropagation(); - } - } - } - } - - /* highlight.js */ + } + + /* utils.js */ /* Constants */ - const debug$9 = new DebugLogging('highlight', false); - debug$9.flag = false; + const debug$b = new DebugLogging('Utils', false); + debug$b.flag = false; - const minWidth = 68; - const minHeight = 27; - const defaultStyleOptions$2 = colorThemes['default']; + /* + * @function getAttributeValue + * + * @desc Return attribute value if present on element, + * otherwise return empty string. + * + * @returns {String} see @desc + */ + function getAttributeValue (element, attribute) { + let value = element.getAttribute(attribute); + return (value === null) ? '' : normalize(value); + } - const styleHighlightTemplate = document.createElement('template'); - styleHighlightTemplate.textContent = ` -:root { - color-scheme: light dark; -} + /* + * @function normalize + * + * @desc Trim leading and trailing whitespace and condense all + * internal sequences of whitespace to a single space. Adapted from + * Mozilla documentation on String.prototype.trim polyfill. Handles + * BOM and NBSP characters. + * + * @return {String} see @desc + */ + function normalize (s) { + let rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; + return s.replace(rtrim, '').replace(/\s+/g, ' '); + } -#${HIGHLIGHT_ID} { + /** + * @fuction isNotEmptyString + * + * @desc Returns true if the string has content, otherwise false + * + * @param {Boolean} see @desc + */ + function isNotEmptyString (str) { + return (typeof str === 'string') && str.length && str.trim() && str !== " "; + } + + /** + * @fuction isVisible + * + * @desc Returns true if the element is visible in the graphical rendering + * + * @param {node} elem - DOM element node of a labelable element + */ + function isVisible (element) { + + function isDisplayNone(el) { + if (!el || (el.nodeType !== Node.ELEMENT_NODE)) { + return false; + } + + if (el.hasAttribute('hidden')) { + return true; + } + + const style = window.getComputedStyle(el, null); + const display = style.getPropertyValue("display"); + if (display === 'none') { + return true; + } + + // check ancestors for display none + if (el.parentNode) { + return isDisplayNone(el.parentNode); + } + + return false; + } + + const computedStyle = window.getComputedStyle(element); + let visibility = computedStyle.getPropertyValue('visibility'); + if ((visibility === 'hidden') || (visibility === 'collapse')) { + return false; + } + + return !isDisplayNone(element); + } + + /* shortcutInfoDialog.js */ + + /* Constants */ + const debug$a = new DebugLogging('[shortcutsInfoDialog]', false); + debug$a.flag = false; + + const defaultStyleOptions$3 = colorThemes['default']; + + + const styleTemplate$1 = document.createElement('template'); + styleTemplate$1.textContent = ` +/* infoDialog.css */ + +dialog#skip-to-info-dialog { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%,-50%); + font-family: $fontFamily; + font-size: $fontSize; + max-width: 70%; margin: 0; padding: 0; - position: absolute; - border-radius: $highlightOffsetpx; - border: $shadowBorderWidthpx solid light-dark($menuBackgroundColor, $menuBackgroundDarkColor); - box-sizing: border-box; - pointer-events:none; - z-index: $zHighlight; -} + background-color: light-dark($dialogBackgroundColor, $dialogBackgroundDarkColor); + color: light-dark($dialogTextColor, $dialogTextDarkColor); + border-width: 2px; + border-style: solid; + border-color: light-dark($focusBorderColor, $focusBorderDarkColor); + border-radius: 5px; + z-index: 2000001; -#${HIGHLIGHT_ID}.hasInfoBottom, -#${HIGHLIGHT_ID} .overlay-border.hasInfoBottom { - border-radius: $highlightOffsetpx $highlightOffsetpx $highlightOffsetpx 0; } -#${HIGHLIGHT_ID}.hasInfoTop, -#${HIGHLIGHT_ID} .overlay-border.hasInfoTop { - border-radius: 0 $highlightOffsetpx $highlightOffsetpx $highlightOffsetpx; +dialog#skip-to-info-dialog .header { + margin: 0; + margin-bottom: 0.5em; + padding: 4px; + border-width: 0; + border-bottom-width: 1px; + border-style: solid; + border-color: light-dark($focusBorderColor, $focusBorderDarkColor); + border-top-left-radius: 5px; + border-top-right-radius: 5px; + font-weight: bold; + background-color: light-dark($dialogBackgroundTitleColor, $dialogBackgroundTitleDarkColor); + color: light-dark($dialogTextColor, $dialogTextDarkColor); + position: relative; + font-size: 100%; } -#${HIGHLIGHT_ID} .overlay-border { +dialog#skip-to-info-dialog .header h2 { margin: 0; padding: 0; - position: relative; - border-radius: $highlightOffsetpx; - border: $overlayBorderWidthpx $highlightBorderStyle light-dark($focusBorderColor, $focusBorderDarkColor); - z-index: $zHighlight; - box-sizing: border-box; - pointer-events:none; + font-size: 1em; +} + +dialog#skip-to-info-dialog .header button { + position: absolute; + top: -0.25em; + right: 0; + border: none; background: transparent; + font-weight: bold; + color: light-dark(black, white); } +dialog#skip-to-info-dialog .content { + margin-left: 2em; + margin-right: 2em; + margin-top: 0; + margin-bottom: 2em; +} -@keyframes fadeIn { - 0% { opacity: 0; } - 100% { opacity: 1; } +dialog#skip-to-info-dialog .content .desc { + max-width: 20em; } -#hidden-elem-msg { - position: absolute; - margin: 0; - padding: .25em; - background-color: light-dark($hiddenHeadingBackgroundColor, $hiddenHeadingBackgroundDarkColor); - color: light-dark($hiddenHeadingColor, $hiddenHeadingDarkColor); - font-family: $fontFamily; - font-size: $fontSize; - font-style: italic; +dialog#skip-to-info-dialog .content .happy { + margin-top: 0.5em; + text-align: center; + font-family: fantasy, cursive; + font-size: 1.25em; font-weight: bold; + font-style: italic; + letter-spacing: 0.05em; +} + + +dialog#skip-to-info-dialog .content .version, +dialog#skip-to-info-dialog .content .copyright { + margin-top: 0.5em; text-align: center; - animation: fadeIn 1.5s; - z-index: $zHighlight; + font-weight: bold; + font-size: 90%; } -#${HIGHLIGHT_ID} .overlay-info { +dialog#skip-to-info-dialog .content table { + width: auto; +} + +dialog#skip-to-info-dialog .content caption { margin: 0; - padding: 2px; - position: relative; + padding: 0; + margin-top: 1em; text-align: left; - font-size: $fontSize; - font-family: $fontFamily; - border: $infoBorderWidthpx solid light-dark($menuBackgroundColor, $menuBackgroundDarkColor); - background-color: light-dark($menuBackgroundColor, $menuBackgroundDarkColor); - color: light-dark($menuTextColor, $menuTextDarkColor); - z-index: $zHighlight; - overflow: hidden; - text-overflow: ellipsis; - pointer-events:none; + font-weight: bold; + font-size: 110%; } -#${HIGHLIGHT_ID} .overlay-info.hasInfoTop { - border-radius: $highlightOffsetpx $highlightOffsetpx 0 0; +dialog#skip-to-info-dialog .content th { + margin: 0; + padding: 0; + padding-top: 0.125em; + padding-bottom: 0.125em; + text-align: left; + font-weight: bold; + font-size: 100%; } -#${HIGHLIGHT_ID} .overlay-info.hasInfoBottom { - border-radius: 0 0 $highlightOffsetpx $highlightOffsetpx; +dialog#skip-to-info-dialog .content th { + border-bottom-width: 1px; + border-bottom-style: solid; + border-bottom-color: light-dark(#999999, #777777); } -@media (forced-colors: active) { +dialog#skip-to-info-dialog .content th.shortcut { + width: 2.5em; +} - #${HIGHLIGHT_ID} { - border-color: ButtonBorder; - } +dialog#skip-to-info-dialog .content td { + margin: 0; + padding: 0; + padding-top: 0.125em; + padding-bottom: 0.125em; + text-align: left; + font-size: 100%; +} - #${HIGHLIGHT_ID} .overlay-border { - border-color: ButtonBorder; - } - #${HIGHLIGHT_ID} .overlay-border.skip-to-hidden { - background-color: ButtonFace; - color: ButtonText; - } +dialog#skip-to-info-dialog .content table tr:nth-child(even) { + background-color: light-dark(#eeeeee, #111111); +} - #${HIGHLIGHT_ID} .overlay-info { - border-color: ButtonBorder; - background-color: ButtonFace; - color: ButtonText; - } +dialog#skip-to-info-dialog .buttons { + float: right; + margin-right: 0.5em; + margin-bottom: 0.5em; +} + +dialog#skip-to-info-dialog button { + margin: 6px; +} + +dialog#skip-to-info-dialog .buttons button { + min-width: 5em; +} +button:focus { + outline: 2px solid currentColor; + outline-offset: 2px; +} + +button:hover { + cursor: pointer; } `; /* - * @class HighlightElement + * * */ - class HighlightElement extends HTMLElement { + class SkipToContentInfoDialog extends HTMLElement { + constructor () { - constructor() { super(); this.attachShadow({ mode: 'open' }); // Get references - this.overlayElem = document.createElement('div'); - this.overlayElem.id = HIGHLIGHT_ID; - this.shadowRoot.appendChild(this.overlayElem); - this.overlayElem.style.display = 'none'; + this.infoDialog = document.createElement('dialog'); + this.infoDialog.id = 'skip-to-info-dialog'; + this.shadowRoot.appendChild(this.infoDialog); - this.borderElem = document.createElement('div'); - this.borderElem.className = 'overlay-border'; - this.overlayElem.appendChild(this.borderElem); + const headerElem = document.createElement('div'); + headerElem.className = 'header'; + this.infoDialog.appendChild(headerElem); - this.infoElem = document.createElement('div'); - this.infoElem.className = 'overlay-info'; - this.overlayElem.appendChild(this.infoElem); + this.h2Elem = document.createElement('h2'); + this.h2Elem.textContent = 'Keyboard Shortcuts'; + headerElem.appendChild(this.h2Elem); - this.hiddenElem = document.createElement('div'); - this.hiddenElem.id = 'hidden-elem-msg'; - this.shadowRoot.appendChild(this.hiddenElem); - this.hiddenElem.style.display = 'none'; + this.closeButton1 = document.createElement('button'); + this.closeButton1.textContent = '✕'; + headerElem.appendChild(this.closeButton1); + this.closeButton1.addEventListener('click', this.onCloseButtonClick.bind(this)); + this.closeButton1.addEventListener('keydown', this.onKeyDown.bind(this)); - this.borderWidth = 0; - this.borderContrast = 0; - this.offset = 0; + this.contentElem = document.createElement('div'); + this.contentElem.className = 'content'; + this.infoDialog.appendChild(this.contentElem); - this.msgHeadingIsHidden = ''; + const buttonsElem = document.createElement('div'); + buttonsElem.className = 'buttons'; + this.infoDialog.appendChild(buttonsElem); - this.configureStyle(); + this.moreInfoButton = document.createElement('button'); + this.moreInfoButton.textContent = 'More Information'; + buttonsElem.appendChild(this.moreInfoButton); + this.moreInfoButton.addEventListener('click', this.onMoreInfoClick.bind(this)); + + this.closeButton2 = document.createElement('button'); + this.closeButton2.textContent = 'Close'; + buttonsElem.appendChild(this.closeButton2); + this.closeButton2.addEventListener('click', this.onCloseButtonClick.bind(this)); + this.closeButton2.addEventListener('keydown', this.onKeyDown.bind(this)); + + this.moreInfoURL = ''; } - /* - * @method configureStyle - * - * @desc Updates stylesheet for styling the highlight information - * - * @param {Object} config : color and font information - */ + onCloseButtonClick () { + this.infoDialog.close(); + } + + openDialog () { + this.infoDialog.showModal(); + this.closeButton2.focus(); + } + + onMoreInfoClick () { + if (this.moreInfoURL) { + window.open(this.moreInfoURL, '_blank').focus(); + } + } configureStyle(config={}) { @@ -1577,304 +1171,711 @@ button:hover { } } - // Get i18n Messages - - this.msgHeadingIsHidden = typeof config.msgHeadingIsHidden === 'string' ? - config.msgHeadingIsHidden : - 'Heading is hidden'; - - this.msgRegionIsHidden = typeof config.msgRegionIsHidden === 'string' ? - config.msgRegionIsHidden : - 'Region is hidden'; - - this.msgElementIsHidden = typeof config.msgElementIsHidden === 'string' ? - config.msgElemenIsHidden : - 'Element is hidden'; - - // make a copy of the template - let style = styleHighlightTemplate.textContent.slice(0); + let style = styleTemplate$1.textContent.slice(0); style = updateOption(style, '$fontFamily', config.fontFamily, - defaultStyleOptions$2.fontFamily); - - style = updateOption(style, - '$buttonBackgroundColor', - config.buttonBackgroundColor, - defaultStyleOptions$2.buttonBackgroundColor); + defaultStyleOptions$3.fontFamily); style = updateOption(style, - '$buttonBackgroundDarkColor', - config.buttonBackgroundDarkColor, - defaultStyleOptions$2.buttonBackgroundDarkColor); + '$fontSize', + config.fontSize, + defaultStyleOptions$3.fontSize); style = updateOption(style, '$focusBorderColor', config.focusBorderColor, - defaultStyleOptions$2.focusBorderColor); + defaultStyleOptions$3.focusBorderColor); style = updateOption(style, '$focusBorderDarkColor', config.focusBorderDarkColor, - defaultStyleOptions$2.focusBorderDarkColor); + defaultStyleOptions$3.focusBorderDarkColor); style = updateOption(style, - '$menuBackgroundColor', - config.menuBackgroundColor, - defaultStyleOptions$2.menuBackgroundColor); + '$dialogTextColor', + config.dialogTextColor, + defaultStyleOptions$3.dialogTextColor); style = updateOption(style, - '$menuBackgroundDarkColor', - config.menuBackgroundDarkColor, - defaultStyleOptions$2.menuBackgroundDarkColor); + '$dialogextDarkColor', + config.dialogextDarkColor, + defaultStyleOptions$3.dialogextDarkColor); style = updateOption(style, - '$menuTextColor', - config.menuTextColor, - defaultStyleOptions$2.menuTextColor); + '$dialogBackgroundColor', + config.dialogBackgroundColor, + defaultStyleOptions$3.dialogBackgroundColor); style = updateOption(style, - '$menuTextDarkColor', - config.menuTextDarkColor, - defaultStyleOptions$2.menuTextDarkColor); + '$dialogBackgroundDarkColor', + config.dialogBackgroundDarkColor, + defaultStyleOptions$3.dialogBackgroundDarkColor); style = updateOption(style, - '$hiddenHeadingColor', - config.hiddenHeadingColor, - defaultStyleOptions$2.hiddenHeadingColor); + '$dialogBackgroundTitleColor', + config.dialogBackgroundTitleColor, + defaultStyleOptions$3.dialogBackgroundTitleColor); style = updateOption(style, - '$hiddenHeadingDarkColor', - config.hiddenHeadingDarkColor, - defaultStyleOptions$2.hiddenHeadingDarkColor); + '$dialogBackgroundTitleDarkColor', + config.dialogBackgroundTitleDarkColor, + defaultStyleOptions$3.dialogBackgroundTitleDarkColor); - style = updateOption(style, - '$hiddenHeadingBackgroundColor', - config.hiddenHeadingBackgroundColor, - defaultStyleOptions$2.hiddenHeadingBackgroundColor); - style = updateOption(style, - '$hiddenHeadingBackgroundDarkColor', - config.hiddenHeadingBackgroundDarkColor, - defaultStyleOptions$2.hiddenHeadingBackgroundDarkColor); + let styleNode = this.shadowRoot.querySelector('style'); - style = updateOption(style, - '$zHighlight', - config.zHighlight, - defaultStyleOptions$2.zHighlight); + if (styleNode) { + styleNode.remove(); + } - style = updateOption(style, - '$highlightBorderStyle', - config.highlightBorderStyle, - defaultStyleOptions$2.highlightBorderStyle); + styleNode = document.createElement('style'); + styleNode.textContent = style; + this.shadowRoot.appendChild(styleNode); - const highlightBorderSize = config.highlightBorderSize ? - config.highlightBorderSize : - defaultStyleOptions$2.highlightBorderSize; + } - switch (highlightBorderSize) { - case 'small': - this.borderWidth = 2; - this.borderContrast = 1; - this.offset = 4; - this.fontSize = '12pt'; - break; - case 'medium': - this.borderWidth = 3; - this.borderContrast = 2; - this.offset = 4; - this.fontSize = '13pt'; - break; + updateShortcutContent (config) { - case 'large': - this.borderWidth = 4; - this.borderContrast = 3; - this.offset = 6; - this.fontSize = '14pt'; - break; + while (this.contentElem.lastElementChild) { + this.contentElem.removeChild(this.contentElem.lastElementChild); + } - case 'x-large': - this.borderWidth = 6; - this.borderContrast = 3; - this.offset = 8; - this.fontSize = '16pt'; - break; + this.moreInfoURL = MORE_SHORTCUT_INFO_URL; - default: - this.borderWidth = 2; - this.borderContrast = 1; - this.offset = 4; - this.fontSize = '12pt'; - break; - } + this.h2Elem.textContent = config.shortcutsInfoLabel; + this.closeButton1.setAttribute('aria-label', config.closeLabel); + this.closeButton2.textContent = config.closeLabel; + this.moreInfoButton.textContent = config.moreInfoLabel; - style = updateOption(style, - '$fontSize', - this.fontSize, - defaultStyleOptions$2.fontSize); + function addRow(tbodyElem, shortcut, desc) { - style = updateOption(style, - '$highlightOffset', - this.offset, - this.offset); + const trElem = document.createElement('tr'); + tbodyElem.appendChild(trElem); - style = updateOption(style, - '$overlayBorderWidth', - this.borderWidth, - this.borderWidth); + const tdElem1 = document.createElement('td'); + tdElem1.className = 'shortcut'; + tdElem1.textContent = shortcut; + trElem.appendChild(tdElem1); - style = updateOption(style, - '$shadowBorderWidth', - this.borderWidth + 2 * this.borderContrast, - this.borderWidth + 2 * this.borderContrast); + const tdElem2 = document.createElement('td'); + tdElem2.className = 'desc'; + tdElem2.textContent = desc; + trElem.appendChild(tdElem2); + } - style = updateOption(style, - '$infoBorderWidth', - this.borderWidth, - this.borderWidth); + // Regions - let styleNode = this.shadowRoot.querySelector('style'); + const tableElem1 = document.createElement('table'); + this.contentElem.appendChild(tableElem1); - if (styleNode) { - styleNode.remove(); - } + const captionElem1 = document.createElement('caption'); + captionElem1.textContent = config.landmarkGroupLabel; + tableElem1.appendChild(captionElem1); - styleNode = document.createElement('style'); - styleNode.textContent = style; - this.shadowRoot.appendChild(styleNode); + const theadElem1 = document.createElement('thead'); + tableElem1.appendChild(theadElem1); - } + const trElem1 = document.createElement('tr'); + theadElem1.appendChild(trElem1); - /* - * @method highlight - * - * @desc Highlights the element on the page when highlighting - * is enabled (NOTE: Highlight is enabled by default) - * - * @param {Object} elem : DOM node of element to highlight - * @param {String} highlightTarget : value of highlight target - * @param {String} info : Information about target - * @param {Boolean} force : If true override isRduced - */ + const thElem1 = document.createElement('th'); + thElem1.className = 'shortcut'; + thElem1.textContent = config.msgKey; + trElem1.appendChild(thElem1); - highlight(elem, highlightTarget, info='', force=false) { - let scrollElement; - const mediaQuery = window.matchMedia(`(prefers-reduced-motion: reduce)`); - const isReduced = !mediaQuery || mediaQuery.matches; + const thElem2 = document.createElement('th'); + thElem2.className = 'desc'; + thElem2.textContent = config.msgDescription; + trElem1.appendChild(thElem2); - if (elem && highlightTarget) { + const tbodyElem1 = document.createElement('tbody'); + tableElem1.appendChild(tbodyElem1); - const rect = elem.getBoundingClientRect(); + addRow(tbodyElem1, config.shortcutRegionNext, config.msgNextRegion); + addRow(tbodyElem1, config.shortcutRegionPrevious, config.msgPreviousRegion); + addRow(tbodyElem1, config.shortcutRegionMain, config.msgMainRegions); + addRow(tbodyElem1, config.shortcutRegionNavigation, config.msgNavigationRegions); + addRow(tbodyElem1, config.shortcutRegionComplementary, config.msgComplementaryRegions); - // If target element is hidden create a visible element - debug$9.flag && debug$9.log(`[ info]: ${info}`); - debug$9.flag && debug$9.log(`[ rect]: Left: ${rect.left} Top: ${rect.top} Width: ${rect.width} height: ${rect.height}`); - debug$9.flag && debug$9.log(`[isHidden]: ${this.isElementHidden(elem)}`); + // Headings - if (this.isElementHidden(elem)) { - // If element is hidden make hidden element message visible - // and use for highlighing - this.hiddenElem.textContent = this.getHiddenMessage(elem); - this.hiddenElem.style.display = 'block'; + const tableElem2 = document.createElement('table'); + this.contentElem.appendChild(tableElem2); - const left = rect.left > 0 ? rect.left + window.scrollX : this.offset; - const top = rect.top > 0 ? rect.top + window.scrollY : this.offset; + const captionElem2 = document.createElement('caption'); + captionElem2.textContent = config.headingGroupLabel; + tableElem2.appendChild(captionElem2); - this.hiddenElem.style.left = left + 'px'; - this.hiddenElem.style.top = top + 'px'; - scrollElement = this.updateHighlightElement(this.hiddenElem, - info, - 0, - this.borderWidth, - this.borderContrast); - } - else { - this.hiddenElem.style.display = 'none'; - scrollElement = this.updateHighlightElement(elem, - info, - this.offset, - this.borderWidth, - this.borderContrast); - } + const theadElem2 = document.createElement('thead'); + tableElem2.appendChild(theadElem2); + + const trElem2 = document.createElement('tr'); + theadElem2.appendChild(trElem2); + + const thElem3 = document.createElement('th'); + thElem3.className = 'shortcut'; + thElem3.textContent = config.msgKey; + trElem2.appendChild(thElem3); + + const thElem4 = document.createElement('th'); + thElem4.className = 'desc'; + thElem4.textContent = config.msgDescription; + trElem2.appendChild(thElem4); + + const tbodyElem2 = document.createElement('tbody'); + tableElem2.appendChild(tbodyElem2); + + addRow(tbodyElem2, config.shortcutHeadingNext, config.msgNextHeading); + addRow(tbodyElem2, config.shortcutHeadingPrevious, config.msgPreviousHeading); + addRow(tbodyElem2, config.shortcutHeadingH1, config.msgH1Headings); + addRow(tbodyElem2, config.shortcutHeadingH2, config.msgH2Headings); + addRow(tbodyElem2, config.shortcutHeadingH3, config.msgH3Headings); + addRow(tbodyElem2, config.shortcutHeadingH4, config.msgH4Headings); + addRow(tbodyElem2, config.shortcutHeadingH5, config.msgH5Headings); + addRow(tbodyElem2, config.shortcutHeadingH6, config.msgH6Headings); - if (this.isElementInHeightLarge(elem)) { - if (!this.isElementStartInViewport(elem) && (!isReduced || force)) { - scrollElement.scrollIntoView({ behavior: highlightTarget, block: 'start', inline: 'nearest' }); - } - } - else { - if (!this.isElementInViewport(elem) && (!isReduced || force)) { - scrollElement.scrollIntoView({ behavior: highlightTarget, block: 'center', inline: 'nearest' }); - } - } - } } - /* - * @method updateHighlightElement - * - * @desc Create an overlay element and set its position on the page. - * - * @param {Object} elem - DOM element node to highlight - * @param {String} info - Description of the element - * @param {Number} offset - Number of pixels for offset - * @param {Number} borderWidth - Number of pixels for border width - * @param {Number} borderContrast - Number of pixels to provide border contrast - * - */ + updateAboutContent (config) { - updateHighlightElement (elem, info, offset, borderWidth, borderContrast) { + while (this.contentElem.lastElementChild) { + this.contentElem.removeChild(this.contentElem.lastElementChild); + } - const adjRect = this.getAdjustedRect(elem, offset, borderWidth, borderContrast); + this.moreInfoURL = MORE_PAGE_SCRIPT_INFO_URL; - const borderElemOffset = -1 * (this.borderWidth + this.borderContrast); + this.h2Elem.textContent = config.aboutInfoLabel; + this.closeButton1.setAttribute('aria-label', config.closeLabel); + this.closeButton2.textContent = config.closeLabel; + this.moreInfoButton.textContent = config.moreInfoLabel; - this.overlayElem.style.left = adjRect.left + 'px'; - this.overlayElem.style.top = adjRect.top + 'px'; - this.borderElem.style.left = borderElemOffset + 'px'; - this.borderElem.style.top = borderElemOffset + 'px'; + let divElem = document.createElement('div'); + divElem.className = 'desc'; + divElem.textContent = config.aboutDesc; + this.contentElem.appendChild(divElem); - this.overlayElem.style.width = adjRect.width + 'px'; - this.overlayElem.style.height = adjRect.height + 'px'; - this.borderElem.style.width = (adjRect.width - (2 * borderContrast)) + 'px'; - this.borderElem.style.height = (adjRect.height - (2 * borderContrast)) + 'px'; + divElem = document.createElement('div'); + divElem.className = 'happy'; + divElem.textContent = config.aboutHappy; + this.contentElem.appendChild(divElem); - this.overlayElem.style.display = 'block'; + divElem = document.createElement('div'); + divElem.className = 'version'; + divElem.textContent = config.aboutVersion; + this.contentElem.appendChild(divElem); - if (info) { + divElem = document.createElement('div'); + divElem.className = 'copyright'; + divElem.textContent = config.aboutCopyright; + this.contentElem.appendChild(divElem); - this.infoElem.style.display = 'inline-block'; - this.infoElem.textContent = info; + } - const infoElemOffsetLeft = -1 * (borderWidth + 2 * borderContrast); - this.infoElem.style.left = infoElemOffsetLeft + 'px'; + onKeyDown (event) { - const infoElemRect = this.infoElem.getBoundingClientRect(); + if ((event.key === "Tab") && + !event.altKey && + !event.ctlKey && + !event.metaKey) { - // Is info displayed above or below the highlighted element - if (adjRect.top >= infoElemRect.height) { - // Info is displayed above the highlighted element (e.g. most of the time) - this.overlayElem.classList.remove('hasInfoBottom'); - this.borderElem.classList.remove('hasInfoBottom'); - this.infoElem.classList.remove('hasInfoBottom'); - this.overlayElem.classList.add('hasInfoTop'); - this.borderElem.classList.add('hasInfoTop'); - this.infoElem.classList.add('hasInfoTop'); - this.infoElem.style.top = (-1 * (adjRect.height + - infoElemRect.height + - borderWidth)) + 'px'; + if (event.shiftKey && + (event.currentTarget === this.closeButton1)) { + this.closeButton2.focus(); + event.preventDefault(); + event.stopPropagation(); } - else { - // Info is displayed below the highlighted element when it is at the top of - // the window - const infoElemOffsetTop = -1 * (borderWidth + borderContrast); - - this.overlayElem.classList.remove('hasInfoTop'); - this.borderElem.classList.remove('hasInfoTop'); + if (!event.shiftKey && + (event.currentTarget === this.closeButton2)) { + this.closeButton1.focus(); + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + + /* highlight.js */ + + /* Constants */ + const debug$9 = new DebugLogging('highlight', false); + debug$9.flag = false; + + const minWidth = 68; + const minHeight = 27; + + const defaultStyleOptions$2 = colorThemes['default']; + + const styleHighlightTemplate = document.createElement('template'); + styleHighlightTemplate.textContent = ` +:root { + color-scheme: light dark; +} + +#${HIGHLIGHT_ID} { + margin: 0; + padding: 0; + position: absolute; + border-radius: $highlightOffsetpx; + border: $shadowBorderWidthpx solid light-dark($menuBackgroundColor, $menuBackgroundDarkColor); + box-sizing: border-box; + pointer-events:none; + z-index: $zHighlight; +} + +#${HIGHLIGHT_ID}.hasInfoBottom, +#${HIGHLIGHT_ID} .overlay-border.hasInfoBottom { + border-radius: $highlightOffsetpx $highlightOffsetpx $highlightOffsetpx 0; +} + +#${HIGHLIGHT_ID}.hasInfoTop, +#${HIGHLIGHT_ID} .overlay-border.hasInfoTop { + border-radius: 0 $highlightOffsetpx $highlightOffsetpx $highlightOffsetpx; +} + +#${HIGHLIGHT_ID} .overlay-border { + margin: 0; + padding: 0; + position: relative; + border-radius: $highlightOffsetpx; + border: $overlayBorderWidthpx $highlightBorderStyle light-dark($focusBorderColor, $focusBorderDarkColor); + z-index: $zHighlight; + box-sizing: border-box; + pointer-events:none; + background: transparent; +} + + +@keyframes fadeIn { + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +#hidden-elem-msg { + position: absolute; + margin: 0; + padding: .25em; + background-color: light-dark($hiddenHeadingBackgroundColor, $hiddenHeadingBackgroundDarkColor); + color: light-dark($hiddenHeadingColor, $hiddenHeadingDarkColor); + font-family: $fontFamily; + font-size: $fontSize; + font-style: italic; + font-weight: bold; + text-align: center; + animation: fadeIn 1.5s; + z-index: $zHighlight; +} + +#${HIGHLIGHT_ID} .overlay-info { + margin: 0; + padding: 2px; + position: relative; + text-align: left; + font-size: $fontSize; + font-family: $fontFamily; + border: $infoBorderWidthpx solid light-dark($menuBackgroundColor, $menuBackgroundDarkColor); + background-color: light-dark($menuBackgroundColor, $menuBackgroundDarkColor); + color: light-dark($menuTextColor, $menuTextDarkColor); + z-index: $zHighlight; + overflow: hidden; + text-overflow: ellipsis; + pointer-events:none; +} + +#${HIGHLIGHT_ID} .overlay-info.hasInfoTop { + border-radius: $highlightOffsetpx $highlightOffsetpx 0 0; +} + +#${HIGHLIGHT_ID} .overlay-info.hasInfoBottom { + border-radius: 0 0 $highlightOffsetpx $highlightOffsetpx; +} + +@media (forced-colors: active) { + + #${HIGHLIGHT_ID} { + border-color: ButtonBorder; + } + + #${HIGHLIGHT_ID} .overlay-border { + border-color: ButtonBorder; + } + + #${HIGHLIGHT_ID} .overlay-border.skip-to-hidden { + background-color: ButtonFace; + color: ButtonText; + } + + #${HIGHLIGHT_ID} .overlay-info { + border-color: ButtonBorder; + background-color: ButtonFace; + color: ButtonText; + } + +} +`; + + /* + * @class HighlightElement + * + */ + + class HighlightElement extends HTMLElement { + + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + + // Get references + + this.overlayElem = document.createElement('div'); + this.overlayElem.id = HIGHLIGHT_ID; + this.shadowRoot.appendChild(this.overlayElem); + this.overlayElem.style.display = 'none'; + + this.borderElem = document.createElement('div'); + this.borderElem.className = 'overlay-border'; + this.overlayElem.appendChild(this.borderElem); + + this.infoElem = document.createElement('div'); + this.infoElem.className = 'overlay-info'; + this.overlayElem.appendChild(this.infoElem); + + this.hiddenElem = document.createElement('div'); + this.hiddenElem.id = 'hidden-elem-msg'; + this.shadowRoot.appendChild(this.hiddenElem); + this.hiddenElem.style.display = 'none'; + + this.borderWidth = 0; + this.borderContrast = 0; + this.offset = 0; + + this.msgHeadingIsHidden = ''; + + this.configureStyle(); + + } + + /* + * @method configureStyle + * + * @desc Updates stylesheet for styling the highlight information + * + * @param {Object} config : color and font information + */ + + configureStyle(config={}) { + + function updateOption(style, option, configOption, defaultOption) { + if (configOption) { + return style.replaceAll(option, configOption); + } + else { + return style.replaceAll(option, defaultOption); + } + } + + // Get i18n Messages + + this.msgHeadingIsHidden = typeof config.msgHeadingIsHidden === 'string' ? + config.msgHeadingIsHidden : + 'Heading is hidden'; + + this.msgRegionIsHidden = typeof config.msgRegionIsHidden === 'string' ? + config.msgRegionIsHidden : + 'Region is hidden'; + + this.msgElementIsHidden = typeof config.msgElementIsHidden === 'string' ? + config.msgElemenIsHidden : + 'Element is hidden'; + + + // make a copy of the template + let style = styleHighlightTemplate.textContent.slice(0); + + style = updateOption(style, + '$fontFamily', + config.fontFamily, + defaultStyleOptions$2.fontFamily); + + style = updateOption(style, + '$buttonBackgroundColor', + config.buttonBackgroundColor, + defaultStyleOptions$2.buttonBackgroundColor); + + style = updateOption(style, + '$buttonBackgroundDarkColor', + config.buttonBackgroundDarkColor, + defaultStyleOptions$2.buttonBackgroundDarkColor); + + style = updateOption(style, + '$focusBorderColor', + config.focusBorderColor, + defaultStyleOptions$2.focusBorderColor); + + style = updateOption(style, + '$focusBorderDarkColor', + config.focusBorderDarkColor, + defaultStyleOptions$2.focusBorderDarkColor); + + style = updateOption(style, + '$menuBackgroundColor', + config.menuBackgroundColor, + defaultStyleOptions$2.menuBackgroundColor); + + style = updateOption(style, + '$menuBackgroundDarkColor', + config.menuBackgroundDarkColor, + defaultStyleOptions$2.menuBackgroundDarkColor); + + style = updateOption(style, + '$menuTextColor', + config.menuTextColor, + defaultStyleOptions$2.menuTextColor); + + style = updateOption(style, + '$menuTextDarkColor', + config.menuTextDarkColor, + defaultStyleOptions$2.menuTextDarkColor); + + style = updateOption(style, + '$hiddenHeadingColor', + config.hiddenHeadingColor, + defaultStyleOptions$2.hiddenHeadingColor); + + style = updateOption(style, + '$hiddenHeadingDarkColor', + config.hiddenHeadingDarkColor, + defaultStyleOptions$2.hiddenHeadingDarkColor); + + style = updateOption(style, + '$hiddenHeadingBackgroundColor', + config.hiddenHeadingBackgroundColor, + defaultStyleOptions$2.hiddenHeadingBackgroundColor); + + style = updateOption(style, + '$hiddenHeadingBackgroundDarkColor', + config.hiddenHeadingBackgroundDarkColor, + defaultStyleOptions$2.hiddenHeadingBackgroundDarkColor); + + style = updateOption(style, + '$zHighlight', + config.zHighlight, + defaultStyleOptions$2.zHighlight); + + style = updateOption(style, + '$highlightBorderStyle', + config.highlightBorderStyle, + defaultStyleOptions$2.highlightBorderStyle); + + const highlightBorderSize = config.highlightBorderSize ? + config.highlightBorderSize : + defaultStyleOptions$2.highlightBorderSize; + + switch (highlightBorderSize) { + case 'small': + this.borderWidth = 2; + this.borderContrast = 1; + this.offset = 4; + this.fontSize = '12pt'; + break; + + case 'medium': + this.borderWidth = 3; + this.borderContrast = 2; + this.offset = 4; + this.fontSize = '13pt'; + break; + + case 'large': + this.borderWidth = 4; + this.borderContrast = 3; + this.offset = 6; + this.fontSize = '14pt'; + break; + + case 'x-large': + this.borderWidth = 6; + this.borderContrast = 3; + this.offset = 8; + this.fontSize = '16pt'; + break; + + default: + this.borderWidth = 2; + this.borderContrast = 1; + this.offset = 4; + this.fontSize = '12pt'; + break; + } + + style = updateOption(style, + '$fontSize', + this.fontSize, + defaultStyleOptions$2.fontSize); + + style = updateOption(style, + '$highlightOffset', + this.offset, + this.offset); + + style = updateOption(style, + '$overlayBorderWidth', + this.borderWidth, + this.borderWidth); + + style = updateOption(style, + '$shadowBorderWidth', + this.borderWidth + 2 * this.borderContrast, + this.borderWidth + 2 * this.borderContrast); + + style = updateOption(style, + '$infoBorderWidth', + this.borderWidth, + this.borderWidth); + + let styleNode = this.shadowRoot.querySelector('style'); + + if (styleNode) { + styleNode.remove(); + } + + styleNode = document.createElement('style'); + styleNode.textContent = style; + this.shadowRoot.appendChild(styleNode); + + } + + /* + * @method highlight + * + * @desc Highlights the element on the page when highlighting + * is enabled (NOTE: Highlight is enabled by default) + * + * @param {Object} elem : DOM node of element to highlight + * @param {String} highlightTarget : value of highlight target + * @param {String} info : Information about target + * @param {Boolean} force : If true override isRduced + */ + + highlight(elem, highlightTarget, info='', force=false) { + let scrollElement; + const mediaQuery = window.matchMedia(`(prefers-reduced-motion: reduce)`); + const isReduced = !mediaQuery || mediaQuery.matches; + + if (elem && highlightTarget) { + + const rect = elem.getBoundingClientRect(); + + // If target element is hidden create a visible element + debug$9.flag && debug$9.log(`[ info]: ${info}`); + debug$9.flag && debug$9.log(`[ rect]: Left: ${rect.left} Top: ${rect.top} Width: ${rect.width} height: ${rect.height}`); + debug$9.flag && debug$9.log(`[isHidden]: ${this.isElementHidden(elem)}`); + + if (this.isElementHidden(elem)) { + // If element is hidden make hidden element message visible + // and use for highlighing + this.hiddenElem.textContent = this.getHiddenMessage(elem); + this.hiddenElem.style.display = 'block'; + + const left = rect.left > 0 ? rect.left + window.scrollX : this.offset; + const top = rect.top > 0 ? rect.top + window.scrollY : this.offset; + + this.hiddenElem.style.left = left + 'px'; + this.hiddenElem.style.top = top + 'px'; + scrollElement = this.updateHighlightElement(this.hiddenElem, + info, + 0, + this.borderWidth, + this.borderContrast); + } + else { + this.hiddenElem.style.display = 'none'; + scrollElement = this.updateHighlightElement(elem, + info, + this.offset, + this.borderWidth, + this.borderContrast); + } + + if (this.isElementInHeightLarge(elem)) { + if (!this.isElementStartInViewport(elem) && (!isReduced || force)) { + scrollElement.scrollIntoView({ behavior: highlightTarget, block: 'start', inline: 'nearest' }); + } + } + else { + if (!this.isElementInViewport(elem) && (!isReduced || force)) { + scrollElement.scrollIntoView({ behavior: highlightTarget, block: 'center', inline: 'nearest' }); + } + } + } + } + + /* + * @method updateHighlightElement + * + * @desc Create an overlay element and set its position on the page. + * + * @param {Object} elem - DOM element node to highlight + * @param {String} info - Description of the element + * @param {Number} offset - Number of pixels for offset + * @param {Number} borderWidth - Number of pixels for border width + * @param {Number} borderContrast - Number of pixels to provide border contrast + * + */ + + updateHighlightElement (elem, info, offset, borderWidth, borderContrast) { + + const adjRect = this.getAdjustedRect(elem, offset, borderWidth, borderContrast); + + const borderElemOffset = -1 * (this.borderWidth + this.borderContrast); + + this.overlayElem.style.left = adjRect.left + 'px'; + this.overlayElem.style.top = adjRect.top + 'px'; + this.borderElem.style.left = borderElemOffset + 'px'; + this.borderElem.style.top = borderElemOffset + 'px'; + + this.overlayElem.style.width = adjRect.width + 'px'; + this.overlayElem.style.height = adjRect.height + 'px'; + this.borderElem.style.width = (adjRect.width - (2 * borderContrast)) + 'px'; + this.borderElem.style.height = (adjRect.height - (2 * borderContrast)) + 'px'; + + this.overlayElem.style.display = 'block'; + + if (info) { + + this.infoElem.style.display = 'inline-block'; + this.infoElem.textContent = info; + + const infoElemOffsetLeft = -1 * (borderWidth + 2 * borderContrast); + this.infoElem.style.left = infoElemOffsetLeft + 'px'; + + const infoElemRect = this.infoElem.getBoundingClientRect(); + + // Is info displayed above or below the highlighted element + if (adjRect.top >= infoElemRect.height) { + // Info is displayed above the highlighted element (e.g. most of the time) + this.overlayElem.classList.remove('hasInfoBottom'); + this.borderElem.classList.remove('hasInfoBottom'); + this.infoElem.classList.remove('hasInfoBottom'); + this.overlayElem.classList.add('hasInfoTop'); + this.borderElem.classList.add('hasInfoTop'); + this.infoElem.classList.add('hasInfoTop'); + this.infoElem.style.top = (-1 * (adjRect.height + + infoElemRect.height + + borderWidth)) + 'px'; + } + else { + // Info is displayed below the highlighted element when it is at the top of + // the window + + const infoElemOffsetTop = -1 * (borderWidth + borderContrast); + + this.overlayElem.classList.remove('hasInfoTop'); + this.borderElem.classList.remove('hasInfoTop'); this.infoElem.classList.remove('hasInfoTop'); this.overlayElem.classList.add('hasInfoBottom'); this.borderElem.classList.add('hasInfoBottom'); @@ -1895,751 +1896,751 @@ button:hover { /* - * @method getAdjustedRect + * @method getAdjustedRect + * + * @desc Returns a object with dimensions adjusted for highlighting element + * + * @param {Object} elem - DOM node of element to be highlighted + * @param {Number} offset - Number of pixels for offset + * @param {Number} borderWidth - Number of pixels for border width + * @param {Number} borderContrast - Number of pixels to provide border contrast + * + * @returns see @desc + */ + + getAdjustedRect(elem, offset, borderWidth, borderContrast) { + + const rect = elem.getBoundingClientRect(); + + const adjRect = { + left: 0, + top: 0, + width: 0, + height: 0 + }; + + const offsetBorder = offset + borderWidth + 2 * borderContrast; + + adjRect.left = rect.left > offset ? + Math.round(rect.left + (-1 * offsetBorder) + window.scrollX) : + Math.round(rect.left + window.scrollX); + + adjRect.width = rect.left > offset ? + Math.max(rect.width + (2 * offsetBorder), minWidth) : + Math.max(rect.width, minWidth); + + + adjRect.top = rect.top > offset ? + Math.round(rect.top + (-1 * offsetBorder) + window.scrollY) : + Math.round(rect.top + window.scrollY); + + adjRect.height = rect.top > offset ? + Math.max(rect.height + (2 * offsetBorder), minHeight) : + Math.max(rect.height, minHeight); + + if ((adjRect.top < 0) || (adjRect.left < 0)) { + // Element is near top or left side of screen + adjRect.left = this.offset; + adjRect.top = this.offset; + } + + return adjRect; + } + + /* + * @method isElementInViewport + * + * @desc Returns true if element is already visible in view port, + * otheriwse false + * + * @param {Object} elem : DOM node of element to highlight + * + * @returns see @desc + */ + + isElementInViewport(elem) { + const rect = elem.getBoundingClientRect(); + return ( + rect.top >= window.screenY && + rect.left >= window.screenX && + rect.bottom <= ((window.screenY + window.innerHeight) || + (window.screenY + document.documentElement.clientHeight)) && + rect.right <= ((window.screenX + window.innerWidth) || + (window.screenX + document.documentElement.clientWidth))); + } + + /* + * @method isElementStartInViewport + * + * @desc Returns true if start of the element is already visible in view port, + * otherwise false + * + * @param {Object} elem : DOM node of element to highlight + * + * @returns see @desc + */ + + isElementStartInViewport(elem) { + const rect = elem.getBoundingClientRect(); + return ( + rect.top >= window.screenY && + rect.top <= ((window.screenY + window.innerHeight) || + (window.screenY + document.documentElement.clientHeight)) && + rect.left >= window.screenX && + rect.left <= ((window.screenX + window.innerWidth) || + (window.screenX + document.documentElement.clientWidth))); + } + + + /* + * @method isElementHeightLarge * - * @desc Returns a object with dimensions adjusted for highlighting element + * @desc Returns true if element client height is larger than clientHeight, + * otheriwse false * - * @param {Object} elem - DOM node of element to be highlighted - * @param {Number} offset - Number of pixels for offset - * @param {Number} borderWidth - Number of pixels for border width - * @param {Number} borderContrast - Number of pixels to provide border contrast + * @param {Object} elem : DOM node of element to highlight * * @returns see @desc */ - getAdjustedRect(elem, offset, borderWidth, borderContrast) { + isElementInHeightLarge(elem) { + var rect = elem.getBoundingClientRect(); + return (1.2 * rect.height) > (window.innerHeight || document.documentElement.clientHeight); + } + + /* + * @method isElementHidden + * + * @desc Returns true if the element is hidden on the + * graphical rendering + * + * @param {Object} elem : DOM node + * + * @returns see @desc + */ + isElementHidden(elem) { + const rect = elem.getBoundingClientRect(); + return (rect.height < 3) || + (rect.width < 3) || + ((rect.left + rect.width) < (rect.width / 2)) || + ((rect.top + rect.height) < (rect.height / 2)); + } + + /* + * @method getHiddenMessage + * + * @desc Returns string describing the hidden element + * + * @param {Object} elem : DOM node + * + * @returns see @desc + */ + getHiddenMessage(elem) { + if (elem.hasAttribute('data-skip-to-info')) { + const info = elem.getAttribute('data-skip-to-info'); + + if (info.includes('heading')) { + return this.msgHeadingIsHidden; + } + + if (info.includes('landmark')) { + return this.msgRegionIsHidden; + } + } + + return this.msgElementIsHidden; + } + + /* + * @method removeHighlight + * + * @desc Hides the highlight element on the page + */ + removeHighlight() { + if (this.overlayElem) { + this.overlayElem.style.display = 'none'; + } + } + + } + + /* shortcutsMessage.js */ + + /* Constants */ + const debug$8 = new DebugLogging('[shortcutsMessage]', false); + debug$8.flag = false; + + const defaultStyleOptions$1 = colorThemes['default']; + + const styleTemplate = document.createElement('template'); + styleTemplate.textContent = ` +/* shortcutsMessage.css */ +:root { + color-scheme: light dark; +} + +#${MESSAGE_ID} { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%,-50%); + + font-family: $fontFamily; + font-size: $fontSize; + max-width: 70%; + margin: 0; + padding: 0; + background-color: light-dark($dialogBackgroundColor, $dialogBackgroundDarkColor); + border: 2px solid light-dark($focusBorderColor, $focusBorderDarkColor); + border-radius: 5px; + color: light-dark($dialogTextColor, $dialogTextDarkColor); + z-index: 2000001; + opacity: 1; +} + +#${MESSAGE_ID} .header { + margin: 0; + padding: 4px; + border-bottom: 1px solid light-dark($focusBorderColor, $focusBorderDarkColor); + border-top-left-radius: 5px; + border-top-right-radius: 5px; + font-weight: bold; + background-color: light-dark($dialogBackgroundTitleColor, $dialogBackgroundTitleDarkColor); + color light-dark($dialogTextColor, $dialogTextDarkColor); + font-size: 100%; +} + +#${MESSAGE_ID} .content { + margin-left: 2em; + margin-right: 2em; + margin-top: 2em; + margin-bottom: 2em; + background-color: light-dark($dialogBackgroundColor, $dialogBackgroundDarkColor); + color: light-dark($dialogTextColor, $dialogTextDarkColor); + font-size: 110%; + text-algin: center; +} + +#${MESSAGE_ID}.hidden { + display: none; +} + +#${MESSAGE_ID}.show { + display: block; + opacity: 1; +} + +#${MESSAGE_ID}.fade { + opacity: 0; + transition: visibility 0s 1s, opacity 1s linear; +} + +@media (forced-colors: active) { + + #${MESSAGE_ID} { + background-color: Canvas; + color CanvasText; + border-color: AccentColor; + } + + #${MESSAGE_ID} .header { + background-color: Canvas; + color CanvasText; + } + + #${MESSAGE_ID} .content { + background-color: Canvas; + color: CanvasText; + } +} + +`; + + class ShortcutsMessage extends HTMLElement { + constructor () { + + super(); + this.attachShadow({ mode: 'open' }); + + // Get references + + this.messageDialog = document.createElement('div'); + this.messageDialog.id = MESSAGE_ID; + this.messageDialog.classList.add('hidden'); + this.shadowRoot.appendChild(this.messageDialog); + + const headerElem = document.createElement('div'); + headerElem.className = 'header'; + headerElem.textContent = 'SkipTo.js Message'; + this.messageDialog.appendChild(headerElem); + + this.contentElem = document.createElement('div'); + this.contentElem.className = 'content'; + this.messageDialog.appendChild(this.contentElem); + + this.timeoutShowID = false; + this.timeoutFadeID = false; + + } + + configureStyle(config={}) { + + function updateOption(style, option, configOption, defaultOption) { + if (configOption) { + return style.replaceAll(option, configOption); + } + else { + return style.replaceAll(option, defaultOption); + } + } + + // make a copy of the template + let style = styleTemplate.textContent.slice(0); + + style = updateOption(style, + '$fontFamily', + config.fontFamily, + defaultStyleOptions$1.fontFamily); + + style = updateOption(style, + '$fontSize', + config.fontSize, + defaultStyleOptions$1.fontSize); + + style = updateOption(style, + '$focusBorderColor', + config.focusBorderColor, + defaultStyleOptions$1.focusBorderColor); + + style = updateOption(style, + '$focusBorderDarkColor', + config.focusBorderDarkColor, + defaultStyleOptions$1.focusBorderDarkColor); + + + style = updateOption(style, + '$dialogTextColor', + config.dialogTextColor, + defaultStyleOptions$1.dialogTextColor); + + style = updateOption(style, + '$dialogTextDarkColor', + config.dialogTextDarkColor, + defaultStyleOptions$1.dialogTextDarkColor); + + style = updateOption(style, + '$dialogBackgroundColor', + config.dialogBackgroundColor, + defaultStyleOptions$1.dialogBackgroundColor); + + style = updateOption(style, + '$dialogBackgroundDarkColor', + config.dialogBackgroundDarkColor, + defaultStyleOptions$1.dialogBackgroundDarkColor); + + style = updateOption(style, + '$dialogBackgroundTitleColor', + config.dialogBackgroundTitleColor, + defaultStyleOptions$1.dialogBackgroundTitleColor); + + style = updateOption(style, + '$dialogBackgroundTitleDarkColor', + config.dialogBackgroundTitleDarkColor, + defaultStyleOptions$1.dialogBackgroundTitleDarkColor); + + let styleNode = this.shadowRoot.querySelector('style'); + + if (styleNode) { + styleNode.remove(); + } + + styleNode = document.createElement('style'); + styleNode.textContent = style; + this.shadowRoot.appendChild(styleNode); + + } + + close() { + this.messageDialog.classList.remove('show'); + this.messageDialog.classList.remove('fade'); + this.messageDialog.classList.add('hidden'); + } + + open(message) { + clearInterval(this.timeoutFadeID); + clearInterval(this.timeoutShowID); + this.messageDialog.classList.remove('hidden'); + this.messageDialog.classList.remove('fade'); + this.messageDialog.classList.add('show'); + this.contentElem.textContent = message; + + const msg = this; + + this.timeoutFadeID = setTimeout( () => { + msg.messageDialog.classList.add('fade'); + msg.messageDialog.classList.remove('show'); + }, 3000); + + this.timeoutShowID = setTimeout( () => { + msg.close(); + }, 4000); + + } + + } + + /* + * namefrom.js + */ + + /* constants */ + + const debug$7 = new DebugLogging('nameFrom', false); + debug$7.flag = false; + + // + // LOW-LEVEL HELPER FUNCTIONS (NOT EXPORTED) + + /* + * @function isDisplayNone + * + * @desc Returns true if the element or parent element has set the CSS + * display property to none or has the hidden attribute, + * otherwise false + * + * @param {Object} node - a DOM node + * + * @returns {Boolean} see @desc + */ + + function isDisplayNone (node) { + + if (!node) { + return false; + } + + if (node.nodeType === Node.TEXT_NODE) { + node = node.parentNode; + } + + if (node.nodeType === Node.ELEMENT_NODE) { + + if (node.hasAttribute('hidden')) { + return true; + } + + // aria-hidden attribute with the value "true" is an same as + // setting the hidden attribute for name calcuation + if (node.hasAttribute('aria-hidden')) { + if (node.getAttribute('aria-hidden').toLowerCase() === 'true') { + return true; + } + } + + const style = window.getComputedStyle(node, null); + + const display = style.getPropertyValue("display"); + + if (display) { + return display === 'none'; + } + } + return false; + } + + /* + * @function isVisibilityHidden + * + * @desc Returns true if the node (or it's parrent) has the CSS visibility + * property set to "hidden" or "collapse", otherwise false + * + * @param {Object} node - DOM node + * + * @return see @desc + */ + + function isVisibilityHidden(node) { + + if (!node) { + return false; + } + + if (node.nodeType === Node.TEXT_NODE) { + node = node.parentNode; + } + + if (node.nodeType === Node.ELEMENT_NODE) { + const style = window.getComputedStyle(node, null); + + const visibility = style.getPropertyValue("visibility"); + if (visibility) { + return (visibility === 'hidden') || (visibility === 'collapse'); + } + } + return false; + } - const rect = elem.getBoundingClientRect(); + /* + * @function isAriaHiddenFalse + * + * @desc Returns true if the node has the aria-hidden property set to + * "false", otherwise false. + * NOTE: This function is important in the accessible namce + * calculation, since content hidden with a CSS technique + * can be included in the accessible name calculation when + * aria-hidden is set to false + * + * @param {Object} node - DOM node + * + * @return see @desc + */ - const adjRect = { - left: 0, - top: 0, - width: 0, - height: 0 - }; + function isAriaHIddenFalse(node) { - const offsetBorder = offset + borderWidth + 2 * borderContrast; + if (!node) { + return false; + } - adjRect.left = rect.left > offset ? - Math.round(rect.left + (-1 * offsetBorder) + window.scrollX) : - Math.round(rect.left + window.scrollX); + if (node.nodeType === Node.TEXT_NODE) { + node = node.parentNode; + } - adjRect.width = rect.left > offset ? - Math.max(rect.width + (2 * offsetBorder), minWidth) : - Math.max(rect.width, minWidth); + if (node.nodeType === Node.ELEMENT_NODE) { + return (node.hasAttribute('aria-hidden') && + (node.getAttribute('aria-hidden').toLowerCase() === 'false')); + } + return false; + } - adjRect.top = rect.top > offset ? - Math.round(rect.top + (-1 * offsetBorder) + window.scrollY) : - Math.round(rect.top + window.scrollY); + /* + * @function includeContentInName + * + * @desc Checks the CSS display and hidden properties, and + * the aria-hidden property to see if the content + * should be included in the accessible name + * calculation. Returns true if it should be + * included, otherwise false + * + * @param {Object} node - DOM node + * + * @return see @desc + */ - adjRect.height = rect.top > offset ? - Math.max(rect.height + (2 * offsetBorder), minHeight) : - Math.max(rect.height, minHeight); + function includeContentInName(node) { + const flag = isAriaHIddenFalse(node) || + (!isVisibilityHidden(node) && + !isDisplayNone(node)); + return flag; + } - if ((adjRect.top < 0) || (adjRect.left < 0)) { - // Element is near top or left side of screen - adjRect.left = this.offset; - adjRect.top = this.offset; - } + /* + * @function getNodeContents + * + * @desc Recursively process element and text nodes by aggregating + * their text values for an ARIA accessible name or description + * calculation. + * + * NOTE: This includes special handling of elements with 'alt' + * text and embedded controls. + * + * @param {Object} node - A DOM node + * + * @return {String} The text content for an accessible name or description + */ + function getNodeContents (node) { + let contents = ''; + let nc; + let arr = []; + + switch (node.nodeType) { + case Node.ELEMENT_NODE: + // If aria-label is present, node recursion stops and + // aria-label value is returned + if (node.hasAttribute('aria-label')) { + if (includeContentInName(node)) { + contents = node.getAttribute('aria-label'); + } + } + else { + if (node instanceof HTMLSlotElement) { + // if no slotted elements, check for default slotted content + const assignedNodes = node.assignedNodes().length ? node.assignedNodes() : node.assignedNodes({ flatten: true }); + assignedNodes.forEach( assignedNode => { + nc = getNodeContents(assignedNode); + if (nc.length) arr.push(nc); + }); + contents = (arr.length) ? arr.join(' ') : ''; + } else { + if (couldHaveAltText(node) && includeContentInName(node)) { + contents = getAttributeValue(node, 'alt'); + } + else { + if (node.hasChildNodes()) { + let children = Array.from(node.childNodes); + children.forEach( child => { + nc = getNodeContents(child); + if (nc.length) arr.push(nc); + }); + contents = (arr.length) ? arr.join(' ') : ''; + } + } + // For all branches of the ELEMENT_NODE case... + } + } + contents = addCssGeneratedContent(node, contents); + break; - return adjRect; + case Node.TEXT_NODE: + if (includeContentInName(node)) { + contents = normalize(node.textContent); + } + break; } - /* - * @method isElementInViewport - * - * @desc Returns true if element is already visible in view port, - * otheriwse false - * - * @param {Object} elem : DOM node of element to highlight - * - * @returns see @desc - */ + return contents; + } - isElementInViewport(elem) { - const rect = elem.getBoundingClientRect(); - return ( - rect.top >= window.screenY && - rect.left >= window.screenX && - rect.bottom <= ((window.screenY + window.innerHeight) || - (window.screenY + document.documentElement.clientHeight)) && - rect.right <= ((window.screenX + window.innerWidth) || - (window.screenX + document.documentElement.clientWidth))); + /* + * @function couldHaveAltText + * + * @desc Based on HTML5 specification, returns true if + * the element could have an 'alt' attribute, + * otherwise false. + * + * @param {Object} element - DOM eleemnt node + * + * @return {Boolean} see @desc + */ + function couldHaveAltText (element) { + let tagName = element.tagName.toLowerCase(); + + switch (tagName) { + case 'img': + case 'area': + return true; + case 'input': + return (element.type && element.type === 'image'); } - /* - * @method isElementStartInViewport - * - * @desc Returns true if start of the element is already visible in view port, - * otherwise false - * - * @param {Object} elem : DOM node of element to highlight - * - * @returns see @desc - */ + return false; + } - isElementStartInViewport(elem) { - const rect = elem.getBoundingClientRect(); - return ( - rect.top >= window.screenY && - rect.top <= ((window.screenY + window.innerHeight) || - (window.screenY + document.documentElement.clientHeight)) && - rect.left >= window.screenX && - rect.left <= ((window.screenX + window.innerWidth) || - (window.screenX + document.documentElement.clientWidth))); + /* + * @function addCssGeneratedContent + * + * @desc Adds CSS-generated content for pseudo-elements + * :before and :after. According to the CSS spec, test that content + * value is other than the default computed value of 'none'. + * + * Note: Even if an author specifies content: 'none', because browsers + * add the double-quote character to the beginning and end of + * computed string values, the result cannot and will not be + * equal to 'none'. + * + * + * @param {Object} element - DOM node element + * @param {String} contents - Text content for DOM node + * + * @returns {String} see @desc + * + */ + function addCssGeneratedContent (element, contents) { + + let result = contents, + prefix = getComputedStyle(element, ':before').content, + suffix = getComputedStyle(element, ':after').content; + + if ((prefix[0] === '"') && !prefix.toLowerCase().includes('moz-')) { + result = prefix.substring(1, (prefix.length-1)) + result; } + if ((suffix[0] === '"') && !suffix.toLowerCase().includes('moz-')) { + result = result + suffix.substring(1, (suffix.length-1)) ; + } - /* - * @method isElementHeightLarge - * - * @desc Returns true if element client height is larger than clientHeight, - * otheriwse false - * - * @param {Object} elem : DOM node of element to highlight - * - * @returns see @desc - */ + return result; + } - isElementInHeightLarge(elem) { - var rect = elem.getBoundingClientRect(); - return (1.2 * rect.height) > (window.innerHeight || document.documentElement.clientHeight); + /* accName.js */ + + /* Constants */ + const debug$6 = new DebugLogging('accName', false); + debug$6.flag = false; + + /** + * @fuction getAccessibleName + * + * @desc Returns the accessible name for an heading or landamrk + * + * @paramn {Object} dom - Document of the current element + * @param {node} element - DOM element node for either a heading or + * landmark + * @param {Boolean} fromContent - if true will compute name from content + * + * @return {String} The accessible name for the landmark or heading element + */ + + function getAccessibleName (doc, element, fromContent=false) { + let accName = ''; + + accName = nameFromAttributeIdRefs(doc, element, 'aria-labelledby'); + + if (accName === '' && element.hasAttribute('aria-label')) { + accName = element.getAttribute('aria-label').trim(); } - /* - * @method isElementHidden - * - * @desc Returns true if the element is hidden on the - * graphical rendering - * - * @param {Object} elem : DOM node - * - * @returns see @desc - */ - isElementHidden(elem) { - const rect = elem.getBoundingClientRect(); - return (rect.height < 3) || - (rect.width < 3) || - ((rect.left + rect.width) < (rect.width / 2)) || - ((rect.top + rect.height) < (rect.height / 2)); + if (accName === '' && fromContent) { + accName = getNodeContents(element); } - /* - * @method getHiddenMessage - * - * @desc Returns string describing the hidden element - * - * @param {Object} elem : DOM node - * - * @returns see @desc - */ - getHiddenMessage(elem) { - if (elem.hasAttribute('data-skip-to-info')) { - const info = elem.getAttribute('data-skip-to-info'); + if (accName === '' && element.title.trim() !== '') { + accName = element.title.trim(); + } - if (info.includes('heading')) { - return this.msgHeadingIsHidden; - } + return accName; + } - if (info.includes('landmark')) { - return this.msgRegionIsHidden; + /* + * @function nameFromAttributeIdRefs + * + * @desc Get the value of attrName on element (a space- + * separated list of IDREFs), visit each referenced element in the order it + * appears in the list and obtain its accessible name (skipping recursive + * aria-labelledby or aria-describedby calculations), and return an object + * with name property set to a string that is a space-separated concatena- + * tion of those results if any, otherwise return empty string. + * + * @param {Object} doc - Browser document object + * @param {Object} element - DOM element node + * @param {String} attribute - Attribute name (e.g. "aria-labelledby", "aria-describedby", + * or "aria-errormessage") + * + * @returns {String} see @desc + */ + function nameFromAttributeIdRefs (doc, element, attribute) { + const value = getAttributeValue(element, attribute); + const arr = []; + + if (value.length) { + const idRefs = value.split(' '); + + for (let i = 0; i < idRefs.length; i++) { + const refElement = doc.getElementById(idRefs[i]); + if (refElement) { + const accName = getNodeContents(refElement); + if (accName && accName.length) arr.push(accName); } } - - return this.msgElementIsHidden; } - /* - * @method removeHighlight - * - * @desc Hides the highlight element on the page - */ - removeHighlight() { - if (this.overlayElem) { - this.overlayElem.style.display = 'none'; - } + if (arr.length) { + return arr.join(' '); } - } - - /* shortcutsMessage.js */ - - /* Constants */ - const debug$8 = new DebugLogging('[shortcutsMessage]', false); - debug$8.flag = false; - - const defaultStyleOptions$1 = colorThemes['default']; - - const styleTemplate = document.createElement('template'); - styleTemplate.textContent = ` -/* shortcutsMessage.css */ -:root { - color-scheme: light dark; -} - -#${MESSAGE_ID} { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%,-50%); - - font-family: $fontFamily; - font-size: $fontSize; - max-width: 70%; - margin: 0; - padding: 0; - background-color: light-dark($dialogBackgroundColor, $dialogBackgroundDarkColor); - border: 2px solid light-dark($focusBorderColor, $focusBorderDarkColor); - border-radius: 5px; - color: light-dark($dialogTextColor, $dialogTextDarkColor); - z-index: 2000001; - opacity: 1; -} - -#${MESSAGE_ID} .header { - margin: 0; - padding: 4px; - border-bottom: 1px solid light-dark($focusBorderColor, $focusBorderDarkColor); - border-top-left-radius: 5px; - border-top-right-radius: 5px; - font-weight: bold; - background-color: light-dark($dialogBackgroundTitleColor, $dialogBackgroundTitleDarkColor); - color light-dark($dialogTextColor, $dialogTextDarkColor); - font-size: 100%; -} - -#${MESSAGE_ID} .content { - margin-left: 2em; - margin-right: 2em; - margin-top: 2em; - margin-bottom: 2em; - background-color: light-dark($dialogBackgroundColor, $dialogBackgroundDarkColor); - color: light-dark($dialogTextColor, $dialogTextDarkColor); - font-size: 110%; - text-algin: center; -} - -#${MESSAGE_ID}.hidden { - display: none; -} - -#${MESSAGE_ID}.show { - display: block; - opacity: 1; -} - -#${MESSAGE_ID}.fade { - opacity: 0; - transition: visibility 0s 1s, opacity 1s linear; -} - -@media (forced-colors: active) { - - #${MESSAGE_ID} { - background-color: Canvas; - color CanvasText; - border-color: AccentColor; - } - - #${MESSAGE_ID} .header { - background-color: Canvas; - color CanvasText; - } - - #${MESSAGE_ID} .content { - background-color: Canvas; - color: CanvasText; - } -} - -`; - - class ShortcutsMessage extends HTMLElement { - constructor () { - - super(); - this.attachShadow({ mode: 'open' }); - - // Get references - - this.messageDialog = document.createElement('div'); - this.messageDialog.id = MESSAGE_ID; - this.messageDialog.classList.add('hidden'); - this.shadowRoot.appendChild(this.messageDialog); - - const headerElem = document.createElement('div'); - headerElem.className = 'header'; - headerElem.textContent = 'SkipTo.js Message'; - this.messageDialog.appendChild(headerElem); - - this.contentElem = document.createElement('div'); - this.contentElem.className = 'content'; - this.messageDialog.appendChild(this.contentElem); - - this.timeoutShowID = false; - this.timeoutFadeID = false; - - } - - configureStyle(config={}) { - - function updateOption(style, option, configOption, defaultOption) { - if (configOption) { - return style.replaceAll(option, configOption); - } - else { - return style.replaceAll(option, defaultOption); - } - } - - // make a copy of the template - let style = styleTemplate.textContent.slice(0); - - style = updateOption(style, - '$fontFamily', - config.fontFamily, - defaultStyleOptions$1.fontFamily); - - style = updateOption(style, - '$fontSize', - config.fontSize, - defaultStyleOptions$1.fontSize); - - style = updateOption(style, - '$focusBorderColor', - config.focusBorderColor, - defaultStyleOptions$1.focusBorderColor); - - style = updateOption(style, - '$focusBorderDarkColor', - config.focusBorderDarkColor, - defaultStyleOptions$1.focusBorderDarkColor); - - - style = updateOption(style, - '$dialogTextColor', - config.dialogTextColor, - defaultStyleOptions$1.dialogTextColor); - - style = updateOption(style, - '$dialogTextDarkColor', - config.dialogTextDarkColor, - defaultStyleOptions$1.dialogTextDarkColor); - - style = updateOption(style, - '$dialogBackgroundColor', - config.dialogBackgroundColor, - defaultStyleOptions$1.dialogBackgroundColor); - - style = updateOption(style, - '$dialogBackgroundDarkColor', - config.dialogBackgroundDarkColor, - defaultStyleOptions$1.dialogBackgroundDarkColor); - - style = updateOption(style, - '$dialogBackgroundTitleColor', - config.dialogBackgroundTitleColor, - defaultStyleOptions$1.dialogBackgroundTitleColor); - - style = updateOption(style, - '$dialogBackgroundTitleDarkColor', - config.dialogBackgroundTitleDarkColor, - defaultStyleOptions$1.dialogBackgroundTitleDarkColor); - - let styleNode = this.shadowRoot.querySelector('style'); - - if (styleNode) { - styleNode.remove(); - } - - styleNode = document.createElement('style'); - styleNode.textContent = style; - this.shadowRoot.appendChild(styleNode); - - } - - close() { - this.messageDialog.classList.remove('show'); - this.messageDialog.classList.remove('fade'); - this.messageDialog.classList.add('hidden'); - } - - open(message) { - clearInterval(this.timeoutFadeID); - clearInterval(this.timeoutShowID); - this.messageDialog.classList.remove('hidden'); - this.messageDialog.classList.remove('fade'); - this.messageDialog.classList.add('show'); - this.contentElem.textContent = message; - - const msg = this; - - this.timeoutFadeID = setTimeout( () => { - msg.messageDialog.classList.add('fade'); - msg.messageDialog.classList.remove('show'); - }, 3000); - - this.timeoutShowID = setTimeout( () => { - msg.close(); - }, 4000); - - } - - } - - /* - * namefrom.js - */ - - /* constants */ - - const debug$7 = new DebugLogging('nameFrom', false); - debug$7.flag = false; - - // - // LOW-LEVEL HELPER FUNCTIONS (NOT EXPORTED) - - /* - * @function isDisplayNone - * - * @desc Returns true if the element or parent element has set the CSS - * display property to none or has the hidden attribute, - * otherwise false - * - * @param {Object} node - a DOM node - * - * @returns {Boolean} see @desc - */ - - function isDisplayNone (node) { - - if (!node) { - return false; - } - - if (node.nodeType === Node.TEXT_NODE) { - node = node.parentNode; - } - - if (node.nodeType === Node.ELEMENT_NODE) { - - if (node.hasAttribute('hidden')) { - return true; - } - - // aria-hidden attribute with the value "true" is an same as - // setting the hidden attribute for name calcuation - if (node.hasAttribute('aria-hidden')) { - if (node.getAttribute('aria-hidden').toLowerCase() === 'true') { - return true; - } - } - - const style = window.getComputedStyle(node, null); - - const display = style.getPropertyValue("display"); - - if (display) { - return display === 'none'; - } - } - return false; - } - - /* - * @function isVisibilityHidden - * - * @desc Returns true if the node (or it's parrent) has the CSS visibility - * property set to "hidden" or "collapse", otherwise false - * - * @param {Object} node - DOM node - * - * @return see @desc - */ - - function isVisibilityHidden(node) { - - if (!node) { - return false; - } - - if (node.nodeType === Node.TEXT_NODE) { - node = node.parentNode; - } - - if (node.nodeType === Node.ELEMENT_NODE) { - const style = window.getComputedStyle(node, null); - - const visibility = style.getPropertyValue("visibility"); - if (visibility) { - return (visibility === 'hidden') || (visibility === 'collapse'); - } - } - return false; - } - - /* - * @function isAriaHiddenFalse - * - * @desc Returns true if the node has the aria-hidden property set to - * "false", otherwise false. - * NOTE: This function is important in the accessible namce - * calculation, since content hidden with a CSS technique - * can be included in the accessible name calculation when - * aria-hidden is set to false - * - * @param {Object} node - DOM node - * - * @return see @desc - */ - - function isAriaHIddenFalse(node) { - - if (!node) { - return false; - } - - if (node.nodeType === Node.TEXT_NODE) { - node = node.parentNode; - } - - if (node.nodeType === Node.ELEMENT_NODE) { - return (node.hasAttribute('aria-hidden') && - (node.getAttribute('aria-hidden').toLowerCase() === 'false')); - } - - return false; - } - - /* - * @function includeContentInName - * - * @desc Checks the CSS display and hidden properties, and - * the aria-hidden property to see if the content - * should be included in the accessible name - * calculation. Returns true if it should be - * included, otherwise false - * - * @param {Object} node - DOM node - * - * @return see @desc - */ - - function includeContentInName(node) { - const flag = isAriaHIddenFalse(node) || - (!isVisibilityHidden(node) && - !isDisplayNone(node)); - return flag; - } - - /* - * @function getNodeContents - * - * @desc Recursively process element and text nodes by aggregating - * their text values for an ARIA accessible name or description - * calculation. - * - * NOTE: This includes special handling of elements with 'alt' - * text and embedded controls. - * - * @param {Object} node - A DOM node - * - * @return {String} The text content for an accessible name or description - */ - function getNodeContents (node) { - let contents = ''; - let nc; - let arr = []; - - switch (node.nodeType) { - case Node.ELEMENT_NODE: - // If aria-label is present, node recursion stops and - // aria-label value is returned - if (node.hasAttribute('aria-label')) { - if (includeContentInName(node)) { - contents = node.getAttribute('aria-label'); - } - } - else { - if (node instanceof HTMLSlotElement) { - // if no slotted elements, check for default slotted content - const assignedNodes = node.assignedNodes().length ? node.assignedNodes() : node.assignedNodes({ flatten: true }); - assignedNodes.forEach( assignedNode => { - nc = getNodeContents(assignedNode); - if (nc.length) arr.push(nc); - }); - contents = (arr.length) ? arr.join(' ') : ''; - } else { - if (couldHaveAltText(node) && includeContentInName(node)) { - contents = getAttributeValue(node, 'alt'); - } - else { - if (node.hasChildNodes()) { - let children = Array.from(node.childNodes); - children.forEach( child => { - nc = getNodeContents(child); - if (nc.length) arr.push(nc); - }); - contents = (arr.length) ? arr.join(' ') : ''; - } - } - // For all branches of the ELEMENT_NODE case... - } - } - contents = addCssGeneratedContent(node, contents); - break; - - case Node.TEXT_NODE: - if (includeContentInName(node)) { - contents = normalize(node.textContent); - } - break; - } - - return contents; - } - - /* - * @function couldHaveAltText - * - * @desc Based on HTML5 specification, returns true if - * the element could have an 'alt' attribute, - * otherwise false. - * - * @param {Object} element - DOM eleemnt node - * - * @return {Boolean} see @desc - */ - function couldHaveAltText (element) { - let tagName = element.tagName.toLowerCase(); - - switch (tagName) { - case 'img': - case 'area': - return true; - case 'input': - return (element.type && element.type === 'image'); - } - - return false; - } - - /* - * @function addCssGeneratedContent - * - * @desc Adds CSS-generated content for pseudo-elements - * :before and :after. According to the CSS spec, test that content - * value is other than the default computed value of 'none'. - * - * Note: Even if an author specifies content: 'none', because browsers - * add the double-quote character to the beginning and end of - * computed string values, the result cannot and will not be - * equal to 'none'. - * - * - * @param {Object} element - DOM node element - * @param {String} contents - Text content for DOM node - * - * @returns {String} see @desc - * - */ - function addCssGeneratedContent (element, contents) { - - let result = contents, - prefix = getComputedStyle(element, ':before').content, - suffix = getComputedStyle(element, ':after').content; - - if ((prefix[0] === '"') && !prefix.toLowerCase().includes('moz-')) { - result = prefix.substring(1, (prefix.length-1)) + result; - } - - if ((suffix[0] === '"') && !suffix.toLowerCase().includes('moz-')) { - result = result + suffix.substring(1, (suffix.length-1)) ; - } - - return result; - } - - /* accName.js */ - - /* Constants */ - const debug$6 = new DebugLogging('accName', false); - debug$6.flag = false; - - /** - * @fuction getAccessibleName - * - * @desc Returns the accessible name for an heading or landamrk - * - * @paramn {Object} dom - Document of the current element - * @param {node} element - DOM element node for either a heading or - * landmark - * @param {Boolean} fromContent - if true will compute name from content - * - * @return {String} The accessible name for the landmark or heading element - */ - - function getAccessibleName (doc, element, fromContent=false) { - let accName = ''; - - accName = nameFromAttributeIdRefs(doc, element, 'aria-labelledby'); - - if (accName === '' && element.hasAttribute('aria-label')) { - accName = element.getAttribute('aria-label').trim(); - } - - if (accName === '' && fromContent) { - accName = getNodeContents(element); - } - - if (accName === '' && element.title.trim() !== '') { - accName = element.title.trim(); - } - - return accName; - } - - /* - * @function nameFromAttributeIdRefs - * - * @desc Get the value of attrName on element (a space- - * separated list of IDREFs), visit each referenced element in the order it - * appears in the list and obtain its accessible name (skipping recursive - * aria-labelledby or aria-describedby calculations), and return an object - * with name property set to a string that is a space-separated concatena- - * tion of those results if any, otherwise return empty string. - * - * @param {Object} doc - Browser document object - * @param {Object} element - DOM element node - * @param {String} attribute - Attribute name (e.g. "aria-labelledby", "aria-describedby", - * or "aria-errormessage") - * - * @returns {String} see @desc - */ - function nameFromAttributeIdRefs (doc, element, attribute) { - const value = getAttributeValue(element, attribute); - const arr = []; - - if (value.length) { - const idRefs = value.split(' '); - - for (let i = 0; i < idRefs.length; i++) { - const refElement = doc.getElementById(idRefs[i]); - if (refElement) { - const accName = getNodeContents(refElement); - if (accName && accName.length) arr.push(accName); - } - } - } - - if (arr.length) { - return arr.join(' '); - } - - return ''; - } - + return ''; + } + /* landmarksHeadings.js */ /* Constants */ @@ -3551,8 +3552,8 @@ button:hover { return allElements; } return [].concat(mainElements, searchElements, navElements, asideElements, regionElements, footerElements, headerElements, otherElements); - } - + } + /* shortcuts.js */ /* Constants */ @@ -3785,141 +3786,141 @@ button:hover { elem = elem.shadowRoot.activeElement; } return elem; - } - - /* keyboardHelper.js */ - - /* Constants */ - const debug$3 = new DebugLogging('[kbdHelpers]', false); - debug$3.flag = false; - - /* - * @method isInteractiveElement - * - * @desc Returns true if the element can use key presses, otherwise false - * - * @param {object} elem - DOM node element - * - * @returns {Boolean} see @desc - */ - - function elementTakesText (elem) { - - const enabledInputTypes = [ - 'button', - 'checkbox', - 'color', - 'image', - 'radio', - 'range', - 'reset', - 'submit' - ]; - - const tagName = elem.tagName ? elem.tagName.toLowerCase() : ''; - const type = tagName === 'input' ? - (elem.type ? elem.type.toLowerCase() : 'text') : - ''; - - debug$3.flag && debug$3.log(`[elementTakesText][type]: ${type} (${enabledInputTypes.includes(type)})`); - - return (tagName === 'select') || - (tagName === 'textarea') || - ((tagName === 'input') && - !enabledInputTypes.includes(type)) || - inContentEditable(elem); - } - - /* - * @function inContentEditable - * - * @desc Returns false if node is not in a content editable element, - * otherwise true if it does - * - * @param {Object} elem - DOM node - * - * @returns {Boolean} see @desc - */ - function inContentEditable (elem) { - let n = elem; - while (n.hasAttribute) { - if (n.hasAttribute('contenteditable') && - (n.getAttribute('contenteditable').toLowerCase().trim() !== 'false')) { - return true; - } - n = n.parentNode; - } - return false; - } - - /* - * @function noModifierPressed - * - * @desc Returns true if no modifier key is pressed, other false - * - * @param {Object} event - Event object - * - * @returns {Boolean} see @desc - */ - - function noModifierPressed (event) { - return !event.altKey && - !event.ctrlKey && - !event.shiftKey && - !event.metaKey; - } - - /* - * @function onlyShiftPressed - * - * @desc Returns true if only the shift modifier key is pressed, other false - * - * @param {Object} event - Event object - * - * @returns {Boolean} see @desc - */ - - function onlyShiftPressed (event) { - return !event.altKey && - !event.ctrlKey && - event.shiftKey && - !event.metaKey; - } - - /* - * @function onlyAltPressed - * - * @desc Returns true if only the alt modifier key is pressed, other false - * - * @param {Object} event - Event object - * - * @returns {Boolean} see @desc - */ - - function onlyAltPressed (event) { - return event.altKey && - !event.ctrlKey && - !event.shiftKey && - !event.metaKey; - } - - /* - * @function onlyOptionPressed - * - * @desc Returns true if only the option modifier key is pressed, other false - * - * @param {Object} event - Event object - * - * @returns {Boolean} see @desc - */ - - function onlyOptionPressed (event) { - return event.altKey && - !event.ctrlKey && - !event.shiftKey && - !event.metaKey; - } - + } + + /* keyboardHelper.js */ + + /* Constants */ + const debug$3 = new DebugLogging('[kbdHelpers]', false); + debug$3.flag = false; + + /* + * @method isInteractiveElement + * + * @desc Returns true if the element can use key presses, otherwise false + * + * @param {object} elem - DOM node element + * + * @returns {Boolean} see @desc + */ + + function elementTakesText (elem) { + + const enabledInputTypes = [ + 'button', + 'checkbox', + 'color', + 'image', + 'radio', + 'range', + 'reset', + 'submit' + ]; + + const tagName = elem.tagName ? elem.tagName.toLowerCase() : ''; + const type = tagName === 'input' ? + (elem.type ? elem.type.toLowerCase() : 'text') : + ''; + + debug$3.flag && debug$3.log(`[elementTakesText][type]: ${type} (${enabledInputTypes.includes(type)})`); + + return (tagName === 'select') || + (tagName === 'textarea') || + ((tagName === 'input') && + !enabledInputTypes.includes(type)) || + inContentEditable(elem); + } + + /* + * @function inContentEditable + * + * @desc Returns false if node is not in a content editable element, + * otherwise true if it does + * + * @param {Object} elem - DOM node + * + * @returns {Boolean} see @desc + */ + function inContentEditable (elem) { + let n = elem; + while (n.hasAttribute) { + if (n.hasAttribute('contenteditable') && + (n.getAttribute('contenteditable').toLowerCase().trim() !== 'false')) { + return true; + } + n = n.parentNode; + } + return false; + } + + /* + * @function noModifierPressed + * + * @desc Returns true if no modifier key is pressed, other false + * + * @param {Object} event - Event object + * + * @returns {Boolean} see @desc + */ + + function noModifierPressed (event) { + return !event.altKey && + !event.ctrlKey && + !event.shiftKey && + !event.metaKey; + } + + /* + * @function onlyShiftPressed + * + * @desc Returns true if only the shift modifier key is pressed, other false + * + * @param {Object} event - Event object + * + * @returns {Boolean} see @desc + */ + + function onlyShiftPressed (event) { + return !event.altKey && + !event.ctrlKey && + event.shiftKey && + !event.metaKey; + } + + /* + * @function onlyAltPressed + * + * @desc Returns true if only the alt modifier key is pressed, other false + * + * @param {Object} event - Event object + * + * @returns {Boolean} see @desc + */ + + function onlyAltPressed (event) { + return event.altKey && + !event.ctrlKey && + !event.shiftKey && + !event.metaKey; + } + + /* + * @function onlyOptionPressed + * + * @desc Returns true if only the option modifier key is pressed, other false + * + * @param {Object} event - Event object + * + * @returns {Boolean} see @desc + */ + + function onlyOptionPressed (event) { + return event.altKey && + !event.ctrlKey && + !event.shiftKey && + !event.metaKey; + } + /* skiptoMenuButton.js */ /* Constants */ @@ -5299,8 +5300,8 @@ button:hover { this.closePopup(); } } - } - + } + /* skiptoContent.js */ /* constants */ @@ -5711,8 +5712,8 @@ button:hover { this.config.shortcuts = 'disabled'; } } - } - + } + /* skipto.js */ /* constants */ @@ -5897,6 +5898,6 @@ button:hover { }); } } - })(); - -})(); + })(); + +})(); From 485eae153a895a36c84475065504fe438285056b Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Tue, 1 Jul 2025 13:34:46 -0500 Subject: [PATCH 06/38] look for all headings in document not just in main landmark --- content/shared/js/skipto.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/content/shared/js/skipto.js b/content/shared/js/skipto.js index b92d0832a2..7e73a16820 100644 --- a/content/shared/js/skipto.js +++ b/content/shared/js/skipto.js @@ -52,7 +52,6 @@ 'aria': { hostnameSelector: 'w3.org', pathnameSelector: 'ARIA/apg', - headings: 'h1 h2', fontFamily: 'sans-serif', fontSize: '10pt', positionLeft: '7%', @@ -5434,7 +5433,7 @@ button:hover { // Selectors for landmark and headings sections landmarks: 'main search navigation complementary', - headings: 'main-only h1 h2', + headings: 'h1 h2', // Highlight options highlightTarget: defaultStyleOptions.highlightTarget, From b029573733f9405f9a11cfde59e3152727380b45 Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Tue, 8 Jul 2025 12:05:35 -0500 Subject: [PATCH 07/38] udpated to version 5.8.0 --- content/shared/js/skipto.js | 4223 ++++++++++++++++++----------------- 1 file changed, 2158 insertions(+), 2065 deletions(-) diff --git a/content/shared/js/skipto.js b/content/shared/js/skipto.js index 7e73a16820..306486c496 100644 --- a/content/shared/js/skipto.js +++ b/content/shared/js/skipto.js @@ -1,279 +1,283 @@ -/* ======================================================================== - * Version: 5.7 - * Copyright (c) 2022, 2023, 2024, 2025 Jon Gunderson; Licensed BSD - * Copyright (c) 2021 PayPal Accessibility Team and University of Illinois; Licensed BSD - * All rights reserved. - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of PayPal or any of its subsidiaries or affiliates, nor the name of the University of Illinois, nor the names of any other contributors contributors may be used to endorse or promote products derived from this software without specific prior written permission. - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * CDN: https://skipto-landmarks-headings.github.io/page-script-5/dist/skipto.min.js - * Documentation: https://skipto-landmarks-headings.github.io/page-script-5 - * Code: https://github.com/skipto-landmarks-headings/page-script-5 - * Report Issues: https://github.com/skipto-landmarks-headings/page-script-5/issues - * ======================================================================== */ - -(function () { - 'use strict'; - - /* colorThemes */ - - const colorThemes = { - 'default': { - - fontFamily: 'inherit', - fontSize: 'inherit', - positionLeft: '46%', - smallBreakPoint: '576', - mediumBreakPoint: '992', - buttonTextColor: '#13294b', - buttonBackgroundColor: '#dddddd', - focusBorderColor: '#c5050c', - menuTextColor: '#13294b', - menuBackgroundColor: '#dddddd', - menuitemFocusTextColor: '#dddddd', - menuitemFocusBackgroundColor: '#13294b', - menuTextDarkColor: '#ffffff', - menuBackgroundDarkColor: '#000000', - menuitemFocusTextDarkColor: '#ffffff', - menuitemFocusBackgroundDarkColor: '#013c93', - focusBorderDarkColor: '#ffffff', - buttonTextDarkColor: '#ffffff', - buttonBackgroundDarkColor: '#013c93', - zIndex: '2000000', - zHighlight: '1999900', - displayOption: 'fixed', - highlightTarget: 'instant', - highlightBorderSize: 'small', - highlightBorderStyle: 'solid' - }, - 'aria': { - hostnameSelector: 'w3.org', - pathnameSelector: 'ARIA/apg', - fontFamily: 'sans-serif', - fontSize: '10pt', - positionLeft: '7%', - menuTextColor: '#000', - menuBackgroundColor: '#def', - menuitemFocusTextColor: '#fff', - menuitemFocusBackgroundColor: '#005a9c', - focusBorderColor: '#005a9c', - buttonTextColor: '#005a9c', - buttonBackgroundColor: '#ddd', - }, - 'illinois': { - hostnameSelector: 'illinois.edu', - menuTextColor: '#00132c', - menuBackgroundColor: '#cad9ef', - menuitemFocusTextColor: '#eeeeee', - menuitemFocusBackgroundColor: '#00132c', - focusBorderColor: '#ff552e', - buttonTextColor: '#444444', - buttonBackgroundColor: '#dddede', - highlightTarget: 'disabled' - }, - 'openweba11y': { - hostnameSelector: 'openweba11y.com', - buttonTextColor: '#13294B', - buttonBackgroundColor: '#dddddd', - focusBorderColor: '#C5050C', - menuTextColor: '#13294B', - menuBackgroundColor: '#dddddd', - menuitemFocusTextColor: '#dddddd', - menuitemFocusBackgroundColor: '#13294B', - fontSize: '90%' - }, - 'skipto': { - hostnameSelector: 'skipto-landmarks-headings.github.io', - positionLeft: '25%', - fontSize: '14px', - menuTextColor: '#00132c', - menuBackgroundColor: '#cad9ef', - menuitemFocusTextColor: '#eeeeee', - menuitemFocusBackgroundColor: '#00132c', - focusBorderColor: '#ff552e', - buttonTextColor: '#444444', - buttonBackgroundColor: '#dddede', - }, - 'uic': { - hostnameSelector: 'uic.edu', - menuTextColor: '#001e62', - menuBackgroundColor: '#f8f8f8', - menuitemFocusTextColor: '#ffffff', - menuitemFocusBackgroundColor: '#001e62', - focusBorderColor: '#d50032', - buttonTextColor: '#ffffff', - buttonBackgroundColor: '#001e62', - }, - 'uillinois': { - hostnameSelector: 'uillinois.edu', - menuTextColor: '#001e62', - menuBackgroundColor: '#e8e9ea', - menuitemFocusTextColor: '#f8f8f8', - menuitemFocusBackgroundColor: '#13294b', - focusBorderColor: '#dd3403', - buttonTextColor: '#e8e9ea', - buttonBackgroundColor: '#13294b', - highlightTarget: 'disabled' - }, - 'uis': { - hostnameSelector: 'uis.edu', - menuTextColor: '#036', - menuBackgroundColor: '#fff', - menuitemFocusTextColor: '#fff', - menuitemFocusBackgroundColor: '#036', - focusBorderColor: '#dd3444', - buttonTextColor: '#fff', - buttonBackgroundColor: '#036', - }, - 'walmart': { - hostnameSelector: 'walmart.com', - buttonTextColor: '#ffffff', - buttonBackgroundColor: '#00419a', - focusBorderColor: '#ffc220', - menuTextColor: '#ffffff', - menuBackgroundColor: '#0071dc', - menuitemFocusTextColor: '#00419a', - menuitemFocusBackgroundColor: '#ffffff', - } - }; - - /* - * debug.js - * - * Usage - * import DebugLogging from './debug.js'; - * const debug = new DebugLogging('myLabel', true); // e.g. 'myModule' - * ... - * if (debug.flag) debug.log('myMessage'); - * - * Notes - * new DebugLogging() - calling the constructor with no arguments results - * in debug.flag set to false and debug.label set to 'debug'; - * constructor accepts 0, 1 or 2 arguments in any order - * @param flag [optional] {boolean} - sets debug.flag - * @param label [optional] {string} - sets debug.label - * Properties - * debug.flag {boolean} allows you to switch debug logging on or off; - * default value is false - * debug.label {string} rendered as a prefix to each log message; - * default value is 'debug' - * Methods - * debug.log calls console.log with label prefix and message - * @param message {object} - console.log calls toString() - * @param spaceAbove [optional] {boolean} - * - * debug.tag outputs tagName and textContent of DOM element - * @param node {DOM node reference} - usually an HTMLElement - * @param spaceAbove [optional] {boolean} - * - * debug.separator outputs only debug.label and a series of hyphens - * @param spaceAbove [optional] {boolean} - */ - - class DebugLogging { - constructor (...args) { - // Default values for cases where fewer than two arguments are provided - this._flag = false; - this._label = 'debug'; - - // The constructor may be called with zero, one or two arguments. If two - // arguments, they can be in any order: one is assumed to be the boolean - // value for '_flag' and the other one the string value for '_label'. - for (const [index, arg] of args.entries()) { - if (index < 2) { - switch (typeof arg) { - case 'boolean': - this._flag = arg; - break; - case 'string': - this._label = arg; - break; - } - } - } - } - - get flag () { return this._flag; } - - set flag (value) { - if (typeof value === 'boolean') { - this._flag = value; - } - } - - get label () { return this._label; } - - set label (value) { - if (typeof value === 'string') { - this._label = value; - } - } - - log (message, spaceAbove) { - const newline = spaceAbove ? '\n' : ''; - console.log(`${newline}[${this._label}] ${message}`); - } - - tag (node, spaceAbove) { - if (node && node.tagName) { - const text = node.textContent.trim().replace(/\s+/g, ' '); - this.log(`[${node.tagName}]: ${text.substring(0, 40)}`, spaceAbove); - } - } - - separator (spaceAbove) { - this.log('-----------------------------', spaceAbove); - } - - } - - /* constants.js */ - - // Element IDs - - const SKIP_TO_ID = 'id-skip-to-ver-5'; - const SKIP_TO_MENU_STYLE_ID = 'id-skip-to-menu-style'; - - const SCRIPT_EXTENSION_ID = `id-skip-to-extension`; - const SCRIPT_BOOKMARKLET_ID = `id-skip-to-bookmarklet`; - - const MENU_ID = 'id-skip-to-menu'; - - const MENU_LANDMARK_GROUP_ID = 'id-skip-to-landmark-group'; - const MENU_LANDMARK_GROUP_LABEL_ID = 'id-skip-to-landmark-group-label'; - - const MENU_HEADINGS_GROUP_ID = 'id-skip-to-heading-group'; - const MENU_HEADINGS_GROUP_LABEL_ID = 'id-skip-to-heading-group-label'; - - const MENU_SHORTCUTS_GROUP_ID = 'id-skip-to-shortcuts-group'; - const MENU_SHORTCUTS_GROUP_LABEL_ID = 'id-skip-to-shortcuts-group-label'; - - const MESSAGE_ID = 'id-skip-to-message'; - - const HIGHLIGHT_ID = 'id-skip-to-highlight-overlay'; - - // Custom element names - - const PAGE_SCRIPT_ELEMENT_NAME = 'skip-to-content'; - const BOOKMARKLET_ELEMENT_NAME = 'skip-to-content-bookmarklet'; - const EXTENSION_ELEMENT_NAME = 'skip-to-content-extension'; - - const INFO_DIALOG_ELEMENT_NAME = 'skip-to-content-info-dialog-575'; - const MESSAGE_ELEMENT_NAME = 'skip-to-content-message-element-575'; - const HIGHLIGHT_ELEMENT_NAME = 'skip-to-content-highlight-element-575'; - - // Attributes - - const ATTR_SKIP_TO_DATA = 'data-skipto'; - - // URLs to more information - - const MORE_PAGE_SCRIPT_INFO_URL ='https://skipto-landmarks-headings.github.io/page-script-5/'; - const MORE_SHORTCUT_INFO_URL ='https://skipto-landmarks-headings.github.io/page-script-5/shortcuts.html'; - +/* ======================================================================== + * Version: 5.8.0 + * Copyright (c) 2022, 2023, 2024, 2025 Jon Gunderson; Licensed BSD + * Copyright (c) 2021 PayPal Accessibility Team and University of Illinois; Licensed BSD + * All rights reserved. + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of PayPal or any of its subsidiaries or affiliates, nor the name of the University of Illinois, nor the names of any other contributors contributors may be used to endorse or promote products derived from this software without specific prior written permission. + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * CDN: https://skipto-landmarks-headings.github.io/page-script-5/dist/skipto.min.js + * Documentation: https://skipto-landmarks-headings.github.io/page-script-5 + * Code: https://github.com/skipto-landmarks-headings/page-script-5 + * Report Issues: https://github.com/skipto-landmarks-headings/page-script-5/issues + * ======================================================================== */ + +(function () { + 'use strict'; + + /* colorThemes */ + + const colorThemes = { + 'default': { + + fontFamily: 'inherit', + fontSize: 'inherit', + positionLeft: '46%', + smallBreakPoint: '580', + mediumBreakPoint: '992', + buttonTextColor: '#13294b', + buttonBackgroundColor: '#dddddd', + focusBorderColor: '#c5050c', + menuTextColor: '#13294b', + menuBackgroundColor: '#dddddd', + menuitemFocusTextColor: '#dddddd', + menuitemFocusBackgroundColor: '#13294b', + menuTextDarkColor: '#ffffff', + menuBackgroundDarkColor: '#000000', + menuitemFocusTextDarkColor: '#ffffff', + menuitemFocusBackgroundDarkColor: '#013c93', + focusBorderDarkColor: '#ffffff', + buttonTextDarkColor: '#ffffff', + buttonBackgroundDarkColor: '#013c93', + zIndex: '2000000', + zHighlight: '1999900', + displayOption: 'fixed', + highlightTarget: 'instant', + highlightBorderSize: 'small', + highlightBorderStyle: 'solid' + }, + 'aria': { + hostnameSelector: 'w3.org', + pathnameSelector: 'ARIA/apg', + fontFamily: 'sans-serif', + fontSize: '10pt', + positionLeft: '7%', + menuTextColor: '#000', + menuBackgroundColor: '#def', + menuitemFocusTextColor: '#fff', + menuitemFocusBackgroundColor: '#005a9c', + focusBorderColor: '#005a9c', + buttonTextColor: '#005a9c', + buttonBackgroundColor: '#ddd', + }, + 'illinois': { + hostnameSelector: 'illinois.edu', + menuTextColor: '#00132c', + menuBackgroundColor: '#cad9ef', + menuitemFocusTextColor: '#eeeeee', + menuitemFocusBackgroundColor: '#00132c', + focusBorderColor: '#ff552e', + buttonTextColor: '#444444', + buttonBackgroundColor: '#dddede', + highlightTarget: 'disabled' + }, + 'openweba11y': { + hostnameSelector: 'openweba11y.com', + buttonTextColor: '#13294B', + buttonBackgroundColor: '#dddddd', + focusBorderColor: '#C5050C', + menuTextColor: '#13294B', + menuBackgroundColor: '#dddddd', + menuitemFocusTextColor: '#dddddd', + menuitemFocusBackgroundColor: '#13294B', + fontSize: '90%' + }, + 'skipto': { + hostnameSelector: 'skipto-landmarks-headings.github.io', + positionLeft: '25%', + fontSize: '14px', + menuTextColor: '#00132c', + menuBackgroundColor: '#cad9ef', + menuitemFocusTextColor: '#eeeeee', + menuitemFocusBackgroundColor: '#00132c', + focusBorderColor: '#ff552e', + buttonTextColor: '#444444', + buttonBackgroundColor: '#dddede', + }, + 'uic': { + hostnameSelector: 'uic.edu', + menuTextColor: '#001e62', + menuBackgroundColor: '#f8f8f8', + menuitemFocusTextColor: '#ffffff', + menuitemFocusBackgroundColor: '#001e62', + focusBorderColor: '#d50032', + buttonTextColor: '#ffffff', + buttonBackgroundColor: '#001e62', + }, + 'uillinois': { + hostnameSelector: 'uillinois.edu', + menuTextColor: '#001e62', + menuBackgroundColor: '#e8e9ea', + menuitemFocusTextColor: '#f8f8f8', + menuitemFocusBackgroundColor: '#13294b', + focusBorderColor: '#dd3403', + buttonTextColor: '#e8e9ea', + buttonBackgroundColor: '#13294b', + highlightTarget: 'disabled' + }, + 'uis': { + hostnameSelector: 'uis.edu', + menuTextColor: '#036', + menuBackgroundColor: '#fff', + menuitemFocusTextColor: '#fff', + menuitemFocusBackgroundColor: '#036', + focusBorderColor: '#dd3444', + buttonTextColor: '#fff', + buttonBackgroundColor: '#036', + }, + 'walmart': { + hostnameSelector: 'walmart.com', + buttonTextColor: '#ffffff', + buttonBackgroundColor: '#00419a', + focusBorderColor: '#ffc220', + menuTextColor: '#ffffff', + menuBackgroundColor: '#0071dc', + menuitemFocusTextColor: '#00419a', + menuitemFocusBackgroundColor: '#ffffff', + } + }; + + /* + * debug.js + * + * Usage + * import DebugLogging from './debug.js'; + * const debug = new DebugLogging('myLabel', true); // e.g. 'myModule' + * ... + * if (debug.flag) debug.log('myMessage'); + * + * Notes + * new DebugLogging() - calling the constructor with no arguments results + * in debug.flag set to false and debug.label set to 'debug'; + * constructor accepts 0, 1 or 2 arguments in any order + * @param flag [optional] {boolean} - sets debug.flag + * @param label [optional] {string} - sets debug.label + * Properties + * debug.flag {boolean} allows you to switch debug logging on or off; + * default value is false + * debug.label {string} rendered as a prefix to each log message; + * default value is 'debug' + * Methods + * debug.log calls console.log with label prefix and message + * @param message {object} - console.log calls toString() + * @param spaceAbove [optional] {boolean} + * + * debug.tag outputs tagName and textContent of DOM element + * @param node {DOM node reference} - usually an HTMLElement + * @param spaceAbove [optional] {boolean} + * + * debug.separator outputs only debug.label and a series of hyphens + * @param spaceAbove [optional] {boolean} + */ + + class DebugLogging { + constructor (...args) { + // Default values for cases where fewer than two arguments are provided + this._flag = false; + this._label = 'debug'; + + // The constructor may be called with zero, one or two arguments. If two + // arguments, they can be in any order: one is assumed to be the boolean + // value for '_flag' and the other one the string value for '_label'. + for (const [index, arg] of args.entries()) { + if (index < 2) { + switch (typeof arg) { + case 'boolean': + this._flag = arg; + break; + case 'string': + this._label = arg; + break; + } + } + } + } + + get flag () { return this._flag; } + + set flag (value) { + if (typeof value === 'boolean') { + this._flag = value; + } + } + + get label () { return this._label; } + + set label (value) { + if (typeof value === 'string') { + this._label = value; + } + } + + log (message, spaceAbove) { + const newline = spaceAbove ? '\n' : ''; + console.log(`${newline}[${this._label}] ${message}`); + } + + tag (node, spaceAbove) { + if (node && node.tagName) { + const text = node.textContent.trim().replace(/\s+/g, ' '); + this.log(`[${node.tagName}]: ${text.substring(0, 40)}`, spaceAbove); + } + } + + separator (spaceAbove) { + this.log('-----------------------------', spaceAbove); + } + + } + + /* constants.js */ + + // Numbers + + const REQUIRE_ACCESSIBLE_NAME_COUNT = 3; + + // Element IDs + + const SKIP_TO_ID = 'id-skip-to-ver-5'; + const SKIP_TO_MENU_STYLE_ID = 'id-skip-to-menu-style'; + + const SCRIPT_EXTENSION_ID = `id-skip-to-extension`; + const SCRIPT_BOOKMARKLET_ID = `id-skip-to-bookmarklet`; + + const MENU_ID = 'id-skip-to-menu'; + + const MENU_LANDMARK_GROUP_ID = 'id-skip-to-landmark-group'; + const MENU_LANDMARK_GROUP_LABEL_ID = 'id-skip-to-landmark-group-label'; + + const MENU_HEADINGS_GROUP_ID = 'id-skip-to-heading-group'; + const MENU_HEADINGS_GROUP_LABEL_ID = 'id-skip-to-heading-group-label'; + + const MENU_SHORTCUTS_GROUP_ID = 'id-skip-to-shortcuts-group'; + const MENU_SHORTCUTS_GROUP_LABEL_ID = 'id-skip-to-shortcuts-group-label'; + + const MESSAGE_ID = 'id-skip-to-message'; + + const HIGHLIGHT_ID = 'id-skip-to-highlight-overlay'; + + // Custom element names + + const PAGE_SCRIPT_ELEMENT_NAME = 'skip-to-content'; + const BOOKMARKLET_ELEMENT_NAME = 'skip-to-content-bookmarklet'; + const EXTENSION_ELEMENT_NAME = 'skip-to-content-extension'; + + const INFO_DIALOG_ELEMENT_NAME = 'skip-to-content-info-dialog-580'; + const MESSAGE_ELEMENT_NAME = 'skip-to-content-message-element-580'; + const HIGHLIGHT_ELEMENT_NAME = 'skip-to-content-highlight-element-580'; + + // Attributes + + const ATTR_SKIP_TO_DATA = 'data-skipto'; + + // URLs to more information + + const MORE_PAGE_SCRIPT_INFO_URL ='https://skipto-landmarks-headings.github.io/page-script-5/'; + const MORE_SHORTCUT_INFO_URL ='https://skipto-landmarks-headings.github.io/page-script-5/shortcuts.html'; + /* style.js */ /* Constants */ @@ -834,330 +838,737 @@ } styleNode.textContent = cssMenu; - } - - /* utils.js */ - - /* Constants */ - const debug$b = new DebugLogging('Utils', false); - debug$b.flag = false; - - - /* - * @function getAttributeValue - * - * @desc Return attribute value if present on element, - * otherwise return empty string. - * - * @returns {String} see @desc - */ - function getAttributeValue (element, attribute) { - let value = element.getAttribute(attribute); - return (value === null) ? '' : normalize(value); - } - - /* - * @function normalize - * - * @desc Trim leading and trailing whitespace and condense all - * internal sequences of whitespace to a single space. Adapted from - * Mozilla documentation on String.prototype.trim polyfill. Handles - * BOM and NBSP characters. - * - * @return {String} see @desc - */ - function normalize (s) { - let rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; - return s.replace(rtrim, '').replace(/\s+/g, ' '); - } - - /** - * @fuction isNotEmptyString - * - * @desc Returns true if the string has content, otherwise false - * - * @param {Boolean} see @desc - */ - function isNotEmptyString (str) { - return (typeof str === 'string') && str.length && str.trim() && str !== " "; - } - - /** - * @fuction isVisible - * - * @desc Returns true if the element is visible in the graphical rendering - * - * @param {node} elem - DOM element node of a labelable element - */ - function isVisible (element) { - - function isDisplayNone(el) { - if (!el || (el.nodeType !== Node.ELEMENT_NODE)) { - return false; - } - - if (el.hasAttribute('hidden')) { - return true; - } - - const style = window.getComputedStyle(el, null); - const display = style.getPropertyValue("display"); - if (display === 'none') { - return true; - } - - // check ancestors for display none - if (el.parentNode) { - return isDisplayNone(el.parentNode); - } - - return false; - } - - const computedStyle = window.getComputedStyle(element); - let visibility = computedStyle.getPropertyValue('visibility'); - if ((visibility === 'hidden') || (visibility === 'collapse')) { - return false; - } - - return !isDisplayNone(element); - } - - /* shortcutInfoDialog.js */ + } + + /* utils.js */ + + /* Constants */ + const debug$b = new DebugLogging('Utils', false); + debug$b.flag = false; + + + /* + * @function getAttributeValue + * + * @desc Return attribute value if present on element, + * otherwise return empty string. + * + * @returns {String} see @desc + */ + function getAttributeValue (element, attribute) { + let value = element.getAttribute(attribute); + return (value === null) ? '' : normalize(value); + } + + /* + * @function normalize + * + * @desc Trim leading and trailing whitespace and condense all + * internal sequences of whitespace to a single space. Adapted from + * Mozilla documentation on String.prototype.trim polyfill. Handles + * BOM and NBSP characters. + * + * @return {String} see @desc + */ + function normalize (s) { + let rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; + return s.replace(rtrim, '').replace(/\s+/g, ' '); + } + + /** + * @fuction isNotEmptyString + * + * @desc Returns true if the string has content, otherwise false + * + * @param {Boolean} see @desc + */ + function isNotEmptyString (str) { + return (typeof str === 'string') && str.length && str.trim() && str !== " "; + } + + /** + * @fuction isVisible + * + * @desc Returns true if the element is visible in the graphical rendering + * + * @param {node} elem - DOM element node of a labelable element + */ + function isVisible (element) { + + function isDisplayNone(el) { + if (!el || (el.nodeType !== Node.ELEMENT_NODE)) { + return false; + } + + if (el.hasAttribute('hidden')) { + return true; + } + + const style = window.getComputedStyle(el, null); + const display = style.getPropertyValue("display"); + if (display === 'none') { + return true; + } + + // check ancestors for display none + if (el.parentNode) { + return isDisplayNone(el.parentNode); + } + + return false; + } + + const computedStyle = window.getComputedStyle(element); + let visibility = computedStyle.getPropertyValue('visibility'); + if ((visibility === 'hidden') || (visibility === 'collapse')) { + return false; + } + + return !isDisplayNone(element); + } + + /* shortcutInfoDialog.js */ + + /* Constants */ + const debug$a = new DebugLogging('[shortcutsInfoDialog]', false); + debug$a.flag = false; + + const defaultStyleOptions$3 = colorThemes['default']; + + + const styleTemplate$1 = document.createElement('template'); + styleTemplate$1.textContent = ` +/* infoDialog.css */ + +dialog#skip-to-info-dialog { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%,-50%); + font-family: $fontFamily; + font-size: $fontSize; + max-width: 70%; + margin: 0; + padding: 0; + background-color: light-dark($dialogBackgroundColor, $dialogBackgroundDarkColor); + color: light-dark($dialogTextColor, $dialogTextDarkColor); + border-width: 2px; + border-style: solid; + border-color: light-dark($focusBorderColor, $focusBorderDarkColor); + border-radius: 5px; + z-index: 2000001; + +} + +dialog#skip-to-info-dialog .header { + margin: 0; + margin-bottom: 0.5em; + padding: 4px; + border-width: 0; + border-bottom-width: 1px; + border-style: solid; + border-color: light-dark($focusBorderColor, $focusBorderDarkColor); + border-top-left-radius: 5px; + border-top-right-radius: 5px; + font-weight: bold; + background-color: light-dark($dialogBackgroundTitleColor, $dialogBackgroundTitleDarkColor); + color: light-dark($dialogTextColor, $dialogTextDarkColor); + position: relative; + font-size: 100%; +} + +dialog#skip-to-info-dialog .header h2 { + margin: 0; + padding: 0; + font-size: 1em; +} + +dialog#skip-to-info-dialog .header button { + position: absolute; + top: -0.25em; + right: 0; + border: none; + background: transparent; + font-weight: bold; + color: light-dark(black, white); +} + +dialog#skip-to-info-dialog .content { + margin-left: 2em; + margin-right: 2em; + margin-top: 0; + margin-bottom: 2em; +} + +dialog#skip-to-info-dialog .content .desc { + max-width: 20em; +} + +dialog#skip-to-info-dialog .content .happy { + margin-top: 0.5em; + text-align: center; + font-family: fantasy, cursive; + font-size: 1.25em; + font-weight: bold; + font-style: italic; + letter-spacing: 0.05em; +} + + +dialog#skip-to-info-dialog .content .version, +dialog#skip-to-info-dialog .content .copyright { + margin-top: 0.5em; + text-align: center; + font-weight: bold; + font-size: 90%; +} + +dialog#skip-to-info-dialog .content table { + width: auto; +} + +dialog#skip-to-info-dialog .content caption { + margin: 0; + padding: 0; + margin-top: 1em; + text-align: left; + font-weight: bold; + font-size: 110%; +} + +dialog#skip-to-info-dialog .content th { + margin: 0; + padding: 0; + padding-top: 0.125em; + padding-bottom: 0.125em; + text-align: left; + font-weight: bold; + font-size: 100%; +} + +dialog#skip-to-info-dialog .content th { + border-bottom-width: 1px; + border-bottom-style: solid; + border-bottom-color: light-dark(#999999, #777777); +} + +dialog#skip-to-info-dialog .content th.shortcut { + width: 2.5em; +} + +dialog#skip-to-info-dialog .content td { + margin: 0; + padding: 0; + padding-top: 0.125em; + padding-bottom: 0.125em; + text-align: left; + font-size: 100%; +} + + +dialog#skip-to-info-dialog .content table tr:nth-child(even) { + background-color: light-dark(#eeeeee, #111111); +} + +dialog#skip-to-info-dialog .buttons { + float: right; + margin-right: 0.5em; + margin-bottom: 0.5em; +} + +dialog#skip-to-info-dialog button { + margin: 6px; +} + +dialog#skip-to-info-dialog .buttons button { + min-width: 5em; +} + +button:focus { + outline: 2px solid currentColor; + outline-offset: 2px; +} + +button:hover { + cursor: pointer; +} +`; + + /* + * + * + */ + + class SkipToContentInfoDialog extends HTMLElement { + constructor () { + + super(); + this.attachShadow({ mode: 'open' }); + + // Get references + + this.infoDialog = document.createElement('dialog'); + this.infoDialog.id = 'skip-to-info-dialog'; + this.shadowRoot.appendChild(this.infoDialog); + + const headerElem = document.createElement('div'); + headerElem.className = 'header'; + this.infoDialog.appendChild(headerElem); + + this.h2Elem = document.createElement('h2'); + this.h2Elem.textContent = 'Keyboard Shortcuts'; + headerElem.appendChild(this.h2Elem); + + this.closeButton1 = document.createElement('button'); + this.closeButton1.textContent = '✕'; + headerElem.appendChild(this.closeButton1); + this.closeButton1.addEventListener('click', this.onCloseButtonClick.bind(this)); + this.closeButton1.addEventListener('keydown', this.onKeyDown.bind(this)); + + this.contentElem = document.createElement('div'); + this.contentElem.className = 'content'; + this.infoDialog.appendChild(this.contentElem); + + const buttonsElem = document.createElement('div'); + buttonsElem.className = 'buttons'; + this.infoDialog.appendChild(buttonsElem); + + this.moreInfoButton = document.createElement('button'); + this.moreInfoButton.textContent = 'More Information'; + buttonsElem.appendChild(this.moreInfoButton); + this.moreInfoButton.addEventListener('click', this.onMoreInfoClick.bind(this)); + + this.closeButton2 = document.createElement('button'); + this.closeButton2.textContent = 'Close'; + buttonsElem.appendChild(this.closeButton2); + this.closeButton2.addEventListener('click', this.onCloseButtonClick.bind(this)); + this.closeButton2.addEventListener('keydown', this.onKeyDown.bind(this)); + + this.moreInfoURL = ''; + + } + + onCloseButtonClick () { + this.infoDialog.close(); + } + + openDialog () { + this.infoDialog.showModal(); + this.closeButton2.focus(); + } + + onMoreInfoClick () { + if (this.moreInfoURL) { + window.open(this.moreInfoURL, '_blank').focus(); + } + } + + configureStyle(config={}) { + + function updateOption(style, option, configOption, defaultOption) { + if (configOption) { + return style.replaceAll(option, configOption); + } + else { + return style.replaceAll(option, defaultOption); + } + } + + // make a copy of the template + let style = styleTemplate$1.textContent.slice(0); + + style = updateOption(style, + '$fontFamily', + config.fontFamily, + defaultStyleOptions$3.fontFamily); + + style = updateOption(style, + '$fontSize', + config.fontSize, + defaultStyleOptions$3.fontSize); + + style = updateOption(style, + '$focusBorderColor', + config.focusBorderColor, + defaultStyleOptions$3.focusBorderColor); + + style = updateOption(style, + '$focusBorderDarkColor', + config.focusBorderDarkColor, + defaultStyleOptions$3.focusBorderDarkColor); + + style = updateOption(style, + '$dialogTextColor', + config.dialogTextColor, + defaultStyleOptions$3.dialogTextColor); + + style = updateOption(style, + '$dialogextDarkColor', + config.dialogextDarkColor, + defaultStyleOptions$3.dialogextDarkColor); + + style = updateOption(style, + '$dialogBackgroundColor', + config.dialogBackgroundColor, + defaultStyleOptions$3.dialogBackgroundColor); + + style = updateOption(style, + '$dialogBackgroundDarkColor', + config.dialogBackgroundDarkColor, + defaultStyleOptions$3.dialogBackgroundDarkColor); + + style = updateOption(style, + '$dialogBackgroundTitleColor', + config.dialogBackgroundTitleColor, + defaultStyleOptions$3.dialogBackgroundTitleColor); + + style = updateOption(style, + '$dialogBackgroundTitleDarkColor', + config.dialogBackgroundTitleDarkColor, + defaultStyleOptions$3.dialogBackgroundTitleDarkColor); + + + let styleNode = this.shadowRoot.querySelector('style'); + + if (styleNode) { + styleNode.remove(); + } + + styleNode = document.createElement('style'); + styleNode.textContent = style; + this.shadowRoot.appendChild(styleNode); + + } + + + updateShortcutContent (config) { + + while (this.contentElem.lastElementChild) { + this.contentElem.removeChild(this.contentElem.lastElementChild); + } + + this.moreInfoURL = MORE_SHORTCUT_INFO_URL; + + this.h2Elem.textContent = config.shortcutsInfoLabel; + this.closeButton1.setAttribute('aria-label', config.closeLabel); + this.closeButton2.textContent = config.closeLabel; + this.moreInfoButton.textContent = config.moreInfoLabel; + + function addRow(tbodyElem, shortcut, desc) { + + const trElem = document.createElement('tr'); + tbodyElem.appendChild(trElem); + + const tdElem1 = document.createElement('td'); + tdElem1.className = 'shortcut'; + tdElem1.textContent = shortcut; + trElem.appendChild(tdElem1); + + const tdElem2 = document.createElement('td'); + tdElem2.className = 'desc'; + tdElem2.textContent = desc; + trElem.appendChild(tdElem2); + } + + // Regions + + const tableElem1 = document.createElement('table'); + this.contentElem.appendChild(tableElem1); + + const captionElem1 = document.createElement('caption'); + captionElem1.textContent = config.landmarkGroupLabel; + tableElem1.appendChild(captionElem1); + + const theadElem1 = document.createElement('thead'); + tableElem1.appendChild(theadElem1); + + const trElem1 = document.createElement('tr'); + theadElem1.appendChild(trElem1); + + const thElem1 = document.createElement('th'); + thElem1.className = 'shortcut'; + thElem1.textContent = config.msgKey; + trElem1.appendChild(thElem1); + + const thElem2 = document.createElement('th'); + thElem2.className = 'desc'; + thElem2.textContent = config.msgDescription; + trElem1.appendChild(thElem2); + + const tbodyElem1 = document.createElement('tbody'); + tableElem1.appendChild(tbodyElem1); + + addRow(tbodyElem1, config.shortcutRegionNext, config.msgNextRegion); + addRow(tbodyElem1, config.shortcutRegionPrevious, config.msgPreviousRegion); + addRow(tbodyElem1, config.shortcutRegionMain, config.msgMainRegions); + addRow(tbodyElem1, config.shortcutRegionNavigation, config.msgNavigationRegions); + addRow(tbodyElem1, config.shortcutRegionComplementary, config.msgComplementaryRegions); + + // Headings + + const tableElem2 = document.createElement('table'); + this.contentElem.appendChild(tableElem2); + + const captionElem2 = document.createElement('caption'); + captionElem2.textContent = config.headingGroupLabel; + tableElem2.appendChild(captionElem2); + + const theadElem2 = document.createElement('thead'); + tableElem2.appendChild(theadElem2); + + const trElem2 = document.createElement('tr'); + theadElem2.appendChild(trElem2); + + const thElem3 = document.createElement('th'); + thElem3.className = 'shortcut'; + thElem3.textContent = config.msgKey; + trElem2.appendChild(thElem3); + + const thElem4 = document.createElement('th'); + thElem4.className = 'desc'; + thElem4.textContent = config.msgDescription; + trElem2.appendChild(thElem4); + + const tbodyElem2 = document.createElement('tbody'); + tableElem2.appendChild(tbodyElem2); + + addRow(tbodyElem2, config.shortcutHeadingNext, config.msgNextHeading); + addRow(tbodyElem2, config.shortcutHeadingPrevious, config.msgPreviousHeading); + addRow(tbodyElem2, config.shortcutHeadingH1, config.msgH1Headings); + addRow(tbodyElem2, config.shortcutHeadingH2, config.msgH2Headings); + addRow(tbodyElem2, config.shortcutHeadingH3, config.msgH3Headings); + addRow(tbodyElem2, config.shortcutHeadingH4, config.msgH4Headings); + addRow(tbodyElem2, config.shortcutHeadingH5, config.msgH5Headings); + addRow(tbodyElem2, config.shortcutHeadingH6, config.msgH6Headings); + + } + + updateAboutContent (config) { + + while (this.contentElem.lastElementChild) { + this.contentElem.removeChild(this.contentElem.lastElementChild); + } + + this.moreInfoURL = MORE_PAGE_SCRIPT_INFO_URL; + + this.h2Elem.textContent = config.aboutInfoLabel; + this.closeButton1.setAttribute('aria-label', config.closeLabel); + this.closeButton2.textContent = config.closeLabel; + this.moreInfoButton.textContent = config.moreInfoLabel; + + let divElem = document.createElement('div'); + divElem.className = 'desc'; + divElem.textContent = config.aboutDesc; + this.contentElem.appendChild(divElem); + + divElem = document.createElement('div'); + divElem.className = 'happy'; + divElem.textContent = config.aboutHappy; + this.contentElem.appendChild(divElem); + + divElem = document.createElement('div'); + divElem.className = 'version'; + divElem.textContent = config.aboutVersion; + this.contentElem.appendChild(divElem); + + divElem = document.createElement('div'); + divElem.className = 'copyright'; + divElem.textContent = config.aboutCopyright; + this.contentElem.appendChild(divElem); + + } + + onKeyDown (event) { + + if ((event.key === "Tab") && + !event.altKey && + !event.ctlKey && + !event.metaKey) { + + if (event.shiftKey && + (event.currentTarget === this.closeButton1)) { + this.closeButton2.focus(); + event.preventDefault(); + event.stopPropagation(); + } + + if (!event.shiftKey && + (event.currentTarget === this.closeButton2)) { + this.closeButton1.focus(); + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + + /* highlight.js */ /* Constants */ - const debug$a = new DebugLogging('[shortcutsInfoDialog]', false); - debug$a.flag = false; - - const defaultStyleOptions$3 = colorThemes['default']; - - - const styleTemplate$1 = document.createElement('template'); - styleTemplate$1.textContent = ` -/* infoDialog.css */ + const debug$9 = new DebugLogging('highlight', false); + debug$9.flag = false; -dialog#skip-to-info-dialog { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%,-50%); - font-family: $fontFamily; - font-size: $fontSize; - max-width: 70%; - margin: 0; - padding: 0; - background-color: light-dark($dialogBackgroundColor, $dialogBackgroundDarkColor); - color: light-dark($dialogTextColor, $dialogTextDarkColor); - border-width: 2px; - border-style: solid; - border-color: light-dark($focusBorderColor, $focusBorderDarkColor); - border-radius: 5px; - z-index: 2000001; + const minWidth = 68; + const minHeight = 27; -} + const defaultStyleOptions$2 = colorThemes['default']; -dialog#skip-to-info-dialog .header { - margin: 0; - margin-bottom: 0.5em; - padding: 4px; - border-width: 0; - border-bottom-width: 1px; - border-style: solid; - border-color: light-dark($focusBorderColor, $focusBorderDarkColor); - border-top-left-radius: 5px; - border-top-right-radius: 5px; - font-weight: bold; - background-color: light-dark($dialogBackgroundTitleColor, $dialogBackgroundTitleDarkColor); - color: light-dark($dialogTextColor, $dialogTextDarkColor); - position: relative; - font-size: 100%; + const styleHighlightTemplate = document.createElement('template'); + styleHighlightTemplate.textContent = ` +:root { + color-scheme: light dark; } -dialog#skip-to-info-dialog .header h2 { +#${HIGHLIGHT_ID} { margin: 0; padding: 0; - font-size: 1em; -} - -dialog#skip-to-info-dialog .header button { position: absolute; - top: -0.25em; - right: 0; - border: none; - background: transparent; - font-weight: bold; - color: light-dark(black, white); + border-radius: $highlightOffsetpx; + border: $shadowBorderWidthpx solid light-dark($menuBackgroundColor, $menuBackgroundDarkColor); + box-sizing: border-box; + pointer-events:none; + z-index: $zHighlight; } -dialog#skip-to-info-dialog .content { - margin-left: 2em; - margin-right: 2em; - margin-top: 0; - margin-bottom: 2em; +#${HIGHLIGHT_ID}.hasInfoBottom, +#${HIGHLIGHT_ID} .overlay-border.hasInfoBottom { + border-radius: $highlightOffsetpx $highlightOffsetpx $highlightOffsetpx 0; } -dialog#skip-to-info-dialog .content .desc { - max-width: 20em; +#${HIGHLIGHT_ID}.hasInfoTop, +#${HIGHLIGHT_ID} .overlay-border.hasInfoTop { + border-radius: 0 $highlightOffsetpx $highlightOffsetpx $highlightOffsetpx; } -dialog#skip-to-info-dialog .content .happy { - margin-top: 0.5em; - text-align: center; - font-family: fantasy, cursive; - font-size: 1.25em; - font-weight: bold; - font-style: italic; - letter-spacing: 0.05em; +#${HIGHLIGHT_ID} .overlay-border { + margin: 0; + padding: 0; + position: relative; + border-radius: $highlightOffsetpx; + border: $overlayBorderWidthpx $highlightBorderStyle light-dark($focusBorderColor, $focusBorderDarkColor); + z-index: $zHighlight; + box-sizing: border-box; + pointer-events:none; + background: transparent; } -dialog#skip-to-info-dialog .content .version, -dialog#skip-to-info-dialog .content .copyright { - margin-top: 0.5em; - text-align: center; - font-weight: bold; - font-size: 90%; -} - -dialog#skip-to-info-dialog .content table { - width: auto; +@keyframes fadeIn { + 0% { opacity: 0; } + 100% { opacity: 1; } } -dialog#skip-to-info-dialog .content caption { +#hidden-elem-msg { + position: absolute; margin: 0; - padding: 0; - margin-top: 1em; - text-align: left; + padding: .25em; + background-color: light-dark($hiddenHeadingBackgroundColor, $hiddenHeadingBackgroundDarkColor); + color: light-dark($hiddenHeadingColor, $hiddenHeadingDarkColor); + font-family: $fontFamily; + font-size: $fontSize; + font-style: italic; font-weight: bold; - font-size: 110%; + text-align: center; + animation: fadeIn 1.5s; + z-index: $zHighlight; } -dialog#skip-to-info-dialog .content th { +#${HIGHLIGHT_ID} .overlay-info { margin: 0; - padding: 0; - padding-top: 0.125em; - padding-bottom: 0.125em; + padding: 2px; + position: relative; text-align: left; - font-weight: bold; - font-size: 100%; -} - -dialog#skip-to-info-dialog .content th { - border-bottom-width: 1px; - border-bottom-style: solid; - border-bottom-color: light-dark(#999999, #777777); + font-size: $fontSize; + font-family: $fontFamily; + border: $infoBorderWidthpx solid light-dark($menuBackgroundColor, $menuBackgroundDarkColor); + background-color: light-dark($menuBackgroundColor, $menuBackgroundDarkColor); + color: light-dark($menuTextColor, $menuTextDarkColor); + z-index: $zHighlight; + overflow: hidden; + text-overflow: ellipsis; + pointer-events:none; } -dialog#skip-to-info-dialog .content th.shortcut { - width: 2.5em; +#${HIGHLIGHT_ID} .overlay-info.hasInfoTop { + border-radius: $highlightOffsetpx $highlightOffsetpx 0 0; } -dialog#skip-to-info-dialog .content td { - margin: 0; - padding: 0; - padding-top: 0.125em; - padding-bottom: 0.125em; - text-align: left; - font-size: 100%; +#${HIGHLIGHT_ID} .overlay-info.hasInfoBottom { + border-radius: 0 0 $highlightOffsetpx $highlightOffsetpx; } +@media (forced-colors: active) { -dialog#skip-to-info-dialog .content table tr:nth-child(even) { - background-color: light-dark(#eeeeee, #111111); -} - -dialog#skip-to-info-dialog .buttons { - float: right; - margin-right: 0.5em; - margin-bottom: 0.5em; -} + #${HIGHLIGHT_ID} { + border-color: ButtonBorder; + } -dialog#skip-to-info-dialog button { - margin: 6px; -} + #${HIGHLIGHT_ID} .overlay-border { + border-color: ButtonBorder; + } -dialog#skip-to-info-dialog .buttons button { - min-width: 5em; -} + #${HIGHLIGHT_ID} .overlay-border.skip-to-hidden { + background-color: ButtonFace; + color: ButtonText; + } -button:focus { - outline: 2px solid currentColor; - outline-offset: 2px; -} + #${HIGHLIGHT_ID} .overlay-info { + border-color: ButtonBorder; + background-color: ButtonFace; + color: ButtonText; + } -button:hover { - cursor: pointer; } `; /* - * + * @class HighlightElement * */ - class SkipToContentInfoDialog extends HTMLElement { - constructor () { + class HighlightElement extends HTMLElement { + constructor() { super(); this.attachShadow({ mode: 'open' }); // Get references - this.infoDialog = document.createElement('dialog'); - this.infoDialog.id = 'skip-to-info-dialog'; - this.shadowRoot.appendChild(this.infoDialog); - - const headerElem = document.createElement('div'); - headerElem.className = 'header'; - this.infoDialog.appendChild(headerElem); - - this.h2Elem = document.createElement('h2'); - this.h2Elem.textContent = 'Keyboard Shortcuts'; - headerElem.appendChild(this.h2Elem); - - this.closeButton1 = document.createElement('button'); - this.closeButton1.textContent = '✕'; - headerElem.appendChild(this.closeButton1); - this.closeButton1.addEventListener('click', this.onCloseButtonClick.bind(this)); - this.closeButton1.addEventListener('keydown', this.onKeyDown.bind(this)); - - this.contentElem = document.createElement('div'); - this.contentElem.className = 'content'; - this.infoDialog.appendChild(this.contentElem); + this.overlayElem = document.createElement('div'); + this.overlayElem.id = HIGHLIGHT_ID; + this.shadowRoot.appendChild(this.overlayElem); + this.overlayElem.style.display = 'none'; - const buttonsElem = document.createElement('div'); - buttonsElem.className = 'buttons'; - this.infoDialog.appendChild(buttonsElem); + this.borderElem = document.createElement('div'); + this.borderElem.className = 'overlay-border'; + this.overlayElem.appendChild(this.borderElem); - this.moreInfoButton = document.createElement('button'); - this.moreInfoButton.textContent = 'More Information'; - buttonsElem.appendChild(this.moreInfoButton); - this.moreInfoButton.addEventListener('click', this.onMoreInfoClick.bind(this)); + this.infoElem = document.createElement('div'); + this.infoElem.className = 'overlay-info'; + this.overlayElem.appendChild(this.infoElem); - this.closeButton2 = document.createElement('button'); - this.closeButton2.textContent = 'Close'; - buttonsElem.appendChild(this.closeButton2); - this.closeButton2.addEventListener('click', this.onCloseButtonClick.bind(this)); - this.closeButton2.addEventListener('keydown', this.onKeyDown.bind(this)); + this.hiddenElem = document.createElement('div'); + this.hiddenElem.id = 'hidden-elem-msg'; + this.shadowRoot.appendChild(this.hiddenElem); + this.hiddenElem.style.display = 'none'; - this.moreInfoURL = ''; + this.borderWidth = 0; + this.borderContrast = 0; + this.offset = 0; - } + this.msgHeadingIsHidden = ''; - onCloseButtonClick () { - this.infoDialog.close(); - } + this.configureStyle(); - openDialog () { - this.infoDialog.showModal(); - this.closeButton2.focus(); } - onMoreInfoClick () { - if (this.moreInfoURL) { - window.open(this.moreInfoURL, '_blank').focus(); - } - } + /* + * @method configureStyle + * + * @desc Updates stylesheet for styling the highlight information + * + * @param {Object} config : color and font information + */ configureStyle(config={}) { @@ -1170,1482 +1581,1104 @@ button:hover { } } + // Get i18n Messages + + this.msgHeadingIsHidden = typeof config.msgHeadingIsHidden === 'string' ? + config.msgHeadingIsHidden : + 'Heading is hidden'; + + this.msgRegionIsHidden = typeof config.msgRegionIsHidden === 'string' ? + config.msgRegionIsHidden : + 'Region is hidden'; + + this.msgElementIsHidden = typeof config.msgElementIsHidden === 'string' ? + config.msgElemenIsHidden : + 'Element is hidden'; + + // make a copy of the template - let style = styleTemplate$1.textContent.slice(0); + let style = styleHighlightTemplate.textContent.slice(0); style = updateOption(style, '$fontFamily', config.fontFamily, - defaultStyleOptions$3.fontFamily); + defaultStyleOptions$2.fontFamily); style = updateOption(style, - '$fontSize', - config.fontSize, - defaultStyleOptions$3.fontSize); + '$buttonBackgroundColor', + config.buttonBackgroundColor, + defaultStyleOptions$2.buttonBackgroundColor); + + style = updateOption(style, + '$buttonBackgroundDarkColor', + config.buttonBackgroundDarkColor, + defaultStyleOptions$2.buttonBackgroundDarkColor); style = updateOption(style, '$focusBorderColor', config.focusBorderColor, - defaultStyleOptions$3.focusBorderColor); + defaultStyleOptions$2.focusBorderColor); style = updateOption(style, '$focusBorderDarkColor', config.focusBorderDarkColor, - defaultStyleOptions$3.focusBorderDarkColor); + defaultStyleOptions$2.focusBorderDarkColor); style = updateOption(style, - '$dialogTextColor', - config.dialogTextColor, - defaultStyleOptions$3.dialogTextColor); + '$menuBackgroundColor', + config.menuBackgroundColor, + defaultStyleOptions$2.menuBackgroundColor); style = updateOption(style, - '$dialogextDarkColor', - config.dialogextDarkColor, - defaultStyleOptions$3.dialogextDarkColor); + '$menuBackgroundDarkColor', + config.menuBackgroundDarkColor, + defaultStyleOptions$2.menuBackgroundDarkColor); style = updateOption(style, - '$dialogBackgroundColor', - config.dialogBackgroundColor, - defaultStyleOptions$3.dialogBackgroundColor); + '$menuTextColor', + config.menuTextColor, + defaultStyleOptions$2.menuTextColor); style = updateOption(style, - '$dialogBackgroundDarkColor', - config.dialogBackgroundDarkColor, - defaultStyleOptions$3.dialogBackgroundDarkColor); + '$menuTextDarkColor', + config.menuTextDarkColor, + defaultStyleOptions$2.menuTextDarkColor); style = updateOption(style, - '$dialogBackgroundTitleColor', - config.dialogBackgroundTitleColor, - defaultStyleOptions$3.dialogBackgroundTitleColor); + '$hiddenHeadingColor', + config.hiddenHeadingColor, + defaultStyleOptions$2.hiddenHeadingColor); style = updateOption(style, - '$dialogBackgroundTitleDarkColor', - config.dialogBackgroundTitleDarkColor, - defaultStyleOptions$3.dialogBackgroundTitleDarkColor); - - - let styleNode = this.shadowRoot.querySelector('style'); - - if (styleNode) { - styleNode.remove(); - } - - styleNode = document.createElement('style'); - styleNode.textContent = style; - this.shadowRoot.appendChild(styleNode); - - } + '$hiddenHeadingDarkColor', + config.hiddenHeadingDarkColor, + defaultStyleOptions$2.hiddenHeadingDarkColor); + style = updateOption(style, + '$hiddenHeadingBackgroundColor', + config.hiddenHeadingBackgroundColor, + defaultStyleOptions$2.hiddenHeadingBackgroundColor); - updateShortcutContent (config) { + style = updateOption(style, + '$hiddenHeadingBackgroundDarkColor', + config.hiddenHeadingBackgroundDarkColor, + defaultStyleOptions$2.hiddenHeadingBackgroundDarkColor); - while (this.contentElem.lastElementChild) { - this.contentElem.removeChild(this.contentElem.lastElementChild); - } + style = updateOption(style, + '$zHighlight', + config.zHighlight, + defaultStyleOptions$2.zHighlight); - this.moreInfoURL = MORE_SHORTCUT_INFO_URL; + style = updateOption(style, + '$highlightBorderStyle', + config.highlightBorderStyle, + defaultStyleOptions$2.highlightBorderStyle); - this.h2Elem.textContent = config.shortcutsInfoLabel; - this.closeButton1.setAttribute('aria-label', config.closeLabel); - this.closeButton2.textContent = config.closeLabel; - this.moreInfoButton.textContent = config.moreInfoLabel; + const highlightBorderSize = config.highlightBorderSize ? + config.highlightBorderSize : + defaultStyleOptions$2.highlightBorderSize; - function addRow(tbodyElem, shortcut, desc) { + switch (highlightBorderSize) { + case 'small': + this.borderWidth = 2; + this.borderContrast = 1; + this.offset = 4; + this.fontSize = '12pt'; + break; - const trElem = document.createElement('tr'); - tbodyElem.appendChild(trElem); + case 'medium': + this.borderWidth = 3; + this.borderContrast = 2; + this.offset = 4; + this.fontSize = '13pt'; + break; - const tdElem1 = document.createElement('td'); - tdElem1.className = 'shortcut'; - tdElem1.textContent = shortcut; - trElem.appendChild(tdElem1); + case 'large': + this.borderWidth = 4; + this.borderContrast = 3; + this.offset = 6; + this.fontSize = '14pt'; + break; - const tdElem2 = document.createElement('td'); - tdElem2.className = 'desc'; - tdElem2.textContent = desc; - trElem.appendChild(tdElem2); - } + case 'x-large': + this.borderWidth = 6; + this.borderContrast = 3; + this.offset = 8; + this.fontSize = '16pt'; + break; - // Regions + default: + this.borderWidth = 2; + this.borderContrast = 1; + this.offset = 4; + this.fontSize = '12pt'; + break; + } - const tableElem1 = document.createElement('table'); - this.contentElem.appendChild(tableElem1); + style = updateOption(style, + '$fontSize', + this.fontSize, + defaultStyleOptions$2.fontSize); - const captionElem1 = document.createElement('caption'); - captionElem1.textContent = config.landmarkGroupLabel; - tableElem1.appendChild(captionElem1); + style = updateOption(style, + '$highlightOffset', + this.offset, + this.offset); - const theadElem1 = document.createElement('thead'); - tableElem1.appendChild(theadElem1); + style = updateOption(style, + '$overlayBorderWidth', + this.borderWidth, + this.borderWidth); - const trElem1 = document.createElement('tr'); - theadElem1.appendChild(trElem1); + style = updateOption(style, + '$shadowBorderWidth', + this.borderWidth + 2 * this.borderContrast, + this.borderWidth + 2 * this.borderContrast); - const thElem1 = document.createElement('th'); - thElem1.className = 'shortcut'; - thElem1.textContent = config.msgKey; - trElem1.appendChild(thElem1); + style = updateOption(style, + '$infoBorderWidth', + this.borderWidth, + this.borderWidth); - const thElem2 = document.createElement('th'); - thElem2.className = 'desc'; - thElem2.textContent = config.msgDescription; - trElem1.appendChild(thElem2); + let styleNode = this.shadowRoot.querySelector('style'); - const tbodyElem1 = document.createElement('tbody'); - tableElem1.appendChild(tbodyElem1); + if (styleNode) { + styleNode.remove(); + } - addRow(tbodyElem1, config.shortcutRegionNext, config.msgNextRegion); - addRow(tbodyElem1, config.shortcutRegionPrevious, config.msgPreviousRegion); - addRow(tbodyElem1, config.shortcutRegionMain, config.msgMainRegions); - addRow(tbodyElem1, config.shortcutRegionNavigation, config.msgNavigationRegions); - addRow(tbodyElem1, config.shortcutRegionComplementary, config.msgComplementaryRegions); + styleNode = document.createElement('style'); + styleNode.textContent = style; + this.shadowRoot.appendChild(styleNode); - // Headings + } - const tableElem2 = document.createElement('table'); - this.contentElem.appendChild(tableElem2); + /* + * @method highlight + * + * @desc Highlights the element on the page when highlighting + * is enabled (NOTE: Highlight is enabled by default) + * + * @param {Object} elem : DOM node of element to highlight + * @param {String} highlightTarget : value of highlight target + * @param {String} info : Information about target + * @param {Boolean} force : If true override isRduced + */ - const captionElem2 = document.createElement('caption'); - captionElem2.textContent = config.headingGroupLabel; - tableElem2.appendChild(captionElem2); + highlight(elem, highlightTarget, info='', force=false) { + let scrollElement; + const mediaQuery = window.matchMedia(`(prefers-reduced-motion: reduce)`); + const isReduced = !mediaQuery || mediaQuery.matches; - const theadElem2 = document.createElement('thead'); - tableElem2.appendChild(theadElem2); + if (elem && highlightTarget) { - const trElem2 = document.createElement('tr'); - theadElem2.appendChild(trElem2); + const rect = elem.getBoundingClientRect(); - const thElem3 = document.createElement('th'); - thElem3.className = 'shortcut'; - thElem3.textContent = config.msgKey; - trElem2.appendChild(thElem3); + // If target element is hidden create a visible element + debug$9.flag && debug$9.log(`[ info]: ${info}`); + debug$9.flag && debug$9.log(`[ rect]: Left: ${rect.left} Top: ${rect.top} Width: ${rect.width} height: ${rect.height}`); + debug$9.flag && debug$9.log(`[isHidden]: ${this.isElementHidden(elem)}`); - const thElem4 = document.createElement('th'); - thElem4.className = 'desc'; - thElem4.textContent = config.msgDescription; - trElem2.appendChild(thElem4); + if (this.isElementHidden(elem)) { + // If element is hidden make hidden element message visible + // and use for highlighing + this.hiddenElem.textContent = this.getHiddenMessage(elem); + this.hiddenElem.style.display = 'block'; - const tbodyElem2 = document.createElement('tbody'); - tableElem2.appendChild(tbodyElem2); + const left = rect.left > 0 ? rect.left + window.scrollX : this.offset; + const top = rect.top > 0 ? rect.top + window.scrollY : this.offset; - addRow(tbodyElem2, config.shortcutHeadingNext, config.msgNextHeading); - addRow(tbodyElem2, config.shortcutHeadingPrevious, config.msgPreviousHeading); - addRow(tbodyElem2, config.shortcutHeadingH1, config.msgH1Headings); - addRow(tbodyElem2, config.shortcutHeadingH2, config.msgH2Headings); - addRow(tbodyElem2, config.shortcutHeadingH3, config.msgH3Headings); - addRow(tbodyElem2, config.shortcutHeadingH4, config.msgH4Headings); - addRow(tbodyElem2, config.shortcutHeadingH5, config.msgH5Headings); - addRow(tbodyElem2, config.shortcutHeadingH6, config.msgH6Headings); + this.hiddenElem.style.left = left + 'px'; + this.hiddenElem.style.top = top + 'px'; + scrollElement = this.updateHighlightElement(this.hiddenElem, + info, + 0, + this.borderWidth, + this.borderContrast); + } + else { + this.hiddenElem.style.display = 'none'; + scrollElement = this.updateHighlightElement(elem, + info, + this.offset, + this.borderWidth, + this.borderContrast); + } + if (this.isElementInHeightLarge(elem)) { + if (!this.isElementStartInViewport(elem) && (!isReduced || force)) { + scrollElement.scrollIntoView({ behavior: highlightTarget, block: 'start', inline: 'nearest' }); + } + } + else { + if (!this.isElementInViewport(elem) && (!isReduced || force)) { + scrollElement.scrollIntoView({ behavior: highlightTarget, block: 'center', inline: 'nearest' }); + } + } + } } - updateAboutContent (config) { + /* + * @method updateHighlightElement + * + * @desc Create an overlay element and set its position on the page. + * + * @param {Object} elem - DOM element node to highlight + * @param {String} info - Description of the element + * @param {Number} offset - Number of pixels for offset + * @param {Number} borderWidth - Number of pixels for border width + * @param {Number} borderContrast - Number of pixels to provide border contrast + * + */ - while (this.contentElem.lastElementChild) { - this.contentElem.removeChild(this.contentElem.lastElementChild); - } + updateHighlightElement (elem, info, offset, borderWidth, borderContrast) { - this.moreInfoURL = MORE_PAGE_SCRIPT_INFO_URL; + const adjRect = this.getAdjustedRect(elem, offset, borderWidth, borderContrast); - this.h2Elem.textContent = config.aboutInfoLabel; - this.closeButton1.setAttribute('aria-label', config.closeLabel); - this.closeButton2.textContent = config.closeLabel; - this.moreInfoButton.textContent = config.moreInfoLabel; + const borderElemOffset = -1 * (this.borderWidth + this.borderContrast); - let divElem = document.createElement('div'); - divElem.className = 'desc'; - divElem.textContent = config.aboutDesc; - this.contentElem.appendChild(divElem); + this.overlayElem.style.left = adjRect.left + 'px'; + this.overlayElem.style.top = adjRect.top + 'px'; + this.borderElem.style.left = borderElemOffset + 'px'; + this.borderElem.style.top = borderElemOffset + 'px'; - divElem = document.createElement('div'); - divElem.className = 'happy'; - divElem.textContent = config.aboutHappy; - this.contentElem.appendChild(divElem); + this.overlayElem.style.width = adjRect.width + 'px'; + this.overlayElem.style.height = adjRect.height + 'px'; + this.borderElem.style.width = (adjRect.width - (2 * borderContrast)) + 'px'; + this.borderElem.style.height = (adjRect.height - (2 * borderContrast)) + 'px'; - divElem = document.createElement('div'); - divElem.className = 'version'; - divElem.textContent = config.aboutVersion; - this.contentElem.appendChild(divElem); + this.overlayElem.style.display = 'block'; - divElem = document.createElement('div'); - divElem.className = 'copyright'; - divElem.textContent = config.aboutCopyright; - this.contentElem.appendChild(divElem); + if (info) { - } + this.infoElem.style.display = 'inline-block'; + this.infoElem.textContent = info; - onKeyDown (event) { + const infoElemOffsetLeft = -1 * (borderWidth + 2 * borderContrast); + this.infoElem.style.left = infoElemOffsetLeft + 'px'; - if ((event.key === "Tab") && - !event.altKey && - !event.ctlKey && - !event.metaKey) { + const infoElemRect = this.infoElem.getBoundingClientRect(); - if (event.shiftKey && - (event.currentTarget === this.closeButton1)) { - this.closeButton2.focus(); - event.preventDefault(); - event.stopPropagation(); + // Is info displayed above or below the highlighted element + if (adjRect.top >= infoElemRect.height) { + // Info is displayed above the highlighted element (e.g. most of the time) + this.overlayElem.classList.remove('hasInfoBottom'); + this.borderElem.classList.remove('hasInfoBottom'); + this.infoElem.classList.remove('hasInfoBottom'); + this.overlayElem.classList.add('hasInfoTop'); + this.borderElem.classList.add('hasInfoTop'); + this.infoElem.classList.add('hasInfoTop'); + this.infoElem.style.top = (-1 * (adjRect.height + + infoElemRect.height + + borderWidth)) + 'px'; } + else { + // Info is displayed below the highlighted element when it is at the top of + // the window - if (!event.shiftKey && - (event.currentTarget === this.closeButton2)) { - this.closeButton1.focus(); - event.preventDefault(); - event.stopPropagation(); - } - } - } - } - - /* highlight.js */ - - /* Constants */ - const debug$9 = new DebugLogging('highlight', false); - debug$9.flag = false; - - const minWidth = 68; - const minHeight = 27; - - const defaultStyleOptions$2 = colorThemes['default']; - - const styleHighlightTemplate = document.createElement('template'); - styleHighlightTemplate.textContent = ` -:root { - color-scheme: light dark; -} - -#${HIGHLIGHT_ID} { - margin: 0; - padding: 0; - position: absolute; - border-radius: $highlightOffsetpx; - border: $shadowBorderWidthpx solid light-dark($menuBackgroundColor, $menuBackgroundDarkColor); - box-sizing: border-box; - pointer-events:none; - z-index: $zHighlight; -} - -#${HIGHLIGHT_ID}.hasInfoBottom, -#${HIGHLIGHT_ID} .overlay-border.hasInfoBottom { - border-radius: $highlightOffsetpx $highlightOffsetpx $highlightOffsetpx 0; -} - -#${HIGHLIGHT_ID}.hasInfoTop, -#${HIGHLIGHT_ID} .overlay-border.hasInfoTop { - border-radius: 0 $highlightOffsetpx $highlightOffsetpx $highlightOffsetpx; -} - -#${HIGHLIGHT_ID} .overlay-border { - margin: 0; - padding: 0; - position: relative; - border-radius: $highlightOffsetpx; - border: $overlayBorderWidthpx $highlightBorderStyle light-dark($focusBorderColor, $focusBorderDarkColor); - z-index: $zHighlight; - box-sizing: border-box; - pointer-events:none; - background: transparent; -} - - -@keyframes fadeIn { - 0% { opacity: 0; } - 100% { opacity: 1; } -} - -#hidden-elem-msg { - position: absolute; - margin: 0; - padding: .25em; - background-color: light-dark($hiddenHeadingBackgroundColor, $hiddenHeadingBackgroundDarkColor); - color: light-dark($hiddenHeadingColor, $hiddenHeadingDarkColor); - font-family: $fontFamily; - font-size: $fontSize; - font-style: italic; - font-weight: bold; - text-align: center; - animation: fadeIn 1.5s; - z-index: $zHighlight; -} - -#${HIGHLIGHT_ID} .overlay-info { - margin: 0; - padding: 2px; - position: relative; - text-align: left; - font-size: $fontSize; - font-family: $fontFamily; - border: $infoBorderWidthpx solid light-dark($menuBackgroundColor, $menuBackgroundDarkColor); - background-color: light-dark($menuBackgroundColor, $menuBackgroundDarkColor); - color: light-dark($menuTextColor, $menuTextDarkColor); - z-index: $zHighlight; - overflow: hidden; - text-overflow: ellipsis; - pointer-events:none; -} - -#${HIGHLIGHT_ID} .overlay-info.hasInfoTop { - border-radius: $highlightOffsetpx $highlightOffsetpx 0 0; -} - -#${HIGHLIGHT_ID} .overlay-info.hasInfoBottom { - border-radius: 0 0 $highlightOffsetpx $highlightOffsetpx; -} - -@media (forced-colors: active) { - - #${HIGHLIGHT_ID} { - border-color: ButtonBorder; - } - - #${HIGHLIGHT_ID} .overlay-border { - border-color: ButtonBorder; - } - - #${HIGHLIGHT_ID} .overlay-border.skip-to-hidden { - background-color: ButtonFace; - color: ButtonText; - } - - #${HIGHLIGHT_ID} .overlay-info { - border-color: ButtonBorder; - background-color: ButtonFace; - color: ButtonText; - } - -} -`; - - /* - * @class HighlightElement - * - */ - - class HighlightElement extends HTMLElement { - - constructor() { - super(); - this.attachShadow({ mode: 'open' }); - - // Get references - - this.overlayElem = document.createElement('div'); - this.overlayElem.id = HIGHLIGHT_ID; - this.shadowRoot.appendChild(this.overlayElem); - this.overlayElem.style.display = 'none'; - - this.borderElem = document.createElement('div'); - this.borderElem.className = 'overlay-border'; - this.overlayElem.appendChild(this.borderElem); - - this.infoElem = document.createElement('div'); - this.infoElem.className = 'overlay-info'; - this.overlayElem.appendChild(this.infoElem); - - this.hiddenElem = document.createElement('div'); - this.hiddenElem.id = 'hidden-elem-msg'; - this.shadowRoot.appendChild(this.hiddenElem); - this.hiddenElem.style.display = 'none'; - - this.borderWidth = 0; - this.borderContrast = 0; - this.offset = 0; - - this.msgHeadingIsHidden = ''; - - this.configureStyle(); - - } - - /* - * @method configureStyle - * - * @desc Updates stylesheet for styling the highlight information - * - * @param {Object} config : color and font information - */ - - configureStyle(config={}) { - - function updateOption(style, option, configOption, defaultOption) { - if (configOption) { - return style.replaceAll(option, configOption); - } - else { - return style.replaceAll(option, defaultOption); - } - } - - // Get i18n Messages - - this.msgHeadingIsHidden = typeof config.msgHeadingIsHidden === 'string' ? - config.msgHeadingIsHidden : - 'Heading is hidden'; - - this.msgRegionIsHidden = typeof config.msgRegionIsHidden === 'string' ? - config.msgRegionIsHidden : - 'Region is hidden'; - - this.msgElementIsHidden = typeof config.msgElementIsHidden === 'string' ? - config.msgElemenIsHidden : - 'Element is hidden'; - - - // make a copy of the template - let style = styleHighlightTemplate.textContent.slice(0); - - style = updateOption(style, - '$fontFamily', - config.fontFamily, - defaultStyleOptions$2.fontFamily); - - style = updateOption(style, - '$buttonBackgroundColor', - config.buttonBackgroundColor, - defaultStyleOptions$2.buttonBackgroundColor); - - style = updateOption(style, - '$buttonBackgroundDarkColor', - config.buttonBackgroundDarkColor, - defaultStyleOptions$2.buttonBackgroundDarkColor); - - style = updateOption(style, - '$focusBorderColor', - config.focusBorderColor, - defaultStyleOptions$2.focusBorderColor); - - style = updateOption(style, - '$focusBorderDarkColor', - config.focusBorderDarkColor, - defaultStyleOptions$2.focusBorderDarkColor); - - style = updateOption(style, - '$menuBackgroundColor', - config.menuBackgroundColor, - defaultStyleOptions$2.menuBackgroundColor); - - style = updateOption(style, - '$menuBackgroundDarkColor', - config.menuBackgroundDarkColor, - defaultStyleOptions$2.menuBackgroundDarkColor); - - style = updateOption(style, - '$menuTextColor', - config.menuTextColor, - defaultStyleOptions$2.menuTextColor); - - style = updateOption(style, - '$menuTextDarkColor', - config.menuTextDarkColor, - defaultStyleOptions$2.menuTextDarkColor); - - style = updateOption(style, - '$hiddenHeadingColor', - config.hiddenHeadingColor, - defaultStyleOptions$2.hiddenHeadingColor); - - style = updateOption(style, - '$hiddenHeadingDarkColor', - config.hiddenHeadingDarkColor, - defaultStyleOptions$2.hiddenHeadingDarkColor); - - style = updateOption(style, - '$hiddenHeadingBackgroundColor', - config.hiddenHeadingBackgroundColor, - defaultStyleOptions$2.hiddenHeadingBackgroundColor); - - style = updateOption(style, - '$hiddenHeadingBackgroundDarkColor', - config.hiddenHeadingBackgroundDarkColor, - defaultStyleOptions$2.hiddenHeadingBackgroundDarkColor); - - style = updateOption(style, - '$zHighlight', - config.zHighlight, - defaultStyleOptions$2.zHighlight); - - style = updateOption(style, - '$highlightBorderStyle', - config.highlightBorderStyle, - defaultStyleOptions$2.highlightBorderStyle); - - const highlightBorderSize = config.highlightBorderSize ? - config.highlightBorderSize : - defaultStyleOptions$2.highlightBorderSize; - - switch (highlightBorderSize) { - case 'small': - this.borderWidth = 2; - this.borderContrast = 1; - this.offset = 4; - this.fontSize = '12pt'; - break; - - case 'medium': - this.borderWidth = 3; - this.borderContrast = 2; - this.offset = 4; - this.fontSize = '13pt'; - break; - - case 'large': - this.borderWidth = 4; - this.borderContrast = 3; - this.offset = 6; - this.fontSize = '14pt'; - break; - - case 'x-large': - this.borderWidth = 6; - this.borderContrast = 3; - this.offset = 8; - this.fontSize = '16pt'; - break; - - default: - this.borderWidth = 2; - this.borderContrast = 1; - this.offset = 4; - this.fontSize = '12pt'; - break; - } - - style = updateOption(style, - '$fontSize', - this.fontSize, - defaultStyleOptions$2.fontSize); - - style = updateOption(style, - '$highlightOffset', - this.offset, - this.offset); - - style = updateOption(style, - '$overlayBorderWidth', - this.borderWidth, - this.borderWidth); - - style = updateOption(style, - '$shadowBorderWidth', - this.borderWidth + 2 * this.borderContrast, - this.borderWidth + 2 * this.borderContrast); - - style = updateOption(style, - '$infoBorderWidth', - this.borderWidth, - this.borderWidth); - - let styleNode = this.shadowRoot.querySelector('style'); - - if (styleNode) { - styleNode.remove(); - } - - styleNode = document.createElement('style'); - styleNode.textContent = style; - this.shadowRoot.appendChild(styleNode); - - } - - /* - * @method highlight - * - * @desc Highlights the element on the page when highlighting - * is enabled (NOTE: Highlight is enabled by default) - * - * @param {Object} elem : DOM node of element to highlight - * @param {String} highlightTarget : value of highlight target - * @param {String} info : Information about target - * @param {Boolean} force : If true override isRduced - */ - - highlight(elem, highlightTarget, info='', force=false) { - let scrollElement; - const mediaQuery = window.matchMedia(`(prefers-reduced-motion: reduce)`); - const isReduced = !mediaQuery || mediaQuery.matches; - - if (elem && highlightTarget) { - - const rect = elem.getBoundingClientRect(); - - // If target element is hidden create a visible element - debug$9.flag && debug$9.log(`[ info]: ${info}`); - debug$9.flag && debug$9.log(`[ rect]: Left: ${rect.left} Top: ${rect.top} Width: ${rect.width} height: ${rect.height}`); - debug$9.flag && debug$9.log(`[isHidden]: ${this.isElementHidden(elem)}`); - - if (this.isElementHidden(elem)) { - // If element is hidden make hidden element message visible - // and use for highlighing - this.hiddenElem.textContent = this.getHiddenMessage(elem); - this.hiddenElem.style.display = 'block'; - - const left = rect.left > 0 ? rect.left + window.scrollX : this.offset; - const top = rect.top > 0 ? rect.top + window.scrollY : this.offset; - - this.hiddenElem.style.left = left + 'px'; - this.hiddenElem.style.top = top + 'px'; - scrollElement = this.updateHighlightElement(this.hiddenElem, - info, - 0, - this.borderWidth, - this.borderContrast); - } - else { - this.hiddenElem.style.display = 'none'; - scrollElement = this.updateHighlightElement(elem, - info, - this.offset, - this.borderWidth, - this.borderContrast); - } - - if (this.isElementInHeightLarge(elem)) { - if (!this.isElementStartInViewport(elem) && (!isReduced || force)) { - scrollElement.scrollIntoView({ behavior: highlightTarget, block: 'start', inline: 'nearest' }); - } - } - else { - if (!this.isElementInViewport(elem) && (!isReduced || force)) { - scrollElement.scrollIntoView({ behavior: highlightTarget, block: 'center', inline: 'nearest' }); - } - } - } - } - - /* - * @method updateHighlightElement - * - * @desc Create an overlay element and set its position on the page. - * - * @param {Object} elem - DOM element node to highlight - * @param {String} info - Description of the element - * @param {Number} offset - Number of pixels for offset - * @param {Number} borderWidth - Number of pixels for border width - * @param {Number} borderContrast - Number of pixels to provide border contrast - * - */ - - updateHighlightElement (elem, info, offset, borderWidth, borderContrast) { - - const adjRect = this.getAdjustedRect(elem, offset, borderWidth, borderContrast); - - const borderElemOffset = -1 * (this.borderWidth + this.borderContrast); - - this.overlayElem.style.left = adjRect.left + 'px'; - this.overlayElem.style.top = adjRect.top + 'px'; - this.borderElem.style.left = borderElemOffset + 'px'; - this.borderElem.style.top = borderElemOffset + 'px'; - - this.overlayElem.style.width = adjRect.width + 'px'; - this.overlayElem.style.height = adjRect.height + 'px'; - this.borderElem.style.width = (adjRect.width - (2 * borderContrast)) + 'px'; - this.borderElem.style.height = (adjRect.height - (2 * borderContrast)) + 'px'; - - this.overlayElem.style.display = 'block'; - - if (info) { - - this.infoElem.style.display = 'inline-block'; - this.infoElem.textContent = info; - - const infoElemOffsetLeft = -1 * (borderWidth + 2 * borderContrast); - this.infoElem.style.left = infoElemOffsetLeft + 'px'; - - const infoElemRect = this.infoElem.getBoundingClientRect(); - - // Is info displayed above or below the highlighted element - if (adjRect.top >= infoElemRect.height) { - // Info is displayed above the highlighted element (e.g. most of the time) - this.overlayElem.classList.remove('hasInfoBottom'); - this.borderElem.classList.remove('hasInfoBottom'); - this.infoElem.classList.remove('hasInfoBottom'); - this.overlayElem.classList.add('hasInfoTop'); - this.borderElem.classList.add('hasInfoTop'); - this.infoElem.classList.add('hasInfoTop'); - this.infoElem.style.top = (-1 * (adjRect.height + - infoElemRect.height + - borderWidth)) + 'px'; - } - else { - // Info is displayed below the highlighted element when it is at the top of - // the window - - const infoElemOffsetTop = -1 * (borderWidth + borderContrast); - - this.overlayElem.classList.remove('hasInfoTop'); - this.borderElem.classList.remove('hasInfoTop'); - this.infoElem.classList.remove('hasInfoTop'); - this.overlayElem.classList.add('hasInfoBottom'); - this.borderElem.classList.add('hasInfoBottom'); - this.infoElem.classList.add('hasInfoBottom'); - this.infoElem.style.top = infoElemOffsetTop + 'px'; - } - return this.infoElem; - } - else { - this.overlayElem.classList.remove('hasInfoTop'); - this.overlayElem.classList.remove('hasInfoBottom'); - this.borderElem.classList.remove('hasInfoTop'); - this.borderElem.classList.remove('hasInfoBottom'); - this.infoElem.style.display = 'none'; - return this.overlayElem; - } - } - - - /* - * @method getAdjustedRect - * - * @desc Returns a object with dimensions adjusted for highlighting element - * - * @param {Object} elem - DOM node of element to be highlighted - * @param {Number} offset - Number of pixels for offset - * @param {Number} borderWidth - Number of pixels for border width - * @param {Number} borderContrast - Number of pixels to provide border contrast - * - * @returns see @desc - */ - - getAdjustedRect(elem, offset, borderWidth, borderContrast) { - - const rect = elem.getBoundingClientRect(); - - const adjRect = { - left: 0, - top: 0, - width: 0, - height: 0 - }; - - const offsetBorder = offset + borderWidth + 2 * borderContrast; - - adjRect.left = rect.left > offset ? - Math.round(rect.left + (-1 * offsetBorder) + window.scrollX) : - Math.round(rect.left + window.scrollX); - - adjRect.width = rect.left > offset ? - Math.max(rect.width + (2 * offsetBorder), minWidth) : - Math.max(rect.width, minWidth); - - - adjRect.top = rect.top > offset ? - Math.round(rect.top + (-1 * offsetBorder) + window.scrollY) : - Math.round(rect.top + window.scrollY); - - adjRect.height = rect.top > offset ? - Math.max(rect.height + (2 * offsetBorder), minHeight) : - Math.max(rect.height, minHeight); - - if ((adjRect.top < 0) || (adjRect.left < 0)) { - // Element is near top or left side of screen - adjRect.left = this.offset; - adjRect.top = this.offset; - } - - return adjRect; - } - - /* - * @method isElementInViewport - * - * @desc Returns true if element is already visible in view port, - * otheriwse false - * - * @param {Object} elem : DOM node of element to highlight - * - * @returns see @desc - */ - - isElementInViewport(elem) { - const rect = elem.getBoundingClientRect(); - return ( - rect.top >= window.screenY && - rect.left >= window.screenX && - rect.bottom <= ((window.screenY + window.innerHeight) || - (window.screenY + document.documentElement.clientHeight)) && - rect.right <= ((window.screenX + window.innerWidth) || - (window.screenX + document.documentElement.clientWidth))); - } - - /* - * @method isElementStartInViewport - * - * @desc Returns true if start of the element is already visible in view port, - * otherwise false - * - * @param {Object} elem : DOM node of element to highlight - * - * @returns see @desc - */ - - isElementStartInViewport(elem) { - const rect = elem.getBoundingClientRect(); - return ( - rect.top >= window.screenY && - rect.top <= ((window.screenY + window.innerHeight) || - (window.screenY + document.documentElement.clientHeight)) && - rect.left >= window.screenX && - rect.left <= ((window.screenX + window.innerWidth) || - (window.screenX + document.documentElement.clientWidth))); - } - - - /* - * @method isElementHeightLarge - * - * @desc Returns true if element client height is larger than clientHeight, - * otheriwse false - * - * @param {Object} elem : DOM node of element to highlight - * - * @returns see @desc - */ - - isElementInHeightLarge(elem) { - var rect = elem.getBoundingClientRect(); - return (1.2 * rect.height) > (window.innerHeight || document.documentElement.clientHeight); - } - - /* - * @method isElementHidden - * - * @desc Returns true if the element is hidden on the - * graphical rendering - * - * @param {Object} elem : DOM node - * - * @returns see @desc - */ - isElementHidden(elem) { - const rect = elem.getBoundingClientRect(); - return (rect.height < 3) || - (rect.width < 3) || - ((rect.left + rect.width) < (rect.width / 2)) || - ((rect.top + rect.height) < (rect.height / 2)); - } - - /* - * @method getHiddenMessage - * - * @desc Returns string describing the hidden element - * - * @param {Object} elem : DOM node - * - * @returns see @desc - */ - getHiddenMessage(elem) { - if (elem.hasAttribute('data-skip-to-info')) { - const info = elem.getAttribute('data-skip-to-info'); - - if (info.includes('heading')) { - return this.msgHeadingIsHidden; - } - - if (info.includes('landmark')) { - return this.msgRegionIsHidden; - } - } - - return this.msgElementIsHidden; - } - - /* - * @method removeHighlight - * - * @desc Hides the highlight element on the page - */ - removeHighlight() { - if (this.overlayElem) { - this.overlayElem.style.display = 'none'; - } - } - - } - - /* shortcutsMessage.js */ - - /* Constants */ - const debug$8 = new DebugLogging('[shortcutsMessage]', false); - debug$8.flag = false; - - const defaultStyleOptions$1 = colorThemes['default']; - - const styleTemplate = document.createElement('template'); - styleTemplate.textContent = ` -/* shortcutsMessage.css */ -:root { - color-scheme: light dark; -} - -#${MESSAGE_ID} { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%,-50%); - - font-family: $fontFamily; - font-size: $fontSize; - max-width: 70%; - margin: 0; - padding: 0; - background-color: light-dark($dialogBackgroundColor, $dialogBackgroundDarkColor); - border: 2px solid light-dark($focusBorderColor, $focusBorderDarkColor); - border-radius: 5px; - color: light-dark($dialogTextColor, $dialogTextDarkColor); - z-index: 2000001; - opacity: 1; -} - -#${MESSAGE_ID} .header { - margin: 0; - padding: 4px; - border-bottom: 1px solid light-dark($focusBorderColor, $focusBorderDarkColor); - border-top-left-radius: 5px; - border-top-right-radius: 5px; - font-weight: bold; - background-color: light-dark($dialogBackgroundTitleColor, $dialogBackgroundTitleDarkColor); - color light-dark($dialogTextColor, $dialogTextDarkColor); - font-size: 100%; -} - -#${MESSAGE_ID} .content { - margin-left: 2em; - margin-right: 2em; - margin-top: 2em; - margin-bottom: 2em; - background-color: light-dark($dialogBackgroundColor, $dialogBackgroundDarkColor); - color: light-dark($dialogTextColor, $dialogTextDarkColor); - font-size: 110%; - text-algin: center; -} - -#${MESSAGE_ID}.hidden { - display: none; -} - -#${MESSAGE_ID}.show { - display: block; - opacity: 1; -} - -#${MESSAGE_ID}.fade { - opacity: 0; - transition: visibility 0s 1s, opacity 1s linear; -} - -@media (forced-colors: active) { - - #${MESSAGE_ID} { - background-color: Canvas; - color CanvasText; - border-color: AccentColor; - } - - #${MESSAGE_ID} .header { - background-color: Canvas; - color CanvasText; - } - - #${MESSAGE_ID} .content { - background-color: Canvas; - color: CanvasText; - } -} - -`; - - class ShortcutsMessage extends HTMLElement { - constructor () { - - super(); - this.attachShadow({ mode: 'open' }); - - // Get references - - this.messageDialog = document.createElement('div'); - this.messageDialog.id = MESSAGE_ID; - this.messageDialog.classList.add('hidden'); - this.shadowRoot.appendChild(this.messageDialog); - - const headerElem = document.createElement('div'); - headerElem.className = 'header'; - headerElem.textContent = 'SkipTo.js Message'; - this.messageDialog.appendChild(headerElem); - - this.contentElem = document.createElement('div'); - this.contentElem.className = 'content'; - this.messageDialog.appendChild(this.contentElem); - - this.timeoutShowID = false; - this.timeoutFadeID = false; - - } - - configureStyle(config={}) { - - function updateOption(style, option, configOption, defaultOption) { - if (configOption) { - return style.replaceAll(option, configOption); - } - else { - return style.replaceAll(option, defaultOption); - } - } - - // make a copy of the template - let style = styleTemplate.textContent.slice(0); - - style = updateOption(style, - '$fontFamily', - config.fontFamily, - defaultStyleOptions$1.fontFamily); - - style = updateOption(style, - '$fontSize', - config.fontSize, - defaultStyleOptions$1.fontSize); - - style = updateOption(style, - '$focusBorderColor', - config.focusBorderColor, - defaultStyleOptions$1.focusBorderColor); - - style = updateOption(style, - '$focusBorderDarkColor', - config.focusBorderDarkColor, - defaultStyleOptions$1.focusBorderDarkColor); - - - style = updateOption(style, - '$dialogTextColor', - config.dialogTextColor, - defaultStyleOptions$1.dialogTextColor); - - style = updateOption(style, - '$dialogTextDarkColor', - config.dialogTextDarkColor, - defaultStyleOptions$1.dialogTextDarkColor); - - style = updateOption(style, - '$dialogBackgroundColor', - config.dialogBackgroundColor, - defaultStyleOptions$1.dialogBackgroundColor); - - style = updateOption(style, - '$dialogBackgroundDarkColor', - config.dialogBackgroundDarkColor, - defaultStyleOptions$1.dialogBackgroundDarkColor); - - style = updateOption(style, - '$dialogBackgroundTitleColor', - config.dialogBackgroundTitleColor, - defaultStyleOptions$1.dialogBackgroundTitleColor); - - style = updateOption(style, - '$dialogBackgroundTitleDarkColor', - config.dialogBackgroundTitleDarkColor, - defaultStyleOptions$1.dialogBackgroundTitleDarkColor); - - let styleNode = this.shadowRoot.querySelector('style'); - - if (styleNode) { - styleNode.remove(); - } - - styleNode = document.createElement('style'); - styleNode.textContent = style; - this.shadowRoot.appendChild(styleNode); - - } - - close() { - this.messageDialog.classList.remove('show'); - this.messageDialog.classList.remove('fade'); - this.messageDialog.classList.add('hidden'); - } - - open(message) { - clearInterval(this.timeoutFadeID); - clearInterval(this.timeoutShowID); - this.messageDialog.classList.remove('hidden'); - this.messageDialog.classList.remove('fade'); - this.messageDialog.classList.add('show'); - this.contentElem.textContent = message; - - const msg = this; - - this.timeoutFadeID = setTimeout( () => { - msg.messageDialog.classList.add('fade'); - msg.messageDialog.classList.remove('show'); - }, 3000); - - this.timeoutShowID = setTimeout( () => { - msg.close(); - }, 4000); - - } - - } - - /* - * namefrom.js - */ - - /* constants */ - - const debug$7 = new DebugLogging('nameFrom', false); - debug$7.flag = false; - - // - // LOW-LEVEL HELPER FUNCTIONS (NOT EXPORTED) - - /* - * @function isDisplayNone - * - * @desc Returns true if the element or parent element has set the CSS - * display property to none or has the hidden attribute, - * otherwise false - * - * @param {Object} node - a DOM node - * - * @returns {Boolean} see @desc - */ - - function isDisplayNone (node) { - - if (!node) { - return false; - } - - if (node.nodeType === Node.TEXT_NODE) { - node = node.parentNode; - } - - if (node.nodeType === Node.ELEMENT_NODE) { - - if (node.hasAttribute('hidden')) { - return true; - } + const infoElemOffsetTop = -1 * (borderWidth + borderContrast); - // aria-hidden attribute with the value "true" is an same as - // setting the hidden attribute for name calcuation - if (node.hasAttribute('aria-hidden')) { - if (node.getAttribute('aria-hidden').toLowerCase() === 'true') { - return true; + this.overlayElem.classList.remove('hasInfoTop'); + this.borderElem.classList.remove('hasInfoTop'); + this.infoElem.classList.remove('hasInfoTop'); + this.overlayElem.classList.add('hasInfoBottom'); + this.borderElem.classList.add('hasInfoBottom'); + this.infoElem.classList.add('hasInfoBottom'); + this.infoElem.style.top = infoElemOffsetTop + 'px'; } + return this.infoElem; } - - const style = window.getComputedStyle(node, null); - - const display = style.getPropertyValue("display"); - - if (display) { - return display === 'none'; - } - } - return false; - } - - /* - * @function isVisibilityHidden - * - * @desc Returns true if the node (or it's parrent) has the CSS visibility - * property set to "hidden" or "collapse", otherwise false - * - * @param {Object} node - DOM node - * - * @return see @desc - */ - - function isVisibilityHidden(node) { - - if (!node) { - return false; - } - - if (node.nodeType === Node.TEXT_NODE) { - node = node.parentNode; - } - - if (node.nodeType === Node.ELEMENT_NODE) { - const style = window.getComputedStyle(node, null); - - const visibility = style.getPropertyValue("visibility"); - if (visibility) { - return (visibility === 'hidden') || (visibility === 'collapse'); + else { + this.overlayElem.classList.remove('hasInfoTop'); + this.overlayElem.classList.remove('hasInfoBottom'); + this.borderElem.classList.remove('hasInfoTop'); + this.borderElem.classList.remove('hasInfoBottom'); + this.infoElem.style.display = 'none'; + return this.overlayElem; } } - return false; - } - - /* - * @function isAriaHiddenFalse - * - * @desc Returns true if the node has the aria-hidden property set to - * "false", otherwise false. - * NOTE: This function is important in the accessible namce - * calculation, since content hidden with a CSS technique - * can be included in the accessible name calculation when - * aria-hidden is set to false - * - * @param {Object} node - DOM node - * - * @return see @desc - */ - - function isAriaHIddenFalse(node) { - - if (!node) { - return false; - } - - if (node.nodeType === Node.TEXT_NODE) { - node = node.parentNode; - } - - if (node.nodeType === Node.ELEMENT_NODE) { - return (node.hasAttribute('aria-hidden') && - (node.getAttribute('aria-hidden').toLowerCase() === 'false')); - } - - return false; - } - - /* - * @function includeContentInName - * - * @desc Checks the CSS display and hidden properties, and - * the aria-hidden property to see if the content - * should be included in the accessible name - * calculation. Returns true if it should be - * included, otherwise false - * - * @param {Object} node - DOM node - * - * @return see @desc - */ - function includeContentInName(node) { - const flag = isAriaHIddenFalse(node) || - (!isVisibilityHidden(node) && - !isDisplayNone(node)); - return flag; - } - /* - * @function getNodeContents - * - * @desc Recursively process element and text nodes by aggregating - * their text values for an ARIA accessible name or description - * calculation. - * - * NOTE: This includes special handling of elements with 'alt' - * text and embedded controls. - * - * @param {Object} node - A DOM node - * - * @return {String} The text content for an accessible name or description - */ - function getNodeContents (node) { - let contents = ''; - let nc; - let arr = []; - - switch (node.nodeType) { - case Node.ELEMENT_NODE: - // If aria-label is present, node recursion stops and - // aria-label value is returned - if (node.hasAttribute('aria-label')) { - if (includeContentInName(node)) { - contents = node.getAttribute('aria-label'); - } - } - else { - if (node instanceof HTMLSlotElement) { - // if no slotted elements, check for default slotted content - const assignedNodes = node.assignedNodes().length ? node.assignedNodes() : node.assignedNodes({ flatten: true }); - assignedNodes.forEach( assignedNode => { - nc = getNodeContents(assignedNode); - if (nc.length) arr.push(nc); - }); - contents = (arr.length) ? arr.join(' ') : ''; - } else { - if (couldHaveAltText(node) && includeContentInName(node)) { - contents = getAttributeValue(node, 'alt'); - } - else { - if (node.hasChildNodes()) { - let children = Array.from(node.childNodes); - children.forEach( child => { - nc = getNodeContents(child); - if (nc.length) arr.push(nc); - }); - contents = (arr.length) ? arr.join(' ') : ''; - } - } - // For all branches of the ELEMENT_NODE case... - } - } - contents = addCssGeneratedContent(node, contents); - break; + /* + * @method getAdjustedRect + * + * @desc Returns a object with dimensions adjusted for highlighting element + * + * @param {Object} elem - DOM node of element to be highlighted + * @param {Number} offset - Number of pixels for offset + * @param {Number} borderWidth - Number of pixels for border width + * @param {Number} borderContrast - Number of pixels to provide border contrast + * + * @returns see @desc + */ - case Node.TEXT_NODE: - if (includeContentInName(node)) { - contents = normalize(node.textContent); - } - break; - } + getAdjustedRect(elem, offset, borderWidth, borderContrast) { - return contents; - } + const rect = elem.getBoundingClientRect(); - /* - * @function couldHaveAltText - * - * @desc Based on HTML5 specification, returns true if - * the element could have an 'alt' attribute, - * otherwise false. - * - * @param {Object} element - DOM eleemnt node - * - * @return {Boolean} see @desc - */ - function couldHaveAltText (element) { - let tagName = element.tagName.toLowerCase(); + const adjRect = { + left: 0, + top: 0, + width: 0, + height: 0 + }; - switch (tagName) { - case 'img': - case 'area': - return true; - case 'input': - return (element.type && element.type === 'image'); - } + const offsetBorder = offset + borderWidth + 2 * borderContrast; - return false; - } + adjRect.left = rect.left > offset ? + Math.round(rect.left + (-1 * offsetBorder) + window.scrollX) : + Math.round(rect.left + window.scrollX); - /* - * @function addCssGeneratedContent - * - * @desc Adds CSS-generated content for pseudo-elements - * :before and :after. According to the CSS spec, test that content - * value is other than the default computed value of 'none'. - * - * Note: Even if an author specifies content: 'none', because browsers - * add the double-quote character to the beginning and end of - * computed string values, the result cannot and will not be - * equal to 'none'. - * - * - * @param {Object} element - DOM node element - * @param {String} contents - Text content for DOM node - * - * @returns {String} see @desc - * - */ - function addCssGeneratedContent (element, contents) { + adjRect.width = rect.left > offset ? + Math.max(rect.width + (2 * offsetBorder), minWidth) : + Math.max(rect.width, minWidth); - let result = contents, - prefix = getComputedStyle(element, ':before').content, - suffix = getComputedStyle(element, ':after').content; - if ((prefix[0] === '"') && !prefix.toLowerCase().includes('moz-')) { - result = prefix.substring(1, (prefix.length-1)) + result; - } + adjRect.top = rect.top > offset ? + Math.round(rect.top + (-1 * offsetBorder) + window.scrollY) : + Math.round(rect.top + window.scrollY); + + adjRect.height = rect.top > offset ? + Math.max(rect.height + (2 * offsetBorder), minHeight) : + Math.max(rect.height, minHeight); + + if ((adjRect.top < 0) || (adjRect.left < 0)) { + // Element is near top or left side of screen + adjRect.left = this.offset; + adjRect.top = this.offset; + } - if ((suffix[0] === '"') && !suffix.toLowerCase().includes('moz-')) { - result = result + suffix.substring(1, (suffix.length-1)) ; + return adjRect; } - return result; - } + /* + * @method isElementInViewport + * + * @desc Returns true if element is already visible in view port, + * otheriwse false + * + * @param {Object} elem : DOM node of element to highlight + * + * @returns see @desc + */ - /* accName.js */ + isElementInViewport(elem) { + const rect = elem.getBoundingClientRect(); + return ( + rect.top >= window.screenY && + rect.left >= window.screenX && + rect.bottom <= ((window.screenY + window.innerHeight) || + (window.screenY + document.documentElement.clientHeight)) && + rect.right <= ((window.screenX + window.innerWidth) || + (window.screenX + document.documentElement.clientWidth))); + } - /* Constants */ - const debug$6 = new DebugLogging('accName', false); - debug$6.flag = false; + /* + * @method isElementStartInViewport + * + * @desc Returns true if start of the element is already visible in view port, + * otherwise false + * + * @param {Object} elem : DOM node of element to highlight + * + * @returns see @desc + */ - /** - * @fuction getAccessibleName - * - * @desc Returns the accessible name for an heading or landamrk - * - * @paramn {Object} dom - Document of the current element - * @param {node} element - DOM element node for either a heading or - * landmark - * @param {Boolean} fromContent - if true will compute name from content - * - * @return {String} The accessible name for the landmark or heading element - */ + isElementStartInViewport(elem) { + const rect = elem.getBoundingClientRect(); + return ( + rect.top >= window.screenY && + rect.top <= ((window.screenY + window.innerHeight) || + (window.screenY + document.documentElement.clientHeight)) && + rect.left >= window.screenX && + rect.left <= ((window.screenX + window.innerWidth) || + (window.screenX + document.documentElement.clientWidth))); + } - function getAccessibleName (doc, element, fromContent=false) { - let accName = ''; - accName = nameFromAttributeIdRefs(doc, element, 'aria-labelledby'); + /* + * @method isElementHeightLarge + * + * @desc Returns true if element client height is larger than clientHeight, + * otheriwse false + * + * @param {Object} elem : DOM node of element to highlight + * + * @returns see @desc + */ - if (accName === '' && element.hasAttribute('aria-label')) { - accName = element.getAttribute('aria-label').trim(); + isElementInHeightLarge(elem) { + var rect = elem.getBoundingClientRect(); + return (1.2 * rect.height) > (window.innerHeight || document.documentElement.clientHeight); } - if (accName === '' && fromContent) { - accName = getNodeContents(element); + /* + * @method isElementHidden + * + * @desc Returns true if the element is hidden on the + * graphical rendering + * + * @param {Object} elem : DOM node + * + * @returns see @desc + */ + isElementHidden(elem) { + const rect = elem.getBoundingClientRect(); + return (rect.height < 3) || + (rect.width < 3) || + ((rect.left + rect.width) < (rect.width / 2)) || + ((rect.top + rect.height) < (rect.height / 2)); } - if (accName === '' && element.title.trim() !== '') { - accName = element.title.trim(); - } + /* + * @method getHiddenMessage + * + * @desc Returns string describing the hidden element + * + * @param {Object} elem : DOM node + * + * @returns see @desc + */ + getHiddenMessage(elem) { + if (elem.hasAttribute('data-skip-to-info')) { + const info = elem.getAttribute('data-skip-to-info'); - return accName; - } + if (info.includes('heading')) { + return this.msgHeadingIsHidden; + } - /* - * @function nameFromAttributeIdRefs - * - * @desc Get the value of attrName on element (a space- - * separated list of IDREFs), visit each referenced element in the order it - * appears in the list and obtain its accessible name (skipping recursive - * aria-labelledby or aria-describedby calculations), and return an object - * with name property set to a string that is a space-separated concatena- - * tion of those results if any, otherwise return empty string. - * - * @param {Object} doc - Browser document object - * @param {Object} element - DOM element node - * @param {String} attribute - Attribute name (e.g. "aria-labelledby", "aria-describedby", - * or "aria-errormessage") - * - * @returns {String} see @desc - */ - function nameFromAttributeIdRefs (doc, element, attribute) { - const value = getAttributeValue(element, attribute); - const arr = []; - - if (value.length) { - const idRefs = value.split(' '); - - for (let i = 0; i < idRefs.length; i++) { - const refElement = doc.getElementById(idRefs[i]); - if (refElement) { - const accName = getNodeContents(refElement); - if (accName && accName.length) arr.push(accName); + if (info.includes('landmark')) { + return this.msgRegionIsHidden; } } - } - if (arr.length) { - return arr.join(' '); + return this.msgElementIsHidden; } - return ''; - } + /* + * @method removeHighlight + * + * @desc Hides the highlight element on the page + */ + removeHighlight() { + if (this.overlayElem) { + this.overlayElem.style.display = 'none'; + } + } + } + + /* shortcutsMessage.js */ + + /* Constants */ + const debug$8 = new DebugLogging('[shortcutsMessage]', false); + debug$8.flag = false; + + const defaultStyleOptions$1 = colorThemes['default']; + + const styleTemplate = document.createElement('template'); + styleTemplate.textContent = ` +/* shortcutsMessage.css */ +:root { + color-scheme: light dark; +} + +#${MESSAGE_ID} { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%,-50%); + + font-family: $fontFamily; + font-size: $fontSize; + max-width: 70%; + margin: 0; + padding: 0; + background-color: light-dark($dialogBackgroundColor, $dialogBackgroundDarkColor); + border: 2px solid light-dark($focusBorderColor, $focusBorderDarkColor); + border-radius: 5px; + color: light-dark($dialogTextColor, $dialogTextDarkColor); + z-index: 2000001; + opacity: 1; +} + +#${MESSAGE_ID} .header { + margin: 0; + padding: 4px; + border-bottom: 1px solid light-dark($focusBorderColor, $focusBorderDarkColor); + border-top-left-radius: 5px; + border-top-right-radius: 5px; + font-weight: bold; + background-color: light-dark($dialogBackgroundTitleColor, $dialogBackgroundTitleDarkColor); + color light-dark($dialogTextColor, $dialogTextDarkColor); + font-size: 100%; +} + +#${MESSAGE_ID} .content { + margin-left: 2em; + margin-right: 2em; + margin-top: 2em; + margin-bottom: 2em; + background-color: light-dark($dialogBackgroundColor, $dialogBackgroundDarkColor); + color: light-dark($dialogTextColor, $dialogTextDarkColor); + font-size: 110%; + text-algin: center; +} + +#${MESSAGE_ID}.hidden { + display: none; +} + +#${MESSAGE_ID}.show { + display: block; + opacity: 1; +} + +#${MESSAGE_ID}.fade { + opacity: 0; + transition: visibility 0s 1s, opacity 1s linear; +} + +@media (forced-colors: active) { + + #${MESSAGE_ID} { + background-color: Canvas; + color CanvasText; + border-color: AccentColor; + } + + #${MESSAGE_ID} .header { + background-color: Canvas; + color CanvasText; + } + + #${MESSAGE_ID} .content { + background-color: Canvas; + color: CanvasText; + } +} + +`; + + class ShortcutsMessage extends HTMLElement { + constructor () { + + super(); + this.attachShadow({ mode: 'open' }); + + // Get references + + this.messageDialog = document.createElement('div'); + this.messageDialog.id = MESSAGE_ID; + this.messageDialog.classList.add('hidden'); + this.shadowRoot.appendChild(this.messageDialog); + + const headerElem = document.createElement('div'); + headerElem.className = 'header'; + headerElem.textContent = 'SkipTo.js Message'; + this.messageDialog.appendChild(headerElem); + + this.contentElem = document.createElement('div'); + this.contentElem.className = 'content'; + this.messageDialog.appendChild(this.contentElem); + + this.timeoutShowID = false; + this.timeoutFadeID = false; + + } + + configureStyle(config={}) { + + function updateOption(style, option, configOption, defaultOption) { + if (configOption) { + return style.replaceAll(option, configOption); + } + else { + return style.replaceAll(option, defaultOption); + } + } + + // make a copy of the template + let style = styleTemplate.textContent.slice(0); + + style = updateOption(style, + '$fontFamily', + config.fontFamily, + defaultStyleOptions$1.fontFamily); + + style = updateOption(style, + '$fontSize', + config.fontSize, + defaultStyleOptions$1.fontSize); + + style = updateOption(style, + '$focusBorderColor', + config.focusBorderColor, + defaultStyleOptions$1.focusBorderColor); + + style = updateOption(style, + '$focusBorderDarkColor', + config.focusBorderDarkColor, + defaultStyleOptions$1.focusBorderDarkColor); + + + style = updateOption(style, + '$dialogTextColor', + config.dialogTextColor, + defaultStyleOptions$1.dialogTextColor); + + style = updateOption(style, + '$dialogTextDarkColor', + config.dialogTextDarkColor, + defaultStyleOptions$1.dialogTextDarkColor); + + style = updateOption(style, + '$dialogBackgroundColor', + config.dialogBackgroundColor, + defaultStyleOptions$1.dialogBackgroundColor); + + style = updateOption(style, + '$dialogBackgroundDarkColor', + config.dialogBackgroundDarkColor, + defaultStyleOptions$1.dialogBackgroundDarkColor); + + style = updateOption(style, + '$dialogBackgroundTitleColor', + config.dialogBackgroundTitleColor, + defaultStyleOptions$1.dialogBackgroundTitleColor); + + style = updateOption(style, + '$dialogBackgroundTitleDarkColor', + config.dialogBackgroundTitleDarkColor, + defaultStyleOptions$1.dialogBackgroundTitleDarkColor); + + let styleNode = this.shadowRoot.querySelector('style'); + + if (styleNode) { + styleNode.remove(); + } + + styleNode = document.createElement('style'); + styleNode.textContent = style; + this.shadowRoot.appendChild(styleNode); + + } + + close() { + this.messageDialog.classList.remove('show'); + this.messageDialog.classList.remove('fade'); + this.messageDialog.classList.add('hidden'); + } + + open(message) { + clearInterval(this.timeoutFadeID); + clearInterval(this.timeoutShowID); + this.messageDialog.classList.remove('hidden'); + this.messageDialog.classList.remove('fade'); + this.messageDialog.classList.add('show'); + this.contentElem.textContent = message; + + const msg = this; + + this.timeoutFadeID = setTimeout( () => { + msg.messageDialog.classList.add('fade'); + msg.messageDialog.classList.remove('show'); + }, 3000); + + this.timeoutShowID = setTimeout( () => { + msg.close(); + }, 4000); + + } + + } + + /* + * namefrom.js + */ + + /* constants */ + + const debug$7 = new DebugLogging('nameFrom', false); + debug$7.flag = false; + + // + // LOW-LEVEL HELPER FUNCTIONS (NOT EXPORTED) + + /* + * @function isDisplayNone + * + * @desc Returns true if the element or parent element has set the CSS + * display property to none or has the hidden attribute, + * otherwise false + * + * @param {Object} node - a DOM node + * + * @returns {Boolean} see @desc + */ + + function isDisplayNone (node) { + + if (!node) { + return false; + } + + if (node.nodeType === Node.TEXT_NODE) { + node = node.parentNode; + } + + if (node.nodeType === Node.ELEMENT_NODE) { + + if (node.hasAttribute('hidden')) { + return true; + } + + // aria-hidden attribute with the value "true" is an same as + // setting the hidden attribute for name calcuation + if (node.hasAttribute('aria-hidden')) { + if (node.getAttribute('aria-hidden').toLowerCase() === 'true') { + return true; + } + } + + const style = window.getComputedStyle(node, null); + + const display = style.getPropertyValue("display"); + + if (display) { + return display === 'none'; + } + } + return false; + } + + /* + * @function isVisibilityHidden + * + * @desc Returns true if the node (or it's parrent) has the CSS visibility + * property set to "hidden" or "collapse", otherwise false + * + * @param {Object} node - DOM node + * + * @return see @desc + */ + + function isVisibilityHidden(node) { + + if (!node) { + return false; + } + + if (node.nodeType === Node.TEXT_NODE) { + node = node.parentNode; + } + + if (node.nodeType === Node.ELEMENT_NODE) { + const style = window.getComputedStyle(node, null); + + const visibility = style.getPropertyValue("visibility"); + if (visibility) { + return (visibility === 'hidden') || (visibility === 'collapse'); + } + } + return false; + } + + /* + * @function isAriaHiddenFalse + * + * @desc Returns true if the node has the aria-hidden property set to + * "false", otherwise false. + * NOTE: This function is important in the accessible namce + * calculation, since content hidden with a CSS technique + * can be included in the accessible name calculation when + * aria-hidden is set to false + * + * @param {Object} node - DOM node + * + * @return see @desc + */ + + function isAriaHIddenFalse(node) { + + if (!node) { + return false; + } + + if (node.nodeType === Node.TEXT_NODE) { + node = node.parentNode; + } + + if (node.nodeType === Node.ELEMENT_NODE) { + return (node.hasAttribute('aria-hidden') && + (node.getAttribute('aria-hidden').toLowerCase() === 'false')); + } + + return false; + } + + /* + * @function includeContentInName + * + * @desc Checks the CSS display and hidden properties, and + * the aria-hidden property to see if the content + * should be included in the accessible name + * calculation. Returns true if it should be + * included, otherwise false + * + * @param {Object} node - DOM node + * + * @return see @desc + */ + + function includeContentInName(node) { + const flag = isAriaHIddenFalse(node) || + (!isVisibilityHidden(node) && + !isDisplayNone(node)); + return flag; + } + + /* + * @function getNodeContents + * + * @desc Recursively process element and text nodes by aggregating + * their text values for an ARIA accessible name or description + * calculation. + * + * NOTE: This includes special handling of elements with 'alt' + * text and embedded controls. + * + * @param {Object} node - A DOM node + * + * @return {String} The text content for an accessible name or description + */ + function getNodeContents (node) { + let contents = ''; + let nc; + let arr = []; + + switch (node.nodeType) { + case Node.ELEMENT_NODE: + // If aria-label is present, node recursion stops and + // aria-label value is returned + if (node.hasAttribute('aria-label')) { + if (includeContentInName(node)) { + contents = node.getAttribute('aria-label'); + } + } + else { + if (node instanceof HTMLSlotElement) { + // if no slotted elements, check for default slotted content + const assignedNodes = node.assignedNodes().length ? node.assignedNodes() : node.assignedNodes({ flatten: true }); + assignedNodes.forEach( assignedNode => { + nc = getNodeContents(assignedNode); + if (nc.length) arr.push(nc); + }); + contents = (arr.length) ? arr.join(' ') : ''; + } else { + if (couldHaveAltText(node) && includeContentInName(node)) { + contents = getAttributeValue(node, 'alt'); + } + else { + if (node.hasChildNodes()) { + let children = Array.from(node.childNodes); + children.forEach( child => { + nc = getNodeContents(child); + if (nc.length) arr.push(nc); + }); + contents = (arr.length) ? arr.join(' ') : ''; + } + } + // For all branches of the ELEMENT_NODE case... + } + } + contents = addCssGeneratedContent(node, contents); + break; + + case Node.TEXT_NODE: + if (includeContentInName(node)) { + contents = normalize(node.textContent); + } + break; + } + + return contents; + } + + /* + * @function couldHaveAltText + * + * @desc Based on HTML5 specification, returns true if + * the element could have an 'alt' attribute, + * otherwise false. + * + * @param {Object} element - DOM eleemnt node + * + * @return {Boolean} see @desc + */ + function couldHaveAltText (element) { + let tagName = element.tagName.toLowerCase(); + + switch (tagName) { + case 'img': + case 'area': + return true; + case 'input': + return (element.type && element.type === 'image'); + } + + return false; + } + + /* + * @function addCssGeneratedContent + * + * @desc Adds CSS-generated content for pseudo-elements + * :before and :after. According to the CSS spec, test that content + * value is other than the default computed value of 'none'. + * + * Note: Even if an author specifies content: 'none', because browsers + * add the double-quote character to the beginning and end of + * computed string values, the result cannot and will not be + * equal to 'none'. + * + * + * @param {Object} element - DOM node element + * @param {String} contents - Text content for DOM node + * + * @returns {String} see @desc + * + */ + + function addCssGeneratedContent (element, contents) { + + function isVisible (style) { + + let flag = true; + + const display = style.getPropertyValue("display"); + if (display) { + flag = flag && display !== 'none'; + } + + const visibility = style.getPropertyValue("visibility"); + if (visibility) { + flag = flag && (visibility !== 'hidden') && (visibility !== 'collapse'); + } + return flag; + } + + let result = contents; + const styleBefore = getComputedStyle(element, ':before'); + const styleAfter = getComputedStyle(element, ':after'); + + const beforeVisible = isVisible(styleBefore); + const afterVisible = isVisible(styleAfter); + + const prefix = beforeVisible ? + styleBefore.content : + ''; + + const suffix = afterVisible ? + styleAfter.content : + ''; + + if ((prefix[0] === '"') && !prefix.toLowerCase().includes('moz-')) { + result = prefix.substring(1, (prefix.length-1)) + result; + } + + if ((suffix[0] === '"') && !suffix.toLowerCase().includes('moz-')) { + result = result + suffix.substring(1, (suffix.length-1)) ; + } + + return result; + } + + /* accName.js */ + + /* Constants */ + const debug$6 = new DebugLogging('accName', false); + debug$6.flag = false; + + /** + * @fuction getAccessibleName + * + * @desc Returns the accessible name for an heading or landamrk + * + * @paramn {Object} dom - Document of the current element + * @param {node} element - DOM element node for either a heading or + * landmark + * @param {Boolean} fromContent - if true will compute name from content + * + * @return {String} The accessible name for the landmark or heading element + */ + + function getAccessibleName (doc, element, fromContent=false) { + let accName = ''; + + accName = nameFromAttributeIdRefs(doc, element, 'aria-labelledby'); + + if (accName === '' && element.hasAttribute('aria-label')) { + accName = element.getAttribute('aria-label').trim(); + } + + if (accName === '' && fromContent) { + accName = getNodeContents(element); + } + + if (accName === '' && element.title.trim() !== '') { + accName = element.title.trim(); + } + + return accName; + } + + /* + * @function nameFromAttributeIdRefs + * + * @desc Get the value of attrName on element (a space- + * separated list of IDREFs), visit each referenced element in the order it + * appears in the list and obtain its accessible name (skipping recursive + * aria-labelledby or aria-describedby calculations), and return an object + * with name property set to a string that is a space-separated concatena- + * tion of those results if any, otherwise return empty string. + * + * @param {Object} doc - Browser document object + * @param {Object} element - DOM element node + * @param {String} attribute - Attribute name (e.g. "aria-labelledby", "aria-describedby", + * or "aria-errormessage") + * + * @returns {String} see @desc + */ + function nameFromAttributeIdRefs (doc, element, attribute) { + const value = getAttributeValue(element, attribute); + const arr = []; + + if (value.length) { + const idRefs = value.split(' '); + + for (let i = 0; i < idRefs.length; i++) { + const refElement = doc.getElementById(idRefs[i]); + if (refElement) { + const accName = getNodeContents(refElement); + if (accName && accName.length) arr.push(accName); + } + } + } + + if (arr.length) { + return arr.join(' '); + } + + return ''; + } + /* landmarksHeadings.js */ /* Constants */ const debug$5 = new DebugLogging('landmarksHeadings', false); debug$5.flag = false; + const skipableElements = [ 'base', 'content', @@ -2659,7 +2692,10 @@ button:hover { 'style', 'template', 'shadow', - 'title' + 'title', + PAGE_SCRIPT_ELEMENT_NAME, + BOOKMARKLET_ELEMENT_NAME, + EXTENSION_ELEMENT_NAME ]; const allowedLandmarkSelectors = [ @@ -3278,7 +3314,7 @@ button:hover { // If targets undefined, use default settings if (typeof headingTargets !== 'string') { console.warn(`[skipto.js]: Error in heading configuration`); - headingTargets = 'main-only h1 h2'; + headingTargets = 'h1 h2'; } const [landmarks, headings] = queryDOMForLandmarksAndHeadings(landmarkTargets, headingTargets, skiptoId); @@ -3307,7 +3343,8 @@ button:hover { if ((typeof role === 'string') && ((role === 'presentation') || role === 'none') ) continue; - if (isVisible(heading.node) && isNotEmptyString(heading.node.textContent)) { + if (isVisible(heading.node) && + isNotEmptyString(heading.node.textContent)) { if (heading.node.hasAttribute('data-skip-to-id')) { dataId = heading.node.getAttribute('data-skip-to-id'); } else { @@ -3425,6 +3462,32 @@ button:hover { return targetLandmarks; } + /* + * @function checkForName + * + * @desc Removes landmark objects without an accessible name if array is longer + * than accessible name count constant + * + * @param {Array} landmarks - Array of landmark objects + * + * @returns {Array} Array of landmark objects + */ + function checkForName (landmarks) { + + let namedLandmarks = []; + + if (landmarks.length > REQUIRE_ACCESSIBLE_NAME_COUNT) { + + landmarks.forEach( (l) => { + if (l.hasName) { + namedLandmarks.push(l); + } + }); + return namedLandmarks; + } + + return landmarks; + } /* * @function getLandmarks @@ -3550,9 +3613,21 @@ button:hover { if (config.landmarks.includes('doc-order')) { return allElements; } - return [].concat(mainElements, searchElements, navElements, asideElements, regionElements, footerElements, headerElements, otherElements); - } + // if (config.excludeHiddenHeadings) { + // } + if (config.showLandmarksWithoutNames === 'false') { + asideElements = checkForName(asideElements); + navElements = checkForName(navElements); + searchElements = checkForName(searchElements); + headerElements = checkForName(headerElements); + footerElements = checkForName(footerElements); + + } + + return [].concat(mainElements, searchElements, navElements, asideElements, regionElements, footerElements, headerElements, otherElements); + } + /* shortcuts.js */ /* Constants */ @@ -3785,141 +3860,141 @@ button:hover { elem = elem.shadowRoot.activeElement; } return elem; - } - - /* keyboardHelper.js */ - - /* Constants */ - const debug$3 = new DebugLogging('[kbdHelpers]', false); - debug$3.flag = false; - - /* - * @method isInteractiveElement - * - * @desc Returns true if the element can use key presses, otherwise false - * - * @param {object} elem - DOM node element - * - * @returns {Boolean} see @desc - */ - - function elementTakesText (elem) { - - const enabledInputTypes = [ - 'button', - 'checkbox', - 'color', - 'image', - 'radio', - 'range', - 'reset', - 'submit' - ]; - - const tagName = elem.tagName ? elem.tagName.toLowerCase() : ''; - const type = tagName === 'input' ? - (elem.type ? elem.type.toLowerCase() : 'text') : - ''; - - debug$3.flag && debug$3.log(`[elementTakesText][type]: ${type} (${enabledInputTypes.includes(type)})`); - - return (tagName === 'select') || - (tagName === 'textarea') || - ((tagName === 'input') && - !enabledInputTypes.includes(type)) || - inContentEditable(elem); - } - - /* - * @function inContentEditable - * - * @desc Returns false if node is not in a content editable element, - * otherwise true if it does - * - * @param {Object} elem - DOM node - * - * @returns {Boolean} see @desc - */ - function inContentEditable (elem) { - let n = elem; - while (n.hasAttribute) { - if (n.hasAttribute('contenteditable') && - (n.getAttribute('contenteditable').toLowerCase().trim() !== 'false')) { - return true; - } - n = n.parentNode; - } - return false; - } - - /* - * @function noModifierPressed - * - * @desc Returns true if no modifier key is pressed, other false - * - * @param {Object} event - Event object - * - * @returns {Boolean} see @desc - */ - - function noModifierPressed (event) { - return !event.altKey && - !event.ctrlKey && - !event.shiftKey && - !event.metaKey; - } - - /* - * @function onlyShiftPressed - * - * @desc Returns true if only the shift modifier key is pressed, other false - * - * @param {Object} event - Event object - * - * @returns {Boolean} see @desc - */ - - function onlyShiftPressed (event) { - return !event.altKey && - !event.ctrlKey && - event.shiftKey && - !event.metaKey; - } - - /* - * @function onlyAltPressed - * - * @desc Returns true if only the alt modifier key is pressed, other false - * - * @param {Object} event - Event object - * - * @returns {Boolean} see @desc - */ - - function onlyAltPressed (event) { - return event.altKey && - !event.ctrlKey && - !event.shiftKey && - !event.metaKey; - } - - /* - * @function onlyOptionPressed - * - * @desc Returns true if only the option modifier key is pressed, other false - * - * @param {Object} event - Event object - * - * @returns {Boolean} see @desc - */ - - function onlyOptionPressed (event) { - return event.altKey && - !event.ctrlKey && - !event.shiftKey && - !event.metaKey; - } - + } + + /* keyboardHelper.js */ + + /* Constants */ + const debug$3 = new DebugLogging('[kbdHelpers]', false); + debug$3.flag = false; + + /* + * @method isInteractiveElement + * + * @desc Returns true if the element can use key presses, otherwise false + * + * @param {object} elem - DOM node element + * + * @returns {Boolean} see @desc + */ + + function elementTakesText (elem) { + + const enabledInputTypes = [ + 'button', + 'checkbox', + 'color', + 'image', + 'radio', + 'range', + 'reset', + 'submit' + ]; + + const tagName = elem.tagName ? elem.tagName.toLowerCase() : ''; + const type = tagName === 'input' ? + (elem.type ? elem.type.toLowerCase() : 'text') : + ''; + + debug$3.flag && debug$3.log(`[elementTakesText][type]: ${type} (${enabledInputTypes.includes(type)})`); + + return (tagName === 'select') || + (tagName === 'textarea') || + ((tagName === 'input') && + !enabledInputTypes.includes(type)) || + inContentEditable(elem); + } + + /* + * @function inContentEditable + * + * @desc Returns false if node is not in a content editable element, + * otherwise true if it does + * + * @param {Object} elem - DOM node + * + * @returns {Boolean} see @desc + */ + function inContentEditable (elem) { + let n = elem; + while (n.hasAttribute) { + if (n.hasAttribute('contenteditable') && + (n.getAttribute('contenteditable').toLowerCase().trim() !== 'false')) { + return true; + } + n = n.parentNode; + } + return false; + } + + /* + * @function noModifierPressed + * + * @desc Returns true if no modifier key is pressed, other false + * + * @param {Object} event - Event object + * + * @returns {Boolean} see @desc + */ + + function noModifierPressed (event) { + return !event.altKey && + !event.ctrlKey && + !event.shiftKey && + !event.metaKey; + } + + /* + * @function onlyShiftPressed + * + * @desc Returns true if only the shift modifier key is pressed, other false + * + * @param {Object} event - Event object + * + * @returns {Boolean} see @desc + */ + + function onlyShiftPressed (event) { + return !event.altKey && + !event.ctrlKey && + event.shiftKey && + !event.metaKey; + } + + /* + * @function onlyAltPressed + * + * @desc Returns true if only the alt modifier key is pressed, other false + * + * @param {Object} event - Event object + * + * @returns {Boolean} see @desc + */ + + function onlyAltPressed (event) { + return event.altKey && + !event.ctrlKey && + !event.shiftKey && + !event.metaKey; + } + + /* + * @function onlyOptionPressed + * + * @desc Returns true if only the option modifier key is pressed, other false + * + * @param {Object} event - Event object + * + * @returns {Boolean} see @desc + */ + + function onlyOptionPressed (event) { + return event.altKey && + !event.ctrlKey && + !event.shiftKey && + !event.metaKey; + } + /* skiptoMenuButton.js */ /* Constants */ @@ -4452,19 +4527,31 @@ button:hover { // Update list of menuitems this.updateMenuitems(); - // Are all headings in the main region - const allInMain = headingElements.length > 0 ? - headingElements.reduce( (flag, item) => { - return flag && item.inMain; - }, true) : - false; - this.landmarkGroupLabelNode.textContent = this.addNumberToGroupLabel(config.landmarkGroupLabel, landmarkElements.length); - if (config.headings.includes('main') && allInMain) { + if (landmarkElements.length === 1) { + this.landmarkGroupLabelNode.setAttribute('aria-label', config.landmarkOneGroupLabel); + } + else { + this.landmarkGroupLabelNode.setAttribute('aria-label', `${landmarkElements.length} ${config.landmarkGroupLabel}`); + } + + if (config.headings.includes('main')) { this.headingGroupLabelNode.textContent = this.addNumberToGroupLabel(config.headingMainGroupLabel, headingElements.length); + if (headingElements.length === 1) { + this.headingGroupLabelNode.setAttribute('aria-label', config.headingOneMainGroupLabel); + } + else { + this.headingGroupLabelNode.setAttribute('aria-label', `${headingElements.length} ${config.headingMainGroupLabel}`); + } } else { this.headingGroupLabelNode.textContent = this.addNumberToGroupLabel(config.headingGroupLabel, headingElements.length); + if (headingElements.length === 1) { + this.headingGroupLabelNode.setAttribute('aria-label', config.headingOneGroupLabel); + } + else { + this.headingGroupLabelNode.setAttribute('aria-label', `${headingElements.length} ${config.headingGroupLabel}`); + } } } @@ -5299,8 +5386,8 @@ button:hover { this.closePopup(); } } - } - + } + /* skiptoContent.js */ /* constants */ @@ -5309,17 +5396,17 @@ button:hover { const defaultStyleOptions = colorThemes['default']; - /* @class SkipToContent575 + /* @class SkipToContent580 * */ - class SkipToContent575 extends HTMLElement { + class SkipToContent580 extends HTMLElement { constructor() { // Always call super first in constructor super(); this.attachShadow({ mode: 'open' }); - this.version = "5.7.5"; + this.version = "5.8.0"; this.buttonSkipTo = false; this.initialized = false; @@ -5329,6 +5416,10 @@ button:hover { enableHeadingLevelShortcuts: true, lightDarkSupported: 'false', + // Content options + + showLandmarksWithoutNames: 'false', + focusOption: 'none', // used by extensions only // Customization of button and menu @@ -5418,7 +5509,9 @@ button:hover { menuLabel: 'Landmarks and Headings', landmarkGroupLabel: 'Landmark Regions', headingGroupLabel: 'Headings', + headingOneGroupLabel: 'One Heading', headingMainGroupLabel: 'Headings in Main Region', + headingOneMainGroupLabel: 'One Heading in Main Region', headingLevelLabel: 'Heading level', mainLabel: 'main', searchLabel: 'search', @@ -5711,8 +5804,8 @@ button:hover { this.config.shortcuts = 'disabled'; } } - } - + } + /* skipto.js */ /* constants */ @@ -5816,7 +5909,7 @@ button:hover { if (!isExtensionLoaded) { if (!isBookmarkletLoaded) { removePageSkipTo(); - window.customElements.define(BOOKMARKLET_ELEMENT_NAME, SkipToContent575); + window.customElements.define(BOOKMARKLET_ELEMENT_NAME, SkipToContent580); skipToContentElem = document.createElement(BOOKMARKLET_ELEMENT_NAME); skipToContentElem.setAttribute('version', skipToContentElem.version); skipToContentElem.setAttribute('type', type); @@ -5832,7 +5925,7 @@ button:hover { if (!isExtensionLoaded) { removePageSkipTo(); removeBookmarkletSkipTo(); - window.customElements.define(EXTENSION_ELEMENT_NAME, SkipToContent575); + window.customElements.define(EXTENSION_ELEMENT_NAME, SkipToContent580); skipToContentElem = document.createElement(EXTENSION_ELEMENT_NAME); skipToContentElem.setAttribute('version', skipToContentElem.version); skipToContentElem.setAttribute('type', type); @@ -5845,7 +5938,7 @@ button:hover { default: if (!isPageLoaded && !isBookmarkletLoaded && !isExtensionLoaded) { - window.customElements.define(PAGE_SCRIPT_ELEMENT_NAME, SkipToContent575); + window.customElements.define(PAGE_SCRIPT_ELEMENT_NAME, SkipToContent580); skipToContentElem = document.createElement(PAGE_SCRIPT_ELEMENT_NAME); skipToContentElem.setAttribute('version', skipToContentElem.version); skipToContentElem.setAttribute('type', type); @@ -5897,6 +5990,6 @@ button:hover { }); } } - })(); - -})(); + })(); + +})(); From 99e0a87e5399a93c318611868abe964fda20428c Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Sun, 13 Jul 2025 16:06:22 -0500 Subject: [PATCH 08/38] udpate skipto.js to 5.8.0 --- content/shared/js/skipto.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/content/shared/js/skipto.js b/content/shared/js/skipto.js index 306486c496..5020a5b977 100644 --- a/content/shared/js/skipto.js +++ b/content/shared/js/skipto.js @@ -2468,7 +2468,7 @@ button:hover { else { if (node instanceof HTMLSlotElement) { // if no slotted elements, check for default slotted content - const assignedNodes = node.assignedNodes().length ? node.assignedNodes() : node.assignedNodes({ flatten: true }); + const assignedNodes = Array.from(node.assignedNodes({ flatten: true })); assignedNodes.forEach( assignedNode => { nc = getNodeContents(assignedNode); if (nc.length) arr.push(nc); @@ -2500,8 +2500,7 @@ button:hover { } break; } - - return contents; + return normalize(contents); } /* @@ -2602,7 +2601,7 @@ button:hover { /** * @fuction getAccessibleName * - * @desc Returns the accessible name for an heading or landamrk + * @desc Returns the accessible name for an heading or landmark * * @paramn {Object} dom - Document of the current element * @param {node} element - DOM element node for either a heading or @@ -3198,6 +3197,7 @@ button:hover { headingTags.includes(tagName) ? tagName.substring(1) : ''; + if (headingTags.includes(tagName) || (isHeadingRole && hasAriaLevel)) { const accName = getAccessibleName(doc, node, true); @@ -3344,7 +3344,7 @@ button:hover { ((role === 'presentation') || role === 'none') ) continue; if (isVisible(heading.node) && - isNotEmptyString(heading.node.textContent)) { + isNotEmptyString(heading.name)) { if (heading.node.hasAttribute('data-skip-to-id')) { dataId = heading.node.getAttribute('data-skip-to-id'); } else { @@ -4152,7 +4152,7 @@ button:hover { this.containerNode.addEventListener('focusin', this.handleFocusin.bind(this)); this.containerNode.addEventListener('focusout', this.handleFocusout.bind(this)); - this.containerNode.addEventListener('pointerdown', this.handleContinerPointerdown.bind(this), true); + this.containerNode.addEventListener('pointerdown', this.handleContainerPointerdown.bind(this), true); document.documentElement.addEventListener('pointerdown', this.handleBodyPointerdown.bind(this), true); if (this.usesAltKey || this.usesOptionKey) { @@ -5298,7 +5298,7 @@ button:hover { event.preventDefault(); } - handleContinerPointerdown(event) { + handleContainerPointerdown(event) { debug$2.flag && debug$2.log(`[down]: target: ${event.pointerId}`); if (this.isOverButton(event.clientX, event.clientY)) { @@ -5306,8 +5306,8 @@ button:hover { } else { this.containerNode.setPointerCapture(event.pointerId); - this.containerNode.addEventListener('pointermove', this.handleContinerPointermove.bind(this)); - this.containerNode.addEventListener('pointerup', this.handleContinerPointerup.bind(this)); + this.containerNode.addEventListener('pointermove', this.handleContainerPointermove.bind(this)); + this.containerNode.addEventListener('pointerup', this.handleContainerPointerup.bind(this)); if (this.containerNode.contains(event.target)) { if (this.isOpen()) { @@ -5331,7 +5331,7 @@ button:hover { event.preventDefault(); } - handleContinerPointermove(event) { + handleContainerPointermove(event) { const mi = this.getMenuitem(event.clientX, event.clientY); if (mi) { this.removeHoverClass(mi); @@ -5349,11 +5349,11 @@ button:hover { event.preventDefault(); } - handleContinerPointerup(event) { + handleContainerPointerup(event) { this.containerNode.releasePointerCapture(event.pointerId); - this.containerNode.removeEventListener('pointermove', this.handleContinerPointermove); - this.containerNode.removeEventListener('pointerup', this.handleContinerPointerup); + this.containerNode.removeEventListener('pointermove', this.handleContainerPointermove); + this.containerNode.removeEventListener('pointerup', this.handleContainerPointerup); const mi = this.getMenuitem(event.clientX, event.clientY); const omb = this.isOverButton(event.clientX, event.clientY); From be5a84e69f14fa6ad647886127f6681eafa9eee3 Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Sun, 13 Jul 2025 19:17:39 -0500 Subject: [PATCH 09/38] fixed broken link --- content/patterns/landmarks/examples/resources.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/patterns/landmarks/examples/resources.html b/content/patterns/landmarks/examples/resources.html index 3162ff144b..9c104c20b5 100644 --- a/content/patterns/landmarks/examples/resources.html +++ b/content/patterns/landmarks/examples/resources.html @@ -67,7 +67,7 @@

Tools

  • SkipTo Landmarks & Headings: Page Script (used on this page)
  • Landmarks Browser Extension
  • Accessibility Bookmarklets: Landmark Bookmarklet
  • -
  • The Visual ARIA Bookmarklet
  • +
  • The Visual ARIA Bookmarklet
  • AInspector for Firefox (Landmark Rule Category)
  • From 9be5aa4c22e1676bb64a6a22eb1f9bd533ae358a Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Tue, 5 Aug 2025 06:32:41 -0500 Subject: [PATCH 10/38] updated to version 5.8.1 --- content/shared/js/skipto.js | 75 +++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 20 deletions(-) diff --git a/content/shared/js/skipto.js b/content/shared/js/skipto.js index 5020a5b977..019d8fd56b 100644 --- a/content/shared/js/skipto.js +++ b/content/shared/js/skipto.js @@ -1,5 +1,5 @@ /* ======================================================================== - * Version: 5.8.0 + * Version: 5.8.1 * Copyright (c) 2022, 2023, 2024, 2025 Jon Gunderson; Licensed BSD * Copyright (c) 2021 PayPal Accessibility Team and University of Illinois; Licensed BSD * All rights reserved. @@ -43,6 +43,7 @@ buttonTextDarkColor: '#ffffff', buttonBackgroundDarkColor: '#013c93', zIndex: '2000000', + z2Index: '20000002', zHighlight: '1999900', displayOption: 'fixed', highlightTarget: 'instant', @@ -327,7 +328,7 @@ border: none; margin-bottom: 4px; transition: left 1s ease; - z-index: $zIndex !important; + z-index: $z1Index !important; user-select: none; touch-action: none; } @@ -345,7 +346,7 @@ z-index: 100000 !important; font-family: $fontFamily; font-size: $fontSize; - z-index: $zIndex !important; + z-index: $z1Index !important; touch-action: none; } @@ -413,7 +414,7 @@ border-style: solid; border-color: light-dark($focusBorderColor, $focusBorderDarkColor); border-radius: 5px; - z-index: $zIndex !important; + z-index: $z1Index !important; touch-action: none; } @@ -444,9 +445,15 @@ grid-template-columns: repeat(6, 1.2rem) 1fr; grid-column-gap: 2px; font-size: 1em; - z-index: $zIndex !important; + z-index: $z1Index; } +#${SKIP_TO_ID} [role="menuitem"].shortcuts, +#${SKIP_TO_ID} [role="menuitem"].about { + z-index: $z2Index; +} + + #${SKIP_TO_ID} [role="menuitem"] .level, #${SKIP_TO_ID} [role="menuitem"] .label { font-size: 100%; @@ -526,7 +533,7 @@ border-bottom-color: light-dark($menuTextColor, $menuTextDarkColor); background-color: light-dark($menuBackgroundColor, $menuBackgroundColor); color: light-dark($menuTextColor, $menuTextDarkColor); - z-index: $zIndex !important; + z-index: $z1Index !important; } #${SKIP_TO_ID} [role="separator"] .mofn { @@ -615,7 +622,7 @@ border-bottom-color: ButtonBorder; background-color: ButtonFace; color: ButtonText; - z-index: $zIndex !important; + z-index: $z1Index !important; } #${SKIP_TO_ID} button:focus, @@ -804,7 +811,13 @@ cssMenu = updateStyle(cssMenu, '$buttonBackgroundColor', config.buttonBackgroundColor, theme.buttonBackgroundColor, defaultTheme.buttonBackgroundColor); cssMenu = updateStyle(cssMenu, '$buttonBackgroundDarkColor', config.buttonBackgroundDarkColor, theme.buttonBackgroundDarkColor, defaultTheme.buttonBackgroundDarkColor); - cssMenu = updateStyle(cssMenu, '$zIndex', config.zIndex, theme.zIndex, defaultTheme.zIndex); + cssMenu = updateStyle(cssMenu, '$z1Index', config.zIndex, theme.zIndex, defaultTheme.zIndex); + + const z2Index = config.zIndex ? + (parseInt(config.zIndex) + 1).toString() : + '2000002'; + + cssMenu = updateStyle(cssMenu, '$z2Index', z2Index, '', defaultTheme.z2Index); // Special case for theme configuration used in Illinois theme if (typeof theme.highlightTarget === 'string') { @@ -957,7 +970,7 @@ dialog#skip-to-info-dialog { border-color: light-dark($focusBorderColor, $focusBorderDarkColor); border-radius: 5px; z-index: 2000001; - + max-width: 350px; } dialog#skip-to-info-dialog .header { @@ -1001,16 +1014,27 @@ dialog#skip-to-info-dialog .content { } dialog#skip-to-info-dialog .content .desc { - max-width: 20em; + margin: 0.25em; + text-align: center; } -dialog#skip-to-info-dialog .content .happy { - margin-top: 0.5em; +dialog#skip-to-info-dialog .content .privacy-label { + margin: 0; + margin-top: 1em; text-align: center; - font-family: fantasy, cursive; - font-size: 1.25em; font-weight: bold; - font-style: italic; +} + +dialog#skip-to-info-dialog .content .privacy { + text-align: center; + margin-bottom: 1em; +} + + +dialog#skip-to-info-dialog .content .happy { + text-align: center; + font-family: 'Brush Script MT', cursive; + font-size: 200%; letter-spacing: 0.05em; } @@ -1019,8 +1043,6 @@ dialog#skip-to-info-dialog .content .version, dialog#skip-to-info-dialog .content .copyright { margin-top: 0.5em; text-align: center; - font-weight: bold; - font-size: 90%; } dialog#skip-to-info-dialog .content table { @@ -1358,9 +1380,20 @@ button:hover { let divElem = document.createElement('div'); divElem.className = 'desc'; + divElem.textContent = config.aboutDesc; this.contentElem.appendChild(divElem); + divElem = document.createElement('div'); + divElem.className = 'privacy-label'; + divElem.textContent = config.aboutPrivacyLabel; + this.contentElem.appendChild(divElem); + + divElem = document.createElement('div'); + divElem.className = 'privacy'; + divElem.textContent = config.aboutPrivacy; + this.contentElem.appendChild(divElem); + divElem = document.createElement('div'); divElem.className = 'happy'; divElem.textContent = config.aboutHappy; @@ -4391,7 +4424,7 @@ button:hover { const menuitemNode = document.createElement('div'); menuitemNode.setAttribute('role', 'menuitem'); menuitemNode.setAttribute('data-about-info', ''); - menuitemNode.className = 'skip-to-nav skip-to-nesting-level-0'; + menuitemNode.className = 'about skip-to-nav skip-to-nesting-level-0'; menuitemNode.tabIndex = -1; const labelNode = document.createElement('span'); @@ -5406,7 +5439,7 @@ button:hover { // Always call super first in constructor super(); this.attachShadow({ mode: 'open' }); - this.version = "5.8.0"; + this.version = "5.8.1"; this.buttonSkipTo = false; this.initialized = false; @@ -5469,7 +5502,9 @@ button:hover { aboutHappy: `Happy Skipping!`, aboutVersion: `Version ${this.version}`, aboutCopyright: 'BSD License, Copyright 2021-2025', - aboutDesc: 'SkipTo.js is a free and open source utility to support authors in implementing the WCAG 4.2.1 Bypass Block requirement on their websites.', + aboutDesc: 'SkipTo.js is a free and open source utility to support the WCAG 2.4.1 Bypass Block requirement. ', + aboutPrivacyLabel: 'Privacy', + aboutPrivacy: 'SkipTo.js does not collect or store any information about users or work with any other parties to collect or share user browsing information.', closeLabel: 'Close', moreInfoLabel: 'More Information', From 3112b86cd89f8df2e14a5139812697805186ba9f Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Tue, 5 Aug 2025 13:36:36 -0500 Subject: [PATCH 11/38] updated to latest version --- content/shared/js/skipto.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/content/shared/js/skipto.js b/content/shared/js/skipto.js index 019d8fd56b..b9511f0365 100644 --- a/content/shared/js/skipto.js +++ b/content/shared/js/skipto.js @@ -1,5 +1,5 @@ /* ======================================================================== - * Version: 5.8.1 + * Version: 5.8.2 * Copyright (c) 2022, 2023, 2024, 2025 Jon Gunderson; Licensed BSD * Copyright (c) 2021 PayPal Accessibility Team and University of Illinois; Licensed BSD * All rights reserved. @@ -4881,7 +4881,7 @@ button:hover { * @return {object} see @desc */ getMenuitem(x, y) { - for (let i = 0; i < this.menuitemNodes.length; i += 1) { + for (let i = (this.menuitemNodes.length - 1); i >= 0; i -= 1) { const node = this.menuitemNodes[i]; const rect = node.getBoundingClientRect(); @@ -5289,6 +5289,7 @@ button:hover { } handleMenuitemClick(event) { + debug$2.log(`[handleMenuitemClick]: ${event.currentTarget.textContent}`); this.handleMenuitemAction(event.currentTarget); event.stopPropagation(); event.preventDefault(); @@ -5324,7 +5325,6 @@ button:hover { } handleMenuitemPointerleave(event) { - debug$2.flag && debug$2.log(`[leave]`); let tgt = event.currentTarget; tgt.classList.remove('hover'); event.stopPropagation(); @@ -5439,7 +5439,7 @@ button:hover { // Always call super first in constructor super(); this.attachShadow({ mode: 'open' }); - this.version = "5.8.1"; + this.version = "5.8.2"; this.buttonSkipTo = false; this.initialized = false; From 47daa70265a22e42c7dc4bce88fa74d01cae0e51 Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Tue, 5 Aug 2025 16:39:06 -0500 Subject: [PATCH 12/38] udpated version of SkipTo.js --- content/shared/js/skipto.js | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/content/shared/js/skipto.js b/content/shared/js/skipto.js index b9511f0365..d8af23092b 100644 --- a/content/shared/js/skipto.js +++ b/content/shared/js/skipto.js @@ -4561,30 +4561,15 @@ button:hover { this.updateMenuitems(); this.landmarkGroupLabelNode.textContent = this.addNumberToGroupLabel(config.landmarkGroupLabel, landmarkElements.length); - if (landmarkElements.length === 1) { - this.landmarkGroupLabelNode.setAttribute('aria-label', config.landmarkOneGroupLabel); - } - else { - this.landmarkGroupLabelNode.setAttribute('aria-label', `${landmarkElements.length} ${config.landmarkGroupLabel}`); - } + this.landmarkGroupLabelNode.setAttribute('aria-label', config.landmarkGroupLabel); if (config.headings.includes('main')) { this.headingGroupLabelNode.textContent = this.addNumberToGroupLabel(config.headingMainGroupLabel, headingElements.length); - if (headingElements.length === 1) { - this.headingGroupLabelNode.setAttribute('aria-label', config.headingOneMainGroupLabel); - } - else { - this.headingGroupLabelNode.setAttribute('aria-label', `${headingElements.length} ${config.headingMainGroupLabel}`); - } + this.headingGroupLabelNode.setAttribute('aria-label', config.headingMainGroupLabel); } else { this.headingGroupLabelNode.textContent = this.addNumberToGroupLabel(config.headingGroupLabel, headingElements.length); - if (headingElements.length === 1) { - this.headingGroupLabelNode.setAttribute('aria-label', config.headingOneGroupLabel); - } - else { - this.headingGroupLabelNode.setAttribute('aria-label', `${headingElements.length} ${config.headingGroupLabel}`); - } + this.headingGroupLabelNode.setAttribute('aria-label', config.headingGroupLabel); } } @@ -5544,9 +5529,7 @@ button:hover { menuLabel: 'Landmarks and Headings', landmarkGroupLabel: 'Landmark Regions', headingGroupLabel: 'Headings', - headingOneGroupLabel: 'One Heading', headingMainGroupLabel: 'Headings in Main Region', - headingOneMainGroupLabel: 'One Heading in Main Region', headingLevelLabel: 'Heading level', mainLabel: 'main', searchLabel: 'search', From 455231ee86b2d284ede904e1461e9c710b27a223 Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Thu, 7 Aug 2025 12:24:33 -0500 Subject: [PATCH 13/38] udpated to version 5.8.3 --- content/shared/js/skipto.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/content/shared/js/skipto.js b/content/shared/js/skipto.js index d8af23092b..666ad08df6 100644 --- a/content/shared/js/skipto.js +++ b/content/shared/js/skipto.js @@ -1,5 +1,5 @@ /* ======================================================================== - * Version: 5.8.2 + * Version: 5.8.3 * Copyright (c) 2022, 2023, 2024, 2025 Jon Gunderson; Licensed BSD * Copyright (c) 2021 PayPal Accessibility Team and University of Illinois; Licensed BSD * All rights reserved. @@ -2096,6 +2096,9 @@ button:hover { if (this.overlayElem) { this.overlayElem.style.display = 'none'; } + if (this.hiddenElem) { + this.hiddenElem.style.display = 'none'; + } } } @@ -3867,7 +3870,8 @@ button:hover { return false; } // end function - passFound = passElem === document.body; + passFound = (passElem === document.body) || + (passElem.parentNode && (passElem.parentNode.id === SKIP_TO_ID)); let node = transverseDOMForElement(document.body); if (!node && useFirst && firstNode) { @@ -5424,7 +5428,7 @@ button:hover { // Always call super first in constructor super(); this.attachShadow({ mode: 'open' }); - this.version = "5.8.2"; + this.version = "5.8.3"; this.buttonSkipTo = false; this.initialized = false; From e542acf562da14518a9cc5b41631a4298b53cee9 Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Fri, 3 Oct 2025 11:41:16 -0500 Subject: [PATCH 14/38] updated skipto.js --- content/shared/js/skipto.js | 2824 ++++++++++++++++------------------- 1 file changed, 1273 insertions(+), 1551 deletions(-) diff --git a/content/shared/js/skipto.js b/content/shared/js/skipto.js index 666ad08df6..7b7f3b724c 100644 --- a/content/shared/js/skipto.js +++ b/content/shared/js/skipto.js @@ -1,5 +1,5 @@ /* ======================================================================== - * Version: 5.8.3 + * Version: 5.9.0 * Copyright (c) 2022, 2023, 2024, 2025 Jon Gunderson; Licensed BSD * Copyright (c) 2021 PayPal Accessibility Team and University of Illinois; Licensed BSD * All rights reserved. @@ -240,7 +240,6 @@ // Element IDs const SKIP_TO_ID = 'id-skip-to-ver-5'; - const SKIP_TO_MENU_STYLE_ID = 'id-skip-to-menu-style'; const SCRIPT_EXTENSION_ID = `id-skip-to-extension`; const SCRIPT_BOOKMARKLET_ID = `id-skip-to-bookmarklet`; @@ -256,9 +255,13 @@ const MENU_SHORTCUTS_GROUP_ID = 'id-skip-to-shortcuts-group'; const MENU_SHORTCUTS_GROUP_LABEL_ID = 'id-skip-to-shortcuts-group-label'; - const MESSAGE_ID = 'id-skip-to-message'; + const MENU_ABOUT_ID = 'id-skip-to-about'; + + const MESSAGE_ID = 'id-skip-to-message'; + + const HIGHLIGHT_ID = 'id-skip-to-highlight-overlay'; + const HIDDEN_ELEMENT_ID = 'id-skip-to-hidden-element'; - const HIGHLIGHT_ID = 'id-skip-to-highlight-overlay'; // Custom element names @@ -266,200 +269,386 @@ const BOOKMARKLET_ELEMENT_NAME = 'skip-to-content-bookmarklet'; const EXTENSION_ELEMENT_NAME = 'skip-to-content-extension'; - const INFO_DIALOG_ELEMENT_NAME = 'skip-to-content-info-dialog-580'; - const MESSAGE_ELEMENT_NAME = 'skip-to-content-message-element-580'; - const HIGHLIGHT_ELEMENT_NAME = 'skip-to-content-highlight-element-580'; - // Attributes const ATTR_SKIP_TO_DATA = 'data-skipto'; // URLs to more information - const MORE_PAGE_SCRIPT_INFO_URL ='https://skipto-landmarks-headings.github.io/page-script-5/'; - const MORE_SHORTCUT_INFO_URL ='https://skipto-landmarks-headings.github.io/page-script-5/shortcuts.html'; + const MORE_ABOUT_INFO_URL ='https://skipto-landmarks-headings.github.io/page-script-5/'; + const MORE_SHORTCUT_INFO_URL ='https://skipto-landmarks-headings.github.io/page-script-5/shortcuts.html'; + + /* utils.js */ + + /* Constants */ + const debug$c = new DebugLogging('Utils', false); + debug$c.flag = false; + + /* + * @function getHighlightInfo + * + * @desc Returns an array of sizes and fonts for highlighting elements + * + * @param {String} size : Highlight border size 'small', 'medium', 'large' or 'x-large' + * + * @returns [borderWidth, shadowWidth, offset, fontSize] + */ + function getHighlightInfo (size) { + + let borderWidth, shadowWidth, offset, fontSize; + + const highlightBorderSize = size ? + size : + 'small'; + + switch (highlightBorderSize) { + case 'small': + borderWidth = 2; + shadowWidth = 1; + offset = 4; + fontSize = '12pt'; + break; + + case 'medium': + borderWidth = 3; + shadowWidth = 2; + offset = 4; + fontSize = '13pt'; + break; + + case 'large': + borderWidth = 4; + shadowWidth = 3; + offset = 6; + fontSize = '14pt'; + break; + + case 'x-large': + borderWidth = 6; + shadowWidth = 3; + offset = 8; + fontSize = '16pt'; + break; + + default: + borderWidth = 2; + shadowWidth = 1; + offset = 4; + fontSize = '12pt'; + break; + } + return [borderWidth, shadowWidth, offset, fontSize]; + } + + /* + * @function getAttributeValue + * + * @desc Return attribute value if present on element, + * otherwise return empty string. + * + * @returns {String} see @desc + */ + function getAttributeValue (element, attribute) { + let value = element.getAttribute(attribute); + return (value === null) ? '' : normalize(value); + } + + /* + * @function normalize + * + * @desc Trim leading and trailing whitespace and condense all + * internal sequences of whitespace to a single space. Adapted from + * Mozilla documentation on String.prototype.trim polyfill. Handles + * BOM and NBSP characters. + * + * @return {String} see @desc + */ + function normalize (s) { + let rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; + return s.replace(rtrim, '').replace(/\s+/g, ' '); + } + + /** + * @fuction isNotEmptyString + * + * @desc Returns true if the string has content, otherwise false + * + * @param {Boolean} see @desc + */ + function isNotEmptyString (str) { + return (typeof str === 'string') && str.length && str.trim() && str !== " "; + } + + /** + * @fuction isVisible + * + * @desc Returns true if the element is visible in the graphical rendering + * + * @param {node} elem - DOM element node of a labelable element + */ + function isVisible (element) { + + function isDisplayNone(el) { + if (!el || (el.nodeType !== Node.ELEMENT_NODE)) { + return false; + } + + if (el.hasAttribute('hidden')) { + return true; + } + + const style = window.getComputedStyle(el, null); + const display = style.getPropertyValue("display"); + if (display === 'none') { + return true; + } + + // check ancestors for display none + if (el.parentNode) { + return isDisplayNone(el.parentNode); + } + + return false; + } + + const computedStyle = window.getComputedStyle(element); + let visibility = computedStyle.getPropertyValue('visibility'); + if ((visibility === 'hidden') || (visibility === 'collapse')) { + return false; + } + + return !isDisplayNone(element); + } /* style.js */ /* Constants */ - const debug$c = new DebugLogging('style', false); - debug$c.flag = false; + const debug$b = new DebugLogging('style', false); + debug$b.flag = false; + - const cssMenuTemplate = document.createElement('template'); - cssMenuTemplate.textContent = ` -:root { + + const cssStyleTemplate = document.createElement('template'); + cssStyleTemplate.textContent = ` +.container { color-scheme: light dark; + + --skipto-popup-offset: -36px; + --skipto-show-border-offset: -28px; + --skipto-menu-offset: 36px; + + --skipto-font-family: 'inherit'; + --skipto-font-size: 'inherit'; + --skipto-position-left: '46%'; + --skipto-small-break-point: '580px'; + --skipto-medium-break-point: '992px'; + + --skipto-button-text-color: '#13294b'; + --skipto-button-text-dark-color: '#ffffff'; + + --skipto-button-background-color: '#dddddd'; + --skipto-button-background-dark-color: '#013c93'; + + --skipto-focus-border-color: '#c5050c'; + --skipto-focus-border-dark-color: '#ffffff'; + + --skipto-menu-text-color: '#13294b'; + --skipto-menu-text-dark-color: '#ffffff'; + + --skipto-menu-background-color: '#dddddd'; + --skipto-menu-background-dark-color: '#000000'; + + --skipto-menuitem-focus-text-color: '#dddddd'; + --skipto-menuitem-focus-text-dark-color: '#ffffff'; + + --skipto-menuitem-focus-background-color: '#13294b'; + --skipto-menuitem-focus-background-dark-color: '#013c93'; + + --skipto-dialog-text-color: '#000000'; + --skipto-dialog-text-dark-color: '#ffffff'; + + --skipto-dialog-background-color: '#ffffff'; + --skipto-dialog-background-dark-color: '#000000'; + + --skipto-dialog-background-title-color: '#eeeeee'; + --skipto-dialog-background-title-dark-color: '#013c93'; + + --skipto-z-index-1: '2000001'; + --skipto-z-index-2: '20000002'; + --skipto-z-index-highlight: '1999900'; + + --skipto-highlight-offset: '6px'; + --skipto-highlight-border-width: '4px': + --skipto-highlight-font-size: '14pt': + --skipto-highlight-shadow-border-width: '10px'; + --skipto-highlight-border-style: 'dashed'; + + --skipto-hidden-text-color: '#000000'; + --skipto-hidden-text-dark-color: '#0000000'; + --skipto-hidden-background-color: '#ffcc00'; + --skipto-hidden-background-dark-color: '#ffcc00'; + +} + +.container { + display: block; + z-index: var(--skipto-z-index-1); } -#${SKIP_TO_ID}.popup { - top: -36px; +.menu-button button.popup { + top: var(--skipto-popup-offset); transition: top 0.35s ease; } -#${SKIP_TO_ID}.popup.show-border { - top: -28px; +.menu-button button.popup.show-border { + top: var(--skipto-show-border-offset); transition: top 0.35s ease; } -#${SKIP_TO_ID} button .skipto-text { +.menu-button button .skipto-text { padding: 6px 8px 6px 8px; display: inline-block; } -#${SKIP_TO_ID} button .skipto-small { +.menu-button button .skipto-small { padding: 6px 8px 6px 8px; display: none; } -#${SKIP_TO_ID} button .skipto-medium { +.menu-button button .skipto-medium { padding: 6px 8px 6px 8px; display: none; } -#${SKIP_TO_ID}, -#${SKIP_TO_ID}.popup.focus, -#${SKIP_TO_ID}.popup:hover { +.menu-button button { position: fixed; - top: 0; - left: $positionLeft; - font-family: $fontFamily; - font-size: $fontSize; - display: block; - border: none; - margin-bottom: 4px; - transition: left 1s ease; - z-index: $z1Index !important; - user-select: none; - touch-action: none; -} - -#${SKIP_TO_ID} button { - position: sticky; + left: var(--skipto-position-left); margin: 0; padding: 0; border-width: 0px 1px 1px 1px; border-style: solid; border-radius: 0px 0px 6px 6px; - border-color: light-dark($buttonBackgroundColor, $buttonBackgroundDarkColor); - color: light-dark($buttonTextColor, $buttonTextDarkColor); - background-color: light-dark($buttonBackgroundColor, $buttonBackgroundDarkColor); + border-color: light-dark(var(--skipto-button-background-color), var(--skipto-button-background-dark-color)); + color: light-dark(var(--skipto-button-text-color), var(--skipto-button-text-dark-color)); + background-color: light-dark(var(--skipto-button-background-color), var(--skipto-button-background-dark-color)); z-index: 100000 !important; - font-family: $fontFamily; - font-size: $fontSize; - z-index: $z1Index !important; - touch-action: none; + z-index: var(--skipto-z-index-1) !important; + font-size: var(--skipto-font-size); + font-family: var(--skipto-font-family); } -@media screen and (max-width: $smallBreakPointpx) { - #${SKIP_TO_ID}:not(.popup) button .skipto-small { +@media screen and (max-width: var(--skipto-small-break-point)) { + .menu-button button:not(.popup) .skipto-small { transition: top 0.35s ease; display: inline-block; } - #${SKIP_TO_ID}:not(.popup) button .skipto-text, - #${SKIP_TO_ID}:not(.popup) button .skipto-medium { + .menu-button button:not(.popup) .skipto-text, + button:not(.popup) .skipto-medium { transition: top 0.35s ease; display: none; } - #${SKIP_TO_ID}:not(.popup).focus button .skipto-text { + .menu-button button:not(.popup):focus .skipto-text { transition: top 0.35s ease; display: inline-block; } - #${SKIP_TO_ID}:not(.popup).focus button .skipto-small, - #${SKIP_TO_ID}:not(.popup).focus button .skipto-medium { + .menu-button button:not(.popup):focus .skipto-small, + .menu-button button:not(.popup):focus .skipto-medium { transition: top 0.35s ease; display: none; } } -@media screen and (min-width: $smallBreakPointpx) and (max-width: $mediumBreakPointpx) { - #${SKIP_TO_ID}:not(.popup) button .skipto-medium { +@media screen and (min-width: var(--skipto-small-break-point)) and (max-width: var(--skipto-medium-break-point)) { + .menu-button button:not(.popup) .skipto-medium { transition: top 0.35s ease; display: inline-block; } - #${SKIP_TO_ID}:not(.popup) button .skipto-text, - #${SKIP_TO_ID}:not(.popup) button .skipto-small { + .menu-button button:not(.popup) .skipto-text, + .menu-button button:not(.popup) .skipto-small { transition: top 0.35s ease; display: none; } - #${SKIP_TO_ID}:not(.popup).focus button .skipto-text { + .menu-button button:not(.popup):focus .skipto-text { transition: top 0.35s ease; display: inline-block; } - #${SKIP_TO_ID}:not(.popup).focus button .skipto-small, - #${SKIP_TO_ID}:not(.popup).focus button .skipto-medium { + .menu-button button:not(.popup):focus .skipto-small, + .menu-button button:not(.popup):focus .skipto-medium { transition: top 0.35s ease; display: none; } } -#${SKIP_TO_ID}.static { +.menu-button button.static { position: absolute !important; } - -#${SKIP_TO_ID} [role="menu"] { - position: absolute; +.menu-button [role="menu"] { + position: fixed; + top: var(--skipto-menu-offset); + left: var(--skipto-position-left); min-width: 16em; display: none; margin: 0; padding: 0.25rem; - background-color: light-dark($menuBackgroundColor, $menuBackgroundDarkColor); + background-color: light-dark(var(--skipto-menu-background-color), var(--skipto-menu-background-dark-color)); border-width: 2px; border-style: solid; - border-color: light-dark($focusBorderColor, $focusBorderDarkColor); + border-color: light-dark(var(--skipto-focus-border-color), var(--skipto-focus-border-dark-color)); border-radius: 5px; - z-index: $z1Index !important; + z-index: var(--skipto-z-index-1) !important; touch-action: none; + font-size: var(--skipto-font-size); + font-family: var(--skipto-font-family); } -#${SKIP_TO_ID} [role="group"] { +.menu-button [role="group"] { display: grid; grid-auto-rows: min-content; grid-row-gap: 1px; } -#${SKIP_TO_ID} [role="group"].overflow { +.menu-button [role="group"].overflow { overflow-x: hidden; overflow-y: scroll; } -#${SKIP_TO_ID} [role="separator"]:first-child { +.menu-button [role="separator"]:first-child { border-radius: 5px 5px 0 0; } -#${SKIP_TO_ID} [role="menuitem"] { +.menu-button [role="menuitem"] { padding: 3px; width: auto; border-width: 0px; border-style: solid; - color: light-dark($menuTextColor, $menuTextDarkColor); - background-color: light-dark($menuBackgroundColor, $menuBackgroundDarkColor); + color: light-dark(var(--skipto-menu-text-color), var(--skipto-menu-text-dark-color)); + background-color: light-dark(var(--skipto-menu-background-color), var(--skipto-menu-background-dark-color)); display: grid; overflow-y: clip; grid-template-columns: repeat(6, 1.2rem) 1fr; grid-column-gap: 2px; - font-size: 1em; - z-index: $z1Index; + z-index: var(--skipto-z-index-1); } -#${SKIP_TO_ID} [role="menuitem"].shortcuts, -#${SKIP_TO_ID} [role="menuitem"].about { - z-index: $z2Index; +.menu-button [role="menuitem"].shortcuts, +.menu-button [role="menuitem"].about { + z-index: var(--skipto-z-index-2); } -#${SKIP_TO_ID} [role="menuitem"] .level, -#${SKIP_TO_ID} [role="menuitem"] .label { +.menu-button [role="menuitem"] .level, +.menu-button [role="menuitem"] .label { font-size: 100%; font-weight: normal; - color: light-dark($menuTextColor, $menuTextDarkColor); - background-color: light-dark($menuBackgroundColor, $menuBackgroundDarkColor); + color: light-dark(var(--skipto-menu-text-color), var(--skipto-menu-text-dark-color)); + background-color: light-dark(var(--skipto-menu-background-color), var(--skipto-menu-background-dark-color)); display: inline-block; line-height: inherit; display: inline-block; @@ -467,12 +656,12 @@ border: none; } -#${SKIP_TO_ID} [role="menuitem"] .level { +.menu-button [role="menuitem"] .level { text-align: right; padding-right: 4px; } -#${SKIP_TO_ID} [role="menuitem"] .label { +.menu-button [role="menuitem"] .label { text-align: left; margin: 0; padding: 0; @@ -480,49 +669,49 @@ text-overflow: ellipsis; } -#${SKIP_TO_ID} [role="menuitem"] .level:first-letter, -#${SKIP_TO_ID} [role="menuitem"] .label:first-letter { +.menu-button [role="menuitem"] .level:first-letter, +.menu-button [role="menuitem"] .label:first-letter { text-decoration: underline; text-transform: uppercase; } -#${SKIP_TO_ID} [role="menuitem"].skip-to-h1 .level { grid-column: 1; } -#${SKIP_TO_ID} [role="menuitem"].skip-to-h2 .level { grid-column: 2; } -#${SKIP_TO_ID} [role="menuitem"].skip-to-h3 .level { grid-column: 3; } -#${SKIP_TO_ID} [role="menuitem"].skip-to-h4 .level { grid-column: 4; } -#${SKIP_TO_ID} [role="menuitem"].skip-to-h5 .level { grid-column: 5; } -#${SKIP_TO_ID} [role="menuitem"].skip-to-h6 .level { grid-column: 6;} - -#${SKIP_TO_ID} [role="menuitem"].skip-to-h1 .label { grid-column: 2 / 8; } -#${SKIP_TO_ID} [role="menuitem"].skip-to-h2 .label { grid-column: 3 / 8; } -#${SKIP_TO_ID} [role="menuitem"].skip-to-h3 .label { grid-column: 4 / 8; } -#${SKIP_TO_ID} [role="menuitem"].skip-to-h4 .label { grid-column: 5 / 8; } -#${SKIP_TO_ID} [role="menuitem"].skip-to-h5 .label { grid-column: 6 / 8; } -#${SKIP_TO_ID} [role="menuitem"].skip-to-h6 .label { grid-column: 7 / 8;} - -#${SKIP_TO_ID} [role="menuitem"].skip-to-h1.no-level .label { grid-column: 1 / 8; } -#${SKIP_TO_ID} [role="menuitem"].skip-to-h2.no-level .label { grid-column: 2 / 8; } -#${SKIP_TO_ID} [role="menuitem"].skip-to-h3.no-level .label { grid-column: 3 / 8; } -#${SKIP_TO_ID} [role="menuitem"].skip-to-h4.no-level .label { grid-column: 4 / 8; } -#${SKIP_TO_ID} [role="menuitem"].skip-to-h5.no-level .label { grid-column: 5 / 8; } -#${SKIP_TO_ID} [role="menuitem"].skip-to-h6.no-level .label { grid-column: 6 / 8; } - -#${SKIP_TO_ID} [role="menuitem"].skip-to-nesting-level-1 .nesting { grid-column: 1; } -#${SKIP_TO_ID} [role="menuitem"].skip-to-nesting-level-2 .nesting { grid-column: 2; } -#${SKIP_TO_ID} [role="menuitem"].skip-to-nesting-level-3 .nesting { grid-column: 3; } - -#${SKIP_TO_ID} [role="menuitem"].skip-to-nesting-level-0 .label { grid-column: 1 / 8; } -#${SKIP_TO_ID} [role="menuitem"].skip-to-nesting-level-1 .label { grid-column: 2 / 8; } -#${SKIP_TO_ID} [role="menuitem"].skip-to-nesting-level-2 .label { grid-column: 3 / 8; } -#${SKIP_TO_ID} [role="menuitem"].skip-to-nesting-level-3 .label { grid-column: 4 / 8; } - -#${SKIP_TO_ID} [role="menuitem"].no-items .label, -#${SKIP_TO_ID} [role="menuitem"].action .label { +.menu-button [role="menuitem"].skip-to-h1 .level { grid-column: 1; } +.menu-button [role="menuitem"].skip-to-h2 .level { grid-column: 2; } +.menu-button [role="menuitem"].skip-to-h3 .level { grid-column: 3; } +.menu-button [role="menuitem"].skip-to-h4 .level { grid-column: 4; } +.menu-button [role="menuitem"].skip-to-h5 .level { grid-column: 5; } +.menu-button [role="menuitem"].skip-to-h6 .level { grid-column: 6;} + +.menu-button [role="menuitem"].skip-to-h1 .label { grid-column: 2 / 8; } +.menu-button [role="menuitem"].skip-to-h2 .label { grid-column: 3 / 8; } +.menu-button [role="menuitem"].skip-to-h3 .label { grid-column: 4 / 8; } +.menu-button [role="menuitem"].skip-to-h4 .label { grid-column: 5 / 8; } +.menu-button [role="menuitem"].skip-to-h5 .label { grid-column: 6 / 8; } +.menu-button [role="menuitem"].skip-to-h6 .label { grid-column: 7 / 8;} + +.menu-button [role="menuitem"].skip-to-h1.no-level .label { grid-column: 1 / 8; } +.menu-button [role="menuitem"].skip-to-h2.no-level .label { grid-column: 2 / 8; } +.menu-button [role="menuitem"].skip-to-h3.no-level .label { grid-column: 3 / 8; } +.menu-button [role="menuitem"].skip-to-h4.no-level .label { grid-column: 4 / 8; } +.menu-button [role="menuitem"].skip-to-h5.no-level .label { grid-column: 5 / 8; } +.menu-button [role="menuitem"].skip-to-h6.no-level .label { grid-column: 6 / 8; } + +.menu-button [role="menuitem"].skip-to-nesting-level-1 .nesting { grid-column: 1; } +.menu-button [role="menuitem"].skip-to-nesting-level-2 .nesting { grid-column: 2; } +.menu-button [role="menuitem"].skip-to-nesting-level-3 .nesting { grid-column: 3; } + +.menu-button [role="menuitem"].skip-to-nesting-level-0 .label { grid-column: 1 / 8; } +.menu-button [role="menuitem"].skip-to-nesting-level-1 .label { grid-column: 2 / 8; } +.menu-button [role="menuitem"].skip-to-nesting-level-2 .label { grid-column: 3 / 8; } +.menu-button [role="menuitem"].skip-to-nesting-level-3 .label { grid-column: 4 / 8; } + +.menu-button [role="menuitem"].no-items .label, +.menu-button [role="menuitem"].action .label { grid-column: 1 / 8; } -#${SKIP_TO_ID} [role="separator"] { +.menu-button [role="separator"] { margin: 1px 0px 1px 0px; padding: 3px; display: block; @@ -530,231 +719,564 @@ font-weight: bold; border-bottom-width: 1px; border-bottom-style: solid; - border-bottom-color: light-dark($menuTextColor, $menuTextDarkColor); - background-color: light-dark($menuBackgroundColor, $menuBackgroundColor); - color: light-dark($menuTextColor, $menuTextDarkColor); - z-index: $z1Index !important; + border-bottom-color: light-dark(var(--skipto-menu-text-color), var(--skipto-menu-text-dark-color)); + background-color: light-dark(var(--skipto-menu-background-color), var(--skipto-menu-background-dark-color)); + color: light-dark(var(--skipto-menu-text-color), var(--skipto-menu-text-dark-color)); + z-index: var(--skipto-z-index-1) !important; } -#${SKIP_TO_ID} [role="separator"] .mofn { +.menu-button [role="separator"] .mofn { font-weight: normal; font-size: 85%; } -#${SKIP_TO_ID} [role="separator"]:first-child { +.menu-button [role="separator"]:first-child { border-radius: 5px 5px 0 0; } -#${SKIP_TO_ID} [role="menuitem"].last { +.menu-button [role="menuitem"].last { border-radius: 0 0 5px 5px; } /* focus styling */ -#${SKIP_TO_ID}.focus { - display: block; -} - -#${SKIP_TO_ID} button:focus, -#${SKIP_TO_ID} button:hover { - background-color: light-dark($menuBackgroundColor, $menuBackgroundDarkColor); - color: light-dark($menuTextColor, $menuTextDarkColor); +.menu-button button:focus, +.menu-button button:hover { + background-color: light-dark(var(--skipto-menu-background-color), var(--skipto-menu-background-dark-color)); + color: light-dark(var(--skipto-menu-text-color), var(--skipto-menu-text-dark-color)); outline: none; border-width: 0px 2px 2px 2px; - border-color: light-dark($focusBorderColor, $focusBorderDarkColor); + border-color: light-dark(var(--skipto-focus-border-color), var(--skipto-focus-border-dark-color)); } +.menu-button button.popup.menu, +.menu-button button.popup:focus, +.menu-button button.popup:hover { + top: 0; + display: block; + transition: left 1s ease; + z-index: var(--skipto-z-index-1) !important; +} -#${SKIP_TO_ID} button:focus .skipto-text, -#${SKIP_TO_ID} button:hover .skipto-text, -#${SKIP_TO_ID} button:focus .skipto-small, -#${SKIP_TO_ID} button:hover .skipto-small, -#${SKIP_TO_ID} button:focus .skipto-medium, -#${SKIP_TO_ID} button:hover .skipto-medium { +.menu-button button:focus .skipto-text, +.menu-button button:hover .skipto-text, +.menu-button button:focus .skipto-small, +.menu-button button:hover .skipto-small, +.menu-button button:focus .skipto-medium, +.menu-button button:hover .skipto-medium { padding: 6px 7px 5px 7px; } -#${SKIP_TO_ID} [role="menuitem"]:focus { +.menu-button [role="menuitem"]:focus { padding: 1px; border-width: 2px; border-style: solid; - border-color: light-dark($focusBorderColor, $focusBorderDarkColor); + border-color: light-dark(var(--skipto-focus-border-color), var(--skipto-focus-border-dark-color)); outline: none; } -#${SKIP_TO_ID} [role="menuitem"].hover, -#${SKIP_TO_ID} [role="menuitem"].hover .level, -#${SKIP_TO_ID} [role="menuitem"].hover .label { - background-color: light-dark($menuitemFocusBackgroundColor, $menuitemFocusBackgroundDarkColor); - color: light-dark($menuitemFocusTextColor, $menuitemFocusTextDarkColor); +.menu-button [role="menuitem"].hover, +.menu-button [role="menuitem"].hover .level, +.menu-button [role="menuitem"].hover .label { + background-color: light-dark(var(--skipto-menuitem-focus-background-color), var(--skipto-menuitem-focus-background-dark-color)); + color: light-dark(var(--skipto-menuitem-focus-text-color), var(--skipto-menuitem-focus-text-dark-color)); } -#${SKIP_TO_ID} [role="separator"].shortcuts-disabled, -#${SKIP_TO_ID} [role="menuitem"].shortcuts-disabled { +.menu-button [role="separator"].shortcuts-disabled, +.menu-button [role="menuitem"].shortcuts-disabled { display: none; } + @media (forced-colors: active) { - #${SKIP_TO_ID} button { + .menu-button button { border-color: ButtonBorder; color: ButtonText; background-color: ButtonFace; } - #${SKIP_TO_ID} [role="menu"] { + .menu-button [role="menu"] { background-color: ButtonFace; border-color: ButtonText; } - #${SKIP_TO_ID} [role="menuitem"] { + .menu-button [role="menuitem"] { color: ButtonText; background-color: ButtonFace; } - #${SKIP_TO_ID} [role="menuitem"] .level, - #${SKIP_TO_ID} [role="menuitem"] .label { + .menu-button [role="menuitem"] .level, + .menu-button [role="menuitem"] .label { color: ButtonText; background-color: ButtonFace; } - #${SKIP_TO_ID} [role="separator"] { + .menu-button [role="separator"] { border-bottom-color: ButtonBorder; background-color: ButtonFace; color: ButtonText; - z-index: $z1Index !important; + z-index: var(--skipto-z-index-1) !important; } - #${SKIP_TO_ID} button:focus, - #${SKIP_TO_ID} button:hover { + .menu-button button:focus, + .menu-button button:hover { background-color: ButtonFace; color: ButtonText; border-color: ButtonBorder; } - #${SKIP_TO_ID} [role="menuitem"]:focus { + .menu-button [role="menuitem"]:focus { background-color: ButtonText; color: ButtonFace; border-color: ButtonBorder; } - #${SKIP_TO_ID} [role="menuitem"].hover, - #${SKIP_TO_ID} [role="menuitem"].hover .level, - #${SKIP_TO_ID} [role="menuitem"].hover .label { + .menu-button [role="menuitem"].hover, + .menu-button [role="menuitem"].hover .level, + .menu-button [role="menuitem"].hover .label { background-color: ButtonText; color: ButtonFace; } - } -`; +/* Dialog Styling */ +dialog { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%,-50%); + font-family: var(--skipto-font-family); + font-size: var(--skipto-font-size); + max-width: 70%; + margin: 0; + padding: 0; + background-color: light-dark(var(--skipto-dialog-background-color), var(--skipto-dialog-background-dark-color)); + color: light-dark(var(--skipto-dialog-text-color), var(--skipto-dialog-text-dark-color)); + border-width: 2px; + border-style: solid; + border-color: light-dark(var(--skipto-focus-border-color), --skipto-focus-border-dark-color)); + border-radius: 5px; + z-index: 2000001; + width: 80%; + max-width: 450px; +} - /* - * @function getTheme - * - * @desc Returns - * - * @param {String} colorTheme - A string identifying a color theme - * - * @returns {Object} see @desc - */ - function getTheme(colorTheme) { - if (typeof colorThemes[colorTheme] === 'object') { - return colorThemes[colorTheme]; - } - // if no theme defined, use urlSelectors - let hostnameMatch = ''; - let pathnameMatch = ''; - let hostandpathnameMatch = ''; +dialog .header { + margin: 0; + margin-bottom: 0.5em; + padding: 4px; + border-width: 0; + border-bottom-width: 1px; + border-style: solid; + border-color: light-dark(--skipto-focus-border-color), --skipto-focus-border-dark-color)); + border-top-left-radius: 5px; + border-top-right-radius: 5px; + font-weight: bold; + background-color: light-dark(var(--skipto-dialog-background-title-color), var(--skipto-dialog-background-title-dark-color)); + color: light-dark(var(--skipto-dialog-text-color), var(--skipto-dialog-text-dark-color)); + position: relative; + font-size: 100%; +} - const locationURL = new URL(location.href); - const hostname = locationURL.hostname; - const pathname = location.pathname; +dialog .header h2 { + margin: 0; + padding: 0; + font-size: 1em; +} - for (let item in colorThemes) { - const hostnameSelector = colorThemes[item].hostnameSelector; - const pathnameSelector = colorThemes[item].pathnameSelector; - let hostnameFlag = false; - let pathnameFlag = false; +dialog .header button { + position: absolute; + top: -0.25em; + right: 0; + border: none; + background: transparent; + font-weight: bold; + color: light-dark(black, white); +} - if (hostnameSelector) { - if (hostname.indexOf(hostnameSelector) >= 0) { - if (!hostnameMatch || - (colorThemes[hostnameMatch].hostnameSelector.length < hostnameSelector.length)) { - hostnameMatch = item; - hostnameFlag = true; - pathnameMatch = ''; - } - else { - // if the same hostname is used in another theme, set the hostnameFlas in case the pathname - // matches - if (colorThemes[hostnameMatch].hostnameSelector.length === hostnameSelector.length) { - hostnameFlag = true; - } - } - } - } +dialog .content { + margin-left: 2em; + margin-right: 2em; + margin-top: 0; + margin-bottom: 2em; +} - if (pathnameSelector) { - if (pathname.indexOf(pathnameSelector) >= 0) { - if (!pathnameMatch || - (colorThemes[pathnameMatch].pathnameSelector.length < pathnameSelector.length)) { - pathnameMatch = item; - pathnameFlag = true; - } - } - } +dialog .content .desc { + margin: 0.25em; + text-align: center; +} - if (hostnameFlag && pathnameFlag) { - hostandpathnameMatch = item; - } - } +dialog .content .privacy-label { + margin: 0; + margin-top: 1em; + text-align: center; + font-weight: bold; +} - if (hostandpathnameMatch) { - return colorThemes[hostandpathnameMatch]; - } - else { - if (hostnameMatch) { - return colorThemes[hostnameMatch]; - } else { - if (pathnameMatch) { - return colorThemes[pathnameMatch]; - } - } - } +dialog .content .privacy { + text-align: center; + margin-bottom: 1em; +} - // if no other theme is found use default theme - return colorThemes['default']; - } +dialog .content .happy { + text-align: center; + font-family: 'Brush Script MT', cursive; + font-size: 200%; + letter-spacing: 0.05em; +} - /* - * @function updateStyle +dialog .content .version, +dialog .content .copyright { + margin-top: 0.5em; + text-align: center; +} + +dialog .content table { + width: auto; +} + +dialog .content caption { + margin: 0; + padding: 0; + margin-top: 1em; + text-align: left; + font-weight: bold; + font-size: 110%; +} + +dialog .content th { + margin: 0; + padding: 0; + padding-top: 0.125em; + padding-bottom: 0.125em; + text-align: left; + font-weight: bold; + font-size: 100%; +} + +dialog .content th { + border-bottom-width: 1px; + border-bottom-style: solid; + border-bottom-color: light-dark(#999999, #777777); +} + +dialog .content th.shortcut { + width: 2.5em; +} + +dialog .content td { + margin: 0; + padding: 0; + padding-top: 0.125em; + padding-bottom: 0.125em; + text-align: left; + font-size: 100%; +} + + +dialog .content table tr:nth-child(even) { + background-color: light-dark(#eeeeee, #111111); +} + +dialog .buttons { + float: right; + margin-right: 0.5em; + margin-bottom: 0.5em; +} + +dialog button { + margin: 6px; +} + +dialog .buttons button { + min-width: 5em; +} + +dialog button:focus { + outline: 2px solid currentColor; + outline-offset: 2px; +} + +dialog button:hover { + cursor: pointer; +} + +/* Navigation Messages */ + +#${MESSAGE_ID} { + position: fixed; + display: block; + opacity: 1; + top: 50%; + left: 50%; + transform: translate(-50%,-50%); + + font-family: $fontFamily; + font-size: $fontSize; + max-width: 70%; + margin: 0; + padding: 0; + background-color: light-dark(var(--skipto-dialog-background-color), var(--skipto-dialog-background-dark-color)); + border: 2px solid light-dark(var(--skipto-focus-border-color), var(--skipto-focus-border-dark-color)); + border-radius: 5px; + color: light-dark(var(--skipto-dialog-text-color), var(--skipto-dialog-text-dark-color)); + z-index: 2000001; + opacity: 1; +} + +#${MESSAGE_ID} .header { + margin: 0; + padding: 4px; + border-bottom: 1px solid light-dark(var(--skipto-focus-border-color), var(--skipto-focus-border-dark-color)); + border-top-left-radius: 5px; + border-top-right-radius: 5px; + font-weight: bold; + background-color: light-dark(var(--skipto-dialog-background-title-color), var(--skipto-dialog-background-title-dark-color)); + color light-dark(var(--skipto-dialog-text-color), var(--skipto-dialog-text-dark-color)); + font-size: 100%; +} + +#${MESSAGE_ID} .content { + margin-left: 2em; + margin-right: 2em; + margin-top: 2em; + margin-bottom: 2em; + background-color: light-dark(var(--skipto-dialog-background-color), var(--skipto-dialog-background-dark-color)); + color: light-dark(var(--skipto-dialog-text-color), var(--skipto-dialog-text-dark-color)); + font-size: 110%; + text-algin: center; +} + +#${MESSAGE_ID}.hidden { + display: none; +} + +#${MESSAGE_ID}.fade { + opacity: 0; + transition: visibility 0s 1s, opacity 1s linear; +} + +@media (forced-colors: active) { + + #${MESSAGE_ID} { + background-color: Canvas; + color CanvasText; + border-color: AccentColor; + } + + #${MESSAGE_ID} .header { + background-color: Canvas; + color CanvasText; + } + + #${MESSAGE_ID} .content { + background-color: Canvas; + color: CanvasText; + } +} + +#${HIGHLIGHT_ID} { + margin: 0; + padding: 0; + position: absolute; + border-radius: var(--skipto-highlight-offset); + border-width: var(--skipto-highlight-shadow-border-width); + border-style: solid; + border-color: light-dark(var(--skipto-menu-background-color), var(--skipto-menu-background-dark-color)); + box-sizing: border-box; + pointer-events:none; + z-index: var(--skipto-z-index-highlight); +} + +#${HIGHLIGHT_ID} .overlay-border { + margin: 0; + padding: 0; + position: relative; + border-radius: var(--skipto-highlight-offset); + border-width: var(--skipto-highlight-border-width); + border-style: var(--skipto-highlight-border-style); + border-color: light-dark(var(--skipto-focus-border-color), var(--skipto-focus-border-dark-color)); + z-index: var(--skipto-z-index-1); + box-sizing: border-box; + pointer-events:none; + background: transparent; +} + + +@keyframes fadeIn { + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +#${HIDDEN_ELEMENT_ID} { + position: absolute; + margin: 0; + padding: .25em; + background-color: light-dark(var(--skipto-hidden-background-color), var(--skipto-hidden-background-dark-color)); + color: light-dark(var(--skipto-hidden-text-color), var(--skipto-hidden-text-dark-color)); + font-family: var(--skipto-font-family); + font-size: var(--skipto-highlight-font-size); + font-style: italic; + font-weight: bold; + text-align: center; + animation: fadeIn 1.5s; + z-index: var(--skipto-z-index-1); +} + +#${HIGHLIGHT_ID} .overlay-info { + margin: 0; + padding: 2px; + position: relative; + text-align: left; + font-size: $fontSize; + font-family: $fontFamily; + border: var(--skipto-highlight-border-width) solid light-dark($menuBackgroundColor, $menuBackgroundDarkColor); + background-color: light-dark(var(--skipto-menu-background-color), var(--skipto-menu-background-dark-color)); + color: light-dark(var(--skipto-menu-text-color), var(--skipto-menu-text-dark-color)); + z-index: var(--skipto-z-index-1); + overflow: hidden; + text-overflow: ellipsis; + pointer-events:none; +} + +#${HIGHLIGHT_ID} .overlay-info.hasInfoTop { + border-radius: var(--skipto-highlight-offset) var(--skipto-highlight-offset) 0 0; +} + +#${HIGHLIGHT_ID} .overlay-info.hasInfoBottom { + border-radius: 0 0 var(--skipto-highlight-offset) var(--skipto-highlight-offset); +} + +@media (forced-colors: active) { + + #${HIGHLIGHT_ID} { + border-color: ButtonBorder; + } + + #${HIGHLIGHT_ID} .overlay-border { + border-color: ButtonBorder; + } + + #${HIGHLIGHT_ID} .overlay-border.skip-to-hidden { + background-color: ButtonFace; + color: ButtonText; + } + + #${HIGHLIGHT_ID} .overlay-info { + border-color: ButtonBorder; + background-color: ButtonFace; + color: ButtonText; + } + +} + +`; + + /* + * @function getTheme * - * @desc + * @desc Returns * - * @param + * @param {String} colorTheme - A string identifying a color theme * - * @returns + * @returns {Object} see @desc */ - function updateStyle(cssContent, stylePlaceholder, configValue, themeValue, defaultValue) { - let value = defaultValue; + function getTheme(colorTheme) { + if (typeof colorThemes[colorTheme] === 'object') { + return colorThemes[colorTheme]; + } + // if no theme defined, use urlSelectors + let hostnameMatch = ''; + let pathnameMatch = ''; + let hostandpathnameMatch = ''; + + const locationURL = new URL(location.href); + const hostname = locationURL.hostname; + const pathname = location.pathname; + + for (let item in colorThemes) { + const hostnameSelector = colorThemes[item].hostnameSelector; + const pathnameSelector = colorThemes[item].pathnameSelector; + let hostnameFlag = false; + let pathnameFlag = false; + + if (hostnameSelector) { + if (hostname.indexOf(hostnameSelector) >= 0) { + if (!hostnameMatch || + (colorThemes[hostnameMatch].hostnameSelector.length < hostnameSelector.length)) { + hostnameMatch = item; + hostnameFlag = true; + pathnameMatch = ''; + } + else { + // if the same hostname is used in another theme, set the hostnameFlas in case the pathname + // matches + if (colorThemes[hostnameMatch].hostnameSelector.length === hostnameSelector.length) { + hostnameFlag = true; + } + } + } + } + + if (pathnameSelector) { + if (pathname.indexOf(pathnameSelector) >= 0) { + if (!pathnameMatch || + (colorThemes[pathnameMatch].pathnameSelector.length < pathnameSelector.length)) { + pathnameMatch = item; + pathnameFlag = true; + } + } + } + + if (hostnameFlag && pathnameFlag) { + hostandpathnameMatch = item; + } + } + + if (hostandpathnameMatch) { + return colorThemes[hostandpathnameMatch]; + } + else { + if (hostnameMatch) { + return colorThemes[hostnameMatch]; + } else { + if (pathnameMatch) { + return colorThemes[pathnameMatch]; + } + } + } + + // if no other theme is found use default theme + return colorThemes['default']; + } + + /* + * @function updateStyle + * + * @desc Updates the value of a css variable + * + * @param {string} cssVariable - + * @param {string} configValue - + * @param {string} themeValue - + */ + function updateStyle(containerNode, cssVariable, configValue, themeValue, defaultValue) { + let value = ''; if (typeof configValue === 'string' && configValue) { value = configValue; } else { if (typeof themeValue === 'string' && themeValue) { value = themeValue; } + else { + value = defaultValue; + } } - let index1 = cssContent.indexOf(stylePlaceholder); - let index2 = index1 + stylePlaceholder.length; - while (index1 >= 0 && index2 < cssContent.length) { - cssContent = cssContent.substring(0, index1) + value + cssContent.substring(index2); - index1 = cssContent.indexOf(stylePlaceholder, index2); - index2 = index1 + stylePlaceholder.length; + if ((typeof value === 'string') && value.length) { + containerNode.style.setProperty(cssVariable, value); } - return cssContent; } /* @@ -763,15 +1285,14 @@ * @desc Updates the styling for the menu and highlight information * and returns the updated strings * - * @param {String} cssMenu - CSS template for the button and menu * @param {Object} config - SkipTo.js configuration information object * @param {Boolean} useURLTheme - When true use the theme associated with the URL * * @returns. see @desc */ - function addCSSColors (cssMenu, config, useURLTheme=false) { + function updateCSS (containerNode, config, useURLTheme=false) { + const d = colorThemes['default']; const theme = useURLTheme ? getTheme(config.colorTheme) : {}; - const defaultTheme = getTheme('default'); // Check for display option in theme if ((typeof config.displayOption === 'string') && @@ -782,50 +1303,95 @@ config.displayOption = theme.displayOption; } else { - config.displayOption = defaultTheme.displayOption; + config.displayOption = 'popup'; } } - cssMenu = updateStyle(cssMenu, '$fontFamily', config.fontFamily, theme.fontFamily, defaultTheme.fontFamily); - cssMenu = updateStyle(cssMenu, '$fontSize', config.fontSize, theme.fontSize, defaultTheme.fontSize); + updateStyle(containerNode, '--skipto-font-family', config.fontFamily, theme.fontFamily, d.fontFamily); + updateStyle(containerNode, '--skipto-font-size', config.fontSize, theme.fontSize, d.fontSize); + + updateStyle(containerNode, '--skipto-position-left', config.positionLeft, theme.positionLeft, d.positionLeft); + updateStyle(containerNode, '--skipto-small-break-point', config.smallBreakPoint, theme.smallBreakPoint, d.smallBreakPoint); + updateStyle(containerNode, '--skipto-medium-break-point', config.mediumBreakPoint, theme.mediumBreakPoint, d.mediumBreakPoint); + + updateStyle(containerNode, '--skipto-menu-text-color', config.menuTextColor, theme.menuTextColor, d.menuTextColor); + updateStyle(containerNode, '--skipto-menu-text-dark-color', config.menuTextDarkColor, theme.menuTextDarkColor, d.menuTextDarkColor); + updateStyle(containerNode, '--skipto-menu-background-color', config.menuBackgroundColor, theme.menuBackgroundColor, d.menuTextDarkColor); + updateStyle(containerNode, '--skipto-menu-background-dark-color', config.menuBackgroundDarkColor, theme.menuBackgroundDarkColor, d.menuBackgroundDarkColor); + + updateStyle(containerNode, '--skipto-menuitem-focus-text-color', config.menuitemFocusTextColor, theme.menuitemFocusTextColor, d.menuitemFocusTextColor); + updateStyle(containerNode, '--skipto-menuitem-focus-text-dark-color', config.menuitemFocusTextDarkColor, theme.menuitemFocusTextDarkColor, d.menuitemFocusTextDarkColor); + updateStyle(containerNode, '--skipto-menuitem-focus-background-color', config.menuitemFocusBackgroundColor, theme.menuitemFocusBackgroundColor, d.menuitemFocusBackgroundColor); + updateStyle(containerNode, '--skipto-menuitem-focus-background-dark-color', config.menuitemFocusBackgroundDarkColor, theme.menuitemFocusBackgroundDarkColor, d.menuitemFocusBackgroundDarkColor); + + updateStyle(containerNode, '--skipto-focus-border-color', config.focusBorderColor, theme.focusBorderColor, d.focusBorderColor); + updateStyle(containerNode, '--skipto-focus-border-dark-color', config.focusBorderDarkColor, theme.focusBorderDarkColor, d.focusBorderDarkColor); + + updateStyle(containerNode, '--skipto-button-text-color', config.buttonTextColor, theme.buttonTextColor, d.buttonTextColor); + updateStyle(containerNode, '--skipto-button-text-dark-color', config.buttonTextDarkColor, theme.buttonTextDarkColor, d.buttonTextDarkColor); + updateStyle(containerNode, '--skipto-button-background-color', config.buttonBackgroundColor, theme.buttonBackgroundColor, d.buttonBackgroundColor); + updateStyle(containerNode, '--skipto-button-background-dark-color', config.buttonBackgroundDarkColor, theme.buttonBackgroundDarkColor, d.buttonBackgroundDarkColor); + + updateStyle(containerNode, '--skipto-dialog-text-color', config.dialogTextColor, theme.dialogTextColorr, d.dialogTextColor); + updateStyle(containerNode, '--skipto-dialog-text-dark-color', config.dialogTextDarkColor, theme.dialogTextDarkColor, d.dialogTextDarkColor); + updateStyle(containerNode, '--skipto-dialog-background-color', config.dialogBackgroundColor, theme.dialogBackgroundColor, d.dialogBackgroundColor); + updateStyle(containerNode, '--skipto-dialog-background-dark-color', config.dialogBackgroundDarkColor, theme.dialogBackgroundDarkColor, d.dialogBackgroundDarkColor); + updateStyle(containerNode, '--skipto-dialog-background-title-color', config.dialogBackgroundTitleColor, theme.dialogBackgroundTitleColor, d.dialogBackgroundTitleColor); + updateStyle(containerNode, '--skipto-dialog-background-title-dark-color', config.dialogBackgroundTitleDarkColor, theme.dialogBackgroundTitleDarkColor, d.dialogBackgroundTitleDarkColor); - cssMenu = updateStyle(cssMenu, '$positionLeft', config.positionLeft, theme.positionLeft, defaultTheme.positionLeft); - cssMenu = updateStyle(cssMenu, '$smallBreakPoint', config.smallBreakPoint, theme.smallBreakPoint, defaultTheme.smallBreakPoint); - cssMenu = updateStyle(cssMenu, '$mediumBreakPoint', config.mediumBreakPoint, theme.mediumBreakPoint, defaultTheme.mediumBreakPoint); + let borderWidth, shadowWidth, offset, fontSize; - cssMenu = updateStyle(cssMenu, '$menuTextColor', config.menuTextColor, theme.menuTextColor, defaultTheme.menuTextColor); - cssMenu = updateStyle(cssMenu, '$menuTextDarkColor', config.menuTextDarkColor, theme.menuTextDarkColor, defaultTheme.menuTextDarkColor); - cssMenu = updateStyle(cssMenu, '$menuBackgroundColor', config.menuBackgroundColor, theme.menuBackgroundColor, defaultTheme.menuBackgroundColor); - cssMenu = updateStyle(cssMenu, '$menuBackgroundDarkColor', config.menuBackgroundDarkColor, theme.menuBackgroundDarkColor, defaultTheme.menuBackgroundDarkColor); + [borderWidth, shadowWidth, offset, fontSize] = getHighlightInfo(config.highlightBorderSize); - cssMenu = updateStyle(cssMenu, '$menuitemFocusTextColor', config.menuitemFocusTextColor, theme.menuitemFocusTextColor, defaultTheme.menuitemFocusTextColor); - cssMenu = updateStyle(cssMenu, '$menuitemFocusTextDarkColor', config.menuitemFocusTextDarkColor, theme.menuitemFocusTextDarkColor, defaultTheme.menuitemFocusTextDarkColor); - cssMenu = updateStyle(cssMenu, '$menuitemFocusBackgroundColor', config.menuitemFocusBackgroundColor, theme.menuitemFocusBackgroundColor, defaultTheme.menuitemFocusBackgroundColor); - cssMenu = updateStyle(cssMenu, '$menuitemFocusBackgroundDarkColor', config.menuitemFocusBackgroundDarkColor, theme.menuitemFocusBackgroundDarkColor, defaultTheme.menuitemFocusBackgroundDarkColor); + const shadowBorderWidth = borderWidth + 2 * shadowWidth; - cssMenu = updateStyle(cssMenu, '$focusBorderColor', config.focusBorderColor, theme.focusBorderColor, defaultTheme.focusBorderColor); - cssMenu = updateStyle(cssMenu, '$focusBorderDarkColor', config.focusBorderDarkColor, theme.focusBorderDarkColor, defaultTheme.focusBorderDarkColor); + updateStyle(containerNode, '--skipto-highlight-offset', `${offset}px`, '', ''); + updateStyle(containerNode, '--skipto-highlight-border-width', `${borderWidth}px`, '', ''); + updateStyle(containerNode, '--skipto-highlight-font-size', fontSize, '', ''); + updateStyle(containerNode, '--skipto-highlight-shadow-border-width', `${shadowBorderWidth}px`, '', ''); + updateStyle(containerNode, '--skipto-highlight-border-style', config.highlightBorderStyle, '', ''); - cssMenu = updateStyle(cssMenu, '$buttonTextColor', config.buttonTextColor, theme.buttonTextColor, defaultTheme.buttonTextColor); - cssMenu = updateStyle(cssMenu, '$buttonTextDarkColor', config.buttonTextDarkColor, theme.buttonTextDarkColor, defaultTheme.buttonTextDarkColor); - cssMenu = updateStyle(cssMenu, '$buttonBackgroundColor', config.buttonBackgroundColor, theme.buttonBackgroundColor, defaultTheme.buttonBackgroundColor); - cssMenu = updateStyle(cssMenu, '$buttonBackgroundDarkColor', config.buttonBackgroundDarkColor, theme.buttonBackgroundDarkColor, defaultTheme.buttonBackgroundDarkColor); + updateStyle(containerNode, '--skipto-hidden-text-color', config.hiddenTextColor, '', d.hiddenTextColor); + updateStyle(containerNode, '--skipto-hidden-text-dark-color', config.hiddenTextDarkColor, '', d.hiddenTextDarkColor); + updateStyle(containerNode, '--skipto-hidden-background-color', config.hiddenBackgroundColor, '', d.hiddenBackgroundColor); + updateStyle(containerNode, '--skipto-hidden-background-dark-color', config.hiddenBackgroundDarkColor, '', d.hiddenBackgroundDarkColor); + + updateStyle(containerNode, '--skipto-z-index-1', config.zIndex, theme.zIndex, d.zIndex); + + + const buttonNode = containerNode.querySelector('button'); + const rect = buttonNode.getBoundingClientRect(); + if (buttonNode.classList.contains('show-border')) { + const borderOffset = -1 * rect.height + 4 + 'px'; + containerNode.style.setProperty('--skipto-show-border-offset', borderOffset); + } + else { + if (buttonNode.classList.contains('popup')) { + const popupOffset = -1 * rect.height - 6 + 'px'; + containerNode.style.setProperty('--skipto-popup-offset', popupOffset); + } + } + containerNode.style.setProperty('--skipto-menu-offset', rect.height + 'px'); - cssMenu = updateStyle(cssMenu, '$z1Index', config.zIndex, theme.zIndex, defaultTheme.zIndex); - const z2Index = config.zIndex ? + + const zIndex2 = config.zIndex ? (parseInt(config.zIndex) + 1).toString() : - '2000002'; + '2000001'; + + updateStyle(containerNode, '--skipto-z-index-2', zIndex2, ''); - cssMenu = updateStyle(cssMenu, '$z2Index', z2Index, '', defaultTheme.z2Index); + const zIndexHighlight = config.zIndex ? + (parseInt(config.zIndex) - 1).toString() : + '199999'; + + updateStyle(containerNode, '--skipto-z-index-highlight', zIndexHighlight, ''); // Special case for theme configuration used in Illinois theme if (typeof theme.highlightTarget === 'string') { config.highlightTarget = theme.highlightTarget; } - return cssMenu; - } /* @@ -835,109 +1401,20 @@ * * @param {Object} attachNode - DOM element node to attach button and menu container element * @param {Object} config - Configuration information object - * @param {String} skipYToStyleId - Id used for the skipto container element * @param {Boolean} useURLTheme - When true use the theme associated with the URL */ - function renderStyleElement (attachNode, config, skipToId, useURLTheme=false) { - let cssMenu = cssMenuTemplate.textContent.slice(0); - - cssMenu = addCSSColors(cssMenu, config, useURLTheme); - - let styleNode = attachNode.querySelector(`#${SKIP_TO_MENU_STYLE_ID}`); + function renderStyleElement (attachNode, config, useURLTheme=false) { + let styleNode = attachNode.querySelector(`style`); if (!styleNode) { styleNode = document.createElement('style'); attachNode.appendChild(styleNode); - styleNode.setAttribute('id', `${SKIP_TO_MENU_STYLE_ID}`); + styleNode.textContent = cssStyleTemplate.textContent; } - styleNode.textContent = cssMenu; - } - - /* utils.js */ - - /* Constants */ - const debug$b = new DebugLogging('Utils', false); - debug$b.flag = false; - - - /* - * @function getAttributeValue - * - * @desc Return attribute value if present on element, - * otherwise return empty string. - * - * @returns {String} see @desc - */ - function getAttributeValue (element, attribute) { - let value = element.getAttribute(attribute); - return (value === null) ? '' : normalize(value); - } - - /* - * @function normalize - * - * @desc Trim leading and trailing whitespace and condense all - * internal sequences of whitespace to a single space. Adapted from - * Mozilla documentation on String.prototype.trim polyfill. Handles - * BOM and NBSP characters. - * - * @return {String} see @desc - */ - function normalize (s) { - let rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; - return s.replace(rtrim, '').replace(/\s+/g, ' '); - } - - /** - * @fuction isNotEmptyString - * - * @desc Returns true if the string has content, otherwise false - * - * @param {Boolean} see @desc - */ - function isNotEmptyString (str) { - return (typeof str === 'string') && str.length && str.trim() && str !== " "; - } - - /** - * @fuction isVisible - * - * @desc Returns true if the element is visible in the graphical rendering - * - * @param {node} elem - DOM element node of a labelable element - */ - function isVisible (element) { - - function isDisplayNone(el) { - if (!el || (el.nodeType !== Node.ELEMENT_NODE)) { - return false; - } - - if (el.hasAttribute('hidden')) { - return true; - } - - const style = window.getComputedStyle(el, null); - const display = style.getPropertyValue("display"); - if (display === 'none') { - return true; - } - - // check ancestors for display none - if (el.parentNode) { - return isDisplayNone(el.parentNode); - } - - return false; - } - - const computedStyle = window.getComputedStyle(element); - let visibility = computedStyle.getPropertyValue('visibility'); - if ((visibility === 'hidden') || (visibility === 'collapse')) { - return false; - } - - return !isDisplayNone(element); + const containerNode = attachNode.querySelector('.container'); + + updateCSS(containerNode, config, useURLTheme); + } /* shortcutInfoDialog.js */ @@ -946,174 +1423,122 @@ const debug$a = new DebugLogging('[shortcutsInfoDialog]', false); debug$a.flag = false; - const defaultStyleOptions$3 = colorThemes['default']; - - - const styleTemplate$1 = document.createElement('template'); - styleTemplate$1.textContent = ` -/* infoDialog.css */ - -dialog#skip-to-info-dialog { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%,-50%); - font-family: $fontFamily; - font-size: $fontSize; - max-width: 70%; - margin: 0; - padding: 0; - background-color: light-dark($dialogBackgroundColor, $dialogBackgroundDarkColor); - color: light-dark($dialogTextColor, $dialogTextDarkColor); - border-width: 2px; - border-style: solid; - border-color: light-dark($focusBorderColor, $focusBorderDarkColor); - border-radius: 5px; - z-index: 2000001; - max-width: 350px; -} - -dialog#skip-to-info-dialog .header { - margin: 0; - margin-bottom: 0.5em; - padding: 4px; - border-width: 0; - border-bottom-width: 1px; - border-style: solid; - border-color: light-dark($focusBorderColor, $focusBorderDarkColor); - border-top-left-radius: 5px; - border-top-right-radius: 5px; - font-weight: bold; - background-color: light-dark($dialogBackgroundTitleColor, $dialogBackgroundTitleDarkColor); - color: light-dark($dialogTextColor, $dialogTextDarkColor); - position: relative; - font-size: 100%; -} - -dialog#skip-to-info-dialog .header h2 { - margin: 0; - padding: 0; - font-size: 1em; -} - -dialog#skip-to-info-dialog .header button { - position: absolute; - top: -0.25em; - right: 0; - border: none; - background: transparent; - font-weight: bold; - color: light-dark(black, white); -} - -dialog#skip-to-info-dialog .content { - margin-left: 2em; - margin-right: 2em; - margin-top: 0; - margin-bottom: 2em; -} - -dialog#skip-to-info-dialog .content .desc { - margin: 0.25em; - text-align: center; -} - -dialog#skip-to-info-dialog .content .privacy-label { - margin: 0; - margin-top: 1em; - text-align: center; - font-weight: bold; -} - -dialog#skip-to-info-dialog .content .privacy { - text-align: center; - margin-bottom: 1em; -} - - -dialog#skip-to-info-dialog .content .happy { - text-align: center; - font-family: 'Brush Script MT', cursive; - font-size: 200%; - letter-spacing: 0.05em; -} - - -dialog#skip-to-info-dialog .content .version, -dialog#skip-to-info-dialog .content .copyright { - margin-top: 0.5em; - text-align: center; -} - -dialog#skip-to-info-dialog .content table { - width: auto; -} - -dialog#skip-to-info-dialog .content caption { - margin: 0; - padding: 0; - margin-top: 1em; - text-align: left; - font-weight: bold; - font-size: 110%; -} - -dialog#skip-to-info-dialog .content th { - margin: 0; - padding: 0; - padding-top: 0.125em; - padding-bottom: 0.125em; - text-align: left; - font-weight: bold; - font-size: 100%; -} - -dialog#skip-to-info-dialog .content th { - border-bottom-width: 1px; - border-bottom-style: solid; - border-bottom-color: light-dark(#999999, #777777); -} - -dialog#skip-to-info-dialog .content th.shortcut { - width: 2.5em; -} - -dialog#skip-to-info-dialog .content td { - margin: 0; - padding: 0; - padding-top: 0.125em; - padding-bottom: 0.125em; - text-align: left; - font-size: 100%; -} - - -dialog#skip-to-info-dialog .content table tr:nth-child(even) { - background-color: light-dark(#eeeeee, #111111); -} - -dialog#skip-to-info-dialog .buttons { - float: right; - margin-right: 0.5em; - margin-bottom: 0.5em; -} - -dialog#skip-to-info-dialog button { - margin: 6px; -} - -dialog#skip-to-info-dialog .buttons button { - min-width: 5em; -} - -button:focus { - outline: 2px solid currentColor; - outline-offset: 2px; -} - -button:hover { - cursor: pointer; -} + const templateInfoDialog = document.createElement('template'); + templateInfoDialog.innerHTML = ` + +
    +

    Keyboard Shortcuts

    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Landmark Regions
    KeyDescription
    rNext region
    RPrevious region
    mMain regions
    nNavigation regions
    cComplementary regions
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Headings
    KeyDescription
    hNext heading
    HPrevious heading
    1Level 1 headings
    2Level 2 headings
    3Level 3 headings
    4Level 4 headings
    5Level 5 headings
    6Level 6 headings
    +
    + +
    +
    + SkipTo.js is a free and open source utility to support the WCAG 2.4.1 Bypass Block requirement. +
    +
    + Privacy +
    +
    + SkipTo.js does not collect or store any information about users or work with any other parties to collect or share user browsing information. +
    +
    + Happy Skipping! +
    +
    + Version 5.9.0 +
    + +
    + +
    + + +
    + +
    `; /* @@ -1121,455 +1546,171 @@ button:hover { * */ - class SkipToContentInfoDialog extends HTMLElement { - constructor () { - - super(); - this.attachShadow({ mode: 'open' }); + class SkipToContentInfoDialog { + constructor (attachElem) { // Get references - this.infoDialog = document.createElement('dialog'); - this.infoDialog.id = 'skip-to-info-dialog'; - this.shadowRoot.appendChild(this.infoDialog); - - const headerElem = document.createElement('div'); - headerElem.className = 'header'; - this.infoDialog.appendChild(headerElem); + attachElem.appendChild(templateInfoDialog.content.cloneNode(true)); - this.h2Elem = document.createElement('h2'); - this.h2Elem.textContent = 'Keyboard Shortcuts'; - headerElem.appendChild(this.h2Elem); + this.dialogElem = attachElem.querySelector('dialog'); - this.closeButton1 = document.createElement('button'); - this.closeButton1.textContent = '✕'; - headerElem.appendChild(this.closeButton1); - this.closeButton1.addEventListener('click', this.onCloseButtonClick.bind(this)); - this.closeButton1.addEventListener('keydown', this.onKeyDown.bind(this)); + this.closeButtonElem1 = attachElem.querySelector('.header button'); + this.closeButtonElem1.addEventListener('click', this.onCloseButtonClick.bind(this)); + this.closeButtonElem1.addEventListener('keydown', this.onKeyDown.bind(this)); - this.contentElem = document.createElement('div'); - this.contentElem.className = 'content'; - this.infoDialog.appendChild(this.contentElem); + this.shortcutContentElem = attachElem.querySelector('.content.shortcuts'); + this.aboutContentElem = attachElem.querySelector('.content.about'); - const buttonsElem = document.createElement('div'); - buttonsElem.className = 'buttons'; - this.infoDialog.appendChild(buttonsElem); + const moreInfoButtonElem = attachElem.querySelector('.buttons button.more'); + moreInfoButtonElem.addEventListener('click', this.onMoreInfoClick.bind(this)); - this.moreInfoButton = document.createElement('button'); - this.moreInfoButton.textContent = 'More Information'; - buttonsElem.appendChild(this.moreInfoButton); - this.moreInfoButton.addEventListener('click', this.onMoreInfoClick.bind(this)); - - this.closeButton2 = document.createElement('button'); - this.closeButton2.textContent = 'Close'; - buttonsElem.appendChild(this.closeButton2); - this.closeButton2.addEventListener('click', this.onCloseButtonClick.bind(this)); - this.closeButton2.addEventListener('keydown', this.onKeyDown.bind(this)); - - this.moreInfoURL = ''; + this.closeButtonElem2 = attachElem.querySelector('.buttons button.close'); + this.closeButtonElem2.addEventListener('click', this.onCloseButtonClick.bind(this)); + this.closeButtonElem2.addEventListener('keydown', this.onKeyDown.bind(this)); + return this; } onCloseButtonClick () { - this.infoDialog.close(); + this.dialogElem.close(); } - openDialog () { - this.infoDialog.showModal(); - this.closeButton2.focus(); - } + openDialog (content) { + this.content = content; - onMoreInfoClick () { - if (this.moreInfoURL) { - window.open(this.moreInfoURL, '_blank').focus(); + if (content === 'shortcuts') { + this.aboutContentElem.style.display = 'none'; + this.shortcutContentElem.style.display = 'block'; } - } - - configureStyle(config={}) { - - function updateOption(style, option, configOption, defaultOption) { - if (configOption) { - return style.replaceAll(option, configOption); - } - else { - return style.replaceAll(option, defaultOption); - } + else { + this.shortcutContentElem.style.display = 'none'; + this.aboutContentElem.style.display = 'block'; } + this.dialogElem.showModal(); + this.closeButtonElem2.focus(); + } - // make a copy of the template - let style = styleTemplate$1.textContent.slice(0); - - style = updateOption(style, - '$fontFamily', - config.fontFamily, - defaultStyleOptions$3.fontFamily); - - style = updateOption(style, - '$fontSize', - config.fontSize, - defaultStyleOptions$3.fontSize); - - style = updateOption(style, - '$focusBorderColor', - config.focusBorderColor, - defaultStyleOptions$3.focusBorderColor); - - style = updateOption(style, - '$focusBorderDarkColor', - config.focusBorderDarkColor, - defaultStyleOptions$3.focusBorderDarkColor); - - style = updateOption(style, - '$dialogTextColor', - config.dialogTextColor, - defaultStyleOptions$3.dialogTextColor); - - style = updateOption(style, - '$dialogextDarkColor', - config.dialogextDarkColor, - defaultStyleOptions$3.dialogextDarkColor); - - style = updateOption(style, - '$dialogBackgroundColor', - config.dialogBackgroundColor, - defaultStyleOptions$3.dialogBackgroundColor); - - style = updateOption(style, - '$dialogBackgroundDarkColor', - config.dialogBackgroundDarkColor, - defaultStyleOptions$3.dialogBackgroundDarkColor); - - style = updateOption(style, - '$dialogBackgroundTitleColor', - config.dialogBackgroundTitleColor, - defaultStyleOptions$3.dialogBackgroundTitleColor); - - style = updateOption(style, - '$dialogBackgroundTitleDarkColor', - config.dialogBackgroundTitleDarkColor, - defaultStyleOptions$3.dialogBackgroundTitleDarkColor); - - - let styleNode = this.shadowRoot.querySelector('style'); - - if (styleNode) { - styleNode.remove(); + onMoreInfoClick () { + const url = this.content === 'shortcut' ? + MORE_SHORTCUT_INFO_URL : + MORE_ABOUT_INFO_URL; + if (url) { + window.open(url, '_blank').focus(); } - - styleNode = document.createElement('style'); - styleNode.textContent = style; - this.shadowRoot.appendChild(styleNode); - } + onKeyDown (event) { - updateShortcutContent (config) { + if ((event.key === "Tab") && + !event.altKey && + !event.ctlKey && + !event.metaKey) { - while (this.contentElem.lastElementChild) { - this.contentElem.removeChild(this.contentElem.lastElementChild); + if (event.shiftKey && + (event.currentTarget === this.closeButtonElem1)) { + this.closeButtonElem2.focus(); + event.preventDefault(); + event.stopPropagation(); } - this.moreInfoURL = MORE_SHORTCUT_INFO_URL; - - this.h2Elem.textContent = config.shortcutsInfoLabel; - this.closeButton1.setAttribute('aria-label', config.closeLabel); - this.closeButton2.textContent = config.closeLabel; - this.moreInfoButton.textContent = config.moreInfoLabel; - - function addRow(tbodyElem, shortcut, desc) { - - const trElem = document.createElement('tr'); - tbodyElem.appendChild(trElem); - - const tdElem1 = document.createElement('td'); - tdElem1.className = 'shortcut'; - tdElem1.textContent = shortcut; - trElem.appendChild(tdElem1); - - const tdElem2 = document.createElement('td'); - tdElem2.className = 'desc'; - tdElem2.textContent = desc; - trElem.appendChild(tdElem2); + if (!event.shiftKey && + (event.currentTarget === this.closeButtonElem2)) { + this.closeButtonElem1.focus(); + event.preventDefault(); + event.stopPropagation(); } - - // Regions - - const tableElem1 = document.createElement('table'); - this.contentElem.appendChild(tableElem1); - - const captionElem1 = document.createElement('caption'); - captionElem1.textContent = config.landmarkGroupLabel; - tableElem1.appendChild(captionElem1); - - const theadElem1 = document.createElement('thead'); - tableElem1.appendChild(theadElem1); - - const trElem1 = document.createElement('tr'); - theadElem1.appendChild(trElem1); - - const thElem1 = document.createElement('th'); - thElem1.className = 'shortcut'; - thElem1.textContent = config.msgKey; - trElem1.appendChild(thElem1); - - const thElem2 = document.createElement('th'); - thElem2.className = 'desc'; - thElem2.textContent = config.msgDescription; - trElem1.appendChild(thElem2); - - const tbodyElem1 = document.createElement('tbody'); - tableElem1.appendChild(tbodyElem1); - - addRow(tbodyElem1, config.shortcutRegionNext, config.msgNextRegion); - addRow(tbodyElem1, config.shortcutRegionPrevious, config.msgPreviousRegion); - addRow(tbodyElem1, config.shortcutRegionMain, config.msgMainRegions); - addRow(tbodyElem1, config.shortcutRegionNavigation, config.msgNavigationRegions); - addRow(tbodyElem1, config.shortcutRegionComplementary, config.msgComplementaryRegions); - - // Headings - - const tableElem2 = document.createElement('table'); - this.contentElem.appendChild(tableElem2); - - const captionElem2 = document.createElement('caption'); - captionElem2.textContent = config.headingGroupLabel; - tableElem2.appendChild(captionElem2); - - const theadElem2 = document.createElement('thead'); - tableElem2.appendChild(theadElem2); - - const trElem2 = document.createElement('tr'); - theadElem2.appendChild(trElem2); - - const thElem3 = document.createElement('th'); - thElem3.className = 'shortcut'; - thElem3.textContent = config.msgKey; - trElem2.appendChild(thElem3); - - const thElem4 = document.createElement('th'); - thElem4.className = 'desc'; - thElem4.textContent = config.msgDescription; - trElem2.appendChild(thElem4); - - const tbodyElem2 = document.createElement('tbody'); - tableElem2.appendChild(tbodyElem2); - - addRow(tbodyElem2, config.shortcutHeadingNext, config.msgNextHeading); - addRow(tbodyElem2, config.shortcutHeadingPrevious, config.msgPreviousHeading); - addRow(tbodyElem2, config.shortcutHeadingH1, config.msgH1Headings); - addRow(tbodyElem2, config.shortcutHeadingH2, config.msgH2Headings); - addRow(tbodyElem2, config.shortcutHeadingH3, config.msgH3Headings); - addRow(tbodyElem2, config.shortcutHeadingH4, config.msgH4Headings); - addRow(tbodyElem2, config.shortcutHeadingH5, config.msgH5Headings); - addRow(tbodyElem2, config.shortcutHeadingH6, config.msgH6Headings); - + } } + } - updateAboutContent (config) { - - while (this.contentElem.lastElementChild) { - this.contentElem.removeChild(this.contentElem.lastElementChild); - } + /* shortcutsMessage.js */ - this.moreInfoURL = MORE_PAGE_SCRIPT_INFO_URL; + /* Constants */ + const debug$9 = new DebugLogging('[shortcutsMessage]', false); + debug$9.flag = false; + + const templateMessage = document.createElement('template'); + templateMessage.innerHTML = ` + +`; - this.h2Elem.textContent = config.aboutInfoLabel; - this.closeButton1.setAttribute('aria-label', config.closeLabel); - this.closeButton2.textContent = config.closeLabel; - this.moreInfoButton.textContent = config.moreInfoLabel; - let divElem = document.createElement('div'); - divElem.className = 'desc'; + class ShortcutsMessage { + constructor (attachElem) { - divElem.textContent = config.aboutDesc; - this.contentElem.appendChild(divElem); + attachElem.appendChild(templateMessage.content.cloneNode(true)); - divElem = document.createElement('div'); - divElem.className = 'privacy-label'; - divElem.textContent = config.aboutPrivacyLabel; - this.contentElem.appendChild(divElem); + // Get references - divElem = document.createElement('div'); - divElem.className = 'privacy'; - divElem.textContent = config.aboutPrivacy; - this.contentElem.appendChild(divElem); + this.messageElem = attachElem.querySelector(`#${MESSAGE_ID}`); - divElem = document.createElement('div'); - divElem.className = 'happy'; - divElem.textContent = config.aboutHappy; - this.contentElem.appendChild(divElem); + this.contentElem = this.messageElem.querySelector(`.content`); - divElem = document.createElement('div'); - divElem.className = 'version'; - divElem.textContent = config.aboutVersion; - this.contentElem.appendChild(divElem); + this.timeoutShowID = false; + this.timeoutFadeID = false; - divElem = document.createElement('div'); - divElem.className = 'copyright'; - divElem.textContent = config.aboutCopyright; - this.contentElem.appendChild(divElem); + return this; + } + close() { + this.messageElem.classList.remove('show'); + this.messageElem.classList.remove('fade'); + this.messageElem.classList.add('hidden'); } - onKeyDown (event) { + open(message) { + clearInterval(this.timeoutFadeID); + clearInterval(this.timeoutShowID); + this.messageElem.classList.remove('hidden'); + this.messageElem.classList.remove('fade'); + this.messageElem.classList.add('show'); + this.contentElem.textContent = message; - if ((event.key === "Tab") && - !event.altKey && - !event.ctlKey && - !event.metaKey) { + const msg = this; - if (event.shiftKey && - (event.currentTarget === this.closeButton1)) { - this.closeButton2.focus(); - event.preventDefault(); - event.stopPropagation(); - } + this.timeoutFadeID = setTimeout( () => { + msg.messageElem.classList.add('fade'); + msg.messageElem.classList.remove('show'); + }, 3000); + + this.timeoutShowID = setTimeout( () => { + msg.close(); + }, 4000); - if (!event.shiftKey && - (event.currentTarget === this.closeButton2)) { - this.closeButton1.focus(); - event.preventDefault(); - event.stopPropagation(); - } - } } + } /* highlight.js */ /* Constants */ - const debug$9 = new DebugLogging('highlight', false); - debug$9.flag = false; + const debug$8 = new DebugLogging('highlight', false); + debug$8.flag = false; const minWidth = 68; const minHeight = 27; - const defaultStyleOptions$2 = colorThemes['default']; - - const styleHighlightTemplate = document.createElement('template'); - styleHighlightTemplate.textContent = ` -:root { - color-scheme: light dark; -} - -#${HIGHLIGHT_ID} { - margin: 0; - padding: 0; - position: absolute; - border-radius: $highlightOffsetpx; - border: $shadowBorderWidthpx solid light-dark($menuBackgroundColor, $menuBackgroundDarkColor); - box-sizing: border-box; - pointer-events:none; - z-index: $zHighlight; -} - -#${HIGHLIGHT_ID}.hasInfoBottom, -#${HIGHLIGHT_ID} .overlay-border.hasInfoBottom { - border-radius: $highlightOffsetpx $highlightOffsetpx $highlightOffsetpx 0; -} - -#${HIGHLIGHT_ID}.hasInfoTop, -#${HIGHLIGHT_ID} .overlay-border.hasInfoTop { - border-radius: 0 $highlightOffsetpx $highlightOffsetpx $highlightOffsetpx; -} - -#${HIGHLIGHT_ID} .overlay-border { - margin: 0; - padding: 0; - position: relative; - border-radius: $highlightOffsetpx; - border: $overlayBorderWidthpx $highlightBorderStyle light-dark($focusBorderColor, $focusBorderDarkColor); - z-index: $zHighlight; - box-sizing: border-box; - pointer-events:none; - background: transparent; -} - - -@keyframes fadeIn { - 0% { opacity: 0; } - 100% { opacity: 1; } -} - -#hidden-elem-msg { - position: absolute; - margin: 0; - padding: .25em; - background-color: light-dark($hiddenHeadingBackgroundColor, $hiddenHeadingBackgroundDarkColor); - color: light-dark($hiddenHeadingColor, $hiddenHeadingDarkColor); - font-family: $fontFamily; - font-size: $fontSize; - font-style: italic; - font-weight: bold; - text-align: center; - animation: fadeIn 1.5s; - z-index: $zHighlight; -} - -#${HIGHLIGHT_ID} .overlay-info { - margin: 0; - padding: 2px; - position: relative; - text-align: left; - font-size: $fontSize; - font-family: $fontFamily; - border: $infoBorderWidthpx solid light-dark($menuBackgroundColor, $menuBackgroundDarkColor); - background-color: light-dark($menuBackgroundColor, $menuBackgroundDarkColor); - color: light-dark($menuTextColor, $menuTextDarkColor); - z-index: $zHighlight; - overflow: hidden; - text-overflow: ellipsis; - pointer-events:none; -} - -#${HIGHLIGHT_ID} .overlay-info.hasInfoTop { - border-radius: $highlightOffsetpx $highlightOffsetpx 0 0; -} - -#${HIGHLIGHT_ID} .overlay-info.hasInfoBottom { - border-radius: 0 0 $highlightOffsetpx $highlightOffsetpx; -} - -@media (forced-colors: active) { - - #${HIGHLIGHT_ID} { - border-color: ButtonBorder; - } - - #${HIGHLIGHT_ID} .overlay-border { - border-color: ButtonBorder; - } - - #${HIGHLIGHT_ID} .overlay-border.skip-to-hidden { - background-color: ButtonFace; - color: ButtonText; - } - - #${HIGHLIGHT_ID} .overlay-info { - border-color: ButtonBorder; - background-color: ButtonFace; - color: ButtonText; - } - -} -`; - /* * @class HighlightElement * */ - class HighlightElement extends HTMLElement { + class HighlightElement { - constructor() { - super(); - this.attachShadow({ mode: 'open' }); + constructor(attachElem) { // Get references this.overlayElem = document.createElement('div'); this.overlayElem.id = HIGHLIGHT_ID; - this.shadowRoot.appendChild(this.overlayElem); + attachElem.appendChild(this.overlayElem); this.overlayElem.style.display = 'none'; this.borderElem = document.createElement('div'); @@ -1581,38 +1722,29 @@ button:hover { this.overlayElem.appendChild(this.infoElem); this.hiddenElem = document.createElement('div'); - this.hiddenElem.id = 'hidden-elem-msg'; - this.shadowRoot.appendChild(this.hiddenElem); + this.hiddenElem.id = HIDDEN_ELEMENT_ID; + attachElem.appendChild(this.hiddenElem); this.hiddenElem.style.display = 'none'; this.borderWidth = 0; - this.borderContrast = 0; + this.shadowWidth = 0; this.offset = 0; this.msgHeadingIsHidden = ''; - this.configureStyle(); + this.configureMessageSizes(); } /* - * @method configureStyle + * @method configureMessageSizes * * @desc Updates stylesheet for styling the highlight information * * @param {Object} config : color and font information */ - configureStyle(config={}) { - - function updateOption(style, option, configOption, defaultOption) { - if (configOption) { - return style.replaceAll(option, configOption); - } - else { - return style.replaceAll(option, defaultOption); - } - } + configureMessageSizes(config={}) { // Get i18n Messages @@ -1628,160 +1760,7 @@ button:hover { config.msgElemenIsHidden : 'Element is hidden'; - - // make a copy of the template - let style = styleHighlightTemplate.textContent.slice(0); - - style = updateOption(style, - '$fontFamily', - config.fontFamily, - defaultStyleOptions$2.fontFamily); - - style = updateOption(style, - '$buttonBackgroundColor', - config.buttonBackgroundColor, - defaultStyleOptions$2.buttonBackgroundColor); - - style = updateOption(style, - '$buttonBackgroundDarkColor', - config.buttonBackgroundDarkColor, - defaultStyleOptions$2.buttonBackgroundDarkColor); - - style = updateOption(style, - '$focusBorderColor', - config.focusBorderColor, - defaultStyleOptions$2.focusBorderColor); - - style = updateOption(style, - '$focusBorderDarkColor', - config.focusBorderDarkColor, - defaultStyleOptions$2.focusBorderDarkColor); - - style = updateOption(style, - '$menuBackgroundColor', - config.menuBackgroundColor, - defaultStyleOptions$2.menuBackgroundColor); - - style = updateOption(style, - '$menuBackgroundDarkColor', - config.menuBackgroundDarkColor, - defaultStyleOptions$2.menuBackgroundDarkColor); - - style = updateOption(style, - '$menuTextColor', - config.menuTextColor, - defaultStyleOptions$2.menuTextColor); - - style = updateOption(style, - '$menuTextDarkColor', - config.menuTextDarkColor, - defaultStyleOptions$2.menuTextDarkColor); - - style = updateOption(style, - '$hiddenHeadingColor', - config.hiddenHeadingColor, - defaultStyleOptions$2.hiddenHeadingColor); - - style = updateOption(style, - '$hiddenHeadingDarkColor', - config.hiddenHeadingDarkColor, - defaultStyleOptions$2.hiddenHeadingDarkColor); - - style = updateOption(style, - '$hiddenHeadingBackgroundColor', - config.hiddenHeadingBackgroundColor, - defaultStyleOptions$2.hiddenHeadingBackgroundColor); - - style = updateOption(style, - '$hiddenHeadingBackgroundDarkColor', - config.hiddenHeadingBackgroundDarkColor, - defaultStyleOptions$2.hiddenHeadingBackgroundDarkColor); - - style = updateOption(style, - '$zHighlight', - config.zHighlight, - defaultStyleOptions$2.zHighlight); - - style = updateOption(style, - '$highlightBorderStyle', - config.highlightBorderStyle, - defaultStyleOptions$2.highlightBorderStyle); - - const highlightBorderSize = config.highlightBorderSize ? - config.highlightBorderSize : - defaultStyleOptions$2.highlightBorderSize; - - switch (highlightBorderSize) { - case 'small': - this.borderWidth = 2; - this.borderContrast = 1; - this.offset = 4; - this.fontSize = '12pt'; - break; - - case 'medium': - this.borderWidth = 3; - this.borderContrast = 2; - this.offset = 4; - this.fontSize = '13pt'; - break; - - case 'large': - this.borderWidth = 4; - this.borderContrast = 3; - this.offset = 6; - this.fontSize = '14pt'; - break; - - case 'x-large': - this.borderWidth = 6; - this.borderContrast = 3; - this.offset = 8; - this.fontSize = '16pt'; - break; - - default: - this.borderWidth = 2; - this.borderContrast = 1; - this.offset = 4; - this.fontSize = '12pt'; - break; - } - - style = updateOption(style, - '$fontSize', - this.fontSize, - defaultStyleOptions$2.fontSize); - - style = updateOption(style, - '$highlightOffset', - this.offset, - this.offset); - - style = updateOption(style, - '$overlayBorderWidth', - this.borderWidth, - this.borderWidth); - - style = updateOption(style, - '$shadowBorderWidth', - this.borderWidth + 2 * this.borderContrast, - this.borderWidth + 2 * this.borderContrast); - - style = updateOption(style, - '$infoBorderWidth', - this.borderWidth, - this.borderWidth); - - let styleNode = this.shadowRoot.querySelector('style'); - - if (styleNode) { - styleNode.remove(); - } - - styleNode = document.createElement('style'); - styleNode.textContent = style; - this.shadowRoot.appendChild(styleNode); + [this.borderWidth, this.shadowWidth, this.offset, this.fontSize] = getHighlightInfo(config.highlightBorderSize); } @@ -1797,7 +1776,7 @@ button:hover { * @param {Boolean} force : If true override isRduced */ - highlight(elem, highlightTarget, info='', force=false) { + highlight(elem, highlightTarget='instant', info='', force=false) { let scrollElement; const mediaQuery = window.matchMedia(`(prefers-reduced-motion: reduce)`); const isReduced = !mediaQuery || mediaQuery.matches; @@ -1807,9 +1786,9 @@ button:hover { const rect = elem.getBoundingClientRect(); // If target element is hidden create a visible element - debug$9.flag && debug$9.log(`[ info]: ${info}`); - debug$9.flag && debug$9.log(`[ rect]: Left: ${rect.left} Top: ${rect.top} Width: ${rect.width} height: ${rect.height}`); - debug$9.flag && debug$9.log(`[isHidden]: ${this.isElementHidden(elem)}`); + debug$8.flag && debug$8.log(`[ info]: ${info}`); + debug$8.flag && debug$8.log(`[ rect]: Left: ${rect.left} Top: ${rect.top} Width: ${rect.width} height: ${rect.height}`); + debug$8.flag && debug$8.log(`[isHidden]: ${this.isElementHidden(elem)}`); if (this.isElementHidden(elem)) { // If element is hidden make hidden element message visible @@ -1826,7 +1805,7 @@ button:hover { info, 0, this.borderWidth, - this.borderContrast); + this.shadowWidth); } else { this.hiddenElem.style.display = 'none'; @@ -1834,7 +1813,7 @@ button:hover { info, this.offset, this.borderWidth, - this.borderContrast); + this.shadowWidth); } if (this.isElementInHeightLarge(elem)) { @@ -1859,15 +1838,15 @@ button:hover { * @param {String} info - Description of the element * @param {Number} offset - Number of pixels for offset * @param {Number} borderWidth - Number of pixels for border width - * @param {Number} borderContrast - Number of pixels to provide border contrast + * @param {Number} shadowWidth - Number of pixels to provide border contrast * */ - updateHighlightElement (elem, info, offset, borderWidth, borderContrast) { + updateHighlightElement (elem, info, offset, borderWidth, shadowWidth) { - const adjRect = this.getAdjustedRect(elem, offset, borderWidth, borderContrast); + const adjRect = this.getAdjustedRect(elem, offset, borderWidth, shadowWidth); - const borderElemOffset = -1 * (this.borderWidth + this.borderContrast); + const borderElemOffset = -1 * (this.borderWidth + this.shadowWidth); this.overlayElem.style.left = adjRect.left + 'px'; this.overlayElem.style.top = adjRect.top + 'px'; @@ -1876,8 +1855,8 @@ button:hover { this.overlayElem.style.width = adjRect.width + 'px'; this.overlayElem.style.height = adjRect.height + 'px'; - this.borderElem.style.width = (adjRect.width - (2 * borderContrast)) + 'px'; - this.borderElem.style.height = (adjRect.height - (2 * borderContrast)) + 'px'; + this.borderElem.style.width = (adjRect.width - (2 * shadowWidth)) + 'px'; + this.borderElem.style.height = (adjRect.height - (2 * shadowWidth)) + 'px'; this.overlayElem.style.display = 'block'; @@ -1886,7 +1865,7 @@ button:hover { this.infoElem.style.display = 'inline-block'; this.infoElem.textContent = info; - const infoElemOffsetLeft = -1 * (borderWidth + 2 * borderContrast); + const infoElemOffsetLeft = -1 * (borderWidth + 2 * shadowWidth); this.infoElem.style.left = infoElemOffsetLeft + 'px'; const infoElemRect = this.infoElem.getBoundingClientRect(); @@ -1908,7 +1887,7 @@ button:hover { // Info is displayed below the highlighted element when it is at the top of // the window - const infoElemOffsetTop = -1 * (borderWidth + borderContrast); + const infoElemOffsetTop = -1 * (borderWidth + shadowWidth); this.overlayElem.classList.remove('hasInfoTop'); this.borderElem.classList.remove('hasInfoTop'); @@ -1939,12 +1918,12 @@ button:hover { * @param {Object} elem - DOM node of element to be highlighted * @param {Number} offset - Number of pixels for offset * @param {Number} borderWidth - Number of pixels for border width - * @param {Number} borderContrast - Number of pixels to provide border contrast + * @param {Number} shadowWidth - Number of pixels to provide border contrast * * @returns see @desc */ - getAdjustedRect(elem, offset, borderWidth, borderContrast) { + getAdjustedRect(elem, offset, borderWidth, shadowWidth) { const rect = elem.getBoundingClientRect(); @@ -1955,7 +1934,7 @@ button:hover { height: 0 }; - const offsetBorder = offset + borderWidth + 2 * borderContrast; + const offsetBorder = offset + borderWidth + 2 * shadowWidth; adjRect.left = rect.left > offset ? Math.round(rect.left + (-1 * offsetBorder) + window.scrollX) : @@ -2103,231 +2082,6 @@ button:hover { } - /* shortcutsMessage.js */ - - /* Constants */ - const debug$8 = new DebugLogging('[shortcutsMessage]', false); - debug$8.flag = false; - - const defaultStyleOptions$1 = colorThemes['default']; - - const styleTemplate = document.createElement('template'); - styleTemplate.textContent = ` -/* shortcutsMessage.css */ -:root { - color-scheme: light dark; -} - -#${MESSAGE_ID} { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%,-50%); - - font-family: $fontFamily; - font-size: $fontSize; - max-width: 70%; - margin: 0; - padding: 0; - background-color: light-dark($dialogBackgroundColor, $dialogBackgroundDarkColor); - border: 2px solid light-dark($focusBorderColor, $focusBorderDarkColor); - border-radius: 5px; - color: light-dark($dialogTextColor, $dialogTextDarkColor); - z-index: 2000001; - opacity: 1; -} - -#${MESSAGE_ID} .header { - margin: 0; - padding: 4px; - border-bottom: 1px solid light-dark($focusBorderColor, $focusBorderDarkColor); - border-top-left-radius: 5px; - border-top-right-radius: 5px; - font-weight: bold; - background-color: light-dark($dialogBackgroundTitleColor, $dialogBackgroundTitleDarkColor); - color light-dark($dialogTextColor, $dialogTextDarkColor); - font-size: 100%; -} - -#${MESSAGE_ID} .content { - margin-left: 2em; - margin-right: 2em; - margin-top: 2em; - margin-bottom: 2em; - background-color: light-dark($dialogBackgroundColor, $dialogBackgroundDarkColor); - color: light-dark($dialogTextColor, $dialogTextDarkColor); - font-size: 110%; - text-algin: center; -} - -#${MESSAGE_ID}.hidden { - display: none; -} - -#${MESSAGE_ID}.show { - display: block; - opacity: 1; -} - -#${MESSAGE_ID}.fade { - opacity: 0; - transition: visibility 0s 1s, opacity 1s linear; -} - -@media (forced-colors: active) { - - #${MESSAGE_ID} { - background-color: Canvas; - color CanvasText; - border-color: AccentColor; - } - - #${MESSAGE_ID} .header { - background-color: Canvas; - color CanvasText; - } - - #${MESSAGE_ID} .content { - background-color: Canvas; - color: CanvasText; - } -} - -`; - - class ShortcutsMessage extends HTMLElement { - constructor () { - - super(); - this.attachShadow({ mode: 'open' }); - - // Get references - - this.messageDialog = document.createElement('div'); - this.messageDialog.id = MESSAGE_ID; - this.messageDialog.classList.add('hidden'); - this.shadowRoot.appendChild(this.messageDialog); - - const headerElem = document.createElement('div'); - headerElem.className = 'header'; - headerElem.textContent = 'SkipTo.js Message'; - this.messageDialog.appendChild(headerElem); - - this.contentElem = document.createElement('div'); - this.contentElem.className = 'content'; - this.messageDialog.appendChild(this.contentElem); - - this.timeoutShowID = false; - this.timeoutFadeID = false; - - } - - configureStyle(config={}) { - - function updateOption(style, option, configOption, defaultOption) { - if (configOption) { - return style.replaceAll(option, configOption); - } - else { - return style.replaceAll(option, defaultOption); - } - } - - // make a copy of the template - let style = styleTemplate.textContent.slice(0); - - style = updateOption(style, - '$fontFamily', - config.fontFamily, - defaultStyleOptions$1.fontFamily); - - style = updateOption(style, - '$fontSize', - config.fontSize, - defaultStyleOptions$1.fontSize); - - style = updateOption(style, - '$focusBorderColor', - config.focusBorderColor, - defaultStyleOptions$1.focusBorderColor); - - style = updateOption(style, - '$focusBorderDarkColor', - config.focusBorderDarkColor, - defaultStyleOptions$1.focusBorderDarkColor); - - - style = updateOption(style, - '$dialogTextColor', - config.dialogTextColor, - defaultStyleOptions$1.dialogTextColor); - - style = updateOption(style, - '$dialogTextDarkColor', - config.dialogTextDarkColor, - defaultStyleOptions$1.dialogTextDarkColor); - - style = updateOption(style, - '$dialogBackgroundColor', - config.dialogBackgroundColor, - defaultStyleOptions$1.dialogBackgroundColor); - - style = updateOption(style, - '$dialogBackgroundDarkColor', - config.dialogBackgroundDarkColor, - defaultStyleOptions$1.dialogBackgroundDarkColor); - - style = updateOption(style, - '$dialogBackgroundTitleColor', - config.dialogBackgroundTitleColor, - defaultStyleOptions$1.dialogBackgroundTitleColor); - - style = updateOption(style, - '$dialogBackgroundTitleDarkColor', - config.dialogBackgroundTitleDarkColor, - defaultStyleOptions$1.dialogBackgroundTitleDarkColor); - - let styleNode = this.shadowRoot.querySelector('style'); - - if (styleNode) { - styleNode.remove(); - } - - styleNode = document.createElement('style'); - styleNode.textContent = style; - this.shadowRoot.appendChild(styleNode); - - } - - close() { - this.messageDialog.classList.remove('show'); - this.messageDialog.classList.remove('fade'); - this.messageDialog.classList.add('hidden'); - } - - open(message) { - clearInterval(this.timeoutFadeID); - clearInterval(this.timeoutShowID); - this.messageDialog.classList.remove('hidden'); - this.messageDialog.classList.remove('fade'); - this.messageDialog.classList.add('show'); - this.contentElem.textContent = message; - - const msg = this; - - this.timeoutFadeID = setTimeout( () => { - msg.messageDialog.classList.add('fade'); - msg.messageDialog.classList.remove('show'); - }, 3000); - - this.timeoutShowID = setTimeout( () => { - msg.close(); - }, 4000); - - } - - } - /* * namefrom.js */ @@ -3678,9 +3432,9 @@ button:hover { */ function monitorKeyboardFocus () { document.addEventListener('focusin', () => { - const highlightElem = document.querySelector(HIGHLIGHT_ELEMENT_NAME); - if (highlightElem) { - highlightElem.removeHighlight(); + const skipToContentElem = document.querySelector(EXTENSION_ELEMENT_NAME) | document.querySelector(BOOKMARKLET_ELEMENT_NAME); + if (skipToContentElem) { + skipToContentElem.removeHighlight(); } }); } @@ -3735,9 +3489,9 @@ button:hover { } } - const highlightElem = document.querySelector(HIGHLIGHT_ELEMENT_NAME); - if (highlightElem) { - highlightElem.highlight(elem, 'instant', info, true); // force highlight + const skipToContentElem = document.querySelector(EXTENSION_ELEMENT_NAME) || document.querySelector(BOOKMARKLET_ELEMENT_NAME); + if (skipToContentElem) { + skipToContentElem.highlight(elem, 'instant', info, true); // force highlight } } @@ -4034,10 +3788,67 @@ button:hover { /* skiptoMenuButton.js */ + /* Constants */ const debug$2 = new DebugLogging('SkipToButton', false); debug$2.flag = false; + const templateMenuButton = document.createElement('template'); + templateMenuButton.innerHTML = ` + + +`; + + /** * @class SkiptoMenuButton * @@ -4054,142 +3865,95 @@ button:hover { this.skipToContentElem = skipToContentElem; this.config = skipToContentElem.config; + this.containerNode = document.createElement('div'); + this.containerNode.className = 'container'; + skipToContentElem.shadowRoot.appendChild(this.containerNode); + // check for 'nav' element, if not use 'div' element const ce = this.config.containerElement.toLowerCase().trim() === 'nav' ? 'nav' : 'div'; - this.containerNode = document.createElement(ce); - skipToContentElem.shadowRoot.appendChild(this.containerNode); + this.menuButtonNode = document.createElement(ce); + this.menuButtonNode.className = 'menu-button'; + this.containerNode.appendChild(this.menuButtonNode); - this.containerNode.id = SKIP_TO_ID; if (ce === 'nav') { - this.containerNode.setAttribute('aria-label', this.config.buttonLabel); + this.menuButtonNode.setAttribute('aria-label', this.config.buttonLabel); } if (isNotEmptyString(this.config.customClass)) { - this.containerNode.classList.add(this.config.customClass); + this.menuButtonNode.classList.add(this.config.customClass); } - this.setDisplayOption(this.config.displayOption); - // Create button const [buttonVisibleLabel, buttonAriaLabel] = this.getBrowserSpecificShortcut(this.config); - this.buttonNode = document.createElement('button'); - this.buttonNode.setAttribute('aria-haspopup', 'menu'); - this.buttonNode.setAttribute('aria-expanded', 'false'); + this.menuButtonNode.appendChild(templateMenuButton.content.cloneNode(true)); + + this.buttonNode = this.containerNode.querySelector('button'); this.buttonNode.setAttribute('aria-label', buttonAriaLabel); - this.buttonNode.setAttribute('aria-controls', MENU_ID); this.buttonNode.addEventListener('keydown', this.handleButtonKeydown.bind(this)); this.buttonNode.addEventListener('click', this.handleButtonClick.bind(this)); - this.containerNode.appendChild(this.buttonNode); - this.textButtonNode = document.createElement('span'); - this.buttonNode.appendChild(this.textButtonNode); - this.textButtonNode.classList.add('skipto-text'); + this.textButtonNode = this.buttonNode.querySelector('span.skipto-text'); this.textButtonNode.textContent = buttonVisibleLabel; - this.smallButtonNode = document.createElement('span'); - this.buttonNode.appendChild(this.smallButtonNode); - this.smallButtonNode.classList.add('skipto-small'); + this.smallButtonNode = this.buttonNode.querySelector('span.skipto-small'); this.smallButtonNode.textContent = this.config.smallButtonLabel; - this.mediumButtonNode = document.createElement('span'); - this.buttonNode.appendChild(this.mediumButtonNode); - this.mediumButtonNode.classList.add('skipto-medium'); + this.mediumButtonNode = this.buttonNode.querySelector('span.skipto-medium'); this.mediumButtonNode.textContent = this.config.buttonLabel; + this.setDisplayOption(this.config.displayOption); + // Create menu container this.menuitemNodes = []; - this.menuNode = document.createElement('div'); - this.menuNode.setAttribute('id', MENU_ID); - this.menuNode.setAttribute('role', 'menu'); + this.menuNode = this.menuButtonNode.querySelector(`#${MENU_ID}`); this.menuNode.setAttribute('aria-label', this.config.menuLabel); - this.containerNode.appendChild(this.menuNode); - this.landmarkGroupLabelNode = document.createElement('div'); - this.landmarkGroupLabelNode.id = MENU_LANDMARK_GROUP_LABEL_ID; - this.landmarkGroupLabelNode.setAttribute('role', 'separator'); + this.landmarkGroupLabelNode = this.menuButtonNode.querySelector(`#${MENU_LANDMARK_GROUP_LABEL_ID}`); this.landmarkGroupLabelNode.textContent = this.addNumberToGroupLabel(this.config.landmarkGroupLabel); - this.menuNode.appendChild(this.landmarkGroupLabelNode); - - this.landmarkGroupNode = document.createElement('div'); - this.landmarkGroupNode.id = MENU_LANDMARK_GROUP_ID; - this.landmarkGroupNode.setAttribute('role', 'group'); - this.landmarkGroupNode.className = 'overflow'; - this.landmarkGroupNode.setAttribute('aria-labelledby', MENU_LANDMARK_GROUP_LABEL_ID); - this.menuNode.appendChild(this.landmarkGroupNode); - - this.headingGroupLabelNode = document.createElement('div'); - this.headingGroupLabelNode.id = MENU_HEADINGS_GROUP_LABEL_ID; - this.headingGroupLabelNode.setAttribute('role', 'separator'); + + this.landmarkGroupNode = this.menuButtonNode.querySelector(`#${MENU_LANDMARK_GROUP_ID}`); + + this.headingGroupLabelNode = this.menuButtonNode.querySelector(`#${MENU_HEADINGS_GROUP_LABEL_ID}`); this.headingGroupLabelNode.textContent = this.addNumberToGroupLabel(this.config.headingGroupLabel); - this.menuNode.appendChild(this.headingGroupLabelNode); - - this.headingGroupNode = document.createElement('div'); - this.headingGroupNode.id = MENU_HEADINGS_GROUP_ID; - this.headingGroupNode.setAttribute('role', 'group'); - this.headingGroupNode.className = 'overflow'; - this.headingGroupNode.setAttribute('aria-labelledby', MENU_HEADINGS_GROUP_LABEL_ID); - this.menuNode.appendChild(this.headingGroupNode); - - this.shortcutsGroupLabelNode = document.createElement('div'); - this.shortcutsGroupLabelNode.setAttribute('id', MENU_SHORTCUTS_GROUP_LABEL_ID); - this.shortcutsGroupLabelNode.setAttribute('role', 'separator'); + + this.headingGroupNode = this.menuButtonNode.querySelector(`#${MENU_HEADINGS_GROUP_ID}`); + + this.shortcutsGroupLabelNode = this.menuButtonNode.querySelector(`#${MENU_SHORTCUTS_GROUP_LABEL_ID}`); if (this.config.shortcuts === 'enabled') { this.shortcutsGroupLabelNode.textContent = this.config.shortcutsGroupEnabledLabel; } else { this.shortcutsGroupLabelNode.textContent = this.config.shortcutsGroupDisabledLabel; } - this.menuNode.appendChild(this.shortcutsGroupLabelNode); - - this.shortcutsGroupNode = document.createElement('div'); - this.shortcutsGroupNode.setAttribute('id', MENU_SHORTCUTS_GROUP_ID); - this.shortcutsGroupNode.setAttribute('role', 'group'); - this.shortcutsGroupNode.setAttribute('aria-labelledby', MENU_SHORTCUTS_GROUP_LABEL_ID); - this.menuNode.appendChild(this.shortcutsGroupNode); - - if (this.config.aboutSupported === 'true') { - this.renderAboutToMenu(this.menuNode, this.config); - } - // Information dialog + this.shortcutsGroupNode = this.menuButtonNode.querySelector(`#${MENU_SHORTCUTS_GROUP_ID}`); - this.infoDialog = document.querySelector(INFO_DIALOG_ELEMENT_NAME); + this.aboutNode = this.menuButtonNode.querySelector(`#${MENU_ABOUT_ID}`); + this.aboutLabelNode = this.menuButtonNode.querySelector(`#${MENU_ABOUT_ID} .label`); + this.aboutLabelNode.textContent = this.config.aboutInfoLabel; - if (!this.infoDialog) { - window.customElements.define(INFO_DIALOG_ELEMENT_NAME, SkipToContentInfoDialog); - this.infoDialog = document.createElement(INFO_DIALOG_ELEMENT_NAME); - this.infoDialog.configureStyle(this.config); - document.body.appendChild(this.infoDialog); + if (this.config.aboutSupported !== 'true') { + this.aboutNode.remove(); + this.aboutLabelNode = false; } - // Highlight element - - this.highlightElem = document.querySelector(HIGHLIGHT_ELEMENT_NAME); - - if (!this.highlightElem) { - window.customElements.define(HIGHLIGHT_ELEMENT_NAME, HighlightElement); - this.highlightElem = document.createElement(HIGHLIGHT_ELEMENT_NAME); - this.highlightElem.configureStyle(this.config); - document.body.appendChild(this.highlightElem); - } + // Information dialog + this.infoDialog = new SkipToContentInfoDialog(this.containerNode); // Shortcut messages + this.shortcutsMessage = new ShortcutsMessage(this.containerNode); - this.shortcutsMessage = document.querySelector(MESSAGE_ELEMENT_NAME); + // Highlight element - if (!this.shortcutsMessage) { - window.customElements.define(MESSAGE_ELEMENT_NAME, ShortcutsMessage); - this.shortcutsMessage = document.createElement(MESSAGE_ELEMENT_NAME); - this.shortcutsMessage.configureStyle(this.config); - document.body.appendChild(this.shortcutsMessage); - } + this.highlightElement = new HighlightElement(this.containerNode); + this.highlightElement.configureMessageSizes(this.config); - this.containerNode.addEventListener('focusin', this.handleFocusin.bind(this)); - this.containerNode.addEventListener('focusout', this.handleFocusout.bind(this)); - this.containerNode.addEventListener('pointerdown', this.handleContainerPointerdown.bind(this), true); + this.menuButtonNode.addEventListener('focusin', this.handleFocusin.bind(this)); + this.menuButtonNode.addEventListener('focusout', this.handleFocusout.bind(this)); + this.menuButtonNode.addEventListener('pointerdown', this.handleContainerPointerdown.bind(this), true); document.documentElement.addEventListener('pointerdown', this.handleBodyPointerdown.bind(this), true); if (this.usesAltKey || this.usesOptionKey) { @@ -4199,17 +3963,15 @@ button:hover { ); } - skipToContentElem.shadowRoot.appendChild(this.containerNode); - this.focusMenuitem = null; } /* - * @get highlightTarget + * @get scrollBehavior * * @desc Returns normalized value for the highlightTarget option */ - get highlightTarget () { + scrollBehavior () { let value = this.config.highlightTarget.trim().toLowerCase(); if ('enabled smooth'.includes(value)) { @@ -4223,14 +3985,30 @@ button:hover { return ''; } + + /* + * @method highlight + * + * @desc Highlights the element on the page when highlighting + * is enabled (NOTE: Highlight is enabled by default) + * + * @param {Object} elem : DOM node of element to highlight + * @param {String} scrollBehavior : value of highlight target + * @param {String} info : Information about target + * @param {Boolean} force : If true override isRduced + */ + + highlight(elem, scrollBehavior='instant', info='', force=false) { + this.highlightElement.highlight(elem, scrollBehavior, info, force); + } + /* - * @method focusButton + * @method removeHighlight * - * @desc Sets keyboard focus on the menu button + * @desc Hides the highlight element on the page */ - focusButton() { - this.buttonNode.focus(); - this.skipToContentElem.setAttribute('focus', 'button'); + removeHighlight() { + this.highlightElement.removeHighlight(); } /* @@ -4273,6 +4051,10 @@ button:hover { this.menuNode.setAttribute('aria-label', config.menuLabel); this.landmarkGroupLabelNode.textContent = this.addNumberToGroupLabel(config.landmarkGroupLabel); this.headingGroupLabelNode.textContent = this.addNumberToGroupLabel(config.headingGroupLabel); + + this.highlightElement.configureMessageSizes(config); + + } /* @@ -4413,33 +4195,6 @@ button:hover { this.updateKeyboardShortCuts(); } - /* - * @method renderAboutToMenu - * - * @desc Render the about menuitem - * - * @param {Object} menuNode - DOM element node for the menu - */ - renderAboutToMenu (menuNode, config) { - - const separatorNode = document.createElement('div'); - separatorNode.setAttribute('role', 'separator'); - - const menuitemNode = document.createElement('div'); - menuitemNode.setAttribute('role', 'menuitem'); - menuitemNode.setAttribute('data-about-info', ''); - menuitemNode.className = 'about skip-to-nav skip-to-nesting-level-0'; - menuitemNode.tabIndex = -1; - - const labelNode = document.createElement('span'); - labelNode.classList.add('label'); - labelNode.textContent = config.aboutInfoLabel; - menuitemNode.appendChild(labelNode); - - menuNode.appendChild(separatorNode); - menuNode.appendChild(menuitemNode); - } - /* * @method renderMenuitemToGroup * @@ -4558,7 +4313,6 @@ button:hover { this.renderMenuitemsToGroup(this.landmarkGroupNode, landmarkElements, config.msgNoLandmarksFound); this.renderMenuitemsToGroup(this.headingGroupNode, headingElements, config.msgNoHeadingsFound); - debug$2.flag && debug$2.log(`[shortcutsSupported]: ${config.shortcutsSupported}`); this.renderMenuitemsToShortcutsGroup(this.shortcutsGroupLabelNode, this.shortcutsGroupNode); // Update list of menuitems @@ -4637,9 +4391,6 @@ button:hover { groupNode.classList.add('shortcuts-disabled'); groupLabelNode.classList.add('shortcuts-disabled'); } - - - } // @@ -4662,10 +4413,10 @@ button:hover { this.focusMenuitem = menuitem; if (menuitem.hasAttribute('data-id')) { const elem = queryDOMForSkipToId(menuitem.getAttribute('data-id')); - this.highlightElem.highlight(elem, this.highlightTarget); + this.highlightElement.highlight(elem, this.scrollBehavior()); } else { - this.highlightElem.removeHighlight(); + this.highlightElement.removeHighlight(); } } } @@ -4794,7 +4545,6 @@ button:hover { * @desc Opens the menu of landmark regions and headings */ openPopup() { - debug$2.flag && debug$2.log(`[openPopup]`); this.menuNode.setAttribute('aria-busy', 'true'); // Compute height of menu to not exceed about 80% of screen height const h = (30 * window.innerHeight) / 100; @@ -4806,19 +4556,22 @@ button:hover { // make sure menu is on screen and not clipped in the right edge of the window const buttonRect = this.buttonNode.getBoundingClientRect(); const menuRect = this.menuNode.getBoundingClientRect(); - const diff = window.innerWidth - buttonRect.left - menuRect.width - 8; + const diff = window.innerWidth - buttonRect.left - menuRect.width; if (diff < 0) { - if (buttonRect.left + diff < 0) { - this.menuNode.style.left = (8 - buttonRect.left) + 'px'; + if (window.innerWidth > menuRect.width) { + this.menuNode.style.left = (window.innerWidth - menuRect.width) + 'px'; } else { - this.menuNode.style.left = diff + 'px'; + this.menuNode.style.left = '0px'; } } + else { + this.menuNode.style.left = buttonRect.left + 'px'; + } this.menuNode.removeAttribute('aria-busy'); this.buttonNode.setAttribute('aria-expanded', 'true'); // use custom element attribute to set focus to the menu - this.skipToContentElem.setAttribute('focus', 'menu'); + this.buttonNode.classList.add('menu'); } /* @@ -4827,11 +4580,11 @@ button:hover { * @desc Closes the memu of landmark regions and headings */ closePopup() { - debug$2.flag && debug$2.log(`[closePopup]`); if (this.isOpen()) { this.buttonNode.setAttribute('aria-expanded', 'false'); this.menuNode.style.display = 'none'; - this.highlightElem.removeHighlight(); + this.highlightElement.removeHighlight(); + this.buttonNode.classList.remove('menu'); } } @@ -4936,23 +4689,23 @@ button:hover { if (typeof value === 'string') { value = value.trim().toLowerCase(); - if (value.length && this.containerNode) { + if (value.length && this.buttonNode) { - this.containerNode.classList.remove('static'); - this.containerNode.classList.remove('popup'); - this.containerNode.classList.remove('show-border'); + this.buttonNode.classList.remove('static'); + this.buttonNode.classList.remove('popup'); + this.buttonNode.classList.remove('show-border'); switch (value) { case 'static': - this.containerNode.classList.add('static'); + this.buttonNode.classList.add('static'); break; case 'onfocus': // Legacy option case 'popup': - this.containerNode.classList.add('popup'); + this.buttonNode.classList.add('popup'); break; case 'popup-border': - this.containerNode.classList.add('popup'); - this.containerNode.classList.add('show-border'); + this.buttonNode.classList.add('popup'); + this.buttonNode.classList.add('show-border'); break; } } @@ -4962,12 +4715,12 @@ button:hover { // Menu event handlers handleFocusin() { - this.containerNode.classList.add('focus'); + this.buttonNode.classList.add('focus'); this.skipToContentElem.setAttribute('focus', 'button'); } handleFocusout() { - this.containerNode.classList.remove('focus'); + this.buttonNode.classList.remove('focus'); this.skipToContentElem.setAttribute('focus', 'none'); } @@ -5004,7 +4757,6 @@ button:hover { } handleButtonClick(event) { - debug$2.flag && debug$2.log(`[handleButtonClick]`); if (this.isOpen()) { this.closePopup(); this.buttonNode.focus(); @@ -5024,7 +4776,7 @@ button:hover { let flag = false; let elem; const focusElem = getFocusElement(); - debug$2.flag && debug$2.log(`[handleDocumentKeydown][elementTakesText][${event.target.tagName}]: ${elementTakesText(focusElem)}`); + if (!elementTakesText(focusElem)) { const altPressed = this.usesAltKey && onlyAltPressed(event); @@ -5187,14 +4939,12 @@ button:hover { } if (tgt.hasAttribute('data-shortcuts-info')) { - this.infoDialog.updateShortcutContent(this.skipToContentElem.config); - this.infoDialog.openDialog(); + this.infoDialog.openDialog('shortcuts'); this.closePopup(); } if (tgt.hasAttribute('data-about-info')) { - this.infoDialog.updateAboutContent(this.skipToContentElem.config); - this.infoDialog.openDialog(); + this.infoDialog.openDialog('skipto'); this.closePopup(); } @@ -5278,36 +5028,33 @@ button:hover { } handleMenuitemClick(event) { - debug$2.log(`[handleMenuitemClick]: ${event.currentTarget.textContent}`); this.handleMenuitemAction(event.currentTarget); event.stopPropagation(); event.preventDefault(); } handleMenuitemPointerenter(event) { - debug$2.flag && debug$2.log(`[enter]`); let tgt = event.currentTarget; tgt.classList.add('hover'); if (tgt.hasAttribute('data-id')) { const elem = queryDOMForSkipToId(tgt.getAttribute('data-id')); - this.highlightElem.highlight(elem, this.highlightTarget); + this.highlightElement.highlight(elem, this.scrollBehavior()); } else { - this.highlightElem.removeHighlight(); + this.highlightElement.removeHighlight(); } event.stopPropagation(); event.preventDefault(); } handleMenuitemPointerover(event) { - debug$2.flag && debug$2.log(`[over]`); let tgt = event.currentTarget; if (tgt.hasAttribute('data-id')) { const elem = queryDOMForSkipToId(tgt.getAttribute('data-id')); - this.highlightElem.highlight(elem, this.highlightTarget); + this.highlightElement.highlight(elem, this.scrollBehavior()); } else { - this.highlightElem.removeHighlight(); + this.highlightElement.removeHighlight(); } event.stopPropagation(); event.preventDefault(); @@ -5321,8 +5068,6 @@ button:hover { } handleContainerPointerdown(event) { - debug$2.flag && debug$2.log(`[down]: target: ${event.pointerId}`); - if (this.isOverButton(event.clientX, event.clientY)) { this.containerNode.releasePointerCapture(event.pointerId); } @@ -5334,14 +5079,12 @@ button:hover { if (this.containerNode.contains(event.target)) { if (this.isOpen()) { if (!this.isOverMenu(event.clientX, event.clientY)) { - debug$2.flag && debug$2.log(`[down][close]`); this.closePopup(); this.buttonNode.focus(); this.skipToContentElem.setAttribute('focus', 'button'); } } else { - debug$2.flag && debug$2.log(`[down][open]`); this.openPopup(); this.setFocusToFirstMenuitem(); } @@ -5360,10 +5103,10 @@ button:hover { mi.classList.add('hover'); if (mi.hasAttribute('data-id')) { const elem = queryDOMForSkipToId(mi.getAttribute('data-id')); - this.highlightElem.highlight(elem, this.highlightTarget); + this.highlightElement.highlight(elem, this.scrollBehavior()); } else { - this.highlightElem.removeHighlight(); + this.highlightElement.removeHighlight(); } } @@ -5379,16 +5122,13 @@ button:hover { const mi = this.getMenuitem(event.clientX, event.clientY); const omb = this.isOverButton(event.clientX, event.clientY); - debug$2.flag && debug$2.log(`[up] isOverButton: ${omb} getMenuitem: ${mi} id: ${event.pointerId}`); if (mi) { this.handleMenuitemAction(mi); } else { if (!omb) { - debug$2.flag && debug$2.log(`[up] not over button `); if (this.isOpen()) { - debug$2.flag && debug$2.log(`[up] close `); this.closePopup(); this.buttonNode.focus(); this.skipToContentElem.setAttribute('focus', 'button'); @@ -5401,8 +5141,6 @@ button:hover { } handleBodyPointerdown(event) { - debug$2.flag && debug$2.log(`[handleBodyPointerdown]: target: ${event.pointerId}`); - if (!this.isOverButton(event.clientX, event.clientY) && !this.isOverMenu(event.clientX, event.clientY)) { this.closePopup(); @@ -5418,17 +5156,17 @@ button:hover { const defaultStyleOptions = colorThemes['default']; - /* @class SkipToContent580 + /* @class SkipToContent590 * */ - class SkipToContent580 extends HTMLElement { + class SkipToContent590 extends HTMLElement { constructor() { // Always call super first in constructor super(); this.attachShadow({ mode: 'open' }); - this.version = "5.8.3"; + this.version = "5.9.0"; this.buttonSkipTo = false; this.initialized = false; @@ -5559,10 +5297,10 @@ button:hover { // options: 'solid' (default), 'dotted', 'dashed' // Hidden heading when highlighting - hiddenHeadingColor: '#000000', - hiddenHeadingDarkColor: '#000000', - hiddenHeadingBackgroundColor: '#ffcc00', - hiddenHeadingBackgroundDarkColor: '#ffcc00', + hiddenTextColor: '#000000', + hiddenTextDarkColor: '#000000', + hiddenBackgroundColor: '#ffcc00', + hiddenBackgroundDarkColor: '#ffcc00', //Dialog styling dialogTextColor: '#000000', @@ -5610,8 +5348,11 @@ button:hover { attributeChangedCallback(name, oldValue, newValue) { - if (name === ATTR_SKIP_TO_DATA) { + if (name === ATTR_SKIP_TO_DATA && newValue) { this.config = this.setupConfigFromDataAttribute(this.config, newValue); + if (newValue.length > 48) { + this.removeAttribute(ATTR_SKIP_TO_DATA); + } } if (name === 'type') { @@ -5682,8 +5423,8 @@ button:hover { } // Add skipto style sheet to document - renderStyleElement(this.shadowRoot, this.config, this.skipToId, globalConfig); this.buttonSkipTo = new SkiptoMenuButton(this); + renderStyleElement(this.shadowRoot, this.config, globalConfig); // Add landmark and heading info to DOM elements for keyboard navigation // if using bookmarklet or extension @@ -5781,31 +5522,13 @@ button:hover { } } - renderStyleElement(this.shadowRoot, config, this.skipToId); if (this.buttonSkipTo) { + renderStyleElement(this.shadowRoot, config); this.buttonSkipTo.updateLabels(config); this.buttonSkipTo.setDisplayOption(config['displayOption']); } - const infoDialog = document.querySelector(INFO_DIALOG_ELEMENT_NAME); - debug$1.flag && debug$1.log(`[infoDialog]: ${infoDialog}`); - if (infoDialog) { - infoDialog.configureStyle(config); - } - - const shortcutsMessage = document.querySelector(MESSAGE_ELEMENT_NAME); - debug$1.flag && debug$1.log(`[shortcutMessage]: ${shortcutsMessage}`); - if (shortcutsMessage) { - shortcutsMessage.configureStyle(config); - } - - const highlight = document.querySelector(HIGHLIGHT_ELEMENT_NAME); - debug$1.flag && debug$1.log(`[highlight]: ${highlight}`); - if (highlight) { - highlight.configureStyle(config); - } - return config; } @@ -5907,7 +5630,6 @@ button:hover { } } - /* *. @function getSkipToContentElement * @@ -5931,7 +5653,7 @@ button:hover { if (!isExtensionLoaded) { if (!isBookmarkletLoaded) { removePageSkipTo(); - window.customElements.define(BOOKMARKLET_ELEMENT_NAME, SkipToContent580); + window.customElements.define(BOOKMARKLET_ELEMENT_NAME, SkipToContent590); skipToContentElem = document.createElement(BOOKMARKLET_ELEMENT_NAME); skipToContentElem.setAttribute('version', skipToContentElem.version); skipToContentElem.setAttribute('type', type); @@ -5947,7 +5669,7 @@ button:hover { if (!isExtensionLoaded) { removePageSkipTo(); removeBookmarkletSkipTo(); - window.customElements.define(EXTENSION_ELEMENT_NAME, SkipToContent580); + window.customElements.define(EXTENSION_ELEMENT_NAME, SkipToContent590); skipToContentElem = document.createElement(EXTENSION_ELEMENT_NAME); skipToContentElem.setAttribute('version', skipToContentElem.version); skipToContentElem.setAttribute('type', type); @@ -5960,7 +5682,7 @@ button:hover { default: if (!isPageLoaded && !isBookmarkletLoaded && !isExtensionLoaded) { - window.customElements.define(PAGE_SCRIPT_ELEMENT_NAME, SkipToContent580); + window.customElements.define(PAGE_SCRIPT_ELEMENT_NAME, SkipToContent590); skipToContentElem = document.createElement(PAGE_SCRIPT_ELEMENT_NAME); skipToContentElem.setAttribute('version', skipToContentElem.version); skipToContentElem.setAttribute('type', type); From ed42b1a2bef2148edc29c9f7df45a303d1a71430 Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Mon, 6 Oct 2025 15:35:57 -0500 Subject: [PATCH 15/38] updated SKipTo.js utility --- content/shared/js/skipto.js | 108 +++++++++++++++++++++++------------- 1 file changed, 69 insertions(+), 39 deletions(-) diff --git a/content/shared/js/skipto.js b/content/shared/js/skipto.js index 7b7f3b724c..9b035dd955 100644 --- a/content/shared/js/skipto.js +++ b/content/shared/js/skipto.js @@ -28,23 +28,21 @@ positionLeft: '46%', smallBreakPoint: '580', mediumBreakPoint: '992', - buttonTextColor: '#13294b', - buttonBackgroundColor: '#dddddd', - focusBorderColor: '#c5050c', - menuTextColor: '#13294b', - menuBackgroundColor: '#dddddd', - menuitemFocusTextColor: '#dddddd', - menuitemFocusBackgroundColor: '#13294b', - menuTextDarkColor: '#ffffff', - menuBackgroundDarkColor: '#000000', - menuitemFocusTextDarkColor: '#ffffff', - menuitemFocusBackgroundDarkColor: '#013c93', - focusBorderDarkColor: '#ffffff', - buttonTextDarkColor: '#ffffff', - buttonBackgroundDarkColor: '#013c93', + buttonTextColor: 'hsl(216, 60%, 18%)', + buttonTextDarkColor: 'hsl(216, 60%, 72%)', + buttonBackgroundColor: 'hsl(0, 0%, 87%)', + buttonBackgroundDarkColor: 'hsl(0, 0%, 13%)', + focusBorderColor: 'hsl(358, 95%, 40%)', + focusBorderDarkColor: 'hsl(358, 95%, 60%)', + menuTextColor: 'hsl(216, 60%, 18%)', + menuTextDarkColor: 'hsl(216, 60%, 72%)', + menuBackgroundColor: 'hsl(0, 0%, 87%)', + menuBackgroundDarkColor: 'hsl(0, 0%, 13%)', + menuitemFocusTextColor: 'hsl(0, 0%, 87%)', + menuitemFocusTextDarkColor: 'hsl(0, 0%, 13%)', + menuitemFocusBackgroundColor: 'hsl(216, 60%, 18%)', + menuitemFocusBackgroundDarkColor: 'hsl(216, 60%, 72%)', zIndex: '2000000', - z2Index: '20000002', - zHighlight: '1999900', displayOption: 'fixed', highlightTarget: 'instant', highlightBorderSize: 'small', @@ -244,7 +242,6 @@ const SCRIPT_EXTENSION_ID = `id-skip-to-extension`; const SCRIPT_BOOKMARKLET_ID = `id-skip-to-bookmarklet`; - const MENU_ID = 'id-skip-to-menu'; const MENU_LANDMARK_GROUP_ID = 'id-skip-to-landmark-group'; const MENU_LANDMARK_GROUP_LABEL_ID = 'id-skip-to-landmark-group-label'; @@ -257,8 +254,10 @@ const MENU_ABOUT_ID = 'id-skip-to-about'; + const BUTTON_ID = 'id-skip-to-button'; + const MENU_ID = 'id-skip-to-menu'; + const DIALOG_ID = 'id-skip-to-dialog'; const MESSAGE_ID = 'id-skip-to-message'; - const HIGHLIGHT_ID = 'id-skip-to-highlight-overlay'; const HIDDEN_ELEMENT_ID = 'id-skip-to-hidden-element'; @@ -1425,7 +1424,7 @@ dialog button:hover { const templateInfoDialog = document.createElement('template'); templateInfoDialog.innerHTML = ` - +

    Keyboard Shortcuts

    @@ -3432,9 +3431,10 @@ dialog button:hover { */ function monitorKeyboardFocus () { document.addEventListener('focusin', () => { - const skipToContentElem = document.querySelector(EXTENSION_ELEMENT_NAME) | document.querySelector(BOOKMARKLET_ELEMENT_NAME); + const skipToContentElem = document.querySelector(EXTENSION_ELEMENT_NAME) || document.querySelector(BOOKMARKLET_ELEMENT_NAME); + debug$4.log(`[monitorKeyboardFocus]: ${skipToContentElem}`); if (skipToContentElem) { - skipToContentElem.removeHighlight(); + skipToContentElem.buttonSkipTo.removeHighlight(); } }); } @@ -3452,13 +3452,12 @@ dialog button:hover { function navigateContent (target, direction, msgHeadingLevel, useFirst=false, nameRequired=false) { - const lastFocusElem = getFocusElement(); + let lastFocusElem = getFocusElement(); let elem = lastFocusElem; let lastElem; let count = 0; // Note: The counter is used as a safety mechanism for any endless loops - do { lastElem = elem; elem = queryDOMForSkipToNavigation(target, direction, elem, useFirst, nameRequired); @@ -3491,7 +3490,7 @@ dialog button:hover { const skipToContentElem = document.querySelector(EXTENSION_ELEMENT_NAME) || document.querySelector(BOOKMARKLET_ELEMENT_NAME); if (skipToContentElem) { - skipToContentElem.highlight(elem, 'instant', info, true); // force highlight + skipToContentElem.buttonSkipTo.highlight(elem, 'instant', info, true); // force highlight } } @@ -3795,7 +3794,8 @@ dialog button:hover { const templateMenuButton = document.createElement('template'); templateMenuButton.innerHTML = ` -
    - Version 5.9.0 + Version 5.9.1
    - Version 5.9.1 + Version 5.9.2
    - Version 5.9.2 + Version ${VERSION}