From 58ce33fed5873f2f7ddedb5bcd42646a633e8441 Mon Sep 17 00:00:00 2001 From: Aslam Date: Tue, 24 Sep 2024 18:55:52 +0700 Subject: [PATCH] fix(topic): Topic list keybind (#181) * fix(page): improve keybind * fix(topic): improve keybind * fix: learning state selector --- .../custom/learning-state-selector.tsx | 23 +- .../custom/sidebar/partial/topic-section.tsx | 20 +- web/components/routes/topics/TopicRoute.tsx | 24 +- web/components/routes/topics/list.tsx | 136 ++++------ .../routes/topics/partials/topic-item.tsx | 249 ++++++++++-------- 5 files changed, 204 insertions(+), 248 deletions(-) diff --git a/web/components/custom/learning-state-selector.tsx b/web/components/custom/learning-state-selector.tsx index 6f00af5c..984f8456 100644 --- a/web/components/custom/learning-state-selector.tsx +++ b/web/components/custom/learning-state-selector.tsx @@ -8,7 +8,7 @@ import { LEARNING_STATES, LearningStateValue } from "@/lib/constants" import { linkLearningStateSelectorAtom } from "@/store/link" import { Command, CommandInput, CommandList, CommandItem, CommandGroup } from "@/components/ui/command" import { ScrollArea } from "@/components/ui/scroll-area" -import type { icons } from "lucide-react" +import { icons } from "lucide-react" interface LearningStateSelectorProps { showSearch?: boolean @@ -37,6 +37,9 @@ export const LearningStateSelector: React.FC = ({ setIsLearningStateSelectorOpen(false) } + const iconName = selectedLearningState?.icon || defaultIcon + const labelText = selectedLearningState?.label || defaultLabel + return ( @@ -47,20 +50,8 @@ export const LearningStateSelector: React.FC = ({ variant="secondary" className={cn("gap-x-2 text-sm", className)} > - {selectedLearningState?.icon || - (defaultIcon && ( - - ))} - - {selectedLearningState?.label || - (defaultLabel && ( - - {selectedLearningState?.label || defaultLabel} - - ))} + {iconName && } + {labelText && {labelText}} @@ -97,7 +88,7 @@ export const LearningStateSelectorContent: React.FC {LEARNING_STATES.map(ls => ( - + {ls.icon && } {ls.label} = ({ pathname }) => { if (!me) return null return ( -
+
= ({ topicCount, isA isActive ? "bg-accent text-accent-foreground" : "hover:bg-accent hover:text-accent-foreground" )} > - +
) @@ -78,7 +74,7 @@ const List: React.FC = ({ topicsWantToLearn, topicsLearning, topicsLe count={topicsWantToLearn.length} label="To Learn" value="wantToLearn" - href="/me/wantToLearn" + href="#" isActive={pathname === "/me/wantToLearn"} /> = ({ topicsWantToLearn, topicsLearning, topicsLe label="Learning" value="learning" count={topicsLearning.length} - href="/me/learning" + href="#" isActive={pathname === "/me/learning"} /> = ({ topicsWantToLearn, topicsLearning, topicsLe label="Learned" value="learned" count={topicsLearned.length} - href="/me/learned" + href="#" isActive={pathname === "/me/learned"} />
@@ -118,7 +114,7 @@ const ListItem: React.FC = ({ label, value, href, count, isActive
(null) - const [isCommandPaletteOpen] = useAtom(commandPaletteOpenAtom) - const [disableEnterKey, setDisableEnterKey] = useState(false) - - const handleCommandPaletteClose = useCallback(() => { - setDisableEnterKey(true) - setTimeout(() => setDisableEnterKey(false), 100) - }, []) - - useEffect(() => { - if (!isCommandPaletteOpen) { - handleCommandPaletteClose() - } - }, [isCommandPaletteOpen, handleCommandPaletteClose]) - return (
- +
) } diff --git a/web/components/routes/topics/list.tsx b/web/components/routes/topics/list.tsx index 4d951498..8b16f767 100644 --- a/web/components/routes/topics/list.tsx +++ b/web/components/routes/topics/list.tsx @@ -1,8 +1,7 @@ -import React, { useCallback, useEffect, useMemo } from "react" +import * as React from "react" import { Primitive } from "@radix-ui/react-primitive" import { useAccount } from "@/lib/providers/jazz-provider" -import { atom, useAtom } from "jotai" -import { commandPaletteOpenAtom } from "@/components/custom/command-palette/command-palette" +import { atom } from "jotai" import { TopicItem } from "./partials/topic-item" import { useMedia } from "@/hooks/use-media" import { useRouter } from "next/navigation" @@ -11,12 +10,9 @@ import { Column } from "@/components/custom/column" import { useColumnStyles } from "./hooks/use-column-styles" import { LaAccount, ListOfTopics, Topic, UserRoot } from "@/lib/schema" import { LearningStateValue } from "@/lib/constants" +import { useKeyDown } from "@/hooks/use-key-down" -interface TopicListProps { - activeItemIndex: number | null - setActiveItemIndex: React.Dispatch> - disableEnterKey: boolean -} +interface TopicListProps {} interface MainTopicListProps extends TopicListProps { me: { @@ -35,32 +31,21 @@ export interface PersonalTopic { export const topicOpenPopoverForIdAtom = atom(null) -export const TopicList: React.FC = ({ activeItemIndex, setActiveItemIndex, disableEnterKey }) => { +export const TopicList: React.FC = () => { const { me } = useAccount({ root: { topicsWantToLearn: [], topicsLearning: [], topicsLearned: [] } }) if (!me) return null - return ( - - ) + return } -export const MainTopicList: React.FC = ({ - me, - activeItemIndex, - setActiveItemIndex, - disableEnterKey -}) => { +export const MainTopicList: React.FC = ({ me }) => { const isTablet = useMedia("(max-width: 640px)") - const [isCommandPaletteOpen] = useAtom(commandPaletteOpenAtom) + const [activeItemIndex, setActiveItemIndex] = React.useState(null) + const [keyboardActiveIndex, setKeyboardActiveIndex] = React.useState(null) const router = useRouter() - const personalTopics = useMemo( + const personalTopics = React.useMemo( () => [ ...me.root.topicsWantToLearn.map(topic => ({ topic, learningState: "wantToLearn" as const })), ...me.root.topicsLearning.map(topic => ({ topic, learningState: "learning" as const })), @@ -69,44 +54,63 @@ export const MainTopicList: React.FC = ({ [me.root.topicsWantToLearn, me.root.topicsLearning, me.root.topicsLearned] ) - const itemCount = personalTopics.length - - const handleEnter = useCallback( + const handleEnter = React.useCallback( (selectedTopic: Topic) => { router.push(`/${selectedTopic.name}`) }, [router] ) - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - if (isCommandPaletteOpen) return + const next = () => Math.min((activeItemIndex ?? 0) + 1, (personalTopics?.length ?? 0) - 1) - if (e.key === "ArrowUp" || e.key === "ArrowDown") { - e.preventDefault() - setActiveItemIndex(prevIndex => { - if (prevIndex === null) return 0 - const newIndex = e.key === "ArrowUp" ? (prevIndex - 1 + itemCount) % itemCount : (prevIndex + 1) % itemCount - return newIndex - }) - } else if (e.key === "Enter" && !disableEnterKey && activeItemIndex !== null && personalTopics) { - e.preventDefault() - const selectedTopic = personalTopics[activeItemIndex] - if (selectedTopic?.topic) handleEnter?.(selectedTopic.topic) - } - }, - [itemCount, isCommandPaletteOpen, activeItemIndex, setActiveItemIndex, disableEnterKey, personalTopics, handleEnter] - ) + const prev = () => Math.max((activeItemIndex ?? 0) - 1, 0) - useEffect(() => { - window.addEventListener("keydown", handleKeyDown) - return () => window.removeEventListener("keydown", handleKeyDown) - }, [handleKeyDown]) + const handleKeyDown = (ev: KeyboardEvent) => { + switch (ev.key) { + case "ArrowDown": + ev.preventDefault() + ev.stopPropagation() + setActiveItemIndex(next()) + setKeyboardActiveIndex(next()) + break + case "ArrowUp": + ev.preventDefault() + ev.stopPropagation() + setActiveItemIndex(prev()) + setKeyboardActiveIndex(prev()) + } + } + + useKeyDown(() => true, handleKeyDown) + + const { setElementRef } = useActiveItemScroll({ activeIndex: keyboardActiveIndex }) return (
{!isTablet && } - + + {personalTopics?.map( + (pt, index) => + pt.topic?.id && ( + setElementRef(el, index)} + topic={pt.topic} + learningState={pt.learningState} + isActive={index === activeItemIndex} + onPointerMove={() => { + setKeyboardActiveIndex(null) + setActiveItemIndex(index) + }} + data-keyboard-active={keyboardActiveIndex === index} + /> + ) + )} +
) } @@ -125,33 +129,3 @@ export const ColumnHeader: React.FC = () => {
) } - -interface TopicListItemsProps { - personalTopics: PersonalTopic[] | null - activeItemIndex: number | null -} - -const TopicListItems: React.FC = ({ personalTopics, activeItemIndex }) => { - const { setElementRef } = useActiveItemScroll({ activeIndex: activeItemIndex }) - - return ( - - {personalTopics?.map( - (pt, index) => - pt.topic?.id && ( - setElementRef(el, index)} - topic={pt.topic} - learningState={pt.learningState} - isActive={index === activeItemIndex} - /> - ) - )} - - ) -} diff --git a/web/components/routes/topics/partials/topic-item.tsx b/web/components/routes/topics/partials/topic-item.tsx index 05aec78d..57b18308 100644 --- a/web/components/routes/topics/partials/topic-item.tsx +++ b/web/components/routes/topics/partials/topic-item.tsx @@ -12,146 +12,163 @@ import { useAtom } from "jotai" import { topicOpenPopoverForIdAtom } from "../list" import { LEARNING_STATES, LearningStateValue } from "@/lib/constants" import { useAccount } from "@/lib/providers/jazz-provider" +import { useRouter } from "next/navigation" -interface TopicItemProps { +interface TopicItemProps extends React.HTMLAttributes { topic: Topic learningState: LearningStateValue isActive: boolean } -export const TopicItem = React.forwardRef(({ topic, learningState, isActive }, ref) => { - const columnStyles = useColumnStyles() - const [openPopoverForId, setOpenPopoverForId] = useAtom(topicOpenPopoverForIdAtom) - const { me } = useAccount({ root: { topicsWantToLearn: [], topicsLearning: [], topicsLearned: [] } }) +export const TopicItem = React.forwardRef( + ({ topic, learningState, isActive, ...props }, ref) => { + const columnStyles = useColumnStyles() + const [openPopoverForId, setOpenPopoverForId] = useAtom(topicOpenPopoverForIdAtom) + const router = useRouter() + const { me } = useAccount({ root: { topicsWantToLearn: [], topicsLearning: [], topicsLearned: [] } }) - let p: { - index: number - topic?: Topic | null - learningState: LearningStateValue - } | null = null + let p: { + index: number + topic?: Topic | null + learningState: LearningStateValue + } | null = null - const wantToLearnIndex = me?.root.topicsWantToLearn.findIndex(t => t?.id === topic.id) ?? -1 - if (wantToLearnIndex !== -1) { - p = { - index: wantToLearnIndex, - topic: me?.root.topicsWantToLearn[wantToLearnIndex], - learningState: "wantToLearn" - } - } - - const learningIndex = me?.root.topicsLearning.findIndex(t => t?.id === topic.id) ?? -1 - if (learningIndex !== -1) { - p = { - index: learningIndex, - topic: me?.root.topicsLearning[learningIndex], - learningState: "learning" - } - } - - const learnedIndex = me?.root.topicsLearned.findIndex(t => t?.id === topic.id) ?? -1 - if (learnedIndex !== -1) { - p = { - index: learnedIndex, - topic: me?.root.topicsLearned[learnedIndex], - learningState: "learned" - } - } - - const selectedLearningState = useMemo(() => LEARNING_STATES.find(ls => ls.value === learningState), [learningState]) - - const handleLearningStateSelect = useCallback( - (value: string) => { - const newLearningState = value as LearningStateValue - - const topicLists: Record = { - wantToLearn: me?.root.topicsWantToLearn, - learning: me?.root.topicsLearning, - learned: me?.root.topicsLearned + const wantToLearnIndex = me?.root.topicsWantToLearn.findIndex(t => t?.id === topic.id) ?? -1 + if (wantToLearnIndex !== -1) { + p = { + index: wantToLearnIndex, + topic: me?.root.topicsWantToLearn[wantToLearnIndex], + learningState: "wantToLearn" } + } - const removeFromList = (state: LearningStateValue, index: number) => { - topicLists[state]?.splice(index, 1) + const learningIndex = me?.root.topicsLearning.findIndex(t => t?.id === topic.id) ?? -1 + if (learningIndex !== -1) { + p = { + index: learningIndex, + topic: me?.root.topicsLearning[learningIndex], + learningState: "learning" } + } - if (p) { - if (newLearningState === p.learningState) { - removeFromList(p.learningState, p.index) - return + const learnedIndex = me?.root.topicsLearned.findIndex(t => t?.id === topic.id) ?? -1 + if (learnedIndex !== -1) { + p = { + index: learnedIndex, + topic: me?.root.topicsLearned[learnedIndex], + learningState: "learned" + } + } + + const selectedLearningState = useMemo(() => LEARNING_STATES.find(ls => ls.value === learningState), [learningState]) + + const handleLearningStateSelect = useCallback( + (value: string) => { + const newLearningState = value as LearningStateValue + + const topicLists: Record = { + wantToLearn: me?.root.topicsWantToLearn, + learning: me?.root.topicsLearning, + learned: me?.root.topicsLearned } - removeFromList(p.learningState, p.index) - } - topicLists[newLearningState]?.push(topic) + const removeFromList = (state: LearningStateValue, index: number) => { + topicLists[state]?.splice(index, 1) + } - setOpenPopoverForId(null) - }, - [setOpenPopoverForId, me?.root.topicsWantToLearn, me?.root.topicsLearning, me?.root.topicsLearned, p, topic] - ) + if (p) { + if (newLearningState === p.learningState) { + removeFromList(p.learningState, p.index) + return + } + removeFromList(p.learningState, p.index) + } - const handlePopoverTriggerClick = (e: React.MouseEvent) => { - e.preventDefault() - e.stopPropagation() + topicLists[newLearningState]?.push(topic) - setOpenPopoverForId(openPopoverForId === topic.id ? null : topic.id) - } + setOpenPopoverForId(null) + }, + [setOpenPopoverForId, me?.root.topicsWantToLearn, me?.root.topicsLearning, me?.root.topicsLearned, p, topic] + ) - return ( -
+ const handlePopoverTriggerClick = (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + + setOpenPopoverForId(openPopoverForId === topic.id ? null : topic.id) + } + + const handleKeyDown = React.useCallback( + (ev: React.KeyboardEvent) => { + if (ev.key === "Enter") { + ev.preventDefault() + ev.stopPropagation() + router.push(`/${topic.name}`) + } + }, + [router, topic.id] + ) + + return ( - - {topic.prettyName} - +
+ + {topic.prettyName} + - - setOpenPopoverForId(open ? topic.id : null)} - > - - - - e.stopPropagation()} + + setOpenPopoverForId(open ? topic.id : null)} > - - - - + + + + e.stopPropagation()} + > + + + + +
-
- ) -}) + ) + } +) TopicItem.displayName = "TopicItem"