@symbiote-native/angular 0.6.0 → 0.6.2
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/babel-register-composed.cjs +11 -3
- 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/components/input-accessory-view/index.d.ts +2 -1
- package/build/angular/components/input-accessory-view/index.js +28 -39
- package/build/angular/components/input-accessory-view/index.js.map +1 -1
- package/build/angular/components/scroll-view/projection.js +70 -48
- package/build/angular/components/scroll-view/projection.js.map +1 -1
- package/build/angular/components/scroll-view/shared.js +28 -25
- package/build/angular/components/scroll-view/shared.js.map +1 -1
- package/build/angular/components/scroll-view/sticky-header.d.ts +6 -7
- package/build/angular/components/scroll-view/sticky-header.js +82 -65
- package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
- package/build/angular/components/touchable/index.d.ts +2 -5
- package/build/angular/components/touchable/index.js +36 -56
- package/build/angular/components/touchable/index.js.map +1 -1
- package/build/angular/components/virtualized-list/index.d.ts +5 -20
- package/build/angular/components/virtualized-list/index.js +145 -275
- package/build/angular/components/virtualized-list/index.js.map +1 -1
- package/build/angular/components/virtualized-section-list/index.js +2 -4
- package/build/angular/components/virtualized-section-list/index.js.map +1 -1
- package/build/angular/index.d.ts +2 -1
- package/build/angular/index.js +6 -1
- package/build/angular/index.js.map +1 -1
- package/build/angular/renderer/index.d.ts +0 -1
- package/build/angular/renderer/index.js +5 -60
- package/build/angular/renderer/index.js.map +1 -1
- package/package.json +8 -4
- package/src/anchor-host-registry.ts +88 -0
- package/src/components/input-accessory-view/index.ts +30 -39
- package/src/components/scroll-view/projection.ts +76 -46
- package/src/components/scroll-view/shared.ts +27 -19
- package/src/components/scroll-view/sticky-header.ts +89 -64
- package/src/components/touchable/index.ts +52 -66
- package/src/components/virtualized-list/index.ts +157 -368
- package/src/components/virtualized-section-list/index.ts +6 -3
- package/src/index.ts +6 -1
- package/src/renderer/index.ts +5 -63
- package/build/bootstrap.d.ts +0 -7
- package/build/bootstrap.js +0 -14
- package/build/components/activity-indicator/index.android.d.ts +0 -9
- package/build/components/activity-indicator/index.android.js +0 -64
- package/build/components/activity-indicator/index.d.ts +0 -1
- package/build/components/activity-indicator/index.ios.d.ts +0 -5
- package/build/components/activity-indicator/index.ios.js +0 -64
- package/build/components/activity-indicator/index.js +0 -3
- package/build/components/activity-indicator/shared.d.ts +0 -64
- package/build/components/activity-indicator/shared.js +0 -424
- package/build/components/button.d.ts +0 -60
- package/build/components/button.js +0 -456
- package/build/components/flat-list/index.d.ts +0 -119
- package/build/components/flat-list/index.js +0 -691
- package/build/components/image/index.android.d.ts +0 -7
- package/build/components/image/index.android.js +0 -88
- package/build/components/image/index.d.ts +0 -1
- package/build/components/image/index.ios.d.ts +0 -7
- package/build/components/image/index.ios.js +0 -88
- package/build/components/image/index.js +0 -2
- package/build/components/image/shared.d.ts +0 -101
- package/build/components/image/shared.js +0 -405
- package/build/components/image-background.d.ts +0 -78
- package/build/components/image-background.js +0 -583
- package/build/components/input-accessory-view/index.d.ts +0 -56
- package/build/components/input-accessory-view/index.js +0 -403
- package/build/components/keyboard-avoiding-view/index.d.ts +0 -79
- package/build/components/keyboard-avoiding-view/index.js +0 -504
- package/build/components/modal/index.d.ts +0 -89
- package/build/components/modal/index.js +0 -529
- package/build/components/pressable/index.d.ts +0 -120
- package/build/components/pressable/index.js +0 -639
- package/build/components/refresh-control/index.d.ts +0 -83
- package/build/components/refresh-control/index.js +0 -509
- package/build/components/safe-area-view/index.d.ts +0 -54
- package/build/components/safe-area-view/index.js +0 -388
- package/build/components/scroll-view/index.android.d.ts +0 -16
- package/build/components/scroll-view/index.android.js +0 -228
- package/build/components/scroll-view/index.d.ts +0 -3
- package/build/components/scroll-view/index.ios.d.ts +0 -6
- package/build/components/scroll-view/index.ios.js +0 -113
- package/build/components/scroll-view/index.js +0 -5
- package/build/components/scroll-view/projection.d.ts +0 -32
- package/build/components/scroll-view/projection.js +0 -311
- package/build/components/scroll-view/shared.d.ts +0 -225
- package/build/components/scroll-view/shared.js +0 -757
- package/build/components/scroll-view/sticky-header.d.ts +0 -38
- package/build/components/scroll-view/sticky-header.js +0 -257
- package/build/components/section-list/index.d.ts +0 -113
- package/build/components/section-list/index.js +0 -768
- package/build/components/switch/index.android.d.ts +0 -17
- package/build/components/switch/index.android.js +0 -107
- package/build/components/switch/index.d.ts +0 -1
- package/build/components/switch/index.ios.d.ts +0 -16
- package/build/components/switch/index.ios.js +0 -106
- package/build/components/switch/index.js +0 -3
- package/build/components/switch/shared.d.ts +0 -37
- package/build/components/switch/shared.js +0 -176
- package/build/components/text-input.d.ts +0 -150
- package/build/components/text-input.js +0 -821
- package/build/components/touchable/index.d.ts +0 -241
- package/build/components/touchable/index.js +0 -1672
- package/build/components/touchable-native-feedback/index.d.ts +0 -80
- package/build/components/touchable-native-feedback/index.js +0 -545
- package/build/components/virtualized-list/directives.d.ts +0 -41
- package/build/components/virtualized-list/directives.js +0 -249
- package/build/components/virtualized-list/index.d.ts +0 -265
- package/build/components/virtualized-list/index.js +0 -1343
- package/build/components/virtualized-section-list/directives.d.ts +0 -28
- package/build/components/virtualized-section-list/directives.js +0 -148
- package/build/components/virtualized-section-list/index.d.ts +0 -175
- package/build/components/virtualized-section-list/index.js +0 -854
- package/build/components.d.ts +0 -39
- package/build/components.js +0 -37
- package/build/create-portal/index.d.ts +0 -17
- package/build/create-portal/index.js +0 -128
- package/build/create-tunnel/index.d.ts +0 -29
- package/build/create-tunnel/index.js +0 -181
- package/build/descriptor-to-angular/index.d.ts +0 -19
- package/build/descriptor-to-angular/index.js +0 -186
- package/build/host-instance/index.d.ts +0 -2
- package/build/host-instance/index.js +0 -36
- package/build/index.d.ts +0 -21
- package/build/index.js +0 -40
- package/build/modules/animated/animated-leaf-binder/index.d.ts +0 -14
- package/build/modules/animated/animated-leaf-binder/index.js +0 -90
- package/build/modules/animated/animated-leaf-binder.d.ts +0 -14
- package/build/modules/animated/animated-leaf-binder.js +0 -90
- package/build/modules/animated/create-animated-component.d.ts +0 -48
- package/build/modules/animated/create-animated-component.js +0 -425
- package/build/modules/animated/index.d.ts +0 -51
- package/build/modules/animated/index.js +0 -58
- package/build/modules/app-registry/index.d.ts +0 -7
- package/build/modules/app-registry/index.js +0 -22
- package/build/modules/status-bar/index.d.ts +0 -17
- package/build/modules/status-bar/index.js +0 -121
- package/build/primitives/index.d.ts +0 -32
- package/build/primitives/index.js +0 -386
- package/build/primitives/shared.d.ts +0 -83
- package/build/primitives/shared.js +0 -230
- package/build/render/index.d.ts +0 -11
- package/build/render/index.js +0 -170
- package/build/renderer/index.d.ts +0 -38
- package/build/renderer/index.js +0 -355
- package/build/services/color-scheme.service.d.ts +0 -13
- package/build/services/color-scheme.service.js +0 -72
- package/build/services/index.d.ts +0 -2
- package/build/services/index.js +0 -2
- package/build/services/window-dimensions.service.d.ts +0 -13
- package/build/services/window-dimensions.service.js +0 -83
- 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.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 -202
- package/src/services/services.test.ts +0 -163
|
@@ -15,10 +15,13 @@ import {
|
|
|
15
15
|
type ISymbioteNode,
|
|
16
16
|
} from '@symbiote-native/engine';
|
|
17
17
|
import {
|
|
18
|
-
|
|
18
|
+
createInitialStickyState,
|
|
19
19
|
readLayoutNumber,
|
|
20
|
-
|
|
20
|
+
reduceSticky,
|
|
21
21
|
STICKY_HEADER_Z_INDEX,
|
|
22
|
+
type IStickyAction,
|
|
23
|
+
type IStickyEffect,
|
|
24
|
+
type IStickyReducerInputs,
|
|
22
25
|
} from '@symbiote-native/components';
|
|
23
26
|
import { descriptorFor } from '@symbiote-native/components';
|
|
24
27
|
import { Platform } from '@symbiote-native/engine';
|
|
@@ -64,17 +67,20 @@ function isProjectedRefreshControl(node: ISymbioteNode): boolean {
|
|
|
64
67
|
return isAnchor(node) && node.children.some(isProjectedRefreshControl);
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
// The SECOND Angular sticky effect-runner (auto-projected children can't be arbitrary Angular
|
|
71
|
+
// component classes, so the controller drives engine nodes directly). It shares the ONE
|
|
72
|
+
// reduceSticky state machine with ScrollViewStickyHeader — the DECISIONS (zero-swallow gate, debounce
|
|
73
|
+
// delay, rebuild ranges, cross-talk record) live there; this runner only EXECUTES the effects on the
|
|
74
|
+
// engine node (build interpolation + wire listener, hold the debounce timer, re-apply the node props).
|
|
75
|
+
// Because it owns the child index, it consumes the reducer's record-header-y effect for cross-talk.
|
|
67
76
|
class StickyProjectionWrapper {
|
|
68
|
-
private
|
|
69
|
-
private layoutY = 0;
|
|
70
|
-
private layoutHeight = 0;
|
|
77
|
+
private readonly state = createInitialStickyState();
|
|
71
78
|
private animatedTranslateY: AnimatedInterpolation;
|
|
79
|
+
private interpolation: AnimatedInterpolation | undefined;
|
|
72
80
|
private animatedProps: AnimatedProps | undefined;
|
|
73
81
|
private cancelBind: (() => void) | undefined;
|
|
74
82
|
private listenerId: string | undefined;
|
|
75
83
|
private debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
76
|
-
private translateY: number | null = null;
|
|
77
|
-
private haveReceivedInitialZeroTranslateY = true;
|
|
78
84
|
|
|
79
85
|
constructor(
|
|
80
86
|
private readonly controller: ScrollViewProjectionController,
|
|
@@ -85,14 +91,14 @@ class StickyProjectionWrapper {
|
|
|
85
91
|
inputRange: [-1, 0],
|
|
86
92
|
outputRange: [0, 0],
|
|
87
93
|
});
|
|
88
|
-
this.
|
|
94
|
+
this.dispatch({ kind: 'inputs-changed' });
|
|
89
95
|
}
|
|
90
96
|
|
|
91
97
|
destroy(): void {
|
|
92
98
|
this.cancelBind?.();
|
|
93
99
|
this.cancelBind = undefined;
|
|
94
|
-
if (this.listenerId !== undefined) {
|
|
95
|
-
this.
|
|
100
|
+
if (this.interpolation !== undefined && this.listenerId !== undefined) {
|
|
101
|
+
this.interpolation.removeListener(this.listenerId);
|
|
96
102
|
this.listenerId = undefined;
|
|
97
103
|
}
|
|
98
104
|
if (this.debounceTimer !== undefined) {
|
|
@@ -105,53 +111,77 @@ class StickyProjectionWrapper {
|
|
|
105
111
|
}
|
|
106
112
|
}
|
|
107
113
|
|
|
114
|
+
// The controller calls this when a cross-talk input changed (a sibling recorded its y): rebuild the
|
|
115
|
+
// ranges off the new nextStickyHeaderY.
|
|
108
116
|
rebuild(): void {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
this.listenerId = undefined;
|
|
112
|
-
}
|
|
117
|
+
this.dispatch({ kind: 'inputs-changed' });
|
|
118
|
+
}
|
|
113
119
|
|
|
114
|
-
|
|
115
|
-
|
|
120
|
+
private inputs(): IStickyReducerInputs {
|
|
121
|
+
return {
|
|
122
|
+
os: Platform.OS,
|
|
116
123
|
inverted: this.controller.config.invertStickyHeaders,
|
|
117
124
|
scrollViewHeight: this.controller.config.scrollViewHeight,
|
|
118
|
-
layoutY: this.layoutY,
|
|
119
|
-
layoutHeight: this.layoutHeight,
|
|
120
125
|
nextHeaderLayoutY: this.controller.nextStickyHeaderY(this.childIndex),
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
this.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
)
|
|
126
|
+
index: this.childIndex,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private dispatch(action: IStickyAction): void {
|
|
131
|
+
this.runEffects(reduceSticky(this.state, action, this.inputs()).effects);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private runEffects(effects: IStickyEffect[]): void {
|
|
135
|
+
for (const effect of effects) {
|
|
136
|
+
switch (effect.kind) {
|
|
137
|
+
case 'rebuild-interpolation': {
|
|
138
|
+
if (this.interpolation !== undefined && this.listenerId !== undefined) {
|
|
139
|
+
this.interpolation.removeListener(this.listenerId);
|
|
140
|
+
this.listenerId = undefined;
|
|
141
|
+
}
|
|
142
|
+
const next = this.controller.config.scrollAnimatedValue.interpolate({
|
|
143
|
+
inputRange: effect.inputRange,
|
|
144
|
+
outputRange: effect.outputRange,
|
|
145
|
+
});
|
|
146
|
+
this.listenerId = next.addListener(this.animatedValueListener);
|
|
147
|
+
this.interpolation = next;
|
|
148
|
+
this.animatedTranslateY = next;
|
|
149
|
+
this.applyProps();
|
|
150
|
+
dlog(
|
|
151
|
+
`Angular ScrollView projection sticky index=${this.childIndex} measured=${this.state.measured} y=${this.state.layoutY}`,
|
|
152
|
+
);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case 'schedule-debounce':
|
|
156
|
+
if (this.debounceTimer !== undefined) clearTimeout(this.debounceTimer);
|
|
157
|
+
this.debounceTimer = setTimeout(() => {
|
|
158
|
+
this.debounceTimer = undefined;
|
|
159
|
+
this.dispatch({ kind: 'debounce-fired', value: effect.value });
|
|
160
|
+
}, effect.delay);
|
|
161
|
+
break;
|
|
162
|
+
case 'apply-passthrough':
|
|
163
|
+
this.applyProps();
|
|
164
|
+
break;
|
|
165
|
+
case 'record-header-y':
|
|
166
|
+
this.controller.recordHeaderLayoutY(effect.index, effect.y);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
131
170
|
}
|
|
132
171
|
|
|
133
172
|
private readonly onLayout = (event: ISymbioteEvent): void => {
|
|
134
173
|
const y = readLayoutNumber(event, 'y');
|
|
135
174
|
const height = readLayoutNumber(event, 'height');
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
175
|
+
// Keep the previous value when a field is absent (RN sets state only on a defined read).
|
|
176
|
+
this.dispatch({
|
|
177
|
+
kind: 'layout',
|
|
178
|
+
y: y ?? this.state.layoutY,
|
|
179
|
+
height: height ?? this.state.layoutHeight,
|
|
180
|
+
});
|
|
141
181
|
};
|
|
142
182
|
|
|
143
183
|
private readonly animatedValueListener = ({ value }: { value: number | string }): void => {
|
|
144
|
-
if (typeof value
|
|
145
|
-
if (value === 0 && !this.haveReceivedInitialZeroTranslateY) {
|
|
146
|
-
this.haveReceivedInitialZeroTranslateY = true;
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
if (this.debounceTimer !== undefined) clearTimeout(this.debounceTimer);
|
|
150
|
-
this.debounceTimer = setTimeout(() => {
|
|
151
|
-
this.translateY = value;
|
|
152
|
-
if (value !== 0) this.haveReceivedInitialZeroTranslateY = false;
|
|
153
|
-
this.applyProps();
|
|
154
|
-
}, stickyDebounceMs(Platform.OS));
|
|
184
|
+
if (typeof value === 'number') this.dispatch({ kind: 'animated-tick', value });
|
|
155
185
|
};
|
|
156
186
|
|
|
157
187
|
private props(): Record<string, unknown> {
|
|
@@ -160,9 +190,9 @@ class StickyProjectionWrapper {
|
|
|
160
190
|
zIndex: STICKY_HEADER_Z_INDEX,
|
|
161
191
|
};
|
|
162
192
|
const passthrough =
|
|
163
|
-
this.translateY === null
|
|
193
|
+
this.state.translateY === null
|
|
164
194
|
? undefined
|
|
165
|
-
: { transform: [{ translateY: this.translateY }], zIndex: STICKY_HEADER_Z_INDEX };
|
|
195
|
+
: { transform: [{ translateY: this.state.translateY }], zIndex: STICKY_HEADER_Z_INDEX };
|
|
166
196
|
return {
|
|
167
197
|
style: passthrough === undefined ? style : [style, passthrough],
|
|
168
198
|
onLayout: this.onLayout,
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
readLayoutDimension,
|
|
39
39
|
resolveAccessibilityProps,
|
|
40
40
|
resolveDecelerationRate,
|
|
41
|
+
resolveScrollForwarding,
|
|
41
42
|
selectScrollIntrinsics,
|
|
42
43
|
splitLayoutProps,
|
|
43
44
|
type IAccessibilityProps,
|
|
@@ -566,27 +567,34 @@ export abstract class ScrollViewBase implements IAngularScrollViewInputs, AfterV
|
|
|
566
567
|
// by the post-commit attach below, so onScroll forwards to the user with throttle 1
|
|
567
568
|
// (ScrollView.js:1798). Without it, Animated.event drives the value each frame (the JS jitter)
|
|
568
569
|
// and forwards the user handler as the listener passthrough.
|
|
570
|
+
// The scroll-forwarding DECISIONS (which onScroll path, the 1/16 throttle defaults, whether to
|
|
571
|
+
// capture the viewport height) are folded out to the shared resolveScrollForwarding; here Angular
|
|
572
|
+
// only EXECUTES them, keeping its stable-reference handlers (stickyOnScrollHandler /
|
|
573
|
+
// handleInvertedStickyLayout / emitterCallback) so a fresh closure never forces a re-clone cascade.
|
|
569
574
|
const nativeStickyAvailable = this.hasStickyHeaders && isNativeAnimatedAvailable();
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
575
|
+
const forwarding = resolveScrollForwarding({
|
|
576
|
+
hasStickyHeaders: this.hasStickyHeaders,
|
|
577
|
+
nativeStickyAvailable,
|
|
578
|
+
invertStickyHeaders: this.invertStickyHeaders,
|
|
579
|
+
scrollEventThrottle: this.scrollEventThrottle,
|
|
580
|
+
maintainVisibleContentPosition: this.maintainVisibleContentPosition,
|
|
581
|
+
snapToAlignment: this.snapToAlignment,
|
|
582
|
+
});
|
|
583
|
+
// onScroll: the JS-fallback path uses the cached Animated.event handler; the native + plain paths
|
|
584
|
+
// forward the user handler as-is (the native driver attaches the value on the UI thread).
|
|
585
|
+
if (forwarding.mode === 'sticky-js') {
|
|
586
|
+
bag.onScroll = this.stickyOnScrollHandler(this.onScroll);
|
|
587
|
+
} else if (this.onScroll !== undefined) {
|
|
588
|
+
bag.onScroll = this.onScroll;
|
|
589
|
+
}
|
|
590
|
+
if (forwarding.scrollEventThrottle !== undefined) {
|
|
591
|
+
bag.scrollEventThrottle = forwarding.scrollEventThrottle;
|
|
592
|
+
}
|
|
593
|
+
// Inverted sticky headers need the viewport height (RN _handleLayout): capture it on the
|
|
594
|
+
// scroll-view onLayout, then call the user's handler. Otherwise forward the layout emitter.
|
|
595
|
+
if (forwarding.capturesViewportHeight) {
|
|
596
|
+
bag.onLayout = this.handleInvertedStickyLayout;
|
|
586
597
|
} else {
|
|
587
|
-
if (this.onScroll !== undefined) bag.onScroll = this.onScroll;
|
|
588
|
-
if (this.scrollEventThrottle !== undefined)
|
|
589
|
-
bag.scrollEventThrottle = this.scrollEventThrottle;
|
|
590
598
|
const layoutCallback = this.emitterCallback(this.layout);
|
|
591
599
|
if (layoutCallback !== undefined) bag.onLayout = layoutCallback;
|
|
592
600
|
}
|
|
@@ -37,11 +37,14 @@ import {
|
|
|
37
37
|
type ISymbioteEvent,
|
|
38
38
|
} from '@symbiote-native/engine';
|
|
39
39
|
import {
|
|
40
|
-
|
|
40
|
+
createInitialStickyState,
|
|
41
41
|
readLayoutNumber,
|
|
42
|
-
|
|
42
|
+
reduceSticky,
|
|
43
43
|
STICKY_HEADER_Z_INDEX,
|
|
44
|
+
type IStickyAction,
|
|
45
|
+
type IStickyEffect,
|
|
44
46
|
type IStickyHeaderProps,
|
|
47
|
+
type IStickyReducerInputs,
|
|
45
48
|
} from '@symbiote-native/components';
|
|
46
49
|
import { AnimatedView } from '../../modules/animated';
|
|
47
50
|
import { anchorHostStyle } from '../../primitives';
|
|
@@ -90,19 +93,21 @@ export class ScrollViewStickyHeader
|
|
|
90
93
|
// previous header learns its collision point. Called after the header measures.
|
|
91
94
|
@Output() readonly layout = new EventEmitter<ISymbioteEvent>();
|
|
92
95
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
//
|
|
97
|
-
|
|
96
|
+
// The one folded state cell, mutated in place by reduceSticky. The DECISIONS — zero-swallow gate,
|
|
97
|
+
// debounce delay, rebuild ranges — all live there; this component supplies only the Angular
|
|
98
|
+
// lifecycle (inputs + manual change detection), the interpolation-node + listener wiring, the
|
|
99
|
+
// debounce setTimeout, and the markForCheck re-render.
|
|
100
|
+
private readonly state = createInitialStickyState();
|
|
101
|
+
// The animated node driving the transform (RN's animatedTranslateY), replaced by the
|
|
102
|
+
// rebuild-interpolation effect. When the scroll value is native, this interpolation runs on the UI
|
|
103
|
+
// thread: the smooth pin.
|
|
98
104
|
private animatedTranslateY: AnimatedInterpolation = new AnimatedValue(0).interpolate({
|
|
99
105
|
inputRange: [-1, 0],
|
|
100
106
|
outputRange: [0, 0],
|
|
101
107
|
});
|
|
102
|
-
// The
|
|
103
|
-
//
|
|
104
|
-
private
|
|
105
|
-
private haveReceivedInitialZeroTranslateY = true;
|
|
108
|
+
// The current interpolation node + its listener id, held so the next rebuild detaches the old
|
|
109
|
+
// listener (an engine call the reducer does not own) and ngOnDestroy cleans up.
|
|
110
|
+
private interpolation: AnimatedInterpolation | undefined;
|
|
106
111
|
private debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
107
112
|
private listenerId: string | undefined;
|
|
108
113
|
|
|
@@ -113,21 +118,17 @@ export class ScrollViewStickyHeader
|
|
|
113
118
|
private readonly elementRef = inject(ElementRef);
|
|
114
119
|
|
|
115
120
|
ngOnInit(): void {
|
|
116
|
-
this.
|
|
117
|
-
inputRange: [-1, 0],
|
|
118
|
-
outputRange: [0, 0],
|
|
119
|
-
});
|
|
120
|
-
this.rebuildInterpolation();
|
|
121
|
+
this.dispatch({ kind: 'inputs-changed' });
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
// Rebuild on any layout/collision input change (RN's effect deps: measured, layoutY, layoutHeight,
|
|
124
125
|
// scrollViewHeight, nextHeaderLayoutY, inverted). scrollAnimatedValue is stable for one ScrollView.
|
|
125
126
|
ngOnChanges(): void {
|
|
126
|
-
this.
|
|
127
|
+
this.dispatch({ kind: 'inputs-changed' });
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
ngOnDestroy(): void {
|
|
130
|
-
this.
|
|
131
|
+
this.teardown();
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
get headerStyle(): unknown {
|
|
@@ -161,65 +162,89 @@ export class ScrollViewStickyHeader
|
|
|
161
162
|
// The EXPLICIT debounced translateY overrides the committed transform for hit-testing while
|
|
162
163
|
// animatedTranslateY does the smooth (native-driven) pin (RN ScrollViewStickyHeader.js).
|
|
163
164
|
get passthrough(): { style: { transform: Array<{ translateY: number }> } } | null {
|
|
164
|
-
return this.translateY !== null
|
|
165
|
-
? { style: { transform: [{ translateY: this.translateY }] } }
|
|
165
|
+
return this.state.translateY !== null
|
|
166
|
+
? { style: { transform: [{ translateY: this.state.translateY }] } }
|
|
166
167
|
: null;
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
private inputs(): IStickyReducerInputs {
|
|
171
|
+
return {
|
|
172
|
+
os: Platform.OS,
|
|
173
|
+
inverted: this.inverted,
|
|
174
|
+
scrollViewHeight: this.scrollViewHeight,
|
|
175
|
+
nextHeaderLayoutY: this.nextHeaderLayoutY,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private dispatch(action: IStickyAction): void {
|
|
180
|
+
this.runEffects(reduceSticky(this.state, action, this.inputs()).effects);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private runEffects(effects: IStickyEffect[]): void {
|
|
184
|
+
for (const effect of effects) {
|
|
185
|
+
switch (effect.kind) {
|
|
186
|
+
case 'rebuild-interpolation': {
|
|
187
|
+
// Detach the old listener, build a fresh interpolation onto the shared scroll value, and
|
|
188
|
+
// wire the settled-value listener (symbiote is always Fabric; RN attaches it only there).
|
|
189
|
+
if (this.interpolation !== undefined && this.listenerId !== undefined) {
|
|
190
|
+
this.interpolation.removeListener(this.listenerId);
|
|
191
|
+
this.listenerId = undefined;
|
|
192
|
+
}
|
|
193
|
+
const next = this.scrollAnimatedValue.interpolate({
|
|
194
|
+
inputRange: effect.inputRange,
|
|
195
|
+
outputRange: effect.outputRange,
|
|
196
|
+
});
|
|
197
|
+
this.listenerId = next.addListener(this.animatedValueListener);
|
|
198
|
+
this.interpolation = next;
|
|
199
|
+
this.animatedTranslateY = next;
|
|
200
|
+
dlog(
|
|
201
|
+
`Angular ScrollViewStickyHeader interpolation measured=${this.state.measured} y=${this.state.layoutY}`,
|
|
202
|
+
);
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
case 'schedule-debounce':
|
|
206
|
+
// The animated value updates several times per frame; debounce the settled value into the
|
|
207
|
+
// committed transform so hit detection stays current (a Fabric issue, worse on Android).
|
|
208
|
+
if (this.debounceTimer !== undefined) clearTimeout(this.debounceTimer);
|
|
209
|
+
this.debounceTimer = setTimeout(() => {
|
|
210
|
+
this.debounceTimer = undefined;
|
|
211
|
+
this.dispatch({ kind: 'debounce-fired', value: effect.value });
|
|
212
|
+
}, effect.delay);
|
|
213
|
+
break;
|
|
214
|
+
case 'apply-passthrough':
|
|
215
|
+
this.changeDetector.markForCheck();
|
|
216
|
+
break;
|
|
217
|
+
case 'record-header-y':
|
|
218
|
+
// The Angular header records through the `layout` EventEmitter (handleLayout below), the
|
|
219
|
+
// public recorder contract; the reducer emits no index for it.
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Record own y/height, mark measured, rebuild the interpolation (all via the reducer), then fire
|
|
226
|
+
// the parent's recorder (RN ScrollViewStickyHeader.js._onLayout order). The wrapped child keeps its
|
|
227
|
+
// own onLayout binding (projected with its bindings intact), so unlike React/Vue we do not forward.
|
|
172
228
|
private handleLayout(event: ISymbioteEvent): void {
|
|
173
229
|
const y = readLayoutNumber(event, 'y');
|
|
174
230
|
const height = readLayoutNumber(event, 'height');
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
231
|
+
// Keep the previous value when a field is absent (RN sets state only on a defined read).
|
|
232
|
+
this.dispatch({
|
|
233
|
+
kind: 'layout',
|
|
234
|
+
y: y ?? this.state.layoutY,
|
|
235
|
+
height: height ?? this.state.layoutHeight,
|
|
236
|
+
});
|
|
179
237
|
this.layout.emit(event);
|
|
180
238
|
this.changeDetector.markForCheck();
|
|
181
239
|
}
|
|
182
240
|
|
|
183
|
-
// The animated value updates several times per frame during scroll; debounce it and push the
|
|
184
|
-
// settled value into the committed transform so hit detection stays current (a Fabric-only issue,
|
|
185
|
-
// worse on Android). A freshly-rebuilt interpolation re-emits 0; swallow that first zero (RN).
|
|
186
241
|
private readonly animatedValueListener = ({ value }: { value: number | string }): void => {
|
|
187
|
-
if (typeof value
|
|
188
|
-
if (value === 0 && !this.haveReceivedInitialZeroTranslateY) {
|
|
189
|
-
this.haveReceivedInitialZeroTranslateY = true;
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
if (this.debounceTimer !== undefined) clearTimeout(this.debounceTimer);
|
|
193
|
-
this.debounceTimer = setTimeout(() => {
|
|
194
|
-
this.translateY = value;
|
|
195
|
-
if (value !== 0) this.haveReceivedInitialZeroTranslateY = false;
|
|
196
|
-
this.changeDetector.markForCheck();
|
|
197
|
-
}, stickyDebounceMs(Platform.OS));
|
|
242
|
+
if (typeof value === 'number') this.dispatch({ kind: 'animated-tick', value });
|
|
198
243
|
};
|
|
199
244
|
|
|
200
|
-
private
|
|
201
|
-
this.
|
|
202
|
-
|
|
203
|
-
measured: this.measured,
|
|
204
|
-
inverted: this.inverted,
|
|
205
|
-
scrollViewHeight: this.scrollViewHeight,
|
|
206
|
-
layoutY: this.layoutY,
|
|
207
|
-
layoutHeight: this.layoutHeight,
|
|
208
|
-
nextHeaderLayoutY: this.nextHeaderLayoutY,
|
|
209
|
-
});
|
|
210
|
-
const interpolation = this.scrollAnimatedValue.interpolate({ inputRange, outputRange });
|
|
211
|
-
// symbiote is always Fabric: listen to the settled value to keep the ShadowTree transform
|
|
212
|
-
// current for hit-testing (RN attaches this listener only under Fabric).
|
|
213
|
-
this.listenerId = interpolation.addListener(this.animatedValueListener);
|
|
214
|
-
this.animatedTranslateY = interpolation;
|
|
215
|
-
dlog(
|
|
216
|
-
`Angular ScrollViewStickyHeader interpolation measured=${this.measured} y=${this.layoutY}`,
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
private teardownInterpolation(): void {
|
|
221
|
-
if (this.listenerId !== undefined) {
|
|
222
|
-
this.animatedTranslateY.removeListener(this.listenerId);
|
|
245
|
+
private teardown(): void {
|
|
246
|
+
if (this.interpolation !== undefined && this.listenerId !== undefined) {
|
|
247
|
+
this.interpolation.removeListener(this.listenerId);
|
|
223
248
|
this.listenerId = undefined;
|
|
224
249
|
}
|
|
225
250
|
if (this.debounceTimer !== undefined) {
|
|
@@ -33,7 +33,9 @@ import {
|
|
|
33
33
|
Output,
|
|
34
34
|
} from '@angular/core';
|
|
35
35
|
import {
|
|
36
|
-
|
|
36
|
+
createTouchableFeedbackHandlers,
|
|
37
|
+
createTouchableFeedbackRuntime,
|
|
38
|
+
highlightPressedStyle,
|
|
37
39
|
DEFAULT_ACTIVE_OPACITY,
|
|
38
40
|
DEFAULT_HIGHLIGHT_CHILD_OPACITY,
|
|
39
41
|
DEFAULT_MIN_PRESS_DURATION_MS,
|
|
@@ -48,13 +50,9 @@ import {
|
|
|
48
50
|
type IPressState,
|
|
49
51
|
type IPressTimingProps,
|
|
50
52
|
type IRectOffset,
|
|
53
|
+
type ITouchableFeedbackHandlers,
|
|
51
54
|
} from '@symbiote-native/components';
|
|
52
|
-
import {
|
|
53
|
-
dlog,
|
|
54
|
-
type ISymbioteEvent,
|
|
55
|
-
type IStyleProp,
|
|
56
|
-
type IViewStyle,
|
|
57
|
-
} from '@symbiote-native/engine';
|
|
55
|
+
import { type ISymbioteEvent, type IStyleProp, type IViewStyle } from '@symbiote-native/engine';
|
|
58
56
|
import { anchorHostStyle, anchorStyleProp } from '../../primitives';
|
|
59
57
|
import { Pressable, type IAngularPressableInputs } from '../pressable';
|
|
60
58
|
import { Animated, AnimatedView } from '../../modules/animated';
|
|
@@ -80,6 +78,13 @@ export type IAngularTouchableHighlightProps = IAngularTouchableBaseProps & {
|
|
|
80
78
|
|
|
81
79
|
export type IAngularTouchableWithoutFeedbackProps = IAngularTouchableBaseProps;
|
|
82
80
|
|
|
81
|
+
// The real setTimeout the shared feedback machine schedules its deferred activation/deactivation on
|
|
82
|
+
// (core/components has no timer globals). Returns a canceller so an early release flushes the timer.
|
|
83
|
+
function scheduleTimeout(callback: () => void, ms: number): () => void {
|
|
84
|
+
const id = setTimeout(callback, ms);
|
|
85
|
+
return () => clearTimeout(id);
|
|
86
|
+
}
|
|
87
|
+
|
|
83
88
|
@Component({
|
|
84
89
|
selector: 'TouchableOpacity',
|
|
85
90
|
standalone: true,
|
|
@@ -232,10 +237,9 @@ export class TouchableOpacity implements IAngularTouchableOpacityProps {
|
|
|
232
237
|
// engine object, never an @Input / reactive wrap); the <symbiote-animated-view> leaf rasterizes
|
|
233
238
|
// it for the first paint and drives it through setNativeProps every frame.
|
|
234
239
|
private readonly opacity = new Animated.Value(RESTING_OPACITY);
|
|
235
|
-
// The
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
private activatedAt: number | undefined;
|
|
240
|
+
// The shared press-scheduling cell (delayPressIn timer + activation clock), persisted on the
|
|
241
|
+
// instance; the machine's handlers are rebuilt per event over live @Input()s.
|
|
242
|
+
private readonly runtime = createTouchableFeedbackRuntime();
|
|
239
243
|
// This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
|
|
240
244
|
// onto (see anchorHostStyle's doc comment) — NOT the <symbiote-animated-view> leaf one level down.
|
|
241
245
|
private readonly elementRef = inject(ElementRef);
|
|
@@ -257,61 +261,43 @@ export class TouchableOpacity implements IAngularTouchableOpacityProps {
|
|
|
257
261
|
}).start();
|
|
258
262
|
}
|
|
259
263
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
264
|
+
// Built per event so the machine reads live @Input()s (delay/opacity); the runtime persists across
|
|
265
|
+
// calls. The shared machine owns the scheduling — the adapter supplies only the native seam: the
|
|
266
|
+
// Animated opacity fade + the @Output() emit, as activate/deactivate.
|
|
267
|
+
private feedbackHandlers(): ITouchableFeedbackHandlers {
|
|
268
|
+
return createTouchableFeedbackHandlers(
|
|
269
|
+
{
|
|
270
|
+
delayPressIn: this.delayPressIn ?? 0,
|
|
271
|
+
delayPressOut: this.delayPressOut ?? 0,
|
|
272
|
+
minPressDuration: this.minPressDuration ?? DEFAULT_MIN_PRESS_DURATION_MS,
|
|
273
|
+
schedule: scheduleTimeout,
|
|
274
|
+
now: Date.now,
|
|
275
|
+
},
|
|
276
|
+
this.runtime,
|
|
277
|
+
{
|
|
278
|
+
activate: (event: ISymbioteEvent): void => {
|
|
279
|
+
this.setOpacityTo(
|
|
280
|
+
this.activeOpacity ?? DEFAULT_ACTIVE_OPACITY,
|
|
281
|
+
OPACITY_ACTIVE_DURATION_MS,
|
|
282
|
+
);
|
|
283
|
+
this.pressIn.emit(event);
|
|
284
|
+
},
|
|
285
|
+
deactivate: (event: ISymbioteEvent): void => {
|
|
286
|
+
this.setOpacityTo(RESTING_OPACITY, OPACITY_INACTIVE_DURATION_MS);
|
|
287
|
+
this.pressOut.emit(event);
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
);
|
|
277
291
|
}
|
|
278
292
|
|
|
279
293
|
// Arrow fields (stable identity for OnPush, `this` intact when Pressable's `(pressIn)` invokes
|
|
280
|
-
// them).
|
|
281
|
-
// pressIn behind the delay (a release before it elapses flushes it synchronously).
|
|
294
|
+
// them). The delayPressIn defer + minPressDuration/delayPressOut hold live in the shared machine.
|
|
282
295
|
handlePressIn = (event: ISymbioteEvent): void => {
|
|
283
|
-
|
|
284
|
-
if (delayPressIn > 0) {
|
|
285
|
-
dlog(`TouchableOpacity pressIn deferred ${delayPressIn}ms`);
|
|
286
|
-
this.pressInTimer = setTimeout(() => {
|
|
287
|
-
this.pressInTimer = undefined;
|
|
288
|
-
this.activate(event);
|
|
289
|
-
}, delayPressIn);
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
this.activate(event);
|
|
296
|
+
this.feedbackHandlers().handlePressIn(event);
|
|
293
297
|
};
|
|
294
298
|
|
|
295
|
-
// delayPressOut + minPressDuration (RN _deactivate): the press-out waits at least minPressDuration
|
|
296
|
-
// past activation (so a fast tap holds the active visual) and at least delayPressOut, whichever is
|
|
297
|
-
// longer.
|
|
298
299
|
handlePressOut = (event: ISymbioteEvent): void => {
|
|
299
|
-
|
|
300
|
-
this.clearPressInTimer();
|
|
301
|
-
this.activate(event);
|
|
302
|
-
}
|
|
303
|
-
const heldFor = this.activatedAt === undefined ? 0 : Date.now() - this.activatedAt;
|
|
304
|
-
const wait = computePressOutWait(
|
|
305
|
-
heldFor,
|
|
306
|
-
this.minPressDuration ?? DEFAULT_MIN_PRESS_DURATION_MS,
|
|
307
|
-
this.delayPressOut ?? 0,
|
|
308
|
-
);
|
|
309
|
-
if (wait > 0) {
|
|
310
|
-
dlog(`TouchableOpacity pressOut deferred ${wait}ms`);
|
|
311
|
-
setTimeout(() => this.deactivate(event), wait);
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
this.deactivate(event);
|
|
300
|
+
this.feedbackHandlers().handlePressOut(event);
|
|
315
301
|
};
|
|
316
302
|
}
|
|
317
303
|
|
|
@@ -473,13 +459,13 @@ export class TouchableHighlight implements IAngularTouchableHighlightProps {
|
|
|
473
459
|
// child opacity (RN drives this with setState, not Animated, so we mirror that faithfully
|
|
474
460
|
// through Pressable's pressed flag, no tween) — the overlay always goes last so it wins.
|
|
475
461
|
pressedStyle = (state: IPressState): IStyleProp<IViewStyle> => {
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
462
|
+
const base: IStyleProp<IViewStyle> = [anchorStyleProp<IViewStyle>(this.elementRef), this.style];
|
|
463
|
+
return highlightPressedStyle(
|
|
464
|
+
state.pressed,
|
|
465
|
+
base,
|
|
466
|
+
this.underlayColor ?? DEFAULT_UNDERLAY_COLOR,
|
|
467
|
+
this.activeOpacity ?? DEFAULT_HIGHLIGHT_CHILD_OPACITY,
|
|
468
|
+
);
|
|
483
469
|
};
|
|
484
470
|
}
|
|
485
471
|
|