avo
avo copied to clipboard
Keyboard shortcuts
Feature
We'd love to add keyboard shortcuts to Avo. We already have Mousetrap installed but I think this solution (2025 update) from Yaro is a bit more Stimulus-oriented.
Initial shortcuts
We don't need to support "everything" in the initial iteration, so let's pick a few we know they might be highly used.
- [ ]
Shift + ?Show a modal with the current shortcuts - [ ]
Command + KOpen search (already supported but we want to show it there) - [ ]
command + returnSave button - [ ]
command + \toggle sidebar - [ ]
escclose modal. Ensure it really closes all modals - [ ]
ccreate button when on an index view - [ ] anything else?
Current workarounds
Add them with custom JS.
Can we make shortcuts configurable? We offer a hash with the default ones and each user can modify if want
Yes. Through JS, they can attach handlers similar to how we do it now in avo.base.js with the m m m shortcut
https://discord.com/channels/740892036978442260/1279079528500564009/1280484223052152902
By adding the following Controller, you can now navigate pages using the left(←) and right(→) arrow keys on the keyboard
With the following controller, the shortcut keys don't work unless the paging buttons are visible. It might be more convenient to make the shortcuts work as long as the table list is displayed on the screen.
- app/javascript/controllers/pagination_shortcut_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
connect() {
this.boundHandleKeyPress = this.handleKeyPress.bind(this);
document.addEventListener("keydown", this.boundHandleKeyPress, { passive: true });
}
disconnect() {
document.removeEventListener("keydown", this.boundHandleKeyPress);
}
handleKeyPress(event) {
const direction = this.getDirection(event.key);
if (direction) this.clickBottomVisibleButton(direction);
}
getDirection(key) {
return (key === "ArrowRight" && "Next") || (key === "ArrowLeft" && "Previous");
}
clickBottomVisibleButton(direction) {
// Find and click the button in the specified direction that is visible on the screen
const button = this.findBottomVisibleButton(direction);
// If the button is found and is not disabled, perform the click
if (button && !button.hasAttribute("aria-disabled")) {
button.click();
}
}
findBottomVisibleButton(direction) {
// Retrieve all buttons for the specified direction (Next or Previous)
// and filter only those that are visible in the viewport
const buttons = Array.from(document.querySelectorAll(`a[aria-label="${direction}"]`))
.filter(button => {
// Determine if the button is within the current viewport
const rect = button.getBoundingClientRect();
const isInViewport = rect.top >= 0 && rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth);
// Determine if the button is inside a hidden tab (if hidden, it won't be selected)
const parent = button.closest('[data-tabs-target="tabPanel"]');
return isInViewport && (!parent || !parent.classList.contains('hidden'));
});
// Return the button closest to the bottom of the screen among the visible ones
return buttons.pop();
}
}
It might be easier for users to understand if you add the following to app/components/avo/paginator_component.html.erb
You can navigate pages using the ←/→ arrow keys
<% if @pagy.pages > 1 %>
<div class="text-sm text-slate-600 mr-4 font-bold">You can navigate pages using the ←/→ arrow keys</div>
<%# @todo: add first & last page.make the first and last buttons rounded %>
<%== helpers.pagy_nav @pagy, anchor_string: "data-turbo-frame=\"#{@turbo_frame}\"" %>
<% end %>