Remove scrollbar from video tag strip and add bounce animation

Hidden scrollbars for `.video-tag-strip--scroll` in CSS to improve aesthetics.
Implemented a "peek" (bounce) animation in `HashtagStripHelper` that triggers when the tag list is rendered and overflows the container, indicating to the user that the list is scrollable.
Added unit tests for the new logic.
This commit is contained in:
google-labs-jules[bot] committed 2026-01-23 01:56:51 +00:00
1 parent a8ce28eb7a
commit 629acb16d2
3 files changed
+189 -18

No files matched your search

+5 -18
View File
@@ -2424,27 +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");
});