react-native-divkit 1.6.5 → 1.8.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 +18 -15
- package/dist/DivKit.d.ts.map +1 -1
- package/dist/DivKit.js +115 -4
- package/dist/DivKit.js.map +1 -1
- package/dist/components/DivComponent.d.ts.map +1 -1
- package/dist/components/DivComponent.js +6 -2
- package/dist/components/DivComponent.js.map +1 -1
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/indicator/DivIndicator.d.ts +19 -0
- package/dist/components/indicator/DivIndicator.d.ts.map +1 -0
- package/dist/components/indicator/DivIndicator.js +112 -0
- package/dist/components/indicator/DivIndicator.js.map +1 -0
- package/dist/components/indicator/index.d.ts +3 -0
- package/dist/components/indicator/index.d.ts.map +1 -0
- package/dist/components/indicator/index.js +2 -0
- package/dist/components/indicator/index.js.map +1 -0
- package/dist/components/indicator/utils.d.ts +61 -0
- package/dist/components/indicator/utils.d.ts.map +1 -0
- package/dist/components/indicator/utils.js +104 -0
- package/dist/components/indicator/utils.js.map +1 -0
- package/dist/components/pager/DivPager.d.ts +22 -0
- package/dist/components/pager/DivPager.d.ts.map +1 -0
- package/dist/components/pager/DivPager.js +269 -0
- package/dist/components/pager/DivPager.js.map +1 -0
- package/dist/components/pager/index.d.ts +3 -0
- package/dist/components/pager/index.d.ts.map +1 -0
- package/dist/components/pager/index.js +2 -0
- package/dist/components/pager/index.js.map +1 -0
- package/dist/components/pager/utils.d.ts +96 -0
- package/dist/components/pager/utils.d.ts.map +1 -0
- package/dist/components/pager/utils.js +142 -0
- package/dist/components/pager/utils.js.map +1 -0
- package/dist/components/state/DivState.d.ts +11 -12
- package/dist/components/state/DivState.d.ts.map +1 -1
- package/dist/components/state/DivState.js +263 -35
- package/dist/components/state/DivState.js.map +1 -1
- package/dist/components/utilities/Background.d.ts.map +1 -1
- package/dist/components/utilities/Background.js +4 -3
- package/dist/components/utilities/Background.js.map +1 -1
- package/dist/components/utilities/Outer.d.ts.map +1 -1
- package/dist/components/utilities/Outer.js +175 -78
- package/dist/components/utilities/Outer.js.map +1 -1
- package/dist/context/DivStateScopeContext.d.ts +18 -0
- package/dist/context/DivStateScopeContext.d.ts.map +1 -0
- package/dist/context/DivStateScopeContext.js +7 -0
- package/dist/context/DivStateScopeContext.js.map +1 -0
- package/dist/context/PagerContext.d.ts +30 -0
- package/dist/context/PagerContext.d.ts.map +1 -0
- package/dist/context/PagerContext.js +76 -0
- package/dist/context/PagerContext.js.map +1 -0
- package/dist/context/index.d.ts +1 -0
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +1 -0
- package/dist/context/index.js.map +1 -1
- package/dist/hooks/useAppearanceTransition.d.ts +86 -0
- package/dist/hooks/useAppearanceTransition.d.ts.map +1 -0
- package/dist/hooks/useAppearanceTransition.js +490 -0
- package/dist/hooks/useAppearanceTransition.js.map +1 -0
- package/dist/hooks/useChangeBoundsTransition.d.ts +46 -0
- package/dist/hooks/useChangeBoundsTransition.d.ts.map +1 -0
- package/dist/hooks/useChangeBoundsTransition.js +151 -0
- package/dist/hooks/useChangeBoundsTransition.js.map +1 -0
- package/dist/utils/configureChangeBoundsLayout.d.ts +11 -0
- package/dist/utils/configureChangeBoundsLayout.d.ts.map +1 -0
- package/dist/utils/configureChangeBoundsLayout.js +65 -0
- package/dist/utils/configureChangeBoundsLayout.js.map +1 -0
- package/dist/utils/flattenTransition.d.ts +5 -0
- package/dist/utils/flattenTransition.d.ts.map +1 -0
- package/dist/utils/flattenTransition.js +27 -0
- package/dist/utils/flattenTransition.js.map +1 -0
- package/package.json +3 -1
- package/src/DivKit.tsx +131 -5
- package/src/components/DivComponent.tsx +8 -2
- package/src/components/README.md +59 -5
- package/src/components/index.ts +4 -0
- package/src/components/indicator/DivIndicator.tsx +175 -0
- package/src/components/indicator/index.ts +2 -0
- package/src/components/indicator/utils.ts +149 -0
- package/src/components/pager/DivPager.tsx +393 -0
- package/src/components/pager/index.ts +2 -0
- package/src/components/pager/utils.ts +214 -0
- package/src/components/state/DivState.tsx +308 -39
- package/src/components/utilities/Background.tsx +4 -3
- package/src/components/utilities/Outer.tsx +192 -75
- package/src/context/DivStateScopeContext.tsx +23 -0
- package/src/context/PagerContext.tsx +108 -0
- package/src/context/index.ts +8 -0
- package/src/hooks/useAppearanceTransition.ts +621 -0
- package/src/hooks/useChangeBoundsTransition.ts +193 -0
- package/src/types/indicator.d.ts +32 -0
- package/src/types/pager.d.ts +36 -0
- package/src/types/shape.d.ts +26 -0
- package/src/utils/configureChangeBoundsLayout.ts +74 -0
- package/src/utils/flattenTransition.ts +36 -0
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
import React, { ReactNode, useMemo, useRef, useCallback } from 'react';
|
|
2
|
-
import { View, Pressable, Animated, ViewStyle, StyleSheet, Easing, EasingFunction } from 'react-native';
|
|
1
|
+
import React, { ReactNode, useMemo, useRef, useCallback, useState } from 'react';
|
|
2
|
+
import { View, Pressable, Animated, ViewStyle, StyleSheet, Easing, EasingFunction, LayoutChangeEvent } from 'react-native';
|
|
3
3
|
import type { ComponentContext } from '../../types/componentContext';
|
|
4
4
|
import type { DivBaseData } from '../../types/base';
|
|
5
|
-
import type { Visibility } from '../../types/base';
|
|
5
|
+
import type { Visibility, AppearanceTransition, TransitionChange } from '../../types/base';
|
|
6
6
|
import type { FixedSize, MatchParentSize, WrapContentSize } from '../../types/sizes';
|
|
7
7
|
import type { MaybeMissing } from '../../expressions/json';
|
|
8
8
|
import type { Animation } from '../../types/animation';
|
|
9
9
|
import type { Interpolation } from '../../../typings/common';
|
|
10
10
|
import { useDerivedFromVarsSimple } from '../../hooks/useDerivedFromVars';
|
|
11
11
|
import { useActionHandler, useHasActions } from '../../hooks/useAction';
|
|
12
|
+
import { useAppearanceTransition } from '../../hooks/useAppearanceTransition';
|
|
13
|
+
import { useChangeBoundsTransition } from '../../hooks/useChangeBoundsTransition';
|
|
12
14
|
import { useDivKitContext } from '../../context/DivKitContext';
|
|
13
15
|
import { useLayoutParams } from '../../context/LayoutParamsContext';
|
|
16
|
+
import { useDivStateScopeOptional } from '../../context/DivStateScopeContext';
|
|
14
17
|
import { Background } from './Background';
|
|
15
18
|
import { flattenAnimation } from '../../utils/flattenAnimation';
|
|
19
|
+
import { configureChangeBoundsLayout } from '../../utils/configureChangeBoundsLayout';
|
|
16
20
|
|
|
17
21
|
function resolveAlignSelf(
|
|
18
22
|
alignment: string | undefined,
|
|
@@ -122,6 +126,7 @@ export function Outer<T extends DivBaseData = DivBaseData>({
|
|
|
122
126
|
const { direction } = useDivKitContext();
|
|
123
127
|
const layoutParams = useLayoutParams();
|
|
124
128
|
const { json, variables } = componentContext;
|
|
129
|
+
const testID = (json as any).id as string | undefined;
|
|
125
130
|
|
|
126
131
|
// Only use reactive hooks for truly dynamic properties (visibility, alpha)
|
|
127
132
|
const visibility = useDerivedFromVarsSimple<Visibility>(json.visibility || 'visible', variables || new Map());
|
|
@@ -194,20 +199,72 @@ export function Outer<T extends DivBaseData = DivBaseData>({
|
|
|
194
199
|
Animated.parallel(anims).start();
|
|
195
200
|
}, [parsedAnimations, animOpacity, animScale]);
|
|
196
201
|
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
202
|
+
// Appearance transitions (transition_in / transition_out) and change_bounds (transition_change)
|
|
203
|
+
const transitionIn = (json as any).transition_in as MaybeMissing<AppearanceTransition> | undefined;
|
|
204
|
+
const transitionOut = (json as any).transition_out as MaybeMissing<AppearanceTransition> | undefined;
|
|
205
|
+
const transitionChange = (json as any).transition_change as MaybeMissing<TransitionChange> | undefined;
|
|
206
|
+
const hasAppearanceTransitions = Boolean(transitionIn || transitionOut);
|
|
207
|
+
const stateScope = useDivStateScopeOptional();
|
|
208
|
+
const insideDivState = Boolean(stateScope);
|
|
209
|
+
|
|
210
|
+
// For neighbors: when WE appear/collapse, queue a coarse LayoutAnimation so they reflow smoothly.
|
|
211
|
+
const triggerChangeBoundsForNeighbors = useCallback(() => {
|
|
212
|
+
configureChangeBoundsLayout(transitionChange);
|
|
213
|
+
}, [transitionChange]);
|
|
214
|
+
|
|
215
|
+
// FLIP-based bounds animation for THIS element: tracks layout deltas and animates via
|
|
216
|
+
// transform (translate+scale) so the spec's interpolator/cubic-bezier actually applies.
|
|
217
|
+
const bounds = useChangeBoundsTransition({
|
|
218
|
+
transitionChange
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Measured size for off-center pivot scale (translate-scale-translate emulation).
|
|
222
|
+
// We reuse the same onLayout for both FLIP and pivot calculations.
|
|
223
|
+
const [layoutSize, setLayoutSize] = useState<{ width: number; height: number } | null>(null);
|
|
224
|
+
const onLayoutMeasure = useCallback((e: LayoutChangeEvent) => {
|
|
225
|
+
const { width, height } = e.nativeEvent.layout;
|
|
226
|
+
setLayoutSize(prev => {
|
|
227
|
+
if (prev && prev.width === width && prev.height === height) return prev;
|
|
228
|
+
return { width, height };
|
|
229
|
+
});
|
|
230
|
+
bounds.onLayout(e);
|
|
231
|
+
}, [bounds]);
|
|
232
|
+
|
|
233
|
+
// Whether this element lives inside a DivState. If so, on state-change DivState will call
|
|
234
|
+
// our playOut (via the scope context) and await it before swapping children. In that mode we
|
|
235
|
+
// disable the visibility-driven first-mount transition_in (DivState will trigger playIn on
|
|
236
|
+
// the new children automatically through their mount-effect with auto-in mode).
|
|
237
|
+
const appearance = useAppearanceTransition({
|
|
238
|
+
visibility: visibility as Visibility,
|
|
239
|
+
transitionIn,
|
|
240
|
+
transitionOut,
|
|
241
|
+
enabled: hasAppearanceTransitions,
|
|
242
|
+
mode: insideDivState ? 'auto-in' : 'visibility',
|
|
243
|
+
onBeforeCollapse: triggerChangeBoundsForNeighbors,
|
|
244
|
+
onBeforeExpand: triggerChangeBoundsForNeighbors,
|
|
245
|
+
layoutWidth: layoutSize?.width,
|
|
246
|
+
layoutHeight: layoutSize?.height,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Register transition_out player with the enclosing DivState scope so it can play before unmount.
|
|
250
|
+
React.useEffect(() => {
|
|
251
|
+
if (!stateScope || !appearance.hasTransitionOut) return;
|
|
252
|
+
return stateScope.registerTransitionOutPlayer(appearance.playOut);
|
|
253
|
+
}, [stateScope, appearance.hasTransitionOut, appearance.playOut]);
|
|
201
254
|
|
|
202
255
|
// Build styles
|
|
203
256
|
const containerStyle = useMemo(() => {
|
|
204
257
|
const styles: ViewStyle = {};
|
|
205
258
|
|
|
206
259
|
// Visibility (invisible = opacity 0, but still takes space)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
260
|
+
// When appearance transitions are present, opacity is driven by Animated values below,
|
|
261
|
+
// not by static styles — otherwise the static value would override animation.
|
|
262
|
+
if (!hasAppearanceTransitions) {
|
|
263
|
+
if (visibility === 'invisible') {
|
|
264
|
+
styles.opacity = 0;
|
|
265
|
+
} else if (typeof alpha === 'number' && alpha !== 1) {
|
|
266
|
+
styles.opacity = Math.max(0, Math.min(1, alpha));
|
|
267
|
+
}
|
|
211
268
|
}
|
|
212
269
|
|
|
213
270
|
// Resolve effective alignment (explicit > parent fallback > 'start')
|
|
@@ -435,7 +492,7 @@ export function Outer<T extends DivBaseData = DivBaseData>({
|
|
|
435
492
|
}
|
|
436
493
|
|
|
437
494
|
return styles;
|
|
438
|
-
}, [visibility, alpha, width, height, paddings, margins, background, border, direction, layoutParams, alignmentHorizontal, alignmentVertical]);
|
|
495
|
+
}, [visibility, alpha, width, height, paddings, margins, background, border, direction, layoutParams, alignmentHorizontal, alignmentVertical, hasAppearanceTransitions]);
|
|
439
496
|
|
|
440
497
|
const finalStyle = useMemo(() => {
|
|
441
498
|
return StyleSheet.flatten([containerStyle, customStyle]);
|
|
@@ -452,83 +509,143 @@ export function Outer<T extends DivBaseData = DivBaseData>({
|
|
|
452
509
|
return res;
|
|
453
510
|
}, [finalStyle]);
|
|
454
511
|
|
|
455
|
-
//
|
|
456
|
-
if (
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
512
|
+
// Collapsed via appearance transitions (or static 'gone' without transitions) — render nothing
|
|
513
|
+
if (appearance.collapsed) {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
if (!hasAppearanceTransitions && visibility === 'gone') {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const hasActionAnim = parsedAnimations.length > 0;
|
|
521
|
+
const hasTransitionChange = Boolean(transitionChange);
|
|
522
|
+
const needsAnimatedWrapper = hasActionAnim || hasAppearanceTransitions || hasTransitionChange;
|
|
523
|
+
|
|
524
|
+
if (needsAnimatedWrapper) {
|
|
525
|
+
// Split finalStyle into outer (layout) and inner (visual) styles.
|
|
526
|
+
// Margins/sizing/alignSelf live on the wrapper that participates in parent layout (Pressable or outer View),
|
|
527
|
+
// visual styles go inside Animated.View.
|
|
528
|
+
const {
|
|
529
|
+
alignSelf, flexGrow, flexShrink, flexBasis,
|
|
530
|
+
width: w, height: h, minWidth, maxWidth, minHeight, maxHeight,
|
|
531
|
+
marginTop, marginBottom, marginLeft, marginRight,
|
|
532
|
+
...innerStyle
|
|
533
|
+
} = (finalStyle || {}) as any;
|
|
534
|
+
|
|
535
|
+
const outerStyle: ViewStyle = {};
|
|
536
|
+
if (alignSelf !== undefined) outerStyle.alignSelf = alignSelf;
|
|
537
|
+
if (flexGrow !== undefined) outerStyle.flexGrow = flexGrow;
|
|
538
|
+
if (flexShrink !== undefined) outerStyle.flexShrink = flexShrink;
|
|
539
|
+
if (flexBasis !== undefined) outerStyle.flexBasis = flexBasis;
|
|
540
|
+
if (w !== undefined) outerStyle.width = w;
|
|
541
|
+
if (h !== undefined) outerStyle.height = h;
|
|
542
|
+
if (minWidth !== undefined) outerStyle.minWidth = minWidth;
|
|
543
|
+
if (maxWidth !== undefined) outerStyle.maxWidth = maxWidth;
|
|
544
|
+
if (minHeight !== undefined) outerStyle.minHeight = minHeight;
|
|
545
|
+
if (maxHeight !== undefined) outerStyle.maxHeight = maxHeight;
|
|
546
|
+
if (marginTop !== undefined) outerStyle.marginTop = marginTop;
|
|
547
|
+
if (marginBottom !== undefined) outerStyle.marginBottom = marginBottom;
|
|
548
|
+
if (marginLeft !== undefined) outerStyle.marginLeft = marginLeft;
|
|
549
|
+
if (marginRight !== undefined) outerStyle.marginRight = marginRight;
|
|
550
|
+
|
|
551
|
+
const shouldFillInner =
|
|
552
|
+
w !== undefined ||
|
|
553
|
+
h !== undefined ||
|
|
554
|
+
minWidth !== undefined ||
|
|
555
|
+
maxWidth !== undefined ||
|
|
556
|
+
minHeight !== undefined ||
|
|
557
|
+
maxHeight !== undefined ||
|
|
558
|
+
flexGrow !== undefined ||
|
|
559
|
+
flexShrink !== undefined ||
|
|
560
|
+
flexBasis !== undefined;
|
|
561
|
+
|
|
562
|
+
const animatedStyle: any = shouldFillInner
|
|
563
|
+
? { ...innerStyle, flex: 1 }
|
|
564
|
+
: { ...innerStyle };
|
|
565
|
+
|
|
566
|
+
// Compose opacity chain: static * action_animation * appearance_transition
|
|
567
|
+
let opacityChain: any = undefined;
|
|
568
|
+
const staticOpacity = animatedStyle.opacity;
|
|
569
|
+
if (staticOpacity !== undefined && staticOpacity !== 1) {
|
|
570
|
+
opacityChain = staticOpacity;
|
|
571
|
+
}
|
|
572
|
+
if (hasActionAnim && hasFadeAnimation) {
|
|
573
|
+
opacityChain = opacityChain !== undefined
|
|
574
|
+
? Animated.multiply(animOpacity, opacityChain)
|
|
575
|
+
: animOpacity;
|
|
576
|
+
}
|
|
577
|
+
if (hasAppearanceTransitions) {
|
|
578
|
+
opacityChain = opacityChain !== undefined
|
|
579
|
+
? Animated.multiply(appearance.opacity as Animated.Value, opacityChain)
|
|
580
|
+
: appearance.opacity;
|
|
581
|
+
}
|
|
582
|
+
if (opacityChain !== undefined) {
|
|
583
|
+
animatedStyle.opacity = opacityChain;
|
|
584
|
+
}
|
|
509
585
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
586
|
+
// Compose transform array. Order matters: in RN transforms apply right-to-left
|
|
587
|
+
// (the LAST element is applied first to the point, then earlier ones).
|
|
588
|
+
// We want FLIP (bounds) to wrap everything → put it FIRST (so it applies last on top
|
|
589
|
+
// of action_animation/appearance). Static transforms from style come first too.
|
|
590
|
+
const transforms: any[] = animatedStyle.transform ? [...animatedStyle.transform] : [];
|
|
591
|
+
if (hasTransitionChange && bounds.transform.length > 0) {
|
|
592
|
+
for (const t of bounds.transform) {
|
|
593
|
+
transforms.push(t);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (hasActionAnim && hasScaleAnimation) {
|
|
597
|
+
transforms.push({ scale: animScale });
|
|
598
|
+
}
|
|
599
|
+
if (hasAppearanceTransitions && appearance.transform.length > 0) {
|
|
600
|
+
for (const t of appearance.transform) {
|
|
601
|
+
transforms.push(t);
|
|
513
602
|
}
|
|
603
|
+
}
|
|
604
|
+
if (transforms.length > 0) {
|
|
605
|
+
animatedStyle.transform = transforms;
|
|
606
|
+
}
|
|
514
607
|
|
|
608
|
+
const innerNeedsOnLayout = hasAppearanceTransitions && !hasTransitionChange;
|
|
609
|
+
const outerNeedsOnLayout = hasTransitionChange;
|
|
610
|
+
const content = (
|
|
611
|
+
<Animated.View
|
|
612
|
+
style={animatedStyle}
|
|
613
|
+
onLayout={innerNeedsOnLayout ? onLayoutMeasure : undefined}
|
|
614
|
+
>
|
|
615
|
+
<Background layers={background as any} style={borderStyle} />
|
|
616
|
+
{children}
|
|
617
|
+
</Animated.View>
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
if (hasActions) {
|
|
515
621
|
return (
|
|
516
622
|
<Pressable
|
|
517
623
|
onPress={handlePress}
|
|
518
624
|
onPressIn={onPressIn}
|
|
519
625
|
onPressOut={onPressOut}
|
|
520
626
|
style={outerStyle}
|
|
627
|
+
onLayout={outerNeedsOnLayout ? onLayoutMeasure : undefined}
|
|
628
|
+
testID={testID}
|
|
521
629
|
>
|
|
522
|
-
|
|
523
|
-
<Background layers={background as any} style={borderStyle} />
|
|
524
|
-
{children}
|
|
525
|
-
</Animated.View>
|
|
630
|
+
{content}
|
|
526
631
|
</Pressable>
|
|
527
632
|
);
|
|
528
633
|
}
|
|
529
634
|
|
|
530
635
|
return (
|
|
531
|
-
<
|
|
636
|
+
<View
|
|
637
|
+
style={outerStyle}
|
|
638
|
+
onLayout={outerNeedsOnLayout ? onLayoutMeasure : undefined}
|
|
639
|
+
testID={testID}
|
|
640
|
+
>
|
|
641
|
+
{content}
|
|
642
|
+
</View>
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (hasActions) {
|
|
647
|
+
return (
|
|
648
|
+
<Pressable onPress={handlePress} style={finalStyle} testID={testID}>
|
|
532
649
|
<Background layers={background as any} style={borderStyle} />
|
|
533
650
|
{children}
|
|
534
651
|
</Pressable>
|
|
@@ -536,7 +653,7 @@ export function Outer<T extends DivBaseData = DivBaseData>({
|
|
|
536
653
|
}
|
|
537
654
|
|
|
538
655
|
return (
|
|
539
|
-
<View style={finalStyle}>
|
|
656
|
+
<View style={finalStyle} testID={testID}>
|
|
540
657
|
<Background layers={background as any} style={borderStyle} />
|
|
541
658
|
{children}
|
|
542
659
|
</View>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Scoped context provided by a single DivState wrapping its currently-rendered children.
|
|
5
|
+
*
|
|
6
|
+
* Allows children that declare `transition_out` (via Outer + useAppearanceTransition) to
|
|
7
|
+
* register a playOut callback so that DivState can await all of them in parallel
|
|
8
|
+
* before swapping to a new state. Analogous to Web's stateCtx.registerChildWithTransitionOut.
|
|
9
|
+
*/
|
|
10
|
+
export interface DivStateScopeValue {
|
|
11
|
+
/**
|
|
12
|
+
* Register a child's transition_out player.
|
|
13
|
+
* Returns an unregister callback (call from cleanup).
|
|
14
|
+
*/
|
|
15
|
+
registerTransitionOutPlayer(play: () => Promise<void>): () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const DivStateScopeContext = createContext<DivStateScopeValue | null>(null);
|
|
19
|
+
|
|
20
|
+
/** Get the nearest DivState scope, or null if outside any DivState. */
|
|
21
|
+
export function useDivStateScopeOptional(): DivStateScopeValue | null {
|
|
22
|
+
return useContext(DivStateScopeContext);
|
|
23
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createContext, useContext, useCallback, useMemo, useRef, ReactNode, createElement } from 'react';
|
|
2
|
+
import type { PagerData, PagerListener, PagerRegisterData } from '../types/componentContext';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PagerContext — shared registry for pagers and their indicators.
|
|
6
|
+
*
|
|
7
|
+
* Based on Web Root.svelte: registerPager / listenPager.
|
|
8
|
+
*
|
|
9
|
+
* - Pager registers itself with `registerPager(pagerId)` and gets back an object with
|
|
10
|
+
* `update(data)` (called whenever current item / size changes) and `destroy()`.
|
|
11
|
+
* - Indicator subscribes to a pager via `listenPager(pagerId, cb)`.
|
|
12
|
+
* - Subscribers receive the current pager state immediately if it's already known
|
|
13
|
+
* (so indicator works regardless of mount order).
|
|
14
|
+
*/
|
|
15
|
+
export interface PagerContextValue {
|
|
16
|
+
registerPager(pagerId: string | undefined): PagerRegisterData;
|
|
17
|
+
listenPager(pagerId: string | undefined, listener: PagerListener): () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const PagerContext = createContext<PagerContextValue | null>(null);
|
|
21
|
+
|
|
22
|
+
export function usePagerContextOptional(): PagerContextValue | null {
|
|
23
|
+
return useContext(PagerContext);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function usePagerContext(): PagerContextValue {
|
|
27
|
+
const context = useContext(PagerContext);
|
|
28
|
+
if (!context) {
|
|
29
|
+
throw new Error('usePagerContext must be used within PagerContext.Provider');
|
|
30
|
+
}
|
|
31
|
+
return context;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PagerProviderProps {
|
|
35
|
+
children: ReactNode;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Provider that holds pager state in stable refs. Mirrors the Web behaviour
|
|
40
|
+
* where registration / listening is keyed by pagerId (with `undefined` allowed
|
|
41
|
+
* so indicators without an explicit pager_id still work).
|
|
42
|
+
*/
|
|
43
|
+
export function PagerProvider({ children }: PagerProviderProps) {
|
|
44
|
+
const pagersRef = useRef<Map<string | undefined, PagerData | null>>(new Map());
|
|
45
|
+
const listenersRef = useRef<Map<string | undefined, PagerListener[]>>(new Map());
|
|
46
|
+
|
|
47
|
+
const notify = useCallback((pagerId: string | undefined, data: PagerData) => {
|
|
48
|
+
const list = listenersRef.current.get(pagerId);
|
|
49
|
+
if (!list) return;
|
|
50
|
+
for (const fn of list) {
|
|
51
|
+
try {
|
|
52
|
+
fn(data);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
// eslint-disable-next-line no-console
|
|
55
|
+
console.error('[DivKit Pager] listener error', err);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
const registerPager = useCallback((pagerId: string | undefined): PagerRegisterData => {
|
|
61
|
+
return {
|
|
62
|
+
update: (data: PagerData) => {
|
|
63
|
+
pagersRef.current.set(pagerId, data);
|
|
64
|
+
notify(pagerId, data);
|
|
65
|
+
},
|
|
66
|
+
destroy: () => {
|
|
67
|
+
pagersRef.current.set(pagerId, null);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}, [notify]);
|
|
71
|
+
|
|
72
|
+
const listenPager = useCallback(
|
|
73
|
+
(pagerId: string | undefined, listener: PagerListener): (() => void) => {
|
|
74
|
+
let list = listenersRef.current.get(pagerId);
|
|
75
|
+
if (!list) {
|
|
76
|
+
list = [];
|
|
77
|
+
listenersRef.current.set(pagerId, list);
|
|
78
|
+
}
|
|
79
|
+
list.push(listener);
|
|
80
|
+
|
|
81
|
+
// Replay last known state so indicator gets current pager position immediately
|
|
82
|
+
const current = pagersRef.current.get(pagerId);
|
|
83
|
+
if (current) {
|
|
84
|
+
try {
|
|
85
|
+
listener(current);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
// eslint-disable-next-line no-console
|
|
88
|
+
console.error('[DivKit Pager] listener error', err);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return () => {
|
|
93
|
+
const arr = listenersRef.current.get(pagerId);
|
|
94
|
+
if (!arr) return;
|
|
95
|
+
const idx = arr.indexOf(listener);
|
|
96
|
+
if (idx >= 0) arr.splice(idx, 1);
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
[]
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const value = useMemo<PagerContextValue>(
|
|
103
|
+
() => ({ registerPager, listenPager }),
|
|
104
|
+
[registerPager, listenPager]
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return createElement(PagerContext.Provider, { value }, children);
|
|
108
|
+
}
|
package/src/context/index.ts
CHANGED