fix(webui): portal custom-select popup to body so cards' hover-transform can't break it
Location config dropdowns (Type, Path, etc.) live inside .task-item cards, whose :hover applies transform: translateY(-2px). A transformed element becomes the containing block for position:fixed descendants, so the popup — previously a child of the card — was positioned with viewport coords against the card instead of the viewport (wrong placement) and perturbed layout (content shifted left). Portal the popup to <body> on open and detach on close, so position:fixed is always relative to the viewport regardless of any transformed/overflow ancestor. flip-up styling moves onto the popup element and the topbar's wider popup is carried via a class, since the popup no longer nests in the wrapper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
parent
9f02d350c5
commit
d75024b22c
@ -884,7 +884,7 @@ select.form-control:focus {
|
|||||||
animation: customSelectIn 0.12s ease-out;
|
animation: customSelectIn 0.12s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-select.flip-up .custom-select-popup {
|
.custom-select-popup.flip-up {
|
||||||
transform-origin: bottom center;
|
transform-origin: bottom center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -893,7 +893,7 @@ select.form-control:focus {
|
|||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-select.flip-up .custom-select-popup {
|
.custom-select-popup.flip-up {
|
||||||
animation-name: customSelectInUp;
|
animation-name: customSelectInUp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -252,6 +252,6 @@
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar-controls .custom-select-popup {
|
.custom-select-popup-topbar {
|
||||||
min-width: 140px;
|
min-width: 140px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,10 +56,19 @@
|
|||||||
this.popup.className = 'custom-select-popup';
|
this.popup.className = 'custom-select-popup';
|
||||||
this.popup.setAttribute('role', 'listbox');
|
this.popup.setAttribute('role', 'listbox');
|
||||||
this.popup.hidden = true;
|
this.popup.hidden = true;
|
||||||
|
// The popup is portaled to <body> on open (below), escaping .topbar-controls,
|
||||||
|
// so carry the topbar's wider-popup styling via a class instead.
|
||||||
|
if (this.select.closest('.topbar-controls')) {
|
||||||
|
this.popup.classList.add('custom-select-popup-topbar');
|
||||||
|
}
|
||||||
|
|
||||||
// Move the native select inside the wrapper so it stays in the form.
|
// Move the native select inside the wrapper so it stays in the form. The
|
||||||
|
// popup is deliberately NOT added here — it's portaled to <body> on
|
||||||
|
// open() so no ancestor's transform (e.g. a card's :hover translateY,
|
||||||
|
// which becomes the containing block for position:fixed) can throw off
|
||||||
|
// its placement or clip it. See open()/close().
|
||||||
this.select.parentNode.insertBefore(this.wrapper, this.select);
|
this.select.parentNode.insertBefore(this.wrapper, this.select);
|
||||||
this.wrapper.append(this.select, this.button, this.popup);
|
this.wrapper.append(this.select, this.button);
|
||||||
this.select.classList.add('custom-select-native');
|
this.select.classList.add('custom-select-native');
|
||||||
// Forward width-related layout from the native select to the wrapper.
|
// Forward width-related layout from the native select to the wrapper.
|
||||||
if (this.select.style.maxWidth) {
|
if (this.select.style.maxWidth) {
|
||||||
@ -105,6 +114,9 @@
|
|||||||
document.querySelectorAll('.custom-select.is-open').forEach(w => {
|
document.querySelectorAll('.custom-select.is-open').forEach(w => {
|
||||||
if (w !== this.wrapper) w.dispatchEvent(new CustomEvent('custom-select:close'));
|
if (w !== this.wrapper) w.dispatchEvent(new CustomEvent('custom-select:close'));
|
||||||
});
|
});
|
||||||
|
// Portal into <body> so position:fixed is relative to the viewport,
|
||||||
|
// immune to any transformed/overflow ancestor.
|
||||||
|
document.body.appendChild(this.popup);
|
||||||
this.popup.hidden = false;
|
this.popup.hidden = false;
|
||||||
this.button.setAttribute('aria-expanded', 'true');
|
this.button.setAttribute('aria-expanded', 'true');
|
||||||
this.wrapper.classList.add('is-open');
|
this.wrapper.classList.add('is-open');
|
||||||
@ -124,6 +136,7 @@
|
|||||||
close() {
|
close() {
|
||||||
if (this.popup.hidden) return;
|
if (this.popup.hidden) return;
|
||||||
this.popup.hidden = true;
|
this.popup.hidden = true;
|
||||||
|
this.popup.remove(); // detach from <body> so popups don't accumulate
|
||||||
this.button.setAttribute('aria-expanded', 'false');
|
this.button.setAttribute('aria-expanded', 'false');
|
||||||
this.wrapper.classList.remove('is-open');
|
this.wrapper.classList.remove('is-open');
|
||||||
this.clearFocused();
|
this.clearFocused();
|
||||||
@ -141,7 +154,7 @@
|
|||||||
const spaceBelow = window.innerHeight - rect.bottom;
|
const spaceBelow = window.innerHeight - rect.bottom;
|
||||||
const popupMax = 280;
|
const popupMax = 280;
|
||||||
const flipUp = spaceBelow < 200 && rect.top > popupMax;
|
const flipUp = spaceBelow < 200 && rect.top > popupMax;
|
||||||
this.wrapper.classList.toggle('flip-up', flipUp);
|
this.popup.classList.toggle('flip-up', flipUp);
|
||||||
|
|
||||||
this.popup.style.left = `${rect.left}px`;
|
this.popup.style.left = `${rect.left}px`;
|
||||||
this.popup.style.width = `${rect.width}px`;
|
this.popup.style.width = `${rect.width}px`;
|
||||||
@ -222,7 +235,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
this._outsideClick = (e) => {
|
this._outsideClick = (e) => {
|
||||||
if (!this.wrapper.contains(e.target)) this.close();
|
if (!this.wrapper.contains(e.target) && !this.popup.contains(e.target)) this.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
this._reposition = () => this.positionPopup();
|
this._reposition = () => this.positionPopup();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user