feat: add keyboard shortcuts

This commit is contained in:
Fernando Porazzi 2023-10-10 23:27:27 +02:00
parent 237ce498b7
commit b0d84779c8
No known key found for this signature in database
GPG Key ID: 76A29568782EB818
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";
export default function Layout() {
const location = useLocation();
@ -30,6 +31,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"];
@ -65,10 +76,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"];
@ -79,6 +101,7 @@ const NoteCreatorButton = () => {
return (
<>
<button
ref={buttonRef}
className="primary note-create-button"
onClick={() =>
update(v => {
@ -97,6 +120,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

@ -494,3 +494,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;
}