@symbiote-native/components 2.0.0 → 3.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 (79) hide show
  1. package/README.md +8 -9
  2. package/build/accessibility-props.d.ts +1 -1
  3. package/build/accessibility-props.js +2 -2
  4. package/build/behaviors/activity-indicator/shared.js +41 -70
  5. package/build/behaviors/button.d.ts +11 -0
  6. package/build/behaviors/button.js +101 -89
  7. package/build/behaviors/image-background.d.ts +1 -0
  8. package/build/behaviors/image-background.js +97 -81
  9. package/build/behaviors/image.d.ts +0 -1
  10. package/build/behaviors/image.js +24 -105
  11. package/build/behaviors/input-accessory-view.d.ts +0 -1
  12. package/build/behaviors/input-accessory-view.js +47 -53
  13. package/build/behaviors/pressable.d.ts +1 -1
  14. package/build/behaviors/pressable.js +68 -97
  15. package/build/behaviors/refresh-control.js +15 -2
  16. package/build/behaviors/scroll-view/index.android.js +25 -37
  17. package/build/behaviors/scroll-view/index.d.ts +1 -0
  18. package/build/behaviors/scroll-view/index.js +3 -0
  19. package/build/behaviors/scroll-view/responder.d.ts +4 -0
  20. package/build/behaviors/scroll-view/responder.js +202 -0
  21. package/build/behaviors/scroll-view/shared.d.ts +1 -3
  22. package/build/behaviors/scroll-view/shared.js +95 -95
  23. package/build/behaviors/scroll-view/sticky.d.ts +1 -0
  24. package/build/behaviors/scroll-view/sticky.js +62 -49
  25. package/build/behaviors/switch.js +43 -86
  26. package/build/behaviors/text-input.js +219 -107
  27. package/build/behaviors/touchable-highlight.js +74 -61
  28. package/build/behaviors/touchable-native-feedback.js +43 -122
  29. package/build/behaviors/touchable-opacity.js +71 -59
  30. package/build/behaviors/touchable-without-feedback.js +35 -100
  31. package/build/component-names/index.android.js +7 -11
  32. package/build/component-names/index.ios.js +0 -7
  33. package/build/component-names/shared.d.ts +1 -1
  34. package/build/index.d.ts +15 -21
  35. package/build/index.js +21 -28
  36. package/build/resolve-intrinsic.js +3 -9
  37. package/build/scroll-view-commands.d.ts +1 -9
  38. package/build/scroll-view-commands.js +13 -74
  39. package/build/state/flat-list.d.ts +2 -2
  40. package/build/state/flat-list.js +10 -2
  41. package/build/state/pressable.d.ts +6 -1
  42. package/build/state/pressable.js +63 -28
  43. package/build/state/section-list.d.ts +2 -0
  44. package/build/state/section-list.js +14 -7
  45. package/build/state/text-input.d.ts +7 -40
  46. package/build/state/text-input.js +17 -186
  47. package/build/state/touchable.d.ts +1 -0
  48. package/build/state/touchable.js +11 -8
  49. package/build/state/virtualized-list-reducer.d.ts +2 -2
  50. package/build/state/virtualized-list.d.ts +6 -6
  51. package/build/state/virtualized-list.js +71 -37
  52. package/build/text-props.d.ts +0 -8
  53. package/build/text-props.js +14 -25
  54. package/build/view/render-button.d.ts +1 -29
  55. package/build/view/render-button.js +44 -81
  56. package/build/view/render-image/index.d.ts +14 -1
  57. package/build/view/render-image/index.js +22 -147
  58. package/build/view/render-input-accessory-view.d.ts +1 -5
  59. package/build/view/render-input-accessory-view.js +26 -48
  60. package/build/view/render-keyboard-avoiding-view.d.ts +7 -1
  61. package/build/view/render-keyboard-avoiding-view.js +40 -1
  62. package/build/view/render-modal.d.ts +1 -1
  63. package/build/view/render-modal.js +15 -5
  64. package/build/view/render-pressable/index.d.ts +1 -0
  65. package/build/view/render-pressable/index.js +4 -0
  66. package/build/view/render-scroll-view.d.ts +0 -4
  67. package/build/view/render-scroll-view.js +3 -49
  68. package/build/view/render-switch.d.ts +0 -14
  69. package/build/view/render-switch.js +4 -42
  70. package/build/view/render-touchable-highlight.d.ts +1 -0
  71. package/build/view/render-touchable-native-feedback.d.ts +0 -1
  72. package/build/view/render-touchable-native-feedback.js +15 -9
  73. package/host-primitives.cjs +49 -203
  74. package/host-primitives.d.cts +0 -1
  75. package/package.json +3 -7
  76. package/build/fold-host-bag.d.ts +0 -15
  77. package/build/fold-host-bag.js +0 -99
  78. package/build/view/render-text-input.d.ts +0 -11
  79. package/build/view/render-text-input.js +0 -39
@@ -1,11 +1,11 @@
1
- import type { IViewableItemsChangedInfo, IViewToken } from './virtualized-list';
1
+ import { type IViewableItemsChangedInfo, type IViewToken } from './virtualized-list';
2
2
  export declare const SINGLE_COLUMN = 1;
3
3
  export interface IRow<ItemT> {
4
4
  items: ItemT[];
5
5
  startIndex: number;
6
6
  }
7
7
  export declare function chunkIntoRows<ItemT>(data: readonly ItemT[], columns: number): IRow<ItemT>[];
8
- export declare function rowKeyExtractor<ItemT>(row: IRow<ItemT>): string;
8
+ export declare function rowKeyExtractor<ItemT>(row: IRow<ItemT>, keyExtractor?: (item: ItemT, index: number) => string): string;
9
9
  export declare function expandRowToken<ItemT>(token: IViewToken<IRow<ItemT>>, keyExtractor?: (item: ItemT, index: number) => string): IViewToken<ItemT>[];
10
10
  export declare function expandRowViewability<ItemT>(info: IViewableItemsChangedInfo<IRow<ItemT>>, keyExtractor?: (item: ItemT, index: number) => string): IViewableItemsChangedInfo<ItemT>;
11
11
  export declare function lastItemOfRow<ItemT>(row: IRow<ItemT> | undefined): ItemT | undefined;
@@ -4,6 +4,7 @@
4
4
  // back to per-item tokens, and the row separator unwraps to the flanking items. All of
5
5
  // this is pure transform — every adapter (React, Vue) reuses it; the adapter supplies
6
6
  // only the element creation (createElement / h) for a row and the ref wiring.
7
+ import { defaultKeyExtractor, } from './virtualized-list.js';
7
8
  export const SINGLE_COLUMN = 1;
8
9
  export function chunkIntoRows(data, columns) {
9
10
  const rows = [];
@@ -12,8 +13,15 @@ export function chunkIntoRows(data, columns) {
12
13
  }
13
14
  return rows;
14
15
  }
15
- export function rowKeyExtractor(row) {
16
- return `row-${row.startIndex}`;
16
+ // RN's own multi-column row key (`FlatList.js`'s `_keyExtractor`): each item's own key, joined
17
+ // with `:`, never a synthetic position string — an app's `keyExtractor` (or the object's own
18
+ // `key`/`id`) is what keeps a row's identity stable across inserts/removes, and a `row-${index}`
19
+ // key defeats that as surely as no key at all.
20
+ export function rowKeyExtractor(row, keyExtractor) {
21
+ const resolve = keyExtractor ?? defaultKeyExtractor;
22
+ return row.items
23
+ .map((item, column) => resolve(item, row.startIndex + column))
24
+ .join(':');
17
25
  }
18
26
  // Expand a row's viewable token to one token per item in that row, all sharing the row's
19
27
  // isViewable flag, so the caller sees item-level visibility rather than row-level.
@@ -1,5 +1,6 @@
1
1
  import { type ISymbioteEvent } from '@symbiote-native/engine';
2
2
  export declare const DEFAULT_DELAY_LONG_PRESS_MS = 500;
3
+ export declare const LONG_PRESS_DEACTIVATION_DISTANCE = 10;
3
4
  export declare const DEFAULT_MIN_PRESS_DURATION_MS = 130;
4
5
  export declare const DEFAULT_PRESS_RECT_OFFSETS: {
5
6
  top: number;
@@ -52,7 +53,6 @@ export declare function readPoint(event: ISymbioteEvent): {
52
53
  y: number;
53
54
  } | undefined;
54
55
  export declare function computeRegion(width: number, height: number, pageX: number, pageY: number): IResponderRegion | undefined;
55
- export declare function rippleProps(config: IPressableAndroidRippleConfig): Record<string, IRippleBackground> | undefined;
56
56
  export interface IPressRuntime {
57
57
  longPressCancel: (() => void) | undefined;
58
58
  longPressFired: boolean;
@@ -62,6 +62,10 @@ export interface IPressRuntime {
62
62
  x: number;
63
63
  y: number;
64
64
  } | undefined;
65
+ activatePosition: {
66
+ x: number;
67
+ y: number;
68
+ } | undefined;
65
69
  driftedOut: boolean;
66
70
  region: IResponderRegion | undefined;
67
71
  active: boolean;
@@ -89,6 +93,7 @@ export interface IPressMachineConfig {
89
93
  minPressDuration?: number;
90
94
  hitSlop?: IRectOffset;
91
95
  pressRetentionOffset?: IRectOffset;
96
+ android_disableSound?: boolean;
92
97
  }
93
98
  export interface IPressHandlers {
94
99
  handlePressIn: IPressHandler;
@@ -9,8 +9,15 @@
9
9
  // re-render, so each framework owns its reactive primitive) and the raw frame-measure (the host
10
10
  // node plus its measure call). The rest (the timers, the geometry, the suppression flags,
11
11
  // the decision of when each callback fires) is here, shared by every adapter.
12
- import { dlog, Platform } from '@symbiote-native/engine';
12
+ import { dlog, Platform, SoundManager, } from '@symbiote-native/engine';
13
13
  export const DEFAULT_DELAY_LONG_PRESS_MS = 500;
14
+ // Pressability.js's DEFAULT_LONG_PRESS_DEACTIVATION_DISTANCE. A SEPARATE, smaller radius than
15
+ // `pressRetentionOffset`/`hitSlop`: any move past it cancels a pending long press even while the
16
+ // finger is still well inside the retention rect — real presses jitter a few px, long-press must
17
+ // not fire mid-scroll. Vendor exposes it only as a rare static override
18
+ // (`Pressability.setLongPressDeactivationDistance`, used by e.g. gesture libraries), never as a
19
+ // per-component prop, so it stays a plain constant here rather than a config field.
20
+ export const LONG_PRESS_DEACTIVATION_DISTANCE = 10;
14
21
  // Pressability's default active-visual floor for a plain Pressable. Touchable* overrides this to 0.
15
22
  export const DEFAULT_MIN_PRESS_DURATION_MS = 130;
16
23
  // RN's default extra slop kept around a press once it is active, before a drift fires pressOut.
@@ -72,22 +79,6 @@ export function computeRegion(width, height, pageX, pageY) {
72
79
  bottom: pageY + height,
73
80
  };
74
81
  }
75
- // Build the Android native-feedback prop the inner View carries from the ripple config. RN runs
76
- // the color through processColor → a native int; we have no native bridge in JS, so we keep the
77
- // string and let Android resolve it (a null color is the documented "no tint"). Inert on iOS.
78
- export function rippleProps(config) {
79
- if (Platform.OS !== 'android')
80
- return undefined;
81
- const background = {
82
- type: 'RippleAndroid',
83
- color: config.color ?? null,
84
- borderless: config.borderless === true,
85
- rippleRadius: config.radius,
86
- };
87
- return config.foreground === true
88
- ? { nativeForegroundAndroid: background }
89
- : { nativeBackgroundAndroid: background };
90
- }
91
82
  export function createPressRuntime() {
92
83
  return {
93
84
  longPressCancel: undefined,
@@ -95,6 +86,7 @@ export function createPressRuntime() {
95
86
  pressDelayCancel: undefined,
96
87
  pressOutCancel: undefined,
97
88
  pressOrigin: undefined,
89
+ activatePosition: undefined,
98
90
  driftedOut: false,
99
91
  region: undefined,
100
92
  active: false,
@@ -154,7 +146,7 @@ function measureRegion(runtime, measureFn) {
154
146
  // (the React adapter's useMemo body) lives here, shared by every adapter. The adapter rebuilds
155
147
  // these per render (the closures capture the live config) while the runtime persists across them.
156
148
  export function createPressHandlers(config, runtime, host) {
157
- const { onPress, onPressIn, onPressOut, onPressMove, onLongPress, delayLongPress, unstable_pressDelay, minPressDuration = DEFAULT_MIN_PRESS_DURATION_MS, } = config;
149
+ const { onPress, onPressIn, onPressOut, onPressMove, onLongPress, delayLongPress, unstable_pressDelay, minPressDuration = DEFAULT_MIN_PRESS_DURATION_MS, android_disableSound, } = config;
158
150
  // Per-edge offsets for the measured-rect retention test (RN's hitSlop + pressRectOffset).
159
151
  // pressRetentionOffset defaults to RN's DEFAULT_PRESS_RECT_OFFSETS when unset; hitSlop to zero.
160
152
  const hitSlopRect = normalizeRect(config.hitSlop);
@@ -165,6 +157,18 @@ export function createPressHandlers(config, runtime, host) {
165
157
  const fallbackThreshold = maxEdge(hitSlopRect) + maxEdge(pressRectOffset);
166
158
  // True iff the touch still belongs to the active press: against the measured rect when we have
167
159
  // one (the RN-faithful path), else the symmetric radius fallback.
160
+ //
161
+ // TODO(rn-parity): `Pressability.js`'s `onResponderMove` (:489-493) returns right after the
162
+ // app's `onPressMove`, before any drift/long-press-jitter check, whenever the region is not yet
163
+ // measured — vendor treats "no measurement yet" as "do nothing until one arrives." The radius
164
+ // fallback below diverges: it judges drift/jitter on synthetic geometry instead of waiting.
165
+ // Confirmed unreachable on a real device (`Tree::measure` in `SymbioteTree.cpp` is a synchronous
166
+ // JSI call, so the region is always populated before `handleResponderMove` can fire), so the
167
+ // fallback only ever fires in this codebase's own test harness, which wires
168
+ // `getMeasureFn: () => undefined` everywhere (`state/pressable.test.ts:84` and every touchable
169
+ // test built on it). A real fix needs a synchronous mock-measure harness under 11+ existing
170
+ // drift/retention/long-press call sites across `pressable.test.ts`, `touchable-opacity.test.ts`
171
+ // and `render-pressable.test.ts`, not a one-line guard — see the audit skill for the finding.
168
172
  function isWithinRetention(point) {
169
173
  const region = runtime.region;
170
174
  if (region !== undefined) {
@@ -184,6 +188,24 @@ export function createPressHandlers(config, runtime, host) {
184
188
  function clearPressOut() {
185
189
  cancelRuntimeTimer(runtime, 'pressOutCancel');
186
190
  }
191
+ // Pressability.js arms this ONCE, at RESPONDER_GRANT, and a drift out/back-in never re-arms it
192
+ // (`onResponderMove` only ever CANCELS via `_cancelLongPressDelayTimeout`, permanently, for the
193
+ // rest of the gesture). Splitting it out of `activate()` is what makes that possible: `activate`
194
+ // itself runs again on every drift-back-in, and used to re-arm a fresh full-duration timer each
195
+ // time, which is wrong on two counts — it can resurrect a long press the finger already
196
+ // disqualified, and if it never left, it's simply the same timer restarted for no reason.
197
+ function armLongPress(event) {
198
+ if (!onLongPress)
199
+ return;
200
+ runtime.longPressCancel = host.schedule(() => {
201
+ runtime.longPressCancel = undefined;
202
+ if (runtime.disposed || !runtime.active || runtime.driftedOut)
203
+ return;
204
+ runtime.longPressFired = true;
205
+ dlog('Pressable longPress timer fired');
206
+ onLongPress(event);
207
+ }, delayLongPress + unstable_pressDelay);
208
+ }
187
209
  function activate(event) {
188
210
  if (runtime.disposed || runtime.active || runtime.driftedOut)
189
211
  return;
@@ -191,18 +213,11 @@ export function createPressHandlers(config, runtime, host) {
191
213
  clearPressOut();
192
214
  runtime.active = true;
193
215
  runtime.activatedAt = host.now();
216
+ // Pressability.js's `_activate`: the jitter check below reads from here, re-set on every
217
+ // (re)activation — including a drift-back-in re-arm, matching vendor exactly.
218
+ runtime.activatePosition = readPoint(event);
194
219
  dlog('Pressable pressIn');
195
220
  host.setPressed(true);
196
- if (!runtime.longPressFired && onLongPress) {
197
- runtime.longPressCancel = host.schedule(() => {
198
- runtime.longPressCancel = undefined;
199
- if (runtime.disposed || !runtime.active || runtime.driftedOut)
200
- return;
201
- runtime.longPressFired = true;
202
- dlog('Pressable longPress timer fired');
203
- onLongPress(event);
204
- }, delayLongPress);
205
- }
206
221
  onPressIn?.(event);
207
222
  }
208
223
  function deactivate(event) {
@@ -262,6 +277,11 @@ export function createPressHandlers(config, runtime, host) {
262
277
  runtime.pressOrigin = readPoint(event);
263
278
  runtime.driftedOut = false;
264
279
  measureRegion(runtime, host.getMeasureFn());
280
+ // Armed at GRANT, unconditionally — before the `unstable_pressDelay` branch below, which only
281
+ // defers the PRESSED VISUAL. `delayLongPress` already has `unstable_pressDelay` baked out of
282
+ // its default (`configFor`), so adding it back here is what keeps the long-press threshold at
283
+ // a constant time-from-touch-down regardless of how long the visual is deferred.
284
+ armLongPress(event);
265
285
  if (unstable_pressDelay > 0) {
266
286
  dlog(`Pressable pressIn deferred ${unstable_pressDelay}ms`);
267
287
  runtime.pressDelayCancel = host.schedule(() => {
@@ -303,6 +323,12 @@ export function createPressHandlers(config, runtime, host) {
303
323
  dlog('Pressable press suppressed by prior longPress');
304
324
  return;
305
325
  }
326
+ // Pressability.js:754-756 — Android only, and only when onPress is actually about to fire.
327
+ if (onPress !== undefined &&
328
+ Platform.OS === 'android' &&
329
+ android_disableSound !== true) {
330
+ SoundManager.playTouchSound();
331
+ }
306
332
  onPress?.(event);
307
333
  },
308
334
  handleResponderMove(event) {
@@ -312,6 +338,15 @@ export function createPressHandlers(config, runtime, host) {
312
338
  const here = readPoint(event);
313
339
  if (!here)
314
340
  return;
341
+ // Pressability.js:502-508 — checked BEFORE the retention-region branch below, and
342
+ // independent of it: a long press must not fire on a jitter, even one well inside the
343
+ // broader retention rect that keeps the press itself alive.
344
+ const activatePosition = runtime.activatePosition;
345
+ if (activatePosition !== undefined) {
346
+ const distance = Math.hypot(activatePosition.x - here.x, activatePosition.y - here.y);
347
+ if (distance > LONG_PRESS_DEACTIVATION_DISTANCE)
348
+ clearLongPress();
349
+ }
315
350
  if (!isWithinRetention(here)) {
316
351
  if (!runtime.driftedOut) {
317
352
  dlog('Pressable drifted past retention region — deactivating');
@@ -2,6 +2,8 @@ import type { IScrollRoutingHandle } from './scroll-routing-handle';
2
2
  export interface ISection<ItemT> {
3
3
  title: string;
4
4
  data: readonly ItemT[];
5
+ key?: string;
6
+ keyExtractor?(item: ItemT, index: number): string;
5
7
  }
6
8
  export type ISectionEntry<ItemT> = {
7
9
  kind: 'header';
@@ -4,6 +4,7 @@
4
4
  // stream, so headers, items, and footers are windowed by the same machinery. All of this
5
5
  // is pure transform - every adapter reuses it; the adapter supplies only the per-entry
6
6
  // element creation (renderSectionHeader / renderItem / etc.) and the ref wiring.
7
+ import { defaultKeyExtractor } from './virtualized-list.js';
7
8
  // Flatten sections into entries AND record where each section header lands in the flat
8
9
  // stream, so scrollToLocation can map (sectionIndex, itemIndex) -> flat index without
9
10
  // re-deriving the layout. withSeparators inserts a section separator between adjacent
@@ -30,16 +31,22 @@ export function flattenSections(sections, withSeparators) {
30
31
  export function unwrapEntryItem(entry) {
31
32
  return entry !== undefined && entry.kind === 'item' ? entry.item : undefined;
32
33
  }
34
+ // RN's own section-relative key (`VirtualizedSectionList.js`'s `_subExtractor`): the section's
35
+ // own `key` when given, else its position — never the flat entry index, which is `entry`'s own
36
+ // but is not the SECTION's identity.
37
+ function sectionKeyPart(section, sectionIndex) {
38
+ return section.key ?? String(sectionIndex);
39
+ }
33
40
  export function sectionEntryKey(entry, index, keyExtractor) {
34
- if (entry.kind === 'header')
35
- return `section-${entry.sectionIndex}`;
36
- if (entry.kind === 'footer')
37
- return `section-${entry.sectionIndex}:footer`;
38
41
  if (entry.kind === 'section-separator')
39
42
  return `section-${entry.sectionIndex}:separator`;
40
- if (keyExtractor)
41
- return keyExtractor(entry.item, entry.itemIndex);
42
- return `entry-${index}`;
43
+ const sectionKey = sectionKeyPart(entry.section, entry.sectionIndex);
44
+ if (entry.kind === 'header')
45
+ return `${sectionKey}:header`;
46
+ if (entry.kind === 'footer')
47
+ return `${sectionKey}:footer`;
48
+ const resolve = entry.section.keyExtractor ?? keyExtractor ?? defaultKeyExtractor;
49
+ return `${sectionKey}:${resolve(entry.item, entry.itemIndex)}`;
43
50
  }
44
51
  // RN's itemIndex is offset by 1 so itemIndex 0 targets the header and itemIndex >= 1
45
52
  // targets that section's items. Returns undefined when the section is out of range.
@@ -1,5 +1,6 @@
1
- import type { IMeasureOnSuccess, IMeasureInWindowOnSuccess, IMeasureLayoutOnSuccess, ISymbioteNode, IPlatformOSType, ISymbioteEvent, ITextStyle } from '@symbiote-native/engine';
1
+ import type { IMeasureOnSuccess, IMeasureInWindowOnSuccess, IMeasureLayoutOnSuccess, ISymbioteNode, ISymbioteEvent, ITextStyle } from '@symbiote-native/engine';
2
2
  import type { IAccessibilityProps, IAriaProps } from '../accessibility-props';
3
+ import type { IRectOffset } from './pressable';
3
4
  export type IInputMode = 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
4
5
  export type IEnterKeyHint = 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send';
5
6
  export type ISubmitBehavior = 'submit' | 'blurAndSubmit' | 'newline';
@@ -10,49 +11,10 @@ export type ITextInputSelection = {
10
11
  export type ITextInputEventHandler = (event: ISymbioteEvent) => void;
11
12
  export declare const INITIAL_EVENT_COUNT = 0;
12
13
  export declare const SELECTION_NONE = -1;
13
- export declare function keyboardTypeForInputMode(inputMode: string, os: IPlatformOSType): string | undefined;
14
- export declare function mapAutoComplete(map: Record<string, string>, token: string): string | undefined;
15
- export declare function foldAutoComplete(token: string | undefined): {
16
- autoComplete: string | undefined;
17
- textContentType: string | undefined;
18
- };
19
- export declare function foldSubmitBehavior(submitBehavior: string | undefined, blurOnSubmit: boolean | undefined, multiline: boolean): string;
20
14
  export declare function foldText(value: string | undefined, defaultValue: string | undefined): string | undefined;
21
15
  export declare function textFromChange(event: ISymbioteEvent): string | undefined;
22
16
  export declare function eventCountFromChange(event: ISymbioteEvent): number | undefined;
23
17
  export declare function shouldCommandText(lastNativeText: string | undefined, value: string | undefined): value is string;
24
- export type ITextInputFoldInput = {
25
- inputMode?: string;
26
- keyboardType?: string;
27
- enterKeyHint?: string;
28
- returnKeyType?: string;
29
- readOnly?: boolean;
30
- editable?: boolean;
31
- submitBehavior?: string;
32
- blurOnSubmit?: boolean;
33
- multiline: boolean;
34
- cursorColor?: string;
35
- selectionColor?: string;
36
- selectionHandleColor?: string;
37
- autoComplete?: string;
38
- textContentType?: string;
39
- showSoftInputOnFocus?: boolean;
40
- underlineColorAndroid?: string;
41
- };
42
- export type IFoldedTextInputProps = {
43
- keyboardType: string | undefined;
44
- returnKeyType: string | undefined;
45
- editable: boolean | undefined;
46
- submitBehavior: string;
47
- selectionColor: string | undefined;
48
- cursorColor: string | undefined;
49
- selectionHandleColor: string | undefined;
50
- underlineColorAndroid: string;
51
- autoComplete: string | undefined;
52
- textContentType: string | undefined;
53
- showSoftInputOnFocus: boolean | undefined;
54
- };
55
- export declare function resolveTextInputProps(input: ITextInputFoldInput): IFoldedTextInputProps;
56
18
  export type ITextInputChangeEvent = ISymbioteEvent & {
57
19
  text: string;
58
20
  };
@@ -89,7 +51,12 @@ export type ITextInputProps = IAccessibilityProps & IAriaProps & {
89
51
  underlineColorAndroid?: string;
90
52
  inputAccessoryViewID?: string;
91
53
  style?: ITextStyle;
54
+ hitSlop?: IRectOffset;
55
+ onPress?: ITextInputEventHandler;
56
+ onPressIn?: ITextInputEventHandler;
57
+ onPressOut?: ITextInputEventHandler;
92
58
  onValueChange?: (event: ITextInputChangeEvent) => void;
59
+ onChangeText?: (event: ITextInputChangeEvent) => void;
93
60
  onFocus?: ITextInputEventHandler;
94
61
  onBlur?: ITextInputEventHandler;
95
62
  onEndEditing?: ITextInputEventHandler;
@@ -11,7 +11,6 @@
11
11
  // latest count, while `lastNativeText` is bookkeeping the controlled-write effect mutates
12
12
  // without a render. So the logic layer is the pure folds/maps + the controlled-write predicate;
13
13
  // each adapter holds the two pieces in ITS own primitives (React useState/useRef, Vue ref/let).
14
- import { Platform } from '@symbiote-native/engine';
15
14
  // The acknowledged count starts at 0, native has reported nothing yet, so the first
16
15
  // controlled write echoes 0 and lands on eventLag 0.
17
16
  export const INITIAL_EVENT_COUNT = 0;
@@ -19,156 +18,20 @@ export const INITIAL_EVENT_COUNT = 0;
19
18
  // leave the caret where it is rather than move it (used on a controlled write with no
20
19
  // explicit selection).
21
20
  export const SELECTION_NONE = -1;
22
- // The W3C/alias → native lookup tables. Typed Record<string, string> (not Record<IInputMode,
23
- // string>) so the one safe-lookup helper (mapAutoComplete) serves every map without a cast;
24
- // the union types above still guard the app-facing prop contract.
25
- // RN's inputMode -> keyboardType map (TextInput.js:815). `search` is platform-split (iOS
26
- // 'web-search', else 'default'); we use the Android/default branch since the folded prop is
27
- // forwarded verbatim and the safe default avoids an unknown keyboardType on Android.
28
- const inputModeToKeyboardType = {
29
- decimal: 'decimal-pad',
30
- email: 'email-address',
31
- none: 'default',
32
- numeric: 'number-pad',
33
- tel: 'phone-pad',
34
- text: 'default',
35
- url: 'url',
36
- };
37
- // `search` is the ONE inputMode token RN resolves per platform (TextInput.js:815-825):
38
- // search: Platform.OS === 'ios' ? 'web-search' : 'default'
39
- // iOS has a dedicated search keyboard whose return key is a magnifier; every other host falls back
40
- // to the default one. It is deliberately absent from the map above, so there is one source for it
41
- // rather than a map entry and an override that can drift apart.
21
+ // THE FOUR W3C->NATIVE LOOKUP TABLES AND THE FOLDS OVER THEM STOOD HERE, and they are the engine's
22
+ // now: `foldTextInputAliases` in `SymbioteFabricProps.cpp`, with
23
+ // `core/engine/cpp/tests/js/text-input-payload.itest.ts` as their contract.
42
24
  //
43
- // The host is an ARGUMENT rather than a `Platform.OS` read inside the body, because the headless
44
- // Platform module always resolves to iOS (platform/index.ts re-exports index.ios) — a direct read
45
- // would leave the android branch permanently unprovable. resolveTextInputProps supplies the real
46
- // value. This costs the callers nothing: the exported fold signature is unchanged.
47
- export function keyboardTypeForInputMode(inputMode, os) {
48
- if (inputMode === 'search')
49
- return os === 'ios' ? 'web-search' : 'default';
50
- return mapAutoComplete(inputModeToKeyboardType, inputMode);
51
- }
52
- // RN's enterKeyHint -> returnKeyType map (TextInput.js:805). Note `enter` -> 'default'.
53
- const enterKeyHintToReturnKeyType = {
54
- done: 'done',
55
- enter: 'default',
56
- go: 'go',
57
- next: 'next',
58
- previous: 'previous',
59
- search: 'search',
60
- send: 'send',
61
- };
62
- // RN's W3C autocomplete -> Android `autoComplete` map (TextInput.js:828). A token with no
63
- // native equivalent passes through unchanged (RN's `?? autoComplete`).
64
- const autoCompleteWebToAndroid = {
65
- 'additional-name': 'name-middle',
66
- 'address-line1': 'postal-address-region',
67
- 'address-line2': 'postal-address-locality',
68
- bday: 'birthdate-full',
69
- 'bday-day': 'birthdate-day',
70
- 'bday-month': 'birthdate-month',
71
- 'bday-year': 'birthdate-year',
72
- 'cc-csc': 'cc-csc',
73
- 'cc-exp': 'cc-exp',
74
- 'cc-exp-month': 'cc-exp-month',
75
- 'cc-exp-year': 'cc-exp-year',
76
- 'cc-number': 'cc-number',
77
- country: 'postal-address-country',
78
- 'current-password': 'password',
79
- email: 'email',
80
- 'family-name': 'name-family',
81
- 'given-name': 'name-given',
82
- 'honorific-prefix': 'name-prefix',
83
- 'honorific-suffix': 'name-suffix',
84
- name: 'name',
85
- 'new-password': 'password-new',
86
- off: 'off',
87
- 'one-time-code': 'sms-otp',
88
- 'postal-code': 'postal-code',
89
- sex: 'gender',
90
- 'street-address': 'street-address',
91
- tel: 'tel',
92
- 'tel-country-code': 'tel-country-code',
93
- 'tel-national': 'tel-national',
94
- username: 'username',
95
- };
96
- // RN's W3C autocomplete -> iOS `textContentType` map (TextInput.js:862). A token absent here
97
- // leaves textContentType undefined on iOS (RN's `autoComplete in map` guard).
98
- const autoCompleteWebToTextContentType = {
99
- 'additional-name': 'middleName',
100
- 'address-line1': 'streetAddressLine1',
101
- 'address-line2': 'streetAddressLine2',
102
- bday: 'birthdate',
103
- 'bday-day': 'birthdateDay',
104
- 'bday-month': 'birthdateMonth',
105
- 'bday-year': 'birthdateYear',
106
- 'cc-additional-name': 'creditCardMiddleName',
107
- 'cc-csc': 'creditCardSecurityCode',
108
- 'cc-exp': 'creditCardExpiration',
109
- 'cc-exp-month': 'creditCardExpirationMonth',
110
- 'cc-exp-year': 'creditCardExpirationYear',
111
- 'cc-family-name': 'creditCardFamilyName',
112
- 'cc-given-name': 'creditCardGivenName',
113
- 'cc-name': 'creditCardName',
114
- 'cc-number': 'creditCardNumber',
115
- 'cc-type': 'creditCardType',
116
- country: 'countryName',
117
- 'current-password': 'password',
118
- email: 'emailAddress',
119
- 'family-name': 'familyName',
120
- 'given-name': 'givenName',
121
- 'honorific-prefix': 'namePrefix',
122
- 'honorific-suffix': 'nameSuffix',
123
- name: 'name',
124
- 'new-password': 'newPassword',
125
- nickname: 'nickname',
126
- off: 'none',
127
- 'one-time-code': 'oneTimeCode',
128
- organization: 'organizationName',
129
- 'organization-title': 'jobTitle',
130
- 'postal-code': 'postalCode',
131
- 'street-address': 'fullStreetAddress',
132
- tel: 'telephoneNumber',
133
- url: 'URL',
134
- username: 'username',
135
- };
136
- // Safe lookup into a W3C->native / alias->native map (no `as`): own-property guard, undefined
137
- // if the token has no native equivalent. The caller decides the per-platform fallback. Named
138
- // for its first user (autoComplete), but it is the one generic safe lookup every map shares.
139
- export function mapAutoComplete(map, token) {
140
- return Object.prototype.hasOwnProperty.call(map, token)
141
- ? map[token]
142
- : undefined;
143
- }
144
- // RN folds W3C `autoComplete` per platform (TextInput.js:938): Android takes the mapped
145
- // `autoComplete` token (falling back to the raw token), iOS takes the mapped `textContentType`
146
- // (only when the token is in the map, else leaves it untouched). Symbiote is Metro-built per
147
- // platform but folds platform-agnostically, resolving BOTH native props from the one token:
148
- // the iOS-only `textContentType` is inert on Android and the Android `autoComplete` token is
149
- // inert on iOS, so emitting both is safe: same shape as the dual-keyed events.
150
- export function foldAutoComplete(token) {
151
- if (token === undefined)
152
- return { autoComplete: undefined, textContentType: undefined };
153
- return {
154
- autoComplete: mapAutoComplete(autoCompleteWebToAndroid, token) ?? token,
155
- textContentType: mapAutoComplete(autoCompleteWebToTextContentType, token),
156
- };
157
- }
158
- // RN's submitBehavior reconciliation (TextInput.js:559). Explicit submitBehavior wins (with
159
- // single-line 'newline' coerced to 'blurAndSubmit'); else it is derived from the legacy
160
- // blurOnSubmit per multiline. Input/output are plain strings: the result is forwarded to the
161
- // native prop verbatim, so no ISubmitBehavior union is needed at the seam.
162
- export function foldSubmitBehavior(submitBehavior, blurOnSubmit, multiline) {
163
- if (submitBehavior !== undefined) {
164
- if (!multiline && submitBehavior === 'newline')
165
- return 'blurAndSubmit';
166
- return submitBehavior;
167
- }
168
- if (multiline)
169
- return blurOnSubmit === true ? 'blurAndSubmit' : 'newline';
170
- return blurOnSubmit !== false ? 'blurAndSubmit' : 'submit';
171
- }
25
+ // Deleted in the same pass as `rippleProps` next door, for the same reason. Nothing called them
26
+ // after the port — only this file's own tests and the barrel — and a rule that lives in two places
27
+ // has two behaviours the day one of them is edited. An exported twin kept alive by its own test is
28
+ // the shape the "no mirrors" rule exists to catch.
29
+ //
30
+ // Gone with them: `inputModeToKeyboardType`, `enterKeyHintToReturnKeyType`,
31
+ // `autoCompleteWebToAndroid`, `autoCompleteWebToTextContentType`, `keyboardTypeForInputMode`,
32
+ // `mapAutoComplete`, `foldAutoComplete`, `foldSubmitBehavior`, `resolveTextInputProps` and its two
33
+ // types. What STAYS below is the machine — `foldText`, the change-event readers, and the
34
+ // controlled-write decision — which runs at gesture rate and calls back into app code.
172
35
  // RN's fold: value wins, else defaultValue, else leave undefined (uncontrolled).
173
36
  export function foldText(value, defaultValue) {
174
37
  if (typeof value === 'string')
@@ -194,44 +57,9 @@ export function eventCountFromChange(event) {
194
57
  export function shouldCommandText(lastNativeText, value) {
195
58
  return typeof value === 'string' && lastNativeText !== value;
196
59
  }
197
- // The whole per-platform-agnostic prop fold in one place (TextInput.js:928-946), shared by every
198
- // adapter: inputMode wins over keyboardType, enterKeyHint over returnKeyType, readOnly over
199
- // editable (inverted), the cursor/selection-handle colors default from selectionColor, the W3C
200
- // autoComplete token folds to the per-platform native prop (an explicit textContentType still
201
- // wins), inputMode forces softInput visibility, and underlineColorAndroid defaults to
202
- // 'transparent' to hide the Material EditText bar.
203
- export function resolveTextInputProps(input) {
204
- const folded = foldAutoComplete(input.autoComplete);
205
- return {
206
- keyboardType: input.inputMode !== undefined
207
- ? keyboardTypeForInputMode(input.inputMode, Platform.OS)
208
- : input.keyboardType,
209
- returnKeyType: input.enterKeyHint !== undefined
210
- ? mapAutoComplete(enterKeyHintToReturnKeyType, input.enterKeyHint)
211
- : input.returnKeyType,
212
- editable: input.readOnly !== undefined ? !input.readOnly : input.editable,
213
- submitBehavior: foldSubmitBehavior(input.submitBehavior, input.blurOnSubmit, input.multiline),
214
- selectionColor: input.selectionColor,
215
- cursorColor: input.cursorColor !== undefined
216
- ? input.cursorColor
217
- : input.selectionColor,
218
- selectionHandleColor: input.selectionHandleColor !== undefined
219
- ? input.selectionHandleColor
220
- : input.selectionColor,
221
- underlineColorAndroid: input.underlineColorAndroid !== undefined
222
- ? input.underlineColorAndroid
223
- : 'transparent',
224
- autoComplete: folded.autoComplete,
225
- textContentType: input.textContentType !== undefined
226
- ? input.textContentType
227
- : folded.textContentType,
228
- showSoftInputOnFocus: input.inputMode !== undefined
229
- ? input.inputMode !== 'none'
230
- : input.showSoftInputOnFocus,
231
- };
232
- }
233
60
  const TEXT_INPUT_CALLBACKS = {
234
61
  onValueChange: true,
62
+ onChangeText: true,
235
63
  onFocus: true,
236
64
  onBlur: true,
237
65
  onEndEditing: true,
@@ -239,5 +67,8 @@ const TEXT_INPUT_CALLBACKS = {
239
67
  onKeyPress: true,
240
68
  onSelectionChange: true,
241
69
  onContentSizeChange: true,
70
+ onPress: true,
71
+ onPressIn: true,
72
+ onPressOut: true,
242
73
  };
243
74
  export const TEXT_INPUT_CALLBACK_NAMES = Object.keys(TEXT_INPUT_CALLBACKS);
@@ -50,6 +50,7 @@ export declare function createHighlightUnderlayRuntime(): IHighlightUnderlayRunt
50
50
  export interface IHighlightUnderlayConfig {
51
51
  delayPressOut: number;
52
52
  hasPressHandler: boolean;
53
+ testOnlyPressed: boolean;
53
54
  schedule: (callback: () => void, ms: number) => () => void;
54
55
  }
55
56
  export interface IHighlightUnderlayCallbacks {
@@ -18,14 +18,15 @@ export const DEFAULT_ACTIVE_OPACITY = 0.2;
18
18
  // finger drifted outside and came back (RESPONDER_INACTIVE_PRESS_OUT -> RESPONDER_ACTIVE_PRESS_IN,
19
19
  // driven by onResponderMove).
20
20
  //
21
- // Our engine dispatches pressIn from exactly ONE place — `core/engine/src/events/index.ts:391`,
22
- // on topTouchStart, ahead of negotiateResponder — and has no drift-back-in re-activation at all.
23
- // So every pressIn we produce is the grant-equivalent, and 0 is the duration that applies. Using
24
- // 150 (which all five adapters did until 2026-08-19) makes every tap fade in visibly slower than
25
- // RN.
21
+ // Our engine's `state/pressable.ts` DOES reproduce that reactivation — `handleResponderMove`
22
+ // re-`activate()`s on a drift-back-in — and `touchable-opacity.ts`'s own `activate` callback picks
23
+ // between the two constants below by checking `event.type === 'responderMove'`, our engine's own
24
+ // dispatch-name equivalent (`core/engine/src/events/index.ts`'s `bubble`/`callOwnListener` both set
25
+ // `type: listenerName`). A prior version of this comment claimed "no drift-back-in re-activation at
26
+ // all" — that was true only until the drift/retention mechanism was added to `state/pressable.ts`
27
+ // and was never re-checked against it; every reactivation was snapping to the active opacity
28
+ // instantly instead of easing over 150ms until this was caught.
26
29
  export const OPACITY_ACTIVE_GRANT_DURATION_MS = 0;
27
- // The re-activation branch. Unreachable today — kept so the constant exists when the press
28
- // machine grows drift-back-in, and because it is the value RN uses there.
29
30
  export const OPACITY_ACTIVE_DURATION_MS = 150;
30
31
  export const OPACITY_INACTIVE_DURATION_MS = 250;
31
32
  export const RESTING_OPACITY = 1;
@@ -128,7 +129,7 @@ export function createHighlightUnderlayRuntime() {
128
129
  return { hideTimerCancel: undefined };
129
130
  }
130
131
  export function createHighlightUnderlayHandlers(config, runtime, callbacks) {
131
- const { delayPressOut, hasPressHandler, schedule } = config;
132
+ const { delayPressOut, hasPressHandler, testOnlyPressed, schedule } = config;
132
133
  const { setShown, onShowUnderlay, onHideUnderlay } = callbacks;
133
134
  function clearHideTimer() {
134
135
  if (runtime.hideTimerCancel !== undefined) {
@@ -144,6 +145,8 @@ export function createHighlightUnderlayHandlers(config, runtime, callbacks) {
144
145
  }
145
146
  function hide() {
146
147
  clearHideTimer();
148
+ if (testOnlyPressed)
149
+ return;
147
150
  if (!hasPressHandler)
148
151
  return;
149
152
  setShown(false);
@@ -57,8 +57,8 @@ export interface IListReducerInputs<ItemT> {
57
57
  initialNumToRender: number;
58
58
  maxToRenderPerBatch: number;
59
59
  updateCellsBatchingPeriod: number;
60
- onEndReachedThreshold: number;
61
- onStartReachedThreshold: number;
60
+ onEndReachedThreshold: number | undefined;
61
+ onStartReachedThreshold: number | undefined;
62
62
  onEndReachedActive: boolean;
63
63
  onStartReachedActive: boolean;
64
64
  viewabilityPairs: IViewabilityConfigCallbackPair<ItemT>[];