Neu veröffentlicht: E-Commerce mit Power Pages, Stripe & Analytics

· Webentwicklung  · 5 minuten Lesezeit

Instagram Tastatur-Shortcuts blockieren Chrome Extension Eingaben

Wer "n" in einem Textfeld einer Chrome Extension tippt und Instagram öffnet plötzlich ein Panel. Das ist kein Zufall. Ein Blick auf Event-Bubbling, document-level Listener und warum stopPropagation() am richtigen Ort alles löst.

Wer "n" in einem Textfeld einer Chrome Extension tippt und Instagram öffnet plötzlich ein Panel. Das ist kein Zufall. Ein Blick auf Event-Bubbling, document-level Listener und warum stopPropagation() am richtigen Ort alles löst.

Inhalt

Das Problem

Das Overlay aus dem Artikel über Notiz und Tags im Capture-Overlay ist fertig. Notiz-Input, Tag-Chips, Keyboard-UX. Alles implementiert.

Beim ersten echten Test auf Instagram: Ich will einen Tag eintippen. Ich drücke “n”. Instagram öffnet ein Panel. Das Overlay bleibt geöffnet, aber der Fokus ist weg. Der “n”-Buchstabe landet nicht im Input-Feld.

Das gleiche mit “j”, “l”, anderen Buchstaben. Instagram hat globale Tastatur-Shortcuts. Und die Erweiterung fängt sie nicht ab.

Warum das passiert

Browser-Tastatur-Events folgen einem Pfad. Ein keydown-Event entsteht auf dem Element, das den Fokus hat. Es wandert dann nach oben durch den DOM-Baum. Dieser Weg heißt Event-Bubbling.

Am Ende des Wegs landet das Event auf document. Dort hört Instagram.

Instagram registriert seine Shortcut-Handler mit document.addEventListener("keydown", handler). Der Handler prüft, welche Taste gedrückt wurde: “n” öffnet Notifications, “j”/“k” scrollt durch den Feed, “l” liked den aktuellen Post.

Das Input-Feld der Extension ist im Shadow DOM. Wie ich diese Shadow-DOM-Isolation für das Overlay aufgebaut habe, beschreibe ich in Phase 2. Shadow DOM isoliert Stile, nicht Events. keydown-Events bubbeln aus Shadow DOM heraus und landen trotzdem auf document.

Input (Shadow DOM)
  → Shadow Root
    → Host Element (<div>)
      → <html>
        → document  ← Instagram's keydown handler wartet hier

Solange das Event document erreicht, sieht Instagram es.

Die ursprüngliche Implementierung

Das Overlay hatte den Keyboard-Handler auf document:

const onKey = (e: KeyboardEvent) => {
  if (e.key === 'Escape') {
    dismiss(false);
    return;
  }
  if (e.key === 'Enter' && e.ctrlKey) {
    dismiss(true);
    return;
  }
  // ...
};
document.addEventListener('keydown', onKey);

Das funktioniert für die Extension-eigene Logik. Aber der Handler liegt auf der gleichen Ebene wie Instagrams Handler. Beide empfangen das Event. Reihenfolge ist nicht garantiert.

Die Lösung

Der Keyboard-Handler gehört nicht auf document. Er gehört auf das Backdrop-Element des Overlays, ein <div> direkt im Shadow Root, das den gesamten sichtbaren Bereich des Overlays abdeckt.

backdrop.addEventListener('keydown', (e: KeyboardEvent) => {
  e.stopPropagation(); // Instagram does not see this event
  if (e.key === 'Escape') {
    dismiss(false);
    return;
  }
  if (e.key === 'Enter' && e.ctrlKey) {
    dismiss(true);
    return;
  }
  if (e.key === 'Enter' && !e.ctrlKey) {
    const focused = shadow.activeElement;
    const isInputFocused = focused instanceof HTMLInputElement || focused instanceof HTMLTextAreaElement;
    if (!isInputFocused) dismiss(true);
  }
});

e.stopPropagation() stoppt das Bubbling an diesem Punkt. Das Event verlässt das Overlay-Element nicht mehr. Es erreicht niemals document. Instagrams Handler sieht es nicht.

Warum die Input-Handler allein nicht reichen

Naiv könnte man denken: Ich füge stopPropagation() in die Input-Handler ein. Fertig.

noteInput.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') {
    e.preventDefault();
    e.stopPropagation(); // stops "Enter" - but what about "n", "j", "l"?
    tagInput.focus();
  }
});

Das stoppt nur die Keys, die ich explizit behandle. “n”, “j”, “l” und alle anderen Buchstaben? Die haben im Input-Handler keinen if-Zweig. Sie propagieren weiter.

Der Backdrop-Handler mit stopPropagation() auf dem ersten Statement fängt alle Keys ab, ohne Ausnahme. Kein einzelner Buchstabe erreicht document mehr.

backdrop.addEventListener('keydown', (e: KeyboardEvent) => {
  e.stopPropagation(); // this single call protects all inputs in the overlay
  // ...
});

keyup und keypress auch stoppen

keydown ist die häufigste Phase für Shortcuts. Aber sicher ist sicher. keyup und keypress werden ebenfalls gestoppt:

backdrop.addEventListener('keyup', (e) => e.stopPropagation());
backdrop.addEventListener('keypress', (e) => e.stopPropagation());

Das verhindert, dass Instagram andere Phasen des gleichen Events abgreift.

Kein Aufräumen nötig

Mit dem alten Ansatz brauchte dismiss() eine Cleanup-Zeile:

const dismiss = (confirmed: boolean) => {
  document.removeEventListener('keydown', onKey); // manual cleanup
  host.remove();
  // ...
};

Der Backdrop-Handler braucht das nicht. Wenn host.remove() den Shadow Host aus dem DOM entfernt, verschwinden alle daran hängenden Event-Listener automatisch mit. Der Browser garbage-collected sie.

Weniger Aufräum-Logik, weniger Fehlerquellen.

Das allgemeine Muster

Dieses Muster tritt überall auf, wo eine Extension UI in eine bestehende Seite injiziert.

Die Seite (Instagram, Twitter, YouTube, whatever) nutzt document-level Listener für Keyboard-Shortcuts. Die Extension injiziert ein Overlay oder ein Widget. Der Nutzer interagiert mit dem Widget. Die Seite sieht die Tastendrücke trotzdem.

Die Lösung ist immer dieselbe: stopPropagation() auf dem Wurzel-Element des injizierten UI, so früh wie möglich im Event-Handler.

// Generic pattern for injected extension UI
overlayRoot.addEventListener('keydown', (e) => {
  e.stopPropagation(); // ← host page does not see this keypress
  // custom logic
});

Shadow DOM hilft bei Stilen. Es hilft nicht bei Events. stopPropagation() hilft bei Events.

Event-Bubbling-Diagramm: Shadow DOM Input bis document, mit stopPropagation am Backdrop

Abbildung: keydown-Event-Pfad vom Shadow DOM Input bis zum document, gestoppt durch stopPropagation am Backdrop-Element.

Das Diagramm zeigt den Pfad eines keydown-Events aus dem Shadow DOM zum document. Ohne Intervention landet es bei Instagrams Handler. Mit stopPropagation() am Backdrop endet der Weg dort.


Alle Artikel der Serie

  1. Vision und Systemübersicht: Chrome Extension, RAG-Architektur, Projekthintergrund: Artikel lesen
  2. RAG-System Aufbau: Qdrant, Embeddings, Cosine-Ähnlichkeit in TypeScript: Artikel lesen
  3. AI Provider Abstraktion: Ollama vs. OpenAI, Interface-Design, kein Vendor-Lock-in: Artikel lesen
  4. Chrome Extension MV3: Drei isolierte Laufzeitkontexte, Message Passing, Strategy Pattern: Artikel lesen
  5. Docker Compose Strategie: Override-Pattern, von lokal zu Azure: Artikel lesen
  6. Ollama lokal vs. Docker: Die Entscheidung und ihre Konsequenzen: Artikel lesen
  7. Ollama Auto-Pull Entrypoint: Automatisiertes Modell-Setup beim Container-Start: Artikel lesen
  8. tsconfig und Vite: Node16 vs. bundler, warum Vite eigene Regeln hat: Artikel lesen
  9. Instagram Caption mit MutationObserver vollständig laden: Artikel lesen
  10. Chrome Extension Foundation mit Health-Dot und Retry-Queue: Artikel lesen
  11. Phase 2 Features: Shadow DOM Overlay, Tailwind v4, Duplicate Detection: Artikel lesen
  12. Race Condition bei der Plattformerkennung: Wie ein UI-Event die Instagram-Erkennung bricht: Artikel lesen
  13. PostId-Extraktion in zwei Instagram-Layouts: querySelector vs. Ancestor-Traversal: Artikel lesen
  14. Instagram Karussell vollständig erfassen mit MutationObserver: Artikel lesen
  15. Notiz und Tags beim Screenshot-Speichern: Artikel lesen
  16. Instagram Tastatur-Shortcuts blockieren Chrome Extension Eingaben (dieser Artikel)
  17. Lowercase-Normalisierung und Duplikat-Erkennung im Tag-Input: Artikel lesen
  18. Zitadel Login V2 in Docker Compose: drei versteckte Fehler: Artikel lesen
  19. PKCE OAuth in einer Chrome MV3 Extension: Artikel lesen
  20. React Frontend mit react-oidc-context und Zitadel: Artikel lesen
  21. Vite Build-Time-Umgebungsvariablen in Docker: Artikel lesen

Du baust ein Datenerfassungssystem auf Basis von Browser Extensions mit komplexen DOM-Interaktionen? Lass uns das gemeinsam einschätzen.

Zurück zum Blog

Ähnliche Beiträge

Alle Beiträge ansehen