Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 6 additions & 0 deletions .changeset/lovely-insects-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@tapsioss/client-socket-manager": patch
---

Preserve the scroll position of channels and logs section inside the DevTool.

119 changes: 119 additions & 0 deletions packages/core/src/devtool/ScrollPreservor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { beforeEach, describe, expect, it } from "vitest";
import { ScrollPreservor } from "./ScrollPreservor.ts";

describe("ScrollPreservor", () => {
let element: HTMLElement;
let scrollPreservor: ScrollPreservor;

beforeEach(() => {
// Set up a mock HTML element for testing
element = document.createElement("div");
Object.defineProperty(element, "scrollTop", { writable: true, value: 0 });
scrollPreservor = new ScrollPreservor({ target: element });
});

it("should be able to set and save the scroll position", () => {
const scrollPosition = 100;

element.scrollTop = scrollPosition;

scrollPreservor.save();

expect(scrollPreservor.savedScrollPosition).toBe(scrollPosition);
});

it("should restore the saved scroll position", () => {
const scrollPosition = 50;

element.scrollTop = scrollPosition;
scrollPreservor.save();

// Change the scroll position to ensure it gets restored
element.scrollTop = 0;
scrollPreservor.restore();

expect(element.scrollTop).toBe(scrollPosition);
});

it("should handle cases where no element is set", () => {
const newScrollPreservor = new ScrollPreservor({});

// It should not throw an error and simply do nothing
expect(() => newScrollPreservor.save()).not.toThrow();
expect(() => newScrollPreservor.restore()).not.toThrow();
});

it("should be able to dynamically set a new element", () => {
const newElement = document.createElement("section");

Object.defineProperty(newElement, "scrollTop", {
writable: true,
value: 75,
});
const scrollPosition = 75;

// Set the new element and test saving and restoring
scrollPreservor.setTarget(newElement);
scrollPreservor.save();

expect(scrollPreservor.savedScrollPosition).toBe(scrollPosition);

newElement.scrollTop = 0;
scrollPreservor.restore();
expect(newElement.scrollTop).toBe(scrollPosition);
});

it("should handle restoring before saving", () => {
// Ensure that restoring when no position has been saved does not throw an error
// and ideally sets scrollTop to 0 or the initial value.
element.scrollTop = 50;
scrollPreservor.restore();

expect(element.scrollTop).toBe(0);
});

it("should handle saving with a null element", () => {
const newScrollPreservor = new ScrollPreservor({ target: null });

expect(() => newScrollPreservor.save()).not.toThrow();
});

it("should handle restoring with a null element", () => {
const newScrollPreservor = new ScrollPreservor({ target: null });

expect(() => newScrollPreservor.restore()).not.toThrow();
});

it("should correctly handle setting a null element", () => {
scrollPreservor.setTarget(null);
expect(() => scrollPreservor.save()).not.toThrow();
expect(() => scrollPreservor.restore()).not.toThrow();
});

it("should overwrite a previously saved position when a new one is saved", () => {
// Save an initial position
element.scrollTop = 10;
scrollPreservor.save();

// Change scroll position and save again
element.scrollTop = 99;
scrollPreservor.save();

// Restore and check if it's the latest saved position
element.scrollTop = 0;
scrollPreservor.restore();
expect(element.scrollTop).toBe(99);
});

it("should handle saving and restoring with scrollTop of 0", () => {
// Ensure saving a 0 value works correctly
element.scrollTop = 0;
scrollPreservor.save();

// Change scroll position and restore
element.scrollTop = 50;
scrollPreservor.restore();

expect(element.scrollTop).toBe(0);
});
});
52 changes: 52 additions & 0 deletions packages/core/src/devtool/ScrollPreservor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export class ScrollPreservor {
private _savedScrollPosition: number = 0;
private _target: HTMLElement | null = null;

constructor(options?: { target?: HTMLElement | null }) {
const { target } = options ?? {};

if (target) {
this._target = target;
}
}

/**
* Sets the HTML element to monitor.
* @param target The HTML element to monitor.
*/
public setTarget(target: HTMLElement | null): void {
if (!target) {
return;
}

this._target = target;
}

public get savedScrollPosition(): number {
return this._savedScrollPosition;
}

/**
* Saves the current scroll position of the element.
* This method stores the `scrollTop` value of the currently set element.
*/
public save(): void {
if (!this._target) {
return;
}

this._savedScrollPosition = this._target.scrollTop;
}

/**
* Restores the saved scroll position to the element.
* This method sets the `scrollTop` value of the element to the previously saved position.
*/
public restore(): void {
if (!this._target) {
return;
}

this._target.scrollTop = this._savedScrollPosition;
}
}
32 changes: 32 additions & 0 deletions packages/core/src/devtool/devtool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,36 @@ describe("Devtool", () => {
document.querySelectorAll(`.${DEVTOOL_LOGS_SECTION_ID}-item`),
).toHaveLength(LOG_CAPACITY);
});

it("should preserve scroll position when updating", () => {
// Populate logs to make the log section scrollable
for (let i = 0; i < LOG_CAPACITY; i++) {
devtool.update(state => {
state.logs.enqueue({
type: LogType.CONNECTION_ERROR,
detail: `log-${i}`,
date: new Date(),
});
});
}

const logSection = devtool.getDevtoolLogSectionElement()!;
const scrollPosition = 50;

logSection.scrollTop = scrollPosition;
expect(logSection.scrollTop).toBe(scrollPosition);

// Update the devtool and check if the scroll position is preserved
devtool.update(state => {
state.logs.enqueue({
type: LogType.CONNECTION_ERROR,
detail: "new log",
date: new Date(),
});
});

const newLogSection = devtool.getDevtoolLogSectionElement()!;

expect(newLogSection.scrollTop).toBe(scrollPosition);
});
});
14 changes: 14 additions & 0 deletions packages/core/src/devtool/devtool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
StatusColorMap,
} from "./constants.ts";
import { FixedQueue } from "./FixedQueue.ts";
import { ScrollPreservor } from "./ScrollPreservor.ts";
import { type DevtoolState, type Log } from "./types.ts";
import {
formatDate,
Expand Down Expand Up @@ -105,6 +106,9 @@ export const getDevtoolSocketIconElement = () =>
export const getDevtoolCloseIconElement = () =>
document.getElementById(DEVTOOL_CLOSE_ICON_ID);

const channelsSectionScroll = new ScrollPreservor();
const logSectionScroll = new ScrollPreservor();

export const renderChannels = () => {
const { channels } = devtool;

Expand Down Expand Up @@ -250,6 +254,10 @@ export const renderDevtool = () => {
};

export const updateInfoSection = () => {
// Save scroll positions before updating the content
logSectionScroll.save();
channelsSectionScroll.save();

const infoSection = getDevtoolInfoElement()!;

const devtoolInfoStyle = generateInlineStyle({
Expand All @@ -275,6 +283,12 @@ export const updateInfoSection = () => {
</div>
`;

logSectionScroll.setTarget(getDevtoolLogSectionElement());
channelsSectionScroll.setTarget(getDevtoolChannelsElement());

logSectionScroll.restore();
channelsSectionScroll.restore();

return infoSection;
};

Expand Down