@gfazioli/mantine-rings-progress 4.0.0 → 4.1.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 +5 -1
- package/dist/cjs/RingsProgress.cjs +188 -18
- package/dist/cjs/RingsProgress.cjs.map +1 -1
- package/dist/cjs/RingsProgress.module.css.cjs +1 -1
- package/dist/esm/RingsProgress.mjs +189 -19
- package/dist/esm/RingsProgress.mjs.map +1 -1
- package/dist/esm/RingsProgress.module.css.mjs +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.layer.css +1 -1
- package/dist/types/RingsProgress.d.ts +31 -2
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import
|
|
3
|
-
import { factory, useMantineTheme, useProps, useStyles, Box, parseThemeColor, RingProgress, alpha, Stack, Group, ColorSwatch, Text, Tooltip } from '@mantine/core';
|
|
2
|
+
import { factory, useMantineTheme, useProps, useStyles, parseThemeColor, Box, RingProgress, alpha, Stack, Group, ColorSwatch, Text, Tooltip } from '@mantine/core';
|
|
4
3
|
import { useReducedMotion } from '@mantine/hooks';
|
|
4
|
+
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
|
5
5
|
import classes from './RingsProgress.module.css.mjs';
|
|
6
6
|
|
|
7
7
|
const defaultProps = {
|
|
@@ -9,7 +9,9 @@ const defaultProps = {
|
|
|
9
9
|
thickness: 12,
|
|
10
10
|
gap: 8,
|
|
11
11
|
animate: false,
|
|
12
|
+
animateValueChanges: false,
|
|
12
13
|
roundCaps: true,
|
|
14
|
+
showValues: false,
|
|
13
15
|
transitionDuration: 0,
|
|
14
16
|
rootColorAlpha: 0.15,
|
|
15
17
|
staggerDelay: 0,
|
|
@@ -31,6 +33,7 @@ const RingsProgress = factory((_props) => {
|
|
|
31
33
|
rootColorAlpha,
|
|
32
34
|
label,
|
|
33
35
|
animate,
|
|
36
|
+
animateValueChanges,
|
|
34
37
|
transitionDuration,
|
|
35
38
|
roundCaps,
|
|
36
39
|
staggerDelay,
|
|
@@ -41,6 +44,8 @@ const RingsProgress = factory((_props) => {
|
|
|
41
44
|
direction,
|
|
42
45
|
onRingComplete,
|
|
43
46
|
withTooltip,
|
|
47
|
+
showValues,
|
|
48
|
+
formatValue,
|
|
44
49
|
classNames,
|
|
45
50
|
styles,
|
|
46
51
|
unstyled,
|
|
@@ -56,6 +61,10 @@ const RingsProgress = factory((_props) => {
|
|
|
56
61
|
unstyled,
|
|
57
62
|
vars
|
|
58
63
|
});
|
|
64
|
+
const [instanceId] = useState(() => Math.random().toString(36).slice(2, 9));
|
|
65
|
+
const ringsRef = useRef(rings);
|
|
66
|
+
ringsRef.current = rings;
|
|
67
|
+
const ringCount = rings.length;
|
|
59
68
|
const [mountedRings, setMountedRings] = useState(
|
|
60
69
|
() => rings.map(() => !animate || reduceMotion)
|
|
61
70
|
);
|
|
@@ -65,15 +74,14 @@ const RingsProgress = factory((_props) => {
|
|
|
65
74
|
timeoutsRef.current = [];
|
|
66
75
|
}, []);
|
|
67
76
|
useEffect(() => {
|
|
68
|
-
const ringCount2 = rings.length;
|
|
69
77
|
if (!animate || reduceMotion) {
|
|
70
|
-
setMountedRings(Array.from({ length:
|
|
78
|
+
setMountedRings(Array.from({ length: ringCount }, () => true));
|
|
71
79
|
return;
|
|
72
80
|
}
|
|
73
|
-
setMountedRings(Array.from({ length:
|
|
81
|
+
setMountedRings(Array.from({ length: ringCount }, () => false));
|
|
74
82
|
cleanupTimeouts();
|
|
75
83
|
if (staggerDelay && staggerDelay > 0) {
|
|
76
|
-
for (let index = 0; index <
|
|
84
|
+
for (let index = 0; index < ringCount; index++) {
|
|
77
85
|
const timeout = setTimeout(() => {
|
|
78
86
|
setMountedRings((prev) => {
|
|
79
87
|
const next = [...prev];
|
|
@@ -85,21 +93,22 @@ const RingsProgress = factory((_props) => {
|
|
|
85
93
|
}
|
|
86
94
|
} else {
|
|
87
95
|
requestAnimationFrame(() => {
|
|
88
|
-
setMountedRings(Array.from({ length:
|
|
96
|
+
setMountedRings(Array.from({ length: ringCount }, () => true));
|
|
89
97
|
});
|
|
90
98
|
}
|
|
91
99
|
return cleanupTimeouts;
|
|
92
|
-
}, [animate, reduceMotion,
|
|
100
|
+
}, [animate, reduceMotion, ringCount, staggerDelay, cleanupTimeouts]);
|
|
93
101
|
const prevValuesRef = useRef(rings.map((r) => r.value));
|
|
94
102
|
const [pulsingRings, setPulsingRings] = useState(rings.map(() => false));
|
|
95
103
|
const ringValuesKey = rings.map((r) => r.value).join(",");
|
|
96
|
-
const ringCount = rings.length;
|
|
97
104
|
useEffect(() => {
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
const current = ringsRef.current;
|
|
106
|
+
prevValuesRef.current = current.map((r) => r.value);
|
|
107
|
+
setPulsingRings(current.map(() => false));
|
|
100
108
|
}, [ringCount]);
|
|
101
109
|
useEffect(() => {
|
|
102
|
-
const
|
|
110
|
+
const current = ringsRef.current;
|
|
111
|
+
const currentValues = current.map((r) => r.value);
|
|
103
112
|
const crossedComplete = currentValues.map((value, i) => {
|
|
104
113
|
const prev = prevValuesRef.current[i] ?? 0;
|
|
105
114
|
return value >= 100 && prev < 100;
|
|
@@ -107,7 +116,7 @@ const RingsProgress = factory((_props) => {
|
|
|
107
116
|
if (onRingComplete) {
|
|
108
117
|
crossedComplete.forEach((crossed, i) => {
|
|
109
118
|
if (crossed) {
|
|
110
|
-
onRingComplete(i,
|
|
119
|
+
onRingComplete(i, current[i]);
|
|
111
120
|
}
|
|
112
121
|
});
|
|
113
122
|
}
|
|
@@ -115,7 +124,20 @@ const RingsProgress = factory((_props) => {
|
|
|
115
124
|
setPulsingRings(crossedComplete);
|
|
116
125
|
}
|
|
117
126
|
prevValuesRef.current = currentValues;
|
|
118
|
-
}, [ringValuesKey, pulseOnComplete, reduceMotion, onRingComplete
|
|
127
|
+
}, [ringValuesKey, pulseOnComplete, reduceMotion, onRingComplete]);
|
|
128
|
+
const gradientDefs = rings.map((ring, index) => {
|
|
129
|
+
if (!ring.gradient) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const id = `rp-grad-${instanceId}-${index}`;
|
|
133
|
+
const cssDeg = ring.gradient.deg ?? 0;
|
|
134
|
+
const svgDeg = cssDeg - 90;
|
|
135
|
+
const fromColor = parseThemeColor({ color: ring.gradient.from, theme }).value;
|
|
136
|
+
const toColor = parseThemeColor({ color: ring.gradient.to, theme }).value;
|
|
137
|
+
return { id, fromColor, toColor, svgDeg };
|
|
138
|
+
}).filter(
|
|
139
|
+
(g) => g !== null
|
|
140
|
+
);
|
|
119
141
|
const handleAnimationEnd = useCallback((index) => {
|
|
120
142
|
setPulsingRings((prev) => {
|
|
121
143
|
const next = [...prev];
|
|
@@ -124,7 +146,7 @@ const RingsProgress = factory((_props) => {
|
|
|
124
146
|
});
|
|
125
147
|
}, []);
|
|
126
148
|
const allMounted = mountedRings.every(Boolean);
|
|
127
|
-
const effectiveTransitionDuration = reduceMotion ? 0 : animate && !allMounted ? transitionDuration || 1e3 : transitionDuration;
|
|
149
|
+
const effectiveTransitionDuration = reduceMotion ? 0 : animate && !allMounted ? transitionDuration || 1e3 : animateValueChanges ? transitionDuration || 500 : transitionDuration;
|
|
128
150
|
const offsets = [];
|
|
129
151
|
let cumulativeOffset = 0;
|
|
130
152
|
for (let i = 0; i < rings.length; i++) {
|
|
@@ -132,16 +154,107 @@ const RingsProgress = factory((_props) => {
|
|
|
132
154
|
const ringThickness = rings[i].thickness ?? thickness;
|
|
133
155
|
cumulativeOffset += (ringThickness ?? 0) + (gap ?? 0);
|
|
134
156
|
}
|
|
157
|
+
const hasInteractive = rings.some((r) => r.onClick || r.onHover);
|
|
158
|
+
const [hoveredIndex, setHoveredIndex] = useState(null);
|
|
159
|
+
const hoveredIndexRef = useRef(null);
|
|
160
|
+
hoveredIndexRef.current = hoveredIndex;
|
|
161
|
+
const ringAtPoint = useCallback(
|
|
162
|
+
(clientX, clientY, rect) => {
|
|
163
|
+
const cx = rect.left + rect.width / 2;
|
|
164
|
+
const cy = rect.top + rect.height / 2;
|
|
165
|
+
const r = Math.hypot(clientX - cx, clientY - cy);
|
|
166
|
+
const current = ringsRef.current;
|
|
167
|
+
for (let i = 0; i < current.length; i++) {
|
|
168
|
+
const t = current[i].thickness ?? thickness ?? 12;
|
|
169
|
+
const ringSize = (size ?? 120) - offsets[i] * 2;
|
|
170
|
+
const centerR = (ringSize * 0.9 - t * 2) / 2;
|
|
171
|
+
if (Math.abs(r - centerR) <= t / 2) {
|
|
172
|
+
return i;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
},
|
|
177
|
+
// offsets is recomputed every render; capture by closure rather than listing it
|
|
178
|
+
// (size/thickness are stable refs through props).
|
|
179
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
180
|
+
[size, thickness, ringCount]
|
|
181
|
+
);
|
|
182
|
+
const handleContainerClick = useCallback(
|
|
183
|
+
(event) => {
|
|
184
|
+
if (!hasInteractive) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
188
|
+
const idx = ringAtPoint(event.clientX, event.clientY, rect);
|
|
189
|
+
if (idx === null) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const ring = ringsRef.current[idx];
|
|
193
|
+
ring.onClick?.(ring, idx);
|
|
194
|
+
},
|
|
195
|
+
[hasInteractive, ringAtPoint]
|
|
196
|
+
);
|
|
197
|
+
const handleContainerMouseMove = useCallback(
|
|
198
|
+
(event) => {
|
|
199
|
+
if (!hasInteractive) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
203
|
+
const idx = ringAtPoint(event.clientX, event.clientY, rect);
|
|
204
|
+
if (idx === hoveredIndexRef.current) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const prev = hoveredIndexRef.current;
|
|
208
|
+
if (prev !== null) {
|
|
209
|
+
const prevRing = ringsRef.current[prev];
|
|
210
|
+
prevRing.onHover?.(prevRing, prev, false);
|
|
211
|
+
}
|
|
212
|
+
setHoveredIndex(idx);
|
|
213
|
+
if (idx !== null) {
|
|
214
|
+
const newRing = ringsRef.current[idx];
|
|
215
|
+
newRing.onHover?.(newRing, idx, true);
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
[hasInteractive, ringAtPoint]
|
|
219
|
+
);
|
|
220
|
+
const handleContainerMouseLeave = useCallback(() => {
|
|
221
|
+
if (hoveredIndexRef.current === null) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const prev = hoveredIndexRef.current;
|
|
225
|
+
const prevRing = ringsRef.current[prev];
|
|
226
|
+
prevRing.onHover?.(prevRing, prev, false);
|
|
227
|
+
setHoveredIndex(null);
|
|
228
|
+
}, []);
|
|
229
|
+
const containerCursor = hoveredIndex !== null && rings[hoveredIndex]?.onClick ? "pointer" : void 0;
|
|
135
230
|
const glowBlur = glow === true ? 6 : typeof glow === "number" ? glow : 0;
|
|
136
231
|
const svgTransform = startAngle !== 0 || direction === "counterclockwise" ? `rotate(${ -90 + (startAngle ?? 0)}deg)${direction === "counterclockwise" ? " scaleX(-1)" : ""}` : void 0;
|
|
137
232
|
const content = /* @__PURE__ */ React.createElement(
|
|
138
233
|
Box,
|
|
139
234
|
{
|
|
140
|
-
...getStyles("root", {
|
|
235
|
+
...getStyles("root", {
|
|
236
|
+
style: { width: size, height: size, cursor: containerCursor }
|
|
237
|
+
}),
|
|
141
238
|
...others,
|
|
142
239
|
role: others.role ?? "group",
|
|
143
|
-
"aria-label": others["aria-label"] ?? (others["aria-labelledby"] ? void 0 : "Progress rings")
|
|
240
|
+
"aria-label": others["aria-label"] ?? (others["aria-labelledby"] ? void 0 : "Progress rings"),
|
|
241
|
+
onClick: hasInteractive ? handleContainerClick : others.onClick,
|
|
242
|
+
onMouseMove: hasInteractive ? handleContainerMouseMove : others.onMouseMove,
|
|
243
|
+
onMouseLeave: hasInteractive ? handleContainerMouseLeave : others.onMouseLeave,
|
|
244
|
+
"data-interactive": hasInteractive || void 0,
|
|
245
|
+
"data-hovered-ring": hoveredIndex ?? void 0
|
|
144
246
|
},
|
|
247
|
+
gradientDefs.length > 0 && /* @__PURE__ */ React.createElement("svg", { "aria-hidden": true, style: { position: "absolute", width: 0, height: 0, overflow: "hidden" } }, /* @__PURE__ */ React.createElement("defs", null, gradientDefs.map((g) => /* @__PURE__ */ React.createElement(
|
|
248
|
+
"linearGradient",
|
|
249
|
+
{
|
|
250
|
+
key: g.id,
|
|
251
|
+
id: g.id,
|
|
252
|
+
gradientUnits: "objectBoundingBox",
|
|
253
|
+
gradientTransform: `rotate(${g.svgDeg}, 0.5, 0.5)`
|
|
254
|
+
},
|
|
255
|
+
/* @__PURE__ */ React.createElement("stop", { offset: "0%", stopColor: g.fromColor }),
|
|
256
|
+
/* @__PURE__ */ React.createElement("stop", { offset: "100%", stopColor: g.toColor })
|
|
257
|
+
)))),
|
|
145
258
|
rings.map((ring, index) => {
|
|
146
259
|
const {
|
|
147
260
|
thickness: ringThicknessOverride,
|
|
@@ -150,6 +263,11 @@ const RingsProgress = factory((_props) => {
|
|
|
150
263
|
glowIntensity,
|
|
151
264
|
glowColor,
|
|
152
265
|
rootColor: ringRootColor,
|
|
266
|
+
onClick: ringOnClick,
|
|
267
|
+
onHover: _ringOnHover,
|
|
268
|
+
showValue: _ringShowValue,
|
|
269
|
+
formatValue: _ringFormatValue,
|
|
270
|
+
gradient: _ringGradient,
|
|
153
271
|
...ringSection
|
|
154
272
|
} = ring;
|
|
155
273
|
const parsedColor = parseThemeColor({ color: ring.color, theme });
|
|
@@ -161,7 +279,15 @@ const RingsProgress = factory((_props) => {
|
|
|
161
279
|
const ringGlowColor = glowColor ? parseThemeColor({ color: glowColor, theme }).value : parsedColor.value;
|
|
162
280
|
const glowFilter = ringGlowBlur > 0 ? `drop-shadow(0 0 ${ringGlowBlur}px ${ringGlowColor})` : void 0;
|
|
163
281
|
const { tooltip: _tooltip, ...sectionWithoutTooltip } = ringSection;
|
|
282
|
+
const sectionColor = ring.gradient ? `url(#rp-grad-${instanceId}-${index})` : sectionWithoutTooltip.color;
|
|
164
283
|
const isPulsing = pulseOnComplete && pulsingRings[index];
|
|
284
|
+
const interactive = Boolean(ringOnClick);
|
|
285
|
+
const handleKeyDown = ringOnClick ? (event) => {
|
|
286
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
287
|
+
event.preventDefault();
|
|
288
|
+
ringOnClick(ring, index);
|
|
289
|
+
}
|
|
290
|
+
} : void 0;
|
|
165
291
|
return /* @__PURE__ */ React.createElement(
|
|
166
292
|
RingProgress,
|
|
167
293
|
{
|
|
@@ -171,12 +297,20 @@ const RingsProgress = factory((_props) => {
|
|
|
171
297
|
thickness: ringThickness,
|
|
172
298
|
roundCaps: ringRoundCaps,
|
|
173
299
|
transitionDuration: effectiveTransitionDuration,
|
|
174
|
-
sections: [
|
|
175
|
-
|
|
300
|
+
sections: [
|
|
301
|
+
{
|
|
302
|
+
...sectionWithoutTooltip,
|
|
303
|
+
value: effectiveValue,
|
|
304
|
+
color: sectionColor
|
|
305
|
+
}
|
|
306
|
+
],
|
|
307
|
+
role: interactive ? "button" : "progressbar",
|
|
176
308
|
"aria-valuenow": Math.round(ring.value),
|
|
177
309
|
"aria-valuemin": 0,
|
|
178
310
|
"aria-valuemax": 100,
|
|
179
311
|
"aria-label": ringAriaLabel,
|
|
312
|
+
tabIndex: interactive ? 0 : void 0,
|
|
313
|
+
onKeyDown: handleKeyDown,
|
|
180
314
|
styles: glowFilter || svgTransform ? {
|
|
181
315
|
svg: {
|
|
182
316
|
...glowFilter ? { filter: glowFilter } : {},
|
|
@@ -195,6 +329,42 @@ const RingsProgress = factory((_props) => {
|
|
|
195
329
|
}
|
|
196
330
|
);
|
|
197
331
|
}),
|
|
332
|
+
(showValues || rings.some((r) => r.showValue)) && rings.map((ring, index) => {
|
|
333
|
+
const shouldShow = ring.showValue ?? showValues;
|
|
334
|
+
if (!shouldShow) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
const ringT = ring.thickness ?? thickness ?? 12;
|
|
338
|
+
const ringSize = (size ?? 120) - offsets[index] * 2;
|
|
339
|
+
const ringR = (ringSize * 0.9 - ringT * 2) / 2;
|
|
340
|
+
const clampedValue = Math.max(0, Math.min(100, ring.value));
|
|
341
|
+
const directionMultiplier = direction === "counterclockwise" ? -1 : 1;
|
|
342
|
+
const endAngleDeg = (startAngle ?? 0) + clampedValue / 100 * 360 * directionMultiplier;
|
|
343
|
+
const angleRad = endAngleDeg * Math.PI / 180;
|
|
344
|
+
const center = (size ?? 120) / 2;
|
|
345
|
+
const x = center + ringR * Math.sin(angleRad);
|
|
346
|
+
const y = center - ringR * Math.cos(angleRad);
|
|
347
|
+
const formatter = ring.formatValue ?? formatValue ?? ((v) => `${Math.round(v)}%`);
|
|
348
|
+
const labelColor = parseThemeColor({ color: ring.color, theme }).value;
|
|
349
|
+
return /* @__PURE__ */ React.createElement(
|
|
350
|
+
Box,
|
|
351
|
+
{
|
|
352
|
+
key: `value-${index}`,
|
|
353
|
+
...getStyles("valueLabel", {
|
|
354
|
+
style: {
|
|
355
|
+
position: "absolute",
|
|
356
|
+
left: x,
|
|
357
|
+
top: y,
|
|
358
|
+
transform: "translate(-50%, -50%)",
|
|
359
|
+
pointerEvents: "none",
|
|
360
|
+
color: labelColor
|
|
361
|
+
}
|
|
362
|
+
}),
|
|
363
|
+
"data-ring-index": index
|
|
364
|
+
},
|
|
365
|
+
formatter(ring.value)
|
|
366
|
+
);
|
|
367
|
+
}),
|
|
198
368
|
label && /* @__PURE__ */ React.createElement(Box, { ...getStyles("label") }, label)
|
|
199
369
|
);
|
|
200
370
|
if (withTooltip) {
|