diff --git a/client/js/tooltip.js b/client/js/tooltip.js index a8c8e1e..f4272de 100644 --- a/client/js/tooltip.js +++ b/client/js/tooltip.js @@ -11,7 +11,18 @@ 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; /** @type {HTMLElement | null} */ @@ -20,6 +31,8 @@ let tooltip = null; let timer = null; /** @type {Element | null} */ let shownFor = null; +/** When a tooltip was last showing, for the neighbour rule. */ +let warmUntil = -Infinity; /** * @param {EventTarget | null} target @@ -49,10 +62,13 @@ function show(element) { 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); timer = null; + if (shownFor) warmUntil = performance.now() + WARM_MS; shownFor = null; + if (cold) warmUntil = -Infinity; if (tooltip) tooltip.hidden = true; } @@ -69,11 +85,12 @@ export function enableTooltips() { if (element === shownFor) return; hide(); trace(`Hover: ${event.pointerType} over ${element.textContent?.trim() || element.id}`); + const wait = performance.now() < warmUntil ? 0 : DELAY_MS; timer = setTimeout(() => { timer = null; trace(`Tooltip: ${element.textContent?.trim() || element.id}`); show(element); - }, DELAY_MS); + }, wait); }); document.addEventListener('pointerleave', (event) => { if (timer !== null || shownFor) trace(`Hover: ${event.pointerType} left the document`); @@ -85,14 +102,14 @@ export function enableTooltips() { let pressedAt = -Infinity; document.addEventListener('pointerdown', () => { pressedAt = performance.now(); - hide(); + hide(true); }); document.addEventListener('focusin', (event) => { if (performance.now() - pressedAt < 500) return; const element = helpElement(event.target); 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. - document.addEventListener('keydown', hide); + document.addEventListener('keydown', () => hide(true)); } diff --git a/docs/guest.md b/docs/guest.md index 704075d..4a180ae 100644 --- a/docs/guest.md +++ b/docs/guest.md @@ -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, **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 -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. ### The page diff --git a/docs/implementation.md b/docs/implementation.md index 11fa090..945c435 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -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`), 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 -by which the page grows (`page_model.js`), the tooltip delay -(`tooltip.js`), the hold that moves an image (600 ms, with 6 pixels of +by which the page grows (`page_model.js`), the tooltip delay (1 s, +`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, `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, diff --git a/docs/quickstart.md b/docs/quickstart.md index b013382..491ddaf 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -70,7 +70,7 @@ rather than in the browser's storage. The toolbar holds the pen, text, eraser and hide tools, colours and line widths, the view commands (Fit, A4, Widen, Shrink, Extend), the 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 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 diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py index 1427df2..37d4cbd 100644 --- a/tests/test_end_to_end.py +++ b/tests/test_end_to_end.py @@ -880,6 +880,42 @@ class EndToEndTest(unittest.TestCase): firefox.switch_to(host_tab) 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): """The teacher's requests of 2026-10-08: a Hide rectangle covers part of the page in white for everyone, marked by blue corners."""