@symbiote-native/angular 0.7.0 → 0.8.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 +3 -3
- package/build/angular/bootstrap.js +1 -1
- package/build/angular/bootstrap.js.map +1 -1
- package/build/angular/components/activity-indicator/index.android.js +7 -2
- package/build/angular/components/activity-indicator/index.android.js.map +1 -1
- package/build/angular/components/activity-indicator/index.ios.js +3 -1
- package/build/angular/components/activity-indicator/index.ios.js.map +1 -1
- package/build/angular/components/activity-indicator/shared.d.ts +1 -1
- package/build/angular/components/activity-indicator/shared.js +1 -1
- package/build/angular/components/activity-indicator/shared.js.map +1 -1
- package/build/angular/components/button.js +13 -5
- package/build/angular/components/button.js.map +1 -1
- package/build/angular/components/flat-list/index.d.ts +1 -1
- package/build/angular/components/flat-list/index.js +47 -14
- package/build/angular/components/flat-list/index.js.map +1 -1
- package/build/angular/components/image/index.android.js +4 -2
- package/build/angular/components/image/index.android.js.map +1 -1
- package/build/angular/components/image/index.ios.js +4 -2
- package/build/angular/components/image/index.ios.js.map +1 -1
- package/build/angular/components/image/shared.js +18 -6
- package/build/angular/components/image/shared.js.map +1 -1
- package/build/angular/components/image-background.js +22 -8
- package/build/angular/components/image-background.js.map +1 -1
- package/build/angular/components/input-accessory-view/index.js +7 -2
- package/build/angular/components/input-accessory-view/index.js.map +1 -1
- package/build/angular/components/keyboard-avoiding-view/index.d.ts +1 -0
- package/build/angular/components/keyboard-avoiding-view/index.js +39 -9
- package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
- package/build/angular/components/modal/index.js +7 -2
- package/build/angular/components/modal/index.js.map +1 -1
- package/build/angular/components/pressable/index.d.ts +1 -1
- package/build/angular/components/pressable/index.js +6 -2
- package/build/angular/components/pressable/index.js.map +1 -1
- package/build/angular/components/refresh-control/index.js +3 -1
- package/build/angular/components/refresh-control/index.js.map +1 -1
- package/build/angular/components/safe-area-view/index.js +3 -1
- package/build/angular/components/safe-area-view/index.js.map +1 -1
- package/build/angular/components/scroll-view/index.android.js +44 -14
- package/build/angular/components/scroll-view/index.android.js.map +1 -1
- package/build/angular/components/scroll-view/index.ios.js +13 -5
- package/build/angular/components/scroll-view/index.ios.js.map +1 -1
- package/build/angular/components/scroll-view/projection.d.ts +32 -0
- package/build/angular/components/scroll-view/projection.js +212 -47
- package/build/angular/components/scroll-view/projection.js.map +1 -1
- package/build/angular/components/scroll-view/shared.js +28 -7
- package/build/angular/components/scroll-view/shared.js.map +1 -1
- package/build/angular/components/scroll-view/sticky-header.js +7 -3
- package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
- package/build/angular/components/section-list/index.d.ts +6 -1
- package/build/angular/components/section-list/index.js +63 -18
- package/build/angular/components/section-list/index.js.map +1 -1
- package/build/angular/components/switch/index.android.js +17 -3
- package/build/angular/components/switch/index.android.js.map +1 -1
- package/build/angular/components/switch/index.ios.js +17 -3
- package/build/angular/components/switch/index.ios.js.map +1 -1
- package/build/angular/components/switch/shared.d.ts +1 -1
- package/build/angular/components/switch/shared.js +5 -2
- package/build/angular/components/switch/shared.js.map +1 -1
- package/build/angular/components/text-input.d.ts +1 -1
- package/build/angular/components/text-input.js +25 -5
- package/build/angular/components/text-input.js.map +1 -1
- package/build/angular/components/touchable/index.d.ts +29 -7
- package/build/angular/components/touchable/index.js +246 -69
- package/build/angular/components/touchable/index.js.map +1 -1
- package/build/angular/components/touchable-native-feedback/index.js +2 -2
- package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
- package/build/angular/components/virtualized-list/directives.js +4 -0
- package/build/angular/components/virtualized-list/directives.js.map +1 -1
- package/build/angular/components/virtualized-list/index.d.ts +5 -2
- package/build/angular/components/virtualized-list/index.js +197 -60
- package/build/angular/components/virtualized-list/index.js.map +1 -1
- package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
- package/build/angular/components/virtualized-section-list/index.d.ts +18 -1
- package/build/angular/components/virtualized-section-list/index.js +90 -21
- 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.map +1 -1
- package/build/angular/create-portal/index.js +5 -1
- package/build/angular/create-portal/index.js.map +1 -1
- package/build/angular/create-tunnel/index.js.map +1 -1
- package/build/angular/descriptor-to-angular/index.js +3 -1
- package/build/angular/descriptor-to-angular/index.js.map +1 -1
- package/build/angular/diagnostics.d.ts +33 -0
- package/build/angular/diagnostics.js +82 -0
- package/build/angular/diagnostics.js.map +1 -0
- package/build/angular/host-instance/index.js +1 -1
- package/build/angular/host-instance/index.js.map +1 -1
- package/build/angular/index.d.ts +5 -2
- package/build/angular/index.js +15 -1
- package/build/angular/index.js.map +1 -1
- package/build/angular/modules/animated/create-animated-component.js +27 -9
- package/build/angular/modules/animated/create-animated-component.js.map +1 -1
- package/build/angular/modules/app-registry/index.js.map +1 -1
- package/build/angular/modules/status-bar/index.js +1 -1
- package/build/angular/modules/status-bar/index.js.map +1 -1
- package/build/angular/primitives/index.d.ts +19 -2
- package/build/angular/primitives/index.js +39 -3
- package/build/angular/primitives/index.js.map +1 -1
- package/build/angular/primitives/shared.d.ts +9 -1
- package/build/angular/primitives/shared.js +49 -3
- package/build/angular/primitives/shared.js.map +1 -1
- package/build/angular/render/index.js +48 -5
- package/build/angular/render/index.js.map +1 -1
- package/build/angular/renderer/index.d.ts +1 -0
- package/build/angular/renderer/index.js +37 -2
- package/build/angular/renderer/index.js.map +1 -1
- package/build/angular/services/color-scheme.service.js.map +1 -1
- package/build/angular/services/window-dimensions.service.js +1 -1
- package/build/angular/services/window-dimensions.service.js.map +1 -1
- package/package.json +5 -5
- package/src/bootstrap.ts +4 -1
- package/src/components/activity-indicator/index.android.ts +7 -2
- package/src/components/activity-indicator/index.ios.ts +3 -1
- package/src/components/activity-indicator/shared.ts +32 -13
- package/src/components/button.ts +19 -6
- package/src/components/flat-list/index.ts +91 -29
- package/src/components/image/index.android.ts +9 -2
- package/src/components/image/index.ios.ts +9 -2
- package/src/components/image/shared.ts +58 -19
- package/src/components/image-background.ts +29 -10
- package/src/components/input-accessory-view/index.ts +27 -10
- package/src/components/keyboard-avoiding-view/index.ts +55 -15
- package/src/components/modal/index.ts +22 -8
- package/src/components/pressable/index.ts +51 -21
- package/src/components/refresh-control/index.ts +22 -8
- package/src/components/safe-area-view/index.ts +16 -7
- package/src/components/scroll-view/index.android.ts +31 -9
- package/src/components/scroll-view/index.ios.ts +17 -4
- package/src/components/scroll-view/projection.ts +290 -63
- package/src/components/scroll-view/shared.ts +93 -26
- package/src/components/scroll-view/sticky-header.ts +25 -8
- package/src/components/section-list/index.ts +61 -19
- package/src/components/switch/index.android.ts +14 -3
- package/src/components/switch/index.ios.ts +14 -3
- package/src/components/switch/shared.ts +15 -4
- package/src/components/text-input.ts +45 -13
- package/src/components/touchable/index.ts +296 -72
- package/src/components/touchable-native-feedback/index.ts +32 -10
- package/src/components/virtualized-list/directives.ts +19 -5
- package/src/components/virtualized-list/index.ts +274 -72
- package/src/components/virtualized-section-list/directives.ts +6 -3
- package/src/components/virtualized-section-list/index.ts +146 -34
- package/src/components.ts +4 -1
- package/src/create-portal/index.ts +8 -2
- package/src/create-tunnel/index.ts +3 -1
- package/src/descriptor-to-angular/index.ts +29 -8
- package/src/diagnostics.ts +138 -0
- package/src/host-instance/index.ts +9 -3
- package/src/index.ts +31 -2
- package/src/modules/animated/create-animated-component.ts +44 -15
- package/src/modules/app-registry/index.ts +3 -1
- package/src/modules/status-bar/index.ts +13 -4
- package/src/primitives/index.ts +46 -2
- package/src/primitives/shared.ts +70 -9
- package/src/render/index.ts +56 -6
- package/src/renderer/index.ts +76 -14
- package/src/services/color-scheme.service.ts +3 -1
- package/src/services/window-dimensions.service.ts +11 -4
|
@@ -64,7 +64,10 @@ import {
|
|
|
64
64
|
type IVListSeparatorContext,
|
|
65
65
|
} from '../virtualized-list';
|
|
66
66
|
import { VListOutletDirective } from '../virtualized-list/directives';
|
|
67
|
-
import {
|
|
67
|
+
import {
|
|
68
|
+
stableAnchorStyle,
|
|
69
|
+
SymbioteStyleInputDirective,
|
|
70
|
+
} from '../../primitives';
|
|
68
71
|
import {
|
|
69
72
|
VSectionFooterDirective,
|
|
70
73
|
VSectionHeaderDirective,
|
|
@@ -92,9 +95,20 @@ export type { IVSectionContext, IVSectionItemContext } from './directives';
|
|
|
92
95
|
// those are the per-adapter children/render fields and become `<ng-template>` directives in Angular.
|
|
93
96
|
// Everything agnostic is the SAME surface as the
|
|
94
97
|
// React/Vue adapters, including the a11y/aria prop family forwarded through VirtualizedList.
|
|
95
|
-
export interface IVirtualizedSectionListProps<ItemT>
|
|
98
|
+
export interface IVirtualizedSectionListProps<ItemT>
|
|
99
|
+
extends IAccessibilityProps, IAriaProps {
|
|
96
100
|
sections: ReadonlyArray<ISection<ItemT>>;
|
|
97
101
|
keyExtractor?: (item: ItemT, index: number) => string;
|
|
102
|
+
// Fixed-layout fast path, FLAT like RN's: the SECTIONS array plus a flat entry index, where every
|
|
103
|
+
// section contributes two rows beyond its items (header, footer) and the caller accounts for them.
|
|
104
|
+
// A `({ section, index })` form would be our invention, not parity - `VirtualizedSectionList.js`
|
|
105
|
+
// has no getItemLayout code at all, the prop rides through `passThroughProps`; that shape is the
|
|
106
|
+
// community react-native-section-list-get-item-layout, layered on top. Without it a fast scroll
|
|
107
|
+
// outruns measurement and leaves blank windows.
|
|
108
|
+
getItemLayout?: (
|
|
109
|
+
data: ReadonlyArray<ISection<ItemT>> | null,
|
|
110
|
+
index: number,
|
|
111
|
+
) => { length: number; offset: number; index: number };
|
|
98
112
|
// Stick each section header to the top as the next section scrolls up. Routed to the inner
|
|
99
113
|
// VirtualizedList's stickyHeaderIndices. Defaults to `Platform.OS === 'ios'`; Android does not
|
|
100
114
|
// stick by default. Pass true/false to override.
|
|
@@ -146,7 +160,9 @@ export type IVirtualizedSectionListInputs<ItemT> = Omit<
|
|
|
146
160
|
@Component({
|
|
147
161
|
selector: 'VirtualizedSectionList',
|
|
148
162
|
standalone: true,
|
|
149
|
-
hostDirectives: [
|
|
163
|
+
hostDirectives: [
|
|
164
|
+
{ directive: SymbioteStyleInputDirective, inputs: ['style'] },
|
|
165
|
+
],
|
|
150
166
|
imports: [
|
|
151
167
|
VirtualizedList,
|
|
152
168
|
VListItemDirective,
|
|
@@ -163,6 +179,7 @@ export type IVirtualizedSectionListInputs<ItemT> = Omit<
|
|
|
163
179
|
[getItem]="getEntry"
|
|
164
180
|
[getItemCount]="getEntryCount"
|
|
165
181
|
[keyExtractor]="entryKeyExtractor"
|
|
182
|
+
[getItemLayout]="entryItemLayout"
|
|
166
183
|
[stickyHeaderIndices]="stickyHeaderIndices"
|
|
167
184
|
[extraData]="extraData"
|
|
168
185
|
[inverted]="inverted"
|
|
@@ -207,8 +224,12 @@ export type IVirtualizedSectionListInputs<ItemT> = Omit<
|
|
|
207
224
|
[accessibilityElementsHidden]="accessibilityElementsHidden"
|
|
208
225
|
[accessibilityIgnoresInvertColors]="accessibilityIgnoresInvertColors"
|
|
209
226
|
[accessibilityLanguage]="accessibilityLanguage"
|
|
210
|
-
[accessibilityRespondsToUserInteraction]="
|
|
211
|
-
|
|
227
|
+
[accessibilityRespondsToUserInteraction]="
|
|
228
|
+
accessibilityRespondsToUserInteraction
|
|
229
|
+
"
|
|
230
|
+
[accessibilityShowsLargeContentViewer]="
|
|
231
|
+
accessibilityShowsLargeContentViewer
|
|
232
|
+
"
|
|
212
233
|
[accessibilityLargeContentTitle]="accessibilityLargeContentTitle"
|
|
213
234
|
(accessibilityAction)="resolvedOnAccessibilityAction?.($event)"
|
|
214
235
|
(accessibilityTap)="resolvedOnAccessibilityTap?.($event)"
|
|
@@ -246,7 +267,9 @@ export type IVirtualizedSectionListInputs<ItemT> = Omit<
|
|
|
246
267
|
></ng-container>
|
|
247
268
|
}
|
|
248
269
|
@case ('section-separator') {
|
|
249
|
-
<ng-container
|
|
270
|
+
<ng-container
|
|
271
|
+
[vListOutlet]="sectionSeparatorDir?.templateRef"
|
|
272
|
+
></ng-container>
|
|
250
273
|
}
|
|
251
274
|
@case ('item') {
|
|
252
275
|
<ng-container
|
|
@@ -260,12 +283,16 @@ export type IVirtualizedSectionListInputs<ItemT> = Omit<
|
|
|
260
283
|
<!-- list-level slots: forward the app's VirtualizedList directives to the inner list. -->
|
|
261
284
|
@if (listHeaderDir !== undefined) {
|
|
262
285
|
<ng-template vListHeader>
|
|
263
|
-
<ng-container
|
|
286
|
+
<ng-container
|
|
287
|
+
[vListOutlet]="listHeaderDir.templateRef"
|
|
288
|
+
></ng-container>
|
|
264
289
|
</ng-template>
|
|
265
290
|
}
|
|
266
291
|
@if (listFooterDir !== undefined) {
|
|
267
292
|
<ng-template vListFooter>
|
|
268
|
-
<ng-container
|
|
293
|
+
<ng-container
|
|
294
|
+
[vListOutlet]="listFooterDir.templateRef"
|
|
295
|
+
></ng-container>
|
|
269
296
|
</ng-template>
|
|
270
297
|
}
|
|
271
298
|
@if (listEmptyDir !== undefined) {
|
|
@@ -283,7 +310,9 @@ export type IVirtualizedSectionListInputs<ItemT> = Omit<
|
|
|
283
310
|
>
|
|
284
311
|
<ng-container
|
|
285
312
|
[vListOutlet]="itemSeparatorDir.templateRef"
|
|
286
|
-
[vListOutletContext]="
|
|
313
|
+
[vListOutletContext]="
|
|
314
|
+
itemSeparatorContextOf(highlighted, leadingItem, trailingItem)
|
|
315
|
+
"
|
|
287
316
|
></ng-container>
|
|
288
317
|
</ng-template>
|
|
289
318
|
}
|
|
@@ -291,15 +320,26 @@ export type IVirtualizedSectionListInputs<ItemT> = Omit<
|
|
|
291
320
|
`,
|
|
292
321
|
})
|
|
293
322
|
export class VirtualizedSectionList<ItemT = unknown>
|
|
294
|
-
implements
|
|
323
|
+
implements
|
|
324
|
+
IVirtualizedSectionListInputs<ItemT>,
|
|
325
|
+
IVirtualizedSectionListHandle,
|
|
326
|
+
DoCheck
|
|
295
327
|
{
|
|
296
328
|
@Input({ required: true }) sections!: ReadonlyArray<ISection<ItemT>>;
|
|
297
329
|
@Input() keyExtractor?: (item: ItemT, index: number) => string;
|
|
330
|
+
@Input() getItemLayout?: (
|
|
331
|
+
data: ReadonlyArray<ISection<ItemT>> | null,
|
|
332
|
+
index: number,
|
|
333
|
+
) => { length: number; offset: number; index: number };
|
|
298
334
|
@Input() stickySectionHeadersEnabled?: boolean;
|
|
299
335
|
@Input() extraData?: unknown;
|
|
300
|
-
@Output() readonly endReached = new EventEmitter<{
|
|
336
|
+
@Output() readonly endReached = new EventEmitter<{
|
|
337
|
+
distanceFromEnd: number;
|
|
338
|
+
}>();
|
|
301
339
|
@Input() onEndReachedThreshold?: number;
|
|
302
|
-
@Output() readonly startReached = new EventEmitter<{
|
|
340
|
+
@Output() readonly startReached = new EventEmitter<{
|
|
341
|
+
distanceFromStart: number;
|
|
342
|
+
}>();
|
|
303
343
|
@Input() onStartReachedThreshold?: number;
|
|
304
344
|
@Output() readonly refresh = new EventEmitter<void>();
|
|
305
345
|
@Input() refreshing?: boolean | null;
|
|
@@ -360,8 +400,10 @@ export class VirtualizedSectionList<ItemT = unknown>
|
|
|
360
400
|
@Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
|
|
361
401
|
@Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
|
|
362
402
|
@Input() accessibilityLabelledBy?: string | string[];
|
|
363
|
-
@Input()
|
|
364
|
-
|
|
403
|
+
@Input()
|
|
404
|
+
importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
|
|
405
|
+
@Input()
|
|
406
|
+
accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
|
|
365
407
|
@Input() screenReaderFocusable?: boolean;
|
|
366
408
|
@Input() accessibilityViewIsModal?: boolean;
|
|
367
409
|
@Input() accessibilityElementsHidden?: boolean;
|
|
@@ -393,20 +435,27 @@ export class VirtualizedSectionList<ItemT = unknown>
|
|
|
393
435
|
// The section cell templates the app authors (the Angular twin of renderItem /
|
|
394
436
|
// renderSectionHeader / renderSectionFooter / SectionSeparatorComponent), captured from projected
|
|
395
437
|
// <ng-template> content and stamped by the synthesized vListItem dispatch.
|
|
396
|
-
@ContentChild(VSectionItemDirective)
|
|
397
|
-
|
|
398
|
-
@ContentChild(
|
|
399
|
-
|
|
438
|
+
@ContentChild(VSectionItemDirective)
|
|
439
|
+
sectionItemDir?: VSectionItemDirective<ItemT>;
|
|
440
|
+
@ContentChild(VSectionHeaderDirective)
|
|
441
|
+
sectionHeaderDir?: VSectionHeaderDirective<ItemT>;
|
|
442
|
+
@ContentChild(VSectionFooterDirective)
|
|
443
|
+
sectionFooterDir?: VSectionFooterDirective<ItemT>;
|
|
444
|
+
@ContentChild(VSectionSeparatorDirective)
|
|
445
|
+
sectionSeparatorDir?: VSectionSeparatorDirective;
|
|
400
446
|
|
|
401
447
|
// list-level slots reuse VirtualizedList's own directives; VSL forwards them to the inner list.
|
|
402
448
|
@ContentChild(VListHeaderDirective) listHeaderDir?: VListHeaderDirective;
|
|
403
449
|
@ContentChild(VListFooterDirective) listFooterDir?: VListFooterDirective;
|
|
404
450
|
@ContentChild(VListEmptyDirective) listEmptyDir?: VListEmptyDirective;
|
|
405
|
-
@ContentChild(VListSeparatorDirective)
|
|
451
|
+
@ContentChild(VListSeparatorDirective)
|
|
452
|
+
itemSeparatorDir?: VListSeparatorDirective<ItemT>;
|
|
406
453
|
|
|
407
454
|
// The composed inner VirtualizedList. Its instance IS an IVirtualizedListHandle, so the section
|
|
408
455
|
// handle delegates straight to it. Available from ngAfterViewInit; reads lazily, no-ops pre-commit.
|
|
409
|
-
@ViewChild(VirtualizedList) private list?: VirtualizedList<
|
|
456
|
+
@ViewChild(VirtualizedList) private list?: VirtualizedList<
|
|
457
|
+
ISectionEntry<ItemT>
|
|
458
|
+
>;
|
|
410
459
|
|
|
411
460
|
// This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
|
|
412
461
|
// onto (see anchorHostStyle's doc comment) — NOT `list` above, which targets the real inner
|
|
@@ -439,7 +488,10 @@ export class VirtualizedSectionList<ItemT = unknown>
|
|
|
439
488
|
this.lastHasSectionSeparator = hasSectionSeparator;
|
|
440
489
|
|
|
441
490
|
const sections = this.sections ?? [];
|
|
442
|
-
const { entries, headerIndices } = flattenSections(
|
|
491
|
+
const { entries, headerIndices } = flattenSections(
|
|
492
|
+
sections,
|
|
493
|
+
hasSectionSeparator,
|
|
494
|
+
);
|
|
443
495
|
this.flatEntries = entries;
|
|
444
496
|
this.headerIndices = headerIndices;
|
|
445
497
|
|
|
@@ -458,42 +510,95 @@ export class VirtualizedSectionList<ItemT = unknown>
|
|
|
458
510
|
|
|
459
511
|
// The derived flat-stream accessors handed to the inner VirtualizedList. Stable arrow fields so
|
|
460
512
|
// the bindings keep a constant identity; they read the live flatEntries.
|
|
461
|
-
getEntry = (_source: unknown, index: number): ISectionEntry<ItemT> =>
|
|
513
|
+
getEntry = (_source: unknown, index: number): ISectionEntry<ItemT> =>
|
|
514
|
+
this.flatEntries[index];
|
|
462
515
|
getEntryCount = (): number => this.flatEntries.length;
|
|
463
516
|
entryKeyExtractor = (entry: ISectionEntry<ItemT>, index: number): string =>
|
|
464
517
|
sectionEntryKey(entry, index, this.keyExtractor);
|
|
465
518
|
|
|
519
|
+
// Hand the callback `sections`, not the entries: RN's inner VirtualizedList gets
|
|
520
|
+
// `data={this.props.sections}` (VirtualizedSectionList.js:216) while ours streams the FLATTENED
|
|
521
|
+
// entries, so the same user code would otherwise see a different argument here than on RN.
|
|
522
|
+
//
|
|
523
|
+
// UPSTREAM-DIVERGENCE(react-native): the flat INDEX matches RN's (two rows per section, header
|
|
524
|
+
// and footer) only while the vSectionSeparator template is unset. With it, flattenSections emits
|
|
525
|
+
// an extra 'section-separator' row per boundary that RN renders inside the neighbouring cell, so
|
|
526
|
+
// indices shift by one per boundary from the second section on. Deliberate - that row is how this
|
|
527
|
+
// adapter paints the separator; a caller combining the two must account for it.
|
|
528
|
+
//
|
|
529
|
+
// Cached on the input's own identity, like resolvedStyle above: the inner VirtualizedList folds
|
|
530
|
+
// getItemLayout into its ngDoCheck dedup array, so a wrapper allocated per getter read would
|
|
531
|
+
// re-run its whole recompute every CD.
|
|
532
|
+
private cachedEntryItemLayout?: (
|
|
533
|
+
data: unknown,
|
|
534
|
+
index: number,
|
|
535
|
+
) => { length: number; offset: number; index: number };
|
|
536
|
+
private lastGetItemLayout?: IVirtualizedSectionListProps<ItemT>['getItemLayout'];
|
|
537
|
+
get entryItemLayout():
|
|
538
|
+
| ((
|
|
539
|
+
data: unknown,
|
|
540
|
+
index: number,
|
|
541
|
+
) => { length: number; offset: number; index: number })
|
|
542
|
+
| undefined {
|
|
543
|
+
if (this.getItemLayout !== this.lastGetItemLayout) {
|
|
544
|
+
const getItemLayout = this.getItemLayout;
|
|
545
|
+
this.lastGetItemLayout = getItemLayout;
|
|
546
|
+
this.cachedEntryItemLayout =
|
|
547
|
+
getItemLayout === undefined
|
|
548
|
+
? undefined
|
|
549
|
+
: (
|
|
550
|
+
_entries: unknown,
|
|
551
|
+
index: number,
|
|
552
|
+
): { length: number; offset: number; index: number } =>
|
|
553
|
+
getItemLayout(this.sections, index);
|
|
554
|
+
}
|
|
555
|
+
return this.cachedEntryItemLayout;
|
|
556
|
+
}
|
|
557
|
+
|
|
466
558
|
// Adapts each @Output() into the plain callback VirtualizedList's own @Input() still wants,
|
|
467
559
|
// gated on `.observed` so an unlistened event forwards `undefined` — the same "nobody cares"
|
|
468
560
|
// contract the old @Input() callback passthrough had (e.g. onRefresh presence gates whether a
|
|
469
561
|
// RefreshControl is built downstream; see the Vue twin's `listens('onRefresh')` gate).
|
|
470
|
-
get resolvedOnEndReached():
|
|
471
|
-
|
|
562
|
+
get resolvedOnEndReached():
|
|
563
|
+
((info: { distanceFromEnd: number }) => void) | undefined {
|
|
564
|
+
return this.endReached.observed
|
|
565
|
+
? info => this.endReached.emit(info)
|
|
566
|
+
: undefined;
|
|
472
567
|
}
|
|
473
568
|
|
|
474
|
-
get resolvedOnStartReached():
|
|
475
|
-
|
|
569
|
+
get resolvedOnStartReached():
|
|
570
|
+
((info: { distanceFromStart: number }) => void) | undefined {
|
|
571
|
+
return this.startReached.observed
|
|
572
|
+
? info => this.startReached.emit(info)
|
|
573
|
+
: undefined;
|
|
476
574
|
}
|
|
477
575
|
|
|
478
576
|
get resolvedOnRefresh(): (() => void) | undefined {
|
|
479
577
|
return this.refresh.observed ? () => this.refresh.emit() : undefined;
|
|
480
578
|
}
|
|
481
579
|
|
|
482
|
-
get resolvedOnAccessibilityAction():
|
|
580
|
+
get resolvedOnAccessibilityAction():
|
|
581
|
+
((event: ISymbioteEvent) => void) | undefined {
|
|
483
582
|
return this.accessibilityAction.observed
|
|
484
583
|
? event => this.accessibilityAction.emit(event)
|
|
485
584
|
: undefined;
|
|
486
585
|
}
|
|
487
586
|
|
|
488
|
-
get resolvedOnAccessibilityTap():
|
|
489
|
-
|
|
587
|
+
get resolvedOnAccessibilityTap():
|
|
588
|
+
((event: ISymbioteEvent) => void) | undefined {
|
|
589
|
+
return this.accessibilityTap.observed
|
|
590
|
+
? event => this.accessibilityTap.emit(event)
|
|
591
|
+
: undefined;
|
|
490
592
|
}
|
|
491
593
|
|
|
492
594
|
get resolvedOnMagicTap(): ((event: ISymbioteEvent) => void) | undefined {
|
|
493
|
-
return this.magicTap.observed
|
|
595
|
+
return this.magicTap.observed
|
|
596
|
+
? event => this.magicTap.emit(event)
|
|
597
|
+
: undefined;
|
|
494
598
|
}
|
|
495
599
|
|
|
496
|
-
get resolvedOnAccessibilityEscape():
|
|
600
|
+
get resolvedOnAccessibilityEscape():
|
|
601
|
+
((event: ISymbioteEvent) => void) | undefined {
|
|
497
602
|
return this.accessibilityEscape.observed
|
|
498
603
|
? event => this.accessibilityEscape.emit(event)
|
|
499
604
|
: undefined;
|
|
@@ -516,7 +621,10 @@ export class VirtualizedSectionList<ItemT = unknown>
|
|
|
516
621
|
return { $implicit: value.section, section: value.section };
|
|
517
622
|
}
|
|
518
623
|
|
|
519
|
-
itemContextOf(
|
|
624
|
+
itemContextOf(
|
|
625
|
+
value: unknown,
|
|
626
|
+
separators: ISeparators,
|
|
627
|
+
): IVSectionItemContext<ItemT> | undefined {
|
|
520
628
|
if (!this.isEntry(value) || value.kind !== 'item') return undefined;
|
|
521
629
|
return {
|
|
522
630
|
$implicit: value.item,
|
|
@@ -539,8 +647,12 @@ export class VirtualizedSectionList<ItemT = unknown>
|
|
|
539
647
|
return {
|
|
540
648
|
$implicit: highlighted === true,
|
|
541
649
|
highlighted: highlighted === true,
|
|
542
|
-
leadingItem: this.isEntry(leadingItem)
|
|
543
|
-
|
|
650
|
+
leadingItem: this.isEntry(leadingItem)
|
|
651
|
+
? unwrapEntryItem(leadingItem)
|
|
652
|
+
: undefined,
|
|
653
|
+
trailingItem: this.isEntry(trailingItem)
|
|
654
|
+
? unwrapEntryItem(trailingItem)
|
|
655
|
+
: undefined,
|
|
544
656
|
};
|
|
545
657
|
}
|
|
546
658
|
|
package/src/components.ts
CHANGED
|
@@ -112,7 +112,10 @@ export type {
|
|
|
112
112
|
IVSectionItemContext,
|
|
113
113
|
} from './components/virtualized-section-list';
|
|
114
114
|
export { SectionList } from './components/section-list';
|
|
115
|
-
export type {
|
|
115
|
+
export type {
|
|
116
|
+
ISectionListHandle,
|
|
117
|
+
ISectionListProps,
|
|
118
|
+
} from './components/section-list';
|
|
116
119
|
|
|
117
120
|
// Re-export primitive hosts so composed components can import them from the public barrel too.
|
|
118
121
|
export {
|
|
@@ -42,7 +42,11 @@ import {
|
|
|
42
42
|
/** Marks the destination for `*portal` — place it on whichever already-mounted host should
|
|
43
43
|
* paint the portaled content (e.g. a persistent overlay-host View near the app root), then
|
|
44
44
|
* export it to a template variable and pass that variable as `*portal`'s target. */
|
|
45
|
-
@Directive({
|
|
45
|
+
@Directive({
|
|
46
|
+
selector: '[portalOutlet]',
|
|
47
|
+
standalone: true,
|
|
48
|
+
exportAs: 'portalOutlet',
|
|
49
|
+
})
|
|
46
50
|
export class PortalOutletDirective {
|
|
47
51
|
readonly viewContainerRef = inject(ViewContainerRef);
|
|
48
52
|
}
|
|
@@ -59,7 +63,9 @@ export class PortalDirective implements OnChanges, OnDestroy {
|
|
|
59
63
|
|
|
60
64
|
ngOnChanges(): void {
|
|
61
65
|
this.viewRef?.destroy();
|
|
62
|
-
this.viewRef = this.portal.viewContainerRef.createEmbeddedView(
|
|
66
|
+
this.viewRef = this.portal.viewContainerRef.createEmbeddedView(
|
|
67
|
+
this.templateRef,
|
|
68
|
+
);
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
ngOnDestroy(): void {
|
|
@@ -109,7 +109,9 @@ export class TunnelOut implements OnInit, OnDestroy {
|
|
|
109
109
|
private stopEffect: (() => void) | undefined;
|
|
110
110
|
|
|
111
111
|
ngOnInit(): void {
|
|
112
|
-
const ref = effect(() => this.sync(this.tunnel.entries()), {
|
|
112
|
+
const ref = effect(() => this.sync(this.tunnel.entries()), {
|
|
113
|
+
injector: this.injector,
|
|
114
|
+
});
|
|
113
115
|
this.stopEffect = () => ref.destroy();
|
|
114
116
|
}
|
|
115
117
|
|
|
@@ -15,7 +15,10 @@ import {
|
|
|
15
15
|
type OnDestroy,
|
|
16
16
|
type SimpleChanges,
|
|
17
17
|
} from '@angular/core';
|
|
18
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
IDescriptor,
|
|
20
|
+
IDescriptorChild,
|
|
21
|
+
} from '@symbiote-native/components';
|
|
19
22
|
|
|
20
23
|
type IRenderedChild = IRenderedElement | IRenderedText;
|
|
21
24
|
|
|
@@ -43,7 +46,8 @@ export class DescriptorOutlet implements OnChanges, OnDestroy {
|
|
|
43
46
|
@Input({ required: true }) node!: IDescriptor;
|
|
44
47
|
|
|
45
48
|
private readonly renderer = inject(Renderer2);
|
|
46
|
-
private readonly hostNode =
|
|
49
|
+
private readonly hostNode =
|
|
50
|
+
inject<ElementRef<unknown>>(ElementRef).nativeElement;
|
|
47
51
|
private rendered: IRenderedElement | undefined;
|
|
48
52
|
|
|
49
53
|
ngOnChanges(changes: SimpleChanges): void {
|
|
@@ -57,7 +61,10 @@ export class DescriptorOutlet implements OnChanges, OnDestroy {
|
|
|
57
61
|
this.rendered = undefined;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
|
-
private patchRoot(
|
|
64
|
+
private patchRoot(
|
|
65
|
+
rendered: IRenderedElement | undefined,
|
|
66
|
+
next: IDescriptor,
|
|
67
|
+
): IRenderedElement {
|
|
61
68
|
if (rendered === undefined) {
|
|
62
69
|
const created = this.createElement(next);
|
|
63
70
|
this.renderer.appendChild(this.hostNode, created.node);
|
|
@@ -67,7 +74,9 @@ export class DescriptorOutlet implements OnChanges, OnDestroy {
|
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
private createChild(child: IDescriptorChild): IRenderedChild {
|
|
70
|
-
return typeof child === 'string'
|
|
77
|
+
return typeof child === 'string'
|
|
78
|
+
? this.createText(child)
|
|
79
|
+
: this.createElement(child);
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
private createText(value: string): IRenderedText {
|
|
@@ -127,12 +136,19 @@ export class DescriptorOutlet implements OnChanges, OnDestroy {
|
|
|
127
136
|
}
|
|
128
137
|
|
|
129
138
|
this.patchProps(rendered, next.props);
|
|
130
|
-
rendered.children = this.patchChildren(
|
|
139
|
+
rendered.children = this.patchChildren(
|
|
140
|
+
rendered.node,
|
|
141
|
+
rendered.children,
|
|
142
|
+
next.children,
|
|
143
|
+
);
|
|
131
144
|
rendered.descriptor = next;
|
|
132
145
|
return rendered;
|
|
133
146
|
}
|
|
134
147
|
|
|
135
|
-
private patchProps(
|
|
148
|
+
private patchProps(
|
|
149
|
+
rendered: IRenderedElement,
|
|
150
|
+
nextProps: Record<string, unknown>,
|
|
151
|
+
): void {
|
|
136
152
|
const prevProps = rendered.props;
|
|
137
153
|
for (const key of Object.keys(prevProps)) {
|
|
138
154
|
if (!(key in nextProps)) {
|
|
@@ -173,13 +189,18 @@ export class DescriptorOutlet implements OnChanges, OnDestroy {
|
|
|
173
189
|
|
|
174
190
|
for (let i = common; i < renderedChildren.length; i++) {
|
|
175
191
|
const rendered = renderedChildren[i];
|
|
176
|
-
if (rendered !== undefined)
|
|
192
|
+
if (rendered !== undefined)
|
|
193
|
+
this.renderer.removeChild(parent, rendered.node);
|
|
177
194
|
}
|
|
178
195
|
|
|
179
196
|
return nextRendered;
|
|
180
197
|
}
|
|
181
198
|
|
|
182
|
-
private replaceChild(
|
|
199
|
+
private replaceChild(
|
|
200
|
+
parent: unknown,
|
|
201
|
+
oldChild: unknown,
|
|
202
|
+
newChild: unknown,
|
|
203
|
+
): void {
|
|
183
204
|
this.renderer.insertBefore(parent, newChild, oldChild);
|
|
184
205
|
this.renderer.removeChild(parent, oldChild);
|
|
185
206
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Per-window counters for the Angular adapter, the twin of the engine's readCommitProfile().
|
|
2
|
+
// The engine's number answers "what did the commit walk cost"; these answer the question one layer
|
|
3
|
+
// up, which is the one Angular keeps losing: HOW MANY TIMES did anything run at all.
|
|
4
|
+
//
|
|
5
|
+
// Angular is the only adapter whose update model can free-run. A change-detection pass that writes
|
|
6
|
+
// a fresh reference into a downstream @Input marks a view dirty, the zoneless scheduler books
|
|
7
|
+
// another pass, and it never settles - the hazard `stableAnchorStyle` and VirtualizedList's
|
|
8
|
+
// `lastRecompute` gate each guard one instance of. A guard that stops holding is INVISIBLE from the
|
|
9
|
+
// outside: the screen paints correctly, only the frame rate collapses. `cdPasses` at rest is what
|
|
10
|
+
// makes it visible, and every other counter here narrows the same window to a suspect.
|
|
11
|
+
//
|
|
12
|
+
// Deliberately NOT gated behind isDebug(): an integer increment is noise next to the work it
|
|
13
|
+
// counts, and the numbers are only meaningful from a RELEASE build (dev-mode Angular runs its own
|
|
14
|
+
// checks and drowns the signal). The per-name detail map IS gated, since it allocates.
|
|
15
|
+
|
|
16
|
+
export interface IAngularProfile {
|
|
17
|
+
// Change-detection passes: RendererFactory2.end() runs once per ApplicationRef.tick().
|
|
18
|
+
cdPasses: number;
|
|
19
|
+
// Primitive host views (symbiote-view / symbiote-text / ...) Angular actually walked. Divided by
|
|
20
|
+
// cdPasses this is the SIZE OF THE TREE one tick touches - the number that separates "the list is
|
|
21
|
+
// expensive" from "every mounted screen is re-checked because a list scrolled". The primitives are
|
|
22
|
+
// CheckAlways, and a Global tick refreshes every CheckAlways view in the application.
|
|
23
|
+
viewsChecked: number;
|
|
24
|
+
// Prop/style/class/text writes that reached the engine. At rest this must be 0: a nonzero idle
|
|
25
|
+
// value means change detection is rewriting props nothing asked to change.
|
|
26
|
+
rendererWrites: number;
|
|
27
|
+
nodesCreated: number;
|
|
28
|
+
nodesInserted: number;
|
|
29
|
+
nodesRemoved: number;
|
|
30
|
+
// The `class` poll every COMPOSED component runs per check (SymbioteStyleInputDirective: Pressable,
|
|
31
|
+
// ScrollView, Image, the lists...; the raw primitives are counted by `viewsChecked` instead).
|
|
32
|
+
// `styleMarks` is how often the poll actually found a change and dirtied the view - the ratio is
|
|
33
|
+
// how much of the polling is wasted, and a nonzero idle `styleMarks` is a free-run engine.
|
|
34
|
+
styleChecks: number;
|
|
35
|
+
styleMarks: number;
|
|
36
|
+
// VirtualizedList: checks vs the ones that got past the `lastRecompute` dedup gate. Recomputes
|
|
37
|
+
// climbing with checks means the gate is not holding, which is the free-run bug itself.
|
|
38
|
+
listChecks: number;
|
|
39
|
+
listRecomputes: number;
|
|
40
|
+
listMarks: number;
|
|
41
|
+
// Scroll events the list saw. With sticky headers RN pins scrollEventThrottle to 1, so this is
|
|
42
|
+
// the frame rate of the gesture and the denominator for everything measured during a drag.
|
|
43
|
+
scrollTicks: number;
|
|
44
|
+
// Cell views. `outletUpdates` is the cheap path (context patched in place); `outletCreates` past
|
|
45
|
+
// the first window means cells are being rebuilt instead of reused, which is the cost CDK's
|
|
46
|
+
// view-recycling strategy exists to avoid.
|
|
47
|
+
outletCreates: number;
|
|
48
|
+
outletUpdates: number;
|
|
49
|
+
outletDestroys: number;
|
|
50
|
+
// Sticky projection: schedules are cheap (a Set insert), flushes walk every projected record.
|
|
51
|
+
projectionSchedules: number;
|
|
52
|
+
projectionFlushes: number;
|
|
53
|
+
// A sticky wrapper rebuilt loses its measured layout and costs a native round trip; reuse is the
|
|
54
|
+
// fixed path. Creates climbing during a plain scroll is the regression this pair pins.
|
|
55
|
+
stickyWrapperCreates: number;
|
|
56
|
+
stickyWrapperReuses: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type IAngularCounter = keyof IAngularProfile;
|
|
60
|
+
|
|
61
|
+
function createCounters(): IAngularProfile {
|
|
62
|
+
return {
|
|
63
|
+
cdPasses: 0,
|
|
64
|
+
viewsChecked: 0,
|
|
65
|
+
rendererWrites: 0,
|
|
66
|
+
nodesCreated: 0,
|
|
67
|
+
nodesInserted: 0,
|
|
68
|
+
nodesRemoved: 0,
|
|
69
|
+
styleChecks: 0,
|
|
70
|
+
styleMarks: 0,
|
|
71
|
+
listChecks: 0,
|
|
72
|
+
listRecomputes: 0,
|
|
73
|
+
listMarks: 0,
|
|
74
|
+
scrollTicks: 0,
|
|
75
|
+
outletCreates: 0,
|
|
76
|
+
outletUpdates: 0,
|
|
77
|
+
outletDestroys: 0,
|
|
78
|
+
projectionSchedules: 0,
|
|
79
|
+
projectionFlushes: 0,
|
|
80
|
+
stickyWrapperCreates: 0,
|
|
81
|
+
stickyWrapperReuses: 0,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let counters = createCounters();
|
|
86
|
+
|
|
87
|
+
export function countAngular(counter: IAngularCounter): void {
|
|
88
|
+
counters[counter] += 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Reading zeroes the counters, so a sampler on an interval gets disjoint windows rather than a
|
|
92
|
+
// growing total - same contract as readCommitProfile().
|
|
93
|
+
export function readAngularProfile(): IAngularProfile {
|
|
94
|
+
const snapshot = counters;
|
|
95
|
+
counters = createCounters();
|
|
96
|
+
return snapshot;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Which prop names the renderer actually rewrites, and which host tags get created. Off by default
|
|
100
|
+
// because it allocates per write; the probe screen turns it on for one sample. A write nothing
|
|
101
|
+
// asked for is the whole diagnosis, and the prop NAME is what points at the binding responsible.
|
|
102
|
+
let isDetailEnabled = false;
|
|
103
|
+
const writesByProp = new Map<string, number>();
|
|
104
|
+
const createsByTag = new Map<string, number>();
|
|
105
|
+
|
|
106
|
+
export function setAngularProfileDetail(enabled: boolean): void {
|
|
107
|
+
isDetailEnabled = enabled;
|
|
108
|
+
if (!enabled) {
|
|
109
|
+
writesByProp.clear();
|
|
110
|
+
createsByTag.clear();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function noteAngularWrite(propName: string): void {
|
|
115
|
+
if (!isDetailEnabled) return;
|
|
116
|
+
writesByProp.set(propName, (writesByProp.get(propName) ?? 0) + 1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function noteAngularCreate(tagName: string): void {
|
|
120
|
+
if (!isDetailEnabled) return;
|
|
121
|
+
createsByTag.set(tagName, (createsByTag.get(tagName) ?? 0) + 1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface IAngularProfileDetail {
|
|
125
|
+
writesByProp: readonly (readonly [string, number])[];
|
|
126
|
+
createsByTag: readonly (readonly [string, number])[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Sorted hottest-first and read-and-cleared, so a sample names its own top offenders.
|
|
130
|
+
export function readAngularProfileDetail(): IAngularProfileDetail {
|
|
131
|
+
const detail = {
|
|
132
|
+
writesByProp: [...writesByProp.entries()].sort((a, b) => b[1] - a[1]),
|
|
133
|
+
createsByTag: [...createsByTag.entries()].sort((a, b) => b[1] - a[1]),
|
|
134
|
+
};
|
|
135
|
+
writesByProp.clear();
|
|
136
|
+
createsByTag.clear();
|
|
137
|
+
return detail;
|
|
138
|
+
}
|
|
@@ -13,14 +13,19 @@
|
|
|
13
13
|
// An uncommitted or unknown input surfaces as null.
|
|
14
14
|
|
|
15
15
|
import { ElementRef } from '@angular/core';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
getNativeTag,
|
|
18
|
+
isSymbioteNode,
|
|
19
|
+
type ISymbioteNode,
|
|
20
|
+
} from '@symbiote-native/engine';
|
|
17
21
|
|
|
18
22
|
export type { IHostInstance } from '@symbiote-native/engine';
|
|
19
23
|
|
|
20
24
|
function resolveHostNode(candidate: unknown): ISymbioteNode | null {
|
|
21
25
|
if (candidate === null || candidate === undefined) return null;
|
|
22
26
|
if (isSymbioteNode(candidate)) return candidate;
|
|
23
|
-
if (candidate instanceof ElementRef)
|
|
27
|
+
if (candidate instanceof ElementRef)
|
|
28
|
+
return resolveHostNode(candidate.nativeElement);
|
|
24
29
|
const maybeHost = candidate as { nativeElement?: unknown };
|
|
25
30
|
if (typeof maybeHost.nativeElement !== 'undefined') {
|
|
26
31
|
return resolveHostNode(maybeHost.nativeElement);
|
|
@@ -29,7 +34,8 @@ function resolveHostNode(candidate: unknown): ISymbioteNode | null {
|
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
export function findNodeHandle(componentOrHandle: unknown): number | null {
|
|
32
|
-
if (componentOrHandle === null || componentOrHandle === undefined)
|
|
37
|
+
if (componentOrHandle === null || componentOrHandle === undefined)
|
|
38
|
+
return null;
|
|
33
39
|
if (typeof componentOrHandle === 'number') return componentOrHandle;
|
|
34
40
|
const node = resolveHostNode(componentOrHandle);
|
|
35
41
|
return node ? (getNativeTag(node) ?? null) : null;
|