Skip to content

feat(docs): page through the guide with left/right arrow keys - #2984

Closed
AnayGarodia wants to merge 7 commits into
maplibre:mainfrom
AnayGarodia:feat/2788-arrow-key-page-nav
Closed

feat(docs): page through the guide with left/right arrow keys#2984
AnayGarodia wants to merge 7 commits into
maplibre:mainfrom
AnayGarodia:feat/2788-arrow-key-page-nav

Conversation

@AnayGarodia

Copy link
Copy Markdown
Contributor

What

Fixes #2788. Left/Right arrow keys now page through the guide via the footer prev/next links — a common paged-docs convenience.

How

A single document-level keydown listener maps ArrowLeft/ArrowRight to the existing footer a.md-footer__link--prev/--next anchors, wired through extra_javascript. It survives navigation.instant, ignores modifier-key combos, and does nothing when a form field is focused.

Testing

  • uvx zensical build → "No issues found"; the script is emitted and referenced in the built HTML.
  • Live browser check: from /installation/, ArrowRight → /run/, ArrowLeft → back to /installation/; with an input focused, ArrowRight did not navigate (guard works).

Add a small keyboard-nav script (wired via zensical extra_javascript) that
maps ArrowLeft/ArrowRight to the previous/next footer links, matching the
common paged-site pattern. A single document-level listener keeps working
across instant navigation, looks up the current page's footer links on each
key press, and ignores key presses while a modifier is held or a form field
(e.g. the search box) is focused.

Fixes maplibre#2788
Copilot AI review requested due to automatic review settings July 8, 2026 22:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Performance Comparison mainfeat/2788-arrow-key-page-nav

Total Elapsed Time: 120.85s → 123.50s (+2.2%)
CPU Baseline: 82.92µs → 82.92µs (+0.0%)
Benchmark ID: timing

timing - Function execution time metrics.

+----------------------------+------------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| Function                   | Calls                        | Avg                              | P95                              | Total                            | % Total                      |
+----------------------------+------------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| martin::main               | 1 → 1 (+0.0%)                | 120.85s → 123.50s (+2.2%)        | 120.86s → 123.55s (+2.2%)        | 120.85s → 123.50s (+2.2%)        | 100.00% → 100.00% (+0.0%)    |
+----------------------------+------------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| martin::start              | 1 → 1 (+0.0%)                | 120.84s → 123.49s (+2.2%)        | 120.86s → 123.55s (+2.2%)        | 120.84s → 123.49s (+2.2%)        | 99.99% → 99.99% (+0.0%)      |
+----------------------------+------------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| content::get_tile          | 3200800 → 3200800 (+0.0%)    | 17.97µs → 18.20µs (+1.3%)        | 29.71µs → 30.00µs (+1.0%)        | 57.53s → 58.26s (+1.3%)          | 47.60% → 47.17% (-0.9%)      |
+----------------------------+------------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| content::get_http_response | 2200600 → 2200600 (+0.0%)    | 18.14µs → 18.74µs (+3.3%)        | 27.77µs → 27.81µs (+0.1%)        | 39.92s → 41.24s (+3.3%)          | 33.03% → 33.39% (+1.1%)      |
+----------------------------+------------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| content::get_tile_content  | 2200600 → 2200600 (+0.0%)    | 15.59µs → 16.24µs (+4.2%)        | 25.42µs → 25.50µs (+0.3%)        | 34.31s → 35.73s (+4.1%)          | 28.39% → 28.93% (+1.9%)      |
+----------------------------+------------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| content::new               | 3200800 → 3200800 (+0.0%)    | 1.83µs → 1.86µs (+1.6%)          | 2.10µs → 2.31µs (+10.0%)         | 5.85s → 5.95s (+1.7%)            | 4.84% → 4.82% (-0.4%)        |
+----------------------------+------------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| source::get_sources        | 3200800 → 3200800 (+0.0%)    | 1.53µs → 1.54µs (+0.7%)          | 1.81µs → 1.98µs (+9.4%)          | 4.90s → 4.93s (+0.6%)            | 4.06% → 3.99% (-1.7%)        |
+----------------------------+------------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| content::recompress        | 2200600 → 2200600 (+0.0%)    | 1.61µs → 1.51µs (-6.2%)          | 130.00ns → 130.00ns (+0.0%)      | 3.56s → 3.34s (-6.2%)            | 2.94% → 2.70% (-8.2%)        |
+----------------------------+------------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| content::encode            | 100100 → 100100 (+0.0%)      | 34.20µs → 32.13µs (-6.1%)        | 45.76µs → 43.94µs (-4.0%)        | 3.42s → 3.22s (-5.8%)            | 2.83% → 2.60% (-8.1%)        |
+----------------------------+------------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| server::new_server         | 1 → 1 (+0.0%)                | 272.95µs → 260.74µs (-4.5%)      | 273.15µs → 260.86µs (-4.5%)      | 272.95µs → 260.74µs (-4.5%)      | 0.00% → 0.00% (+0.0%)        |
+----------------------------+------------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| content::decode            | 1 → 1 (+0.0%)                | 40.00ns → 140.00ns (+250.0%) ⚠️  | 40.00ns → 140.00ns (+250.0%) ⚠️  | 40.00ns → 140.00ns (+250.0%) ⚠️  | 0.00% → 0.00% (+0.0%)        |
+----------------------------+------------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+

alloc-bytes - Exclusive allocation bytes by each function (excluding nested calls).

+----------------------------+------------------------------+--------------------------------+--------------------------------+--------------------------------+----------------------------+
| Function                   | Calls                        | Avg                            | P95                            | Total                          | % Total                    |
+----------------------------+------------------------------+--------------------------------+--------------------------------+--------------------------------+----------------------------+
| content::get_tile_content  | 2200600 → 2200600 (+0.0%)    | 93.0 KB → 93.0 KB (+0.0%)      | 181.9 KB → 181.9 KB (+0.0%)    | 195.2 GB → 195.2 GB (+0.0%)    | 83.91% → 83.91% (+0.0%)    |
+----------------------------+------------------------------+--------------------------------+--------------------------------+--------------------------------+----------------------------+
| content::encode            | 100100 → 100100 (+0.0%)      | 344.5 KB → 344.5 KB (+0.0%)    | 347.2 KB → 347.2 KB (+0.0%)    | 32.9 GB → 32.9 GB (+0.0%)      | 14.14% → 14.14% (+0.0%)    |
+----------------------------+------------------------------+--------------------------------+--------------------------------+--------------------------------+----------------------------+
| source::get_sources        | 3200800 → 3200800 (+0.0%)    | 1.1 KB → 1.1 KB (+0.0%)        | 2.4 KB → 2.4 KB (+0.0%)        | 3.3 GB → 3.3 GB (+0.0%)        | 1.43% → 1.43% (+0.0%)      |
+----------------------------+------------------------------+--------------------------------+--------------------------------+--------------------------------+----------------------------+
| content::get_tile          | 3200800 → 3200800 (+0.0%)    | 287 B → 287 B (+0.0%)          | 296 B → 296 B (+0.0%)          | 876.6 MB → 876.6 MB (+0.0%)    | 0.37% → 0.37% (+0.0%)      |
+----------------------------+------------------------------+--------------------------------+--------------------------------+--------------------------------+----------------------------+
| content::get_http_response | 2200600 → 2200600 (+0.0%)    | 182 B → 182 B (+0.0%)          | 200 B → 200 B (+0.0%)          | 383.6 MB → 383.6 MB (+0.0%)    | 0.16% → 0.16% (+0.0%)      |
+----------------------------+------------------------------+--------------------------------+--------------------------------+--------------------------------+----------------------------+
| martin::start              | 1 → 1 (+0.0%)                | 3.0 MB → 3.0 MB (+0.0%)        | 3.0 MB → 3.0 MB (+0.0%)        | 3.0 MB → 3.0 MB (+0.0%)        | 0.00% → 0.00% (+0.0%)      |
+----------------------------+------------------------------+--------------------------------+--------------------------------+--------------------------------+----------------------------+
| martin::main               | 1 → 1 (+0.0%)                | 139.9 KB → 139.9 KB (+0.0%)    | 139.9 KB → 139.9 KB (+0.0%)    | 139.9 KB → 139.9 KB (+0.0%)    | 0.00% → 0.00% (+0.0%)      |
+----------------------------+------------------------------+--------------------------------+--------------------------------+--------------------------------+----------------------------+
| server::new_server         | 1 → 1 (+0.0%)                | 31.1 KB → 31.1 KB (+0.0%)      | 31.2 KB → 31.2 KB (+0.0%)      | 31.1 KB → 31.1 KB (+0.0%)      | 0.00% → 0.00% (+0.0%)      |
+----------------------------+------------------------------+--------------------------------+--------------------------------+--------------------------------+----------------------------+
| content::decode            | 1 → 1 (+0.0%)                | 0 B → 0 B (+0.0%)              | 0 B → 0 B (+0.0%)              | 0 B → 0 B (+0.0%)              | 0.00% → 0.00% (+0.0%)      |
+----------------------------+------------------------------+--------------------------------+--------------------------------+--------------------------------+----------------------------+
| content::new               | 3200800 → 3200800 (+0.0%)    | 0 B → 0 B (+0.0%)              | 0 B → 0 B (+0.0%)              | 0 B → 0 B (+0.0%)              | 0.00% → 0.00% (+0.0%)      |
+----------------------------+------------------------------+--------------------------------+--------------------------------+--------------------------------+----------------------------+
| content::recompress        | 2200600 → 2200600 (+0.0%)    | 0 B → 0 B (+0.0%)              | 0 B → 0 B (+0.0%)              | 0 B → 0 B (+0.0%)              | 0.00% → 0.00% (+0.0%)      |
+----------------------------+------------------------------+--------------------------------+--------------------------------+--------------------------------+----------------------------+

Threads

Total Alloc: 4.0 MB → 4.1 MB (+2.5%)
Total Dealloc: 302.6 MB → 302.6 MB (+0.0%)
Mem Diff: -298.6 MB → -298.5 MB (+0.0%)

+--------+--------------------------+----------------------------+---------------------------------+----------------------------+---------------------------------+
| Thread | CPU % Avg                | CPU % Max                  | Alloc                           | Dealloc                    | Mem Diff                        |
+--------+--------------------------+----------------------------+---------------------------------+----------------------------+---------------------------------+
| martin | 0.00% → 0.00% (+0.0%)    | 12.00% → 11.90% (-0.8%)    | 2.8 MB → 2.8 MB (+0.0%)         | 1.9 MB → 1.8 MB (-5.3%)    | 974.6 KB → 1017.5 KB (+4.4%)    |
+--------+--------------------------+----------------------------+---------------------------------+----------------------------+---------------------------------+
| hp-mcp | 0.00% → 0.00% (+0.0%)    | 0.00% → 0.00% (+0.0%)      | 11.2 KB → 54.7 KB (+388.4%) ⚠️  | 3.1 KB → 3.1 KB (+0.0%)    | 8.1 KB → 51.6 KB (+537.1%) ⚠️   |
+--------+--------------------------+----------------------------+---------------------------------+----------------------------+---------------------------------+

Generated with hotpath-rs

Comment thread zensical.toml Outdated

@CommanderStorm CommanderStorm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tesed it and it does not page through the docs. Am I using it wrong?

@CommanderStorm
CommanderStorm marked this pull request as draft July 9, 2026 09:16
@AnayGarodia

Copy link
Copy Markdown
Contributor Author

weird, it pages fine on my end. built the docs fresh (uvx zensical build, and tried serve too) and ←/→ walk through prev/next on the guide pages. poked at the built html to be sure: the <script src=.../keyboard-nav.js> tag is on every page and the footer links it hooks into (md-footer__link--prev/--next) render normally.

my hunch is zensical serve doesn't re-read the new extra_javascript line without a full restart, so if you had a serve already running (or a warm target/book) when you switched to the branch, the script never got injected and the arrows just do nothing. could you kill the serve, re-run uvx zensical serve, and hard-refresh? if it's still dead after that, a quick check: does view-source on a page show the keyboard-nav.js script tag? that'd tell us config-loading vs browser.

can drop a short screen capture of it paging if that's easier.

@CommanderStorm CommanderStorm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I think I had something in the cache, can reproduce the buttons changing pages now, so it partially works now.

The search bar does not work anymore though...

Comment on lines +4 to +13
function isTypingTarget(el) {
if (!el) {
return false;
}
if (el.isContentEditable) {
return true;
}
var tag = el.tagName;
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
}

@CommanderStorm CommanderStorm Jul 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this does not seem to work seme to quite work

Recording 2026-07-10 at 02 21 01

The keydown handler only bailed when focus was in the search input, but
arrowing through the search results moves focus onto a result link, so
Left/Right got hijacked and navigated the page out from under the open
search. Bail whenever the search overlay is open, keyed on the theme's
own `#__search` toggle checkbox, so arrow-key handling stays with search.
@AnayGarodia

Copy link
Copy Markdown
Contributor Author

good catch, reproduced and fixed.

the bug was in the focus check. i only bailed when you were typing in the search box (activeElement is the input), but when you arrow down into the results the theme moves focus onto the result link - so activeElement is an <a>, my guard missed it, and left/right got hijacked and navigated the page out from under the open search.

now it bails whenever the search overlay is open at all, keyed on the theme's own #__search toggle checkbox rather than the focused element.

verified against the real search UI in a local build:

  • open search via the button or the / hotkey -> #__search is checked
  • with it open, left/right no longer navigate, regardless of where focus landed
  • close search -> paging works again
  • typing in the box, left/right move the cursor with no nav

on the zensical.toml suggestion: extra_javascript is the documented way to register a script (the config comment right above it points there), so i kept it. if you'd rather load it through a template override or hook, happy to switch.

@AnayGarodia
AnayGarodia marked this pull request as ready for review July 11, 2026 04:59
@AnayGarodia

Copy link
Copy Markdown
Contributor Author

Marked ready. Re-verified the search-vs-paging interaction end to end in a fresh local build (uvx zensical build + serve), driving a real headless browser rather than eyeballing:

  • on /quick-start/, → navigates to the next guide page, ← back to the previous one
  • open the search overlay (#__search checked), then ←/→ no longer navigate the page — regardless of whether focus is on the input or an arrowed-into result link
  • close search, ←/→ page through the guide again

That covers the regression you hit (paging worked but search broke): the guard now keys on the theme's #__search toggle being open, not on which element has focus, so it can't be defeated by focus moving onto a result link.

@CommanderStorm

Copy link
Copy Markdown
Member

No, does not work. Please actually test your work..

Recording 2026-07-12 at 09 58 07

@CommanderStorm
CommanderStorm marked this pull request as draft July 13, 2026 20:19
@CommanderStorm

Copy link
Copy Markdown
Member

Does not currently work I think, so lets close this PR for now.
I would really like to see this fixed though, so another attempt would be greatly appreciated ^^

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

make left/right arrow keys page through the guide

4 participants