diff --git a/app/assets/stylesheets/application.sass.scss b/app/assets/stylesheets/application.sass.scss index 43f030dd..969e0231 100644 --- a/app/assets/stylesheets/application.sass.scss +++ b/app/assets/stylesheets/application.sass.scss @@ -107,6 +107,7 @@ $unicodeRangeValues in Lexend.$unicodeMap { "components/comments", "components/container", "components/entry", +"components/hotkey", "components/icons", "components/inbox-actions", "components/inbox-entry", diff --git a/app/assets/stylesheets/components/_hotkey.scss b/app/assets/stylesheets/components/_hotkey.scss new file mode 100644 index 00000000..8e573947 --- /dev/null +++ b/app/assets/stylesheets/components/_hotkey.scss @@ -0,0 +1,5 @@ +.js-hotkey-navigating { + [data-navigation-target="current"] { + outline: var(--primary) solid 4px; + } +} diff --git a/app/javascript/retrospring/controllers/navigation_controller.ts b/app/javascript/retrospring/controllers/navigation_controller.ts new file mode 100644 index 00000000..cfadbc73 --- /dev/null +++ b/app/javascript/retrospring/controllers/navigation_controller.ts @@ -0,0 +1,48 @@ +import { Controller } from "@hotwired/stimulus"; +import { install, uninstall } from "@github/hotkey"; + +export default class extends Controller { + static classes = ["current"]; + static targets = ["current", "traversable"]; + + declare readonly hasCurrentTarget: boolean; + declare readonly currentTarget: HTMLElement; + declare readonly traversableTargets: HTMLElement[]; + + traversableTargetConnected(target: HTMLElement) { + if (!this.hasCurrentTarget) { + const first = this.traversableTargets[0]; + first.dataset.navigationTarget = "current"; + } + } + + currentTargetConnected(target: HTMLElement) { + target.querySelectorAll("[data-selection-hotkey]") + .forEach(el => install(el, el.dataset.selectionHotkey)) + } + + currentTargetDisconnected(target: HTMLElement) { + target.querySelectorAll("[data-selection-hotkey]") + .forEach(el => uninstall(el)) + } + + up(): void { + this.navigate(this.currentTarget.previousElementSibling as HTMLElement); + } + + down(): void { + this.navigate(this.currentTarget.nextElementSibling as HTMLElement); + } + + navigate(target: HTMLElement): void { + if (!document.body.classList.contains("js-hotkey-navigating")) { + document.body.classList.add("js-hotkey-navigating"); + } + + if (target.dataset.navigationTarget == "traversable") { + this.currentTarget.dataset.navigationTarget = "traversable"; + target.dataset.navigationTarget = "current"; + target.scrollIntoView(false); + } + } +} diff --git a/app/javascript/retrospring/initializers/stimulus.ts b/app/javascript/retrospring/initializers/stimulus.ts index b0c93d0f..77a11a62 100644 --- a/app/javascript/retrospring/initializers/stimulus.ts +++ b/app/javascript/retrospring/initializers/stimulus.ts @@ -11,6 +11,7 @@ import CropperController from "retrospring/controllers/cropper_controller"; import InboxSharingController from "retrospring/controllers/inbox_sharing_controller"; import ToastController from "retrospring/controllers/toast_controller"; import PwaBadgeController from "retrospring/controllers/pwa_badge_controller"; +import NavigationController from "retrospring/controllers/navigation_controller"; /** * This module sets up Stimulus and our controllers @@ -31,6 +32,7 @@ export default function (): void { window['Stimulus'].register('format-popup', FormatPopupController); window['Stimulus'].register('inbox-sharing', InboxSharingController); window['Stimulus'].register('pwa-badge', PwaBadgeController); + window['Stimulus'].register('navigation', NavigationController); window['Stimulus'].register('theme', ThemeController); window['Stimulus'].register('toast', ToastController); } diff --git a/app/views/answerbox/_actions.html.haml b/app/views/answerbox/_actions.html.haml index a6438cc2..6003b863 100644 --- a/app/views/answerbox/_actions.html.haml +++ b/app/views/answerbox/_actions.html.haml @@ -1,8 +1,8 @@ -%button.btn.btn-link.answerbox__action{ type: :button, name: "ab-smile", data: { a_id: a.id, action: current_user&.smiled?(a) ? :unsmile : :smile }, disabled: !user_signed_in? } +%button.btn.btn-link.answerbox__action{ type: :button, name: "ab-smile", data: { a_id: a.id, action: current_user&.smiled?(a) ? :unsmile : :smile, selection_hotkey: "l" }, disabled: !user_signed_in? } %i.fa.fa-fw.fa-smile-o %span{ id: "ab-smile-count-#{a.id}" }= a.smiles.count - unless display_all - %button.btn.btn-link.answerbox__action{ type: :button, name: "ab-comments", data: { a_id: a.id, state: :hidden } } + %button.btn.btn-link.answerbox__action{ type: :button, name: "ab-comments", data: { a_id: a.id, state: :hidden, selection_hotkey: "x" } } %i.fa.fa-fw.fa-comments %span{ id: "ab-comment-count-#{a.id}" }= a.comment_count .btn-group diff --git a/app/views/application/_answerbox.html.haml b/app/views/application/_answerbox.html.haml index 20b88963..586f7391 100644 --- a/app/views/application/_answerbox.html.haml +++ b/app/views/application/_answerbox.html.haml @@ -1,5 +1,5 @@ - display_all ||= nil -.card.answerbox{ data: { id: a.id, q_id: a.question.id } } +.card.answerbox{ data: { id: a.id, q_id: a.question.id, navigation_target: "traversable" } } - if @question.nil? = render "answerbox/header", a: a, display_all: display_all .card-body diff --git a/app/views/timeline/timeline.html.haml b/app/views/timeline/timeline.html.haml index 06610059..88c9a690 100644 --- a/app/views/timeline/timeline.html.haml +++ b/app/views/timeline/timeline.html.haml @@ -1,4 +1,6 @@ -#timeline +#timeline{ data: { controller: "navigation" } } + %button.d-none{ data: { hotkey: "j", action: "navigation#down" } } + %button.d-none{ data: { hotkey: "k", action: "navigation#up" } } - @timeline.each do |answer| = render "answerbox", a: answer