ux(tooltip): fix(tooltip): move tooltip into top-bar, that fix issue with RTE languages

This commit is contained in:
Edouard Vanbelle
2026-05-30 09:41:56 +02:00
parent fe9cc10648
commit 3bcb3a5b3f
4 changed files with 71 additions and 75 deletions
+24 -47
View File
@@ -1,29 +1,19 @@
/* ── Item tooltip — "technical sheet" ─────────────────────────────────────── */ /* ── Item tooltip — "technical sheet" ─────────────────────────────────────── */
/* */ /* */
/* Overlays the search bar area in the top-bar while hovering a file item. */ /* Absolutely overlays `.search-slot` in the top-bar. The search bar stays in */
/* Spans from the sidebar right-edge to just before .user-controls. */ /* place underneath; only the tooltip's opacity is animated, so there is no */
/* second element to keep in sync and no display:none flicker between rapid */
/* hover transitions. RTL just works (inset:0). */
/* */ /* */
/* 3-column CSS grid: [icon] [label] [value] */ /* 3-column CSS grid: [icon] [label] [value] */
/* All values start at the same x position regardless of label width. */ /* All values start at the same x position regardless of label width. */
.path-tooltip { .path-tooltip {
position: fixed; /* Cover the search slot, sit above it (later in DOM order). */
position: absolute;
/* Fill most of the top-bar (topbar height 70px, 8px margin top/bottom) */ inset: 0;
top: 8px; /* Opaque background so the search bar underneath is fully masked. */
background-color: var(--color-bg-surface);
/* Desktop: starts at sidebar right edge + top-bar padding */
left: calc(var(--sidebar-width) + 30px);
/*
* Right edge stops just before .user-controls:
* 30px (top-bar right padding) + ~90px (notif bell + gap + avatar btn) + 4px gap
* Using left + right instead of width so the browser computes it dynamically.
*/
right: 124px;
/* High enough to sit above the top-bar content */
z-index: 200;
display: grid; display: grid;
grid-template-columns: 1em max-content 1fr; grid-template-columns: 1em max-content 1fr;
@@ -31,20 +21,24 @@
row-gap: 6px; row-gap: 6px;
align-items: center; align-items: center;
/* Fill the top-bar height minus an 8px margin on each side */
height: 54px;
padding: 0 16px;
border-radius: 10px;
border: 2px solid var(--color-border-medium);
background-color: var(--color-bg-input);
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: 0.75rem; font-size: 0.75rem;
pointer-events: none; pointer-events: none;
/* avoid blinking when pointer moves between items */ /* Show instantly. The hide direction (.hidden) overrides this with a
transition: display 0.2s allow-discrete; 1s delay so the tooltip lingers after the mouse leaves an item;
rapid item-to-item moves cancel the pending fade before it fires. */
opacity: 1;
transition: opacity 0.2s ease 0s;
}
/* Hidden state — override the global `.hidden { display:none !important }`
so we can fade with opacity instead of snapping to display:none. */
.path-tooltip.hidden {
display: grid !important;
opacity: 0;
transition-delay: 1s;
} }
/* Icon column */ /* Icon column */
@@ -85,23 +79,6 @@
font-style: italic; font-style: italic;
} }
/* ── Narrow screens (sidebar hidden) ────────────────────────────────────── */
/* On mobile the sidebar slides off-screen; align after the #sidebar-toggle */
/* button (≈44px) that takes its place in the top-bar. */
@media (max-width: 768px) {
.path-tooltip {
/* 16px topbar padding + ~44px sidebar-toggle + 8px gap */
left: 68px;
/*
* Right edge stops before search-toggle-btn + user-controls:
* ~40px (search-toggle) + 12px (gap) + ~90px (user-controls) + 16px (padding)
*/
right: 158px;
}
}
/* ── Vignette inside tooltip ─────────────────────────────────────────────── */ /* ── Vignette inside tooltip ─────────────────────────────────────────────── */
/* The vignette name hard-codes 14px; scale it down to match the tooltip. */ /* The vignette name hard-codes 14px; scale it down to match the tooltip. */
+15 -5
View File
@@ -86,11 +86,14 @@
color: var(--color-text-heading); color: var(--color-text-heading);
} }
.search-container { /* Slot wrapping .search-container + #path-tooltip — it owns the flex slot
so the tooltip can absolutely overlay the search bar via inset:0 instead
of viewport-fixed positioning. */
.search-slot {
flex-grow: 1; flex-grow: 1;
max-width: 600px; max-width: 600px;
position: relative;
margin-right: 20px; margin-right: 20px;
position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -100,6 +103,13 @@
} }
} }
.search-container {
flex-grow: 1;
position: relative;
display: flex;
align-items: center;
}
.search-container input { .search-container input {
width: 100%; width: 100%;
padding: 12px 50px 12px 44px; padding: 12px 50px 12px 44px;
@@ -204,8 +214,8 @@
justify-content: center; justify-content: center;
} }
/* Hide full search bar on mobile by default */ /* Hide full search slot on mobile by default */
.search-container { .search-slot {
display: none; display: none;
} }
@@ -224,7 +234,7 @@
justify-content: center; justify-content: center;
} }
.top-bar--search-active .search-container { .top-bar--search-active .search-slot {
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
max-width: none; max-width: none;
+11 -6
View File
@@ -136,12 +136,17 @@
<button class="search-back-btn" id="search-back-btn" aria-label="Close search"> <button class="search-back-btn" id="search-back-btn" aria-label="Close search">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
</button> </button>
<div class="search-container"> <!-- Search slot — positioning context for the item tooltip, which
<i class="fas fa-search search-icon"></i> absolutely overlays the search bar when an item is hovered. -->
<input type="text" id="search-input" data-i18n-placeholder="actions.search" placeholder="Search files, folders..."> <div class="search-slot">
<button id="search-button" class="search-button" data-i18n-title="actions.search_btn" title="Search"> <div class="search-container">
<i class="fas fa-search"></i> <i class="fas fa-search search-icon"></i>
</button> <input type="text" id="search-input" data-i18n-placeholder="actions.search" placeholder="Search files, folders...">
<button id="search-button" class="search-button" data-i18n-title="actions.search_btn" title="Search">
<i class="fas fa-search"></i>
</button>
</div>
<div id="path-tooltip" class="path-tooltip hidden" aria-hidden="true"></div>
</div> </div>
<div class="user-controls"> <div class="user-controls">
+21 -17
View File
@@ -27,21 +27,19 @@ import { i18n } from '../core/i18n.js';
import { systemUsers } from '../model/systemUsers.js'; import { systemUsers } from '../model/systemUsers.js';
// ── Tooltip DOM ─────────────────────────────────────────────────────────────── // ── Tooltip DOM ───────────────────────────────────────────────────────────────
//
// The tooltip element is a static node inside `.search-slot` (see index.html)
// and is absolutely positioned over `.search-container`. Show/hide is a
// simple `.hidden` toggle on the tooltip itself — the search bar underneath
// is never touched, so there's no two-element swap to keep in sync.
/** @returns {HTMLElement} */ /** @returns {HTMLElement | null} */
function _getOrCreateTooltip() { function _getTooltip() {
let el = document.getElementById('path-tooltip'); return document.getElementById('path-tooltip');
if (!el) {
el = document.createElement('div');
el.id = 'path-tooltip';
el.className = 'path-tooltip hidden';
document.body.appendChild(el);
}
return el;
} }
function _hide() { function _hide() {
const el = document.getElementById('path-tooltip'); const el = _getTooltip();
if (el) el.classList.add('hidden'); if (el) el.classList.add('hidden');
} }
@@ -98,7 +96,8 @@ function _onEnter(e) {
// Nothing to show — don't display an all-? tooltip. // Nothing to show — don't display an all-? tooltip.
if (!ownerId && !path) return; if (!ownerId && !path) return;
const tooltip = _getOrCreateTooltip(); const tooltip = _getTooltip();
if (!tooltip) return;
// Clear previous content. // Clear previous content.
while (tooltip.firstChild) tooltip.removeChild(tooltip.firstChild); while (tooltip.firstChild) tooltip.removeChild(tooltip.firstChild);
@@ -140,14 +139,19 @@ const _registry = new WeakMap();
// ── Public API ──────────────────────────────────────────────────────────────── // ── Public API ────────────────────────────────────────────────────────────────
/** Containers known to expose `data-path`/`data-owner-id` for tooltip use.
* Add new opt-in row classes here when other views want the tooltip — each
* one must stamp the dataset attributes itself. */
const _TOOLTIP_SELECTOR = '.file-item, .ms-resource-row';
/** /**
* Attach tooltip listeners to every `.file-item` inside `container`. * Attach tooltip listeners to every tooltip-capable row inside `container`.
* Items with neither `data-owner-id` nor `data-path` will not trigger the * Rows with neither `data-owner-id` nor `data-path` will not trigger the
* tooltip. Safe to call repeatedly — already-wired elements are skipped. * tooltip. Safe to call repeatedly — already-wired elements are skipped.
* @param {HTMLElement} container * @param {HTMLElement} container
*/ */
function init(container) { function init(container) {
for (const item of container.querySelectorAll('.file-item')) { for (const item of container.querySelectorAll(_TOOLTIP_SELECTOR)) {
const el = /** @type {HTMLElement} */ (item); const el = /** @type {HTMLElement} */ (item);
if (_registry.has(el)) continue; // already wired if (_registry.has(el)) continue; // already wired
@@ -161,12 +165,12 @@ function init(container) {
} }
/** /**
* Remove tooltip listeners from all `.file-item` elements inside `container` * Remove tooltip listeners from all tooltip-capable rows inside `container`
* and hide any visible tooltip. * and hide any visible tooltip.
* @param {HTMLElement} container * @param {HTMLElement} container
*/ */
function destroy(container) { function destroy(container) {
for (const item of container.querySelectorAll('.file-item')) { for (const item of container.querySelectorAll(_TOOLTIP_SELECTOR)) {
const el = /** @type {HTMLElement} */ (item); const el = /** @type {HTMLElement} */ (item);
const h = _registry.get(el); const h = _registry.get(el);
if (h) { if (h) {