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.
- package/lib/commonjs/hooks/use-inline-auto-width.js +105 -18
- package/lib/commonjs/hooks/use-inline-auto-width.js.map +1 -1
- package/lib/commonjs/hooks/use-text-glyphs.js +24 -21
- package/lib/commonjs/hooks/use-text-glyphs.js.map +1 -1
- package/lib/commonjs/index.js +6 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/view/glyph-run.js +38 -19
- package/lib/commonjs/view/glyph-run.js.map +1 -1
- package/lib/commonjs/view/morph-viewport.js +2 -0
- package/lib/commonjs/view/morph-viewport.js.map +1 -1
- package/lib/commonjs/view/text-run.js +34 -12
- package/lib/commonjs/view/text-run.js.map +1 -1
- package/lib/module/hooks/use-inline-auto-width.js +106 -19
- package/lib/module/hooks/use-inline-auto-width.js.map +1 -1
- package/lib/module/hooks/use-text-glyphs.js +24 -21
- package/lib/module/hooks/use-text-glyphs.js.map +1 -1
- package/lib/module/index.js +6 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/view/glyph-run.js +39 -20
- package/lib/module/view/glyph-run.js.map +1 -1
- package/lib/module/view/morph-viewport.js +2 -0
- package/lib/module/view/morph-viewport.js.map +1 -1
- package/lib/module/view/text-run.js +34 -12
- package/lib/module/view/text-run.js.map +1 -1
- package/lib/typescript/commonjs/hooks/use-inline-auto-width.d.ts +2 -0
- package/lib/typescript/commonjs/hooks/use-inline-auto-width.d.ts.map +1 -1
- package/lib/typescript/commonjs/hooks/use-text-glyphs.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/view/glyph-run.d.ts +3 -1
- package/lib/typescript/commonjs/view/glyph-run.d.ts.map +1 -1
- package/lib/typescript/commonjs/view/morph-viewport.d.ts +3 -2
- package/lib/typescript/commonjs/view/morph-viewport.d.ts.map +1 -1
- package/lib/typescript/commonjs/view/text-run.d.ts +3 -1
- package/lib/typescript/commonjs/view/text-run.d.ts.map +1 -1
- package/lib/typescript/module/hooks/use-inline-auto-width.d.ts +2 -0
- package/lib/typescript/module/hooks/use-inline-auto-width.d.ts.map +1 -1
- package/lib/typescript/module/hooks/use-text-glyphs.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/view/glyph-run.d.ts +3 -1
- package/lib/typescript/module/view/glyph-run.d.ts.map +1 -1
- package/lib/typescript/module/view/morph-viewport.d.ts +3 -2
- package/lib/typescript/module/view/morph-viewport.d.ts.map +1 -1
- package/lib/typescript/module/view/text-run.d.ts +3 -1
- package/lib/typescript/module/view/text-run.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/hooks/use-inline-auto-width.ts +160 -18
- package/src/hooks/use-text-glyphs.ts +35 -28
- package/src/index.tsx +11 -1
- package/src/view/glyph-run.tsx +62 -33
- package/src/view/morph-viewport.tsx +7 -2
- package/src/view/text-run.tsx +59 -15
package/src/view/glyph-run.tsx
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
{
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
96
|
+
<Animated.View
|
|
97
|
+
onLayout={onVisibleLayout}
|
|
98
|
+
style={[resolvedViewportStyle, animatedWidthStyle]}
|
|
99
|
+
>
|
|
95
100
|
{children}
|
|
96
101
|
</Animated.View>
|
|
97
102
|
</>
|
package/src/view/text-run.tsx
CHANGED
|
@@ -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
|
|
39
|
-
const
|
|
40
|
-
const
|
|
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(
|
|
74
|
+
Boolean(visibleLeading) &&
|
|
44
75
|
lastLeadingPresenceRef.current &&
|
|
45
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
Boolean(
|
|
80
|
-
|
|
118
|
+
visibleValue !== lastValueRef.current ||
|
|
119
|
+
Boolean(visibleLeading) !== lastLeadingPresenceRef.current ||
|
|
120
|
+
visibleLeadingKey !== lastLeadingKeyRef.current
|
|
81
121
|
) {
|
|
82
122
|
hasAnimatedRef.current = true;
|
|
83
|
-
lastValueRef.current =
|
|
84
|
-
lastLeadingPresenceRef.current = Boolean(
|
|
85
|
-
lastLeadingKeyRef.current =
|
|
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={
|
|
150
|
+
leadingGap={visibleLeadingGap}
|
|
107
151
|
align={align}
|
|
108
152
|
textStyle={textStyle}
|
|
109
153
|
className={className}
|