zkit-ui 2.0.21 → 2.0.22

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/SliderCaptcha/index.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/SliderCaptcha/index.tsx"],"names":[],"mappings":"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"}
@@ -55,6 +55,8 @@ const CARD_HORIZONTAL_PADDING = (0, zkit_tools_1.wp)(24);
55
55
  const CANVAS_BORDER_WIDTH = (0, zkit_tools_1.wp)(1);
56
56
  const CAPTCHA_LAYER_Z_INDEX = 4000;
57
57
  const SLIDER_THUMB_SIZE = (0, zkit_tools_1.wp)(44);
58
+ const WEB_SLIDER_EDGE_GESTURE_INSET = react_native_1.Platform.OS === 'web' ? (0, zkit_tools_1.wp)(16) : 0;
59
+ const SLIDER_CANCEL_DURATION = 160;
58
60
  const CAPTCHA_ENTER_DURATION = 280;
59
61
  const CAPTCHA_EXIT_DURATION = 220;
60
62
  const CAPTCHA_CARD_OFFSET_Y = (0, zkit_tools_1.wp)(18);
@@ -89,6 +91,51 @@ function getSafePositiveNumber(value, fallback) {
89
91
  }
90
92
  return value;
91
93
  }
94
+ let webHorizontalOverscrollLockCount = 0;
95
+ let webHorizontalOverscrollSnapshots = [];
96
+ function acquireWebHorizontalOverscrollLock() {
97
+ if (react_native_1.Platform.OS !== 'web')
98
+ return undefined;
99
+ const webDocument = globalThis.document;
100
+ const targets = [webDocument === null || webDocument === void 0 ? void 0 : webDocument.documentElement, webDocument === null || webDocument === void 0 ? void 0 : webDocument.body].filter((target) => Boolean(target));
101
+ if (targets.length === 0)
102
+ return undefined;
103
+ if (webHorizontalOverscrollLockCount === 0) {
104
+ webHorizontalOverscrollSnapshots = targets.map((target) => ({
105
+ previousValue: target.style.overscrollBehaviorX,
106
+ target,
107
+ }));
108
+ }
109
+ webHorizontalOverscrollLockCount += 1;
110
+ targets.forEach((target) => {
111
+ target.style.overscrollBehaviorX = 'none';
112
+ });
113
+ let released = false;
114
+ return () => {
115
+ if (released)
116
+ return;
117
+ released = true;
118
+ webHorizontalOverscrollLockCount = Math.max(webHorizontalOverscrollLockCount - 1, 0);
119
+ if (webHorizontalOverscrollLockCount > 0)
120
+ return;
121
+ webHorizontalOverscrollSnapshots.forEach(({ previousValue, target }) => {
122
+ if (target.style.overscrollBehaviorX === 'none') {
123
+ target.style.overscrollBehaviorX = previousValue;
124
+ }
125
+ });
126
+ webHorizontalOverscrollSnapshots = [];
127
+ };
128
+ }
129
+ function resolveWebSliderInteractionStyle(canSlide) {
130
+ if (react_native_1.Platform.OS !== 'web')
131
+ return undefined;
132
+ return {
133
+ cursor: canSlide ? 'grab' : 'default',
134
+ overscrollBehaviorX: 'none',
135
+ touchAction: 'none',
136
+ userSelect: 'none',
137
+ };
138
+ }
92
139
  const styles = react_native_1.StyleSheet.create({
93
140
  modalRoot: {
94
141
  ...react_native_1.StyleSheet.absoluteFillObject,
@@ -192,7 +239,7 @@ const styles = react_native_1.StyleSheet.create({
192
239
  },
193
240
  sliderThumb: {
194
241
  position: 'absolute',
195
- left: 0,
242
+ left: WEB_SLIDER_EDGE_GESTURE_INSET,
196
243
  width: SLIDER_THUMB_SIZE,
197
244
  height: SLIDER_THUMB_SIZE,
198
245
  borderRadius: SLIDER_THUMB_SIZE / 2,
@@ -265,7 +312,7 @@ function SliderCaptcha({ visible, onClose, initialChallenge = null, loadChalleng
265
312
  const blockDisplayHeight = blockHeight * scale;
266
313
  const blockTop = Math.max(0, ((_a = challenge === null || challenge === void 0 ? void 0 : challenge.blockY) !== null && _a !== void 0 ? _a : 0) * scale);
267
314
  const maxBlockOffset = Math.max(canvasContentWidth - blockDisplayWidth, 0);
268
- const maxThumbOffset = Math.max(canvasOuterWidth - SLIDER_THUMB_SIZE, 0);
315
+ const maxThumbOffset = Math.max(canvasOuterWidth - SLIDER_THUMB_SIZE - WEB_SLIDER_EDGE_GESTURE_INSET * 2, 0);
269
316
  const blockTranslateRatio = maxThumbOffset > 0 ? maxBlockOffset / maxThumbOffset : 0;
270
317
  const isBusy = status === 'loading' || status === 'verifying';
271
318
  const canSlide = visible && Boolean(challenge) && !isBusy;
@@ -292,6 +339,7 @@ function SliderCaptcha({ visible, onClose, initialChallenge = null, loadChalleng
292
339
  opacity: 0.15,
293
340
  radius: (0, zkit_tools_1.wp)(4),
294
341
  }), [theme.colors.onSurface]);
342
+ const webSliderInteractionStyle = React.useMemo(() => resolveWebSliderInteractionStyle(canSlide), [canSlide]);
295
343
  const requestChallenge = React.useCallback(async (reason) => {
296
344
  if (!visibleRef.current)
297
345
  return;
@@ -476,6 +524,11 @@ function SliderCaptcha({ visible, onClose, initialChallenge = null, loadChalleng
476
524
  });
477
525
  return () => subscription.remove();
478
526
  }, [onClose, visible]);
527
+ React.useEffect(() => {
528
+ if (!visible || react_native_1.Platform.OS !== 'web')
529
+ return;
530
+ return acquireWebHorizontalOverscrollLock();
531
+ }, [visible]);
479
532
  const handleCanvasLayout = React.useCallback((event) => {
480
533
  const nextWidth = event.nativeEvent.layout.width;
481
534
  if (!Number.isFinite(nextWidth) || nextWidth <= 0)
@@ -521,6 +574,14 @@ function SliderCaptcha({ visible, onClose, initialChallenge = null, loadChalleng
521
574
  const blockAnimatedStyle = (0, react_native_reanimated_1.useAnimatedStyle)(() => ({
522
575
  transform: [{ translateX: thumbX.value * blockTranslateRatio }],
523
576
  }));
577
+ const cancelCurrentDrag = React.useCallback(() => {
578
+ const startPosition = clamp(thumbStartRef.current, 0, maxThumbOffset);
579
+ thumbXRef.current = startPosition;
580
+ thumbX.value = (0, react_native_reanimated_1.withTiming)(startPosition, {
581
+ duration: SLIDER_CANCEL_DURATION,
582
+ easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.cubic),
583
+ });
584
+ }, [maxThumbOffset, thumbX]);
524
585
  const panResponder = React.useMemo(() => react_native_1.PanResponder.create({
525
586
  onStartShouldSetPanResponder: () => canSlide,
526
587
  onMoveShouldSetPanResponder: () => canSlide,
@@ -537,7 +598,10 @@ function SliderCaptcha({ visible, onClose, initialChallenge = null, loadChalleng
537
598
  onPanResponderRelease: () => {
538
599
  void verifyCurrentChallenge();
539
600
  },
540
- }), [canSlide, maxThumbOffset, thumbX, verifyCurrentChallenge]);
601
+ onPanResponderTerminate: cancelCurrentDrag,
602
+ onPanResponderTerminationRequest: () => false,
603
+ onShouldBlockNativeResponder: () => true,
604
+ }), [canSlide, cancelCurrentDrag, maxThumbOffset, thumbX, verifyCurrentChallenge]);
541
605
  if (!mounted)
542
606
  return null;
543
607
  return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { pointerEvents: "box-none", style: styles.modalRoot, children: [(0, jsx_runtime_1.jsx)(react_native_reanimated_1.default.View, { style: [styles.overlayBackdrop, backdropAnimatedStyle] }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { style: styles.overlayPressable, onPress: handleBackdropPress, children: (0, jsx_runtime_1.jsx)(react_native_reanimated_1.default.View, { style: [styles.cardContainer, cardAnimatedStyle], collapsable: false, renderToHardwareTextureAndroid: react_native_1.Platform.OS === 'android', shouldRasterizeIOS: react_native_1.Platform.OS === 'ios', needsOffscreenAlphaCompositing: react_native_1.Platform.OS === 'android', children: (0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { style: [
@@ -584,6 +648,7 @@ function SliderCaptcha({ visible, onClose, initialChallenge = null, loadChalleng
584
648
  ], children: (0, jsx_runtime_1.jsx)(Text_1.Text, { style: styles.feedbackText, children: feedbackMessage }) })) : null] }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
585
649
  styles.sliderTrack,
586
650
  { backgroundColor: theme.colors.secondary, borderColor: theme.colors.border },
651
+ webSliderInteractionStyle,
587
652
  ], ...panResponder.panHandlers, children: (0, jsx_runtime_1.jsx)(react_native_reanimated_1.default.View, { style: [
588
653
  styles.sliderThumb,
589
654
  thumbAnimatedStyle,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zkit-ui",
3
- "version": "2.0.21",
3
+ "version": "2.0.22",
4
4
  "private": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,6 +8,7 @@
8
8
  - 弹层展示
9
9
  - 图片缩放与坐标换算
10
10
  - 滑块拖拽与释放触发校验
11
+ - Web 端浏览器滚动、侧滑历史返回与 Android 边缘返回冲突规避
11
12
  - `loading / verifying / success / error` 状态反馈
12
13
  - 校验失败后的自动重载
13
14
  - 业务方负责:
@@ -37,6 +37,8 @@ const CARD_HORIZONTAL_PADDING = wp(24);
37
37
  const CANVAS_BORDER_WIDTH = wp(1);
38
38
  const CAPTCHA_LAYER_Z_INDEX = 4000;
39
39
  const SLIDER_THUMB_SIZE = wp(44);
40
+ const WEB_SLIDER_EDGE_GESTURE_INSET = Platform.OS === 'web' ? wp(16) : 0;
41
+ const SLIDER_CANCEL_DURATION = 160;
40
42
  const CAPTCHA_ENTER_DURATION = 280;
41
43
  const CAPTCHA_EXIT_DURATION = 220;
42
44
  const CAPTCHA_CARD_OFFSET_Y = wp(18);
@@ -152,6 +154,73 @@ function getSafePositiveNumber(value: number | undefined, fallback: number) {
152
154
  return value;
153
155
  }
154
156
 
157
+ type WebOverscrollTarget = {
158
+ style: {
159
+ overscrollBehaviorX: string;
160
+ };
161
+ };
162
+
163
+ type WebDocument = {
164
+ body?: WebOverscrollTarget;
165
+ documentElement?: WebOverscrollTarget;
166
+ };
167
+
168
+ type WebOverscrollSnapshot = {
169
+ previousValue: string;
170
+ target: WebOverscrollTarget;
171
+ };
172
+
173
+ let webHorizontalOverscrollLockCount = 0;
174
+ let webHorizontalOverscrollSnapshots: WebOverscrollSnapshot[] = [];
175
+
176
+ function acquireWebHorizontalOverscrollLock() {
177
+ if (Platform.OS !== 'web') return undefined;
178
+
179
+ const webDocument = (globalThis as { document?: WebDocument }).document;
180
+ const targets = [webDocument?.documentElement, webDocument?.body].filter(
181
+ (target): target is WebOverscrollTarget => Boolean(target)
182
+ );
183
+ if (targets.length === 0) return undefined;
184
+
185
+ if (webHorizontalOverscrollLockCount === 0) {
186
+ webHorizontalOverscrollSnapshots = targets.map((target) => ({
187
+ previousValue: target.style.overscrollBehaviorX,
188
+ target,
189
+ }));
190
+ }
191
+
192
+ webHorizontalOverscrollLockCount += 1;
193
+ targets.forEach((target) => {
194
+ target.style.overscrollBehaviorX = 'none';
195
+ });
196
+
197
+ let released = false;
198
+ return () => {
199
+ if (released) return;
200
+ released = true;
201
+ webHorizontalOverscrollLockCount = Math.max(webHorizontalOverscrollLockCount - 1, 0);
202
+ if (webHorizontalOverscrollLockCount > 0) return;
203
+
204
+ webHorizontalOverscrollSnapshots.forEach(({ previousValue, target }) => {
205
+ if (target.style.overscrollBehaviorX === 'none') {
206
+ target.style.overscrollBehaviorX = previousValue;
207
+ }
208
+ });
209
+ webHorizontalOverscrollSnapshots = [];
210
+ };
211
+ }
212
+
213
+ function resolveWebSliderInteractionStyle(canSlide: boolean): StyleProp<ViewStyle> {
214
+ if (Platform.OS !== 'web') return undefined;
215
+
216
+ return {
217
+ cursor: canSlide ? 'grab' : 'default',
218
+ overscrollBehaviorX: 'none',
219
+ touchAction: 'none',
220
+ userSelect: 'none',
221
+ } as unknown as ViewStyle;
222
+ }
223
+
155
224
  const styles = StyleSheet.create({
156
225
  modalRoot: {
157
226
  ...StyleSheet.absoluteFillObject,
@@ -255,7 +324,7 @@ const styles = StyleSheet.create({
255
324
  },
256
325
  sliderThumb: {
257
326
  position: 'absolute',
258
- left: 0,
327
+ left: WEB_SLIDER_EDGE_GESTURE_INSET,
259
328
  width: SLIDER_THUMB_SIZE,
260
329
  height: SLIDER_THUMB_SIZE,
261
330
  borderRadius: SLIDER_THUMB_SIZE / 2,
@@ -352,7 +421,10 @@ export function SliderCaptcha<TChallenge extends SliderCaptchaChallenge = Slider
352
421
  const blockDisplayHeight = blockHeight * scale;
353
422
  const blockTop = Math.max(0, (challenge?.blockY ?? 0) * scale);
354
423
  const maxBlockOffset = Math.max(canvasContentWidth - blockDisplayWidth, 0);
355
- const maxThumbOffset = Math.max(canvasOuterWidth - SLIDER_THUMB_SIZE, 0);
424
+ const maxThumbOffset = Math.max(
425
+ canvasOuterWidth - SLIDER_THUMB_SIZE - WEB_SLIDER_EDGE_GESTURE_INSET * 2,
426
+ 0
427
+ );
356
428
  const blockTranslateRatio = maxThumbOffset > 0 ? maxBlockOffset / maxThumbOffset : 0;
357
429
  const isBusy = status === 'loading' || status === 'verifying';
358
430
  const canSlide = visible && Boolean(challenge) && !isBusy;
@@ -392,6 +464,10 @@ export function SliderCaptcha<TChallenge extends SliderCaptchaChallenge = Slider
392
464
  }),
393
465
  [theme.colors.onSurface]
394
466
  );
467
+ const webSliderInteractionStyle = React.useMemo(
468
+ () => resolveWebSliderInteractionStyle(canSlide),
469
+ [canSlide]
470
+ );
395
471
 
396
472
  const requestChallenge = React.useCallback(
397
473
  async (reason: SliderCaptchaLoadReason) => {
@@ -603,6 +679,11 @@ export function SliderCaptcha<TChallenge extends SliderCaptchaChallenge = Slider
603
679
  return () => subscription.remove();
604
680
  }, [onClose, visible]);
605
681
 
682
+ React.useEffect(() => {
683
+ if (!visible || Platform.OS !== 'web') return;
684
+ return acquireWebHorizontalOverscrollLock();
685
+ }, [visible]);
686
+
606
687
  const handleCanvasLayout = React.useCallback((event: LayoutChangeEvent) => {
607
688
  const nextWidth = event.nativeEvent.layout.width;
608
689
  if (!Number.isFinite(nextWidth) || nextWidth <= 0) return;
@@ -654,6 +735,15 @@ export function SliderCaptcha<TChallenge extends SliderCaptchaChallenge = Slider
654
735
  transform: [{ translateX: thumbX.value * blockTranslateRatio }],
655
736
  }));
656
737
 
738
+ const cancelCurrentDrag = React.useCallback(() => {
739
+ const startPosition = clamp(thumbStartRef.current, 0, maxThumbOffset);
740
+ thumbXRef.current = startPosition;
741
+ thumbX.value = withTiming(startPosition, {
742
+ duration: SLIDER_CANCEL_DURATION,
743
+ easing: Easing.out(Easing.cubic),
744
+ });
745
+ }, [maxThumbOffset, thumbX]);
746
+
657
747
  const panResponder = React.useMemo(
658
748
  () =>
659
749
  PanResponder.create({
@@ -672,8 +762,11 @@ export function SliderCaptcha<TChallenge extends SliderCaptchaChallenge = Slider
672
762
  onPanResponderRelease: () => {
673
763
  void verifyCurrentChallenge();
674
764
  },
765
+ onPanResponderTerminate: cancelCurrentDrag,
766
+ onPanResponderTerminationRequest: () => false,
767
+ onShouldBlockNativeResponder: () => true,
675
768
  }),
676
- [canSlide, maxThumbOffset, thumbX, verifyCurrentChallenge]
769
+ [canSlide, cancelCurrentDrag, maxThumbOffset, thumbX, verifyCurrentChallenge]
677
770
  );
678
771
 
679
772
  if (!mounted) return null;
@@ -809,6 +902,7 @@ export function SliderCaptcha<TChallenge extends SliderCaptchaChallenge = Slider
809
902
  style={[
810
903
  styles.sliderTrack,
811
904
  { backgroundColor: theme.colors.secondary, borderColor: theme.colors.border },
905
+ webSliderInteractionStyle,
812
906
  ]}
813
907
  {...panResponder.panHandlers}
814
908
  >