feat: enhance hotkey functionality and command structure
- Added new hotkey scope for sub-layer functionality. - Introduced `useCommandBinding` hook to streamline command binding. - Updated command IDs to include focus to entry render functionality. - Refactored hotkey registration to improve clarity and maintainability. - Adjusted event handling for better focus management in the global hotkeys provider. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
596694c610
commit
243cd37a88
|
|
@ -8,4 +8,5 @@ export enum HotkeyScope {
|
|||
Timeline = "timeline",
|
||||
EntryRender = "entry-render",
|
||||
SubscriptionList = "subscription-list",
|
||||
SubLayer = "sub-layer",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ import { EnvironmentIndicator } from "~/modules/app/EnvironmentIndicator"
|
|||
import { NetworkStatusIndicator } from "~/modules/app/NetworkStatusIndicator"
|
||||
import { LoginModalContent } from "~/modules/auth/LoginModalContent"
|
||||
import { COMMAND_ID } from "~/modules/command/commands/id"
|
||||
import { useCommandBinding } from "~/modules/command/hooks/use-register-hotkey"
|
||||
import { useCommandBinding } from "~/modules/command/hooks/use-command-binding"
|
||||
import { DebugRegistry } from "~/modules/debug/registry"
|
||||
import { CmdF } from "~/modules/panel/cmdf"
|
||||
import { SearchCmdK } from "~/modules/panel/cmdk"
|
||||
|
|
|
|||
|
|
@ -6,10 +6,13 @@ import { ELECTRON_BUILD } from "@follow/shared/constants"
|
|||
import { springScrollTo } from "@follow/utils/scroller"
|
||||
import { cn, getOS } from "@follow/utils/utils"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { useHotkeys } from "react-hotkeys-hook"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { NavigationType, Outlet, useLocation, useNavigate, useNavigationType } from "react-router"
|
||||
|
||||
import { FABContainer, FABPortable } from "~/components/ui/fab"
|
||||
import { HotkeyScope } from "~/constants"
|
||||
import { useConditionalHotkeyScope } from "~/hooks/common"
|
||||
|
||||
import { useSubViewTitleValue } from "./hooks"
|
||||
|
||||
|
|
@ -53,6 +56,16 @@ export function SubviewLayout() {
|
|||
// electron window has pt-[calc(var(--fo-window-padding-top)_-10px)]
|
||||
const isElectronWindows = ELECTRON_BUILD && getOS() === "Windows"
|
||||
|
||||
useConditionalHotkeyScope(HotkeyScope.SubLayer, true)
|
||||
|
||||
const backHandler = () => {
|
||||
if (prevLocation.pathname === location.pathname) {
|
||||
navigate({ pathname: "" })
|
||||
} else {
|
||||
navigate(prevLocation)
|
||||
}
|
||||
}
|
||||
useHotkeys("esc", backHandler)
|
||||
return (
|
||||
<div className="relative flex size-full">
|
||||
<div
|
||||
|
|
@ -64,13 +77,7 @@ export function SubviewLayout() {
|
|||
)}
|
||||
>
|
||||
<MotionButtonBase
|
||||
onClick={() => {
|
||||
if (prevLocation.pathname === location.pathname) {
|
||||
navigate({ pathname: "" })
|
||||
} else {
|
||||
navigate(prevLocation)
|
||||
}
|
||||
}}
|
||||
onClick={backHandler}
|
||||
className="no-drag-region hover:text-accent inline-flex items-center gap-1 duration-200"
|
||||
>
|
||||
<i className="i-mingcute-left-line" />
|
||||
|
|
|
|||
|
|
@ -48,12 +48,12 @@ export const COMMAND_ID = {
|
|||
toggleTimelineColumn: "layout:toggle-timeline-column",
|
||||
focusToTimeline: "layout:focus-to-timeline",
|
||||
focusToSubscription: "layout:focus-to-subscription",
|
||||
focusToEntryRender: "layout:focus-to-entry-render",
|
||||
},
|
||||
timeline: {
|
||||
switchToNext: "timeline:switch-to-next",
|
||||
switchToPrevious: "timeline:switch-to-previous",
|
||||
refetch: "timeline:refetch",
|
||||
enter: "timeline:enter",
|
||||
},
|
||||
entryRender: {
|
||||
scrollDown: "entry-render:scroll-down",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ declare module "@follow/utils/event-bus" {
|
|||
interface EventBusMap {
|
||||
"layout:focus-to-timeline": never
|
||||
"layout:focus-to-subscription": never
|
||||
"layout:focus-to-entry-render": never
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -36,6 +37,13 @@ export const useRegisterLayoutCommands = () => {
|
|||
EventBus.dispatch(COMMAND_ID.layout.focusToSubscription)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: COMMAND_ID.layout.focusToEntryRender,
|
||||
label: "Enter Selected Entry",
|
||||
run: () => {
|
||||
EventBus.dispatch(COMMAND_ID.layout.focusToEntryRender)
|
||||
},
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
|
|
@ -53,7 +61,12 @@ export type FocusToTimelineCommand = Command<{
|
|||
id: typeof COMMAND_ID.layout.focusToTimeline
|
||||
fn: () => void
|
||||
}>
|
||||
export type FocusToEntryRenderCommand = Command<{
|
||||
id: typeof COMMAND_ID.layout.focusToEntryRender
|
||||
fn: () => void
|
||||
}>
|
||||
export type LayoutCommand =
|
||||
| ToggleTimelineColumnCommand
|
||||
| FocusToTimelineCommand
|
||||
| FocusToSubscriptionCommand
|
||||
| FocusToEntryRenderCommand
|
||||
|
|
|
|||
|
|
@ -36,13 +36,6 @@ export const useRegisterTimelineCommand = () => {
|
|||
EventBus.dispatch("timeline:refetch")
|
||||
},
|
||||
},
|
||||
{
|
||||
id: COMMAND_ID.timeline.enter,
|
||||
label: "Enter Selected Entry",
|
||||
run: () => {
|
||||
EventBus.dispatch("timeline:enter")
|
||||
},
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
|
|
@ -61,13 +54,7 @@ export type RefetchTimelineCommand = Command<{
|
|||
fn: () => void
|
||||
}>
|
||||
|
||||
export type EnterTimelineCommand = Command<{
|
||||
id: typeof COMMAND_ID.timeline.enter
|
||||
fn: () => void
|
||||
}>
|
||||
|
||||
export type TimelineCommand =
|
||||
| SwitchToNextTimelineCommand
|
||||
| SwitchToPreviousTimelineCommand
|
||||
| RefetchTimelineCommand
|
||||
| EnterTimelineCommand
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import type { BindingCommandId } from "./use-command-shortcut"
|
||||
import { useCommandShortcut } from "./use-command-shortcut"
|
||||
import type { RegisterHotkeyOptions } from "./use-register-hotkey"
|
||||
import { useCommandHotkey } from "./use-register-hotkey"
|
||||
|
||||
export const useCommandBinding = <T extends BindingCommandId>({
|
||||
commandId,
|
||||
when = true,
|
||||
args,
|
||||
}: Omit<RegisterHotkeyOptions<T>, "shortcut">) => {
|
||||
const commandShortcut = useCommandShortcut(commandId)
|
||||
|
||||
return useCommandHotkey({
|
||||
shortcut: commandShortcut,
|
||||
commandId,
|
||||
when,
|
||||
args,
|
||||
})
|
||||
}
|
||||
|
|
@ -4,13 +4,11 @@ import { tinykeys } from "tinykeys"
|
|||
|
||||
import type { FollowCommand, FollowCommandId } from "../types"
|
||||
import { getCommand } from "./use-command"
|
||||
import type { BindingCommandId } from "./use-command-shortcut"
|
||||
import { useCommandShortcut } from "./use-command-shortcut"
|
||||
|
||||
export interface HotkeyOptions {
|
||||
forceInputElement?: true
|
||||
}
|
||||
interface RegisterHotkeyOptions<T extends FollowCommandId> {
|
||||
export interface RegisterHotkeyOptions<T extends FollowCommandId> {
|
||||
shortcut: string
|
||||
commandId: T
|
||||
args?: Parameters<Extract<FollowCommand, { id: T }>["run"]>
|
||||
|
|
@ -80,18 +78,3 @@ export const useCommandHotkey = <T extends FollowCommandId>({
|
|||
return tinykeys(document.documentElement, keyMap)
|
||||
}, [shortcut, commandId, when, argsRef, options?.forceInputElement])
|
||||
}
|
||||
|
||||
export const useCommandBinding = <T extends BindingCommandId>({
|
||||
commandId,
|
||||
when = true,
|
||||
args,
|
||||
}: Omit<RegisterHotkeyOptions<T>, "shortcut">) => {
|
||||
const commandShortcut = useCommandShortcut(commandId)
|
||||
|
||||
return useCommandHotkey({
|
||||
shortcut: commandShortcut,
|
||||
commandId,
|
||||
when,
|
||||
args,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ import { useConditionalHotkeyScope } from "~/hooks/common"
|
|||
import { useHotkeyScope } from "~/providers/hotkey-provider"
|
||||
|
||||
import { COMMAND_ID } from "../command/commands/id"
|
||||
import { useCommandBinding, useCommandHotkey } from "../command/hooks/use-register-hotkey"
|
||||
import { useCommandBinding } from "../command/hooks/use-command-binding"
|
||||
import { useCommandHotkey } from "../command/hooks/use-register-hotkey"
|
||||
|
||||
export const EntryColumnShortcutHandler: FC<{
|
||||
refetch: () => void
|
||||
|
|
@ -43,7 +44,7 @@ export const EntryColumnShortcutHandler: FC<{
|
|||
})
|
||||
|
||||
useCommandHotkey({
|
||||
commandId: COMMAND_ID.timeline.enter,
|
||||
commandId: COMMAND_ID.layout.focusToEntryRender,
|
||||
shortcut: "Enter",
|
||||
when,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { CommandActionButton } from "~/components/ui/button/CommandActionButton"
|
|||
import { useHasModal } from "~/components/ui/modal/stacked/hooks"
|
||||
import { useSortedEntryActions } from "~/hooks/biz/useEntryActions"
|
||||
import { COMMAND_ID } from "~/modules/command/commands/id"
|
||||
import { useCommandBinding } from "~/modules/command/hooks/use-register-hotkey"
|
||||
import { useCommandBinding } from "~/modules/command/hooks/use-command-binding"
|
||||
import { useEntry } from "~/store/entry/hooks"
|
||||
|
||||
export const EntryHeaderActions = ({
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ import { useFeedById } from "~/store/feed"
|
|||
import { useInboxById } from "~/store/inbox"
|
||||
|
||||
import { COMMAND_ID } from "../command/commands/id"
|
||||
import { useCommandBinding, useCommandHotkey } from "../command/hooks/use-register-hotkey"
|
||||
import { useCommandBinding } from "../command/hooks/use-command-binding"
|
||||
import { useCommandHotkey } from "../command/hooks/use-register-hotkey"
|
||||
import { EntryContentHTMLRenderer } from "../renderer/html"
|
||||
import { AISummary } from "./AISummary"
|
||||
import { EntryTimelineSidebar } from "./components/EntryTimelineSidebar"
|
||||
|
|
@ -235,6 +236,7 @@ const EntryScrollArea: Component<{
|
|||
}
|
||||
return (
|
||||
<ScrollArea.ScrollArea
|
||||
focusable={false}
|
||||
mask={false}
|
||||
rootClassName={cn(
|
||||
"h-0 min-w-0 grow overflow-y-auto print:h-auto print:overflow-visible",
|
||||
|
|
@ -353,7 +355,7 @@ const RegisterCommands = ({
|
|||
springScrollTo(currentScroll + delta, scrollerRef.current!)
|
||||
}
|
||||
}),
|
||||
EventBus.subscribe(COMMAND_ID.timeline.enter, () => {
|
||||
EventBus.subscribe(COMMAND_ID.layout.focusToEntryRender, () => {
|
||||
const $scroller = scrollerRef.current
|
||||
if ($scroller) {
|
||||
springScrollTo(0, $scroller)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ import {
|
|||
} from "~/store/subscription"
|
||||
|
||||
import { COMMAND_ID } from "../command/commands/id"
|
||||
import { useCommandBinding, useCommandHotkey } from "../command/hooks/use-register-hotkey"
|
||||
import { useCommandBinding } from "../command/hooks/use-command-binding"
|
||||
import { useCommandHotkey } from "../command/hooks/use-register-hotkey"
|
||||
import { useIsPreviewFeed } from "../entry-column/hooks/useIsPreviewFeed"
|
||||
import {
|
||||
resetSelectedFeedIds,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import { useHotkeyScope } from "~/providers/hotkey-provider"
|
|||
|
||||
import { WindowUnderBlur } from "../../components/ui/background"
|
||||
import { COMMAND_ID } from "../command/commands/id"
|
||||
import { useCommandBinding } from "../command/hooks/use-register-hotkey"
|
||||
import { useCommandBinding } from "../command/hooks/use-command-binding"
|
||||
import { getSelectedFeedIds, resetSelectedFeedIds, setSelectedFeedIds } from "./atom"
|
||||
import { useShouldFreeUpSpace } from "./hook"
|
||||
import { TimelineColumnHeader } from "./TimelineColumnHeader"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import { highlightElement } from "@follow/components/common/Focusable/utils.js"
|
||||
import { nextFrame } from "@follow/utils/dom"
|
||||
import { EventBus } from "@follow/utils/event-bus"
|
||||
import { useEffect } from "react"
|
||||
import { tinykeys } from "tinykeys"
|
||||
import { useEventListener } from "usehooks-ts"
|
||||
|
||||
import { HotkeyScope } from "~/constants/hotkeys"
|
||||
import { COMMAND_ID } from "~/modules/command/commands/id"
|
||||
import { useCommandBinding } from "~/modules/command/hooks/use-register-hotkey"
|
||||
import { useCommandBinding } from "~/modules/command/hooks/use-command-binding"
|
||||
|
||||
import { useHotkeyScope } from "./hotkey-provider"
|
||||
|
||||
|
|
@ -31,5 +34,36 @@ export const GlobalHotkeysProvider = () => {
|
|||
}
|
||||
})
|
||||
|
||||
// Re force to sidebar focusable
|
||||
useEventListener("focusin", (e) => {
|
||||
if (
|
||||
activeScopes.length === 1 &&
|
||||
activeScopes[0] === HotkeyScope.Home &&
|
||||
e.target === document.body
|
||||
) {
|
||||
EventBus.dispatch(COMMAND_ID.layout.focusToTimeline)
|
||||
}
|
||||
})
|
||||
// Re force to sidebar focusable
|
||||
useEventListener("focusout", () => {
|
||||
const { activeElement } = document
|
||||
if (
|
||||
activeElement === document.body &&
|
||||
activeScopes.length === 1 &&
|
||||
activeScopes[0] === HotkeyScope.Home
|
||||
) {
|
||||
EventBus.dispatch(COMMAND_ID.layout.focusToTimeline)
|
||||
}
|
||||
})
|
||||
|
||||
// Show current focused element
|
||||
useEffect(() => {
|
||||
return tinykeys(window, {
|
||||
"$mod+Period": () => {
|
||||
highlightElement(document.activeElement as HTMLElement)
|
||||
},
|
||||
})
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,9 +75,11 @@ const Viewport = ({
|
|||
ref: forwardedRef,
|
||||
className,
|
||||
mask = false,
|
||||
focusable = true,
|
||||
...rest
|
||||
}: React.ComponentPropsWithoutRef<typeof ScrollAreaBase.Viewport> & {
|
||||
mask?: boolean
|
||||
focusable?: boolean
|
||||
} & { ref?: React.Ref<React.ElementRef<typeof ScrollAreaBase.Viewport> | null> }) => {
|
||||
const ref = React.useRef<HTMLDivElement>(null)
|
||||
const [shouldAddMask, setShouldAddMask] = React.useState(false)
|
||||
|
|
@ -105,7 +107,7 @@ const Viewport = ({
|
|||
<ScrollAreaBase.Viewport
|
||||
{...rest}
|
||||
ref={ref}
|
||||
tabIndex={-1}
|
||||
tabIndex={focusable ? -1 : void 0}
|
||||
className={cn("block size-full", shouldAddMask && styles["mask-scroller"], className)}
|
||||
/>
|
||||
)
|
||||
|
|
@ -143,6 +145,8 @@ export const ScrollArea = ({
|
|||
onScroll,
|
||||
orientation = "vertical",
|
||||
asChild = false,
|
||||
|
||||
focusable = true,
|
||||
}: React.PropsWithChildren & {
|
||||
rootClassName?: string
|
||||
viewportClassName?: string
|
||||
|
|
@ -152,6 +156,7 @@ export const ScrollArea = ({
|
|||
onScroll?: (e: React.UIEvent<HTMLDivElement>) => void
|
||||
orientation?: "vertical" | "horizontal"
|
||||
asChild?: boolean
|
||||
focusable?: boolean
|
||||
} & { ref?: React.Ref<HTMLDivElement | null> }) => {
|
||||
const [viewportRef, setViewportRef] = React.useState<HTMLDivElement | null>(null)
|
||||
React.useImperativeHandle(ref, () => viewportRef as HTMLDivElement)
|
||||
|
|
@ -166,6 +171,7 @@ export const ScrollArea = ({
|
|||
mask={mask}
|
||||
asChild={asChild}
|
||||
onScroll={onScroll}
|
||||
focusable={focusable}
|
||||
>
|
||||
{children}
|
||||
</Viewport>
|
||||
|
|
|
|||
Loading…
Reference in New Issue