@symbiote-native/angular 2.0.1 → 3.0.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/README.md +5 -4
- package/babel-register-composed.cjs +0 -8
- package/build/angular/callback-host.d.ts +25 -0
- package/build/angular/callback-host.js +211 -0
- package/build/angular/callback-host.js.map +1 -0
- package/build/angular/change-detection-flush.d.ts +14 -2
- package/build/angular/change-detection-flush.js +54 -6
- package/build/angular/change-detection-flush.js.map +1 -1
- package/build/angular/components/flat-list/index.js +39 -86
- package/build/angular/components/flat-list/index.js.map +1 -1
- package/build/angular/components/image-shared.js +49 -110
- package/build/angular/components/image-shared.js.map +1 -1
- package/build/angular/components/keyboard-avoiding-view/index.js +33 -72
- package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
- package/build/angular/components/modal/index.js +58 -103
- package/build/angular/components/modal/index.js.map +1 -1
- package/build/angular/components/pressable-props.d.ts +2 -0
- package/build/angular/components/section-list/index.js +20 -103
- package/build/angular/components/section-list/index.js.map +1 -1
- package/build/angular/components/touchable-props.d.ts +1 -0
- package/build/angular/components/virtualized-list/directives.js +32 -22
- package/build/angular/components/virtualized-list/directives.js.map +1 -1
- package/build/angular/components/virtualized-list/index.d.ts +7 -1
- package/build/angular/components/virtualized-list/index.js +350 -359
- package/build/angular/components/virtualized-list/index.js.map +1 -1
- package/build/angular/components/virtualized-section-list/directives.js +20 -12
- package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
- package/build/angular/components/virtualized-section-list/index.js +36 -142
- package/build/angular/components/virtualized-section-list/index.js.map +1 -1
- package/build/angular/components.d.ts +1 -1
- package/build/angular/components.js +1 -1
- package/build/angular/components.js.map +1 -1
- package/build/angular/create-portal/index.js +9 -8
- package/build/angular/create-portal/index.js.map +1 -1
- package/build/angular/create-tunnel/index.js +13 -12
- package/build/angular/create-tunnel/index.js.map +1 -1
- package/build/angular/descriptor-to-angular/index.js +6 -6
- package/build/angular/descriptor-to-angular/index.js.map +1 -1
- package/build/angular/diagnostics.d.ts +13 -0
- package/build/angular/diagnostics.js +18 -0
- package/build/angular/diagnostics.js.map +1 -1
- package/build/angular/element-props.d.ts +1 -3
- package/build/angular/elements.d.ts +46 -32
- package/build/angular/elements.js +217 -434
- package/build/angular/elements.js.map +1 -1
- package/build/angular/index.d.ts +3 -1
- package/build/angular/index.js +44 -13
- package/build/angular/index.js.map +1 -1
- package/build/angular/modules/animated/animated-leaf-binder.js +0 -2
- package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -1
- package/build/angular/modules/animated/create-animated-component.js +69 -74
- package/build/angular/modules/animated/create-animated-component.js.map +1 -1
- package/build/angular/modules/animated/index.d.ts +30 -19
- package/build/angular/modules/status-bar/index.js +2 -8
- package/build/angular/modules/status-bar/index.js.map +1 -1
- package/build/angular/primitives/index.d.ts +14 -28
- package/build/angular/primitives/index.js +44 -107
- package/build/angular/primitives/index.js.map +1 -1
- package/build/angular/primitives/shared.d.ts +1 -0
- package/build/angular/primitives/shared.js +47 -32
- package/build/angular/primitives/shared.js.map +1 -1
- package/build/angular/render/index.js +40 -3
- package/build/angular/render/index.js.map +1 -1
- package/build/angular/renderer/index.d.ts +80 -0
- package/build/angular/renderer/index.js +324 -114
- package/build/angular/renderer/index.js.map +1 -1
- package/build/angular/runtime-matching.d.ts +9 -0
- package/build/angular/runtime-matching.js +82 -0
- package/build/angular/runtime-matching.js.map +1 -0
- package/build/angular/services/color-scheme.service.js +4 -4
- package/build/angular/services/color-scheme.service.js.map +1 -1
- package/build/angular/services/window-dimensions.service.js +4 -4
- package/build/angular/services/window-dimensions.service.js.map +1 -1
- package/build/angular/style-host.d.ts +11 -0
- package/build/angular/style-host.js +86 -0
- package/build/angular/style-host.js.map +1 -0
- package/package.json +6 -6
- package/src/callback-host.ts +229 -0
- package/src/change-detection-flush.ts +62 -9
- package/src/components/flat-list/index.ts +2 -1
- package/src/components/keyboard-avoiding-view/index.ts +8 -0
- package/src/components/pressable-props.ts +8 -0
- package/src/components/touchable-props.ts +4 -0
- package/src/components/virtualized-list/index.ts +98 -45
- package/src/components.ts +0 -2
- package/src/diagnostics.ts +18 -0
- package/src/element-props.ts +1 -5
- package/src/elements.ts +159 -38
- package/src/index.ts +47 -14
- package/src/primitives/index.ts +14 -70
- package/src/primitives/shared.ts +21 -6
- package/src/render/index.ts +56 -2
- package/src/renderer/index.ts +378 -104
- package/src/runtime-matching.ts +80 -0
- package/src/style-host.ts +94 -0
- package/tsconfig.angular.base.json +11 -0
|
@@ -58,7 +58,23 @@ interface IViewMarker {
|
|
|
58
58
|
markForCheck(): void;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
type ICallbackWrapper = (key: string, value: unknown) => unknown;
|
|
61
|
+
export type ICallbackWrapper = (key: string, value: unknown) => unknown;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Is this prop write one the wrapper would touch at all?
|
|
65
|
+
*
|
|
66
|
+
* Exported so a CALLER can ask before building a wrapper it may never need — an element directive is
|
|
67
|
+
* constructed per TAG, and a screen's tags overwhelmingly carry no `on*` function prop. Keeping the
|
|
68
|
+
* question here rather than copying the two tests at the call site is what stops the predicate and
|
|
69
|
+
* the wrapper drifting apart.
|
|
70
|
+
*/
|
|
71
|
+
export function isWrappableCallback(key: string, value: unknown): boolean {
|
|
72
|
+
return (
|
|
73
|
+
ON_PREFIX.test(key) &&
|
|
74
|
+
typeof value === 'function' &&
|
|
75
|
+
key !== PER_FRAME_CALLBACK
|
|
76
|
+
);
|
|
77
|
+
}
|
|
62
78
|
|
|
63
79
|
/**
|
|
64
80
|
* Makes an app callback handed to the engine as a PROP re-enter Angular's update loop.
|
|
@@ -82,14 +98,18 @@ type ICallbackWrapper = (key: string, value: unknown) => unknown;
|
|
|
82
98
|
* ONE wrapper per original handler, for the caller's lifetime. A fresh closure per push would make
|
|
83
99
|
* the engine store a new listener every time and would defeat every downstream identity check.
|
|
84
100
|
*/
|
|
85
|
-
export function createCallbackWrapper(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
101
|
+
export function createCallbackWrapper(node: unknown): ICallbackWrapper {
|
|
102
|
+
// Allocated on the FIRST wrap, not at construction. The map is per-CALLER, so an eager one is one
|
|
103
|
+
// `WeakMap` per directive instance — 7 000 on a 1 000-row create of the benchmark row, none of
|
|
104
|
+
// which is ever read, because that row carries no `on*` prop.
|
|
105
|
+
let wrappers: WeakMap<object, (...args: unknown[]) => unknown> | undefined;
|
|
90
106
|
return (key: string, value: unknown): unknown => {
|
|
91
|
-
|
|
92
|
-
|
|
107
|
+
// The second test is redundant at RUNTIME and load-bearing for the TYPE: `isWrappableCallback`
|
|
108
|
+
// answers a boolean, which narrows nothing, and the alternative is a cast. Keeping the `typeof`
|
|
109
|
+
// here is what lets `value` be a `Function` below without one.
|
|
110
|
+
if (!isWrappableCallback(key, value) || typeof value !== 'function')
|
|
111
|
+
return value;
|
|
112
|
+
wrappers ??= new WeakMap();
|
|
93
113
|
const cached = wrappers.get(value);
|
|
94
114
|
if (cached !== undefined) return cached;
|
|
95
115
|
// Reflect.apply rather than a cast-to-signature local: `typeof value === 'function'` narrows to
|
|
@@ -97,7 +117,7 @@ export function createCallbackWrapper(
|
|
|
97
117
|
// `undefined` as the receiver preserves the previous unbound call.
|
|
98
118
|
const wrapper = (...args: unknown[]): unknown => {
|
|
99
119
|
const result: unknown = Reflect.apply(value, undefined, args);
|
|
100
|
-
|
|
120
|
+
markViewFor(node);
|
|
101
121
|
flushViewFor(node);
|
|
102
122
|
return result;
|
|
103
123
|
};
|
|
@@ -106,6 +126,39 @@ export function createCallbackWrapper(
|
|
|
106
126
|
};
|
|
107
127
|
}
|
|
108
128
|
|
|
129
|
+
/**
|
|
130
|
+
* The MARKER half, keyed on the node rather than injected.
|
|
131
|
+
*
|
|
132
|
+
* `markForCheck` needs the view holding the binding, and until 2026-09-18 the only way to have one
|
|
133
|
+
* was for `SymbioteElement` to inject a `ChangeDetectorRef` — on EVERY element, so that its lazy
|
|
134
|
+
* `on*` wrapper could reach one on the few that carry a callback. An injection is ~1-2.4 us per
|
|
135
|
+
* element on JavaScriptCore (`core/engine/cpp/tests/js/angular-directive-cost.itest.ts`), so a
|
|
136
|
+
* thousand-row screen paid for ten thousand `ViewRef`s to serve none.
|
|
137
|
+
*
|
|
138
|
+
* `SymbioteCallbackHost` registers here instead, and it MATCHES on the callback attributes rather
|
|
139
|
+
* than being injected — so an element with no `on*` binding neither instantiates it nor pays for it.
|
|
140
|
+
* Measured as the whole trade, selector included: -3.9 / -9.3 / -25.5 ms over ten thousand elements.
|
|
141
|
+
*
|
|
142
|
+
* Separate from `viewFlushes` deliberately. They come from different directives, on overlapping but
|
|
143
|
+
* different sets of tags, and they are not the same instruction: this one SCHEDULES and reaches every
|
|
144
|
+
* ancestor, that one re-checks ONE view synchronously. Collapsing them into a single entry would make
|
|
145
|
+
* whichever directive registered last decide which of the two an element gets.
|
|
146
|
+
*/
|
|
147
|
+
const viewMarkers = new WeakMap<object, IViewMarker>();
|
|
148
|
+
|
|
149
|
+
export function registerViewMarker(node: object, marker: IViewMarker): void {
|
|
150
|
+
viewMarkers.set(node, marker);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function unregisterViewMarker(node: object): void {
|
|
154
|
+
viewMarkers.delete(node);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function markViewFor(node: unknown): void {
|
|
158
|
+
if (typeof node !== 'object' || node === null) return;
|
|
159
|
+
viewMarkers.get(node)?.markForCheck();
|
|
160
|
+
}
|
|
161
|
+
|
|
109
162
|
export function registerViewFlush(node: object, flush: () => void): void {
|
|
110
163
|
viewFlushes.set(node, flush);
|
|
111
164
|
}
|
|
@@ -516,7 +516,8 @@ export class FlatList<ItemT = unknown>
|
|
|
516
516
|
// ---- multi-column data adaptation (the virtualized stream is rows) ----
|
|
517
517
|
getRow = (_data: unknown, index: number): IRow<ItemT> => this.rows[index];
|
|
518
518
|
getRowCount = (_data: unknown): number => this.rows.length;
|
|
519
|
-
rowKey = (row: IRow<ItemT>, _index: number): string =>
|
|
519
|
+
rowKey = (row: IRow<ItemT>, _index: number): string =>
|
|
520
|
+
rowKeyExtractor(row, this.keyExtractor);
|
|
520
521
|
|
|
521
522
|
// Viewability over rows expands back to per-item tokens, so the caller sees item-level
|
|
522
523
|
// visibility, not row-level (shared expandRowViewability), matching Vue.
|
|
@@ -26,7 +26,9 @@ import {
|
|
|
26
26
|
} from '@angular/core';
|
|
27
27
|
import {
|
|
28
28
|
computeInset,
|
|
29
|
+
configureKeyboardAvoidingAnimation,
|
|
29
30
|
keyboardAvoidingEventNamesFor,
|
|
31
|
+
readKeyboardAnimationTiming,
|
|
30
32
|
readKeyboardFrame,
|
|
31
33
|
readLayoutFrame,
|
|
32
34
|
readPrefersCrossFadeTransitions,
|
|
@@ -227,6 +229,12 @@ export class KeyboardAvoidingView
|
|
|
227
229
|
previousInset: this.inset,
|
|
228
230
|
prefersCrossFadeTransitions: this.prefersCrossFadeTransitions,
|
|
229
231
|
});
|
|
232
|
+
// RN's `_updateBottomIfNecessary` skips the animation when the inset did not change.
|
|
233
|
+
if (next !== this.inset)
|
|
234
|
+
configureKeyboardAvoidingAnimation(
|
|
235
|
+
readKeyboardAnimationTiming(payload),
|
|
236
|
+
this.enabled !== false,
|
|
237
|
+
);
|
|
230
238
|
dlog(`KeyboardAvoidingView show -> inset ${next}`);
|
|
231
239
|
this.inset = next;
|
|
232
240
|
this.changeDetector.markForCheck();
|
|
@@ -33,7 +33,15 @@ export interface IAngularPressableProps
|
|
|
33
33
|
delayLongPress?: number;
|
|
34
34
|
disabled?: boolean;
|
|
35
35
|
cancelable?: boolean;
|
|
36
|
+
// Tells native to stand down once this Pressable claims the gesture (RN Pressable.js:123,
|
|
37
|
+
// Pressability.js onResponderGrant), so a parent ScrollView cannot steal it mid-drag. Default
|
|
38
|
+
// false.
|
|
39
|
+
blockNativeResponder?: boolean;
|
|
36
40
|
hitSlop?: IRectOffset;
|
|
41
|
+
// RN's snapshot affordance (`Pressable.js:151`, `TouchableHighlight.js:61`): render the control in
|
|
42
|
+
// its pressed state with no gesture, so a test can capture it. Consumed by the engine and stripped
|
|
43
|
+
// before the payload — no ViewConfig declares it.
|
|
44
|
+
testOnly_pressed?: boolean;
|
|
37
45
|
pressRetentionOffset?: IRectOffset;
|
|
38
46
|
unstable_pressDelay?: number;
|
|
39
47
|
android_ripple?: IPressableAndroidRippleConfig;
|
|
@@ -27,4 +27,8 @@ export type IAngularTouchableOpacityProps = IAngularTouchableBaseProps & {
|
|
|
27
27
|
export type IAngularTouchableHighlightProps = IAngularTouchableBaseProps & {
|
|
28
28
|
activeOpacity?: number;
|
|
29
29
|
underlayColor?: string;
|
|
30
|
+
// RN's snapshot affordance (`Pressable.js:151`, `TouchableHighlight.js:61`): render the control in
|
|
31
|
+
// its pressed state with no gesture, so a test can capture it. Consumed by the engine and stripped
|
|
32
|
+
// before the payload — no ViewConfig declares it.
|
|
33
|
+
testOnly_pressed?: boolean;
|
|
30
34
|
};
|
|
@@ -44,10 +44,8 @@ import {
|
|
|
44
44
|
type OnDestroy,
|
|
45
45
|
} from '@angular/core';
|
|
46
46
|
import {
|
|
47
|
-
DEFAULT_END_REACHED_THRESHOLD,
|
|
48
47
|
DEFAULT_INITIAL_NUM_TO_RENDER,
|
|
49
48
|
DEFAULT_MAX_TO_RENDER_PER_BATCH,
|
|
50
|
-
DEFAULT_START_REACHED_THRESHOLD,
|
|
51
49
|
DEFAULT_UPDATE_CELLS_BATCHING_PERIOD,
|
|
52
50
|
DEFAULT_WINDOW_SIZE,
|
|
53
51
|
EMPTY_OFFSET,
|
|
@@ -214,6 +212,13 @@ interface IWindowCell<ItemT> {
|
|
|
214
212
|
context: IVListItemContext<ItemT>;
|
|
215
213
|
measure: (event: ISymbioteEvent) => void;
|
|
216
214
|
separatorContext?: IVListSeparatorContext<ItemT>;
|
|
215
|
+
/**
|
|
216
|
+
* Picks the cell's TAG in the template — `sticky-header` pins, `view` does not.
|
|
217
|
+
*
|
|
218
|
+
* It is stable per cell key (an index is a section header or it is not), so the `@if` never
|
|
219
|
+
* swaps an element type under a live cell and nothing remounts as the window slides.
|
|
220
|
+
*/
|
|
221
|
+
isSticky: boolean;
|
|
217
222
|
}
|
|
218
223
|
|
|
219
224
|
// The scroll body is identical on both axes; only the outer tag differs (the axis comes from the
|
|
@@ -257,7 +262,7 @@ interface IWindowCell<ItemT> {
|
|
|
257
262
|
<view [style]="leadingSpacerStyle"></view>
|
|
258
263
|
}
|
|
259
264
|
@if (forcedStickyCell !== null) {
|
|
260
|
-
<
|
|
265
|
+
<sticky-header
|
|
261
266
|
(layout)="handleCellLayout(forcedStickyCell.measure, $event)"
|
|
262
267
|
[style]="cellStyle"
|
|
263
268
|
>
|
|
@@ -265,27 +270,45 @@ interface IWindowCell<ItemT> {
|
|
|
265
270
|
[vListOutlet]="itemDir?.templateRef"
|
|
266
271
|
[vListOutletContext]="forcedStickyCell.context"
|
|
267
272
|
></ng-container>
|
|
268
|
-
</
|
|
273
|
+
</sticky-header>
|
|
269
274
|
}
|
|
270
275
|
@if (gapSpacerStyle !== null) {
|
|
271
276
|
<view [style]="gapSpacerStyle"></view>
|
|
272
277
|
}
|
|
273
278
|
@for (cell of windowCells; track cell.key) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
[vListOutlet]="itemDir?.templateRef"
|
|
280
|
-
[vListOutletContext]="cell.context"
|
|
281
|
-
></ng-container>
|
|
282
|
-
@if (cell.separatorContext !== undefined) {
|
|
279
|
+
@if (cell.isSticky) {
|
|
280
|
+
<sticky-header
|
|
281
|
+
(layout)="handleCellLayout(cell.measure, $event)"
|
|
282
|
+
[style]="cellStyle"
|
|
283
|
+
>
|
|
283
284
|
<ng-container
|
|
284
|
-
[vListOutlet]="
|
|
285
|
-
[vListOutletContext]="cell.
|
|
285
|
+
[vListOutlet]="itemDir?.templateRef"
|
|
286
|
+
[vListOutletContext]="cell.context"
|
|
286
287
|
></ng-container>
|
|
287
|
-
|
|
288
|
-
|
|
288
|
+
@if (cell.separatorContext !== undefined) {
|
|
289
|
+
<ng-container
|
|
290
|
+
[vListOutlet]="separatorDir?.templateRef"
|
|
291
|
+
[vListOutletContext]="cell.separatorContext"
|
|
292
|
+
></ng-container>
|
|
293
|
+
}
|
|
294
|
+
</sticky-header>
|
|
295
|
+
} @else {
|
|
296
|
+
<view
|
|
297
|
+
(layout)="handleCellLayout(cell.measure, $event)"
|
|
298
|
+
[style]="cellStyle"
|
|
299
|
+
>
|
|
300
|
+
<ng-container
|
|
301
|
+
[vListOutlet]="itemDir?.templateRef"
|
|
302
|
+
[vListOutletContext]="cell.context"
|
|
303
|
+
></ng-container>
|
|
304
|
+
@if (cell.separatorContext !== undefined) {
|
|
305
|
+
<ng-container
|
|
306
|
+
[vListOutlet]="separatorDir?.templateRef"
|
|
307
|
+
[vListOutletContext]="cell.separatorContext"
|
|
308
|
+
></ng-container>
|
|
309
|
+
}
|
|
310
|
+
</view>
|
|
311
|
+
}
|
|
289
312
|
}
|
|
290
313
|
@if (trailingSpacerStyle !== null) {
|
|
291
314
|
<view [style]="trailingSpacerStyle"></view>
|
|
@@ -323,7 +346,7 @@ interface IWindowCell<ItemT> {
|
|
|
323
346
|
<view [style]="leadingSpacerStyle"></view>
|
|
324
347
|
}
|
|
325
348
|
@if (forcedStickyCell !== null) {
|
|
326
|
-
<
|
|
349
|
+
<sticky-header
|
|
327
350
|
(layout)="handleCellLayout(forcedStickyCell.measure, $event)"
|
|
328
351
|
[style]="cellStyle"
|
|
329
352
|
>
|
|
@@ -331,7 +354,7 @@ interface IWindowCell<ItemT> {
|
|
|
331
354
|
[vListOutlet]="itemDir?.templateRef"
|
|
332
355
|
[vListOutletContext]="forcedStickyCell.context"
|
|
333
356
|
></ng-container>
|
|
334
|
-
</
|
|
357
|
+
</sticky-header>
|
|
335
358
|
}
|
|
336
359
|
@if (gapSpacerStyle !== null) {
|
|
337
360
|
<view [style]="gapSpacerStyle"></view>
|
|
@@ -344,21 +367,39 @@ interface IWindowCell<ItemT> {
|
|
|
344
367
|
first index moves. Measured at exactly 17px on device 2026-08-19; see
|
|
345
368
|
.claude/rules/list-geometry-feedback-loop.md. -->
|
|
346
369
|
@for (cell of windowCells; track cell.key) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
[vListOutlet]="itemDir?.templateRef"
|
|
353
|
-
[vListOutletContext]="cell.context"
|
|
354
|
-
></ng-container>
|
|
355
|
-
@if (cell.separatorContext !== undefined) {
|
|
370
|
+
@if (cell.isSticky) {
|
|
371
|
+
<sticky-header
|
|
372
|
+
(layout)="handleCellLayout(cell.measure, $event)"
|
|
373
|
+
[style]="cellStyle"
|
|
374
|
+
>
|
|
356
375
|
<ng-container
|
|
357
|
-
[vListOutlet]="
|
|
358
|
-
[vListOutletContext]="cell.
|
|
376
|
+
[vListOutlet]="itemDir?.templateRef"
|
|
377
|
+
[vListOutletContext]="cell.context"
|
|
359
378
|
></ng-container>
|
|
360
|
-
|
|
361
|
-
|
|
379
|
+
@if (cell.separatorContext !== undefined) {
|
|
380
|
+
<ng-container
|
|
381
|
+
[vListOutlet]="separatorDir?.templateRef"
|
|
382
|
+
[vListOutletContext]="cell.separatorContext"
|
|
383
|
+
></ng-container>
|
|
384
|
+
}
|
|
385
|
+
</sticky-header>
|
|
386
|
+
} @else {
|
|
387
|
+
<view
|
|
388
|
+
(layout)="handleCellLayout(cell.measure, $event)"
|
|
389
|
+
[style]="cellStyle"
|
|
390
|
+
>
|
|
391
|
+
<ng-container
|
|
392
|
+
[vListOutlet]="itemDir?.templateRef"
|
|
393
|
+
[vListOutletContext]="cell.context"
|
|
394
|
+
></ng-container>
|
|
395
|
+
@if (cell.separatorContext !== undefined) {
|
|
396
|
+
<ng-container
|
|
397
|
+
[vListOutlet]="separatorDir?.templateRef"
|
|
398
|
+
[vListOutletContext]="cell.separatorContext"
|
|
399
|
+
></ng-container>
|
|
400
|
+
}
|
|
401
|
+
</view>
|
|
402
|
+
}
|
|
362
403
|
}
|
|
363
404
|
@if (trailingSpacerStyle !== null) {
|
|
364
405
|
<view [style]="trailingSpacerStyle"></view>
|
|
@@ -540,7 +581,6 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
540
581
|
gapSpacerStyle: IViewStyle | null = null;
|
|
541
582
|
trailingSpacerStyle: IViewStyle | null = null;
|
|
542
583
|
cellStyle: IViewStyle | undefined = undefined;
|
|
543
|
-
renderedStickyIndices: number[] | undefined = undefined;
|
|
544
584
|
// Bound to the template's `[style]="resolvedStyle"`, which Angular compiles to the built-in
|
|
545
585
|
// ɵɵstyleMap instruction — it only understands a flat object, never an array (RN's own
|
|
546
586
|
// `style={[a, b]}` composition idiom crashes deep inside Angular's styling engine), so this
|
|
@@ -700,7 +740,10 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
700
740
|
keyboardShouldPersistTaps: this.keyboardShouldPersistTaps,
|
|
701
741
|
keyboardDismissMode: this.keyboardDismissMode,
|
|
702
742
|
contentOffset: this.commandedOffset,
|
|
703
|
-
stickyHeaderIndices
|
|
743
|
+
// `stickyHeaderIndices` is deliberately NOT forwarded — it numbers the scroll view's PAINT
|
|
744
|
+
// children, and a windowed list paints a header, a spacer and a slice, so the positions move
|
|
745
|
+
// every time the window slides and the behavior re-wraps a different child each pass. The
|
|
746
|
+
// cells carry the `sticky-header` TAG instead, which pins by DOCUMENT order.
|
|
704
747
|
maintainVisibleContentPosition:
|
|
705
748
|
this.resolvedMaintainVisibleContentPosition,
|
|
706
749
|
// The four boolean-gated Fabric events (`.claude/rules/fabric-boolean-event-gates.md`): the
|
|
@@ -750,11 +793,11 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
750
793
|
this.updateCellsBatchingPeriod ?? DEFAULT_UPDATE_CELLS_BATCHING_PERIOD
|
|
751
794
|
);
|
|
752
795
|
}
|
|
753
|
-
private get onEndReachedThresholdValue(): number {
|
|
754
|
-
return this.onEndReachedThreshold
|
|
796
|
+
private get onEndReachedThresholdValue(): number | undefined {
|
|
797
|
+
return this.onEndReachedThreshold;
|
|
755
798
|
}
|
|
756
|
-
private get onStartReachedThresholdValue(): number {
|
|
757
|
-
return this.onStartReachedThreshold
|
|
799
|
+
private get onStartReachedThresholdValue(): number | undefined {
|
|
800
|
+
return this.onStartReachedThreshold;
|
|
758
801
|
}
|
|
759
802
|
|
|
760
803
|
// onScroll stays a plain callback bag key (an Animated.event(...) target must be able to flow
|
|
@@ -933,7 +976,12 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
933
976
|
const info = effect.info;
|
|
934
977
|
const map = effect.map;
|
|
935
978
|
const fire = (): void => {
|
|
936
|
-
for (const pair of pairs)
|
|
979
|
+
for (const pair of pairs) {
|
|
980
|
+
pair.onViewableItemsChanged({
|
|
981
|
+
...info,
|
|
982
|
+
viewabilityConfig: pair.viewabilityConfig,
|
|
983
|
+
});
|
|
984
|
+
}
|
|
937
985
|
this.dispatch({ kind: 'viewable-fired', map });
|
|
938
986
|
};
|
|
939
987
|
if (this.viewableTimer !== null) {
|
|
@@ -1084,7 +1132,6 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
1084
1132
|
this.leadingSpacerStyle = null;
|
|
1085
1133
|
this.gapSpacerStyle = null;
|
|
1086
1134
|
this.trailingSpacerStyle = null;
|
|
1087
|
-
this.renderedStickyIndices = undefined;
|
|
1088
1135
|
dlog(
|
|
1089
1136
|
`Angular VirtualizedList empty (viewport=${this.listState.viewportLength})`,
|
|
1090
1137
|
);
|
|
@@ -1112,10 +1159,7 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
1112
1159
|
plan.trailingExtent > EMPTY_OFFSET
|
|
1113
1160
|
? this.spacerStyle(plan.trailingExtent)
|
|
1114
1161
|
: null;
|
|
1115
|
-
|
|
1116
|
-
stickySet !== undefined && plan.stickyChildPositions.length > 0
|
|
1117
|
-
? plan.stickyChildPositions
|
|
1118
|
-
: undefined;
|
|
1162
|
+
// `plan.stickyChildPositions` is deliberately NOT read — it was the input to the index form.
|
|
1119
1163
|
dlog(
|
|
1120
1164
|
`STICKY[list] stickySet=${stickySet === undefined ? 'undefined' : JSON.stringify([...stickySet])} ` +
|
|
1121
1165
|
`childPositions=${JSON.stringify(plan.stickyChildPositions)} ` +
|
|
@@ -1129,6 +1173,8 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
1129
1173
|
plan.forcedStickyCell.index,
|
|
1130
1174
|
plan.forcedStickyCell.key,
|
|
1131
1175
|
false,
|
|
1176
|
+
// Sticky by construction — `forcedStickyCell` exists only for an index in `stickySet`.
|
|
1177
|
+
true,
|
|
1132
1178
|
)
|
|
1133
1179
|
: null;
|
|
1134
1180
|
|
|
@@ -1141,7 +1187,12 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
1141
1187
|
// 2026-08-19 as a run of cells all shifting by exactly the divider's 1px.
|
|
1142
1188
|
const includeSeparator = hasSeparators && planned.index < m.count - 1;
|
|
1143
1189
|
cells.push(
|
|
1144
|
-
this.buildWindowCell(
|
|
1190
|
+
this.buildWindowCell(
|
|
1191
|
+
planned.index,
|
|
1192
|
+
planned.key,
|
|
1193
|
+
includeSeparator,
|
|
1194
|
+
stickySet?.has(planned.index) === true,
|
|
1195
|
+
),
|
|
1145
1196
|
);
|
|
1146
1197
|
}
|
|
1147
1198
|
this.windowCells = cells;
|
|
@@ -1162,6 +1213,7 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
1162
1213
|
index: number,
|
|
1163
1214
|
key: string,
|
|
1164
1215
|
includeSeparator: boolean,
|
|
1216
|
+
isSticky: boolean,
|
|
1165
1217
|
): IWindowCell<ItemT> {
|
|
1166
1218
|
const item = this.getItem(this.data, index);
|
|
1167
1219
|
return {
|
|
@@ -1172,6 +1224,7 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
1172
1224
|
separatorContext: includeSeparator
|
|
1173
1225
|
? this.buildSeparatorContext(index, item)
|
|
1174
1226
|
: undefined,
|
|
1227
|
+
isSticky,
|
|
1175
1228
|
};
|
|
1176
1229
|
}
|
|
1177
1230
|
|
package/src/components.ts
CHANGED
package/src/diagnostics.ts
CHANGED
|
@@ -116,6 +116,24 @@ export function noteAngularWrite(propName: string): void {
|
|
|
116
116
|
writesByProp.set(propName, (writesByProp.get(propName) ?? 0) + 1);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* A style KEY write, named `style.<key>` in the readout — and the prefix is built HERE.
|
|
121
|
+
*
|
|
122
|
+
* `noteAngularWrite` gates itself, but an ARGUMENT is evaluated before the callee can refuse it, so
|
|
123
|
+
* `noteAngularWrite(`style.${key}`)` builds a string on every `setStyle` in a Release build with the
|
|
124
|
+
* detail map off. `setStyle` is Angular's only styling channel — one call per key per node — so the
|
|
125
|
+
* bench row's create allocated ~7 000 of them for nobody.
|
|
126
|
+
*
|
|
127
|
+
* It is the same trap `CLAUDE.md` records for `dlog` arguments on this very renderer, in a different
|
|
128
|
+
* function; `tests/dlog-argument-budget.test.ts` scans for the `dlog` spelling and does not see this
|
|
129
|
+
* one.
|
|
130
|
+
*/
|
|
131
|
+
export function noteAngularStyleWrite(key: string): void {
|
|
132
|
+
if (!isDetailEnabled) return;
|
|
133
|
+
const name = `style.${key}`;
|
|
134
|
+
writesByProp.set(name, (writesByProp.get(name) ?? 0) + 1);
|
|
135
|
+
}
|
|
136
|
+
|
|
119
137
|
export function noteAngularCreate(tagName: string): void {
|
|
120
138
|
if (!isDetailEnabled) return;
|
|
121
139
|
createsByTag.set(tagName, (createsByTag.get(tagName) ?? 0) + 1);
|
package/src/element-props.ts
CHANGED
|
@@ -15,8 +15,6 @@
|
|
|
15
15
|
// wrapper component accepted. The claim that a directive can reclaim it "solely by executing a
|
|
16
16
|
// linked AOT artifact" was answered rather than assumed: `elements.test.ts` compiles the binding
|
|
17
17
|
// through real ngtsc and runs the LINKED output. See `SymbioteElement.style`.
|
|
18
|
-
//
|
|
19
|
-
// `symbioteStyle` stays as an alias — it is public API and the renderer still folds it.
|
|
20
18
|
import type {
|
|
21
19
|
IAccessibilityProps,
|
|
22
20
|
IAriaProps,
|
|
@@ -45,10 +43,8 @@ export interface IElementProps
|
|
|
45
43
|
renderToHardwareTextureAndroid?: boolean;
|
|
46
44
|
shouldRasterizeIOS?: boolean;
|
|
47
45
|
needsOffscreenAlphaCompositing?: boolean;
|
|
48
|
-
/** The array-capable `[style]`, aliased to `style` in the renderer. */
|
|
49
|
-
symbioteStyle?: IStyleProp<IViewStyle>;
|
|
50
46
|
/**
|
|
51
|
-
*
|
|
47
|
+
* The press-state callback is in the union because a subclass cannot widen an
|
|
52
48
|
* inherited property and `<pressable>`/`<touchable-*>` are the tags that take one; on every other
|
|
53
49
|
* tag the engine resolves it at `pressed: false`.
|
|
54
50
|
*/
|