From f99260372b64d373f15461a27f8bfac90d8090f8 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe <259325684+Sucukdeluxe@users.noreply.github.com> Date: Tue, 22 Sep 2026 05:38:13 +0200 Subject: [PATCH] Fix backup submenu hover gaps and keyboard interactions --- PROJECT_MEMORY.md | 2 + renderer/app.js | 29 +++++++++--- renderer/styles.css | 20 ++++++++- tests/startup-renderer.test.js | 82 ++++++++++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 8 deletions(-) diff --git a/PROJECT_MEMORY.md b/PROJECT_MEMORY.md index 5ca92f9..ddb7df1 100644 --- a/PROJECT_MEMORY.md +++ b/PROJECT_MEMORY.md @@ -6,6 +6,7 @@ Multi-Hoster-Upload ist eine Electron-Desktopanwendung für Windows, die große ## Aktueller Zustand +- Nach `2.1.49`, noch nicht veröffentlicht: Backup-Untermenü erhält eine durchgängige Hover-Fläche über den sichtbaren Abstand und 250 ms abbrechbare Schließverzögerung. Hover plus Klick bleibt offen, erneutes Betreten startet keine neue Öffnungsanimation; Tastaturfokus hält das Untermenü offen. Pfeil und Pfeiltasten folgen der linken Öffnungsrichtung, Escape schließt nur die aktuelle Untermenüebene. Fremde `animationend`-Ereignisse schließen kein übergeordnetes Panel mehr. Electron-Regression prüft DOM-Treffertests im Abstand, verzögerte Ein-/Austritte, Klick, Fokus und Escape bei 850 und 600 Pixel Fensterbreite. Kein Release beauftragt. - Release `2.1.49` auf beiden Plattformen veröffentlicht und geprüft: Vidmoly-Sitzungsprüfung, getrennte Upload-Sperrmeldungen, Checkbox-Fokusfix und GB-Dateigrößenlimit. Live-Diagnose am 22.09.2026 bestätigt zuerst gültige Anmeldung mit `UPLOAD_DISABLED` (Upload-Konfiguration HTTP 403), danach erfolgreiche Anmeldung und vollständige Upload-Konfiguration (HTTP 200). Nutzer bestätigt funktionierende Anmeldung und gibt Release frei. Ein tatsächlicher Dateitransfer wurde dadurch nicht nachgewiesen. Ältere Angaben zu unveröffentlichten Änderungen darunter dokumentieren die Vorbereitung; diese Änderungen sind jetzt in `2.1.49` enthalten. - Vidmoly nach `2.1.48`, noch nicht veröffentlicht: Anmeldung wird über `/api/auth/me` statt anhand der Upload-Konfiguration verifiziert. Öffentliches Website-JavaScript am 22.09.2026 bestätigt unveränderte Upload-Felder sowie `DISK_FULL`, `UPLOAD_IP_BLACKLIST` und `UPLOAD_DISABLED`; diese Antworten werden jetzt getrennt und ohne Antwortdaten angezeigt. Upload übernimmt zusätzlich das aktuelle Webfeld `tos=1`, weiterhin ohne API-Key. HTTP-Fehler, fehlende Benutzer, OTP-Anforderung und ungültige Serveradressen werden geprüft. Die konkrete Serverantwort des gemeldeten Accounts war in den lokalen Logs nicht vorhanden; erfolgreicher Live-Login/Upload ist noch nicht bestätigt. - Nach `2.1.48` lokal geändert, noch nicht veröffentlicht: Checkboxen in den Hoster-Einstellungen übernehmen beim Mausklick nicht mehr den Fokus-Schatten von Textfeldern. Tastatur-Fokusmarkierung bleibt erhalten; echter isolierter Electron-Test prüft An-/Abhaken, Tabulator, Leertaste und unveränderten Zahlenfeld-Fokus. @@ -88,6 +89,7 @@ npm audit --omit=dev ## Zuletzt verifiziert +- Backup-Untermenü nach `2.1.49`: 838 Haupttests, 18 Servertests und Lint erfolgreich. Gezielter Electron-Test nach abschließender Fokuskorrektur erneut erfolgreich; ein Mausklick auf den Trigger hält das Menü beim Verlassen nicht unbeabsichtigt offen. Änderungen noch nicht veröffentlicht. - Release `2.1.49`: Titel, Tags, englischer GitHub- und deutscher Forgejo-Changelog, jeweils vier Assets und Update-Metadaten geprüft. Alle acht veröffentlichten Dateien erneut heruntergeladen und Größe sowie SHA-512 mit lokalen Dateien verglichen. Echter Updater erkennt `2.1.49` ausgehend von `2.1.48` und akzeptiert englische Hinweise sowie Forgejo-Manifest. Öffentlicher Quellcheck einschließlich Screenshots erfolgreich (164 Dateien). Keine produktive Serveränderung erforderlich. - Releasevorbereitung `2.1.49`: 837 Haupttests und 18 Backup-API-Tests erfolgreich, Lint erfolgreich, Produktionsaudit ohne Schwachstellen. Installer 98.033.241 Bytes, Portable 97.808.535 Bytes, beide Produktversion `2.1.49`. Gepackte Vidmoly-, Upload-, UI- und Backupmodule entsprechen dem Quellstand; keine Nutzerdaten enthalten. Installer-SHA-512 und Größe stimmen mit dem Update-Manifest überein. - Vidmoly-Änderung vom 22.09.2026: alle sieben neuen Regressionen und Lint erfolgreich. Hauptlauf: 836 von 837 Tests erfolgreich; bestehender Electron-Checkbox-Fokustest einmal mit Timingfehler, separater Wiederholungslauf der gesamten Datei mit 23 von 23 erfolgreich. Backup-API weiterhin 18 von 18 erfolgreich. Öffentlicher Quellcheck umfasst 164 Dateien. Keine Live-Anmeldung und kein Release durchgeführt. diff --git a/renderer/app.js b/renderer/app.js index 72cffbf..ca83956 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -1585,7 +1585,8 @@ function initMenuBar() { panel.classList.remove('menu-opening', 'menu-closing'); void panel.offsetHeight; panel.classList.add('menu-closing'); - const finish = () => { + const finish = (event) => { + if (event && event.target !== panel) return; if (!Object.is(panelTokens.get(panel), token)) return; panel.style.display = 'none'; panel.classList.remove('menu-closing'); @@ -1623,30 +1624,44 @@ function initMenuBar() { menuBar.querySelectorAll('.menu-submenu').forEach(sm => { const sub = sm.querySelector('.menu-submenu-dropdown'); const trigger = sm.querySelector('.menu-submenu-trigger'); + let closeTimer = null; + const cancelClose = () => { clearTimeout(closeTimer); closeTimer = null; }; + const opensLeft = () => window.getComputedStyle(sub).right !== 'auto'; const openSubmenu = (focusFirst = false) => { - openPanel(sub); + cancelClose(); + if (trigger?.getAttribute('aria-expanded') !== 'true') openPanel(sub); trigger?.setAttribute('aria-expanded', 'true'); if (focusFirst) requestAnimationFrame(() => sub.querySelector('[data-menu-action]')?.focus()); }; const closeSubmenu = () => { + cancelClose(); closePanel(sub); trigger?.setAttribute('aria-expanded', 'false'); }; sm.addEventListener('mouseenter', () => openSubmenu()); - sm.addEventListener('mouseleave', closeSubmenu); + sm.addEventListener('mouseleave', () => { + cancelClose(); + closeTimer = setTimeout(() => { + if (!sub.contains(document.activeElement)) closeSubmenu(); + }, 250); + }); + sm.addEventListener('focusin', cancelClose); + sm.addEventListener('focusout', (event) => { + if (!sm.contains(event.relatedTarget) && !sm.matches(':hover')) closeSubmenu(); + }); trigger?.addEventListener('click', (event) => { event.stopPropagation(); - if (trigger.getAttribute('aria-expanded') === 'true') closeSubmenu(); - else openSubmenu(); + openSubmenu(); }); trigger?.addEventListener('keydown', (event) => { - if (!['Enter', ' ', 'ArrowRight', 'ArrowDown'].includes(event.key)) return; + if (!['Enter', ' ', opensLeft() ? 'ArrowLeft' : 'ArrowRight', 'ArrowDown'].includes(event.key)) return; event.preventDefault(); openSubmenu(true); }); sub.addEventListener('keydown', (event) => { - if (event.key !== 'Escape' && event.key !== 'ArrowLeft') return; + if (event.key !== 'Escape' && event.key !== (opensLeft() ? 'ArrowRight' : 'ArrowLeft')) return; event.preventDefault(); + event.stopPropagation(); closeSubmenu(); trigger?.focus(); }); diff --git a/renderer/styles.css b/renderer/styles.css index 6371e38..ea30851 100644 --- a/renderer/styles.css +++ b/renderer/styles.css @@ -130,7 +130,7 @@ body { .menu-submenu-trigger:hover { background: var(--bg-card-hover); } .menu-submenu-trigger::after { content: "\25B6"; - font-size: 8px; + font-size: 10px; color: var(--text-dim); margin-left: 12px; } @@ -2754,6 +2754,24 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; } right: calc(100% + 6px); } +.header-utilities [data-menu="datei"] .menu-submenu-trigger::after { + content: "\25C0"; +} + +.menu-submenu:has(> .menu-submenu-trigger[aria-expanded="true"])::after { + content: ""; + position: absolute; + top: 0; + left: 100%; + width: 8px; + height: 100%; +} + +.header-utilities [data-menu="datei"] .menu-submenu::after { + left: auto; + right: 100%; +} + .menu-dropdown-item, .menu-submenu-trigger { min-height: 34px; diff --git a/tests/startup-renderer.test.js b/tests/startup-renderer.test.js index 80c0f88..92774bd 100644 --- a/tests/startup-renderer.test.js +++ b/tests/startup-renderer.test.js @@ -82,6 +82,88 @@ app.whenReady().then(async () => { assert.equal(data.ok, true); }); +test('backup submenu survives slow gap crossing, hover clicks and keyboard navigation', { skip: process.platform !== 'win32' }, t => { + const root = path.resolve(__dirname, '..'); + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'mhu-menu-hover-')); + t.after(() => fs.rmSync(directory, { recursive: true, force: true })); + const probe = path.join(directory, 'probe.cjs'); + const result = path.join(directory, 'result.json'); + fs.writeFileSync(probe, ` +const { app, BrowserWindow } = require('electron'); +const fs = require('node:fs'); +const path = require('node:path'); +const assert = require('node:assert/strict'); +app.whenReady().then(async () => { + const root = process.env.MHU_MENU_ROOT; + const css = fs.readFileSync(path.join(root, 'renderer/styles.css'), 'utf8'); + const html = fs.readFileSync(path.join(root, 'renderer/index.html'), 'utf8'); + const source = fs.readFileSync(path.join(root, 'renderer/app.js'), 'utf8'); + const init = source.slice(source.indexOf('function initMenuBar()'), source.indexOf('async function _handleMenuAction(')); + const nav = html.match(/