@symbiote-native/engine 1.2.0 → 1.3.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 (71) hide show
  1. package/android/CMakeLists.txt +31 -1
  2. package/android/build.gradle +4 -1
  3. package/build/accessibility-info/index.android.js +21 -17
  4. package/build/accessibility-info/index.ios.js +2 -0
  5. package/build/accessibility-info/shared.d.ts +1 -1
  6. package/build/alert/shared.d.ts +1 -1
  7. package/build/alert/shared.js +3 -7
  8. package/build/app-state/index.d.ts +1 -1
  9. package/build/app-state/index.js +35 -27
  10. package/build/back-handler/index.d.ts +7 -2
  11. package/build/back-handler/index.js +19 -10
  12. package/build/events/index.js +10 -1
  13. package/build/fabric-props.js +1 -1
  14. package/build/fabric.d.ts +7 -0
  15. package/build/fabric.js +14 -0
  16. package/build/host-behavior.d.ts +1 -0
  17. package/build/image-loader.d.ts +4 -2
  18. package/build/image-loader.js +14 -25
  19. package/build/image-source-write.d.ts +1 -1
  20. package/build/image-source-write.js +14 -2
  21. package/build/index.d.ts +2 -2
  22. package/build/index.js +6 -6
  23. package/build/invariant.d.ts +1 -0
  24. package/build/invariant.js +10 -0
  25. package/build/keyboard/index.js +11 -32
  26. package/build/linking/index.android.js +5 -3
  27. package/build/linking/shared.d.ts +1 -1
  28. package/build/linking/shared.js +16 -19
  29. package/build/native-events.d.ts +5 -0
  30. package/build/native-events.js +21 -0
  31. package/build/node.js +18 -2
  32. package/build/permissions-android/index.android.d.ts +59 -0
  33. package/build/permissions-android/index.android.js +47 -0
  34. package/build/permissions-android/index.d.ts +1 -115
  35. package/build/permissions-android/index.ios.d.ts +59 -0
  36. package/build/permissions-android/index.ios.js +31 -0
  37. package/build/permissions-android/index.js +3 -184
  38. package/build/permissions-android/shared.d.ts +63 -0
  39. package/build/permissions-android/shared.js +66 -0
  40. package/build/platform/index.android.js +3 -5
  41. package/build/platform/index.ios.js +3 -3
  42. package/build/platform/shared.d.ts +4 -0
  43. package/build/platform/shared.js +9 -0
  44. package/build/platform-color/index.android.d.ts +4 -0
  45. package/build/platform-color/index.android.js +7 -0
  46. package/build/platform-color/index.d.ts +2 -20
  47. package/build/platform-color/index.js +1 -45
  48. package/build/platform-color/shared.d.ts +21 -0
  49. package/build/platform-color/shared.js +41 -0
  50. package/build/settings/index.android.d.ts +6 -0
  51. package/build/settings/index.android.js +21 -0
  52. package/build/settings/index.d.ts +1 -8
  53. package/build/settings/index.ios.d.ts +8 -0
  54. package/build/settings/index.ios.js +122 -0
  55. package/build/settings/index.js +3 -122
  56. package/build/share/index.android.js +9 -32
  57. package/build/share/index.ios.js +14 -15
  58. package/build/share/shared.d.ts +4 -2
  59. package/build/share/shared.js +7 -10
  60. package/build/status-bar/index.android.js +1 -1
  61. package/build/status-bar/index.ios.js +4 -3
  62. package/build/toast-android/index.android.d.ts +10 -0
  63. package/build/toast-android/index.android.js +108 -0
  64. package/build/toast-android/index.d.ts +1 -10
  65. package/build/toast-android/index.ios.d.ts +10 -0
  66. package/build/toast-android/index.ios.js +19 -0
  67. package/build/toast-android/index.js +3 -108
  68. package/cpp/SymbioteFabricProps.cpp +204 -120
  69. package/cpp/SymbioteFabricProps.h +5 -0
  70. package/cpp/SymbioteTree.cpp +20 -6
  71. package/package.json +2 -2
@@ -26,6 +26,19 @@ add_library(symbiote_engine
26
26
  # So the JNI shim can say `#include "SymbioteEngineBindings.h"` rather than walk four levels up.
27
27
  target_include_directories(symbiote_engine PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../cpp)
28
28
 
29
+ # `measure` / `measureInWindow` / `measureLayout` need `react/renderer/dom/DOM.h`. libreactnative.so
30
+ # EXPORTS `facebook::react::dom::measure*`, but the prefab ships no `dom/` headers, so without this
31
+ # SymbioteTree.cpp's `__has_include` compiles the throwing stubs. Only that one header is copied into
32
+ # a private root: putting all of ReactCommon on the path would shadow the prefab's own headers.
33
+ set(SYMBIOTE_RN_DOM_HEADER ${REACT_NATIVE_DIR}/ReactCommon/react/renderer/dom/DOM.h)
34
+ if(DEFINED REACT_NATIVE_DIR AND EXISTS ${SYMBIOTE_RN_DOM_HEADER})
35
+ set(SYMBIOTE_RN_DOM_ROOT ${CMAKE_CURRENT_BINARY_DIR}/rn-dom-include)
36
+ file(COPY ${SYMBIOTE_RN_DOM_HEADER} DESTINATION ${SYMBIOTE_RN_DOM_ROOT}/react/renderer/dom)
37
+ target_include_directories(symbiote_engine PRIVATE ${SYMBIOTE_RN_DOM_ROOT})
38
+ else()
39
+ message(WARNING "symbiote_engine: ${SYMBIOTE_RN_DOM_HEADER} not found, measure* will throw")
40
+ endif()
41
+
29
42
  set_target_properties(symbiote_engine PROPERTIES
30
43
  CXX_STANDARD 20
31
44
  CXX_STANDARD_REQUIRED ON
@@ -33,13 +46,30 @@ set_target_properties(symbiote_engine PROPERTIES
33
46
  POSITION_INDEPENDENT_CODE ON
34
47
  )
35
48
 
36
- target_compile_definitions(symbiote_engine PRIVATE -DFOLLY_NO_CONFIG=1)
49
+ # Must match the flags libreactnative.so's folly was built with
50
+ # (ReactAndroid/cmake-utils/folly-flags.cmake). FOLLY_MOBILE is ABI, not tuning: without it arm64
51
+ # picks the SIMD F14 layout while RN's folly is the fallback one, and the link fails on
52
+ # `folly::f14::detail::F14LinkCheck<1>::check()` / `tlsPendingSafeInserts` from any folly::dynamic.
53
+ target_compile_definitions(symbiote_engine PRIVATE
54
+ -DFOLLY_NO_CONFIG=1
55
+ -DFOLLY_HAVE_CLOCK_GETTIME=1
56
+ -DFOLLY_USE_LIBCPP=1
57
+ -DFOLLY_CFG_NO_COROUTINES=1
58
+ -DFOLLY_MOBILE=1
59
+ -DFOLLY_HAVE_RECVMMSG=1
60
+ -DFOLLY_HAVE_PTHREAD=1
61
+ -DFOLLY_HAVE_XSI_STRERROR_R=1
62
+ )
37
63
 
38
64
  # `jsi` and `reactnative` are the only two prefab modules ReactAndroid publishes
39
65
  # (ReactAndroid/build.gradle.kts, the `prefab { }` block — the third is `hermestooling`).
40
66
  # `reactnative` is the merged library that carries react/renderer/uimanager, react/renderer/mounting
41
67
  # and the turbomodule JNI headers our C++ includes; there is no separate
42
68
  # `react_renderer_uimanager` prefab target to link.
69
+ # 16 KB page-size devices (Android 15+) refuse a 4 KB-aligned LOAD segment and drop the whole app
70
+ # into compat mode. NDK r27 still links 4 KB by default; RN's own libraries already ship 16 KB.
71
+ target_link_options(symbiote_engine PRIVATE -Wl,-z,max-page-size=16384)
72
+
43
73
  find_package(ReactAndroid REQUIRED CONFIG)
44
74
  find_package(fbjni REQUIRED CONFIG)
45
75
 
@@ -37,7 +37,10 @@ android {
37
37
 
38
38
  externalNativeBuild {
39
39
  cmake {
40
- arguments "-DANDROID_STL=c++_shared"
40
+ // REACT_NATIVE_DIR: the app's own react-native (the plugin's `reactNativeDir`), so CMake can
41
+ // reach the one ReactCommon header the prefab does not ship (see CMakeLists.txt).
42
+ arguments "-DANDROID_STL=c++_shared",
43
+ "-DREACT_NATIVE_DIR=${project.extensions.getByName('react').reactNativeDir.get().asFile.absolutePath}"
41
44
  }
42
45
  }
43
46
  }
@@ -9,6 +9,7 @@
9
9
  import { createDeviceEventModule } from '../native-modules/index.js';
10
10
  import {} from '../native-events.js';
11
11
  import { dlog } from '../debug.js';
12
+ import { sendAccessibilityEventByTag } from '../fabric.js';
12
13
  import { isBoolean, routeSendAccessibilityEvent, } from './shared.js';
13
14
  // The Android native module name. This is the module the Android JS wrapper
14
15
  // (INativeAccessibilityInfoAndroid) resolves: the stock RN `AccessibilityInfo` Turbo/legacy
@@ -19,6 +20,8 @@ const ACCESSIBILITY_MODULE = 'AccessibilityInfo';
19
20
  // most of them; events with no Android source (iOS-only) are absent and yield an inert
20
21
  // subscription. (RN maps both `change` and `screenReaderChanged` to touchExplorationDidChange.)
21
22
  const ANDROID_DEVICE_EVENT = {
23
+ // RN's deprecated alias (AccessibilityInfo.js EventNames).
24
+ change: 'touchExplorationDidChange',
22
25
  screenReaderChanged: 'touchExplorationDidChange',
23
26
  reduceMotionChanged: 'reduceMotionDidChange',
24
27
  highTextContrastChanged: 'highTextContrastDidChange',
@@ -39,19 +42,18 @@ function getModule() {
39
42
  function getEmitter() {
40
43
  return deviceEventModule.getEmitter();
41
44
  }
42
- // Run a single-callback Android getter as a Promise. Resolves false when the module is
43
- // unlinked OR the optional method is absent on this host; mirrors RN's "missing query ->
44
- // false" contract for the cross-platform getters. The dlog records the miss.
45
- function queryState(pick, label) {
45
+ const MODULE_UNAVAILABLE = 'NativeAccessibilityInfoAndroid is not available';
46
+ // Run a single-callback Android getter as a Promise, REJECTING when it cannot run, as RN does
47
+ // (AccessibilityInfo.js): a required getter names the module, an optional one names itself.
48
+ function queryState(pick, label, isOptional = false) {
46
49
  const module = getModule();
47
- if (module === null) {
48
- dlog(`AccessibilityInfo(android).${label} -> no module (false)`);
49
- return Promise.resolve(false);
50
- }
51
- const getter = pick(module);
52
- if (getter === undefined) {
53
- dlog(`AccessibilityInfo(android).${label} -> method absent (false)`);
54
- return Promise.resolve(false);
50
+ const getter = module === null ? undefined : pick(module);
51
+ if (module === null || getter === undefined) {
52
+ const message = isOptional
53
+ ? `NativeAccessibilityInfoAndroid.${label} is not available`
54
+ : MODULE_UNAVAILABLE;
55
+ dlog(`AccessibilityInfo(android).${label} -> rejected: ${message}`);
56
+ return Promise.reject(new Error(message));
55
57
  }
56
58
  return new Promise(resolve => {
57
59
  getter.call(module, enabled => resolve(enabled));
@@ -70,17 +72,17 @@ class AccessibilityInfoAndroid {
70
72
  return Promise.resolve(false);
71
73
  }
72
74
  isGrayscaleEnabled() {
73
- return queryState(m => m.isGrayscaleEnabled, 'isGrayscaleEnabled');
75
+ return queryState(m => m.isGrayscaleEnabled, 'isGrayscaleEnabled', true);
74
76
  }
75
77
  isInvertColorsEnabled() {
76
- return queryState(m => m.isInvertColorsEnabled, 'isInvertColorsEnabled');
78
+ return queryState(m => m.isInvertColorsEnabled, 'isInvertColorsEnabled', true);
77
79
  }
78
80
  // iOS-only query; resolve false (RN parity).
79
81
  isReduceTransparencyEnabled() {
80
82
  return Promise.resolve(false);
81
83
  }
82
84
  isHighTextContrastEnabled() {
83
- return queryState(m => m.isHighTextContrastEnabled, 'isHighTextContrastEnabled');
85
+ return queryState(m => m.isHighTextContrastEnabled, 'isHighTextContrastEnabled', true);
84
86
  }
85
87
  // iOS-only "Increase Contrast"; Android has no equivalent, so resolve false (RN parity).
86
88
  isDarkerSystemColorsEnabled() {
@@ -91,7 +93,7 @@ class AccessibilityInfoAndroid {
91
93
  return Promise.resolve(false);
92
94
  }
93
95
  isAccessibilityServiceEnabled() {
94
- return queryState(m => m.isAccessibilityServiceEnabled, 'isAccessibilityServiceEnabled');
96
+ return queryState(m => m.isAccessibilityServiceEnabled, 'isAccessibilityServiceEnabled', true);
95
97
  }
96
98
  // Post a string to be announced by the screen reader. No-op without a module.
97
99
  announceForAccessibility(announcement) {
@@ -111,8 +113,10 @@ class AccessibilityInfoAndroid {
111
113
  // bare reactTag can't be resolved back to its SymbioteNode (the mirror is node-keyed), so
112
114
  // this best-effort path is a logged no-op. Callers should use sendAccessibilityEvent(node,
113
115
  // 'focus') with a host ref, which routes a real node through the slot.
116
+ // RN: legacySendAccessibilityEvent(tag, 'focus') -> bridgeless UIManager resolves the tag.
114
117
  setAccessibilityFocus(reactTag) {
115
- dlog(`AccessibilityInfo(android).setAccessibilityFocus(${reactTag}) -> tag-only, no node to route (no-op)`);
118
+ dlog(`AccessibilityInfo(android).setAccessibilityFocus(${reactTag})`);
119
+ sendAccessibilityEventByTag(reactTag, 'focus');
116
120
  }
117
121
  // Recommended UI-change timeout for this user. Resolves the original when the module or
118
122
  // the query is absent (RN parity).
@@ -19,6 +19,8 @@ const ACCESSIBILITY_MODULE = 'AccessibilityManager';
19
19
  // Public event name -> the iOS device event the native side emits. iOS keeps the names
20
20
  // 1:1; the indirection exists only so the mapping stays explicit (Android renames them).
21
21
  const IOS_DEVICE_EVENT = {
22
+ // RN's deprecated alias (AccessibilityInfo.js EventNames).
23
+ change: 'screenReaderChanged',
22
24
  screenReaderChanged: 'screenReaderChanged',
23
25
  reduceMotionChanged: 'reduceMotionChanged',
24
26
  boldTextChanged: 'boldTextChanged',
@@ -1,7 +1,7 @@
1
1
  import { type ISymbioteNode } from '../node';
2
2
  import type { IEventSubscription } from '../native-events';
3
3
  export { isBoolean } from '../type-guards';
4
- export type IAccessibilityChangeEventName = 'screenReaderChanged' | 'reduceMotionChanged' | 'boldTextChanged' | 'grayscaleChanged' | 'invertColorsChanged' | 'reduceTransparencyChanged' | 'darkerSystemColorsChanged' | 'announcementFinished' | 'accessibilityServiceChanged' | 'highTextContrastChanged';
4
+ export type IAccessibilityChangeEventName = 'change' | 'screenReaderChanged' | 'reduceMotionChanged' | 'boldTextChanged' | 'grayscaleChanged' | 'invertColorsChanged' | 'reduceTransparencyChanged' | 'darkerSystemColorsChanged' | 'announcementFinished' | 'accessibilityServiceChanged' | 'highTextContrastChanged';
5
5
  export type IAccessibilityChangeEvent = IAccessibilityChangeEventName;
6
6
  export type IAccessibilityHandle = ISymbioteNode | number | null | undefined;
7
7
  export interface IAccessibilityAnnouncementFinishedEvent {
@@ -16,4 +16,4 @@ export interface IAlertStatic {
16
16
  alert(title?: string, message?: string, buttons?: IAlertButtons, options?: IAlertOptions): void;
17
17
  }
18
18
  export declare const DEFAULT_POSITIVE_TEXT = "OK";
19
- export declare function normalizeButtons(buttons?: IAlertButtons): IAlertButtons;
19
+ export declare function normalizeButtons(buttons?: IAlertButtons | null): IAlertButtons;
@@ -6,12 +6,8 @@
6
6
  // a factory. No native, no `Platform.OS` read here.
7
7
  // The default positive label RN uses when a button carries no text.
8
8
  export const DEFAULT_POSITIVE_TEXT = 'OK';
9
- // Normalize the `buttons` arg into a consistent list: undefined/empty becomes a single
10
- // default "OK" button, exactly as RN does before handing the list to native. Both
11
- // platform files start from this so the no-buttons case behaves identically.
9
+ // Android's `buttons ? buttons : [{text: 'OK'}]` (Alert.js): only an ABSENT list gets the default
10
+ // OK. An explicit empty array is a dialog with no buttons.
12
11
  export function normalizeButtons(buttons) {
13
- if (buttons === undefined || buttons.length === 0) {
14
- return [{ text: DEFAULT_POSITIVE_TEXT }];
15
- }
16
- return buttons;
12
+ return buttons ?? [{ text: DEFAULT_POSITIVE_TEXT }];
17
13
  }
@@ -9,7 +9,7 @@ export type IAppStateStatus = 'inactive' | 'background' | 'active' | 'extension'
9
9
  export type IAppStateEvent = (typeof APP_STATE_EVENT)[keyof typeof APP_STATE_EVENT];
10
10
  declare class AppStateImpl {
11
11
  get isAvailable(): boolean;
12
- get currentState(): string | null;
12
+ get currentState(): string | null | undefined;
13
13
  addEventListener(type: IAppStateEvent, handler: (...args: unknown[]) => void): IEventSubscription;
14
14
  }
15
15
  export declare const AppState: AppStateImpl;
@@ -24,30 +24,39 @@ const APP_STATE_EVENT = {
24
24
  focus: 'focus',
25
25
  blur: 'blur',
26
26
  };
27
- function isStateChangePayload(value) {
28
- return typeof value === 'object' && value !== null && 'app_state' in value;
27
+ // `appStateData.app_state`, read the way RN reads it: no shape check, `undefined` when absent,
28
+ // and a TypeError on a null payload as the property read would throw.
29
+ function appStateOf(payload) {
30
+ if (payload === null || payload === undefined) {
31
+ throw new TypeError(`Cannot read property 'app_state' of ${String(payload)}`);
32
+ }
33
+ const state = Reflect.get(Object(payload), 'app_state');
34
+ return typeof state === 'string' ? state : undefined;
29
35
  }
30
- // Lazily resolved so importing this module has no native side effect: a headless
31
- // run without a fake __turboModuleProxy still loads it; resolution happens on first
32
- // use. `null` when the module isn't linked.
36
+ // `null` until the module resolves (or when it is not linked), as RN's field starts.
33
37
  let currentState = null;
34
- // The self-subscription policy that diverges from a plain lazy-resolve+emitter:
35
- // AppState hydrates `currentState` from the module's initial constants, then keeps it
36
- // fresh forever via a permanent 'appStateDidChange' listener, so a read after a native
37
- // change returns the new value even with nobody else listening (RN parity).
38
+ // AppState.js's constructor, run once when the emitter is built: seed from the constant, keep it
39
+ // fresh from 'appStateDidChange', and ask native for the live state — which wins, and is
40
+ // re-emitted to listeners, unless an event arrived first.
38
41
  const deviceEventModule = createDeviceEventModule({
39
42
  moduleName: APP_STATE_MODULE,
40
43
  moduleLogPrefix: 'AppState: module',
41
44
  onEmitterCreated: (emitter, module) => {
42
- if (module !== null) {
43
- currentState = module.getConstants().initialAppState;
44
- }
45
+ if (module === null)
46
+ return;
47
+ currentState = module.getConstants().initialAppState;
48
+ let eventUpdated = false;
45
49
  emitter.addListener(NATIVE_EVENT.stateDidChange, payload => {
46
- if (!isStateChangePayload(payload))
47
- return;
48
- dlog(`AppState: ${NATIVE_EVENT.stateDidChange} -> ${payload.app_state}`);
49
- currentState = payload.app_state;
50
+ eventUpdated = true;
51
+ currentState = appStateOf(payload);
52
+ dlog(`AppState: ${NATIVE_EVENT.stateDidChange} -> ${String(currentState)}`);
50
53
  });
54
+ module.getCurrentAppState(data => {
55
+ if (!eventUpdated && currentState !== data.app_state) {
56
+ currentState = data.app_state;
57
+ emitter.emit(NATIVE_EVENT.stateDidChange, data);
58
+ }
59
+ }, error => dlog(`AppState.getCurrentAppState failed: ${String(error)}`));
51
60
  },
52
61
  });
53
62
  function getModule() {
@@ -68,33 +77,32 @@ class AppStateImpl {
68
77
  getEmitter();
69
78
  return currentState;
70
79
  }
71
- // Subscribe to an AppState event. Native delivers `appStateDidChange`,
72
- // `memoryWarning`, and `appStateFocusChange`; this maps each onto the requested
73
- // public event. Never throws; a missing module yields a live-but-silent
74
- // subscription (the counters are no-ops without a module).
80
+ // Subscribe to an AppState event, mapped onto the native `appStateDidChange` /
81
+ // `memoryWarning` / `appStateFocusChange`. Throws, as RN does, without a module or for an
82
+ // event it does not know.
75
83
  addEventListener(type, handler) {
84
+ if (getModule() === null) {
85
+ throw new Error('Cannot use AppState when `isAvailable` is false.');
86
+ }
76
87
  const eventEmitter = getEmitter();
77
88
  dlog(`AppState.addEventListener -> ${type}`);
78
89
  switch (type) {
79
90
  case APP_STATE_EVENT.change:
80
- return eventEmitter.addListener(NATIVE_EVENT.stateDidChange, payload => {
81
- if (!isStateChangePayload(payload))
82
- return;
83
- handler(payload.app_state);
84
- });
91
+ return eventEmitter.addListener(NATIVE_EVENT.stateDidChange, payload => handler(appStateOf(payload)));
85
92
  case APP_STATE_EVENT.memoryWarning:
86
93
  return eventEmitter.addListener(NATIVE_EVENT.memoryWarning, () => handler());
87
94
  case APP_STATE_EVENT.focus:
88
95
  return eventEmitter.addListener(NATIVE_EVENT.focusChange, hasFocus => {
89
- if (hasFocus === true)
96
+ if (hasFocus)
90
97
  handler();
91
98
  });
92
99
  case APP_STATE_EVENT.blur:
93
100
  return eventEmitter.addListener(NATIVE_EVENT.focusChange, hasFocus => {
94
- if (hasFocus === false)
101
+ if (!hasFocus)
95
102
  handler();
96
103
  });
97
104
  }
105
+ throw new Error(`Trying to subscribe to unknown event: ${String(type)}`);
98
106
  }
99
107
  }
100
108
  export const AppState = new AppStateImpl();
@@ -1,14 +1,19 @@
1
1
  import { type IEventSubscription } from '../native-events';
2
+ declare const DEVICE_BACK_EVENT = "hardwareBackPress";
2
3
  declare const BACK_PRESS_EVENT: {
3
4
  readonly backPress: "backPress";
4
5
  readonly hardwareBackPress: "hardwareBackPress";
5
6
  };
6
7
  export type IBackPressEventName = (typeof BACK_PRESS_EVENT)[keyof typeof BACK_PRESS_EVENT];
7
- export type IBackPressHandler = () => boolean | null | undefined | void;
8
+ export type IHardwareBackPressEvent = {
9
+ readonly type: typeof DEVICE_BACK_EVENT;
10
+ readonly timeStamp: number;
11
+ };
12
+ export type IBackPressHandler = (event: IHardwareBackPressEvent) => boolean | null | undefined | void;
8
13
  declare class BackHandlerImpl {
9
14
  exitApp(): void;
10
15
  addEventListener(_eventName: IBackPressEventName, handler: IBackPressHandler): IEventSubscription;
11
- removeEventListener(_eventName: IBackPressEventName, handler: IBackPressHandler): void;
12
16
  }
13
17
  export declare const BackHandler: BackHandlerImpl;
18
+ export declare function installBackHandler(): void;
14
19
  export {};
@@ -23,6 +23,12 @@ const BACK_PRESS_EVENT = {
23
23
  backPress: 'backPress',
24
24
  hardwareBackPress: 'hardwareBackPress',
25
25
  };
26
+ function nativeTimeStamp(nativeEvent) {
27
+ if (typeof nativeEvent !== 'object' || nativeEvent === null)
28
+ return undefined;
29
+ const timeStamp = Reflect.get(nativeEvent, 'timeStamp');
30
+ return typeof timeStamp === 'number' ? timeStamp : undefined;
31
+ }
26
32
  // The registry, in registration order. Invoked in reverse on a back press.
27
33
  const backPressSubscriptions = [];
28
34
  function isHandled(result) {
@@ -50,10 +56,14 @@ function getEmitter() {
50
56
  }
51
57
  // Run handlers last-registered-first; the first to return true consumes the press
52
58
  // and the rest are skipped. If none consume it, fall through to the native default.
53
- function dispatchBackPress() {
59
+ function dispatchBackPress(nativeEvent) {
60
+ const event = {
61
+ type: DEVICE_BACK_EVENT,
62
+ timeStamp: nativeTimeStamp(nativeEvent) ?? performance.now(),
63
+ };
54
64
  for (let i = backPressSubscriptions.length - 1; i >= 0; i--) {
55
65
  const handler = backPressSubscriptions[i];
56
- if (handler !== undefined && isHandled(handler())) {
66
+ if (handler !== undefined && isHandled(handler(event))) {
57
67
  dlog(`BackHandler: back press consumed by handler ${i}`);
58
68
  return;
59
69
  }
@@ -90,13 +100,12 @@ class BackHandlerImpl {
90
100
  },
91
101
  };
92
102
  }
93
- // Legacy unsubscribe kept for RN parity. The modern path is the subscription's
94
- // remove() returned by addEventListener.
95
- removeEventListener(_eventName, handler) {
96
- const index = backPressSubscriptions.indexOf(handler);
97
- if (index !== -1) {
98
- backPressSubscriptions.splice(index, 1);
99
- }
100
- }
101
103
  }
102
104
  export const BackHandler = new BackHandlerImpl();
105
+ // Subscribes to `hardwareBackPress` up front, as RN does when BackHandler.android.js loads: Android
106
+ // exits only when JS answers the event with invokeDefaultBackPressHandler, so an app with no handler
107
+ // of its own would otherwise have a dead back button. Called by the host bootstrap, not at module
108
+ // load — a barrel-reached module's load-time side effect does not run in a release bundle.
109
+ export function installBackHandler() {
110
+ getEmitter();
111
+ }
@@ -380,8 +380,13 @@ export function installEventHandler() {
380
380
  if (topLevelType === TOUCH_START) {
381
381
  // The ternary is gone with the gate: `isSymbioteNode(instanceHandle)` was re-asked here
382
382
  // three lines after the early return above already proved it.
383
- if (isDebug())
383
+ if (isDebug()) {
384
384
  dlog(`event ${TOUCH_START} on ${instanceHandle.component}`);
385
+ // A responder surviving into a one-touch start lost its end/cancel. If it is an ancestor
386
+ // of the target, this whole tap goes to it and the release finally clears it.
387
+ if (currentResponder !== undefined)
388
+ dlog(`touchStart while ${currentResponder.component} still holds the responder`);
389
+ }
385
390
  // Update the touch bank, then attach it so responder handlers (PanResponder)
386
391
  // read each touch's own previous->current delta; RN records before dispatch.
387
392
  recordTouchTrack('start', nativeEvent);
@@ -519,6 +524,10 @@ export function installEventHandler() {
519
524
  return;
520
525
  }
521
526
  if (topLevelType === TOUCH_CANCEL) {
527
+ // Android: a ScrollView whose scroller has not finished (fling tail, spring-back) intercepts
528
+ // the next down natively, and the tap reaches JS as start + cancel with no press.
529
+ if (isDebug())
530
+ dlog(`event ${TOUCH_CANCEL} on ${instanceHandle.component}`);
522
531
  recordTouchTrack('end', nativeEvent);
523
532
  attachTouchHistory(nativeEvent);
524
533
  // A cancel is scoped to the finger(s) removed from `touches`, just like an end. An unrelated
@@ -29,7 +29,7 @@
29
29
  // What is left is the framework-agnostic half: colour processing, the style hoist, and a node's own
30
30
  // `payloadFold`.
31
31
  import { RAW_TEXT_COMPONENT } from './node.js';
32
- import { isProcessableColor, processColor } from './platform-color/index.js';
32
+ import { isProcessableColor, processColor } from './platform-color';
33
33
  import { configProcessedKeys } from './registry.js';
34
34
  import { isRecord } from './type-guards.js';
35
35
  // Color props must reach Fabric as platform ints, not CSS strings. Fabric's C++
package/build/fabric.d.ts CHANGED
@@ -32,7 +32,14 @@ interface IFabricHost extends Omit<IFabricSlot, 'cloneNodeWithNewChildren' | 'cl
32
32
  cloneNodeWithNewChildren(node: IFabricNode, children?: readonly IFabricNode[]): IFabricNode;
33
33
  cloneNodeWithNewChildrenAndProps(node: IFabricNode, newProps: IFabricProps): IFabricNode;
34
34
  cloneNodeWithNewChildrenAndProps(node: IFabricNode, children: readonly IFabricNode[], newProps: IFabricProps): IFabricNode;
35
+ findShadowNodeByTag_DEPRECATED?(tag: number): IFabricNode | null;
35
36
  }
37
+ /**
38
+ * An accessibility event addressed by a bare native TAG, the way RN's bridgeless
39
+ * `UIManager.sendAccessibilityEvent` does it: resolve the tag to its committed shadow node, then send.
40
+ * An unknown tag is dropped, as RN drops it (with a log rather than a throw).
41
+ */
42
+ export declare function sendAccessibilityEventByTag(tag: number, eventType: string): void;
36
43
  declare global {
37
44
  var nativeFabricUIManager: IFabricHost | undefined;
38
45
  }
package/build/fabric.js CHANGED
@@ -6,6 +6,20 @@
6
6
  // host function, so we read each method once and cache a plain facade.
7
7
  import { dlog } from './debug.js';
8
8
  import { installNativeTreeHost } from './native-tree-host.js';
9
+ /**
10
+ * An accessibility event addressed by a bare native TAG, the way RN's bridgeless
11
+ * `UIManager.sendAccessibilityEvent` does it: resolve the tag to its committed shadow node, then send.
12
+ * An unknown tag is dropped, as RN drops it (with a log rather than a throw).
13
+ */
14
+ export function sendAccessibilityEventByTag(tag, eventType) {
15
+ const host = globalThis.nativeFabricUIManager;
16
+ const node = host?.findShadowNodeByTag_DEPRECATED?.(tag);
17
+ if (host === undefined || node === undefined || node === null) {
18
+ dlog(`sendAccessibilityEvent("${eventType}") dropped: no view with tag #${tag}`);
19
+ return;
20
+ }
21
+ host.sendAccessibilityEvent(node, eventType);
22
+ }
9
23
  let cached;
10
24
  // BATCHING IS GONE, and it is worth one paragraph because the idea recurs. `batching-slot.ts`
11
25
  // recorded this slot's calls and replayed them once per commit — three ways, the last handing the
@@ -22,6 +22,7 @@ export interface IHostBehavior {
22
22
  readonly ownedListeners?: readonly string[];
23
23
  readonly slotProps?: Readonly<Record<string, string>>;
24
24
  readonly slotPropsExcept?: readonly string[];
25
+ readonly slotValueFor?: (slotKey: string, value: unknown) => unknown;
25
26
  readonly slotTakesNoChildren?: boolean;
26
27
  readonly slotDerived?: readonly string[];
27
28
  readonly claimedChildren?: Readonly<Record<string, IClaimMode>>;
@@ -6,8 +6,10 @@ export type IImageSize = {
6
6
  export type IImageCacheStatus = 'memory' | 'disk' | 'disk/memory';
7
7
  type ISizeSuccess = (width: number, height: number) => void;
8
8
  type ISizeFailure = (error: unknown) => void;
9
- declare function getSize(uri: string, success?: ISizeSuccess, failure?: ISizeFailure): Promise<IImageSize>;
10
- declare function getSizeWithHeaders(uri: string, headers: Record<string, string>, success?: ISizeSuccess, failure?: ISizeFailure): Promise<IImageSize>;
9
+ declare function getSize(uri: string): Promise<IImageSize>;
10
+ declare function getSize(uri: string, success: ISizeSuccess, failure?: ISizeFailure): undefined;
11
+ declare function getSizeWithHeaders(uri: string, headers: Record<string, string>): Promise<IImageSize>;
12
+ declare function getSizeWithHeaders(uri: string, headers: Record<string, string>, success: ISizeSuccess, failure?: ISizeFailure): undefined;
11
13
  declare function prefetch(uri: string, callback?: (requestId: number) => void): Promise<boolean>;
12
14
  declare function abortPrefetch(requestId: number): void;
13
15
  declare function queryCache(uris: string[]): Promise<Record<string, IImageCacheStatus>>;
@@ -54,40 +54,29 @@ function requireLoader(method) {
54
54
  }
55
55
  return loader;
56
56
  }
57
- // Resolve image dimensions, optionally via success/failure callbacks. Always returns the Promise
58
- // too (RN returns void when a callback is passed, but a promise-and-callback shape is friendlier
59
- // and a strict superset).
57
+ // Image.ios.js / Image.android.js: the promise when no success callback is given; otherwise the
58
+ // result goes to the callbacks and nothing is returned, a missing `failure` becoming a warning.
59
+ function deliverSize(promise, uri, success, failure) {
60
+ if (typeof success !== 'function')
61
+ return promise;
62
+ promise
63
+ .then(size => success(size.width, size.height))
64
+ .catch(typeof failure === 'function'
65
+ ? failure
66
+ : () => console.warn('Failed to get size for image: ' + uri));
67
+ return undefined;
68
+ }
60
69
  function getSize(uri, success, failure) {
61
70
  const promise = Promise.resolve()
62
71
  .then(() => requireLoader('getSize').getSize(uri))
63
72
  .then(toImageSize);
64
- if (typeof success === 'function') {
65
- promise
66
- .then(size => success(size.width, size.height))
67
- .catch((error) => {
68
- if (typeof failure === 'function')
69
- failure(error);
70
- else
71
- dlog(`Image.getSize failed for ${uri}: ${String(error)}`);
72
- });
73
- }
74
- return promise;
73
+ return deliverSize(promise, uri, success, failure);
75
74
  }
76
75
  function getSizeWithHeaders(uri, headers, success, failure) {
77
76
  const promise = Promise.resolve()
78
77
  .then(() => requireLoader('getSizeWithHeaders').getSizeWithHeaders(uri, headers))
79
78
  .then(toImageSize);
80
- if (typeof success === 'function') {
81
- promise
82
- .then(size => success(size.width, size.height))
83
- .catch((error) => {
84
- if (typeof failure === 'function')
85
- failure(error);
86
- else
87
- dlog(`Image.getSizeWithHeaders failed for ${uri}: ${String(error)}`);
88
- });
89
- }
90
- return promise;
79
+ return deliverSize(promise, uri, success, failure);
91
80
  }
92
81
  // Android keys an in-flight prefetch by a monotonic requestId (so abortRequest can cancel it);
93
82
  // RN's Image.android.js generates the same way. iOS ignores the arg.
@@ -10,7 +10,7 @@ export declare const IMAGE_SOURCE_PROPS: ReadonlySet<string>;
10
10
  * whatever it is handed, and a tag with no image behavior must keep its props verbatim — the
11
11
  * control case in `image-payload.itest.ts` is what holds that line.
12
12
  */
13
- export declare function resolveImageSourceProp(value: unknown): unknown;
13
+ export declare function resolveImageSourceProp(value: unknown, dropsSingleSourceHeaders?: boolean): unknown;
14
14
  export declare const IMAGE_LOAD_EVENT_NAMES: ReadonlySet<string>;
15
15
  /** Whether at least one of the four still has a listener installed on the node. */
16
16
  export declare function anyImageLoadEventListenerWired(listeners: ReadonlyMap<string, unknown> | undefined): boolean;
@@ -30,13 +30,25 @@ export const IMAGE_SOURCE_PROPS = new Set([
30
30
  * whatever it is handed, and a tag with no image behavior must keep its props verbatim — the
31
31
  * control case in `image-payload.itest.ts` is what holds that line.
32
32
  */
33
- export function resolveImageSourceProp(value) {
33
+ export function resolveImageSourceProp(value, dropsSingleSourceHeaders = false) {
34
34
  if (value === undefined || value === null)
35
35
  return value;
36
36
  if (typeof value !== 'number' && typeof value !== 'object')
37
37
  return value;
38
38
  const resolved = resolveImageSource(value);
39
- return Array.isArray(resolved) ? resolved : [resolved];
39
+ if (Array.isArray(resolved))
40
+ return resolved;
41
+ // Android `source` only: `Image.android.js` lifts headers to the native `headers` prop just for an
42
+ // ARRAY source, and ReactImageView ignores per-source headers, so RN sends none for a single
43
+ // object. Wrapping here erases that shape, so its headers go here too.
44
+ if (dropsSingleSourceHeaders &&
45
+ typeof resolved === 'object' &&
46
+ resolved !== null &&
47
+ 'headers' in resolved) {
48
+ const { headers: _dropped, ...rest } = resolved;
49
+ return [rest];
50
+ }
51
+ return [resolved];
40
52
  }
41
53
  // `ReactImageView.setShouldNotifyLoadEvents` (Android) — `downloadListener` stays `null`, and
42
54
  // none of these four ever fires, until this prop is `true`. `Image.android.js` sets it whenever
package/build/index.d.ts CHANGED
@@ -90,8 +90,8 @@ export type { IKeyboardEventName, IKeyboardEvent, IKeyboardMetrics, } from './ke
90
90
  export { currentlyFocusedInput, setInputFocused, setInputBlurred, blurTextInput, focusTextInput, } from './text-input-state';
91
91
  export { LayoutAnimation } from './layout-animation';
92
92
  export type { ILayoutAnimationType, ILayoutAnimationProperty, ILayoutAnimationConfig, ILayoutAnimationAnim, ILayoutAnimationTypes, ILayoutAnimationProperties, } from './layout-animation';
93
- export { BackHandler } from './back-handler';
94
- export type { IBackPressEventName, IBackPressHandler } from './back-handler';
93
+ export { BackHandler, installBackHandler } from './back-handler';
94
+ export type { IBackPressEventName, IBackPressHandler, IHardwareBackPressEvent, } from './back-handler';
95
95
  export { PermissionsAndroid, PERMISSIONS, RESULTS, } from './permissions-android';
96
96
  export type { IPermission, IPermissionStatus, IRationale, } from './permissions-android';
97
97
  export { AccessibilityInfo } from './accessibility-info';