Skip to content

Commit dbf2b47

Browse files
jeremymanningclaude
andcommitted
Custom tooltip system, fix import reliability, fix active mode button hover
- Replace native title attrs with JS-powered [data-tooltip] tooltip system that escapes overflow:hidden containers by appending to <body> - Style matches map tooltips: dark surface, subtle border/shadow, 200ms delay - Tooltips work on all buttons: header icons, reset/export/import, mode pills, quiz toggle — including on the welcome screen before domain selection - Hidden on mobile (480px breakpoint) to avoid touch overlap - Fix import: attach <input type="file"> to DOM before triggering click (some browsers suppress change event on detached inputs); add cancel handler and visible success banner after import - Fix active mode button hover: .mode-btn.active:hover keeps white text on green background instead of turning text green (invisible) - Remove old CSS ::after tooltip from disabled mode buttons (replaced by global JS tooltip system) Co-Authored-By: Claude (claude-opus-4-6) <noreply@anthropic.com>
1 parent 0acfac6 commit dbf2b47

File tree

4 files changed

+128
-41
lines changed

4 files changed

+128
-41
lines changed

index.html

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,27 @@
144144
cursor: not-allowed;
145145
}
146146

147+
/* ── Custom tooltip (JS-positioned, appended to body) ── */
148+
.ui-tooltip {
149+
position: fixed;
150+
pointer-events: none;
151+
z-index: 99999;
152+
background: var(--color-surface);
153+
color: var(--color-text);
154+
padding: 6px 10px;
155+
border-radius: 6px;
156+
font-size: 0.72rem;
157+
font-family: var(--font-body);
158+
font-weight: 400;
159+
line-height: 1.4;
160+
white-space: nowrap;
161+
border: 1px solid var(--color-border);
162+
box-shadow: 0 4px 16px rgba(0,0,0,0.35);
163+
opacity: 0;
164+
transition: opacity 0.15s ease;
165+
}
166+
.ui-tooltip.visible { opacity: 1; }
167+
147168
#app-main {
148169
display: flex;
149170
position: relative;
@@ -634,6 +655,8 @@
634655
.btn-icon { min-width: 34px; min-height: 34px; font-size: 0.85rem; }
635656
/* Make dropdown arrow easier to see on mobile */
636657
.custom-select-arrow { font-size: 0.9rem; }
658+
/* Hide custom tooltips on touch devices */
659+
.ui-tooltip { display: none !important; }
637660
#quiz-panel {
638661
position: absolute;
639662
top: auto; bottom: 0; left: 0; right: 0;
@@ -688,16 +711,16 @@
688711
<div class="domain-selector" hidden aria-label="Select knowledge domain"></div>
689712
</div>
690713
<div class="header-right">
691-
<button id="trophy-btn" class="btn-icon" aria-label="My areas of expertise" title="My areas of expertise" disabled>
714+
<button id="trophy-btn" class="btn-icon" aria-label="My areas of expertise" data-tooltip="My areas of expertise" disabled>
692715
<i class="fa-solid fa-trophy"></i>
693716
</button>
694-
<button id="suggest-btn" class="btn-icon" aria-label="Suggest articles to learn" title="Suggest articles to maximally boost my knowledge" disabled>
717+
<button id="suggest-btn" class="btn-icon" aria-label="Suggest articles to learn" data-tooltip="Suggested learning" disabled>
695718
<i class="fa-solid fa-graduation-cap"></i>
696719
</button>
697-
<button id="share-btn" class="btn-icon" aria-label="Share your knowledge map" title="Share your knowledge map">
720+
<button id="share-btn" class="btn-icon" aria-label="Share your knowledge map" data-tooltip="Share">
698721
<i class="fa-solid fa-share-nodes"></i>
699722
</button>
700-
<button id="about-btn" class="btn-icon" aria-label="About this project" title="About this project">
723+
<button id="about-btn" class="btn-icon" aria-label="About this project" data-tooltip="About">
701724
<i class="fa-solid fa-circle-info"></i>
702725
</button>
703726
</div>
@@ -739,7 +762,7 @@ <h2>Map out (an approximation of) everything you know!</h2>
739762
<section id="map-container" aria-label="Interactive Heatmap" tabindex="0"></section>
740763

741764
<!-- Quiz Panel Toggle -->
742-
<button id="quiz-toggle" class="quiz-toggle-btn" hidden aria-label="Open quiz panel" title="Press to show/hide the questions">
765+
<button id="quiz-toggle" class="quiz-toggle-btn" hidden aria-label="Open quiz panel" data-tooltip="Show/hide questions">
743766
<i class="fa-solid fa-chevron-left"></i>
744767
</button>
745768

@@ -864,6 +887,70 @@ <h1>JavaScript Required</h1>
864887
<!-- Feature Detection (T066) - runs before ES modules to detect unsupported browsers -->
865888
<script src="feature-detection.js"></script>
866889

890+
<!-- Global custom tooltip system for [data-tooltip] elements -->
891+
<script>
892+
(function() {
893+
var tip = document.createElement('div');
894+
tip.className = 'ui-tooltip';
895+
document.body.appendChild(tip);
896+
var showTimer = null;
897+
var DELAY = 200;
898+
899+
function show(el) {
900+
var text = el.getAttribute('data-tooltip');
901+
if (!text) return;
902+
tip.textContent = text;
903+
// Measure and position
904+
tip.style.opacity = '0';
905+
tip.classList.add('visible');
906+
var r = el.getBoundingClientRect();
907+
var tw = tip.offsetWidth;
908+
var th = tip.offsetHeight;
909+
// Default: centered above the element
910+
var left = r.left + r.width / 2 - tw / 2;
911+
var top = r.top - th - 8;
912+
// If clipped at top, show below
913+
if (top < 4) top = r.bottom + 8;
914+
// Keep within horizontal viewport
915+
if (left < 4) left = 4;
916+
if (left + tw > window.innerWidth - 4) left = window.innerWidth - tw - 4;
917+
tip.style.left = left + 'px';
918+
tip.style.top = top + 'px';
919+
tip.style.opacity = '';
920+
}
921+
922+
function hide() {
923+
clearTimeout(showTimer);
924+
showTimer = null;
925+
tip.classList.remove('visible');
926+
}
927+
928+
function tooltipTarget(e) {
929+
var t = e.target;
930+
if (!t || !t.closest) return null;
931+
return t.closest('[data-tooltip]');
932+
}
933+
934+
document.addEventListener('pointerenter', function(e) {
935+
var el = tooltipTarget(e);
936+
if (!el) return;
937+
clearTimeout(showTimer);
938+
showTimer = setTimeout(function() { show(el); }, DELAY);
939+
}, true);
940+
941+
document.addEventListener('pointerleave', function(e) {
942+
var el = tooltipTarget(e);
943+
if (!el) return;
944+
hide();
945+
}, true);
946+
947+
// Also hide on click (tooltip served its purpose)
948+
document.addEventListener('pointerdown', hide, true);
949+
// Hide on scroll
950+
document.addEventListener('scroll', hide, true);
951+
})();
952+
</script>
953+
867954
<!-- Vite Entry Point -->
868955
<script type="module" src="/src/app.js"></script>
869956
</body>

src/app.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,7 @@ function handleExport() {
500500
}
501501

502502
function handleImport(data) {
503+
console.log('[import] handleImport called with', data ? 'data' : 'null');
503504
if (!data) return;
504505

505506
let responses = [];
@@ -561,7 +562,10 @@ function handleImport(data) {
561562
renderer.setAnsweredQuestions(responsesToAnsweredDots(merged, questionIndex));
562563
}
563564

564-
announce(`Imported ${newResponses.length} new responses (${valid.length} total in file, ${existing.length} already existed).`);
565+
const msg = `Imported ${newResponses.length} new responses (${valid.length} total in file, ${existing.length} already existed).`;
566+
announce(msg);
567+
_showBanner(msg, 'success');
568+
console.log('[import]', msg);
565569
}
566570

567571
function handleViewportChange(viewport) {

src/ui/controls.js

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ export function init(headerElement) {
181181
resetButton = document.createElement('button');
182182
resetButton.className = 'control-btn';
183183
resetButton.ariaLabel = 'Reset all progress';
184-
resetButton.title = 'Reset all progress';
184+
resetButton.dataset.tooltip = 'Reset all progress';
185185
resetButton.innerHTML = '<i class="fa-solid fa-rotate-right"></i>';
186186
resetButton.hidden = true;
187187
resetButton.addEventListener('click', () => {
@@ -192,7 +192,7 @@ export function init(headerElement) {
192192
exportButton = document.createElement('button');
193193
exportButton.className = 'control-btn';
194194
exportButton.ariaLabel = 'Export progress as JSON';
195-
exportButton.title = 'Export progress as JSON';
195+
exportButton.dataset.tooltip = 'Export progress';
196196
exportButton.innerHTML = '<i class="fa-solid fa-download"></i>';
197197
exportButton.hidden = true;
198198
exportButton.addEventListener('click', () => {
@@ -203,19 +203,25 @@ export function init(headerElement) {
203203
importButton = document.createElement('button');
204204
importButton.className = 'control-btn';
205205
importButton.ariaLabel = 'Import saved progress';
206-
importButton.title = 'Import saved progress';
206+
importButton.dataset.tooltip = 'Import progress';
207207
importButton.innerHTML = '<i class="fa-solid fa-upload"></i>';
208208
importButton.hidden = true;
209-
// Keep a module-level reference to avoid GC before FileReader fires
210-
let _importInput = null;
211209

212210
importButton.addEventListener('click', () => {
213-
_importInput = document.createElement('input');
214-
_importInput.type = 'file';
215-
_importInput.accept = '.json,application/json';
216-
_importInput.addEventListener('change', (e) => {
217-
const file = e.target.files[0];
218-
if (!file) return;
211+
// Create a file input, attach to DOM (required by some browsers for
212+
// the change event to fire), then remove after reading.
213+
const input = document.createElement('input');
214+
input.type = 'file';
215+
input.accept = '.json,application/json';
216+
input.style.cssText = 'position:fixed;left:-9999px;opacity:0;pointer-events:none;';
217+
document.body.appendChild(input);
218+
219+
input.addEventListener('change', () => {
220+
const file = input.files[0];
221+
if (!file) {
222+
document.body.removeChild(input);
223+
return;
224+
}
219225
const reader = new FileReader();
220226
reader.onload = () => {
221227
try {
@@ -225,16 +231,22 @@ export function init(headerElement) {
225231
console.error('[controls] Failed to parse import file:', err);
226232
alert('Invalid file format. Please select a Knowledge Mapper export JSON file.');
227233
}
228-
_importInput = null; // Release reference after successful read
234+
document.body.removeChild(input);
229235
};
230236
reader.onerror = () => {
231237
console.error('[controls] FileReader error:', reader.error);
232238
alert('Could not read file. Please try again.');
233-
_importInput = null;
239+
document.body.removeChild(input);
234240
};
235241
reader.readAsText(file);
236242
});
237-
_importInput.click();
243+
244+
// Clean up if user cancels the file dialog
245+
input.addEventListener('cancel', () => {
246+
document.body.removeChild(input);
247+
});
248+
249+
input.click();
238250
});
239251
container.appendChild(importButton);
240252

src/ui/modes.js

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ export function init(container) {
5353
color: var(--color-primary);
5454
box-shadow: 0 0 8px var(--color-glow-primary);
5555
}
56-
.mode-btn.active {
56+
.mode-btn.active,
57+
.mode-btn.active:hover {
5758
background: var(--color-primary);
5859
color: #ffffff;
5960
border-color: var(--color-primary);
@@ -75,32 +76,15 @@ export function init(container) {
7576
color: var(--color-secondary);
7677
box-shadow: 0 0 8px var(--color-glow-secondary);
7778
}
78-
.mode-btn--insight.active {
79+
.mode-btn--insight.active,
80+
.mode-btn--insight.active:hover {
7981
border-style: solid;
8082
background: var(--color-secondary);
8183
color: #ffffff;
8284
border-color: var(--color-secondary);
8385
box-shadow: 0 0 12px var(--color-glow-secondary);
8486
}
85-
.mode-btn:disabled:hover::after {
86-
content: attr(data-tooltip);
87-
position: absolute;
88-
bottom: calc(100% + 8px);
89-
left: 50%;
90-
transform: translateX(-50%);
91-
background: var(--color-surface);
92-
color: var(--color-text);
93-
padding: 6px 12px;
94-
border-radius: 6px;
95-
font-size: 0.75rem;
96-
white-space: nowrap;
97-
z-index: 100;
98-
pointer-events: none;
99-
border: 1px solid #00693e;
100-
border-left: 3px solid #00693e;
101-
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
102-
max-width: 200px;
103-
}
87+
/* Disabled mode button tooltips handled by global [data-tooltip] JS system */
10488
`;
10589
document.head.appendChild(style);
10690
}

0 commit comments

Comments
 (0)