Merge pull request 'feat: add keyboard shortcuts' (#649) from fernandoporazzi/snort:scroll-up-shortcut into main

Reviewed-on: Kieran/snort#649
This commit is contained in:
Kieran 2023-10-11 18:25:07 +00:00
commit 5360c5ad3b
3 changed files with 71 additions and 2 deletions

View File

@ -0,0 +1,30 @@
import { useCallback, useEffect, useLayoutEffect, useRef } from "react";
export default function useKeyboardShortcut(key: string, callback: (event: KeyboardEvent) => void, node = null) {
// implement the callback ref pattern
const callbackRef = useRef(callback);
useLayoutEffect(() => {
callbackRef.current = callback;
});
// handle what happens on key press
const handleKeyPress = useCallback(
(event: KeyboardEvent) => {
// check if one of the key is part of the ones we want
if (event.key === key) {
callbackRef.current(event);
}
},
[key],
);
useEffect(() => {
// target is either the provided node or the document
const targetNode = node ?? document;
// attach the event listener
targetNode && targetNode.addEventListener("keydown", handleKeyPress);
// remove the event listener
return () => targetNode && targetNode.removeEventListener("keydown", handleKeyPress);
}, [handleKeyPress, node]);
}

View File

@ -1,5 +1,5 @@
import "./Layout.css";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
import { FormattedMessage, useIntl } from "react-intl";
import { useUserProfile } from "@snort/system-react";
@ -13,7 +13,7 @@ import { NoteCreator } from "Element/Event/NoteCreator";
import { mapPlanName } from "./subscribe";
import useLogin from "Hooks/useLogin";
import Avatar from "Element/User/Avatar";
import { profileLink } from "SnortUtils";
import { isFormElement, profileLink } from "SnortUtils";
import { getCurrentSubscription } from "Subscription";
import Toaster from "Toaster";
import Spinner from "Icons/Spinner";
@ -22,6 +22,7 @@ import { useTheme } from "Hooks/useTheme";
import { useLoginRelays } from "Hooks/useLoginRelays";
import { useNoteCreator } from "State/NoteCreator";
import { LoginUnlock } from "Element/PinPrompt";
import useKeyboardShortcut from "Hooks/useKeyboardShortcut";
import { LoginStore } from "Login";
export default function Layout() {
@ -32,6 +33,16 @@ export default function Layout() {
useLoginFeed();
useTheme();
useLoginRelays();
useKeyboardShortcut(".", event => {
// if event happened in a form element, do nothing, otherwise focus on search input
if (event.target && !isFormElement(event.target as HTMLElement)) {
event.preventDefault();
window.scrollTo({
top: 0,
behavior: "smooth",
});
}
});
const shouldHideHeader = useMemo(() => {
const hideOn = ["/login", "/new"];
@ -78,10 +89,21 @@ export default function Layout() {
}
const NoteCreatorButton = () => {
const buttonRef = useRef<HTMLButtonElement>(null);
const location = useLocation();
const { readonly } = useLogin(s => ({ readonly: s.readonly }));
const { show, replyTo, update } = useNoteCreator(v => ({ show: v.show, replyTo: v.replyTo, update: v.update }));
useKeyboardShortcut("n", event => {
// if event happened in a form element, do nothing, otherwise focus on search input
if (event.target && !isFormElement(event.target as HTMLElement)) {
event.preventDefault();
if (buttonRef.current) {
buttonRef.current.click();
}
}
});
const shouldHideNoteCreator = useMemo(() => {
const isReplyNoteCreatorShowing = replyTo && show;
const hideOn = ["/settings", "/messages", "/new", "/login", "/donate", "/e", "/subscribe"];
@ -92,6 +114,7 @@ const NoteCreatorButton = () => {
return (
<>
<button
ref={buttonRef}
className="primary note-create-button"
onClick={() =>
update(v => {
@ -110,6 +133,14 @@ const AccountHeader = () => {
const navigate = useNavigate();
const { formatMessage } = useIntl();
useKeyboardShortcut("/", event => {
// if event happened in a form element, do nothing, otherwise focus on search input
if (event.target && !isFormElement(event.target as HTMLElement)) {
event.preventDefault();
document.querySelector<HTMLInputElement>(".search input")?.focus();
}
});
const { publicKey, latestNotification, readNotifications, readonly } = useLogin(s => ({
publicKey: s.publicKey,
latestNotification: s.latestNotification,

View File

@ -504,3 +504,11 @@ export function kvToObject<T>(o: string, sep?: string) {
export function defaultAvatar(input: string) {
return `https://robohash.v0l.io/${input}.png`;
}
export function isFormElement(target: HTMLElement): boolean {
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
return true;
}
return false;
}