Skip to content

Commit

Permalink
Merge pull request #264 from masonmcelvain/shortcuts
Browse files Browse the repository at this point in the history
Add keyboard shortcuts
  • Loading branch information
masonmcelvain authored May 20, 2023
2 parents 75cc0bb + 2dece03 commit 965f923
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 29 deletions.
18 changes: 3 additions & 15 deletions src/components/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Button, Text, useBoolean, VStack } from "@chakra-ui/react";
import { navigateCurrentTab, openInNewTab } from "@lib/webextension";
import { LinkData } from "@models/link-state";
import * as React from "react";
import { useDrag } from "react-dnd";
Expand All @@ -15,8 +14,9 @@ export type CardDragItem = {
type CardProps = {
linkData: LinkData;
isInEditMode: boolean;
onClick: React.MouseEventHandler;
};
export function Card({ linkData, isInEditMode }: CardProps) {
export function Card({ linkData, isInEditMode, onClick }: CardProps) {
const { id, name, url } = linkData;
const item: CardDragItem = { id };
const [isMouseOver, setIsMouseOver] = useBoolean();
Expand All @@ -40,26 +40,14 @@ export function Card({ linkData, isInEditMode }: CardProps) {
}
: {};

const clickHandler = React.useCallback<React.MouseEventHandler>(
(event) => {
if (event.ctrlKey) {
event.preventDefault();
openInNewTab(url.toString());
} else {
navigateCurrentTab(url.toString());
}
},
[url]
);

return (
<Button
pos="absolute"
top={0}
as="a"
target="_self"
href={url.toString()}
onClick={clickHandler}
onClick={onClick}
variant="ghost"
w="92%"
minH="92%"
Expand Down
60 changes: 53 additions & 7 deletions src/components/Cell.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { IconButton, Square, useBoolean, VStack } from "@chakra-ui/react";
import { useLinkStore } from "@hooks/useLinkStore";
import { getStorageKeyForLink, setStoredLinkKeys } from "@lib/webextension";
import {
getStorageKeyForLink,
navigateCurrentTab,
openInNewTab,
setStoredLinkKeys,
} from "@lib/webextension";
import * as React from "react";
import { useDrop } from "react-dnd";
import { Edit2, X } from "react-feather";
Expand Down Expand Up @@ -30,15 +35,56 @@ export default function Cell({
const isEmpty = index >= linkKeys.length;
const link = React.useMemo(
() =>
!isEmpty &&
links.find(
(link) => link && getStorageKeyForLink(link) === linkKeys[index]
),
(!isEmpty &&
links.find(
(link) => link && getStorageKeyForLink(link) === linkKeys[index]
)) ||
null,
[isEmpty, index, links, linkKeys]
);

const onClick = React.useCallback(
(event: React.MouseEvent | KeyboardEvent) => {
if (!link?.url) return;
if (event.ctrlKey) {
event.preventDefault();
openInNewTab(link.url);
} else {
navigateCurrentTab(link.url);
}
},
[link?.url]
);
const onKeyDown = React.useCallback(
(event: KeyboardEvent) => {
if (isEmpty) return;
if (String(index + 1) === event.key) {
event.preventDefault();
if (isInEditMode) {
openUpdateLinkModal(index);
} else {
onClick(event);
}
}
},
[index, isEmpty, isInEditMode, onClick, openUpdateLinkModal]
);
React.useEffect(() => {
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
};
}, [onKeyDown]);
const card = React.useMemo(
() => link && <Card linkData={link} isInEditMode={isInEditMode} />,
[isInEditMode, link]
() =>
link && (
<Card
linkData={link}
isInEditMode={isInEditMode}
onClick={onClick}
/>
),
[isInEditMode, link, onClick]
);

const reorderLinks = useLinkStore((state) => state.reorderLinks);
Expand Down
12 changes: 11 additions & 1 deletion src/components/LinkEditModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ export default function LinkEditModal({
getFormValuesForLink(link)
);

const onKeyDown = React.useCallback<React.KeyboardEventHandler<HTMLElement>>(
(event) => {
if (isOpen && event.key === "Escape") {
event.preventDefault();
onClose();
}
},
[isOpen, onClose]
);

const populateFormWithTab = React.useCallback(async () => {
const tab = await getCurrentTab();
setFormValues({
Expand Down Expand Up @@ -149,7 +159,7 @@ export default function LinkEditModal({
<Modal isOpen={isOpen} onClose={onClose} size="full">
<ModalOverlay />
<form>
<ModalContent borderRadius="none">
<ModalContent borderRadius="none" onKeyDown={onKeyDown}>
<ModalHeader>Update Link</ModalHeader>
<ModalCloseButton />
<ModalBody>
Expand Down
17 changes: 17 additions & 0 deletions src/components/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,23 @@ export default function Page() {
onClose: onLinkEditModalClose,
} = useDisclosure();

const onKeyDown = React.useCallback(
(event: KeyboardEvent) => {
if (event.key === "e") {
toggleEditMode();
} else if (event.key === "n") {
onLinkEditModalOpen();
}
},
[onLinkEditModalOpen, toggleEditMode]
);
React.useEffect(() => {
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
};
}, [onKeyDown]);

return (
<VStack w="full" p={2}>
<DndProvider backend={HTML5Backend}>
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useLinkStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
UpdateLinkData,
} from "@lib/links";
import { LinkState } from "@models/link-state";
import create from "zustand";
import { create } from "zustand";

interface LinkStore extends LinkState {
addLink: (link: AddLinkData) => void;
Expand Down
11 changes: 6 additions & 5 deletions src/public/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot } from "react-dom/client";
import { ChakraProvider, ColorModeScript } from "@chakra-ui/react";
import "./index.css";
import theme from "@ui/theme";
import App from "@components/App";

ReactDOM.render(
<>
const domNode = document.getElementById("root");
if (!domNode) throw new Error("No root element found");
createRoot(domNode).render(
<div>
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
<ChakraProvider theme={theme}>
<App />
</ChakraProvider>
</>,
document.querySelector("#root")
</div>
);
66 changes: 66 additions & 0 deletions tests/shortcuts.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { test, expect } from "@fixtures/extension";

test("can open link in new tab", async ({
context,
links,
pageWithOneLink: page,
}) => {
const [{ url }] = links;
await page.keyboard.down("Control");
const [newPage] = await Promise.all([
context.waitForEvent("page"),
page.keyboard.down("Digit1"),
]);
expect(newPage.url()).toBe(url);
});

test("can create new link", async ({ page, links }) => {
const [{ name, url, imageUrl }] = links;
await page.keyboard.down("n");

await page.getByPlaceholder("Name").fill(name);
await page.getByPlaceholder("Link URL").fill(url);
await page.getByPlaceholder("Image URL").fill(imageUrl);
await page.getByRole("button", { name: "Create", exact: true }).click();

const link = page.getByRole("link", { name });
await expect(link).toHaveAttribute("href", url);
await expect(link).toHaveText(name);
await expect(page.getByAltText(name)).toHaveAttribute("src", imageUrl);
});

test("can edit link", async ({ pageWithOneLink: page }) => {
const name = "React DnD";
const url = "https://react-dnd.github.io/react-dnd/about";
const imageUrl =
"https://react-dnd.github.io/react-dnd/favicon-32x32.png?v=b4e3a877490f33e678d9e30b115e75c3";

await page.keyboard.down("e");
await page.keyboard.down("Digit1");

await page.getByPlaceholder("Name").fill(name);
await page.getByPlaceholder("Link URL").fill(url);
await page.getByPlaceholder("Image URL").fill(imageUrl);
await page.getByRole("button", { name: "Update" }).click();

const link = page.getByRole("link");
await expect(link).toHaveAttribute("href", url);
await expect(link).toHaveText(name);
await expect(page.getByAltText(name)).toHaveAttribute("src", imageUrl);
});

test("can escape new link modal", async ({ page }) => {
await page.keyboard.down("n");
await page.keyboard.down("Escape");
await expect(
page.getByRole("button", { name: "Create new link" })
).toBeVisible();
});

test("can escape edit link modal", async ({ page }) => {
await page.keyboard.down("e");
await page.keyboard.down("Escape");
await expect(
page.getByRole("button", { name: "Create new link" })
).toBeVisible();
});

0 comments on commit 965f923

Please sign in to comment.