components/button/button.ts
38 lines and 5 functions in DrDustinEdwards/capsomer, at commit d3966cc on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| reduced | 7 to 7 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| morphFrom | 13 to 31 | 11 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| ms | 20 to 20 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:27> | 27 to 27 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| morphLabel | 35 to 41 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
Imports
It imports no other module of this repo. Imported by components/button/button.react.tsx.
Source
// A label that changes in place (Save, Saving…, Saved): the button's width eases to the new
// label's while the old words blur out and the new ones blur in (uiarc's morphing label). Opt
// in: the words sit in `.cap-btn-label`, and the page changes them through morphLabel. Under
// reduced motion, or with no `.cap-btn-label`, the label just changes. The leaving words are a
// copy hidden from assistive technology, so the button's name is only ever the new label.
const reduced = () => typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
// Animates a label that has already changed, from the button's width before the change and a
// copy of the old label. The React wrapper calls it after React has written the new label.
// Widths are layout widths (offsetWidth), so a pressed button's scale does not count. Returns
// the new label's width.
export function morphFrom(button: HTMLElement, from: number, old: HTMLElement): number {
const label = button.querySelector<HTMLElement>(":scope > .cap-btn-label:not([data-leaving])");
for (const a of button.getAnimations?.() ?? []) if (a.id === "cap-morph") a.cancel();
for (const g of button.querySelectorAll(":scope > [data-leaving]")) g.remove();
const to = button.offsetWidth;
if (!label || reduced() || typeof button.animate !== "function") return to;
const css = getComputedStyle(button);
const ms = (name: string, fallback: number) => parseFloat(css.getPropertyValue(name)) || fallback;
const timing = { duration: ms("--dur-base", 200), easing: css.getPropertyValue("--ease-out").trim() || "ease-out" };
old.dataset.leaving = "";
old.setAttribute("aria-hidden", "true");
button.append(old);
if (Math.abs(to - from) > 0.5) button.animate({ width: [`${from}px`, `${to}px`], overflow: ["clip", "clip"] }, { ...timing, id: "cap-morph" });
const out = old.animate({ opacity: [1, 0], filter: ["blur(0)", "blur(4px)"] }, { ...timing, duration: ms("--dur-fast", 100), fill: "forwards" });
out.onfinish = out.oncancel = () => old.remove();
// A pending button hides its label under the spinner; its own fade must not show it.
if (button.getAttribute("aria-busy") !== "true") label.animate({ opacity: [0, 1], filter: ["blur(4px)", "blur(0)"] }, timing);
return to;
}
// Changes a button's label with the morph: `change` writes the new label (into
// `.cap-btn-label`), e.g. morphLabel(save, () => (label.textContent = "Saving…")).
export function morphLabel(button: HTMLElement, change: () => void): void {
const label = button.querySelector<HTMLElement>(":scope > .cap-btn-label:not([data-leaving])");
const from = button.offsetWidth;
const old = label?.cloneNode(true) as HTMLElement | undefined;
change();
if (old) morphFrom(button, from, old);
}