Merge pull request 'feat: add keyboard shortcuts' (#649) from fernandoporazzi/snort:scroll-up-shortcut into main
Reviewed-on: #649
This commit is contained in:
commit
5360c5ad3b
30
packages/app/src/Hooks/useKeyboardShortcut.ts
Normal file
30
packages/app/src/Hooks/useKeyboardShortcut.ts
Normal 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]);
|
||||
}
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user