feat: Accessible Tooltips

This commit is contained in:
Toerd@archlinux 2020-05-25 20:47:19 +02:00
parent 25cd0081d0
commit 2b4333fed4
2 changed files with 242 additions and 0 deletions

View File

@ -0,0 +1,222 @@
// Alias für die Queryselector Methoden
let $$ = document.querySelector.bind(document);
let $$$ = document.querySelectorAll.bind(document);
// Array für momentan angezeigte/aktive Tooltips
let visibleTooltips = [];
// Wenn der DOM fertig ist, alles für Screenreader vorbereiten
document.onload = makeTooltipsAccessible();
/**
* Durchsucht den DOM nach Elementen mit dem data-tooltip Attribut
* und erstellt für Screenreader kompatible Tooltips.
* Die Tooltips können mit den Klassen .ttip und .ttip-simple angepasst werden
*/
function makeTooltipsAccessible() {
$$$("[data-tooltip]").forEach(el => {
// Bestimmen ob simples Tooltip mit Text aus data-tooltip
// oder komplexeres Tooltip aus Element mit ID XYZ angefordert wird
tooltip = getTooltip(el);
// Wenn es ein Simples Tooltip war Element generieren und im Caller
// das data-tooltip Attribut anpassen. Das Element soll dauerhaft auf
// im DOM bleiben, damit Screenreader darauf zugriff haben
if (typeof tooltip === "string") {
let text = tooltip;
// Tooltip Element erstellen
tooltip = document.createElement("div");
tooltip.classList = "ttip-simple ttip";
// Randomisierte ID einfügen
tooltip.id = getRandomId();
el.dataset.tooltip = "#" + tooltip.id;
// Tooltip mit Text befüllen
tooltip.innerHTML = text;
document.body.appendChild(tooltip);
} else {
// Komlexeres Tooltip mit zuätzlicher Klasse ausstatten
tooltip.classList = (tooltip.classList.length > 0) ? tooltip.classList + " ttip" : "ttip";
}
// Screenreader-spezifische Attribute hinzufügen
if (!tooltip.hasAttribute("role")) {
tooltip.setAttribute("role", "tooltip");
}
if (!el.hasAttribute("aria-labelledby")) {
el.setAttribute("aria-labelledby", tooltip.id);
}
// Tooltips standardmäßig verstecken
tooltip.style.display = "none";
});
}
/**
* @return Eine pseudo-randomisierte id im Format: tt-xxxxxxxx...
*/
function getRandomId() {
let rid = "";
// Leere IDs vermeiden
while (!rid) {
// Float in Base 36 umwandeln und dann die 0. vorne wegschneiden
rid = Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2);
}
return "tt-" + rid;
}
/**
* Dieses Script funktioniert wie folgt:
* Einem beliebigen HTML Element kann ein Attribut mit dem Namen `data-tooltip`
* hinzugefügt werden. Dieses kann folgendes enthalten:
* 1. Den Tooltip Text selbst: data-tooltip='Mein Tooltip Text hier'
* 2. Eine ID eines anderen Elements: data-tooltip='#mein-tooltip-elem'
* Der `#` ist hierbei wichtig!
* Methode 2 kann z.B. für etwas komplexere Tooltips verwendet werden
*
* Die Styles von Tooltips findet man in _layout.scss (.ttip, .ttip-simple, .ttip-triangle)
*/
document.addEventListener("mouseover", e => {
let t = e.target;
if (typeof t.dataset.tooltip !== "undefined") {
// Mehrfaches erstellen bei hinundher zwischen Tooltip und Caller vermeiden
removeTooltips();
let ttip = positionTooltip(t);
// Nicht sicher ob das tatsächlich die eleganteste Variante ist.
// Man könnte auch ein unsichtbares Element verwenden, das die Lücke
// zwischen Caller und Tooltip ausfüllt um ein "Übergehen" auf das
// Tooltip zu ermöglichen.
// Hier hat man 500ms Zeit um das zu tun.
let oneshot = e => {
setTimeout((e) => {
let onTooltip = false;
$$$(":hover").forEach(el => {
// Man ist auf dem eigentlichen Tooltip
if (el.className.includes("ttip")) {
onTooltip = true;
return;
}
// Man ist noch immer auf dem Caller
if (typeof el.dataset.tooltip !== "undefined") {
onTooltip = true;
return;
}
});
if (!onTooltip) {
removeTooltips();
t.removeEventListener("mouseleave", oneshot);
}
}, 500);
};
t.addEventListener("mouseleave", oneshot);
ttip.addEventListener("mouseleave", e => {
removeTooltips();
});
}
});
/**
* Positioniert ein Tooltip abhänging von der Position von `caller` im Viewport
* und erzeugt ein kleines Dreieck, dass auf den `caller` zeigt.
* @param {object} caller Ein beliebiges Element mit einem `data` feld data-tooltip
* @return {object}
*/
function positionTooltip(caller) {
// Computed Style des Callers in Bezug auf den Viewport
let cBR = caller.getBoundingClientRect();
// Tooltip anfragen
let tooltip = getTooltip(caller);
// Farbe des kleinen Tooltip Dreiecks
let triColor = "white";
let triSize = 5;
// Dreieck für Tooltip erstellen
let tri = document.createElement("div");
tri.classList = "ttip-triangle";
tri.style.borderLeft = triSize + "px solid transparent";
tri.style.borderRight = triSize + "px solid transparent";
tooltip.style.display = "block";
// Dreieck in Tooltipfenster einfügen
tooltip.appendChild(tri);
// Position des Anfordenden Elements
let hPos = cBR.left + cBR.width / 2;
let vPos = cBR.top + cBR.height / 2;
// Mittelpunkt
let hMid = window.innerWidth / 2;
let vMid = window.innerHeight / 2;
// Position des Tooltips
let ttop;
let tleft;
// Position des Dreiecks
let mtop;
tooltip.style.position = "fixed";
let tBR = tooltip.getBoundingClientRect();
let triBR = tri.getBoundingClientRect();
// Die Position des "Tooltip-Callers" in diesem Schema:
/*
-----------------
| | |
| o | |
| | o |
-----------------
| | o |
| | |
|o | |
-----------------
*/
// bestimmt die Positionierung des Tooltips und des kleinen Dreiecks
// Links
if (hPos <= hMid) {
tleft = cBR.left;
// Dreieck positionieren
tri.style.left = cBR.width / 2 - triSize + "px";
// Rechts
} else {
tleft = cBR.right - tBR.width;
// Dreieck positionieren
tri.style.right = cBR.width / 2 - triSize + "px";
}
// Oben
if (vPos <= vMid) {
// Tooltip Abstand zum Caller
ttop = cBR.bottom + triSize;
// Dreieck positionieren
tri.style.borderBottom = triSize + "px solid " + triColor;
tri.style.top = -triSize + "px";
// Unten
} else {
// Tooltip Abstand zum Caller
ttop = cBR.top - tBR.height - triSize;
// Dreieck positioniere
tri.style.borderTop = triSize + "px solid " + triColor;
tri.style.bottom = -triSize + "px";
}
// Tooltip
tooltip.style.top = ttop + "px";
tooltip.style.left = tleft + "px";
// Zum Array der momentan angezeigten Tooltips hinzufügen um später zu entfernen
visibleTooltips.push(tooltip);
return tooltip;
}
function getTooltip(element) {
let tt = element.dataset.tooltip;
// Entweder das Tooltip Element oder den Tooltip Text zurückgeben
return (/^#.+$/.test(tt)) ? $$(tt) : tt;
}
function removeTooltips() {
// Elemente ausblenden und Dreieck entfernen
visibleTooltips.forEach(ele => {
ele.style.display = "none";
// Hinzugefügtes Dreieck entfernen
ele.removeChild(ele.lastChild);
});
// Array leeren, da alle Tooltips weg sein sollten
visibleTooltips = [];
}

20
dynamic_tooltips.scss Normal file
View File

@ -0,0 +1,20 @@
/* Styles für die in tooltip.js erzeugten Tooltip Elemente */
/* tooltip war schon anderweitig benutzt (Bootstrap) */
.ttip {
overflow-wrap: break-word;
max-width: 250px;
background: white;
filter: drop-shadow(0 0 8px rgba(0, 0, 0, 0.1));
}
/* Styles für die Autogenerierten Tooltips */
.ttip-simple {
padding: 10px;
}
/* Die Farbe des Dreiecks muss über /assets/js/tooltip.js gesteuert werden */
.ttip-triangle {
position: absolute;
width: 0;
height: 0;
}