@base-ripple/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +165 -0
- package/dist/index.js +179 -0
- package/dist/styles.css +1 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# @base-ripple/core
|
|
2
|
+
|
|
3
|
+
Framework-agnostic ripple effect for DOM elements. Use this core package for
|
|
4
|
+
vanilla JS or to build your own wrapper. If you are using a framework, prefer
|
|
5
|
+
the framework-specific package (for example `@base-ripple/react`).
|
|
6
|
+
|
|
7
|
+
Demo: <https://base-ripple.vercel.app/>
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i @base-ripple/core
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start (vanilla)
|
|
16
|
+
|
|
17
|
+
```html
|
|
18
|
+
<button class="base-ripple-container">Click me</button>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import "@base-ripple/core/styles.css";
|
|
23
|
+
import { attachBaseRipple } from "@base-ripple/core";
|
|
24
|
+
|
|
25
|
+
const button = document.querySelector<HTMLButtonElement>(
|
|
26
|
+
".base-ripple-container",
|
|
27
|
+
);
|
|
28
|
+
if (!button) throw new Error("Missing button");
|
|
29
|
+
|
|
30
|
+
const dispose = attachBaseRipple(button, {
|
|
31
|
+
origin: "pointer",
|
|
32
|
+
sizeOffset: 96,
|
|
33
|
+
attributes: { "data-ripple": "true" },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// later
|
|
37
|
+
dispose();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The ripple attaches listeners to the target element and global pointer events.
|
|
41
|
+
Call the returned `dispose` function on teardown.
|
|
42
|
+
|
|
43
|
+
## Styles (required)
|
|
44
|
+
|
|
45
|
+
The ripple uses two class names:
|
|
46
|
+
|
|
47
|
+
- `.base-ripple-container` on the target element
|
|
48
|
+
- `.base-ripple` on the injected `<span>`
|
|
49
|
+
|
|
50
|
+
Option A: Import the default styles (recommended)
|
|
51
|
+
|
|
52
|
+
```css
|
|
53
|
+
@import "@base-ripple/core/styles.css";
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The default stylesheet handles layout and animation. You still need to set a
|
|
57
|
+
fill color and opacity, for example:
|
|
58
|
+
|
|
59
|
+
```css
|
|
60
|
+
.base-ripple {
|
|
61
|
+
background: currentColor;
|
|
62
|
+
opacity: 0.12;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Option B: Provide your own styles (advanced)
|
|
67
|
+
|
|
68
|
+
If you do not import `styles.css`, create your own CSS using the same class
|
|
69
|
+
names. Make sure the container is positioned and clips overflow. Here is the
|
|
70
|
+
default `styles.css` you can copy and customize:
|
|
71
|
+
|
|
72
|
+
```css
|
|
73
|
+
.base-ripple-container {
|
|
74
|
+
position: relative;
|
|
75
|
+
overflow: hidden;
|
|
76
|
+
touch-action: manipulation;
|
|
77
|
+
-webkit-tap-highlight-color: transparent;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.base-ripple {
|
|
81
|
+
position: absolute;
|
|
82
|
+
top: 0;
|
|
83
|
+
left: 0;
|
|
84
|
+
border-radius: 50%;
|
|
85
|
+
pointer-events: none;
|
|
86
|
+
animation: baseRipple 600ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
|
87
|
+
transition: opacity 600ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
88
|
+
will-change: transform, opacity;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@keyframes baseRipple {
|
|
92
|
+
from {
|
|
93
|
+
transform: translate3d(
|
|
94
|
+
calc(var(--base-ripple-keyframes-from-x) - 50%),
|
|
95
|
+
calc(var(--base-ripple-keyframes-from-y) - 50%),
|
|
96
|
+
0
|
|
97
|
+
)
|
|
98
|
+
scale(0);
|
|
99
|
+
}
|
|
100
|
+
to {
|
|
101
|
+
transform: translate3d(
|
|
102
|
+
calc(var(--base-ripple-keyframes-to-x) - 50%),
|
|
103
|
+
calc(var(--base-ripple-keyframes-to-y) - 50%),
|
|
104
|
+
0
|
|
105
|
+
)
|
|
106
|
+
scale(1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
IMPORTANT: If you write your own `.base-ripple` styles, it MUST include a
|
|
112
|
+
`transition` for `opacity`. Cleanup relies on the `transitionend` event for
|
|
113
|
+
`opacity`. Without it, ripple elements never remove themselves. If you want no
|
|
114
|
+
transition, set a `transition` with 0 duration, for example:
|
|
115
|
+
|
|
116
|
+
```css
|
|
117
|
+
.base-ripple {
|
|
118
|
+
transition: opacity 0ms linear;
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## API
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import type { AttachBaseRippleOptions } from "@base-ripple/core";
|
|
126
|
+
|
|
127
|
+
attachBaseRipple(
|
|
128
|
+
container: HTMLElement,
|
|
129
|
+
options?: AttachBaseRippleOptions
|
|
130
|
+
): () => void;
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Options
|
|
134
|
+
|
|
135
|
+
| Prop | Type | Default | Description |
|
|
136
|
+
| ------------ | ------------------------ | ----------- | ------------------------------------------------------------------------------------------------------ |
|
|
137
|
+
| `origin` | `"pointer" \| "center"` | `pointer` | Where the ripple starts. `pointer` uses the pointer position; `center` starts from the element center. |
|
|
138
|
+
| `sizeOffset` | `number` | `0` | Extra pixels added to the ripple diameter, useful when adding blur or glow. |
|
|
139
|
+
| `attributes` | `Record<string, string>` | `undefined` | Extra attributes added to each ripple `<span>`. |
|
|
140
|
+
|
|
141
|
+
## Behavior notes
|
|
142
|
+
|
|
143
|
+
- Uses pointer and keyboard interactions (Space, Enter, NumpadEnter).
|
|
144
|
+
- Respects `prefers-reduced-motion` by disabling ripples and clearing existing.
|
|
145
|
+
- Skips ripples for disabled elements (`disabled` or `aria-disabled="true"`).
|
|
146
|
+
- Adds `aria-hidden="true"` to the ripple spans for accessibility.
|
|
147
|
+
- Browser-only API; call it after the DOM is available.
|
|
148
|
+
|
|
149
|
+
## Framework packages
|
|
150
|
+
|
|
151
|
+
Use the framework-specific package when available:
|
|
152
|
+
|
|
153
|
+
- React: `@base-ripple/react`
|
|
154
|
+
|
|
155
|
+
This core package is ideal for vanilla usage or custom integrations.
|
|
156
|
+
|
|
157
|
+
## Planned framework wrappers (not implemented yet)
|
|
158
|
+
|
|
159
|
+
- Preact
|
|
160
|
+
- Vue
|
|
161
|
+
- Svelte
|
|
162
|
+
- Solid
|
|
163
|
+
- Angular
|
|
164
|
+
- Lit
|
|
165
|
+
- Qwik
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const f = /* @__PURE__ */ new Set();
|
|
2
|
+
let m = !1;
|
|
3
|
+
function S() {
|
|
4
|
+
m || (m = !0, addEventListener("blur", c), addEventListener("pagehide", c), addEventListener("beforeunload", c), addEventListener("pointerup", h, { passive: !0 }), addEventListener("pointercancel", h, { passive: !0 }), document.addEventListener("visibilitychange", B));
|
|
5
|
+
}
|
|
6
|
+
function N() {
|
|
7
|
+
!m || f.size > 0 || (m = !1, removeEventListener("blur", c), removeEventListener("pagehide", c), removeEventListener("beforeunload", c), removeEventListener("pointerup", h), removeEventListener("pointercancel", h), document.removeEventListener("visibilitychange", B));
|
|
8
|
+
}
|
|
9
|
+
function c() {
|
|
10
|
+
for (const e of f)
|
|
11
|
+
e.fadeAll();
|
|
12
|
+
}
|
|
13
|
+
function h(e) {
|
|
14
|
+
for (const i of f)
|
|
15
|
+
i.fadeFromPointer(e);
|
|
16
|
+
}
|
|
17
|
+
function B() {
|
|
18
|
+
document.visibilityState === "hidden" && c();
|
|
19
|
+
}
|
|
20
|
+
function _(e, {
|
|
21
|
+
origin: i = "pointer",
|
|
22
|
+
sizeOffset: s = 0,
|
|
23
|
+
attributes: n
|
|
24
|
+
} = {}) {
|
|
25
|
+
const a = /* @__PURE__ */ new Set(), o = /* @__PURE__ */ new Map(), d = /* @__PURE__ */ new Map(), b = i === "pointer";
|
|
26
|
+
let v = !1, u = O();
|
|
27
|
+
const g = (t, r, l) => ({
|
|
28
|
+
from: {
|
|
29
|
+
size: 0,
|
|
30
|
+
x: r,
|
|
31
|
+
y: l
|
|
32
|
+
},
|
|
33
|
+
to: {
|
|
34
|
+
size: (b ? Q(
|
|
35
|
+
t.width,
|
|
36
|
+
t.height,
|
|
37
|
+
r,
|
|
38
|
+
l
|
|
39
|
+
) : K(
|
|
40
|
+
t.width,
|
|
41
|
+
t.height
|
|
42
|
+
)) + s,
|
|
43
|
+
x: b ? r : t.width / 2,
|
|
44
|
+
y: b ? l : t.height / 2
|
|
45
|
+
}
|
|
46
|
+
}), x = (t) => {
|
|
47
|
+
if (t.defaultPrevented || I(e) || u) return;
|
|
48
|
+
const r = e.getBoundingClientRect(), l = g(
|
|
49
|
+
r,
|
|
50
|
+
t.clientX - r.x,
|
|
51
|
+
t.clientY - r.y
|
|
52
|
+
), p = D({
|
|
53
|
+
rippleKeyframes: l,
|
|
54
|
+
removeHandler: R,
|
|
55
|
+
attributes: n
|
|
56
|
+
});
|
|
57
|
+
a.add(p), o.set(t.pointerId, p), e.appendChild(p);
|
|
58
|
+
}, w = (t) => {
|
|
59
|
+
if (t.defaultPrevented || I(e) || u || t.repeat || d.has(t.code) || !F(t.code) || q(t.target)) return;
|
|
60
|
+
const r = e.getBoundingClientRect(), l = g(
|
|
61
|
+
r,
|
|
62
|
+
r.width / 2,
|
|
63
|
+
r.height / 2
|
|
64
|
+
), p = D({
|
|
65
|
+
rippleKeyframes: l,
|
|
66
|
+
removeHandler: R,
|
|
67
|
+
attributes: n
|
|
68
|
+
});
|
|
69
|
+
a.add(p), d.set(t.code, p), e.appendChild(p);
|
|
70
|
+
}, y = (t) => {
|
|
71
|
+
t.style.opacity = "0";
|
|
72
|
+
}, E = (t) => {
|
|
73
|
+
const r = o.get(t.pointerId);
|
|
74
|
+
r && (y(r), o.delete(t.pointerId));
|
|
75
|
+
}, z = (t) => {
|
|
76
|
+
const r = d.get(t.code);
|
|
77
|
+
r && (y(r), d.delete(t.code));
|
|
78
|
+
}, L = () => {
|
|
79
|
+
for (const t of o.values()) y(t);
|
|
80
|
+
o.clear();
|
|
81
|
+
for (const t of d.values()) y(t);
|
|
82
|
+
d.clear();
|
|
83
|
+
}, R = (t) => {
|
|
84
|
+
a.delete(t);
|
|
85
|
+
for (const [r, l] of o)
|
|
86
|
+
if (l === t) {
|
|
87
|
+
o.delete(r);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
for (const [r, l] of d)
|
|
91
|
+
if (l === t) {
|
|
92
|
+
d.delete(r);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
t.remove();
|
|
96
|
+
}, P = () => {
|
|
97
|
+
for (const t of a)
|
|
98
|
+
t.remove();
|
|
99
|
+
a.clear(), o.clear(), d.clear();
|
|
100
|
+
}, M = () => {
|
|
101
|
+
v || (e.addEventListener("pointerdown", x, {
|
|
102
|
+
passive: !0
|
|
103
|
+
}), v = !0);
|
|
104
|
+
}, k = () => {
|
|
105
|
+
v && (e.removeEventListener("pointerdown", x), v = !1);
|
|
106
|
+
}, G = T((t) => {
|
|
107
|
+
u !== t && (u = t, u ? (k(), P()) : M());
|
|
108
|
+
}), A = (t) => {
|
|
109
|
+
F(t.code) && z(t);
|
|
110
|
+
};
|
|
111
|
+
u || M(), e.addEventListener("keyup", A), e.addEventListener("keydown", w), e.addEventListener("contextmenu", L), e.addEventListener("pointerleave", E, {
|
|
112
|
+
passive: !0
|
|
113
|
+
}), S();
|
|
114
|
+
const C = {
|
|
115
|
+
fadeAll: L,
|
|
116
|
+
fadeFromPointer: E
|
|
117
|
+
};
|
|
118
|
+
return f.add(C), () => {
|
|
119
|
+
G(), k(), e.removeEventListener("keyup", A), e.removeEventListener("keydown", w), e.removeEventListener("contextmenu", L), e.removeEventListener("pointerleave", E), f.delete(C), N(), P();
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const H = "(prefers-reduced-motion: reduce)";
|
|
123
|
+
function O() {
|
|
124
|
+
return typeof matchMedia != "function" ? !1 : matchMedia(H).matches;
|
|
125
|
+
}
|
|
126
|
+
function T(e) {
|
|
127
|
+
if (typeof matchMedia != "function") return () => {
|
|
128
|
+
};
|
|
129
|
+
const i = matchMedia(H), s = (n) => e(n.matches);
|
|
130
|
+
return typeof i.addEventListener == "function" ? (i.addEventListener("change", s), () => i.removeEventListener("change", s)) : typeof i.addListener == "function" ? (i.addListener(s), () => i.removeListener(s)) : () => {
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function D({
|
|
134
|
+
rippleKeyframes: e,
|
|
135
|
+
removeHandler: i,
|
|
136
|
+
attributes: s
|
|
137
|
+
}) {
|
|
138
|
+
const n = document.createElement("span");
|
|
139
|
+
if (n.setAttribute("aria-hidden", "true"), n.className = "base-ripple", n.style.width = e.to.size + "px", n.style.height = e.to.size + "px", n.style.setProperty(
|
|
140
|
+
"--base-ripple-keyframes-from-x",
|
|
141
|
+
e.from.x + "px"
|
|
142
|
+
), n.style.setProperty(
|
|
143
|
+
"--base-ripple-keyframes-from-y",
|
|
144
|
+
e.from.y + "px"
|
|
145
|
+
), n.style.setProperty(
|
|
146
|
+
"--base-ripple-keyframes-to-x",
|
|
147
|
+
e.to.x + "px"
|
|
148
|
+
), n.style.setProperty(
|
|
149
|
+
"--base-ripple-keyframes-to-y",
|
|
150
|
+
e.to.y + "px"
|
|
151
|
+
), s)
|
|
152
|
+
for (const [o, d] of Object.entries(s))
|
|
153
|
+
n.setAttribute(o, d);
|
|
154
|
+
const a = (o) => {
|
|
155
|
+
o.propertyName === "opacity" && (i(n), n.removeEventListener("transitionend", a));
|
|
156
|
+
};
|
|
157
|
+
return n.addEventListener("transitionend", a), n;
|
|
158
|
+
}
|
|
159
|
+
function I(e) {
|
|
160
|
+
const i = e.getAttribute("aria-disabled");
|
|
161
|
+
return i !== null ? i.toLowerCase() === "true" : "disabled" in e && !!e.disabled;
|
|
162
|
+
}
|
|
163
|
+
function F(e) {
|
|
164
|
+
return e === "Space" || e === "Enter" || e === "NumpadEnter";
|
|
165
|
+
}
|
|
166
|
+
const U = 'input:not([type]),input[type="text"],input[type="search"],input[type="url"],input[type="tel"],input[type="email"],input[type="password"],input[type="number"],input[type="date"],input[type="time"],input[type="datetime-local"],input[type="month"],input[type="week"],textarea,[contenteditable]';
|
|
167
|
+
function q(e) {
|
|
168
|
+
return e instanceof Element ? !!e.closest(U) : !1;
|
|
169
|
+
}
|
|
170
|
+
function K(e, i) {
|
|
171
|
+
return Math.sqrt(e * e + i * i);
|
|
172
|
+
}
|
|
173
|
+
function Q(e, i, s, n) {
|
|
174
|
+
const a = Math.max(s, e - s), o = Math.max(n, i - n);
|
|
175
|
+
return 2 * Math.sqrt(a * a + o * o);
|
|
176
|
+
}
|
|
177
|
+
export {
|
|
178
|
+
_ as attachBaseRipple
|
|
179
|
+
};
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.base-ripple-container{touch-action:manipulation;-webkit-tap-highlight-color:transparent;position:relative;overflow:hidden}.base-ripple{pointer-events:none;will-change:transform,opacity;border-radius:50%;transition:opacity .6s cubic-bezier(.4,0,.2,1);animation:.6s cubic-bezier(.4,0,.2,1) forwards baseRipple;position:absolute;top:0;left:0}@keyframes baseRipple{0%{transform:translate3d(calc(var(--base-ripple-keyframes-from-x) - 50%),calc(var(--base-ripple-keyframes-from-y) - 50%),0)scale(0)}to{transform:translate3d(calc(var(--base-ripple-keyframes-to-x) - 50%),calc(var(--base-ripple-keyframes-to-y) - 50%),0)scale(1)}}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@base-ripple/core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
"./package.json": "./package.json",
|
|
10
|
+
".": {
|
|
11
|
+
"@base-ripple/source": "./src/index.ts",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./styles.css": {
|
|
17
|
+
"@base-ripple/source": "./src/styles.css",
|
|
18
|
+
"default": "./dist/styles.css"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"sideEffects": [
|
|
22
|
+
"./dist/styles.css"
|
|
23
|
+
],
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"!**/*.tsbuildinfo"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"tslib": "^2.3.0"
|
|
30
|
+
},
|
|
31
|
+
"nx": {
|
|
32
|
+
"tags": [
|
|
33
|
+
"scope:core"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
}
|
|
39
|
+
}
|