leilukin-site/assets/js/tooltips.js

310 lines
9.7 KiB
JavaScript

/* ARIA Tooltips by Scott O'Hara: https://github.com/scottaohara/a11y_tooltips */
'use strict';
if (typeof Object.assign != 'function') {
// Must be writable: true, enumerable: false, configurable: true
Object.defineProperty(Object, "assign", {
value: function assign(target, varArgs) { // .length of function is 2
'use strict';
if (target == null) { // TypeError if undefined or null
throw new TypeError('Cannot convert undefined or null to object');
}
var to = Object(target);
for (var index = 1; index < arguments.length; index++) {
var nextSource = arguments[index];
if (nextSource != null) { // Skip over if undefined or null
for (var nextKey in nextSource) {
// Avoid bugs when hasOwnProperty is shadowed
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
},
writable: true,
configurable: true
});
}
var util = {
keyCodes: {
ESC: 27
},
generateID: function ( base ) {
return base + Math.floor(Math.random() * 999);
}
};
(function ( w, doc, undefined ) {
/**
* ARIA Tooltips
* Widget to reveal a short description, or
* the element's accessible name on hover/focus.
*
* Author: Scott O'Hara
* Version: 1.0.0
* License: MIT
*/
var tipConfig = {
baseID: 'tt_',
ariaHiddenTip: true,
tipWrapperClass: 'tooltip',
tipContentClass: 'tooltip__content',
tipTypeAttr: 'data-tooltip',
tipContentAttr: 'data-tooltip-content',
tipSelector: '[data-tooltip-tip]',
triggerSelector: '[data-tooltip-trigger]'
};
var ARIAtip = function ( inst, options ) {
var el = inst;
var elID;
var elTip;
var elTipID;
var elTrigger;
var tipContent;
var tipType;
var _options = Object.assign(tipConfig, options);
var init = function () {
// if an element has an ID, use that as the
// basis for the tooltip's ID. Or, generate one.
elID = el.id || util.generateID(_options.baseID);
// the element that will trigger the tooltip on hover or focus.
elTrigger = el.querySelector(_options.triggerSelector);
// base the tip's ID off from the element's tip
elTipID = elID + '_tip';
// determine the type of tip
tipType = tipType();
// retrieve the content for the tip (flatted text string)
tipContent = getTipContent();
// create the tip
createTip();
// add/modify the necessary attributes of the trigger.
setupTrigger();
// get the generated tooltip
elTip = el.querySelector('.'+_options.tipContentClass);
// Attach the various events to the triggers
attachEvents();
};
/**
* A tooltip can either provide a description to
* the element that it is associated with, or it
* can provide a means to visually display the element's
* accessible name (making it not actually a tooltip then,
* but rather the accessible name that is revealed on
* hover/focus).
*/
var tipType = function () {
if ( el.getAttribute(_options.tipTypeAttr) === 'label' ) {
return 'label';
}
else {
return 'description';
}
};
/**
* The content of a tooltip can come from different sources
* so as to allow for fallback content in case this script
* cannot run.
*
* A tip could be sourced from an element in the DOM via
* the attribute data-tooltip-tip (a child of the widget),
* or an ID referenced from data-tooltip-source (does not
* have to be a child of the widget),
* the trigger's title or aria-label attribute
*/
var getTipContent = function () {
var returnTextString; // text string to return
var tipAttrContent = el.getAttribute(_options.tipContentAttr);
var tipAriaLabel = elTrigger.getAttribute('aria-label');
var widgetChild = el.querySelector(_options.tipSelector);
if ( tipAttrContent ) {
returnTextString = tipAttrContent;
}
else if ( widgetChild ) {
returnTextString = widgetChild.textContent;
widgetChild.parentNode.removeChild(widgetChild);
}
else if ( tipAriaLabel && tipType === 'label' ) {
returnTextString = tipAriaLabel;
elTrigger.removeAttribute('aria-label');
}
else if ( elTrigger.title ) {
returnTextString = elTrigger.title;
}
// an element cannot have both a custom tooltip
// and a tooltip from a title attribute. So no
// matter what, remove the title attribute.
elTrigger.removeAttribute('title');
return returnTextString;
};
/**
* Create the necessary tooltip components for each
* instance of the widget.
*/
var createTip = function () {
var tipOuter = doc.createElement('span');
var tipInner = doc.createElement('span');
tipOuter.classList.add(_options.tipWrapperClass);
tipInner.classList.add(_options.tipContentClass);
tipInner.textContent = tipContent;
tipInner.id = elTipID;
if ( tipType !== 'label') {
tipInner.setAttribute('role', 'tooltip');
}
// this is a bit silly, as it shows how unnecessary the
// tooltip role is, but it ensures that a screen reader's
// virtual cursor cannot interact with the tooltip by itself,
// and helps reduce Chrome on PC w/JAWS and NVDA announcing the tooltip
// multiple times, when it's shown/hidden.
// If you want to reveal the tips anyway,
// set the ariaHiddenTip config to false.
if ( _options.ariaHiddenTip ) {
tipInner.setAttribute('aria-hidden', 'true');
}
tipOuter.appendChild(tipInner);
el.appendChild(tipOuter);
};
/**
* Ensure the tooltip trigger has the appropriate
* attributes on it, and that they point to the
* correct IDs.
*/
var setupTrigger = function () {
if ( tipType === 'label' ) {
elTrigger.setAttribute('aria-labelledby', elTipID);
}
else {
elTrigger.setAttribute('aria-describedby', elTipID);
}
};
/**
* Check the current viewport to determine if the tooltip
* will be revealed outside of the current viewport's bounds.
* If so, try to reposition to ensure it's within.
*/
var checkPositioning = function () {
var bounding = elTip.getBoundingClientRect();
if ( bounding.bottom > w.innerHeight ) {
el.classList.add('push-up');
}
if ( bounding.right > w.innerWidth ) {
el.classList.add('push-right');
}
};
/**
* Remove the positioning classes, assuming the next time
* the element is interacted with, it will require the
* default positioning.
*/
var resetPositioning = function () {
el.classList.remove('push-up', 'push-right');
};
/**
* Add class to show tooltip.
* Checks positioning to help ensure within viewport.
* Adds global event to escape tip.
*/
var showTip = function () {
el.classList.add(_options.tipWrapperClass + '--show');
checkPositioning();
doc.addEventListener('keydown', globalEscape, false);
doc.addEventListener('touchend', hideTip, false);
};
/**
* Removes classes for show and/or suppressed tip.
* Removes classes for positioning.
* Removes global event to escape tip.
*/
var hideTip = function () {
el.classList.remove(_options.tipWrapperClass + '--show');
resetPositioning();
doc.removeEventListener('keydown', globalEscape);
doc.addEventListener('touchend', hideTip);
};
/**
* Global event to allow the ESC key to close
* an invoked tooltip, regardless of where focus/hover
* is in the DOM.
*
* Calls both hideTip and suppressTip functions to help
* better replicate native tooltip suppression behavior.
*/
var globalEscape = function ( e ) {
var keyCode = e.keyCode || e.which;
switch ( keyCode ) {
case util.keyCodes.ESC:
e.preventDefault();
hideTip();
break;
default:
break;
}
};
var attachEvents = function () {
elTrigger.addEventListener('mouseenter', showTip, false);
elTrigger.addEventListener('focus', showTip, false);
el.addEventListener('mouseleave', hideTip, false);
elTrigger.addEventListener('blur', hideTip, false);
};
init.call(this);
return this;
}; // ARIAtip
w.ARIAtip = ARIAtip;
})( window, document );
var selector = '[data-tooltip]';
var els = document.querySelectorAll(selector);
for ( var i = 0; i < els.length; i++ ) {
var dm = new ARIAtip( els[i] );
}