mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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:
3 files changed
+189
-17
No files matched your search
+5
-17
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
Reference in new issue
Block a user