Skip to content

Commit c20350f

Browse files
committed
feat: enhance focus management and context menu shortcuts
- Added new focusable presets for timeline and entry render states. - Introduced a custom hook `useContextMenuActionShortCutTrigger` to manage context menu shortcuts based on focusable states. - Updated various components to utilize the new focusable presets and context menu shortcuts for improved user interaction. Signed-off-by: Innei <[email protected]>
1 parent 1f53a97 commit c20350f

File tree

16 files changed

+102
-22
lines changed

16 files changed

+102
-22
lines changed

apps/desktop/layer/renderer/src/components/common/Focusable.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,6 @@ export const FocusablePresets = {
2424
isSubscriptionOrTimeline: (v: Set<string>) => {
2525
return v.has(HotkeyScope.SubscriptionList) || v.has(HotkeyScope.Timeline) || v.size === 0
2626
},
27-
}
27+
isTimeline: (v) => v.has(HotkeyScope.Timeline) && !v.has(HotkeyScope.EntryRender),
28+
isEntryRender: (v) => v.has(HotkeyScope.EntryRender),
29+
} satisfies Record<string, (v: Set<string>) => boolean>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { checkIsEditableElement } from "@follow/utils"
2+
import { useEffect } from "react"
3+
import { tinykeys } from "tinykeys"
4+
5+
import type { MenuItemInput } from "~/atoms/context-menu"
6+
import { MenuItemText } from "~/atoms/context-menu"
7+
8+
export const useContextMenuActionShortCutTrigger = (items: MenuItemInput[], when: boolean) => {
9+
useEffect(() => {
10+
if (!when) return
11+
12+
const actionMap = items.reduce(
13+
(acc, item) => {
14+
if (item instanceof MenuItemText) {
15+
if (!item.shortcut) return acc
16+
acc[item.shortcut] = (event: KeyboardEvent) => {
17+
if (checkIsEditableElement(event.target as HTMLElement)) return
18+
event.preventDefault()
19+
event.stopPropagation()
20+
if (item.disabled) return
21+
if (item.hide) return
22+
item.click()
23+
}
24+
}
25+
return acc
26+
},
27+
28+
{} as Record<string, (e: KeyboardEvent) => void>,
29+
)
30+
31+
return tinykeys(window, actionMap)
32+
}, [items, when])
33+
}

apps/desktop/layer/renderer/src/modules/app-layout/feed-column/desktop.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ const FeedResponsiveResizerContainer = ({
242242

243243
useCommandBinding({
244244
commandId: COMMAND_ID.layout.toggleSubscriptionColumn,
245-
when: !FloatingLayerScope.some((scope) => activeScopes.has(scope)),
245+
when: !activeScopes.or(...FloatingLayerScope),
246246
})
247247

248248
const [delayShowSplitter, setDelayShowSplitter] = useState(feedColumnShow)

apps/desktop/layer/renderer/src/modules/entry-column/EntryColumnShortcutHandler.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { EventBus } from "@follow/utils/event-bus"
99
import type { FC } from "react"
1010
import { memo, useEffect } from "react"
1111

12-
import { HotkeyScope } from "~/constants"
12+
import { FocusablePresets } from "~/components/common/Focusable"
1313
import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry"
1414
import { useRouteEntryId } from "~/hooks/biz/useRouteParams"
1515

@@ -26,7 +26,7 @@ export const EntryColumnShortcutHandler: FC<{
2626

2727
const activeScope = useGlobalFocusableScope()
2828

29-
const when = activeScope.has(HotkeyScope.Timeline) && !activeScope.has(HotkeyScope.EntryRender)
29+
const when = FocusablePresets.isTimeline(activeScope)
3030

3131
useCommandBinding({
3232
commandId: COMMAND_ID.timeline.switchToNext,

apps/desktop/layer/renderer/src/modules/entry-column/components/mark-all-button.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,7 @@ export const MarkAllReadButton = ({
3636
const activeScope = useGlobalFocusableScope()
3737
useCommandBinding({
3838
commandId: COMMAND_ID.subscription.markAllAsRead,
39-
when: [HotkeyScope.Timeline, HotkeyScope.SubscriptionList].some((scope) =>
40-
activeScope.has(scope),
41-
),
39+
when: activeScope.or(HotkeyScope.Timeline, HotkeyScope.SubscriptionList),
4240
})
4341

4442
useEffect(() => {

apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryItemWrapper.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useGlobalFocusableScope } from "@follow/components/common/Focusable/hooks.js"
12
import { useMobile } from "@follow/components/hooks/useMobile.js"
23
import type { FeedViewType } from "@follow/constants"
34
import { views } from "@follow/constants"
@@ -15,7 +16,9 @@ import {
1516
useShowContextMenu,
1617
} from "~/atoms/context-menu"
1718
import { useGeneralSettingKey } from "~/atoms/settings/general"
19+
import { FocusablePresets } from "~/components/common/Focusable"
1820
import { useEntryIsRead } from "~/hooks/biz/useAsRead"
21+
import { useContextMenuActionShortCutTrigger } from "~/hooks/biz/useContextMenuActionShortCutTrigger"
1922
import { useEntryActions } from "~/hooks/biz/useEntryActions"
2023
import { useFeedActions } from "~/hooks/biz/useFeedActions"
2124
import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry"
@@ -34,6 +37,7 @@ export const EntryItemWrapper: FC<
3437
} & PropsWithChildren
3538
> = ({ entry, view, children, itemClassName, style }) => {
3639
const actionConfigs = useEntryActions({ entryId: entry.entries.id })
40+
3741
const feedItems = useFeedActions({
3842
feedId: entry.feedId || entry.inboxId,
3943
view,
@@ -47,6 +51,8 @@ export const EntryItemWrapper: FC<
4751
({ entryId }) => entryId === entry.entries.id,
4852
[entry.entries.id],
4953
)
54+
const scope = useGlobalFocusableScope()
55+
useContextMenuActionShortCutTrigger(actionConfigs, isActive && FocusablePresets.isTimeline(scope))
5056

5157
const asRead = useEntryIsRead(entry)
5258
const hoverMarkUnread = useGeneralSettingKey("hoverMarkUnread")

apps/desktop/layer/renderer/src/modules/entry-content/actions/header-actions.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { useGlobalFocusableScopeSelector } from "@follow/components/common/Focusable/hooks.js"
12
import type { FeedViewType } from "@follow/constants"
23

34
import { MenuItemText } from "~/atoms/context-menu"
5+
import { FocusablePresets } from "~/components/common/Focusable"
46
import { CommandActionButton } from "~/components/ui/button/CommandActionButton"
57
import { useHasModal } from "~/components/ui/modal/stacked/hooks"
68
import { useSortedEntryActions } from "~/hooks/biz/useEntryActions"
@@ -22,8 +24,10 @@ export const EntryHeaderActions = ({
2224

2325
const hasModal = useHasModal()
2426

27+
const when = useGlobalFocusableScopeSelector(FocusablePresets.isEntryRender)
28+
2529
useCommandBinding({
26-
when: !!entry?.entries.url && !hasModal,
30+
when: !!entry?.entries.url && !hasModal && when,
2731
commandId: COMMAND_ID.entry.openInBrowser,
2832
args: [{ entryId }],
2933
})
@@ -35,7 +39,7 @@ export const EntryHeaderActions = ({
3539
<CommandActionButton
3640
active={config.active}
3741
key={config.id}
38-
disableTriggerShortcut={hasModal}
42+
disableTriggerShortcut={!when}
3943
commandId={config.id}
4044
onClick={config.onClick!}
4145
shortcut={config.shortcut!}

apps/desktop/layer/renderer/src/modules/entry-content/index.electron.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {
22
useFocusActions,
3-
useGlobalFocusableScope,
3+
useGlobalFocusableScopeSelector,
44
} from "@follow/components/common/Focusable/index.js"
55
import { MemoedDangerousHTMLStyle } from "@follow/components/common/MemoedDangerousHTMLStyle.js"
66
import { Spring } from "@follow/components/constants/spring.js"
@@ -21,7 +21,7 @@ import { useEffect, useMemo, useRef, useState } from "react"
2121

2222
import { useEntryIsInReadability } from "~/atoms/readability"
2323
import { useIsZenMode, useUISettingKey } from "~/atoms/settings/ui"
24-
import { Focusable } from "~/components/common/Focusable"
24+
import { Focusable, FocusablePresets } from "~/components/common/Focusable"
2525
import { ShadowDOM } from "~/components/common/ShadowDOM"
2626
import type { TocRef } from "~/components/ui/markdown/components/Toc"
2727
import { useInPeekModal } from "~/components/ui/modal/inspire/InPeekModal"
@@ -316,8 +316,7 @@ const RegisterCommands = ({
316316
const isAlreadyScrolledBottomRef = useRef(false)
317317
const [showKeepScrollingPanel, setShowKeepScrollingPanel] = useState(false)
318318

319-
const activeScope = useGlobalFocusableScope()
320-
const when = activeScope.has(HotkeyScope.EntryRender)
319+
const when = useGlobalFocusableScopeSelector(FocusablePresets.isEntryRender)
321320

322321
useCommandBinding({
323322
commandId: COMMAND_ID.entryRender.scrollUp,

apps/desktop/layer/renderer/src/modules/subscription-column/FeedItem.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useGlobalFocusableScope } from "@follow/components/common/Focusable/hooks.js"
12
import { useMobile } from "@follow/components/hooks/useMobile.js"
23
import { OouiUserAnonymous } from "@follow/components/icons/OouiUserAnonymous.jsx"
34
import { Button } from "@follow/components/ui/button/index.js"
@@ -16,6 +17,8 @@ import { useTranslation } from "react-i18next"
1617
import { MenuItemSeparator, MenuItemText, useShowContextMenu } from "~/atoms/context-menu"
1718
import { useHideAllReadSubscriptions } from "~/atoms/settings/general"
1819
import { ErrorTooltip } from "~/components/common/ErrorTooltip"
20+
import { FocusablePresets } from "~/components/common/Focusable"
21+
import { useContextMenuActionShortCutTrigger } from "~/hooks/biz/useContextMenuActionShortCutTrigger"
1922
import { useFeedActions, useInboxActions, useListActions } from "~/hooks/biz/useFeedActions"
2023
import { useFollow } from "~/hooks/biz/useFollow"
2124
import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry"
@@ -117,6 +120,11 @@ const FeedItemImpl = ({ view, feedId, className, isPreview }: FeedItemProps) =>
117120
view,
118121
})
119122

123+
const scope = useGlobalFocusableScope()
124+
125+
const whenTrigger = FocusablePresets.isSubscriptionList(scope) && isActive
126+
useContextMenuActionShortCutTrigger(items, whenTrigger)
127+
120128
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false)
121129
const showContextMenu = useShowContextMenu()
122130
const contextMenuProps = useContextMenu({
@@ -252,6 +260,9 @@ const ListItemImpl: Component<ListItemProps> = ({
252260
const isActive = useRouteParamsSelector((routerParams) => routerParams.listId === listId)
253261
const items = useListActions({ listId, view })
254262

263+
const scope = useGlobalFocusableScope()
264+
useContextMenuActionShortCutTrigger(items, FocusablePresets.isSubscriptionList(scope) && isActive)
265+
255266
const listUnread = useUnreadByListId(listId)
256267

257268
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false)
@@ -358,6 +369,9 @@ const InboxItemImpl: Component<InboxItemProps> = ({ view, inboxId, className, ic
358369
const isActive = useRouteParamsSelector((routerParams) => routerParams.inboxId === inboxId)
359370
const { items } = useInboxActions({ inboxId })
360371

372+
const scope = useGlobalFocusableScope()
373+
useContextMenuActionShortCutTrigger(items, FocusablePresets.isSubscriptionList(scope) && isActive)
374+
361375
const inboxUnread = useUnreadById(inboxId)
362376

363377
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false)

apps/desktop/layer/renderer/src/modules/subscription-column/index.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,7 @@ const CommandsHandler = ({
245245
}) => {
246246
const activeScope = useGlobalFocusableScope()
247247
const when =
248-
activeScope.has(HotkeyScope.SubscriptionList) ||
249-
activeScope.has(HotkeyScope.Timeline) ||
250-
activeScope.size === 0
248+
activeScope.or(HotkeyScope.SubscriptionList, HotkeyScope.Timeline) || activeScope.size === 0
251249

252250
useCommandBinding({
253251
commandId: COMMAND_ID.subscription.switchTabToNext,

0 commit comments

Comments
 (0)