mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
fix(page): improve keybind (#180)
This commit is contained in:
@@ -258,7 +258,6 @@ const LinkList: React.FC<LinkListProps> = () => {
|
||||
setEditId(null)
|
||||
setActiveItemIndex(lastActiveIndexRef.current)
|
||||
setKeyboardActiveIndex(lastActiveIndexRef.current)
|
||||
console.log(keyboardActiveIndex)
|
||||
}}
|
||||
index={index}
|
||||
onItemSelected={link => setEditId(link.id)}
|
||||
|
||||
@@ -87,7 +87,10 @@ export const LinkItem = React.forwardRef<HTMLDivElement, LinkItemProps>(
|
||||
aria-selected={isActive}
|
||||
data-disabled={disabled}
|
||||
data-active={isActive}
|
||||
className="w-full overflow-visible border-b-[0.5px] border-transparent outline-none data-[active='true']:bg-[var(--link-background-muted)] data-[keyboard-active='true']:focus-visible:shadow-[var(--link-shadow)_0px_0px_0px_1px_inset]"
|
||||
className={cn(
|
||||
"w-full overflow-visible border-b-[0.5px] border-transparent outline-none",
|
||||
"data-[active='true']:bg-[var(--link-background-muted)] data-[keyboard-active='true']:focus-visible:shadow-[var(--link-shadow)_0px_0px_0px_1px_inset]"
|
||||
)}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -1,35 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { PageHeader } from "./header"
|
||||
import { PageList } from "./list"
|
||||
import { useAtom } from "jotai"
|
||||
import { commandPaletteOpenAtom } from "@/components/custom/command-palette/command-palette"
|
||||
|
||||
export function PageRoute() {
|
||||
const [activeItemIndex, setActiveItemIndex] = useState<number | null>(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 (
|
||||
<div className="flex h-full flex-auto flex-col overflow-hidden">
|
||||
<PageHeader />
|
||||
<PageList
|
||||
activeItemIndex={activeItemIndex}
|
||||
setActiveItemIndex={setActiveItemIndex}
|
||||
disableEnterKey={disableEnterKey}
|
||||
/>
|
||||
<PageList />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,66 +1,71 @@
|
||||
import React, { useMemo, useCallback, useEffect } from "react"
|
||||
import * as React from "react"
|
||||
import { Primitive } from "@radix-ui/react-primitive"
|
||||
import { useAccount } from "@/lib/providers/jazz-provider"
|
||||
import { useAtom } from "jotai"
|
||||
import { commandPaletteOpenAtom } from "@/components/custom/command-palette/command-palette"
|
||||
import { PageItem } from "./partials/page-item"
|
||||
import { useMedia } from "@/hooks/use-media"
|
||||
import { useColumnStyles } from "./hooks/use-column-styles"
|
||||
import { PersonalPage, PersonalPageLists } from "@/lib/schema"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useActiveItemScroll } from "@/hooks/use-active-item-scroll"
|
||||
import { Column } from "@/components/custom/column"
|
||||
import { useKeyDown } from "@/hooks/use-key-down"
|
||||
|
||||
interface PageListProps {
|
||||
activeItemIndex: number | null
|
||||
setActiveItemIndex: React.Dispatch<React.SetStateAction<number | null>>
|
||||
disableEnterKey: boolean
|
||||
}
|
||||
interface PageListProps {}
|
||||
|
||||
export const PageList: React.FC<PageListProps> = ({ activeItemIndex, setActiveItemIndex, disableEnterKey }) => {
|
||||
export const PageList: React.FC<PageListProps> = () => {
|
||||
const isTablet = useMedia("(max-width: 640px)")
|
||||
const [isCommandPaletteOpen] = useAtom(commandPaletteOpenAtom)
|
||||
const { me } = useAccount({ root: { personalPages: [] } })
|
||||
const personalPages = useMemo(() => me?.root?.personalPages, [me?.root?.personalPages])
|
||||
const router = useRouter()
|
||||
const itemCount = personalPages?.length || 0
|
||||
const [activeItemIndex, setActiveItemIndex] = React.useState<number | null>(null)
|
||||
const [keyboardActiveIndex, setKeyboardActiveIndex] = React.useState<number | null>(null)
|
||||
const personalPages = React.useMemo(() => me?.root?.personalPages, [me?.root?.personalPages])
|
||||
|
||||
const handleEnter = useCallback(
|
||||
(selectedPage: PersonalPage) => {
|
||||
router.push(`/pages/${selectedPage.id}`)
|
||||
},
|
||||
[router]
|
||||
)
|
||||
const next = () => Math.min((activeItemIndex ?? 0) + 1, (personalPages?.length ?? 0) - 1)
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (isCommandPaletteOpen) return
|
||||
const prev = () => Math.max((activeItemIndex ?? 0) - 1, 0)
|
||||
|
||||
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 && personalPages) {
|
||||
e.preventDefault()
|
||||
const selectedPage = personalPages[activeItemIndex]
|
||||
if (selectedPage) handleEnter?.(selectedPage)
|
||||
}
|
||||
},
|
||||
[itemCount, isCommandPaletteOpen, activeItemIndex, setActiveItemIndex, disableEnterKey, personalPages, handleEnter]
|
||||
)
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||
}, [handleKeyDown])
|
||||
useKeyDown(() => true, handleKeyDown)
|
||||
|
||||
const { setElementRef } = useActiveItemScroll<HTMLAnchorElement>({ activeIndex: keyboardActiveIndex })
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col overflow-hidden border-t">
|
||||
{!isTablet && <ColumnHeader />}
|
||||
<PageListItems personalPages={personalPages} activeItemIndex={activeItemIndex} />
|
||||
<Primitive.div
|
||||
className="divide-primary/5 flex flex-1 flex-col divide-y overflow-y-auto outline-none [scrollbar-gutter:stable]"
|
||||
tabIndex={-1}
|
||||
role="list"
|
||||
>
|
||||
{personalPages?.map(
|
||||
(page, index) =>
|
||||
page?.id && (
|
||||
<PageItem
|
||||
key={page.id}
|
||||
ref={el => setElementRef(el, index)}
|
||||
page={page}
|
||||
isActive={index === activeItemIndex}
|
||||
onPointerMove={() => {
|
||||
setKeyboardActiveIndex(null)
|
||||
setActiveItemIndex(index)
|
||||
}}
|
||||
data-keyboard-active={keyboardActiveIndex === index}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Primitive.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -82,32 +87,3 @@ export const ColumnHeader: React.FC = () => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface PageListItemsProps {
|
||||
personalPages?: PersonalPageLists | null
|
||||
activeItemIndex: number | null
|
||||
}
|
||||
|
||||
const PageListItems: React.FC<PageListItemsProps> = ({ personalPages, activeItemIndex }) => {
|
||||
const { setElementRef } = useActiveItemScroll<HTMLAnchorElement>({ activeIndex: activeItemIndex })
|
||||
|
||||
return (
|
||||
<Primitive.div
|
||||
className="divide-primary/5 flex flex-1 flex-col divide-y overflow-y-auto outline-none [scrollbar-gutter:stable]"
|
||||
tabIndex={-1}
|
||||
role="list"
|
||||
>
|
||||
{personalPages?.map(
|
||||
(page, index) =>
|
||||
page?.id && (
|
||||
<PageItem
|
||||
key={page.id}
|
||||
ref={el => setElementRef(el, index)}
|
||||
page={page}
|
||||
isActive={index === activeItemIndex}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Primitive.div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,26 +7,43 @@ import { useMedia } from "@/hooks/use-media"
|
||||
import { useColumnStyles } from "../hooks/use-column-styles"
|
||||
import { format } from "date-fns"
|
||||
import { Column } from "@/components/custom/column"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
interface PageItemProps {
|
||||
interface PageItemProps extends React.HTMLAttributes<HTMLAnchorElement> {
|
||||
page: PersonalPage
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
export const PageItem = React.forwardRef<HTMLAnchorElement, PageItemProps>(({ page, isActive }, ref) => {
|
||||
export const PageItem = React.forwardRef<HTMLAnchorElement, PageItemProps>(({ page, isActive, ...props }, ref) => {
|
||||
const isTablet = useMedia("(max-width: 640px)")
|
||||
const columnStyles = useColumnStyles()
|
||||
const router = useRouter()
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(ev: React.KeyboardEvent<HTMLAnchorElement>) => {
|
||||
if (ev.key === "Enter") {
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
router.push(`/pages/${page.id}`)
|
||||
}
|
||||
},
|
||||
[router, page.id]
|
||||
)
|
||||
|
||||
return (
|
||||
<Link
|
||||
ref={ref}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
className={cn("relative block cursor-default outline-none", "min-h-12 py-2 max-lg:px-4 sm:px-6", {
|
||||
"bg-muted-foreground/5": isActive,
|
||||
"hover:bg-muted/50": !isActive
|
||||
})}
|
||||
className={cn(
|
||||
"relative block cursor-default outline-none",
|
||||
"min-h-12 py-2 max-lg:px-4 sm:px-6",
|
||||
"data-[active='true']:bg-[var(--link-background-muted)] data-[keyboard-active='true']:focus-visible:shadow-[var(--link-shadow)_0px_0px_0px_1px_inset]"
|
||||
)}
|
||||
href={`/pages/${page.id}`}
|
||||
role="listitem"
|
||||
aria-selected={isActive}
|
||||
data-active={isActive}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex h-full items-center gap-4">
|
||||
<Column.Wrapper style={columnStyles.title}>
|
||||
|
||||
Reference in New Issue
Block a user