@symbiote-native/components 1.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -13
- package/build/accessibility-props.d.ts +1 -1
- package/build/accessibility-props.js +2 -2
- 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 +120 -0
- package/build/behaviors/button.d.ts +13 -0
- package/build/behaviors/button.js +340 -0
- package/build/behaviors/image-background.d.ts +3 -0
- package/build/behaviors/image-background.js +155 -0
- package/build/behaviors/image.d.ts +1 -2
- package/build/behaviors/image.js +25 -106
- package/build/behaviors/input-accessory-view.d.ts +1 -2
- package/build/behaviors/input-accessory-view.js +49 -55
- package/build/behaviors/pressable.d.ts +59 -1
- package/build/behaviors/pressable.js +142 -96
- package/build/behaviors/refresh-control.d.ts +2 -0
- package/build/behaviors/refresh-control.js +96 -0
- package/build/behaviors/scroll-view/index.android.d.ts +1 -0
- package/build/behaviors/scroll-view/index.android.js +40 -0
- package/build/behaviors/scroll-view/index.d.ts +3 -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 +8 -0
- package/build/behaviors/scroll-view/responder.d.ts +4 -0
- package/build/behaviors/scroll-view/responder.js +202 -0
- package/build/behaviors/scroll-view/shared.d.ts +9 -0
- package/build/behaviors/scroll-view/shared.js +291 -0
- package/build/behaviors/scroll-view/sticky.d.ts +18 -0
- package/build/behaviors/scroll-view/sticky.js +581 -0
- package/build/behaviors/switch.d.ts +1 -1
- package/build/behaviors/switch.js +49 -88
- package/build/behaviors/text-input.d.ts +2 -2
- package/build/behaviors/text-input.js +244 -104
- package/build/behaviors/touchable-highlight.d.ts +9 -0
- package/build/behaviors/touchable-highlight.js +205 -0
- package/build/behaviors/touchable-native-feedback.d.ts +20 -0
- package/build/behaviors/touchable-native-feedback.js +254 -0
- package/build/behaviors/touchable-opacity.d.ts +12 -0
- package/build/behaviors/touchable-opacity.js +239 -0
- package/build/behaviors/touchable-without-feedback.d.ts +2 -0
- package/build/behaviors/touchable-without-feedback.js +231 -0
- package/build/component-names/index.android.js +31 -25
- package/build/component-names/index.ios.js +28 -23
- package/build/component-names/shared.d.ts +2 -1
- package/build/component-names/shared.js +16 -6
- package/build/descriptor.js +4 -4
- package/build/index.d.ts +26 -26
- package/build/index.js +56 -27
- package/build/register.d.ts +1 -0
- package/build/register.js +55 -0
- package/build/resolve-intrinsic.js +3 -9
- package/build/scroll-view-commands.d.ts +1 -5
- package/build/scroll-view-commands.js +23 -85
- package/build/state/flat-list.d.ts +2 -2
- package/build/state/flat-list.js +10 -2
- package/build/state/pressable.d.ts +6 -1
- package/build/state/pressable.js +63 -28
- package/build/state/section-list.d.ts +2 -0
- package/build/state/section-list.js +14 -7
- package/build/state/text-input.d.ts +10 -40
- package/build/state/text-input.js +17 -186
- package/build/state/touchable.d.ts +1 -0
- package/build/state/touchable.js +11 -8
- package/build/state/virtualized-list-reducer.d.ts +2 -2
- package/build/state/virtualized-list.d.ts +6 -6
- package/build/state/virtualized-list.js +71 -37
- package/build/text-props.d.ts +0 -8
- package/build/text-props.js +14 -25
- package/build/view/render-button.d.ts +11 -4
- package/build/view/render-button.js +74 -22
- package/build/view/render-image/index.d.ts +14 -1
- package/build/view/render-image/index.js +22 -147
- package/build/view/render-input-accessory-view.d.ts +1 -5
- package/build/view/render-input-accessory-view.js +26 -48
- package/build/view/render-keyboard-avoiding-view.d.ts +7 -1
- package/build/view/render-keyboard-avoiding-view.js +40 -1
- package/build/view/render-modal.d.ts +1 -1
- package/build/view/render-modal.js +18 -8
- package/build/view/render-pressable/index.d.ts +3 -0
- package/build/view/render-pressable/index.js +28 -0
- package/build/view/render-scroll-view.d.ts +1 -4
- package/build/view/render-scroll-view.js +17 -54
- package/build/view/render-switch.d.ts +4 -15
- package/build/view/render-switch.js +4 -42
- package/build/view/render-touchable-highlight.d.ts +1 -0
- package/build/view/render-touchable-native-feedback.d.ts +19 -1
- package/build/view/render-touchable-native-feedback.js +34 -9
- package/host-primitives.cjs +178 -280
- package/host-primitives.d.cts +0 -3
- package/package.json +8 -21
- package/build/fold-host-bag.d.ts +0 -15
- package/build/fold-host-bag.js +0 -99
- package/build/state-style.d.ts +0 -15
- package/build/state-style.js +0 -47
- package/build/view/render-activity-indicator.d.ts +0 -25
- package/build/view/render-activity-indicator.js +0 -88
- package/build/view/render-image-background.d.ts +0 -9
- package/build/view/render-image-background.js +0 -48
- package/build/view/render-text-input.d.ts +0 -11
- package/build/view/render-text-input.js +0 -39
- package/lowering-fixtures.cjs +0 -259
- package/lowering-fixtures.d.cts +0 -17
- package/specialize-state-style.cjs +0 -219
- package/specialize-state-style.d.cts +0 -15
|
@@ -3,11 +3,10 @@ import type { ISymbioteEvent } from '@symbiote-native/engine';
|
|
|
3
3
|
import type { IScrollRoutingHandle } from './scroll-routing-handle';
|
|
4
4
|
export declare const DEFAULT_WINDOW_SIZE = 21;
|
|
5
5
|
export declare const DEFAULT_INITIAL_NUM_TO_RENDER = 10;
|
|
6
|
-
export declare const DEFAULT_END_REACHED_THRESHOLD = 2;
|
|
7
6
|
export declare const DEFAULT_MAX_TO_RENDER_PER_BATCH = 10;
|
|
8
7
|
export declare const DEFAULT_UPDATE_CELLS_BATCHING_PERIOD = 50;
|
|
9
8
|
export declare const DEFAULT_VIEW_AREA_COVERAGE_PERCENT_THRESHOLD = 0;
|
|
10
|
-
export declare const
|
|
9
|
+
export declare const DEFAULT_EDGE_REACHED_THRESHOLD_PX = 2;
|
|
11
10
|
export declare const FIRST_INDEX = 0;
|
|
12
11
|
export declare const EMPTY_OFFSET = 0;
|
|
13
12
|
export declare const NO_INDEX = -1;
|
|
@@ -41,6 +40,7 @@ export interface IViewToken<ItemT> {
|
|
|
41
40
|
export interface IViewableItemsChangedInfo<ItemT> {
|
|
42
41
|
viewableItems: IViewToken<ItemT>[];
|
|
43
42
|
changed: IViewToken<ItemT>[];
|
|
43
|
+
viewabilityConfig?: IViewabilityConfig;
|
|
44
44
|
}
|
|
45
45
|
export interface IViewabilityConfig {
|
|
46
46
|
minimumViewTime?: number;
|
|
@@ -94,19 +94,18 @@ export declare function throttleWindow(target: {
|
|
|
94
94
|
first: number;
|
|
95
95
|
last: number;
|
|
96
96
|
};
|
|
97
|
-
export declare function
|
|
98
|
-
export declare function isCellViewable(percent: number, config: IViewabilityConfig): boolean;
|
|
97
|
+
export declare function isCellViewable(cellOffset: number, cellLength: number, scrollOffset: number, viewportLength: number, config: IViewabilityConfig): boolean;
|
|
99
98
|
export declare function offsetForIndex(index: number, viewPosition: number, viewOffset: number, count: number, offsets: number[], lengths: number[], viewportLength: number): number;
|
|
100
99
|
export declare const LAYOUT_EPSILON = 0.01;
|
|
101
100
|
export declare function isSettledLayout(known: number | undefined, reported: number): boolean;
|
|
102
101
|
export declare function averageMeasuredLength(measured: Map<number, number>): number;
|
|
103
102
|
export declare function averageMeasuredStride(measuredOffsets: Map<number, number>, fallback: number): number;
|
|
104
103
|
export declare function highestMeasuredIndex(measured: Map<number, number>): number;
|
|
105
|
-
export declare function computeEndReached(total: number, scrollOffset: number, viewportLength: number, thresholdMultiplier: number): {
|
|
104
|
+
export declare function computeEndReached(total: number, scrollOffset: number, viewportLength: number, thresholdMultiplier: number | undefined): {
|
|
106
105
|
distanceFromEnd: number;
|
|
107
106
|
withinThreshold: boolean;
|
|
108
107
|
};
|
|
109
|
-
export declare function computeStartReached(scrollOffset: number, viewportLength: number, thresholdMultiplier: number): {
|
|
108
|
+
export declare function computeStartReached(scrollOffset: number, viewportLength: number, thresholdMultiplier: number | undefined): {
|
|
110
109
|
distanceFromStart: number;
|
|
111
110
|
withinThreshold: boolean;
|
|
112
111
|
};
|
|
@@ -181,6 +180,7 @@ export interface IMvcpAdjustmentResult {
|
|
|
181
180
|
action: IMvcpAction;
|
|
182
181
|
}
|
|
183
182
|
export declare function computeMvcpAdjustment(params: IMvcpAdjustmentParams): IMvcpAdjustmentResult;
|
|
183
|
+
export declare function defaultKeyExtractor<ItemT>(item: ItemT, index: number): string;
|
|
184
184
|
export declare function resolveItemKey<ItemT>(item: ItemT, index: number, keyExtractor: ((item: ItemT, index: number) => string) | undefined): string;
|
|
185
185
|
export declare function indexOfItem(data: unknown, getItem: (data: unknown, index: number) => unknown, count: number, item: unknown): number;
|
|
186
186
|
export declare function offsetForEnd(total: number, viewportLength: number): number;
|
|
@@ -21,18 +21,20 @@
|
|
|
21
21
|
// module, not a view/render-*.ts.
|
|
22
22
|
import { dlog } from '@symbiote-native/engine';
|
|
23
23
|
// Defaults match RN. windowSize is measured in viewport-lengths (21 => ten screens
|
|
24
|
-
// of buffer on each side of the visible region).
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// maxToRenderPerBatch / batching period mirror RN's incremental fill defaults.
|
|
24
|
+
// of buffer on each side of the visible region). initialNumToRender bounds the first
|
|
25
|
+
// paint before any layout is measured. maxToRenderPerBatch / batching period mirror
|
|
26
|
+
// RN's incremental fill defaults.
|
|
28
27
|
export const DEFAULT_WINDOW_SIZE = 21;
|
|
29
28
|
export const DEFAULT_INITIAL_NUM_TO_RENDER = 10;
|
|
30
|
-
export const DEFAULT_END_REACHED_THRESHOLD = 2;
|
|
31
29
|
export const DEFAULT_MAX_TO_RENDER_PER_BATCH = 10;
|
|
32
30
|
export const DEFAULT_UPDATE_CELLS_BATCHING_PERIOD = 50;
|
|
33
31
|
export const DEFAULT_VIEW_AREA_COVERAGE_PERCENT_THRESHOLD = 0;
|
|
34
|
-
//
|
|
35
|
-
|
|
32
|
+
// `_maybeCallOnEdgeReached`'s OWN fallback (`VirtualizedList.js:1567`) for whether to actually
|
|
33
|
+
// FIRE onEndReached/onStartReached when the app gives no threshold — a flat 2 PIXELS. This is a
|
|
34
|
+
// different RN default from `onEndReachedThresholdOrDefault`'s `?? 2`, which is a MULTIPLE of the
|
|
35
|
+
// visible length used only for internal render-ahead windowing (a concern this engine does not
|
|
36
|
+
// separate out); conflating the two here used to fire the callback two whole screens early.
|
|
37
|
+
export const DEFAULT_EDGE_REACHED_THRESHOLD_PX = 2;
|
|
36
38
|
export const FIRST_INDEX = 0;
|
|
37
39
|
export const EMPTY_OFFSET = 0;
|
|
38
40
|
export const NO_INDEX = -1;
|
|
@@ -178,26 +180,39 @@ export function throttleWindow(target, previous, maxToRenderPerBatch) {
|
|
|
178
180
|
return target;
|
|
179
181
|
return { first, last };
|
|
180
182
|
}
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
183
|
+
// A cell is viewable when its visible fraction clears the configured threshold
|
|
184
|
+
// (`ViewabilityHelper.js`'s `_isViewable` + `computeViewableItems`). Two percents exist and they
|
|
185
|
+
// are NOT interchangeable: `viewAreaCoveragePercentThreshold` is a fraction of the VIEWPORT,
|
|
186
|
+
// `itemVisiblePercentThreshold` a fraction of the CELL's own length — a short cell mostly visible
|
|
187
|
+
// in a tall viewport clears the second easily while failing the first. Area wins whenever it is
|
|
188
|
+
// set (vendor checks `viewAreaCoveragePercentThreshold != null` first); item only when it is not.
|
|
189
|
+
export function isCellViewable(cellOffset, cellLength, scrollOffset, viewportLength, config) {
|
|
190
|
+
const top = cellOffset - scrollOffset;
|
|
191
|
+
const bottom = top + cellLength;
|
|
192
|
+
// No overlap at all: RN's own caller loop never reaches `_isViewable` for such a cell
|
|
193
|
+
// (`top < viewportHeight && bottom > 0` gates the whole scan), so this is exclusion by
|
|
194
|
+
// construction, not a zero percent happening to clear a zero threshold.
|
|
195
|
+
if (bottom <= EMPTY_OFFSET || top >= viewportLength)
|
|
196
|
+
return false;
|
|
197
|
+
// RN's own `_isEntirelyVisible` shortcut: viewable in EITHER mode regardless of the cell's
|
|
198
|
+
// share of the viewport, since an area threshold sized to the viewport could otherwise reject
|
|
199
|
+
// every fully-visible cell smaller than that share. `bottom > top` excludes a zero-length cell
|
|
200
|
+
// (no measurement yet), which vendor falls through to the percent math instead.
|
|
201
|
+
if (top >= EMPTY_OFFSET && bottom <= viewportLength && bottom > top) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
const visiblePixels = Math.min(bottom, viewportLength) - Math.max(top, EMPTY_OFFSET);
|
|
205
|
+
const areaThreshold = config.viewAreaCoveragePercentThreshold;
|
|
206
|
+
const areaMode = areaThreshold !== undefined;
|
|
207
|
+
const threshold = areaMode
|
|
208
|
+
? areaThreshold
|
|
209
|
+
: (config.itemVisiblePercentThreshold ??
|
|
210
|
+
DEFAULT_VIEW_AREA_COVERAGE_PERCENT_THRESHOLD);
|
|
211
|
+
const denominator = areaMode ? viewportLength : cellLength;
|
|
212
|
+
if (denominator <= EMPTY_OFFSET)
|
|
213
|
+
return false;
|
|
214
|
+
const percent = (visiblePixels / denominator) * FULLY_VISIBLE_PERCENT;
|
|
215
|
+
return percent >= threshold;
|
|
201
216
|
}
|
|
202
217
|
// Resolve an index to a pixel offset, optionally biasing where in the viewport the item
|
|
203
218
|
// lands (viewPosition 0=top, 1=bottom, 0.5=center) and an absolute viewOffset nudge,
|
|
@@ -271,11 +286,17 @@ export function highestMeasuredIndex(measured) {
|
|
|
271
286
|
// onEndReached distance + threshold test (RN _maybeCallOnEdgeReached). The adapter still
|
|
272
287
|
// gates on "the last cell is actually rendered" and dedups by content length via its own
|
|
273
288
|
// ref; this returns only the pure geometry.
|
|
289
|
+
//
|
|
290
|
+
// `thresholdMultiplier` is `undefined` for "the app gave no onEndReachedThreshold" — RN's own
|
|
291
|
+
// unset-case answer is a flat `DEFAULT_EDGE_REACHED_THRESHOLD_PX`, never a viewport-length
|
|
292
|
+
// multiple, so `undefined` must NOT be defaulted to a multiplier at the call site.
|
|
274
293
|
export function computeEndReached(total, scrollOffset, viewportLength, thresholdMultiplier) {
|
|
275
294
|
let distanceFromEnd = total - (scrollOffset + viewportLength);
|
|
276
295
|
if (distanceFromEnd < ON_EDGE_REACHED_EPSILON)
|
|
277
296
|
distanceFromEnd = EMPTY_OFFSET;
|
|
278
|
-
const threshold = thresholdMultiplier
|
|
297
|
+
const threshold = thresholdMultiplier != null
|
|
298
|
+
? thresholdMultiplier * viewportLength
|
|
299
|
+
: DEFAULT_EDGE_REACHED_THRESHOLD_PX;
|
|
279
300
|
return { distanceFromEnd, withinThreshold: distanceFromEnd <= threshold };
|
|
280
301
|
}
|
|
281
302
|
// onStartReached twin of computeEndReached. distanceFromStart is just the scroll offset.
|
|
@@ -283,7 +304,9 @@ export function computeStartReached(scrollOffset, viewportLength, thresholdMulti
|
|
|
283
304
|
let distanceFromStart = scrollOffset;
|
|
284
305
|
if (distanceFromStart < ON_EDGE_REACHED_EPSILON)
|
|
285
306
|
distanceFromStart = EMPTY_OFFSET;
|
|
286
|
-
const threshold = thresholdMultiplier
|
|
307
|
+
const threshold = thresholdMultiplier != null
|
|
308
|
+
? thresholdMultiplier * viewportLength
|
|
309
|
+
: DEFAULT_EDGE_REACHED_THRESHOLD_PX;
|
|
287
310
|
return { distanceFromStart, withinThreshold: distanceFromStart <= threshold };
|
|
288
311
|
}
|
|
289
312
|
// Fold the single-config and pairs forms into one list (RN supports either, not both).
|
|
@@ -306,18 +329,15 @@ export function computeViewableSet(params) {
|
|
|
306
329
|
const tokens = [];
|
|
307
330
|
const map = new Map();
|
|
308
331
|
for (let index = params.first; index <= params.last && index < params.count; index += 1) {
|
|
309
|
-
const percent = visiblePercent(params.offsets[index], params.lengths[index], params.scrollOffset, params.viewportLength);
|
|
310
332
|
const item = params.getItem(params.data, index);
|
|
311
|
-
const key = params.keyExtractor
|
|
312
|
-
? params.keyExtractor(item, index)
|
|
313
|
-
: String(index);
|
|
333
|
+
const key = resolveItemKey(item, index, params.keyExtractor);
|
|
314
334
|
let anyViewable = false;
|
|
315
335
|
for (const pair of params.pairs) {
|
|
316
336
|
if (pair.viewabilityConfig.waitForInteraction === true &&
|
|
317
337
|
!params.hasInteracted) {
|
|
318
338
|
continue;
|
|
319
339
|
}
|
|
320
|
-
if (isCellViewable(
|
|
340
|
+
if (isCellViewable(params.offsets[index], params.lengths[index], params.scrollOffset, params.viewportLength, pair.viewabilityConfig)) {
|
|
321
341
|
anyViewable = true;
|
|
322
342
|
break;
|
|
323
343
|
}
|
|
@@ -488,10 +508,24 @@ export function computeMvcpAdjustment(params) {
|
|
|
488
508
|
action: { kind: 'shift', offset: params.scrollOffset + insertedExtent },
|
|
489
509
|
};
|
|
490
510
|
}
|
|
491
|
-
//
|
|
492
|
-
//
|
|
511
|
+
// RN's real default (`VirtualizeUtils.js`'s `keyExtractor`): an object item's own `key`, else its
|
|
512
|
+
// `id`, else the index. Most apps never pass `keyExtractor` at all and rely on this to keep list
|
|
513
|
+
// identity stable across inserts/removes — falling straight to the index (what this used to do)
|
|
514
|
+
// silently breaks that the moment two items swap position.
|
|
515
|
+
export function defaultKeyExtractor(item, index) {
|
|
516
|
+
if (typeof item === 'object' && item !== null) {
|
|
517
|
+
const record = item;
|
|
518
|
+
if (record.key !== undefined && record.key !== null)
|
|
519
|
+
return String(record.key);
|
|
520
|
+
if (record.id !== undefined && record.id !== null)
|
|
521
|
+
return String(record.id);
|
|
522
|
+
}
|
|
523
|
+
return String(index);
|
|
524
|
+
}
|
|
525
|
+
// The default key extractor: the caller's keyExtractor when set, else `defaultKeyExtractor`.
|
|
526
|
+
// Centralized so every adapter's keyForIndex resolves keys identically.
|
|
493
527
|
export function resolveItemKey(item, index, keyExtractor) {
|
|
494
|
-
return keyExtractor
|
|
528
|
+
return (keyExtractor ?? defaultKeyExtractor)(item, index);
|
|
495
529
|
}
|
|
496
530
|
// Linear item -> index lookup for scrollToItem (RN scans by reference identity). NO_INDEX when the
|
|
497
531
|
// item is not in data.
|
package/build/text-props.d.ts
CHANGED
|
@@ -1,9 +1 @@
|
|
|
1
1
|
export type IEllipsizeMode = 'head' | 'middle' | 'tail' | 'clip';
|
|
2
|
-
export interface ITextDefaultableProps {
|
|
3
|
-
ellipsizeMode?: IEllipsizeMode;
|
|
4
|
-
allowFontScaling?: boolean;
|
|
5
|
-
}
|
|
6
|
-
export declare function resolveTextProps<T extends ITextDefaultableProps>(props: T): T & {
|
|
7
|
-
ellipsizeMode: IEllipsizeMode;
|
|
8
|
-
allowFontScaling: boolean;
|
|
9
|
-
};
|
package/build/text-props.js
CHANGED
|
@@ -1,25 +1,14 @@
|
|
|
1
|
-
// The
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
// stable. A fold with two branches that emit different key sets is the exact hazard
|
|
16
|
-
// .claude/rules/solid-descriptor-bridge.md §1 exists for, and Svelte's shim diffs the same way.
|
|
17
|
-
export function resolveTextProps(props) {
|
|
18
|
-
return {
|
|
19
|
-
...props,
|
|
20
|
-
ellipsizeMode: props.ellipsizeMode ?? 'tail',
|
|
21
|
-
// `!== false`, not `?? true`: RN treats an explicit `undefined` and a missing prop alike, and
|
|
22
|
-
// only a literal `false` opts out.
|
|
23
|
-
allowFontScaling: props.allowFontScaling !== false,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
1
|
+
// The ellipsize modes a Text accepts. THE DEFAULTS THAT USED TO BE APPLIED HERE ARE GONE (2026-09-18)
|
|
2
|
+
// — they are the platform's, so they live in the engine's payload builder and reach every `RCTText`
|
|
3
|
+
// whoever authored it: `foldTextDefaults`, `SymbioteFabricProps.cpp`.
|
|
4
|
+
//
|
|
5
|
+
// `resolveTextProps` was the third implementation of a two-line rule. Its one runtime caller was
|
|
6
|
+
// Button's label, which wrote both keys onto a node the builder was about to default anyway — the
|
|
7
|
+
// same seed shape `seedTextDefaults` had in three adapters, at a smaller scale. The rule reaches
|
|
8
|
+
// that node by being keyed on the COMPONENT, so nothing has to hand it down.
|
|
9
|
+
//
|
|
10
|
+
// Why the original fold existed at all, kept because the bug is easy to reintroduce: we declared both
|
|
11
|
+
// props in all four adapters and applied NEITHER default, so native fell back to its own `clip`.
|
|
12
|
+
// Device-observed 2026-08-19 on examples/svelte — a Text with `numberOfLines={1}` cut mid-word with
|
|
13
|
+
// no ellipsis. Nothing failed; the text was simply wrong.
|
|
14
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { 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;
|
|
@@ -14,6 +14,13 @@ export interface IButtonProps extends IAccessibilityProps, IAriaProps {
|
|
|
14
14
|
nextFocusRight?: number;
|
|
15
15
|
nextFocusUp?: number;
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Whether the button is disabled, which `aria-disabled` may decide on its own.
|
|
19
|
+
*
|
|
20
|
+
* Button.js:337-338 — `props.disabled != null ? props.disabled : accessibilityState.disabled`. The
|
|
21
|
+
* engine already folds `aria-disabled` into the committed `accessibilityState`, but that fold is a
|
|
22
|
+
* PAYLOAD fold: it cannot suppress the press or grey the label, which is what this is for.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveButtonDisabled(disabled: boolean | undefined, ariaDisabled: boolean | undefined, accessibilityState: {
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
} | undefined): boolean | undefined;
|
|
@@ -1,23 +1,75 @@
|
|
|
1
|
-
// Button: shared render half (framework-agnostic)
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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. The LABEL's seven
|
|
16
|
+
// went with `resolveButtonTextStyle` on 2026-09-18 — they live in `SymbioteFabricProps.cpp` beside
|
|
17
|
+
// the rule that reads them and are deliberately not duplicated here.
|
|
18
|
+
const ANDROID_BUTTON_BLUE = '#2196F3';
|
|
19
|
+
const ANDROID_DISABLED_BACKGROUND = '#dfdfdf';
|
|
20
|
+
const ANDROID_ELEVATION = 4;
|
|
21
|
+
const ANDROID_DISABLED_ELEVATION = 0;
|
|
22
|
+
const ANDROID_BORDER_RADIUS = 2;
|
|
23
|
+
// `BUTTON_ACCESSIBILITY_ROLE` and `resolveButtonImportantForAccessibility` WERE HERE and are gone
|
|
24
|
+
// (2026-09-18). Both are `foldButtonProps` in `SymbioteFabricProps.cpp` now, and neither had a
|
|
25
|
+
// caller left afterwards — only its own unit test, which is the shape this project calls a mirror:
|
|
26
|
+
// a JS copy of a rule that runs elsewhere, kept alive by the test that asserts it. It would have
|
|
27
|
+
// stayed green forever while meaning nothing.
|
|
28
|
+
// `buttonTextStyle` AND `resolveButtonTextStyle` ARE GONE (2026-09-18) — the label's style is
|
|
29
|
+
// `foldButtonLabelStyle` in `SymbioteFabricProps.cpp`, reached off the label text's own tag. Its
|
|
30
|
+
// constants live THERE now and are not mirrored here; this file keeps only what Android's own fold
|
|
31
|
+
// still needs.
|
|
32
|
+
//
|
|
33
|
+
// It was the last rule in this primitive to move and it needed a seam none of the others did. Its
|
|
34
|
+
// inputs are the BUTTON's `color` and `disabled`, and the node it hangs on is the button's
|
|
35
|
+
// GRANDCHILD on iOS (`button -> view -> text`) and its child on Android — so `ownerProps`, which
|
|
36
|
+
// answers "my parent", could not reach it. `IAncestorLookup` asks for the nearest ancestor carrying
|
|
37
|
+
// a tag instead, which is a CSS ancestor selector and makes one rule right on both trees.
|
|
38
|
+
// `buttonViewStyle` AND `resolveButtonViewStyle` ARE GONE (2026-09-18), and with them the last of
|
|
39
|
+
// Button's folds. The Material look is inside `foldButtonProps` in `SymbioteFabricProps.cpp`, behind
|
|
40
|
+
// `#ifdef ANDROID` — where it belongs, since `{}` on iOS was the whole of its other branch.
|
|
41
|
+
//
|
|
42
|
+
// Its five constants went too rather than staying as a copy nothing reads.
|
|
43
|
+
//
|
|
44
|
+
// WHAT MADE THIS ONE DIFFERENT from the four ports before it: the Android branch is no longer
|
|
45
|
+
// untestable. The test host grew an arm that compiles `#ifdef ANDROID`
|
|
46
|
+
// (`core/engine/cpp/tests/CMakeLists.txt`, `SYMBIOTE_PLATFORM_ANDROID`), so the style, the `color`
|
|
47
|
+
// override and the disabled greying are asserted against the COMMITTED PAYLOAD in
|
|
48
|
+
// `core/engine/cpp/tests/js/android-rules.itest.ts` — strictly better than the mocked-`Platform.OS`
|
|
49
|
+
// unit test that went with them, which asserted a JS function rather than what Fabric receives.
|
|
50
|
+
// `resolveButtonTitle` IS GONE (2026-09-18) — the uppercase-on-Android rule is `foldButtonLabel` in
|
|
51
|
+
// `SymbioteFabricProps.cpp`, reached off the label's own tag. It had no caller left but its own two
|
|
52
|
+
// unit tests, which is the orphan shape this migration keeps turning up: a JS copy of a rule that
|
|
53
|
+
// runs elsewhere, kept alive by the test asserting it, green forever and proving nothing.
|
|
54
|
+
//
|
|
55
|
+
// A COVERAGE GAP WENT WITH IT, recorded rather than hidden. The C++ rule is `#ifdef ANDROID` — a raw
|
|
56
|
+
// text commits as `RCTRawText` on both platforms, so unlike `Switch`/`AndroidSwitch` there is no view
|
|
57
|
+
// NAME for a rule to branch on — and this host is not Android. The deleted Android test reached the
|
|
58
|
+
// branch by mocking `Platform.OS`; what it mocked was a JS function that no longer exists. Same
|
|
59
|
+
// class as `android_ripple` and `decelerationRate`'s constants, and closing it means an Android arm
|
|
60
|
+
// of the test host, not a mock.
|
|
61
|
+
//
|
|
62
|
+
// One behaviour difference shipped with the move and is deliberate: RN uppercases through
|
|
63
|
+
// JavaScript's full-Unicode `toUpperCase`, and the C++ rule is ASCII-only. See `foldButtonLabel`.
|
|
64
|
+
/**
|
|
65
|
+
* Whether the button is disabled, which `aria-disabled` may decide on its own.
|
|
66
|
+
*
|
|
67
|
+
* Button.js:337-338 — `props.disabled != null ? props.disabled : accessibilityState.disabled`. The
|
|
68
|
+
* engine already folds `aria-disabled` into the committed `accessibilityState`, but that fold is a
|
|
69
|
+
* PAYLOAD fold: it cannot suppress the press or grey the label, which is what this is for.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveButtonDisabled(disabled, ariaDisabled, accessibilityState) {
|
|
72
|
+
if (disabled !== undefined)
|
|
73
|
+
return disabled;
|
|
74
|
+
return ariaDisabled ?? accessibilityState?.disabled;
|
|
23
75
|
}
|
|
@@ -54,5 +54,18 @@ export type IImageViewProps = {
|
|
|
54
54
|
passthrough: Record<string, unknown>;
|
|
55
55
|
};
|
|
56
56
|
export declare const IMAGE_VIEW_PROP_NAMES: readonly ["source", "defaultSource", "loadingIndicatorSource", "style", "resizeMode", "tintColor", "src", "srcSet", "alt", "width", "height", "crossOrigin", "referrerPolicy"];
|
|
57
|
-
|
|
57
|
+
/**
|
|
58
|
+
* `mapImageProps` STOOD HERE and is now the engine's — `foldImageProps` in
|
|
59
|
+
* `SymbioteFabricProps.cpp`, with `core/engine/cpp/tests/js/image-payload.itest.ts` as its contract.
|
|
60
|
+
*
|
|
61
|
+
* WHAT IS LEFT IS A GATHER, NOT A FOLD, and the distinction is the whole change. This flattens a
|
|
62
|
+
* typed view object back into the bag an app authored — the `<image>` tag then receives those names
|
|
63
|
+
* verbatim and the engine resolves them at commit, once, for every adapter. Angular is the caller
|
|
64
|
+
* that kept this path alive (its `<Image>` component has typed `@Input()`s rather than a prop bag);
|
|
65
|
+
* it now hands the tag raw props like every other adapter does.
|
|
66
|
+
*
|
|
67
|
+
* So the aliases (`src`, `srcSet`, `alt`, `width`, `height`, `crossOrigin`, `referrerPolicy`) leave
|
|
68
|
+
* here UNRESOLVED on purpose. Resolving them would be the second implementation this file's own
|
|
69
|
+
* header spent four lines warning about.
|
|
70
|
+
*/
|
|
58
71
|
export declare function renderImage(view: IImageViewProps): IDescriptor;
|
|
@@ -1,117 +1,5 @@
|
|
|
1
|
-
import { dlog,
|
|
1
|
+
import { dlog, } from '@symbiote-native/engine';
|
|
2
2
|
import { el } from '../../descriptor.js';
|
|
3
|
-
// Resolve the source, then normalize to the array shape native expects. A single
|
|
4
|
-
// object/number becomes a one-element array; an already-array source passes through.
|
|
5
|
-
function normalizeSource(source) {
|
|
6
|
-
const resolved = resolveImageSource(source);
|
|
7
|
-
const sources = Array.isArray(resolved) ? resolved : [resolved];
|
|
8
|
-
dlog(`Image source resolved to ${JSON.stringify(sources)}`);
|
|
9
|
-
return sources;
|
|
10
|
-
}
|
|
11
|
-
// The HTTP headers the W3C aliases (crossOrigin / referrerPolicy) contribute to
|
|
12
|
-
// every folded source, mirroring ImageSourceUtils.js getImageSourcesFromImageProps:
|
|
13
|
-
// 'use-credentials' adds the credentials header; referrerPolicy adds Referrer-Policy.
|
|
14
|
-
function headersFromAliases(view) {
|
|
15
|
-
const headers = {};
|
|
16
|
-
if (view.crossOrigin === 'use-credentials') {
|
|
17
|
-
headers['Access-Control-Allow-Credentials'] = 'true';
|
|
18
|
-
}
|
|
19
|
-
if (view.referrerPolicy !== undefined) {
|
|
20
|
-
headers['Referrer-Policy'] = view.referrerPolicy;
|
|
21
|
-
}
|
|
22
|
-
return headers;
|
|
23
|
-
}
|
|
24
|
-
// Expand a `srcSet` descriptor list into scaled sources, falling back to `src` for
|
|
25
|
-
// the 1x slot when srcSet omits it. Direct port of getImageSourcesFromImageProps'
|
|
26
|
-
// srcSet branch (ImageSourceUtils.js:48). Invalid scale tokens are skipped, matching
|
|
27
|
-
// RN's parse-and-warn behavior.
|
|
28
|
-
function expandSrcSet(srcSet, view, headers) {
|
|
29
|
-
const sources = [];
|
|
30
|
-
let useSrcForDefaultScale = true;
|
|
31
|
-
for (const entry of srcSet.split(', ')) {
|
|
32
|
-
const [uri, xScale = '1x'] = entry.split(' ');
|
|
33
|
-
if (!xScale.endsWith('x')) {
|
|
34
|
-
dlog(`Image srcSet: unsupported scale token "${xScale}", skipping`);
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
const scale = parseInt(xScale.slice(0, -1), 10);
|
|
38
|
-
if (Number.isNaN(scale))
|
|
39
|
-
continue;
|
|
40
|
-
if (scale === 1)
|
|
41
|
-
useSrcForDefaultScale = false;
|
|
42
|
-
sources.push({
|
|
43
|
-
uri,
|
|
44
|
-
scale,
|
|
45
|
-
width: view.width,
|
|
46
|
-
height: view.height,
|
|
47
|
-
...{ headers },
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
if (useSrcForDefaultScale && view.src !== undefined) {
|
|
51
|
-
sources.push({
|
|
52
|
-
uri: view.src,
|
|
53
|
-
scale: 1,
|
|
54
|
-
width: view.width,
|
|
55
|
-
height: view.height,
|
|
56
|
-
...{ headers },
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
if (sources.length === 0)
|
|
60
|
-
dlog('Image srcSet: produced no valid sources');
|
|
61
|
-
return sources;
|
|
62
|
-
}
|
|
63
|
-
// Resolve the native `source` array from whichever of source / src / srcSet the
|
|
64
|
-
// caller provided. Mirrors ImageSourceUtils.js getImageSourcesFromImageProps:
|
|
65
|
-
// srcSet wins, then src, then a header-decorated source, then the plain source.
|
|
66
|
-
// Always returns the array shape native expects (the same contract normalizeSource
|
|
67
|
-
// guarantees), so the component never sends a bare object.
|
|
68
|
-
function resolveSourceArray(view) {
|
|
69
|
-
const headers = headersFromAliases(view);
|
|
70
|
-
if (view.srcSet !== undefined) {
|
|
71
|
-
return expandSrcSet(view.srcSet, view, headers);
|
|
72
|
-
}
|
|
73
|
-
if (view.src !== undefined) {
|
|
74
|
-
return [
|
|
75
|
-
{ uri: view.src, width: view.width, height: view.height, ...{ headers } },
|
|
76
|
-
];
|
|
77
|
-
}
|
|
78
|
-
if (view.source === undefined) {
|
|
79
|
-
dlog('Image: no source / src / srcSet provided');
|
|
80
|
-
return [];
|
|
81
|
-
}
|
|
82
|
-
const sources = normalizeSource(view.source);
|
|
83
|
-
// A header-decorated single object source gets the headers merged in, per RN's
|
|
84
|
-
// `source.uri && headers` branch; the array/number shapes pass through untouched.
|
|
85
|
-
if (Object.keys(headers).length > 0 && sources.length === 1) {
|
|
86
|
-
const [only] = sources;
|
|
87
|
-
if (typeof only === 'object' &&
|
|
88
|
-
only !== null &&
|
|
89
|
-
typeof Reflect.get(only, 'uri') === 'string') {
|
|
90
|
-
return [{ ...only, headers }];
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return sources;
|
|
94
|
-
}
|
|
95
|
-
function readStyleString(style, key) {
|
|
96
|
-
if (style === undefined)
|
|
97
|
-
return undefined;
|
|
98
|
-
// style is a StyleProp (possibly a nested array), so flatten before reading a key.
|
|
99
|
-
const flat = flattenStyle(style);
|
|
100
|
-
const value = Object.hasOwn(flat, key) ? flat[key] : undefined;
|
|
101
|
-
return typeof value === 'string' ? value : undefined;
|
|
102
|
-
}
|
|
103
|
-
// Resolve an asset source and read its single uri. RN forwards the Android
|
|
104
|
-
// loading indicator as a bare uri string (`loadingIndicatorSrc`), not the
|
|
105
|
-
// array shape the main source uses, so we resolve and pluck the uri.
|
|
106
|
-
function readSourceUri(source) {
|
|
107
|
-
const [resolved] = normalizeSource(source);
|
|
108
|
-
if (typeof resolved === 'object' && resolved !== null) {
|
|
109
|
-
const uri = Reflect.get(resolved, 'uri');
|
|
110
|
-
if (typeof uri === 'string')
|
|
111
|
-
return uri;
|
|
112
|
-
}
|
|
113
|
-
return undefined;
|
|
114
|
-
}
|
|
115
3
|
// The names `renderImage` CONSUMES rather than forwards — the fold's input list, and the reason it
|
|
116
4
|
// is exported: an adapter that splits props before calling renderImage was copying this list by
|
|
117
5
|
// hand, and a name added here reached the copy only if someone remembered. The Image behavior's
|
|
@@ -131,40 +19,27 @@ export const IMAGE_VIEW_PROP_NAMES = [
|
|
|
131
19
|
'crossOrigin',
|
|
132
20
|
'referrerPolicy',
|
|
133
21
|
];
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
source: resolveSourceArray(view),
|
|
149
|
-
resizeMode: view.resizeMode ?? readStyleString(view.style, 'resizeMode'),
|
|
150
|
-
tintColor: view.tintColor ?? readStyleString(view.style, 'tintColor'),
|
|
151
|
-
};
|
|
152
|
-
// `alt` is the accessibility text: it sets accessibilityLabel and marks the image
|
|
153
|
-
// accessible (Image.ios.js / Image.android.js: alt -> accessibilityLabel + accessible).
|
|
154
|
-
// An explicit accessibilityLabel (already folded into passthrough) still wins.
|
|
155
|
-
if (view.alt !== undefined) {
|
|
156
|
-
if (mapped.accessibilityLabel === undefined)
|
|
157
|
-
mapped.accessibilityLabel = view.alt;
|
|
158
|
-
mapped.accessible = true;
|
|
159
|
-
}
|
|
160
|
-
if (view.defaultSource !== undefined)
|
|
161
|
-
mapped.defaultSource = normalizeSource(view.defaultSource);
|
|
162
|
-
if (view.loadingIndicatorSource !== undefined) {
|
|
163
|
-
mapped.loadingIndicatorSrc = readSourceUri(view.loadingIndicatorSource);
|
|
164
|
-
}
|
|
165
|
-
return mapped;
|
|
166
|
-
}
|
|
22
|
+
/**
|
|
23
|
+
* `mapImageProps` STOOD HERE and is now the engine's — `foldImageProps` in
|
|
24
|
+
* `SymbioteFabricProps.cpp`, with `core/engine/cpp/tests/js/image-payload.itest.ts` as its contract.
|
|
25
|
+
*
|
|
26
|
+
* WHAT IS LEFT IS A GATHER, NOT A FOLD, and the distinction is the whole change. This flattens a
|
|
27
|
+
* typed view object back into the bag an app authored — the `<image>` tag then receives those names
|
|
28
|
+
* verbatim and the engine resolves them at commit, once, for every adapter. Angular is the caller
|
|
29
|
+
* that kept this path alive (its `<Image>` component has typed `@Input()`s rather than a prop bag);
|
|
30
|
+
* it now hands the tag raw props like every other adapter does.
|
|
31
|
+
*
|
|
32
|
+
* So the aliases (`src`, `srcSet`, `alt`, `width`, `height`, `crossOrigin`, `referrerPolicy`) leave
|
|
33
|
+
* here UNRESOLVED on purpose. Resolving them would be the second implementation this file's own
|
|
34
|
+
* header spent four lines warning about.
|
|
35
|
+
*/
|
|
167
36
|
export function renderImage(view) {
|
|
168
37
|
dlog('Image -> RCTImageView');
|
|
169
|
-
|
|
38
|
+
const props = { ...view.passthrough };
|
|
39
|
+
for (const name of IMAGE_VIEW_PROP_NAMES) {
|
|
40
|
+
const value = view[name];
|
|
41
|
+
if (value !== undefined)
|
|
42
|
+
props[name] = value;
|
|
43
|
+
}
|
|
44
|
+
return el('image', props);
|
|
170
45
|
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { type IDescriptor } from '../descriptor';
|
|
1
|
+
import type { IStyleProp, IViewStyle } from '@symbiote-native/engine';
|
|
3
2
|
export type IInputAccessoryViewViewProps = {
|
|
4
3
|
nativeID?: string;
|
|
5
4
|
backgroundColor?: string;
|
|
6
5
|
style?: IStyleProp<IViewStyle>;
|
|
7
6
|
passthrough: Record<string, unknown>;
|
|
8
7
|
};
|
|
9
|
-
export declare const INPUT_ACCESSORY_VIEW_PROP_NAMES: readonly ["nativeID", "backgroundColor", "style"];
|
|
10
|
-
export declare function mapInputAccessoryViewProps(view: IInputAccessoryViewViewProps): Record<string, unknown>;
|
|
11
|
-
export declare function renderInputAccessoryView(view: IInputAccessoryViewViewProps): IDescriptor;
|