@symbiote-native/react 0.4.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -16
- package/build/components/activity-indicator-props.d.ts +4 -0
- package/build/components/activity-indicator-props.js +1 -0
- package/build/components/{button.d.ts → button-props.d.ts} +0 -2
- package/build/components/button-props.js +1 -0
- package/build/components/flat-list/index.js +2 -2
- package/build/components/image/image-props.d.ts +4 -0
- package/build/components/image/image-props.js +1 -0
- package/build/components/image-background-props.d.ts +9 -0
- package/build/components/image-background-props.js +1 -0
- package/build/components/input-accessory-view-props.d.ts +10 -0
- package/build/components/input-accessory-view-props.js +1 -0
- package/build/components/keyboard-avoiding-view/index.js +4 -4
- package/build/components/modal/index.js +3 -3
- package/build/components/pressable/{index.d.ts → pressable-props.d.ts} +4 -4
- package/build/components/pressable/pressable-props.js +1 -0
- package/build/components/{refresh-control/index.d.ts → refresh-control-props.d.ts} +3 -3
- package/build/components/refresh-control-props.js +1 -0
- package/build/components/safe-area-view-props.d.ts +11 -0
- package/build/components/safe-area-view-props.js +1 -0
- package/build/components/scroll-view/{shared.d.ts → scroll-view-props.d.ts} +4 -26
- package/build/components/scroll-view/scroll-view-props.js +1 -0
- package/build/components/switch/switch-props.d.ts +4 -0
- package/build/components/switch/switch-props.js +1 -0
- package/build/components/text-input/text-input-props.d.ts +4 -0
- package/build/components/text-input/text-input-props.js +1 -0
- package/build/components/touchable/touchable-props.d.ts +17 -0
- package/build/components/touchable/touchable-props.js +1 -0
- package/build/components/touchable-native-feedback/touchable-native-feedback-props.d.ts +8 -0
- package/build/components/touchable-native-feedback/touchable-native-feedback-props.js +1 -0
- package/build/components/touchable-without-feedback/touchable-without-feedback-props.d.ts +6 -0
- package/build/components/touchable-without-feedback/touchable-without-feedback-props.js +1 -0
- package/build/components/virtualized-list/index.js +83 -38
- package/build/components.d.ts +3 -4
- package/build/components.js +5 -17
- package/build/create-portal/index.js +1 -1
- package/build/create-tunnel/index.js +1 -1
- package/build/descriptor-to-react/index.js +1 -1
- package/build/host-config.d.ts +2 -2
- package/build/host-config.js +51 -10
- package/build/host-instance.js +1 -1
- package/build/index.d.ts +23 -27
- package/build/index.js +14 -14
- package/build/jsx-dev-runtime.d.ts +3 -0
- package/build/jsx-dev-runtime.js +10 -0
- package/build/jsx-namespace.type-check.d.ts +1 -0
- package/build/jsx-namespace.type-check.js +7 -0
- package/build/jsx-runtime.d.ts +62 -0
- package/build/jsx-runtime.js +1 -0
- package/build/modules/animated/create-animated-component.d.ts +2 -1
- package/build/modules/animated/create-animated-component.js +7 -1
- package/build/modules/animated/index.d.ts +0 -1
- package/build/modules/animated/index.js +16 -22
- package/build/modules/image/index.d.ts +3 -0
- package/build/modules/image/index.js +14 -0
- package/build/register.d.ts +1 -0
- package/build/register.js +15 -0
- package/build/render.js +2 -4
- package/metro-css-parser.cjs +2 -1
- package/package.json +28 -8
- package/build/components/activity-indicator/index.android.d.ts +0 -3
- package/build/components/activity-indicator/index.android.js +0 -17
- package/build/components/activity-indicator/index.d.ts +0 -1
- package/build/components/activity-indicator/index.ios.d.ts +0 -3
- package/build/components/activity-indicator/index.ios.js +0 -16
- package/build/components/activity-indicator/index.js +0 -4
- package/build/components/activity-indicator/shared.d.ts +0 -6
- package/build/components/activity-indicator/shared.js +0 -22
- package/build/components/button.js +0 -42
- package/build/components/image/index.d.ts +0 -9
- package/build/components/image/index.js +0 -28
- package/build/components/image-background/index.d.ts +0 -10
- package/build/components/image-background/index.js +0 -46
- package/build/components/input-accessory-view/index.d.ts +0 -11
- package/build/components/input-accessory-view/index.js +0 -19
- package/build/components/pressable/index.js +0 -100
- package/build/components/refresh-control/index.js +0 -21
- package/build/components/safe-area-view/index.d.ts +0 -11
- package/build/components/safe-area-view/index.js +0 -18
- package/build/components/scroll-view/index.android.d.ts +0 -3
- package/build/components/scroll-view/index.android.js +0 -61
- package/build/components/scroll-view/index.d.ts +0 -1
- package/build/components/scroll-view/index.ios.d.ts +0 -3
- package/build/components/scroll-view/index.ios.js +0 -29
- package/build/components/scroll-view/index.js +0 -4
- package/build/components/scroll-view/shared.js +0 -240
- package/build/components/scroll-view/sticky-header.d.ts +0 -18
- package/build/components/scroll-view/sticky-header.js +0 -227
- package/build/components/switch/index.android.d.ts +0 -3
- package/build/components/switch/index.android.js +0 -18
- package/build/components/switch/index.d.ts +0 -1
- package/build/components/switch/index.ios.d.ts +0 -3
- package/build/components/switch/index.ios.js +0 -16
- package/build/components/switch/index.js +0 -6
- package/build/components/switch/shared.d.ts +0 -9
- package/build/components/switch/shared.js +0 -60
- package/build/components/text-input/index.d.ts +0 -50
- package/build/components/text-input/index.js +0 -172
- package/build/components/touchable/index.d.ts +0 -22
- package/build/components/touchable/index.js +0 -165
- package/build/components/touchable-native-feedback/index.d.ts +0 -18
- package/build/components/touchable-native-feedback/index.js +0 -32
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
// Pressable: the React lifecycle half. The press lifecycle (the long-press timer, unstable_press-
|
|
2
|
-
// Delay deferral, the pressRetentionOffset drift test, the suppression flags) lives in
|
|
3
|
-
// @symbiote-native/components/state as a pure machine over a runtime + host; the render decisions (the
|
|
4
|
-
// responder listeners, the disabled→accessibilityState fold, the ripple prop) in
|
|
5
|
-
// @symbiote-native/components/view. Here React owns only the lifecycle: useState for `pressed`, a ref for
|
|
6
|
-
// the runtime, a ref for the View instance (the measure handle), and a useMemo that rebuilds the
|
|
7
|
-
// handlers when the config changes. This is the React twin of the Vue adapter's setup-scope
|
|
8
|
-
// runtime + setPressed ref.
|
|
9
|
-
//
|
|
10
|
-
// Three RN interaction props that change real app feel ride on top of the shared synthesis,
|
|
11
|
-
// entirely in JS: android_ripple (an Android native-feedback prop on a dedicated inner View, inert
|
|
12
|
-
// on iOS), unstable_pressDelay, and pressRetentionOffset (the drift region), all handled by the
|
|
13
|
-
// shared machine; here we only wire its host + render the View.
|
|
14
|
-
import { createElement, useMemo, useRef, useState, } from 'react';
|
|
15
|
-
import { createPressHandlers, createPressRuntime, rippleProps, buildPressableListeners, resolveDisabledAccessibilityState, noteHoverNoop, DEFAULT_DELAY_LONG_PRESS_MS, } from '@symbiote-native/components';
|
|
16
|
-
import { View } from '../../components.js';
|
|
17
|
-
function resolveStyle(style, state) {
|
|
18
|
-
if (typeof style === 'function')
|
|
19
|
-
return style(state);
|
|
20
|
-
return style;
|
|
21
|
-
}
|
|
22
|
-
function resolveChildren(children, state) {
|
|
23
|
-
if (typeof children === 'function')
|
|
24
|
-
return children(state);
|
|
25
|
-
return children;
|
|
26
|
-
}
|
|
27
|
-
export const Pressable = props => {
|
|
28
|
-
const { onPress, onPressIn, onPressOut, onPressMove, onLongPress, delayLongPress = DEFAULT_DELAY_LONG_PRESS_MS, disabled, cancelable, hitSlop, pressRetentionOffset, unstable_pressDelay = 0, android_ripple, android_disableSound, onHoverIn, onHoverOut, delayHoverIn, delayHoverOut, accessibilityState, testID, style, children,
|
|
29
|
-
// The remaining accessibility / aria props forward to View untouched; View runs
|
|
30
|
-
// resolveAccessibilityProps, so aria/role fold there, once.
|
|
31
|
-
...accessibilityRest } = props;
|
|
32
|
-
const [pressed, setPressed] = useState(false);
|
|
33
|
-
// The mutable press runtime (timers, suppression flags, measured region), a ref so mutating it
|
|
34
|
-
// never triggers a re-render; only setPressed does.
|
|
35
|
-
const runtime = useRef(createPressRuntime()).current;
|
|
36
|
-
// The View instance, so the machine can measure its on-screen rect on responder grant.
|
|
37
|
-
const viewRef = useRef(null);
|
|
38
|
-
// The lifecycle seam the machine fills: flip pressed-state, and expose the View's raw measure.
|
|
39
|
-
const host = useMemo(() => ({
|
|
40
|
-
setPressed,
|
|
41
|
-
getMeasureFn: () => {
|
|
42
|
-
const view = viewRef.current;
|
|
43
|
-
if (view === null)
|
|
44
|
-
return undefined;
|
|
45
|
-
return callback => view.measure(callback);
|
|
46
|
-
},
|
|
47
|
-
schedule: (callback, ms) => {
|
|
48
|
-
const id = setTimeout(callback, ms);
|
|
49
|
-
return () => clearTimeout(id);
|
|
50
|
-
},
|
|
51
|
-
}), []);
|
|
52
|
-
const handlers = useMemo(() => createPressHandlers({
|
|
53
|
-
onPress,
|
|
54
|
-
onPressIn,
|
|
55
|
-
onPressOut,
|
|
56
|
-
onPressMove,
|
|
57
|
-
onLongPress,
|
|
58
|
-
delayLongPress,
|
|
59
|
-
unstable_pressDelay,
|
|
60
|
-
hitSlop,
|
|
61
|
-
pressRetentionOffset,
|
|
62
|
-
}, runtime, host), [
|
|
63
|
-
onPress,
|
|
64
|
-
onPressIn,
|
|
65
|
-
onPressOut,
|
|
66
|
-
onPressMove,
|
|
67
|
-
onLongPress,
|
|
68
|
-
delayLongPress,
|
|
69
|
-
unstable_pressDelay,
|
|
70
|
-
hitSlop,
|
|
71
|
-
pressRetentionOffset,
|
|
72
|
-
runtime,
|
|
73
|
-
host,
|
|
74
|
-
]);
|
|
75
|
-
noteHoverNoop(onHoverIn, onHoverOut);
|
|
76
|
-
void delayHoverIn;
|
|
77
|
-
void delayHoverOut;
|
|
78
|
-
const state = { pressed };
|
|
79
|
-
const resolvedAccessibilityState = resolveDisabledAccessibilityState(accessibilityState, disabled);
|
|
80
|
-
const viewProps = {
|
|
81
|
-
...accessibilityRest,
|
|
82
|
-
// The ref is the handle the retention measure reaches through (measure on grant).
|
|
83
|
-
ref: viewRef,
|
|
84
|
-
style: resolveStyle(style, state),
|
|
85
|
-
hitSlop,
|
|
86
|
-
accessibilityState: resolvedAccessibilityState,
|
|
87
|
-
testID,
|
|
88
|
-
};
|
|
89
|
-
// Forward the Android tap-sound suppressor under RN's own key; inert on iOS.
|
|
90
|
-
if (android_disableSound !== undefined)
|
|
91
|
-
viewProps.android_disableSound = android_disableSound;
|
|
92
|
-
Object.assign(viewProps, buildPressableListeners(handlers, { disabled, cancelable }));
|
|
93
|
-
// android_ripple rides a dedicated inner View (the Pressable's own View only forwards a fixed
|
|
94
|
-
// prop set), mirroring touchable-native-feedback. On iOS the ripple prop is undefined, so the
|
|
95
|
-
// child renders unwrapped, no extra node.
|
|
96
|
-
const ripple = android_ripple !== undefined ? rippleProps(android_ripple) : undefined;
|
|
97
|
-
const content = resolveChildren(children, state);
|
|
98
|
-
const inner = ripple !== undefined ? createElement(View, ripple, content) : content;
|
|
99
|
-
return createElement(View, viewProps, inner);
|
|
100
|
-
};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
// RefreshControl primitive. On iOS this is the PullToRefreshView Fabric node that
|
|
2
|
-
// lives INSIDE a ScrollView (a sibling of the content container), giving the
|
|
3
|
-
// pull-to-refresh gesture. `refreshing` is a controlled prop: the parent owns it
|
|
4
|
-
// and pushes it down each commit; native reports the gesture via the direct
|
|
5
|
-
// `topRefresh` event, which shared routes to the `refresh` listener (onRefresh).
|
|
6
|
-
import { createElement } from 'react';
|
|
7
|
-
import { dlog } from '@symbiote-native/engine';
|
|
8
|
-
import { resolveAccessibilityProps, } from '@symbiote-native/components';
|
|
9
|
-
export const RefreshControl = rawProps => {
|
|
10
|
-
// Owns its host element (symbiote-refresh-control), so it folds aria/role here;
|
|
11
|
-
// the resolved accessibility* fields ride down via `...nativeProps`.
|
|
12
|
-
const props = resolveAccessibilityProps(rawProps);
|
|
13
|
-
const { children, ...nativeProps } = props;
|
|
14
|
-
dlog('RefreshControl -> PullToRefreshView');
|
|
15
|
-
dlog(`RefreshControl refreshing=${String(props.refreshing)}`);
|
|
16
|
-
if (props.enabled !== undefined)
|
|
17
|
-
dlog(`RefreshControl enabled=${String(props.enabled)} (Android-only)`);
|
|
18
|
-
if (props.onRefresh !== undefined)
|
|
19
|
-
dlog('RefreshControl onRefresh listener wired');
|
|
20
|
-
return createElement('symbiote-refresh-control', { ...nativeProps }, children);
|
|
21
|
-
};
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { type FC, type ReactNode } from 'react';
|
|
2
|
-
import { type ISymbioteEvent } from '@symbiote-native/engine';
|
|
3
|
-
import { type IAccessibilityProps, type IAriaProps } from '@symbiote-native/components';
|
|
4
|
-
import type { IStyleProp, IViewStyle } from '../../utils/styles';
|
|
5
|
-
export interface ISafeAreaViewProps extends IAccessibilityProps, IAriaProps {
|
|
6
|
-
style?: IStyleProp<IViewStyle>;
|
|
7
|
-
className?: string;
|
|
8
|
-
children?: ReactNode;
|
|
9
|
-
onLayout?: (event: ISymbioteEvent) => void;
|
|
10
|
-
}
|
|
11
|
-
export declare const SafeAreaView: FC<ISafeAreaViewProps>;
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// SafeAreaView primitive. A plain view whose native side insets its children to
|
|
2
|
-
// the safe area (notch, rounded corners, system bars). There is no JS-side
|
|
3
|
-
// translation; RN just renders the native RCTSafeAreaView and lets the host do
|
|
4
|
-
// the inset math, so this maps style + children straight onto the intrinsic.
|
|
5
|
-
import { createElement } from 'react';
|
|
6
|
-
import { dlog } from '@symbiote-native/engine';
|
|
7
|
-
import { resolveAccessibilityProps, } from '@symbiote-native/components';
|
|
8
|
-
export const SafeAreaView = rawProps => {
|
|
9
|
-
// Owns its host element (symbiote-safe-area-view), so it folds aria/role here;
|
|
10
|
-
// the resolved accessibility* surface rides the node via `...accessibilityRest`.
|
|
11
|
-
const props = resolveAccessibilityProps(rawProps);
|
|
12
|
-
const { style, children, onLayout, ...accessibilityRest } = props;
|
|
13
|
-
dlog('SafeAreaView -> SafeAreaView');
|
|
14
|
-
const nodeProps = { ...accessibilityRest, style };
|
|
15
|
-
if (onLayout !== undefined)
|
|
16
|
-
nodeProps.onLayout = onLayout;
|
|
17
|
-
return createElement('symbiote-safe-area-view', nodeProps, children);
|
|
18
|
-
};
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import { type IScrollViewHandle, type IScrollViewProps } from './shared';
|
|
2
|
-
export type { IScrollViewProps, IScrollViewHandle } from './shared';
|
|
3
|
-
export declare const ScrollView: import("react").ForwardRefExoticComponent<IScrollViewProps & import("react").RefAttributes<IScrollViewHandle>>;
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
// ScrollView on Android: an Android ScrollView accepts only ONE child, so a RefreshControl
|
|
2
|
-
// can't be a sibling of the content the way iOS allows ("addViewAt: failed to insert view
|
|
3
|
-
// ... at index 1"). Instead the RefreshControl (AndroidSwipeRefreshLayout) WRAPS the
|
|
4
|
-
// scroll view, with the scroll view nested inside and nestedScrollEnabled so the inner
|
|
5
|
-
// scroll handles the gesture before the refresh parent, which mirrors RN's ScrollView.js
|
|
6
|
-
// android branch (cloneElement(refreshControl, {style}, <ScrollView nestedScrollEnabled
|
|
7
|
-
// style={flex:1}>{content}</ScrollView>)). Metro picks this on an Android host; no
|
|
8
|
-
// Platform.OS read.
|
|
9
|
-
import { cloneElement, createElement, forwardRef, useImperativeHandle, useRef, } from 'react';
|
|
10
|
-
import { dlog } from '@symbiote-native/engine';
|
|
11
|
-
import { buildScrollViewHandle, splitLayoutProps, } from '@symbiote-native/components';
|
|
12
|
-
import { usePreparedScrollView, useNativeStickyScrollAttach, } from './shared.js';
|
|
13
|
-
export const ScrollView = forwardRef((props, forwardedRef) => {
|
|
14
|
-
const { scrollViewIntrinsic, scrollViewBaseStyle, outerProps, style, layoutSplitStyle, content, refreshControl, scrollAnimatedValue, nativeStickyAvailable, } = usePreparedScrollView(props);
|
|
15
|
-
// The node ref backs the imperative handle; it attaches to the inner scroll-view element
|
|
16
|
-
// (the wrap shape leaves the scroll view as the command target, not the RefreshControl).
|
|
17
|
-
const ref = useRef(null);
|
|
18
|
-
// Lazy getter, not the ref itself: the node is null until the element commits, so the handle
|
|
19
|
-
// must read ref.current on each command, an eager capture would freeze null.
|
|
20
|
-
useImperativeHandle(forwardedRef, () => buildScrollViewHandle(() => ref.current), []);
|
|
21
|
-
// Drive the sticky scroll value on the native UI thread (RN attachNativeEvent). No-op on a
|
|
22
|
-
// host without the native animated module. The JS sticky path stays in effect.
|
|
23
|
-
useNativeStickyScrollAttach(ref, scrollAnimatedValue, nativeStickyAvailable);
|
|
24
|
-
dlog('ScrollView.ANDROID refreshControl=' +
|
|
25
|
-
(refreshControl === undefined ? 'NONE(1child)' : 'WRAP'));
|
|
26
|
-
if (refreshControl === undefined) {
|
|
27
|
-
// Base style (flexDirection: row for horizontal) under user style; undefined base
|
|
28
|
-
// (vertical) passes the user style through unchanged.
|
|
29
|
-
const scrollStyle = scrollViewBaseStyle
|
|
30
|
-
? [scrollViewBaseStyle, style]
|
|
31
|
-
: style;
|
|
32
|
-
return createElement(scrollViewIntrinsic, { ...outerProps, style: scrollStyle, ref }, content);
|
|
33
|
-
}
|
|
34
|
-
// RN splits the flattened style across the two boxes (ScrollView.js android branch):
|
|
35
|
-
// LAYOUT props (margin/flex/size/position/transform/gap/…) drive the outer
|
|
36
|
-
// AndroidSwipeRefreshLayout frame; VISUAL props (background/padding/border/opacity/…) paint
|
|
37
|
-
// the inner scroll view. So the wrapper carries `outer`, and the inner scroll view its base
|
|
38
|
-
// (flexDirection/overflow) plus the visual `inner` composed over it, NOT a hardcoded flex:1
|
|
39
|
-
// that would override an explicit user height/width.
|
|
40
|
-
//
|
|
41
|
-
// layoutSplitStyle (not style): a class-only layout prop (flex/height/gap/…) is invisible to
|
|
42
|
-
// `style` (it never carries the resolved `className` value), so splitting on `style` alone
|
|
43
|
-
// starves the wrapper of its layout style and it collapses to nothing. layoutSplitStyle is
|
|
44
|
-
// `style` merged with the resolved className style (see usePreparedScrollView).
|
|
45
|
-
const { outer: outerStyle, inner: innerStyle } = splitLayoutProps(layoutSplitStyle);
|
|
46
|
-
const scrollStyle = scrollViewBaseStyle
|
|
47
|
-
? [scrollViewBaseStyle, innerStyle]
|
|
48
|
-
: innerStyle;
|
|
49
|
-
// className is stripped from the spread here: layoutSplitStyle already folded its resolved
|
|
50
|
-
// value into `outer`/`inner` above, so forwarding the raw prop too would re-apply its LAYOUT
|
|
51
|
-
// half onto the inner scroll view a second time (on top of the outer wrapper).
|
|
52
|
-
const { className: _classNameAppliedViaSplit, ...innerOuterProps } = outerProps;
|
|
53
|
-
const scrollView = createElement(scrollViewIntrinsic, {
|
|
54
|
-
...innerOuterProps,
|
|
55
|
-
style: scrollStyle,
|
|
56
|
-
nestedScrollEnabled: true,
|
|
57
|
-
ref,
|
|
58
|
-
}, content);
|
|
59
|
-
return cloneElement(refreshControl, { style: outerStyle }, scrollView);
|
|
60
|
-
});
|
|
61
|
-
ScrollView.displayName = 'ScrollView';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './index.ios';
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import { type IScrollViewHandle, type IScrollViewProps } from './shared';
|
|
2
|
-
export type { IScrollViewProps, IScrollViewHandle } from './shared';
|
|
3
|
-
export declare const ScrollView: import("react").ForwardRefExoticComponent<IScrollViewProps & import("react").RefAttributes<IScrollViewHandle>>;
|
|
@@ -1,29 +0,0 @@
|
|
|
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
|
-
import { createElement, forwardRef, useImperativeHandle, useRef } from 'react';
|
|
5
|
-
import { buildScrollViewHandle } from '@symbiote-native/components';
|
|
6
|
-
import { usePreparedScrollView, useNativeStickyScrollAttach, } from './shared.js';
|
|
7
|
-
export const ScrollView = forwardRef((props, forwardedRef) => {
|
|
8
|
-
const { scrollViewIntrinsic, scrollViewBaseStyle, outerProps, style, content, refreshControl, scrollAnimatedValue, nativeStickyAvailable, } = usePreparedScrollView(props);
|
|
9
|
-
// Backs the imperative handle; passing `ref` through createElement props binds it to the
|
|
10
|
-
// SymbioteNode below, as TextInput does.
|
|
11
|
-
const ref = useRef(null);
|
|
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
|
-
useImperativeHandle(forwardedRef, () => buildScrollViewHandle(() => ref.current), []);
|
|
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
|
-
useNativeStickyScrollAttach(ref, scrollAnimatedValue, nativeStickyAvailable);
|
|
18
|
-
// Base style stays under user style so an explicit value wins; undefined base (vertical)
|
|
19
|
-
// passes the user style through unchanged.
|
|
20
|
-
const scrollStyle = scrollViewBaseStyle
|
|
21
|
-
? [scrollViewBaseStyle, style]
|
|
22
|
-
: style;
|
|
23
|
-
const scrollProps = { ...outerProps, style: scrollStyle, ref };
|
|
24
|
-
if (refreshControl === undefined) {
|
|
25
|
-
return createElement(scrollViewIntrinsic, scrollProps, content);
|
|
26
|
-
}
|
|
27
|
-
return createElement(scrollViewIntrinsic, scrollProps, refreshControl, content);
|
|
28
|
-
});
|
|
29
|
-
ScrollView.displayName = 'ScrollView';
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
// Base / default ScrollView: re-exports the iOS build. Metro overrides this with
|
|
2
|
-
// scroll-view.ios.ts / .android.ts on a real host; under tsx / tsc / web the host config
|
|
3
|
-
// resolves here. Filename is the selector, no Platform.OS read.
|
|
4
|
-
export * from './index.ios.js';
|
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
// ScrollView: shared core. The Fabric tree is nested: the scroll view wraps a content
|
|
2
|
-
// view that holds the children (RN's own ScrollView.js shape). Building that content
|
|
3
|
-
// node, resolving decelerationRate, and the prop plumbing are platform-invariant and
|
|
4
|
-
// live here. What diverges is how a RefreshControl integrates: on iOS it is a
|
|
5
|
-
// CHILD of the scroll view (sibling of the content), on Android it WRAPS the scroll view
|
|
6
|
-
// (AndroidSwipeRefreshLayout is the parent, ScrollView nested inside). So the .ios/.android
|
|
7
|
-
// files assemble the final element; the filename selects, no Platform.OS read.
|
|
8
|
-
//
|
|
9
|
-
// The framework-agnostic pieces (decelerationRate, the per-axis intrinsics/base style, the
|
|
10
|
-
// content-size dedupe, the imperative handle, splitLayoutProps, the sticky math, the native
|
|
11
|
-
// scroll-attach) live in @symbiote-native/components; this file holds only the React
|
|
12
|
-
// lifecycle (refs/state/effects) and the element assembly that consumes them.
|
|
13
|
-
import { createElement, useEffect, useRef, useState, } from 'react';
|
|
14
|
-
import { AnimatedValue, dlog, event as animatedEvent, isClassNameValue, isNativeAnimatedAvailable, resolveClassName, } from '@symbiote-native/engine';
|
|
15
|
-
import { attachStickyScroll, didContentSizeChange, forwardScrollEvent, readLayoutDimension, resolveDecelerationRate, resolveScrollForwarding, selectScrollIntrinsics, } from '@symbiote-native/components';
|
|
16
|
-
import { resolveAccessibilityProps, } from '@symbiote-native/components';
|
|
17
|
-
import { wrapStickyHeaders, } from './sticky-header.js';
|
|
18
|
-
export function usePreparedScrollView(rawProps) {
|
|
19
|
-
// ScrollView forwards its outer props straight to the native scroll view (not a View
|
|
20
|
-
// wrapper), so it folds aria/role into accessibility* here before forwarding.
|
|
21
|
-
const props = resolveAccessibilityProps(rawProps);
|
|
22
|
-
const { style, className, contentContainerStyle, horizontal, decelerationRate, refreshControl, children, onContentSizeChange, stickyHeaderIndices, invertStickyHeaders, StickyHeaderComponent, onLayout, onScroll, scrollEventThrottle, ...outer } = props;
|
|
23
|
-
const isHorizontal = horizontal === true;
|
|
24
|
-
const hasStickyHeaders = stickyHeaderIndices !== undefined && stickyHeaderIndices.length > 0;
|
|
25
|
-
// A single AnimatedValue tracks the scroll offset and drives every sticky header's
|
|
26
|
-
// translateY (RN's _scrollAnimatedValue). Stable across renders via a ref so the headers'
|
|
27
|
-
// bindings survive re-renders. Allocated even when no sticky headers are present (hooks
|
|
28
|
-
// run unconditionally: usePreparedScrollView is always called at the top of the render body).
|
|
29
|
-
const scrollAnimatedValueRef = useRef(null);
|
|
30
|
-
if (scrollAnimatedValueRef.current === null)
|
|
31
|
-
scrollAnimatedValueRef.current = new AnimatedValue(0);
|
|
32
|
-
const scrollAnimatedValue = scrollAnimatedValueRef.current;
|
|
33
|
-
// Inverted sticky headers stick to the BOTTOM, so they need the viewport height (RN reads
|
|
34
|
-
// it in _handleLayout). Tracked here and fed back into the wrapped headers.
|
|
35
|
-
const [viewportHeight, setViewportHeight] = useState(undefined);
|
|
36
|
-
// Sticky-header cross-talk (RN ScrollView.js _headerLayoutYs, line 754): a child-index→measured-y
|
|
37
|
-
// map the parent keeps so each header can learn where the NEXT sticky header starts (its push-off
|
|
38
|
-
// collision point). The map lives in a ref (mutated imperatively from each header's onLayout, like
|
|
39
|
-
// RN's _onStickyHeaderLayout), and a state bump forces the re-render that feeds the freshly-recorded
|
|
40
|
-
// y forward into the previous header's nextHeaderLayoutY prop.
|
|
41
|
-
const headerLayoutYsRef = useRef(null);
|
|
42
|
-
if (headerLayoutYsRef.current === null)
|
|
43
|
-
headerLayoutYsRef.current = new Map();
|
|
44
|
-
const headerLayoutYs = headerLayoutYsRef.current;
|
|
45
|
-
// The mounted headers by child index (RN's _stickyHeaderRefs, ScrollView.js:750), so a fresh
|
|
46
|
-
// measurement reaches its predecessor directly.
|
|
47
|
-
const stickyHeaderRefsRef = useRef(null);
|
|
48
|
-
if (stickyHeaderRefsRef.current === null)
|
|
49
|
-
stickyHeaderRefsRef.current = new Map();
|
|
50
|
-
const stickyHeaderRefs = stickyHeaderRefsRef.current;
|
|
51
|
-
const onHeaderRef = (index, handle) => {
|
|
52
|
-
if (handle === null)
|
|
53
|
-
stickyHeaderRefs.delete(index);
|
|
54
|
-
else
|
|
55
|
-
stickyHeaderRefs.set(index, handle);
|
|
56
|
-
};
|
|
57
|
-
// Measurements waiting to be handed to their predecessors, flushed together (see below).
|
|
58
|
-
const pendingHeaderYsRef = useRef(null);
|
|
59
|
-
if (pendingHeaderYsRef.current === null)
|
|
60
|
-
pendingHeaderYsRef.current = new Map();
|
|
61
|
-
const pendingHeaderYs = pendingHeaderYsRef.current;
|
|
62
|
-
const flushScheduledRef = useRef(false);
|
|
63
|
-
const flushHeaderYs = () => {
|
|
64
|
-
flushScheduledRef.current = false;
|
|
65
|
-
if (stickyHeaderIndices === undefined)
|
|
66
|
-
return;
|
|
67
|
-
for (const [index, y] of pendingHeaderYs) {
|
|
68
|
-
const previousIndex = stickyHeaderIndices[stickyHeaderIndices.indexOf(index) - 1];
|
|
69
|
-
if (previousIndex === undefined)
|
|
70
|
-
continue;
|
|
71
|
-
stickyHeaderRefs.get(previousIndex)?.setNextHeaderY(y);
|
|
72
|
-
}
|
|
73
|
-
pendingHeaderYs.clear();
|
|
74
|
-
};
|
|
75
|
-
// RN _onStickyHeaderLayout (ScrollView.js:1136): record this header's y, then hand it to the
|
|
76
|
-
// PREVIOUS sticky header as its push-off collision point.
|
|
77
|
-
//
|
|
78
|
-
// The handoff is COALESCED rather than immediate, and that is the point. Cross-talk runs BACKWARDS
|
|
79
|
-
// (a header learns its collision point from the one after it), so a mount measures every header
|
|
80
|
-
// and walks the chain end to start. Pushing each value the moment it arrives makes every link its
|
|
81
|
-
// own update generation - a setState on the predecessor, whose effect setStates again - and React
|
|
82
|
-
// caps nested updates at 50: three headers go unnoticed, two hundred abort the app with "Maximum
|
|
83
|
-
// update depth exceeded", which reads like an infinite loop but is really depth. Draining on a
|
|
84
|
-
// microtask puts one burst of layouts into ONE render pass, so depth stops tracking header count.
|
|
85
|
-
const onHeaderLayoutY = (index, y) => {
|
|
86
|
-
if (headerLayoutYs.get(index) === y)
|
|
87
|
-
return;
|
|
88
|
-
headerLayoutYs.set(index, y);
|
|
89
|
-
dlog(`ScrollView sticky-header layoutY index=${index} y=${y}`);
|
|
90
|
-
pendingHeaderYs.set(index, y);
|
|
91
|
-
if (flushScheduledRef.current)
|
|
92
|
-
return;
|
|
93
|
-
flushScheduledRef.current = true;
|
|
94
|
-
void Promise.resolve().then(flushHeaderYs);
|
|
95
|
-
};
|
|
96
|
-
// A class-name string resolves through the shared registry before it reaches the
|
|
97
|
-
// framework-agnostic selector below, which only understands style objects/arrays.
|
|
98
|
-
const resolvedContentContainerStyle = typeof contentContainerStyle === 'string'
|
|
99
|
-
? resolveClassName(contentContainerStyle)
|
|
100
|
-
: contentContainerStyle;
|
|
101
|
-
// The per-axis intrinsics, base style, and content style come from the shared selector
|
|
102
|
-
// (@symbiote-native/components): on Android horizontal resolves to its own ViewManager, on iOS both
|
|
103
|
-
// map back to RCTScrollView; here we only pass the axis.
|
|
104
|
-
const { scrollViewIntrinsic, contentIntrinsic, scrollViewBaseStyle, contentStyle, } = selectScrollIntrinsics(isHorizontal, resolvedContentContainerStyle);
|
|
105
|
-
const outerProps = { ...outer };
|
|
106
|
-
// className is pulled out above (unlike the rest of `...outer`) so `layoutSplitStyle` below can
|
|
107
|
-
// resolve it eagerly; re-added here so the simple single-node paths (iOS, Android with no
|
|
108
|
-
// refreshControl) keep forwarding it raw, exactly as it did when it flowed through `...outer`.
|
|
109
|
-
if (className !== undefined)
|
|
110
|
-
outerProps.className = className;
|
|
111
|
-
// RN defaults nested scrolling ON (ScrollView.js:1862 `nestedScrollEnabled ?? true`).
|
|
112
|
-
// Android needs the flag to scroll a scrollable nested inside another scroll view
|
|
113
|
-
// independently; without it the inner one stays put. iOS handles nesting natively, so
|
|
114
|
-
// it is a no-op there. Default to true so nested lists scroll out of the box, like RN.
|
|
115
|
-
outerProps.nestedScrollEnabled = props.nestedScrollEnabled ?? true;
|
|
116
|
-
// iOS needs `horizontal` to flip RCTScrollView's axis; Android's dedicated horizontal
|
|
117
|
-
// manager ignores it. Harmless on Android, load-bearing on iOS, so always forward it.
|
|
118
|
-
if (horizontal !== undefined)
|
|
119
|
-
outerProps.horizontal = horizontal;
|
|
120
|
-
if (decelerationRate !== undefined) {
|
|
121
|
-
outerProps.decelerationRate = resolveDecelerationRate(decelerationRate);
|
|
122
|
-
}
|
|
123
|
-
// The scroll-forwarding DECISIONS (which onScroll path, the 1/16 throttle defaults, whether to
|
|
124
|
-
// capture the viewport height for inverted sticky, whether to keep cells un-flattened) are folded
|
|
125
|
-
// out to the shared resolveScrollForwarding; here React only EXECUTES them with its own primitives.
|
|
126
|
-
const nativeStickyAvailable = hasStickyHeaders && isNativeAnimatedAvailable();
|
|
127
|
-
const forwarding = resolveScrollForwarding({
|
|
128
|
-
hasStickyHeaders,
|
|
129
|
-
nativeStickyAvailable,
|
|
130
|
-
invertStickyHeaders,
|
|
131
|
-
scrollEventThrottle,
|
|
132
|
-
maintainVisibleContentPosition: props.maintainVisibleContentPosition,
|
|
133
|
-
snapToAlignment: props.snapToAlignment,
|
|
134
|
-
});
|
|
135
|
-
// onScroll: the JS-fallback path wraps the user's handler in Animated.event so the offset drives
|
|
136
|
-
// the AnimatedValue each frame (RN _scrollAnimatedValueAttachment); the native + plain paths
|
|
137
|
-
// forward the user's handler as-is (the native driver attaches the value on the UI thread).
|
|
138
|
-
if (forwarding.mode === 'sticky-js') {
|
|
139
|
-
outerProps.onScroll = animatedEvent([{ nativeEvent: { contentOffset: { y: scrollAnimatedValue } } }], onScroll === undefined
|
|
140
|
-
? undefined
|
|
141
|
-
: { listener: (...args) => forwardScrollEvent(onScroll, args) });
|
|
142
|
-
}
|
|
143
|
-
else if (onScroll !== undefined) {
|
|
144
|
-
outerProps.onScroll = onScroll;
|
|
145
|
-
}
|
|
146
|
-
if (forwarding.scrollEventThrottle !== undefined) {
|
|
147
|
-
outerProps.scrollEventThrottle = forwarding.scrollEventThrottle;
|
|
148
|
-
}
|
|
149
|
-
// onLayout on the scroll-view node: capture the viewport height for inverted sticky headers
|
|
150
|
-
// (RN _handleLayout), then call the user's handler. Pass through unchanged otherwise.
|
|
151
|
-
if (forwarding.capturesViewportHeight) {
|
|
152
|
-
outerProps.onLayout = (layoutEvent) => {
|
|
153
|
-
const height = readLayoutDimension(layoutEvent, 'height');
|
|
154
|
-
if (height !== undefined)
|
|
155
|
-
setViewportHeight(height);
|
|
156
|
-
onLayout?.(layoutEvent);
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
else if (onLayout !== undefined) {
|
|
160
|
-
outerProps.onLayout = onLayout;
|
|
161
|
-
}
|
|
162
|
-
dlog(`ScrollView -> ${scrollViewIntrinsic} (horizontal=${isHorizontal} sticky=${hasStickyHeaders})`);
|
|
163
|
-
// onContentSizeChange is synthesized from the content view's own onLayout (RN
|
|
164
|
-
// _handleContentOnLayout): read width/height off nativeEvent.layout and fire only when the
|
|
165
|
-
// size actually changed (dedupe via a ref, like RN). Composed with any content onLayout.
|
|
166
|
-
const lastContentSizeRef = useRef(null);
|
|
167
|
-
const contentProps = {
|
|
168
|
-
style: contentStyle,
|
|
169
|
-
collapsable: false,
|
|
170
|
-
};
|
|
171
|
-
// maintainVisibleContentPosition (and Android snapToAlignment) anchor against the metrics
|
|
172
|
-
// of MOUNTED cell views. Android Fabric view-flattens layout-only cells away, so the native
|
|
173
|
-
// MaintainVisibleScrollPositionHelper has nothing to anchor to and the list jumps on prepend.
|
|
174
|
-
// RN keeps the cells as real views via collapsableChildren={false} on the content container
|
|
175
|
-
// (ScrollView.js:1731-1748 `preserveChildren`). iOS never flattens, so it is a no-op there.
|
|
176
|
-
if (forwarding.collapsableChildren) {
|
|
177
|
-
contentProps.collapsableChildren = false;
|
|
178
|
-
}
|
|
179
|
-
if (onContentSizeChange !== undefined) {
|
|
180
|
-
contentProps.onLayout = (layoutEvent) => {
|
|
181
|
-
const width = readLayoutDimension(layoutEvent, 'width');
|
|
182
|
-
const height = readLayoutDimension(layoutEvent, 'height');
|
|
183
|
-
if (width === undefined || height === undefined)
|
|
184
|
-
return;
|
|
185
|
-
const last = lastContentSizeRef.current;
|
|
186
|
-
if (!didContentSizeChange(last, { width, height }))
|
|
187
|
-
return;
|
|
188
|
-
lastContentSizeRef.current = { width, height };
|
|
189
|
-
dlog(`ScrollView onContentSizeChange ${width}x${height}`);
|
|
190
|
-
onContentSizeChange(width, height);
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
// Wrap the flagged children so they pin to the scroll offset (see the sticky-headers note
|
|
194
|
-
// above); no-op when none are flagged.
|
|
195
|
-
const contentChildren = hasStickyHeaders
|
|
196
|
-
? wrapStickyHeaders(children, stickyHeaderIndices, scrollAnimatedValue, invertStickyHeaders, viewportHeight, StickyHeaderComponent, { headerLayoutYs, onHeaderLayoutY, onHeaderRef })
|
|
197
|
-
: children;
|
|
198
|
-
// `collapsable: false` is load-bearing on Android. The content container is a
|
|
199
|
-
// layout-only View, which Android Fabric view-flattens away, hoisting the cells
|
|
200
|
-
// up as DIRECT children of the scroll view, which strictly hosts exactly one
|
|
201
|
-
// child ("ScrollView can host only one direct child" → addViewAt crash). RN pins
|
|
202
|
-
// its own NativeScrollContentView the same way (ScrollView.js, collapsable={false};
|
|
203
|
-
// ReactScrollView.java: "the 'content' View … non-collapsable so it will never be
|
|
204
|
-
// View-flattened away"). iOS doesn't flatten, so this is a no-op there.
|
|
205
|
-
const content = createElement(contentIntrinsic, contentProps, contentChildren);
|
|
206
|
-
// resolveClassName(undefined) is a cheap {} no-op, so this is safe with no className too.
|
|
207
|
-
const resolvedClassName = isClassNameValue(className) ? className : undefined;
|
|
208
|
-
const layoutSplitStyle = [
|
|
209
|
-
resolveClassName(resolvedClassName),
|
|
210
|
-
style,
|
|
211
|
-
];
|
|
212
|
-
return {
|
|
213
|
-
scrollViewIntrinsic,
|
|
214
|
-
scrollViewBaseStyle,
|
|
215
|
-
outerProps,
|
|
216
|
-
style,
|
|
217
|
-
layoutSplitStyle,
|
|
218
|
-
content,
|
|
219
|
-
refreshControl,
|
|
220
|
-
scrollAnimatedValue,
|
|
221
|
-
nativeStickyAvailable,
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
// Attach the scroll event to the scroll-offset value on the NATIVE driver, RN's
|
|
225
|
-
// _updateAnimatedNodeAttachment / AnimatedImplementation.attachNativeEvent (ScrollView.js:1087).
|
|
226
|
-
// Called by each platform ScrollView with its committed scroll-node ref; the value then tracks
|
|
227
|
-
// scroll on the UI thread and the sticky-header interpolations ride it natively (no JS jitter).
|
|
228
|
-
// No-op when native sticky is unavailable or the node hasn't committed. Detaches on unmount.
|
|
229
|
-
// The attach/detach itself lives in @symbiote-native/components (attachStickyScroll); this is the React
|
|
230
|
-
// effect that drives it.
|
|
231
|
-
export function useNativeStickyScrollAttach(scrollNodeRef, scrollAnimatedValue, enabled) {
|
|
232
|
-
useEffect(() => {
|
|
233
|
-
if (!enabled)
|
|
234
|
-
return;
|
|
235
|
-
const node = scrollNodeRef.current;
|
|
236
|
-
if (node === null)
|
|
237
|
-
return;
|
|
238
|
-
return attachStickyScroll(node, scrollAnimatedValue);
|
|
239
|
-
}, [scrollNodeRef, scrollAnimatedValue, enabled]);
|
|
240
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { type ComponentType, type ReactNode, type Ref } from 'react';
|
|
2
|
-
import { AnimatedValue } from '@symbiote-native/engine';
|
|
3
|
-
import { type IStickyHeaderProps } from '@symbiote-native/components';
|
|
4
|
-
export type IStickyHeaderHandle = {
|
|
5
|
-
setNextHeaderY: (y: number) => void;
|
|
6
|
-
};
|
|
7
|
-
export type IStickyCrossTalk = {
|
|
8
|
-
headerLayoutYs: ReadonlyMap<number, number>;
|
|
9
|
-
onHeaderLayoutY: (index: number, y: number) => void;
|
|
10
|
-
onHeaderRef: (index: number, handle: IStickyHeaderHandle | null) => void;
|
|
11
|
-
};
|
|
12
|
-
export type IStickyHeaderComponentProps = IStickyHeaderProps & {
|
|
13
|
-
children?: ReactNode;
|
|
14
|
-
ref?: Ref<IStickyHeaderHandle>;
|
|
15
|
-
};
|
|
16
|
-
export type IStickyHeaderComponentType = ComponentType<IStickyHeaderComponentProps>;
|
|
17
|
-
export declare const ScrollViewStickyHeader: IStickyHeaderComponentType;
|
|
18
|
-
export declare function wrapStickyHeaders(children: ReactNode, stickyHeaderIndices: number[] | undefined, scrollAnimatedValue: AnimatedValue, invertStickyHeaders: boolean | undefined, scrollViewHeight: number | undefined, StickyHeaderComponent: IStickyHeaderComponentType | undefined, crossTalk: IStickyCrossTalk): ReactNode;
|