etudes 6.2.1 → 6.2.2
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/components/Carousel.js +84 -87
- package/hooks/useTimeout.d.ts +12 -10
- package/hooks/useTimeout.js +23 -15
- package/package.json +1 -1
package/components/Carousel.js
CHANGED
|
@@ -6,42 +6,15 @@ import { useTimeout } from '../hooks/useTimeout.js';
|
|
|
6
6
|
import { Each } from '../operators/Each.js';
|
|
7
7
|
import { asStyleDict, styles } from '../utils/index.js';
|
|
8
8
|
export const Carousel = forwardRef(({ autoAdvanceInterval = 0, index = 0, isDragEnabled = true, items = [], orientation = 'horizontal', tracksItemExposure = false, onAutoAdvancePause, onAutoAdvanceResume, onIndexChange, ItemComponent, ...props }, ref) => {
|
|
9
|
-
const
|
|
10
|
-
const viewportElement = viewportRef.current;
|
|
11
|
-
if (!viewportElement)
|
|
12
|
-
return undefined;
|
|
13
|
-
const exposures = [];
|
|
14
|
-
for (let i = 0; i < viewportElement.children.length; i++) {
|
|
15
|
-
exposures.push(getItemExposureAt(i));
|
|
16
|
-
}
|
|
17
|
-
return exposures;
|
|
18
|
-
};
|
|
19
|
-
const getItemExposureAt = (idx) => {
|
|
20
|
-
const viewportElement = viewportRef.current;
|
|
21
|
-
const child = viewportElement?.children[idx];
|
|
22
|
-
if (!child)
|
|
23
|
-
return 0;
|
|
24
|
-
const intersection = Rect.intersecting(child, viewportElement);
|
|
25
|
-
if (!intersection)
|
|
26
|
-
return 0;
|
|
27
|
-
switch (orientation) {
|
|
28
|
-
case 'horizontal':
|
|
29
|
-
return Math.max(0, Math.min(1, Math.round((intersection.width / viewportElement.clientWidth + Number.EPSILON) * 1000) / 1000));
|
|
30
|
-
case 'vertical':
|
|
31
|
-
return Math.max(0, Math.min(1, Math.round((intersection.height / viewportElement.clientHeight + Number.EPSILON) * 1000) / 1000));
|
|
32
|
-
default:
|
|
33
|
-
throw new Error(`Unsupported orientation '${orientation}'`);
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
const handleIndexChange = (newValue) => {
|
|
37
|
-
onIndexChange?.(newValue);
|
|
38
|
-
};
|
|
9
|
+
const handleIndexChange = (newValue) => onIndexChange?.(newValue);
|
|
39
10
|
const handlePointerDown = (event) => {
|
|
40
11
|
pointerDownPositionRef.current = Point.make(event.clientX, event.clientY);
|
|
41
12
|
setIsPointerDown(true);
|
|
42
13
|
};
|
|
43
14
|
const handlePointerUp = (event) => {
|
|
44
15
|
pointerUpPositionRef.current = Point.make(event.clientX, event.clientY);
|
|
16
|
+
if (!isPointerDown)
|
|
17
|
+
return;
|
|
45
18
|
setIsPointerDown(false);
|
|
46
19
|
};
|
|
47
20
|
const handleClick = (event) => {
|
|
@@ -57,66 +30,50 @@ export const Carousel = forwardRef(({ autoAdvanceInterval = 0, index = 0, isDrag
|
|
|
57
30
|
pointerDownPositionRef.current = undefined;
|
|
58
31
|
pointerUpPositionRef.current = undefined;
|
|
59
32
|
};
|
|
60
|
-
const
|
|
61
|
-
const viewportElement = viewportRef.current;
|
|
62
|
-
if (!viewportElement)
|
|
63
|
-
return;
|
|
64
|
-
const top = orientation === 'horizontal' ? 0 : viewportElement.clientHeight * index;
|
|
65
|
-
const left = orientation === 'horizontal' ? viewportElement.clientWidth * index : 0;
|
|
66
|
-
viewportElement.scrollTo({ top, left, behavior: 'smooth' });
|
|
67
|
-
clearTimeout(autoScrollTimeoutRef.current);
|
|
68
|
-
autoScrollTimeoutRef.current = setTimeout(() => {
|
|
69
|
-
clearTimeout(autoScrollTimeoutRef.current);
|
|
70
|
-
autoScrollTimeoutRef.current = undefined;
|
|
71
|
-
}, autoScrollTimeoutMs);
|
|
72
|
-
};
|
|
33
|
+
const normalizeScrollPosition = () => scrollToIndex(viewportRef, index, orientation);
|
|
73
34
|
const prevIndexRef = useRef();
|
|
74
35
|
const viewportRef = useRef(null);
|
|
75
36
|
const pointerDownPositionRef = useRef();
|
|
76
37
|
const pointerUpPositionRef = useRef();
|
|
77
|
-
const [exposures, setExposures] = useState(getItemExposures());
|
|
78
|
-
const autoScrollTimeoutRef = useRef();
|
|
79
|
-
const autoScrollTimeoutMs = 1000;
|
|
38
|
+
const [exposures, setExposures] = useState(getItemExposures(viewportRef, orientation));
|
|
80
39
|
const [isPointerDown, setIsPointerDown] = useState(false);
|
|
40
|
+
const fixedStyles = getFixedStyles({ scrollSnapEnabled: !isPointerDown, orientation });
|
|
41
|
+
const shouldAutoAdvance = autoAdvanceInterval > 0;
|
|
81
42
|
useEffect(() => {
|
|
82
|
-
const
|
|
83
|
-
if (!
|
|
43
|
+
const viewport = viewportRef.current;
|
|
44
|
+
if (!viewport)
|
|
84
45
|
return;
|
|
85
|
-
const
|
|
46
|
+
const isInitialRender = prevIndexRef.current === undefined;
|
|
47
|
+
const isIndexModifiedFromManualScrolling = prevIndexRef.current === index;
|
|
48
|
+
const scrollHandler = (e) => {
|
|
86
49
|
if (tracksItemExposure) {
|
|
87
|
-
setExposures(getItemExposures());
|
|
50
|
+
setExposures(getItemExposures(viewportRef, orientation));
|
|
88
51
|
}
|
|
89
|
-
if (autoScrollTimeoutRef.current !== undefined)
|
|
90
|
-
return;
|
|
91
52
|
const newIndex = orientation === 'horizontal'
|
|
92
|
-
? Math.round(
|
|
93
|
-
: Math.round(
|
|
53
|
+
? Math.round(viewport.scrollLeft / viewport.clientWidth)
|
|
54
|
+
: Math.round(viewport.scrollTop / viewport.clientHeight);
|
|
94
55
|
const clampedIndex = Math.max(0, Math.min(items.length - 1, newIndex));
|
|
95
56
|
if (clampedIndex === index)
|
|
96
57
|
return;
|
|
97
|
-
// Set previous index
|
|
98
|
-
//
|
|
58
|
+
// Set previous index before emitting index change event to differentiate
|
|
59
|
+
// between index change from scroll vs from prop.
|
|
99
60
|
prevIndexRef.current = clampedIndex;
|
|
100
61
|
handleIndexChange(clampedIndex);
|
|
101
62
|
};
|
|
102
|
-
|
|
63
|
+
viewport.addEventListener('scroll', scrollHandler);
|
|
64
|
+
if (!isIndexModifiedFromManualScrolling) {
|
|
65
|
+
prevIndexRef.current = index;
|
|
66
|
+
if (!isInitialRender) {
|
|
67
|
+
handleIndexChange(index);
|
|
68
|
+
normalizeScrollPosition();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
103
71
|
return () => {
|
|
104
|
-
|
|
72
|
+
viewport.removeEventListener('scroll', scrollHandler);
|
|
105
73
|
};
|
|
106
|
-
}, [index, orientation]);
|
|
74
|
+
}, [index, orientation, tracksItemExposure]);
|
|
107
75
|
useEffect(() => {
|
|
108
|
-
|
|
109
|
-
const isIndexModifiedFromManualScrolling = prevIndexRef.current === index;
|
|
110
|
-
if (isIndexModifiedFromManualScrolling)
|
|
111
|
-
return;
|
|
112
|
-
prevIndexRef.current = index;
|
|
113
|
-
if (isInitialRender)
|
|
114
|
-
return;
|
|
115
|
-
handleIndexChange(index);
|
|
116
|
-
autoScrollToCurrentIndex();
|
|
117
|
-
}, [index, orientation]);
|
|
118
|
-
useEffect(() => {
|
|
119
|
-
if (autoAdvanceInterval <= 0)
|
|
76
|
+
if (!shouldAutoAdvance)
|
|
120
77
|
return;
|
|
121
78
|
if (isPointerDown) {
|
|
122
79
|
onAutoAdvancePause?.();
|
|
@@ -124,64 +81,104 @@ export const Carousel = forwardRef(({ autoAdvanceInterval = 0, index = 0, isDrag
|
|
|
124
81
|
else {
|
|
125
82
|
onAutoAdvanceResume?.();
|
|
126
83
|
}
|
|
127
|
-
}, [isPointerDown]);
|
|
84
|
+
}, [isPointerDown, shouldAutoAdvance]);
|
|
128
85
|
useDragEffect(viewportRef, {
|
|
129
86
|
isEnabled: isDragEnabled && items.length > 1,
|
|
130
|
-
onDragMove: (
|
|
87
|
+
onDragMove: ({ x, y }) => {
|
|
131
88
|
switch (orientation) {
|
|
132
89
|
case 'horizontal':
|
|
133
90
|
requestAnimationFrame(() => {
|
|
134
91
|
if (!viewportRef.current)
|
|
135
92
|
return;
|
|
136
|
-
viewportRef.current.scrollLeft +=
|
|
93
|
+
viewportRef.current.scrollLeft += x * 1.5;
|
|
137
94
|
});
|
|
138
95
|
break;
|
|
139
96
|
case 'vertical':
|
|
140
97
|
requestAnimationFrame(() => {
|
|
141
98
|
if (!viewportRef.current)
|
|
142
99
|
return;
|
|
143
|
-
viewportRef.current.scrollTop +=
|
|
100
|
+
viewportRef.current.scrollTop += y * 1.5;
|
|
144
101
|
});
|
|
145
102
|
break;
|
|
146
103
|
default:
|
|
147
104
|
throw Error(`Unsupported orientation '${orientation}'`);
|
|
148
105
|
}
|
|
149
106
|
},
|
|
150
|
-
}, [
|
|
151
|
-
useTimeout((
|
|
152
|
-
|
|
153
|
-
|
|
107
|
+
}, [isDragEnabled, items.length, orientation]);
|
|
108
|
+
useTimeout((isPointerDown || !shouldAutoAdvance) ? -1 : autoAdvanceInterval, {
|
|
109
|
+
onTimeout: () => {
|
|
110
|
+
const nextIndex = (index + items.length + 1) % items.length;
|
|
111
|
+
handleIndexChange(nextIndex);
|
|
112
|
+
},
|
|
113
|
+
}, [autoAdvanceInterval, isPointerDown, index, items.length, shouldAutoAdvance, handleIndexChange]);
|
|
114
|
+
return (_jsx("div", { ...props, ref: ref, role: 'region', onClick: event => handleClick(event), onPointerCancel: event => handlePointerUp(event), onPointerDown: event => handlePointerDown(event), onPointerLeave: event => handlePointerUp(event), onPointerUp: event => handlePointerUp(event), children: _jsx("div", { ref: viewportRef, style: styles(fixedStyles.viewport), children: _jsx(Each, { in: items, children: ({ style: itemStyle, ...itemProps }, idx) => (_jsx("div", { style: styles(fixedStyles.itemContainer), children: _jsx(ItemComponent, { "aria-hidden": idx !== index, exposure: tracksItemExposure ? exposures?.[idx] : undefined, style: styles(itemStyle, fixedStyles.item), ...itemProps }) })) }) }) }));
|
|
154
115
|
});
|
|
155
|
-
function
|
|
116
|
+
function scrollToIndex(ref, index, orientation) {
|
|
117
|
+
const viewport = ref.current;
|
|
118
|
+
if (!viewport)
|
|
119
|
+
return;
|
|
120
|
+
const top = orientation === 'horizontal' ? 0 : viewport.clientHeight * index;
|
|
121
|
+
const left = orientation === 'horizontal' ? viewport.clientWidth * index : 0;
|
|
122
|
+
if (viewport.scrollTop === top && viewport.scrollLeft === left)
|
|
123
|
+
return;
|
|
124
|
+
viewport.scrollTo({ top, left, behavior: 'smooth' });
|
|
125
|
+
}
|
|
126
|
+
function getItemExposures(ref, orientation) {
|
|
127
|
+
const viewport = ref.current;
|
|
128
|
+
if (!viewport)
|
|
129
|
+
return undefined;
|
|
130
|
+
const exposures = [];
|
|
131
|
+
for (let i = 0; i < viewport.children.length; i++) {
|
|
132
|
+
exposures.push(getItemExposureAt(i, ref, orientation));
|
|
133
|
+
}
|
|
134
|
+
return exposures;
|
|
135
|
+
}
|
|
136
|
+
function getItemExposureAt(idx, ref, orientation) {
|
|
137
|
+
const viewport = ref.current;
|
|
138
|
+
const child = viewport?.children[idx];
|
|
139
|
+
if (!child)
|
|
140
|
+
return 0;
|
|
141
|
+
const intersection = Rect.intersecting(child, viewport);
|
|
142
|
+
if (!intersection)
|
|
143
|
+
return 0;
|
|
144
|
+
switch (orientation) {
|
|
145
|
+
case 'horizontal':
|
|
146
|
+
return Math.max(0, Math.min(1, Math.round((intersection.width / viewport.clientWidth + Number.EPSILON) * 1000) / 1000));
|
|
147
|
+
case 'vertical':
|
|
148
|
+
return Math.max(0, Math.min(1, Math.round((intersection.height / viewport.clientHeight + Number.EPSILON) * 1000) / 1000));
|
|
149
|
+
default:
|
|
150
|
+
throw new Error(`Unsupported orientation '${orientation}'`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function getFixedStyles({ scrollSnapEnabled = false, orientation = 'horizontal' }) {
|
|
156
154
|
return asStyleDict({
|
|
157
155
|
viewport: {
|
|
158
156
|
alignItems: 'center',
|
|
159
157
|
display: 'flex',
|
|
160
158
|
height: '100%',
|
|
161
|
-
userSelect:
|
|
159
|
+
userSelect: scrollSnapEnabled ? 'auto' : 'none',
|
|
162
160
|
justifyContent: 'flex-start',
|
|
163
|
-
scrollBehavior:
|
|
164
|
-
scrollSnapStop:
|
|
161
|
+
scrollBehavior: scrollSnapEnabled ? 'smooth' : 'auto',
|
|
162
|
+
scrollSnapStop: scrollSnapEnabled ? 'always' : 'unset',
|
|
165
163
|
WebkitOverflowScrolling: 'touch',
|
|
166
164
|
width: '100%',
|
|
167
165
|
...orientation === 'horizontal' ? {
|
|
168
166
|
flexDirection: 'row',
|
|
169
167
|
overflowX: 'scroll',
|
|
170
168
|
overflowY: 'hidden',
|
|
171
|
-
scrollSnapType:
|
|
169
|
+
scrollSnapType: scrollSnapEnabled ? 'x mandatory' : 'none',
|
|
172
170
|
} : {
|
|
173
171
|
flexDirection: 'column',
|
|
174
172
|
overflowX: 'hidden',
|
|
175
173
|
overflowY: 'scroll',
|
|
176
|
-
scrollSnapType:
|
|
174
|
+
scrollSnapType: scrollSnapEnabled ? 'y mandatory' : 'none',
|
|
177
175
|
},
|
|
178
176
|
},
|
|
179
177
|
itemContainer: {
|
|
180
178
|
height: '100%',
|
|
181
179
|
overflow: 'hidden',
|
|
182
|
-
scrollSnapAlign: '
|
|
180
|
+
scrollSnapAlign: 'center',
|
|
183
181
|
width: '100%',
|
|
184
|
-
scrollBehavior: 'smooth',
|
|
185
182
|
flex: '0 0 auto',
|
|
186
183
|
},
|
|
187
184
|
item: {
|
package/hooks/useTimeout.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { type DependencyList } from 'react';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { type DependencyList, type RefObject } from 'react';
|
|
2
|
+
type Options = {
|
|
3
|
+
autoStart?: boolean;
|
|
4
|
+
onTimeout?: () => void;
|
|
5
|
+
};
|
|
6
|
+
type ReturnValue = {
|
|
7
|
+
start: () => void;
|
|
8
|
+
stop: () => void;
|
|
9
|
+
ref: RefObject<NodeJS.Timeout | undefined>;
|
|
10
|
+
};
|
|
11
|
+
export declare function useTimeout(timeout?: number, { autoStart, onTimeout }?: Options, deps?: DependencyList): ReturnValue;
|
|
12
|
+
export {};
|
package/hooks/useTimeout.js
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
|
-
import { useEffect, useRef } from 'react';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
* @param handler The method to invoke.
|
|
6
|
-
* @param timeout Time (in milliseconds) for the timeout. If the value is
|
|
7
|
-
* `undefined` or less than 0, the timeout is disabled.
|
|
8
|
-
* @param deps Dependencies that trigger this effect.
|
|
9
|
-
*/
|
|
10
|
-
export function useTimeout(handler, timeout, deps = []) {
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
export function useTimeout(timeout = 0, { autoStart = true, onTimeout } = {}, deps = []) {
|
|
3
|
+
const timeoutRef = useRef();
|
|
11
4
|
const handlerRef = useRef();
|
|
5
|
+
const start = useCallback(() => {
|
|
6
|
+
stop();
|
|
7
|
+
if (timeout < 0)
|
|
8
|
+
return;
|
|
9
|
+
timeoutRef.current = setTimeout(() => {
|
|
10
|
+
stop();
|
|
11
|
+
handlerRef.current?.();
|
|
12
|
+
}, timeout);
|
|
13
|
+
}, [timeout]);
|
|
14
|
+
const stop = useCallback(() => {
|
|
15
|
+
clearTimeout(timeoutRef.current);
|
|
16
|
+
timeoutRef.current = undefined;
|
|
17
|
+
}, []);
|
|
12
18
|
useEffect(() => {
|
|
13
|
-
handlerRef.current =
|
|
14
|
-
}, [
|
|
19
|
+
handlerRef.current = onTimeout;
|
|
20
|
+
}, [onTimeout]);
|
|
15
21
|
useEffect(() => {
|
|
16
|
-
if (timeout
|
|
22
|
+
if (timeout < 0)
|
|
17
23
|
return;
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
if (autoStart)
|
|
25
|
+
start();
|
|
26
|
+
return () => stop();
|
|
20
27
|
}, [timeout, ...deps]);
|
|
28
|
+
return { start, stop, ref: timeoutRef };
|
|
21
29
|
}
|