fix(page): improve keybind (#180)

This commit is contained in:
Aslam
2024-09-24 18:55:40 +07:00
committed by GitHub
parent 867478d55c
commit cffe65ec5f
5 changed files with 78 additions and 105 deletions

View File

@@ -258,7 +258,6 @@ const LinkList: React.FC<LinkListProps> = () => {
setEditId(null) setEditId(null)
setActiveItemIndex(lastActiveIndexRef.current) setActiveItemIndex(lastActiveIndexRef.current)
setKeyboardActiveIndex(lastActiveIndexRef.current) setKeyboardActiveIndex(lastActiveIndexRef.current)
console.log(keyboardActiveIndex)
}} }}
index={index} index={index}
onItemSelected={link => setEditId(link.id)} onItemSelected={link => setEditId(link.id)}

View File

@@ -87,7 +87,10 @@ export const LinkItem = React.forwardRef<HTMLDivElement, LinkItemProps>(
aria-selected={isActive} aria-selected={isActive}
data-disabled={disabled} data-disabled={disabled}
data-active={isActive} 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} onKeyDown={handleKeyDown}
> >
<div <div

View File

@@ -1,35 +1,13 @@
"use client" "use client"
import { useCallback, useEffect, useState } from "react"
import { PageHeader } from "./header" import { PageHeader } from "./header"
import { PageList } from "./list" import { PageList } from "./list"
import { useAtom } from "jotai"
import { commandPaletteOpenAtom } from "@/components/custom/command-palette/command-palette"
export function PageRoute() { 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 ( return (
<div className="flex h-full flex-auto flex-col overflow-hidden"> <div className="flex h-full flex-auto flex-col overflow-hidden">
<PageHeader /> <PageHeader />
<PageList <PageList />
activeItemIndex={activeItemIndex}
setActiveItemIndex={setActiveItemIndex}
disableEnterKey={disableEnterKey}
/>
</div> </div>
) )
} }

View File

@@ -1,66 +1,71 @@
import React, { useMemo, useCallback, useEffect } from "react" import * as React from "react"
import { Primitive } from "@radix-ui/react-primitive" import { Primitive } from "@radix-ui/react-primitive"
import { useAccount } from "@/lib/providers/jazz-provider" 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 { PageItem } from "./partials/page-item"
import { useMedia } from "@/hooks/use-media" import { useMedia } from "@/hooks/use-media"
import { useColumnStyles } from "./hooks/use-column-styles" 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 { useActiveItemScroll } from "@/hooks/use-active-item-scroll"
import { Column } from "@/components/custom/column" import { Column } from "@/components/custom/column"
import { useKeyDown } from "@/hooks/use-key-down"
interface PageListProps { interface PageListProps {}
activeItemIndex: number | null
setActiveItemIndex: React.Dispatch<React.SetStateAction<number | null>>
disableEnterKey: boolean
}
export const PageList: React.FC<PageListProps> = ({ activeItemIndex, setActiveItemIndex, disableEnterKey }) => { export const PageList: React.FC<PageListProps> = () => {
const isTablet = useMedia("(max-width: 640px)") const isTablet = useMedia("(max-width: 640px)")
const [isCommandPaletteOpen] = useAtom(commandPaletteOpenAtom)
const { me } = useAccount({ root: { personalPages: [] } }) const { me } = useAccount({ root: { personalPages: [] } })
const personalPages = useMemo(() => me?.root?.personalPages, [me?.root?.personalPages]) const [activeItemIndex, setActiveItemIndex] = React.useState<number | null>(null)
const router = useRouter() const [keyboardActiveIndex, setKeyboardActiveIndex] = React.useState<number | null>(null)
const itemCount = personalPages?.length || 0 const personalPages = React.useMemo(() => me?.root?.personalPages, [me?.root?.personalPages])
const handleEnter = useCallback( const next = () => Math.min((activeItemIndex ?? 0) + 1, (personalPages?.length ?? 0) - 1)
(selectedPage: PersonalPage) => {
router.push(`/pages/${selectedPage.id}`)
},
[router]
)
const handleKeyDown = useCallback( const prev = () => Math.max((activeItemIndex ?? 0) - 1, 0)
(e: KeyboardEvent) => {
if (isCommandPaletteOpen) return
if (e.key === "ArrowUp" || e.key === "ArrowDown") { const handleKeyDown = (ev: KeyboardEvent) => {
e.preventDefault() switch (ev.key) {
setActiveItemIndex(prevIndex => { case "ArrowDown":
if (prevIndex === null) return 0 ev.preventDefault()
const newIndex = e.key === "ArrowUp" ? (prevIndex - 1 + itemCount) % itemCount : (prevIndex + 1) % itemCount ev.stopPropagation()
return newIndex setActiveItemIndex(next())
}) setKeyboardActiveIndex(next())
} else if (e.key === "Enter" && !disableEnterKey && activeItemIndex !== null && personalPages) { break
e.preventDefault() case "ArrowUp":
const selectedPage = personalPages[activeItemIndex] ev.preventDefault()
if (selectedPage) handleEnter?.(selectedPage) ev.stopPropagation()
} setActiveItemIndex(prev())
}, setKeyboardActiveIndex(prev())
[itemCount, isCommandPaletteOpen, activeItemIndex, setActiveItemIndex, disableEnterKey, personalPages, handleEnter] }
) }
useEffect(() => { useKeyDown(() => true, handleKeyDown)
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown) const { setElementRef } = useActiveItemScroll<HTMLAnchorElement>({ activeIndex: keyboardActiveIndex })
}, [handleKeyDown])
return ( return (
<div className="flex h-full w-full flex-col overflow-hidden border-t"> <div className="flex h-full w-full flex-col overflow-hidden border-t">
{!isTablet && <ColumnHeader />} {!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> </div>
) )
} }
@@ -82,32 +87,3 @@ export const ColumnHeader: React.FC = () => {
</div> </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>
)
}

View File

@@ -7,26 +7,43 @@ import { useMedia } from "@/hooks/use-media"
import { useColumnStyles } from "../hooks/use-column-styles" import { useColumnStyles } from "../hooks/use-column-styles"
import { format } from "date-fns" import { format } from "date-fns"
import { Column } from "@/components/custom/column" import { Column } from "@/components/custom/column"
import { useRouter } from "next/navigation"
interface PageItemProps { interface PageItemProps extends React.HTMLAttributes<HTMLAnchorElement> {
page: PersonalPage page: PersonalPage
isActive: boolean 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 isTablet = useMedia("(max-width: 640px)")
const columnStyles = useColumnStyles() 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 ( return (
<Link <Link
ref={ref} ref={ref}
tabIndex={isActive ? 0 : -1} tabIndex={isActive ? 0 : -1}
className={cn("relative block cursor-default outline-none", "min-h-12 py-2 max-lg:px-4 sm:px-6", { className={cn(
"bg-muted-foreground/5": isActive, "relative block cursor-default outline-none",
"hover:bg-muted/50": !isActive "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}`} href={`/pages/${page.id}`}
role="listitem" aria-selected={isActive}
data-active={isActive}
onKeyDown={handleKeyDown}
{...props}
> >
<div className="flex h-full items-center gap-4"> <div className="flex h-full items-center gap-4">
<Column.Wrapper style={columnStyles.title}> <Column.Wrapper style={columnStyles.title}>