refactor: use static classes for beacon utilities

This commit is contained in:
thePR0M3TH3AN committed 2025-10-14 08:01:34 -04:00
1 parent f57abdf824
commit 27c1c4fa11
6 files changed
+77 -157

No files matched your search

+22
View File
@@ -1268,10 +1268,15 @@
}
/* Torrent helper classes */
.beacon-hidden-download,
[data-beacon-hidden-download],
.torrent-download-anchor {
display: none !important;
}
.beacon-clipboard-offscreen,
[data-beacon-clipboard-state="offscreen"],
[data-beacon-clipboard="offscreen"],
.torrent-clipboard-textarea {
position: fixed;
top: 0;
@@ -1282,9 +1287,26 @@
pointer-events: none;
}
.beacon-toast-motion,
.torrent-toast-motion {
transition-property: opacity, transform;
will-change: opacity, transform;
opacity: 0;
transform: translateY(0.75rem);
}
.beacon-toast-motion[data-beacon-motion="enter"],
.beacon-toast-motion[data-beacon-motion="show"],
.torrent-toast-motion[data-beacon-motion="enter"],
.torrent-toast-motion[data-beacon-motion="show"] {
opacity: 1;
transform: translateY(0);
}
.beacon-toast-motion[data-beacon-motion="exit"],
.torrent-toast-motion[data-beacon-motion="exit"] {
opacity: 0;
transform: translateY(0.75rem);
}
.torrent-toast {
+22 -101
View File
@@ -1,5 +1,5 @@
import { strict as assert } from "node:assert";
import { describe, it, beforeEach, afterEach } from "node:test";
import { describe, it } from "node:test";
import { JSDOM } from "jsdom";
import {
@@ -8,113 +8,34 @@ import {
beaconDynamicFallbackClasses,
} from "../../torrent/ui/styleHelpers.js";
const ORIGINAL_WINDOW = globalThis.window;
const ORIGINAL_DOCUMENT = globalThis.document;
const ORIGINAL_ELEMENT = globalThis.HTMLElement;
const ORIGINAL_STYLE_ELEMENT = globalThis.HTMLStyleElement;
let dom;
function bootstrapDom() {
dom = new JSDOM("<!doctype html><html><head></head><body></body></html>", {
pretendToBeVisual: true,
});
globalThis.window = dom.window;
globalThis.document = dom.window.document;
globalThis.HTMLElement = dom.window.HTMLElement;
globalThis.HTMLStyleElement = dom.window.HTMLStyleElement;
}
function teardownDom() {
if (dom) {
dom.window.close();
dom = null;
}
if (typeof ORIGINAL_WINDOW === "undefined") {
delete globalThis.window;
} else {
globalThis.window = ORIGINAL_WINDOW;
}
if (typeof ORIGINAL_DOCUMENT === "undefined") {
delete globalThis.document;
} else {
globalThis.document = ORIGINAL_DOCUMENT;
}
if (typeof ORIGINAL_ELEMENT === "undefined") {
delete globalThis.HTMLElement;
} else {
globalThis.HTMLElement = ORIGINAL_ELEMENT;
}
if (typeof ORIGINAL_STYLE_ELEMENT === "undefined") {
delete globalThis.HTMLStyleElement;
} else {
globalThis.HTMLStyleElement = ORIGINAL_STYLE_ELEMENT;
}
}
describe("torrent/ui/styleHelpers", () => {
beforeEach(() => {
bootstrapDom();
it("returns null when no element is provided", () => {
assert.equal(applyBeaconDynamicStyles(null), null);
});
afterEach(() => {
teardownDom();
it("returns the original element without mutations", () => {
const dom = new JSDOM("<!doctype html><html><head></head><body></body></html>");
const { document } = dom.window;
const element = document.createElement("div");
const result = applyBeaconDynamicStyles(element);
assert.equal(result, element);
assert.equal(element.getAttributeNames().length, 0);
});
it("registers fallback classes for known slots", () => {
assert.equal(
beaconDynamicFallbackClasses.hiddenDownload,
"torrent-download-anchor",
);
assert.equal(
beaconDynamicFallbackClasses.clipboard,
"torrent-clipboard-textarea",
);
assert.equal(beaconDynamicFallbackClasses.toast, "torrent-toast-motion");
it("provides an empty, frozen fallback map", () => {
assert.deepEqual(beaconDynamicFallbackClasses, {});
assert.throws(() => {
beaconDynamicFallbackClasses.test = "nope";
}, TypeError);
});
it("applies dynamic styles and fallback classes", () => {
const element = document.createElement("a");
document.body.appendChild(element);
const className = applyBeaconDynamicStyles(
element,
{ display: "none" },
"hiddenDownload",
);
assert.ok(className, "should receive a generated class name");
assert.ok(
element.classList.contains("torrent-download-anchor"),
"fallback class should be present",
);
assert.ok(
element.classList.contains(className),
"dynamic class should be applied",
);
const styleTag = document.getElementById("bitvid-style-system");
assert.ok(styleTag, "style system tag should be created");
});
it("removes dynamic styles and fallback classes", () => {
const element = document.createElement("textarea");
document.body.appendChild(element);
applyBeaconDynamicStyles(element, { opacity: "0" }, "clipboard");
assert.ok(element.classList.contains("torrent-clipboard-textarea"));
removeBeaconDynamicStyles(element, "clipboard");
assert.ok(
!element.classList.contains("torrent-clipboard-textarea"),
"fallback class should be removed",
);
});
it("gracefully handles missing elements", () => {
const result = applyBeaconDynamicStyles(null, { display: "none" });
assert.equal(result, null);
it("no-ops when removing styles", () => {
assert.doesNotThrow(() => removeBeaconDynamicStyles(null));
const dom = new JSDOM("<!doctype html><html><head></head><body></body></html>");
const { document } = dom.window;
const element = document.createElement("div");
assert.doesNotThrow(() => removeBeaconDynamicStyles(element));
});
});
+4 -1
View File
@@ -22,6 +22,7 @@ describe("torrent/ui/toastService", () => {
it("renders a toast with Tailwind token classes and removes it on dismiss", async () => {
const documentRef = dom.window.document;
documentRef.defaultView.requestAnimationFrame = (cb) => cb();
const toastManager = createToastManager(documentRef);
const handle = toastManager.success("Magnet copied", { sticky: true });
@@ -31,7 +32,8 @@ describe("torrent/ui/toastService", () => {
const wrapper = container.firstElementChild;
assert.ok(wrapper, "toast wrapper should exist");
assert.equal(wrapper.classList.contains("torrent-toast-motion"), true);
assert.equal(wrapper.classList.contains("beacon-toast-motion"), true);
assert.equal(wrapper.getAttribute("data-beacon-motion"), "enter");
const toast = wrapper.querySelector("div[role='status']");
assert.ok(toast, "toast element should be present");
@@ -43,6 +45,7 @@ describe("torrent/ui/toastService", () => {
assert.ok(closeButton, "close button should be rendered");
handle.dismiss();
assert.equal(wrapper.getAttribute("data-beacon-motion"), "exit");
await new Promise((resolve) => setTimeout(resolve, 250));
+6 -15
View File
@@ -1,5 +1,4 @@
/* global WebTorrent, angular, moment */
import { applyBeaconDynamicStyles } from "./ui/styleHelpers.js";
import { registerBeaconToast } from "./ui/toastService.js";
import { registerTorrentTable } from "./ui/torrentTable.js";
@@ -192,7 +191,8 @@ app.controller("BTorrentCtrl", [
// Create an anchor to trigger the download.
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
applyBeaconDynamicStyles(a, { display: "none" }, "hiddenDownload");
a.classList.add("beacon-hidden-download");
a.setAttribute("data-beacon-hidden-download", "true");
a.href = blobUrl;
a.download = file.name;
// Append, click, remove, revoke.
@@ -224,18 +224,8 @@ app.controller("BTorrentCtrl", [
try {
const textarea = document.createElement("textarea");
textarea.value = magnetURI;
applyBeaconDynamicStyles(
textarea,
{
position: "fixed",
top: "0",
left: "-9999px",
width: "0",
height: "0",
opacity: "0",
},
"clipboard",
);
textarea.classList.add("beacon-clipboard-offscreen");
textarea.setAttribute("data-beacon-clipboard-state", "offscreen");
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
@@ -256,7 +246,8 @@ app.controller("BTorrentCtrl", [
const fileName = torrent.fileName || `${torrent.name}.torrent`;
// Create a hidden <a> to force download of the .torrent file.
const a = document.createElement("a");
applyBeaconDynamicStyles(a, { display: "none" }, "hiddenDownload");
a.classList.add("beacon-hidden-download");
a.setAttribute("data-beacon-hidden-download", "true");
a.href = torrent.torrentFileBlobURL;
a.download = fileName;
document.body.appendChild(a);
+4 -34
View File
@@ -1,37 +1,7 @@
import { applyDynamicStyles, removeDynamicStyles } from "../../js/ui/styleSystem.js";
const FALLBACK_CLASSES = {
hiddenDownload: "torrent-download-anchor",
clipboard: "torrent-clipboard-textarea",
toast: "torrent-toast-motion",
};
export function applyBeaconDynamicStyles(element, styleObject = {}, slotKey) {
if (!element) {
return null;
}
const slot = slotKey ? String(slotKey) : undefined;
if (slot && FALLBACK_CLASSES[slot]) {
element.classList.add(FALLBACK_CLASSES[slot]);
}
return applyDynamicStyles(element, styleObject, slot ? { slot } : {});
export function applyBeaconDynamicStyles(element) {
return element ?? null;
}
export function removeBeaconDynamicStyles(element, slotKey) {
if (!element) {
return;
}
export function removeBeaconDynamicStyles() {}
const slot = slotKey ? String(slotKey) : undefined;
if (slot && FALLBACK_CLASSES[slot]) {
element.classList.remove(FALLBACK_CLASSES[slot]);
}
removeDynamicStyles(element, slot ? { slot } : {});
}
export const beaconDynamicFallbackClasses = { ...FALLBACK_CLASSES };
export const beaconDynamicFallbackClasses = Object.freeze({});
+19 -6
View File
@@ -1,5 +1,3 @@
import { applyBeaconDynamicStyles } from "./styleHelpers.js";
const TOAST_TYPE_CLASSES = {
info: "torrent-toast torrent-toast--info",
success: "torrent-toast torrent-toast--success",
@@ -8,6 +6,19 @@ const TOAST_TYPE_CLASSES = {
};
const TRANSITION_CLASSES = "transition duration-200 ease-out";
const MOTION_CLASS = "beacon-toast-motion";
const MOTION_ATTR = "data-beacon-motion";
function scheduleMotionFrame(documentRef, callback) {
const view = documentRef?.defaultView;
if (view && typeof view.requestAnimationFrame === "function") {
view.requestAnimationFrame(callback);
return;
}
setTimeout(callback, 16);
}
function ensureContainer(doc) {
const documentRef = doc || (typeof document !== "undefined" ? document : null);
@@ -76,9 +87,9 @@ function buildToastElement(documentRef, message, typeClass) {
"relative",
"w-full",
TRANSITION_CLASSES,
"translate-y-3",
"opacity-0",
MOTION_CLASS,
].join(" ");
wrapper.setAttribute(MOTION_ATTR, "exit");
wrapper.appendChild(toast);
toast.appendChild(closeButton);
@@ -108,11 +119,13 @@ export function createToastManager(documentRef, timeoutRef = setTimeout, clearTi
container.appendChild(wrapper);
applyBeaconDynamicStyles(wrapper, { opacity: "1", transform: "translateY(0)" }, "toast");
scheduleMotionFrame(documentRef, () => {
wrapper.setAttribute(MOTION_ATTR, "enter");
});
const duration = typeof options.duration === "number" ? options.duration : 3500;
const cleanup = () => {
applyBeaconDynamicStyles(wrapper, { opacity: "0", transform: "translateY(0.75rem)" }, "toast");
wrapper.setAttribute(MOTION_ATTR, "exit");
const removalTimer = timeoutRef(() => {
if (wrapper.parentNode) {
wrapper.parentNode.removeChild(wrapper);