v1.03 - Fix table popups
This commit is contained in:
+12
-2
@@ -575,7 +575,7 @@
|
|||||||
padding: 14px 16px 16px;
|
padding: 14px 16px 16px;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
/* Arrow points DOWN toward the column header */
|
/* Arrow points DOWN toward the column header (default — popup is above the th) */
|
||||||
.col-info-popup::after {
|
.col-info-popup::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -586,7 +586,17 @@
|
|||||||
background: #fffcf2;
|
background: #fffcf2;
|
||||||
border-right: 1.5px solid #c9b08a;
|
border-right: 1.5px solid #c9b08a;
|
||||||
border-bottom: 1.5px solid #c9b08a;
|
border-bottom: 1.5px solid #c9b08a;
|
||||||
transform: rotate(45deg);
|
transform: translateX(-50%) rotate(45deg);
|
||||||
|
}
|
||||||
|
/* Arrow points UP toward the column header (popup is below the th) */
|
||||||
|
.col-info-popup.col-info-popup--below::after {
|
||||||
|
bottom: auto;
|
||||||
|
top: -7px;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: none;
|
||||||
|
border-left: 1.5px solid #c9b08a;
|
||||||
|
border-top: 1.5px solid #c9b08a;
|
||||||
|
transform: translateX(-50%) rotate(45deg);
|
||||||
}
|
}
|
||||||
.col-info-title {
|
.col-info-title {
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+96
-49
@@ -239,6 +239,9 @@ export function UTCIForecast() {
|
|||||||
// State holds { key, x, y } or null when closed.
|
// State holds { key, x, y } or null when closed.
|
||||||
const [colPopup, setColPopup] = useState(null);
|
const [colPopup, setColPopup] = useState(null);
|
||||||
const colPopupRef = useRef(null);
|
const colPopupRef = useRef(null);
|
||||||
|
const colPopupThRef = useRef(null);
|
||||||
|
const hoverTimerRef = useRef(null);
|
||||||
|
const closeTimerRef = useRef(null);
|
||||||
|
|
||||||
const COL_DESCRIPTIONS = {
|
const COL_DESCRIPTIONS = {
|
||||||
hour: { title: 'Hour', desc: 'Local wall-clock time for this forecast row. Each row covers one hour.' },
|
hour: { title: 'Hour', desc: 'Local wall-clock time for this forecast row. Each row covers one hour.' },
|
||||||
@@ -268,33 +271,75 @@ export function UTCIForecast() {
|
|||||||
managedT: { title: 'Managed Indoors', desc: 'Estimated indoor temperature with curtains closed and windows opened when outdoor air is cooler than inside — the standard UK heatwave advice. Curtains block most direct solar gain; smart ventilation pulls the temperature down during cooler periods.' },
|
managedT: { title: 'Managed Indoors', desc: 'Estimated indoor temperature with curtains closed and windows opened when outdoor air is cooler than inside — the standard UK heatwave advice. Curtains block most direct solar gain; smart ventilation pulls the temperature down during cooler periods.' },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Dismiss popup on click outside
|
const calcPopupPos = (thEl) => {
|
||||||
|
const rect = thEl.getBoundingClientRect();
|
||||||
|
const popupW = 260, popupH = 110, margin = 8, gap = 6;
|
||||||
|
let x = rect.left + rect.width / 2;
|
||||||
|
x = Math.max(popupW / 2 + margin, Math.min(x, window.innerWidth - popupW / 2 - margin));
|
||||||
|
const below = rect.top < popupH + gap + margin;
|
||||||
|
const y = below ? rect.bottom + gap : rect.top - gap;
|
||||||
|
const popupLeft = x - popupW / 2;
|
||||||
|
const arrowLeft = Math.max(16, Math.min(rect.left + rect.width / 2 - popupLeft, popupW - 16));
|
||||||
|
return { x, y, arrowLeft, below };
|
||||||
|
};
|
||||||
|
|
||||||
|
const openPopup = (key, thEl) => {
|
||||||
|
colPopupThRef.current = thEl;
|
||||||
|
setColPopup({ key, ...calcPopupPos(thEl) });
|
||||||
|
};
|
||||||
|
|
||||||
|
const closePopup = () => {
|
||||||
|
setColPopup(null);
|
||||||
|
colPopupThRef.current = null;
|
||||||
|
clearTimeout(hoverTimerRef.current);
|
||||||
|
clearTimeout(closeTimerRef.current);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleThClick = (key, e) => {
|
||||||
|
clearTimeout(hoverTimerRef.current);
|
||||||
|
clearTimeout(closeTimerRef.current);
|
||||||
|
if (colPopup?.key === key) { closePopup(); return; }
|
||||||
|
openPopup(key, e.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleThEnter = (key, e) => {
|
||||||
|
clearTimeout(hoverTimerRef.current);
|
||||||
|
clearTimeout(closeTimerRef.current);
|
||||||
|
// If a different popup is open, close it immediately and start fresh timer
|
||||||
|
if (colPopup && colPopup.key !== key) closePopup();
|
||||||
|
if (colPopup?.key === key) return; // already showing this one
|
||||||
|
const thEl = e.currentTarget;
|
||||||
|
hoverTimerRef.current = setTimeout(() => openPopup(key, thEl), 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Leaving a th: just cancel the pending open. Don't auto-close —
|
||||||
|
// the user might be moving into the popup, or just passing through.
|
||||||
|
const handleThLeave = () => {
|
||||||
|
clearTimeout(hoverTimerRef.current);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Popup mouse handlers: keep it open while hovering, close on leave.
|
||||||
|
const handlePopupEnter = () => clearTimeout(closeTimerRef.current);
|
||||||
|
const handlePopupLeave = () => { closeTimerRef.current = setTimeout(closePopup, 200); };
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!colPopup) return;
|
if (!colPopup) return;
|
||||||
const onClickOutside = (e) => {
|
const onClickOutside = (e) => {
|
||||||
if (colPopupRef.current && !colPopupRef.current.contains(e.target)) {
|
if (colPopupRef.current && !colPopupRef.current.contains(e.target)) closePopup();
|
||||||
setColPopup(null);
|
};
|
||||||
}
|
const onScrollOrResize = () => {
|
||||||
|
if (!colPopupThRef.current) return;
|
||||||
|
setColPopup(prev => prev ? { ...prev, ...calcPopupPos(colPopupThRef.current) } : null);
|
||||||
};
|
};
|
||||||
document.addEventListener('mousedown', onClickOutside);
|
document.addEventListener('mousedown', onClickOutside);
|
||||||
return () => document.removeEventListener('mousedown', onClickOutside);
|
window.addEventListener('scroll', onScrollOrResize, { passive: true, capture: true });
|
||||||
}, [colPopup]);
|
window.addEventListener('resize', onScrollOrResize, { passive: true });
|
||||||
|
return () => {
|
||||||
const handleThClick = (key, e) => {
|
document.removeEventListener('mousedown', onClickOutside);
|
||||||
if (colPopup?.key === key) { setColPopup(null); return; }
|
window.removeEventListener('scroll', onScrollOrResize, { capture: true });
|
||||||
const rect = e.currentTarget.getBoundingClientRect();
|
window.removeEventListener('resize', onScrollOrResize);
|
||||||
const popupW = 260;
|
|
||||||
const margin = 8; // min gap from screen edge
|
|
||||||
// Centre on the th, then clamp so popup stays within viewport
|
|
||||||
let x = rect.left + rect.width / 2;
|
|
||||||
x = Math.max(popupW / 2 + margin, Math.min(x, window.innerWidth - popupW / 2 - margin));
|
|
||||||
// Position popup ABOVE the header; arrow points down toward the th
|
|
||||||
const y = rect.top - 6; // 6px gap above the th top edge
|
|
||||||
// Store arrowLeft as offset from popup left edge so arrow stays over the th
|
|
||||||
const popupLeft = x - popupW / 2;
|
|
||||||
const arrowLeft = Math.max(16, Math.min(rect.left + rect.width / 2 - popupLeft, popupW - 16));
|
|
||||||
setColPopup({ key, x, y, arrowLeft });
|
|
||||||
};
|
};
|
||||||
|
}, [colPopup]);
|
||||||
|
|
||||||
// ─── TABLE SCROLL INDICATORS ─────────────────────────────────────────
|
// ─── TABLE SCROLL INDICATORS ─────────────────────────────────────────
|
||||||
// Track whether the body scroller can scroll left/right so we can show
|
// Track whether the body scroller can scroll left/right so we can show
|
||||||
@@ -1259,31 +1304,31 @@ ${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['a
|
|||||||
<table class="utci-table utci-table-head" ref=${headTableRef}>
|
<table class="utci-table utci-table-head" ref=${headTableRef}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('hour', e)}>Hour</th>
|
<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('hour', e)} onMouseEnter=${(e) => handleThEnter('hour', e)} onMouseLeave=${handleThLeave}>Hour</th>
|
||||||
${visibleCols.air && html`<th class="utci-tight-head col-info-th" scope="col" onClick=${(e) => handleThClick('air', e)}>Air <span class="col-unit">°C</span></th>`}
|
${visibleCols.air && html`<th class="utci-tight-head col-info-th" scope="col" onClick=${(e) => handleThClick('air', e)} onMouseEnter=${(e) => handleThEnter('air', e)} onMouseLeave=${handleThLeave}>Air <span class="col-unit">°C</span></th>`}
|
||||||
${visibleCols.rh && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('rh', e)}>RH <span class="col-unit">%</span></th>`}
|
${visibleCols.rh && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('rh', e)} onMouseEnter=${(e) => handleThEnter('rh', e)} onMouseLeave=${handleThLeave}>RH <span class="col-unit">%</span></th>`}
|
||||||
${visibleCols.dew && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('dew', e)}>Dew <span class="col-unit">°C</span></th>`}
|
${visibleCols.dew && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('dew', e)} onMouseEnter=${(e) => handleThEnter('dew', e)} onMouseLeave=${handleThLeave}>Dew <span class="col-unit">°C</span></th>`}
|
||||||
${visibleCols.soilT && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('soilT', e)}>Soil °C <span class="col-unit">surface</span></th>`}
|
${visibleCols.soilT && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('soilT', e)} onMouseEnter=${(e) => handleThEnter('soilT', e)} onMouseLeave=${handleThLeave}>Soil °C <span class="col-unit">surface</span></th>`}
|
||||||
${visibleCols.soilT6 && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('soilT6', e)}>Soil 6cm <span class="col-unit">°C root</span></th>`}
|
${visibleCols.soilT6 && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('soilT6', e)} onMouseEnter=${(e) => handleThEnter('soilT6', e)} onMouseLeave=${handleThLeave}>Soil 6cm <span class="col-unit">°C root</span></th>`}
|
||||||
${visibleCols.soilM && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('soilM', e)}>Soil moist <span class="col-unit">m³/m³</span></th>`}
|
${visibleCols.soilM && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('soilM', e)} onMouseEnter=${(e) => handleThEnter('soilM', e)} onMouseLeave=${handleThLeave}>Soil moist <span class="col-unit">m³/m³</span></th>`}
|
||||||
${visibleCols.concreteT && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('concreteT', e)}>Concrete <span class="col-unit">°C surface</span></th>`}
|
${visibleCols.concreteT && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('concreteT', e)} onMouseEnter=${(e) => handleThEnter('concreteT', e)} onMouseLeave=${handleThLeave}>Concrete <span class="col-unit">°C surface</span></th>`}
|
||||||
${visibleCols.wind && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('wind', e)}>Wind <span class="col-unit">m/s (gust)</span></th>`}
|
${visibleCols.wind && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('wind', e)} onMouseEnter=${(e) => handleThEnter('wind', e)} onMouseLeave=${handleThLeave}>Wind <span class="col-unit">m/s (gust)</span></th>`}
|
||||||
${visibleCols.dir && html`<th class="utci-dir-cell col-info-th" scope="col" onClick=${(e) => handleThClick('dir', e)}>Dir <span class="col-unit">-</span></th>`}
|
${visibleCols.dir && html`<th class="utci-dir-cell col-info-th" scope="col" onClick=${(e) => handleThClick('dir', e)} onMouseEnter=${(e) => handleThEnter('dir', e)} onMouseLeave=${handleThLeave}>Dir <span class="col-unit">-</span></th>`}
|
||||||
${visibleCols.cloud && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('cloud', e)}>Cloud <span class="col-unit">%</span></th>`}
|
${visibleCols.cloud && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('cloud', e)} onMouseEnter=${(e) => handleThEnter('cloud', e)} onMouseLeave=${handleThLeave}>Cloud <span class="col-unit">%</span></th>`}
|
||||||
${visibleCols.sun && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('sun', e)}>Sun <span class="col-unit">elev°</span></th>`}
|
${visibleCols.sun && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('sun', e)} onMouseEnter=${(e) => handleThEnter('sun', e)} onMouseLeave=${handleThLeave}>Sun <span class="col-unit">elev°</span></th>`}
|
||||||
${visibleCols.direct && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('direct', e)}>Direct <span class="col-unit">W/m²</span></th>`}
|
${visibleCols.direct && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('direct', e)} onMouseEnter=${(e) => handleThEnter('direct', e)} onMouseLeave=${handleThLeave}>Direct <span class="col-unit">W/m²</span></th>`}
|
||||||
${visibleCols.diffuse && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('diffuse', e)}>Diffuse <span class="col-unit">W/m²</span></th>`}
|
${visibleCols.diffuse && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('diffuse', e)} onMouseEnter=${(e) => handleThEnter('diffuse', e)} onMouseLeave=${handleThLeave}>Diffuse <span class="col-unit">W/m²</span></th>`}
|
||||||
${visibleCols.tmrt && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('tmrt', e)}>Tmrt <span class="col-unit">°C</span></th>`}
|
${visibleCols.tmrt && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('tmrt', e)} onMouseEnter=${(e) => handleThEnter('tmrt', e)} onMouseLeave=${handleThLeave}>Tmrt <span class="col-unit">°C</span></th>`}
|
||||||
${visibleCols.delta && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('delta', e)}>Δ <span class="col-unit">UTCI−Air</span></th>`}
|
${visibleCols.delta && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('delta', e)} onMouseEnter=${(e) => handleThEnter('delta', e)} onMouseLeave=${handleThLeave}>Δ <span class="col-unit">UTCI−Air</span></th>`}
|
||||||
${visibleCols.utci && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('utci', e)}>UTCI <span class="col-unit">°C felt</span></th>`}
|
${visibleCols.utci && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('utci', e)} onMouseEnter=${(e) => handleThEnter('utci', e)} onMouseLeave=${handleThLeave}>UTCI <span class="col-unit">°C felt</span></th>`}
|
||||||
${visibleCols.uvA && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('uvA', e)}>UV-A <span class="col-unit">est. idx</span></th>`}
|
${visibleCols.uvA && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('uvA', e)} onMouseEnter=${(e) => handleThEnter('uvA', e)} onMouseLeave=${handleThLeave}>UV-A <span class="col-unit">est. idx</span></th>`}
|
||||||
${visibleCols.uvB && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('uvB', e)}>UV-B <span class="col-unit">est. idx</span></th>`}
|
${visibleCols.uvB && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('uvB', e)} onMouseEnter=${(e) => handleThEnter('uvB', e)} onMouseLeave=${handleThLeave}>UV-B <span class="col-unit">est. idx</span></th>`}
|
||||||
${visibleCols.burn && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('burn', e)}>Burn <span class="col-unit">to MED</span></th>`}
|
${visibleCols.burn && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('burn', e)} onMouseEnter=${(e) => handleThEnter('burn', e)} onMouseLeave=${handleThLeave}>Burn <span class="col-unit">to MED</span></th>`}
|
||||||
${visibleCols.vehicleT && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('vehicleT', e)}>Vehicle <span class="col-unit">°C peak</span></th>`}
|
${visibleCols.vehicleT && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('vehicleT', e)} onMouseEnter=${(e) => handleThEnter('vehicleT', e)} onMouseLeave=${handleThLeave}>Vehicle <span class="col-unit">°C peak</span></th>`}
|
||||||
${indoorMode === 'on' && !indoorManaged && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('indoorT', e)}>Indoors <span class="col-unit">°C est.</span></th>`}
|
${indoorMode === 'on' && !indoorManaged && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('indoorT', e)} onMouseEnter=${(e) => handleThEnter('indoorT', e)} onMouseLeave=${handleThLeave}>Indoors <span class="col-unit">°C est.</span></th>`}
|
||||||
${indoorMode === 'on' && indoorManaged && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('managedT', e)}>Managed <span class="col-unit">°C est.</span></th>`}
|
${indoorMode === 'on' && indoorManaged && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('managedT', e)} onMouseEnter=${(e) => handleThEnter('managedT', e)} onMouseLeave=${handleThLeave}>Managed <span class="col-unit">°C est.</span></th>`}
|
||||||
${visibleCols.precip && html`<th class="utci-tight-head col-info-th" scope="col" onClick=${(e) => handleThClick('precip', e)}>Pcpt <span class="col-unit">mm/h</span></th>`}
|
${visibleCols.precip && html`<th class="utci-tight-head col-info-th" scope="col" onClick=${(e) => handleThClick('precip', e)} onMouseEnter=${(e) => handleThEnter('precip', e)} onMouseLeave=${handleThLeave}>Pcpt <span class="col-unit">mm/h</span></th>`}
|
||||||
${visibleCols.utciP && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('utciP', e)}>UTCI+P <span class="col-unit">°C adj.</span></th>`}
|
${visibleCols.utciP && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('utciP', e)} onMouseEnter=${(e) => handleThEnter('utciP', e)} onMouseLeave=${handleThLeave}>UTCI+P <span class="col-unit">°C adj.</span></th>`}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
@@ -1460,16 +1505,18 @@ ${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['a
|
|||||||
${colPopup && COL_DESCRIPTIONS[colPopup.key] && html`
|
${colPopup && COL_DESCRIPTIONS[colPopup.key] && html`
|
||||||
<div
|
<div
|
||||||
ref=${colPopupRef}
|
ref=${colPopupRef}
|
||||||
class="col-info-popup"
|
class=${`col-info-popup${colPopup.below ? ' col-info-popup--below' : ''}`}
|
||||||
style=${{
|
style=${{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
left: `${colPopup.x}px`,
|
left: `${colPopup.x}px`,
|
||||||
top: `${colPopup.y}px`,
|
top: `${colPopup.y}px`,
|
||||||
transform: 'translateX(-50%) translateY(-100%)',
|
transform: colPopup.below ? 'translateX(-50%)' : 'translateX(-50%) translateY(-100%)',
|
||||||
'--arrow-left': `${colPopup.arrowLeft}px`,
|
'--arrow-left': `${colPopup.arrowLeft}px`,
|
||||||
}}
|
}}
|
||||||
|
onMouseEnter=${handlePopupEnter}
|
||||||
|
onMouseLeave=${handlePopupLeave}
|
||||||
>
|
>
|
||||||
<button class="col-info-close" onClick=${() => setColPopup(null)} aria-label="Close">×</button>
|
<button class="col-info-close" onClick=${closePopup} aria-label="Close">×</button>
|
||||||
<strong class="col-info-title">${COL_DESCRIPTIONS[colPopup.key].title}</strong>
|
<strong class="col-info-title">${COL_DESCRIPTIONS[colPopup.key].title}</strong>
|
||||||
<p class="col-info-desc">${COL_DESCRIPTIONS[colPopup.key].desc}</p>
|
<p class="col-info-desc">${COL_DESCRIPTIONS[colPopup.key].desc}</p>
|
||||||
</div>`}
|
</div>`}
|
||||||
|
|||||||
Reference in New Issue
Block a user