mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
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
This commit is contained in:
@@ -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<HTMLDivElement | null>(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])
|
||||
|
||||
@@ -21,7 +21,8 @@ export function LinkRoute() {
|
||||
<div className="flex h-full flex-auto flex-col overflow-hidden">
|
||||
<LinkHeader />
|
||||
<LinkManage />
|
||||
<LinkList />
|
||||
{/* Refresh list everytime editId is changed */}
|
||||
<LinkList key={nuqsEditId} />
|
||||
<LinkBottomBar />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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<LinkListProps> = () => {
|
||||
const [isCommandPalettePpen] = useAtom(commandPaletteOpenAtom)
|
||||
const [editId, setEditId] = useQueryState("editId")
|
||||
const [activeLearningState] = useAtom(learningStateAtom)
|
||||
const [activeItemIndex, setActiveItemIndex] = useState<number | null>(null)
|
||||
|
||||
const { me } = useAccount({
|
||||
root: { personalLinks: [] }
|
||||
@@ -34,9 +36,7 @@ const LinkList: React.FC<LinkListProps> = () => {
|
||||
const personalLinks = useMemo(() => me?.root?.personalLinks || [], [me?.root?.personalLinks])
|
||||
|
||||
const [sort] = useAtom(linkSortAtom)
|
||||
const [focusedId, setFocusedId] = useState<string | null>(null)
|
||||
const [draggingId, setDraggingId] = useState<UniqueIdentifier | null>(null)
|
||||
const linkRefs = useRef<{ [key: string]: HTMLLIElement | null }>({})
|
||||
|
||||
const filteredLinks = useMemo(
|
||||
() =>
|
||||
@@ -67,10 +67,6 @@ const LinkList: React.FC<LinkListProps> = () => {
|
||||
})
|
||||
)
|
||||
|
||||
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<LinkListProps> = () => {
|
||||
|
||||
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<LinkListProps> = () => {
|
||||
})
|
||||
|
||||
updateSequences(me.root.personalLinks)
|
||||
setActiveItemIndex(newIndex)
|
||||
} catch (error) {
|
||||
console.error("Error during link reordering:", error)
|
||||
}
|
||||
@@ -194,7 +174,10 @@ const LinkList: React.FC<LinkListProps> = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-14 flex w-full flex-1 flex-col overflow-y-auto [scrollbar-gutter:stable]">
|
||||
<Primitive.div
|
||||
className="mb-14 flex w-full flex-1 flex-col overflow-y-auto outline-none [scrollbar-gutter:stable]"
|
||||
tabIndex={0}
|
||||
>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
@@ -204,7 +187,7 @@ const LinkList: React.FC<LinkListProps> = () => {
|
||||
<SortableContext items={sortedLinks.map(item => item?.id || "") || []} strategy={verticalListSortingStrategy}>
|
||||
<ul role="list" className="divide-primary/5 divide-y">
|
||||
{sortedLinks.map(
|
||||
linkItem =>
|
||||
(linkItem, index) =>
|
||||
linkItem && (
|
||||
<LinkItem
|
||||
key={linkItem.id}
|
||||
@@ -212,17 +195,17 @@ const LinkList: React.FC<LinkListProps> = () => {
|
||||
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}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
</Primitive.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<LinkItemProps> = ({
|
||||
@@ -34,9 +32,9 @@ export const LinkItem: React.FC<LinkItemProps> = ({
|
||||
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<LinkItemProps> = ({
|
||||
[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<LinkItemProps> = ({
|
||||
|
||||
return (
|
||||
<li
|
||||
ref={refCallback}
|
||||
ref={setNodeRef}
|
||||
style={style as React.CSSProperties}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
tabIndex={0}
|
||||
onFocus={() => 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}
|
||||
>
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user