From afaef5d3c564abb8980bcb3440f7bc688dbcb0d5 Mon Sep 17 00:00:00 2001 From: Aslam Date: Thu, 19 Sep 2024 21:11:52 +0700 Subject: [PATCH] fix(link): Navigate between item and fix Enter keybind (#165) * feat: add item scroll to active * fix: reset enterkey and scroll to view * fix: link item displayName --- web/components/routes/link/LinkRoute.tsx | 8 +- web/components/routes/link/header.tsx | 2 +- web/components/routes/link/list.tsx | 10 +- .../routes/link/partials/link-item.tsx | 246 +++++++++--------- web/hooks/use-active-item-scroll.ts | 30 +++ 5 files changed, 167 insertions(+), 129 deletions(-) create mode 100644 web/hooks/use-active-item-scroll.ts diff --git a/web/components/routes/link/LinkRoute.tsx b/web/components/routes/link/LinkRoute.tsx index ce81bee5..bfbc424a 100644 --- a/web/components/routes/link/LinkRoute.tsx +++ b/web/components/routes/link/LinkRoute.tsx @@ -8,11 +8,12 @@ import { parseAsBoolean, useQueryState } from "nuqs" import { atom, useAtom } from "jotai" import { LinkBottomBar } from "./bottom-bar" import { commandPaletteOpenAtom } from "@/components/custom/command-palette/command-palette" +import { useKey } from "react-use" export const isDeleteConfirmShownAtom = atom(false) export function LinkRoute(): React.ReactElement { - const [nuqsEditId] = useQueryState("editId") + const [nuqsEditId, setNuqsEditId] = useQueryState("editId") const [activeItemIndex, setActiveItemIndex] = useState(null) const [isInCreateMode] = useQueryState("create", parseAsBoolean) const [isCommandPaletteOpen] = useAtom(commandPaletteOpenAtom) @@ -50,6 +51,11 @@ export function LinkRoute(): React.ReactElement { } }, [isDeleteConfirmShown, isCommandPaletteOpen, isInCreateMode, handleCommandPaletteClose]) + useKey("Escape", () => { + setDisableEnterKey(false) + setNuqsEditId(null) + }) + return ( <> diff --git a/web/components/routes/link/header.tsx b/web/components/routes/link/header.tsx index 3fa3d863..93d14e7c 100644 --- a/web/components/routes/link/header.tsx +++ b/web/components/routes/link/header.tsx @@ -26,7 +26,7 @@ export const LinkHeader = React.memo(() => { return ( <> - +
diff --git a/web/components/routes/link/list.tsx b/web/components/routes/link/list.tsx index 47f510ca..dfd10cd5 100644 --- a/web/components/routes/link/list.tsx +++ b/web/components/routes/link/list.tsx @@ -24,6 +24,7 @@ import { commandPaletteOpenAtom } from "@/components/custom/command-palette/comm import { useConfirm } from "@omit/react-confirm-dialog" import { useLinkActions } from "./hooks/use-link-actions" import { isDeleteConfirmShownAtom } from "./LinkRoute" +import { useActiveItemScroll } from "@/hooks/use-active-item-scroll" interface LinkListProps { activeItemIndex: number | null @@ -77,12 +78,6 @@ const LinkList: React.FC = ({ activeItemIndex, setActiveItemIndex }) ) - useKey("Escape", () => { - if (editId) { - setEditId(null) - } - }) - useKey( event => (event.metaKey || event.ctrlKey) && event.key === "Backspace", async () => { @@ -245,6 +240,8 @@ const LinkList: React.FC = ({ activeItemIndex, setActiveItemIndex setDraggingId(null) } + const setElementRef = useActiveItemScroll({ activeIndex: activeItemIndex }) + return ( = ({ activeItemIndex, setActiveItemIndex isActive={activeItemIndex === index} setActiveItemIndex={setActiveItemIndex} index={index} + ref={el => setElementRef(el, index)} /> ) )} diff --git a/web/components/routes/link/partials/link-item.tsx b/web/components/routes/link/partials/link-item.tsx index 1718896f..9a430d8b 100644 --- a/web/components/routes/link/partials/link-item.tsx +++ b/web/components/routes/link/partials/link-item.tsx @@ -15,7 +15,7 @@ import { cn, ensureUrlProtocol } from "@/lib/utils" import { LEARNING_STATES, LearningStateValue } from "@/lib/constants" import { linkOpenPopoverForIdAtom } from "@/store/link" -interface LinkItemProps { +interface LinkItemProps extends React.HTMLAttributes { personalLink: PersonalLink disabled?: boolean isEditing: boolean @@ -26,134 +26,138 @@ interface LinkItemProps { index: number } -export const LinkItem: React.FC = ({ - isEditing, - setEditId, - personalLink, - disabled = false, - isDragging, - isActive, - setActiveItemIndex, - index -}) => { - const [openPopoverForId, setOpenPopoverForId] = useAtom(linkOpenPopoverForIdAtom) - const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: personalLink.id, disabled }) +export const LinkItem = React.forwardRef( + ({ personalLink, disabled, isEditing, setEditId, isDragging, isActive, setActiveItemIndex, index }, ref) => { + const [openPopoverForId, setOpenPopoverForId] = useAtom(linkOpenPopoverForIdAtom) + const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: personalLink.id, disabled }) - const style = useMemo( - () => ({ - transform: CSS.Transform.toString(transform), - transition, - pointerEvents: isDragging ? "none" : "auto" - }), - [transform, transition, isDragging] - ) + const style = useMemo( + () => ({ + transform: CSS.Transform.toString(transform), + transition, + pointerEvents: isDragging ? "none" : "auto" + }), + [transform, transition, isDragging] + ) - const handleSuccess = useCallback(() => setEditId(null), [setEditId]) - const handleOnClose = useCallback(() => setEditId(null), [setEditId]) - const handleRowDoubleClick = useCallback(() => setEditId(personalLink.id), [setEditId, personalLink.id]) + const handleSuccess = useCallback(() => setEditId(null), [setEditId]) + const handleOnClose = useCallback(() => setEditId(null), [setEditId]) + const handleRowDoubleClick = useCallback(() => setEditId(personalLink.id), [setEditId, personalLink.id]) - const selectedLearningState = useMemo( - () => LEARNING_STATES.find(ls => ls.value === personalLink.learningState), - [personalLink.learningState] - ) + const selectedLearningState = useMemo( + () => LEARNING_STATES.find(ls => ls.value === personalLink.learningState), + [personalLink.learningState] + ) - const handleLearningStateSelect = useCallback( - (value: string) => { - const learningState = value as LearningStateValue - personalLink.learningState = personalLink.learningState === learningState ? undefined : learningState - setOpenPopoverForId(null) - }, - [personalLink, setOpenPopoverForId] - ) + const handleLearningStateSelect = useCallback( + (value: string) => { + const learningState = value as LearningStateValue + personalLink.learningState = personalLink.learningState === learningState ? undefined : learningState + setOpenPopoverForId(null) + }, + [personalLink, setOpenPopoverForId] + ) - if (isEditing) { - return {}} /> - } + if (isEditing) { + return ( + {}} /> + ) + } - return ( -
  • setActiveItemIndex(index)} - onBlur={() => setActiveItemIndex(null)} - className={cn( - "relative cursor-default outline-none", - "mx-auto grid w-[98%] grid-cols-[auto_1fr_auto] items-center gap-x-2 rounded-lg p-2", - { - "bg-muted-foreground/5": isActive, - "hover:bg-muted/50": !isActive - } - )} - onDoubleClick={handleRowDoubleClick} - > - setOpenPopoverForId(open ? personalLink.id : null)} - > - - - - e.preventDefault()} - > - - - - -
    - {personalLink.icon && ( - {personalLink.title} + return ( +
  • { + setNodeRef(node) + if (typeof ref === "function") { + ref(node) + } else if (ref) { + ref.current = node + } + }} + style={style as React.CSSProperties} + {...attributes} + {...listeners} + tabIndex={0} + onFocus={() => setActiveItemIndex(index)} + onBlur={() => setActiveItemIndex(null)} + className={cn( + "relative cursor-default outline-none", + "grid grid-cols-[auto_1fr_auto] items-center gap-x-2 py-2 max-lg:px-4 sm:px-5 sm:py-2", + { + "bg-muted-foreground/5": isActive, + "hover:bg-muted/50": !isActive + } )} + onDoubleClick={handleRowDoubleClick} + > + setOpenPopoverForId(open ? personalLink.id : null)} + > + + + + e.preventDefault()} + > + + + +
    -

    {personalLink.title}

    - {personalLink.url && ( -
    -
    + {personalLink.icon && ( + {personalLink.title} + )} +
    +

    {personalLink.title}

    + {personalLink.url && ( +
    +
    + )} +
    +
    + +
    + {personalLink.topic && ( + + {personalLink.topic.prettyName} + )}
    -
  • + + ) + } +) -
    - {personalLink.topic && ( - - {personalLink.topic.prettyName} - - )} -
    - - ) -} +LinkItem.displayName = "LinkItem" diff --git a/web/hooks/use-active-item-scroll.ts b/web/hooks/use-active-item-scroll.ts new file mode 100644 index 00000000..81f01fee --- /dev/null +++ b/web/hooks/use-active-item-scroll.ts @@ -0,0 +1,30 @@ +import { useEffect, useRef, useCallback } from "react" + +type ElementRef = T | null +type ElementRefs = ElementRef[] + +interface ActiveItemScrollOptions { + activeIndex: number | null +} + +export function useActiveItemScroll(options: ActiveItemScrollOptions) { + const { activeIndex } = options + const elementRefs = useRef>([]) + + const scrollActiveElementIntoView = useCallback((index: number) => { + const activeElement = elementRefs.current[index] + activeElement?.scrollIntoView({ block: "nearest" }) + }, []) + + useEffect(() => { + if (activeIndex !== null) { + scrollActiveElementIntoView(activeIndex) + } + }, [activeIndex, scrollActiveElementIntoView]) + + const setElementRef = useCallback((element: ElementRef, index: number) => { + elementRefs.current[index] = element + }, []) + + return setElementRef +}