@udixio/ui-react 2.10.10 → 2.10.12
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/CHANGELOG.md +31 -0
- package/dist/index.cjs +3 -3
- package/dist/index.js +2740 -2655
- package/dist/lib/components/Carousel.d.ts.map +1 -1
- package/dist/lib/components/CarouselItem.d.ts.map +1 -1
- package/dist/lib/components/ProgressIndicator.d.ts.map +1 -1
- package/dist/lib/effects/ThemeProvider.d.ts.map +1 -1
- package/dist/lib/effects/theme.worker.d.ts +13 -0
- package/dist/lib/effects/theme.worker.d.ts.map +1 -0
- package/dist/lib/interfaces/progress-indicator.interface.d.ts +7 -1
- package/dist/lib/interfaces/progress-indicator.interface.d.ts.map +1 -1
- package/dist/lib/styles/carousel-item.style.d.ts.map +1 -1
- package/dist/lib/styles/progress-indicator.style.d.ts +2 -2
- package/dist/lib/styles/progress-indicator.style.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/lib/components/Card.tsx +1 -1
- package/src/lib/components/Carousel.tsx +148 -259
- package/src/lib/components/CarouselItem.tsx +3 -10
- package/src/lib/components/ProgressIndicator.tsx +80 -26
- package/src/lib/effects/ThemeProvider.tsx +70 -26
- package/src/lib/effects/theme.worker.ts +97 -0
- package/src/lib/interfaces/progress-indicator.interface.ts +7 -1
- package/src/lib/styles/card.style.ts +2 -2
- package/src/lib/styles/carousel-item.style.ts +1 -5
- package/src/lib/styles/progress-indicator.style.ts +24 -8
- package/vite.config.ts +0 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
1
|
+
import React, { useEffect, useLayoutEffect, useRef, useState, useCallback } from 'react';
|
|
2
2
|
import { animate } from 'motion/react';
|
|
3
3
|
import { CarouselInterface, CarouselItemInterface } from '../interfaces';
|
|
4
4
|
|
|
@@ -7,10 +7,6 @@ import { CustomScroll } from '../effects';
|
|
|
7
7
|
import { ReactProps } from '../utils';
|
|
8
8
|
import { CarouselItem, normalize } from './CarouselItem';
|
|
9
9
|
|
|
10
|
-
function clamp(v: number, min: number, max: number) {
|
|
11
|
-
return Math.max(min, Math.min(max, v));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
10
|
/**
|
|
15
11
|
* Carousels show a collection of items that can be scrolled on and off the screen
|
|
16
12
|
*
|
|
@@ -39,11 +35,9 @@ export const Carousel = ({
|
|
|
39
35
|
scrollSensitivity = 1.25,
|
|
40
36
|
...restProps
|
|
41
37
|
}: ReactProps<CarouselInterface>) => {
|
|
42
|
-
const defaultRef = useRef(null);
|
|
38
|
+
const defaultRef = useRef<HTMLDivElement>(null);
|
|
43
39
|
const ref = optionalRef || defaultRef;
|
|
44
40
|
|
|
45
|
-
const [translateX, setTranslateX] = useState(0);
|
|
46
|
-
|
|
47
41
|
const styles = useCarouselStyle({
|
|
48
42
|
index,
|
|
49
43
|
className,
|
|
@@ -63,78 +57,64 @@ export const Carousel = ({
|
|
|
63
57
|
);
|
|
64
58
|
|
|
65
59
|
const trackRef = useRef<HTMLDivElement>(null);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
scrollProgress:
|
|
71
|
-
scrollTotal:
|
|
72
|
-
scrollVisible:
|
|
73
|
-
scroll:
|
|
74
|
-
}
|
|
60
|
+
|
|
61
|
+
// OPTIMIZATION: We no longer store width and translate in React state to avoid laggy 60fps re-renders.
|
|
62
|
+
// We use refs instead.
|
|
63
|
+
const getScrollState = useRef({
|
|
64
|
+
scrollProgress: 0,
|
|
65
|
+
scrollTotal: 0,
|
|
66
|
+
scrollVisible: 0,
|
|
67
|
+
scroll: 0,
|
|
68
|
+
});
|
|
69
|
+
|
|
75
70
|
// Smoothed scroll progress using framer-motion animate()
|
|
76
71
|
const smoothedProgressRef = useRef(0);
|
|
77
72
|
const scrollAnimationRef = useRef<ReturnType<typeof animate> | null>(null);
|
|
78
73
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
!trackRef.current ||
|
|
82
|
-
!ref.current ||
|
|
83
|
-
scroll?.scrollProgress === undefined
|
|
84
|
-
)
|
|
85
|
-
return [];
|
|
86
|
-
|
|
87
|
-
const scrollVisible =
|
|
88
|
-
scroll?.scrollVisible ?? (ref.current as any)?.clientWidth ?? 0;
|
|
89
|
-
|
|
90
|
-
function assignRelativeIndexes(
|
|
91
|
-
values: number[],
|
|
92
|
-
progressScroll: number,
|
|
93
|
-
): {
|
|
94
|
-
itemScrollXCenter: number;
|
|
95
|
-
relativeIndex: number;
|
|
96
|
-
index: number;
|
|
97
|
-
width: number;
|
|
98
|
-
}[] {
|
|
99
|
-
return values.map((value, index) => {
|
|
100
|
-
const relativeIndex =
|
|
101
|
-
(value - progressScroll) / Math.abs(values[1] - values[0]);
|
|
102
|
-
return {
|
|
103
|
-
itemScrollXCenter: value,
|
|
104
|
-
relativeIndex,
|
|
105
|
-
index: index,
|
|
106
|
-
width: 0,
|
|
107
|
-
};
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const itemsScrollXCenter = items.map((_, index) => {
|
|
112
|
-
const itemRef = itemRefs[index];
|
|
74
|
+
const itemRefs = useRef<React.RefObject<HTMLDivElement | null>[]>([]).current;
|
|
75
|
+
const [selectedItem, setSelectedItem] = useState(0);
|
|
113
76
|
|
|
114
|
-
|
|
77
|
+
if (itemRefs.length !== items.length) {
|
|
78
|
+
itemRefs.length = 0; // reset
|
|
79
|
+
items.forEach((_, i) => {
|
|
80
|
+
itemRefs[i] = React.createRef<HTMLDivElement>();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
115
83
|
|
|
116
|
-
|
|
84
|
+
// Pure mathematical logic - kept from original
|
|
85
|
+
const updateLayoutFromCalculations = useCallback(() => {
|
|
86
|
+
if (!trackRef.current || !ref.current) return;
|
|
87
|
+
const currentScrollProgress = smoothedProgressRef.current;
|
|
88
|
+
|
|
89
|
+
// Need dimensions
|
|
90
|
+
const scrollVisible = getScrollState.current.scrollVisible || (ref.current as any).clientWidth || 0;
|
|
91
|
+
|
|
92
|
+
function assignRelativeIndexes(values: number[], progressScroll: number) {
|
|
93
|
+
return values.map((value, idx) => {
|
|
94
|
+
const relativeIndex = (value - progressScroll) / Math.abs(values[1] - values[0]);
|
|
95
|
+
return { itemScrollXCenter: value, relativeIndex, index: idx, width: 0 };
|
|
96
|
+
});
|
|
97
|
+
}
|
|
117
98
|
|
|
99
|
+
const itemsScrollXCenter = items.map((_, idx) => {
|
|
100
|
+
// Calculate original center normalized
|
|
101
|
+
const itemScrollXCenter = idx / Math.max(1, items.length - 1);
|
|
118
102
|
return normalize(itemScrollXCenter, [0, 1], [0, 1]);
|
|
119
103
|
});
|
|
120
104
|
|
|
121
105
|
const itemValues = assignRelativeIndexes(
|
|
122
106
|
itemsScrollXCenter,
|
|
123
|
-
|
|
107
|
+
currentScrollProgress,
|
|
124
108
|
).sort((a, b) => a.index - b.index);
|
|
125
109
|
|
|
126
|
-
let widthLeft =
|
|
127
|
-
(ref.current?.clientWidth ?? scrollVisible) + gap + outputRange[0] + gap;
|
|
110
|
+
let widthLeft = scrollVisible + gap + outputRange[0] + gap;
|
|
128
111
|
|
|
112
|
+
let localSelected = selectedItem;
|
|
129
113
|
const visibleItemValues = itemValues
|
|
130
114
|
.sort((a, b) => Math.abs(a.relativeIndex) - Math.abs(b.relativeIndex))
|
|
131
|
-
.map((item,
|
|
132
|
-
if (widthLeft <= 0)
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
if (index == 0) {
|
|
136
|
-
setSelectedItem(item.index);
|
|
137
|
-
}
|
|
115
|
+
.map((item, idx) => {
|
|
116
|
+
if (widthLeft <= 0) return undefined;
|
|
117
|
+
if (idx === 0) localSelected = item.index;
|
|
138
118
|
|
|
139
119
|
item.width = normalize(
|
|
140
120
|
widthLeft - gap,
|
|
@@ -144,48 +124,30 @@ export const Carousel = ({
|
|
|
144
124
|
|
|
145
125
|
widthLeft -= item.width + gap;
|
|
146
126
|
|
|
147
|
-
if (widthLeft
|
|
148
|
-
const newWidth =
|
|
149
|
-
item.width - ((outputRange[0] + gap) * 2 - widthLeft);
|
|
150
|
-
|
|
127
|
+
if (widthLeft !== 0 && widthLeft < (outputRange[0] + gap) * 2) {
|
|
128
|
+
const newWidth = item.width - ((outputRange[0] + gap) * 2 - widthLeft);
|
|
151
129
|
widthLeft += item.width;
|
|
152
130
|
item.width = newWidth;
|
|
153
131
|
widthLeft -= item.width;
|
|
154
|
-
} else if (widthLeft
|
|
132
|
+
} else if (widthLeft === 0 && item.width >= outputRange[0] * 2 + gap) {
|
|
155
133
|
const newWidth = item.width - (outputRange[0] + gap - widthLeft);
|
|
156
|
-
|
|
157
134
|
widthLeft += item.width;
|
|
158
135
|
item.width = newWidth;
|
|
159
136
|
widthLeft -= item.width;
|
|
160
137
|
}
|
|
161
138
|
return item;
|
|
162
139
|
})
|
|
163
|
-
.filter(Boolean) as
|
|
164
|
-
itemScrollXCenter: number;
|
|
165
|
-
relativeIndex: number;
|
|
166
|
-
index: number;
|
|
167
|
-
width: number;
|
|
168
|
-
}[];
|
|
169
|
-
|
|
170
|
-
const reverseItemsVisible = visibleItemValues.reverse();
|
|
171
|
-
const itemsVisibleByIndex = [...visibleItemValues].sort(
|
|
172
|
-
(a, b) => Math.abs(a.index) - Math.abs(b.index),
|
|
173
|
-
);
|
|
140
|
+
.filter(Boolean) as { itemScrollXCenter: number; relativeIndex: number; index: number; width: number; }[];
|
|
174
141
|
|
|
175
|
-
|
|
142
|
+
const reverseItemsVisible = [...visibleItemValues].reverse();
|
|
143
|
+
const itemsVisibleByIndex = [...visibleItemValues].sort((a, b) => Math.abs(a.index) - Math.abs(b.index));
|
|
176
144
|
|
|
177
|
-
reverseItemsVisible.forEach((item,
|
|
178
|
-
const nextItem = reverseItemsVisible[
|
|
145
|
+
reverseItemsVisible.forEach((item, idx) => {
|
|
146
|
+
const nextItem = reverseItemsVisible[idx + 1];
|
|
179
147
|
if (!nextItem) return;
|
|
180
148
|
|
|
181
|
-
const test =
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const newWidth = normalize(
|
|
185
|
-
test,
|
|
186
|
-
[0, 2],
|
|
187
|
-
[item.width + widthLeft, nextItem.width],
|
|
188
|
-
);
|
|
149
|
+
const test = 1 - (Math.abs(item.relativeIndex) - Math.abs(nextItem.relativeIndex));
|
|
150
|
+
const newWidth = normalize(test, [0, 2], [item.width + widthLeft, nextItem.width]);
|
|
189
151
|
|
|
190
152
|
widthLeft += item.width;
|
|
191
153
|
item.width = newWidth;
|
|
@@ -195,71 +157,69 @@ export const Carousel = ({
|
|
|
195
157
|
const percentMax = visibleItemValues.length / 2;
|
|
196
158
|
const percent = normalize(
|
|
197
159
|
Math.abs(itemsVisibleByIndex[0].relativeIndex),
|
|
198
|
-
[itemsVisibleByIndex[0].index
|
|
160
|
+
[itemsVisibleByIndex[0].index === 0 ? 0 : percentMax - 1, percentMax],
|
|
199
161
|
[0, 1],
|
|
200
162
|
);
|
|
201
163
|
|
|
202
|
-
const translate =
|
|
203
|
-
|
|
164
|
+
const translate = normalize(percent, [0, 1], [0, 1]) * -(outputRange[0] + gap);
|
|
165
|
+
|
|
166
|
+
// ===================================
|
|
167
|
+
// DOM INJECTION OPTIMIZATION
|
|
168
|
+
// ===================================
|
|
169
|
+
|
|
170
|
+
// Apply width to each visible item using DOM instead of setItemWidths React state
|
|
171
|
+
// First, fallback everything to outputRange[0] (or hide them)
|
|
172
|
+
itemRefs.forEach((refItem, i) => {
|
|
173
|
+
if (refItem.current) {
|
|
174
|
+
const match = visibleItemValues.find(v => v.index === i);
|
|
175
|
+
if (match) {
|
|
176
|
+
refItem.current.style.setProperty('--carousel-item-width', `${match.width}px`);
|
|
177
|
+
refItem.current.style.display = 'block';
|
|
178
|
+
} else {
|
|
179
|
+
refItem.current.style.setProperty('--carousel-item-width', `${outputRange[0]}px`);
|
|
180
|
+
refItem.current.style.display = 'none';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
});
|
|
204
184
|
|
|
205
|
-
|
|
185
|
+
// Apply track translate directly via DOM
|
|
186
|
+
trackRef.current.style.transform = `translateX(${translate}px)`;
|
|
187
|
+
|
|
188
|
+
if (localSelected !== selectedItem) {
|
|
189
|
+
setSelectedItem(localSelected);
|
|
190
|
+
}
|
|
191
|
+
}, [items.length, outputRange, gap, selectedItem]);
|
|
192
|
+
|
|
193
|
+
useLayoutEffect(() => {
|
|
194
|
+
updateLayoutFromCalculations();
|
|
195
|
+
}, [updateLayoutFromCalculations, items.length]);
|
|
206
196
|
|
|
207
|
-
return Object.fromEntries(
|
|
208
|
-
visibleItemValues.map((item) => [item.index, item.width]),
|
|
209
|
-
);
|
|
210
|
-
};
|
|
211
|
-
const itemRefs = useRef<React.RefObject<HTMLDivElement | null>[]>([]).current;
|
|
212
|
-
const [selectedItem, setSelectedItem] = useState(0);
|
|
213
197
|
|
|
214
198
|
useEffect(() => {
|
|
215
199
|
if (onChange) onChange(selectedItem);
|
|
216
|
-
}, [selectedItem]);
|
|
200
|
+
}, [selectedItem, onChange]);
|
|
217
201
|
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
if (
|
|
221
|
-
typeof index === 'number' &&
|
|
222
|
-
items.length > 0 &&
|
|
223
|
-
index !== selectedItem
|
|
224
|
-
) {
|
|
225
|
-
centerOnIndex(index);
|
|
226
|
-
}
|
|
227
|
-
}, [index, items.length]);
|
|
202
|
+
// accessibility and interaction states
|
|
203
|
+
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
228
204
|
|
|
229
|
-
// keep focused index aligned with selected when selection changes through scroll
|
|
230
205
|
useEffect(() => {
|
|
231
206
|
setFocusedIndex(selectedItem);
|
|
232
207
|
}, [selectedItem]);
|
|
233
208
|
|
|
234
|
-
if (itemRefs.length !== items.length) {
|
|
235
|
-
items.forEach((_, i) => {
|
|
236
|
-
if (!itemRefs[i]) {
|
|
237
|
-
itemRefs[i] = React.createRef<HTMLDivElement>(); // Crée une nouvelle ref si manquante
|
|
238
|
-
}
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// accessibility and interaction states
|
|
243
|
-
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
244
209
|
|
|
245
|
-
const centerOnIndex = (
|
|
246
|
-
// Guard: need valid refs and at least one item
|
|
210
|
+
const centerOnIndex = (idx: number, opts: { animate?: boolean } = {}) => {
|
|
247
211
|
if (!items.length) return 0;
|
|
248
|
-
const itemRef = itemRefs[
|
|
212
|
+
const itemRef = itemRefs[idx];
|
|
249
213
|
if (!itemRef || !itemRef.current || !trackRef.current) return 0;
|
|
250
214
|
|
|
251
|
-
// Compute progress (0..1) for the target item center within the track
|
|
252
215
|
const itemScrollXCenter = normalize(
|
|
253
|
-
|
|
216
|
+
idx / Math.max(1, items.length - 1),
|
|
254
217
|
[0, 1],
|
|
255
218
|
[0, 1],
|
|
256
219
|
);
|
|
257
220
|
|
|
258
|
-
|
|
259
|
-
setFocusedIndex(index);
|
|
221
|
+
setFocusedIndex(idx);
|
|
260
222
|
|
|
261
|
-
// Ask CustomScroll to move to the computed progress. This will trigger onScroll,
|
|
262
|
-
// which in turn drives the smoothed animation via handleScroll().
|
|
263
223
|
const track = trackRef.current as HTMLElement;
|
|
264
224
|
track.dispatchEvent(
|
|
265
225
|
new CustomEvent('udx:customScroll:set', {
|
|
@@ -275,32 +235,11 @@ export const Carousel = ({
|
|
|
275
235
|
return itemScrollXCenter;
|
|
276
236
|
};
|
|
277
237
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
existingOnClick?.(e);
|
|
284
|
-
// centerOnIndex(index);
|
|
285
|
-
};
|
|
286
|
-
const handleFocus = () => setFocusedIndex(index);
|
|
287
|
-
|
|
288
|
-
return React.cloneElement(
|
|
289
|
-
child as React.ReactElement<ReactProps<CarouselItemInterface>>,
|
|
290
|
-
{
|
|
291
|
-
width: itemsWidth[index],
|
|
292
|
-
outputRange,
|
|
293
|
-
ref: itemRefs[index],
|
|
294
|
-
key: index,
|
|
295
|
-
index,
|
|
296
|
-
role: 'option',
|
|
297
|
-
'aria-selected': selectedItem === index,
|
|
298
|
-
tabIndex: selectedItem === index ? 0 : -1,
|
|
299
|
-
onClick: handleClick,
|
|
300
|
-
onFocus: handleFocus,
|
|
301
|
-
} as any,
|
|
302
|
-
);
|
|
303
|
-
});
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
if (typeof index === 'number' && items.length > 0 && index !== selectedItem) {
|
|
240
|
+
centerOnIndex(index);
|
|
241
|
+
}
|
|
242
|
+
}, [index, items.length]);
|
|
304
243
|
|
|
305
244
|
const handleScroll = (args: {
|
|
306
245
|
scrollProgress: number;
|
|
@@ -308,15 +247,14 @@ export const Carousel = ({
|
|
|
308
247
|
scrollVisible: number;
|
|
309
248
|
scroll: number;
|
|
310
249
|
}) => {
|
|
250
|
+
getScrollState.current = args;
|
|
251
|
+
|
|
311
252
|
if (args.scrollTotal > 0) {
|
|
312
|
-
// Smooth and inertial transition of scrollProgress using framer-motion animate()
|
|
313
|
-
// Stop any previous animation to avoid stacking
|
|
314
253
|
scrollAnimationRef.current?.stop();
|
|
315
254
|
const from = smoothedProgressRef.current ?? 0;
|
|
316
255
|
const to = args.scrollProgress ?? 0;
|
|
317
256
|
|
|
318
257
|
scrollAnimationRef.current = animate(from, to, {
|
|
319
|
-
// Spring tuning to add a bit of inertia and smoothness
|
|
320
258
|
type: 'spring',
|
|
321
259
|
stiffness: 260,
|
|
322
260
|
damping: 32,
|
|
@@ -324,46 +262,33 @@ export const Carousel = ({
|
|
|
324
262
|
restDelta: 0.0005,
|
|
325
263
|
onUpdate: (v) => {
|
|
326
264
|
smoothedProgressRef.current = v;
|
|
327
|
-
|
|
265
|
+
requestAnimationFrame(() => {
|
|
266
|
+
updateLayoutFromCalculations(); // Apply DOM updates synchronously to animation
|
|
267
|
+
});
|
|
328
268
|
},
|
|
329
269
|
});
|
|
330
270
|
}
|
|
331
271
|
};
|
|
332
272
|
|
|
333
|
-
useEffect(() => {
|
|
334
|
-
const updatedPercentages = calculatePercentages();
|
|
335
|
-
setItemsWidth(updatedPercentages);
|
|
336
|
-
}, [scroll]);
|
|
337
|
-
|
|
338
273
|
// Keep latest onMetricsChange in a ref to avoid effect dependency loops
|
|
339
274
|
const onMetricsChangeRef = useRef(onMetricsChange);
|
|
340
275
|
useEffect(() => {
|
|
341
276
|
onMetricsChangeRef.current = onMetricsChange;
|
|
342
277
|
}, [onMetricsChange]);
|
|
343
278
|
|
|
344
|
-
// Cache last emitted metrics to prevent redundant calls
|
|
345
279
|
const lastMetricsRef = useRef<any>(null);
|
|
346
280
|
|
|
347
|
-
// Compute and emit live metrics for external control
|
|
348
281
|
useEffect(() => {
|
|
349
282
|
const cb = onMetricsChangeRef.current;
|
|
350
|
-
if (!cb) return;
|
|
351
|
-
if (!ref?.current) return;
|
|
283
|
+
if (!cb || !ref?.current || items.length <= 0) return;
|
|
352
284
|
const total = items.length;
|
|
353
|
-
|
|
354
|
-
const viewportWidth = (ref.current as any)?.clientWidth ?? 0;
|
|
285
|
+
const viewportWidth = (ref.current as any).clientWidth || 0;
|
|
355
286
|
const itemMaxWidth = outputRange[1];
|
|
356
|
-
const sProgress =
|
|
357
|
-
smoothedProgressRef.current ?? scroll?.scrollProgress ?? 0;
|
|
287
|
+
const sProgress = smoothedProgressRef.current;
|
|
358
288
|
const visibleApprox = (viewportWidth + gap) / (itemMaxWidth + gap);
|
|
359
289
|
const visibleFull = Math.max(1, Math.floor(visibleApprox));
|
|
360
290
|
const stepHalf = Math.max(1, Math.round(visibleFull * (2 / 3)));
|
|
361
|
-
const selectedIndexSafe = Math.min(
|
|
362
|
-
Math.max(0, selectedItem),
|
|
363
|
-
Math.max(0, total - 1),
|
|
364
|
-
);
|
|
365
|
-
const canPrev = selectedIndexSafe > 0;
|
|
366
|
-
const canNext = selectedIndexSafe < total - 1;
|
|
291
|
+
const selectedIndexSafe = Math.min(Math.max(0, selectedItem), Math.max(0, total - 1));
|
|
367
292
|
|
|
368
293
|
const metrics = {
|
|
369
294
|
total,
|
|
@@ -371,20 +296,19 @@ export const Carousel = ({
|
|
|
371
296
|
visibleApprox,
|
|
372
297
|
visibleFull,
|
|
373
298
|
stepHalf,
|
|
374
|
-
canPrev,
|
|
375
|
-
canNext,
|
|
299
|
+
canPrev: selectedIndexSafe > 0,
|
|
300
|
+
canNext: selectedIndexSafe < total - 1,
|
|
376
301
|
scrollProgress: sProgress,
|
|
377
302
|
viewportWidth,
|
|
378
303
|
itemMaxWidth,
|
|
379
304
|
gap,
|
|
380
|
-
}
|
|
305
|
+
};
|
|
381
306
|
|
|
382
|
-
// Shallow compare with last metrics to avoid spamming parent and loops
|
|
383
307
|
const last = lastMetricsRef.current;
|
|
384
308
|
let changed = !last;
|
|
385
309
|
if (!changed) {
|
|
386
310
|
for (const k in metrics) {
|
|
387
|
-
if (metrics[k] !== last[k]) {
|
|
311
|
+
if ((metrics as any)[k] !== last[k]) {
|
|
388
312
|
changed = true;
|
|
389
313
|
break;
|
|
390
314
|
}
|
|
@@ -395,34 +319,8 @@ export const Carousel = ({
|
|
|
395
319
|
lastMetricsRef.current = metrics;
|
|
396
320
|
cb(metrics);
|
|
397
321
|
}
|
|
398
|
-
}, [ref, items.length, selectedItem,
|
|
399
|
-
|
|
400
|
-
// // Recalculate on scrollMV changes (e.g., programmatic animations)
|
|
401
|
-
// useEffect(() => {
|
|
402
|
-
// const unsubscribe = scrollMV.on('change', (p) => {
|
|
403
|
-
// // Keep CustomScroll container in sync by dispatching a bubbling control event
|
|
404
|
-
// const track = trackRef.current as HTMLElement | null;
|
|
405
|
-
// if (track) {
|
|
406
|
-
// track.dispatchEvent(
|
|
407
|
-
// new CustomEvent('udx:customScroll:set', {
|
|
408
|
-
// bubbles: true,
|
|
409
|
-
// detail: { progress: p, orientation: 'horizontal' },
|
|
410
|
-
// }),
|
|
411
|
-
// );
|
|
412
|
-
// }
|
|
413
|
-
// const updated = calculatePercentages();
|
|
414
|
-
// if (updated.length) setItemsWidth(updated);
|
|
415
|
-
// });
|
|
416
|
-
// return () => unsubscribe();
|
|
417
|
-
// }, [scrollMV, trackRef]);
|
|
418
|
-
|
|
419
|
-
// Initial compute on mount and when items count changes
|
|
420
|
-
// useLayoutEffect(() => {
|
|
421
|
-
// const updated = calculatePercentages();
|
|
422
|
-
// if (updated.length) setItemsWidth(updated);
|
|
423
|
-
// }, [items.length]);
|
|
424
|
-
|
|
425
|
-
// Cleanup any pending animation on unmount
|
|
322
|
+
}, [ref, items.length, selectedItem, gap, outputRange]);
|
|
323
|
+
|
|
426
324
|
useEffect(() => {
|
|
427
325
|
return () => {
|
|
428
326
|
scrollAnimationRef.current?.stop();
|
|
@@ -432,40 +330,14 @@ export const Carousel = ({
|
|
|
432
330
|
const [scrollSize, setScrollSize] = useState(0);
|
|
433
331
|
useLayoutEffect(() => {
|
|
434
332
|
let maxWidth = outputRange[1];
|
|
435
|
-
|
|
436
|
-
|
|
333
|
+
const scrollState = getScrollState.current;
|
|
334
|
+
if (scrollState && maxWidth > scrollState.scrollVisible && scrollState.scrollVisible > 0) {
|
|
335
|
+
maxWidth = scrollState.scrollVisible;
|
|
437
336
|
}
|
|
438
|
-
const result = ((maxWidth + gap) *
|
|
439
|
-
setScrollSize(result);
|
|
440
|
-
}, [ref,
|
|
441
|
-
|
|
442
|
-
// Recompute sizes on container/track resize
|
|
443
|
-
// useEffect(() => {
|
|
444
|
-
// const root = ref.current as unknown as HTMLElement | null;
|
|
445
|
-
// const track = trackRef.current as unknown as HTMLElement | null;
|
|
446
|
-
// if (!root || !track) return;
|
|
447
|
-
// const ro = new ResizeObserver(() => {
|
|
448
|
-
// const updated = calculatePercentages();
|
|
449
|
-
// if (updated.length) setItemsWidth(updated);
|
|
450
|
-
// let maxWidth = outputRange[1];
|
|
451
|
-
// const visible = scroll?.scrollVisible ?? root.clientWidth;
|
|
452
|
-
// if (maxWidth > visible) maxWidth = visible;
|
|
453
|
-
// const result =
|
|
454
|
-
// ((maxWidth + gap) * renderItems.length) / scrollSensitivity;
|
|
455
|
-
// setScrollSize(result);
|
|
456
|
-
// });
|
|
457
|
-
// ro.observe(root);
|
|
458
|
-
// ro.observe(track);
|
|
459
|
-
// return () => ro.disconnect();
|
|
460
|
-
// }, [
|
|
461
|
-
// ref,
|
|
462
|
-
// trackRef,
|
|
463
|
-
// renderItems.length,
|
|
464
|
-
// gap,
|
|
465
|
-
// outputRange,
|
|
466
|
-
// scrollSensitivity,
|
|
467
|
-
// scroll,
|
|
468
|
-
// ]);
|
|
337
|
+
const result = ((maxWidth + gap) * items.length) / scrollSensitivity;
|
|
338
|
+
setScrollSize(result || 400); // Fail-safe
|
|
339
|
+
}, [ref, items.length, gap, outputRange, scrollSensitivity]);
|
|
340
|
+
|
|
469
341
|
|
|
470
342
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
471
343
|
if (!items.length) return;
|
|
@@ -488,34 +360,52 @@ export const Carousel = ({
|
|
|
488
360
|
centerOnIndex(items.length - 1);
|
|
489
361
|
break;
|
|
490
362
|
case 'Enter':
|
|
491
|
-
case ' ':
|
|
363
|
+
case ' ':
|
|
492
364
|
e.preventDefault();
|
|
493
365
|
centerOnIndex(idx);
|
|
494
366
|
break;
|
|
495
367
|
}
|
|
496
368
|
};
|
|
497
369
|
|
|
498
|
-
// External control via CustomEvent on root element
|
|
499
370
|
useEffect(() => {
|
|
500
371
|
const root = ref.current as any;
|
|
501
372
|
if (!root) return;
|
|
502
373
|
const handler = (ev: Event) => {
|
|
503
|
-
const detail = (ev as CustomEvent).detail
|
|
504
|
-
| { index?: number }
|
|
505
|
-
| undefined;
|
|
374
|
+
const detail = (ev as CustomEvent).detail;
|
|
506
375
|
if (detail && typeof detail.index === 'number') {
|
|
507
376
|
centerOnIndex(detail.index);
|
|
508
377
|
}
|
|
509
378
|
};
|
|
510
|
-
root.addEventListener('udx:carousel:centerIndex', handler
|
|
379
|
+
root.addEventListener('udx:carousel:centerIndex', handler);
|
|
511
380
|
return () => {
|
|
512
|
-
root.removeEventListener(
|
|
513
|
-
'udx:carousel:centerIndex',
|
|
514
|
-
handler as EventListener,
|
|
515
|
-
);
|
|
381
|
+
root.removeEventListener('udx:carousel:centerIndex', handler);
|
|
516
382
|
};
|
|
517
383
|
}, [ref, items.length]);
|
|
518
384
|
|
|
385
|
+
const renderItems = items.map((child, idx) => {
|
|
386
|
+
const existingOnClick = (child as any).props?.onClick;
|
|
387
|
+
const handleClick = (e: any) => {
|
|
388
|
+
existingOnClick?.(e);
|
|
389
|
+
// centerOnIndex(idx);
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
return React.cloneElement(
|
|
393
|
+
child as React.ReactElement<ReactProps<CarouselItemInterface>>,
|
|
394
|
+
{
|
|
395
|
+
outputRange,
|
|
396
|
+
ref: itemRefs[idx],
|
|
397
|
+
key: idx,
|
|
398
|
+
index: idx,
|
|
399
|
+
role: 'option',
|
|
400
|
+
'aria-selected': selectedItem === idx,
|
|
401
|
+
tabIndex: selectedItem === idx ? 0 : -1,
|
|
402
|
+
onClick: handleClick,
|
|
403
|
+
onFocus: () => setFocusedIndex(idx),
|
|
404
|
+
// NOTE: We REMOVED the 'width' prop from here!
|
|
405
|
+
} as any,
|
|
406
|
+
);
|
|
407
|
+
});
|
|
408
|
+
|
|
519
409
|
return (
|
|
520
410
|
<div
|
|
521
411
|
className={styles.carousel}
|
|
@@ -536,8 +426,7 @@ export const Carousel = ({
|
|
|
536
426
|
ref={trackRef}
|
|
537
427
|
style={{
|
|
538
428
|
gap: `${gap}px`,
|
|
539
|
-
|
|
540
|
-
willChange: 'translate',
|
|
429
|
+
willChange: 'transform',
|
|
541
430
|
}}
|
|
542
431
|
>
|
|
543
432
|
{renderItems}
|
|
@@ -41,23 +41,16 @@ export const CarouselItem = ({
|
|
|
41
41
|
const styles = useCarouselItemStyle({
|
|
42
42
|
className,
|
|
43
43
|
index,
|
|
44
|
-
width,
|
|
45
44
|
children,
|
|
46
|
-
outputRange,
|
|
47
45
|
});
|
|
48
46
|
|
|
49
47
|
return (
|
|
50
48
|
<div
|
|
51
49
|
ref={ref}
|
|
52
50
|
style={{
|
|
53
|
-
width: width
|
|
54
|
-
maxWidth: outputRange[1] + 'px',
|
|
55
|
-
minWidth: outputRange[0] + 'px',
|
|
56
|
-
willChange: 'width',
|
|
57
|
-
}}
|
|
58
|
-
transition={{
|
|
59
|
-
duration: 0.5,
|
|
60
|
-
ease: 'linear',
|
|
51
|
+
width: 'var(--carousel-item-width, 100%)',
|
|
52
|
+
maxWidth: outputRange ? outputRange[1] + 'px' : undefined,
|
|
53
|
+
minWidth: outputRange ? outputRange[0] + 'px' : undefined,
|
|
61
54
|
}}
|
|
62
55
|
className={styles.carouselItem}
|
|
63
56
|
{...restProps}
|