From 262a7ce55e0057fcbf2d213873e3c75f9de7decd Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 13 May 2025 21:12:47 +0800 Subject: [PATCH] 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 --- .../app-layout/feed-column/desktop.tsx | 6 +- .../modules/command/commands/entry-render.tsx | 42 +++++++++- .../src/modules/command/commands/id.ts | 2 + .../command/hooks/use-command-shortcut.ts | 40 +++++++++ .../command/hooks/use-register-hotkey.ts | 34 +++++--- .../EntryColumnShortcutHandler.tsx | 15 ++-- .../entry-content/actions/header-actions.tsx | 6 +- .../src/modules/entry-content/hooks.tsx | 19 +---- .../modules/entry-content/index.electron.tsx | 82 +++++++++---------- .../modules/entry-content/index.shared.tsx | 4 - .../src/modules/timeline-column/index.tsx | 9 +- .../src/providers/global-hotkeys-provider.tsx | 9 +- .../components/src/common/Focusable.tsx | 49 +++++------ packages/internal/utils/src/utils.ts | 10 +++ 14 files changed, 199 insertions(+), 128 deletions(-) create mode 100644 apps/desktop/layer/renderer/src/modules/command/hooks/use-command-shortcut.ts diff --git a/apps/desktop/layer/renderer/src/modules/app-layout/feed-column/desktop.tsx b/apps/desktop/layer/renderer/src/modules/app-layout/feed-column/desktop.tsx index 111ec8d80..5bc5ec81a 100644 --- a/apps/desktop/layer/renderer/src/modules/app-layout/feed-column/desktop.tsx +++ b/apps/desktop/layer/renderer/src/modules/app-layout/feed-column/desktop.tsx @@ -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), }) diff --git a/apps/desktop/layer/renderer/src/modules/command/commands/entry-render.tsx b/apps/desktop/layer/renderer/src/modules/command/commands/entry-render.tsx index 1a2094f30..29682f14f 100644 --- a/apps/desktop/layer/renderer/src/modules/command/commands/entry-render.tsx +++ b/apps/desktop/layer/renderer/src/modules/command/commands/entry-render.tsx @@ -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 diff --git a/apps/desktop/layer/renderer/src/modules/command/commands/id.ts b/apps/desktop/layer/renderer/src/modules/command/commands/id.ts index dd37fcea8..dc5264fb7 100644 --- a/apps/desktop/layer/renderer/src/modules/command/commands/id.ts +++ b/apps/desktop/layer/renderer/src/modules/command/commands/id.ts @@ -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", diff --git a/apps/desktop/layer/renderer/src/modules/command/hooks/use-command-shortcut.ts b/apps/desktop/layer/renderer/src/modules/command/hooks/use-command-shortcut.ts new file mode 100644 index 000000000..939e1ed6b --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/command/hooks/use-command-shortcut.ts @@ -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 +} diff --git a/apps/desktop/layer/renderer/src/modules/command/hooks/use-register-hotkey.ts b/apps/desktop/layer/renderer/src/modules/command/hooks/use-register-hotkey.ts index 9adc46c98..304310d5c 100644 --- a/apps/desktop/layer/renderer/src/modules/command/hooks/use-register-hotkey.ts +++ b/apps/desktop/layer/renderer/src/modules/command/hooks/use-register-hotkey.ts @@ -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 { shortcut: string @@ -14,14 +16,15 @@ interface RegisterHotkeyOptions { export const useCommandHotkey = ({ shortcut, commandId, - when = true, + when, args, }: RegisterHotkeyOptions) => { - const unsubscribeRef = useRef<() => void>(void 0) - useEffect(() => { if (!when) { - unsubscribeRef.current?.() + return + } + + if (!shortcut) { return } @@ -55,10 +58,21 @@ export const useCommandHotkey = ({ } }) - unsubscribeRef.current = tinykeys(document.documentElement, keyMap) - - return () => { - unsubscribeRef.current?.() - } + return tinykeys(document.documentElement, keyMap) }, [shortcut, commandId, when, args]) } + +export const useCommandBinding = ({ + commandId, + when = true, + args, +}: Omit, "shortcut">) => { + const commandShortcut = useCommandShortcut(commandId) + + return useCommandHotkey({ + shortcut: commandShortcut, + commandId, + when, + args, + }) +} diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/EntryColumnShortcutHandler.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/EntryColumnShortcutHandler.tsx index 17e918885..2d0119c89 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/EntryColumnShortcutHandler.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/EntryColumnShortcutHandler.tsx @@ -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, () => { diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/actions/header-actions.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/actions/header-actions.tsx index dc08ac81a..a76305d51 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/actions/header-actions.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/actions/header-actions.tsx @@ -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 }], }) diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/hooks.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/hooks.tsx index 4087e8626..13cf41526 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/hooks.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/hooks.tsx @@ -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, -) => { - useEffect(() => { - return EventBus.subscribe("FOCUS_ENTRY_CONTAINER", () => { - ref.current?.focus() - }) - }, [ref]) -} - export const useGalleryModal = () => { const { present } = useModalStack() const { t } = useTranslation() diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/index.electron.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/index.electron.tsx index bb55f1fd8..2e6a0f1db 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/index.electron.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/index.electron.tsx @@ -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 = ({ const isInReadabilityMode = useEntryIsInReadability(entryId) const isReadabilitySuccess = useEntryIsInReadabilitySuccess(entryId) - const scrollerRef = useRef(null) - useEffect(() => { - scrollerRef.current?.scrollTo(0, 0) - scrollerRef.current?.focus() - }, [entryId]) - useFocusEntryContainerSubscriptions(scrollerRef) + const scrollerRef = useRef(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 = ({ 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 = ({ /> )} -
+ setIsUserInteraction(true)} + > +
= ({ >
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 = ({
-
+ ) } @@ -286,41 +296,37 @@ const Renderer: React.FC<{ ) }) -const useRegisterCommands = ({ - entryId, +const RegisterCommands = ({ scrollerRef, + isUserInteraction, }: { entryId: string scrollerRef: React.RefObject + 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 } diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/index.shared.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/index.shared.tsx index 012188cb8..4e911f811 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/index.shared.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/index.shared.tsx @@ -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(
{ - 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", diff --git a/apps/desktop/layer/renderer/src/modules/timeline-column/index.tsx b/apps/desktop/layer/renderer/src/modules/timeline-column/index.tsx index 952c4ecff..7ed859516 100644 --- a/apps/desktop/layer/renderer/src/modules/timeline-column/index.tsx +++ b/apps/desktop/layer/renderer/src/modules/timeline-column/index.tsx @@ -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, }) diff --git a/apps/desktop/layer/renderer/src/providers/global-hotkeys-provider.tsx b/apps/desktop/layer/renderer/src/providers/global-hotkeys-provider.tsx index 7306820fe..7f6c85298 100644 --- a/apps/desktop/layer/renderer/src/providers/global-hotkeys-provider.tsx +++ b/apps/desktop/layer/renderer/src/providers/global-hotkeys-provider.tsx @@ -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 diff --git a/packages/internal/components/src/common/Focusable.tsx b/packages/internal/components/src/common/Focusable.tsx index 08e87fee1..50fbca1b6 100644 --- a/packages/internal/components/src/common/Focusable.tsx +++ b/packages/internal/components/src/common/Focusable.tsx @@ -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>(null!) export const Focusable: Component< React.DetailedHTMLProps, HTMLDivElement> > = ({ ref, ...props }) => { const { onBlur, onFocus, ...rest } = props const [isFocusWithIn, setIsFocusWithIn] = useState(false) - const handleFocus = useCallback( - (e: FocusEvent) => { - onFocus?.(e) + const focusTargetRef = useRef(void 0) + + const containerRef = useRef(null) + useImperativeHandle(ref, () => containerRef.current!) + useEventListener("focusin", (e) => { + if (containerRef.current?.contains(e.target as Node)) { setIsFocusWithIn(true) - }, - [onFocus], - ) - const handleBlur = useCallback( - (e: FocusEvent) => { - onBlur?.(e) + } else { setIsFocusWithIn(false) - }, - [onBlur], - ) + } + }) + + // useEventListener("focusout", (e) => { + // if (!containerRef.current?.contains(e.target as Node)) { + // setIsFocusWithIn(false) + // } + // }) return ( -
+ +
+ ) } @@ -42,3 +39,7 @@ export const Focusable: Component< export const useFocusable = () => { return use(FocusableContext) } + +export const useFocusTargetRef = () => { + return use(FocusTargetRefContext) +} diff --git a/packages/internal/utils/src/utils.ts b/packages/internal/utils/src/utils.ts index 2db0c0f8e..71b7c7726 100644 --- a/packages/internal/utils/src/utils.ts +++ b/packages/internal/utils/src/utils.ts @@ -407,3 +407,13 @@ export function duplicateIfLengthLessThan(text: string, length: number) { ? text.repeat(Math.ceil(length / text.length)) : text } + +export function combineCleanupFunctions(...fns: Array void) | void>>) { + return () => { + fns.forEach((fn) => { + if (typeof fn === "function") { + fn() + } + }) + } +}