mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
🎯 fix: Correct Off-by-One Rail Scrub After Pinning the Terminus (#14409)
Some checks failed
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Helm Chart Tags / Ignore non-main push (push) Has been cancelled
Sync Helm Chart Tags / Sync chart tags (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
Some checks failed
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Helm Chart Tags / Ignore non-main push (push) Has been cancelled
Sync Helm Chart Tags / Sync chart tags (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
Pinning the scroll-to-bottom rib moved it out of the column, but scrubTo kept enumerating ribs from the nav (messages + terminus) while measuring the fraction against the column, which now spans the messages alone. Every drag position mapped one rib late: pointing at the middle of the rail scrolled to the message below the rib under the cursor. Enumerate the column's own ribs for the proportional mapping and reach the terminus by dragging past the column's bottom edge, where it now sits.
This commit is contained in:
parent
142973e7e8
commit
21dc4a2ef4
2 changed files with 72 additions and 5 deletions
|
|
@ -528,16 +528,22 @@ function MessageNav({ scrollableRef }: { scrollableRef: React.RefObject<HTMLDivE
|
|||
const scrubTo = useCallback(
|
||||
(clientY: number) => {
|
||||
const col = columnRef.current;
|
||||
const nav = navRef.current;
|
||||
if (!col || !nav) {
|
||||
if (!col) {
|
||||
return;
|
||||
}
|
||||
const ribs = nav.querySelectorAll<HTMLElement>('[data-msg-id]');
|
||||
const rect = col.getBoundingClientRect();
|
||||
/** The terminus is pinned below the column, so the pointer reaches it by
|
||||
* travelling past the bottom edge — the proportional mapping covers only
|
||||
* the ribs the column actually spans, or every position lands one late. */
|
||||
if (endEntry && clientY >= rect.bottom) {
|
||||
scrollToImmediate(MESSAGES_END_ID);
|
||||
return;
|
||||
}
|
||||
const ribs = col.querySelectorAll<HTMLElement>('[data-msg-id]');
|
||||
const count = ribs.length;
|
||||
if (count === 0) {
|
||||
return;
|
||||
}
|
||||
const rect = col.getBoundingClientRect();
|
||||
const fraction = rect.height > 0 ? (clientY - rect.top) / rect.height : 0;
|
||||
const index = Math.max(0, Math.min(count - 1, Math.round(fraction * (count - 1))));
|
||||
const id = ribs[index].getAttribute('data-msg-id');
|
||||
|
|
@ -545,7 +551,7 @@ function MessageNav({ scrollableRef }: { scrollableRef: React.RefObject<HTMLDivE
|
|||
scrollToImmediate(id);
|
||||
}
|
||||
},
|
||||
[scrollToImmediate],
|
||||
[scrollToImmediate, endEntry],
|
||||
);
|
||||
|
||||
const handlePointerDown = useCallback(
|
||||
|
|
|
|||
|
|
@ -1645,6 +1645,67 @@ describe('MessageNav', () => {
|
|||
expect(column.scrollTop).toBe(13);
|
||||
});
|
||||
|
||||
it('scrubs to the rib under the pointer, not one past it, with the terminus pinned', () => {
|
||||
const messages = Array.from({ length: 5 }, (_, i) =>
|
||||
buildMessage({ messageId: `m-${i}`, text: `message ${i}`, isCreatedByUser: i % 2 === 0 }),
|
||||
);
|
||||
const { container } = renderNavWithEnd(messages);
|
||||
const column = container.querySelector('nav > div') as HTMLDivElement;
|
||||
column.getBoundingClientRect = () => ({ top: 0, bottom: 50, height: 50 }) as DOMRect;
|
||||
const getById = jest.spyOn(document, 'getElementById');
|
||||
|
||||
act(() => {
|
||||
fireEvent.pointerDown(column, { pointerId: 1, button: 0, buttons: 1, clientY: 0 });
|
||||
fireEvent.pointerMove(document, { pointerId: 1, buttons: 1, clientY: 25 });
|
||||
});
|
||||
|
||||
const scrubbed = getById.mock.calls.map((c) => c[0]);
|
||||
expect(scrubbed).toContain('m-2');
|
||||
expect(scrubbed).not.toContain('m-3');
|
||||
getById.mockRestore();
|
||||
});
|
||||
|
||||
it('peaks the fisheye and preview on the rib under the pointer', () => {
|
||||
const messages = Array.from({ length: 6 }, (_, i) =>
|
||||
buildMessage({ messageId: `m-${i}`, text: `message ${i}` }),
|
||||
);
|
||||
const asRect = (top: number, height: number): DOMRect =>
|
||||
({
|
||||
top,
|
||||
bottom: top + height,
|
||||
height,
|
||||
left: 200,
|
||||
right: 214,
|
||||
width: 14,
|
||||
x: 200,
|
||||
y: top,
|
||||
toJSON: () => ({}),
|
||||
}) as DOMRect;
|
||||
/** Rib i occupies [i*12, i*12+6] — a 6px rib on a 6px gap. */
|
||||
const rectSpy = jest
|
||||
.spyOn(Element.prototype, 'getBoundingClientRect')
|
||||
.mockImplementation(function (this: Element) {
|
||||
const id = this.getAttribute?.('data-msg-id');
|
||||
const index = id != null ? messages.findIndex((m) => m.messageId === id) : -1;
|
||||
return index >= 0 ? asRect(index * 12, 6) : asRect(0, messages.length * 12);
|
||||
});
|
||||
|
||||
const { container } = renderNavWithEnd(messages);
|
||||
const column = container.querySelector('nav > div') as HTMLDivElement;
|
||||
|
||||
act(() => {
|
||||
fireEvent.pointerMove(column, { pointerId: 1, clientY: 3 * 12 + 3 });
|
||||
jest.advanceTimersByTime(80);
|
||||
});
|
||||
|
||||
expect(document.body.querySelector('[role="tooltip"]')).toHaveTextContent('message 3');
|
||||
const highlighted = Array.from(container.querySelectorAll('[data-msg-id]')).filter((r) =>
|
||||
r.querySelector('span')?.className.includes('bg-gray-800'),
|
||||
);
|
||||
expect(highlighted.map((r) => r.getAttribute('data-msg-id'))).toEqual(['m-3']);
|
||||
rectSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('starts a scrub drag from the pinned terminus', () => {
|
||||
const messages = Array.from({ length: 5 }, (_, i) =>
|
||||
buildMessage({ messageId: `m-${i}`, text: `message ${i}`, isCreatedByUser: i % 2 === 0 }),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue