@mrmeg/expo-ui 0.24.0 → 0.25.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.
- package/CHANGELOG.md +58 -0
- package/LLM_USAGE.md +26 -1
- package/README.md +46 -2
- package/dist/components/Avatar.js +6 -1
- package/dist/components/Button.d.ts +1 -1
- package/dist/components/Carousel.js +2 -0
- package/dist/components/Dialog.d.ts +15 -93
- package/dist/components/Dialog.js +5 -0
- package/dist/components/DismissKeyboard.d.ts +8 -6
- package/dist/components/DismissKeyboard.js +9 -7
- package/dist/components/DropdownMenu.d.ts +5 -34
- package/dist/components/DropdownMenu.js +2 -1
- package/dist/components/InputOTP.js +2 -0
- package/dist/components/Popover.d.ts +11 -11
- package/dist/components/Popover.js +3 -2
- package/dist/components/Select.d.ts +12 -20
- package/dist/components/Select.js +2 -1
- package/dist/components/StatusBar.js +2 -6
- package/dist/components/StyledText.context.d.ts +1 -1
- package/dist/components/StyledText.d.ts +6 -2
- package/dist/components/TextInput.d.ts +7 -3
- package/dist/components/TextInput.js +35 -28
- package/dist/components/Tooltip.d.ts +10 -24
- package/dist/components/Tooltip.js +5 -2
- package/dist/components/keyboardController.d.ts +4 -2
- package/dist/components/keyboardController.js +4 -0
- package/dist/components/keyboardDismiss.js +126 -55
- package/dist/components/keyboardFocusRegistry.d.ts +6 -4
- package/dist/constants/spacing.d.ts +2 -2
- package/dist/constants/spacing.js +2 -2
- package/dist/design-system.json +2236 -0
- package/dist/hooks/useTheme.d.ts +11 -3
- package/dist/state/themeStore.d.ts +1 -1
- package/package.json +11 -8
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect } from "react";
|
|
3
3
|
import { useTheme } from "../hooks/useTheme.js";
|
|
4
|
-
import { StatusBar as RNStatusBar
|
|
4
|
+
import { StatusBar as RNStatusBar } from "react-native";
|
|
5
5
|
export const StatusBar = () => {
|
|
6
6
|
const { scheme, theme } = useTheme();
|
|
7
7
|
const barStyle = scheme === "dark" ? "light-content" : "dark-content";
|
|
@@ -10,10 +10,6 @@ export const StatusBar = () => {
|
|
|
10
10
|
*/
|
|
11
11
|
useEffect(() => {
|
|
12
12
|
RNStatusBar.setBarStyle(barStyle, true);
|
|
13
|
-
if (Platform.OS === "android") {
|
|
14
|
-
RNStatusBar.setBackgroundColor("transparent", true);
|
|
15
|
-
RNStatusBar.setTranslucent(true);
|
|
16
|
-
}
|
|
17
13
|
}, [barStyle]);
|
|
18
|
-
return
|
|
14
|
+
return _jsx(RNStatusBar, { barStyle: barStyle });
|
|
19
15
|
};
|
|
@@ -2,5 +2,5 @@ import React from "react";
|
|
|
2
2
|
import type { StyleProp, TextStyle } from "react-native";
|
|
3
3
|
export declare const TextClassContext: React.Context<string | undefined>;
|
|
4
4
|
export declare const TextColorContext: React.Context<string | undefined>;
|
|
5
|
-
export declare const TextStyleContext: React.Context<StyleProp<TextStyle
|
|
5
|
+
export declare const TextStyleContext: React.Context<StyleProp<TextStyle> | undefined>;
|
|
6
6
|
export declare const TextSelectabilityContext: React.Context<boolean | undefined>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Ref } from "react";
|
|
1
|
+
import { type ComponentRef, type Ref } from "react";
|
|
2
2
|
import { Text as RNText, TextProps as RNTextProps } from "react-native";
|
|
3
3
|
import { type FontVariant } from "../constants/fonts";
|
|
4
4
|
/**
|
|
@@ -55,8 +55,12 @@ export type TextProps = RNTextProps & {
|
|
|
55
55
|
txOptions?: object;
|
|
56
56
|
/**
|
|
57
57
|
* Forwarded ref to the underlying RNText element.
|
|
58
|
+
*
|
|
59
|
+
* Written as `ComponentRef<typeof RNText>` rather than RN 0.88's `TextInstance`
|
|
60
|
+
* alias so the emitted declarations stay valid on RN 0.85/0.86, whose typings
|
|
61
|
+
* do not export that name.
|
|
58
62
|
*/
|
|
59
|
-
ref?: Ref<RNText
|
|
63
|
+
ref?: Ref<ComponentRef<typeof RNText>>;
|
|
60
64
|
};
|
|
61
65
|
/**
|
|
62
66
|
* Base Text component with theme and variant support
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ReactNode, type Ref } from "react";
|
|
1
|
+
import { type ComponentRef, type ReactNode, type Ref } from "react";
|
|
2
2
|
import { TextInput as RNTextInput, ViewStyle, TextStyle, TextInputProps, StyleProp } from "react-native";
|
|
3
3
|
/**
|
|
4
4
|
* Size variants for TextInput
|
|
@@ -10,9 +10,13 @@ export type TextInputSize = "sm" | "md" | "lg";
|
|
|
10
10
|
export type TextInputVariant = "outline" | "filled" | "underlined";
|
|
11
11
|
interface TextInputCustomProps extends TextInputProps {
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* RNTextInput ref on web; an RN-compatible imperative handle on native.
|
|
14
|
+
*
|
|
15
|
+
* Written as `ComponentRef<typeof RNTextInput>` rather than RN 0.88's
|
|
16
|
+
* `TextInputInstance` alias so the emitted declarations stay valid on
|
|
17
|
+
* RN 0.85/0.86, whose typings do not export that name.
|
|
14
18
|
*/
|
|
15
|
-
ref?: Ref<RNTextInput
|
|
19
|
+
ref?: Ref<ComponentRef<typeof RNTextInput>>;
|
|
16
20
|
/**
|
|
17
21
|
* Visual variant
|
|
18
22
|
* @default "outline"
|
|
@@ -191,11 +191,10 @@ function WebTextInput({ variant = "outline", size = "md", label, helperText, err
|
|
|
191
191
|
* never round-trips through React state — eliminating the cursor flicker seen on
|
|
192
192
|
* controlled RN inputs.
|
|
193
193
|
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* layering RN views over the native host view.
|
|
194
|
+
* This path supports label / helper / error text and a password visibility
|
|
195
|
+
* toggle laid out beside the native Host. Clear buttons, left/right elements,
|
|
196
|
+
* and the error icon remain web-only; RN overlays are not layered over the
|
|
197
|
+
* native host view.
|
|
199
198
|
*/
|
|
200
199
|
function NativeTextInput({ variant = "outline", size = "md", label, helperText, errorText, error, required, rows, forceLight, inputMode, onChangeText, onFocus, onBlur, value, defaultValue, editable = true, multiline, secureTextEntry, showSecureEntryToggle, ref, wrapperStyle,
|
|
201
200
|
// Web-only affordances. Destructured out of `rest` so they're NOT forwarded to
|
|
@@ -231,9 +230,9 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
231
230
|
// parity) once it settles.
|
|
232
231
|
const queuedTogglesRef = useRef(0);
|
|
233
232
|
const toggleRef = useRef(() => { });
|
|
234
|
-
//
|
|
235
|
-
// view so React
|
|
236
|
-
// instead of reusing one
|
|
233
|
+
// iOS-only per-flavour mount generation. Bumped whenever a flavour becomes
|
|
234
|
+
// the incoming view so React mounts a FRESH native view (whose `autoFocus`
|
|
235
|
+
// will run) instead of reusing one still unmounting from the previous handoff.
|
|
237
236
|
const generationRef = useRef({ secure: 0, plain: 0 });
|
|
238
237
|
// Last text the JS side knows about; see handleChangeText.
|
|
239
238
|
const lastTextRef = useRef(value ?? defaultValue ?? "");
|
|
@@ -256,12 +255,18 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
256
255
|
// RNTextInput because that's what the public TextInputCustomProps declares.
|
|
257
256
|
// We expose the subset consumers use, plus a `setNativeProps` shim so the
|
|
258
257
|
// uncontrolled AuthTextField can push corrected text into the native buffer.
|
|
259
|
-
//
|
|
260
|
-
// iOS
|
|
258
|
+
// Android keeps one field/ref as visualTransformation changes in place.
|
|
259
|
+
// iOS has one ref per flavour; both are mounted only during a handoff.
|
|
260
|
+
const androidRef = useRef(null);
|
|
261
261
|
const secureRef = useRef(null);
|
|
262
262
|
const plainRef = useRef(null);
|
|
263
|
-
const activeInput = useCallback(() =>
|
|
263
|
+
const activeInput = useCallback(() => {
|
|
264
|
+
if (Platform.OS === "android")
|
|
265
|
+
return androidRef.current;
|
|
266
|
+
return (activeSecureRef.current ? secureRef : plainRef).current;
|
|
267
|
+
}, []);
|
|
264
268
|
const blurAll = useCallback(() => {
|
|
269
|
+
androidRef.current?.blur();
|
|
265
270
|
secureRef.current?.blur();
|
|
266
271
|
plainRef.current?.blur();
|
|
267
272
|
}, []);
|
|
@@ -282,19 +287,15 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
282
287
|
}, []);
|
|
283
288
|
const parentOnFocus = onFocus;
|
|
284
289
|
const parentOnBlur = onBlur;
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
// React is unmounting.
|
|
290
|
+
// On iOS, ignore focus/blur from the outgoing flavour during a handoff or
|
|
291
|
+
// unmount. Android events always belong to the same mounted field.
|
|
288
292
|
const handleFocus = useCallback((secure) => {
|
|
289
|
-
if (secure !== activeSecureRef.current)
|
|
293
|
+
if (Platform.OS === "ios" && secure !== activeSecureRef.current)
|
|
290
294
|
return;
|
|
291
295
|
isFocusedRef.current = true;
|
|
292
|
-
// Register
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
// Blurring through the field's own ref is window-independent — inside a
|
|
296
|
-
// native bottom sheet `KeyboardController.dismiss()` can miss, the ref
|
|
297
|
-
// never does.
|
|
296
|
+
// Register the field's own blur handle for tap-away dismissal and the
|
|
297
|
+
// BottomSheet overlay. RN supports Expo-hosted field interop; this direct
|
|
298
|
+
// ref also targets the actual field inside a native sheet's window.
|
|
298
299
|
setKeyboardFocusedInput(focusRegistryToken, blurAll);
|
|
299
300
|
if (handoffRef.current) {
|
|
300
301
|
// The incoming view took first responder; the parent never saw focus leave.
|
|
@@ -305,7 +306,7 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
305
306
|
parentOnFocus?.();
|
|
306
307
|
}, [blurAll, finishHandoff, focusRegistryToken, parentOnFocus]);
|
|
307
308
|
const handleBlur = useCallback((secure) => {
|
|
308
|
-
if (secure !== activeSecureRef.current)
|
|
309
|
+
if (Platform.OS === "ios" && secure !== activeSecureRef.current)
|
|
309
310
|
return;
|
|
310
311
|
isFocusedRef.current = false;
|
|
311
312
|
restoreAfterSecureHandoffRef.current = false;
|
|
@@ -321,10 +322,12 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
321
322
|
}
|
|
322
323
|
restoreAfterSecureHandoffRef.current = false;
|
|
323
324
|
const outgoingSecure = activeSecureRef.current;
|
|
324
|
-
if (
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
325
|
+
if (Platform.OS === "ios") {
|
|
326
|
+
if (outgoingSecure)
|
|
327
|
+
generationRef.current.plain += 1;
|
|
328
|
+
else
|
|
329
|
+
generationRef.current.secure += 1;
|
|
330
|
+
}
|
|
328
331
|
if (Platform.OS === "ios" && isFocusedRef.current) {
|
|
329
332
|
const timer = setTimeout(() => {
|
|
330
333
|
// The incoming view never took first responder. Rather than unmount
|
|
@@ -460,7 +463,7 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
460
463
|
fontSize: sizeConfig.fontSize,
|
|
461
464
|
...(nativeFontFamily ? { fontFamily: nativeFontFamily } : null),
|
|
462
465
|
};
|
|
463
|
-
return (_jsxs(View, { style: wrapperStyle, children: [!!label && (_jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, size: "base", fontWeight: "medium", style: styles.label, children: [label, required && _jsx(StyledText, { selectable: false, fontWeight: "bold", style: styles.required, children: " *" })] }) })), _jsxs(View, { style: [surfaceStyle, hasSecureToggle && styles.nativeRow], hitSlop: { top: surfaceHitSlop, bottom: surfaceHitSlop }, ...surfaceResponderProps, children: [[true, false].map((secure) => {
|
|
466
|
+
return (_jsxs(View, { style: wrapperStyle, children: [!!label && (_jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, size: "base", fontWeight: "medium", style: styles.label, children: [label, required && _jsx(StyledText, { selectable: false, fontWeight: "bold", style: styles.required, children: " *" })] }) })), _jsxs(View, { style: [surfaceStyle, hasSecureToggle && styles.nativeRow], hitSlop: { top: surfaceHitSlop, bottom: surfaceHitSlop }, ...surfaceResponderProps, children: [(Platform.OS === "android" ? [effectiveSecureTextEntry] : [true, false]).map((secure) => {
|
|
464
467
|
const active = secure === effectiveSecureTextEntry;
|
|
465
468
|
const outgoing = handoff != null && secure === handoff.outgoingSecure;
|
|
466
469
|
if (!active && !outgoing)
|
|
@@ -472,7 +475,11 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
472
475
|
? hasSecureToggle
|
|
473
476
|
? styles.nativeHostFlex
|
|
474
477
|
: styles.nativeHost
|
|
475
|
-
: styles.nativeHostHandoff, pointerEvents: inLayout ? "auto" : "none", accessibilityElementsHidden: !inLayout, importantForAccessibility: inLayout ? "auto" : "no-hide-descendants", children: _jsx(ExpoTextInput, { ...rest, autoFocus: rest.autoFocus || (handoff != null && active), ref: secure ? secureRef : plainRef, value: state, defaultValue: defaultValue, onChangeText: handleChangeText, onFocus: () => handleFocus(secure), onBlur: () => handleBlur(secure), editable: editable, multiline: multiline, rows: rows, inputMode: inputMode, secureTextEntry: secure, placeholderTextColor: theme.colors.textDim, style: boxStyle, textStyle: textStyle }) },
|
|
478
|
+
: styles.nativeHostHandoff, pointerEvents: inLayout ? "auto" : "none", accessibilityElementsHidden: !inLayout, importantForAccessibility: inLayout ? "auto" : "no-hide-descendants", children: _jsx(ExpoTextInput, { ...rest, autoFocus: rest.autoFocus || (handoff != null && active), ref: Platform.OS === "android" ? androidRef : secure ? secureRef : plainRef, value: state, defaultValue: defaultValue, onChangeText: handleChangeText, onFocus: () => handleFocus(secure), onBlur: () => handleBlur(secure), editable: editable, multiline: multiline, rows: rows, inputMode: inputMode, secureTextEntry: secure, placeholderTextColor: theme.colors.textDim, style: boxStyle, textStyle: textStyle }) }, Platform.OS === "android"
|
|
479
|
+
? "android"
|
|
480
|
+
: secure
|
|
481
|
+
? `secure-${generationRef.current.secure}`
|
|
482
|
+
: `plain-${generationRef.current.plain}`));
|
|
476
483
|
}), hasSecureToggle && (_jsx(Pressable, { style: styles.nativePasswordToggle, onPress: togglePasswordVisible, accessibilityLabel: passwordVisible ? "Hide password" : "Show password", accessibilityRole: "button", children: _jsx(Icon, { name: passwordVisible ? "eye-off" : "eye", size: spacing.iconSm + 4, color: "textDim" }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, size: "sm", style: [styles.helperText, hasError && styles.errorText], children: errorText || helperText }))] }));
|
|
477
484
|
}
|
|
478
485
|
const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { ViewProps } from "react-native";
|
|
3
3
|
import * as TooltipPrimitive from "@rn-primitives/tooltip";
|
|
4
4
|
/**
|
|
5
5
|
* Tooltip Trigger Component
|
|
6
6
|
* The element that triggers the tooltip to appear on hover (web) or press (native)
|
|
7
7
|
*/
|
|
8
|
-
declare const TooltipTrigger:
|
|
9
|
-
({ asChild, onPress: onPressProp, disabled, ref, ...props }: Omit<import("react-native").PressableProps & React.RefAttributes<View>, "ref"> & {
|
|
10
|
-
asChild?: boolean;
|
|
11
|
-
} & {
|
|
12
|
-
onKeyDown?: (ev: React.KeyboardEvent) => void;
|
|
13
|
-
onKeyUp?: (ev: React.KeyboardEvent) => void;
|
|
14
|
-
} & React.RefAttributes<import("@rn-primitives/tooltip").TriggerRef>): React.JSX.Element;
|
|
15
|
-
displayName: string;
|
|
16
|
-
};
|
|
8
|
+
declare const TooltipTrigger: typeof TooltipPrimitive.Trigger;
|
|
17
9
|
/**
|
|
18
10
|
* Tooltip variant styles
|
|
19
11
|
*/
|
|
@@ -100,22 +92,16 @@ interface TooltipProps extends TooltipPrimitive.RootProps {
|
|
|
100
92
|
* ```
|
|
101
93
|
*/
|
|
102
94
|
declare function Tooltip({ delayDuration, skipDelayDuration, ...props }: TooltipProps): React.JSX.Element;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
* Properly typed interface for dot notation access (e.g., Tooltip.Trigger)
|
|
106
|
-
*/
|
|
107
|
-
declare const TooltipComponent: typeof Tooltip & {
|
|
108
|
-
Trigger: {
|
|
109
|
-
({ asChild, onPress: onPressProp, disabled, ref, ...props }: Omit<import("react-native").PressableProps & React.RefAttributes<View>, "ref"> & {
|
|
110
|
-
asChild?: boolean;
|
|
111
|
-
} & {
|
|
112
|
-
onKeyDown?: (ev: React.KeyboardEvent) => void;
|
|
113
|
-
onKeyUp?: (ev: React.KeyboardEvent) => void;
|
|
114
|
-
} & React.RefAttributes<import("@rn-primitives/tooltip").TriggerRef>): React.JSX.Element;
|
|
115
|
-
displayName: string;
|
|
116
|
-
};
|
|
95
|
+
type TooltipCompound = typeof Tooltip & {
|
|
96
|
+
Trigger: typeof TooltipTrigger;
|
|
117
97
|
Content: typeof TooltipContent;
|
|
118
98
|
Body: typeof TooltipBody;
|
|
119
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* Tooltip Component with Sub-components
|
|
102
|
+
* Properly typed interface for dot notation access (e.g., Tooltip.Trigger).
|
|
103
|
+
* The annotation is what keeps declaration emit portable — see Dialog.tsx (TS2883).
|
|
104
|
+
*/
|
|
105
|
+
declare const TooltipComponent: TooltipCompound;
|
|
120
106
|
export { TooltipComponent as Tooltip, TooltipTrigger, TooltipContent, TooltipBody, };
|
|
121
107
|
export type { TooltipContentProps, TooltipBodyProps, TooltipProps, };
|
|
@@ -12,6 +12,8 @@ import { palette } from "../constants/colors.js";
|
|
|
12
12
|
* Tooltip Trigger Component
|
|
13
13
|
* The element that triggers the tooltip to appear on hover (web) or press (native)
|
|
14
14
|
*/
|
|
15
|
+
// Annotated with `typeof` for portable declaration emit — see the note in
|
|
16
|
+
// Dialog.tsx (TS2883). Without it the .d.ts inlines RN 0.88's `PressableInstance`.
|
|
15
17
|
const TooltipTrigger = TooltipPrimitive.Trigger;
|
|
16
18
|
/**
|
|
17
19
|
* FullWindowOverlay wrapper - uses native overlay on iOS for proper z-index handling
|
|
@@ -68,7 +70,7 @@ function TooltipContent({ side = "top", align = "center", sideOffset = 4, portal
|
|
|
68
70
|
maxWidth: 250,
|
|
69
71
|
...getShadowStyle("soft"),
|
|
70
72
|
},
|
|
71
|
-
]);
|
|
73
|
+
]) ?? undefined;
|
|
72
74
|
return (_jsx(TooltipPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: _jsx(TooltipPrimitive.Overlay, { style: Platform.select({ native: StyleSheet.absoluteFill }), children: _jsx(AnimatedView, { type: "fade", enterDuration: 150, children: _jsx(TextColorContext.Provider, { value: colors.text, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(TooltipPrimitive.Content, { side: side, align: align, sideOffset: sideOffset, style: contentStyle, ...props }) }) }) }) }) }) }) }));
|
|
73
75
|
}
|
|
74
76
|
function TooltipBody({ children, style, ...props }) {
|
|
@@ -122,7 +124,8 @@ function Tooltip({ delayDuration = 700, skipDelayDuration = 300, ...props }) {
|
|
|
122
124
|
}
|
|
123
125
|
/**
|
|
124
126
|
* Tooltip Component with Sub-components
|
|
125
|
-
* Properly typed interface for dot notation access (e.g., Tooltip.Trigger)
|
|
127
|
+
* Properly typed interface for dot notation access (e.g., Tooltip.Trigger).
|
|
128
|
+
* The annotation is what keeps declaration emit portable — see Dialog.tsx (TS2883).
|
|
126
129
|
*/
|
|
127
130
|
const TooltipComponent = Object.assign(Tooltip, {
|
|
128
131
|
Trigger: TooltipTrigger,
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
* function that has no hook calls of its own).
|
|
20
20
|
*/
|
|
21
21
|
import React from "react";
|
|
22
|
-
import { View } from "react-native";
|
|
23
22
|
import type { IKeyboardState, KeyboardAnimationContext, KeyboardAvoidingViewProps, KeyboardControllerModule } from "react-native-keyboard-controller";
|
|
24
23
|
/** No-op imperative module: there is no IME to dismiss or preload on web. */
|
|
25
24
|
export declare const KeyboardController: KeyboardControllerModule;
|
|
@@ -41,6 +40,9 @@ type NativeKeyboardAvoidingViewProps = KeyboardAvoidingViewProps & {
|
|
|
41
40
|
* Plain `View` passthrough. Keyboard-avoidance props are accepted (so callers
|
|
42
41
|
* stay identical across platforms) and dropped, since the browser handles
|
|
43
42
|
* viewport resizing itself.
|
|
43
|
+
*
|
|
44
|
+
* The explicit return type keeps declaration emit from inlining `View`'s ref
|
|
45
|
+
* type (RN 0.88's `ViewInstance`), which older RN typings do not export.
|
|
44
46
|
*/
|
|
45
|
-
export declare function NativeKeyboardAvoidingView({ behavior, contentContainerStyle, keyboardVerticalOffset, automaticOffset, enabled, children, ...viewProps }: NativeKeyboardAvoidingViewProps): React.
|
|
47
|
+
export declare function NativeKeyboardAvoidingView({ behavior, contentContainerStyle, keyboardVerticalOffset, automaticOffset, enabled, children, ...viewProps }: NativeKeyboardAvoidingViewProps): React.ReactElement;
|
|
46
48
|
export {};
|
|
@@ -53,6 +53,7 @@ export const KeyboardController = {
|
|
|
53
53
|
setDefaultMode: () => undefined,
|
|
54
54
|
setInputMode: () => undefined,
|
|
55
55
|
preload: () => undefined,
|
|
56
|
+
setTranslucent: () => undefined,
|
|
56
57
|
dismiss: () => Promise.resolve(),
|
|
57
58
|
setFocusTo: () => undefined,
|
|
58
59
|
isVisible: () => false,
|
|
@@ -77,6 +78,9 @@ export function useKeyboardContext() {
|
|
|
77
78
|
* Plain `View` passthrough. Keyboard-avoidance props are accepted (so callers
|
|
78
79
|
* stay identical across platforms) and dropped, since the browser handles
|
|
79
80
|
* viewport resizing itself.
|
|
81
|
+
*
|
|
82
|
+
* The explicit return type keeps declaration emit from inlining `View`'s ref
|
|
83
|
+
* type (RN 0.88's `ViewInstance`), which older RN typings do not export.
|
|
80
84
|
*/
|
|
81
85
|
export function NativeKeyboardAvoidingView({ behavior, contentContainerStyle, keyboardVerticalOffset, automaticOffset, enabled, children, ...viewProps }) {
|
|
82
86
|
// Dropped rather than forwarded so they never reach the DOM node.
|
|
@@ -1,34 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tap-away keyboard dismissal that never steals a touch.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Let the deepest interactive view win the responder negotiation, and blur on
|
|
5
|
+
* release only for an unclaimed, single-finger tap within the travel slop.
|
|
6
|
+
* Hosted `@expo/ui` inputs register with RN's `TextInputState`. A ScrollView
|
|
7
|
+
* using `keyboardShouldPersistTaps="handled"` can therefore claim dead space
|
|
8
|
+
* and blur on release if it observed no scroll, even after substantial travel.
|
|
9
|
+
* `DismissKeyboard`'s ScrollView uses `always` so this boundary owns tap dismissal
|
|
10
|
+
* with an explicit travel policy, independent of whether content can scroll.
|
|
11
|
+
* Two signals need no rectangles and no responder ownership:
|
|
10
12
|
*
|
|
11
13
|
* 1. `onStartShouldSetResponder` in the *bubble* phase is only ever called on a
|
|
12
|
-
* view when no descendant claimed the touch. Pressables
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
* view when no descendant claimed the touch. Pressables and RN TextInputs
|
|
15
|
+
* claim, as can scroll views depending on their tap policy. Being asked
|
|
16
|
+
* means no descendant took responder ownership, not necessarily dead space.
|
|
15
17
|
* 2. The package `TextInput` tags touches that begin on its surface
|
|
16
18
|
* (`markTextInputTouchStart`). Its bubble handler runs before any enclosing
|
|
17
19
|
* boundary's, so the boundary can tell a tap on *any* native field — focused
|
|
18
20
|
* or not — from dead space, and leave focus handoff, double-tap selection
|
|
19
21
|
* and the eye toggle to the platform.
|
|
20
22
|
*
|
|
21
|
-
* The boundary always answers `false`, so
|
|
22
|
-
* controls (SwiftUI/Compose fields, sliders) keep receiving every touch phase
|
|
23
|
+
* The boundary always answers `false`, so it never becomes the JS responder.
|
|
24
|
+
* Native controls (SwiftUI/Compose fields, sliders) keep receiving every touch phase
|
|
23
25
|
* on both platforms, and scroll views take over drags exactly as before.
|
|
24
26
|
* Dismissal happens in `onTouchEnd`, a plain bubbling event that fires whether
|
|
25
|
-
* or not anyone became the responder
|
|
27
|
+
* or not anyone became the responder. Moves cancel permanently once past the
|
|
28
|
+
* slop; release coordinates also enforce it when move delivery was missed.
|
|
29
|
+
* Cancellation and multitouch drop the tap without rearming another finger.
|
|
26
30
|
*/
|
|
27
31
|
import { useMemo, useRef } from "react";
|
|
28
32
|
import { Platform } from "react-native";
|
|
29
33
|
import { KeyboardController, useKeyboardState } from "./keyboardController";
|
|
30
34
|
import { dismissKeyboardFocusedInput, hasKeyboardFocusedInput } from "./keyboardFocusRegistry.js";
|
|
31
|
-
/**
|
|
35
|
+
/** Maximum displacement per axis (RN logical units) for a tap, scrollable or not. */
|
|
32
36
|
const MOVE_SLOP = 10;
|
|
33
37
|
function keyOf(event) {
|
|
34
38
|
const { identifier, timestamp } = event.nativeEvent;
|
|
@@ -42,6 +46,10 @@ function isSameEvent(key, event) {
|
|
|
42
46
|
}
|
|
43
47
|
let textInputTouch = null;
|
|
44
48
|
let claimedTouch = null;
|
|
49
|
+
// An ancestor can observe a second finger outside an inner boundary/surface.
|
|
50
|
+
// Invalidate already-armed taps there too, even if that finger lifts first and
|
|
51
|
+
// the inner view only receives its original finger's final touches=[] event.
|
|
52
|
+
let multiTouchVersion = 0;
|
|
45
53
|
/**
|
|
46
54
|
* `onStartShouldSetResponder` for a native text-field surface: decline the
|
|
47
55
|
* responder (the native field keeps every touch phase) but tag the touch so an
|
|
@@ -72,6 +80,77 @@ function movedPastSlop(tap, event) {
|
|
|
72
80
|
return (Math.abs(event.nativeEvent.pageX - tap.pageX) > MOVE_SLOP ||
|
|
73
81
|
Math.abs(event.nativeEvent.pageY - tap.pageY) > MOVE_SLOP);
|
|
74
82
|
}
|
|
83
|
+
/** Shared single-finger travel policy for tap-away dismissal and surface focus. */
|
|
84
|
+
function createTapTracker() {
|
|
85
|
+
let pending = null;
|
|
86
|
+
let armedVersion = multiTouchVersion;
|
|
87
|
+
let lastStart = null;
|
|
88
|
+
const active = new Set();
|
|
89
|
+
function readTouches(event) {
|
|
90
|
+
const { touches } = event.nativeEvent;
|
|
91
|
+
if (touches == null)
|
|
92
|
+
return false;
|
|
93
|
+
active.clear();
|
|
94
|
+
touches.forEach(({ identifier }) => active.add(identifier));
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
start(event) {
|
|
99
|
+
// Negotiation precedes the plain bubbling touch-start for the same event.
|
|
100
|
+
// Observe it once; a child-claimed start reaches only onTouchStart, which
|
|
101
|
+
// clears any stale pending tap but never arms one.
|
|
102
|
+
if (isSameEvent(lastStart, event))
|
|
103
|
+
return false;
|
|
104
|
+
lastStart = keyOf(event);
|
|
105
|
+
pending = null;
|
|
106
|
+
// RN's array is authoritative, including after a missed end. The set is
|
|
107
|
+
// also a fallback for callers/tests omitting arrays, retaining other down
|
|
108
|
+
// fingers even after movement has cancelled the pending tap.
|
|
109
|
+
if (!readTouches(event))
|
|
110
|
+
active.add(event.nativeEvent.identifier);
|
|
111
|
+
if (active.size > 1 || (event.nativeEvent.changedTouches?.length ?? 1) > 1) {
|
|
112
|
+
multiTouchVersion += 1;
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
return active.size === 1;
|
|
116
|
+
},
|
|
117
|
+
arm(event) {
|
|
118
|
+
pending = { ...keyOf(event), pageX: event.nativeEvent.pageX, pageY: event.nativeEvent.pageY };
|
|
119
|
+
armedVersion = multiTouchVersion;
|
|
120
|
+
},
|
|
121
|
+
move(event) {
|
|
122
|
+
readTouches(event);
|
|
123
|
+
if (active.size > 1 || (event.nativeEvent.changedTouches?.length ?? 1) > 1) {
|
|
124
|
+
multiTouchVersion += 1;
|
|
125
|
+
pending = null;
|
|
126
|
+
}
|
|
127
|
+
else if (isSameTouch(pending, event) && movedPastSlop(pending, event)) {
|
|
128
|
+
pending = null;
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
end(event) {
|
|
132
|
+
const tap = pending;
|
|
133
|
+
pending = null;
|
|
134
|
+
// On touch end, touches contains the REMAINING fingers (empty on the last
|
|
135
|
+
// release); changedTouches contains those that ended. Never treat the
|
|
136
|
+
// final empty array as an invalid single-finger tap.
|
|
137
|
+
if (!readTouches(event)) {
|
|
138
|
+
active.delete(event.nativeEvent.identifier);
|
|
139
|
+
event.nativeEvent.changedTouches?.forEach(({ identifier }) => active.delete(identifier));
|
|
140
|
+
}
|
|
141
|
+
if (active.size === 0)
|
|
142
|
+
lastStart = null;
|
|
143
|
+
if (active.size > 0 || (event.nativeEvent.changedTouches?.length ?? 1) > 1) {
|
|
144
|
+
multiTouchVersion += 1;
|
|
145
|
+
}
|
|
146
|
+
return (armedVersion === multiTouchVersion &&
|
|
147
|
+
active.size === 0 &&
|
|
148
|
+
(event.nativeEvent.changedTouches?.length ?? 1) <= 1 &&
|
|
149
|
+
isSameTouch(tap, event) &&
|
|
150
|
+
!movedPastSlop(tap, event));
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
75
154
|
/**
|
|
76
155
|
* View props for the painted surface around a native (`@expo/ui`) text field.
|
|
77
156
|
*
|
|
@@ -87,31 +166,30 @@ function movedPastSlop(tap, event) {
|
|
|
87
166
|
export function useTextInputSurfaceResponder(focus) {
|
|
88
167
|
const focusRef = useRef(focus);
|
|
89
168
|
focusRef.current = focus;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
tap.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}), []);
|
|
169
|
+
return useMemo(() => {
|
|
170
|
+
const tap = createTapTracker();
|
|
171
|
+
return {
|
|
172
|
+
onStartShouldSetResponder: (event) => {
|
|
173
|
+
markTextInputTouchStart(event);
|
|
174
|
+
if (tap.start(event))
|
|
175
|
+
tap.arm(event);
|
|
176
|
+
return false;
|
|
177
|
+
},
|
|
178
|
+
onTouchStart: (event) => {
|
|
179
|
+
tap.start(event);
|
|
180
|
+
},
|
|
181
|
+
onTouchMove: (event) => {
|
|
182
|
+
tap.move(event);
|
|
183
|
+
},
|
|
184
|
+
onTouchEnd: (event) => {
|
|
185
|
+
if (tap.end(event))
|
|
186
|
+
focusRef.current();
|
|
187
|
+
},
|
|
188
|
+
onTouchCancel: (event) => {
|
|
189
|
+
tap.end(event);
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}, []);
|
|
115
193
|
}
|
|
116
194
|
/**
|
|
117
195
|
* View props for a tap-away keyboard-dismiss boundary. Spread onto the wrapper
|
|
@@ -123,13 +201,14 @@ export function useKeyboardDismissResponder() {
|
|
|
123
201
|
const isVisible = useKeyboardState((state) => state.isVisible);
|
|
124
202
|
const isVisibleRef = useRef(isVisible);
|
|
125
203
|
isVisibleRef.current = isVisible;
|
|
126
|
-
const pending = useRef(null);
|
|
127
204
|
return useMemo(() => {
|
|
128
205
|
if (Platform.OS === "web")
|
|
129
206
|
return {};
|
|
207
|
+
const tap = createTapTracker();
|
|
130
208
|
return {
|
|
131
209
|
onStartShouldSetResponder: (event) => {
|
|
132
|
-
|
|
210
|
+
if (!tap.start(event))
|
|
211
|
+
return false;
|
|
133
212
|
// `hasKeyboardFocusedInput` covers the window where a field has focus but
|
|
134
213
|
// keyboard-controller has not reported the keyboard yet (or a hardware
|
|
135
214
|
// keyboard is attached and there is no software keyboard to observe).
|
|
@@ -141,29 +220,21 @@ export function useKeyboardDismissResponder() {
|
|
|
141
220
|
if (isSameEvent(claimedTouch, event))
|
|
142
221
|
return false;
|
|
143
222
|
claimedTouch = keyOf(event);
|
|
144
|
-
|
|
223
|
+
tap.arm(event);
|
|
145
224
|
return false;
|
|
146
225
|
},
|
|
147
226
|
onTouchStart: (event) => {
|
|
148
|
-
|
|
149
|
-
// event, so a fresh pending tap matches here; anything else is stale
|
|
150
|
-
// (e.g. a touch whose end never reached this view).
|
|
151
|
-
if (pending.current && !isSameEvent(pending.current, event))
|
|
152
|
-
pending.current = null;
|
|
227
|
+
tap.start(event);
|
|
153
228
|
},
|
|
154
229
|
onTouchMove: (event) => {
|
|
155
|
-
|
|
156
|
-
pending.current = null;
|
|
157
|
-
}
|
|
230
|
+
tap.move(event);
|
|
158
231
|
},
|
|
159
232
|
onTouchEnd: (event) => {
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
pending.current = null;
|
|
163
|
-
dismissKeyboard();
|
|
233
|
+
if (tap.end(event))
|
|
234
|
+
dismissKeyboard();
|
|
164
235
|
},
|
|
165
|
-
onTouchCancel: () => {
|
|
166
|
-
|
|
236
|
+
onTouchCancel: (event) => {
|
|
237
|
+
tap.end(event);
|
|
167
238
|
},
|
|
168
239
|
};
|
|
169
240
|
}, []);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Registry of the currently focused native (`@expo/ui`) text field.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Hosted `@expo/ui` inputs also register with React Native's `TextInputState`.
|
|
5
|
+
* This package registry supplies a window-independent native blur handle and
|
|
6
|
+
* focus presence even when keyboard-controller cannot observe the keyboard
|
|
7
|
+
* (for example, in an isolated native sheet window). The package `TextInput`
|
|
8
|
+
* registers on focus; tap-away dismissal (`keyboardDismiss.ts`) and the
|
|
9
|
+
* `BottomSheet` overlay resign the field through that handle.
|
|
8
10
|
*/
|
|
9
11
|
export type KeyboardFocusedInputToken = object;
|
|
10
12
|
/**
|
|
@@ -36,7 +36,7 @@ export declare const spacing: {
|
|
|
36
36
|
readonly radiusNone: 0;
|
|
37
37
|
readonly radiusXs: 4;
|
|
38
38
|
readonly radiusSm: 8;
|
|
39
|
-
readonly radiusMd:
|
|
39
|
+
readonly radiusMd: 10;
|
|
40
40
|
readonly radiusLg: 14;
|
|
41
41
|
readonly radiusXl: 18;
|
|
42
42
|
readonly radius2xl: 24;
|
|
@@ -47,5 +47,5 @@ export declare const spacing: {
|
|
|
47
47
|
readonly iconLg: 32;
|
|
48
48
|
readonly iconXl: 48;
|
|
49
49
|
};
|
|
50
|
-
export declare const base: 8, xxs: 2, xs: 4, sm: 8, smd: 12, md: 16, mdl: 20, lg: 24, xl: 32, xxl: 48, xxxl: 64, gutter: 16, gutterVertical: 24, screenPadding: 16, sectionSpacing: 24, rowPaddingY: 10, rowPaddingX: 16, rowGap: 12, rowMinHeight: 40, formRowMinHeight: 32, cardPadding: 16, dialogPadding: 20, touchTarget: 44, buttonPadding: 10, inputPadding: 10, listItemSpacing: 8, radiusNone: 0, radiusXs: 4, radiusSm: 8, radiusMd:
|
|
50
|
+
export declare const base: 8, xxs: 2, xs: 4, sm: 8, smd: 12, md: 16, mdl: 20, lg: 24, xl: 32, xxl: 48, xxxl: 64, gutter: 16, gutterVertical: 24, screenPadding: 16, sectionSpacing: 24, rowPaddingY: 10, rowPaddingX: 16, rowGap: 12, rowMinHeight: 40, formRowMinHeight: 32, cardPadding: 16, dialogPadding: 20, touchTarget: 44, buttonPadding: 10, inputPadding: 10, listItemSpacing: 8, radiusNone: 0, radiusXs: 4, radiusSm: 8, radiusMd: 10, radiusLg: 14, radiusXl: 18, radius2xl: 24, radiusFull: 9999, iconXs: 12, iconSm: 16, iconMd: 24, iconLg: 32, iconXl: 48;
|
|
51
51
|
export declare const space: (multiplier: number) => number;
|
|
@@ -38,11 +38,11 @@ export const spacing = {
|
|
|
38
38
|
buttonPadding: 10, // Default button padding
|
|
39
39
|
inputPadding: 10, // Default input padding
|
|
40
40
|
listItemSpacing: 8, // Space between list items
|
|
41
|
-
// Border radius — shadcn-inspired scale (radiusMd =
|
|
41
|
+
// Border radius — shadcn-inspired scale (radiusMd = 10px default)
|
|
42
42
|
radiusNone: 0,
|
|
43
43
|
radiusXs: 4,
|
|
44
44
|
radiusSm: 8,
|
|
45
|
-
radiusMd:
|
|
45
|
+
radiusMd: 10,
|
|
46
46
|
radiusLg: 14,
|
|
47
47
|
radiusXl: 18,
|
|
48
48
|
radius2xl: 24,
|