panelui-native 0.13.0 → 0.16.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 +41 -23
- package/lib/module/components/button/index.js +12 -9
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/chip/index.js +313 -0
- package/lib/module/components/chip/index.js.map +1 -0
- package/lib/module/components/direction/index.js +86 -0
- package/lib/module/components/direction/index.js.map +1 -0
- package/lib/module/components/frame/index.js +41 -39
- package/lib/module/components/frame/index.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +772 -0
- package/lib/module/components/heatmap-chart/index.js.map +1 -0
- package/lib/module/components/input/index.js +4 -0
- package/lib/module/components/input/index.js.map +1 -1
- package/lib/module/components/message/index.js +16 -1
- package/lib/module/components/message/index.js.map +1 -1
- package/lib/module/components/scroll-canvas/index.js +175 -0
- package/lib/module/components/scroll-canvas/index.js.map +1 -0
- package/lib/module/components/scroll-text/index.js +213 -0
- package/lib/module/components/scroll-text/index.js.map +1 -0
- package/lib/module/components/select/index.js +20 -24
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/slider/index.js +2 -5
- package/lib/module/components/slider/index.js.map +1 -1
- package/lib/module/components/soundwave/index.js +813 -0
- package/lib/module/components/soundwave/index.js.map +1 -0
- package/lib/module/components/switch/index.js +15 -17
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/components/thinking-orb/index.js +741 -0
- package/lib/module/components/thinking-orb/index.js.map +1 -0
- package/lib/module/components/toggle-button/index.js +12 -3
- package/lib/module/components/toggle-button/index.js.map +1 -1
- package/lib/module/components/typography/index.js +196 -5
- package/lib/module/components/typography/index.js.map +1 -1
- package/lib/module/hooks/index.js +1 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/use-keyboard-avoidance.js +99 -59
- package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
- package/lib/module/hooks/use-reveal-progress.js +75 -0
- package/lib/module/hooks/use-reveal-progress.js.map +1 -0
- package/lib/module/icons/index.js +69 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +9 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/index.js.map +1 -1
- package/lib/module/primitives/keyboard-avoider.js +21 -2
- package/lib/module/primitives/keyboard-avoider.js.map +1 -1
- package/lib/module/primitives/scroll-progress.js +114 -0
- package/lib/module/primitives/scroll-progress.js.map +1 -0
- package/lib/typescript/src/components/button/index.d.ts +3 -4
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/chip/index.d.ts +304 -0
- package/lib/typescript/src/components/chip/index.d.ts.map +1 -0
- package/lib/typescript/src/components/direction/index.d.ts +64 -0
- package/lib/typescript/src/components/direction/index.d.ts.map +1 -0
- package/lib/typescript/src/components/frame/index.d.ts +15 -13
- package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts +260 -0
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/input/index.d.ts +19 -4
- package/lib/typescript/src/components/input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/message/index.d.ts.map +1 -1
- package/lib/typescript/src/components/scroll-canvas/index.d.ts +66 -0
- package/lib/typescript/src/components/scroll-canvas/index.d.ts.map +1 -0
- package/lib/typescript/src/components/scroll-text/index.d.ts +46 -0
- package/lib/typescript/src/components/scroll-text/index.d.ts.map +1 -0
- package/lib/typescript/src/components/select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
- package/lib/typescript/src/components/soundwave/index.d.ts +91 -0
- package/lib/typescript/src/components/soundwave/index.d.ts.map +1 -0
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/components/thinking-orb/index.d.ts +28 -0
- package/lib/typescript/src/components/thinking-orb/index.d.ts.map +1 -0
- package/lib/typescript/src/components/toggle-button/index.d.ts +12 -0
- package/lib/typescript/src/components/toggle-button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/typography/index.d.ts +154 -1
- package/lib/typescript/src/components/typography/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/index.d.ts +2 -1
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +16 -3
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-reveal-progress.d.ts +56 -0
- package/lib/typescript/src/hooks/use-reveal-progress.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +6 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +9 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native/index.d.ts +15 -7
- package/lib/typescript/src/native/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts +30 -3
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -1
- package/lib/typescript/src/primitives/scroll-progress.d.ts +61 -0
- package/lib/typescript/src/primitives/scroll-progress.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/button/index.tsx +13 -11
- package/src/components/direction/index.tsx +90 -0
- package/src/components/frame/index.tsx +39 -50
- package/src/components/heatmap-chart/index.tsx +1024 -0
- package/src/components/input/index.tsx +23 -4
- package/src/components/message/index.tsx +17 -1
- package/src/components/scroll-canvas/index.tsx +220 -0
- package/src/components/scroll-text/index.tsx +277 -0
- package/src/components/select/index.tsx +4 -9
- package/src/components/slider/index.tsx +4 -7
- package/src/components/soundwave/index.tsx +1029 -0
- package/src/components/switch/index.tsx +5 -8
- package/src/components/thinking-orb/index.tsx +792 -0
- package/src/components/typography/index.tsx +231 -10
- package/src/hooks/index.ts +6 -0
- package/src/hooks/use-keyboard-avoidance.ts +111 -65
- package/src/hooks/use-reveal-progress.ts +106 -0
- package/src/icons/index.tsx +36 -0
- package/src/index.ts +53 -0
- package/src/native/index.ts +15 -7
- package/src/primitives/keyboard-avoider.tsx +41 -4
- package/src/primitives/scroll-progress.tsx +167 -0
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Typography — semantic text presets.
|
|
2
|
+
* Typography — semantic text presets, and the marks that go on them.
|
|
3
3
|
*
|
|
4
4
|
* The type scale in one place, so headings stay consistent instead of being
|
|
5
5
|
* rebuilt out of size and weight classes at each call site. Built on the Text
|
|
6
6
|
* primitive, so every preset keeps `className` passthrough and theme colours.
|
|
7
|
+
*
|
|
8
|
+
* ```tsx
|
|
9
|
+
* <Typography type="h2">Billing</Typography>
|
|
10
|
+
* <Typography>Your plan renews on the 1st.</Typography>
|
|
11
|
+
* <Typography underline>Terms of service</Typography>
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* A preset sets size, weight and tracking together; the marks — `underline`,
|
|
15
|
+
* `italic`, `strike` — and `weight`, `align` and `transform` layer on top of
|
|
16
|
+
* whichever preset is in force. They are props rather than class names because
|
|
17
|
+
* the point of this component is that a screen never has to know which
|
|
18
|
+
* utilities add up to "a bolded lead paragraph".
|
|
19
|
+
*
|
|
20
|
+
* React Native draws no list markers and has no blockquote, so
|
|
21
|
+
* `Typography.List` and `Typography.Blockquote` build both out of a row and a
|
|
22
|
+
* rule — which is exactly the kind of thing this component exists to stop
|
|
23
|
+
* people rebuilding per screen.
|
|
7
24
|
*/
|
|
8
|
-
import { forwardRef } from 'react';
|
|
25
|
+
import { Children, forwardRef, type ReactNode } from 'react';
|
|
9
26
|
import { View, type Text as RNText, type ViewProps } from 'react-native';
|
|
10
27
|
import { tv, type VariantProps } from 'tailwind-variants';
|
|
11
28
|
import { Text, type TextProps } from '../../primitives/text';
|
|
29
|
+
import { cn } from '../../utils/cn';
|
|
12
30
|
|
|
13
31
|
const typographyVariants = tv({
|
|
14
32
|
base: 'text-foreground',
|
|
@@ -20,11 +38,43 @@ const typographyVariants = tv({
|
|
|
20
38
|
h4: 'text-xl font-semibold',
|
|
21
39
|
h5: 'text-lg font-semibold',
|
|
22
40
|
h6: 'text-base font-semibold',
|
|
41
|
+
/** The sentence under a heading, set larger and quieter than body. */
|
|
42
|
+
lead: 'text-xl font-normal text-muted-foreground',
|
|
23
43
|
body: 'text-base font-normal',
|
|
24
44
|
'body-sm': 'text-sm font-normal',
|
|
25
45
|
'body-xs': 'text-xs font-normal',
|
|
46
|
+
/** Body, one step up — for a number or a name that carries the row. */
|
|
47
|
+
large: 'text-lg font-semibold',
|
|
48
|
+
/** Body, one step down and tighter — captions, footnotes, meta. */
|
|
49
|
+
small: 'text-sm font-medium leading-none',
|
|
50
|
+
blockquote: 'text-base font-normal italic',
|
|
26
51
|
code: 'font-mono text-sm text-foreground',
|
|
27
52
|
},
|
|
53
|
+
weight: {
|
|
54
|
+
normal: 'font-normal',
|
|
55
|
+
medium: 'font-medium',
|
|
56
|
+
semibold: 'font-semibold',
|
|
57
|
+
bold: 'font-bold',
|
|
58
|
+
},
|
|
59
|
+
align: {
|
|
60
|
+
left: 'text-left',
|
|
61
|
+
center: 'text-center',
|
|
62
|
+
right: 'text-right',
|
|
63
|
+
},
|
|
64
|
+
transform: {
|
|
65
|
+
uppercase: 'uppercase',
|
|
66
|
+
lowercase: 'lowercase',
|
|
67
|
+
capitalize: 'capitalize',
|
|
68
|
+
},
|
|
69
|
+
underline: {
|
|
70
|
+
true: 'underline',
|
|
71
|
+
},
|
|
72
|
+
italic: {
|
|
73
|
+
true: 'italic',
|
|
74
|
+
},
|
|
75
|
+
strike: {
|
|
76
|
+
true: 'line-through',
|
|
77
|
+
},
|
|
28
78
|
muted: {
|
|
29
79
|
true: 'text-muted-foreground',
|
|
30
80
|
},
|
|
@@ -38,16 +88,38 @@ export type TypographyType = NonNullable<
|
|
|
38
88
|
VariantProps<typeof typographyVariants>['type']
|
|
39
89
|
>;
|
|
40
90
|
|
|
91
|
+
export type TypographyWeight = NonNullable<
|
|
92
|
+
VariantProps<typeof typographyVariants>['weight']
|
|
93
|
+
>;
|
|
94
|
+
|
|
41
95
|
export interface TypographyProps
|
|
42
96
|
extends Omit<TextProps, 'size' | 'weight'>,
|
|
43
97
|
VariantProps<typeof typographyVariants> {
|
|
44
98
|
className?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Overrides the weight the preset sets. This is the one to reach for when a
|
|
101
|
+
* paragraph needs a bolded run and a heading would be wrong.
|
|
102
|
+
*/
|
|
103
|
+
weight?: TypographyWeight;
|
|
104
|
+
/** Underlines the text — a link, a defined term, a signature line. */
|
|
105
|
+
underline?: boolean;
|
|
106
|
+
/** Slants the text. */
|
|
107
|
+
italic?: boolean;
|
|
108
|
+
/** Strikes the text through: an old price, a completed task. */
|
|
109
|
+
strike?: boolean;
|
|
110
|
+
/** Horizontal alignment within whatever the text is laid out in. */
|
|
111
|
+
align?: 'left' | 'center' | 'right';
|
|
112
|
+
/** Case, applied for display without changing the string underneath. */
|
|
113
|
+
transform?: 'uppercase' | 'lowercase' | 'capitalize';
|
|
45
114
|
}
|
|
46
115
|
|
|
47
116
|
/** Heading levels, for `Typography.Heading`. */
|
|
48
117
|
type HeadingType = Extract<TypographyType, 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'>;
|
|
49
118
|
/** Body sizes, for `Typography.Paragraph`. */
|
|
50
|
-
type ParagraphType = Extract<
|
|
119
|
+
type ParagraphType = Extract<
|
|
120
|
+
TypographyType,
|
|
121
|
+
'body' | 'body-sm' | 'body-xs' | 'lead' | 'large' | 'small'
|
|
122
|
+
>;
|
|
51
123
|
|
|
52
124
|
/** Maps a heading preset to its accessibility heading level. */
|
|
53
125
|
const HEADING_LEVEL: Record<HeadingType, number> = {
|
|
@@ -60,8 +132,25 @@ const HEADING_LEVEL: Record<HeadingType, number> = {
|
|
|
60
132
|
};
|
|
61
133
|
|
|
62
134
|
const TypographyRoot = forwardRef<RNText, TypographyProps>(
|
|
63
|
-
(
|
|
64
|
-
|
|
135
|
+
(
|
|
136
|
+
{ className, type, muted, weight, align, transform, underline, italic, strike, ...props },
|
|
137
|
+
ref
|
|
138
|
+
) => (
|
|
139
|
+
<Text
|
|
140
|
+
ref={ref}
|
|
141
|
+
className={typographyVariants({
|
|
142
|
+
type,
|
|
143
|
+
muted,
|
|
144
|
+
weight,
|
|
145
|
+
align,
|
|
146
|
+
transform,
|
|
147
|
+
underline,
|
|
148
|
+
italic,
|
|
149
|
+
strike,
|
|
150
|
+
className,
|
|
151
|
+
})}
|
|
152
|
+
{...props}
|
|
153
|
+
/>
|
|
65
154
|
)
|
|
66
155
|
);
|
|
67
156
|
TypographyRoot.displayName = 'Typography';
|
|
@@ -72,12 +161,25 @@ export interface TypographyHeadingProps extends Omit<TypographyProps, 'type'> {
|
|
|
72
161
|
|
|
73
162
|
/** Heading text, wired up with the matching accessibility heading level. */
|
|
74
163
|
const TypographyHeading = forwardRef<RNText, TypographyHeadingProps>(
|
|
75
|
-
(
|
|
164
|
+
(
|
|
165
|
+
{ className, type = 'h2', muted, weight, align, transform, underline, italic, strike, ...props },
|
|
166
|
+
ref
|
|
167
|
+
) => (
|
|
76
168
|
<Text
|
|
77
169
|
ref={ref}
|
|
78
170
|
accessibilityRole="header"
|
|
79
171
|
aria-level={HEADING_LEVEL[type]}
|
|
80
|
-
className={typographyVariants({
|
|
172
|
+
className={typographyVariants({
|
|
173
|
+
type,
|
|
174
|
+
muted,
|
|
175
|
+
weight,
|
|
176
|
+
align,
|
|
177
|
+
transform,
|
|
178
|
+
underline,
|
|
179
|
+
italic,
|
|
180
|
+
strike,
|
|
181
|
+
className,
|
|
182
|
+
})}
|
|
81
183
|
{...props}
|
|
82
184
|
/>
|
|
83
185
|
)
|
|
@@ -89,8 +191,25 @@ export interface TypographyParagraphProps extends Omit<TypographyProps, 'type'>
|
|
|
89
191
|
}
|
|
90
192
|
|
|
91
193
|
const TypographyParagraph = forwardRef<RNText, TypographyParagraphProps>(
|
|
92
|
-
(
|
|
93
|
-
|
|
194
|
+
(
|
|
195
|
+
{ className, type = 'body', muted, weight, align, transform, underline, italic, strike, ...props },
|
|
196
|
+
ref
|
|
197
|
+
) => (
|
|
198
|
+
<Text
|
|
199
|
+
ref={ref}
|
|
200
|
+
className={typographyVariants({
|
|
201
|
+
type,
|
|
202
|
+
muted,
|
|
203
|
+
weight,
|
|
204
|
+
align,
|
|
205
|
+
transform,
|
|
206
|
+
underline,
|
|
207
|
+
italic,
|
|
208
|
+
strike,
|
|
209
|
+
className,
|
|
210
|
+
})}
|
|
211
|
+
{...props}
|
|
212
|
+
/>
|
|
94
213
|
)
|
|
95
214
|
);
|
|
96
215
|
TypographyParagraph.displayName = 'Typography.Paragraph';
|
|
@@ -106,7 +225,7 @@ const TypographyCode = forwardRef<View, TypographyCodeProps & Pick<ViewProps, 't
|
|
|
106
225
|
<View
|
|
107
226
|
ref={ref}
|
|
108
227
|
testID={testID}
|
|
109
|
-
className={
|
|
228
|
+
className={cn('self-start rounded-md bg-muted px-1.5 py-1', containerClassName)}
|
|
110
229
|
>
|
|
111
230
|
<Text className={typographyVariants({ type: 'code', muted, className })} {...props} />
|
|
112
231
|
</View>
|
|
@@ -114,8 +233,110 @@ const TypographyCode = forwardRef<View, TypographyCodeProps & Pick<ViewProps, 't
|
|
|
114
233
|
);
|
|
115
234
|
TypographyCode.displayName = 'Typography.Code';
|
|
116
235
|
|
|
236
|
+
export interface TypographyBlockquoteProps extends Omit<TypographyProps, 'type'> {
|
|
237
|
+
/** Classes for the row that carries the rule. */
|
|
238
|
+
containerClassName?: string;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* A quotation, marked by a rule down its leading edge.
|
|
243
|
+
*
|
|
244
|
+
* The rule uses `border-s`, so it moves to the right-hand side under a
|
|
245
|
+
* right-to-left `Direction` without the quote having to know.
|
|
246
|
+
*/
|
|
247
|
+
const TypographyBlockquote = forwardRef<View, TypographyBlockquoteProps>(
|
|
248
|
+
({ className, containerClassName, muted, weight, align, transform, underline, italic = true, strike, children, ...props }, ref) => (
|
|
249
|
+
<View ref={ref} className={cn('border-s-2 border-border ps-4', containerClassName)}>
|
|
250
|
+
<Text
|
|
251
|
+
className={typographyVariants({
|
|
252
|
+
type: 'blockquote',
|
|
253
|
+
muted,
|
|
254
|
+
weight,
|
|
255
|
+
align,
|
|
256
|
+
transform,
|
|
257
|
+
underline,
|
|
258
|
+
italic,
|
|
259
|
+
strike,
|
|
260
|
+
className,
|
|
261
|
+
})}
|
|
262
|
+
{...props}
|
|
263
|
+
>
|
|
264
|
+
{children}
|
|
265
|
+
</Text>
|
|
266
|
+
</View>
|
|
267
|
+
)
|
|
268
|
+
);
|
|
269
|
+
TypographyBlockquote.displayName = 'Typography.Blockquote';
|
|
270
|
+
|
|
271
|
+
export interface TypographyListProps extends ViewProps {
|
|
272
|
+
className?: string;
|
|
273
|
+
/** Numbered rather than bulleted. The numbers are drawn, not counted by CSS. */
|
|
274
|
+
ordered?: boolean;
|
|
275
|
+
children?: ReactNode;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* A bulleted or numbered list.
|
|
280
|
+
*
|
|
281
|
+
* React Native has no list markers at all, so each row is a marker and a text
|
|
282
|
+
* block side by side. The marker is drawn here rather than in the item, because
|
|
283
|
+
* only the list knows whether it is a bullet or a number — and only the list
|
|
284
|
+
* knows which number.
|
|
285
|
+
*/
|
|
286
|
+
const TypographyList = forwardRef<View, TypographyListProps>(
|
|
287
|
+
({ className, ordered = false, children, ...props }, ref) => (
|
|
288
|
+
<View ref={ref} accessibilityRole="list" className={cn('gap-2', className)} {...props}>
|
|
289
|
+
{Children.map(children, (child, index) => (
|
|
290
|
+
<View className="w-full flex-row gap-2">
|
|
291
|
+
{ordered ? (
|
|
292
|
+
<Text className="text-base text-muted-foreground">{index + 1}.</Text>
|
|
293
|
+
) : (
|
|
294
|
+
// A dot rather than "•": the character's size and baseline vary by
|
|
295
|
+
// platform font, and a view does not.
|
|
296
|
+
<View className="mt-2.5 size-1.5 shrink-0 rounded-full bg-muted-foreground" />
|
|
297
|
+
)}
|
|
298
|
+
<View className="flex-1">{child}</View>
|
|
299
|
+
</View>
|
|
300
|
+
))}
|
|
301
|
+
</View>
|
|
302
|
+
)
|
|
303
|
+
);
|
|
304
|
+
TypographyList.displayName = 'Typography.List';
|
|
305
|
+
|
|
306
|
+
export interface TypographyListItemProps extends Omit<TypographyProps, 'type'> {
|
|
307
|
+
type?: ParagraphType;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** One line of a list. The marker beside it belongs to the list. */
|
|
311
|
+
const TypographyListItem = forwardRef<RNText, TypographyListItemProps>(
|
|
312
|
+
(
|
|
313
|
+
{ className, type = 'body', muted, weight, align, transform, underline, italic, strike, ...props },
|
|
314
|
+
ref
|
|
315
|
+
) => (
|
|
316
|
+
<Text
|
|
317
|
+
ref={ref}
|
|
318
|
+
className={typographyVariants({
|
|
319
|
+
type,
|
|
320
|
+
muted,
|
|
321
|
+
weight,
|
|
322
|
+
align,
|
|
323
|
+
transform,
|
|
324
|
+
underline,
|
|
325
|
+
italic,
|
|
326
|
+
strike,
|
|
327
|
+
className,
|
|
328
|
+
})}
|
|
329
|
+
{...props}
|
|
330
|
+
/>
|
|
331
|
+
)
|
|
332
|
+
);
|
|
333
|
+
TypographyListItem.displayName = 'Typography.ListItem';
|
|
334
|
+
|
|
117
335
|
export const Typography = Object.assign(TypographyRoot, {
|
|
118
336
|
Heading: TypographyHeading,
|
|
119
337
|
Paragraph: TypographyParagraph,
|
|
120
338
|
Code: TypographyCode,
|
|
339
|
+
Blockquote: TypographyBlockquote,
|
|
340
|
+
List: TypographyList,
|
|
341
|
+
ListItem: TypographyListItem,
|
|
121
342
|
});
|
package/src/hooks/index.ts
CHANGED
|
@@ -17,9 +17,15 @@ export {
|
|
|
17
17
|
export { useKeyboard, type UseKeyboardResult } from './use-keyboard';
|
|
18
18
|
export {
|
|
19
19
|
useKeyboardAvoidance,
|
|
20
|
+
type KeyboardAvoidanceMode,
|
|
20
21
|
type UseKeyboardAvoidanceOptions,
|
|
21
22
|
type UseKeyboardAvoidanceResult,
|
|
22
23
|
} from './use-keyboard-avoidance';
|
|
24
|
+
export {
|
|
25
|
+
useRevealProgress,
|
|
26
|
+
type UseRevealProgressOptions,
|
|
27
|
+
type UseRevealProgressResult,
|
|
28
|
+
} from './use-reveal-progress';
|
|
23
29
|
export {
|
|
24
30
|
useScrollSections,
|
|
25
31
|
type UseScrollSectionsOptions,
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* useKeyboardAvoidance —
|
|
2
|
+
* useKeyboardAvoidance — keep an element clear of the software keyboard.
|
|
3
3
|
*
|
|
4
4
|
* `KeyboardAvoidingView` shifts or pads an entire subtree by the full keyboard
|
|
5
5
|
* height regardless of where the element actually sits, which over-scrolls
|
|
6
6
|
* short forms and does nothing useful for an element already above the fold.
|
|
7
|
-
* This
|
|
8
|
-
*
|
|
7
|
+
* This works from the element's own position instead.
|
|
8
|
+
*
|
|
9
|
+
* Two modes, because "get out of the keyboard's way" and "ride the keyboard"
|
|
10
|
+
* are different jobs:
|
|
11
|
+
*
|
|
12
|
+
* - **`lift`** (default) — for a field sitting in the page's flow. The element
|
|
13
|
+
* is measured every frame while the keyboard is up and moved by exactly the
|
|
14
|
+
* current overlap, so it follows the scroll: scroll it clear and the lift
|
|
15
|
+
* decays to nothing, scroll it back under and the lift returns. Measuring
|
|
16
|
+
* once and holding the result is what leaves a field hanging out of its own
|
|
17
|
+
* slot the moment the page moves underneath it.
|
|
18
|
+
* - **`dock`** — for an absolutely-positioned composer, toolbar or search bar
|
|
19
|
+
* pinned near the bottom edge. There is nothing to measure: the element
|
|
20
|
+
* simply travels with the keyboard, less whatever bottom inset it is already
|
|
21
|
+
* sitting above.
|
|
9
22
|
*
|
|
10
23
|
* ## Install the keyboard controller
|
|
11
24
|
*
|
|
@@ -31,19 +44,34 @@
|
|
|
31
44
|
* </Animated.View>
|
|
32
45
|
* ```
|
|
33
46
|
*/
|
|
34
|
-
import { useCallback, useEffect } from 'react';
|
|
47
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
35
48
|
import { useWindowDimensions, type LayoutChangeEvent, type View } from 'react-native';
|
|
36
49
|
import {
|
|
37
50
|
measure,
|
|
51
|
+
runOnJS,
|
|
38
52
|
useAnimatedKeyboard,
|
|
39
53
|
useAnimatedReaction,
|
|
40
54
|
useAnimatedRef,
|
|
41
55
|
useAnimatedStyle,
|
|
56
|
+
useFrameCallback,
|
|
42
57
|
useSharedValue,
|
|
58
|
+
withTiming,
|
|
43
59
|
type AnimatedRef,
|
|
44
60
|
type SharedValue,
|
|
45
61
|
} from 'react-native-reanimated';
|
|
46
62
|
|
|
63
|
+
/** How long a lifted element takes to settle back after it stops being active. */
|
|
64
|
+
const SETTLE_DURATION = 200;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Movement below this is dropped. The element's measured position already
|
|
68
|
+
* includes the translation applied on the previous frame, so the loop reads
|
|
69
|
+
* its own output — a dead band keeps sub-pixel rounding from making it hum.
|
|
70
|
+
*/
|
|
71
|
+
const EPSILON = 0.5;
|
|
72
|
+
|
|
73
|
+
export type KeyboardAvoidanceMode = 'lift' | 'dock';
|
|
74
|
+
|
|
47
75
|
export interface UseKeyboardAvoidanceOptions {
|
|
48
76
|
/** Set false to leave the element where it is. */
|
|
49
77
|
enabled?: boolean;
|
|
@@ -56,14 +84,26 @@ export interface UseKeyboardAvoidanceOptions {
|
|
|
56
84
|
* is tapped, and they all arrive at the same place on top of each other.
|
|
57
85
|
*/
|
|
58
86
|
active?: boolean;
|
|
59
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* `lift` moves an in-flow element by its overlap with the keyboard and
|
|
89
|
+
* tracks it as the page scrolls. `dock` travels with the keyboard outright,
|
|
90
|
+
* for an element already pinned to the bottom edge.
|
|
91
|
+
*/
|
|
92
|
+
mode?: KeyboardAvoidanceMode;
|
|
93
|
+
/** Gap to keep between the element's bottom edge and the keyboard. `lift` only. */
|
|
60
94
|
offset?: number;
|
|
95
|
+
/**
|
|
96
|
+
* How far above the bottom edge the element already sits — usually the safe
|
|
97
|
+
* area inset it is offset by. Subtracted from the travel, since the keyboard
|
|
98
|
+
* covers that strip too. `dock` only.
|
|
99
|
+
*/
|
|
100
|
+
bottomInset?: number;
|
|
61
101
|
}
|
|
62
102
|
|
|
63
103
|
export interface UseKeyboardAvoidanceResult {
|
|
64
104
|
/** Attach to the element that should stay visible. */
|
|
65
105
|
ref: AnimatedRef<View>;
|
|
66
|
-
/** Attach to the same element
|
|
106
|
+
/** Attach to the same element, so a re-layout at rest cannot leave it offset. */
|
|
67
107
|
onLayout: (event: LayoutChangeEvent) => void;
|
|
68
108
|
/** Apply to the same element. */
|
|
69
109
|
animatedStyle: ReturnType<typeof useAnimatedStyle>;
|
|
@@ -110,96 +150,102 @@ export function hasKeyboardController(): boolean {
|
|
|
110
150
|
export function useKeyboardAvoidance({
|
|
111
151
|
enabled = true,
|
|
112
152
|
active = true,
|
|
153
|
+
mode = 'lift',
|
|
113
154
|
offset = 16,
|
|
155
|
+
bottomInset = 0,
|
|
114
156
|
}: UseKeyboardAvoidanceOptions = {}): UseKeyboardAvoidanceResult {
|
|
115
157
|
const ref = useAnimatedRef<View>();
|
|
116
158
|
const rawHeight = useKeyboardHeight();
|
|
117
159
|
const { height: screenHeight } = useWindowDimensions();
|
|
118
160
|
|
|
119
|
-
/**
|
|
120
|
-
const
|
|
161
|
+
/** The translation currently applied, in pixels. Zero or negative. */
|
|
162
|
+
const translation = useSharedValue(0);
|
|
121
163
|
|
|
122
|
-
// `active` is a plain prop and the
|
|
123
|
-
// mirrored
|
|
124
|
-
//
|
|
164
|
+
// `active` is a plain prop and the worklets below cannot read props, so it is
|
|
165
|
+
// mirrored — written in an effect, because touching a shared value during
|
|
166
|
+
// render is a Reanimated strict-mode violation.
|
|
125
167
|
const isActive = useSharedValue(active && enabled);
|
|
126
168
|
useEffect(() => {
|
|
127
169
|
isActive.value = active && enabled;
|
|
128
170
|
}, [active, enabled, isActive]);
|
|
129
171
|
|
|
130
172
|
/*
|
|
131
|
-
* The
|
|
132
|
-
*
|
|
173
|
+
* The whole of `lift` is this callback, and it runs only while the element is
|
|
174
|
+
* the active one *and* the keyboard is up — see the reaction below.
|
|
133
175
|
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* has scrolled since it was laid out is measured where it used to be.
|
|
176
|
+
* The element is re-measured every frame rather than once, because every
|
|
177
|
+
* interesting thing that moves it happens after the keyboard opens: the page
|
|
178
|
+
* scrolls, a sheet settles, content above it grows. A position captured at
|
|
179
|
+
* the moment of focus is right for exactly one frame, and the element spends
|
|
180
|
+
* the rest of the time holding an offset that belongs to where it used to be.
|
|
140
181
|
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
182
|
+
* What is measured already includes the translation applied on the previous
|
|
183
|
+
* frame, so that is subtracted back out to recover the honest resting edge.
|
|
184
|
+
* Without it the callback would chase its own output down the screen.
|
|
185
|
+
*/
|
|
186
|
+
const track = useFrameCallback(() => {
|
|
187
|
+
'worklet';
|
|
188
|
+
const keyboardHeight = Math.abs(rawHeight.value);
|
|
189
|
+
if (keyboardHeight === 0) return;
|
|
190
|
+
|
|
191
|
+
const frame = measure(ref);
|
|
192
|
+
if (!frame || frame.height <= 0) return;
|
|
193
|
+
|
|
194
|
+
const restingBottom = frame.pageY + frame.height - translation.value;
|
|
195
|
+
const keyboardTop = screenHeight - keyboardHeight;
|
|
196
|
+
const overlap = restingBottom + offset - keyboardTop;
|
|
197
|
+
const next = overlap > 0 ? -overlap : 0;
|
|
198
|
+
|
|
199
|
+
if (Math.abs(next - translation.value) > EPSILON) translation.value = next;
|
|
200
|
+
}, false);
|
|
201
|
+
|
|
202
|
+
/*
|
|
203
|
+
* Tracking is switched on the transition rather than left running, so a
|
|
204
|
+
* screen full of fields costs nothing until one of them is being typed into.
|
|
205
|
+
* `setActive` lives on the JS side, hence the hop.
|
|
148
206
|
*/
|
|
207
|
+
const [tracking, setTracking] = useState(false);
|
|
149
208
|
useAnimatedReaction(
|
|
150
209
|
() => isActive.value && Math.abs(rawHeight.value) > 0,
|
|
151
|
-
(
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const frame = measure(ref);
|
|
162
|
-
if (frame && frame.height > 0 && frame.pageY >= 0) {
|
|
163
|
-
restingBottom.value = frame.pageY + frame.height;
|
|
210
|
+
(shouldTrack, wasTracking) => {
|
|
211
|
+
if (shouldTrack === wasTracking) return;
|
|
212
|
+
runOnJS(setTracking)(shouldTrack);
|
|
213
|
+
|
|
214
|
+
// Moving straight from one field to another never closes the keyboard,
|
|
215
|
+
// so the field being left has nothing to follow back down — it is sent
|
|
216
|
+
// home explicitly, or it stays hanging where the keyboard left it.
|
|
217
|
+
if (!shouldTrack && translation.value !== 0) {
|
|
218
|
+
translation.value = withTiming(0, { duration: SETTLE_DURATION });
|
|
164
219
|
}
|
|
165
220
|
}
|
|
166
221
|
);
|
|
167
222
|
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (restingBottom.value !== 0 && Math.abs(rawHeight.value) > 0) return;
|
|
174
|
-
|
|
175
|
-
ref.current?.measureInWindow((_x, y, _width, height) => {
|
|
176
|
-
if (height > 0 && y >= 0) restingBottom.value = y + height;
|
|
177
|
-
});
|
|
178
|
-
}, [ref, rawHeight, restingBottom]);
|
|
223
|
+
const { setActive } = track;
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
setActive(tracking && mode === 'lift');
|
|
226
|
+
return () => setActive(false);
|
|
227
|
+
}, [tracking, mode, setActive]);
|
|
179
228
|
|
|
180
229
|
const onLayout = useCallback(
|
|
181
230
|
(_event: LayoutChangeEvent) => {
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
requestAnimationFrame(seed);
|
|
231
|
+
// A layout pass while the element is at rest means its slot moved for
|
|
232
|
+
// some reason other than the keyboard. Anything left over from the last
|
|
233
|
+
// lift belongs to the old slot.
|
|
234
|
+
if (!tracking && translation.value !== 0) translation.value = 0;
|
|
187
235
|
},
|
|
188
|
-
[
|
|
236
|
+
[tracking, translation]
|
|
189
237
|
);
|
|
190
238
|
|
|
191
239
|
const animatedStyle = useAnimatedStyle(() => {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
240
|
+
if (mode === 'dock') {
|
|
241
|
+
if (!isActive.value) return { transform: [{ translateY: 0 }] };
|
|
242
|
+
// Both sources are normalised to a positive height here.
|
|
243
|
+
const keyboardHeight = Math.abs(rawHeight.value);
|
|
244
|
+
const travel = Math.max(keyboardHeight - bottomInset, 0);
|
|
245
|
+
return { transform: [{ translateY: -travel }] };
|
|
197
246
|
}
|
|
198
247
|
|
|
199
|
-
|
|
200
|
-
const overlap = restingBottom.value + offset - keyboardTop;
|
|
201
|
-
|
|
202
|
-
return { transform: [{ translateY: overlap > 0 ? -overlap : 0 }] };
|
|
248
|
+
return { transform: [{ translateY: translation.value }] };
|
|
203
249
|
});
|
|
204
250
|
|
|
205
251
|
return { ref, onLayout, animatedStyle };
|