react-native-laminar 1.4.2 → 1.4.4

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 (51) hide show
  1. package/lib/commonjs/hooks/use-inline-auto-width.js +105 -18
  2. package/lib/commonjs/hooks/use-inline-auto-width.js.map +1 -1
  3. package/lib/commonjs/hooks/use-text-glyphs.js +24 -21
  4. package/lib/commonjs/hooks/use-text-glyphs.js.map +1 -1
  5. package/lib/commonjs/index.js +6 -1
  6. package/lib/commonjs/index.js.map +1 -1
  7. package/lib/commonjs/view/glyph-run.js +38 -19
  8. package/lib/commonjs/view/glyph-run.js.map +1 -1
  9. package/lib/commonjs/view/morph-viewport.js +2 -0
  10. package/lib/commonjs/view/morph-viewport.js.map +1 -1
  11. package/lib/commonjs/view/text-run.js +34 -12
  12. package/lib/commonjs/view/text-run.js.map +1 -1
  13. package/lib/module/hooks/use-inline-auto-width.js +106 -19
  14. package/lib/module/hooks/use-inline-auto-width.js.map +1 -1
  15. package/lib/module/hooks/use-text-glyphs.js +24 -21
  16. package/lib/module/hooks/use-text-glyphs.js.map +1 -1
  17. package/lib/module/index.js +6 -1
  18. package/lib/module/index.js.map +1 -1
  19. package/lib/module/view/glyph-run.js +39 -20
  20. package/lib/module/view/glyph-run.js.map +1 -1
  21. package/lib/module/view/morph-viewport.js +2 -0
  22. package/lib/module/view/morph-viewport.js.map +1 -1
  23. package/lib/module/view/text-run.js +34 -12
  24. package/lib/module/view/text-run.js.map +1 -1
  25. package/lib/typescript/commonjs/hooks/use-inline-auto-width.d.ts +2 -0
  26. package/lib/typescript/commonjs/hooks/use-inline-auto-width.d.ts.map +1 -1
  27. package/lib/typescript/commonjs/hooks/use-text-glyphs.d.ts.map +1 -1
  28. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  29. package/lib/typescript/commonjs/view/glyph-run.d.ts +3 -1
  30. package/lib/typescript/commonjs/view/glyph-run.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/view/morph-viewport.d.ts +3 -2
  32. package/lib/typescript/commonjs/view/morph-viewport.d.ts.map +1 -1
  33. package/lib/typescript/commonjs/view/text-run.d.ts +3 -1
  34. package/lib/typescript/commonjs/view/text-run.d.ts.map +1 -1
  35. package/lib/typescript/module/hooks/use-inline-auto-width.d.ts +2 -0
  36. package/lib/typescript/module/hooks/use-inline-auto-width.d.ts.map +1 -1
  37. package/lib/typescript/module/hooks/use-text-glyphs.d.ts.map +1 -1
  38. package/lib/typescript/module/index.d.ts.map +1 -1
  39. package/lib/typescript/module/view/glyph-run.d.ts +3 -1
  40. package/lib/typescript/module/view/glyph-run.d.ts.map +1 -1
  41. package/lib/typescript/module/view/morph-viewport.d.ts +3 -2
  42. package/lib/typescript/module/view/morph-viewport.d.ts.map +1 -1
  43. package/lib/typescript/module/view/text-run.d.ts +3 -1
  44. package/lib/typescript/module/view/text-run.d.ts.map +1 -1
  45. package/package.json +1 -1
  46. package/src/hooks/use-inline-auto-width.ts +160 -18
  47. package/src/hooks/use-text-glyphs.ts +35 -28
  48. package/src/index.tsx +11 -1
  49. package/src/view/glyph-run.tsx +62 -33
  50. package/src/view/morph-viewport.tsx +7 -2
  51. package/src/view/text-run.tsx +59 -15
@@ -13,6 +13,12 @@ const rowStyle = {
13
13
  alignSelf: "flex-start",
14
14
  } as const;
15
15
 
16
+ const textRowStyle = {
17
+ flexDirection: "row",
18
+ alignItems: "center",
19
+ alignSelf: "flex-start",
20
+ } as const;
21
+
16
22
  const rowAlignStyles = {
17
23
  left: { alignSelf: "flex-start" },
18
24
  center: { alignSelf: "center" },
@@ -24,6 +30,8 @@ export const GlyphRun = React.memo(
24
30
  ({
25
31
  glyphs,
26
32
  layoutTransition,
33
+ leadingLayoutTransition,
34
+ leadingLayoutGroup = false,
27
35
  enterTransition,
28
36
  elementEnterTransition,
29
37
  exitTransition,
@@ -35,6 +43,8 @@ export const GlyphRun = React.memo(
35
43
  }: Readonly<{
36
44
  glyphs: readonly GlyphToken[];
37
45
  layoutTransition?: ComplexAnimationBuilder;
46
+ leadingLayoutTransition?: ComplexAnimationBuilder;
47
+ leadingLayoutGroup?: boolean;
38
48
  enterTransition?: EntryExitAnimationFunction;
39
49
  elementEnterTransition?: EntryExitAnimationFunction;
40
50
  exitTransition?: EntryExitAnimationFunction;
@@ -43,41 +53,60 @@ export const GlyphRun = React.memo(
43
53
  align: LaminarAlign;
44
54
  textStyle?: StyleProp<TextStyle>;
45
55
  className?: string;
46
- }>) => (
47
- // each token keeps its own view so reanimated can animate only changed content
56
+ }>) => {
57
+ // The leading token is always first. Keep it outside the text row so its
58
+ // appearance moves the text as one unit instead of reflowing every glyph.
59
+ const leadingGlyph = glyphs[0]?.kind === "element" ? glyphs[0] : undefined;
60
+ const textGlyphs = leadingGlyph ? glyphs.slice(1) : glyphs;
61
+ const shouldGroupText =
62
+ Boolean(leadingGlyph) || Boolean(leadingLayoutTransition) || leadingLayoutGroup;
63
+
64
+ const renderTextGlyph = (glyph: GlyphToken) =>
65
+ glyph.kind === "text" ? (
66
+ <Animated.Text
67
+ key={glyph.id}
68
+ layout={layoutTransition}
69
+ entering={enterTransition}
70
+ exiting={exitTransition}
71
+ style={textStyle}
72
+ className={className}
73
+ >
74
+ {glyph.value}
75
+ </Animated.Text>
76
+ ) : null;
77
+
78
+ const textRow = (
79
+ <Animated.View
80
+ key="text-row"
81
+ layout={leadingLayoutTransition}
82
+ style={textRowStyle}
83
+ >
84
+ {textGlyphs.map(renderTextGlyph)}
85
+ </Animated.View>
86
+ );
87
+
88
+ return (
48
89
  <View style={[rowStyle, rowAlignStyles[align]]}>
49
- {/* glyph ids decide what swaps, layout handles the row reflow */}
50
- {glyphs.map((glyph, glyphIndex) =>
51
- glyph.kind === "element" ? (
52
- <Animated.View
53
- key={glyph.id}
54
- layout={layoutTransition}
55
- entering={elementEnterTransition ?? enterTransition}
56
- exiting={elementExitTransition ?? exitTransition}
57
- style={[
58
- { alignItems: "center", justifyContent: "center" },
59
- glyphIndex === 0 && (leadingGap ?? 0) > 0
60
- ? { marginRight: leadingGap }
61
- : undefined,
62
- ]}
63
- >
64
- {glyph.element}
65
- </Animated.View>
66
- ) : (
67
- <Animated.Text
68
- key={glyph.id}
69
- layout={layoutTransition}
70
- entering={enterTransition}
71
- exiting={exitTransition}
72
- style={textStyle}
73
- className={className}
74
- >
75
- {glyph.value}
76
- </Animated.Text>
77
- )
78
- )}
90
+ {leadingGlyph ? (
91
+ <Animated.View
92
+ key={leadingGlyph.id}
93
+ // leading elements own their enter/exit geometry; generic layout
94
+ // motion here would compete with those transitions
95
+ layout={undefined}
96
+ entering={elementEnterTransition ?? enterTransition}
97
+ exiting={elementExitTransition ?? exitTransition}
98
+ style={[
99
+ { alignItems: "center", justifyContent: "center" },
100
+ (leadingGap ?? 0) > 0 ? { marginRight: leadingGap } : undefined,
101
+ ]}
102
+ >
103
+ {leadingGlyph.element}
104
+ </Animated.View>
105
+ ) : null}
106
+ {shouldGroupText ? textRow : textGlyphs.map(renderTextGlyph)}
79
107
  </View>
80
- )
108
+ );
109
+ }
81
110
  );
82
111
 
83
112
  GlyphRun.displayName = "GlyphRun";
@@ -1,5 +1,5 @@
1
1
  import React, { useMemo } from "react";
2
- import type { StyleProp, ViewStyle } from "react-native";
2
+ import type { LayoutChangeEvent, StyleProp, ViewStyle } from "react-native";
3
3
  import { View } from "react-native";
4
4
  import Animated from "react-native-reanimated";
5
5
  import type { LaminarAlign } from "../types";
@@ -52,6 +52,7 @@ type MorphViewportProps = {
52
52
  readonly align: LaminarAlign;
53
53
  readonly containerStyle?: StyleProp<ViewStyle>;
54
54
  readonly animatedWidthStyle?: React.ComponentProps<typeof Animated.View>["style"];
55
+ readonly onVisibleLayout?: (event: LayoutChangeEvent) => void;
55
56
  readonly measurement?: React.ReactNode;
56
57
  readonly children: React.ReactNode;
57
58
  };
@@ -64,6 +65,7 @@ export const MorphViewport = React.memo(
64
65
  align,
65
66
  containerStyle,
66
67
  animatedWidthStyle,
68
+ onVisibleLayout,
67
69
  measurement,
68
70
  children,
69
71
  }: MorphViewportProps) => {
@@ -91,7 +93,10 @@ export const MorphViewport = React.memo(
91
93
  >
92
94
  {measurement}
93
95
  </View>
94
- <Animated.View style={[resolvedViewportStyle, animatedWidthStyle]}>
96
+ <Animated.View
97
+ onLayout={onVisibleLayout}
98
+ style={[resolvedViewportStyle, animatedWidthStyle]}
99
+ >
95
100
  {children}
96
101
  </Animated.View>
97
102
  </>
@@ -15,8 +15,10 @@ type TextRunProps = {
15
15
  readonly motionRecipe: MotionRecipe;
16
16
  readonly align: LaminarAlign;
17
17
  readonly leading?: ReactNode;
18
+ readonly leadingLayoutGroup?: boolean;
18
19
  readonly leadingKey?: string | number;
19
20
  readonly leadingGap?: number;
21
+ readonly ready?: boolean;
20
22
  readonly textStyle?: StyleProp<TextStyle>;
21
23
  readonly className?: string;
22
24
  };
@@ -28,27 +30,65 @@ export const TextRun = React.memo(
28
30
  motionRecipe,
29
31
  align,
30
32
  leading,
33
+ leadingLayoutGroup = false,
31
34
  leadingKey,
32
35
  leadingGap = 0,
36
+ ready = true,
33
37
  textStyle,
34
38
  className,
35
39
  }: TextRunProps) => {
36
40
  // namespace ids per instance so repeated strings do not collide
37
41
  const scopeId = useId();
38
- const lastValueRef = useRef(value);
39
- const lastLeadingPresenceRef = useRef(Boolean(leading));
40
- const lastLeadingKeyRef = useRef(leadingKey);
42
+ const hasDisplayedRef = useRef(false);
43
+ const displayedValueRef = useRef(value);
44
+ const displayedLeadingRef = useRef(leading);
45
+ const displayedLeadingKeyRef = useRef(leadingKey);
46
+ const displayedLeadingGapRef = useRef(leadingGap);
47
+ const shouldDisplayTarget = ready || !hasDisplayedRef.current;
48
+ const visibleValue = shouldDisplayTarget
49
+ ? value
50
+ : displayedValueRef.current;
51
+ const visibleLeading = shouldDisplayTarget
52
+ ? leading
53
+ : displayedLeadingRef.current;
54
+ const visibleLeadingKey = shouldDisplayTarget
55
+ ? leadingKey
56
+ : displayedLeadingKeyRef.current;
57
+ const visibleLeadingGap = shouldDisplayTarget
58
+ ? leadingGap
59
+ : displayedLeadingGapRef.current;
60
+
61
+ if (shouldDisplayTarget) {
62
+ hasDisplayedRef.current = true;
63
+ displayedValueRef.current = value;
64
+ displayedLeadingRef.current = leading;
65
+ displayedLeadingKeyRef.current = leadingKey;
66
+ displayedLeadingGapRef.current = leadingGap;
67
+ }
68
+
69
+ const lastValueRef = useRef(visibleValue);
70
+ const lastLeadingPresenceRef = useRef(Boolean(visibleLeading));
71
+ const lastLeadingKeyRef = useRef(visibleLeadingKey);
41
72
  const hasAnimatedRef = useRef(false);
42
73
  const isLeadingSwap =
43
- Boolean(leading) &&
74
+ Boolean(visibleLeading) &&
44
75
  lastLeadingPresenceRef.current &&
45
- leadingKey !== lastLeadingKeyRef.current;
76
+ visibleLeadingKey !== lastLeadingKeyRef.current;
77
+ const isLeadingChange =
78
+ Boolean(visibleLeading) !== lastLeadingPresenceRef.current ||
79
+ (Boolean(visibleLeading) &&
80
+ visibleLeadingKey !== lastLeadingKeyRef.current);
46
81
  // update the worklet flag after commit so render never writes a shared value
47
82
  const leadingSwapProgress = useSharedValue(0);
48
83
  useLayoutEffect(() => {
49
84
  leadingSwapProgress.value = isLeadingSwap ? 1 : 0;
50
85
  }, [isLeadingSwap, leadingSwapProgress]);
51
- const glyphs = useTextGlyphs(value, scopeId, leading, leadingKey);
86
+ const glyphs = useTextGlyphs(
87
+ visibleValue,
88
+ scopeId,
89
+ visibleLeading,
90
+ visibleLeadingKey
91
+ );
52
92
  const elementEnterTransition = useMemo(
53
93
  () =>
54
94
  createLeadingEnterTransition({
@@ -62,11 +102,11 @@ export const TextRun = React.memo(
62
102
  createLeadingExitTransition({
63
103
  durationMs: motionRecipe.durationMs,
64
104
  easing: motionRecipe.easing,
65
- leadingGap,
105
+ leadingGap: visibleLeadingGap,
66
106
  scale: leadingSwapProgress,
67
107
  }),
68
108
  [
69
- leadingGap,
109
+ visibleLeadingGap,
70
110
  leadingSwapProgress,
71
111
  motionRecipe.durationMs,
72
112
  motionRecipe.easing,
@@ -75,14 +115,14 @@ export const TextRun = React.memo(
75
115
 
76
116
  // mark the first value as settled and later changes as eligible for motion
77
117
  if (
78
- value !== lastValueRef.current ||
79
- Boolean(leading) !== lastLeadingPresenceRef.current ||
80
- leadingKey !== lastLeadingKeyRef.current
118
+ visibleValue !== lastValueRef.current ||
119
+ Boolean(visibleLeading) !== lastLeadingPresenceRef.current ||
120
+ visibleLeadingKey !== lastLeadingKeyRef.current
81
121
  ) {
82
122
  hasAnimatedRef.current = true;
83
- lastValueRef.current = value;
84
- lastLeadingPresenceRef.current = Boolean(leading);
85
- lastLeadingKeyRef.current = leadingKey;
123
+ lastValueRef.current = visibleValue;
124
+ lastLeadingPresenceRef.current = Boolean(visibleLeading);
125
+ lastLeadingKeyRef.current = visibleLeadingKey;
86
126
  }
87
127
 
88
128
  const hasAnimated = hasAnimatedRef.current;
@@ -93,6 +133,10 @@ export const TextRun = React.memo(
93
133
  layoutTransition={
94
134
  hasAnimated ? motionRecipe.layoutTransition : undefined
95
135
  }
136
+ leadingLayoutTransition={
137
+ isLeadingChange ? motionRecipe.layoutTransition : undefined
138
+ }
139
+ leadingLayoutGroup={leadingLayoutGroup}
96
140
  enterTransition={
97
141
  hasAnimatedRef.current
98
142
  ? motionRecipe.enterTransition
@@ -103,7 +147,7 @@ export const TextRun = React.memo(
103
147
  }
104
148
  exitTransition={motionRecipe.exitTransition}
105
149
  elementExitTransition={elementExitTransition}
106
- leadingGap={leadingGap}
150
+ leadingGap={visibleLeadingGap}
107
151
  align={align}
108
152
  textStyle={textStyle}
109
153
  className={className}