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" ─────────────────────────────────────── */
/* */
/* Overlays the search bar area in the top-bar while hovering a file item. */
/* Spans from the sidebar right-edge to just before .user-controls. */
/* Absolutely overlays `.search-slot` in the top-bar. The search bar stays in */
/* 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] */
/* All values start at the same x position regardless of label width. */
/* 3-column CSS grid: [icon] [label] [value] */
/* All values start at the same x position regardless of label width. */
.path-tooltip {
position: fixed;
/* Fill most of the top-bar (topbar height 70px, 8px margin top/bottom) */
top: 8px;
/* 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;
/* Cover the search slot, sit above it (later in DOM order). */
position: absolute;
inset: 0;
/* Opaque background so the search bar underneath is fully masked. */
background-color: var(--color-bg-surface);
display: grid;
grid-template-columns: 1em max-content 1fr;
@@ -31,20 +21,24 @@
row-gap: 6px;
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);
font-size: 0.75rem;
pointer-events: none;
/* avoid blinking when pointer moves between items */
transition: display 0.2s allow-discrete;
/* Show instantly. The hide direction (.hidden) overrides this with a
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 */
@@ -85,23 +79,6 @@
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 ─────────────────────────────────────────────── */
/* 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);
}
.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;
max-width: 600px;
position: relative;
margin-right: 20px;
position: relative;
display: flex;
align-items: center;
@@ -100,6 +103,13 @@
}
}
.search-container {
flex-grow: 1;
position: relative;
display: flex;
align-items: center;
}
.search-container input {
width: 100%;
padding: 12px 50px 12px 44px;
@@ -204,8 +214,8 @@
justify-content: center;
}
/* Hide full search bar on mobile by default */
.search-container {
/* Hide full search slot on mobile by default */
.search-slot {
display: none;
}
@@ -224,7 +234,7 @@
justify-content: center;
}
.top-bar--search-active .search-container {
.top-bar--search-active .search-slot {
display: flex;
flex-grow: 1;
max-width: none;
+11 -6
View File
@@ -136,12 +136,17 @@
<button class="search-back-btn" id="search-back-btn" aria-label="Close search">
<i class="fas fa-arrow-left"></i>
</button>
<div class="search-container">
<i class="fas fa-search search-icon"></i>
<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>
<!-- Search slot — positioning context for the item tooltip, which
absolutely overlays the search bar when an item is hovered. -->
<div class="search-slot">
<div class="search-container">
<i class="fas fa-search search-icon"></i>
<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 class="user-controls">
+21 -17
View File
@@ -27,21 +27,19 @@ import { i18n } from '../core/i18n.js';
import { systemUsers } from '../model/systemUsers.js';
// ── 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} */
function _getOrCreateTooltip() {
let el = 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;
/** @returns {HTMLElement | null} */
function _getTooltip() {
return document.getElementById('path-tooltip');
}
function _hide() {
const el = document.getElementById('path-tooltip');
const el = _getTooltip();
if (el) el.classList.add('hidden');
}
@@ -98,7 +96,8 @@ function _onEnter(e) {
// Nothing to show — don't display an all-? tooltip.
if (!ownerId && !path) return;
const tooltip = _getOrCreateTooltip();
const tooltip = _getTooltip();
if (!tooltip) return;
// Clear previous content.
while (tooltip.firstChild) tooltip.removeChild(tooltip.firstChild);
@@ -140,14 +139,19 @@ const _registry = new WeakMap();
// ── 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`.
* Items with neither `data-owner-id` nor `data-path` will not trigger the
* Attach tooltip listeners to every tooltip-capable row inside `container`.
* Rows with neither `data-owner-id` nor `data-path` will not trigger the
* tooltip. Safe to call repeatedly — already-wired elements are skipped.
* @param {HTMLElement} 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);
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.
* @param {HTMLElement} 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 h = _registry.get(el);
if (h) {