mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-30 01:12:39 +00:00
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:
2 files changed
+200
No files matched your search
@@ -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 };
|
||||
}
|
||||
@@ -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] }]);
|
||||
});
|
||||
Reference in new issue
Block a user