Implement Scott O'Hara's ARIA tooltips
This commit is contained in:
parent
4a73587a53
commit
cf4040da6d
|
@ -9,6 +9,9 @@
|
|||
|
||||
{# CSS #}
|
||||
{% include "global/css-bundle.njk" %}
|
||||
{% if hasTooltips %}
|
||||
<link rel="stylesheet" href="{{'/assets/css/tooltips.css' | url | safe}}">
|
||||
{% endif %}
|
||||
{% if customCSSSheets %}
|
||||
{%- for sheet in customCSSSheets -%}
|
||||
<link rel="stylesheet" href="{{'/assets/css/' + sheet + '.css' | url | safe}}">
|
||||
|
@ -46,5 +49,16 @@
|
|||
{{ content | safe }}
|
||||
</main>
|
||||
{% block footer %}{% include "global/footer.njk" %}{% endblock %}
|
||||
{% if hasTooltips %}
|
||||
<script src="{{'/assets/js/tooltips.js' | url | safe}}"></script>
|
||||
<script>
|
||||
var selector = '[data-tooltip]';
|
||||
var els = document.querySelectorAll(selector);
|
||||
|
||||
for ( var i = 0; i < els.length; i++ ) {
|
||||
var dm = new ARIAtip( els[i] );
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
|
@ -128,28 +128,6 @@
|
|||
|
||||
p + .adoptables { margin-top: 1em; }
|
||||
|
||||
/* Tooltips */
|
||||
.tooltip { display: none; }
|
||||
.tipcontainer { position: relative; }
|
||||
|
||||
.tipactivator {
|
||||
display: block;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.tipactivator:hover + .tooltip,
|
||||
.tipactivator:focus + .tooltip,
|
||||
.tooltip:hover {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
background-color: var(--clr-quote-bg);
|
||||
border: 0.15em solid var(--clr-main-heading);
|
||||
padding: 0.25em 0.5em;
|
||||
z-index: 998;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Web button lists */
|
||||
.web-btn-wrapper {
|
||||
display: flex;
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/* ARIA Tooltips by Scott O'Hara: https://github.com/scottaohara/a11y_tooltips */
|
||||
|
||||
[data-tooltip] {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-tooltip-block] { display: block; }
|
||||
|
||||
.tooltip {
|
||||
font-size: .825rem;
|
||||
left: 0;
|
||||
min-width: 20ch;
|
||||
max-width: 44ch;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.push-up .tooltip {
|
||||
bottom: 100%;
|
||||
top: auto;
|
||||
}
|
||||
|
||||
.push-right .tooltip {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.tooltip__content {
|
||||
background-color: var(--clr-quote-bg);
|
||||
border: 0.15em solid var(--clr-main-heading);
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
padding: .625em;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.tooltip__content > * { margin: .5em 0; }
|
||||
.tooltip__content :last-child { margin-bottom: 0; }
|
||||
.tooltip__content :first-child { margin-top: 0; }
|
||||
|
||||
.tooltip--show .tooltip__content {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transition: opacity .1s ease-in;
|
||||
visibility: visible;
|
||||
}
|
|
@ -0,0 +1,303 @@
|
|||
/* 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 );
|
|
@ -4,6 +4,7 @@ tags: pages
|
|||
metadata:
|
||||
title: Home
|
||||
isContentDivided: true
|
||||
hasTooltips: true
|
||||
eleventyNavigation:
|
||||
key: Home
|
||||
order: 1
|
||||
|
@ -74,11 +75,10 @@ eleventyComputed:
|
|||
<h2>Always Proud</h2>
|
||||
<div class="flag-progress-intersex-lesbian" role="img" aria-label="Custom pride flag which combines the lesbian pride flag and the progress pride flag triangle" style="height: 20rem; margin-bottom: 1em;"></div>
|
||||
{%- macro prideButton(file, alt, tooltip=alt, width=88, height=31) -%}
|
||||
<li class="tipcontainer">
|
||||
<button class="tipactivator" style="cursor: unset;">
|
||||
<li data-tooltip>
|
||||
<button data-tooltip-trigger title="{{ tooltip }}" style="cursor: unset; padding: 0; margin: 0; border: 0;">
|
||||
<img src="/assets/buttons/pride/{{ file }}.png" alt="{{ alt }}" width="{{ width }}" height="{{ height }}" loading="lazy">
|
||||
</button>
|
||||
<p class="tooltip">{{ tooltip }}</p>
|
||||
</li>
|
||||
{%- endmacro -%}
|
||||
<ul class="web-btn-wrapper">
|
||||
|
|
|
@ -4,11 +4,10 @@ date: 2023-01-28
|
|||
---
|
||||
|
||||
{%- macro siteButton(url, file, alt, tooltip=alt, width=88, height=31) -%}
|
||||
<li class="tipcontainer">
|
||||
<a href="{{ url }}" class="tipactivator">
|
||||
<li data-tooltip>
|
||||
<a href="{{ url }}" data-tooltip-trigger title="{{ tooltip }}">
|
||||
<img src="/assets/buttons/{{ file }}" alt="{{ alt }}" width="{{ width }}" height="{{ height }}" loading="lazy">
|
||||
</a>
|
||||
<p class="tooltip">{{ tooltip }}</p>
|
||||
</li>
|
||||
{%- endmacro -%}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
layout: main/links.njk
|
||||
title: Links
|
||||
desc: A curated collection of links.
|
||||
hasTooltips: true
|
||||
eleventyNavigation:
|
||||
order: 8
|
||||
---
|
||||
|
|
Loading…
Reference in New Issue