From 1cd606376880c74cb7c900961f2fac114ede24c1 Mon Sep 17 00:00:00 2001 From: Aslam Date: Sun, 8 Sep 2024 08:32:10 +0700 Subject: [PATCH] chore: improve link accesibility and keybind (#153) * fix(topic): handleSelectLearningState missing depth * fix(link): use active index instead of native focus * chore(palette): use atom for maintain state * chore(link): prevent keydown if command palette active --- .../command-palette/command-palette.tsx | 9 +- web/components/routes/link/LinkRoute.tsx | 3 +- web/components/routes/link/list.tsx | 89 ++++++++----------- .../routes/link/partials/link-item.tsx | 34 +++---- .../topics/detail/partials/link-item.tsx | 2 +- 5 files changed, 57 insertions(+), 80 deletions(-) diff --git a/web/components/custom/command-palette/command-palette.tsx b/web/components/custom/command-palette/command-palette.tsx index 4986d557..e6a45aa9 100644 --- a/web/components/custom/command-palette/command-palette.tsx +++ b/web/components/custom/command-palette/command-palette.tsx @@ -10,18 +10,21 @@ import { useAccount } from "@/lib/providers/jazz-provider" import { searchSafeRegExp, toTitleCase } from "@/lib/utils" import { GraphNode } from "@/components/routes/public/PublicHomeRoute" import { useCommandActions } from "./hooks/use-command-actions" +import { atom, useAtom } from "jotai" let graph_data_promise = import("@/components/routes/public/graph-data.json").then(a => a.default) const filterItems = (items: CommandItemType[], searchRegex: RegExp) => items.filter(item => searchRegex.test(item.value)).slice(0, 6) +export const commandPaletteOpenAtom = atom(false) + export function CommandPalette() { const { me } = useAccount({ root: { personalLinks: [], personalPages: [] } }) const dialogRef = React.useRef(null) const [inputValue, setInputValue] = React.useState("") const [activePage, setActivePage] = React.useState("home") - const [open, setOpen] = React.useState(false) + const [open, setOpen] = useAtom(commandPaletteOpenAtom) const actions = useCommandActions() const commandGroups = React.useMemo(() => me && createCommandGroups(actions, me), [actions, me]) @@ -38,7 +41,7 @@ export function CommandPalette() { document.addEventListener("keydown", down) return () => document.removeEventListener("keydown", down) - }, []) + }, [setOpen]) const bounce = React.useCallback(() => { if (dialogRef.current) { @@ -177,7 +180,7 @@ export function CommandPalette() { closeDialog() } }, - [bounce] + [bounce, setOpen] ) const filteredCommands = React.useMemo(() => getFilteredCommands(), [getFilteredCommands]) diff --git a/web/components/routes/link/LinkRoute.tsx b/web/components/routes/link/LinkRoute.tsx index 1505b45d..32e3605d 100644 --- a/web/components/routes/link/LinkRoute.tsx +++ b/web/components/routes/link/LinkRoute.tsx @@ -21,7 +21,8 @@ export function LinkRoute() {
- + {/* Refresh list everytime editId is changed */} +
) diff --git a/web/components/routes/link/list.tsx b/web/components/routes/link/list.tsx index 0173a8cb..b9b58350 100644 --- a/web/components/routes/link/list.tsx +++ b/web/components/routes/link/list.tsx @@ -1,5 +1,4 @@ -"use client" - +import React, { useCallback, useEffect, useMemo, useState } from "react" import { DndContext, closestCenter, @@ -11,6 +10,7 @@ import { DragStartEvent, UniqueIdentifier } from "@dnd-kit/core" +import { Primitive } from "@radix-ui/react-primitive" import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy } from "@dnd-kit/sortable" import { useAccount } from "@/lib/providers/jazz-provider" import { PersonalLinkLists } from "@/lib/schema/personal-link" @@ -18,15 +18,17 @@ import { useAtom } from "jotai" import { linkSortAtom } from "@/store/link" import { useKey } from "react-use" import { LinkItem } from "./partials/link-item" -import { useRef, useState, useCallback, useEffect, useMemo } from "react" -import { learningStateAtom } from "./header" import { useQueryState } from "nuqs" +import { learningStateAtom } from "./header" +import { commandPaletteOpenAtom } from "@/components/custom/command-palette/command-palette" interface LinkListProps {} const LinkList: React.FC = () => { + const [isCommandPalettePpen] = useAtom(commandPaletteOpenAtom) const [editId, setEditId] = useQueryState("editId") const [activeLearningState] = useAtom(learningStateAtom) + const [activeItemIndex, setActiveItemIndex] = useState(null) const { me } = useAccount({ root: { personalLinks: [] } @@ -34,9 +36,7 @@ const LinkList: React.FC = () => { const personalLinks = useMemo(() => me?.root?.personalLinks || [], [me?.root?.personalLinks]) const [sort] = useAtom(linkSortAtom) - const [focusedId, setFocusedId] = useState(null) const [draggingId, setDraggingId] = useState(null) - const linkRefs = useRef<{ [key: string]: HTMLLIElement | null }>({}) const filteredLinks = useMemo( () => @@ -67,10 +67,6 @@ const LinkList: React.FC = () => { }) ) - const registerRef = useCallback((id: string, ref: HTMLLIElement | null) => { - linkRefs.current[id] = ref - }, []) - useKey("Escape", () => { if (editId) { setEditId(null) @@ -87,57 +83,40 @@ const LinkList: React.FC = () => { useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if (!me?.root?.personalLinks || sortedLinks.length === 0 || editId !== null) return - - const currentIndex = sortedLinks.findIndex(link => link?.id === focusedId) + if (isCommandPalettePpen || !me?.root?.personalLinks || sortedLinks.length === 0 || editId !== null) return if (e.key === "ArrowUp" || e.key === "ArrowDown") { e.preventDefault() - const newIndex = - e.key === "ArrowUp" ? Math.max(0, currentIndex - 1) : Math.min(sortedLinks.length - 1, currentIndex + 1) + setActiveItemIndex(prevIndex => { + if (prevIndex === null) return 0 + const newIndex = + e.key === "ArrowUp" ? Math.max(0, prevIndex - 1) : Math.min(sortedLinks.length - 1, prevIndex + 1) - if (e.metaKey && sort === "manual") { - const currentLink = me.root.personalLinks[currentIndex] - if (!currentLink) return + if (e.metaKey && sort === "manual") { + const linksArray = [...me.root.personalLinks] + const newLinks = arrayMove(linksArray, prevIndex, newIndex) - const linksArray = [...me.root.personalLinks] - const newLinks = arrayMove(linksArray, currentIndex, newIndex) - - while (me.root.personalLinks.length > 0) { - me.root.personalLinks.pop() - } - - newLinks.forEach(link => { - if (link) { - me.root.personalLinks.push(link) + while (me.root.personalLinks.length > 0) { + me.root.personalLinks.pop() } - }) - updateSequences(me.root.personalLinks) - - const newFocusedLink = me.root.personalLinks[newIndex] - if (newFocusedLink) { - setFocusedId(newFocusedLink.id) - - requestAnimationFrame(() => { - linkRefs.current[newFocusedLink.id]?.focus() + newLinks.forEach(link => { + if (link) { + me.root.personalLinks.push(link) + } }) + + updateSequences(me.root.personalLinks) } - } else { - const newFocusedLink = sortedLinks[newIndex] - if (newFocusedLink) { - setFocusedId(newFocusedLink.id) - requestAnimationFrame(() => { - linkRefs.current[newFocusedLink.id]?.focus() - }) - } - } + + return newIndex + }) } } window.addEventListener("keydown", handleKeyDown) return () => window.removeEventListener("keydown", handleKeyDown) - }, [me?.root?.personalLinks, sortedLinks, focusedId, editId, sort, updateSequences]) + }, [me?.root?.personalLinks, sortedLinks, editId, sort, updateSequences, isCommandPalettePpen]) const handleDragStart = useCallback( (event: DragStartEvent) => { @@ -185,6 +164,7 @@ const LinkList: React.FC = () => { }) updateSequences(me.root.personalLinks) + setActiveItemIndex(newIndex) } catch (error) { console.error("Error during link reordering:", error) } @@ -194,7 +174,10 @@ const LinkList: React.FC = () => { } return ( -
+ = () => { item?.id || "") || []} strategy={verticalListSortingStrategy}>
    {sortedLinks.map( - linkItem => + (linkItem, index) => linkItem && ( = () => { setEditId={setEditId} personalLink={linkItem} disabled={sort !== "manual" || editId !== null} - registerRef={registerRef} isDragging={draggingId === linkItem.id} - isFocused={focusedId === linkItem.id} - setFocusedId={setFocusedId} + isActive={activeItemIndex === index} + setActiveItemIndex={setActiveItemIndex} + index={index} /> ) )}
-
+ ) } diff --git a/web/components/routes/link/partials/link-item.tsx b/web/components/routes/link/partials/link-item.tsx index 6f1a0a09..2c8c6901 100644 --- a/web/components/routes/link/partials/link-item.tsx +++ b/web/components/routes/link/partials/link-item.tsx @@ -1,5 +1,3 @@ -"use client" - import React, { useCallback, useMemo } from "react" import Image from "next/image" import Link from "next/link" @@ -15,7 +13,7 @@ import { PersonalLink } from "@/lib/schema/personal-link" import { LinkForm } from "./form/link-form" import { cn } from "@/lib/utils" import { LEARNING_STATES, LearningStateValue } from "@/lib/constants" -import { linkOpenPopoverForIdAtom, linkShowCreateAtom } from "@/store/link" +import { linkOpenPopoverForIdAtom } from "@/store/link" interface LinkItemProps { personalLink: PersonalLink @@ -23,9 +21,9 @@ interface LinkItemProps { isEditing: boolean setEditId: (id: string | null) => void isDragging: boolean - isFocused: boolean - setFocusedId: (id: string | null) => void - registerRef: (id: string, ref: HTMLLIElement | null) => void + isActive: boolean + setActiveItemIndex: (index: number | null) => void + index: number } export const LinkItem: React.FC = ({ @@ -34,9 +32,9 @@ export const LinkItem: React.FC = ({ personalLink, disabled = false, isDragging, - isFocused, - setFocusedId, - registerRef + isActive, + setActiveItemIndex, + index }) => { const [openPopoverForId, setOpenPopoverForId] = useAtom(linkOpenPopoverForIdAtom) const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: personalLink.id, disabled }) @@ -50,14 +48,6 @@ export const LinkItem: React.FC = ({ [transform, transition, isDragging] ) - const refCallback = useCallback( - (node: HTMLLIElement | null) => { - setNodeRef(node) - registerRef(personalLink.id, node) - }, - [setNodeRef, registerRef, personalLink.id] - ) - const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "Enter") { @@ -92,17 +82,17 @@ export const LinkItem: React.FC = ({ return (
  • setFocusedId(personalLink.id)} - onBlur={() => setFocusedId(null)} + onFocus={() => setActiveItemIndex(index)} + onBlur={() => setActiveItemIndex(null)} onKeyDown={handleKeyDown} className={cn("relative flex h-14 cursor-default items-center outline-none xl:h-11", { - "bg-muted-foreground/10": isFocused, - "hover:bg-muted/50": !isFocused + "bg-muted-foreground/10": isActive, + "hover:bg-muted/50": !isActive })} onDoubleClick={handleRowDoubleClick} > diff --git a/web/components/routes/topics/detail/partials/link-item.tsx b/web/components/routes/topics/detail/partials/link-item.tsx index 24baeb69..8273b86c 100644 --- a/web/components/routes/topics/detail/partials/link-item.tsx +++ b/web/components/routes/topics/detail/partials/link-item.tsx @@ -110,7 +110,7 @@ export const LinkItem = React.memo( setOpenPopoverForId(null) setIsPopoverOpen(false) }, - [personalLink, personalLinks, me, link, router, setOpenPopoverForId, topic] + [personalLink, personalLinks, me, link, router, setOpenPopoverForId, topic, clerk, pathname] ) const handlePopoverOpenChange = useCallback(