From 30df93a4cc13ae9b838f5f2e3b8b661aadad4cf6 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Thu, 16 Apr 2026 20:13:47 +0200 Subject: [PATCH] fix(widgets): harden minimize transitionend with fallback and property filter - Filter transitionend by propertyName=opacity to prevent double-fire - Add 350ms fallback setTimeout for prefers-reduced-motion / zero-duration - Initialize _minimizing: false in create() for clean state - Dispatch events after save() for consistent state in listeners Co-Authored-By: Claude Opus 4.6 --- src/js/widgets.js | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/js/widgets.js b/src/js/widgets.js index 36a9a14..b16ac56 100644 --- a/src/js/widgets.js +++ b/src/js/widgets.js @@ -52,7 +52,7 @@ const WidgetManager = { const el = this._buildDOM(state); document.body.appendChild(el); - this._widgets.set(id, { el, type, state }); + this._widgets.set(id, { el, type, state, _minimizing: false }); this._initDrag(el); this._initResize(el); this.bringToFront(id); @@ -170,7 +170,9 @@ const WidgetManager = { }, /** - * Widget minimieren (aus DOM verstecken, bleibt im Notebook) + * Widget minimieren (aus DOM verstecken, bleibt im Notebook). + * Nutzt transitionend statt setTimeout — _minimizing Flag verhindert Race Condition + * mit openWidget(). Fallback-Timer fuer prefers-reduced-motion / fehlende Transition. * @param {string} id */ async minimize(id) { @@ -180,17 +182,30 @@ const WidgetManager = { entry._minimizing = true; entry.el.classList.add('widget-minimized'); - entry.el.addEventListener('transitionend', function onEnd(e) { - if (e.target !== entry.el) return; + const MINIMIZE_FALLBACK_MS = 350; + + function onEnd(e) { + if (e.target !== entry.el || e.propertyName !== 'opacity') return; + clearTimeout(fallbackTimer); entry.el.removeEventListener('transitionend', onEnd); if (entry._minimizing) { entry.el.style.display = 'none'; } entry._minimizing = false; - }); + } + + entry.el.addEventListener('transitionend', onEnd); + + const fallbackTimer = setTimeout(() => { + entry.el.removeEventListener('transitionend', onEnd); + if (entry._minimizing) { + entry.el.style.display = 'none'; + entry._minimizing = false; + } + }, MINIMIZE_FALLBACK_MS); - this._emitter.dispatchEvent(new CustomEvent('widget:minimize', { detail: { id } })); await this.save(); + this._emitter.dispatchEvent(new CustomEvent('widget:minimize', { detail: { id } })); }, /** @@ -207,8 +222,8 @@ const WidgetManager = { entry.el.classList.remove('widget-minimized'); }); this.bringToFront(id); - this._emitter.dispatchEvent(new CustomEvent('widget:open', { detail: { id } })); await this.save(); + this._emitter.dispatchEvent(new CustomEvent('widget:open', { detail: { id } })); }, /**