feat(diagram): Knoten-Fenster öffnet nur noch auf Klick, nicht mehr bei Hover (D92, SPEC §6/§9)

Der einfache Klick toggelt das Fenster überall — wie der Tipp auf Touch
(D52); der Link eines Knotens ist damit auch am Zeiger der ↗-Knopf im
Fenster, Enter und Strg+Klick öffnen die URL weiterhin direkt. Tastatur-
fokus öffnet nur noch echten (:focus-visible), focusout schließt nicht in
das Fenster hinein, und die Fokus-Rückgabe nach dem Falten ruft das
Fenster ausdrücklich nie.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-28 16:43:01 +02:00
co-authored by Claude Fable 5
parent a223e6af57
commit 0fe4c07851
5 changed files with 154 additions and 71 deletions
+2
View File
@@ -19,6 +19,8 @@ reverse.
## 2026-08-28
- The node window no longer opens on hover: click a node to open it, click again or elsewhere to close — folding and moving across the tree stay snappy
- A linked node's plain click now opens the window everywhere, like on touch — the URL sits behind the ↗ button, and Enter on the focused node or Ctrl+click still open it directly
- The Taiga buttons in the node window sit side by side in one row now — the window grows a little wider for them, and they wrap only when space truly runs out
- The open button moved into the ticket box's action row, next to write and take, and its label shrank to "Open #US-123" — the neighbours say where
- "Link ticket" in the node window binds an existing Taiga story or task to a node: paste the ticket's URL — it carries project, type and number by itself — or enter a ref or bare number
+65
View File
@@ -8568,3 +8568,68 @@ längsten Labels): drei Anlege-Knöpfe eine Zeile, „→ nach Taiga schreiben
Ref-Knoten ohne Projekt-Slug (keine Knöpfe) fällt auf 336px zurück —
`tipwide` ist weg. 628 Tests unverändert (reine UI-Verdrahtung,
Browser-geprüft — die D54-Nachtrag-3-Grenze).
## D92 — Das Knoten-Fenster öffnet nur noch auf Klick, nicht mehr beim Überfahren
Gemeldet vom Nutzer: „Mit mehr Funktionalität in dem Popup, so dass es auch
träger geworden ist als der einfache Tooltip vorher, gibt es ein
Usability-Problem: Wenn man nur einen Knoten auf-/zuklappen will, kommt schon
das Popup." Das trifft die Sache: D57 hat dem Fenster den Hover gegeben, als
es noch ein besserer Tooltip war — Beschreibung plus Kurz-Fakten. Seit D91
trägt es Ticket-Stand, Anlege-, Verknüpfen- und Abgleich-Knöpfe und ist
breiter geworden (`tipwide`, D91-Nachtrag 12); beim Ansteuern des Falt-Chips
stand es regelmäßig schon im Weg. Ein Fenster mit Aktionen ist kein Tooltip
mehr — es verdient eine ausdrückliche Geste.
**Der einfache Klick öffnet, ein zweiter (oder daneben) schließt** — wörtlich
die Touch-Regel aus D52, jetzt überall. Damit fällt zwangsläufig eine
Entscheidung über die **verlinkten Knoten** (§6: der einfache Klick öffnete
dort die URL): Eine Geste kann nicht beides tun, und D25 hat ausdrücklich
abgelehnt, dass derselbe Klick je nach Knoten Verschiedenes tut. Also gilt
die D52-Abwägung jetzt auch am Zeiger — „ansehen ist die häufigere Absicht,
und der Link bleibt einen Klick entfernt": Der **↗-Knopf steht in jedem
Fenster eines verlinkten Knotens**, nicht mehr nur auf Touch. Direkt zur URL
führen weiterhin **Enter** am fokussierten Knoten (der native Weg; im
Klick-Handler an `e.detail === 0` erkannt und durchgelassen) und
**Strg+Klick** (Browser-Default, Hintergrund-Tab — auf Zeilen mit
Ticket-Referenz gewinnt das Taiga-Öffnen, D91-Nachtrag 5, wie bisher).
Alt+Klick (Sprung, D25) und der Falt-Chip (D38) sind im Handler ausgenommen
und bleiben, was sie sind.
**Tastaturfokus öffnet weiter sofort — aber nur echten.** Das war die
verdeckte Falle des Umbaus: Auch der Mausklick fokussiert den Knoten, und der
`focusin`-Handler (D57) hätte das Fenster geöffnet, bevor der Klick-Toggle es
für „schon offen" hielte und sofort wieder schlösse — netto bliebe es zu.
Unterschieden wird mit `:focus-visible`, der Browser-eigenen Heuristik, die
D25-Nachtrag 2 für genau diese Frage schon vermessen hat (nach Mausklick
falsch, nach Tab wahr). Nebenwirkung, bewusst hingenommen: Auch der
programmatische Fokus nach Alt+Klick aus dem Text und nach dem
Stationen-Knopf (D47) folgt der Heuristik — nach einer Maus-Geste öffnet dort
kein Fenster mehr mit. Das passt zur Stoßrichtung dieses Eintrags: weniger
ungefragte Fenster.
**Und `focusout` darf den Knopf-Klick nicht mehr töten.** Mit dem Klick ist
der Knoten beim offenen Fenster jetzt regelmäßig **fokussiert** — vorher
(Hover) war er das nicht, und der `focusout`-Schließer war harmlos. Die
Knöpfe im Fenster sind trotz `tabindex="-1"` klick-fokussierbar: mousedown
auf einen Taiga-Knopf blurte den Knoten, `focusout` schlösse das Fenster
zwischen mousedown und mouseup, und der Klick ginge an einem
`display:none`-Element verloren. Deshalb schließt `focusout` nicht, wenn
`relatedTarget` im Fenster liegt; Tab zum nächsten Knoten schließt weiter.
**Ersatzlos weg:** die 350-ms-Öffnungs- und 120-ms-Schließ-Timer, die
Sofort-Weiterreiche bei schon offenem Fenster, `pointerenter`/`pointerleave`
am Fenster und die `finePointer`-Weiche — das Fenster ist jetzt auf allen
Geräten dieselbe Klick-Sache, und ein einmal geöffnetes bleibt stehen, bis
etwas es ausdrücklich schließt (zweiter Klick, Klick daneben, Esc, ×,
Scrollen, Neubau — die D52-Liste, unverändert). Die 400-ms-Verzögerung des
Ticket-Abrufs (D91-Nachtrag 6) bleibt: Sie schützt jetzt das Durch-Tabben
statt des Vorüberfahrens, und der Bulk-Cache (D91-Nachtrag 10) macht sie
ohnehin meist unsichtbar.
**Der Preis, benannt:** Beim schnellen Überfliegen eines fremden Plans
zeigte der Hover die Beschreibungen ohne Klick — das ist weg, je Knoten
kostet es jetzt einen Klick. Die ”-Marke (D40) und der ↗ hinter dem Label
sagen weiterhin, wo sich einer lohnt. Und ein Doppelklick öffnet und
schließt in einem Zug — hinnehmbar, auf Knoten doppelklickt niemand
absichtlich.
+29 -23
View File
@@ -414,13 +414,16 @@ Größe als **Bereich** gelesen (Untergrenzen verdoppeln sich):
## 6. Links
- Ein nacktes `https://…`-Token macht den ganzen Knoten klickbar
(neuer Tab, `rel="noopener"`); Kennzeichnung mit ↗ hinter dem Label.
- **Ausnahme auf Touch-Geräten:** Dort öffnet der einfache Tipp nicht die URL,
sondern das Knoten-Fenster (§9) — der Link steht darin als eigener Knopf. Ohne
Zeiger gibt es keinen Tooltip, und ein einziger Tipp kann nicht beides tun;
„ansehen" ist die häufigere Absicht, und der Link bleibt einen Tipp entfernt.
Siehe D52.
- Ein nacktes `https://…`-Token verlinkt den Knoten; Kennzeichnung mit ↗
hinter dem Label.
- Geöffnet wird die URL über den **↗-Knopf im Knoten-Fenster** (§9; neuer
Tab, `rel="noopener"`): Der einfache Klick wie der einfache Tipp öffnen das
**Fenster**, nicht die URL — eine Geste kann nicht beides tun, „ansehen"
ist die häufigere Absicht, und der Link bleibt einen Klick entfernt
(D52 für Touch, D92 für den Zeiger). **Enter** am fokussierten Knoten und
**Strg+Klick** (Browser-Default, Hintergrund-Tab) öffnen die URL weiterhin
direkt — auf Zeilen mit Ticket-Referenz gewinnt bei Strg+Klick das
Taiga-Öffnen (§9).
## 7. Personen-Tags
@@ -469,11 +472,13 @@ Grafikexport: Der Text selbst kann dort nicht angezeigt werden, eine Marke
ohne Ziel wäre Rauschen. Siehe D40.
**Gezeigt wird das immer im eigenen Knoten-Fenster**, nicht im Tooltip des
Browsers: am Zeiger nach kurzer Verzögerung beim Überfahren, bei Tastaturfokus
sofort, auf Touch beim einfachen Tipp (§6). Ein `title` kann weder Absätze noch
eine Linie und erscheint nie beim Tastaturfokus; das Fenster kann beides. Der
Inhalt ist derselbe — die Beschreibung als Absatz, die Kurz-Fakten dahinter
durch eine echte Linie abgesetzt. Siehe D57.
Browsers: beim **einfachen Klick bzw. Tipp** auf den Knoten (ein zweiter
schließt), bei **Tastaturfokus** sofort. Beim bloßen Überfahren mit dem
Zeiger öffnet es **nicht** — das tat es zwischenzeitlich (D57) und stand
damit dem Falten und dem Bewegen über den Baum im Weg (D92). Ein `title`
kann weder Absätze noch eine Linie und erscheint nie beim Tastaturfokus; das
Fenster kann beides. Der Inhalt ist derselbe — die Beschreibung als Absatz,
die Kurz-Fakten dahinter durch eine echte Linie abgesetzt. Siehe D57, D92.
- Das Fenster hängt am angetippten Knoten (kleine Spitze zu ihm hin) und weicht
nach oben aus, wenn unten kein Platz ist. Der Knoten trägt, solange es offen
@@ -481,13 +486,12 @@ durch eine echte Linie abgesetzt. Siehe D57.
- Es schließt beim Tipp daneben, beim erneuten Tipp auf denselben Knoten, mit
**Esc**, über sein ×, beim Scrollen des Diagramms und bei jedem Neubau.
- Ein **verlinkter** Knoten (§6) bekommt darin einen ↗-Knopf, der die URL im
neuen Tab öffnet — auf Touch ist das der Weg zum Link.
- Der **Sprung-Hinweis** nennt hier den langen Druck statt Alt+Klick (§9,
neuen Tab öffnet — der einfache Klick/Tipp gehört dem Fenster (§6, D92).
- Der **Sprung-Hinweis** nennt auf Touch den langen Druck statt Alt+Klick (§9,
Sprung zwischen Diagramm und Text) — Alt gibt es auf dem Telefon nicht.
- Reine Bedienhilfe: nicht im Grafikexport, nicht im Druck. Auf Geräten mit
Zeiger ändert sich nichts, dort bleibt es beim Tooltip.
- Reine Bedienhilfe: nicht im Grafikexport, nicht im Druck.
Siehe D52.
Siehe D52, D92.
**Ticket-Stand im Knoten-Fenster (§11).** Trägt die Zeile eine
Ticket-Referenz (`#US-123` / `#T-1234`) und ist ihr Teilbaum per
@@ -519,8 +523,9 @@ es einen gibt, den **Zuständigen**.
Farbe des Knotens ist die Rückmeldung.
- Beide Richtungen betreffen **einen** Knoten; eine Sammelaktion über einen
Teilbaum gibt es nicht.
- Geholt wird erst, wenn das Fenster **kurz stehen bleibt** (nicht im
Vorüberfahren), und je Ticket **einmal je Sitzung** ein ↻-Knopf im Fenster
- Geholt wird erst, wenn das Fenster **kurz stehen bleibt** (nicht beim
Durch-Tabben von Knoten zu Knoten), und je Ticket **einmal je Sitzung**
ein ↻-Knopf im Fenster
holt neu. Ohne Anmeldung an der Instanz, ohne Projekt-Zuordnung oder ohne
konfiguriertes Backend geschieht still nichts; ein Fehler steht als Zeile im
Fenster.
@@ -1086,8 +1091,9 @@ verknüpft (siehe D25):
freistehend — dort behält Strg+Klick den Sprung zur Zeile; die
D67-Ausschlüsse (Kommentar, Beschreibungsteil, URL) gelten auch hier.
Siehe D91-Nachtrag 5.
- Der **einfache** Klick bleibt unverändert der Link (§6): Ein Knoten mit URL
öffnet sie weiterhin im neuen Tab. Deshalb Alt und nicht der einfache Klick.
- Der **einfache** Klick gehört dem Knoten-Fenster (§6/§9, D92): Er öffnet die
Beschreibung, nicht die Zeile im Text. Deshalb Alt und nicht der einfache
Klick.
- **Auffindbarkeit:** Solange **Alt gedrückt** ist, zeigen alle Knoten den
Sprung-Cursor und der Knoten unter dem Zeiger einen Petrol-Ring — die Geste
ist im Moment des Ausprobierens sichtbar, auch auf verlinkten Knoten. Dazu
@@ -1207,8 +1213,8 @@ Label: **▾** offen, **„▸ n“** eingeklappt (n = Zahl der verborgenen Knot
als kleiner **gerahmter Chip**, damit das Klickziel zu treffen ist (D64; das
nackte Glyph war zu klein). Klick auf das Zeichen klappt um — der einfache
Klick auf den Knoten selbst
bleibt der Link (§6); Tastatur: **←** klappt zu, **→** klappt auf am
fokussierten Knoten (WAI-ARIA-Baum-Idiom).
öffnet das Knoten-Fenster (§9, D92); Tastatur: **←** klappt zu, **→** klappt
auf am fokussierten Knoten (WAI-ARIA-Baum-Idiom).
- **Anfangszustand aus dem Text (§1):** `>` klappt ein. `<` holt seinen
Teilbaum hervor, indem die Faltung die Pfad-Ebenen **hinunterwandert**:
+9
View File
@@ -654,6 +654,15 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
Es schließt bei allem, was seine Aussage hinfällig macht — Tipp daneben,
zweiter Tipp, Esc, ×, Diagramm-Scroll, Bereichswechsel, Sprung in den Text,
`render()`. Die letzten drei sind beim Bauen zuerst vergessen worden.
- **Das Knoten-Fenster öffnet auf Klick, nie auf Hover (D92):** Der einfache
Klick toggelt es überall (auch auf Link-Knoten — die URL ist der ↗-Knopf im
Fenster; Enter kommt als Klick mit `e.detail === 0` und wird durchgelassen).
Zwei Fallen: `focusin` öffnet nur bei `:focus-visible` — der Mausklick
fokussiert den Knoten ebenfalls, und ohne die Weiche schlösse der
Klick-Toggle das vom focusin geöffnete Fenster sofort wieder. Und
`focusout` schließt nicht, wenn `relatedTarget` im Fenster liegt — die
Knöpfe darin sind trotz `tabindex="-1"` klick-fokussierbar, sonst stirbt
der Knopf-Klick zwischen mousedown und mouseup.
- **Taiga-Ticket-Anlage (D91):** Die entscheidbaren Regeln liegen headless in
`taiga.js` (`ticketRefOf` = Idempotenz-Marker, `taskCandidates` =
Dialog-Vorbelegung, `storyAncestor` = der nächste Vorfahr mit Story-Ref,
+49 -48
View File
@@ -1462,7 +1462,18 @@ function applyFoldPreset(mode){
/* Faltung umklappen (SPEC §9, D38). Gelingt das Zurückschreiben, ist der Text
der Zustand die Überlagerungen werden dann geleert, damit sie ihn nicht
maskieren können. Nach dem Neubau bekommt derselbe Knoten den Fokus zurück,
sonst risse die Tastaturbedienung ab (das alte Element ist weg). */
sonst risse die Tastaturbedienung ab (das alte Element ist weg) aber ohne
das Knoten-Fenster zu rufen: Falten ist genau die Geste, der das Fenster im
Weg stand (D92), und die `:focus-visible`-Heuristik ist beim
programmatischen Fokus nicht verlässlich genug für diese Zusage. */
let suppressTipFocus = false;
function refocusNode(line){
const again = out.querySelector('.node[data-line="' + line + '"]');
if(!again) return;
suppressTipFocus = true; /* focusin läuft synchron im focus() */
again.focus({preventScroll: true});
suppressTipFocus = false;
}
function toggleFold(el){
const line = +el.dataset.line;
const st = foldByLine.get(line);
@@ -1473,16 +1484,14 @@ function toggleFold(el){
if(lens){
lensOverrides.set(st.key, !st.collapsed);
render();
const again2 = out.querySelector('.node[data-line="' + line + '"]');
if(again2) again2.focus({preventScroll: true});
refocusNode(line);
return;
}
foldOverrides.set(st.key, !st.collapsed);
/* Das Schreiben löst per `input`-Ereignis schon ein render() aus. */
if(writeFoldToText(line, !st.collapsed)) foldOverrides.clear();
else render();
const again = out.querySelector('.node[data-line="' + line + '"]');
if(again) again.focus({preventScroll: true});
refocusNode(line);
}
/* Klick auf das Falt-Zeichen ▾/ klappt um. preventDefault, weil das Zeichen
@@ -1577,7 +1586,7 @@ out.addEventListener('touchend', e => {
`preventDefault()` öffnete ein Link-Knoten zusätzlich seine URL auf Touch
ist der Link stattdessen ein Knopf IM Fenster. */
e.preventDefault();
toggleNodeTip(tapped);
toggleNodeTip(tapped, true);
}, {passive: false});
out.addEventListener('touchcancel', disarmPress);
out.addEventListener('contextmenu', e => { if(pressTimer || armedEl) e.preventDefault(); });
@@ -1605,14 +1614,14 @@ function closeNodeTip(){
nodeTip.hidden = true;
}
function toggleNodeTip(el){
function toggleNodeTip(el, touch){
if(tipNode === el){ closeNodeTip(); return; } /* zweiter Tipp schließt */
showNodeTip(el, true);
showNodeTip(el, touch);
}
/* `touch` unterscheidet die beiden Anlässe: Am Zeiger ist der ganze Knoten der
Link (§6), ein -Knopf wäre dort ein zweiter Weg zum selben Ziel; und der
Sprung-Hinweis nennt Alt+Klick, den es auf dem Telefon nicht gibt. */
/* `touch` wählt nur noch den Sprung-Hinweis (langer Druck statt Alt+Klick).
Der -Knopf steht seit D92 an JEDEM verlinkten Knoten: Der einfache Klick
öffnet überall das Fenster, nicht mehr die URL wie der Tipp auf Touch. */
function showNodeTip(el, touch){
closeNodeTip();
const title = el.getAttribute('data-tip') || '';
@@ -1621,7 +1630,7 @@ function showNodeTip(el, touch){
const desc = i >= 0 ? title.slice(0, i) : '';
const roh = i >= 0 ? title.slice(i + sep.length) : title;
const facts = touch ? roh.replace(t('jumpHint'), t('jumpHintTouch')) : roh;
const href = touch && el.tagName === 'A' ? el.getAttribute('href') : null;
const href = el.tagName === 'A' ? el.getAttribute('href') : null;
/* Absätze: Leerzeilen trennen (SPEC §1), einzelne Zeilenumbrüche sind bloß
der Umbruch der Quelldatei und werden zu Leerzeichen. Der `title` kann das
nicht er zeigt die harten Umbrüche und sah im schmalen Fenster
@@ -1661,49 +1670,41 @@ function placeNodeTip(el){
Math.max(12, Math.min(anchorX - left, w - 12)).toFixed(1) + 'px');
}
/* ---------- Dasselbe Fenster am Zeiger und an der Tastatur (D57) ----------
/* ---------- Dasselbe Fenster am Klick und an der Tastatur (D57, D92) ----------
Es löst den nativen `title` ab: Der konnte weder Absätze noch eine Linie
(der Trennstrich war aus 24 `` gemalt, D40-Nachtrag) und erschien nie beim
Tastaturfokus. Beides kann das Fenster ohnehin schon es wurde nur für
Touch gebaut (D52).
Die Verzögerung ist Absicht-Erkennung: Über einen dichten Baum fährt man
hinweg, ohne etwas wissen zu wollen. Beim Wechsel von Knoten zu Knoten
zeigt es sofort weiter wer schon liest, wartet nicht noch einmal. */
const TIP_DELAY = 350;
let tipTimer = null, tipLeave = null;
const finePointer = () => window.matchMedia('(hover:hover) and (pointer:fine)').matches;
function cancelTipTimers(){
clearTimeout(tipTimer); clearTimeout(tipLeave);
tipTimer = tipLeave = null;
Tastaturfokus. Der Hover-Weg aus D57 ist ausgebaut (D92): Mit den
Taiga-Aktionen wurde das Fenster gewichtiger, und beim bloßen Ansteuern des
Falt-Chips stand es schon im Weg. Der einfache Klick öffnet es jetzt
überall wie der Tipp auf Touch (D52); der Link eines Knotens ist damit
auch am Zeiger der -Knopf IM Fenster (Strg+Klick und Enter behalten den
Browser-Default und öffnen die URL direkt). */
out.addEventListener('click', e => {
if(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
if(e.detail === 0) return; /* Enter am Link-Knoten bleibt der Link (D25) */
if(e.target && e.target.closest && e.target.closest('.fold')) return;
const el = nodeFromEvent(e);
if(!el) return;
e.preventDefault(); /* der Klick gehört dem Fenster, nicht der URL */
toggleNodeTip(el, false);
});
/* Tastaturfokus öffnet weiter sofort (D57) aber nur echten
(`:focus-visible`): Auch der Mausklick fokussiert den Knoten, und dann
entschiede sonst der focusin statt des Klick-Handlers der Toggle darüber
schlösse das eben geöffnete Fenster sofort wieder. */
function kbFocus(el){
try{ return el.matches(':focus-visible'); }catch(_){ return false; }
}
out.addEventListener('pointerover', e => {
if(e.pointerType === 'touch' || !finePointer()) return;
const el = e.target.closest && e.target.closest('.node');
if(!el || el === tipNode) return;
cancelTipTimers();
/* Steht schon eines offen, ist die Absicht erwiesen — dann ohne Warten. */
tipTimer = setTimeout(() => showNodeTip(el, false), tipNode ? 0 : TIP_DELAY);
});
out.addEventListener('pointerout', e => {
if(e.pointerType === 'touch' || !finePointer()) return;
clearTimeout(tipTimer);
/* Nicht sofort zumachen: Der Weg ins Fenster führt über den Zwischenraum,
und dort ist der Zeiger kurz über keinem von beiden. */
clearTimeout(tipLeave);
tipLeave = setTimeout(() => {
if(!nodeTip.matches(':hover')) closeNodeTip();
}, 120);
});
nodeTip.addEventListener('pointerenter', cancelTipTimers);
nodeTip.addEventListener('pointerleave', () => { closeNodeTip(); });
/* Tastaturfokus: ohne Verzögerung er ist bereits die ausdrückliche Absicht,
und ein `title` hat hier noch nie etwas gezeigt. */
out.addEventListener('focusin', e => {
if(suppressTipFocus) return; /* Fokus-Rückgabe nach dem Falten (D38/D92) */
const el = e.target.closest && e.target.closest('.node');
if(el && el !== tipNode) showNodeTip(el, false);
if(el && el !== tipNode && kbFocus(el)) showNodeTip(el, false);
});
out.addEventListener('focusout', e => {
/* Fokus INS Fenster (die Knöpfe darin sind klick-fokussierbar) schließt
nicht sonst verschwände der Knopf zwischen mousedown und mouseup und
sein Klick ginge verloren. */
if(e.relatedTarget && nodeTip.contains(e.relatedTarget)) return;
const el = e.target.closest && e.target.closest('.node');
if(el && el === tipNode) closeNodeTip();
});