@symbiote-native/react 0.4.0 → 2.0.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 (102) hide show
  1. package/README.md +32 -16
  2. package/build/components/activity-indicator-props.d.ts +4 -0
  3. package/build/components/activity-indicator-props.js +1 -0
  4. package/build/components/{button.d.ts → button-props.d.ts} +0 -2
  5. package/build/components/button-props.js +1 -0
  6. package/build/components/flat-list/index.js +2 -2
  7. package/build/components/image/image-props.d.ts +4 -0
  8. package/build/components/image/image-props.js +1 -0
  9. package/build/components/image-background-props.d.ts +9 -0
  10. package/build/components/image-background-props.js +1 -0
  11. package/build/components/input-accessory-view-props.d.ts +10 -0
  12. package/build/components/input-accessory-view-props.js +1 -0
  13. package/build/components/keyboard-avoiding-view/index.js +4 -4
  14. package/build/components/modal/index.js +3 -3
  15. package/build/components/pressable/{index.d.ts → pressable-props.d.ts} +4 -4
  16. package/build/components/pressable/pressable-props.js +1 -0
  17. package/build/components/{refresh-control/index.d.ts → refresh-control-props.d.ts} +3 -3
  18. package/build/components/refresh-control-props.js +1 -0
  19. package/build/components/safe-area-view-props.d.ts +11 -0
  20. package/build/components/safe-area-view-props.js +1 -0
  21. package/build/components/scroll-view/{shared.d.ts → scroll-view-props.d.ts} +4 -26
  22. package/build/components/scroll-view/scroll-view-props.js +1 -0
  23. package/build/components/switch/switch-props.d.ts +4 -0
  24. package/build/components/switch/switch-props.js +1 -0
  25. package/build/components/text-input/text-input-props.d.ts +4 -0
  26. package/build/components/text-input/text-input-props.js +1 -0
  27. package/build/components/touchable/touchable-props.d.ts +17 -0
  28. package/build/components/touchable/touchable-props.js +1 -0
  29. package/build/components/touchable-native-feedback/touchable-native-feedback-props.d.ts +8 -0
  30. package/build/components/touchable-native-feedback/touchable-native-feedback-props.js +1 -0
  31. package/build/components/touchable-without-feedback/touchable-without-feedback-props.d.ts +6 -0
  32. package/build/components/touchable-without-feedback/touchable-without-feedback-props.js +1 -0
  33. package/build/components/virtualized-list/index.js +83 -38
  34. package/build/components.d.ts +3 -4
  35. package/build/components.js +5 -17
  36. package/build/create-portal/index.js +1 -1
  37. package/build/create-tunnel/index.js +1 -1
  38. package/build/descriptor-to-react/index.js +1 -1
  39. package/build/host-config.d.ts +2 -2
  40. package/build/host-config.js +51 -10
  41. package/build/host-instance.js +1 -1
  42. package/build/index.d.ts +23 -27
  43. package/build/index.js +14 -14
  44. package/build/jsx-dev-runtime.d.ts +3 -0
  45. package/build/jsx-dev-runtime.js +10 -0
  46. package/build/jsx-namespace.type-check.d.ts +1 -0
  47. package/build/jsx-namespace.type-check.js +7 -0
  48. package/build/jsx-runtime.d.ts +62 -0
  49. package/build/jsx-runtime.js +1 -0
  50. package/build/modules/animated/create-animated-component.d.ts +2 -1
  51. package/build/modules/animated/create-animated-component.js +7 -1
  52. package/build/modules/animated/index.d.ts +0 -1
  53. package/build/modules/animated/index.js +16 -22
  54. package/build/modules/image/index.d.ts +3 -0
  55. package/build/modules/image/index.js +14 -0
  56. package/build/register.d.ts +1 -0
  57. package/build/register.js +15 -0
  58. package/build/render.js +2 -4
  59. package/metro-css-parser.cjs +2 -1
  60. package/package.json +28 -8
  61. package/build/components/activity-indicator/index.android.d.ts +0 -3
  62. package/build/components/activity-indicator/index.android.js +0 -17
  63. package/build/components/activity-indicator/index.d.ts +0 -1
  64. package/build/components/activity-indicator/index.ios.d.ts +0 -3
  65. package/build/components/activity-indicator/index.ios.js +0 -16
  66. package/build/components/activity-indicator/index.js +0 -4
  67. package/build/components/activity-indicator/shared.d.ts +0 -6
  68. package/build/components/activity-indicator/shared.js +0 -22
  69. package/build/components/button.js +0 -42
  70. package/build/components/image/index.d.ts +0 -9
  71. package/build/components/image/index.js +0 -28
  72. package/build/components/image-background/index.d.ts +0 -10
  73. package/build/components/image-background/index.js +0 -46
  74. package/build/components/input-accessory-view/index.d.ts +0 -11
  75. package/build/components/input-accessory-view/index.js +0 -19
  76. package/build/components/pressable/index.js +0 -100
  77. package/build/components/refresh-control/index.js +0 -21
  78. package/build/components/safe-area-view/index.d.ts +0 -11
  79. package/build/components/safe-area-view/index.js +0 -18
  80. package/build/components/scroll-view/index.android.d.ts +0 -3
  81. package/build/components/scroll-view/index.android.js +0 -61
  82. package/build/components/scroll-view/index.d.ts +0 -1
  83. package/build/components/scroll-view/index.ios.d.ts +0 -3
  84. package/build/components/scroll-view/index.ios.js +0 -29
  85. package/build/components/scroll-view/index.js +0 -4
  86. package/build/components/scroll-view/shared.js +0 -240
  87. package/build/components/scroll-view/sticky-header.d.ts +0 -18
  88. package/build/components/scroll-view/sticky-header.js +0 -227
  89. package/build/components/switch/index.android.d.ts +0 -3
  90. package/build/components/switch/index.android.js +0 -18
  91. package/build/components/switch/index.d.ts +0 -1
  92. package/build/components/switch/index.ios.d.ts +0 -3
  93. package/build/components/switch/index.ios.js +0 -16
  94. package/build/components/switch/index.js +0 -6
  95. package/build/components/switch/shared.d.ts +0 -9
  96. package/build/components/switch/shared.js +0 -60
  97. package/build/components/text-input/index.d.ts +0 -50
  98. package/build/components/text-input/index.js +0 -172
  99. package/build/components/touchable/index.d.ts +0 -22
  100. package/build/components/touchable/index.js +0 -165
  101. package/build/components/touchable-native-feedback/index.d.ts +0 -18
  102. package/build/components/touchable-native-feedback/index.js +0 -32
@@ -1,227 +0,0 @@
1
- // Sticky headers: the JS layer RN implements in ScrollView.js / ScrollViewStickyHeader.js.
2
- //
3
- // RN does stickiness purely in JS: ScrollView.js (~line 1690) wraps each flagged child in a
4
- // ScrollViewStickyHeader, fed by a single `_scrollAnimatedValue` that Animated.event drives
5
- // from `onScroll` (~line 1095). The native Fabric scroll view does not honor the index array,
6
- // so forwarding `stickyHeaderIndices` to native would be a silent no-op - we replicate the JS
7
- // layer instead. The interpolation mirrors ScrollViewStickyHeader.js and now lives,
8
- // framework-agnostic, in @symbiote-native/components (computeStickyInterpolation); this file
9
- // holds only the React component shell, layout state, and child-wrapping.
10
- import { Children, createElement, isValidElement, useCallback, useEffect, useImperativeHandle, useReducer, useRef, useState, } from 'react';
11
- import { AnimatedInterpolation, AnimatedValue, Platform, dlog, } from '@symbiote-native/engine';
12
- import { createInitialStickyState, nextStickyHeaderY, readLayoutNumber, reduceSticky, STICKY_HEADER_Z_INDEX, } from '@symbiote-native/components';
13
- import { Animated } from '../../modules/animated/index.js';
14
- function readChildOnLayout(child) {
15
- const childProps = child.props;
16
- if (typeof childProps !== 'object' || childProps === null)
17
- return undefined;
18
- const handler = Reflect.get(childProps, 'onLayout');
19
- return typeof handler === 'function' ? handler : undefined;
20
- }
21
- function firstChild(children) {
22
- const first = Children.toArray(children)[0];
23
- return isValidElement(first) ? first : undefined;
24
- }
25
- // One sticky header. Measures its own y/height via onLayout, interpolates the shared scroll
26
- // offset into a translateY that keeps it pinned to the top (or bottom, inverted) until the next
27
- // header collides with it, and drives that translate through the native driver when available so
28
- // the pin tracks scroll on the UI thread (no JS jitter). The DECISIONS — the zero-swallow gate, the
29
- // debounce delay, the rebuild-on-input-change ranges — live in `reduceSticky`
30
- // (@symbiote-native/components); this component supplies only the React lifecycle: the ONE folded
31
- // state cell, the interpolation-node + listener wiring, the debounce setTimeout, and the re-render.
32
- export const ScrollViewStickyHeader = props => {
33
- const { inverted, scrollViewHeight, scrollAnimatedValue, nextHeaderLayoutY, children, ref, } = props;
34
- // The next header's y lives HERE, not in a prop the parent re-renders to update (RN keeps it in
35
- // the child too, ScrollViewStickyHeader.js:65). The prop only seeds the first render; afterwards
36
- // the ScrollView pushes updates through setNextHeaderY. That keeps the cost linear - a
37
- // parent-state round trip re-renders every child on every header's layout, so N headers cost N
38
- // re-renders of N children. At N=200 it churned enough native animated nodes to trip Fabric's view
39
- // registry ("Attempt to dequeue already registered component") seconds after mount.
40
- const [nextHeaderY, setNextHeaderY] = useState(nextHeaderLayoutY);
41
- useImperativeHandle(ref, () => ({ setNextHeaderY }), []);
42
- // The imperative push is a fast path, not the source of truth. When the parent DOES re-render
43
- // with a different value the prop wins, because the successor may be gone: a VirtualizedList
44
- // unmounts headers as they scroll out, and a value pushed in earlier would otherwise stick
45
- // around as a collision point that no longer exists.
46
- const lastNextHeaderProp = useRef(nextHeaderLayoutY);
47
- if (lastNextHeaderProp.current !== nextHeaderLayoutY) {
48
- lastNextHeaderProp.current = nextHeaderLayoutY;
49
- setNextHeaderY(nextHeaderLayoutY);
50
- }
51
- // The one folded state cell (RN's scattered useState/useRef collapsed into IStickyHeaderState),
52
- // mutated in place by reduceSticky. Lazily created once.
53
- const stateRef = useRef(null);
54
- const state = (stateRef.current ??= createInitialStickyState());
55
- const [, forceRender] = useReducer((tick) => tick + 1, 0);
56
- // The animated node that drives the transform (RN's animatedTranslateY), rebuilt by the
57
- // rebuild-interpolation effect. When the scroll value is native, this interpolation runs on the
58
- // UI thread: the smooth pin.
59
- const [animatedTranslateY, setAnimatedTranslateY] = useState(() => scrollAnimatedValue.interpolate({
60
- inputRange: [-1, 0],
61
- outputRange: [0, 0],
62
- }));
63
- const debounceTimer = useRef(null);
64
- // The current interpolation node + its settled-value listener id, held so the next rebuild can
65
- // detach the old listener (engine calls the reducer does NOT own) and unmount can clean up.
66
- const interpolationRef = useRef(null);
67
- const listenerIdRef = useRef(null);
68
- const inputsRef = useRef({
69
- os: Platform.OS,
70
- inverted,
71
- scrollViewHeight,
72
- nextHeaderLayoutY: nextHeaderY,
73
- });
74
- inputsRef.current = {
75
- os: Platform.OS,
76
- inverted,
77
- scrollViewHeight,
78
- nextHeaderLayoutY: nextHeaderY,
79
- };
80
- // dispatch reaches through a ref because the effect executors dispatch follow-up actions
81
- // (the listener -> animated-tick, the debounce timer -> debounce-fired).
82
- const dispatchRef = useRef(() => { });
83
- const runEffects = useCallback((effects) => {
84
- for (const effect of effects) {
85
- switch (effect.kind) {
86
- case 'rebuild-interpolation': {
87
- // Detach the old listener, build a fresh interpolation onto the shared scroll value, and
88
- // wire the settled-value listener (symbiote is always Fabric; RN attaches it only there).
89
- const previous = interpolationRef.current;
90
- if (previous !== null && listenerIdRef.current !== null) {
91
- previous.removeListener(listenerIdRef.current);
92
- listenerIdRef.current = null;
93
- }
94
- const next = scrollAnimatedValue.interpolate({
95
- inputRange: effect.inputRange,
96
- outputRange: effect.outputRange,
97
- });
98
- listenerIdRef.current = next.addListener(({ value }) => {
99
- if (typeof value === 'number')
100
- dispatchRef.current({ kind: 'animated-tick', value });
101
- });
102
- interpolationRef.current = next;
103
- setAnimatedTranslateY(next);
104
- break;
105
- }
106
- case 'schedule-debounce': {
107
- // The animated value updates several times per frame; debounce the settled value into the
108
- // committed transform so hit detection stays current (a Fabric issue, worse on Android).
109
- if (debounceTimer.current !== null)
110
- clearTimeout(debounceTimer.current);
111
- debounceTimer.current = setTimeout(() => {
112
- debounceTimer.current = null;
113
- dispatchRef.current({
114
- kind: 'debounce-fired',
115
- value: effect.value,
116
- });
117
- }, effect.delay);
118
- break;
119
- }
120
- case 'apply-passthrough':
121
- forceRender();
122
- break;
123
- case 'record-header-y':
124
- // React records through the wrapper's onLayout closure (props.onLayout, below), which
125
- // honors the public IStickyHeaderProps contract; the reducer emits no index for it.
126
- break;
127
- }
128
- }
129
- }, [scrollAnimatedValue]);
130
- const dispatch = useCallback((action) => {
131
- const current = stateRef.current;
132
- if (current === null)
133
- return;
134
- runEffects(reduceSticky(current, action, inputsRef.current).effects);
135
- }, [runEffects]);
136
- dispatchRef.current = dispatch;
137
- // Rebuild when the collision/viewport inputs change (RN effect deps minus the layout state, which
138
- // dispatches 'layout' itself); also does the initial identity build on mount.
139
- useEffect(() => {
140
- dispatchRef.current({ kind: 'inputs-changed' });
141
- }, [inverted, scrollViewHeight, nextHeaderY, scrollAnimatedValue]);
142
- // Detach the listener + clear the debounce on unmount.
143
- useEffect(() => () => {
144
- const previous = interpolationRef.current;
145
- if (previous !== null && listenerIdRef.current !== null)
146
- previous.removeListener(listenerIdRef.current);
147
- if (debounceTimer.current !== null)
148
- clearTimeout(debounceTimer.current);
149
- }, []);
150
- const onLayout = (event) => {
151
- const y = readLayoutNumber(event, 'y');
152
- const height = readLayoutNumber(event, 'height');
153
- // Keep the previous value when a field is absent (RN sets state only on a defined read).
154
- dispatch({
155
- kind: 'layout',
156
- y: y ?? state.layoutY,
157
- height: height ?? state.layoutHeight,
158
- });
159
- props.onLayout(event);
160
- const child = firstChild(children);
161
- const childOnLayout = child === undefined ? undefined : readChildOnLayout(child);
162
- childOnLayout?.(event);
163
- };
164
- // The EXPLICIT debounced translateY overrides the committed transform for hit-testing, while
165
- // `animatedTranslateY` does the smooth (native-driven) pin, per RN ScrollViewStickyHeader.js.
166
- const passthroughAnimatedPropExplicitValues = state.translateY !== null
167
- ? { style: { transform: [{ translateY: state.translateY }] } }
168
- : null;
169
- // collapsable:false keeps the wrapper a real Yoga node; zIndex makes the pinned header paint
170
- // OVER the rows scrolling under it. `style` is `unknown` on Animated.View, so the interpolation
171
- // transform passes with no cast.
172
- return createElement(Animated.View, {
173
- style: {
174
- transform: [{ translateY: animatedTranslateY }],
175
- zIndex: STICKY_HEADER_Z_INDEX,
176
- },
177
- onLayout,
178
- collapsable: false,
179
- passthroughAnimatedPropExplicitValues,
180
- }, children);
181
- };
182
- ScrollViewStickyHeader.displayName = 'ScrollViewStickyHeader';
183
- // Wrap each child flagged by `stickyHeaderIndices` in the sticky header component, fed by the
184
- // shared scroll AnimatedValue. Mirrors ScrollView.js's render-time children.map (~line 1690).
185
- // Returns the children unchanged when no indices are flagged.
186
- //
187
- // Cross-talk plumbing (RN's _headerLayoutYs + _onStickyHeaderLayout, ScrollView.js:1115-1143):
188
- // `headerLayoutYs` is a child-index→measured-y map the parent keeps; each header reports its own
189
- // y through `onHeaderLayoutY` as it measures, and we feed every header the y of the NEXT flagged
190
- // header (the collision point past which it scrolls off) by looking up its successor's index in
191
- // `stickyHeaderIndices`. The LAST flagged header has no successor, so its `nextHeaderLayoutY`
192
- // stays undefined and it sticks indefinitely (correct).
193
- export function wrapStickyHeaders(children, stickyHeaderIndices, scrollAnimatedValue, invertStickyHeaders, scrollViewHeight, StickyHeaderComponent, crossTalk) {
194
- const { headerLayoutYs, onHeaderLayoutY, onHeaderRef } = crossTalk;
195
- if (stickyHeaderIndices === undefined || stickyHeaderIndices.length === 0)
196
- return children;
197
- const Wrapper = StickyHeaderComponent ?? ScrollViewStickyHeader;
198
- return Children.toArray(children).map((child, index) => {
199
- const indexOfIndex = stickyHeaderIndices.indexOf(index);
200
- if (indexOfIndex === -1 || !isValidElement(child))
201
- return child;
202
- // The next flagged header's measured y, by index order in stickyHeaderIndices (RN
203
- // ScrollView.js:1695 nextIndex). undefined until that header has measured (or for the last).
204
- const nextIndex = stickyHeaderIndices[indexOfIndex + 1];
205
- const nextHeaderLayoutY = nextStickyHeaderY(stickyHeaderIndices, indexOfIndex, headerLayoutYs);
206
- dlog(`ScrollView sticky-header wrap index=${index} next=${nextIndex} nextY=${nextHeaderLayoutY}`);
207
- return createElement(Wrapper, {
208
- key: child.key ?? `sticky-${index}`,
209
- // Registers this header so the ScrollView can push the NEXT header's y straight in
210
- // (RN ScrollView.js:1109). Without it a measurement would have to travel through parent
211
- // state, re-rendering every child.
212
- ref: (handle) => onHeaderRef(index, handle),
213
- nextHeaderLayoutY,
214
- // RN _onStickyHeaderLayout: record this header's own y, then push it to the previous
215
- // header as its nextHeaderLayoutY. We record into the parent map; the lookup above feeds
216
- // it forward on the resulting re-render.
217
- onLayout: (event) => {
218
- const y = readLayoutNumber(event, 'y');
219
- if (y !== undefined)
220
- onHeaderLayoutY(index, y);
221
- },
222
- scrollAnimatedValue,
223
- inverted: invertStickyHeaders,
224
- scrollViewHeight,
225
- }, child);
226
- });
227
- }
@@ -1,3 +0,0 @@
1
- import type { ISwitchProps } from './shared';
2
- export type { ISwitchProps, ISwitchTrackColor } from './shared';
3
- export declare function Switch(rawProps: ISwitchProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
@@ -1,18 +0,0 @@
1
- // Switch, Android host binding. AndroidSwitch takes trackColorForTrue / trackColorForFalse
2
- // plus trackTintColor (the color for the CURRENT value, which RN computes as value ? true :
3
- // false), and snaps native back via the `setNativeValue` command (Switch.js:221-225).
4
- import { descriptorToReact } from '../../descriptor-to-react/index.js';
5
- import { useSwitchLogic } from './shared.js';
6
- const PLATFORM = {
7
- snapBackCommand: 'setNativeValue',
8
- trackColorProps: (value, trackColor) => ({
9
- trackColorForFalse: trackColor?.false,
10
- trackColorForTrue: trackColor?.true,
11
- trackTintColor: value ? trackColor?.true : trackColor?.false,
12
- }),
13
- };
14
- // A top-level named function, not a factory-returned closure: React Compiler's
15
- // component detection only walks top-level declarations (see shared.ts).
16
- export function Switch(rawProps) {
17
- return descriptorToReact(useSwitchLogic(rawProps, PLATFORM));
18
- }
@@ -1 +0,0 @@
1
- export * from './index.ios';
@@ -1,3 +0,0 @@
1
- import type { ISwitchProps } from './shared';
2
- export type { ISwitchProps, ISwitchTrackColor } from './shared';
3
- export declare function Switch(rawProps: ISwitchProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
@@ -1,16 +0,0 @@
1
- // Switch, iOS host binding. iOS's native Switch takes onTintColor (ON-track) / tintColor
2
- // (OFF-track), and snaps native back via the `setValue` command (Switch.js:221-225).
3
- import { descriptorToReact } from '../../descriptor-to-react/index.js';
4
- import { useSwitchLogic } from './shared.js';
5
- const PLATFORM = {
6
- snapBackCommand: 'setValue',
7
- trackColorProps: (_value, trackColor) => ({
8
- onTintColor: trackColor?.true,
9
- tintColor: trackColor?.false,
10
- }),
11
- };
12
- // A top-level named function, not a factory-returned closure: React Compiler's
13
- // component detection only walks top-level declarations (see shared.ts).
14
- export function Switch(rawProps) {
15
- return descriptorToReact(useSwitchLogic(rawProps, PLATFORM));
16
- }
@@ -1,6 +0,0 @@
1
- // Base / default Switch: re-exports the iOS build. Metro overrides this with switch.ios.ts /
2
- // switch.android.ts on a real host; under tsx / tsc / web the host config resolves here.
3
- // Filename is the selector, no Platform.OS read. The logic lives in
4
- // @symbiote-native/components/state, the render in @symbiote-native/components/view, and the
5
- // hook in switch-shared.
6
- export * from './index.ios.js';
@@ -1,9 +0,0 @@
1
- import type { ISwitchPlatform, ISwitchProps as ISwitchBaseProps } from '@symbiote-native/components';
2
- export type { ISwitchTrackColor } from '@symbiote-native/components';
3
- export type ISwitchProps = ISwitchBaseProps & {
4
- className?: string;
5
- };
6
- export type ISwitchHostPlatform = ISwitchPlatform & {
7
- snapBackCommand: string;
8
- };
9
- export declare function useSwitchLogic(rawProps: ISwitchProps, platform: ISwitchHostPlatform): import("@symbiote-native/components").IDescriptor;
@@ -1,60 +0,0 @@
1
- // Switch is the React lifecycle half. The logic (the lastNativeReport reducer, valueFromChange,
2
- // the snap-back decision) lives in @symbiote-native/components/state, the render (value fold, track
3
- // colors, ios_backgroundColor) in @symbiote-native/components/view; here the adapter only owns the
4
- // hook: useReducer over the shared reducer, a ref for the imperative command, and the
5
- // useLayoutEffect that snaps native back when the parent rejects a toggle.
6
- //
7
- // Switch is controlled exactly like RN's: the parent's onValueChange MUST update `value` for
8
- // the toggle to stick. If the handler is a no-op, native has already flipped its own grip, so
9
- // JS commands the old value back down (a plain prop re-push cannot cover the no-op case: the
10
- // prop never changes, so the retained tree never diverges). The command name is platform-
11
- // specific (iOS setValue / Android setNativeValue) and arrives via `platform`.
12
- //
13
- // useSwitchLogic is a plain hook, not a component factory returning a closure: React Compiler's
14
- // component/hook detection only walks top-level declarations, so index.ios.ts / index.android.ts
15
- // each declare their OWN top-level `Switch` function that calls this hook, rather than exporting
16
- // whatever a factory here returns.
17
- import { useCallback, useLayoutEffect, useReducer, useRef } from 'react';
18
- import { renderSwitch, switchReducer, createInitialSwitchState, shouldSnapBack, valueFromChange, } from '@symbiote-native/components';
19
- import { dispatchViewCommand, dlog, } from '@symbiote-native/engine';
20
- import { resolveAccessibilityProps } from '@symbiote-native/components';
21
- export function useSwitchLogic(rawProps, platform) {
22
- // Switch owns its host element rather than rendering through a symbiote View, so it folds
23
- // aria/role into accessibility* here; the resolved accessibility* fields (plus testID etc.)
24
- // ride down to the host node via `passthrough`.
25
- const props = resolveAccessibilityProps(rawProps);
26
- const { value, onValueChange, disabled, trackColor, thumbColor, ios_backgroundColor, style, ...passthrough } = props;
27
- const ref = useRef(null);
28
- const [state, dispatch] = useReducer(switchReducer, undefined, createInitialSwitchState);
29
- const handleChange = useCallback((event) => {
30
- const next = valueFromChange(event);
31
- dlog(`Switch onChange value=${String(next)} eventCount=${String(event.nativeEvent.eventCount)}`);
32
- if (next === undefined)
33
- return;
34
- onValueChange?.(next, event);
35
- dispatch({ type: 'native-reported', value: next });
36
- }, [onValueChange]);
37
- // value is a real Fabric prop, folded to a strict boolean: RN sends `value === true`, so
38
- // an undefined prop reads as "off" rather than absent.
39
- const fabricValue = value === true;
40
- useLayoutEffect(() => {
41
- const node = ref.current;
42
- if (node === null)
43
- return;
44
- if (!shouldSnapBack(state, fabricValue)) {
45
- dlog(`Switch snap-back no-op reported=${String(state.lastNativeReport)} value=${fabricValue}`);
46
- return;
47
- }
48
- dlog(`Switch ${platform.snapBackCommand} snap-back reported=${String(state.lastNativeReport)} value=${fabricValue}`);
49
- dispatchViewCommand(node, platform.snapBackCommand, [fabricValue]);
50
- }, [fabricValue, state]);
51
- return renderSwitch({
52
- value: fabricValue,
53
- disabled,
54
- trackColor,
55
- thumbColor,
56
- ios_backgroundColor,
57
- style,
58
- passthrough: { ...passthrough, ref, onChange: handleChange },
59
- }, platform);
60
- }
@@ -1,50 +0,0 @@
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>>;
@@ -1,172 +0,0 @@
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 '@symbiote-native/engine';
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/index.js';
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', [
136
- mostRecentEventCount,
137
- '',
138
- 0,
139
- 0,
140
- ]);
141
- lastNativeText.current = '';
142
- },
143
- isFocused: () => focused.current,
144
- setSelection: (start, end) => {
145
- const node = ref.current;
146
- if (node === null)
147
- return;
148
- const current = lastNativeText.current ?? '';
149
- dispatchViewCommand(node, 'setTextAndSelection', [
150
- mostRecentEventCount,
151
- current,
152
- start,
153
- end,
154
- ]);
155
- },
156
- }), [mostRecentEventCount]);
157
- return descriptorToReact(renderTextInput({
158
- multiline: isMultiline,
159
- text,
160
- mostRecentEventCount,
161
- selection,
162
- folded,
163
- passthrough: {
164
- ...passthrough,
165
- ref,
166
- onChange: handleChange,
167
- onFocus: handleFocus,
168
- onBlur: handleBlur,
169
- },
170
- }));
171
- });
172
- TextInput.displayName = 'TextInput';
@@ -1,22 +0,0 @@
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
- onShowUnderlay?: () => void;
17
- onHideUnderlay?: () => void;
18
- }
19
- export declare const TouchableHighlight: FC<ITouchableHighlightProps>;
20
- export type ITouchableWithoutFeedbackProps = ITouchableBaseProps;
21
- export declare const TouchableWithoutFeedback: FC<ITouchableWithoutFeedbackProps>;
22
- export {};