react-native-divkit 1.7.0 → 1.8.1

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.
Files changed (48) hide show
  1. package/README.md +17 -16
  2. package/dist/DivKit.d.ts.map +1 -1
  3. package/dist/DivKit.js +109 -1
  4. package/dist/DivKit.js.map +1 -1
  5. package/dist/components/pager/utils.d.ts.map +1 -1
  6. package/dist/components/pager/utils.js +17 -4
  7. package/dist/components/pager/utils.js.map +1 -1
  8. package/dist/components/state/DivState.d.ts +11 -12
  9. package/dist/components/state/DivState.d.ts.map +1 -1
  10. package/dist/components/state/DivState.js +263 -35
  11. package/dist/components/state/DivState.js.map +1 -1
  12. package/dist/components/utilities/Background.d.ts.map +1 -1
  13. package/dist/components/utilities/Background.js +4 -3
  14. package/dist/components/utilities/Background.js.map +1 -1
  15. package/dist/components/utilities/Outer.d.ts.map +1 -1
  16. package/dist/components/utilities/Outer.js +172 -76
  17. package/dist/components/utilities/Outer.js.map +1 -1
  18. package/dist/context/DivStateScopeContext.d.ts +18 -0
  19. package/dist/context/DivStateScopeContext.d.ts.map +1 -0
  20. package/dist/context/DivStateScopeContext.js +7 -0
  21. package/dist/context/DivStateScopeContext.js.map +1 -0
  22. package/dist/hooks/useAppearanceTransition.d.ts +86 -0
  23. package/dist/hooks/useAppearanceTransition.d.ts.map +1 -0
  24. package/dist/hooks/useAppearanceTransition.js +490 -0
  25. package/dist/hooks/useAppearanceTransition.js.map +1 -0
  26. package/dist/hooks/useChangeBoundsTransition.d.ts +46 -0
  27. package/dist/hooks/useChangeBoundsTransition.d.ts.map +1 -0
  28. package/dist/hooks/useChangeBoundsTransition.js +151 -0
  29. package/dist/hooks/useChangeBoundsTransition.js.map +1 -0
  30. package/dist/utils/configureChangeBoundsLayout.d.ts +11 -0
  31. package/dist/utils/configureChangeBoundsLayout.d.ts.map +1 -0
  32. package/dist/utils/configureChangeBoundsLayout.js +65 -0
  33. package/dist/utils/configureChangeBoundsLayout.js.map +1 -0
  34. package/dist/utils/flattenTransition.d.ts +5 -0
  35. package/dist/utils/flattenTransition.d.ts.map +1 -0
  36. package/dist/utils/flattenTransition.js +27 -0
  37. package/dist/utils/flattenTransition.js.map +1 -0
  38. package/package.json +2 -1
  39. package/src/DivKit.tsx +125 -2
  40. package/src/components/pager/utils.ts +18 -4
  41. package/src/components/state/DivState.tsx +308 -39
  42. package/src/components/utilities/Background.tsx +4 -3
  43. package/src/components/utilities/Outer.tsx +188 -73
  44. package/src/context/DivStateScopeContext.tsx +23 -0
  45. package/src/hooks/useAppearanceTransition.ts +621 -0
  46. package/src/hooks/useChangeBoundsTransition.ts +193 -0
  47. package/src/utils/configureChangeBoundsLayout.ts +74 -0
  48. 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,
@@ -195,20 +199,72 @@ export function Outer<T extends DivBaseData = DivBaseData>({
195
199
  Animated.parallel(anims).start();
196
200
  }, [parsedAnimations, animOpacity, animScale]);
197
201
 
198
- // Early return for gone visibility
199
- if (visibility === 'gone') {
200
- return null;
201
- }
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]);
202
254
 
203
255
  // Build styles
204
256
  const containerStyle = useMemo(() => {
205
257
  const styles: ViewStyle = {};
206
258
 
207
259
  // Visibility (invisible = opacity 0, but still takes space)
208
- if (visibility === 'invisible') {
209
- styles.opacity = 0;
210
- } else if (typeof alpha === 'number' && alpha !== 1) {
211
- styles.opacity = Math.max(0, Math.min(1, alpha));
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
+ }
212
268
  }
213
269
 
214
270
  // Resolve effective alignment (explicit > parent fallback > 'start')
@@ -436,7 +492,7 @@ export function Outer<T extends DivBaseData = DivBaseData>({
436
492
  }
437
493
 
438
494
  return styles;
439
- }, [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]);
440
496
 
441
497
  const finalStyle = useMemo(() => {
442
498
  return StyleSheet.flatten([containerStyle, customStyle]);
@@ -453,82 +509,141 @@ export function Outer<T extends DivBaseData = DivBaseData>({
453
509
  return res;
454
510
  }, [finalStyle]);
455
511
 
456
- // Render with actions and animation
457
- if (hasActions) {
458
- const hasAnimation = parsedAnimations.length > 0;
459
-
460
- if (hasAnimation) {
461
- // Split finalStyle into outer (layout) and inner (visual) styles
462
- // Margins must be on Pressable to affect parent layout
463
- const {
464
- alignSelf, flexGrow, flexShrink, flexBasis,
465
- width: w, height: h, minWidth, maxWidth, minHeight, maxHeight,
466
- marginTop, marginBottom, marginLeft, marginRight,
467
- ...innerStyle
468
- } = (finalStyle || {}) as any;
469
-
470
- const outerStyle: ViewStyle = {};
471
- if (alignSelf !== undefined) outerStyle.alignSelf = alignSelf;
472
- if (flexGrow !== undefined) outerStyle.flexGrow = flexGrow;
473
- if (flexShrink !== undefined) outerStyle.flexShrink = flexShrink;
474
- if (flexBasis !== undefined) outerStyle.flexBasis = flexBasis;
475
- if (w !== undefined) outerStyle.width = w;
476
- if (h !== undefined) outerStyle.height = h;
477
- if (minWidth !== undefined) outerStyle.minWidth = minWidth;
478
- if (maxWidth !== undefined) outerStyle.maxWidth = maxWidth;
479
- if (minHeight !== undefined) outerStyle.minHeight = minHeight;
480
- if (maxHeight !== undefined) outerStyle.maxHeight = maxHeight;
481
- if (marginTop !== undefined) outerStyle.marginTop = marginTop;
482
- if (marginBottom !== undefined) outerStyle.marginBottom = marginBottom;
483
- if (marginLeft !== undefined) outerStyle.marginLeft = marginLeft;
484
- if (marginRight !== undefined) outerStyle.marginRight = marginRight;
485
-
486
- // Build animated style from inner (visual) properties
487
- const shouldFillInner =
488
- w !== undefined ||
489
- h !== undefined ||
490
- minWidth !== undefined ||
491
- maxWidth !== undefined ||
492
- minHeight !== undefined ||
493
- maxHeight !== undefined ||
494
- flexGrow !== undefined ||
495
- flexShrink !== undefined ||
496
- flexBasis !== undefined;
497
-
498
- const animatedStyle: any = shouldFillInner
499
- ? { ...innerStyle, flex: 1 }
500
- : { ...innerStyle };
501
-
502
- if (hasFadeAnimation) {
503
- const staticOpacity = animatedStyle.opacity;
504
- if (staticOpacity !== undefined && staticOpacity !== 1) {
505
- animatedStyle.opacity = Animated.multiply(animOpacity, staticOpacity);
506
- } else {
507
- animatedStyle.opacity = animOpacity;
508
- }
509
- }
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
+ }
510
585
 
511
- if (hasScaleAnimation) {
512
- const existingTransform = animatedStyle.transform || [];
513
- animatedStyle.transform = [...existingTransform, { scale: animScale }];
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);
514
602
  }
603
+ }
604
+ if (transforms.length > 0) {
605
+ animatedStyle.transform = transforms;
606
+ }
515
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) {
516
621
  return (
517
622
  <Pressable
518
623
  onPress={handlePress}
519
624
  onPressIn={onPressIn}
520
625
  onPressOut={onPressOut}
521
626
  style={outerStyle}
627
+ onLayout={outerNeedsOnLayout ? onLayoutMeasure : undefined}
522
628
  testID={testID}
523
629
  >
524
- <Animated.View style={animatedStyle}>
525
- <Background layers={background as any} style={borderStyle} />
526
- {children}
527
- </Animated.View>
630
+ {content}
528
631
  </Pressable>
529
632
  );
530
633
  }
531
634
 
635
+ return (
636
+ <View
637
+ style={outerStyle}
638
+ onLayout={outerNeedsOnLayout ? onLayoutMeasure : undefined}
639
+ testID={testID}
640
+ >
641
+ {content}
642
+ </View>
643
+ );
644
+ }
645
+
646
+ if (hasActions) {
532
647
  return (
533
648
  <Pressable onPress={handlePress} style={finalStyle} testID={testID}>
534
649
  <Background layers={background as any} style={borderStyle} />
@@ -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
+ }