@symbiote-native/react 0.2.7 → 0.2.8
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/build/components/activity-indicator/index.android.d.ts +2 -1
- package/build/components/activity-indicator/index.android.js +9 -3
- package/build/components/activity-indicator/index.ios.d.ts +2 -1
- package/build/components/activity-indicator/index.ios.js +9 -3
- package/build/components/activity-indicator/shared.d.ts +1 -2
- package/build/components/activity-indicator/shared.js +13 -11
- package/build/components/scroll-view/shared.js +26 -30
- package/build/components/scroll-view/sticky-header.js +91 -65
- package/build/components/switch/index.android.d.ts +2 -1
- package/build/components/switch/index.android.js +9 -3
- package/build/components/switch/index.ios.d.ts +2 -1
- package/build/components/switch/index.ios.js +9 -3
- package/build/components/switch/shared.d.ts +2 -3
- package/build/components/switch/shared.js +44 -43
- package/build/components/touchable/index.js +22 -54
- package/build/components/virtualized-list/index.js +194 -305
- package/build/components/virtualized-section-list/index.js +2 -4
- package/build/render.js +8 -0
- package/package.json +2 -2
|
@@ -1,2 +1,3 @@
|
|
|
1
|
+
import type { IActivityIndicatorProps } from './shared';
|
|
1
2
|
export type { IActivityIndicatorProps } from './shared';
|
|
2
|
-
export declare
|
|
3
|
+
export declare function ActivityIndicator(rawProps: IActivityIndicatorProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
@@ -5,9 +5,15 @@
|
|
|
5
5
|
// picks this on an Android host; no Platform.OS read.
|
|
6
6
|
// device-verify-pending: prop names mirror RN's AndroidProgressBar, proven on a real
|
|
7
7
|
// host by the absence of the "setStyle() not called" red box.
|
|
8
|
-
import {
|
|
9
|
-
|
|
8
|
+
import { descriptorToReact } from '../../descriptor-to-react/index.js';
|
|
9
|
+
import { useActivityIndicatorLogic } from './shared.js';
|
|
10
|
+
const PLATFORM = {
|
|
10
11
|
// RN: `color = Platform.OS === 'ios' ? GRAY : null`; Android lets the theme color it.
|
|
11
12
|
defaultColor: null,
|
|
12
13
|
nativeExtras: { styleAttr: 'Normal', indeterminate: true },
|
|
13
|
-
}
|
|
14
|
+
};
|
|
15
|
+
// A top-level named function, not a factory-returned closure: React Compiler's
|
|
16
|
+
// component detection only walks top-level declarations (see shared.ts).
|
|
17
|
+
export function ActivityIndicator(rawProps) {
|
|
18
|
+
return descriptorToReact(useActivityIndicatorLogic(rawProps, PLATFORM));
|
|
19
|
+
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
+
import type { IActivityIndicatorProps } from './shared';
|
|
1
2
|
export type { IActivityIndicatorProps } from './shared';
|
|
2
|
-
export declare
|
|
3
|
+
export declare function ActivityIndicator(rawProps: IActivityIndicatorProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
// ActivityIndicator on iOS: RCTActivityIndicatorView takes the size enum + a GRAY
|
|
2
2
|
// default color and no extra native props. Also the base (activity-indicator.ts
|
|
3
3
|
// re-exports it) for headless / web.
|
|
4
|
-
import {
|
|
4
|
+
import { descriptorToReact } from '../../descriptor-to-react/index.js';
|
|
5
|
+
import { useActivityIndicatorLogic } from './shared.js';
|
|
5
6
|
// RN's iOS default spinner color (Libraries/.../ActivityIndicator.js GRAY).
|
|
6
7
|
const IOS_DEFAULT_COLOR = '#999999';
|
|
7
|
-
|
|
8
|
+
const PLATFORM = {
|
|
8
9
|
defaultColor: IOS_DEFAULT_COLOR,
|
|
9
10
|
nativeExtras: {},
|
|
10
|
-
}
|
|
11
|
+
};
|
|
12
|
+
// A top-level named function, not a factory-returned closure: React Compiler's
|
|
13
|
+
// component detection only walks top-level declarations (see shared.ts).
|
|
14
|
+
export function ActivityIndicator(rawProps) {
|
|
15
|
+
return descriptorToReact(useActivityIndicatorLogic(rawProps, PLATFORM));
|
|
16
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import type { FC } from 'react';
|
|
2
1
|
import type { IActivityIndicatorPlatform, IActivityIndicatorProps as IActivityIndicatorBaseProps } from '@symbiote-native/components';
|
|
3
2
|
export type { IActivityIndicatorPlatform } from '@symbiote-native/components';
|
|
4
3
|
export type IActivityIndicatorProps = IActivityIndicatorBaseProps & {
|
|
5
4
|
className?: string;
|
|
6
5
|
};
|
|
7
|
-
export declare function
|
|
6
|
+
export declare function useActivityIndicatorLogic(rawProps: IActivityIndicatorProps, platform: IActivityIndicatorPlatform): import("@symbiote-native/components").IDescriptor;
|
|
@@ -5,16 +5,18 @@
|
|
|
5
5
|
//
|
|
6
6
|
// The per-platform bits (iOS GRAY default + no extras; Android theme/null + styleAttr +
|
|
7
7
|
// indeterminate) are supplied by the .ios/.android files via `IActivityIndicatorPlatform`.
|
|
8
|
+
//
|
|
9
|
+
// useActivityIndicatorLogic is a plain top-level hook, not a component factory returning a
|
|
10
|
+
// closure: React Compiler's component/hook detection only walks top-level declarations, so
|
|
11
|
+
// index.ios.ts / index.android.ts each declare their OWN top-level `ActivityIndicator`
|
|
12
|
+
// function that calls this hook, rather than exporting whatever a factory here returns (same
|
|
13
|
+
// shape as switch/shared.ts's useSwitchLogic).
|
|
8
14
|
import { renderActivityIndicator, resolveAccessibilityProps } from '@symbiote-native/components';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const { animating = true, color, hidesWhenStopped = true, size = 'small', style, ...passthrough } = props;
|
|
17
|
-
const descriptor = renderActivityIndicator({ animating, color, hidesWhenStopped, size, style, passthrough }, platform);
|
|
18
|
-
return descriptorToReact(descriptor);
|
|
19
|
-
};
|
|
15
|
+
export function useActivityIndicatorLogic(rawProps, platform) {
|
|
16
|
+
// The wrapper is a raw symbiote-view, not the View FC, so it never runs
|
|
17
|
+
// resolveAccessibilityProps itself, so fold aria/role here, then forward the resolved
|
|
18
|
+
// accessibility* surface (plus testID / onLayout) onto the wrapper via `passthrough`.
|
|
19
|
+
const props = resolveAccessibilityProps(rawProps);
|
|
20
|
+
const { animating = true, color, hidesWhenStopped = true, size = 'small', style, ...passthrough } = props;
|
|
21
|
+
return renderActivityIndicator({ animating, color, hidesWhenStopped, size, style, passthrough }, platform);
|
|
20
22
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// lifecycle (refs/state/effects) and the element assembly that consumes them.
|
|
13
13
|
import { createElement, useEffect, useRef, useState, } from 'react';
|
|
14
14
|
import { AnimatedValue, dlog, event as animatedEvent, isClassNameValue, isNativeAnimatedAvailable, resolveClassName, } from '@symbiote-native/engine';
|
|
15
|
-
import { attachStickyScroll, didContentSizeChange, forwardScrollEvent, readLayoutDimension, resolveDecelerationRate, selectScrollIntrinsics, } from '@symbiote-native/components';
|
|
15
|
+
import { attachStickyScroll, didContentSizeChange, forwardScrollEvent, readLayoutDimension, resolveDecelerationRate, resolveScrollForwarding, selectScrollIntrinsics, } from '@symbiote-native/components';
|
|
16
16
|
import { resolveAccessibilityProps, } from '@symbiote-native/components';
|
|
17
17
|
import { wrapStickyHeaders } from './sticky-header.js';
|
|
18
18
|
export function usePreparedScrollView(rawProps) {
|
|
@@ -77,39 +77,35 @@ export function usePreparedScrollView(rawProps) {
|
|
|
77
77
|
if (decelerationRate !== undefined) {
|
|
78
78
|
outerProps.decelerationRate = resolveDecelerationRate(decelerationRate);
|
|
79
79
|
}
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
80
|
+
// The scroll-forwarding DECISIONS (which onScroll path, the 1/16 throttle defaults, whether to
|
|
81
|
+
// capture the viewport height for inverted sticky, whether to keep cells un-flattened) are folded
|
|
82
|
+
// out to the shared resolveScrollForwarding; here React only EXECUTES them with its own primitives.
|
|
83
83
|
const nativeStickyAvailable = hasStickyHeaders && isNativeAnimatedAvailable();
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
84
|
+
const forwarding = resolveScrollForwarding({
|
|
85
|
+
hasStickyHeaders,
|
|
86
|
+
nativeStickyAvailable,
|
|
87
|
+
invertStickyHeaders,
|
|
88
|
+
scrollEventThrottle,
|
|
89
|
+
maintainVisibleContentPosition: props.maintainVisibleContentPosition,
|
|
90
|
+
snapToAlignment: props.snapToAlignment,
|
|
91
|
+
});
|
|
92
|
+
// onScroll: the JS-fallback path wraps the user's handler in Animated.event so the offset drives
|
|
93
|
+
// the AnimatedValue each frame (RN _scrollAnimatedValueAttachment); the native + plain paths
|
|
94
|
+
// forward the user's handler as-is (the native driver attaches the value on the UI thread).
|
|
95
|
+
if (forwarding.mode === 'sticky-js') {
|
|
96
|
+
outerProps.onScroll = animatedEvent([{ nativeEvent: { contentOffset: { y: scrollAnimatedValue } } }], onScroll === undefined
|
|
97
|
+
? undefined
|
|
98
|
+
: { listener: (...args) => forwardScrollEvent(onScroll, args) });
|
|
99
|
+
}
|
|
100
|
+
else if (onScroll !== undefined) {
|
|
101
|
+
outerProps.onScroll = onScroll;
|
|
103
102
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
outerProps.onScroll = onScroll;
|
|
107
|
-
if (scrollEventThrottle !== undefined)
|
|
108
|
-
outerProps.scrollEventThrottle = scrollEventThrottle;
|
|
103
|
+
if (forwarding.scrollEventThrottle !== undefined) {
|
|
104
|
+
outerProps.scrollEventThrottle = forwarding.scrollEventThrottle;
|
|
109
105
|
}
|
|
110
106
|
// onLayout on the scroll-view node: capture the viewport height for inverted sticky headers
|
|
111
107
|
// (RN _handleLayout), then call the user's handler. Pass through unchanged otherwise.
|
|
112
|
-
if (
|
|
108
|
+
if (forwarding.capturesViewportHeight) {
|
|
113
109
|
outerProps.onLayout = (layoutEvent) => {
|
|
114
110
|
const height = readLayoutDimension(layoutEvent, 'height');
|
|
115
111
|
if (height !== undefined)
|
|
@@ -131,7 +127,7 @@ export function usePreparedScrollView(rawProps) {
|
|
|
131
127
|
// MaintainVisibleScrollPositionHelper has nothing to anchor to and the list jumps on prepend.
|
|
132
128
|
// RN keeps the cells as real views via collapsableChildren={false} on the content container
|
|
133
129
|
// (ScrollView.js:1731-1748 `preserveChildren`). iOS never flattens, so it is a no-op there.
|
|
134
|
-
if (
|
|
130
|
+
if (forwarding.collapsableChildren) {
|
|
135
131
|
contentProps.collapsableChildren = false;
|
|
136
132
|
}
|
|
137
133
|
if (onContentSizeChange !== undefined) {
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
// The interpolation mirrors ScrollViewStickyHeader.js (non-inverted + inverted branches) and now
|
|
10
10
|
// lives, framework-agnostic, in @symbiote-native/components (computeStickyInterpolation); this
|
|
11
11
|
// file holds the React component shell, the layout state, and the child-wrapping.
|
|
12
|
-
import { Children, createElement, isValidElement, useCallback, useEffect, useRef, useState, } from 'react';
|
|
12
|
+
import { Children, createElement, isValidElement, useCallback, useEffect, useReducer, useRef, useState, } from 'react';
|
|
13
13
|
import { AnimatedInterpolation, AnimatedValue, Platform, dlog, } from '@symbiote-native/engine';
|
|
14
|
-
import {
|
|
14
|
+
import { createInitialStickyState, nextStickyHeaderY, readLayoutNumber, reduceSticky, STICKY_HEADER_Z_INDEX, } from '@symbiote-native/components';
|
|
15
15
|
import { Animated } from '../../modules/animated/index.js';
|
|
16
16
|
function readChildOnLayout(child) {
|
|
17
17
|
const childProps = child.props;
|
|
@@ -27,79 +27,105 @@ function firstChild(children) {
|
|
|
27
27
|
// One sticky header. Measures its own y/height via onLayout, interpolates the shared scroll
|
|
28
28
|
// offset into a translateY that keeps it pinned to the top (or bottom, inverted) until the next
|
|
29
29
|
// header collides with it, and drives that translate through the native driver when available so
|
|
30
|
-
// the pin tracks scroll on the UI thread (no JS jitter).
|
|
31
|
-
//
|
|
30
|
+
// the pin tracks scroll on the UI thread (no JS jitter). The DECISIONS — the zero-swallow gate, the
|
|
31
|
+
// debounce delay, the rebuild-on-input-change ranges — live in `reduceSticky`
|
|
32
|
+
// (@symbiote-native/components); this component supplies only the React lifecycle: the ONE folded
|
|
33
|
+
// state cell, the interpolation-node + listener wiring, the debounce setTimeout, and the re-render.
|
|
32
34
|
export const ScrollViewStickyHeader = props => {
|
|
33
35
|
const { inverted, scrollViewHeight, scrollAnimatedValue, nextHeaderLayoutY, children } = props;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
// The one folded state cell (RN's scattered useState/useRef collapsed into IStickyHeaderState),
|
|
37
|
+
// mutated in place by reduceSticky. Lazily created once.
|
|
38
|
+
const stateRef = useRef(null);
|
|
39
|
+
const state = (stateRef.current ??= createInitialStickyState());
|
|
40
|
+
const [, forceRender] = useReducer((tick) => tick + 1, 0);
|
|
41
|
+
// The animated node that drives the transform (RN's animatedTranslateY), rebuilt by the
|
|
42
|
+
// rebuild-interpolation effect. When the scroll value is native, this interpolation runs on the
|
|
43
|
+
// UI thread: the smooth pin.
|
|
39
44
|
const [animatedTranslateY, setAnimatedTranslateY] = useState(() => scrollAnimatedValue.interpolate({ inputRange: [-1, 0], outputRange: [0, 0] }));
|
|
40
|
-
// The debounced EXPLICIT translateY pushed to the committed transform via
|
|
41
|
-
// passthroughAnimatedPropExplicitValues, so the Fabric ShadowTree (hit-testing) knows the pinned
|
|
42
|
-
// position while the native driver animates. null until the listener first fires.
|
|
43
|
-
const [translateY, setTranslateY] = useState(null);
|
|
44
|
-
const haveReceivedInitialZeroTranslateY = useRef(true);
|
|
45
45
|
const debounceTimer = useRef(null);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
46
|
+
// The current interpolation node + its settled-value listener id, held so the next rebuild can
|
|
47
|
+
// detach the old listener (engine calls the reducer does NOT own) and unmount can clean up.
|
|
48
|
+
const interpolationRef = useRef(null);
|
|
49
|
+
const listenerIdRef = useRef(null);
|
|
50
|
+
const inputsRef = useRef({
|
|
51
|
+
os: Platform.OS,
|
|
52
|
+
inverted,
|
|
53
|
+
scrollViewHeight,
|
|
54
|
+
nextHeaderLayoutY,
|
|
55
|
+
});
|
|
56
|
+
inputsRef.current = { os: Platform.OS, inverted, scrollViewHeight, nextHeaderLayoutY };
|
|
57
|
+
// dispatch reaches through a ref because the effect executors dispatch follow-up actions
|
|
58
|
+
// (the listener -> animated-tick, the debounce timer -> debounce-fired).
|
|
59
|
+
const dispatchRef = useRef(() => { });
|
|
60
|
+
const runEffects = useCallback((effects) => {
|
|
61
|
+
for (const effect of effects) {
|
|
62
|
+
switch (effect.kind) {
|
|
63
|
+
case 'rebuild-interpolation': {
|
|
64
|
+
// Detach the old listener, build a fresh interpolation onto the shared scroll value, and
|
|
65
|
+
// wire the settled-value listener (symbiote is always Fabric; RN attaches it only there).
|
|
66
|
+
const previous = interpolationRef.current;
|
|
67
|
+
if (previous !== null && listenerIdRef.current !== null) {
|
|
68
|
+
previous.removeListener(listenerIdRef.current);
|
|
69
|
+
listenerIdRef.current = null;
|
|
70
|
+
}
|
|
71
|
+
const next = scrollAnimatedValue.interpolate({
|
|
72
|
+
inputRange: effect.inputRange,
|
|
73
|
+
outputRange: effect.outputRange,
|
|
74
|
+
});
|
|
75
|
+
listenerIdRef.current = next.addListener(({ value }) => {
|
|
76
|
+
if (typeof value === 'number')
|
|
77
|
+
dispatchRef.current({ kind: 'animated-tick', value });
|
|
78
|
+
});
|
|
79
|
+
interpolationRef.current = next;
|
|
80
|
+
setAnimatedTranslateY(next);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case 'schedule-debounce': {
|
|
84
|
+
// The animated value updates several times per frame; debounce the settled value into the
|
|
85
|
+
// committed transform so hit detection stays current (a Fabric issue, worse on Android).
|
|
86
|
+
if (debounceTimer.current !== null)
|
|
87
|
+
clearTimeout(debounceTimer.current);
|
|
88
|
+
debounceTimer.current = setTimeout(() => {
|
|
89
|
+
debounceTimer.current = null;
|
|
90
|
+
dispatchRef.current({ kind: 'debounce-fired', value: effect.value });
|
|
91
|
+
}, effect.delay);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case 'apply-passthrough':
|
|
95
|
+
forceRender();
|
|
96
|
+
break;
|
|
97
|
+
case 'record-header-y':
|
|
98
|
+
// React records through the wrapper's onLayout closure (props.onLayout, below), which
|
|
99
|
+
// honors the public IStickyHeaderProps contract; the reducer emits no index for it.
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
61
102
|
}
|
|
103
|
+
}, [scrollAnimatedValue]);
|
|
104
|
+
const dispatch = useCallback((action) => {
|
|
105
|
+
const current = stateRef.current;
|
|
106
|
+
if (current === null)
|
|
107
|
+
return;
|
|
108
|
+
runEffects(reduceSticky(current, action, inputsRef.current).effects);
|
|
109
|
+
}, [runEffects]);
|
|
110
|
+
dispatchRef.current = dispatch;
|
|
111
|
+
// Rebuild when the collision/viewport inputs change (RN effect deps minus the layout state, which
|
|
112
|
+
// dispatches 'layout' itself); also does the initial identity build on mount.
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
dispatchRef.current({ kind: 'inputs-changed' });
|
|
115
|
+
}, [inverted, scrollViewHeight, nextHeaderLayoutY, scrollAnimatedValue]);
|
|
116
|
+
// Detach the listener + clear the debounce on unmount.
|
|
117
|
+
useEffect(() => () => {
|
|
118
|
+
const previous = interpolationRef.current;
|
|
119
|
+
if (previous !== null && listenerIdRef.current !== null)
|
|
120
|
+
previous.removeListener(listenerIdRef.current);
|
|
62
121
|
if (debounceTimer.current !== null)
|
|
63
122
|
clearTimeout(debounceTimer.current);
|
|
64
|
-
debounceTimer.current = setTimeout(() => setTranslateY(value), timeout);
|
|
65
123
|
}, []);
|
|
66
|
-
useEffect(() => {
|
|
67
|
-
const { inputRange, outputRange } = computeStickyInterpolation({
|
|
68
|
-
measured,
|
|
69
|
-
inverted,
|
|
70
|
-
scrollViewHeight,
|
|
71
|
-
layoutY,
|
|
72
|
-
layoutHeight,
|
|
73
|
-
nextHeaderLayoutY,
|
|
74
|
-
});
|
|
75
|
-
const newAnimatedTranslateY = scrollAnimatedValue.interpolate({ inputRange, outputRange });
|
|
76
|
-
// symbiote is always Fabric: listen to the settled value to keep the ShadowTree transform
|
|
77
|
-
// current for hit-testing (RN attaches this listener only under Fabric).
|
|
78
|
-
const listenerId = newAnimatedTranslateY.addListener(animatedValueListener);
|
|
79
|
-
setAnimatedTranslateY(newAnimatedTranslateY);
|
|
80
|
-
return () => {
|
|
81
|
-
newAnimatedTranslateY.removeListener(listenerId);
|
|
82
|
-
if (debounceTimer.current !== null)
|
|
83
|
-
clearTimeout(debounceTimer.current);
|
|
84
|
-
};
|
|
85
|
-
}, [
|
|
86
|
-
measured,
|
|
87
|
-
layoutY,
|
|
88
|
-
layoutHeight,
|
|
89
|
-
scrollViewHeight,
|
|
90
|
-
nextHeaderLayoutY,
|
|
91
|
-
inverted,
|
|
92
|
-
scrollAnimatedValue,
|
|
93
|
-
animatedValueListener,
|
|
94
|
-
]);
|
|
95
124
|
const onLayout = (event) => {
|
|
96
125
|
const y = readLayoutNumber(event, 'y');
|
|
97
126
|
const height = readLayoutNumber(event, 'height');
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (height !== undefined)
|
|
101
|
-
setLayoutHeight(height);
|
|
102
|
-
setMeasured(true);
|
|
127
|
+
// Keep the previous value when a field is absent (RN sets state only on a defined read).
|
|
128
|
+
dispatch({ kind: 'layout', y: y ?? state.layoutY, height: height ?? state.layoutHeight });
|
|
103
129
|
props.onLayout(event);
|
|
104
130
|
const child = firstChild(children);
|
|
105
131
|
const childOnLayout = child === undefined ? undefined : readChildOnLayout(child);
|
|
@@ -107,7 +133,7 @@ export const ScrollViewStickyHeader = props => {
|
|
|
107
133
|
};
|
|
108
134
|
// The EXPLICIT debounced translateY overrides the committed transform for hit-testing, while
|
|
109
135
|
// `animatedTranslateY` does the smooth (native-driven) pin, per RN ScrollViewStickyHeader.js.
|
|
110
|
-
const passthroughAnimatedPropExplicitValues = translateY !== null ? { style: { transform: [{ translateY }] } } : null;
|
|
136
|
+
const passthroughAnimatedPropExplicitValues = state.translateY !== null ? { style: { transform: [{ translateY: state.translateY }] } } : null;
|
|
111
137
|
// collapsable:false keeps the wrapper a real Yoga node; zIndex makes the pinned header paint
|
|
112
138
|
// OVER the rows scrolling under it. `style` is `unknown` on Animated.View, so the interpolation
|
|
113
139
|
// transform passes with no cast.
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
+
import type { ISwitchProps } from './shared';
|
|
1
2
|
export type { ISwitchProps, ISwitchTrackColor } from './shared';
|
|
2
|
-
export declare
|
|
3
|
+
export declare function Switch(rawProps: ISwitchProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
// Switch, Android host binding. AndroidSwitch takes trackColorForTrue / trackColorForFalse
|
|
2
2
|
// plus trackTintColor (the color for the CURRENT value, which RN computes as value ? true :
|
|
3
3
|
// false), and snaps native back via the `setNativeValue` command (Switch.js:221-225).
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import { descriptorToReact } from '../../descriptor-to-react/index.js';
|
|
5
|
+
import { useSwitchLogic } from './shared.js';
|
|
6
|
+
const PLATFORM = {
|
|
6
7
|
snapBackCommand: 'setNativeValue',
|
|
7
8
|
trackColorProps: (value, trackColor) => ({
|
|
8
9
|
trackColorForFalse: trackColor?.false,
|
|
9
10
|
trackColorForTrue: trackColor?.true,
|
|
10
11
|
trackTintColor: value ? trackColor?.true : trackColor?.false,
|
|
11
12
|
}),
|
|
12
|
-
}
|
|
13
|
+
};
|
|
14
|
+
// A top-level named function, not a factory-returned closure: React Compiler's
|
|
15
|
+
// component detection only walks top-level declarations (see shared.ts).
|
|
16
|
+
export function Switch(rawProps) {
|
|
17
|
+
return descriptorToReact(useSwitchLogic(rawProps, PLATFORM));
|
|
18
|
+
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
+
import type { ISwitchProps } from './shared';
|
|
1
2
|
export type { ISwitchProps, ISwitchTrackColor } from './shared';
|
|
2
|
-
export declare
|
|
3
|
+
export declare function Switch(rawProps: ISwitchProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
// Switch, iOS host binding. iOS's native Switch takes onTintColor (ON-track) / tintColor
|
|
2
2
|
// (OFF-track), and snaps native back via the `setValue` command (Switch.js:221-225).
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import { descriptorToReact } from '../../descriptor-to-react/index.js';
|
|
4
|
+
import { useSwitchLogic } from './shared.js';
|
|
5
|
+
const PLATFORM = {
|
|
5
6
|
snapBackCommand: 'setValue',
|
|
6
7
|
trackColorProps: (_value, trackColor) => ({
|
|
7
8
|
onTintColor: trackColor?.true,
|
|
8
9
|
tintColor: trackColor?.false,
|
|
9
10
|
}),
|
|
10
|
-
}
|
|
11
|
+
};
|
|
12
|
+
// A top-level named function, not a factory-returned closure: React Compiler's
|
|
13
|
+
// component detection only walks top-level declarations (see shared.ts).
|
|
14
|
+
export function Switch(rawProps) {
|
|
15
|
+
return descriptorToReact(useSwitchLogic(rawProps, PLATFORM));
|
|
16
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import type { FC } from 'react';
|
|
2
1
|
import type { ISwitchPlatform, ISwitchProps as ISwitchBaseProps } from '@symbiote-native/components';
|
|
3
2
|
export type { ISwitchTrackColor } from '@symbiote-native/components';
|
|
4
3
|
export type ISwitchProps = ISwitchBaseProps & {
|
|
5
4
|
className?: string;
|
|
6
5
|
};
|
|
7
|
-
type ISwitchHostPlatform = ISwitchPlatform & {
|
|
6
|
+
export type ISwitchHostPlatform = ISwitchPlatform & {
|
|
8
7
|
snapBackCommand: string;
|
|
9
8
|
};
|
|
10
|
-
export declare function
|
|
9
|
+
export declare function useSwitchLogic(rawProps: ISwitchProps, platform: ISwitchHostPlatform): import("@symbiote-native/components").IDescriptor;
|
|
@@ -9,51 +9,52 @@
|
|
|
9
9
|
// JS commands the old value back down (a plain prop re-push cannot cover the no-op case: the
|
|
10
10
|
// prop never changes, so the retained tree never diverges). The command name is platform-
|
|
11
11
|
// specific (iOS setValue / Android setNativeValue) and arrives via `platform`.
|
|
12
|
+
//
|
|
13
|
+
// useSwitchLogic is a plain hook, not a component factory returning a closure: React Compiler's
|
|
14
|
+
// component/hook detection only walks top-level declarations, so index.ios.ts / index.android.ts
|
|
15
|
+
// each declare their OWN top-level `Switch` function that calls this hook, rather than exporting
|
|
16
|
+
// whatever a factory here returns.
|
|
12
17
|
import { useCallback, useLayoutEffect, useReducer, useRef } from 'react';
|
|
13
18
|
import { renderSwitch, switchReducer, createInitialSwitchState, shouldSnapBack, valueFromChange, } from '@symbiote-native/components';
|
|
14
19
|
import { dispatchViewCommand, dlog, } from '@symbiote-native/engine';
|
|
15
20
|
import { resolveAccessibilityProps } from '@symbiote-native/components';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
passthrough: { ...passthrough, ref, onChange: handleChange },
|
|
56
|
-
}, platform);
|
|
57
|
-
return descriptorToReact(descriptor);
|
|
58
|
-
};
|
|
21
|
+
export function useSwitchLogic(rawProps, platform) {
|
|
22
|
+
// Switch owns its host element rather than rendering through a symbiote View, so it folds
|
|
23
|
+
// aria/role into accessibility* here; the resolved accessibility* fields (plus testID etc.)
|
|
24
|
+
// ride down to the host node via `passthrough`.
|
|
25
|
+
const props = resolveAccessibilityProps(rawProps);
|
|
26
|
+
const { value, onValueChange, disabled, trackColor, thumbColor, ios_backgroundColor, style, ...passthrough } = props;
|
|
27
|
+
const ref = useRef(null);
|
|
28
|
+
const [state, dispatch] = useReducer(switchReducer, undefined, createInitialSwitchState);
|
|
29
|
+
const handleChange = useCallback((event) => {
|
|
30
|
+
const next = valueFromChange(event);
|
|
31
|
+
dlog(`Switch onChange value=${String(next)} eventCount=${String(event.nativeEvent.eventCount)}`);
|
|
32
|
+
if (next === undefined)
|
|
33
|
+
return;
|
|
34
|
+
onValueChange?.(next, event);
|
|
35
|
+
dispatch({ type: 'native-reported', value: next });
|
|
36
|
+
}, [onValueChange]);
|
|
37
|
+
// value is a real Fabric prop, folded to a strict boolean: RN sends `value === true`, so
|
|
38
|
+
// an undefined prop reads as "off" rather than absent.
|
|
39
|
+
const fabricValue = value === true;
|
|
40
|
+
useLayoutEffect(() => {
|
|
41
|
+
const node = ref.current;
|
|
42
|
+
if (node === null)
|
|
43
|
+
return;
|
|
44
|
+
if (!shouldSnapBack(state, fabricValue)) {
|
|
45
|
+
dlog(`Switch snap-back no-op reported=${String(state.lastNativeReport)} value=${fabricValue}`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
dlog(`Switch ${platform.snapBackCommand} snap-back reported=${String(state.lastNativeReport)} value=${fabricValue}`);
|
|
49
|
+
dispatchViewCommand(node, platform.snapBackCommand, [fabricValue]);
|
|
50
|
+
}, [fabricValue, state]);
|
|
51
|
+
return renderSwitch({
|
|
52
|
+
value: fabricValue,
|
|
53
|
+
disabled,
|
|
54
|
+
trackColor,
|
|
55
|
+
thumbColor,
|
|
56
|
+
ios_backgroundColor,
|
|
57
|
+
style,
|
|
58
|
+
passthrough: { ...passthrough, ref, onChange: handleChange },
|
|
59
|
+
}, platform);
|
|
59
60
|
}
|