Fix IconSelect grid overflow and scroll jump
- Set maxHeight dynamically based on available viewport space - Clamp popup horizontally to stay within viewport - Remove max-height CSS transition that caused scroll jumps - Auto-close popup on ancestor scroll to prevent stale positioning Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -618,19 +618,15 @@ textarea:focus-visible {
|
||||
.icon-select-popup {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transition: max-height 0.2s ease, opacity 0.15s ease, margin 0.2s ease;
|
||||
margin-top: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
.icon-select-popup.open {
|
||||
max-height: 600px;
|
||||
opacity: 1;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.icon-select-popup.open.settled {
|
||||
overflow-y: auto;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.icon-select-grid {
|
||||
|
||||
@@ -18,14 +18,12 @@
|
||||
* Call sel.setValue(v) to change programmatically, sel.destroy() to remove.
|
||||
*/
|
||||
|
||||
import { t } from './i18n.js';
|
||||
|
||||
const POPUP_CLASS = 'icon-select-popup';
|
||||
|
||||
/** Close every open icon-select popup. */
|
||||
export function closeAllIconSelects() {
|
||||
document.querySelectorAll(`.${POPUP_CLASS}`).forEach(p => {
|
||||
p.classList.remove('open', 'settled');
|
||||
p.classList.remove('open');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,7 +76,6 @@ export class IconSelect {
|
||||
this._popup = document.createElement('div');
|
||||
this._popup.className = POPUP_CLASS;
|
||||
this._popup.addEventListener('click', (e) => e.stopPropagation());
|
||||
this._popup.addEventListener('transitionend', this._onTransitionEnd);
|
||||
this._popup.innerHTML = this._buildGrid();
|
||||
document.body.appendChild(this._popup);
|
||||
|
||||
@@ -86,7 +83,8 @@ export class IconSelect {
|
||||
this._popup.querySelectorAll('.icon-select-cell').forEach(cell => {
|
||||
cell.addEventListener('click', () => {
|
||||
this.setValue(cell.dataset.value, true);
|
||||
this._popup.classList.remove('open', 'settled');
|
||||
this._popup.classList.remove('open');
|
||||
this._removeScrollListener();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -127,17 +125,32 @@ export class IconSelect {
|
||||
|
||||
_positionPopup() {
|
||||
const rect = this._trigger.getBoundingClientRect();
|
||||
this._popup.style.left = rect.left + 'px';
|
||||
this._popup.style.width = Math.max(rect.width, 200) + 'px';
|
||||
const gap = 6; // visual gap between trigger and popup
|
||||
const pad = 8; // min distance from viewport edge
|
||||
const popupW = Math.max(rect.width, 200);
|
||||
const spaceBelow = window.innerHeight - rect.bottom - gap - pad;
|
||||
const spaceAbove = rect.top - gap - pad;
|
||||
|
||||
// Check if there's enough space below, otherwise open upward
|
||||
const spaceBelow = window.innerHeight - rect.bottom;
|
||||
const spaceAbove = rect.top;
|
||||
if (spaceBelow < 250 && spaceAbove > spaceBelow) {
|
||||
// Determine direction
|
||||
const openUp = spaceBelow < 200 && spaceAbove > spaceBelow;
|
||||
const available = openUp ? spaceAbove : spaceBelow;
|
||||
|
||||
// Horizontal: clamp so popup doesn't overflow right edge
|
||||
let left = rect.left;
|
||||
if (left + popupW > window.innerWidth - pad) {
|
||||
left = window.innerWidth - pad - popupW;
|
||||
}
|
||||
if (left < pad) left = pad;
|
||||
|
||||
this._popup.style.left = left + 'px';
|
||||
this._popup.style.width = popupW + 'px';
|
||||
this._popup.style.maxHeight = available + 'px';
|
||||
|
||||
if (openUp) {
|
||||
this._popup.style.top = '';
|
||||
this._popup.style.bottom = (window.innerHeight - rect.top) + 'px';
|
||||
this._popup.style.bottom = (window.innerHeight - rect.top + gap) + 'px';
|
||||
} else {
|
||||
this._popup.style.top = rect.bottom + 'px';
|
||||
this._popup.style.top = (rect.bottom + gap) + 'px';
|
||||
this._popup.style.bottom = '';
|
||||
}
|
||||
}
|
||||
@@ -148,14 +161,39 @@ export class IconSelect {
|
||||
if (!wasOpen) {
|
||||
this._positionPopup();
|
||||
this._popup.classList.add('open');
|
||||
this._addScrollListener();
|
||||
}
|
||||
}
|
||||
|
||||
_onTransitionEnd = (e) => {
|
||||
if (e.propertyName === 'max-height' && this._popup.classList.contains('open')) {
|
||||
this._popup.classList.add('settled');
|
||||
}
|
||||
/** Close popup when any scrollable ancestor scrolls (prevents stale position). */
|
||||
_addScrollListener() {
|
||||
if (this._scrollHandler) return;
|
||||
this._scrollHandler = () => {
|
||||
this._popup.classList.remove('open');
|
||||
this._removeScrollListener();
|
||||
};
|
||||
// Listen on capture phase to catch scroll on any ancestor
|
||||
let el = this._trigger.parentNode;
|
||||
this._scrollTargets = [];
|
||||
while (el && el !== document) {
|
||||
if (el.scrollHeight > el.clientHeight || el.classList?.contains('modal-content')) {
|
||||
el.addEventListener('scroll', this._scrollHandler, { passive: true });
|
||||
this._scrollTargets.push(el);
|
||||
}
|
||||
el = el.parentNode;
|
||||
}
|
||||
window.addEventListener('scroll', this._scrollHandler, { passive: true });
|
||||
this._scrollTargets.push(window);
|
||||
}
|
||||
|
||||
_removeScrollListener() {
|
||||
if (!this._scrollHandler) return;
|
||||
for (const el of this._scrollTargets) {
|
||||
el.removeEventListener('scroll', this._scrollHandler);
|
||||
}
|
||||
this._scrollTargets = [];
|
||||
this._scrollHandler = null;
|
||||
}
|
||||
|
||||
/** Change the value programmatically. */
|
||||
setValue(value, fireChange = false) {
|
||||
@@ -175,7 +213,7 @@ export class IconSelect {
|
||||
this._popup.querySelectorAll('.icon-select-cell').forEach(cell => {
|
||||
cell.addEventListener('click', () => {
|
||||
this.setValue(cell.dataset.value, true);
|
||||
this._popup.classList.remove('open', 'settled');
|
||||
this._popup.classList.remove('open');
|
||||
});
|
||||
});
|
||||
this._syncTrigger();
|
||||
@@ -183,6 +221,7 @@ export class IconSelect {
|
||||
|
||||
/** Remove the enhancement, restore native <select>. */
|
||||
destroy() {
|
||||
this._removeScrollListener();
|
||||
this._trigger.remove();
|
||||
this._popup.remove();
|
||||
this._select.style.display = '';
|
||||
|
||||
Reference in New Issue
Block a user