feat: enhance command binding and shortcut management
- Introduced `useCommandBinding` to streamline the registration of command hotkeys. - Added new entry navigation commands for scrolling to the next and previous entries. - Updated existing components to utilize the new command binding approach, improving consistency and maintainability. - Refactored shortcut handling to support new entry render commands, enhancing user experience. This update aims to improve the overall efficiency of command and shortcut management in the application. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
93f19c4f6c
commit
262a7ce55e
|
|
@ -32,7 +32,6 @@ import { PlainModal } from "~/components/ui/modal/stacked/custom-modal"
|
|||
import { DeclarativeModal } from "~/components/ui/modal/stacked/declarative-modal"
|
||||
import { HotkeyScope } from "~/constants"
|
||||
import { ROOT_CONTAINER_ID } from "~/constants/dom"
|
||||
import { shortcuts } from "~/constants/shortcuts"
|
||||
import { useDailyTask } from "~/hooks/biz/useDailyTask"
|
||||
import { useBatchUpdateSubscription } from "~/hooks/biz/useSubscriptionActions"
|
||||
import { useI18n } from "~/hooks/common"
|
||||
|
|
@ -40,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 { useCommandHotkey } from "~/modules/command/hooks/use-register-hotkey"
|
||||
import { useCommandBinding } from "~/modules/command/hooks/use-register-hotkey"
|
||||
import { DebugRegistry } from "~/modules/debug/registry"
|
||||
import { CmdF } from "~/modules/panel/cmdf"
|
||||
import { SearchCmdK } from "~/modules/panel/cmdk"
|
||||
|
|
@ -244,9 +243,8 @@ const FeedResponsiveResizerContainer = ({
|
|||
|
||||
const activeScopes = useHotkeyScope()
|
||||
|
||||
useCommandHotkey({
|
||||
useCommandBinding({
|
||||
commandId: COMMAND_ID.layout.toggleTimelineColumn,
|
||||
shortcut: shortcuts.layout.toggleSidebar.key,
|
||||
when: activeScopes.includes(HotkeyScope.Home),
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,13 @@ declare module "@follow/utils/event-bus" {
|
|||
interface EventBusMap {
|
||||
"entry-render:scroll-down": never
|
||||
"entry-render:scroll-up": never
|
||||
"entry-render:next-entry": never
|
||||
"entry-render:previous-entry": never
|
||||
}
|
||||
}
|
||||
const LABEL_PREFIX = "Entry Render"
|
||||
|
||||
const category = "follow:entry-render"
|
||||
export const useRegisterEntryRenderCommand = () => {
|
||||
useRegisterCommandEffect([
|
||||
{
|
||||
|
|
@ -18,7 +22,7 @@ export const useRegisterEntryRenderCommand = () => {
|
|||
run: () => {
|
||||
EventBus.dispatch(COMMAND_ID.entryRender.scrollDown)
|
||||
},
|
||||
category: "follow:entry-render",
|
||||
category,
|
||||
label: `${LABEL_PREFIX}: Scroll down`,
|
||||
},
|
||||
{
|
||||
|
|
@ -26,9 +30,27 @@ export const useRegisterEntryRenderCommand = () => {
|
|||
run: () => {
|
||||
EventBus.dispatch(COMMAND_ID.entryRender.scrollUp)
|
||||
},
|
||||
category: "follow:entry-render",
|
||||
category,
|
||||
label: `${LABEL_PREFIX}: Scroll up`,
|
||||
},
|
||||
{
|
||||
id: COMMAND_ID.entryRender.nextEntry,
|
||||
run: () => {
|
||||
EventBus.dispatch(COMMAND_ID.timeline.switchToNext)
|
||||
EventBus.dispatch(COMMAND_ID.entryRender.nextEntry)
|
||||
},
|
||||
category,
|
||||
label: `${LABEL_PREFIX}: Next entry`,
|
||||
},
|
||||
{
|
||||
id: COMMAND_ID.entryRender.previousEntry,
|
||||
run: () => {
|
||||
EventBus.dispatch(COMMAND_ID.timeline.switchToPrevious)
|
||||
EventBus.dispatch(COMMAND_ID.entryRender.previousEntry)
|
||||
},
|
||||
category,
|
||||
label: `${LABEL_PREFIX}: Previous entry`,
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
|
|
@ -42,4 +64,18 @@ type EntryScrollUpCommand = Command<{
|
|||
fn: () => void
|
||||
}>
|
||||
|
||||
export type EntryRenderCommand = EntryScrollDownCommand | EntryScrollUpCommand
|
||||
type EntryNextEntryCommand = Command<{
|
||||
id: typeof COMMAND_ID.entryRender.nextEntry
|
||||
fn: () => void
|
||||
}>
|
||||
|
||||
type EntryPreviousEntryCommand = Command<{
|
||||
id: typeof COMMAND_ID.entryRender.previousEntry
|
||||
fn: () => void
|
||||
}>
|
||||
|
||||
export type EntryRenderCommand =
|
||||
| EntryScrollDownCommand
|
||||
| EntryScrollUpCommand
|
||||
| EntryNextEntryCommand
|
||||
| EntryPreviousEntryCommand
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ export const COMMAND_ID = {
|
|||
entryRender: {
|
||||
scrollDown: "entry-render:scroll-down",
|
||||
scrollUp: "entry-render:scroll-up",
|
||||
nextEntry: "entry-render:next-entry",
|
||||
previousEntry: "entry-render:previous-entry",
|
||||
},
|
||||
subscription: {
|
||||
switchTabToNext: "subscription:switch-tab-to-next",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
import { shortcuts } from "~/constants/shortcuts"
|
||||
|
||||
import { COMMAND_ID } from "../commands/id"
|
||||
|
||||
const defaultCommandShortcuts = {
|
||||
[COMMAND_ID.entry.read]: shortcuts.entry.toggleRead.key,
|
||||
[COMMAND_ID.entry.openInBrowser]: shortcuts.entry.openInBrowser.key,
|
||||
[COMMAND_ID.entry.star]: shortcuts.entry.toggleStarred.key,
|
||||
[COMMAND_ID.entry.copyLink]: shortcuts.entry.copyLink.key,
|
||||
[COMMAND_ID.entry.copyTitle]: shortcuts.entry.copyTitle.key,
|
||||
[COMMAND_ID.entry.tts]: shortcuts.entry.tts.key,
|
||||
[COMMAND_ID.entry.tip]: shortcuts.entry.tip.key,
|
||||
[COMMAND_ID.entry.share]: shortcuts.entry.share.key,
|
||||
|
||||
[COMMAND_ID.entryRender.scrollUp]: shortcuts.entry.scrollUp.key,
|
||||
[COMMAND_ID.entryRender.scrollDown]: shortcuts.entry.scrollDown.key,
|
||||
|
||||
[COMMAND_ID.timeline.switchToNext]: shortcuts.entries.next.key,
|
||||
[COMMAND_ID.timeline.switchToPrevious]: shortcuts.entries.previous.key,
|
||||
[COMMAND_ID.timeline.refetch]: shortcuts.entries.refetch.key,
|
||||
|
||||
[COMMAND_ID.layout.toggleTimelineColumn]: shortcuts.layout.toggleSidebar.key,
|
||||
|
||||
[COMMAND_ID.subscription.switchTabToNext]: shortcuts.feeds.switchNextView.key,
|
||||
[COMMAND_ID.subscription.switchTabToPrevious]: shortcuts.feeds.switchPreviousView.key,
|
||||
|
||||
[COMMAND_ID.global.showShortcuts]: shortcuts.layout.showShortcuts.key,
|
||||
|
||||
[COMMAND_ID.entryRender.nextEntry]: shortcuts.entry.nextEntry.key,
|
||||
[COMMAND_ID.entryRender.previousEntry]: shortcuts.entry.previousEntry.key,
|
||||
} as const
|
||||
|
||||
export type BindingCommandId = keyof typeof defaultCommandShortcuts
|
||||
|
||||
// eslint-disable-next-line @eslint-react/hooks-extra/no-unnecessary-use-prefix, @eslint-react/hooks-extra/ensure-custom-hooks-using-other-hooks
|
||||
export const useCommandShortcut = (commandId: BindingCommandId): string => {
|
||||
const commandShortcut = defaultCommandShortcuts[commandId]
|
||||
|
||||
return commandShortcut
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import { useEffect, useRef } from "react"
|
||||
import { useEffect } from "react"
|
||||
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"
|
||||
|
||||
interface RegisterHotkeyOptions<T extends FollowCommandId> {
|
||||
shortcut: string
|
||||
|
|
@ -14,14 +16,15 @@ interface RegisterHotkeyOptions<T extends FollowCommandId> {
|
|||
export const useCommandHotkey = <T extends FollowCommandId>({
|
||||
shortcut,
|
||||
commandId,
|
||||
when = true,
|
||||
when,
|
||||
args,
|
||||
}: RegisterHotkeyOptions<T>) => {
|
||||
const unsubscribeRef = useRef<() => void>(void 0)
|
||||
|
||||
useEffect(() => {
|
||||
if (!when) {
|
||||
unsubscribeRef.current?.()
|
||||
return
|
||||
}
|
||||
|
||||
if (!shortcut) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -55,10 +58,21 @@ export const useCommandHotkey = <T extends FollowCommandId>({
|
|||
}
|
||||
})
|
||||
|
||||
unsubscribeRef.current = tinykeys(document.documentElement, keyMap)
|
||||
|
||||
return () => {
|
||||
unsubscribeRef.current?.()
|
||||
}
|
||||
return tinykeys(document.documentElement, keyMap)
|
||||
}, [shortcut, commandId, when, args])
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,13 @@ import { memo, useEffect, useLayoutEffect, useState } from "react"
|
|||
|
||||
import { useMainContainerElement } from "~/atoms/dom"
|
||||
import { HotkeyScope } from "~/constants"
|
||||
import { shortcuts } from "~/constants/shortcuts"
|
||||
import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry"
|
||||
import { useRouteEntryId } from "~/hooks/biz/useRouteParams"
|
||||
import { useConditionalHotkeyScope } from "~/hooks/common"
|
||||
import { useHotkeyScope } from "~/providers/hotkey-provider"
|
||||
|
||||
import { COMMAND_ID } from "../command/commands/id"
|
||||
import { useCommandHotkey } from "../command/hooks/use-register-hotkey"
|
||||
import { useCommandBinding } from "../command/hooks/use-register-hotkey"
|
||||
|
||||
export const EntryColumnShortcutHandler: FC<{
|
||||
refetch: () => void
|
||||
|
|
@ -26,26 +25,24 @@ export const EntryColumnShortcutHandler: FC<{
|
|||
const when =
|
||||
activeScope.includes(HotkeyScope.Timeline) && !activeScope.includes(HotkeyScope.EntryRender)
|
||||
|
||||
useCommandHotkey({
|
||||
shortcut: shortcuts.entries.next.key,
|
||||
useCommandBinding({
|
||||
commandId: COMMAND_ID.timeline.switchToNext,
|
||||
when,
|
||||
})
|
||||
|
||||
useCommandHotkey({
|
||||
shortcut: shortcuts.entries.previous.key,
|
||||
useCommandBinding({
|
||||
commandId: COMMAND_ID.timeline.switchToPrevious,
|
||||
when,
|
||||
})
|
||||
|
||||
useCommandHotkey({
|
||||
shortcut: shortcuts.entries.refetch.key,
|
||||
useCommandBinding({
|
||||
commandId: COMMAND_ID.timeline.refetch,
|
||||
when,
|
||||
})
|
||||
|
||||
const currentEntryIdRef = useRefValue(useRouteEntryId())
|
||||
const navigate = useNavigateEntry()
|
||||
|
||||
useEffect(() => {
|
||||
return EventBus.subscribe(COMMAND_ID.timeline.switchToNext, () => {
|
||||
const data = dataRef.current
|
||||
|
|
@ -60,7 +57,7 @@ export const EntryColumnShortcutHandler: FC<{
|
|||
entryId: nextId,
|
||||
})
|
||||
})
|
||||
}, [currentEntryIdRef, dataRef, handleScrollTo, navigate])
|
||||
}, [currentEntryIdRef, dataRef, handleScrollTo, navigate, when])
|
||||
|
||||
useEffect(() => {
|
||||
return EventBus.subscribe(COMMAND_ID.timeline.switchToPrevious, () => {
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@ import type { FeedViewType } from "@follow/constants"
|
|||
import { MenuItemText } from "~/atoms/context-menu"
|
||||
import { CommandActionButton } from "~/components/ui/button/CommandActionButton"
|
||||
import { useHasModal } from "~/components/ui/modal/stacked/hooks"
|
||||
import { shortcuts } from "~/constants/shortcuts"
|
||||
import { useSortedEntryActions } from "~/hooks/biz/useEntryActions"
|
||||
import { COMMAND_ID } from "~/modules/command/commands/id"
|
||||
import { useCommandHotkey } from "~/modules/command/hooks/use-register-hotkey"
|
||||
import { useCommandBinding } from "~/modules/command/hooks/use-register-hotkey"
|
||||
import { useEntry } from "~/store/entry/hooks"
|
||||
|
||||
export const EntryHeaderActions = ({
|
||||
|
|
@ -23,9 +22,8 @@ export const EntryHeaderActions = ({
|
|||
|
||||
const hasModal = useHasModal()
|
||||
|
||||
useCommandHotkey({
|
||||
useCommandBinding({
|
||||
when: !!entry?.entries.url && !hasModal,
|
||||
shortcut: shortcuts.entry.openInBrowser.key,
|
||||
commandId: COMMAND_ID.entry.openInBrowser,
|
||||
args: [{ entryId }],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { tracker } from "@follow/tracker"
|
||||
import { EventBus } from "@follow/utils/event-bus"
|
||||
import { createElement, useCallback, useEffect } from "react"
|
||||
import { createElement, useCallback } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
|
||||
|
|
@ -8,22 +7,6 @@ import { useModalStack } from "~/components/ui/modal/stacked/hooks"
|
|||
|
||||
import { ImageGalleryContent } from "./components/ImageGalleryContent"
|
||||
|
||||
declare module "@follow/utils/event-bus" {
|
||||
export interface CustomEvent {
|
||||
FOCUS_ENTRY_CONTAINER: never
|
||||
}
|
||||
}
|
||||
|
||||
export const useFocusEntryContainerSubscriptions = (
|
||||
ref: React.RefObject<HTMLDivElement | null>,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
return EventBus.subscribe("FOCUS_ENTRY_CONTAINER", () => {
|
||||
ref.current?.focus()
|
||||
})
|
||||
}, [ref])
|
||||
}
|
||||
|
||||
export const useGalleryModal = () => {
|
||||
const { present } = useModalStack()
|
||||
const { t } = useTranslation()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Focusable, useFocusable } from "@follow/components/common/Focusable.js"
|
||||
import { MemoedDangerousHTMLStyle } from "@follow/components/common/MemoedDangerousHTMLStyle.js"
|
||||
import { ScrollArea } from "@follow/components/ui/scroll-area/index.js"
|
||||
import type { FeedViewType } from "@follow/constants"
|
||||
|
|
@ -6,10 +7,10 @@ import type { FeedModel, InboxModel } from "@follow/models/types"
|
|||
import { stopPropagation } from "@follow/utils/dom"
|
||||
import { EventBus } from "@follow/utils/event-bus"
|
||||
import { springScrollTo } from "@follow/utils/scroller"
|
||||
import { cn } from "@follow/utils/utils"
|
||||
import { cn, combineCleanupFunctions } from "@follow/utils/utils"
|
||||
import { ErrorBoundary } from "@sentry/react"
|
||||
import * as React from "react"
|
||||
import { useEffect, useMemo, useRef } from "react"
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
|
||||
import {
|
||||
useEntryIsInReadability,
|
||||
|
|
@ -21,7 +22,6 @@ import { ShadowDOM } from "~/components/common/ShadowDOM"
|
|||
import type { TocRef } from "~/components/ui/markdown/components/Toc"
|
||||
import { useInPeekModal } from "~/components/ui/modal/inspire/InPeekModal"
|
||||
import { HotkeyScope } from "~/constants"
|
||||
import { shortcuts } from "~/constants/shortcuts"
|
||||
import { useRenderStyle } from "~/hooks/biz/useRenderStyle"
|
||||
import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
|
||||
import { useAuthQuery, useConditionalHotkeyScope } from "~/hooks/common"
|
||||
|
|
@ -35,7 +35,7 @@ import { useFeedById } from "~/store/feed"
|
|||
import { useInboxById } from "~/store/inbox"
|
||||
|
||||
import { COMMAND_ID } from "../command/commands/id"
|
||||
import { useCommandHotkey } from "../command/hooks/use-register-hotkey"
|
||||
import { useCommandBinding } from "../command/hooks/use-register-hotkey"
|
||||
import { EntryContentHTMLRenderer } from "../renderer/html"
|
||||
import { AISummary } from "./AISummary"
|
||||
import { EntryTimelineSidebar } from "./components/EntryTimelineSidebar"
|
||||
|
|
@ -43,7 +43,6 @@ import { EntryTitle } from "./components/EntryTitle"
|
|||
import { SourceContentPanel } from "./components/SourceContentView"
|
||||
import { SupportCreator } from "./components/SupportCreator"
|
||||
import { EntryHeader } from "./header"
|
||||
import { useFocusEntryContainerSubscriptions } from "./hooks"
|
||||
import type { EntryContentProps } from "./index.shared"
|
||||
import {
|
||||
ContainerToc,
|
||||
|
|
@ -82,13 +81,20 @@ export const EntryContent: Component<EntryContentProps> = ({
|
|||
|
||||
const isInReadabilityMode = useEntryIsInReadability(entryId)
|
||||
const isReadabilitySuccess = useEntryIsInReadabilitySuccess(entryId)
|
||||
const scrollerRef = useRef<HTMLDivElement | null>(null)
|
||||
useEffect(() => {
|
||||
scrollerRef.current?.scrollTo(0, 0)
|
||||
scrollerRef.current?.focus()
|
||||
}, [entryId])
|
||||
|
||||
useFocusEntryContainerSubscriptions(scrollerRef)
|
||||
const scrollerRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const scrollAndFocus = () => {
|
||||
scrollerRef.current?.scrollTo(0, 0)
|
||||
}
|
||||
|
||||
scrollAndFocus()
|
||||
return combineCleanupFunctions(
|
||||
EventBus.subscribe(COMMAND_ID.timeline.switchToNext, scrollAndFocus),
|
||||
EventBus.subscribe(COMMAND_ID.timeline.switchToPrevious, scrollAndFocus),
|
||||
)
|
||||
}, [])
|
||||
|
||||
const safeUrl = useFeedSafeUrl(entryId)
|
||||
|
||||
|
|
@ -101,11 +107,7 @@ export const EntryContent: Component<EntryContentProps> = ({
|
|||
|
||||
const isInPeekModal = useInPeekModal()
|
||||
|
||||
const { setIsUserInteraction } = useRegisterCommands({
|
||||
entryId,
|
||||
scrollerRef,
|
||||
})
|
||||
|
||||
const [isUserInteraction, setIsUserInteraction] = useState(false)
|
||||
if (!entry) return null
|
||||
|
||||
const entryContent = isInReadabilityMode
|
||||
|
|
@ -129,7 +131,15 @@ export const EntryContent: Component<EntryContentProps> = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
<div className="@container relative flex size-full flex-col overflow-hidden print:size-auto print:overflow-visible">
|
||||
<Focusable
|
||||
className="@container relative flex size-full flex-col overflow-hidden print:size-auto print:overflow-visible"
|
||||
onFocus={() => setIsUserInteraction(true)}
|
||||
>
|
||||
<RegisterCommands
|
||||
entryId={entry.entries.id}
|
||||
scrollerRef={scrollerRef}
|
||||
isUserInteraction={isUserInteraction}
|
||||
/>
|
||||
<EntryTimelineSidebar entryId={entry.entries.id} />
|
||||
<EntryScrollArea className={className} scrollerRef={scrollerRef}>
|
||||
<div
|
||||
|
|
@ -138,7 +148,7 @@ export const EntryContent: Component<EntryContentProps> = ({
|
|||
>
|
||||
<article
|
||||
tabIndex={-1}
|
||||
onClick={() => setIsUserInteraction(true)}
|
||||
onFocus={() => setIsUserInteraction(true)}
|
||||
data-testid="entry-render"
|
||||
onContextMenu={stopPropagation}
|
||||
className="@[950px]:max-w-[70ch] @7xl:max-w-[80ch] relative m-auto min-w-0 max-w-[550px]"
|
||||
|
|
@ -202,7 +212,7 @@ export const EntryContent: Component<EntryContentProps> = ({
|
|||
</div>
|
||||
</EntryScrollArea>
|
||||
<SourceContentPanel src={safeUrl ?? "#"} />
|
||||
</div>
|
||||
</Focusable>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -286,41 +296,37 @@ const Renderer: React.FC<{
|
|||
)
|
||||
})
|
||||
|
||||
const useRegisterCommands = ({
|
||||
entryId,
|
||||
const RegisterCommands = ({
|
||||
scrollerRef,
|
||||
isUserInteraction,
|
||||
}: {
|
||||
entryId: string
|
||||
scrollerRef: React.RefObject<HTMLDivElement | null>
|
||||
isUserInteraction: boolean
|
||||
}) => {
|
||||
const [isUserInteraction, setIsUserInteraction] = React.useState(false)
|
||||
useConditionalHotkeyScope(HotkeyScope.EntryRender, isUserInteraction, true)
|
||||
const containerFocused = useFocusable()
|
||||
useConditionalHotkeyScope(HotkeyScope.EntryRender, isUserInteraction && containerFocused, true)
|
||||
|
||||
const activeScope = useHotkeyScope()
|
||||
const when = activeScope.includes(HotkeyScope.EntryRender)
|
||||
|
||||
useCommandHotkey({
|
||||
shortcut: shortcuts.entry.scrollUp.key,
|
||||
useCommandBinding({
|
||||
commandId: COMMAND_ID.entryRender.scrollUp,
|
||||
when,
|
||||
})
|
||||
|
||||
useCommandHotkey({
|
||||
shortcut: shortcuts.entry.scrollDown.key,
|
||||
useCommandBinding({
|
||||
commandId: COMMAND_ID.entryRender.scrollDown,
|
||||
when,
|
||||
})
|
||||
|
||||
useCommandHotkey({
|
||||
shortcut: shortcuts.entry.nextEntry.key,
|
||||
commandId: COMMAND_ID.timeline.switchToNext,
|
||||
|
||||
useCommandBinding({
|
||||
commandId: COMMAND_ID.entryRender.nextEntry,
|
||||
when,
|
||||
})
|
||||
|
||||
useCommandHotkey({
|
||||
shortcut: shortcuts.entry.previousEntry.key,
|
||||
commandId: COMMAND_ID.timeline.switchToPrevious,
|
||||
useCommandBinding({
|
||||
commandId: COMMAND_ID.entryRender.previousEntry,
|
||||
when,
|
||||
})
|
||||
|
||||
|
|
@ -345,11 +351,5 @@ const useRegisterCommands = ({
|
|||
})
|
||||
}, [scrollerRef])
|
||||
|
||||
useEffect(() => {
|
||||
return () => setIsUserInteraction(false)
|
||||
}, [entryId])
|
||||
|
||||
return {
|
||||
setIsUserInteraction,
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { LoadingWithIcon } from "@follow/components/ui/loading/index.jsx"
|
|||
import { RootPortal } from "@follow/components/ui/portal/index.jsx"
|
||||
import { useScrollViewElement } from "@follow/components/ui/scroll-area/hooks.js"
|
||||
import { WEB_BUILD } from "@follow/shared/constants"
|
||||
import { EventBus } from "@follow/utils/event-bus"
|
||||
import { springScrollTo } from "@follow/utils/scroller"
|
||||
import { cn } from "@follow/utils/utils"
|
||||
import type { FallbackRender } from "@sentry/react"
|
||||
|
|
@ -261,9 +260,6 @@ export const ContainerToc = memo(
|
|||
<div className="sticky top-0">
|
||||
<Toc
|
||||
ref={ref}
|
||||
onItemClick={() => {
|
||||
EventBus.dispatch("FOCUS_ENTRY_CONTAINER")
|
||||
}}
|
||||
className={cn(
|
||||
"animate-in fade-in-0 slide-in-from-bottom-12 easing-spring spring-soft flex flex-col items-end",
|
||||
"scrollbar-none max-h-[calc(100vh-100px)] overflow-auto",
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import { useRootContainerElement } from "~/atoms/dom"
|
|||
import { useUISettingKey } from "~/atoms/settings/ui"
|
||||
import { setTimelineColumnShow, useTimelineColumnShow } from "~/atoms/sidebar"
|
||||
import { HotkeyScope } from "~/constants"
|
||||
import { shortcuts } from "~/constants/shortcuts"
|
||||
import { navigateEntry, useBackHome } from "~/hooks/biz/useNavigateEntry"
|
||||
import { useReduceMotion } from "~/hooks/biz/useReduceMotion"
|
||||
import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
|
||||
|
|
@ -27,7 +26,7 @@ import { useHotkeyScope } from "~/providers/hotkey-provider"
|
|||
|
||||
import { WindowUnderBlur } from "../../components/ui/background"
|
||||
import { COMMAND_ID } from "../command/commands/id"
|
||||
import { useCommandHotkey } from "../command/hooks/use-register-hotkey"
|
||||
import { useCommandBinding } from "../command/hooks/use-register-hotkey"
|
||||
import { getSelectedFeedIds, resetSelectedFeedIds, setSelectedFeedIds } from "./atom"
|
||||
import { useShouldFreeUpSpace } from "./hook"
|
||||
import { TimelineColumnHeader } from "./TimelineColumnHeader"
|
||||
|
|
@ -245,15 +244,13 @@ const useRegisterCommands = ({
|
|||
const activeScope = useHotkeyScope()
|
||||
const when =
|
||||
activeScope.includes(HotkeyScope.SubscriptionList) || activeScope.includes(HotkeyScope.Timeline)
|
||||
useCommandHotkey({
|
||||
useCommandBinding({
|
||||
commandId: COMMAND_ID.subscription.switchTabToNext,
|
||||
shortcut: shortcuts.feeds.switchNextView.key,
|
||||
when,
|
||||
})
|
||||
|
||||
useCommandHotkey({
|
||||
useCommandBinding({
|
||||
commandId: COMMAND_ID.subscription.switchTabToPrevious,
|
||||
shortcut: shortcuts.feeds.switchPreviousView.key,
|
||||
when,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
import { HotkeyScope } from "~/constants/hotkeys"
|
||||
import { shortcuts } from "~/constants/shortcuts"
|
||||
import { COMMAND_ID } from "~/modules/command/commands/id"
|
||||
import { useCommandHotkey } from "~/modules/command/hooks/use-register-hotkey"
|
||||
import { useCommandBinding } from "~/modules/command/hooks/use-register-hotkey"
|
||||
|
||||
import { useHotkeyScope } from "./hotkey-provider"
|
||||
|
||||
export const GlobalHotkeysProvider = () => {
|
||||
const activeScopes = useHotkeyScope()
|
||||
|
||||
useCommandHotkey({
|
||||
useCommandBinding({
|
||||
commandId: COMMAND_ID.global.showShortcuts,
|
||||
shortcut: shortcuts.layout.showShortcuts.key,
|
||||
when: activeScopes.includes(HotkeyScope.Home),
|
||||
when:
|
||||
activeScopes.includes(HotkeyScope.Home) && !activeScopes.includes(HotkeyScope.EntryRender),
|
||||
})
|
||||
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -1,40 +1,37 @@
|
|||
import type { FocusEvent } from "react"
|
||||
import { createContext, use, useCallback, useState } from "react"
|
||||
import { createContext, use, useImperativeHandle, useRef, useState } from "react"
|
||||
import { useEventListener } from "usehooks-ts"
|
||||
|
||||
// const
|
||||
const FocusableContext = createContext(false)
|
||||
const FocusTargetRefContext = createContext<React.RefObject<HTMLElement | undefined>>(null!)
|
||||
export const Focusable: Component<
|
||||
React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
|
||||
> = ({ ref, ...props }) => {
|
||||
const { onBlur, onFocus, ...rest } = props
|
||||
const [isFocusWithIn, setIsFocusWithIn] = useState(false)
|
||||
const handleFocus = useCallback(
|
||||
(e: FocusEvent<HTMLDivElement>) => {
|
||||
onFocus?.(e)
|
||||
const focusTargetRef = useRef<HTMLElement | undefined>(void 0)
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
useImperativeHandle(ref, () => containerRef.current!)
|
||||
useEventListener("focusin", (e) => {
|
||||
if (containerRef.current?.contains(e.target as Node)) {
|
||||
setIsFocusWithIn(true)
|
||||
},
|
||||
[onFocus],
|
||||
)
|
||||
const handleBlur = useCallback(
|
||||
(e: FocusEvent<HTMLDivElement>) => {
|
||||
onBlur?.(e)
|
||||
} else {
|
||||
setIsFocusWithIn(false)
|
||||
},
|
||||
[onBlur],
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// useEventListener("focusout", (e) => {
|
||||
// if (!containerRef.current?.contains(e.target as Node)) {
|
||||
// setIsFocusWithIn(false)
|
||||
// }
|
||||
// })
|
||||
|
||||
return (
|
||||
<FocusableContext value={isFocusWithIn}>
|
||||
<div
|
||||
tabIndex={-1}
|
||||
role="region"
|
||||
ref={ref}
|
||||
{...rest}
|
||||
onBlur={handleBlur}
|
||||
onFocusCapture={handleFocus}
|
||||
onBlurCapture={handleBlur}
|
||||
onFocus={handleFocus}
|
||||
/>
|
||||
<FocusTargetRefContext value={focusTargetRef}>
|
||||
<div tabIndex={-1} role="region" ref={containerRef} {...rest} />
|
||||
</FocusTargetRefContext>
|
||||
</FocusableContext>
|
||||
)
|
||||
}
|
||||
|
|
@ -42,3 +39,7 @@ export const Focusable: Component<
|
|||
export const useFocusable = () => {
|
||||
return use(FocusableContext)
|
||||
}
|
||||
|
||||
export const useFocusTargetRef = () => {
|
||||
return use(FocusTargetRefContext)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -407,3 +407,13 @@ export function duplicateIfLengthLessThan(text: string, length: number) {
|
|||
? text.repeat(Math.ceil(length / text.length))
|
||||
: text
|
||||
}
|
||||
|
||||
export function combineCleanupFunctions(...fns: Array<Nullable<(() => void) | void>>) {
|
||||
return () => {
|
||||
fns.forEach((fn) => {
|
||||
if (typeof fn === "function") {
|
||||
fn()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue