Merge pull request #1538 from PR0M3TH3AN/hashtag-strip-bounce-and-scrollbar-15986408955537395770

Hashtag strip UI improvements: hide scrollbar and add bounce hint
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-22 20:58:04 -05:00
commit d883fad1f3
3 files changed
+189 -17

No files matched your search

+5 -17
View File
@@ -2424,26 +2424,14 @@
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
padding-bottom: var(--space-xs);
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) transparent;
scrollbar-width: none;
-ms-overflow-style: none;
}
.video-tag-strip--scroll::-webkit-scrollbar {
height: 4px;
}
.video-tag-strip--scroll::-webkit-scrollbar-track {
background: linear-gradient(
to bottom,
transparent calc(50% - 0.5px),
var(--scrollbar-track) calc(50% - 0.5px) calc(50% + 0.5px),
transparent calc(50% + 0.5px)
);
}
.video-tag-strip--scroll::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb);
border-radius: var(--radius-pill);
display: none;
width: 0;
height: 0;
}
.video-tag-pill {
+40
View File
@@ -200,6 +200,8 @@ export class HashtagStripHelper {
this._tagStrip = strip;
if (!this.scrollable) {
trimTagPillStripToFit({ strip, container: root });
} else {
this._triggerScrollHint();
}
root.hidden = strip.childElementCount === 0;
} finally {
@@ -218,6 +220,44 @@ export class HashtagStripHelper {
this._sortedTags = [];
}
_triggerScrollHint() {
const strip = this._tagStrip;
if (!strip) {
return;
}
if (
this.window &&
typeof this.window.requestAnimationFrame === "function" &&
typeof strip.scrollTo === "function"
) {
this.window.requestAnimationFrame(() => {
if (!strip.isConnected) {
return;
}
if (strip.scrollWidth > strip.clientWidth) {
if (this.window && typeof this.window.setTimeout === "function") {
this.window.setTimeout(() => {
if (!strip.isConnected || strip.scrollLeft !== 0) {
return;
}
strip.scrollTo({ left: 40, behavior: "smooth" });
this.window.setTimeout(() => {
if (!strip.isConnected) {
return;
}
strip.scrollTo({ left: 0, behavior: "smooth" });
}, 800);
}, 600);
}
}
});
}
}
_resolveTagState(tag) {
const resolver = this.tagStateResolver;
if (typeof resolver !== "function") {
@@ -0,0 +1,144 @@
import test from "node:test";
import assert from "node:assert/strict";
import { JSDOM } from "jsdom";
import { HashtagStripHelper } from "../../js/ui/components/hashtagStripHelper.js";
function createDom() {
const dom = new JSDOM("<!DOCTYPE html><body><div id='container'></div></body>", {
pretendToBeVisual: true,
});
// Mock requestAnimationFrame to capture callback
let rafCallback = null;
dom.window.requestAnimationFrame = (cb) => {
rafCallback = cb;
return 1;
};
// Helper to trigger pending RAF
dom.window.triggerRaf = () => {
if (rafCallback) {
const cb = rafCallback;
rafCallback = null;
cb();
return true;
}
return false;
};
// Mock createElement to ensure scrollTo exists on created elements
// This is required because JSDOM might not implement scrollTo on div elements
// and _triggerScrollHint checks for it.
const originalCreateElement = dom.window.document.createElement;
dom.window.document.createElement = function(tagName, options) {
const el = originalCreateElement.call(dom.window.document, tagName, options);
if (typeof el.scrollTo !== 'function') {
el.scrollTo = () => {};
}
return el;
};
return { dom, window: dom.window, document: dom.window.document };
}
test("HashtagStripHelper triggers scroll hint when content overflows", (t) => {
const { window, document } = createDom();
const container = document.getElementById("container");
// Mock setTimeout
const timeouts = [];
window.setTimeout = (cb, delay) => {
timeouts.push({ cb, delay });
return timeouts.length;
};
const helper = new HashtagStripHelper({
window,
document,
scrollable: true,
});
helper.mount(container);
helper._sortedTags = ["Tag1", "Tag2", "Tag3"];
// Render creates the element and requests RAF
helper.render();
const strip = container.querySelector(".video-tag-strip");
assert(strip, "Strip element should be rendered");
// Mock scroll properties on the newly created element
Object.defineProperty(strip, "scrollWidth", { value: 200, configurable: true });
Object.defineProperty(strip, "clientWidth", { value: 100, configurable: true });
Object.defineProperty(strip, "scrollLeft", { value: 0, writable: true, configurable: true });
Object.defineProperty(strip, "isConnected", { value: true, configurable: true });
// Mock scrollTo to spy on calls
let scrollToCalls = [];
strip.scrollTo = (options) => {
scrollToCalls.push(options);
};
// Trigger the RAF callback which was scheduled during render()
const triggered = window.triggerRaf();
assert(triggered, "Should have scheduled a RAF callback");
assert.equal(timeouts.length, 1, "Should schedule initial timeout");
// Execute first timeout
const firstTimeout = timeouts.shift();
assert.equal(firstTimeout.delay, 600, "First timeout should be 600ms");
firstTimeout.cb(); // Execute
assert.equal(scrollToCalls.length, 1, "Should have called scrollTo once");
assert.equal(scrollToCalls[0].left, 40, "Should scroll to 40px");
assert.equal(timeouts.length, 1, "Should schedule second timeout");
const secondTimeout = timeouts.shift();
assert.equal(secondTimeout.delay, 800, "Second timeout should be 800ms");
// Execute second timeout
secondTimeout.cb();
assert.equal(scrollToCalls.length, 2, "Should have called scrollTo twice");
assert.equal(scrollToCalls[1].left, 0, "Should scroll back to 0px");
});
test("HashtagStripHelper does not trigger scroll hint when no overflow", (t) => {
const { window, document } = createDom();
const container = document.getElementById("container");
const timeouts = [];
window.setTimeout = (cb, delay) => {
timeouts.push({ cb, delay });
return timeouts.length;
};
const helper = new HashtagStripHelper({
window,
document,
scrollable: true,
});
helper.mount(container);
helper._sortedTags = ["Tag1"];
helper.render();
const strip = container.querySelector(".video-tag-strip");
// Mock properties
Object.defineProperty(strip, "scrollWidth", { value: 100, configurable: true });
Object.defineProperty(strip, "clientWidth", { value: 100, configurable: true });
Object.defineProperty(strip, "isConnected", { value: true, configurable: true });
strip.scrollTo = () => {
throw new Error("Should not scroll");
};
// Trigger RAF
const triggered = window.triggerRaf();
assert(triggered, "Should have scheduled a RAF callback");
assert.equal(timeouts.length, 0, "Should not schedule timeout");
});