@symbiote-native/components 0.5.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -10
- package/build/accessibility-props.d.ts +11 -0
- package/build/accessibility-props.js +30 -118
- package/build/behaviors/activity-indicator/index.android.d.ts +1 -0
- package/build/behaviors/activity-indicator/index.android.js +16 -0
- package/build/behaviors/activity-indicator/index.d.ts +3 -0
- package/build/behaviors/activity-indicator/index.ios.d.ts +1 -0
- package/build/behaviors/activity-indicator/index.ios.js +14 -0
- package/build/behaviors/activity-indicator/index.js +5 -0
- package/build/behaviors/activity-indicator/shared.d.ts +18 -0
- package/build/behaviors/activity-indicator/shared.js +149 -0
- package/build/behaviors/button.d.ts +2 -0
- package/build/behaviors/button.js +328 -0
- package/build/behaviors/image-background.d.ts +2 -0
- package/build/behaviors/image-background.js +139 -0
- package/build/behaviors/image.d.ts +3 -0
- package/build/behaviors/image.js +123 -0
- package/build/behaviors/input-accessory-view.d.ts +3 -0
- package/build/behaviors/input-accessory-view.js +70 -0
- package/build/behaviors/pressable.d.ts +60 -0
- package/build/behaviors/pressable.js +385 -0
- package/build/behaviors/refresh-control.d.ts +2 -0
- package/build/behaviors/refresh-control.js +83 -0
- package/build/behaviors/scroll-view/index.android.d.ts +1 -0
- package/build/behaviors/scroll-view/index.android.js +52 -0
- package/build/behaviors/scroll-view/index.d.ts +2 -0
- package/build/behaviors/scroll-view/index.ios.d.ts +1 -0
- package/build/behaviors/scroll-view/index.ios.js +10 -0
- package/build/behaviors/scroll-view/index.js +5 -0
- package/build/behaviors/scroll-view/shared.d.ts +11 -0
- package/build/behaviors/scroll-view/shared.js +291 -0
- package/build/behaviors/scroll-view/sticky.d.ts +17 -0
- package/build/behaviors/scroll-view/sticky.js +568 -0
- package/build/behaviors/switch.d.ts +2 -0
- package/build/behaviors/switch.js +186 -0
- package/build/behaviors/text-input.d.ts +14 -0
- package/build/behaviors/text-input.js +319 -0
- package/build/behaviors/touchable-highlight.d.ts +9 -0
- package/build/behaviors/touchable-highlight.js +192 -0
- package/build/behaviors/touchable-native-feedback.d.ts +20 -0
- package/build/behaviors/touchable-native-feedback.js +333 -0
- package/build/behaviors/touchable-opacity.d.ts +12 -0
- package/build/behaviors/touchable-opacity.js +227 -0
- package/build/behaviors/touchable-without-feedback.d.ts +2 -0
- package/build/behaviors/touchable-without-feedback.js +296 -0
- package/build/component-names/index.android.js +47 -15
- package/build/component-names/index.ios.js +35 -15
- package/build/component-names/shared.d.ts +2 -1
- package/build/component-names/shared.js +49 -3
- package/build/descriptor.js +4 -4
- package/build/fold-host-bag.d.ts +15 -0
- package/build/fold-host-bag.js +99 -0
- package/build/index.d.ts +23 -12
- package/build/index.js +50 -9
- package/build/register.d.ts +1 -0
- package/build/register.js +55 -0
- package/build/resolve-intrinsic.d.ts +7 -0
- package/build/resolve-intrinsic.js +49 -0
- package/build/scroll-view-commands.d.ts +4 -0
- package/build/scroll-view-commands.js +30 -31
- package/build/state/pressable.d.ts +9 -0
- package/build/state/pressable.js +120 -34
- package/build/state/text-input.d.ts +10 -2
- package/build/state/text-input.js +11 -0
- package/build/text-props.js +2 -1
- package/build/view/render-button.d.ts +36 -1
- package/build/view/render-button.js +101 -12
- package/build/view/render-image/index.d.ts +2 -0
- package/build/view/render-image/index.js +29 -2
- package/build/view/render-input-accessory-view.d.ts +2 -0
- package/build/view/render-input-accessory-view.js +37 -5
- package/build/view/render-modal.js +3 -3
- package/build/view/render-pressable/index.d.ts +2 -0
- package/build/view/render-pressable/index.js +24 -0
- package/build/view/render-scroll-view.d.ts +1 -0
- package/build/view/render-scroll-view.js +18 -9
- package/build/view/render-switch.d.ts +4 -1
- package/build/view/render-switch.js +8 -2
- package/build/view/render-text-input.js +6 -2
- package/build/view/render-touchable-native-feedback.d.ts +19 -0
- package/build/view/render-touchable-native-feedback.js +19 -0
- package/host-primitives.cjs +432 -0
- package/host-primitives.d.cts +33 -0
- package/package.json +33 -5
- package/build/view/render-activity-indicator.d.ts +0 -25
- package/build/view/render-activity-indicator.js +0 -62
- package/build/view/render-image-background.d.ts +0 -9
- package/build/view/render-image-background.js +0 -48
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// the Android RefreshControl wrap, the scroll-event guard/forwarder, and the native sticky
|
|
4
4
|
// scroll-attach are all platform- and framework-invariant, so they live here. The adapter
|
|
5
5
|
// supplies the lifecycle (the node getter, the effect) and re-exports these.
|
|
6
|
-
import { attachNativeEvent,
|
|
6
|
+
import { attachNativeEvent, dlog, flattenStyle, isSymbioteEvent, } from '@symbiote-native/engine';
|
|
7
7
|
// RN's splitLayoutProps key partition (StyleSheet/splitLayoutProps.js): the LAYOUT keys
|
|
8
8
|
// that belong on the OUTER box when a layout-affecting wrapper sits between the laid-out
|
|
9
9
|
// frame and the visual content. Everything NOT in this set (background*, padding*, border*,
|
|
@@ -58,6 +58,24 @@ export function splitLayoutProps(style) {
|
|
|
58
58
|
}
|
|
59
59
|
return { outer, inner };
|
|
60
60
|
}
|
|
61
|
+
// The whole Android wrap style decision: the layout/visual split, AND the axis base composed onto
|
|
62
|
+
// BOTH boxes. RN does the second half too (`StyleSheet.compose(baseStyle, outer)` beside
|
|
63
|
+
// `compose(baseStyle, inner)`, ScrollView.js:1856), and every adapter had dropped it from the
|
|
64
|
+
// wrapper — so an AndroidSwipeRefreshLayout with no explicit user layout style lost `flexGrow: 1`
|
|
65
|
+
// and collapsed to its content height inside a flex parent, where RN's grows.
|
|
66
|
+
//
|
|
67
|
+
// One function rather than five call sites composing `[base, outer]` by hand, because that is what
|
|
68
|
+
// the last one drifted into: a fold written inline is invisible to
|
|
69
|
+
// `tests/lowered-primitive-fold-parity.test.ts`, whose oracle is shared value imports.
|
|
70
|
+
// `style` is `unknown` rather than `IStyleProp`, because a `payloadFold` reads it off an untyped
|
|
71
|
+
// props bag and `flattenStyle` — the only thing that touches it here — already takes `unknown`.
|
|
72
|
+
// Narrowing it would buy a guard at every fold call site and no safety.
|
|
73
|
+
export function splitScrollViewStyle(base, style) {
|
|
74
|
+
const { outer, inner } = splitLayoutProps(flattenStyle(style));
|
|
75
|
+
// Base UNDER the split half on both, so an explicit user value still wins — the same order the
|
|
76
|
+
// unwrapped scroll view composes.
|
|
77
|
+
return { outer: [base, outer], inner: [base, inner] };
|
|
78
|
+
}
|
|
61
79
|
// Re-exported so the package barrel (index.ts) can still export this guard to
|
|
62
80
|
// '@symbiote-native/components' callers, now that it lives in the engine, next to ISymbioteEvent.
|
|
63
81
|
export { isSymbioteEvent };
|
|
@@ -69,42 +87,23 @@ export function forwardScrollEvent(handler, args) {
|
|
|
69
87
|
if (isSymbioteEvent(first))
|
|
70
88
|
handler(first);
|
|
71
89
|
}
|
|
72
|
-
// The imperative handle is identical across platforms
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
90
|
+
// The imperative handle is identical across platforms — only the surrounding element assembly
|
|
91
|
+
// diverges (iOS sibling RefreshControl vs Android wrap) — so it is built once here and both
|
|
92
|
+
// platform files back it with their scroll node getter.
|
|
93
|
+
//
|
|
94
|
+
// It DELEGATES to the node's own methods rather than dispatching commands itself. The commands and
|
|
95
|
+
// their defaults live on `ISymbioteNode` because a LOWERED ScrollView hands the app its engine
|
|
96
|
+
// node directly, with no wrapper to build a handle: two implementations would let `scrollTo()` with
|
|
97
|
+
// no argument mean one thing through a ref and another through a tag, and nothing would report it.
|
|
77
98
|
//
|
|
78
99
|
// `getNode` is a LAZY getter (React `() => ref.current`, Vue `() => nodeRef.value`), read on
|
|
79
100
|
// every call, NOT the node captured once. The node is null at mount and only set after the
|
|
80
101
|
// element commits, so an eager capture would freeze `null` and every command would no-op.
|
|
81
102
|
export function buildScrollViewHandle(getNode) {
|
|
82
103
|
return {
|
|
83
|
-
scrollTo: (options) =>
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return;
|
|
87
|
-
const x = options?.x ?? 0;
|
|
88
|
-
const y = options?.y ?? 0;
|
|
89
|
-
const animated = options?.animated ?? true;
|
|
90
|
-
dlog(`ScrollView.scrollTo x=${x} y=${y} animated=${animated}`);
|
|
91
|
-
dispatchViewCommand(node, 'scrollTo', [x, y, animated]);
|
|
92
|
-
},
|
|
93
|
-
scrollToEnd: (options) => {
|
|
94
|
-
const node = getNode();
|
|
95
|
-
if (node === null)
|
|
96
|
-
return;
|
|
97
|
-
const animated = options?.animated ?? true;
|
|
98
|
-
dlog(`ScrollView.scrollToEnd animated=${animated}`);
|
|
99
|
-
dispatchViewCommand(node, 'scrollToEnd', [animated]);
|
|
100
|
-
},
|
|
101
|
-
flashScrollIndicators: () => {
|
|
102
|
-
const node = getNode();
|
|
103
|
-
if (node === null)
|
|
104
|
-
return;
|
|
105
|
-
dlog('ScrollView.flashScrollIndicators');
|
|
106
|
-
dispatchViewCommand(node, 'flashScrollIndicators', []);
|
|
107
|
-
},
|
|
104
|
+
scrollTo: (options) => getNode()?.scrollTo(options),
|
|
105
|
+
scrollToEnd: (options) => getNode()?.scrollToEnd(options),
|
|
106
|
+
flashScrollIndicators: () => getNode()?.flashScrollIndicators(),
|
|
108
107
|
getScrollNode: () => getNode(),
|
|
109
108
|
};
|
|
110
109
|
}
|
|
@@ -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 DEFAULT_MIN_PRESS_DURATION_MS = 130;
|
|
3
4
|
export declare const DEFAULT_PRESS_RECT_OFFSETS: {
|
|
4
5
|
top: number;
|
|
5
6
|
left: number;
|
|
@@ -56,19 +57,26 @@ export interface IPressRuntime {
|
|
|
56
57
|
longPressCancel: (() => void) | undefined;
|
|
57
58
|
longPressFired: boolean;
|
|
58
59
|
pressDelayCancel: (() => void) | undefined;
|
|
60
|
+
pressOutCancel: (() => void) | undefined;
|
|
59
61
|
pressOrigin: {
|
|
60
62
|
x: number;
|
|
61
63
|
y: number;
|
|
62
64
|
} | undefined;
|
|
63
65
|
driftedOut: boolean;
|
|
64
66
|
region: IResponderRegion | undefined;
|
|
67
|
+
active: boolean;
|
|
68
|
+
activatedAt: number | undefined;
|
|
69
|
+
delayElapsed: boolean;
|
|
70
|
+
disposed: boolean;
|
|
65
71
|
}
|
|
66
72
|
export declare function createPressRuntime(): IPressRuntime;
|
|
73
|
+
export declare function disposePressRuntime(runtime: IPressRuntime): void;
|
|
67
74
|
export type IFrameCallback = (x: number, y: number, width: number, height: number, pageX: number, pageY: number) => void;
|
|
68
75
|
export interface IPressHost {
|
|
69
76
|
setPressed: (pressed: boolean) => void;
|
|
70
77
|
getMeasureFn: () => ((callback: IFrameCallback) => void) | undefined;
|
|
71
78
|
schedule: (callback: () => void, ms: number) => () => void;
|
|
79
|
+
now: () => number;
|
|
72
80
|
}
|
|
73
81
|
export interface IPressMachineConfig {
|
|
74
82
|
onPress?: IPressHandler;
|
|
@@ -78,6 +86,7 @@ export interface IPressMachineConfig {
|
|
|
78
86
|
onLongPress?: IPressHandler;
|
|
79
87
|
delayLongPress: number;
|
|
80
88
|
unstable_pressDelay: number;
|
|
89
|
+
minPressDuration?: number;
|
|
81
90
|
hitSlop?: IRectOffset;
|
|
82
91
|
pressRetentionOffset?: IRectOffset;
|
|
83
92
|
}
|
package/build/state/pressable.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
// the decision of when each callback fires) is here, shared by every adapter.
|
|
12
12
|
import { dlog, Platform } from '@symbiote-native/engine';
|
|
13
13
|
export const DEFAULT_DELAY_LONG_PRESS_MS = 500;
|
|
14
|
+
// Pressability's default active-visual floor for a plain Pressable. Touchable* overrides this to 0.
|
|
15
|
+
export const DEFAULT_MIN_PRESS_DURATION_MS = 130;
|
|
14
16
|
// RN's default extra slop kept around a press once it is active, before a drift fires pressOut.
|
|
15
17
|
// The PressRect extension (Pressability.js DEFAULT_PRESS_RECT_OFFSETS). Per-edge, deeper bottom.
|
|
16
18
|
export const DEFAULT_PRESS_RECT_OFFSETS = {
|
|
@@ -91,11 +93,40 @@ export function createPressRuntime() {
|
|
|
91
93
|
longPressCancel: undefined,
|
|
92
94
|
longPressFired: false,
|
|
93
95
|
pressDelayCancel: undefined,
|
|
96
|
+
pressOutCancel: undefined,
|
|
94
97
|
pressOrigin: undefined,
|
|
95
98
|
driftedOut: false,
|
|
96
99
|
region: undefined,
|
|
100
|
+
active: false,
|
|
101
|
+
activatedAt: undefined,
|
|
102
|
+
delayElapsed: false,
|
|
103
|
+
disposed: false,
|
|
97
104
|
};
|
|
98
105
|
}
|
|
106
|
+
function cancelRuntimeTimer(runtime, key) {
|
|
107
|
+
const cancel = runtime[key];
|
|
108
|
+
if (cancel === undefined)
|
|
109
|
+
return;
|
|
110
|
+
cancel();
|
|
111
|
+
runtime[key] = undefined;
|
|
112
|
+
}
|
|
113
|
+
// RN Pressability.reset(): cancel every timer and make any already-queued callback inert. The
|
|
114
|
+
// adapter invokes this exactly once from its own unmount/destroy hook.
|
|
115
|
+
export function disposePressRuntime(runtime) {
|
|
116
|
+
if (runtime.disposed)
|
|
117
|
+
return;
|
|
118
|
+
runtime.disposed = true;
|
|
119
|
+
cancelRuntimeTimer(runtime, 'longPressCancel');
|
|
120
|
+
cancelRuntimeTimer(runtime, 'pressDelayCancel');
|
|
121
|
+
cancelRuntimeTimer(runtime, 'pressOutCancel');
|
|
122
|
+
runtime.longPressFired = false;
|
|
123
|
+
runtime.pressOrigin = undefined;
|
|
124
|
+
runtime.driftedOut = false;
|
|
125
|
+
runtime.region = undefined;
|
|
126
|
+
runtime.active = false;
|
|
127
|
+
runtime.activatedAt = undefined;
|
|
128
|
+
runtime.delayElapsed = false;
|
|
129
|
+
}
|
|
99
130
|
// Measure the responder's on-screen frame and cache it as the retention region for the life of
|
|
100
131
|
// the press (RN measures on responder grant, _measureResponderRegion). When measure is
|
|
101
132
|
// unavailable (no node yet, an uncommitted node, or a host slot without a measure method,
|
|
@@ -123,7 +154,7 @@ function measureRegion(runtime, measureFn) {
|
|
|
123
154
|
// (the React adapter's useMemo body) lives here, shared by every adapter. The adapter rebuilds
|
|
124
155
|
// these per render (the closures capture the live config) while the runtime persists across them.
|
|
125
156
|
export function createPressHandlers(config, runtime, host) {
|
|
126
|
-
const { onPress, onPressIn, onPressOut, onPressMove, onLongPress, delayLongPress, unstable_pressDelay, } = config;
|
|
157
|
+
const { onPress, onPressIn, onPressOut, onPressMove, onLongPress, delayLongPress, unstable_pressDelay, minPressDuration = DEFAULT_MIN_PRESS_DURATION_MS, } = config;
|
|
127
158
|
// Per-edge offsets for the measured-rect retention test (RN's hitSlop + pressRectOffset).
|
|
128
159
|
// pressRetentionOffset defaults to RN's DEFAULT_PRESS_RECT_OFFSETS when unset; hitSlop to zero.
|
|
129
160
|
const hitSlopRect = normalizeRect(config.hitSlop);
|
|
@@ -145,20 +176,28 @@ export function createPressHandlers(config, runtime, host) {
|
|
|
145
176
|
return (Math.hypot(point.x - origin.x, point.y - origin.y) <= fallbackThreshold);
|
|
146
177
|
}
|
|
147
178
|
function clearLongPress() {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
179
|
+
cancelRuntimeTimer(runtime, 'longPressCancel');
|
|
180
|
+
}
|
|
181
|
+
function clearPressDelay() {
|
|
182
|
+
cancelRuntimeTimer(runtime, 'pressDelayCancel');
|
|
183
|
+
}
|
|
184
|
+
function clearPressOut() {
|
|
185
|
+
cancelRuntimeTimer(runtime, 'pressOutCancel');
|
|
152
186
|
}
|
|
153
|
-
// The real activation: flip pressed-state on, arm the long-press timer, fire onPressIn. Split
|
|
154
|
-
// out so unstable_pressDelay can defer it behind a timer while an early release flushes it.
|
|
155
187
|
function activate(event) {
|
|
188
|
+
if (runtime.disposed || runtime.active || runtime.driftedOut)
|
|
189
|
+
return;
|
|
190
|
+
// A new grant or drift-back-in supersedes a delayed out from the previous active interval.
|
|
191
|
+
clearPressOut();
|
|
192
|
+
runtime.active = true;
|
|
193
|
+
runtime.activatedAt = host.now();
|
|
156
194
|
dlog('Pressable pressIn');
|
|
157
195
|
host.setPressed(true);
|
|
158
|
-
runtime.longPressFired
|
|
159
|
-
if (onLongPress) {
|
|
196
|
+
if (!runtime.longPressFired && onLongPress) {
|
|
160
197
|
runtime.longPressCancel = host.schedule(() => {
|
|
161
198
|
runtime.longPressCancel = undefined;
|
|
199
|
+
if (runtime.disposed || !runtime.active || runtime.driftedOut)
|
|
200
|
+
return;
|
|
162
201
|
runtime.longPressFired = true;
|
|
163
202
|
dlog('Pressable longPress timer fired');
|
|
164
203
|
onLongPress(event);
|
|
@@ -166,51 +205,99 @@ export function createPressHandlers(config, runtime, host) {
|
|
|
166
205
|
}
|
|
167
206
|
onPressIn?.(event);
|
|
168
207
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
208
|
+
function deactivate(event) {
|
|
209
|
+
if (runtime.disposed || !runtime.active)
|
|
210
|
+
return;
|
|
211
|
+
runtime.active = false;
|
|
212
|
+
clearLongPress();
|
|
213
|
+
const activatedAt = runtime.activatedAt ?? host.now();
|
|
214
|
+
runtime.activatedAt = undefined;
|
|
215
|
+
const heldFor = Math.max(0, host.now() - activatedAt);
|
|
216
|
+
const wait = Math.max(minPressDuration - heldFor, 0);
|
|
217
|
+
const finish = () => {
|
|
218
|
+
runtime.pressOutCancel = undefined;
|
|
219
|
+
if (runtime.disposed)
|
|
220
|
+
return;
|
|
221
|
+
host.setPressed(false);
|
|
222
|
+
onPressOut?.(event);
|
|
223
|
+
};
|
|
224
|
+
if (wait > 0) {
|
|
225
|
+
dlog(`Pressable pressOut deferred ${wait}ms`);
|
|
226
|
+
runtime.pressOutCancel = host.schedule(finish, wait);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
finish();
|
|
176
230
|
}
|
|
177
231
|
}
|
|
232
|
+
// An honest release arrives as press then pressOut. If it beat delayPressIn, activate now so the
|
|
233
|
+
// tap still flashes and calls onPressIn; a cancelled/out-of-bounds gesture never calls this path.
|
|
234
|
+
function completePressDelay(event) {
|
|
235
|
+
if (runtime.pressDelayCancel !== undefined)
|
|
236
|
+
clearPressDelay();
|
|
237
|
+
if (!runtime.delayElapsed)
|
|
238
|
+
runtime.delayElapsed = true;
|
|
239
|
+
if (!runtime.driftedOut)
|
|
240
|
+
activate(event);
|
|
241
|
+
}
|
|
242
|
+
function resetGesture() {
|
|
243
|
+
runtime.pressOrigin = undefined;
|
|
244
|
+
runtime.region = undefined;
|
|
245
|
+
runtime.driftedOut = false;
|
|
246
|
+
runtime.delayElapsed = false;
|
|
247
|
+
runtime.longPressFired = false;
|
|
248
|
+
}
|
|
178
249
|
return {
|
|
179
250
|
handlePressIn(event) {
|
|
251
|
+
if (runtime.disposed)
|
|
252
|
+
return;
|
|
253
|
+
// A new grant cancels a delayed out from the prior gesture, matching Pressability's
|
|
254
|
+
// onResponderGrant -> _cancelPressOutDelayTimeout.
|
|
255
|
+
clearPressDelay();
|
|
256
|
+
clearLongPress();
|
|
257
|
+
clearPressOut();
|
|
258
|
+
runtime.active = false;
|
|
259
|
+
runtime.activatedAt = undefined;
|
|
260
|
+
runtime.longPressFired = false;
|
|
261
|
+
runtime.delayElapsed = false;
|
|
180
262
|
runtime.pressOrigin = readPoint(event);
|
|
181
263
|
runtime.driftedOut = false;
|
|
182
|
-
// Measure now so the move stream tests against the real frame, not a press-start radius.
|
|
183
264
|
measureRegion(runtime, host.getMeasureFn());
|
|
184
265
|
if (unstable_pressDelay > 0) {
|
|
185
266
|
dlog(`Pressable pressIn deferred ${unstable_pressDelay}ms`);
|
|
186
267
|
runtime.pressDelayCancel = host.schedule(() => {
|
|
187
268
|
runtime.pressDelayCancel = undefined;
|
|
188
|
-
|
|
269
|
+
if (runtime.disposed)
|
|
270
|
+
return;
|
|
271
|
+
runtime.delayElapsed = true;
|
|
272
|
+
if (!runtime.driftedOut)
|
|
273
|
+
activate(event);
|
|
189
274
|
}, unstable_pressDelay);
|
|
190
275
|
return;
|
|
191
276
|
}
|
|
277
|
+
runtime.delayElapsed = true;
|
|
192
278
|
activate(event);
|
|
193
279
|
},
|
|
194
280
|
handlePressOut(event) {
|
|
281
|
+
if (runtime.disposed)
|
|
282
|
+
return;
|
|
195
283
|
dlog('Pressable pressOut');
|
|
196
|
-
|
|
284
|
+
// A cancellation before delayPressIn must stay inert. An honest release already flushed the
|
|
285
|
+
// delay in handlePress, because the engine emits press immediately before pressOut.
|
|
286
|
+
clearPressDelay();
|
|
197
287
|
clearLongPress();
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
host.setPressed(false);
|
|
201
|
-
onPressOut?.(event);
|
|
288
|
+
deactivate(event);
|
|
289
|
+
resetGesture();
|
|
202
290
|
},
|
|
203
291
|
handlePress(event) {
|
|
292
|
+
if (runtime.disposed)
|
|
293
|
+
return;
|
|
204
294
|
dlog('Pressable press');
|
|
205
|
-
|
|
295
|
+
completePressDelay(event);
|
|
206
296
|
clearLongPress();
|
|
207
|
-
// A drift past the retention region cancels the tap (RN); reset and suppress.
|
|
208
297
|
if (runtime.driftedOut) {
|
|
209
|
-
runtime.driftedOut = false;
|
|
210
298
|
dlog('Pressable press suppressed by drift past retention region');
|
|
211
299
|
return;
|
|
212
300
|
}
|
|
213
|
-
// A fired long press cancels the tap: reset the flag and suppress onPress.
|
|
214
301
|
if (runtime.longPressFired) {
|
|
215
302
|
runtime.longPressFired = false;
|
|
216
303
|
dlog('Pressable press suppressed by prior longPress');
|
|
@@ -218,11 +305,9 @@ export function createPressHandlers(config, runtime, host) {
|
|
|
218
305
|
}
|
|
219
306
|
onPress?.(event);
|
|
220
307
|
},
|
|
221
|
-
// Responder-move stream of the Pressable's own View. onPressMove fires on every move while
|
|
222
|
-
// the press is live (RN). Then the retention test: a move outside the measured region
|
|
223
|
-
// (expanded by hitSlop+pressRectOffset) fires an early pressOut and marks the tap suppressed;
|
|
224
|
-
// a move back inside re-activates.
|
|
225
308
|
handleResponderMove(event) {
|
|
309
|
+
if (runtime.disposed)
|
|
310
|
+
return;
|
|
226
311
|
onPressMove?.(event);
|
|
227
312
|
const here = readPoint(event);
|
|
228
313
|
if (!here)
|
|
@@ -232,14 +317,15 @@ export function createPressHandlers(config, runtime, host) {
|
|
|
232
317
|
dlog('Pressable drifted past retention region — deactivating');
|
|
233
318
|
runtime.driftedOut = true;
|
|
234
319
|
clearLongPress();
|
|
235
|
-
|
|
236
|
-
|
|
320
|
+
// Before delayPressIn there was no activation, so there is no onPressOut to emit.
|
|
321
|
+
deactivate(event);
|
|
237
322
|
}
|
|
238
323
|
}
|
|
239
324
|
else if (runtime.driftedOut) {
|
|
240
325
|
dlog('Pressable returned inside retention region — reactivating');
|
|
241
326
|
runtime.driftedOut = false;
|
|
242
|
-
|
|
327
|
+
if (runtime.delayElapsed)
|
|
328
|
+
activate(event);
|
|
243
329
|
}
|
|
244
330
|
},
|
|
245
331
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IPlatformOSType, ISymbioteEvent, ITextStyle } from '@symbiote-native/engine';
|
|
1
|
+
import type { IMeasureOnSuccess, IMeasureInWindowOnSuccess, IMeasureLayoutOnSuccess, ISymbioteNode, IPlatformOSType, ISymbioteEvent, ITextStyle } from '@symbiote-native/engine';
|
|
2
2
|
import type { IAccessibilityProps, IAriaProps } from '../accessibility-props';
|
|
3
3
|
export type IInputMode = 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
|
|
4
4
|
export type IEnterKeyHint = 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send';
|
|
@@ -53,6 +53,9 @@ export type IFoldedTextInputProps = {
|
|
|
53
53
|
showSoftInputOnFocus: boolean | undefined;
|
|
54
54
|
};
|
|
55
55
|
export declare function resolveTextInputProps(input: ITextInputFoldInput): IFoldedTextInputProps;
|
|
56
|
+
export type ITextInputChangeEvent = ISymbioteEvent & {
|
|
57
|
+
text: string;
|
|
58
|
+
};
|
|
56
59
|
export type ITextInputProps = IAccessibilityProps & IAriaProps & {
|
|
57
60
|
value?: string;
|
|
58
61
|
defaultValue?: string;
|
|
@@ -86,7 +89,7 @@ export type ITextInputProps = IAccessibilityProps & IAriaProps & {
|
|
|
86
89
|
underlineColorAndroid?: string;
|
|
87
90
|
inputAccessoryViewID?: string;
|
|
88
91
|
style?: ITextStyle;
|
|
89
|
-
onValueChange?: (
|
|
92
|
+
onValueChange?: (event: ITextInputChangeEvent) => void;
|
|
90
93
|
onFocus?: ITextInputEventHandler;
|
|
91
94
|
onBlur?: ITextInputEventHandler;
|
|
92
95
|
onEndEditing?: ITextInputEventHandler;
|
|
@@ -95,10 +98,15 @@ export type ITextInputProps = IAccessibilityProps & IAriaProps & {
|
|
|
95
98
|
onSelectionChange?: ITextInputEventHandler;
|
|
96
99
|
onContentSizeChange?: ITextInputEventHandler;
|
|
97
100
|
};
|
|
101
|
+
export declare const TEXT_INPUT_CALLBACK_NAMES: readonly string[];
|
|
98
102
|
export type ITextInputHandle = {
|
|
99
103
|
focus(): void;
|
|
100
104
|
blur(): void;
|
|
101
105
|
clear(): void;
|
|
102
106
|
isFocused(): boolean;
|
|
103
107
|
setSelection(start: number, end: number): void;
|
|
108
|
+
measure(callback: IMeasureOnSuccess): void;
|
|
109
|
+
measureInWindow(callback: IMeasureInWindowOnSuccess): void;
|
|
110
|
+
measureLayout(relativeToNativeNode: ISymbioteNode | number, onSuccess: IMeasureLayoutOnSuccess, onFail?: () => void): void;
|
|
111
|
+
setNativeProps(nativeProps: Record<string, unknown>): void;
|
|
104
112
|
};
|
|
@@ -230,3 +230,14 @@ export function resolveTextInputProps(input) {
|
|
|
230
230
|
: input.showSoftInputOnFocus,
|
|
231
231
|
};
|
|
232
232
|
}
|
|
233
|
+
const TEXT_INPUT_CALLBACKS = {
|
|
234
|
+
onValueChange: true,
|
|
235
|
+
onFocus: true,
|
|
236
|
+
onBlur: true,
|
|
237
|
+
onEndEditing: true,
|
|
238
|
+
onSubmitEditing: true,
|
|
239
|
+
onKeyPress: true,
|
|
240
|
+
onSelectionChange: true,
|
|
241
|
+
onContentSizeChange: true,
|
|
242
|
+
};
|
|
243
|
+
export const TEXT_INPUT_CALLBACK_NAMES = Object.keys(TEXT_INPUT_CALLBACKS);
|
package/build/text-props.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The defaults RN's Text.js applies before handing props to native, folded once for every adapter.
|
|
2
2
|
//
|
|
3
|
-
// RN does this unconditionally on the outer (non-virtual) path, Text.js:
|
|
3
|
+
// RN does this unconditionally on the outer (non-virtual) path, Text.js:289 and :291 (the two
|
|
4
|
+
// lines between them, 288 and 290, are `accessible` and `disabled` — not part of this fold):
|
|
4
5
|
//
|
|
5
6
|
// processedProps.allowFontScaling = allowFontScaling !== false;
|
|
6
7
|
// processedProps.ellipsizeMode = ellipsizeMode ?? 'tail';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ITextStyle, ISymbioteEvent } from '@symbiote-native/engine';
|
|
1
|
+
import type { ITextStyle, IViewStyle, ISymbioteEvent } from '@symbiote-native/engine';
|
|
2
2
|
import type { IAccessibilityProps, IAriaProps } from '../accessibility-props';
|
|
3
3
|
export interface IButtonProps extends IAccessibilityProps, IAriaProps {
|
|
4
4
|
title: string;
|
|
@@ -16,4 +16,39 @@ export interface IButtonProps extends IAccessibilityProps, IAriaProps {
|
|
|
16
16
|
}
|
|
17
17
|
export declare const BUTTON_ACCESSIBILITY_ROLE = "button";
|
|
18
18
|
export declare const buttonTextStyle: ITextStyle;
|
|
19
|
+
export declare const buttonViewStyle: IViewStyle;
|
|
20
|
+
/**
|
|
21
|
+
* The label style with `color` and `disabled` folded in.
|
|
22
|
+
*
|
|
23
|
+
* `color` tints the TEXT on iOS and the BUTTON on Android (Button.js:318-324), so on Android this
|
|
24
|
+
* ignores it — see `resolveButtonViewStyle`, which is where it lands there. `disabled` wins over
|
|
25
|
+
* `color` on both, because RN pushes `textDisabled` after the tint.
|
|
26
|
+
*/
|
|
19
27
|
export declare function resolveButtonTextStyle(color: string | undefined, disabled: boolean | undefined): ITextStyle;
|
|
28
|
+
/**
|
|
29
|
+
* The inner view's style with `color` and `disabled` folded in — `{}` on iOS in every combination,
|
|
30
|
+
* which is RN's own answer there and the reason the node looked droppable.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveButtonViewStyle(color: string | undefined, disabled: boolean | undefined): IViewStyle;
|
|
33
|
+
/**
|
|
34
|
+
* The label as it is rendered: UPPERCASE on Android (Button.js:352-353).
|
|
35
|
+
*
|
|
36
|
+
* RN also asserts the title is a string one line above. Not reproduced: the assert is a dev-only
|
|
37
|
+
* `invariant`, and every adapter here types `title: string`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveButtonTitle(title: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Whether the button is disabled, which `aria-disabled` may decide on its own.
|
|
42
|
+
*
|
|
43
|
+
* Button.js:337-338 — `props.disabled != null ? props.disabled : accessibilityState.disabled`. The
|
|
44
|
+
* engine already folds `aria-disabled` into the committed `accessibilityState`, but that fold is a
|
|
45
|
+
* PAYLOAD fold: it cannot suppress the press or grey the label, which is what this is for.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveButtonDisabled(disabled: boolean | undefined, ariaDisabled: boolean | undefined, accessibilityState: {
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
} | undefined): boolean | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Button.js:356-359 — `'no'` becomes `'no-hide-descendants'`, so the label inside cannot take
|
|
52
|
+
* focus separately from the button that contains it.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveButtonImportantForAccessibility(value: IAccessibilityProps['importantForAccessibility']): IAccessibilityProps['importantForAccessibility'];
|
|
@@ -1,23 +1,112 @@
|
|
|
1
|
-
// Button: shared render half (framework-agnostic)
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Button: shared render half (framework-agnostic). Ported against RN's own
|
|
2
|
+
// `Libraries/Components/Button.js` — a touchable wrapping a `View` wrapping a `Text`, where the
|
|
3
|
+
// VIEW is what carries the look on Android and is empty on iOS.
|
|
4
|
+
//
|
|
5
|
+
// The inner view is not optional chrome. RN's Button is the one control in the library that ships
|
|
6
|
+
// a finished appearance, and on Android that appearance — a filled, elevated, rounded Material
|
|
7
|
+
// button with an uppercased label — lives entirely on that node. A port that renders only the
|
|
8
|
+
// touchable and the text is an iOS port, which is what this file was until 2026-09-09: blue text on
|
|
9
|
+
// nothing, wherever Android was.
|
|
10
|
+
//
|
|
11
|
+
// What is NOT here, because a layer below already does it: the `aria-*` -> `accessibilityState`
|
|
12
|
+
// fold RN performs in `Button.js:326-331`. The engine folds those for every node
|
|
13
|
+
// (`core/engine/src/accessibility-props.ts`), so repeating it here would fold twice.
|
|
14
|
+
import { Platform } from '@symbiote-native/engine';
|
|
15
|
+
// Button.js:394-437, one constant per literal so a value cannot drift silently.
|
|
4
16
|
const IOS_BUTTON_BLUE = '#007AFF';
|
|
5
|
-
const
|
|
17
|
+
const IOS_DISABLED_TEXT = '#cdcdcd';
|
|
18
|
+
const ANDROID_BUTTON_BLUE = '#2196F3';
|
|
19
|
+
const ANDROID_DISABLED_BACKGROUND = '#dfdfdf';
|
|
20
|
+
const ANDROID_DISABLED_TEXT = '#a1a1a1';
|
|
21
|
+
const ANDROID_TEXT = 'white';
|
|
22
|
+
const ANDROID_ELEVATION = 4;
|
|
23
|
+
const ANDROID_DISABLED_ELEVATION = 0;
|
|
24
|
+
const ANDROID_BORDER_RADIUS = 2;
|
|
25
|
+
const TEXT_MARGIN = 8;
|
|
26
|
+
const IOS_FONT_SIZE = 18;
|
|
27
|
+
const ANDROID_FONT_WEIGHT = '500';
|
|
6
28
|
// RN's Button is accessibilityRole="button"; the role string is a native accessibility enum value.
|
|
7
29
|
export const BUTTON_ACCESSIBILITY_ROLE = 'button';
|
|
30
|
+
// `styles.text` — the platform-invariant half plus the platform's own. RN spells the margin as
|
|
31
|
+
// MARGIN, not padding: the label pushes the button's edges outward rather than insetting itself,
|
|
32
|
+
// so a background (Android) or a tap target (both) is 16pt taller than the glyphs.
|
|
8
33
|
export const buttonTextStyle = {
|
|
9
|
-
color: IOS_BUTTON_BLUE,
|
|
10
34
|
textAlign: 'center',
|
|
11
|
-
|
|
12
|
-
|
|
35
|
+
margin: TEXT_MARGIN,
|
|
36
|
+
...Platform.select({
|
|
37
|
+
ios: { color: IOS_BUTTON_BLUE, fontSize: IOS_FONT_SIZE },
|
|
38
|
+
android: { color: ANDROID_TEXT, fontWeight: ANDROID_FONT_WEIGHT },
|
|
39
|
+
default: { color: IOS_BUTTON_BLUE, fontSize: IOS_FONT_SIZE },
|
|
40
|
+
}),
|
|
13
41
|
};
|
|
14
|
-
//
|
|
15
|
-
|
|
42
|
+
// `styles.button` — empty on iOS, the whole Material look on Android.
|
|
43
|
+
export const buttonViewStyle = Platform.select({
|
|
44
|
+
ios: {},
|
|
45
|
+
android: {
|
|
46
|
+
elevation: ANDROID_ELEVATION,
|
|
47
|
+
backgroundColor: ANDROID_BUTTON_BLUE,
|
|
48
|
+
borderRadius: ANDROID_BORDER_RADIUS,
|
|
49
|
+
},
|
|
50
|
+
default: {},
|
|
51
|
+
}) ?? {};
|
|
52
|
+
/**
|
|
53
|
+
* The label style with `color` and `disabled` folded in.
|
|
54
|
+
*
|
|
55
|
+
* `color` tints the TEXT on iOS and the BUTTON on Android (Button.js:318-324), so on Android this
|
|
56
|
+
* ignores it — see `resolveButtonViewStyle`, which is where it lands there. `disabled` wins over
|
|
57
|
+
* `color` on both, because RN pushes `textDisabled` after the tint.
|
|
58
|
+
*/
|
|
16
59
|
export function resolveButtonTextStyle(color, disabled) {
|
|
17
60
|
const style = { ...buttonTextStyle };
|
|
18
|
-
if (color !== undefined)
|
|
61
|
+
if (color !== undefined && Platform.OS !== 'android')
|
|
19
62
|
style.color = color;
|
|
20
|
-
if (disabled === true)
|
|
21
|
-
style.color =
|
|
63
|
+
if (disabled === true) {
|
|
64
|
+
style.color =
|
|
65
|
+
Platform.OS === 'android' ? ANDROID_DISABLED_TEXT : IOS_DISABLED_TEXT;
|
|
66
|
+
}
|
|
67
|
+
return style;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The inner view's style with `color` and `disabled` folded in — `{}` on iOS in every combination,
|
|
71
|
+
* which is RN's own answer there and the reason the node looked droppable.
|
|
72
|
+
*/
|
|
73
|
+
export function resolveButtonViewStyle(color, disabled) {
|
|
74
|
+
if (Platform.OS !== 'android')
|
|
75
|
+
return buttonViewStyle;
|
|
76
|
+
const style = { ...buttonViewStyle };
|
|
77
|
+
if (color !== undefined)
|
|
78
|
+
style.backgroundColor = color;
|
|
79
|
+
if (disabled === true) {
|
|
80
|
+
style.elevation = ANDROID_DISABLED_ELEVATION;
|
|
81
|
+
style.backgroundColor = ANDROID_DISABLED_BACKGROUND;
|
|
82
|
+
}
|
|
22
83
|
return style;
|
|
23
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* The label as it is rendered: UPPERCASE on Android (Button.js:352-353).
|
|
87
|
+
*
|
|
88
|
+
* RN also asserts the title is a string one line above. Not reproduced: the assert is a dev-only
|
|
89
|
+
* `invariant`, and every adapter here types `title: string`.
|
|
90
|
+
*/
|
|
91
|
+
export function resolveButtonTitle(title) {
|
|
92
|
+
return Platform.OS === 'android' ? title.toUpperCase() : title;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Whether the button is disabled, which `aria-disabled` may decide on its own.
|
|
96
|
+
*
|
|
97
|
+
* Button.js:337-338 — `props.disabled != null ? props.disabled : accessibilityState.disabled`. The
|
|
98
|
+
* engine already folds `aria-disabled` into the committed `accessibilityState`, but that fold is a
|
|
99
|
+
* PAYLOAD fold: it cannot suppress the press or grey the label, which is what this is for.
|
|
100
|
+
*/
|
|
101
|
+
export function resolveButtonDisabled(disabled, ariaDisabled, accessibilityState) {
|
|
102
|
+
if (disabled !== undefined)
|
|
103
|
+
return disabled;
|
|
104
|
+
return ariaDisabled ?? accessibilityState?.disabled;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Button.js:356-359 — `'no'` becomes `'no-hide-descendants'`, so the label inside cannot take
|
|
108
|
+
* focus separately from the button that contains it.
|
|
109
|
+
*/
|
|
110
|
+
export function resolveButtonImportantForAccessibility(value) {
|
|
111
|
+
return value === 'no' ? 'no-hide-descendants' : value;
|
|
112
|
+
}
|
|
@@ -53,4 +53,6 @@ export type IImageViewProps = {
|
|
|
53
53
|
referrerPolicy?: string;
|
|
54
54
|
passthrough: Record<string, unknown>;
|
|
55
55
|
};
|
|
56
|
+
export declare const IMAGE_VIEW_PROP_NAMES: readonly ["source", "defaultSource", "loadingIndicatorSource", "style", "resizeMode", "tintColor", "src", "srcSet", "alt", "width", "height", "crossOrigin", "referrerPolicy"];
|
|
57
|
+
export declare function mapImageProps(view: IImageViewProps): Record<string, unknown>;
|
|
56
58
|
export declare function renderImage(view: IImageViewProps): IDescriptor;
|