livewire/livewire: Prefetch on keyboard focus: wire:navigate.hover now works for Tab users too
Contributed by calebporzio
Part of the livewire v4.4.7 release
Pull request #10719 closes a small but noticeable accessibility gap in Livewire's navigation prefetching. Until now, wire:navigate.hover only rewarded mouse users: hovering a link for 60 milliseconds fetched the destination page in the background, but tabbing to that same link fetched nothing until Enter was pressed. Prompted by discussion #10353, this change teaches the existing delayed-prefetch helper to respond to keyboard focus as well, so a link that holds focus for 60 ms is prefetched exactly as if it had been hovered. It lands on the 4.x line as a small feature, together with documentation updates.
What changed under the hood
The core of the PR lives in js/plugins/navigate/links.js, where whenThisLinkIsHoveredFor is renamed to whenThisLinkIsHoveredOrFocusedFor. Rather than wiring a single mouseenter/mouseleave pair, the helper now loops over two enter/leave pairs (simplified here):
for (let [enterEvent, leaveEvent] of [['mouseenter', 'mouseleave'], ['focus', 'blur']]) {
// start a 60 ms timer on the enter event,
// cancel it if the leave event fires first
}
Hover and focus each own their timer: mouseenter starts one that mouseleave cancels, and focus starts a separate one that blur cancels. In the navigate plugin, the wire:navigate.hover branch simply calls the renamed helper, so focus now feeds the same prefetch callback hover always used. The existing URL cache keeps this cheap: when a link is hovered and then focused (or the reverse), the cache prevents a duplicate request from going out.
Why it matters
For mouse users, nothing changes. For keyboard users, the prefetch promise of .hover previously delivered none of its benefit: the first keyboard-driven navigation always paid the full round trip — the very cost .hover exists to remove. With this change, tabbing through a nav bar behaves like sweeping a cursor across it. Any link you pause on for 60 ms is downloaded before you commit to it, so pressing Enter can swap the page instantly. That brings mouse and keyboard parity to one of Livewire's flagship SPA-feel features, which matters for accessibility-minded teams and for anyone who simply prefers the keyboard.
The guardrails all still apply
Everything that made hover prefetching polite carries over unchanged:
- The 60 ms delay is identical, so incidental passes over or past a link fetch nothing.
- Leaving early cancels:
blurcancels a pending focus prefetch just asmouseleavecancels a hover one. A new browser test rapidly tabs through several links and asserts no prefetch request fires. - Native-link destinations and same-page fragment links are still excluded; those checks run before any request is made.
- The 30-second prefetch cache is untouched, and hover-plus-focus on the same link still produces one request, not two.
Upgrade impact
There is nothing to migrate. If you are coming from v3.8.10, .hover there is mouse-only; after upgrading to the 4.x release containing this PR, the same directive gains focus prefetching automatically:
<a href="/posts" wire:navigate.hover>Posts</a>
- Hovering for 60 ms: prefetch fires (as before).
- Tabbing to the link and holding focus for 60 ms: prefetch fires (new).
- Tabbing past the link in under 60 ms: nothing is fetched.
- Following the link afterward: served from the prefetch cache when available.
The one thing to be aware of is server load. Because keyboard users now generate prefetches too, total prefetch volume can rise slightly. The documentation's warning about .hover increasing server usage was updated to say "hover over or focus" — the 60 ms delay and the shared cache remain the mitigations. No configuration, no new modifiers, and no template changes are required.
How it was verified
The new real-keyboard browser test failed before the change with "Livewire navigate prefetch request was never triggered." After rebuilding assets, 10 focused browser tests pass with 37 assertions, covering keyboard focus prefetching, rapid Tab cancellation, hover/focus cache reuse, Enter navigation, the existing 30-second hover cache, failed-prefetch fallback, native links, and same-page fragments. A manual before/after check in a separate Laravel app told the same story: with a destination request counter in place, tabbing onto a wire:navigate.hover link went from zero requests to exactly one, Enter then opened the destination, and the console stayed clean.
You can run the focused suite yourself:
vendor/bin/phpunit src/Features/SupportNavigate/BrowserTest.php \
src/Features/SupportNavigate/FragmentBrowserTest.php \
--filter='prefetch|test_navigate_falls_back_to_browser_for_native_links|test_same_page_fragment_links_do_not_fetch_or_swap_the_page|test_fragment_links_support_keyboard_activation_and_repeated_clicks'
Takeaways
wire:navigate.hovernow prefetches on keyboard focus after the same 60 ms delay as hover.- Blur and mouseleave cancel their own pending timers, so tabbing quickly past links triggers no requests.
- Hover and focus share the existing URL cache, and the native-link and same-page-fragment exclusions are unchanged.
- Upgrading requires no code changes, but expect marginally more prefetch requests now that keyboard users benefit too.
- Both navigation docs (
navigate.mdandwire-navigate.md) document the new focus behavior.