fix(webui/forms): stop the custom-select enhancer double-wrapping a dropdown

On the app-details Config tab, switching main tabs re-renders the config form
(showAppDetail → displayConfigForm runs on every tab switch). A re-render race
let the enhancer wrap a <select> that was already enhanced, nesting a second
.custom-select inside the first — so the dropdown showed twice, one on top of
the other (reproduced: CFG_<APP>_BACKUP_STRATEGY / _COMPOSE_FILE ended up with
two nested wrappers). The per-element ENHANCED symbol didn't catch it.

Harden the guard with two DOM-based checks (which survive any symbol loss/race):
skip a <select> that already carries the .custom-select-native class or already
sits inside a .custom-select wrapper — in both shouldEnhance() and the
constructor. Port-manager selects were unaffected (they regenerate their
container each render); this fixes the plain form-select fields.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
librelad 2026-07-06 22:05:00 +01:00
parent 02b74baa6f
commit c7d5de7a4f

View File

@ -23,7 +23,15 @@
class CustomSelect {
constructor(selectEl) {
if (selectEl[ENHANCED]) return;
// Guard against double-wrapping. The ENHANCED symbol is per-element, but a
// re-render race (the app config form re-renders on every tab switch) could
// slip a second enhancement through and nest a .custom-select inside another
// — showing the dropdown twice, one atop the other. The class + wrapper
// checks below survive that (they live on the DOM, not a JS symbol), so a
// select that's already enhanced or already inside a wrapper is never wrapped again.
if (selectEl[ENHANCED]
|| selectEl.classList.contains('custom-select-native')
|| selectEl.closest('.custom-select')) return;
selectEl[ENHANCED] = true;
this.select = selectEl;
this.build();
@ -300,6 +308,10 @@
function shouldEnhance(el) {
if (!(el instanceof HTMLSelectElement)) return false;
if (el[ENHANCED]) return false;
// Already enhanced or already inside a wrapper — never nest a second widget
// (see the constructor). DOM-based so it survives a lost ENHANCED symbol.
if (el.classList.contains('custom-select-native')) return false;
if (el.closest('.custom-select')) return false;
if (el.multiple) return false; // multi-selects need different UX
if (el.hasAttribute('data-no-enhance')) return false;
return ENHANCE_CLASSES.some(c => el.classList.contains(c));