@symbiote-native/angular 0.3.3 → 0.5.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/babel-register-composed.cjs +119 -0
- package/bin/symbiote-angular-dev.cjs +43 -1
- package/build/angular/components/image/shared.d.ts +4 -4
- package/build/angular/components/image/shared.js +1 -3
- package/build/angular/components/image/shared.js.map +1 -1
- package/build/angular/components/{input-accessory-view.d.ts → input-accessory-view/index.d.ts} +1 -1
- package/build/angular/components/{input-accessory-view.js → input-accessory-view/index.js} +3 -7
- package/build/angular/components/input-accessory-view/index.js.map +1 -0
- package/build/angular/components/{keyboard-avoiding-view.js → keyboard-avoiding-view/index.js} +3 -8
- package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -0
- package/build/angular/components/{modal.js → modal/index.js} +3 -8
- package/build/angular/components/modal/index.js.map +1 -0
- package/build/angular/components/pressable/index.js +13 -14
- package/build/angular/components/pressable/index.js.map +1 -1
- package/build/angular/components/{refresh-control.js → refresh-control/index.js} +3 -8
- package/build/angular/components/refresh-control/index.js.map +1 -0
- package/build/angular/components/{safe-area-view.d.ts → safe-area-view/index.d.ts} +1 -1
- package/build/angular/components/{safe-area-view.js → safe-area-view/index.js} +3 -7
- package/build/angular/components/safe-area-view/index.js.map +1 -0
- package/build/angular/components/text-input.js +1 -6
- package/build/angular/components/text-input.js.map +1 -1
- package/build/angular/components/{touchable.d.ts → touchable/index.d.ts} +1 -1
- package/build/angular/components/{touchable.js → touchable/index.js} +4 -4
- package/build/angular/components/touchable/index.js.map +1 -0
- package/build/angular/components/{touchable-native-feedback.d.ts → touchable-native-feedback/index.d.ts} +1 -1
- package/build/angular/components/{touchable-native-feedback.js → touchable-native-feedback/index.js} +4 -9
- package/build/angular/components/touchable-native-feedback/index.js.map +1 -0
- package/build/angular/{create-portal.js → create-portal/index.js} +1 -1
- package/build/angular/create-portal/index.js.map +1 -0
- package/build/angular/{create-tunnel.js → create-tunnel/index.js} +1 -1
- package/build/angular/create-tunnel/index.js.map +1 -0
- package/build/angular/index.d.ts +1 -1
- package/build/angular/index.js +1 -1
- package/build/angular/index.js.map +1 -1
- 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 +3 -12
- package/build/angular/modules/animated/create-animated-component.js +48 -113
- package/build/angular/modules/animated/create-animated-component.js.map +1 -1
- package/build/angular/modules/{status-bar.js → status-bar/index.js} +1 -1
- package/build/angular/modules/status-bar/index.js.map +1 -0
- package/build/angular/{render.js → render/index.js} +15 -4
- package/build/angular/render/index.js.map +1 -0
- package/build/{renderer.js → angular/renderer/index.js} +17 -18
- package/build/angular/renderer/index.js.map +1 -0
- package/build/components/image/shared.d.ts +4 -4
- package/build/components/image/shared.js +1 -3
- package/build/components/{input-accessory-view.d.ts → input-accessory-view/index.d.ts} +1 -1
- package/build/components/{input-accessory-view.js → input-accessory-view/index.js} +2 -6
- package/build/components/{keyboard-avoiding-view.js → keyboard-avoiding-view/index.js} +2 -7
- package/build/components/{modal.js → modal/index.js} +2 -7
- package/build/components/pressable/index.js +13 -14
- package/build/components/{refresh-control.js → refresh-control/index.js} +2 -7
- package/build/components/{safe-area-view.d.ts → safe-area-view/index.d.ts} +1 -1
- package/build/components/{safe-area-view.js → safe-area-view/index.js} +2 -6
- package/build/components/text-input.js +1 -6
- package/build/components/{touchable.d.ts → touchable/index.d.ts} +1 -1
- package/build/components/{touchable.js → touchable/index.js} +3 -3
- package/build/components/{touchable-native-feedback.d.ts → touchable-native-feedback/index.d.ts} +1 -1
- package/build/components/{touchable-native-feedback.js → touchable-native-feedback/index.js} +3 -8
- package/build/index.d.ts +1 -1
- package/build/index.js +1 -1
- package/build/modules/animated/animated-leaf-binder/index.d.ts +14 -0
- package/build/modules/animated/animated-leaf-binder/index.js +90 -0
- package/build/modules/animated/animated-leaf-binder.d.ts +14 -0
- package/build/modules/animated/animated-leaf-binder.js +90 -0
- package/build/modules/animated/create-animated-component.d.ts +3 -12
- package/build/modules/animated/create-animated-component.js +47 -112
- package/build/{render.js → render/index.js} +14 -3
- package/build/{angular/renderer.js → renderer/index.js} +16 -19
- package/package.json +10 -5
- package/src/angular-gaps.test.ts +1 -1
- package/src/components/image/shared.ts +6 -5
- package/src/components/{input-accessory-view.ts → input-accessory-view/index.ts} +11 -8
- package/src/components/{input-accessory-view.test.ts → input-accessory-view/input-accessory-view.test.ts} +2 -2
- package/src/components/{keyboard-avoiding-view.ts → keyboard-avoiding-view/index.ts} +2 -7
- package/src/components/{keyboard-avoiding-view.test.ts → keyboard-avoiding-view/keyboard-avoiding-view.test.ts} +2 -2
- package/src/components/{modal.ts → modal/index.ts} +2 -7
- package/src/components/{modal.test.ts → modal/modal.test.ts} +2 -2
- package/src/components/pressable/index.ts +17 -15
- package/src/components/{refresh-control.ts → refresh-control/index.ts} +2 -7
- package/src/components/{refresh-control.test.ts → refresh-control/refresh-control.test.ts} +2 -2
- package/src/components/{safe-area-view.ts → safe-area-view/index.ts} +7 -8
- package/src/components/{safe-area-view.test.ts → safe-area-view/safe-area-view.test.ts} +2 -2
- package/src/components/text-input.ts +1 -6
- package/src/components/{touchable.ts → touchable/index.ts} +3 -3
- package/src/components/{touchable.test.ts → touchable/touchable.test.ts} +2 -2
- package/src/components/{touchable-native-feedback.ts → touchable-native-feedback/index.ts} +3 -9
- package/src/components/{touchable-native-feedback.test.ts → touchable-native-feedback/touchable-native-feedback.test.ts} +2 -2
- package/src/{create-portal.test.ts → create-portal/create-portal.test.ts} +3 -3
- package/src/{create-tunnel.test.ts → create-tunnel/create-tunnel.test.ts} +3 -3
- package/src/index.ts +1 -1
- 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 +17 -0
- package/src/modules/animated/create-animated-component.ts +53 -123
- package/src/modules/{status-bar.test.ts → status-bar/status-bar.test.ts} +2 -2
- package/src/{render.ts → render/index.ts} +14 -2
- package/src/{render.test.ts → render/render.test.ts} +1 -1
- package/src/{renderer.ts → renderer/index.ts} +58 -58
- package/src/{renderer.test.ts → renderer/renderer.test.ts} +24 -1
- package/build/angular/components/input-accessory-view.js.map +0 -1
- package/build/angular/components/keyboard-avoiding-view.js.map +0 -1
- package/build/angular/components/modal.js.map +0 -1
- package/build/angular/components/refresh-control.js.map +0 -1
- package/build/angular/components/safe-area-view.js.map +0 -1
- package/build/angular/components/touchable-native-feedback.js.map +0 -1
- package/build/angular/components/touchable.js.map +0 -1
- package/build/angular/create-portal.js.map +0 -1
- package/build/angular/create-tunnel.js.map +0 -1
- package/build/angular/modules/status-bar.js.map +0 -1
- package/build/angular/render.js.map +0 -1
- package/build/angular/renderer.js.map +0 -1
- /package/build/angular/components/{keyboard-avoiding-view.d.ts → keyboard-avoiding-view/index.d.ts} +0 -0
- /package/build/angular/components/{modal.d.ts → modal/index.d.ts} +0 -0
- /package/build/angular/components/{refresh-control.d.ts → refresh-control/index.d.ts} +0 -0
- /package/build/angular/{create-portal.d.ts → create-portal/index.d.ts} +0 -0
- /package/build/angular/{create-tunnel.d.ts → create-tunnel/index.d.ts} +0 -0
- /package/build/angular/modules/{status-bar.d.ts → status-bar/index.d.ts} +0 -0
- /package/build/angular/{render.d.ts → render/index.d.ts} +0 -0
- /package/build/angular/{renderer.d.ts → renderer/index.d.ts} +0 -0
- /package/build/components/{keyboard-avoiding-view.d.ts → keyboard-avoiding-view/index.d.ts} +0 -0
- /package/build/components/{modal.d.ts → modal/index.d.ts} +0 -0
- /package/build/components/{refresh-control.d.ts → refresh-control/index.d.ts} +0 -0
- /package/build/{create-portal.d.ts → create-portal/index.d.ts} +0 -0
- /package/build/{create-portal.js → create-portal/index.js} +0 -0
- /package/build/{create-tunnel.d.ts → create-tunnel/index.d.ts} +0 -0
- /package/build/{create-tunnel.js → create-tunnel/index.js} +0 -0
- /package/build/modules/{status-bar.d.ts → status-bar/index.d.ts} +0 -0
- /package/build/modules/{status-bar.js → status-bar/index.js} +0 -0
- /package/build/{render.d.ts → render/index.d.ts} +0 -0
- /package/build/{renderer.d.ts → renderer/index.d.ts} +0 -0
- /package/src/{create-portal.ts → create-portal/index.ts} +0 -0
- /package/src/{create-tunnel.ts → create-tunnel/index.ts} +0 -0
- /package/src/modules/{status-bar.ts → status-bar/index.ts} +0 -0
|
@@ -14,9 +14,12 @@ import {
|
|
|
14
14
|
createPressHandlers,
|
|
15
15
|
createPressRuntime,
|
|
16
16
|
DEFAULT_DELAY_LONG_PRESS_MS,
|
|
17
|
+
isTerminationAllowed,
|
|
17
18
|
resolveAccessibilityProps,
|
|
18
19
|
resolveDisabledAccessibilityState,
|
|
19
20
|
rippleProps,
|
|
21
|
+
shouldClaimResponder,
|
|
22
|
+
shouldSuppressPress,
|
|
20
23
|
type IAccessibilityProps,
|
|
21
24
|
type IAccessibilityStateValue,
|
|
22
25
|
type IAriaProps,
|
|
@@ -43,7 +46,7 @@ export type { IPressState, IPressableAndroidRippleConfig } from '@symbiote-nativ
|
|
|
43
46
|
|
|
44
47
|
// The full logical Pressable surface, callback-shaped press/hover events included. Touchable* and
|
|
45
48
|
// TouchableNativeFeedback still take their OWN onPress/onPressIn/... as @Input() callbacks (that is
|
|
46
|
-
// a separate, not-yet-converted decision) and derive their public prop types from this
|
|
49
|
+
// a separate, not-yet-converted decision) and derive their public prop types from this - so it keeps
|
|
47
50
|
// the callback fields even though the Pressable component below no longer implements them directly.
|
|
48
51
|
export interface IAngularPressableProps extends IAccessibilityProps, IAriaProps {
|
|
49
52
|
onPress?: IPressHandler;
|
|
@@ -76,7 +79,7 @@ export interface IAngularPressableProps extends IAccessibilityProps, IAriaProps
|
|
|
76
79
|
|
|
77
80
|
// What the Pressable component itself takes as plain @Input()s: the full surface minus the press/
|
|
78
81
|
// hover events and the four accessibility callbacks, all of which it exposes as real @Output()
|
|
79
|
-
// EventEmitters instead (see the class below)
|
|
82
|
+
// EventEmitters instead (see the class below) - the one place in the family where the
|
|
80
83
|
// callback-vs-EventEmitter tradeoff is actually fixed.
|
|
81
84
|
export type IAngularPressableInputs = Omit<
|
|
82
85
|
IAngularPressableProps,
|
|
@@ -131,7 +134,7 @@ function asSymbioteEvent(event: unknown): ISymbioteEvent | undefined {
|
|
|
131
134
|
export class Pressable implements IAngularPressableInputs {
|
|
132
135
|
// The press/hover lifecycle as real Angular events: `(press)="onTap($event)"`, not
|
|
133
136
|
// `[onPress]="onTap"`. createPressHandlers still wants plain IPressHandler callbacks, so
|
|
134
|
-
// emitterHandler() below adapts each EventEmitter into one
|
|
137
|
+
// emitterHandler() below adapts each EventEmitter into one - only while something is actually
|
|
135
138
|
// subscribed (`.observed`), so an unbound onLongPress still skips arming the timer, exactly as
|
|
136
139
|
// the old @Input()-absent case did.
|
|
137
140
|
@Output() readonly press = new EventEmitter<ISymbioteEvent>();
|
|
@@ -223,8 +226,8 @@ export class Pressable implements IAngularPressableInputs {
|
|
|
223
226
|
};
|
|
224
227
|
|
|
225
228
|
private readonly changeDetector = inject(ChangeDetectorRef);
|
|
226
|
-
// This component's OWN host
|
|
227
|
-
// onto (see anchorHostStyle's doc comment)
|
|
229
|
+
// This component's OWN host - the non-painting anchor `class="..."` at the use site resolves
|
|
230
|
+
// onto (see anchorHostStyle's doc comment) - NOT `hostElement` above, which targets the real
|
|
228
231
|
// inner `symbiote-view` one level down.
|
|
229
232
|
private readonly elementRef = inject(ElementRef);
|
|
230
233
|
|
|
@@ -275,8 +278,7 @@ export class Pressable implements IAngularPressableInputs {
|
|
|
275
278
|
return this.android_ripple === undefined ? undefined : rippleProps(this.android_ripple);
|
|
276
279
|
}
|
|
277
280
|
|
|
278
|
-
//
|
|
279
|
-
// flat bag for `[symbioteHostProps]` (Renderer2.setProperty per key) — see primitives/shared.ts.
|
|
281
|
+
// Flat bag for `[symbioteHostProps]` (Renderer2.setProperty per key) - see primitives/shared.ts.
|
|
280
282
|
get hostProps(): Record<string, unknown> {
|
|
281
283
|
return {
|
|
282
284
|
testID: this.testID,
|
|
@@ -298,34 +300,34 @@ export class Pressable implements IAngularPressableInputs {
|
|
|
298
300
|
|
|
299
301
|
handlePressIn(event: unknown): void {
|
|
300
302
|
const symbioteEvent = asSymbioteEvent(event);
|
|
301
|
-
if (this.disabled
|
|
303
|
+
if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined) return;
|
|
302
304
|
this.handlers.handlePressIn(symbioteEvent);
|
|
303
305
|
}
|
|
304
306
|
|
|
305
307
|
handlePressOut(event: unknown): void {
|
|
306
308
|
const symbioteEvent = asSymbioteEvent(event);
|
|
307
|
-
if (this.disabled
|
|
309
|
+
if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined) return;
|
|
308
310
|
this.handlers.handlePressOut(symbioteEvent);
|
|
309
311
|
}
|
|
310
312
|
|
|
311
313
|
handlePress(event: unknown): void {
|
|
312
314
|
const symbioteEvent = asSymbioteEvent(event);
|
|
313
|
-
if (this.disabled
|
|
315
|
+
if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined) return;
|
|
314
316
|
this.handlers.handlePress(symbioteEvent);
|
|
315
317
|
}
|
|
316
318
|
|
|
317
319
|
handleResponderMove(event: unknown): void {
|
|
318
320
|
const symbioteEvent = asSymbioteEvent(event);
|
|
319
|
-
if (this.disabled
|
|
321
|
+
if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined) return;
|
|
320
322
|
this.handlers.handleResponderMove(symbioteEvent);
|
|
321
323
|
}
|
|
322
324
|
|
|
323
325
|
claimResponder(): boolean {
|
|
324
|
-
return this.disabled
|
|
326
|
+
return shouldClaimResponder(this.disabled);
|
|
325
327
|
}
|
|
326
328
|
|
|
327
329
|
allowTermination(): boolean {
|
|
328
|
-
return this.cancelable
|
|
330
|
+
return isTerminationAllowed(this.cancelable);
|
|
329
331
|
}
|
|
330
332
|
|
|
331
333
|
emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
|
|
@@ -342,8 +344,8 @@ export class Pressable implements IAngularPressableInputs {
|
|
|
342
344
|
}
|
|
343
345
|
|
|
344
346
|
// Wraps an @Output() as an IPressHandler only while it has a subscriber, so an unbound
|
|
345
|
-
// (longPress) still skips arming the timer
|
|
346
|
-
//
|
|
347
|
+
// (longPress) still skips arming the timer, matching createPressHandlers' "undefined means
|
|
348
|
+
// nobody cares" contract.
|
|
347
349
|
private emitterHandler(emitter: EventEmitter<ISymbioteEvent>): IPressHandler | undefined {
|
|
348
350
|
return emitter.observed ? event => emitter.emit(event) : undefined;
|
|
349
351
|
}
|
|
@@ -36,23 +36,18 @@ import {
|
|
|
36
36
|
import {
|
|
37
37
|
dispatchViewCommand,
|
|
38
38
|
dlog,
|
|
39
|
+
isSymbioteEvent,
|
|
39
40
|
isSymbioteNode,
|
|
40
41
|
type IStyleProp,
|
|
41
42
|
type ISymbioteEvent,
|
|
42
43
|
type IViewStyle,
|
|
43
44
|
} from '@symbiote-native/engine';
|
|
44
|
-
import { anchorHostStyle, RefreshControlHost, SymbioteHostPropsDirective } from '
|
|
45
|
+
import { anchorHostStyle, RefreshControlHost, SymbioteHostPropsDirective } from '../../primitives';
|
|
45
46
|
|
|
46
47
|
// Record<string, unknown> already tolerates `style` holding the [anchorHostStyle, this.style]
|
|
47
48
|
// array anchorHostStyle's merge produces (see hostProps below) — no widening needed.
|
|
48
49
|
type IHostProps = Record<string, unknown>;
|
|
49
50
|
|
|
50
|
-
// Angular infers an `(event)` binding's $event as the DOM Event for a custom-schema element,
|
|
51
|
-
// so each handler enters as `unknown` and is narrowed here before reaching a typed callback.
|
|
52
|
-
function isSymbioteEvent(value: unknown): value is ISymbioteEvent {
|
|
53
|
-
return typeof value === 'object' && value !== null && 'nativeEvent' in value;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
51
|
// Mirrors React's IRefreshControlProps minus children (Angular takes the Android-wrapped scroll view
|
|
57
52
|
// via <ng-content>), declared per-adapter over the shared accessibility base since the framework-specific
|
|
58
53
|
// children slot keeps it from being fully shared across adapters. Read the React reference for the
|
|
@@ -4,8 +4,8 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
|
4
4
|
import { clearGlobalStyles, registerStyles } from '@symbiote-native/engine';
|
|
5
5
|
import { installFabric } from '@symbiote-native/test-utils';
|
|
6
6
|
|
|
7
|
-
import { mount, unmount } from '
|
|
8
|
-
import { RefreshControl } from './
|
|
7
|
+
import { mount, unmount } from '../../render';
|
|
8
|
+
import { RefreshControl } from './index';
|
|
9
9
|
|
|
10
10
|
const ROOT_TAG = 914;
|
|
11
11
|
const fabric = installFabric();
|
|
@@ -22,14 +22,13 @@ import {
|
|
|
22
22
|
type IAccessibilityStateValue,
|
|
23
23
|
type IAriaProps,
|
|
24
24
|
} from '@symbiote-native/components';
|
|
25
|
-
import
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
25
|
+
import {
|
|
26
|
+
isSymbioteEvent,
|
|
27
|
+
type IStyleProp,
|
|
28
|
+
type ISymbioteEvent,
|
|
29
|
+
type IViewStyle,
|
|
30
|
+
} from '@symbiote-native/engine';
|
|
31
|
+
import { anchorHostStyle, SafeAreaViewHost, SymbioteHostPropsDirective } from '../../primitives';
|
|
33
32
|
|
|
34
33
|
// Mirrors React's ISafeAreaViewProps minus children (Angular takes children via <ng-content>),
|
|
35
34
|
// declared per-adapter over the shared accessibility base since the framework-specific children
|
|
@@ -4,8 +4,8 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
|
4
4
|
import { clearGlobalStyles, registerStyles } from '@symbiote-native/engine';
|
|
5
5
|
import { installFabric } from '@symbiote-native/test-utils';
|
|
6
6
|
|
|
7
|
-
import { mount, unmount } from '
|
|
8
|
-
import { SafeAreaView } from './
|
|
7
|
+
import { mount, unmount } from '../../render';
|
|
8
|
+
import { SafeAreaView } from './index';
|
|
9
9
|
|
|
10
10
|
const ROOT_TAG = 910;
|
|
11
11
|
const fabric = installFabric();
|
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
blurTextInput,
|
|
66
66
|
dispatchViewCommand,
|
|
67
67
|
dlog,
|
|
68
|
+
isSymbioteEvent,
|
|
68
69
|
isSymbioteNode,
|
|
69
70
|
setInputBlurred,
|
|
70
71
|
setInputFocused,
|
|
@@ -83,12 +84,6 @@ import {
|
|
|
83
84
|
|
|
84
85
|
export type { ITextInputHandle, ITextInputSelection, IInputMode, IEnterKeyHint, ISubmitBehavior };
|
|
85
86
|
|
|
86
|
-
// Angular infers an (event) binding's $event as the DOM Event for a custom-schema element, so
|
|
87
|
-
// each handler enters as `unknown` and is narrowed here before reaching a typed callback.
|
|
88
|
-
function isSymbioteEvent(value: unknown): value is ISymbioteEvent {
|
|
89
|
-
return typeof value === 'object' && value !== null && 'nativeEvent' in value;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
87
|
// Mirrors React's ITextInputProps minus nothing (TextInput has no children) — declared
|
|
93
88
|
// per-adapter over the shared accessibility base because Angular's input surface aliases the
|
|
94
89
|
// aria-* keys to camelCase @Inputs, unlike the plain agnostic fields shared across adapters.
|
|
@@ -55,9 +55,9 @@ import {
|
|
|
55
55
|
type IStyleProp,
|
|
56
56
|
type IViewStyle,
|
|
57
57
|
} from '@symbiote-native/engine';
|
|
58
|
-
import { anchorHostStyle, anchorStyleProp } from '
|
|
59
|
-
import { Pressable, type IAngularPressableInputs } from '
|
|
60
|
-
import { Animated, AnimatedView } from '
|
|
58
|
+
import { anchorHostStyle, anchorStyleProp } from '../../primitives';
|
|
59
|
+
import { Pressable, type IAngularPressableInputs } from '../pressable';
|
|
60
|
+
import { Animated, AnimatedView } from '../../modules/animated';
|
|
61
61
|
|
|
62
62
|
// The shared field base for all three: the Pressable INPUT surface (minus style, which each
|
|
63
63
|
// Touchable routes differently, and minus the press/hover events, which each Touchable declares as
|
|
@@ -9,8 +9,8 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
|
9
9
|
import { clearGlobalStyles, registerStyles } from '@symbiote-native/engine';
|
|
10
10
|
import { installFabric } from '@symbiote-native/test-utils';
|
|
11
11
|
|
|
12
|
-
import { mount, unmount } from '
|
|
13
|
-
import { TouchableHighlight, TouchableOpacity, TouchableWithoutFeedback } from './
|
|
12
|
+
import { mount, unmount } from '../../render';
|
|
13
|
+
import { TouchableHighlight, TouchableOpacity, TouchableWithoutFeedback } from './index';
|
|
14
14
|
|
|
15
15
|
const ROOT_TAG = 940;
|
|
16
16
|
const fabric = installFabric();
|
|
@@ -44,9 +44,9 @@ import {
|
|
|
44
44
|
type IPressableAndroidRippleConfig,
|
|
45
45
|
type IRectOffset,
|
|
46
46
|
} from '@symbiote-native/components';
|
|
47
|
-
import { dlog, type ISymbioteEvent } from '@symbiote-native/engine';
|
|
48
|
-
import { anchorHostStyle, SymbioteHostPropsDirective, ViewHost } from '
|
|
49
|
-
import { Pressable, type IAngularPressableInputs } from '
|
|
47
|
+
import { dlog, isSymbioteEvent, type ISymbioteEvent } from '@symbiote-native/engine';
|
|
48
|
+
import { anchorHostStyle, SymbioteHostPropsDirective, ViewHost } from '../../primitives';
|
|
49
|
+
import { Pressable, type IAngularPressableInputs } from '../pressable';
|
|
50
50
|
|
|
51
51
|
export type {
|
|
52
52
|
INativeFeedbackBackground,
|
|
@@ -63,12 +63,6 @@ export type IAngularTouchableNativeFeedbackProps = Omit<IAngularPressableInputs,
|
|
|
63
63
|
useForeground?: boolean;
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
-
// Angular infers an `(event)` binding's $event as the DOM Event for a custom-schema element,
|
|
67
|
-
// so each handler enters as `unknown` and is narrowed here before reaching a typed callback.
|
|
68
|
-
function isSymbioteEvent(value: unknown): value is ISymbioteEvent {
|
|
69
|
-
return typeof value === 'object' && value !== null && 'nativeEvent' in value;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
66
|
@Component({
|
|
73
67
|
selector: 'TouchableNativeFeedback',
|
|
74
68
|
standalone: true,
|
|
@@ -10,8 +10,8 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
|
10
10
|
import { clearGlobalStyles, registerStyles } from '@symbiote-native/engine';
|
|
11
11
|
import { installFabric } from '@symbiote-native/test-utils';
|
|
12
12
|
|
|
13
|
-
import { mount, unmount } from '
|
|
14
|
-
import { TouchableNativeFeedback } from './
|
|
13
|
+
import { mount, unmount } from '../../render';
|
|
14
|
+
import { TouchableNativeFeedback } from './index';
|
|
15
15
|
|
|
16
16
|
const ROOT_TAG = 941;
|
|
17
17
|
const fabric = installFabric();
|
|
@@ -8,9 +8,9 @@ import { Component, signal } from '@angular/core';
|
|
|
8
8
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
9
9
|
import { installFabric, type IFakeNode } from '@symbiote-native/test-utils';
|
|
10
10
|
|
|
11
|
-
import { mount, unmount } from '
|
|
12
|
-
import { ViewHost, TextHost } from '
|
|
13
|
-
import { PortalDirective, PortalOutletDirective } from './
|
|
11
|
+
import { mount, unmount } from '../render';
|
|
12
|
+
import { ViewHost, TextHost } from '../primitives';
|
|
13
|
+
import { PortalDirective, PortalOutletDirective } from './index';
|
|
14
14
|
|
|
15
15
|
const ROOT_TAG = 930;
|
|
16
16
|
|
|
@@ -8,9 +8,9 @@ import { Component } from '@angular/core';
|
|
|
8
8
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
9
9
|
import { installFabric, type IFakeNode } from '@symbiote-native/test-utils';
|
|
10
10
|
|
|
11
|
-
import { mount, unmount } from '
|
|
12
|
-
import { ViewHost, TextHost } from '
|
|
13
|
-
import { createTunnel, TunnelInDirective, TunnelOut } from './
|
|
11
|
+
import { mount, unmount } from '../render';
|
|
12
|
+
import { ViewHost, TextHost } from '../primitives';
|
|
13
|
+
import { createTunnel, TunnelInDirective, TunnelOut } from './index';
|
|
14
14
|
|
|
15
15
|
const SOURCE_TAG = 920;
|
|
16
16
|
const TARGET_TAG = 921;
|
package/src/index.ts
CHANGED
|
@@ -135,7 +135,7 @@ export { DescriptorOutlet } from './descriptor-to-angular';
|
|
|
135
135
|
// call.
|
|
136
136
|
export { PortalDirective, PortalOutletDirective } from './create-portal';
|
|
137
137
|
export { createTunnel, TunnelInDirective, TunnelOut, type ITunnelStore } from './create-tunnel';
|
|
138
|
-
export { SymbioteRenderer, SymbioteRendererFactory } from './renderer';
|
|
138
|
+
export { registerComposedComponent, SymbioteRenderer, SymbioteRendererFactory } from './renderer';
|
|
139
139
|
export { findNodeHandle } from './host-instance';
|
|
140
140
|
export type { IHostInstance } from './host-instance';
|
|
141
141
|
export { StatusBar } from './modules/status-bar';
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Unit tests for AnimatedLeafBinder in isolation — no Angular involved. Both
|
|
2
|
+
// AnimatedComponentBase and AnimatedImage delegate their leaf-lifecycle (build a leaf,
|
|
3
|
+
// bind it to the committed Fabric node, swap leaves on re-render, tear down) to this Pure
|
|
4
|
+
// Fabrication; these tests exercise the binder directly against a real engine node
|
|
5
|
+
// (SymbioteSurface + the fake Fabric backend), so a break here isolates to the shared
|
|
6
|
+
// orchestration rather than either owning component.
|
|
7
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
8
|
+
import { installFabric } from '@symbiote-native/test-utils';
|
|
9
|
+
import {
|
|
10
|
+
AnimatedValue,
|
|
11
|
+
createElement,
|
|
12
|
+
createSurface,
|
|
13
|
+
type ISymbioteNode,
|
|
14
|
+
} from '@symbiote-native/engine';
|
|
15
|
+
import { AnimatedLeafBinder } from './index';
|
|
16
|
+
|
|
17
|
+
const fabric = installFabric();
|
|
18
|
+
let nextRootTag = 9001;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => fabric.reset());
|
|
21
|
+
|
|
22
|
+
// A node that has already gone through a commit — whenCommitted fires synchronously.
|
|
23
|
+
function committedNode(): ISymbioteNode {
|
|
24
|
+
const surface = createSurface(nextRootTag++);
|
|
25
|
+
const node = createElement('RCTView');
|
|
26
|
+
surface.appendChild(node);
|
|
27
|
+
surface.commit();
|
|
28
|
+
return node;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// A node that is attached to a surface but has NOT been committed yet — whenCommitted
|
|
32
|
+
// must defer until the caller commits the surface.
|
|
33
|
+
function uncommittedNode(): { node: ISymbioteNode; commit: () => void } {
|
|
34
|
+
const surface = createSurface(nextRootTag++);
|
|
35
|
+
const node = createElement('RCTView');
|
|
36
|
+
surface.appendChild(node);
|
|
37
|
+
return { node, commit: () => surface.commit() };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// `appRoot()` unwraps RN's synthetic box-none AppContainer, which is ALSO an RCTView, so a
|
|
41
|
+
// plain `fabric.find(viewName === 'RCTView')` would match it instead of the node under test.
|
|
42
|
+
// Each test appends exactly one child, so the container's first (only) child is the real node.
|
|
43
|
+
function fakeView(): ReturnType<typeof fabric.find> {
|
|
44
|
+
return fabric.appRoot().children[0];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('AnimatedLeafBinder', () => {
|
|
48
|
+
it('creates and attaches an AnimatedProps leaf, wiring value changes onto the host node', () => {
|
|
49
|
+
const node = committedNode();
|
|
50
|
+
const binder = new AnimatedLeafBinder(() => node, 'test');
|
|
51
|
+
const opacity = new AnimatedValue(1);
|
|
52
|
+
|
|
53
|
+
// The leaf itself never pushes the INITIAL value — that first paint comes from the
|
|
54
|
+
// owning component's own reduceProps template binding (out of scope here). The leaf
|
|
55
|
+
// only drives value CHANGES after it is bound.
|
|
56
|
+
binder.reconcile({ style: { opacity } }, false);
|
|
57
|
+
|
|
58
|
+
opacity.setValue(0.4);
|
|
59
|
+
expect(fakeView()?.props.opacity).toBe(0.4);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('binds to the host node via whenCommitted, deferring until the node is actually committed', () => {
|
|
63
|
+
const { node, commit } = uncommittedNode();
|
|
64
|
+
const binder = new AnimatedLeafBinder(() => node, 'test');
|
|
65
|
+
const opacity = new AnimatedValue(1);
|
|
66
|
+
|
|
67
|
+
binder.reconcile({ style: { opacity } }, false);
|
|
68
|
+
// Not committed yet: the value graph exists, but no Fabric node to flush onto.
|
|
69
|
+
opacity.setValue(0.5);
|
|
70
|
+
expect(fabric.created).toHaveLength(0);
|
|
71
|
+
|
|
72
|
+
commit();
|
|
73
|
+
// whenCommitted's post-commit retry binds the leaf now; a subsequent change flushes.
|
|
74
|
+
opacity.setValue(0.7);
|
|
75
|
+
expect(fakeView()?.props.opacity).toBe(0.7);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('attaches the new leaf before detaching the old one on a second reconcile', () => {
|
|
79
|
+
const node = committedNode();
|
|
80
|
+
const binder = new AnimatedLeafBinder(() => node, 'test');
|
|
81
|
+
const shared = new AnimatedValue(1);
|
|
82
|
+
const order: string[] = [];
|
|
83
|
+
const originalAddChild = shared.__addChild.bind(shared);
|
|
84
|
+
const originalRemoveChild = shared.__removeChild.bind(shared);
|
|
85
|
+
shared.__addChild = child => {
|
|
86
|
+
order.push('add');
|
|
87
|
+
originalAddChild(child);
|
|
88
|
+
};
|
|
89
|
+
shared.__removeChild = child => {
|
|
90
|
+
order.push('remove');
|
|
91
|
+
originalRemoveChild(child);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
binder.reconcile({ style: { opacity: shared } }, false);
|
|
95
|
+
order.length = 0; // only care about the SECOND reconcile's swap order
|
|
96
|
+
|
|
97
|
+
binder.reconcile({ style: { opacity: shared } }, false);
|
|
98
|
+
|
|
99
|
+
expect(order).toEqual(['add', 'remove']);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('detaches the leaf and cancels any pending bind on destroy', () => {
|
|
103
|
+
const { node, commit } = uncommittedNode();
|
|
104
|
+
const binder = new AnimatedLeafBinder(() => node, 'test');
|
|
105
|
+
const opacity = new AnimatedValue(1);
|
|
106
|
+
|
|
107
|
+
binder.reconcile({ style: { opacity } }, false);
|
|
108
|
+
binder.destroy();
|
|
109
|
+
commit();
|
|
110
|
+
|
|
111
|
+
// The pending whenCommitted bind was cancelled by destroy, so committing afterward
|
|
112
|
+
// must not retroactively bind the (already-detached) leaf.
|
|
113
|
+
opacity.setValue(0.9);
|
|
114
|
+
expect(fakeView()?.props.opacity).toBeUndefined();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Pure Fabrication (GRASP): the leaf-lifecycle orchestration shared by every Animated*
|
|
2
|
+
// wrapper, extracted out of AnimatedComponentBase so AnimatedImage — which cannot extend
|
|
3
|
+
// AnimatedComponentBase, since it must extend ImageBase for TS's-native image handling —
|
|
4
|
+
// gets the SAME logic via composition instead of copy-pasting it. Framework-thin on
|
|
5
|
+
// purpose: it knows nothing about Angular (@ViewChild, DI, change detection), only the
|
|
6
|
+
// engine's Animated leaf + commit primitives, so it stays testable without any Angular
|
|
7
|
+
// bootstrap. NOTE: react/vue drive this same dance through their own runtime HOC
|
|
8
|
+
// (createAnimatedComponent) rather than a class, so there is no cross-adapter dedup
|
|
9
|
+
// opportunity here yet — this Pure Fabrication is local to the angular adapter.
|
|
10
|
+
import {
|
|
11
|
+
AnimatedProps,
|
|
12
|
+
attachNativeEventHandler,
|
|
13
|
+
dlog,
|
|
14
|
+
whenCommitted,
|
|
15
|
+
type ISymbioteNode,
|
|
16
|
+
} from '@symbiote-native/engine';
|
|
17
|
+
|
|
18
|
+
export class AnimatedLeafBinder {
|
|
19
|
+
// The leaf currently wired into the value graph. Null until the first reconcile.
|
|
20
|
+
private attached: AnimatedProps | null = null;
|
|
21
|
+
// The whenCommitted waiter for the current leaf's node binding, cancelled before the next.
|
|
22
|
+
private cancelBind: (() => void) | undefined;
|
|
23
|
+
// Detachers for any Animated.event prop bound natively to the committed node (JS path: none).
|
|
24
|
+
private eventDetachers: Array<() => void> = [];
|
|
25
|
+
|
|
26
|
+
// `resolveNode` stays a caller-supplied resolver (not a constructor-captured node) so this
|
|
27
|
+
// class never needs to know HOW the host node is found — AnimatedComponentBase and
|
|
28
|
+
// AnimatedImage both resolve it the same way (resolveHostNode + isSymbioteNode over their
|
|
29
|
+
// own @ViewChild), but that's Angular DI state this binder has no business touching.
|
|
30
|
+
// `label` is purely diagnostic (dlog), mirroring the constructor.name the pre-extraction
|
|
31
|
+
// dlog call used.
|
|
32
|
+
constructor(
|
|
33
|
+
private readonly resolveNode: () => ISymbioteNode | null,
|
|
34
|
+
private readonly label: string,
|
|
35
|
+
) {}
|
|
36
|
+
|
|
37
|
+
// Build a fresh AnimatedProps leaf from the current props (the React/Vue per-render
|
|
38
|
+
// useMemo(rest)), wire it into the graph, and swap the previous one out. Attach the NEW leaf
|
|
39
|
+
// BEFORE detaching the OLD one: a shared Value self-detaches (dropping its native node) the
|
|
40
|
+
// instant its child count hits zero, so detaching first would kill a running native animation
|
|
41
|
+
// on any unrelated re-render (mirrors RN's AnimatedComponent._attachProps). Then bind the
|
|
42
|
+
// committed node, go native if wanted, and rebind native events.
|
|
43
|
+
reconcile(props: Record<string, unknown>, wantsNative: boolean): void {
|
|
44
|
+
dlog(`${this.label} reconcile`);
|
|
45
|
+
const newLeaf = new AnimatedProps(props);
|
|
46
|
+
newLeaf.__attach();
|
|
47
|
+
if (this.attached !== null && this.attached !== newLeaf) this.attached.__detach();
|
|
48
|
+
this.attached = newLeaf;
|
|
49
|
+
|
|
50
|
+
this.bindNode(newLeaf, props, wantsNative);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Bind the leaf to the host's Fabric node THROUGH whenCommitted: under Angular's zoneless
|
|
54
|
+
// batched change detection the inner host's tag does not exist yet at ngAfterViewInit time
|
|
55
|
+
// (the same async-commit gotcha Vue documents), so binding eagerly would no-op. whenCommitted
|
|
56
|
+
// runs the action now if already committed, else after the commit that assigns the tag.
|
|
57
|
+
private bindNode(
|
|
58
|
+
leaf: AnimatedProps,
|
|
59
|
+
props: Record<string, unknown>,
|
|
60
|
+
wantsNative: boolean,
|
|
61
|
+
): void {
|
|
62
|
+
this.cancelBind?.();
|
|
63
|
+
this.cancelBind = undefined;
|
|
64
|
+
const node = this.resolveNode();
|
|
65
|
+
if (node === null) return;
|
|
66
|
+
this.cancelBind = whenCommitted(node, () => {
|
|
67
|
+
leaf.setNativeView(node);
|
|
68
|
+
if (wantsNative) leaf.__makeNative();
|
|
69
|
+
this.attachEvents(node, props);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Native-attach any Animated.event prop (e.g. onScroll={Animated.event(…,{useNativeDriver:true})})
|
|
74
|
+
// to the committed node. attachNativeEventHandler no-ops (returns undefined) unless the prop is a
|
|
75
|
+
// native event handler with a committed tag, so the JS path stays the fallback. Rebound each
|
|
76
|
+
// reconcile so a new inline event re-attaches; detached first to avoid leaking the prior binding.
|
|
77
|
+
private attachEvents(node: ISymbioteNode, props: Record<string, unknown>): void {
|
|
78
|
+
this.detachEvents();
|
|
79
|
+
for (const key of Object.keys(props)) {
|
|
80
|
+
const attachment = attachNativeEventHandler(node, key, props[key]);
|
|
81
|
+
if (attachment !== undefined) this.eventDetachers.push(attachment.detach);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private detachEvents(): void {
|
|
86
|
+
for (const detach of this.eventDetachers) detach();
|
|
87
|
+
this.eventDetachers = [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Tear down everything: cancel a still-pending bind, drop native event listeners, detach the
|
|
91
|
+
// current leaf from the value graph. Safe to call even if reconcile() was never called.
|
|
92
|
+
destroy(): void {
|
|
93
|
+
this.cancelBind?.();
|
|
94
|
+
this.cancelBind = undefined;
|
|
95
|
+
this.detachEvents();
|
|
96
|
+
if (this.attached !== null) {
|
|
97
|
+
this.attached.__detach();
|
|
98
|
+
this.attached = null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Unit tests for AnimatedLeafBinder in isolation — no Angular involved. Both
|
|
2
|
+
// AnimatedComponentBase and AnimatedImage delegate their leaf-lifecycle (build a leaf,
|
|
3
|
+
// bind it to the committed Fabric node, swap leaves on re-render, tear down) to this Pure
|
|
4
|
+
// Fabrication; these tests exercise the binder directly against a real engine node
|
|
5
|
+
// (SymbioteSurface + the fake Fabric backend), so a break here isolates to the shared
|
|
6
|
+
// orchestration rather than either owning component.
|
|
7
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
8
|
+
import { installFabric } from '@symbiote-native/test-utils';
|
|
9
|
+
import {
|
|
10
|
+
AnimatedValue,
|
|
11
|
+
createElement,
|
|
12
|
+
createSurface,
|
|
13
|
+
type ISymbioteNode,
|
|
14
|
+
} from '@symbiote-native/engine';
|
|
15
|
+
import { AnimatedLeafBinder } from './animated-leaf-binder';
|
|
16
|
+
|
|
17
|
+
const fabric = installFabric();
|
|
18
|
+
let nextRootTag = 9001;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => fabric.reset());
|
|
21
|
+
|
|
22
|
+
// A node that has already gone through a commit — whenCommitted fires synchronously.
|
|
23
|
+
function committedNode(): ISymbioteNode {
|
|
24
|
+
const surface = createSurface(nextRootTag++);
|
|
25
|
+
const node = createElement('RCTView');
|
|
26
|
+
surface.appendChild(node);
|
|
27
|
+
surface.commit();
|
|
28
|
+
return node;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// A node that is attached to a surface but has NOT been committed yet — whenCommitted
|
|
32
|
+
// must defer until the caller commits the surface.
|
|
33
|
+
function uncommittedNode(): { node: ISymbioteNode; commit: () => void } {
|
|
34
|
+
const surface = createSurface(nextRootTag++);
|
|
35
|
+
const node = createElement('RCTView');
|
|
36
|
+
surface.appendChild(node);
|
|
37
|
+
return { node, commit: () => surface.commit() };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// `appRoot()` unwraps RN's synthetic box-none AppContainer, which is ALSO an RCTView, so a
|
|
41
|
+
// plain `fabric.find(viewName === 'RCTView')` would match it instead of the node under test.
|
|
42
|
+
// Each test appends exactly one child, so the container's first (only) child is the real node.
|
|
43
|
+
function fakeView(): ReturnType<typeof fabric.find> {
|
|
44
|
+
return fabric.appRoot().children[0];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('AnimatedLeafBinder', () => {
|
|
48
|
+
it('creates and attaches an AnimatedProps leaf, wiring value changes onto the host node', () => {
|
|
49
|
+
const node = committedNode();
|
|
50
|
+
const binder = new AnimatedLeafBinder(() => node, 'test');
|
|
51
|
+
const opacity = new AnimatedValue(1);
|
|
52
|
+
|
|
53
|
+
// The leaf itself never pushes the INITIAL value — that first paint comes from the
|
|
54
|
+
// owning component's own reduceProps template binding (out of scope here). The leaf
|
|
55
|
+
// only drives value CHANGES after it is bound.
|
|
56
|
+
binder.reconcile({ style: { opacity } }, false);
|
|
57
|
+
|
|
58
|
+
opacity.setValue(0.4);
|
|
59
|
+
expect(fakeView()?.props.opacity).toBe(0.4);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('binds to the host node via whenCommitted, deferring until the node is actually committed', () => {
|
|
63
|
+
const { node, commit } = uncommittedNode();
|
|
64
|
+
const binder = new AnimatedLeafBinder(() => node, 'test');
|
|
65
|
+
const opacity = new AnimatedValue(1);
|
|
66
|
+
|
|
67
|
+
binder.reconcile({ style: { opacity } }, false);
|
|
68
|
+
// Not committed yet: the value graph exists, but no Fabric node to flush onto.
|
|
69
|
+
opacity.setValue(0.5);
|
|
70
|
+
expect(fabric.created).toHaveLength(0);
|
|
71
|
+
|
|
72
|
+
commit();
|
|
73
|
+
// whenCommitted's post-commit retry binds the leaf now; a subsequent change flushes.
|
|
74
|
+
opacity.setValue(0.7);
|
|
75
|
+
expect(fakeView()?.props.opacity).toBe(0.7);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('attaches the new leaf before detaching the old one on a second reconcile', () => {
|
|
79
|
+
const node = committedNode();
|
|
80
|
+
const binder = new AnimatedLeafBinder(() => node, 'test');
|
|
81
|
+
const shared = new AnimatedValue(1);
|
|
82
|
+
const order: string[] = [];
|
|
83
|
+
const originalAddChild = shared.__addChild.bind(shared);
|
|
84
|
+
const originalRemoveChild = shared.__removeChild.bind(shared);
|
|
85
|
+
shared.__addChild = child => {
|
|
86
|
+
order.push('add');
|
|
87
|
+
originalAddChild(child);
|
|
88
|
+
};
|
|
89
|
+
shared.__removeChild = child => {
|
|
90
|
+
order.push('remove');
|
|
91
|
+
originalRemoveChild(child);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
binder.reconcile({ style: { opacity: shared } }, false);
|
|
95
|
+
order.length = 0; // only care about the SECOND reconcile's swap order
|
|
96
|
+
|
|
97
|
+
binder.reconcile({ style: { opacity: shared } }, false);
|
|
98
|
+
|
|
99
|
+
expect(order).toEqual(['add', 'remove']);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('detaches the leaf and cancels any pending bind on destroy', () => {
|
|
103
|
+
const { node, commit } = uncommittedNode();
|
|
104
|
+
const binder = new AnimatedLeafBinder(() => node, 'test');
|
|
105
|
+
const opacity = new AnimatedValue(1);
|
|
106
|
+
|
|
107
|
+
binder.reconcile({ style: { opacity } }, false);
|
|
108
|
+
binder.destroy();
|
|
109
|
+
commit();
|
|
110
|
+
|
|
111
|
+
// The pending whenCommitted bind was cancelled by destroy, so committing afterward
|
|
112
|
+
// must not retroactively bind the (already-detached) leaf.
|
|
113
|
+
opacity.setValue(0.9);
|
|
114
|
+
expect(fakeView()?.props.opacity).toBeUndefined();
|
|
115
|
+
});
|
|
116
|
+
});
|