@symbiote-native/angular 0.6.1 → 0.7.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 +1 -1
- package/bin/symbiote-angular-dev.cjs +20 -19
- package/build/angular/anchor-host-registry.js +2 -2
- package/build/angular/anchor-host-registry.js.map +1 -1
- package/build/angular/components/activity-indicator/index.android.d.ts +2 -1
- package/build/angular/components/activity-indicator/index.android.js +6 -3
- package/build/angular/components/activity-indicator/index.android.js.map +1 -1
- package/build/angular/components/activity-indicator/index.ios.d.ts +2 -1
- package/build/angular/components/activity-indicator/index.ios.js +6 -3
- package/build/angular/components/activity-indicator/index.ios.js.map +1 -1
- package/build/angular/components/activity-indicator/shared.js +3 -3
- package/build/angular/components/button.d.ts +4 -2
- package/build/angular/components/button.js +28 -17
- package/build/angular/components/button.js.map +1 -1
- package/build/angular/components/flat-list/index.d.ts +5 -3
- package/build/angular/components/flat-list/index.js +35 -38
- package/build/angular/components/flat-list/index.js.map +1 -1
- package/build/angular/components/image/index.android.d.ts +8 -3
- package/build/angular/components/image/index.android.js +39 -9
- package/build/angular/components/image/index.android.js.map +1 -1
- package/build/angular/components/image/index.ios.d.ts +8 -3
- package/build/angular/components/image/index.ios.js +39 -9
- package/build/angular/components/image/index.ios.js.map +1 -1
- package/build/angular/components/image/shared.d.ts +4 -1
- package/build/angular/components/image/shared.js +33 -2
- package/build/angular/components/image/shared.js.map +1 -1
- package/build/angular/components/image-background.d.ts +2 -1
- package/build/angular/components/image-background.js +6 -4
- package/build/angular/components/image-background.js.map +1 -1
- package/build/angular/components/input-accessory-view/index.d.ts +9 -4
- package/build/angular/components/input-accessory-view/index.js +43 -9
- package/build/angular/components/input-accessory-view/index.js.map +1 -1
- package/build/angular/components/keyboard-avoiding-view/index.d.ts +2 -1
- package/build/angular/components/keyboard-avoiding-view/index.js +6 -4
- package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
- package/build/angular/components/modal/index.d.ts +11 -8
- package/build/angular/components/modal/index.js +45 -28
- package/build/angular/components/modal/index.js.map +1 -1
- package/build/angular/components/pressable/index.d.ts +11 -5
- package/build/angular/components/pressable/index.js +61 -12
- package/build/angular/components/pressable/index.js.map +1 -1
- package/build/angular/components/refresh-control/index.d.ts +8 -4
- package/build/angular/components/refresh-control/index.js +52 -16
- package/build/angular/components/refresh-control/index.js.map +1 -1
- package/build/angular/components/safe-area-view/index.d.ts +9 -4
- package/build/angular/components/safe-area-view/index.js +46 -9
- package/build/angular/components/safe-area-view/index.js.map +1 -1
- package/build/angular/components/scroll-view/index.android.d.ts +2 -3
- package/build/angular/components/scroll-view/index.android.js +49 -52
- package/build/angular/components/scroll-view/index.android.js.map +1 -1
- package/build/angular/components/scroll-view/index.ios.d.ts +2 -3
- package/build/angular/components/scroll-view/index.ios.js +15 -17
- package/build/angular/components/scroll-view/index.ios.js.map +1 -1
- package/build/angular/components/scroll-view/projection.d.ts +3 -0
- package/build/angular/components/scroll-view/projection.js +68 -7
- package/build/angular/components/scroll-view/projection.js.map +1 -1
- package/build/angular/components/scroll-view/shared.d.ts +14 -5
- package/build/angular/components/scroll-view/shared.js +198 -69
- package/build/angular/components/scroll-view/shared.js.map +1 -1
- package/build/angular/components/scroll-view/sticky-header.js +14 -19
- package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
- package/build/angular/components/section-list/index.d.ts +2 -1
- package/build/angular/components/section-list/index.js +31 -39
- package/build/angular/components/section-list/index.js.map +1 -1
- package/build/angular/components/switch/index.android.d.ts +7 -3
- package/build/angular/components/switch/index.android.js +38 -10
- package/build/angular/components/switch/index.android.js.map +1 -1
- package/build/angular/components/switch/index.ios.d.ts +7 -3
- package/build/angular/components/switch/index.ios.js +38 -10
- package/build/angular/components/switch/index.ios.js.map +1 -1
- package/build/angular/components/switch/shared.d.ts +12 -3
- package/build/angular/components/switch/shared.js +66 -5
- package/build/angular/components/switch/shared.js.map +1 -1
- package/build/angular/components/text-input.d.ts +10 -2
- package/build/angular/components/text-input.js +46 -6
- package/build/angular/components/text-input.js.map +1 -1
- package/build/angular/components/touchable/index.d.ts +17 -8
- package/build/angular/components/touchable/index.js +78 -19
- package/build/angular/components/touchable/index.js.map +1 -1
- package/build/angular/components/touchable-native-feedback/index.d.ts +6 -3
- package/build/angular/components/touchable-native-feedback/index.js +61 -25
- package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
- package/build/angular/components/virtualized-list/directives.js +18 -18
- package/build/angular/components/virtualized-list/index.d.ts +12 -4
- package/build/angular/components/virtualized-list/index.js +163 -68
- package/build/angular/components/virtualized-list/index.js.map +1 -1
- package/build/angular/components/virtualized-section-list/directives.js +12 -12
- package/build/angular/components/virtualized-section-list/index.d.ts +2 -1
- package/build/angular/components/virtualized-section-list/index.js +6 -4
- 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 +5 -0
- package/build/angular/components.js.map +1 -1
- package/build/angular/create-portal/index.js +29 -35
- package/build/angular/create-portal/index.js.map +1 -1
- package/build/angular/create-tunnel/index.js +19 -22
- package/build/angular/create-tunnel/index.js.map +1 -1
- package/build/angular/descriptor-to-angular/index.js +3 -3
- package/build/angular/index.d.ts +4 -2
- package/build/angular/index.js +2 -2
- package/build/angular/index.js.map +1 -1
- package/build/angular/modules/animated/animated-leaf-binder.d.ts +1 -7
- package/build/angular/modules/animated/animated-leaf-binder.js +22 -72
- package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -1
- package/build/angular/modules/animated/create-animated-component.d.ts +11 -6
- package/build/angular/modules/animated/create-animated-component.js +83 -50
- 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 +3 -3
- package/build/angular/primitives/index.d.ts +1 -1
- package/build/angular/primitives/index.js +46 -46
- package/build/angular/primitives/index.js.map +1 -1
- package/build/angular/primitives/shared.d.ts +39 -1
- package/build/angular/primitives/shared.js +110 -9
- package/build/angular/primitives/shared.js.map +1 -1
- package/build/angular/render/index.js +23 -35
- package/build/angular/render/index.js.map +1 -1
- package/build/angular/services/color-scheme.service.js +3 -3
- package/build/angular/services/window-dimensions.service.js +3 -3
- package/package.json +13 -8
- package/src/anchor-host-registry.ts +2 -2
- package/src/components/activity-indicator/index.android.ts +2 -0
- package/src/components/activity-indicator/index.ios.ts +2 -0
- package/src/components/button.ts +28 -14
- package/src/components/flat-list/index.ts +34 -36
- package/src/components/image/index.android.ts +50 -6
- package/src/components/image/index.ios.ts +50 -6
- package/src/components/image/shared.ts +34 -2
- package/src/components/image-background.ts +2 -1
- package/src/components/input-accessory-view/index.ts +42 -4
- package/src/components/keyboard-avoiding-view/index.ts +7 -1
- package/src/components/modal/index.ts +51 -26
- package/src/components/pressable/index.ts +67 -8
- package/src/components/refresh-control/index.ts +54 -12
- package/src/components/safe-area-view/index.ts +50 -5
- package/src/components/scroll-view/index.android.ts +34 -41
- package/src/components/scroll-view/index.ios.ts +10 -21
- package/src/components/scroll-view/projection.ts +79 -7
- package/src/components/scroll-view/shared.ts +223 -68
- package/src/components/scroll-view/sticky-header.ts +11 -16
- package/src/components/section-list/index.ts +27 -36
- package/src/components/switch/index.android.ts +48 -7
- package/src/components/switch/index.ios.ts +48 -7
- package/src/components/switch/shared.ts +79 -3
- package/src/components/text-input.ts +52 -2
- package/src/components/touchable/index.ts +76 -16
- package/src/components/touchable-native-feedback/index.ts +62 -21
- package/src/components/virtualized-list/index.ts +137 -44
- package/src/components/virtualized-section-list/index.ts +2 -1
- package/src/components.ts +5 -0
- package/src/create-portal/index.ts +23 -29
- package/src/create-tunnel/index.ts +13 -16
- package/src/index.ts +30 -1
- package/src/modules/animated/animated-leaf-binder.ts +28 -77
- package/src/modules/animated/create-animated-component.ts +75 -36
- package/src/modules/app-registry/index.ts +6 -8
- package/src/primitives/index.ts +1 -0
- package/src/primitives/shared.ts +102 -3
- package/src/render/index.ts +23 -35
- package/build/angular/modules/animated/animated-leaf-binder/index.d.ts +0 -14
- package/build/angular/modules/animated/animated-leaf-binder/index.js +0 -91
- package/build/angular/modules/animated/animated-leaf-binder/index.js.map +0 -1
- package/src/__tests__/responder-change-detection.test.ts +0 -102
- package/src/__tests__/responder-nested-cd.test.ts +0 -113
- package/src/angular-gaps.test.ts +0 -142
- package/src/angular-imports.test.ts +0 -100
- package/src/components/activity-indicator/activity-indicator-class-style.test.ts +0 -44
- package/src/components/activity-indicator/activity-indicator.test.ts +0 -131
- package/src/components/flat-list/column-wrapper-style-class.test.ts +0 -116
- package/src/components/flat-list/flat-list-array-style.test.ts +0 -131
- package/src/components/flat-list/flat-list-class-style.test.ts +0 -74
- package/src/components/flat-list/flat-list-scroll-containment.test.ts +0 -107
- package/src/components/flat-list/flat-list.test.ts +0 -99
- package/src/components/image/image-class-style.test.ts +0 -46
- package/src/components/image-background-class-style.test.ts +0 -48
- package/src/components/image-background-style-class.test.ts +0 -79
- package/src/components/input-accessory-view/input-accessory-view.test.ts +0 -43
- package/src/components/keyboard-avoiding-view/keyboard-avoiding-view.test.ts +0 -54
- package/src/components/modal/modal.test.ts +0 -43
- package/src/components/pressable/pressable.test.ts +0 -85
- package/src/components/refresh-control/refresh-control.test.ts +0 -41
- package/src/components/safe-area-view/safe-area-view.test.ts +0 -43
- package/src/components/scroll-view/android-scroll-view-axis-projection.test.ts +0 -123
- package/src/components/scroll-view/content-container-style-class.test.ts +0 -75
- package/src/components/scroll-view/scroll-view-class-style.test.ts +0 -134
- package/src/components/scroll-view/scroll-view-projection.test.ts +0 -194
- package/src/components/switch/android-switch-class.test.ts +0 -42
- package/src/components/switch/switch.test.ts +0 -92
- package/src/components/text-input-class-style.test.ts +0 -54
- package/src/components/touchable/touchable.test.ts +0 -102
- package/src/components/touchable-native-feedback/touchable-native-feedback.test.ts +0 -49
- package/src/components/virtualized-list/virtualized-list-class-style.test.ts +0 -76
- package/src/components/virtualized-section-list/virtualized-section-list-class-style.test.ts +0 -60
- package/src/create-portal/create-portal.test.ts +0 -90
- package/src/create-tunnel/create-tunnel.test.ts +0 -120
- package/src/descriptor-to-angular/descriptor-outlet.test.ts +0 -94
- package/src/host-instance/host-instance.test.ts +0 -49
- package/src/modules/animated/animated-leaf-binder/animated-leaf-binder.test.ts +0 -116
- package/src/modules/animated/animated-leaf-binder/index.ts +0 -101
- package/src/modules/animated/animated-leaf-binder.test.ts +0 -116
- package/src/modules/animated/animated-scroll-view.test.ts +0 -102
- package/src/modules/app-registry/app-registry.test.ts +0 -106
- package/src/modules/status-bar/status-bar.test.ts +0 -50
- package/src/render/render.test.ts +0 -147
- package/src/renderer/renderer.test.ts +0 -203
- package/src/services/services.test.ts +0 -163
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
// TouchableNativeFeedback: the Angular lifecycle half. Android's ripple / state-drawable
|
|
2
|
-
// touchable, built on Pressable like the rest of the family
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// platforms), so this stays a flat single file, mirroring React/Vue.
|
|
2
|
+
// touchable, built on Pressable like the rest of the family. RN realizes its feedback by cloning
|
|
3
|
+
// the child into an RCTView carrying native ripple props; we instead nest the child under a
|
|
4
|
+
// feedback <symbiote-view> that carries those props (nativeBackgroundAndroid /
|
|
5
|
+
// nativeForegroundAndroid), inside a <Pressable> that owns the press wiring. The native props are
|
|
6
|
+
// read by Android's ReactViewManager; on iOS they are inert, so the child still renders with
|
|
7
|
+
// working press wiring. The static factories + background mapping are shared in
|
|
8
|
+
// @symbiote-native/components and reused verbatim. One Fabric path both platforms, so this stays
|
|
9
|
+
// a flat single file, mirroring React/Vue.
|
|
11
10
|
//
|
|
12
11
|
// This component's own press/hover events are real @Output() EventEmitters too, matching
|
|
13
|
-
// Pressable (which it wraps) — `(press)="press.emit($event)"`, never `[onPress]="onPress"`.
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// only). disabled folds into the a11y state via resolveDisabledAccessibilityState, exactly as
|
|
19
|
-
// Pressable does for its own host.
|
|
12
|
+
// Pressable (which it wraps) — `(press)="press.emit($event)"`, never `[onPress]="onPress"`. The
|
|
13
|
+
// a11y identity bag + the four a11y events live on the feedback view — the one host intrinsic here
|
|
14
|
+
// — folded through the shared resolveAccessibilityProps and routed through emit() on the (event)
|
|
15
|
+
// channel (Angular blocks [onX] property bindings on host elements). disabled folds into the a11y
|
|
16
|
+
// state via resolveDisabledAccessibilityState, exactly as Pressable does for its own host.
|
|
20
17
|
|
|
21
18
|
import {
|
|
22
19
|
CUSTOM_ELEMENTS_SCHEMA,
|
|
@@ -24,9 +21,12 @@ import {
|
|
|
24
21
|
Component,
|
|
25
22
|
ElementRef,
|
|
26
23
|
EventEmitter,
|
|
24
|
+
computed,
|
|
27
25
|
inject,
|
|
28
26
|
Input,
|
|
29
27
|
Output,
|
|
28
|
+
signal,
|
|
29
|
+
type DoCheck,
|
|
30
30
|
type OnChanges,
|
|
31
31
|
} from '@angular/core';
|
|
32
32
|
import {
|
|
@@ -90,7 +90,7 @@ export type IAngularTouchableNativeFeedbackProps = Omit<IAngularPressableInputs,
|
|
|
90
90
|
[android_disableSound]="android_disableSound"
|
|
91
91
|
>
|
|
92
92
|
<symbiote-view
|
|
93
|
-
[symbioteHostProps]="hostProps"
|
|
93
|
+
[symbioteHostProps]="hostProps()"
|
|
94
94
|
(accessibilityAction)="emit(accessibilityAction, $event)"
|
|
95
95
|
(accessibilityTap)="emit(accessibilityTap, $event)"
|
|
96
96
|
(magicTap)="emit(magicTap, $event)"
|
|
@@ -101,7 +101,9 @@ export type IAngularTouchableNativeFeedbackProps = Omit<IAngularPressableInputs,
|
|
|
101
101
|
</Pressable>
|
|
102
102
|
`,
|
|
103
103
|
})
|
|
104
|
-
export class TouchableNativeFeedback
|
|
104
|
+
export class TouchableNativeFeedback
|
|
105
|
+
implements IAngularTouchableNativeFeedbackProps, OnChanges, DoCheck
|
|
106
|
+
{
|
|
105
107
|
// The static helpers are pure config-dict producers; they live as members on the component class
|
|
106
108
|
// so callers reach them as `TouchableNativeFeedback.Ripple(...)`, exactly like RN.
|
|
107
109
|
static readonly SelectableBackground = selectableBackground;
|
|
@@ -175,7 +177,37 @@ export class TouchableNativeFeedback implements IAngularTouchableNativeFeedbackP
|
|
|
175
177
|
// anchor's class-derived style is the ONLY style source for hostProps.style below.
|
|
176
178
|
private readonly elementRef = inject(ElementRef);
|
|
177
179
|
|
|
180
|
+
// Bridges the non-reactive @Input fields above into the reactive graph, so the computed() bag
|
|
181
|
+
// below can memoize a value derived from them. Signal inputs would make this unnecessary, but
|
|
182
|
+
// `input()` is visible only to the AOT compiler and this package's unit suite runs on JIT - see
|
|
183
|
+
// the `angular-adapter-change-detection` skill, §6. Mirrors ScrollView's identical bridge
|
|
184
|
+
// (components/scroll-view/shared.ts), including its constraint: ONLY safe for a bag whose every
|
|
185
|
+
// dependency is an @Input or carries its own signal, never for plain internal mutable state.
|
|
186
|
+
private readonly inputsRevision = signal(0);
|
|
187
|
+
|
|
188
|
+
// The anchor's class-derived style is hostProps' one dependency that is NOT an @Input:
|
|
189
|
+
// `class="..."`/`[class.x]`/`[ngClass]` at the use site resolves through the renderer's
|
|
190
|
+
// addClass/removeClass onto the anchor node (anchorHostStyle's doc comment), so it never appears
|
|
191
|
+
// in SimpleChanges and ngOnChanges cannot cover it. The engine's commitClassStyle allocates a
|
|
192
|
+
// fresh [classStyle, explicitStyle] array on exactly the passes where a class token actually
|
|
193
|
+
// moved, so signal.set's own Object.is check makes an unchanged poll a no-op that dirties nothing.
|
|
194
|
+
private readonly anchorStyle = signal<unknown>(undefined);
|
|
195
|
+
|
|
196
|
+
// ngDoCheck is the only hook that observes the anchor AFTER the parent wrote it. Angular flushes
|
|
197
|
+
// a node's pre-order hooks at the next `advance` PAST it (hooks.ts callHooks runs "until that
|
|
198
|
+
// node index exclusive") or in refreshView's post-template flush - both strictly after the
|
|
199
|
+
// parent's classProp on this element, and strictly before this component's own view refreshes.
|
|
200
|
+
// Angular also nulls the active consumer around lifecycle hooks (render3/hooks.ts), so this
|
|
201
|
+
// write registers no dependency on the caller's view and cannot throw NG0600.
|
|
202
|
+
ngDoCheck(): void {
|
|
203
|
+
this.anchorStyle.set(anchorHostStyle(this.elementRef));
|
|
204
|
+
}
|
|
205
|
+
|
|
178
206
|
ngOnChanges(): void {
|
|
207
|
+
// Tells the computed bag below that an @Input moved - plain class fields are not reactive on
|
|
208
|
+
// their own. Must stay in ngOnChanges: that is the single moment Angular has finished writing
|
|
209
|
+
// every changed input for this pass.
|
|
210
|
+
this.inputsRevision.update(revision => revision + 1);
|
|
179
211
|
dlog(
|
|
180
212
|
`TouchableNativeFeedback render ${this.resolvedBackground.type} useForeground ${this.useForeground === true}`,
|
|
181
213
|
);
|
|
@@ -195,17 +227,26 @@ export class TouchableNativeFeedback implements IAngularTouchableNativeFeedbackP
|
|
|
195
227
|
|
|
196
228
|
// Assembles the SAME resolved values the template used to bind one-by-one into a single
|
|
197
229
|
// flat bag for `[symbioteHostProps]` (Renderer2.setProperty per key) — see primitives/shared.ts.
|
|
198
|
-
|
|
230
|
+
//
|
|
231
|
+
// MEASURED 2026-08-16: as a getter this rebuilt the bag on every refresh of this view, and a
|
|
232
|
+
// press dirties this very view (SymbioteHostPropsDirective wraps each flat-bag onX with
|
|
233
|
+
// markForCheck), so `[symbioteHostProps]`'s reference check failed every time and re-pushed
|
|
234
|
+
// every key through Renderer2.setProperty -> routeProp for an identical bag. Memoized, an
|
|
235
|
+
// unchanged bag costs one reference compare. Every dependency below is either an @Input (covered
|
|
236
|
+
// by inputsRevision) or the anchor style (covered by its own signal) - nothing here reads plain
|
|
237
|
+
// internal mutable state, which is what makes memoizing this one safe.
|
|
238
|
+
readonly hostProps = computed<Record<string, unknown>>(() => {
|
|
239
|
+
this.inputsRevision();
|
|
199
240
|
return {
|
|
200
241
|
nativeBackgroundAndroid: this.feedback['nativeBackgroundAndroid'],
|
|
201
242
|
nativeForegroundAndroid: this.feedback['nativeForegroundAndroid'],
|
|
202
|
-
style:
|
|
243
|
+
style: this.anchorStyle(),
|
|
203
244
|
testID: this.testID,
|
|
204
245
|
nativeID: this.nativeID,
|
|
205
246
|
accessible: this.accessible,
|
|
206
247
|
...this.folded,
|
|
207
248
|
};
|
|
208
|
-
}
|
|
249
|
+
});
|
|
209
250
|
|
|
210
251
|
// Forward an engine event to the matching @Output(), narrowing the template's untyped $event.
|
|
211
252
|
emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
|
|
@@ -35,9 +35,12 @@ import {
|
|
|
35
35
|
Input,
|
|
36
36
|
Output,
|
|
37
37
|
ViewChild,
|
|
38
|
+
computed,
|
|
38
39
|
inject,
|
|
40
|
+
signal,
|
|
39
41
|
type AfterViewChecked,
|
|
40
42
|
type DoCheck,
|
|
43
|
+
type OnChanges,
|
|
41
44
|
type OnDestroy,
|
|
42
45
|
} from '@angular/core';
|
|
43
46
|
import {
|
|
@@ -86,7 +89,7 @@ import {
|
|
|
86
89
|
} from '@symbiote-native/engine';
|
|
87
90
|
import { ScrollView } from '../scroll-view';
|
|
88
91
|
import { RefreshControl } from '../refresh-control';
|
|
89
|
-
import { anchorHostStyle, ViewHost } from '../../primitives';
|
|
92
|
+
import { anchorHostStyle, SymbioteStyleInputDirective, ViewHost } from '../../primitives';
|
|
90
93
|
import {
|
|
91
94
|
VListEmptyDirective,
|
|
92
95
|
VListFooterDirective,
|
|
@@ -202,6 +205,7 @@ interface IWindowCell<ItemT> {
|
|
|
202
205
|
@Component({
|
|
203
206
|
selector: 'VirtualizedList',
|
|
204
207
|
standalone: true,
|
|
208
|
+
hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
|
|
205
209
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
206
210
|
imports: [ScrollView, RefreshControl, VListOutletDirective, ViewHost],
|
|
207
211
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -210,30 +214,30 @@ interface IWindowCell<ItemT> {
|
|
|
210
214
|
[horizontal]="isHorizontal"
|
|
211
215
|
[style]="resolvedStyle"
|
|
212
216
|
[contentContainerStyle]="resolvedContentContainerStyle"
|
|
213
|
-
[testID]="foldedAccessibility.testID"
|
|
214
|
-
[nativeID]="foldedAccessibility.nativeID"
|
|
215
|
-
[accessible]="foldedAccessibility.accessible"
|
|
216
|
-
[accessibilityLabel]="foldedAccessibility.accessibilityLabel"
|
|
217
|
-
[accessibilityHint]="foldedAccessibility.accessibilityHint"
|
|
218
|
-
[accessibilityRole]="foldedAccessibility.accessibilityRole"
|
|
219
|
-
[accessibilityState]="foldedAccessibility.accessibilityState"
|
|
220
|
-
[accessibilityValue]="foldedAccessibility.accessibilityValue"
|
|
221
|
-
[accessibilityActions]="foldedAccessibility.accessibilityActions"
|
|
222
|
-
[accessibilityLabelledBy]="foldedAccessibility.accessibilityLabelledBy"
|
|
223
|
-
[importantForAccessibility]="foldedAccessibility.importantForAccessibility"
|
|
224
|
-
[accessibilityLiveRegion]="foldedAccessibility.accessibilityLiveRegion"
|
|
225
|
-
[screenReaderFocusable]="foldedAccessibility.screenReaderFocusable"
|
|
226
|
-
[accessibilityViewIsModal]="foldedAccessibility.accessibilityViewIsModal"
|
|
227
|
-
[accessibilityElementsHidden]="foldedAccessibility.accessibilityElementsHidden"
|
|
228
|
-
[accessibilityIgnoresInvertColors]="foldedAccessibility.accessibilityIgnoresInvertColors"
|
|
229
|
-
[accessibilityLanguage]="foldedAccessibility.accessibilityLanguage"
|
|
217
|
+
[testID]="foldedAccessibility().testID"
|
|
218
|
+
[nativeID]="foldedAccessibility().nativeID"
|
|
219
|
+
[accessible]="foldedAccessibility().accessible"
|
|
220
|
+
[accessibilityLabel]="foldedAccessibility().accessibilityLabel"
|
|
221
|
+
[accessibilityHint]="foldedAccessibility().accessibilityHint"
|
|
222
|
+
[accessibilityRole]="foldedAccessibility().accessibilityRole"
|
|
223
|
+
[accessibilityState]="foldedAccessibility().accessibilityState"
|
|
224
|
+
[accessibilityValue]="foldedAccessibility().accessibilityValue"
|
|
225
|
+
[accessibilityActions]="foldedAccessibility().accessibilityActions"
|
|
226
|
+
[accessibilityLabelledBy]="foldedAccessibility().accessibilityLabelledBy"
|
|
227
|
+
[importantForAccessibility]="foldedAccessibility().importantForAccessibility"
|
|
228
|
+
[accessibilityLiveRegion]="foldedAccessibility().accessibilityLiveRegion"
|
|
229
|
+
[screenReaderFocusable]="foldedAccessibility().screenReaderFocusable"
|
|
230
|
+
[accessibilityViewIsModal]="foldedAccessibility().accessibilityViewIsModal"
|
|
231
|
+
[accessibilityElementsHidden]="foldedAccessibility().accessibilityElementsHidden"
|
|
232
|
+
[accessibilityIgnoresInvertColors]="foldedAccessibility().accessibilityIgnoresInvertColors"
|
|
233
|
+
[accessibilityLanguage]="foldedAccessibility().accessibilityLanguage"
|
|
230
234
|
[accessibilityRespondsToUserInteraction]="
|
|
231
|
-
foldedAccessibility.accessibilityRespondsToUserInteraction
|
|
235
|
+
foldedAccessibility().accessibilityRespondsToUserInteraction
|
|
232
236
|
"
|
|
233
237
|
[accessibilityShowsLargeContentViewer]="
|
|
234
|
-
foldedAccessibility.accessibilityShowsLargeContentViewer
|
|
238
|
+
foldedAccessibility().accessibilityShowsLargeContentViewer
|
|
235
239
|
"
|
|
236
|
-
[accessibilityLargeContentTitle]="foldedAccessibility.accessibilityLargeContentTitle"
|
|
240
|
+
[accessibilityLargeContentTitle]="foldedAccessibility().accessibilityLargeContentTitle"
|
|
237
241
|
(accessibilityAction)="accessibilityActionTick($event)"
|
|
238
242
|
(accessibilityTap)="accessibilityTapTick($event)"
|
|
239
243
|
(magicTap)="magicTapTick($event)"
|
|
@@ -275,6 +279,20 @@ interface IWindowCell<ItemT> {
|
|
|
275
279
|
@if (leadingSpacerStyle !== null) {
|
|
276
280
|
<symbiote-view [style]="leadingSpacerStyle"></symbiote-view>
|
|
277
281
|
}
|
|
282
|
+
@if (forcedStickyCell !== null) {
|
|
283
|
+
<symbiote-view
|
|
284
|
+
(layout)="handleCellLayout(forcedStickyCell.measure, $event)"
|
|
285
|
+
[style]="cellStyle"
|
|
286
|
+
>
|
|
287
|
+
<ng-container
|
|
288
|
+
[vListOutlet]="itemDir?.templateRef"
|
|
289
|
+
[vListOutletContext]="forcedStickyCell.context"
|
|
290
|
+
></ng-container>
|
|
291
|
+
</symbiote-view>
|
|
292
|
+
}
|
|
293
|
+
@if (gapSpacerStyle !== null) {
|
|
294
|
+
<symbiote-view [style]="gapSpacerStyle"></symbiote-view>
|
|
295
|
+
}
|
|
278
296
|
@for (cell of windowCells; track cell.key) {
|
|
279
297
|
<symbiote-view (layout)="handleCellLayout(cell.measure, $event)" [style]="cellStyle">
|
|
280
298
|
<ng-container
|
|
@@ -310,6 +328,7 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
310
328
|
IVirtualizedListHandle,
|
|
311
329
|
DoCheck,
|
|
312
330
|
AfterViewChecked,
|
|
331
|
+
OnChanges,
|
|
313
332
|
OnDestroy
|
|
314
333
|
{
|
|
315
334
|
// The list's edge/viewability/failure events as real Angular events: `(endReached)="…"`, not
|
|
@@ -426,7 +445,13 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
426
445
|
// --- template-bound view state, assembled in ngDoCheck (recomputeView) ---
|
|
427
446
|
itemCount = EMPTY_OFFSET;
|
|
428
447
|
windowCells: IWindowCell<ItemT>[] = [];
|
|
448
|
+
// The nearest sticky-header cell force-mounted outside [first,last] (plan.forcedStickyCell), the
|
|
449
|
+
// Angular twin of RN's _ensureClosestStickyHeader. Rendered ahead of windowCells, right after the
|
|
450
|
+
// leading spacer, with gapSpacerStyle filling the gap to the window's own first cell — see
|
|
451
|
+
// buildListPlan's doc comment for the exact child order this assumes.
|
|
452
|
+
forcedStickyCell: IWindowCell<ItemT> | null = null;
|
|
429
453
|
leadingSpacerStyle: IViewStyle | null = null;
|
|
454
|
+
gapSpacerStyle: IViewStyle | null = null;
|
|
430
455
|
trailingSpacerStyle: IViewStyle | null = null;
|
|
431
456
|
cellStyle: IViewStyle | undefined = undefined;
|
|
432
457
|
renderedStickyIndices: number[] | undefined = undefined;
|
|
@@ -457,6 +482,9 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
457
482
|
private batchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
458
483
|
// Dedupes the after-commit effects: they run only when the windowing signature changed.
|
|
459
484
|
private lastEffectSignature = '';
|
|
485
|
+
// Dedupes dispatch's markForCheck against the RENDERED window (see dispatch below). Distinct from
|
|
486
|
+
// lastEffectSignature, which gates the batch-fill timer and deliberately includes scrollOffset.
|
|
487
|
+
private lastRenderSignature = '';
|
|
460
488
|
// Dedupes ngDoCheck's own recompute. WITHOUT this, recomputeView() unconditionally rebuilds
|
|
461
489
|
// windowCells (and each cell's `separators` handle — fresh closures every call) on EVERY CD pass,
|
|
462
490
|
// including ones triggered by something else entirely in the app. A fresh context object flows
|
|
@@ -476,7 +504,27 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
476
504
|
// inner `<ScrollView>` one level down (itself its own separate anchor host).
|
|
477
505
|
private readonly elementRef = inject(ElementRef);
|
|
478
506
|
|
|
479
|
-
|
|
507
|
+
// Bridges the non-reactive @Input fields into the reactive graph so the computed below can
|
|
508
|
+
// memoize a bag derived from them (the same bridge ScrollView's shared.ts uses - read its comment
|
|
509
|
+
// for why signal inputs are not an option while this package's unit suite runs on JIT).
|
|
510
|
+
//
|
|
511
|
+
// ONLY safe for a bag whose every dependency is an @Input. This list's window machinery
|
|
512
|
+
// (listState, renderVersion, windowCells, the measured metrics) is driven from scroll/layout
|
|
513
|
+
// callbacks that never touch ngOnChanges, so nothing derived from it may ride this signal - it
|
|
514
|
+
// would memoize a window that stops re-computing mid-scroll. Make that state its own signal
|
|
515
|
+
// instead of widening this one.
|
|
516
|
+
private readonly inputsRevision = signal(0);
|
|
517
|
+
|
|
518
|
+
// MEASURED: the template feeds 20 separate <ScrollView> inputs off this one bag, and Angular does
|
|
519
|
+
// not cache a getter across binding expressions - as a getter it rebuilt the object 20 times per
|
|
520
|
+
// refresh, and every consumer saw a fresh reference, so all 20 reported "changed" and wrote
|
|
521
|
+
// through to the ScrollView. A computed evaluates once and hands back the SAME object until an
|
|
522
|
+
// input actually changes. Every dependency below is an @Input, which is what makes
|
|
523
|
+
// inputsRevision a complete dependency set here.
|
|
524
|
+
readonly foldedAccessibility = computed<
|
|
525
|
+
IAccessibilityProps & IAriaProps & Record<string, unknown>
|
|
526
|
+
>(() => {
|
|
527
|
+
this.inputsRevision();
|
|
480
528
|
return resolveAccessibilityProps({
|
|
481
529
|
testID: this.testID,
|
|
482
530
|
nativeID: this.nativeID,
|
|
@@ -514,7 +562,7 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
514
562
|
'aria-valuenow': this.ariaValueNow,
|
|
515
563
|
'aria-valuetext': this.ariaValueText,
|
|
516
564
|
});
|
|
517
|
-
}
|
|
565
|
+
});
|
|
518
566
|
|
|
519
567
|
get shouldRenderRefreshControl(): boolean {
|
|
520
568
|
return this.refreshRequested ?? this.refresh.observed;
|
|
@@ -631,10 +679,22 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
631
679
|
const inputs = this.buildInputs();
|
|
632
680
|
const result = reduceList(this.listState, action, inputs);
|
|
633
681
|
this.runEffects(result.effects, inputs);
|
|
634
|
-
if (result.changed)
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
682
|
+
if (!result.changed) return;
|
|
683
|
+
this.renderVersion += 1;
|
|
684
|
+
// The reducer reports `changed` for EVERY scroll offset - it has to, since the offset feeds
|
|
685
|
+
// end-reached distance, viewability and the batch-fill timer, all handled above in runEffects.
|
|
686
|
+
// The TEMPLATE reads none of those; it reads the window. Marking on the offset repainted the
|
|
687
|
+
// whole ancestor chain 60 times a second on a sticky screen for a window that had not moved.
|
|
688
|
+
const signature = this.renderSignature();
|
|
689
|
+
if (signature === this.lastRenderSignature) return;
|
|
690
|
+
this.lastRenderSignature = signature;
|
|
691
|
+
this.cdr.markForCheck();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// The window, not the offset: exactly the metrics recomputeView turns into cells and spacers.
|
|
695
|
+
private renderSignature(): string {
|
|
696
|
+
const m = this.listState.metrics;
|
|
697
|
+
return `${m.first}|${m.last}|${m.count}|${m.total}`;
|
|
638
698
|
}
|
|
639
699
|
|
|
640
700
|
private runEffects(effects: IListEffect<ItemT>[], inputs: IListReducerInputs<ItemT>): void {
|
|
@@ -690,6 +750,14 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
690
750
|
}
|
|
691
751
|
}
|
|
692
752
|
|
|
753
|
+
// foldedAccessibility reads plain @Input fields, which are NOT reactive on their own - this bump
|
|
754
|
+
// is what tells that computed an input changed. It must stay in ngOnChanges: that is the single
|
|
755
|
+
// moment Angular has finished writing every changed input for this pass, and it runs before
|
|
756
|
+
// ngDoCheck below, so the recompute already sees the current values.
|
|
757
|
+
ngOnChanges(): void {
|
|
758
|
+
this.inputsRevision.update(revision => revision + 1);
|
|
759
|
+
}
|
|
760
|
+
|
|
693
761
|
// Once-per-CD recompute, BEFORE the template bindings are read (so the freshly computed
|
|
694
762
|
// template-bound fields render this pass — computing them in ngAfterContentChecked instead would
|
|
695
763
|
// trip ExpressionChangedAfterItHasBeenChecked). The dedup guard runs refresh-metrics (which owns
|
|
@@ -710,6 +778,10 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
710
778
|
this.stickyHeaderIndices,
|
|
711
779
|
this.maintainVisibleContentPosition,
|
|
712
780
|
this.style,
|
|
781
|
+
// `recomputeView` folds this into the committed style, but it arrives through
|
|
782
|
+
// addClass/removeClass, never as an @Input - so without it here no entry moves on a class
|
|
783
|
+
// toggle and the gate skips the recompute forever. Reference-stable, so the dedup holds.
|
|
784
|
+
anchorHostStyle(this.elementRef),
|
|
713
785
|
this.contentContainerStyle,
|
|
714
786
|
// Folds scroll / layout / measure / batch-tick: dispatch bumps renderVersion on any change.
|
|
715
787
|
this.renderVersion,
|
|
@@ -779,7 +851,9 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
779
851
|
|
|
780
852
|
if (m.count === FIRST_INDEX) {
|
|
781
853
|
this.windowCells = [];
|
|
854
|
+
this.forcedStickyCell = null;
|
|
782
855
|
this.leadingSpacerStyle = null;
|
|
856
|
+
this.gapSpacerStyle = null;
|
|
783
857
|
this.trailingSpacerStyle = null;
|
|
784
858
|
this.renderedStickyIndices = undefined;
|
|
785
859
|
dlog(`Angular VirtualizedList empty (viewport=${this.listState.viewportLength})`);
|
|
@@ -800,32 +874,29 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
800
874
|
});
|
|
801
875
|
this.leadingSpacerStyle =
|
|
802
876
|
plan.leadingExtent > EMPTY_OFFSET ? this.spacerStyle(plan.leadingExtent) : null;
|
|
877
|
+
this.gapSpacerStyle = plan.gapExtent > EMPTY_OFFSET ? this.spacerStyle(plan.gapExtent) : null;
|
|
803
878
|
this.trailingSpacerStyle =
|
|
804
879
|
plan.trailingExtent > EMPTY_OFFSET ? this.spacerStyle(plan.trailingExtent) : null;
|
|
805
880
|
this.renderedStickyIndices =
|
|
806
881
|
stickySet !== undefined && plan.stickyChildPositions.length > 0
|
|
807
882
|
? plan.stickyChildPositions
|
|
808
883
|
: undefined;
|
|
884
|
+
dlog(
|
|
885
|
+
`STICKY[list] stickySet=${stickySet === undefined ? 'undefined' : JSON.stringify([...stickySet])} ` +
|
|
886
|
+
`childPositions=${JSON.stringify(plan.stickyChildPositions)} ` +
|
|
887
|
+
`forcedStickyCell=${plan.forcedStickyCell === undefined ? 'none' : plan.forcedStickyCell.index} ` +
|
|
888
|
+
`window=[${m.first},${m.last}] hasHeader=${this.headerDir !== undefined}`,
|
|
889
|
+
);
|
|
890
|
+
|
|
891
|
+
this.forcedStickyCell =
|
|
892
|
+
plan.forcedStickyCell !== undefined
|
|
893
|
+
? this.buildWindowCell(plan.forcedStickyCell.index, plan.forcedStickyCell.key, false)
|
|
894
|
+
: null;
|
|
809
895
|
|
|
810
896
|
const cells: IWindowCell<ItemT>[] = [];
|
|
811
897
|
for (const planned of plan.cells) {
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
$implicit: item,
|
|
815
|
-
index: planned.index,
|
|
816
|
-
separators: this.makeSeparators(planned.index),
|
|
817
|
-
};
|
|
818
|
-
let separatorContext: IVListSeparatorContext<ItemT> | undefined;
|
|
819
|
-
if (hasSeparators && planned.index < m.last) {
|
|
820
|
-
separatorContext = this.buildSeparatorContext(planned.index, item);
|
|
821
|
-
}
|
|
822
|
-
cells.push({
|
|
823
|
-
key: planned.key,
|
|
824
|
-
index: planned.index,
|
|
825
|
-
context,
|
|
826
|
-
measure: this.cellMeasure(planned.index),
|
|
827
|
-
separatorContext,
|
|
828
|
-
});
|
|
898
|
+
const includeSeparator = hasSeparators && planned.index < m.last;
|
|
899
|
+
cells.push(this.buildWindowCell(planned.index, planned.key, includeSeparator));
|
|
829
900
|
}
|
|
830
901
|
this.windowCells = cells;
|
|
831
902
|
|
|
@@ -835,6 +906,28 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
835
906
|
);
|
|
836
907
|
}
|
|
837
908
|
|
|
909
|
+
// Assembles one cell (window or forced-sticky) — the context, measure closure, and optional
|
|
910
|
+
// separator context — so both plan.cells and plan.forcedStickyCell build identically.
|
|
911
|
+
private buildWindowCell(
|
|
912
|
+
index: number,
|
|
913
|
+
key: string,
|
|
914
|
+
includeSeparator: boolean,
|
|
915
|
+
): IWindowCell<ItemT> {
|
|
916
|
+
const item = this.getItem(this.data, index);
|
|
917
|
+
const context: IVListItemContext<ItemT> = {
|
|
918
|
+
$implicit: item,
|
|
919
|
+
index,
|
|
920
|
+
separators: this.makeSeparators(index),
|
|
921
|
+
};
|
|
922
|
+
return {
|
|
923
|
+
key,
|
|
924
|
+
index,
|
|
925
|
+
context,
|
|
926
|
+
measure: this.cellMeasure(index),
|
|
927
|
+
separatorContext: includeSeparator ? this.buildSeparatorContext(index, item) : undefined,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
838
931
|
private buildSeparatorContext(index: number, item: ItemT): IVListSeparatorContext<ItemT> {
|
|
839
932
|
const overrides = this.separatorOverrides.get(index);
|
|
840
933
|
const highlighted = overrides?.highlighted ?? false;
|
|
@@ -64,7 +64,7 @@ import {
|
|
|
64
64
|
type IVListSeparatorContext,
|
|
65
65
|
} from '../virtualized-list';
|
|
66
66
|
import { VListOutletDirective } from '../virtualized-list/directives';
|
|
67
|
-
import { stableAnchorStyle } from '../../primitives';
|
|
67
|
+
import { stableAnchorStyle, SymbioteStyleInputDirective } from '../../primitives';
|
|
68
68
|
import {
|
|
69
69
|
VSectionFooterDirective,
|
|
70
70
|
VSectionHeaderDirective,
|
|
@@ -146,6 +146,7 @@ export type IVirtualizedSectionListInputs<ItemT> = Omit<
|
|
|
146
146
|
@Component({
|
|
147
147
|
selector: 'VirtualizedSectionList',
|
|
148
148
|
standalone: true,
|
|
149
|
+
hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
|
|
149
150
|
imports: [
|
|
150
151
|
VirtualizedList,
|
|
151
152
|
VListItemDirective,
|
package/src/components.ts
CHANGED
|
@@ -134,6 +134,11 @@ export {
|
|
|
134
134
|
RefreshControlHost,
|
|
135
135
|
InputAccessoryViewHost,
|
|
136
136
|
SymbioteHostPropsDirective,
|
|
137
|
+
// Public even though no app names it: it rides `hostDirectives` on every component declaring a
|
|
138
|
+
// `style` input, so ngtsc writes it into their public type metadata and a consuming app's AOT
|
|
139
|
+
// build fails with NG3004 without it. Invisible to tsc and vitest; only a consumer's ngc catches
|
|
140
|
+
// it.
|
|
141
|
+
SymbioteStyleInputDirective,
|
|
137
142
|
// Exported so an OUT-OF-PACKAGE composed component (e.g. @symbiote-native/slider's Angular wrapper,
|
|
138
143
|
// itself listed in ANCHOR_HOST_COMPONENTS) can merge its own anchor host's class-derived style
|
|
139
144
|
// the same way every in-package composed component does — see anchorHostStyle's doc comment.
|
|
@@ -1,38 +1,32 @@
|
|
|
1
1
|
// createPortal for @symbiote-native/angular — the Angular twin of the React/Vue same-surface portal
|
|
2
|
-
// (create-portal.ts / Teleport in runtime-helpers.ts). Scope is identical: the target must be
|
|
3
|
-
//
|
|
4
|
-
// across independently-mounted surfaces has no safe host-level primitive
|
|
5
|
-
//
|
|
2
|
+
// (create-portal.ts / Teleport in runtime-helpers.ts). Scope is identical: the target must be an
|
|
3
|
+
// already-mounted location WITHIN THE SAME SURFACE as the portal's call site — moving content
|
|
4
|
+
// across independently-mounted surfaces has no safe host-level primitive, so createTunnel
|
|
5
|
+
// (create-tunnel.ts) covers that cross-surface case instead.
|
|
6
6
|
//
|
|
7
|
-
// React/Vue implement this as a thin validating wrapper around a mechanism the
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// point), not from wherever a node was manually moved to afterwards, so a raw Renderer2 move
|
|
15
|
-
// would desync Angular's internals from the retained tree.
|
|
7
|
+
// React/Vue implement this as a thin validating wrapper around a mechanism the framework itself
|
|
8
|
+
// already has (react-reconciler's Fiber-level HostPortal, Vue's own <Teleport>). Angular has no
|
|
9
|
+
// such built-in (no `@angular/cdk` dependency here, core-only — see package.json), and physically
|
|
10
|
+
// moving an EmbeddedView's already-created root nodes via Renderer2 after the fact is NOT safe —
|
|
11
|
+
// Angular's own view-destroy path removes a view's nodes from wherever ITS OWN bookkeeping thinks
|
|
12
|
+
// they live, not from wherever a node was manually moved to, so a raw Renderer2 move would desync
|
|
13
|
+
// Angular's internals from the retained tree.
|
|
16
14
|
//
|
|
17
|
-
// PortalDirective is a STRUCTURAL directive (`*portal="overlayHost"`), not a component
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// injects that template's own TemplateRef into the directive automatically.
|
|
15
|
+
// PortalDirective is a STRUCTURAL directive (`*portal="overlayHost"`), not a component taking a
|
|
16
|
+
// separate `<ng-template>` + `[content]` binding — that two-step reads as foreign next to
|
|
17
|
+
// `*ngIf`/`*ngFor`/`*ngTemplateOutlet`. `*portal="x"` desugars the same way `*ngIf` does: Angular
|
|
18
|
+
// wraps the host element in an `<ng-template>` and injects that template's own TemplateRef into
|
|
19
|
+
// the directive automatically.
|
|
23
20
|
//
|
|
24
21
|
// The safe, fully-public-API mechanism: create the embedded view DIRECTLY inside a
|
|
25
22
|
// ViewContainerRef anchored at the destination, so there is nothing to move at all. That
|
|
26
|
-
// ViewContainerRef
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
// `
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// one is Angular's own template compiler resolving a template reference variable, so
|
|
34
|
-
// `strictTemplates` rejects anything else at compile time and there is nothing left to guard
|
|
35
|
-
// against at runtime.
|
|
23
|
+
// ViewContainerRef comes from `PortalOutletDirective`, a marker placed on the target host
|
|
24
|
+
// (`<View portalOutlet #overlayHost="portalOutlet">`) purely to expose its ViewContainerRef, the
|
|
25
|
+
// same export-as-template-variable idiom `#form="ngForm"` uses. This also replaces the
|
|
26
|
+
// `isSymbioteNode` runtime guard React/Vue need (there `to` is an arbitrary JS value until
|
|
27
|
+
// validated): here `to` is typed as `PortalOutletDirective` — the only way to construct one is
|
|
28
|
+
// Angular's own template compiler resolving a template reference variable, so `strictTemplates`
|
|
29
|
+
// rejects anything else at compile time and there is nothing left to guard at runtime.
|
|
36
30
|
|
|
37
31
|
import {
|
|
38
32
|
Directive,
|
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
// createTunnel for @symbiote-native/angular — the Angular twin of the React/Vue cross-surface tunnel
|
|
2
2
|
// (adapters/react/src/create-tunnel.tsx, adapters/vue/src/create-tunnel.ts — read their
|
|
3
|
-
// headers for the
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// share a surface or not.
|
|
3
|
+
// headers for the "why not just extend the portal across surfaces" rationale; same prior art:
|
|
4
|
+
// pmndrs/tunnel-rat, facebook/react#17147). `TunnelOut` lives in whichever component should PAINT
|
|
5
|
+
// the content; its own read of the shared store drives that surface's own normal render cycle —
|
|
6
|
+
// no cross-surface reach-in, works whether TunnelIn and TunnelOut share a surface or not.
|
|
8
7
|
//
|
|
9
|
-
// Angular cannot mirror React's/Vue's shape verbatim: both
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// Metro/Hermes (see modules/animated/create-animated-component.ts's header for the same
|
|
8
|
+
// Angular cannot mirror React's/Vue's shape verbatim: both build a FRESH `In`/`Out` pair per
|
|
9
|
+
// `createTunnel()` call, and Angular components can't be synthesized at runtime — no JIT compiler
|
|
10
|
+
// under Metro/Hermes (see modules/animated/create-animated-component.ts's header for the same
|
|
13
11
|
// constraint). So here `createTunnel()` returns only a plain reactive STORE (a signal);
|
|
14
12
|
// `TunnelInDirective` and `TunnelOut` are ONE static, pre-authored, AOT-compilable pair,
|
|
15
|
-
// parameterized by that store through an input —
|
|
16
|
-
//
|
|
13
|
+
// parameterized by that store through an input — the relationship `VListOutletDirective` has to
|
|
14
|
+
// the per-cell template it stamps, generalized to an open-ended, changing SET of templates.
|
|
17
15
|
//
|
|
18
|
-
// TunnelInDirective is a STRUCTURAL directive (`*tunnelIn="overlayTunnel"`),
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// same shape as Angular's own `<router-outlet>`, not content that needs a structural directive.
|
|
16
|
+
// TunnelInDirective is a STRUCTURAL directive (`*tunnelIn="overlayTunnel"`), matching `*portal`
|
|
17
|
+
// (see create-portal.ts's header for why that reads as native Angular). `TunnelOut` stays a plain
|
|
18
|
+
// component (`<tunnel-out>`) — a rendering SLOT, the same shape as Angular's own
|
|
19
|
+
// `<router-outlet>`, not content that needs a structural directive.
|
|
23
20
|
//
|
|
24
21
|
// App code stays fully declarative — no ViewContainerRef/imperative rendering — by putting
|
|
25
22
|
// `*tunnelIn` directly on the portable content, the same way `*ngIf` sits directly on an
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ export {
|
|
|
27
27
|
stableAnchorStyle,
|
|
28
28
|
Switch,
|
|
29
29
|
SymbioteHostPropsDirective,
|
|
30
|
+
SymbioteStyleInputDirective,
|
|
30
31
|
Text,
|
|
31
32
|
TextInput,
|
|
32
33
|
TouchableHighlight,
|
|
@@ -249,7 +250,6 @@ export type {
|
|
|
249
250
|
} from '@symbiote-native/engine';
|
|
250
251
|
export {
|
|
251
252
|
dlog,
|
|
252
|
-
flattenStyle,
|
|
253
253
|
isDebug,
|
|
254
254
|
Platform,
|
|
255
255
|
processColor,
|
|
@@ -259,3 +259,32 @@ export {
|
|
|
259
259
|
StyleSheet,
|
|
260
260
|
} from '@symbiote-native/engine';
|
|
261
261
|
export type { IRootTag, ISymbioteEvent, ISymbioteNode } from '@symbiote-native/engine';
|
|
262
|
+
// The agnostic style / platform / view-config types behind the values above. Angular takes its
|
|
263
|
+
// component props as @Input()s and exports no per-component prop type, but an app still needs
|
|
264
|
+
// these to type a style object or a Platform.select spec.
|
|
265
|
+
export type {
|
|
266
|
+
IViewStyle,
|
|
267
|
+
ITextStyle,
|
|
268
|
+
IFlexAlign,
|
|
269
|
+
IFlexJustify,
|
|
270
|
+
IPlatformStatic,
|
|
271
|
+
IPlatformOSType,
|
|
272
|
+
IPlatformConstantsIOS,
|
|
273
|
+
IPlatformConstantsAndroid,
|
|
274
|
+
IPlatformSelectSpec,
|
|
275
|
+
INativeViewConfig,
|
|
276
|
+
INativeViewConfigSource,
|
|
277
|
+
} from '@symbiote-native/engine';
|
|
278
|
+
export type {
|
|
279
|
+
IAccessibilityProps,
|
|
280
|
+
IAriaProps,
|
|
281
|
+
IAccessibilityRole,
|
|
282
|
+
IRole,
|
|
283
|
+
IAccessibilityStateValue,
|
|
284
|
+
IAccessibilityValue,
|
|
285
|
+
IAccessibilityActionInfo,
|
|
286
|
+
IResponderProps,
|
|
287
|
+
IPressState,
|
|
288
|
+
IPressableAndroidRippleConfig,
|
|
289
|
+
IImageStatics,
|
|
290
|
+
} from '@symbiote-native/components';
|