From c22f6cb7a97407d7af9b683764d8269744505071 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:33:39 -0700 Subject: [PATCH] Enable scrolling via J and K keys --- oxlint-suppressions.json | 11 - src/components/Link.tsx | 33 ++- src/lib/hotkeys/KeyboardActivation.tsx | 27 ++ src/lib/hotkeys/index.native.tsx | 20 +- src/lib/hotkeys/index.tsx | 233 +++++++++++++++++- src/lib/hotkeys/types.ts | 22 ++ src/screens/Search/Explore.tsx | 182 +++++++++----- src/screens/Search/SearchResults.tsx | 88 ++++++- .../Search/components/ModuleHeader.tsx | 3 + .../Search/modules/ExploreInterestsCard.tsx | 9 +- src/state/events.ts | 24 ++ src/view/com/feeds/FeedPage.tsx | 1 + src/view/com/post/Post.tsx | 6 + src/view/com/posts/PostFeed.tsx | 88 +++++-- src/view/com/posts/PostFeedItem.tsx | 20 +- src/view/com/util/Link.tsx | 95 +++---- src/view/screens/Feeds.tsx | 83 +++++-- 17 files changed, 755 insertions(+), 190 deletions(-) create mode 100644 src/lib/hotkeys/KeyboardActivation.tsx create mode 100644 src/lib/hotkeys/types.ts diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index ae3513a254..6302501cab 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -1739,17 +1739,6 @@ "count": 1 } }, - "src/view/screens/Feeds.tsx": { - "typescript/no-floating-promises": { - "count": 3 - }, - "typescript/no-misused-promises": { - "count": 1 - }, - "typescript/no-unsafe-member-access": { - "count": 1 - } - }, "src/view/screens/Home.tsx": { "typescript/no-floating-promises": { "count": 1 diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 6d60edb615..f187f0abf3 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -4,6 +4,7 @@ import { Linking, type NativeSyntheticEvent, type TargetedEvent, + type View, } from 'react-native' import {sanitizeUrl} from '@braintree/sanitize-url' import {useLingui} from '@lingui/react/macro' @@ -16,6 +17,7 @@ import {BSKY_DOWNLOAD_URL} from '#/lib/constants' import {useGroupChatJoinIntent} from '#/lib/hooks/useIntentHandler' import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped' import {useOpenLink} from '#/lib/hooks/useOpenLink' +import * as KeyboardActivation from '#/lib/hotkeys/KeyboardActivation' import {type AllNavigatorParams, type RouteParams} from '#/lib/routes/types' import {shareUrl} from '#/lib/sharing' import { @@ -144,8 +146,8 @@ export function useLink({ const groupChatJoinIntent = useGroupChatJoinIntent() const onPress = useCallback( - (e: GestureResponderEvent) => { - const exitEarlyIfFalse = outerOnPress?.(e) + (e?: GestureResponderEvent) => { + const exitEarlyIfFalse = outerOnPress?.(e as GestureResponderEvent) if (exitEarlyIfFalse === false) return @@ -157,7 +159,7 @@ export function useLink({ ) if (IS_WEB) { - e.preventDefault() + e?.preventDefault() } const chatInviteCode = getChatInviteCodeFromUrl(href) @@ -175,7 +177,7 @@ export function useLink({ if (isExternal) { void openLink(href, overridePresentation, shouldProxy) } else { - const shouldOpenInNewTab = shouldClickOpenNewTab(e) + const shouldOpenInNewTab = e ? shouldClickOpenNewTab(e) : false if (isBskyDownloadUrl(href)) { void shareUrl(BSKY_DOWNLOAD_URL) @@ -300,6 +302,7 @@ export function useLink({ export type LinkProps = Omit & Omit & { overridePresentation?: boolean + ref?: React.Ref } /** @@ -320,6 +323,7 @@ export function Link({ shouldProxy, overridePresentation, peek, + ref, ...rest }: LinkProps) { const {href, isExternal, onPress, onLongPress, openExternally} = useLink({ @@ -331,12 +335,15 @@ export function Link({ shouldProxy: shouldProxy, overridePresentation, }) + const activate = useCallback(() => onPress(), [onPress]) + KeyboardActivation.useRegistration(activate) // Peek is iOS-only and only makes sense for external web links. const peekEnabled = Boolean(peek && IS_IOS && isExternal) const button = (