Skip to content

Commit 5b8eee2

Browse files
Yash-Singh1rynfar
authored andcommitted
feat(web): add PageUp/PageDown chat navigation (#9315)
(cherry picked from commit 5f84efa1ec2fb3cd6f6c54545cc34bb77ac1ddb8)
1 parent d372b9d commit 5b8eee2

5 files changed

Lines changed: 685 additions & 0 deletions

File tree

apps/web/src/components/ChatView.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import {
7878
Suspense,
7979
useCallback,
8080
useEffect,
81+
useEffectEvent,
8182
useLayoutEffect,
8283
useMemo,
8384
useRef,
@@ -336,6 +337,7 @@ import {
336337
} from "../state/entities";
337338
import { environmentShell } from "../state/shell";
338339
import { ChatComposer, type ChatComposerHandle } from "./chat/ChatComposer";
340+
import { createPageScrollController, type PageScrollKey } from "./chat/pageScrollController";
339341
import { DraftHeroHeadline } from "./chat/DraftHeroHeadline";
340342
import { ExpandedImageDialog } from "./chat/ExpandedImageDialog";
341343
import { PullRequestThreadDialog } from "./PullRequestThreadDialog";
@@ -4482,6 +4484,38 @@ function ChatViewContent(props: ChatViewProps) {
44824484
},
44834485
[composerOverlayHeight],
44844486
);
4487+
const pageScrollControllerRef = useRef<ReturnType<typeof createPageScrollController> | null>(
4488+
null,
4489+
);
4490+
const handlePageScrollStart = useEffectEvent((key: PageScrollKey) => {
4491+
if (key === "PageUp" && timelineRealContentOverflowsViewport()) {
4492+
cancelTimelineLiveFollowForUserNavigation();
4493+
}
4494+
});
4495+
useEffect(() => {
4496+
const controller = createPageScrollController({
4497+
getContainer: () => legendListRef.current?.getScrollableNode() ?? null,
4498+
getScrollPaddingBottomPx: () => composerOverlayElement?.getBoundingClientRect().height ?? 0,
4499+
onScrollStart: handlePageScrollStart,
4500+
});
4501+
pageScrollControllerRef.current = controller;
4502+
4503+
return () => {
4504+
controller.dispose();
4505+
if (pageScrollControllerRef.current === controller) {
4506+
pageScrollControllerRef.current = null;
4507+
}
4508+
};
4509+
}, [composerOverlayElement]);
4510+
const onComposerPageScrollKeyDown = useCallback((key: PageScrollKey) => {
4511+
pageScrollControllerRef.current?.handleKeyDown(key);
4512+
}, []);
4513+
const onComposerPageScrollKeyUp = useCallback((key: string) => {
4514+
pageScrollControllerRef.current?.handleKeyUp(key);
4515+
}, []);
4516+
const onComposerPageScrollRelease = useCallback(() => {
4517+
pageScrollControllerRef.current?.releaseActiveKey();
4518+
}, []);
44854519
// Live-follow stays active after send/thread-open until an actual list scroll
44864520
// gesture opts out.
44874521
const scrollToEnd = useCallback((animated = false) => {
@@ -8631,6 +8665,9 @@ function ChatViewContent(props: ChatViewProps) {
86318665
composerFilesRef={composerFilesRef}
86328666
composerTerminalContextsRef={composerTerminalContextsRef}
86338667
composerElementContextsRef={composerElementContextsRef}
8668+
onPageScrollKeyDown={onComposerPageScrollKeyDown}
8669+
onPageScrollKeyUp={onComposerPageScrollKeyUp}
8670+
onPageScrollRelease={onComposerPageScrollRelease}
86348671
onSend={onSend}
86358672
onQueueFollowUp={() =>
86368673
onSend(undefined, "foreground", undefined, "follow-up")

apps/web/src/components/ComposerPromptEditor.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ import {
8383
} from "./composerInlineChip";
8484
import { FILE_TAG_CHIP_CLASS_NAME, FileTagChipContent } from "./chat/FileTagChip";
8585
import { ComposerPendingTerminalContextChip } from "./chat/ComposerPendingTerminalContexts";
86+
import { getTimelinePageScrollKey } from "./chat/pageScrollController";
8687
import { formatProviderSkillDisplayName } from "@t3tools/client-runtime/providerSkills";
8788
import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
8889
import { registerComposerInlineTokenPaste } from "./composerInlineTokenPaste";
@@ -916,6 +917,9 @@ interface ComposerPromptEditorProps {
916917
key: "ArrowDown" | "ArrowUp" | "Enter" | "Tab",
917918
event: KeyboardEvent,
918919
) => boolean;
920+
onPageScrollKeyDown?: (key: "PageUp" | "PageDown") => void;
921+
onPageScrollKeyUp?: (key: string) => void;
922+
onPageScrollRelease?: () => void;
919923
onPaste: React.ClipboardEventHandler<HTMLElement>;
920924
editorRef: React.RefObject<ComposerPromptEditorHandle | null>;
921925
}
@@ -1557,6 +1561,9 @@ function ComposerPromptEditorInner({
15571561
onRemoveTerminalContext,
15581562
onChange,
15591563
onCommandKeyDown,
1564+
onPageScrollKeyDown,
1565+
onPageScrollKeyUp,
1566+
onPageScrollRelease,
15601567
onPaste,
15611568
editorRef,
15621569
}: ComposerPromptEditorProps) {
@@ -1829,6 +1836,46 @@ function ComposerPromptEditorInner({
18291836
data-testid="composer-editor"
18301837
aria-placeholder={placeholder}
18311838
placeholder={<span />}
1839+
onKeyDown={(event) => {
1840+
if (
1841+
event.key === "Control" ||
1842+
event.key === "Meta" ||
1843+
event.key === "Alt" ||
1844+
event.key === "Shift"
1845+
) {
1846+
onPageScrollRelease?.();
1847+
}
1848+
1849+
if (event.key !== "PageUp" && event.key !== "PageDown") {
1850+
return;
1851+
}
1852+
1853+
const pageScrollKey = getTimelinePageScrollKey({
1854+
altKey: event.altKey,
1855+
clientHeight: event.currentTarget.clientHeight,
1856+
ctrlKey: event.ctrlKey,
1857+
defaultPrevented: event.defaultPrevented,
1858+
isComposing: event.nativeEvent.isComposing,
1859+
key: event.key,
1860+
keyCode: event.keyCode,
1861+
metaKey: event.metaKey,
1862+
scrollHeight: event.currentTarget.scrollHeight,
1863+
scrollTop: event.currentTarget.scrollTop,
1864+
shiftKey: event.shiftKey,
1865+
});
1866+
if (!pageScrollKey) {
1867+
onPageScrollRelease?.();
1868+
return;
1869+
}
1870+
if (!onPageScrollKeyDown) {
1871+
return;
1872+
}
1873+
1874+
event.preventDefault();
1875+
onPageScrollKeyDown(pageScrollKey);
1876+
}}
1877+
onKeyUp={(event) => onPageScrollKeyUp?.(event.key)}
1878+
onBlur={onPageScrollRelease}
18321879
onPaste={onPaste}
18331880
/>
18341881
}
@@ -1868,6 +1915,9 @@ export function ComposerPromptEditor({
18681915
onRemoveTerminalContext,
18691916
onChange,
18701917
onCommandKeyDown,
1918+
onPageScrollKeyDown,
1919+
onPageScrollKeyUp,
1920+
onPageScrollRelease,
18711921
onPaste,
18721922
editorRef,
18731923
}: ComposerPromptEditorProps) {
@@ -1912,6 +1962,9 @@ export function ComposerPromptEditor({
19121962
onPaste={onPaste}
19131963
editorRef={editorRef}
19141964
{...(onCommandKeyDown ? { onCommandKeyDown } : {})}
1965+
{...(onPageScrollKeyDown ? { onPageScrollKeyDown } : {})}
1966+
{...(onPageScrollKeyUp ? { onPageScrollKeyUp } : {})}
1967+
{...(onPageScrollRelease ? { onPageScrollRelease } : {})}
19151968
{...(className ? { className } : {})}
19161969
/>
19171970
</LexicalComposer>

apps/web/src/components/chat/ChatComposer.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,9 @@ export interface ChatComposerProps {
840840
composerTerminalContextsRef: React.RefObject<TerminalContextDraft[]>;
841841
composerElementContextsRef: React.RefObject<ElementContextDraft[]>;
842842
composerRef: React.RefObject<ChatComposerHandle | null>;
843+
onPageScrollKeyDown: (key: "PageUp" | "PageDown") => void;
844+
onPageScrollKeyUp: (key: string) => void;
845+
onPageScrollRelease: () => void;
843846

844847
// Cross-account handoff. Resolved by the parent, which owns the thread
845848
// creation the offer leads to; the composer only shows it.
@@ -969,6 +972,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
969972
threadHandoffOffer,
970973
isContinuingThreadOnAccount,
971974
isStartingProviderConflictThread,
975+
onPageScrollKeyDown,
976+
onPageScrollKeyUp,
977+
onPageScrollRelease,
972978
onSend,
973979
onQueueFollowUp,
974980
onClearSessionInputQueue,
@@ -5008,6 +5014,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
50085014
onRemoveTerminalContext={removeComposerTerminalContextFromDraft}
50095015
onChange={onPromptChange}
50105016
onCommandKeyDown={onComposerCommandKey}
5017+
onPageScrollKeyDown={onPageScrollKeyDown}
5018+
onPageScrollKeyUp={onPageScrollKeyUp}
5019+
onPageScrollRelease={onPageScrollRelease}
50115020
onPaste={onComposerPaste}
50125021
placeholder={
50135022
isComposerApprovalState

0 commit comments

Comments
 (0)