From 6644020c992b8473d4b98ac4ae2e448afd4a6128 Mon Sep 17 00:00:00 2001 From: Amirhossein Alibakhshi Date: Sun, 10 Aug 2025 11:34:50 +0330 Subject: [PATCH 1/6] fix(devtool): break long words in log detail for preventing horizonral scroll --- packages/core/src/devtool/devtool.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/devtool/devtool.ts b/packages/core/src/devtool/devtool.ts index a320b68..b65701b 100644 --- a/packages/core/src/devtool/devtool.ts +++ b/packages/core/src/devtool/devtool.ts @@ -143,6 +143,7 @@ export const renderLog = (log: Log) => { "font-size": "0.625rem", "margin-top": "0", color: LogTypeColor[log.type], + "overflow-wrap": "break-word", }); const timeStyle = generateInlineStyle({ From b667dad434047e55ba7d7fcf351ed656ba02ea00 Mon Sep 17 00:00:00 2001 From: Amirhossein Alibakhshi Date: Sun, 10 Aug 2025 11:40:09 +0330 Subject: [PATCH 2/6] docs: update changeset --- .changeset/puny-horses-grab.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/puny-horses-grab.md diff --git a/.changeset/puny-horses-grab.md b/.changeset/puny-horses-grab.md new file mode 100644 index 0000000..f2c5dc8 --- /dev/null +++ b/.changeset/puny-horses-grab.md @@ -0,0 +1,6 @@ +--- +"@tapsioss/client-socket-manager": patch +--- + +Break long words in DevTool's log details for preventing horizontal scroll. + \ No newline at end of file From 0dd329e219ce87e84c65338b57987cd8d6f6d495 Mon Sep 17 00:00:00 2001 From: Amirhossein Alibakhshi Date: Sun, 10 Aug 2025 13:11:37 +0330 Subject: [PATCH 3/6] feat: add ScrollPreservor component --- .../core/src/devtool/ScrollPreservor.test.ts | 119 ++++++++++++++++++ packages/core/src/devtool/ScrollPreservor.ts | 52 ++++++++ 2 files changed, 171 insertions(+) create mode 100644 packages/core/src/devtool/ScrollPreservor.test.ts create mode 100644 packages/core/src/devtool/ScrollPreservor.ts diff --git a/packages/core/src/devtool/ScrollPreservor.test.ts b/packages/core/src/devtool/ScrollPreservor.test.ts new file mode 100644 index 0000000..1423622 --- /dev/null +++ b/packages/core/src/devtool/ScrollPreservor.test.ts @@ -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({ 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.setElement(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({ element: null }); + + expect(() => newScrollPreservor.save()).not.toThrow(); + }); + + it("should handle restoring with a null element", () => { + const newScrollPreservor = new ScrollPreservor({ element: null }); + + expect(() => newScrollPreservor.restore()).not.toThrow(); + }); + + it("should correctly handle setting a null element", () => { + scrollPreservor.setElement(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); + }); +}); diff --git a/packages/core/src/devtool/ScrollPreservor.ts b/packages/core/src/devtool/ScrollPreservor.ts new file mode 100644 index 0000000..3410374 --- /dev/null +++ b/packages/core/src/devtool/ScrollPreservor.ts @@ -0,0 +1,52 @@ +export class ScrollPreservor { + private _savedScrollPosition: number = 0; + private _element: HTMLElement | null = null; + + constructor(options?: { element?: HTMLElement | null }) { + const { element } = options ?? {}; + + if (element) { + this._element = element; + } + } + + /** + * Sets the HTML element to monitor. + * @param target The HTML element to monitor. + */ + public setElement(target: HTMLElement | null): void { + if (!target) { + return; + } + + this._element = 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._element) { + return; + } + + this._savedScrollPosition = this._element.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._element) { + return; + } + + this._element.scrollTop = this._savedScrollPosition; + } +} From 1332f19a609494cafa01a39aa8244939d16c0b1b Mon Sep 17 00:00:00 2001 From: Amirhossein Alibakhshi Date: Sun, 10 Aug 2025 13:12:17 +0330 Subject: [PATCH 4/6] feat: integrate the ScrollPreservor into devtool component --- packages/core/src/devtool/devtool.test.ts | 32 +++++++++++++++++++++++ packages/core/src/devtool/devtool.ts | 14 ++++++++++ 2 files changed, 46 insertions(+) diff --git a/packages/core/src/devtool/devtool.test.ts b/packages/core/src/devtool/devtool.test.ts index 003ee7f..38f8f6e 100644 --- a/packages/core/src/devtool/devtool.test.ts +++ b/packages/core/src/devtool/devtool.test.ts @@ -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); + }); }); diff --git a/packages/core/src/devtool/devtool.ts b/packages/core/src/devtool/devtool.ts index b65701b..a1f3030 100644 --- a/packages/core/src/devtool/devtool.ts +++ b/packages/core/src/devtool/devtool.ts @@ -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, @@ -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; @@ -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({ @@ -275,6 +283,12 @@ export const updateInfoSection = () => { `; + logSectionScroll.setElement(getDevtoolLogSectionElement()); + channelsSectionScroll.setElement(getDevtoolChannelsElement()); + + logSectionScroll.restore(); + channelsSectionScroll.restore(); + return infoSection; }; From f8df52ba094545a2c024f29d77e8c305ccda578c Mon Sep 17 00:00:00 2001 From: Amirhossein Alibakhshi Date: Sun, 10 Aug 2025 13:12:31 +0330 Subject: [PATCH 5/6] docs: update changeset --- .changeset/lovely-insects-shout.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/lovely-insects-shout.md diff --git a/.changeset/lovely-insects-shout.md b/.changeset/lovely-insects-shout.md new file mode 100644 index 0000000..4d88519 --- /dev/null +++ b/.changeset/lovely-insects-shout.md @@ -0,0 +1,6 @@ +--- +"@tapsioss/client-socket-manager": patch +--- + +Preserve the scroll position of channels and logs section inside the DevTool. + \ No newline at end of file From 5a54bbd5756e49ba4aef02681cd9f6589ffad75c Mon Sep 17 00:00:00 2001 From: Amirhossein Alibakhshi Date: Sun, 10 Aug 2025 14:05:51 +0330 Subject: [PATCH 6/6] refactor(devtool): rename the setElement method to setTarget --- .../core/src/devtool/ScrollPreservor.test.ts | 10 ++++----- packages/core/src/devtool/ScrollPreservor.ts | 22 +++++++++---------- packages/core/src/devtool/devtool.ts | 4 ++-- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/core/src/devtool/ScrollPreservor.test.ts b/packages/core/src/devtool/ScrollPreservor.test.ts index 1423622..b913e9d 100644 --- a/packages/core/src/devtool/ScrollPreservor.test.ts +++ b/packages/core/src/devtool/ScrollPreservor.test.ts @@ -9,7 +9,7 @@ describe("ScrollPreservor", () => { // Set up a mock HTML element for testing element = document.createElement("div"); Object.defineProperty(element, "scrollTop", { writable: true, value: 0 }); - scrollPreservor = new ScrollPreservor({ element }); + scrollPreservor = new ScrollPreservor({ target: element }); }); it("should be able to set and save the scroll position", () => { @@ -53,7 +53,7 @@ describe("ScrollPreservor", () => { const scrollPosition = 75; // Set the new element and test saving and restoring - scrollPreservor.setElement(newElement); + scrollPreservor.setTarget(newElement); scrollPreservor.save(); expect(scrollPreservor.savedScrollPosition).toBe(scrollPosition); @@ -73,19 +73,19 @@ describe("ScrollPreservor", () => { }); it("should handle saving with a null element", () => { - const newScrollPreservor = new ScrollPreservor({ element: null }); + const newScrollPreservor = new ScrollPreservor({ target: null }); expect(() => newScrollPreservor.save()).not.toThrow(); }); it("should handle restoring with a null element", () => { - const newScrollPreservor = new ScrollPreservor({ element: null }); + const newScrollPreservor = new ScrollPreservor({ target: null }); expect(() => newScrollPreservor.restore()).not.toThrow(); }); it("should correctly handle setting a null element", () => { - scrollPreservor.setElement(null); + scrollPreservor.setTarget(null); expect(() => scrollPreservor.save()).not.toThrow(); expect(() => scrollPreservor.restore()).not.toThrow(); }); diff --git a/packages/core/src/devtool/ScrollPreservor.ts b/packages/core/src/devtool/ScrollPreservor.ts index 3410374..e9564e0 100644 --- a/packages/core/src/devtool/ScrollPreservor.ts +++ b/packages/core/src/devtool/ScrollPreservor.ts @@ -1,12 +1,12 @@ export class ScrollPreservor { private _savedScrollPosition: number = 0; - private _element: HTMLElement | null = null; + private _target: HTMLElement | null = null; - constructor(options?: { element?: HTMLElement | null }) { - const { element } = options ?? {}; + constructor(options?: { target?: HTMLElement | null }) { + const { target } = options ?? {}; - if (element) { - this._element = element; + if (target) { + this._target = target; } } @@ -14,12 +14,12 @@ export class ScrollPreservor { * Sets the HTML element to monitor. * @param target The HTML element to monitor. */ - public setElement(target: HTMLElement | null): void { + public setTarget(target: HTMLElement | null): void { if (!target) { return; } - this._element = target; + this._target = target; } public get savedScrollPosition(): number { @@ -31,11 +31,11 @@ export class ScrollPreservor { * This method stores the `scrollTop` value of the currently set element. */ public save(): void { - if (!this._element) { + if (!this._target) { return; } - this._savedScrollPosition = this._element.scrollTop; + this._savedScrollPosition = this._target.scrollTop; } /** @@ -43,10 +43,10 @@ export class ScrollPreservor { * This method sets the `scrollTop` value of the element to the previously saved position. */ public restore(): void { - if (!this._element) { + if (!this._target) { return; } - this._element.scrollTop = this._savedScrollPosition; + this._target.scrollTop = this._savedScrollPosition; } } diff --git a/packages/core/src/devtool/devtool.ts b/packages/core/src/devtool/devtool.ts index a1f3030..aef1ca0 100644 --- a/packages/core/src/devtool/devtool.ts +++ b/packages/core/src/devtool/devtool.ts @@ -283,8 +283,8 @@ export const updateInfoSection = () => { `; - logSectionScroll.setElement(getDevtoolLogSectionElement()); - channelsSectionScroll.setElement(getDevtoolChannelsElement()); + logSectionScroll.setTarget(getDevtoolLogSectionElement()); + channelsSectionScroll.setTarget(getDevtoolChannelsElement()); logSectionScroll.restore(); channelsSectionScroll.restore();