m3-svelte
Version:
M3 Svelte implements the Material 3 design system in Svelte. See the [website](https://kendell.dev/m3-svelte/) for demos and usage instructions.
143 lines (142 loc) • 6.01 kB
JavaScript
import "./layer.css";
const activePointerRipples = [];
const activeKeyboardRipples = [];
const isEnabled = (node) => {
if (node instanceof HTMLButtonElement && node.disabled)
return false;
if (node instanceof HTMLInputElement && node.disabled)
return false;
if (node instanceof HTMLLabelElement) {
const control = node.control;
if (control instanceof HTMLInputElement && control.disabled)
return false;
}
return true;
};
const createRippleSvg = (node, x, y, width, height) => {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches)
return null;
const size = Math.hypot(Math.max(x, width - x), Math.max(y, height - y)) * 2.5;
const speed = Math.max(Math.min(Math.log(size) * 50, 600), 200);
const gradient = document.createElementNS("http://www.w3.org/2000/svg", "radialGradient");
gradient.id = `ripple-${Date.now()}-${Math.random().toString(36).slice(2)}`;
const stops = [
{ offset: "0%", opacity: "0.12" },
{ offset: "70%", opacity: "0.12" },
{ offset: "100%", opacity: "0" },
];
for (const { offset, opacity } of stops) {
const stop = document.createElementNS("http://www.w3.org/2000/svg", "stop");
stop.setAttribute("offset", offset);
stop.setAttribute("stop-color", "currentColor");
stop.setAttribute("stop-opacity", opacity);
gradient.appendChild(stop);
}
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
circle.setAttribute("cx", `${x}`);
circle.setAttribute("cy", `${y}`);
circle.setAttribute("r", "0");
circle.setAttribute("fill", `url(#${gradient.id})`);
const expand = document.createElementNS("http://www.w3.org/2000/svg", "animate");
expand.setAttribute("attributeName", "r");
expand.setAttribute("from", "0");
expand.setAttribute("to", `${size / 2}`);
expand.setAttribute("dur", `${speed}ms`);
expand.setAttribute("fill", "freeze");
expand.setAttribute("calcMode", "spline");
expand.setAttribute("keySplines", "0.4 0, 0.2 1");
circle.appendChild(expand);
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.classList.add("active-ripple");
svg.style.cssText = [
"position: absolute",
"inset: 0",
"width: 100%",
"height: 100%",
"overflow: hidden",
"border-radius: inherit",
"pointer-events: none",
].join(";");
svg.appendChild(gradient);
svg.appendChild(circle);
const ua = navigator.userAgent;
const isFirefox = ua.includes("Firefox");
const isTrulySafari = !ua.includes("Chrome") && ua.includes("Safari");
if (!isFirefox && !isTrulySafari && size > 100) {
const filter = document.createElementNS("http://www.w3.org/2000/svg", "filter");
filter.id = `noise-${Date.now()}-${Math.random().toString(36).slice(2)}`;
const turb = document.createElementNS("http://www.w3.org/2000/svg", "feTurbulence");
turb.setAttribute("type", "fractalNoise");
turb.setAttribute("baseFrequency", "0.6");
turb.setAttribute("seed", Math.random().toString());
const blur = document.createElementNS("http://www.w3.org/2000/svg", "feDisplacementMap");
blur.setAttribute("in", "SourceGraphic");
// implicitly uses result of previous filter primitive as in2
blur.setAttribute("scale", `${size ** 2 * 0.0002}`);
blur.setAttribute("xChannelSelector", "R");
blur.setAttribute("yChannelSelector", "B");
filter.appendChild(turb);
filter.appendChild(blur);
circle.setAttribute("filter", `url(#${filter.id})`);
svg.appendChild(filter);
}
node.appendChild(svg);
return () => {
const fade = document.createElementNS("http://www.w3.org/2000/svg", "animate");
fade.setAttribute("attributeName", "opacity");
fade.setAttribute("from", "1");
fade.setAttribute("to", "0");
fade.setAttribute("dur", "800ms");
fade.setAttribute("fill", "freeze");
fade.setAttribute("calcMode", "spline");
fade.setAttribute("keySplines", "0.4 0, 0.2 1");
circle.appendChild(fade);
fade.beginElement();
setTimeout(() => svg.remove(), 800);
};
};
if (typeof document != "undefined") {
document.documentElement.classList.add("js");
// Pointer events
document.addEventListener("pointerdown", (e) => {
if (e.button != 0)
return;
const layer = e.target.closest(".m3-layer");
if (!layer || !isEnabled(layer))
return;
const rect = layer.getBoundingClientRect();
const cancel = createRippleSvg(layer, e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height);
if (cancel) {
activePointerRipples.push(cancel);
}
});
const cancelPointerRipples = () => {
for (const cancel of activePointerRipples)
cancel();
activePointerRipples.length = 0;
};
document.addEventListener("pointerup", cancelPointerRipples);
document.addEventListener("dragend", cancelPointerRipples);
// Keyboard events
document.addEventListener("keydown", (e) => {
if (e.repeat)
return;
const target = e.target;
const layer = target.closest(".m3-layer");
if (!layer || !isEnabled(layer))
return;
const isActivate = e.key == "Enter" || (e.key == " " && layer.tagName == "BUTTON");
if (!isActivate)
return;
const rect = layer.getBoundingClientRect();
const cancel = createRippleSvg(layer, rect.width / 2, rect.height / 2, rect.width, rect.height);
if (cancel) {
activeKeyboardRipples.push(cancel);
}
});
document.addEventListener("keyup", () => {
for (const cancel of activeKeyboardRipples)
cancel();
activeKeyboardRipples.length = 0;
});
}