Tooltips after one second, and at once for the next button

From the development repository at b7ffe0e.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ackandClaude Fable 5.1 committed 2026-10-11 17:47:04 +02:00
1 parent bc3c9bb886
commit fcd6117618
5 files changed
+64 -10

No files matched your search

+23 -6
View File
@@ -11,7 +11,18 @@
import { trace } from './log.js'; import { trace } from './log.js';
const DELAY_MS = 2000; /**
* How long the pointer must have been in a button before its tooltip
* appears: the time in the button, movement inside it not counting
* (Andy, 2026-10-11: one second; two until then, one until 2026-10-06).
*/
export const DELAY_MS = 1000;
/**
* The neighbour rule: within this long after a tooltip was shown, the
* next button's tooltip appears at once, so that reading a row of
* buttons costs one wait, not one per button. A press or a key ends it.
*/
export const WARM_MS = 1000;
const GAP_PX = 6; const GAP_PX = 6;
/** @type {HTMLElement | null} */ /** @type {HTMLElement | null} */
@@ -20,6 +31,8 @@ let tooltip = null;
let timer = null; let timer = null;
/** @type {Element | null} */ /** @type {Element | null} */
let shownFor = null; let shownFor = null;
/** When a tooltip was last showing, for the neighbour rule. */
let warmUntil = -Infinity;
/** /**
* @param {EventTarget | null} target * @param {EventTarget | null} target
@@ -49,10 +62,13 @@ function show(element) {
tooltip.style.top = `${box.bottom + GAP_PX}px`; tooltip.style.top = `${box.bottom + GAP_PX}px`;
} }
function hide() { /** @param {boolean} [cold] Whether the neighbour rule ends here too. */
function hide(cold = false) {
if (timer !== null) clearTimeout(timer); if (timer !== null) clearTimeout(timer);
timer = null; timer = null;
if (shownFor) warmUntil = performance.now() + WARM_MS;
shownFor = null; shownFor = null;
if (cold) warmUntil = -Infinity;
if (tooltip) tooltip.hidden = true; if (tooltip) tooltip.hidden = true;
} }
@@ -69,11 +85,12 @@ export function enableTooltips() {
if (element === shownFor) return; if (element === shownFor) return;
hide(); hide();
trace(`Hover: ${event.pointerType} over ${element.textContent?.trim() || element.id}`); trace(`Hover: ${event.pointerType} over ${element.textContent?.trim() || element.id}`);
const wait = performance.now() < warmUntil ? 0 : DELAY_MS;
timer = setTimeout(() => { timer = setTimeout(() => {
timer = null; timer = null;
trace(`Tooltip: ${element.textContent?.trim() || element.id}`); trace(`Tooltip: ${element.textContent?.trim() || element.id}`);
show(element); show(element);
}, DELAY_MS); }, wait);
}); });
document.addEventListener('pointerleave', (event) => { document.addEventListener('pointerleave', (event) => {
if (timer !== null || shownFor) trace(`Hover: ${event.pointerType} left the document`); if (timer !== null || shownFor) trace(`Hover: ${event.pointerType} left the document`);
@@ -85,14 +102,14 @@ export function enableTooltips() {
let pressedAt = -Infinity; let pressedAt = -Infinity;
document.addEventListener('pointerdown', () => { document.addEventListener('pointerdown', () => {
pressedAt = performance.now(); pressedAt = performance.now();
hide(); hide(true);
}); });
document.addEventListener('focusin', (event) => { document.addEventListener('focusin', (event) => {
if (performance.now() - pressedAt < 500) return; if (performance.now() - pressedAt < 500) return;
const element = helpElement(event.target); const element = helpElement(event.target);
if (element) show(element); if (element) show(element);
}); });
document.addEventListener('focusout', hide); document.addEventListener('focusout', () => hide());
// Typing means the user's attention is on the keys, not the tooltip. // Typing means the user's attention is on the keys, not the tooltip.
document.addEventListener('keydown', hide); document.addEventListener('keydown', () => hide(true));
} }
+1 -1
View File
@@ -53,7 +53,7 @@ groups: on the left the tools, their settings and **Undo**; in the
middle the page and the view; on the right the host's page buttons, middle the page and the view; on the right the host's page buttons,
**Notebooks**, your notebook, and **Session**, which opens a window with **Notebooks**, your notebook, and **Session**, which opens a window with
the participants and the invitation. Below it the page has the whole width of the window. Every button the participants and the invitation. Below it the page has the whole width of the window. Every button
explains itself: rest the pointer on it for two seconds and its explains itself: rest the pointer on it for a second and its
explanation appears beneath it. explanation appears beneath it.
### The page ### The page
+3 -2
View File
@@ -278,8 +278,9 @@ file that uses them, so that they can be changed in one place, or offered
as preferences later: the line widths and the five colours (`main.js`), as preferences later: the line widths and the five colours (`main.js`),
the zoom step of the wheel and the zoom per 100 pixels of a pen drag the zoom step of the wheel and the zoom per 100 pixels of a pen drag
(`renderer.js`), the dead zone of that drag (`pen_input.js`), the margin (`renderer.js`), the dead zone of that drag (`pen_input.js`), the margin
by which the page grows (`page_model.js`), the tooltip delay by which the page grows (`page_model.js`), the tooltip delay (1 s,
(`tooltip.js`), the hold that moves an image (600 ms, with 6 pixels of `DELAY_MS` in `tooltip.js`) and how long after one tooltip the next
button's comes at once (1 s, `WARM_MS` there), the hold that moves an image (600 ms, with 6 pixels of
slack, `pen_input.js`), the reach of an image's corner (24 pixels, slack, `pen_input.js`), the reach of an image's corner (24 pixels,
`renderer.js`), the smallest an image can be scaled to (1 cm, `renderer.js`), the smallest an image can be scaled to (1 cm,
`page_model.js`), how often a dragged image's place is sent (40 ms, `page_model.js`), how often a dragged image's place is sent (40 ms,
+1 -1
View File
@@ -70,7 +70,7 @@ rather than in the browser's storage.
The toolbar holds the pen, text, eraser and hide tools, colours and The toolbar holds the pen, text, eraser and hide tools, colours and
line widths, the view commands (Fit, A4, Widen, Shrink, Extend), the line widths, the view commands (Fit, A4, Widen, Shrink, Extend), the
host's page buttons (New, Image, Notebooks), your notebook, Keep, Export host's page buttons (New, Image, Notebooks), your notebook, Keep, Export
and Session. Rest the pointer on any button for two seconds for its and Session. Rest the pointer on any button for a second for its
explanation. Each participant writes on a layer of their own that explanation. Each participant writes on a layer of their own that
nobody else can change. The page is saved continuously to the current nobody else can change. The page is saved continuously to the current
notebook, `Current.svg`; Keep makes a dated copy. Rest the pen on an notebook, `Current.svg`; Keep makes a dated copy. Rest the pen on an
+36
View File
@@ -880,6 +880,42 @@ class EndToEndTest(unittest.TestCase):
firefox.switch_to(host_tab) firefox.switch_to(host_tab)
firefox.go("about:blank") firefox.go("about:blank")
def test_tooltips(self):
"""A button's explanation appears after the pointer has been in it
for a second; once one is showing, a neighbour's appears at once;
a press ends that."""
firefox = self.firefox
firefox.go(self.address("Late"))
self.enter_name("Reader", "start_session")
self.wait_until(lambda s: s["page"]["number"] == 1, "the page")
hover = lambda tool: firefox.run("""
document.querySelector('[data-tool=' + arguments[0] + ']')
.dispatchEvent(new PointerEvent('pointerover', {bubbles: true, pointerType: 'pen'}));
""", tool)
showing = lambda: firefox.run("""
const t = document.querySelector('.tooltip');
return t && !t.hidden ? t.textContent.slice(0, 20) : null;
""")
hover("pen")
time.sleep(0.5)
self.assertIsNone(showing())
time.sleep(0.8)
self.assertEqual(showing(), "Write on the page. O")
# The neighbour rule: the next tooltip comes without the wait.
firefox.run("document.body.dispatchEvent(new PointerEvent('pointerover', {bubbles: true}));")
self.assertIsNone(showing())
hover("text")
time.sleep(0.1)
self.assertEqual(showing(), "Type on the page. Ta")
# A press ends the tooltip and the rule.
firefox.run("document.body.dispatchEvent(new PointerEvent('pointerdown', {bubbles: true}));")
self.assertIsNone(showing())
hover("pen")
time.sleep(0.5)
self.assertIsNone(showing())
time.sleep(0.8)
self.assertEqual(showing(), "Write on the page. O")
def test_hide_rectangles(self): def test_hide_rectangles(self):
"""The teacher's requests of 2026-10-08: a Hide rectangle covers part """The teacher's requests of 2026-10-08: a Hide rectangle covers part
of the page in white for everyone, marked by blue corners.""" of the page in white for everyone, marked by blue corners."""