Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/src/components/chat/ChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2482,7 +2482,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)

const scrollNode = getTimelineScrollableNode();
if (!scrollNode) return;
const targetsTimeline = isTimelineScrollTarget(event.target, scrollNode, event.deltaY);
const targetsTimeline = isTimelineScrollTarget(event.target, scrollNode, event.deltaY, event);
if (
!scrollNode.contains(event.target) &&
!composerScrollGestureRef.current.collapseSuppressed
Expand Down
43 changes: 43 additions & 0 deletions apps/web/src/components/chat/timelineScrollTarget.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,49 @@ beforeEach(() => {
afterEach(() => vi.unstubAllGlobals());

describe("timeline scroll targets", () => {
it("retains vertical wheel and keyboard scroll intent", () => {
const { timeline } = setup();
const content = new ScrollElement(timeline);
expect(
isTimelineScrollTarget(content, timeline as unknown as HTMLElement, -30, {
deltaX: 5,
shiftKey: false,
}),
).toBe(true);
expect(targetsTimeline(content, timeline, -30)).toBe(true);
expect(
isTimelineScrollTarget(content, timeline as unknown as HTMLElement, 0, {
deltaX: 80,
shiftKey: false,
}),
).toBe(false);
});
it.each([
{ deltaX: 80, deltaY: -30, shiftKey: false },
{ deltaX: -80, deltaY: 30, shiftKey: false },
{ deltaX: 0, deltaY: -30, shiftKey: true },
])("keeps horizontal gestures out of composer collapse and timeline follow: %j", (gesture) => {
const { timeline } = setup();
const table = new ScrollElement(timeline);
const targets = isTimelineScrollTarget(
table,
timeline as unknown as HTMLElement,
gesture.deltaY,
gesture,
);
expect(targets).toBe(false);
expect(
recordComposerScrollGestureEvent(createComposerScrollGestureState(), {
now: 0,
deltaPx: Math.abs(gesture.deltaY),
collapseThresholdPx: 24,
collapseEligible: targets,
canScrollInGestureDirection: true,
scrollsTowardLogicalEnd: false,
}),
).toBe(false);
});

it.each([-30, 30])("keeps a nested tool group's scroll out of the timeline: %i", (deltaY) => {
const { timeline, group, content } = setup();
expect(targetsTimeline(content, timeline, deltaY)).toBe(false);
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/components/chat/timelineScrollTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ export function isTimelineScrollTarget(
target: EventTarget | null,
timeline: HTMLElement,
deltaY: number,
wheel?: Pick<WheelEvent, "deltaX" | "shiftKey">,
): boolean {
if (!(target instanceof Element) || !timeline.contains(target) || deltaY === 0) return false;
// Shift-wheel scrolls sideways; trackpads can add vertical noise to a horizontal swipe.
if (wheel?.shiftKey || Math.abs(wheel?.deltaX ?? 0) > Math.abs(deltaY)) return false;

for (
let element: Element | null = target;
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/components/chat/useTimelineFollow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,10 @@ export function useTimelineFollow(input: TimelineFollowInput) {
const viewportIsAwayFromEnd = () =>
resolveTimelineIsAtEnd(legendListRef.current?.getState()) === false;
const handleWheel = (event: WheelEvent) => {
if (event.ctrlKey || !isTimelineScrollTarget(event.target, scrollNode, event.deltaY))
if (
event.ctrlKey ||
!isTimelineScrollTarget(event.target, scrollNode, event.deltaY, event)
)
return;
if (event.deltaY > 0) {
timelineScrollIntentRef.current = "toward-end";
Expand Down
Loading