Răsfoiți Sursa

fix: ripple works much better now

Sv443 10 luni în urmă
părinte
comite
179da517ca

+ 5 - 3
contributing.md

@@ -113,6 +113,8 @@ To edit an existing translation, please follow these steps:
   - `npm run preview` - Same as `npm run build-preview`, but also starts the dev server for a few seconds so the extension that's waiting for file changes can update the script
   - `npm run preview` - Same as `npm run build-preview`, but also starts the dev server for a few seconds so the extension that's waiting for file changes can update the script
 - **`npm run lint`**  
 - **`npm run lint`**  
   Builds the userscript with the TypeScript compiler and lints it with ESLint. Doesn't verify the functionality of the script, only checks for syntax and TypeScript errors!
   Builds the userscript with the TypeScript compiler and lints it with ESLint. Doesn't verify the functionality of the script, only checks for syntax and TypeScript errors!
+- **`npm run storybook`**  
+  Starts Storybook for developing and testing components. After launching, it will automatically open in your default browser.
 - **`npm run gen-readme`**  
 - **`npm run gen-readme`**  
   Updates the README files by inserting different parts of generated sections into them.
   Updates the README files by inserting different parts of generated sections into them.
 - **`npm run tr-progress`**  
 - **`npm run tr-progress`**  
@@ -1428,15 +1430,15 @@ The usage and example blocks on each are written in TypeScript but can be used i
 > #### createRipple()
 > #### createRipple()
 > Usage:  
 > Usage:  
 > ```ts
 > ```ts
-> unsafeWindow.BYTM.createRipple(rippleElement?: HTMLElement): HTMLElement
+> unsafeWindow.BYTM.createRipple(rippleElement?: HTMLElement, props?: RippleProps): HTMLElement
 > ```
 > ```
 >   
 >   
 > Creates a circular, expanding ripple effect on the specified element or creates a new one with the effect already applied if none is provided.  
 > Creates a circular, expanding ripple effect on the specified element or creates a new one with the effect already applied if none is provided.  
 > Returns either the new element or the initially passed one.  
 > Returns either the new element or the initially passed one.  
 > Custom CSS overrides can be applied to change the color, size and transparency.  
 > Custom CSS overrides can be applied to change the color, size and transparency.  
-> Just make sure not to make the animation slower than it is, since the script will automatically remove the ripple elements after the animation is done.  
 >   
 >   
-> **This component is still experimental and might not work in some cases!**
+> Properties:  
+> - `speed?: string` - The speed of the ripple effect. Can be "fast", "normal" or "slow" (defaults to "normal")
 > 
 > 
 > <details><summary><b>Example <i>(click to expand)</i></b></summary>
 > <details><summary><b>Example <i>(click to expand)</i></b></summary>
 > 
 > 

+ 22 - 6
src/components/ripple.css

@@ -1,10 +1,26 @@
-:root html body .bytm-ripple {
+/* use `html body` prefix for more specificity */
+
+html body .bytm-ripple {
   position: relative;
   position: relative;
-  width: var(--bytm-ripple-width, 100%);
-  height: var(--bytm-ripple-height, 100%);
   overflow: hidden;
   overflow: hidden;
   white-space: nowrap;
   white-space: nowrap;
   text-overflow: ellipsis;
   text-overflow: ellipsis;
+  width: var(--bytm-ripple-width);
+  height: var(--bytm-ripple-height);
+  user-select: none;
+}
+
+html body .bytm-ripple,
+html body .bytm-ripple.normal {
+  --bytm-ripple-anim-duration: 0.35s;
+}
+
+html body .bytm-ripple.fast {
+  --bytm-ripple-anim-duration: 0.2s;
+}
+
+html body .bytm-ripple.slow {
+  --bytm-ripple-anim-duration: 0.5s;
 }
 }
 
 
 .bytm-ripple-area {
 .bytm-ripple-area {
@@ -13,7 +29,7 @@
   transform: translate(-50%, -50%);
   transform: translate(-50%, -50%);
   pointer-events: none;
   pointer-events: none;
   border-radius: 50%;
   border-radius: 50%;
-  animation: bytm-scale-ripple 0.25s ease-in;
+  animation: bytm-scale-ripple var(--bytm-ripple-anim-duration) ease-out;
 }
 }
 
 
 @keyframes bytm-scale-ripple {
 @keyframes bytm-scale-ripple {
@@ -23,8 +39,8 @@
     opacity: 0.5;
     opacity: 0.5;
   }
   }
   100% {
   100% {
-    width: 400px;
-    height: 400px;
+    width: calc(var(--bytm-ripple-cont-width) * 2);
+    height: calc(var(--bytm-ripple-cont-width) * 2);
     opacity: 0;
     opacity: 0;
   }
   }
 }
 }

+ 23 - 7
src/components/ripple.ts

@@ -1,29 +1,44 @@
 import "./ripple.css";
 import "./ripple.css";
 
 
+type RippleProps = {
+  /** How fast should the animation be */
+  speed?: "normal" | "fast" | "slow";
+};
+
 /**
 /**
  * Creates an element with a ripple effect on click.
  * Creates an element with a ripple effect on click.
  * @param clickEl If passed, this element will be modified to have the ripple effect. Otherwise, a new element will be created.
  * @param clickEl If passed, this element will be modified to have the ripple effect. Otherwise, a new element will be created.
  * @returns The passed element or the newly created element with the ripple effect.
  * @returns The passed element or the newly created element with the ripple effect.
  */
  */
-export function createRipple<TElem extends HTMLElement>(rippleElement: TElem): TElem;
+export function createRipple<TElem extends HTMLElement>(rippleElement: TElem, properties?: RippleProps): TElem;
 /**
 /**
  * Creates an element with a ripple effect on click.
  * Creates an element with a ripple effect on click.
  * @param clickEl If passed, this element will be modified to have the ripple effect. Otherwise, a new element will be created.
  * @param clickEl If passed, this element will be modified to have the ripple effect. Otherwise, a new element will be created.
  * @returns The passed element or the newly created element with the ripple effect.
  * @returns The passed element or the newly created element with the ripple effect.
  */
  */
-export function createRipple(rippleElement: undefined): HTMLDivElement;
+export function createRipple(rippleElement?: undefined, properties?: RippleProps): HTMLDivElement;
 /**
 /**
  * Creates an element with a ripple effect on click.
  * Creates an element with a ripple effect on click.
  * @param clickEl If passed, this element will be modified to have the ripple effect. Otherwise, a new element will be created.
  * @param clickEl If passed, this element will be modified to have the ripple effect. Otherwise, a new element will be created.
  * @returns The passed element or the newly created element with the ripple effect.
  * @returns The passed element or the newly created element with the ripple effect.
  */
  */
-export function createRipple<TElem extends HTMLElement>(rippleElement?: TElem) {
+export function createRipple<TElem extends HTMLElement>(rippleElement?: TElem, properties?: RippleProps) {
+  const props = {
+    speed: "normal",
+    ...properties,
+  };
+
   const rippleEl = rippleElement ?? document.createElement("div");
   const rippleEl = rippleElement ?? document.createElement("div");
-  rippleEl.classList.add("bytm-ripple");
+  rippleEl.classList.add("bytm-ripple", props.speed);
+
+  const updateRippleWidth = () => 
+    rippleEl.style.setProperty("--bytm-ripple-cont-width", rippleEl.clientWidth + "px");
 
 
   rippleEl.addEventListener("click", (e) => {
   rippleEl.addEventListener("click", (e) => {
-    const x = (e as MouseEvent).clientX - (e.target as HTMLElement)?.offsetLeft ?? 0;
-    const y = (e as MouseEvent).clientY - (e.target as HTMLElement)?.offsetTop ?? 0;
+    updateRippleWidth();
+
+    const x = (e as MouseEvent).clientX - rippleEl.getBoundingClientRect().left;
+    const y = (e as MouseEvent).clientY - rippleEl.getBoundingClientRect().top;
 
 
     const rippleAreaEl = document.createElement("span");
     const rippleAreaEl = document.createElement("span");
     rippleAreaEl.classList.add("bytm-ripple-area");
     rippleAreaEl.classList.add("bytm-ripple-area");
@@ -35,8 +50,9 @@ export function createRipple<TElem extends HTMLElement>(rippleElement?: TElem) {
     else
     else
       rippleEl.appendChild(rippleAreaEl);
       rippleEl.appendChild(rippleAreaEl);
 
 
-    setTimeout(() => rippleAreaEl.remove(), 250);
+    rippleAreaEl.addEventListener("animationend", () => rippleAreaEl.remove());
   });
   });
 
 
+  updateRippleWidth();
   return rippleEl;
   return rippleEl;
 }
 }

+ 1 - 1
src/stories/Button.stories.ts → src/stories/Button.stories.ts.disabled

@@ -5,7 +5,7 @@ import { createButton, type ButtonProps } from "./Button.js";
 // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
 // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
 const meta = {
 const meta = {
   title: "Example/Example Button",
   title: "Example/Example Button",
-  tags: ["autodocs"],
+  // tags: ["autodocs"],
   render: (args) => {
   render: (args) => {
     // You can either use a function to create DOM elements or use a plain html string!
     // You can either use a function to create DOM elements or use a plain html string!
     // return `<div>${label}</div>`;
     // return `<div>${label}</div>`;

+ 2 - 0
src/stories/README.md

@@ -0,0 +1,2 @@
+## Stories
+This folder contains stories for developing and testing components, see https://storybook.js.org/docs/get-started

+ 63 - 0
src/stories/Ripple.stories.ts

@@ -0,0 +1,63 @@
+import type { StoryObj, Meta } from "@storybook/html";
+import { fn } from "@storybook/test";
+import { createRipple } from "../components/ripple.js";
+import "../components/ripple.css";
+
+const meta = {
+  title: "Ripple Button",
+  render: (args) => {
+    const btn = document.createElement("div");
+    btn.tabIndex = 0;
+    btn.style.height = "24px";
+    btn.style.display = "inline-flex";
+    btn.style.justifyContent = "center";
+    btn.style.alignItems = "center";
+    btn.style.borderRadius = "24px";
+    btn.style.cursor = "pointer";
+
+    btn.innerHTML = args.label;
+    btn.style.backgroundColor = args.backgroundColor;
+    btn.style.color = args.color;
+    btn.style.fontSize = args.fontSize;
+    btn.style.padding = args.padding;
+
+    btn.addEventListener("click", args.onClick);
+    return createRipple(btn, { speed: args.speed });
+  },
+  argTypes: {
+    backgroundColor: { control: "color" },
+    color: { control: "color" },
+    label: { control: "text" },
+    onClick: { action: "onClick" },
+    padding: { control: "text" },
+    fontSize: { control: "text" },
+    speed: { control: { type: "select" }, options: ["normal", "fast", "slow"] },
+  },
+  // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
+  args: { onClick: fn() },
+} satisfies Meta<RippleProps>;
+
+export default meta;
+
+type RippleProps = {
+  backgroundColor: string;
+  color: string;
+  label: string;
+  onClick: (e: MouseEvent) => void;
+  padding: string;
+  fontSize: string;
+  speed: "normal" | "fast" | "slow";
+};
+
+type Story = StoryObj<RippleProps>;
+
+export const Primary: Story = {
+  args: {
+    backgroundColor: "#123489",
+    color: "white",
+    label: "Hello I am a button",
+    padding: "8px 36px",
+    fontSize: "16px",
+    speed: "slow",
+  },
+};

+ 0 - 30
src/utils/dom.ts

@@ -147,36 +147,6 @@ export function clearNode(element: Element) {
   element.parentNode!.removeChild(element);
   element.parentNode!.removeChild(element);
 }
 }
 
 
-const interactionKeys = ["Enter", " ", "Space"];
-
-/**
- * Adds generic, accessible interaction listeners to the passed element.  
- * All listeners have the default behavior prevented and stop propagation (for keyboard events only as long as the captured key is valid).
- * @param listenerOptions Provide a {@linkcode listenerOptions} object to configure the listeners
- */
-export function onInteraction<TElem extends HTMLElement>(elem: TElem, listener: (evt: MouseEvent | KeyboardEvent) => void, listenerOptions?: AddEventListenerOptions) {
-  const proxListener = (e: MouseEvent | KeyboardEvent) => {
-    if(e instanceof KeyboardEvent) {
-      if(interactionKeys.includes(e.key)) {
-        e.preventDefault();
-        e.stopPropagation();
-      }
-      else return;
-    }
-    else if(e instanceof MouseEvent) {
-      e.preventDefault();
-      e.stopPropagation();
-    }
-
-    // clean up the other listener that isn't automatically removed if `once` is set
-    listenerOptions?.once && e.type === "keydown" && elem.removeEventListener("click", proxListener, listenerOptions);
-    listenerOptions?.once && e.type === "click" && elem.removeEventListener("keydown", proxListener, listenerOptions);
-    listener(e);
-  };
-  elem.addEventListener("click", proxListener, listenerOptions);
-  elem.addEventListener("keydown", proxListener, listenerOptions);
-}
-
 /**
 /**
  * Adds a style element to the DOM at runtime.
  * Adds a style element to the DOM at runtime.
  * @param css The CSS stylesheet to add
  * @param css The CSS stylesheet to add

+ 1 - 0
src/utils/index.ts

@@ -1,4 +1,5 @@
 export * from "./dom.js";
 export * from "./dom.js";
+export * from "./input.js";
 export * from "./logging.js";
 export * from "./logging.js";
 export * from "./misc.js";
 export * from "./misc.js";
 export * from "./NanoEmitter.js";
 export * from "./NanoEmitter.js";

+ 29 - 0
src/utils/input.ts

@@ -0,0 +1,29 @@
+const interactionKeys = ["Enter", " ", "Space"];
+
+/**
+ * Adds generic, accessible interaction listeners to the passed element.  
+ * All listeners have the default behavior prevented and stop propagation (for keyboard events only as long as the captured key is valid).
+ * @param listenerOptions Provide a {@linkcode listenerOptions} object to configure the listeners
+ */
+export function onInteraction<TElem extends HTMLElement>(elem: TElem, listener: (evt: MouseEvent | KeyboardEvent) => void, listenerOptions?: AddEventListenerOptions) {
+  const proxListener = (e: MouseEvent | KeyboardEvent) => {
+    if(e instanceof KeyboardEvent) {
+      if(interactionKeys.includes(e.key)) {
+        e.preventDefault();
+        e.stopPropagation();
+      }
+      else return;
+    }
+    else if(e instanceof MouseEvent) {
+      e.preventDefault();
+      e.stopPropagation();
+    }
+
+    // clean up the other listener that isn't automatically removed if `once` is set
+    listenerOptions?.once && e.type === "keydown" && elem.removeEventListener("click", proxListener, listenerOptions);
+    listenerOptions?.once && e.type === "click" && elem.removeEventListener("keydown", proxListener, listenerOptions);
+    listener(e);
+  };
+  elem.addEventListener("click", proxListener, listenerOptions);
+  elem.addEventListener("keydown", proxListener, listenerOptions);
+}