@symbiote-native/react 0.2.7 → 0.3.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/build/bootstrap.js +4 -5
- package/build/components/activity-indicator/index.android.d.ts +2 -1
- package/build/components/activity-indicator/index.android.js +14 -10
- 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/keyboard-avoiding-view/index.js +1 -1
- package/build/components/modal/index.d.ts +3 -2
- package/build/components/scroll-view/index.android.js +0 -2
- package/build/components/scroll-view/index.ios.js +10 -10
- package/build/components/scroll-view/shared.js +28 -32
- package/build/components/scroll-view/sticky-header.js +98 -74
- package/build/components/section-list/index.js +4 -6
- 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/text-input/index.js +1 -1
- package/build/components/touchable/index.js +22 -54
- package/build/components/virtualized-list/index.js +219 -305
- package/build/components/virtualized-section-list/index.js +2 -4
- package/build/create-tunnel/index.js +14 -20
- package/build/hooks/use-color-scheme.d.ts +1 -1
- package/build/hooks/use-color-scheme.js +1 -1
- package/build/hooks/use-window-dimensions.d.ts +1 -1
- package/build/hooks/use-window-dimensions.js +1 -1
- package/build/host-instance.js +5 -6
- package/build/index.d.ts +6 -31
- package/build/index.js +15 -25
- package/build/modules/animated/create-animated-component.js +24 -65
- package/build/render.js +28 -2
- package/metro-css-parser.cjs +7 -12
- package/package.json +4 -4
- package/build/modules/action-sheet-ios.d.ts +0 -2
- package/build/modules/action-sheet-ios.js +0 -4
- package/build/modules/alert.d.ts +0 -2
- package/build/modules/alert.js +0 -5
- package/build/modules/app-state.d.ts +0 -2
- package/build/modules/app-state.js +0 -3
- package/build/modules/appearance.d.ts +0 -2
- package/build/modules/appearance.js +0 -3
- package/build/modules/back-handler.d.ts +0 -2
- package/build/modules/back-handler.js +0 -3
- package/build/modules/dimensions.d.ts +0 -2
- package/build/modules/dimensions.js +0 -3
- package/build/modules/i18n-manager.d.ts +0 -2
- package/build/modules/i18n-manager.js +0 -4
- package/build/modules/keyboard.d.ts +0 -2
- package/build/modules/keyboard.js +0 -3
- package/build/modules/layout-animation.d.ts +0 -2
- package/build/modules/layout-animation.js +0 -3
- package/build/modules/linking.d.ts +0 -2
- package/build/modules/linking.js +0 -4
- package/build/modules/permissions-android.d.ts +0 -2
- package/build/modules/permissions-android.js +0 -3
- package/build/modules/settings.d.ts +0 -1
- package/build/modules/settings.js +0 -4
- package/build/modules/share.d.ts +0 -2
- package/build/modules/share.js +0 -4
- package/build/modules/text-input-state.d.ts +0 -1
- package/build/modules/text-input-state.js +0 -3
- package/build/modules/toast-android.d.ts +0 -1
- package/build/modules/toast-android.js +0 -4
- package/build/modules/vibration.d.ts +0 -1
- package/build/modules/vibration.js +0 -4
package/build/bootstrap.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
// Zero-config app entry: wires the four RN-backed host seams (via @symbiote-native/components'
|
|
1
|
+
// Zero-config app entry: wires the four RN-backed host seams (via @symbiote-native/components's
|
|
2
2
|
// bootstrapHost) plus this adapter's own AppRegistry host-registrar bridge, then registers the
|
|
3
|
-
// root component
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// @symbiote-native/components/bootstrap for the full reason).
|
|
3
|
+
// root component in one call. Lives OUTSIDE the package's main barrel (package.json's
|
|
4
|
+
// "./bootstrap" export) because it imports react-native directly, which Vitest's Flow-unaware
|
|
5
|
+
// transform can't parse (see @symbiote-native/components/bootstrap for the full reason).
|
|
7
6
|
import { AppRegistry as RNAppRegistry } from 'react-native';
|
|
8
7
|
import { bootstrapHost } from '@symbiote-native/components/bootstrap';
|
|
9
8
|
import { AppRegistry, setHostRegistrar } from './modules/app-registry/index.js';
|
|
@@ -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,13 +1,17 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export const ActivityIndicator = createActivityIndicator({
|
|
1
|
+
// AndroidProgressBar needs `styleAttr` (drives ProgressBar.setStyle(); missing it throws
|
|
2
|
+
// "setStyle() not called") and `indeterminate: true`; default color is the theme (null) —
|
|
3
|
+
// mirrors RN's ActivityIndicator.js android branch. Metro selects this file on an Android
|
|
4
|
+
// host, no Platform.OS read needed.
|
|
5
|
+
// device-verify-pending: confirmed by the absence of the setStyle() red box on a real host.
|
|
6
|
+
import { descriptorToReact } from '../../descriptor-to-react/index.js';
|
|
7
|
+
import { useActivityIndicatorLogic } from './shared.js';
|
|
8
|
+
const PLATFORM = {
|
|
10
9
|
// RN: `color = Platform.OS === 'ios' ? GRAY : null`; Android lets the theme color it.
|
|
11
10
|
defaultColor: null,
|
|
12
11
|
nativeExtras: { styleAttr: 'Normal', indeterminate: true },
|
|
13
|
-
}
|
|
12
|
+
};
|
|
13
|
+
// A top-level named function, not a factory-returned closure: React Compiler's
|
|
14
|
+
// component detection only walks top-level declarations (see shared.ts).
|
|
15
|
+
export function ActivityIndicator(rawProps) {
|
|
16
|
+
return descriptorToReact(useActivityIndicatorLogic(rawProps, PLATFORM));
|
|
17
|
+
}
|
|
@@ -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
|
}
|
|
@@ -11,7 +11,7 @@ import { createElement, useEffect, useRef, useState, } from 'react';
|
|
|
11
11
|
import { dlog } from '@symbiote-native/engine';
|
|
12
12
|
import { computeInset, readKeyboardFrame, readLayoutFrame, resolveKeyboardAvoidingLayout, DEFAULT_VERTICAL_OFFSET, } from '@symbiote-native/components';
|
|
13
13
|
import { View } from '../../components.js';
|
|
14
|
-
import { Keyboard, KEYBOARD_EVENT } from '
|
|
14
|
+
import { Keyboard, KEYBOARD_EVENT } from '@symbiote-native/engine';
|
|
15
15
|
export const KeyboardAvoidingView = props => {
|
|
16
16
|
const { behavior, enabled = true, keyboardVerticalOffset = DEFAULT_VERTICAL_OFFSET, contentContainerStyle, style, children, onLayout,
|
|
17
17
|
// The wrapper is the View FC, which runs resolveAccessibilityProps itself, so
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type FC, type ReactNode } from 'react';
|
|
2
|
-
import { type
|
|
2
|
+
import { type ISymbioteEvent } from '@symbiote-native/engine';
|
|
3
|
+
import { type IAccessibilityProps, type IAriaProps, type IModalAnimationType, type IModalOrientation, type IModalPresentationStyle } from '@symbiote-native/components';
|
|
3
4
|
import type { IStyleProp, IViewStyle } from '../../utils/styles';
|
|
4
5
|
export type { IModalAnimationType, IModalPresentationStyle, IModalOrientation, IModalOrientationChangeEvent, } from '@symbiote-native/components';
|
|
5
6
|
export interface IModalProps extends IAccessibilityProps, IAriaProps {
|
|
@@ -16,7 +17,7 @@ export interface IModalProps extends IAccessibilityProps, IAriaProps {
|
|
|
16
17
|
onShow?: () => void;
|
|
17
18
|
onDismiss?: () => void;
|
|
18
19
|
onRequestClose?: () => void;
|
|
19
|
-
onOrientationChange?: (event:
|
|
20
|
+
onOrientationChange?: (event: ISymbioteEvent) => void;
|
|
20
21
|
style?: IStyleProp<IViewStyle>;
|
|
21
22
|
className?: string;
|
|
22
23
|
children?: ReactNode;
|
|
@@ -6,8 +6,6 @@
|
|
|
6
6
|
// android branch (cloneElement(refreshControl, {style}, <ScrollView nestedScrollEnabled
|
|
7
7
|
// style={flex:1}>{content}</ScrollView>)). Metro picks this on an Android host; no
|
|
8
8
|
// Platform.OS read.
|
|
9
|
-
// device-verify-pending: the wrap shape mirrors RN, proven on a real host by the absence
|
|
10
|
-
// of the "addViewAt: failed to insert" crash.
|
|
11
9
|
import { cloneElement, createElement, forwardRef, useImperativeHandle, useRef } from 'react';
|
|
12
10
|
import { dlog } from '@symbiote-native/engine';
|
|
13
11
|
import { buildScrollViewHandle, splitLayoutProps } from '@symbiote-native/components';
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
// ScrollView
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// ScrollView (iOS): RefreshControl renders as a sibling BEFORE the content container, matching
|
|
2
|
+
// RN's ScrollView.js ({refreshControl}{contentContainer}). Also the base export
|
|
3
|
+
// (scroll-view.ts re-exports it) for headless / web.
|
|
4
4
|
import { createElement, forwardRef, useImperativeHandle, useRef } from 'react';
|
|
5
5
|
import { buildScrollViewHandle } from '@symbiote-native/components';
|
|
6
6
|
import { usePreparedScrollView, useNativeStickyScrollAttach, } from './shared.js';
|
|
7
7
|
export const ScrollView = forwardRef((props, forwardedRef) => {
|
|
8
8
|
const { scrollViewIntrinsic, scrollViewBaseStyle, outerProps, style, content, refreshControl, scrollAnimatedValue, nativeStickyAvailable, } = usePreparedScrollView(props);
|
|
9
|
-
//
|
|
10
|
-
//
|
|
9
|
+
// Backs the imperative handle; passing `ref` through createElement props binds it to the
|
|
10
|
+
// SymbioteNode below, as TextInput does.
|
|
11
11
|
const ref = useRef(null);
|
|
12
|
-
// Lazy getter
|
|
13
|
-
//
|
|
12
|
+
// Lazy getter: the node is null until commit, so the handle reads ref.current per call
|
|
13
|
+
// instead of capturing eagerly, which would freeze null.
|
|
14
14
|
useImperativeHandle(forwardedRef, () => buildScrollViewHandle(() => ref.current), []);
|
|
15
|
-
//
|
|
16
|
-
//
|
|
15
|
+
// Drives sticky scroll on the native UI thread (RN attachNativeEvent); no-ops without the
|
|
16
|
+
// native animated module, falling back to the JS sticky path.
|
|
17
17
|
useNativeStickyScrollAttach(ref, scrollAnimatedValue, nativeStickyAvailable);
|
|
18
|
-
// Base style under user style so an explicit
|
|
18
|
+
// Base style stays under user style so an explicit value wins; undefined base (vertical)
|
|
19
19
|
// passes the user style through unchanged.
|
|
20
20
|
const scrollStyle = scrollViewBaseStyle ? [scrollViewBaseStyle, style] : style;
|
|
21
21
|
const scrollProps = { ...outerProps, style: scrollStyle, ref };
|
|
@@ -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) {
|
|
@@ -148,8 +144,8 @@ export function usePreparedScrollView(rawProps) {
|
|
|
148
144
|
onContentSizeChange(width, height);
|
|
149
145
|
};
|
|
150
146
|
}
|
|
151
|
-
//
|
|
152
|
-
//
|
|
147
|
+
// Wrap the flagged children so they pin to the scroll offset (see the sticky-headers note
|
|
148
|
+
// above); no-op when none are flagged.
|
|
153
149
|
const contentChildren = hasStickyHeaders
|
|
154
150
|
? wrapStickyHeaders(children, stickyHeaderIndices, scrollAnimatedValue, invertStickyHeaders, viewportHeight, StickyHeaderComponent, headerLayoutYs, onHeaderLayoutY)
|
|
155
151
|
: children;
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
// Sticky headers: the JS layer RN implements in ScrollView.js / ScrollViewStickyHeader.js.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
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';
|
|
3
|
+
// RN does stickiness purely in JS: ScrollView.js (~line 1690) wraps each flagged child in a
|
|
4
|
+
// ScrollViewStickyHeader, fed by a single `_scrollAnimatedValue` that Animated.event drives
|
|
5
|
+
// from `onScroll` (~line 1095). The native Fabric scroll view does not honor the index array,
|
|
6
|
+
// so forwarding `stickyHeaderIndices` to native would be a silent no-op - we replicate the JS
|
|
7
|
+
// layer instead. The interpolation mirrors ScrollViewStickyHeader.js and now lives,
|
|
8
|
+
// framework-agnostic, in @symbiote-native/components (computeStickyInterpolation); this file
|
|
9
|
+
// holds only the React component shell, layout state, and child-wrapping.
|
|
10
|
+
import { Children, createElement, isValidElement, useCallback, useEffect, useReducer, useRef, useState, } from 'react';
|
|
13
11
|
import { AnimatedInterpolation, AnimatedValue, Platform, dlog, } from '@symbiote-native/engine';
|
|
14
|
-
import {
|
|
12
|
+
import { createInitialStickyState, nextStickyHeaderY, readLayoutNumber, reduceSticky, STICKY_HEADER_Z_INDEX, } from '@symbiote-native/components';
|
|
15
13
|
import { Animated } from '../../modules/animated/index.js';
|
|
16
14
|
function readChildOnLayout(child) {
|
|
17
15
|
const childProps = child.props;
|
|
@@ -27,79 +25,105 @@ function firstChild(children) {
|
|
|
27
25
|
// One sticky header. Measures its own y/height via onLayout, interpolates the shared scroll
|
|
28
26
|
// offset into a translateY that keeps it pinned to the top (or bottom, inverted) until the next
|
|
29
27
|
// 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
|
-
//
|
|
28
|
+
// the pin tracks scroll on the UI thread (no JS jitter). The DECISIONS — the zero-swallow gate, the
|
|
29
|
+
// debounce delay, the rebuild-on-input-change ranges — live in `reduceSticky`
|
|
30
|
+
// (@symbiote-native/components); this component supplies only the React lifecycle: the ONE folded
|
|
31
|
+
// state cell, the interpolation-node + listener wiring, the debounce setTimeout, and the re-render.
|
|
32
32
|
export const ScrollViewStickyHeader = props => {
|
|
33
33
|
const { inverted, scrollViewHeight, scrollAnimatedValue, nextHeaderLayoutY, children } = props;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
// The one folded state cell (RN's scattered useState/useRef collapsed into IStickyHeaderState),
|
|
35
|
+
// mutated in place by reduceSticky. Lazily created once.
|
|
36
|
+
const stateRef = useRef(null);
|
|
37
|
+
const state = (stateRef.current ??= createInitialStickyState());
|
|
38
|
+
const [, forceRender] = useReducer((tick) => tick + 1, 0);
|
|
39
|
+
// The animated node that drives the transform (RN's animatedTranslateY), rebuilt by the
|
|
40
|
+
// rebuild-interpolation effect. When the scroll value is native, this interpolation runs on the
|
|
41
|
+
// UI thread: the smooth pin.
|
|
39
42
|
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
43
|
const debounceTimer = useRef(null);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
44
|
+
// The current interpolation node + its settled-value listener id, held so the next rebuild can
|
|
45
|
+
// detach the old listener (engine calls the reducer does NOT own) and unmount can clean up.
|
|
46
|
+
const interpolationRef = useRef(null);
|
|
47
|
+
const listenerIdRef = useRef(null);
|
|
48
|
+
const inputsRef = useRef({
|
|
49
|
+
os: Platform.OS,
|
|
50
|
+
inverted,
|
|
51
|
+
scrollViewHeight,
|
|
52
|
+
nextHeaderLayoutY,
|
|
53
|
+
});
|
|
54
|
+
inputsRef.current = { os: Platform.OS, inverted, scrollViewHeight, nextHeaderLayoutY };
|
|
55
|
+
// dispatch reaches through a ref because the effect executors dispatch follow-up actions
|
|
56
|
+
// (the listener -> animated-tick, the debounce timer -> debounce-fired).
|
|
57
|
+
const dispatchRef = useRef(() => { });
|
|
58
|
+
const runEffects = useCallback((effects) => {
|
|
59
|
+
for (const effect of effects) {
|
|
60
|
+
switch (effect.kind) {
|
|
61
|
+
case 'rebuild-interpolation': {
|
|
62
|
+
// Detach the old listener, build a fresh interpolation onto the shared scroll value, and
|
|
63
|
+
// wire the settled-value listener (symbiote is always Fabric; RN attaches it only there).
|
|
64
|
+
const previous = interpolationRef.current;
|
|
65
|
+
if (previous !== null && listenerIdRef.current !== null) {
|
|
66
|
+
previous.removeListener(listenerIdRef.current);
|
|
67
|
+
listenerIdRef.current = null;
|
|
68
|
+
}
|
|
69
|
+
const next = scrollAnimatedValue.interpolate({
|
|
70
|
+
inputRange: effect.inputRange,
|
|
71
|
+
outputRange: effect.outputRange,
|
|
72
|
+
});
|
|
73
|
+
listenerIdRef.current = next.addListener(({ value }) => {
|
|
74
|
+
if (typeof value === 'number')
|
|
75
|
+
dispatchRef.current({ kind: 'animated-tick', value });
|
|
76
|
+
});
|
|
77
|
+
interpolationRef.current = next;
|
|
78
|
+
setAnimatedTranslateY(next);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case 'schedule-debounce': {
|
|
82
|
+
// The animated value updates several times per frame; debounce the settled value into the
|
|
83
|
+
// committed transform so hit detection stays current (a Fabric issue, worse on Android).
|
|
84
|
+
if (debounceTimer.current !== null)
|
|
85
|
+
clearTimeout(debounceTimer.current);
|
|
86
|
+
debounceTimer.current = setTimeout(() => {
|
|
87
|
+
debounceTimer.current = null;
|
|
88
|
+
dispatchRef.current({ kind: 'debounce-fired', value: effect.value });
|
|
89
|
+
}, effect.delay);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case 'apply-passthrough':
|
|
93
|
+
forceRender();
|
|
94
|
+
break;
|
|
95
|
+
case 'record-header-y':
|
|
96
|
+
// React records through the wrapper's onLayout closure (props.onLayout, below), which
|
|
97
|
+
// honors the public IStickyHeaderProps contract; the reducer emits no index for it.
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
61
100
|
}
|
|
101
|
+
}, [scrollAnimatedValue]);
|
|
102
|
+
const dispatch = useCallback((action) => {
|
|
103
|
+
const current = stateRef.current;
|
|
104
|
+
if (current === null)
|
|
105
|
+
return;
|
|
106
|
+
runEffects(reduceSticky(current, action, inputsRef.current).effects);
|
|
107
|
+
}, [runEffects]);
|
|
108
|
+
dispatchRef.current = dispatch;
|
|
109
|
+
// Rebuild when the collision/viewport inputs change (RN effect deps minus the layout state, which
|
|
110
|
+
// dispatches 'layout' itself); also does the initial identity build on mount.
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
dispatchRef.current({ kind: 'inputs-changed' });
|
|
113
|
+
}, [inverted, scrollViewHeight, nextHeaderLayoutY, scrollAnimatedValue]);
|
|
114
|
+
// Detach the listener + clear the debounce on unmount.
|
|
115
|
+
useEffect(() => () => {
|
|
116
|
+
const previous = interpolationRef.current;
|
|
117
|
+
if (previous !== null && listenerIdRef.current !== null)
|
|
118
|
+
previous.removeListener(listenerIdRef.current);
|
|
62
119
|
if (debounceTimer.current !== null)
|
|
63
120
|
clearTimeout(debounceTimer.current);
|
|
64
|
-
debounceTimer.current = setTimeout(() => setTranslateY(value), timeout);
|
|
65
121
|
}, []);
|
|
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
122
|
const onLayout = (event) => {
|
|
96
123
|
const y = readLayoutNumber(event, 'y');
|
|
97
124
|
const height = readLayoutNumber(event, 'height');
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (height !== undefined)
|
|
101
|
-
setLayoutHeight(height);
|
|
102
|
-
setMeasured(true);
|
|
125
|
+
// Keep the previous value when a field is absent (RN sets state only on a defined read).
|
|
126
|
+
dispatch({ kind: 'layout', y: y ?? state.layoutY, height: height ?? state.layoutHeight });
|
|
103
127
|
props.onLayout(event);
|
|
104
128
|
const child = firstChild(children);
|
|
105
129
|
const childOnLayout = child === undefined ? undefined : readChildOnLayout(child);
|
|
@@ -107,7 +131,7 @@ export const ScrollViewStickyHeader = props => {
|
|
|
107
131
|
};
|
|
108
132
|
// The EXPLICIT debounced translateY overrides the committed transform for hit-testing, while
|
|
109
133
|
// `animatedTranslateY` does the smooth (native-driven) pin, per RN ScrollViewStickyHeader.js.
|
|
110
|
-
const passthroughAnimatedPropExplicitValues = translateY !== null ? { style: { transform: [{ translateY }] } } : null;
|
|
134
|
+
const passthroughAnimatedPropExplicitValues = state.translateY !== null ? { style: { transform: [{ translateY: state.translateY }] } } : null;
|
|
111
135
|
// collapsable:false keeps the wrapper a real Yoga node; zIndex makes the pinned header paint
|
|
112
136
|
// OVER the rows scrolling under it. `style` is `unknown` on Animated.View, so the interpolation
|
|
113
137
|
// transform passes with no cast.
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
// SectionList:
|
|
2
|
-
//
|
|
3
|
-
// (SectionList -> VirtualizedSectionList -> VirtualizedList). All the
|
|
1
|
+
// SectionList: public wrapper over VirtualizedSectionList, mirroring RN's
|
|
2
|
+
// SectionList -> VirtualizedSectionList -> VirtualizedList layering. All
|
|
4
3
|
// section-flattening / windowing / imperative-scroll logic lives in
|
|
5
|
-
// VirtualizedSectionList; this
|
|
6
|
-
// SectionList name
|
|
7
|
-
// here, threading the imperative ref straight through.
|
|
4
|
+
// VirtualizedSectionList; this re-exposes the same surface under the
|
|
5
|
+
// SectionList name, threading the imperative ref straight through.
|
|
8
6
|
import { createElement } from 'react';
|
|
9
7
|
import { VirtualizedSectionList, } from '../virtualized-section-list/index.js';
|
|
10
8
|
export function SectionList(props) {
|
|
@@ -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;
|