@symbiote-native/angular 0.0.0-canary-20260724081123
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/LICENSE +21 -0
- package/README.md +157 -0
- package/babel-linker.cjs +7 -0
- package/babel-register-composed.cjs +127 -0
- package/bin/symbiote-angular-dev.cjs +93 -0
- package/build/angular/anchor-host-registry.d.ts +2 -0
- package/build/angular/anchor-host-registry.js +84 -0
- package/build/angular/anchor-host-registry.js.map +1 -0
- package/build/angular/bootstrap.d.ts +7 -0
- package/build/angular/bootstrap.js +15 -0
- package/build/angular/bootstrap.js.map +1 -0
- package/build/angular/components/activity-indicator/index.android.d.ts +12 -0
- package/build/angular/components/activity-indicator/index.android.js +21 -0
- package/build/angular/components/activity-indicator/index.android.js.map +1 -0
- package/build/angular/components/activity-indicator/index.d.ts +1 -0
- package/build/angular/components/activity-indicator/index.ios.d.ts +8 -0
- package/build/angular/components/activity-indicator/index.ios.js +21 -0
- package/build/angular/components/activity-indicator/index.ios.js.map +1 -0
- package/build/angular/components/activity-indicator/index.js +4 -0
- package/build/angular/components/activity-indicator/index.js.map +1 -0
- package/build/angular/components/activity-indicator/shared.d.ts +67 -0
- package/build/angular/components/activity-indicator/shared.js +260 -0
- package/build/angular/components/activity-indicator/shared.js.map +1 -0
- package/build/angular/components/button.d.ts +63 -0
- package/build/angular/components/button.js +332 -0
- package/build/angular/components/button.js.map +1 -0
- package/build/angular/components/flat-list/index.d.ts +122 -0
- package/build/angular/components/flat-list/index.js +688 -0
- package/build/angular/components/flat-list/index.js.map +1 -0
- package/build/angular/components/image/index.android.d.ts +10 -0
- package/build/angular/components/image/index.android.js +59 -0
- package/build/angular/components/image/index.android.js.map +1 -0
- package/build/angular/components/image/index.d.ts +1 -0
- package/build/angular/components/image/index.ios.d.ts +10 -0
- package/build/angular/components/image/index.ios.js +59 -0
- package/build/angular/components/image/index.ios.js.map +1 -0
- package/build/angular/components/image/index.js +3 -0
- package/build/angular/components/image/index.js.map +1 -0
- package/build/angular/components/image/shared.d.ts +101 -0
- package/build/angular/components/image/shared.js +406 -0
- package/build/angular/components/image/shared.js.map +1 -0
- package/build/angular/components/image-background.d.ts +81 -0
- package/build/angular/components/image-background.js +418 -0
- package/build/angular/components/image-background.js.map +1 -0
- package/build/angular/components/input-accessory-view/index.d.ts +60 -0
- package/build/angular/components/input-accessory-view/index.js +251 -0
- package/build/angular/components/input-accessory-view/index.js.map +1 -0
- package/build/angular/components/keyboard-avoiding-view/index.d.ts +82 -0
- package/build/angular/components/keyboard-avoiding-view/index.js +358 -0
- package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -0
- package/build/angular/components/modal/index.d.ts +92 -0
- package/build/angular/components/modal/index.js +360 -0
- package/build/angular/components/modal/index.js.map +1 -0
- package/build/angular/components/pressable/index.d.ts +123 -0
- package/build/angular/components/pressable/index.js +420 -0
- package/build/angular/components/pressable/index.js.map +1 -0
- package/build/angular/components/refresh-control/index.d.ts +86 -0
- package/build/angular/components/refresh-control/index.js +342 -0
- package/build/angular/components/refresh-control/index.js.map +1 -0
- package/build/angular/components/safe-area-view/index.d.ts +57 -0
- package/build/angular/components/safe-area-view/index.js +248 -0
- package/build/angular/components/safe-area-view/index.js.map +1 -0
- package/build/angular/components/scroll-view/index.android.d.ts +21 -0
- package/build/angular/components/scroll-view/index.android.js +235 -0
- package/build/angular/components/scroll-view/index.android.js.map +1 -0
- package/build/angular/components/scroll-view/index.d.ts +3 -0
- package/build/angular/components/scroll-view/index.ios.d.ts +9 -0
- package/build/angular/components/scroll-view/index.ios.js +86 -0
- package/build/angular/components/scroll-view/index.ios.js.map +1 -0
- package/build/angular/components/scroll-view/index.js +6 -0
- package/build/angular/components/scroll-view/index.js.map +1 -0
- package/build/angular/components/scroll-view/projection.d.ts +32 -0
- package/build/angular/components/scroll-view/projection.js +334 -0
- package/build/angular/components/scroll-view/projection.js.map +1 -0
- package/build/angular/components/scroll-view/shared.d.ts +230 -0
- package/build/angular/components/scroll-view/shared.js +682 -0
- package/build/angular/components/scroll-view/shared.js.map +1 -0
- package/build/angular/components/scroll-view/sticky-header.d.ts +40 -0
- package/build/angular/components/scroll-view/sticky-header.js +225 -0
- package/build/angular/components/scroll-view/sticky-header.js.map +1 -0
- package/build/angular/components/section-list/index.d.ts +116 -0
- package/build/angular/components/section-list/index.js +641 -0
- package/build/angular/components/section-list/index.js.map +1 -0
- package/build/angular/components/switch/index.android.d.ts +20 -0
- package/build/angular/components/switch/index.android.js +70 -0
- package/build/angular/components/switch/index.android.js.map +1 -0
- package/build/angular/components/switch/index.d.ts +1 -0
- package/build/angular/components/switch/index.ios.d.ts +19 -0
- package/build/angular/components/switch/index.ios.js +69 -0
- package/build/angular/components/switch/index.ios.js.map +1 -0
- package/build/angular/components/switch/index.js +4 -0
- package/build/angular/components/switch/index.js.map +1 -0
- package/build/angular/components/switch/shared.d.ts +40 -0
- package/build/angular/components/switch/shared.js +127 -0
- package/build/angular/components/switch/shared.js.map +1 -0
- package/build/angular/components/text-input.d.ts +153 -0
- package/build/angular/components/text-input.js +599 -0
- package/build/angular/components/text-input.js.map +1 -0
- package/build/angular/components/touchable/index.d.ts +245 -0
- package/build/angular/components/touchable/index.js +1186 -0
- package/build/angular/components/touchable/index.js.map +1 -0
- package/build/angular/components/touchable-native-feedback/index.d.ts +83 -0
- package/build/angular/components/touchable-native-feedback/index.js +371 -0
- package/build/angular/components/touchable-native-feedback/index.js.map +1 -0
- package/build/angular/components/virtualized-list/directives.d.ts +54 -0
- package/build/angular/components/virtualized-list/directives.js +153 -0
- package/build/angular/components/virtualized-list/directives.js.map +1 -0
- package/build/angular/components/virtualized-list/index.d.ts +253 -0
- package/build/angular/components/virtualized-list/index.js +1031 -0
- package/build/angular/components/virtualized-list/index.js.map +1 -0
- package/build/angular/components/virtualized-section-list/directives.d.ts +37 -0
- package/build/angular/components/virtualized-section-list/directives.js +76 -0
- package/build/angular/components/virtualized-section-list/directives.js.map +1 -0
- package/build/angular/components/virtualized-section-list/index.d.ts +178 -0
- package/build/angular/components/virtualized-section-list/index.js +720 -0
- package/build/angular/components/virtualized-section-list/index.js.map +1 -0
- package/build/angular/components.d.ts +39 -0
- package/build/angular/components.js +38 -0
- package/build/angular/components.js.map +1 -0
- package/build/angular/create-portal/index.d.ts +22 -0
- package/build/angular/create-portal/index.js +74 -0
- package/build/angular/create-portal/index.js.map +1 -0
- package/build/angular/create-tunnel/index.d.ts +34 -0
- package/build/angular/create-tunnel/index.js +126 -0
- package/build/angular/create-tunnel/index.js.map +1 -0
- package/build/angular/descriptor-to-angular/index.d.ts +22 -0
- package/build/angular/descriptor-to-angular/index.js +142 -0
- package/build/angular/descriptor-to-angular/index.js.map +1 -0
- package/build/angular/host-instance/index.d.ts +2 -0
- package/build/angular/host-instance/index.js +37 -0
- package/build/angular/host-instance/index.js.map +1 -0
- package/build/angular/index.d.ts +22 -0
- package/build/angular/index.js +46 -0
- package/build/angular/index.js.map +1 -0
- package/build/angular/modules/animated/animated-leaf-binder/index.d.ts +14 -0
- package/build/angular/modules/animated/animated-leaf-binder/index.js +91 -0
- package/build/angular/modules/animated/animated-leaf-binder/index.js.map +1 -0
- package/build/angular/modules/animated/animated-leaf-binder.d.ts +14 -0
- package/build/angular/modules/animated/animated-leaf-binder.js +91 -0
- package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -0
- package/build/angular/modules/animated/create-animated-component.d.ts +59 -0
- package/build/angular/modules/animated/create-animated-component.js +364 -0
- package/build/angular/modules/animated/create-animated-component.js.map +1 -0
- package/build/angular/modules/animated/index.d.ts +51 -0
- package/build/angular/modules/animated/index.js +59 -0
- package/build/angular/modules/animated/index.js.map +1 -0
- package/build/angular/modules/app-registry/index.d.ts +7 -0
- package/build/angular/modules/app-registry/index.js +23 -0
- package/build/angular/modules/app-registry/index.js.map +1 -0
- package/build/angular/modules/status-bar/index.d.ts +20 -0
- package/build/angular/modules/status-bar/index.js +59 -0
- package/build/angular/modules/status-bar/index.js.map +1 -0
- package/build/angular/primitives/index.d.ts +63 -0
- package/build/angular/primitives/index.js +189 -0
- package/build/angular/primitives/index.js.map +1 -0
- package/build/angular/primitives/shared.d.ts +88 -0
- package/build/angular/primitives/shared.js +171 -0
- package/build/angular/primitives/shared.js.map +1 -0
- package/build/angular/render/index.d.ts +11 -0
- package/build/angular/render/index.js +171 -0
- package/build/angular/render/index.js.map +1 -0
- package/build/angular/renderer/index.d.ts +37 -0
- package/build/angular/renderer/index.js +301 -0
- package/build/angular/renderer/index.js.map +1 -0
- package/build/angular/services/color-scheme.service.d.ts +16 -0
- package/build/angular/services/color-scheme.service.js +31 -0
- package/build/angular/services/color-scheme.service.js.map +1 -0
- package/build/angular/services/index.d.ts +2 -0
- package/build/angular/services/index.js +3 -0
- package/build/angular/services/index.js.map +1 -0
- package/build/angular/services/window-dimensions.service.d.ts +16 -0
- package/build/angular/services/window-dimensions.service.js +42 -0
- package/build/angular/services/window-dimensions.service.js.map +1 -0
- package/metro-config.cjs +48 -0
- package/metro-css-parser.cjs +13 -0
- package/package.json +80 -0
- package/src/__tests__/responder-change-detection.test.ts +102 -0
- package/src/__tests__/responder-nested-cd.test.ts +113 -0
- package/src/anchor-host-registry.ts +88 -0
- package/src/angular-gaps.test.ts +142 -0
- package/src/angular-imports.test.ts +100 -0
- package/src/bootstrap.ts +26 -0
- package/src/components/activity-indicator/activity-indicator-class-style.test.ts +44 -0
- package/src/components/activity-indicator/activity-indicator.test.ts +131 -0
- package/src/components/activity-indicator/index.android.ts +16 -0
- package/src/components/activity-indicator/index.ios.ts +17 -0
- package/src/components/activity-indicator/index.ts +4 -0
- package/src/components/activity-indicator/shared.ts +206 -0
- package/src/components/button.ts +193 -0
- package/src/components/flat-list/column-wrapper-style-class.test.ts +116 -0
- package/src/components/flat-list/flat-list-array-style.test.ts +131 -0
- package/src/components/flat-list/flat-list-class-style.test.ts +74 -0
- package/src/components/flat-list/flat-list-scroll-containment.test.ts +107 -0
- package/src/components/flat-list/flat-list.test.ts +99 -0
- package/src/components/flat-list/index.ts +574 -0
- package/src/components/image/image-class-style.test.ts +46 -0
- package/src/components/image/index.android.ts +48 -0
- package/src/components/image/index.ios.ts +48 -0
- package/src/components/image/index.ts +3 -0
- package/src/components/image/shared.ts +479 -0
- package/src/components/image-background-class-style.test.ts +48 -0
- package/src/components/image-background-style-class.test.ts +79 -0
- package/src/components/image-background.ts +270 -0
- package/src/components/input-accessory-view/index.ts +185 -0
- package/src/components/input-accessory-view/input-accessory-view.test.ts +43 -0
- package/src/components/keyboard-avoiding-view/index.ts +303 -0
- package/src/components/keyboard-avoiding-view/keyboard-avoiding-view.test.ts +54 -0
- package/src/components/modal/index.ts +299 -0
- package/src/components/modal/modal.test.ts +43 -0
- package/src/components/pressable/index.ts +373 -0
- package/src/components/pressable/pressable.test.ts +85 -0
- package/src/components/refresh-control/index.ts +284 -0
- package/src/components/refresh-control/refresh-control.test.ts +41 -0
- package/src/components/safe-area-view/index.ts +177 -0
- package/src/components/safe-area-view/safe-area-view.test.ts +43 -0
- package/src/components/scroll-view/android-scroll-view-axis-projection.test.ts +123 -0
- package/src/components/scroll-view/content-container-style-class.test.ts +75 -0
- package/src/components/scroll-view/index.android.ts +190 -0
- package/src/components/scroll-view/index.ios.ts +79 -0
- package/src/components/scroll-view/index.ts +7 -0
- package/src/components/scroll-view/projection.ts +414 -0
- package/src/components/scroll-view/scroll-view-class-style.test.ts +134 -0
- package/src/components/scroll-view/scroll-view-projection.test.ts +194 -0
- package/src/components/scroll-view/shared.ts +844 -0
- package/src/components/scroll-view/sticky-header.ts +255 -0
- package/src/components/section-list/index.ts +445 -0
- package/src/components/switch/android-switch-class.test.ts +42 -0
- package/src/components/switch/index.android.ts +61 -0
- package/src/components/switch/index.ios.ts +60 -0
- package/src/components/switch/index.ts +4 -0
- package/src/components/switch/shared.ts +197 -0
- package/src/components/switch/switch.test.ts +92 -0
- package/src/components/text-input-class-style.test.ts +54 -0
- package/src/components/text-input.ts +531 -0
- package/src/components/touchable/index.ts +629 -0
- package/src/components/touchable/touchable.test.ts +102 -0
- package/src/components/touchable-native-feedback/index.ts +254 -0
- package/src/components/touchable-native-feedback/touchable-native-feedback.test.ts +49 -0
- package/src/components/virtualized-list/directives.ts +168 -0
- package/src/components/virtualized-list/index.ts +992 -0
- package/src/components/virtualized-list/virtualized-list-class-style.test.ts +76 -0
- package/src/components/virtualized-section-list/directives.ts +90 -0
- package/src/components/virtualized-section-list/index.ts +601 -0
- package/src/components/virtualized-section-list/virtualized-section-list-class-style.test.ts +60 -0
- package/src/components.ts +149 -0
- package/src/create-portal/create-portal.test.ts +90 -0
- package/src/create-portal/index.ts +74 -0
- package/src/create-tunnel/create-tunnel.test.ts +120 -0
- package/src/create-tunnel/index.ts +136 -0
- package/src/descriptor-to-angular/descriptor-outlet.test.ts +94 -0
- package/src/descriptor-to-angular/index.ts +190 -0
- package/src/host-instance/host-instance.test.ts +49 -0
- package/src/host-instance/index.ts +36 -0
- package/src/index.ts +261 -0
- package/src/modules/animated/animated-leaf-binder/animated-leaf-binder.test.ts +116 -0
- package/src/modules/animated/animated-leaf-binder/index.ts +101 -0
- package/src/modules/animated/animated-leaf-binder.test.ts +116 -0
- package/src/modules/animated/animated-leaf-binder.ts +101 -0
- package/src/modules/animated/animated-scroll-view.test.ts +102 -0
- package/src/modules/animated/create-animated-component.ts +365 -0
- package/src/modules/animated/index.ts +105 -0
- package/src/modules/app-registry/app-registry.test.ts +106 -0
- package/src/modules/app-registry/index.ts +60 -0
- package/src/modules/status-bar/index.ts +57 -0
- package/src/modules/status-bar/status-bar.test.ts +50 -0
- package/src/primitives/index.ts +119 -0
- package/src/primitives/shared.ts +190 -0
- package/src/render/index.ts +235 -0
- package/src/render/render.test.ts +147 -0
- package/src/renderer/index.ts +347 -0
- package/src/renderer/renderer.test.ts +203 -0
- package/src/services/color-scheme.service.ts +27 -0
- package/src/services/index.ts +2 -0
- package/src/services/services.test.ts +163 -0
- package/src/services/window-dimensions.service.ts +41 -0
- package/tsconfig.angular.base.json +30 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
// VirtualizedSectionList, the Angular wrapper that flattens sections into one virtualized stream
|
|
2
|
+
// over VirtualizedList. Each section contributes a header row, its item rows, then a footer row
|
|
3
|
+
// (RN counts 2 per section); the flattened tagged sequence is fed to VirtualizedList as one list,
|
|
4
|
+
// so headers/items/footers are all windowed by the same machinery. The flattening, entry keying,
|
|
5
|
+
// separator-item unwrap, and scrollToLocation mapping are shared from @symbiote-native/components; this
|
|
6
|
+
// file wires Angular's lifecycle (typed @Input/@Output surface + the imperative handle + the
|
|
7
|
+
// per-cell-tag template dispatch). The Angular twin of the React/Vue adapters' VirtualizedSectionList.
|
|
8
|
+
//
|
|
9
|
+
// The KEY mechanic — per-cell-tag template dispatch. VirtualizedList renders every cell through ONE
|
|
10
|
+
// `vListItem` template, but a section list has DIFFERENT cell types (section header, item, section
|
|
11
|
+
// footer, between-section separators). So VSL supplies the inner list a SINGLE synthesized
|
|
12
|
+
// `vListItem` template that @switch-es on the flattened entry's tag and stamps the matching app
|
|
13
|
+
// template (captured via @ContentChild on the vSection* directives) through VListOutletDirective.
|
|
14
|
+
// list-level header/footer/empty/item-separator reuse VirtualizedList's own directives, forwarded
|
|
15
|
+
// the same way. Only the cell-AUTHORING shape is framework-specific; the rest of the surface
|
|
16
|
+
// stays shared with React/Vue.
|
|
17
|
+
//
|
|
18
|
+
// Accessibility / aria props are forwarded unchanged to the inner VirtualizedList, which owns the
|
|
19
|
+
// shared aria→accessibility* fold before the props reach the ScrollView host.
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
ChangeDetectionStrategy,
|
|
23
|
+
ChangeDetectorRef,
|
|
24
|
+
Component,
|
|
25
|
+
ContentChild,
|
|
26
|
+
ElementRef,
|
|
27
|
+
EventEmitter,
|
|
28
|
+
Input,
|
|
29
|
+
Output,
|
|
30
|
+
ViewChild,
|
|
31
|
+
inject,
|
|
32
|
+
type DoCheck,
|
|
33
|
+
} from '@angular/core';
|
|
34
|
+
import {
|
|
35
|
+
flattenSections,
|
|
36
|
+
resolveStickySectionHeaders,
|
|
37
|
+
scrollLocationToFlatIndex,
|
|
38
|
+
sectionEntryKey,
|
|
39
|
+
unwrapEntryItem,
|
|
40
|
+
type IAccessibilityProps,
|
|
41
|
+
type IAccessibilityStateValue,
|
|
42
|
+
type IAriaProps,
|
|
43
|
+
type ISection,
|
|
44
|
+
type ISectionEntry,
|
|
45
|
+
type ISeparators,
|
|
46
|
+
type IScrollViewHandle,
|
|
47
|
+
type IVirtualizedSectionListHandle,
|
|
48
|
+
} from '@symbiote-native/components';
|
|
49
|
+
import {
|
|
50
|
+
Platform,
|
|
51
|
+
dlog,
|
|
52
|
+
type IStyleProp,
|
|
53
|
+
type ISymbioteEvent,
|
|
54
|
+
type ISymbioteNode,
|
|
55
|
+
type IViewStyle,
|
|
56
|
+
} from '@symbiote-native/engine';
|
|
57
|
+
import {
|
|
58
|
+
VirtualizedList,
|
|
59
|
+
VListEmptyDirective,
|
|
60
|
+
VListFooterDirective,
|
|
61
|
+
VListHeaderDirective,
|
|
62
|
+
VListItemDirective,
|
|
63
|
+
VListSeparatorDirective,
|
|
64
|
+
type IVListSeparatorContext,
|
|
65
|
+
} from '../virtualized-list';
|
|
66
|
+
import { VListOutletDirective } from '../virtualized-list/directives';
|
|
67
|
+
import { stableAnchorStyle } from '../../primitives';
|
|
68
|
+
import {
|
|
69
|
+
VSectionFooterDirective,
|
|
70
|
+
VSectionHeaderDirective,
|
|
71
|
+
VSectionItemDirective,
|
|
72
|
+
VSectionSeparatorDirective,
|
|
73
|
+
type IVSectionContext,
|
|
74
|
+
type IVSectionItemContext,
|
|
75
|
+
} from './directives';
|
|
76
|
+
|
|
77
|
+
export type { ISection } from '@symbiote-native/components';
|
|
78
|
+
// Re-export the shared handle type so section-list imports it from '../virtualized-section-list'.
|
|
79
|
+
export type { IVirtualizedSectionListHandle } from '@symbiote-native/components';
|
|
80
|
+
// Re-export the section authoring directives + their contexts so flat consumers (and SectionList)
|
|
81
|
+
// import them from '../virtualized-section-list', mirroring how VirtualizedList re-exports its own.
|
|
82
|
+
export {
|
|
83
|
+
VSectionFooterDirective,
|
|
84
|
+
VSectionHeaderDirective,
|
|
85
|
+
VSectionItemDirective,
|
|
86
|
+
VSectionSeparatorDirective,
|
|
87
|
+
} from './directives';
|
|
88
|
+
export type { IVSectionContext, IVSectionItemContext } from './directives';
|
|
89
|
+
|
|
90
|
+
// The Angular VirtualizedSectionList prop surface. Mirrors React/Vue's IVirtualizedSectionListProps
|
|
91
|
+
// MINUS the element-returning props (renderItem, renderSectionHeader/Footer, the *Component slots):
|
|
92
|
+
// those are the per-adapter children/render fields and become `<ng-template>` directives in Angular.
|
|
93
|
+
// Everything agnostic is the SAME surface as the
|
|
94
|
+
// React/Vue adapters, including the a11y/aria prop family forwarded through VirtualizedList.
|
|
95
|
+
export interface IVirtualizedSectionListProps<ItemT> extends IAccessibilityProps, IAriaProps {
|
|
96
|
+
sections: ReadonlyArray<ISection<ItemT>>;
|
|
97
|
+
keyExtractor?: (item: ItemT, index: number) => string;
|
|
98
|
+
// Stick each section header to the top as the next section scrolls up. Routed to the inner
|
|
99
|
+
// VirtualizedList's stickyHeaderIndices. Defaults to `Platform.OS === 'ios'`; Android does not
|
|
100
|
+
// stick by default. Pass true/false to override.
|
|
101
|
+
stickySectionHeadersEnabled?: boolean;
|
|
102
|
+
extraData?: unknown;
|
|
103
|
+
onEndReached?: (info: { distanceFromEnd: number }) => void;
|
|
104
|
+
onEndReachedThreshold?: number;
|
|
105
|
+
onStartReached?: (info: { distanceFromStart: number }) => void;
|
|
106
|
+
onStartReachedThreshold?: number;
|
|
107
|
+
onRefresh?: () => void;
|
|
108
|
+
refreshing?: boolean | null;
|
|
109
|
+
progressViewOffset?: number;
|
|
110
|
+
initialNumToRender?: number;
|
|
111
|
+
initialScrollIndex?: number;
|
|
112
|
+
maxToRenderPerBatch?: number;
|
|
113
|
+
updateCellsBatchingPeriod?: number;
|
|
114
|
+
windowSize?: number;
|
|
115
|
+
inverted?: boolean;
|
|
116
|
+
maintainVisibleContentPosition?: {
|
|
117
|
+
minIndexForVisible: number;
|
|
118
|
+
autoscrollToTopThreshold?: number;
|
|
119
|
+
};
|
|
120
|
+
onScroll?: (event: ISymbioteEvent) => void;
|
|
121
|
+
onScrollBeginDrag?: (event: ISymbioteEvent) => void;
|
|
122
|
+
onScrollEndDrag?: (event: ISymbioteEvent) => void;
|
|
123
|
+
onMomentumScrollBegin?: (event: ISymbioteEvent) => void;
|
|
124
|
+
onMomentumScrollEnd?: (event: ISymbioteEvent) => void;
|
|
125
|
+
scrollEventThrottle?: number;
|
|
126
|
+
keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
|
|
127
|
+
keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
|
|
128
|
+
style?: IStyleProp<IViewStyle>;
|
|
129
|
+
contentContainerStyle?: IStyleProp<IViewStyle>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// What the VirtualizedSectionList component itself takes as plain @Input()s: the full surface
|
|
133
|
+
// minus the list-lifecycle events, which it exposes as real @Output() EventEmitters instead (see
|
|
134
|
+
// the class below) — mirrors IAngularPressableInputs in pressable/index.ts.
|
|
135
|
+
export type IVirtualizedSectionListInputs<ItemT> = Omit<
|
|
136
|
+
IVirtualizedSectionListProps<ItemT>,
|
|
137
|
+
| 'onEndReached'
|
|
138
|
+
| 'onStartReached'
|
|
139
|
+
| 'onRefresh'
|
|
140
|
+
| 'onAccessibilityAction'
|
|
141
|
+
| 'onAccessibilityTap'
|
|
142
|
+
| 'onMagicTap'
|
|
143
|
+
| 'onAccessibilityEscape'
|
|
144
|
+
>;
|
|
145
|
+
|
|
146
|
+
@Component({
|
|
147
|
+
selector: 'VirtualizedSectionList',
|
|
148
|
+
standalone: true,
|
|
149
|
+
imports: [
|
|
150
|
+
VirtualizedList,
|
|
151
|
+
VListItemDirective,
|
|
152
|
+
VListHeaderDirective,
|
|
153
|
+
VListFooterDirective,
|
|
154
|
+
VListEmptyDirective,
|
|
155
|
+
VListSeparatorDirective,
|
|
156
|
+
VListOutletDirective,
|
|
157
|
+
],
|
|
158
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
159
|
+
template: `
|
|
160
|
+
<VirtualizedList
|
|
161
|
+
[data]="flatEntries"
|
|
162
|
+
[getItem]="getEntry"
|
|
163
|
+
[getItemCount]="getEntryCount"
|
|
164
|
+
[keyExtractor]="entryKeyExtractor"
|
|
165
|
+
[stickyHeaderIndices]="stickyHeaderIndices"
|
|
166
|
+
[extraData]="extraData"
|
|
167
|
+
[inverted]="inverted"
|
|
168
|
+
[refreshing]="refreshing"
|
|
169
|
+
[progressViewOffset]="progressViewOffset"
|
|
170
|
+
[refreshRequested]="refreshRequested ?? refresh.observed"
|
|
171
|
+
(refresh)="resolvedOnRefresh?.()"
|
|
172
|
+
(endReached)="resolvedOnEndReached?.($event)"
|
|
173
|
+
[onEndReachedThreshold]="onEndReachedThreshold"
|
|
174
|
+
(startReached)="resolvedOnStartReached?.($event)"
|
|
175
|
+
[onStartReachedThreshold]="onStartReachedThreshold"
|
|
176
|
+
[initialNumToRender]="initialNumToRender"
|
|
177
|
+
[initialScrollIndex]="initialScrollIndex"
|
|
178
|
+
[maxToRenderPerBatch]="maxToRenderPerBatch"
|
|
179
|
+
[updateCellsBatchingPeriod]="updateCellsBatchingPeriod"
|
|
180
|
+
[windowSize]="windowSize"
|
|
181
|
+
[maintainVisibleContentPosition]="maintainVisibleContentPosition"
|
|
182
|
+
[onScroll]="onScroll"
|
|
183
|
+
[onScrollBeginDrag]="onScrollBeginDrag"
|
|
184
|
+
[onScrollEndDrag]="onScrollEndDrag"
|
|
185
|
+
[onMomentumScrollBegin]="onMomentumScrollBegin"
|
|
186
|
+
[onMomentumScrollEnd]="onMomentumScrollEnd"
|
|
187
|
+
[scrollEventThrottle]="scrollEventThrottle"
|
|
188
|
+
[keyboardShouldPersistTaps]="keyboardShouldPersistTaps"
|
|
189
|
+
[keyboardDismissMode]="keyboardDismissMode"
|
|
190
|
+
[style]="resolvedStyle"
|
|
191
|
+
[contentContainerStyle]="contentContainerStyle"
|
|
192
|
+
[testID]="testID"
|
|
193
|
+
[nativeID]="nativeID"
|
|
194
|
+
[accessible]="accessible"
|
|
195
|
+
[accessibilityLabel]="accessibilityLabel"
|
|
196
|
+
[accessibilityHint]="accessibilityHint"
|
|
197
|
+
[accessibilityRole]="accessibilityRole"
|
|
198
|
+
[accessibilityState]="accessibilityState"
|
|
199
|
+
[accessibilityValue]="accessibilityValue"
|
|
200
|
+
[accessibilityActions]="accessibilityActions"
|
|
201
|
+
[accessibilityLabelledBy]="accessibilityLabelledBy"
|
|
202
|
+
[importantForAccessibility]="importantForAccessibility"
|
|
203
|
+
[accessibilityLiveRegion]="accessibilityLiveRegion"
|
|
204
|
+
[screenReaderFocusable]="screenReaderFocusable"
|
|
205
|
+
[accessibilityViewIsModal]="accessibilityViewIsModal"
|
|
206
|
+
[accessibilityElementsHidden]="accessibilityElementsHidden"
|
|
207
|
+
[accessibilityIgnoresInvertColors]="accessibilityIgnoresInvertColors"
|
|
208
|
+
[accessibilityLanguage]="accessibilityLanguage"
|
|
209
|
+
[accessibilityRespondsToUserInteraction]="accessibilityRespondsToUserInteraction"
|
|
210
|
+
[accessibilityShowsLargeContentViewer]="accessibilityShowsLargeContentViewer"
|
|
211
|
+
[accessibilityLargeContentTitle]="accessibilityLargeContentTitle"
|
|
212
|
+
(accessibilityAction)="resolvedOnAccessibilityAction?.($event)"
|
|
213
|
+
(accessibilityTap)="resolvedOnAccessibilityTap?.($event)"
|
|
214
|
+
(magicTap)="resolvedOnMagicTap?.($event)"
|
|
215
|
+
(accessibilityEscape)="resolvedOnAccessibilityEscape?.($event)"
|
|
216
|
+
[role]="role"
|
|
217
|
+
[ariaLabel]="ariaLabel"
|
|
218
|
+
[ariaLabelledBy]="ariaLabelledBy"
|
|
219
|
+
[ariaLive]="ariaLive"
|
|
220
|
+
[ariaHidden]="ariaHidden"
|
|
221
|
+
[ariaBusy]="ariaBusy"
|
|
222
|
+
[ariaChecked]="ariaChecked"
|
|
223
|
+
[ariaDisabled]="ariaDisabled"
|
|
224
|
+
[ariaExpanded]="ariaExpanded"
|
|
225
|
+
[ariaSelected]="ariaSelected"
|
|
226
|
+
[ariaModal]="ariaModal"
|
|
227
|
+
[ariaValueMax]="ariaValueMax"
|
|
228
|
+
[ariaValueMin]="ariaValueMin"
|
|
229
|
+
[ariaValueNow]="ariaValueNow"
|
|
230
|
+
[ariaValueText]="ariaValueText"
|
|
231
|
+
>
|
|
232
|
+
<!-- The single synthesized cell template: dispatch per flattened entry tag. -->
|
|
233
|
+
<ng-template vListItem let-entry let-separators="separators">
|
|
234
|
+
@switch (entryKind(entry)) {
|
|
235
|
+
@case ('header') {
|
|
236
|
+
<ng-container
|
|
237
|
+
[vListOutlet]="sectionHeaderDir?.templateRef"
|
|
238
|
+
[vListOutletContext]="sectionContextOf(entry)"
|
|
239
|
+
></ng-container>
|
|
240
|
+
}
|
|
241
|
+
@case ('footer') {
|
|
242
|
+
<ng-container
|
|
243
|
+
[vListOutlet]="sectionFooterDir?.templateRef"
|
|
244
|
+
[vListOutletContext]="sectionContextOf(entry)"
|
|
245
|
+
></ng-container>
|
|
246
|
+
}
|
|
247
|
+
@case ('section-separator') {
|
|
248
|
+
<ng-container [vListOutlet]="sectionSeparatorDir?.templateRef"></ng-container>
|
|
249
|
+
}
|
|
250
|
+
@case ('item') {
|
|
251
|
+
<ng-container
|
|
252
|
+
[vListOutlet]="sectionItemDir?.templateRef"
|
|
253
|
+
[vListOutletContext]="itemContextOf(entry, separators)"
|
|
254
|
+
></ng-container>
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
</ng-template>
|
|
258
|
+
|
|
259
|
+
<!-- list-level slots: forward the app's VirtualizedList directives to the inner list. -->
|
|
260
|
+
@if (listHeaderDir !== undefined) {
|
|
261
|
+
<ng-template vListHeader>
|
|
262
|
+
<ng-container [vListOutlet]="listHeaderDir.templateRef"></ng-container>
|
|
263
|
+
</ng-template>
|
|
264
|
+
}
|
|
265
|
+
@if (listFooterDir !== undefined) {
|
|
266
|
+
<ng-template vListFooter>
|
|
267
|
+
<ng-container [vListOutlet]="listFooterDir.templateRef"></ng-container>
|
|
268
|
+
</ng-template>
|
|
269
|
+
}
|
|
270
|
+
@if (listEmptyDir !== undefined) {
|
|
271
|
+
<ng-template vListEmpty>
|
|
272
|
+
<ng-container [vListOutlet]="listEmptyDir.templateRef"></ng-container>
|
|
273
|
+
</ng-template>
|
|
274
|
+
}
|
|
275
|
+
<!-- item separator: forward, unwrapping each flattened entry back to its ItemT. -->
|
|
276
|
+
@if (itemSeparatorDir !== undefined) {
|
|
277
|
+
<ng-template
|
|
278
|
+
vListSeparator
|
|
279
|
+
let-highlighted
|
|
280
|
+
let-leadingItem="leadingItem"
|
|
281
|
+
let-trailingItem="trailingItem"
|
|
282
|
+
>
|
|
283
|
+
<ng-container
|
|
284
|
+
[vListOutlet]="itemSeparatorDir.templateRef"
|
|
285
|
+
[vListOutletContext]="itemSeparatorContextOf(highlighted, leadingItem, trailingItem)"
|
|
286
|
+
></ng-container>
|
|
287
|
+
</ng-template>
|
|
288
|
+
}
|
|
289
|
+
</VirtualizedList>
|
|
290
|
+
`,
|
|
291
|
+
})
|
|
292
|
+
export class VirtualizedSectionList<ItemT = unknown>
|
|
293
|
+
implements IVirtualizedSectionListInputs<ItemT>, IVirtualizedSectionListHandle, DoCheck
|
|
294
|
+
{
|
|
295
|
+
@Input({ required: true }) sections!: ReadonlyArray<ISection<ItemT>>;
|
|
296
|
+
@Input() keyExtractor?: (item: ItemT, index: number) => string;
|
|
297
|
+
@Input() stickySectionHeadersEnabled?: boolean;
|
|
298
|
+
@Input() extraData?: unknown;
|
|
299
|
+
@Output() readonly endReached = new EventEmitter<{ distanceFromEnd: number }>();
|
|
300
|
+
@Input() onEndReachedThreshold?: number;
|
|
301
|
+
@Output() readonly startReached = new EventEmitter<{ distanceFromStart: number }>();
|
|
302
|
+
@Input() onStartReachedThreshold?: number;
|
|
303
|
+
@Output() readonly refresh = new EventEmitter<void>();
|
|
304
|
+
@Input() refreshing?: boolean | null;
|
|
305
|
+
@Input() progressViewOffset?: number;
|
|
306
|
+
// See VirtualizedList's own refreshRequested doc comment: `(refresh)="resolvedOnRefresh?.()"`
|
|
307
|
+
// above always subscribes to the inner VirtualizedList's refresh output (to re-forward it),
|
|
308
|
+
// which would otherwise make its `.observed` permanently true. SectionList passes ITS OWN
|
|
309
|
+
// public `refresh.observed` here; direct usage falls back to this component's own `.observed`.
|
|
310
|
+
@Input() refreshRequested?: boolean;
|
|
311
|
+
@Input() initialNumToRender?: number;
|
|
312
|
+
@Input() initialScrollIndex?: number;
|
|
313
|
+
@Input() maxToRenderPerBatch?: number;
|
|
314
|
+
@Input() updateCellsBatchingPeriod?: number;
|
|
315
|
+
@Input() windowSize?: number;
|
|
316
|
+
@Input() inverted?: boolean;
|
|
317
|
+
@Input() maintainVisibleContentPosition?: {
|
|
318
|
+
minIndexForVisible: number;
|
|
319
|
+
autoscrollToTopThreshold?: number;
|
|
320
|
+
};
|
|
321
|
+
@Input() onScroll?: (event: ISymbioteEvent) => void;
|
|
322
|
+
@Input() onScrollBeginDrag?: (event: ISymbioteEvent) => void;
|
|
323
|
+
@Input() onScrollEndDrag?: (event: ISymbioteEvent) => void;
|
|
324
|
+
@Input() onMomentumScrollBegin?: (event: ISymbioteEvent) => void;
|
|
325
|
+
@Input() onMomentumScrollEnd?: (event: ISymbioteEvent) => void;
|
|
326
|
+
@Input() scrollEventThrottle?: number;
|
|
327
|
+
@Input() keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
|
|
328
|
+
@Input() keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
|
|
329
|
+
@Input() style?: IStyleProp<IViewStyle>;
|
|
330
|
+
@Input() contentContainerStyle?: IStyleProp<IViewStyle>;
|
|
331
|
+
@Input() testID?: string;
|
|
332
|
+
@Input() nativeID?: string;
|
|
333
|
+
|
|
334
|
+
// Bound to the template's `[style]="resolvedStyle"`, which Angular compiles to the built-in
|
|
335
|
+
// ɵɵstyleMap instruction — it only understands a flat object, never an array (RN's own
|
|
336
|
+
// `style={[a, b]}` composition idiom crashes deep inside Angular's styling engine), so this
|
|
337
|
+
// flattens `style` via the engine's own flattenStyle before it ever reaches that binding.
|
|
338
|
+
// anchorHostStyle merges in this component's OWN anchor's class-derived style (see its doc
|
|
339
|
+
// comment) — `elementRef` below is VirtualizedSectionList's OWN host, not `list`'s inner
|
|
340
|
+
// VirtualizedList. A plain `flattenStyle([...])` here would allocate a FRESH object on every
|
|
341
|
+
// getter read, which — bound onto the inner VirtualizedList's own `@Input() style` — defeats
|
|
342
|
+
// its `ngDoCheck` dedup gate and free-runs change detection forever (see stableAnchorStyle's
|
|
343
|
+
// doc comment and the `flat-list-array-style.test.ts` regression it fixes).
|
|
344
|
+
// `cachedResolvedStyle` is the getter's own persisted "previous" value across reads.
|
|
345
|
+
private cachedResolvedStyle: Record<string, unknown> | undefined;
|
|
346
|
+
get resolvedStyle(): IViewStyle {
|
|
347
|
+
this.cachedResolvedStyle = stableAnchorStyle(
|
|
348
|
+
this.elementRef,
|
|
349
|
+
this.style,
|
|
350
|
+
this.cachedResolvedStyle,
|
|
351
|
+
);
|
|
352
|
+
return this.cachedResolvedStyle;
|
|
353
|
+
}
|
|
354
|
+
@Input() accessible?: boolean;
|
|
355
|
+
@Input() accessibilityLabel?: string;
|
|
356
|
+
@Input() accessibilityHint?: string;
|
|
357
|
+
@Input() accessibilityRole?: IAccessibilityProps['accessibilityRole'];
|
|
358
|
+
@Input() accessibilityState?: IAccessibilityStateValue;
|
|
359
|
+
@Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
|
|
360
|
+
@Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
|
|
361
|
+
@Input() accessibilityLabelledBy?: string | string[];
|
|
362
|
+
@Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
|
|
363
|
+
@Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
|
|
364
|
+
@Input() screenReaderFocusable?: boolean;
|
|
365
|
+
@Input() accessibilityViewIsModal?: boolean;
|
|
366
|
+
@Input() accessibilityElementsHidden?: boolean;
|
|
367
|
+
@Input() accessibilityIgnoresInvertColors?: boolean;
|
|
368
|
+
@Input() accessibilityLanguage?: string;
|
|
369
|
+
@Input() accessibilityRespondsToUserInteraction?: boolean;
|
|
370
|
+
@Input() accessibilityShowsLargeContentViewer?: boolean;
|
|
371
|
+
@Input() accessibilityLargeContentTitle?: string;
|
|
372
|
+
@Output() readonly accessibilityAction = new EventEmitter<ISymbioteEvent>();
|
|
373
|
+
@Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
|
|
374
|
+
@Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
|
|
375
|
+
@Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
|
|
376
|
+
@Input() role?: IAriaProps['role'];
|
|
377
|
+
@Input() ariaLabel?: string;
|
|
378
|
+
@Input() ariaLabelledBy?: string;
|
|
379
|
+
@Input() ariaLive?: IAriaProps['aria-live'];
|
|
380
|
+
@Input() ariaHidden?: boolean;
|
|
381
|
+
@Input() ariaBusy?: boolean;
|
|
382
|
+
@Input() ariaChecked?: boolean | 'mixed';
|
|
383
|
+
@Input() ariaDisabled?: boolean;
|
|
384
|
+
@Input() ariaExpanded?: boolean;
|
|
385
|
+
@Input() ariaSelected?: boolean;
|
|
386
|
+
@Input() ariaModal?: boolean;
|
|
387
|
+
@Input() ariaValueMax?: number;
|
|
388
|
+
@Input() ariaValueMin?: number;
|
|
389
|
+
@Input() ariaValueNow?: number;
|
|
390
|
+
@Input() ariaValueText?: string;
|
|
391
|
+
|
|
392
|
+
// The section cell templates the app authors (the Angular twin of renderItem /
|
|
393
|
+
// renderSectionHeader / renderSectionFooter / SectionSeparatorComponent), captured from projected
|
|
394
|
+
// <ng-template> content and stamped by the synthesized vListItem dispatch.
|
|
395
|
+
@ContentChild(VSectionItemDirective) sectionItemDir?: VSectionItemDirective<ItemT>;
|
|
396
|
+
@ContentChild(VSectionHeaderDirective) sectionHeaderDir?: VSectionHeaderDirective<ItemT>;
|
|
397
|
+
@ContentChild(VSectionFooterDirective) sectionFooterDir?: VSectionFooterDirective<ItemT>;
|
|
398
|
+
@ContentChild(VSectionSeparatorDirective) sectionSeparatorDir?: VSectionSeparatorDirective;
|
|
399
|
+
|
|
400
|
+
// list-level slots reuse VirtualizedList's own directives; VSL forwards them to the inner list.
|
|
401
|
+
@ContentChild(VListHeaderDirective) listHeaderDir?: VListHeaderDirective;
|
|
402
|
+
@ContentChild(VListFooterDirective) listFooterDir?: VListFooterDirective;
|
|
403
|
+
@ContentChild(VListEmptyDirective) listEmptyDir?: VListEmptyDirective;
|
|
404
|
+
@ContentChild(VListSeparatorDirective) itemSeparatorDir?: VListSeparatorDirective<ItemT>;
|
|
405
|
+
|
|
406
|
+
// The composed inner VirtualizedList. Its instance IS an IVirtualizedListHandle, so the section
|
|
407
|
+
// handle delegates straight to it. Available from ngAfterViewInit; reads lazily, no-ops pre-commit.
|
|
408
|
+
@ViewChild(VirtualizedList) private list?: VirtualizedList<ISectionEntry<ItemT>>;
|
|
409
|
+
|
|
410
|
+
// This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
|
|
411
|
+
// onto (see anchorHostStyle's doc comment) — NOT `list` above, which targets the real inner
|
|
412
|
+
// `<VirtualizedList>` one level down (itself its own separate anchor host).
|
|
413
|
+
private readonly elementRef = inject(ElementRef);
|
|
414
|
+
|
|
415
|
+
// --- template-bound state, recomputed in ngDoCheck when sections / separator presence change ---
|
|
416
|
+
flatEntries: ISectionEntry<ItemT>[] = [];
|
|
417
|
+
stickyHeaderIndices: number[] | undefined = undefined;
|
|
418
|
+
|
|
419
|
+
// headerIndices are the flat positions of every section header; scrollToLocation maps a
|
|
420
|
+
// (sectionIndex, itemIndex) coordinate through them without re-deriving the flat layout.
|
|
421
|
+
private headerIndices: number[] = [];
|
|
422
|
+
// Memo guards so flattenSections does not re-run on every CD (the inner list marks for check on
|
|
423
|
+
// every scroll tick, which re-runs VSL's ngDoCheck too).
|
|
424
|
+
private lastSectionsRef: ReadonlyArray<ISection<ItemT>> | null = null;
|
|
425
|
+
private lastHasSectionSeparator = false;
|
|
426
|
+
|
|
427
|
+
private readonly cdr = inject(ChangeDetectorRef);
|
|
428
|
+
|
|
429
|
+
ngDoCheck(): void {
|
|
430
|
+
const hasSectionSeparator = this.sectionSeparatorDir !== undefined;
|
|
431
|
+
if (
|
|
432
|
+
this.sections === this.lastSectionsRef &&
|
|
433
|
+
hasSectionSeparator === this.lastHasSectionSeparator
|
|
434
|
+
) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
this.lastSectionsRef = this.sections;
|
|
438
|
+
this.lastHasSectionSeparator = hasSectionSeparator;
|
|
439
|
+
|
|
440
|
+
const sections = this.sections ?? [];
|
|
441
|
+
const { entries, headerIndices } = flattenSections(sections, hasSectionSeparator);
|
|
442
|
+
this.flatEntries = entries;
|
|
443
|
+
this.headerIndices = headerIndices;
|
|
444
|
+
|
|
445
|
+
// RN sticks section headers by default only on iOS; Android does not unless asked.
|
|
446
|
+
this.stickyHeaderIndices = resolveStickySectionHeaders(
|
|
447
|
+
this.stickySectionHeadersEnabled,
|
|
448
|
+
headerIndices,
|
|
449
|
+
Platform.OS,
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
dlog(
|
|
453
|
+
`Angular VirtualizedSectionList: ${sections.length} sections flattened to ${entries.length} entries`,
|
|
454
|
+
);
|
|
455
|
+
this.cdr.markForCheck();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// The derived flat-stream accessors handed to the inner VirtualizedList. Stable arrow fields so
|
|
459
|
+
// the bindings keep a constant identity; they read the live flatEntries.
|
|
460
|
+
getEntry = (_source: unknown, index: number): ISectionEntry<ItemT> => this.flatEntries[index];
|
|
461
|
+
getEntryCount = (): number => this.flatEntries.length;
|
|
462
|
+
entryKeyExtractor = (entry: ISectionEntry<ItemT>, index: number): string =>
|
|
463
|
+
sectionEntryKey(entry, index, this.keyExtractor);
|
|
464
|
+
|
|
465
|
+
// Adapts each @Output() into the plain callback VirtualizedList's own @Input() still wants,
|
|
466
|
+
// gated on `.observed` so an unlistened event forwards `undefined` — the same "nobody cares"
|
|
467
|
+
// contract the old @Input() callback passthrough had (e.g. onRefresh presence gates whether a
|
|
468
|
+
// RefreshControl is built downstream; see the Vue twin's `listens('onRefresh')` gate).
|
|
469
|
+
get resolvedOnEndReached(): ((info: { distanceFromEnd: number }) => void) | undefined {
|
|
470
|
+
return this.endReached.observed ? info => this.endReached.emit(info) : undefined;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
get resolvedOnStartReached(): ((info: { distanceFromStart: number }) => void) | undefined {
|
|
474
|
+
return this.startReached.observed ? info => this.startReached.emit(info) : undefined;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
get resolvedOnRefresh(): (() => void) | undefined {
|
|
478
|
+
return this.refresh.observed ? () => this.refresh.emit() : undefined;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
get resolvedOnAccessibilityAction(): ((event: ISymbioteEvent) => void) | undefined {
|
|
482
|
+
return this.accessibilityAction.observed
|
|
483
|
+
? event => this.accessibilityAction.emit(event)
|
|
484
|
+
: undefined;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
get resolvedOnAccessibilityTap(): ((event: ISymbioteEvent) => void) | undefined {
|
|
488
|
+
return this.accessibilityTap.observed ? event => this.accessibilityTap.emit(event) : undefined;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
get resolvedOnMagicTap(): ((event: ISymbioteEvent) => void) | undefined {
|
|
492
|
+
return this.magicTap.observed ? event => this.magicTap.emit(event) : undefined;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
get resolvedOnAccessibilityEscape(): ((event: ISymbioteEvent) => void) | undefined {
|
|
496
|
+
return this.accessibilityEscape.observed
|
|
497
|
+
? event => this.accessibilityEscape.emit(event)
|
|
498
|
+
: undefined;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// --- per-cell-tag dispatch (the inner vListItem context is typed `unknown` under strictTemplates,
|
|
502
|
+
// since VListItemDirective's generic is not bound here; narrow with a runtime guard, no casts). ---
|
|
503
|
+
|
|
504
|
+
private isEntry(value: unknown): value is ISectionEntry<ItemT> {
|
|
505
|
+
return typeof value === 'object' && value !== null && 'kind' in value;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
entryKind(value: unknown): ISectionEntry<ItemT>['kind'] | 'unknown' {
|
|
509
|
+
return this.isEntry(value) ? value.kind : 'unknown';
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
sectionContextOf(value: unknown): IVSectionContext<ItemT> | undefined {
|
|
513
|
+
if (!this.isEntry(value)) return undefined;
|
|
514
|
+
if (value.kind !== 'header' && value.kind !== 'footer') return undefined;
|
|
515
|
+
return { $implicit: value.section, section: value.section };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
itemContextOf(value: unknown, separators: ISeparators): IVSectionItemContext<ItemT> | undefined {
|
|
519
|
+
if (!this.isEntry(value) || value.kind !== 'item') return undefined;
|
|
520
|
+
return {
|
|
521
|
+
$implicit: value.item,
|
|
522
|
+
item: value.item,
|
|
523
|
+
index: value.itemIndex,
|
|
524
|
+
section: value.section,
|
|
525
|
+
separators,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// The inner list's separator context carries the flattened ENTRY as leading/trailing; unwrap each
|
|
530
|
+
// back to its ItemT (shared unwrapEntryItem) so the app's item separator, typed on ItemT, sees
|
|
531
|
+
// real items (header/footer/section-separator gaps unwrap to undefined). Mirrors the React/Vue
|
|
532
|
+
// entrySeparatorComponent wrap.
|
|
533
|
+
itemSeparatorContextOf(
|
|
534
|
+
highlighted: unknown,
|
|
535
|
+
leadingItem: unknown,
|
|
536
|
+
trailingItem: unknown,
|
|
537
|
+
): IVListSeparatorContext<ItemT> {
|
|
538
|
+
return {
|
|
539
|
+
$implicit: highlighted === true,
|
|
540
|
+
highlighted: highlighted === true,
|
|
541
|
+
leadingItem: this.isEntry(leadingItem) ? unwrapEntryItem(leadingItem) : undefined,
|
|
542
|
+
trailingItem: this.isEntry(trailingItem) ? unwrapEntryItem(trailingItem) : undefined,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ---- imperative handle (the shared IVirtualizedSectionListHandle surface) ----
|
|
547
|
+
|
|
548
|
+
scrollToLocation(params: {
|
|
549
|
+
sectionIndex: number;
|
|
550
|
+
itemIndex: number;
|
|
551
|
+
viewOffset?: number;
|
|
552
|
+
viewPosition?: number;
|
|
553
|
+
animated?: boolean;
|
|
554
|
+
}): void {
|
|
555
|
+
const flatIndex = scrollLocationToFlatIndex(
|
|
556
|
+
this.headerIndices,
|
|
557
|
+
params.sectionIndex,
|
|
558
|
+
params.itemIndex,
|
|
559
|
+
);
|
|
560
|
+
if (flatIndex === undefined) {
|
|
561
|
+
dlog(
|
|
562
|
+
`Angular VirtualizedSectionList scrollToLocation: section ${params.sectionIndex} out of range`,
|
|
563
|
+
);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
dlog(
|
|
567
|
+
`Angular VirtualizedSectionList scrollToLocation section=${params.sectionIndex} ` +
|
|
568
|
+
`item=${params.itemIndex} -> flat ${flatIndex}`,
|
|
569
|
+
);
|
|
570
|
+
this.list?.scrollToIndex({
|
|
571
|
+
index: flatIndex,
|
|
572
|
+
viewOffset: params.viewOffset,
|
|
573
|
+
viewPosition: params.viewPosition,
|
|
574
|
+
animated: params.animated,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
flashScrollIndicators(): void {
|
|
579
|
+
this.list?.flashScrollIndicators();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
getNativeScrollRef(): IScrollViewHandle | null {
|
|
583
|
+
return this.list?.getNativeScrollRef() ?? null;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
getScrollableNode(): IScrollViewHandle | null {
|
|
587
|
+
return this.list?.getScrollableNode() ?? null;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
getScrollResponder(): IScrollViewHandle | null {
|
|
591
|
+
return this.list?.getScrollResponder() ?? null;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
getScrollNode(): ISymbioteNode | null {
|
|
595
|
+
return this.list?.getScrollNode() ?? null;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
recordInteraction(): void {
|
|
599
|
+
this.list?.recordInteraction();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Regression test for the anchor/class bug: VirtualizedSectionList is its
|
|
2
|
+
// own ANCHOR_HOST_COMPONENTS entry — a class= on <VirtualizedSectionList> resolves onto its OWN
|
|
3
|
+
// anchor and needs its OWN anchorHostStyle merge (see index.ts's resolvedStyle getter), it is NOT
|
|
4
|
+
// transitively fixed by VirtualizedList's/ScrollView's own fixes further down. Mirrors
|
|
5
|
+
// pressable.test.ts's "resolves a class=" case.
|
|
6
|
+
import '@angular/compiler';
|
|
7
|
+
import { Component } from '@angular/core';
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
9
|
+
import { clearGlobalStyles, registerStyles } from '@symbiote-native/engine';
|
|
10
|
+
import { installFabric } from '@symbiote-native/test-utils';
|
|
11
|
+
import type { ISection } from '@symbiote-native/components';
|
|
12
|
+
|
|
13
|
+
import { mount, unmount } from '../../render';
|
|
14
|
+
import { VirtualizedSectionList, VSectionItemDirective } from './index';
|
|
15
|
+
|
|
16
|
+
const ROOT_TAG = 954;
|
|
17
|
+
const fabric = installFabric();
|
|
18
|
+
const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
|
|
19
|
+
|
|
20
|
+
interface IRow {
|
|
21
|
+
id: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const sections: ISection<IRow>[] = [{ title: 'A', data: [{ id: 'a1' }, { id: 'a2' }] }];
|
|
25
|
+
|
|
26
|
+
@Component({
|
|
27
|
+
selector: 'symbiote-virtualized-section-list-class-host',
|
|
28
|
+
standalone: true,
|
|
29
|
+
imports: [VirtualizedSectionList, VSectionItemDirective],
|
|
30
|
+
template: `
|
|
31
|
+
<VirtualizedSectionList class="card" [sections]="sections">
|
|
32
|
+
<ng-template vSectionItem let-item>
|
|
33
|
+
<symbiote-text [testID]="item.id">{{ item.id }}</symbiote-text>
|
|
34
|
+
</ng-template>
|
|
35
|
+
</VirtualizedSectionList>
|
|
36
|
+
`,
|
|
37
|
+
})
|
|
38
|
+
class VirtualizedSectionListClassHost {
|
|
39
|
+
sections = sections;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
fabric.reset();
|
|
44
|
+
registerStyles({ card: { backgroundColor: 'red' } });
|
|
45
|
+
});
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
unmount(ROOT_TAG);
|
|
48
|
+
clearGlobalStyles();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('VirtualizedSectionList anchor class= resolution', () => {
|
|
52
|
+
it('resolves a class= on the VirtualizedSectionList use site onto the real committed scroll host', async () => {
|
|
53
|
+
mount(ROOT_TAG, VirtualizedSectionListClassHost);
|
|
54
|
+
await tick();
|
|
55
|
+
await tick();
|
|
56
|
+
|
|
57
|
+
const node = fabric.find(n => n.props.backgroundColor === 'red');
|
|
58
|
+
expect(node, 'a real Fabric node carries the class-derived style').toBeDefined();
|
|
59
|
+
});
|
|
60
|
+
});
|