@symbiote-native/engine 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/CMakeLists.txt +31 -1
- package/android/build.gradle +4 -1
- package/build/accessibility-info/index.android.js +21 -17
- package/build/accessibility-info/index.ios.js +2 -0
- package/build/accessibility-info/shared.d.ts +1 -1
- package/build/accessibility-props.d.ts +0 -11
- package/build/accessibility-props.js +30 -68
- package/build/alert/shared.d.ts +1 -1
- package/build/alert/shared.js +3 -7
- package/build/animated/graph.js +1 -1
- package/build/animated/leaf-lifecycle.js +2 -2
- package/build/app-state/index.d.ts +1 -1
- package/build/app-state/index.js +35 -27
- package/build/asset-source-resolver.d.ts +2 -0
- package/build/asset-source-resolver.js +13 -0
- package/build/back-handler/index.d.ts +7 -6
- package/build/back-handler/index.js +19 -16
- package/build/debug.js +8 -22
- package/build/dispatch.js +3 -9
- package/build/events/index.js +12 -3
- package/build/fabric-props.js +75 -180
- package/build/fabric.d.ts +2 -8
- package/build/fabric.js +27 -33
- package/build/host-access.d.ts +1 -128
- package/build/host-access.js +96 -205
- package/build/host-behavior.d.ts +1 -100
- package/build/host-behavior.js +125 -311
- package/build/image-loader.d.ts +4 -2
- package/build/image-loader.js +24 -48
- package/build/image-source-resolver.js +3 -7
- package/build/image-source-write.d.ts +1 -12
- package/build/image-source-write.js +28 -36
- package/build/imperative.d.ts +0 -28
- package/build/imperative.js +42 -92
- package/build/index.d.ts +3 -2
- package/build/index.js +30 -43
- package/build/invariant.d.ts +1 -0
- package/build/invariant.js +10 -0
- package/build/keyboard/index.js +11 -32
- package/build/linking/index.android.js +5 -3
- package/build/linking/shared.d.ts +1 -1
- package/build/linking/shared.js +16 -19
- package/build/mutation-buffer.d.ts +0 -177
- package/build/mutation-buffer.js +137 -308
- package/build/native-engine.d.ts +0 -102
- package/build/native-engine.js +38 -98
- package/build/native-events.d.ts +5 -0
- package/build/native-events.js +30 -18
- package/build/native-tree-host.d.ts +0 -21
- package/build/native-tree-host.js +14 -31
- package/build/node.d.ts +0 -212
- package/build/node.js +354 -774
- package/build/permissions-android/index.android.d.ts +59 -0
- package/build/permissions-android/index.android.js +47 -0
- package/build/permissions-android/index.d.ts +1 -115
- package/build/permissions-android/index.ios.d.ts +59 -0
- package/build/permissions-android/index.ios.js +31 -0
- package/build/permissions-android/index.js +3 -184
- package/build/permissions-android/shared.d.ts +63 -0
- package/build/permissions-android/shared.js +66 -0
- package/build/platform/index.android.js +3 -5
- package/build/platform/index.ios.js +3 -3
- package/build/platform/shared.d.ts +4 -0
- package/build/platform/shared.js +9 -0
- package/build/platform-color/index.android.d.ts +4 -0
- package/build/platform-color/index.android.js +7 -0
- package/build/platform-color/index.d.ts +2 -20
- package/build/platform-color/index.js +1 -45
- package/build/platform-color/shared.d.ts +21 -0
- package/build/platform-color/shared.js +41 -0
- package/build/post-commit.js +3 -8
- package/build/process-aspect-ratio.js +3 -7
- package/build/process-background-longhands.js +10 -19
- package/build/process-filter.js +11 -19
- package/build/process-font-variant.js +3 -7
- package/build/registry.d.ts +0 -33
- package/build/registry.js +22 -57
- package/build/report-error.js +4 -18
- package/build/settings/index.android.d.ts +6 -0
- package/build/settings/index.android.js +21 -0
- package/build/settings/index.d.ts +1 -8
- package/build/settings/index.ios.d.ts +8 -0
- package/build/settings/index.ios.js +122 -0
- package/build/settings/index.js +3 -122
- package/build/share/index.android.js +9 -32
- package/build/share/index.ios.js +14 -15
- package/build/share/shared.d.ts +4 -2
- package/build/share/shared.js +7 -10
- package/build/status-bar/index.android.js +1 -1
- package/build/status-bar/index.ios.js +4 -3
- package/build/structured-style.d.ts +0 -9
- package/build/structured-style.js +16 -31
- package/build/styles.js +3 -6
- package/build/surface.d.ts +0 -26
- package/build/surface.js +29 -76
- package/build/text-input-state.js +4 -8
- package/build/toast-android/index.android.d.ts +10 -0
- package/build/toast-android/index.android.js +108 -0
- package/build/toast-android/index.d.ts +1 -10
- package/build/toast-android/index.ios.d.ts +10 -0
- package/build/toast-android/index.ios.js +19 -0
- package/build/toast-android/index.js +3 -108
- package/build/touch-history.js +5 -11
- package/build/tree-host.d.ts +0 -270
- package/build/tree-host.js +63 -153
- package/build/view-config.js +17 -37
- package/cpp/SymbioteFabricProps.cpp +204 -120
- package/cpp/SymbioteFabricProps.h +5 -0
- package/cpp/SymbioteTree.cpp +20 -6
- package/package.json +2 -2
package/android/CMakeLists.txt
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/android/build.gradle
CHANGED
|
@@ -37,7 +37,10 @@ android {
|
|
|
37
37
|
|
|
38
38
|
externalNativeBuild {
|
|
39
39
|
cmake {
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
//
|
|
44
|
-
//
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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})
|
|
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 {
|
|
@@ -1,13 +1,2 @@
|
|
|
1
1
|
export declare const ARIA_ALIAS_KEYS: readonly ["role", "aria-label", "aria-labelledby", "aria-live", "aria-hidden", "aria-busy", "aria-checked", "aria-disabled", "aria-expanded", "aria-selected", "aria-modal", "aria-valuemax", "aria-valuemin", "aria-valuenow", "aria-valuetext"];
|
|
2
|
-
/**
|
|
3
|
-
* Fold the web-alias `aria-*` / `role` props into RN's canonical `accessibility*` props.
|
|
4
|
-
*
|
|
5
|
-
* Returns the input BY IDENTITY when no alias is present — the fast path that keeps this off the
|
|
6
|
-
* hot path for the ~99% of nodes carrying none, and the property idempotence rests on: pass 1
|
|
7
|
-
* blanks every alias, so a second pass finds nothing and returns by identity again.
|
|
8
|
-
*
|
|
9
|
-
* The alias keys are blanked to `undefined` rather than deleted. That is not laziness: `setProp`
|
|
10
|
-
* treats an `undefined` write as a delete and `fabricProps` skips undefined, so a blanked alias
|
|
11
|
-
* cannot reach Fabric, while a `delete` would deoptimise the object's shape on every folded node.
|
|
12
|
-
*/
|
|
13
2
|
export declare function foldAriaProps(props: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -1,27 +1,14 @@
|
|
|
1
|
-
// RN's
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// PRECEDENCE RULES: for the scalars an explicit `accessibility*` WINS and the alias only fills a
|
|
10
|
-
// hole (`if (next.X === undefined)`), while INSIDE the `accessibilityState` / `accessibilityValue`
|
|
11
|
-
// composites the ALIAS wins per field (`ariaBusy ?? existing?.busy`). Both mirror RN's View.js.
|
|
12
|
-
// Anyone "cleaning this up" collapses them into one rule, and every component test stays green
|
|
13
|
-
// while real accessibility silently changes. `core/components/src/accessibility-props.test.ts`
|
|
14
|
-
// pins both directions; read it before touching the branches below.
|
|
15
|
-
//
|
|
16
|
-
// Record-level rather than typed, because the engine's caller has a raw `node.props` bag and an
|
|
17
|
-
// interface is not assignable to `Record<string, unknown>` (no index signature). The typed
|
|
18
|
-
// `resolveAccessibilityProps<T>` in `core/components` stays where adapters already import it and
|
|
19
|
-
// delegates here, keeping its own typed gate so the fast path allocates nothing.
|
|
1
|
+
// RN's aria-* / role -> accessibility* fold, at the layer every path goes through: it can't run
|
|
2
|
+
// per attribute (aria-checked folds against a sibling accessibilityState), so it belongs at the
|
|
3
|
+
// one point where the whole bag is known, the payload build.
|
|
4
|
+
// A move, not a rewrite: the function carries two contradictory precedence rules — scalars let an
|
|
5
|
+
// explicit accessibility* win, alias only fills a hole; the composites let the alias win per field
|
|
6
|
+
// instead. Both mirror RN's View.js. accessibility-props.test.ts pins both directions.
|
|
7
|
+
// Record-level rather than typed: the engine's caller has a raw node.props bag with no index
|
|
8
|
+
// signature. The typed resolveAccessibilityProps<T> in core/components delegates here.
|
|
20
9
|
import { dlog } from './debug.js';
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// unmapped, so `role="listitem"` would have reached Fabric as `listitem` instead of `list` with
|
|
24
|
-
// nothing red anywhere. Diff this against RN's View.js rather than reading it for plausibility.
|
|
10
|
+
// A role that falls through here passes unmapped, reaching Fabric under the wrong name with
|
|
11
|
+
// nothing red anywhere. Diff against RN's View.js, don't just read for plausibility.
|
|
25
12
|
const ROLE_TO_ACCESSIBILITY_ROLE = {
|
|
26
13
|
alert: 'alert',
|
|
27
14
|
button: 'button',
|
|
@@ -52,14 +39,11 @@ const ROLE_TO_ACCESSIBILITY_ROLE = {
|
|
|
52
39
|
timer: 'timer',
|
|
53
40
|
toolbar: 'toolbar',
|
|
54
41
|
};
|
|
55
|
-
// Exported so a behavior
|
|
56
|
-
// list
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
// consumer index a prop type with them — `pickAccessibilityProps` in the Svelte adapter forwards the
|
|
61
|
-
// aria half by looping this list — and it makes the two sides check each other: a name here that is
|
|
62
|
-
// not a key of `IAriaProps` stops compiling at the use site rather than going quietly unforwarded.
|
|
42
|
+
// Exported so a behavior folding a different node's bag can name these without restating the
|
|
43
|
+
// list — slotDerived (host-behavior.ts) takes prop names, so a derived primitive enumerates them.
|
|
44
|
+
// as const rather than readonly string[], so the members are literals: pickAccessibilityProps
|
|
45
|
+
// (Svelte adapter) indexes IAriaProps with them, and a name here not a key of IAriaProps then
|
|
46
|
+
// fails to compile at the use site instead of going quietly unforwarded.
|
|
63
47
|
export const ARIA_ALIAS_KEYS = [
|
|
64
48
|
'role',
|
|
65
49
|
'aria-label',
|
|
@@ -86,28 +70,17 @@ function hasAnyAriaKey(props) {
|
|
|
86
70
|
}
|
|
87
71
|
return false;
|
|
88
72
|
}
|
|
89
|
-
// `isAriaAliasKey` WAS HERE AND IS GONE (2026-09-18). Its whole reason was to maintain
|
|
90
|
-
// `node.hasAriaAlias` from `setProp` — the hottest write path in the engine, 32 001 writes on one
|
|
91
|
-
// benchmark create — and that flag existed only to gate the aria fold inside the headless payload
|
|
92
|
-
// builder. The fold left for `SymbioteFabricProps.cpp`, which recomputes presence from the bag it
|
|
93
|
-
// holds, so the flag became write-only and this function became its only maintainer. Both went.
|
|
94
73
|
function isRecord(value) {
|
|
95
74
|
return typeof value === 'object' && value !== null;
|
|
96
75
|
}
|
|
97
76
|
function fieldOf(source, field) {
|
|
98
77
|
return isRecord(source) ? source[field] : undefined;
|
|
99
78
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
* blanks every alias, so a second pass finds nothing and returns by identity again.
|
|
106
|
-
*
|
|
107
|
-
* The alias keys are blanked to `undefined` rather than deleted. That is not laziness: `setProp`
|
|
108
|
-
* treats an `undefined` write as a delete and `fabricProps` skips undefined, so a blanked alias
|
|
109
|
-
* cannot reach Fabric, while a `delete` would deoptimise the object's shape on every folded node.
|
|
110
|
-
*/
|
|
79
|
+
// Fold the web-alias aria-*/role props into RN's canonical accessibility* props. Returns the input
|
|
80
|
+
// by identity when no alias is present, keeping this off the hot path for nodes carrying none —
|
|
81
|
+
// and idempotent, since pass 1 blanks every alias so a second pass finds nothing.
|
|
82
|
+
// Alias keys are blanked to undefined rather than deleted: setProp treats undefined as a delete
|
|
83
|
+
// and fabricProps skips it, while a real delete would deoptimise the object's shape.
|
|
111
84
|
export function foldAriaProps(props) {
|
|
112
85
|
if (!hasAnyAriaKey(props))
|
|
113
86
|
return props;
|
|
@@ -157,27 +130,16 @@ export function foldAriaProps(props) {
|
|
|
157
130
|
if (typeof role === 'string' && bag.accessibilityRole === undefined) {
|
|
158
131
|
bag.accessibilityRole = ROLE_TO_ACCESSIBILITY_ROLE[role] ?? role;
|
|
159
132
|
}
|
|
160
|
-
//
|
|
161
|
-
// the
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
// A SECOND, SMALLER DIVERGENCE, and this one is ours rather than upstream's — it predates the
|
|
172
|
-
// move down and is kept only because changing it here would be an undeclared behaviour change:
|
|
173
|
-
// upstream gates the composite on `!= null` (View.js:87-92) and this gates on `!== undefined`.
|
|
174
|
-
// So an explicit `aria-busy={null}` builds an all-undefined `accessibilityState` here and builds
|
|
175
|
-
// nothing upstream. The VALUES agree either way — `??` treats null and undefined alike — so only
|
|
176
|
-
// the composite's existence differs.
|
|
177
|
-
//
|
|
178
|
-
// The composite is REPLACED by a fresh literal listing exactly the known fields, so an unknown
|
|
179
|
-
// field riding on the incoming object is dropped. Faithful to RN and pinned by a test; it is the
|
|
180
|
-
// shape of bug that only shows for whoever passes a field RN adds later.
|
|
133
|
+
// Rule two, inside the composites: the polarity inverts and the alias wins per field. Read from
|
|
134
|
+
// the original props, not bag — the loop above has already blanked the aliases there.
|
|
135
|
+
// Upstream bug, ported verbatim: RN's View.js does `checked: ariaChecked ?? accessibilityState
|
|
136
|
+
// ?.checked` with no coercion, so a string "true" reaches native where it declares boolean |
|
|
137
|
+
// 'mixed'. No cast without recording a divergence (aria-fold-parity.test.ts pins it).
|
|
138
|
+
// A second, smaller divergence, ours rather than upstream's: upstream gates the composite on
|
|
139
|
+
// `!= null`, this on `!== undefined`, so `aria-busy={null}` builds an all-undefined
|
|
140
|
+
// accessibilityState here but nothing upstream — the values agree either way.
|
|
141
|
+
// The composite is replaced by a fresh literal listing exactly the known fields, so an unknown
|
|
142
|
+
// field riding on the incoming object is dropped, faithful to RN.
|
|
181
143
|
const existingState = fieldOf(props, 'accessibilityState');
|
|
182
144
|
if (existingState !== undefined ||
|
|
183
145
|
ariaBusy !== undefined ||
|
package/build/alert/shared.d.ts
CHANGED
|
@@ -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;
|
package/build/alert/shared.js
CHANGED
|
@@ -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
|
-
//
|
|
10
|
-
//
|
|
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
|
-
|
|
14
|
-
return [{ text: DEFAULT_POSITIVE_TEXT }];
|
|
15
|
-
}
|
|
16
|
-
return buttons;
|
|
12
|
+
return buttons ?? [{ text: DEFAULT_POSITIVE_TEXT }];
|
|
17
13
|
}
|
package/build/animated/graph.js
CHANGED
|
@@ -278,7 +278,7 @@ export function flushValue(rootNode) {
|
|
|
278
278
|
// in RELEASE builds only (dev was fine, every headless test was fine, and the code was even
|
|
279
279
|
// present in the bundle — it just never ran). Adding a bare `import './interpolation-node'` to
|
|
280
280
|
// the barrel does NOT fix it either: Babel merges that import with the adjacent re-export of the
|
|
281
|
-
// same specifier, and the merged dependency stays lazy. Both were tried on device
|
|
281
|
+
// same specifier, and the merged dependency stays lazy. Both were tried on device.
|
|
282
282
|
//
|
|
283
283
|
// Keeping the class next to its base class removes the cycle, the factory, and the entire class
|
|
284
284
|
// of load-order bug with it: `interpolate()` constructs it directly and there is nothing left to
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// create-animated-component.tsx, Vue's create-animated-component.ts, Svelte's
|
|
8
8
|
// animated-props-runtime.ts, Angular's animated-leaf-binder.ts). Each re-derived the same policy,
|
|
9
9
|
// and they drifted: only Svelte ever grew the rebuild guard below, after a day of device
|
|
10
|
-
// debugging
|
|
10
|
+
// debugging. React's `useMemo(..., [rest])` LOOKS like the same guard but is not - its
|
|
11
11
|
// dependency is a fresh rest-destructured object every render - and Vue's `pendingLeaf === null`
|
|
12
12
|
// answers "has render run yet", not "did anything change". A fix landing in one copy is exactly
|
|
13
13
|
// what <adapters_reach_full_feature_parity> forbids: parity has to be structural.
|
|
@@ -86,7 +86,7 @@ export function createAnimatedLeafLifecycle(label) {
|
|
|
86
86
|
lastWantsNative = wantsNative;
|
|
87
87
|
// A SNAPSHOT, never the reference. A caller can legitimately hand back the SAME object on
|
|
88
88
|
// every update, mutating what its keys resolve to rather than allocating (Svelte's rest
|
|
89
|
-
// proxy does exactly that, device-confirmed
|
|
89
|
+
// proxy does exactly that, device-confirmed); storing it would make the next
|
|
90
90
|
// call run shallowEqualProps on that object against ITSELF - reading the same current values
|
|
91
91
|
// through both sides - which is unconditionally true. `propsChanged` could then never be
|
|
92
92
|
// true again and reconcile would be skipped forever, so every rebuilt AnimatedInterpolation
|
|
@@ -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;
|
package/build/app-state/index.js
CHANGED
|
@@ -24,30 +24,39 @@ const APP_STATE_EVENT = {
|
|
|
24
24
|
focus: 'focus',
|
|
25
25
|
blur: 'blur',
|
|
26
26
|
};
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
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
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
dlog(`AppState: ${NATIVE_EVENT.stateDidChange} -> ${
|
|
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
|
|
72
|
-
// `memoryWarning
|
|
73
|
-
//
|
|
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
|
|
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
|
|
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();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Generic asset-id resolution seam, the non-Image sibling of image-source-resolver.ts. Kept as a
|
|
2
|
+
// separate file so a non-image consumer (audio's `number` source, sqlite's `assetSource`) never
|
|
3
|
+
// has to import something named "image" to resolve its own assets.
|
|
4
|
+
let sourceResolver = source => source;
|
|
5
|
+
export function setAssetSourceResolver(resolve) {
|
|
6
|
+
sourceResolver = resolve;
|
|
7
|
+
}
|
|
8
|
+
// Public mirror of image-source-resolver.ts's resolveImageSource, for non-Image consumers of RN's
|
|
9
|
+
// require()'d-asset resolution (audio's `number` source form, sqlite's `assetSource` option).
|
|
10
|
+
// Headless (no resolver wired) it is the identity, so smokes see the input unchanged.
|
|
11
|
+
export function resolveAssetSource(source) {
|
|
12
|
+
return sourceResolver(source);
|
|
13
|
+
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { type IEventSubscription } from '../native-events';
|
|
2
|
-
declare const
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
declare const DEVICE_BACK_EVENT = "hardwareBackPress";
|
|
3
|
+
export type IBackPressEventName = 'backPress' | 'hardwareBackPress';
|
|
4
|
+
export type IHardwareBackPressEvent = {
|
|
5
|
+
readonly type: typeof DEVICE_BACK_EVENT;
|
|
6
|
+
readonly timeStamp: number;
|
|
5
7
|
};
|
|
6
|
-
export type
|
|
7
|
-
export type IBackPressHandler = () => boolean | null | undefined | void;
|
|
8
|
+
export type IBackPressHandler = (event: IHardwareBackPressEvent) => boolean | null | undefined | void;
|
|
8
9
|
declare class BackHandlerImpl {
|
|
9
10
|
exitApp(): void;
|
|
10
11
|
addEventListener(_eventName: IBackPressEventName, handler: IBackPressHandler): IEventSubscription;
|
|
11
|
-
removeEventListener(_eventName: IBackPressEventName, handler: IBackPressHandler): void;
|
|
12
12
|
}
|
|
13
13
|
export declare const BackHandler: BackHandlerImpl;
|
|
14
|
+
export declare function installBackHandler(): void;
|
|
14
15
|
export {};
|
|
@@ -17,12 +17,12 @@ import { dlog } from '../debug.js';
|
|
|
17
17
|
const DEVICE_EVENT_MANAGER_MODULE = 'DeviceEventManager';
|
|
18
18
|
// The device event native emits when the hardware back button is pressed.
|
|
19
19
|
const DEVICE_BACK_EVENT = 'hardwareBackPress';
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
20
|
+
function nativeTimeStamp(nativeEvent) {
|
|
21
|
+
if (typeof nativeEvent !== 'object' || nativeEvent === null)
|
|
22
|
+
return undefined;
|
|
23
|
+
const timeStamp = Reflect.get(nativeEvent, 'timeStamp');
|
|
24
|
+
return typeof timeStamp === 'number' ? timeStamp : undefined;
|
|
25
|
+
}
|
|
26
26
|
// The registry, in registration order. Invoked in reverse on a back press.
|
|
27
27
|
const backPressSubscriptions = [];
|
|
28
28
|
function isHandled(result) {
|
|
@@ -50,10 +50,14 @@ function getEmitter() {
|
|
|
50
50
|
}
|
|
51
51
|
// Run handlers last-registered-first; the first to return true consumes the press
|
|
52
52
|
// and the rest are skipped. If none consume it, fall through to the native default.
|
|
53
|
-
function dispatchBackPress() {
|
|
53
|
+
function dispatchBackPress(nativeEvent) {
|
|
54
|
+
const event = {
|
|
55
|
+
type: DEVICE_BACK_EVENT,
|
|
56
|
+
timeStamp: nativeTimeStamp(nativeEvent) ?? performance.now(),
|
|
57
|
+
};
|
|
54
58
|
for (let i = backPressSubscriptions.length - 1; i >= 0; i--) {
|
|
55
59
|
const handler = backPressSubscriptions[i];
|
|
56
|
-
if (handler !== undefined && isHandled(handler())) {
|
|
60
|
+
if (handler !== undefined && isHandled(handler(event))) {
|
|
57
61
|
dlog(`BackHandler: back press consumed by handler ${i}`);
|
|
58
62
|
return;
|
|
59
63
|
}
|
|
@@ -90,13 +94,12 @@ class BackHandlerImpl {
|
|
|
90
94
|
},
|
|
91
95
|
};
|
|
92
96
|
}
|
|
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
97
|
}
|
|
102
98
|
export const BackHandler = new BackHandlerImpl();
|
|
99
|
+
// Subscribes to `hardwareBackPress` up front, as RN does when BackHandler.android.js loads: Android
|
|
100
|
+
// exits only when JS answers the event with invokeDefaultBackPressHandler, so an app with no handler
|
|
101
|
+
// of its own would otherwise have a dead back button. Called by the host bootstrap, not at module
|
|
102
|
+
// load — a barrel-reached module's load-time side effect does not run in a release bundle.
|
|
103
|
+
export function installBackHandler() {
|
|
104
|
+
getEmitter();
|
|
105
|
+
}
|