@symbiote-native/react 0.1.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.
Files changed (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/build/components/activity-indicator/index.android.d.ts +2 -0
  4. package/build/components/activity-indicator/index.android.js +13 -0
  5. package/build/components/activity-indicator/index.d.ts +1 -0
  6. package/build/components/activity-indicator/index.ios.d.ts +2 -0
  7. package/build/components/activity-indicator/index.ios.js +10 -0
  8. package/build/components/activity-indicator/index.js +4 -0
  9. package/build/components/activity-indicator/shared.d.ts +7 -0
  10. package/build/components/activity-indicator/shared.js +20 -0
  11. package/build/components/button.d.ts +6 -0
  12. package/build/components/button.js +42 -0
  13. package/build/components/drawer-layout-android/index.android.d.ts +3 -0
  14. package/build/components/drawer-layout-android/index.android.js +93 -0
  15. package/build/components/drawer-layout-android/index.d.ts +3 -0
  16. package/build/components/drawer-layout-android/index.js +19 -0
  17. package/build/components/drawer-layout-android/shared.d.ts +19 -0
  18. package/build/components/drawer-layout-android/shared.js +7 -0
  19. package/build/components/flat-list/index.d.ts +73 -0
  20. package/build/components/flat-list/index.js +86 -0
  21. package/build/components/image/index.d.ts +9 -0
  22. package/build/components/image/index.js +28 -0
  23. package/build/components/image-background/index.d.ts +10 -0
  24. package/build/components/image-background/index.js +46 -0
  25. package/build/components/input-accessory-view/index.d.ts +11 -0
  26. package/build/components/input-accessory-view/index.js +14 -0
  27. package/build/components/keyboard-avoiding-view/index.d.ts +17 -0
  28. package/build/components/keyboard-avoiding-view/index.js +85 -0
  29. package/build/components/modal/index.d.ts +24 -0
  30. package/build/components/modal/index.js +56 -0
  31. package/build/components/pressable/index.d.ts +30 -0
  32. package/build/components/pressable/index.js +100 -0
  33. package/build/components/refresh-control/index.d.ts +17 -0
  34. package/build/components/refresh-control/index.js +21 -0
  35. package/build/components/safe-area-view/index.d.ts +11 -0
  36. package/build/components/safe-area-view/index.js +18 -0
  37. package/build/components/scroll-view/index.android.d.ts +3 -0
  38. package/build/components/scroll-view/index.android.js +53 -0
  39. package/build/components/scroll-view/index.d.ts +1 -0
  40. package/build/components/scroll-view/index.ios.d.ts +3 -0
  41. package/build/components/scroll-view/index.ios.js +27 -0
  42. package/build/components/scroll-view/index.js +4 -0
  43. package/build/components/scroll-view/shared.d.ts +98 -0
  44. package/build/components/scroll-view/shared.js +195 -0
  45. package/build/components/scroll-view/sticky-header.d.ts +9 -0
  46. package/build/components/scroll-view/sticky-header.js +161 -0
  47. package/build/components/section-list/index.d.ts +8 -0
  48. package/build/components/section-list/index.js +12 -0
  49. package/build/components/switch/index.android.d.ts +2 -0
  50. package/build/components/switch/index.android.js +12 -0
  51. package/build/components/switch/index.d.ts +1 -0
  52. package/build/components/switch/index.ios.d.ts +2 -0
  53. package/build/components/switch/index.ios.js +10 -0
  54. package/build/components/switch/index.js +6 -0
  55. package/build/components/switch/shared.d.ts +10 -0
  56. package/build/components/switch/shared.js +59 -0
  57. package/build/components/text-input/index.d.ts +50 -0
  58. package/build/components/text-input/index.js +167 -0
  59. package/build/components/touchable/index.d.ts +20 -0
  60. package/build/components/touchable/index.js +95 -0
  61. package/build/components/touchable-native-feedback/index.d.ts +18 -0
  62. package/build/components/touchable-native-feedback/index.js +32 -0
  63. package/build/components/virtualized-list/index.d.ts +73 -0
  64. package/build/components/virtualized-list/index.js +552 -0
  65. package/build/components/virtualized-section-list/index.d.ts +66 -0
  66. package/build/components/virtualized-section-list/index.js +99 -0
  67. package/build/components.d.ts +54 -0
  68. package/build/components.js +17 -0
  69. package/build/create-portal.d.ts +4 -0
  70. package/build/create-portal.js +23 -0
  71. package/build/create-tunnel.d.ts +12 -0
  72. package/build/create-tunnel.js +60 -0
  73. package/build/descriptor-to-react/index.d.ts +3 -0
  74. package/build/descriptor-to-react/index.js +15 -0
  75. package/build/hooks/use-color-scheme.d.ts +2 -0
  76. package/build/hooks/use-color-scheme.js +13 -0
  77. package/build/hooks/use-window-dimensions.d.ts +2 -0
  78. package/build/hooks/use-window-dimensions.js +30 -0
  79. package/build/host-config.d.ts +7 -0
  80. package/build/host-config.js +169 -0
  81. package/build/host-instance.d.ts +3 -0
  82. package/build/host-instance.js +20 -0
  83. package/build/index.d.ts +98 -0
  84. package/build/index.js +85 -0
  85. package/build/modules/accessibility-info/index.android.d.ts +2 -0
  86. package/build/modules/accessibility-info/index.android.js +4 -0
  87. package/build/modules/accessibility-info/index.d.ts +1 -0
  88. package/build/modules/accessibility-info/index.ios.d.ts +2 -0
  89. package/build/modules/accessibility-info/index.ios.js +3 -0
  90. package/build/modules/accessibility-info/index.js +5 -0
  91. package/build/modules/accessibility-info/shared.d.ts +1 -0
  92. package/build/modules/accessibility-info/shared.js +1 -0
  93. package/build/modules/action-sheet-ios.d.ts +2 -0
  94. package/build/modules/action-sheet-ios.js +4 -0
  95. package/build/modules/alert.d.ts +2 -0
  96. package/build/modules/alert.js +5 -0
  97. package/build/modules/animated/create-animated-component.d.ts +12 -0
  98. package/build/modules/animated/create-animated-component.js +125 -0
  99. package/build/modules/animated/index.d.ts +52 -0
  100. package/build/modules/animated/index.js +84 -0
  101. package/build/modules/app-registry/index.d.ts +7 -0
  102. package/build/modules/app-registry/index.js +26 -0
  103. package/build/modules/app-state.d.ts +2 -0
  104. package/build/modules/app-state.js +3 -0
  105. package/build/modules/appearance.d.ts +2 -0
  106. package/build/modules/appearance.js +3 -0
  107. package/build/modules/back-handler.d.ts +2 -0
  108. package/build/modules/back-handler.js +3 -0
  109. package/build/modules/dimensions.d.ts +2 -0
  110. package/build/modules/dimensions.js +3 -0
  111. package/build/modules/i18n-manager.d.ts +2 -0
  112. package/build/modules/i18n-manager.js +4 -0
  113. package/build/modules/keyboard.d.ts +2 -0
  114. package/build/modules/keyboard.js +3 -0
  115. package/build/modules/layout-animation.d.ts +2 -0
  116. package/build/modules/layout-animation.js +3 -0
  117. package/build/modules/linking.d.ts +2 -0
  118. package/build/modules/linking.js +4 -0
  119. package/build/modules/permissions-android.d.ts +2 -0
  120. package/build/modules/permissions-android.js +3 -0
  121. package/build/modules/settings.d.ts +1 -0
  122. package/build/modules/settings.js +4 -0
  123. package/build/modules/share.d.ts +2 -0
  124. package/build/modules/share.js +4 -0
  125. package/build/modules/status-bar/index.android.d.ts +3 -0
  126. package/build/modules/status-bar/index.android.js +89 -0
  127. package/build/modules/status-bar/index.d.ts +1 -0
  128. package/build/modules/status-bar/index.ios.d.ts +3 -0
  129. package/build/modules/status-bar/index.ios.js +64 -0
  130. package/build/modules/status-bar/index.js +4 -0
  131. package/build/modules/status-bar/shared.d.ts +6 -0
  132. package/build/modules/status-bar/shared.js +6 -0
  133. package/build/modules/text-input-state.d.ts +1 -0
  134. package/build/modules/text-input-state.js +3 -0
  135. package/build/modules/toast-android.d.ts +1 -0
  136. package/build/modules/toast-android.js +4 -0
  137. package/build/modules/vibration.d.ts +1 -0
  138. package/build/modules/vibration.js +4 -0
  139. package/build/reconciler-constants.d.ts +7 -0
  140. package/build/reconciler-constants.js +12 -0
  141. package/build/render.d.ts +7 -0
  142. package/build/render.js +60 -0
  143. package/build/utils/responder-props.d.ts +1 -0
  144. package/build/utils/responder-props.js +1 -0
  145. package/build/utils/styles.d.ts +1 -0
  146. package/build/utils/styles.js +1 -0
  147. package/metro-css-parser.cjs +11 -0
  148. package/package.json +53 -0
@@ -0,0 +1,50 @@
1
+ import { type ISymbioteEvent } from '@symbiote-native/engine';
2
+ import type { ITextInputProps as ITextInputBaseProps, ITextInputHandle } from '@symbiote-native/components';
3
+ export type { ITextInputHandle } from '@symbiote-native/components';
4
+ export type ITextInputProps = ITextInputBaseProps & {
5
+ className?: string;
6
+ };
7
+ export declare const TextInput: import("react").ForwardRefExoticComponent<import("@symbiote-native/components").IAccessibilityProps & import("@symbiote-native/components").IAriaProps & {
8
+ value?: string;
9
+ defaultValue?: string;
10
+ placeholder?: string;
11
+ placeholderTextColor?: string;
12
+ editable?: boolean;
13
+ keyboardType?: string;
14
+ secureTextEntry?: boolean;
15
+ maxLength?: number;
16
+ multiline?: boolean;
17
+ selection?: import("@symbiote-native/components").ITextInputSelection;
18
+ autoCapitalize?: "none" | "sentences" | "words" | "characters";
19
+ autoCorrect?: boolean;
20
+ autoComplete?: string;
21
+ textContentType?: string;
22
+ autoFocus?: boolean;
23
+ showSoftInputOnFocus?: boolean;
24
+ returnKeyType?: string;
25
+ selectTextOnFocus?: boolean;
26
+ scrollEnabled?: boolean;
27
+ numberOfLines?: number;
28
+ textAlign?: "left" | "center" | "right";
29
+ blurOnSubmit?: boolean;
30
+ inputMode?: import("@symbiote-native/components").IInputMode;
31
+ enterKeyHint?: import("@symbiote-native/components").IEnterKeyHint;
32
+ readOnly?: boolean;
33
+ submitBehavior?: import("@symbiote-native/components").ISubmitBehavior;
34
+ cursorColor?: string;
35
+ selectionColor?: string;
36
+ selectionHandleColor?: string;
37
+ underlineColorAndroid?: string;
38
+ inputAccessoryViewID?: string;
39
+ style?: import("@symbiote-native/engine").ITextStyle;
40
+ onValueChange?: (text: string, event: ISymbioteEvent) => void;
41
+ onFocus?: import("@symbiote-native/components").ITextInputEventHandler;
42
+ onBlur?: import("@symbiote-native/components").ITextInputEventHandler;
43
+ onEndEditing?: import("@symbiote-native/components").ITextInputEventHandler;
44
+ onSubmitEditing?: import("@symbiote-native/components").ITextInputEventHandler;
45
+ onKeyPress?: import("@symbiote-native/components").ITextInputEventHandler;
46
+ onSelectionChange?: import("@symbiote-native/components").ITextInputEventHandler;
47
+ onContentSizeChange?: import("@symbiote-native/components").ITextInputEventHandler;
48
+ } & {
49
+ className?: string;
50
+ } & import("react").RefAttributes<ITextInputHandle>>;
@@ -0,0 +1,167 @@
1
+ // TextInput: the React lifecycle half. The folds/maps (value->text, the W3C/alias resolution)
2
+ // and the controlled-write predicate live in @symbiote-native/components/state; the render (intrinsic
3
+ // + native-prop mapping) in @symbiote-native/components/view; both shared verbatim with the Vue
4
+ // adapter. Here React supplies only the lifecycle: useState for the acknowledged event count
5
+ // (it must re-render so the imperative handle echoes the latest), a ref for the host node and
6
+ // the last text native holds, the useLayoutEffect controlled-write, the mount autoFocus, and
7
+ // useImperativeHandle for focus/blur/clear/isFocused/setSelection.
8
+ //
9
+ // The controlled handshake hinges on commanding native back with the ACKNOWLEDGED event count
10
+ // (setTextAndSelection), never a plain prop re-push. See @symbiote-native/components/state/text-input.
11
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState, } from 'react';
12
+ import { dispatchViewCommand, dlog, } from '@symbiote-native/engine';
13
+ import { blurTextInput, setInputBlurred, setInputFocused } from '../../modules/text-input-state';
14
+ import { resolveAccessibilityProps, resolveTextInputProps, renderTextInput, foldText, textFromChange, eventCountFromChange, shouldCommandText, INITIAL_EVENT_COUNT, SELECTION_NONE, } from '@symbiote-native/components';
15
+ import { descriptorToReact } from '../../descriptor-to-react';
16
+ export const TextInput = forwardRef((rawProps, forwardedRef) => {
17
+ // TextInput is its own host element (not a View wrapper), so it folds aria/role here.
18
+ const props = resolveAccessibilityProps(rawProps);
19
+ // Pull out the fields the lifecycle owns (controlled value, the wrapped handlers, the folded
20
+ // aliases) so they don't reach Fabric raw; everything else rides down via `passthrough`.
21
+ const { value, defaultValue, multiline, selection, onValueChange, onFocus, onBlur, inputMode, enterKeyHint, readOnly, submitBehavior, blurOnSubmit, cursorColor, selectionColor, selectionHandleColor, keyboardType, returnKeyType, editable, autoComplete, textContentType, autoFocus, showSoftInputOnFocus, underlineColorAndroid, ...passthrough } = props;
22
+ const isMultiline = multiline === true;
23
+ const folded = resolveTextInputProps({
24
+ inputMode,
25
+ keyboardType,
26
+ enterKeyHint,
27
+ returnKeyType,
28
+ readOnly,
29
+ editable,
30
+ submitBehavior,
31
+ blurOnSubmit,
32
+ multiline: isMultiline,
33
+ cursorColor,
34
+ selectionColor,
35
+ selectionHandleColor,
36
+ autoComplete,
37
+ textContentType,
38
+ showSoftInputOnFocus,
39
+ underlineColorAndroid,
40
+ });
41
+ const ref = useRef(null);
42
+ // JS-side focus state, mirrored from the focus/blur events for isFocused(). RN's
43
+ // TextInputState holds the same; native exposes no synchronous focus getter.
44
+ const focused = useRef(false);
45
+ // The count native last acknowledged. We echo it back on every controlled write so native's
46
+ // eventLag lands on 0 and the write applies.
47
+ const [mostRecentEventCount, setMostRecentEventCount] = useState(INITIAL_EVENT_COUNT);
48
+ // The last text native holds, as far as JS knows. Seeded from the mount-time value because the
49
+ // `text` prop already carries that value down via createNode, so the FIRST controlled value is
50
+ // not a divergence and must NOT re-command. Only later, post-keystroke divergences flow through
51
+ // setTextAndSelection.
52
+ const lastNativeText = useRef(foldText(value, defaultValue));
53
+ const handleChange = useCallback((event) => {
54
+ // Event seam: the controlled handshake hinges on the change payload carrying `text`
55
+ // (+ `eventCount`). iOS and Android Fabric can key these differently, so log the actual
56
+ // shape here; a missing `text` means onValueChange never fires.
57
+ dlog(`TextInput change keys=[${Object.keys(event.nativeEvent).join(',')}] ` +
58
+ `text=${JSON.stringify(event.nativeEvent.text)} count=${JSON.stringify(event.nativeEvent.eventCount)}`);
59
+ const text = textFromChange(event);
60
+ if (text !== undefined) {
61
+ lastNativeText.current = text;
62
+ onValueChange?.(text, event);
63
+ }
64
+ // Ordering matters: record the text first, then bump the acknowledged count, so the count
65
+ // never runs ahead of the text it stands for.
66
+ const count = eventCountFromChange(event);
67
+ if (count !== undefined)
68
+ setMostRecentEventCount(count);
69
+ }, [onValueChange]);
70
+ const text = foldText(value, defaultValue);
71
+ // Controlled write: when JS-side `value` diverges from what native reported, command the new
72
+ // text down with the acknowledged count. A plain prop re-push would race the user's keystrokes;
73
+ // the command is the only stale-safe path. No deps; it runs after every render and the
74
+ // predicate makes it a no-op unless the text diverged.
75
+ useLayoutEffect(() => {
76
+ const node = ref.current;
77
+ if (node === null)
78
+ return;
79
+ if (!shouldCommandText(lastNativeText.current, value))
80
+ return;
81
+ const selStart = selection?.start ?? SELECTION_NONE;
82
+ const selEnd = selection?.end ?? selection?.start ?? SELECTION_NONE;
83
+ dlog(`TextInput setTextAndSelection count=${mostRecentEventCount} text=${JSON.stringify(value)}`);
84
+ dispatchViewCommand(node, 'setTextAndSelection', [
85
+ mostRecentEventCount,
86
+ value,
87
+ selStart,
88
+ selEnd,
89
+ ]);
90
+ lastNativeText.current = value;
91
+ });
92
+ // autoFocus is driven in JS, not as a native prop: on mount, command `focus` down once (RN does
93
+ // the same via TextInputState.focusInput, TextInput.js:538). Empty deps so it fires only on
94
+ // mount; the native `focus` command is idempotent if already focused.
95
+ useEffect(() => {
96
+ if (autoFocus !== true)
97
+ return;
98
+ const node = ref.current;
99
+ if (node === null)
100
+ return;
101
+ dlog('TextInput autoFocus -> focus command');
102
+ dispatchViewCommand(node, 'focus', []);
103
+ // eslint-disable-next-line react-hooks/exhaustive-deps
104
+ }, []);
105
+ const handleFocus = useCallback((event) => {
106
+ focused.current = true;
107
+ // Track focus app-wide so Keyboard.dismiss can blur this input without a ref.
108
+ if (ref.current !== null)
109
+ setInputFocused(ref.current);
110
+ onFocus?.(event);
111
+ }, [onFocus]);
112
+ const handleBlur = useCallback((event) => {
113
+ focused.current = false;
114
+ if (ref.current !== null)
115
+ setInputBlurred(ref.current);
116
+ onBlur?.(event);
117
+ }, [onBlur]);
118
+ // The imperative API RN exposes on the ref. focus/blur drive native view commands; clear and
119
+ // setSelection reuse setTextAndSelection (the same stale-safe path as a controlled write),
120
+ // echoing the acknowledged event count so native applies them.
121
+ useImperativeHandle(forwardedRef, () => ({
122
+ focus: () => {
123
+ const node = ref.current;
124
+ if (node !== null)
125
+ dispatchViewCommand(node, 'focus', []);
126
+ },
127
+ blur: () => {
128
+ // Routes through TextInputState so the app-wide focus tracking clears too.
129
+ blurTextInput(ref.current);
130
+ },
131
+ clear: () => {
132
+ const node = ref.current;
133
+ if (node === null)
134
+ return;
135
+ dispatchViewCommand(node, 'setTextAndSelection', [mostRecentEventCount, '', 0, 0]);
136
+ lastNativeText.current = '';
137
+ },
138
+ isFocused: () => focused.current,
139
+ setSelection: (start, end) => {
140
+ const node = ref.current;
141
+ if (node === null)
142
+ return;
143
+ const current = lastNativeText.current ?? '';
144
+ dispatchViewCommand(node, 'setTextAndSelection', [
145
+ mostRecentEventCount,
146
+ current,
147
+ start,
148
+ end,
149
+ ]);
150
+ },
151
+ }), [mostRecentEventCount]);
152
+ return descriptorToReact(renderTextInput({
153
+ multiline: isMultiline,
154
+ text,
155
+ mostRecentEventCount,
156
+ selection,
157
+ folded,
158
+ passthrough: {
159
+ ...passthrough,
160
+ ref,
161
+ onChange: handleChange,
162
+ onFocus: handleFocus,
163
+ onBlur: handleBlur,
164
+ },
165
+ }));
166
+ });
167
+ TextInput.displayName = 'TextInput';
@@ -0,0 +1,20 @@
1
+ import { type FC, type ReactNode } from 'react';
2
+ import { type IPressTimingProps } from '@symbiote-native/components';
3
+ import { type IPressableProps } from '../pressable';
4
+ import type { IStyleProp, IViewStyle } from '../../utils/styles';
5
+ type ITouchableBaseProps = Omit<IPressableProps, 'style' | 'children'> & IPressTimingProps & {
6
+ style?: IStyleProp<IViewStyle>;
7
+ children?: ReactNode;
8
+ };
9
+ export interface ITouchableOpacityProps extends ITouchableBaseProps {
10
+ activeOpacity?: number;
11
+ }
12
+ export declare const TouchableOpacity: FC<ITouchableOpacityProps>;
13
+ export interface ITouchableHighlightProps extends ITouchableBaseProps {
14
+ activeOpacity?: number;
15
+ underlayColor?: string;
16
+ }
17
+ export declare const TouchableHighlight: FC<ITouchableHighlightProps>;
18
+ export type ITouchableWithoutFeedbackProps = ITouchableBaseProps;
19
+ export declare const TouchableWithoutFeedback: FC<ITouchableWithoutFeedbackProps>;
20
+ export {};
@@ -0,0 +1,95 @@
1
+ // The Touchable* family, all built on Pressable. RN realizes their feedback with Animated, and so
2
+ // do we. The press-timing constants and the deactivation-floor math are shared with every adapter
3
+ // (@symbiote-native/components/state/touchable); here React owns only the Animated wiring + the press
4
+ // scheduling refs:
5
+ // TouchableOpacity: animate an Animated.Value opacity toward activeOpacity on press-in and
6
+ // back to 1 on press-out, driven imperatively from onPressIn/onPressOut.
7
+ // TouchableHighlight: paint underlayColor + lower child opacity while pressed, via Pressable's
8
+ // pressed-state style (RN drives this with setState, not Animated, faithfully).
9
+ // TouchableWithoutFeedback: no visual change, just the press wiring.
10
+ import { createElement, useRef } from 'react';
11
+ import { dlog } from '@symbiote-native/engine';
12
+ import { computePressOutWait, DEFAULT_ACTIVE_OPACITY, DEFAULT_HIGHLIGHT_CHILD_OPACITY, DEFAULT_MIN_PRESS_DURATION_MS, DEFAULT_UNDERLAY_COLOR, OPACITY_ACTIVE_DURATION_MS, OPACITY_INACTIVE_DURATION_MS, RESTING_OPACITY, } from '@symbiote-native/components';
13
+ import { Pressable } from '../pressable';
14
+ import { Animated } from '../../modules/animated';
15
+ export const TouchableOpacity = props => {
16
+ const { activeOpacity = DEFAULT_ACTIVE_OPACITY, style,
17
+ // className is pulled out here, like style, and applied to the inner Animated.View below —
18
+ // left in ...rest it would land on the outer Pressable instead, which is not where `style`
19
+ // (the opacity-fade node) goes.
20
+ className, children, onPressIn, onPressOut, delayPressIn = 0, delayPressOut = 0, minPressDuration = DEFAULT_MIN_PRESS_DURATION_MS, ...rest } = props;
21
+ // One Animated.Value per mount, resting at full opacity. The Animated.View leaf commits its
22
+ // current value every frame, so timing it animates the real view.
23
+ const opacity = useRef(new Animated.Value(RESTING_OPACITY)).current;
24
+ // The pending delayPressIn timer, so a release before it fires can flush it.
25
+ const pressInTimer = useRef(undefined);
26
+ // When the active visual actually started, to floor onPressOut by minPressDuration.
27
+ const activatedAt = useRef(undefined);
28
+ function setOpacityTo(toValue, duration) {
29
+ Animated.timing(opacity, {
30
+ toValue,
31
+ duration,
32
+ easing: Animated.Easing.inOut(Animated.Easing.quad),
33
+ useNativeDriver: false,
34
+ }).start();
35
+ }
36
+ function clearPressInTimer() {
37
+ if (pressInTimer.current !== undefined) {
38
+ clearTimeout(pressInTimer.current);
39
+ pressInTimer.current = undefined;
40
+ }
41
+ }
42
+ function activate(event) {
43
+ activatedAt.current = Date.now();
44
+ setOpacityTo(activeOpacity, OPACITY_ACTIVE_DURATION_MS);
45
+ onPressIn?.(event);
46
+ }
47
+ function deactivate(event) {
48
+ activatedAt.current = undefined;
49
+ setOpacityTo(RESTING_OPACITY, OPACITY_INACTIVE_DURATION_MS);
50
+ onPressOut?.(event);
51
+ }
52
+ // RN's _createPressabilityConfig forwards delayPressIn: defer the active visual and onPressIn
53
+ // behind the delay (a release before it elapses flushes it synchronously).
54
+ function handlePressIn(event) {
55
+ if (delayPressIn > 0) {
56
+ dlog(`TouchableOpacity pressIn deferred ${delayPressIn}ms`);
57
+ pressInTimer.current = setTimeout(() => {
58
+ pressInTimer.current = undefined;
59
+ activate(event);
60
+ }, delayPressIn);
61
+ return;
62
+ }
63
+ activate(event);
64
+ }
65
+ // delayPressOut + minPressDuration (RN _deactivate): the press-out waits at least minPress-
66
+ // Duration past activation (so a fast tap holds the active visual) and at least delayPressOut.
67
+ function handlePressOut(event) {
68
+ if (pressInTimer.current !== undefined) {
69
+ clearPressInTimer();
70
+ activate(event);
71
+ }
72
+ const heldFor = activatedAt.current === undefined ? 0 : Date.now() - activatedAt.current;
73
+ const wait = computePressOutWait(heldFor, minPressDuration, delayPressOut);
74
+ if (wait > 0) {
75
+ dlog(`TouchableOpacity pressOut deferred ${wait}ms`);
76
+ setTimeout(() => deactivate(event), wait);
77
+ return;
78
+ }
79
+ deactivate(event);
80
+ }
81
+ return createElement(Pressable, { ...rest, onPressIn: handlePressIn, onPressOut: handlePressOut }, createElement(Animated.View, { style: [style, { opacity }], className }, children));
82
+ };
83
+ export const TouchableHighlight = props => {
84
+ const { activeOpacity = DEFAULT_HIGHLIGHT_CHILD_OPACITY, underlayColor = DEFAULT_UNDERLAY_COLOR, style, children, ...rest } = props;
85
+ function pressedStyle({ pressed }) {
86
+ if (!pressed)
87
+ return style;
88
+ return [style, { backgroundColor: underlayColor, opacity: activeOpacity }];
89
+ }
90
+ return createElement(Pressable, { ...rest, style: pressedStyle }, children);
91
+ };
92
+ export const TouchableWithoutFeedback = props => {
93
+ const { children, ...rest } = props;
94
+ return createElement(Pressable, rest, children);
95
+ };
@@ -0,0 +1,18 @@
1
+ import { type FC, type ReactNode } from 'react';
2
+ import { type INativeFeedbackBackground, type IThemeAttrBackground, type IRippleBackground } from '@symbiote-native/components';
3
+ import { type IPressableProps } from '../pressable';
4
+ export type { INativeFeedbackBackground, IThemeAttrBackground, IRippleBackground, } from '@symbiote-native/components';
5
+ type ITouchableNativeFeedbackBaseProps = Omit<IPressableProps, 'style' | 'children'> & {
6
+ background?: INativeFeedbackBackground;
7
+ useForeground?: boolean;
8
+ children?: ReactNode;
9
+ };
10
+ export type ITouchableNativeFeedbackProps = ITouchableNativeFeedbackBaseProps;
11
+ interface ITouchableNativeFeedbackComponent extends FC<ITouchableNativeFeedbackProps> {
12
+ SelectableBackground: (rippleRadius?: number) => IThemeAttrBackground;
13
+ SelectableBackgroundBorderless: (rippleRadius?: number) => IThemeAttrBackground;
14
+ Ripple: (color: string, borderless: boolean, rippleRadius?: number) => IRippleBackground;
15
+ canUseNativeForeground: () => boolean;
16
+ }
17
+ declare const TouchableNativeFeedback: ITouchableNativeFeedbackComponent;
18
+ export { TouchableNativeFeedback };
@@ -0,0 +1,32 @@
1
+ // TouchableNativeFeedback: Android's ripple/state-drawable touchable, built on Pressable like the
2
+ // rest of the family. RN realizes its feedback by cloning the child into an RCTView carrying native
3
+ // ripple props; we instead nest the child under a feedback View that carries those props, inside a
4
+ // Pressable that owns the press wiring. The native props (nativeBackgroundAndroid /
5
+ // nativeForegroundAndroid) are read by Android's ReactViewManager; on iOS they are inert props, so
6
+ // the component still renders its child with working press wiring. The static factories +
7
+ // background mapping are shared in @symbiote-native/components/view. React only attaches them onto the
8
+ // component value and nests the feedback View.
9
+ import { createElement } from 'react';
10
+ import { dlog } from '@symbiote-native/engine';
11
+ import { backgroundProps, canUseNativeForeground, rippleBackground, selectableBackground, selectableBackgroundBorderless, } from '@symbiote-native/components';
12
+ import { View } from '../../components';
13
+ import { Pressable } from '../pressable';
14
+ const TouchableNativeFeedbackImpl = props => {
15
+ const { background, useForeground = false, children, ...rest } = props;
16
+ // RN defaults a missing background to SelectableBackground() so the touchable always has
17
+ // feedback; mirror that here.
18
+ const resolved = background ?? selectableBackground();
19
+ dlog(`TouchableNativeFeedback render ${resolved.type} useForeground ${useForeground}`);
20
+ const nativeProps = backgroundProps(resolved, useForeground);
21
+ // The native ripple props ride a dedicated feedback View nested under the Pressable; the
22
+ // Pressable owns the press wiring, the feedback View owns the drawable.
23
+ const feedback = createElement(View, nativeProps, children);
24
+ return createElement(Pressable, rest, feedback);
25
+ };
26
+ const TouchableNativeFeedback = Object.assign(TouchableNativeFeedbackImpl, {
27
+ SelectableBackground: selectableBackground,
28
+ SelectableBackgroundBorderless: selectableBackgroundBorderless,
29
+ Ripple: rippleBackground,
30
+ canUseNativeForeground,
31
+ });
32
+ export { TouchableNativeFeedback };
@@ -0,0 +1,73 @@
1
+ import { type ComponentType, type ReactElement, type ReactNode, type Ref } from 'react';
2
+ import { type ISymbioteEvent } from '@symbiote-native/engine';
3
+ import { type ICellLayout, type ISeparators, type ISeparatorProps, type IViewToken, type IViewableItemsChangedInfo, type IViewabilityConfig, type IViewabilityConfigCallbackPair, type IVirtualizedListHandle } from '@symbiote-native/components';
4
+ import type { IAccessibilityProps, IAriaProps } from '@symbiote-native/components';
5
+ import type { IStyleProp, IViewStyle } from '../../utils/styles';
6
+ export type { ICellLayout, ISeparators, ISeparatorProps, IViewToken, IViewableItemsChangedInfo, IViewabilityConfig, IViewabilityConfigCallbackPair, IVirtualizedListHandle, };
7
+ type IRenderItem<ItemT> = (info: {
8
+ item: ItemT;
9
+ index: number;
10
+ separators: ISeparators;
11
+ }) => ReactNode;
12
+ export interface IVirtualizedListProps<ItemT> extends IAccessibilityProps, IAriaProps {
13
+ data: unknown;
14
+ getItem: (data: unknown, index: number) => ItemT;
15
+ getItemCount: (data: unknown) => number;
16
+ renderItem: IRenderItem<ItemT>;
17
+ keyExtractor?: (item: ItemT, index: number) => string;
18
+ getItemLayout?: (data: unknown, index: number) => {
19
+ length: number;
20
+ offset: number;
21
+ index: number;
22
+ };
23
+ ItemSeparatorComponent?: ComponentType<ISeparatorProps<ItemT>>;
24
+ ListHeaderComponent?: ComponentType<Record<string, never>> | ReactElement;
25
+ ListFooterComponent?: ComponentType<Record<string, never>> | ReactElement;
26
+ ListEmptyComponent?: ComponentType<Record<string, never>> | ReactElement;
27
+ horizontal?: boolean;
28
+ inverted?: boolean;
29
+ extraData?: unknown;
30
+ onEndReached?: (info: {
31
+ distanceFromEnd: number;
32
+ }) => void;
33
+ onEndReachedThreshold?: number;
34
+ onStartReached?: (info: {
35
+ distanceFromStart: number;
36
+ }) => void;
37
+ onStartReachedThreshold?: number;
38
+ onRefresh?: () => void;
39
+ refreshing?: boolean | null;
40
+ progressViewOffset?: number;
41
+ onViewableItemsChanged?: (info: IViewableItemsChangedInfo<ItemT>) => void;
42
+ viewabilityConfig?: IViewabilityConfig;
43
+ viewabilityConfigCallbackPairs?: IViewabilityConfigCallbackPair<ItemT>[];
44
+ onScrollToIndexFailed?: (info: {
45
+ index: number;
46
+ highestMeasuredFrameIndex: number;
47
+ averageItemLength: number;
48
+ }) => void;
49
+ initialNumToRender?: number;
50
+ initialScrollIndex?: number;
51
+ maxToRenderPerBatch?: number;
52
+ updateCellsBatchingPeriod?: number;
53
+ windowSize?: number;
54
+ stickyHeaderIndices?: number[];
55
+ maintainVisibleContentPosition?: {
56
+ minIndexForVisible: number;
57
+ autoscrollToTopThreshold?: number;
58
+ };
59
+ onScroll?: (event: ISymbioteEvent) => void;
60
+ onScrollBeginDrag?: (event: ISymbioteEvent) => void;
61
+ onScrollEndDrag?: (event: ISymbioteEvent) => void;
62
+ onMomentumScrollBegin?: (event: ISymbioteEvent) => void;
63
+ onMomentumScrollEnd?: (event: ISymbioteEvent) => void;
64
+ scrollEventThrottle?: number;
65
+ keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
66
+ keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
67
+ style?: IStyleProp<IViewStyle>;
68
+ contentContainerStyle?: IStyleProp<IViewStyle>;
69
+ className?: string;
70
+ }
71
+ export declare function VirtualizedList<ItemT>(props: IVirtualizedListProps<ItemT> & {
72
+ ref?: Ref<IVirtualizedListHandle>;
73
+ }): ReactElement;