diff --git a/content/shared/js/skipto.js b/content/shared/js/skipto.js
index d65ab3c766..ac34e93c91 100644
--- a/content/shared/js/skipto.js
+++ b/content/shared/js/skipto.js
@@ -1,5 +1,5 @@
/* ========================================================================
- * Version: 5.3.2
+ * Version: 5.6.3
* Copyright (c) 2022, 2023, 2024 Jon Gunderson; Licensed BSD
* Copyright (c) 2021 PayPal Accessibility Team and University of Illinois; Licensed BSD
* All rights reserved.
@@ -18,6 +18,118 @@
(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',
+ 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 +223,18 @@
/* style.js */
/* Constants */
- const debug$6 = new DebugLogging('style', false);
- debug$6.flag = false;
+ const debug$8 = new DebugLogging('style', false);
+ debug$8.flag = false;
- const styleTemplate = document.createElement('template');
- styleTemplate.innerHTML = `
-
+`;
+
+ 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;
+}
+
+$skipToId-overlay div.overlay-border {
+ margin: 0;
+ padding: 0;
+ position: relative;
+ top: -2px;
+ left: -2px;
+ border-radius: 3px;
+ border: 2px solid $focusBorderColor;
+ z-index: $zHighlight;
+ box-sizing: border-box;
+}
`;
/*
@@ -405,12 +547,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 +570,6 @@ $skipToId [role="menuitem"].hover .label {
let hostnameFlag = false;
let pathnameFlag = false;
-
if (hostnameSelector) {
if (hostname.indexOf(hostnameSelector) >= 0) {
if (!hostnameMatch ||
@@ -489,7 +629,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 +639,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 +646,116 @@ $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 {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) {
+ const theme = 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;
+ if ((typeof config.displayOption === 'string') &&
+ (['popup-border', 'fixed', 'popup', 'static'].includes(config.displayOption.toLowerCase()) < 0)) {
+
+ 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('$fontFamily', config.fontFamily, theme.fontFamily, defaultTheme.fontFamily);
- updateStyle('$fontSize', config.fontSize, theme.fontSize, defaultTheme.fontSize);
+ cssMenu = updateStyle(cssMenu, '$fontFamily', config.fontFamily, theme.fontFamily, defaultTheme.fontFamily);
+ cssMenu = updateStyle(cssMenu, '$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);
+ 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);
- updateStyle('$menuTextColor', config.menuTextColor, theme.menuTextColor, defaultTheme.menuTextColor);
- updateStyle('$menuBackgroundColor', config.menuBackgroundColor, theme.menuBackgroundColor, defaultTheme.menuBackgroundColor);
+ cssMenu = updateStyle(cssMenu, '$menuTextColor', config.menuTextColor, theme.menuTextColor, defaultTheme.menuTextColor);
+ cssMenu = updateStyle(cssMenu, '$menuBackgroundColor', config.menuBackgroundColor, theme.menuBackgroundColor, defaultTheme.menuBackgroundColor);
- updateStyle('$menuitemFocusTextColor', config.menuitemFocusTextColor, theme.menuitemFocusTextColor, defaultTheme.menuitemFocusTextColor);
- updateStyle('$menuitemFocusBackgroundColor', config.menuitemFocusBackgroundColor, theme.menuitemFocusBackgroundColor, defaultTheme.menuitemFocusBackgroundColor);
+ cssMenu = updateStyle(cssMenu, '$menuitemFocusTextColor', config.menuitemFocusTextColor, theme.menuitemFocusTextColor, defaultTheme.menuitemFocusTextColor);
+ cssMenu = updateStyle(cssMenu, '$menuitemFocusBackgroundColor', config.menuitemFocusBackgroundColor, theme.menuitemFocusBackgroundColor, defaultTheme.menuitemFocusBackgroundColor);
- updateStyle('$focusBorderColor', config.focusBorderColor, theme.focusBorderColor, defaultTheme.focusBorderColor);
+ cssMenu = updateStyle(cssMenu, '$focusBorderColor', config.focusBorderColor, theme.focusBorderColor, defaultTheme.focusBorderColor);
- updateStyle('$buttonTextColor', config.buttonTextColor, theme.buttonTextColor, defaultTheme.buttonTextColor);
- updateStyle('$buttonBackgroundColor', config.buttonBackgroundColor, theme.buttonBackgroundColor, defaultTheme.buttonBackgroundColor);
+ cssMenu = updateStyle(cssMenu, '$buttonTextColor', config.buttonTextColor, theme.buttonTextColor, defaultTheme.buttonTextColor);
+ cssMenu = updateStyle(cssMenu, '$buttonBackgroundColor', config.buttonBackgroundColor, theme.buttonBackgroundColor, defaultTheme.buttonBackgroundColor);
- updateStyle('$zIndex', config.zIndex, theme.zIndex, defaultTheme.zIndex);
+ 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, '$focusBorderColor', config.focusBorderColor, theme.focusBorderColor, defaultTheme.focusBorderColor);
+
+ // Special case for theme configuration used in Illinois theme
+ if (typeof theme.highlightTarget === 'string') {
+ config.highlightTarget = theme.highlightTarget;
+ }
+
+ 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
*/
- 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) {
+ 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);
+
+
+ let styleNode = attachNode.querySelector('#id-skip-to-style');
+ if (!styleNode) {
+ styleNode = document.createElement('style');
+ attachNode.appendChild(styleNode);
+ styleNode.setAttribute('id', 'id-skip-to-style');
+ }
+ styleNode.textContent = cssMenu;
+
+ const headNode = document.querySelector('head');
+ if (headNode) {
+ let highlightStyleNode = headNode.querySelector('#id-skip-to-highlight');
+ if (!highlightStyleNode) {
+ highlightStyleNode = document.createElement('style');
+ headNode.appendChild(highlightStyleNode);
+ highlightStyleNode.setAttribute('id', 'id-skip-to-highlight');
+ }
+ highlightStyleNode.textContent = cssHighlight;
+ }
+
}
/* utils.js */
/* Constants */
- const debug$5 = new DebugLogging('Utils', false);
- debug$5.flag = false;
+ const debug$7 = new DebugLogging('Utils', false);
+ debug$7.flag = false;
/*
@@ -662,8 +844,8 @@ $skipToId [role="menuitem"].hover .label {
/* constants */
- const debug$4 = new DebugLogging('nameFrom', false);
- debug$4.flag = false;
+ const debug$6 = new DebugLogging('nameFrom', false);
+ debug$6.flag = false;
//
// LOW-LEVEL HELPER FUNCTIONS (NOT EXPORTED)
@@ -932,8 +1114,8 @@ $skipToId [role="menuitem"].hover .label {
/* accName.js */
/* Constants */
- const debug$3 = new DebugLogging('accName', false);
- debug$3.flag = false;
+ const debug$5 = new DebugLogging('accName', false);
+ debug$5.flag = false;
/**
* @fuction getAccessibleName
@@ -1011,8 +1193,8 @@ $skipToId [role="menuitem"].hover .label {
/* landmarksHeadings.js */
/* Constants */
- const debug$2 = new DebugLogging('landmarksHeadings', false);
- debug$2.flag = false;
+ const debug$4 = new DebugLogging('landmarksHeadings', false);
+ debug$4.flag = false;
const skipableElements = [
'base',
@@ -1027,7 +1209,8 @@ $skipToId [role="menuitem"].hover .label {
'style',
'template',
'shadow',
- 'title'
+ 'title',
+ 'skip-to-content'
];
const allowedLandmarkSelectors = [
@@ -1250,6 +1433,12 @@ $skipToId [role="menuitem"].hover .label {
return targetNode;
}
}
+ else {
+ targetNode = transverseDOMForSkipToId(node);
+ if (targetNode) {
+ return targetNode;
+ }
+ }
} else {
targetNode = transverseDOMForSkipToId(node);
if (targetNode) {
@@ -1268,12 +1457,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) {
@@ -1312,6 +1501,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){
@@ -1455,7 +1650,7 @@ $skipToId [role="menuitem"].hover .label {
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 ) {
@@ -1512,6 +1707,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);
}
@@ -1569,7 +1767,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);
@@ -1595,7 +1793,7 @@ $skipToId [role="menuitem"].hover .label {
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 (isVisible(heading.node) && isNotEmptyString(heading.node.textContent)) {
if (heading.node.hasAttribute('data-skip-to-id')) {
dataId = heading.node.getAttribute('data-skip-to-id');
} else {
@@ -1672,14 +1870,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 = [];
@@ -1727,6 +1925,7 @@ $skipToId [role="menuitem"].hover .label {
* @returns {Array} see @desc
*/
function getLandmarks(config, landmarks) {
+ let allElements = [];
let mainElements = [];
let searchElements = [];
let navElements = [];
@@ -1794,6 +1993,8 @@ $skipToId [role="menuitem"].hover .label {
landmarkItem.nestingLevel = 0;
incSkipToIdIndex();
+ allElements.push(landmarkItem);
+
// For sorting landmarks into groups
switch (tagName) {
case 'main':
@@ -1823,14 +2024,205 @@ $skipToId [role="menuitem"].hover .label {
}
}
}
+ if (config.landmarks.includes('doc-order')) {
+ return allElements;
+ }
return [].concat(mainElements, searchElements, navElements, asideElements, regionElements, footerElements, otherElements);
}
+ /* highlight.js */
+
+ /* Constants */
+ const debug$3 = new DebugLogging('highlight', false);
+ debug$3.flag = false;
+
+ const minWidth = 68;
+ const minHeight = 27;
+ const offset = 6;
+ const borderWidth = 2;
+
+ const overlayId = 'id-skip-to-overlay';
+
+ /*
+ * @function getOverlayElement
+ *
+ * @desc Returns DOM node for the overlay element
+ *
+ * @returns {Object} see @desc
+ */
+
+ function getOverlayElement() {
+
+ let overlayElem = document.getElementById(overlayId);
+
+ if (overlayElem === null) {
+ overlayElem = document.createElement('div');
+ overlayElem.style.display = 'none';
+ overlayElem.id = overlayId;
+ document.body.appendChild(overlayElem);
+
+ const overlayElemChild = document.createElement('div');
+ overlayElemChild.className = 'overlay-border';
+ overlayElem.appendChild(overlayElemChild);
+ }
+
+ 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,
+ * otheriwse 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 {String} id : id of the element to highlight
+ * @param {String} ihighlightTarget : value of highlight target
+ */
+ function highlightElement(id, highlightTarget) {
+ const mediaQuery = window.matchMedia(`(prefers-reduced-motion: reduce)`);
+ const isReduced = !mediaQuery || mediaQuery.matches;
+ const element = queryDOMForSkipToId(id);
+
+ if (element && highlightTarget) {
+
+ if (isElementInHeightLarge(element)) {
+ if (!isElementStartInViewport(element) && !isReduced) {
+ element.scrollIntoView({ behavior: highlightTarget, block: 'start', inline: 'nearest' });
+ }
+ }
+ else {
+ if (!isElementInViewport(element) && !isReduced) {
+ element.scrollIntoView({ behavior: highlightTarget, block: 'start', inline: 'nearest' });
+ }
+ }
+
+ const overlayElement = getOverlayElement();
+ updateOverlayElement(overlayElement, element);
+ }
+ }
+
+ /*
+ * @function removeHighlight
+ *
+ * @desc Hides the highlight element on the page
+ */
+ function removeHighlight() {
+ const overlayElement = getOverlayElement();
+ 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
+ *
+ */
+
+ function updateOverlayElement (overlayElem, element) {
+
+ const childElem = overlayElem.firstElementChild;
+
+ const rect = element.getBoundingClientRect();
+
+ const left = rect.left > offset ?
+ Math.round(rect.left - offset + window.scrollX) :
+ Math.round(rect.left + window.scrollX);
+
+ const width = rect.left > offset ?
+ Math.max(rect.width + offset * 2, minWidth) :
+ Math.max(rect.width, minWidth);
+
+ const top = rect.top > offset ?
+ Math.round(rect.top - offset + window.scrollY) :
+ Math.round(rect.top + window.scrollY);
+
+ const height = rect.top > offset ?
+ Math.max(rect.height + offset * 2, minHeight) :
+ Math.max(rect.height, minHeight);
+
+ overlayElem.style.left = left + 'px';
+ overlayElem.style.width = width + 'px';
+ overlayElem.style.top = top + 'px';
+ overlayElem.style.height = height + 'px';
+
+ childElem.style.width = (width - 2 * borderWidth) + 'px';
+ childElem.style.height = (height - 2 * borderWidth) + 'px';
+
+
+ overlayElem.style.display = 'block';
+ }
+
/* skiptoMenuButton.js */
/* Constants */
- const debug$1 = new DebugLogging('SkipToButton', false);
- debug$1.flag = false;
+ const debug$2 = new DebugLogging('SkipToButton', false);
+ debug$2.flag = false;
/**
* @class SkiptoMenuButton
@@ -1838,104 +2230,98 @@ $skipToId [role="menuitem"].hover .label {
* @desc Constructor for creating a button to open a menu of headings and landmarks on
* a web page
*
- * @param {Object} attachNode - DOM eleemnt node to attach button and menu container element
+ * @param {Object} skipToContentElem - The skip-to-content objecy
*
- * @returns {Object} DOM element node that is the contatiner for the button and the menu
+ * @returns {Object} DOM element node that is the container for the button and the menu
*/
class SkiptoMenuButton {
- constructor (attachNode, config, id) {
- this.config = config;
- this.skiptoId = id;
+ constructor (skipToContentElem) {
+ this.skipToContentElem = skipToContentElem;
+ this.config = skipToContentElem.config;
+ this.skiptoId = skipToContentElem.skipToId;
- this.containerNode = document.createElement(config.containerElement);
- if (config.containerElement === 'nav') {
- this.containerNode.setAttribute('aria-label', config.buttonLabel);
- }
+ // check for 'nav' element, if not use 'div' element
+ const ce = this.config.containerElement.toLowerCase().trim() === 'nav' ? 'nav' : 'div';
- this.containerNode.id = id;
+ this.containerNode = document.createElement(ce);
+ skipToContentElem.shadowRoot.appendChild(this.containerNode);
- if (isNotEmptyString(config.customClass)) {
- this.containerNode.classList.add(config.customClass);
+ this.containerNode.id = this.skiptoId;
+ if (ce === 'nav') {
+ this.containerNode.setAttribute('aria-label', this.config.buttonLabel);
}
-
- 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;
- }
- }
+ if (isNotEmptyString(this.config.customClass)) {
+ this.containerNode.classList.add(this.config.customClass);
}
+ this.setDisplayOption(this.config.displayOption);
+
// Create button
- const [buttonVisibleLabel, buttonAriaLabel] = this.getBrowserSpecificShortcut(config);
+ 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.buttonTextNode = document.createElement('span');
- this.buttonTextNode.classList.add('skipto-text');
- this.buttonTextNode.textContent = buttonVisibleLabel;
- this.buttonNode.appendChild(this.buttonTextNode);
+ this.textButtonNode = document.createElement('span');
+ this.buttonNode.appendChild(this.textButtonNode);
+ this.textButtonNode.classList.add('skipto-text');
+ this.textButtonNode.textContent = buttonVisibleLabel;
- const smallButtonNode = document.createElement('span');
- smallButtonNode.classList.add('skipto-small');
- smallButtonNode.textContent = config.smallButtonLabel;
- this.buttonNode.appendChild(smallButtonNode);
+ this.smallButtonNode = document.createElement('span');
+ this.buttonNode.appendChild(this.smallButtonNode);
+ this.smallButtonNode.classList.add('skipto-small');
+ this.smallButtonNode.textContent = this.config.smallButtonLabel;
- const mediumButtonNode = document.createElement('span');
- mediumButtonNode.classList.add('skipto-medium');
- mediumButtonNode.textContent = config.buttonLabel;
- this.buttonNode.appendChild(mediumButtonNode);
+ 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.id = 'id-skip-to-menu';
+ this.menuNode.setAttribute('id', 'id-skip-to-menu');
this.menuNode.setAttribute('role', 'menu');
- this.menuNode.setAttribute('aria-label', config.menuLabel);
- this.menuNode.setAttribute('aria-busy', 'true');
+ this.menuNode.setAttribute('aria-label', this.config.menuLabel);
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);
+ 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.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.setAttribute('aria-labelledby', landmarkGroupLabelNode.id);
- this.landmarkGroupNode.id = '#id-skip-to-menu-landmark-group';
+ this.landmarkGroupNode.setAttribute('aria-labelledby', 'id-skip-to-menu-landmark-group-label');
this.menuNode.appendChild(this.landmarkGroupNode);
- 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);
+ 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.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.setAttribute('aria-labelledby', headingGroupLabelNode.id);
- this.headingGroupNode.id = '#id-skip-to-menu-heading-group';
+ this.headingGroupNode.setAttribute('aria-labelledby', 'id-skip-to-menu-heading-group-label');
this.menuNode.appendChild(this.headingGroupNode);
this.containerNode.addEventListener('focusin', this.handleFocusin.bind(this));
this.containerNode.addEventListener('focusout', this.handleFocusout.bind(this));
- window.addEventListener('pointerdown', this.handleBackgroundPointerdown.bind(this), true);
+ 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(
@@ -1944,14 +2330,64 @@ $skipToId [role="menuitem"].hover .label {
);
}
- attachNode.insertBefore(this.containerNode, attachNode.firstElementChild);
+ 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();
- return this.containerNode;
+ 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 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 = config.landmarkGroupLabel;
+ this.headingGroupLabelNode.textContent = config.headingGroupLabel;
+ }
+
/*
* @method getBrowserSpecificShortcut
*
@@ -1993,7 +2429,10 @@ $skipToId [role="menuitem"].hover .label {
config.altLabel
);
label = label + buttonShortcut;
- ariaLabel = config.altButtonAriaLabel.replace('$key', config.altShortcut);
+ 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) {
@@ -2002,7 +2441,10 @@ $skipToId [role="menuitem"].hover .label {
config.optionLabel
);
label = label + buttonShortcut;
- ariaLabel = config.optionButtonAriaLabel.replace('$key', config.altShortcut);
+ 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];
@@ -2106,6 +2548,7 @@ $skipToId [role="menuitem"].hover .label {
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));
groupNode.appendChild(menuitemNode);
// add heading level and label
@@ -2158,7 +2601,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 +2628,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,10 +2639,10 @@ $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);
// Update list of menuitems
this.updateMenuitems();
@@ -2214,10 +2661,12 @@ $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;
+ highlightElement(menuitem.getAttribute('data-id'), this.highlightTarget);
}
}
@@ -2342,13 +2791,14 @@ $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();
+ this.renderMenu(this.config, this.skipToId);
this.menuNode.style.display = 'block';
const buttonRect = this.buttonNode.getBoundingClientRect();
const menuRect = this.menuNode.getBoundingClientRect();
@@ -2362,6 +2812,7 @@ $skipToId [role="menuitem"].hover .label {
}
this.menuNode.removeAttribute('aria-busy');
this.buttonNode.setAttribute('aria-expanded', 'true');
+ this.skipToContentElem.setAttribute('focus', 'menu');
}
/*
@@ -2370,9 +2821,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 +2845,125 @@ $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 +2982,7 @@ $skipToId [role="menuitem"].hover .label {
case 'Escape':
this.closePopup();
this.buttonNode.focus();
+ this.skipToContentElem.setAttribute('focus', 'button');
flag = true;
break;
case 'Up':
@@ -2440,9 +2999,11 @@ $skipToId [role="menuitem"].hover .label {
}
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();
@@ -2530,8 +3091,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,6 +3107,7 @@ $skipToId [role="menuitem"].hover .label {
case 'Escape':
this.closePopup();
this.buttonNode.focus();
+ this.skipToContentElem.setAttribute('focus', 'button');
flag = true;
break;
case 'Up':
@@ -2591,53 +3154,145 @@ $skipToId [role="menuitem"].hover .label {
}
handleMenuitemPointerenter(event) {
+ debug$2.flag && debug$2.log(`[enter]`);
let tgt = event.currentTarget;
- this.removeHoverClass();
tgt.classList.add('hover');
+ highlightElement(tgt.getAttribute('data-id'), this.highlightTarget);
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
+ handleMenuitemPointerover(event) {
+ debug$2.flag && debug$2.log(`[over]`);
+ let tgt = event.currentTarget;
+ highlightElement(tgt.getAttribute('data-id'), this.highlightTarget);
+ 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');
+ highlightElement(mi.getAttribute('data-id'), this.highlightTarget);
+ }
+
+ 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 SkipToContent extends HTMLElement {
+
+ constructor() {
+ // Always call super first in constructor
+ super();
+ this.attachShadow({ mode: 'open' });
+ this.skipToId = 'id-skip-to';
+ this.version = "5.6.3";
+ 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,9 +3303,9 @@ $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',
// Menu labels and messages
menuLabel: 'Landmarks and Headings',
@@ -2670,7 +3325,10 @@ $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'
// Place holders for configuration
colorTheme: '',
@@ -2687,235 +3345,268 @@ $skipToId [role="menuitem"].hover .label {
buttonTextColor: '',
buttonBackgroundColor: '',
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"
+ ];
+ }
- // 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 === '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);
+ this.buttonSkipTo = new SkiptoMenuButton(this);
+ }
- 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 => {
+ let [prop, value] = v.split(':');
+ 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);
}
+ }
- return config;
- },
+ renderStyleElement(this.shadowRoot, config, this.skipToId);
+ if (this.buttonSkipTo) {
+ this.buttonSkipTo.updateLabels(config);
+ this.buttonSkipTo.setDisplayOption(config['displayOption']);
+ }
- /*
- * @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 = {};
+ return config;
+ }
- // 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;
- }
- });
- }
- }
- 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);
+ }
+
+ /* skipto.js */
+
+ /* constants */
+ const debug = new DebugLogging('skipto', false);
+ debug.flag = false;
+
+ (function() {
+
+ /*
+ * @function removeLegacySkipToJS
+ *
+ * @desc Removes legacy and duplicate versions of SkipTo.js
+ */
+
+ function removeLegacySkipToJS(skipToContentElem = null) {
+ const node = document.getElementById('id-skip-to');
+ debug.flag && debug.log(`[removeLegacySkipToJS]: ${node}`);
+ if (node !== null) {
+ // remove legacy SkipTo.js
+ node.remove();
+ const cssNode = document.getElementById('id-skip-to-css');
+ if (cssNode) {
+ cssNode.remove();
+ }
+ console.warn('[skipTo.js] legacy version removed, using SkipTo.js extension');
+ }
+ const nodes = document.querySelectorAll('skip-to-content');
+ if (nodes && nodes.length > 1) {
+ debug.flag && debug.log(`[removeLegacySkipToJS][${nodes.length}]: Removing duplicate copy of SkipTo.js`);
+ for (let i = 0; i