avo icon indicating copy to clipboard operation
avo copied to clipboard

Keyboard shortcuts

Open adrianthedev opened this issue 1 year ago • 3 comments

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 + K Open search (already supported but we want to show it there)
  • [ ] command + return Save button
  • [ ] command + \ toggle sidebar
  • [ ] esc close modal. Ensure it really closes all modals
  • [ ] c create button when on an index view
  • [ ] anything else?

Current workarounds

Add them with custom JS.

adrianthedev avatar Apr 06 '24 19:04 adrianthedev

Can we make shortcuts configurable? We offer a hash with the default ones and each user can modify if want

Paul-Bob avatar Apr 08 '24 09:04 Paul-Bob

Yes. Through JS, they can attach handlers similar to how we do it now in avo.base.js with the m m m shortcut

adrianthedev avatar Apr 15 '24 06:04 adrianthedev

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 %>

yuki-yogi avatar Sep 08 '24 19:09 yuki-yogi