Merge pull request #827 from PR0M3TH3AN/codex/add-tag-pill-list-component-with-tests

Add tag pill strip renderer helper
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-23 11:30:11 -04:00
commit dd1ddd515a
2 files changed
+200

No files matched your search

+103
View File
@@ -0,0 +1,103 @@
/**
* Tag pill list helpers for rendering and updating the compact tag strip used on video cards.
*
* Usage:
* ```js
* const { root } = renderTagPillStrip({ tags: ["nostr", "bitvid"], onTagActivate });
* container.append(root);
* ```
*/
function resolveDocument(doc) {
if (doc) {
return doc;
}
return typeof globalThis !== "undefined" ? globalThis.document : undefined;
}
function assertDocument(doc) {
if (!doc) {
throw new Error("A document instance is required to render tag pill lists.");
}
}
function createTagButton({ doc, tag, onTagActivate }) {
const button = doc.createElement("button");
button.type = "button";
button.classList.add("pill", "video-tag-pill", "focus-ring");
button.dataset.size = "compact";
button.dataset.tag = tag;
button.title = `#${tag}`;
const label = doc.createElement("span");
label.className = "video-tag-pill__label";
label.textContent = `#${tag}`;
const icon = doc.createElement("span");
icon.className = "video-tag-pill__icon";
icon.setAttribute("aria-hidden", "true");
icon.textContent = "+";
button.append(label, icon);
if (onTagActivate) {
const handler = (event) => {
onTagActivate(tag, { event, button });
};
button.addEventListener("click", handler);
// Store the handler for cleanup when the strip is updated.
button.__tagPillClickHandler = handler;
}
return button;
}
function cleanupExistingButtons(root) {
for (const button of root.querySelectorAll("button")) {
const handler = button.__tagPillClickHandler;
if (handler) {
button.removeEventListener("click", handler);
delete button.__tagPillClickHandler;
}
}
root.textContent = "";
}
export function renderTagPillStrip({ document: doc, tags = [], onTagActivate } = {}) {
const resolvedDocument = resolveDocument(doc);
assertDocument(resolvedDocument);
const root = resolvedDocument.createElement("div");
root.classList.add("video-tag-strip");
const buttons = tags.map((tag) =>
createTagButton({ doc: resolvedDocument, tag, onTagActivate }),
);
root.append(...buttons);
return { root, buttons };
}
export function updateTagPillStrip({
root,
tags = [],
onTagActivate,
document: doc,
} = {}) {
if (!root) {
throw new Error("A root element is required to update the tag pill strip.");
}
const resolvedDocument = resolveDocument(doc ?? root?.ownerDocument);
assertDocument(resolvedDocument);
cleanupExistingButtons(root);
const buttons = tags.map((tag) =>
createTagButton({ doc: resolvedDocument, tag, onTagActivate }),
);
root.append(...buttons);
return { root, buttons };
}
+97
View File
@@ -0,0 +1,97 @@
import test from "node:test";
import assert from "node:assert/strict";
import { JSDOM } from "jsdom";
import { renderTagPillStrip, updateTagPillStrip } from "../../js/ui/components/tagPillList.js";
test("renderTagPillStrip builds buttons with labels and icons", () => {
const { document } = new JSDOM("<!DOCTYPE html>").window;
const { root, buttons } = renderTagPillStrip({
document,
tags: ["nostr", "video"],
});
assert.equal(root.tagName, "DIV");
assert(root.classList.contains("video-tag-strip"));
assert.equal(buttons.length, 2);
for (const [index, button] of buttons.entries()) {
const tag = index === 0 ? "nostr" : "video";
assert.equal(button.tagName, "BUTTON");
assert.equal(button.type, "button");
assert.ok(button.classList.contains("pill"));
assert.ok(button.classList.contains("video-tag-pill"));
assert.ok(button.classList.contains("focus-ring"));
assert.equal(button.dataset.size, "compact");
assert.equal(button.dataset.tag, tag);
assert.equal(button.title, `#${tag}`);
const label = button.querySelector(".video-tag-pill__label");
assert(label, "label span should be present");
assert.equal(label.textContent, `#${tag}`);
const icon = button.querySelector(".video-tag-pill__icon");
assert(icon, "icon span should be present");
assert.equal(icon.textContent, "+");
assert.equal(icon.getAttribute("aria-hidden"), "true");
}
});
test("renderTagPillStrip wires the activation callback", () => {
const { document } = new JSDOM("<!DOCTYPE html>").window;
const activations = [];
const { root, buttons } = renderTagPillStrip({
document,
tags: ["nostr"],
onTagActivate(tag, { button, event }) {
activations.push({ tag, button, event });
},
});
document.body.append(root);
buttons[0].click();
assert.equal(activations.length, 1);
assert.equal(activations[0].tag, "nostr");
assert.equal(activations[0].button, buttons[0]);
assert(activations[0].event instanceof document.defaultView.Event);
});
test("updateTagPillStrip replaces buttons and rewires handlers", () => {
const { document } = new JSDOM("<!DOCTYPE html>").window;
const firstActivations = [];
const secondActivations = [];
const { root, buttons: initialButtons } = renderTagPillStrip({
document,
tags: ["nostr"],
onTagActivate(tag) {
firstActivations.push(tag);
},
});
document.body.append(root);
initialButtons[0].click();
assert.deepEqual(firstActivations, ["nostr"]);
const { buttons: updatedButtons } = updateTagPillStrip({
root,
document,
tags: ["video"],
onTagActivate(tag, { button }) {
secondActivations.push({ tag, button });
},
});
assert.equal(root.querySelectorAll("button").length, 1);
assert.equal(updatedButtons[0].dataset.tag, "video");
// Ensure the old handler is removed by dispatching a click on the detached button.
initialButtons[0].dispatchEvent(new document.defaultView.Event("click", { bubbles: true }));
assert.deepEqual(firstActivations, ["nostr"]);
updatedButtons[0].click();
assert.deepEqual(secondActivations, [{ tag: "video", button: updatedButtons[0] }]);
});