feat: Accessible Tooltips
This commit is contained in:
parent
25cd0081d0
commit
2b4333fed4
222
dynamic_accessible_tooltips.js
Normal file
222
dynamic_accessible_tooltips.js
Normal 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
20
dynamic_tooltips.scss
Normal 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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user