Implement Scott O'Hara's ARIA tooltips
This commit is contained in:
parent
4a73587a53
commit
cf4040da6d
|
@ -9,6 +9,9 @@
|
||||||
|
|
||||||
{# CSS #}
|
{# CSS #}
|
||||||
{% include "global/css-bundle.njk" %}
|
{% include "global/css-bundle.njk" %}
|
||||||
|
{% if hasTooltips %}
|
||||||
|
<link rel="stylesheet" href="{{'/assets/css/tooltips.css' | url | safe}}">
|
||||||
|
{% endif %}
|
||||||
{% if customCSSSheets %}
|
{% if customCSSSheets %}
|
||||||
{%- for sheet in customCSSSheets -%}
|
{%- for sheet in customCSSSheets -%}
|
||||||
<link rel="stylesheet" href="{{'/assets/css/' + sheet + '.css' | url | safe}}">
|
<link rel="stylesheet" href="{{'/assets/css/' + sheet + '.css' | url | safe}}">
|
||||||
|
@ -46,5 +49,16 @@
|
||||||
{{ content | safe }}
|
{{ content | safe }}
|
||||||
</main>
|
</main>
|
||||||
{% block footer %}{% include "global/footer.njk" %}{% endblock %}
|
{% 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>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -128,28 +128,6 @@
|
||||||
|
|
||||||
p + .adoptables { margin-top: 1em; }
|
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 button lists */
|
||||||
.web-btn-wrapper {
|
.web-btn-wrapper {
|
||||||
display: flex;
|
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:
|
metadata:
|
||||||
title: Home
|
title: Home
|
||||||
isContentDivided: true
|
isContentDivided: true
|
||||||
|
hasTooltips: true
|
||||||
eleventyNavigation:
|
eleventyNavigation:
|
||||||
key: Home
|
key: Home
|
||||||
order: 1
|
order: 1
|
||||||
|
@ -74,11 +75,10 @@ eleventyComputed:
|
||||||
<h2>Always Proud</h2>
|
<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>
|
<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) -%}
|
{%- macro prideButton(file, alt, tooltip=alt, width=88, height=31) -%}
|
||||||
<li class="tipcontainer">
|
<li data-tooltip>
|
||||||
<button class="tipactivator" style="cursor: unset;">
|
<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">
|
<img src="/assets/buttons/pride/{{ file }}.png" alt="{{ alt }}" width="{{ width }}" height="{{ height }}" loading="lazy">
|
||||||
</button>
|
</button>
|
||||||
<p class="tooltip">{{ tooltip }}</p>
|
|
||||||
</li>
|
</li>
|
||||||
{%- endmacro -%}
|
{%- endmacro -%}
|
||||||
<ul class="web-btn-wrapper">
|
<ul class="web-btn-wrapper">
|
||||||
|
|
|
@ -4,11 +4,10 @@ date: 2023-01-28
|
||||||
---
|
---
|
||||||
|
|
||||||
{%- macro siteButton(url, file, alt, tooltip=alt, width=88, height=31) -%}
|
{%- macro siteButton(url, file, alt, tooltip=alt, width=88, height=31) -%}
|
||||||
<li class="tipcontainer">
|
<li data-tooltip>
|
||||||
<a href="{{ url }}" class="tipactivator">
|
<a href="{{ url }}" data-tooltip-trigger title="{{ tooltip }}">
|
||||||
<img src="/assets/buttons/{{ file }}" alt="{{ alt }}" width="{{ width }}" height="{{ height }}" loading="lazy">
|
<img src="/assets/buttons/{{ file }}" alt="{{ alt }}" width="{{ width }}" height="{{ height }}" loading="lazy">
|
||||||
</a>
|
</a>
|
||||||
<p class="tooltip">{{ tooltip }}</p>
|
|
||||||
</li>
|
</li>
|
||||||
{%- endmacro -%}
|
{%- endmacro -%}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
layout: main/links.njk
|
layout: main/links.njk
|
||||||
title: Links
|
title: Links
|
||||||
desc: A curated collection of links.
|
desc: A curated collection of links.
|
||||||
|
hasTooltips: true
|
||||||
eleventyNavigation:
|
eleventyNavigation:
|
||||||
order: 8
|
order: 8
|
||||||
---
|
---
|
||||||
|
|
Loading…
Reference in New Issue