@symbiote-native/components 0.4.0 → 1.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 +30 -8
- package/build/accessibility-props.d.ts +11 -0
- package/build/accessibility-props.js +30 -117
- package/build/behaviors/image.d.ts +3 -0
- package/build/behaviors/image.js +123 -0
- package/build/behaviors/input-accessory-view.d.ts +3 -0
- package/build/behaviors/input-accessory-view.js +70 -0
- package/build/behaviors/pressable.d.ts +2 -0
- package/build/behaviors/pressable.js +310 -0
- package/build/behaviors/switch.d.ts +2 -0
- package/build/behaviors/switch.js +182 -0
- package/build/behaviors/text-input.d.ts +14 -0
- package/build/behaviors/text-input.js +291 -0
- package/build/bootstrap/index.js +1 -1
- package/build/component-names/index.android.js +23 -1
- package/build/component-names/index.ios.js +9 -1
- package/build/component-names/shared.d.ts +1 -1
- package/build/component-names/shared.js +40 -1
- package/build/descriptor.d.ts +8 -0
- package/build/descriptor.js +27 -0
- package/build/fold-host-bag.d.ts +15 -0
- package/build/fold-host-bag.js +99 -0
- package/build/index.d.ts +25 -12
- package/build/index.js +18 -6
- package/build/resolve-intrinsic.d.ts +7 -0
- package/build/resolve-intrinsic.js +49 -0
- package/build/state/pressable.d.ts +9 -0
- package/build/state/pressable.js +133 -37
- package/build/state/sticky-header-reducer.js +28 -5
- package/build/state/switch.js +1 -1
- package/build/state/text-input.d.ts +7 -1
- package/build/state/text-input.js +46 -8
- package/build/state/touchable.d.ts +30 -1
- package/build/state/touchable.js +94 -5
- package/build/state/virtualized-list-diagnostics.d.ts +31 -0
- package/build/state/virtualized-list-diagnostics.js +33 -0
- package/build/state/virtualized-list-reducer.d.ts +14 -0
- package/build/state/virtualized-list-reducer.js +179 -45
- package/build/state/virtualized-list.d.ts +5 -2
- package/build/state/virtualized-list.js +143 -34
- package/build/state-style.d.ts +15 -0
- package/build/state-style.js +47 -0
- package/build/text-props.d.ts +9 -0
- package/build/text-props.js +25 -0
- package/build/view/render-activity-indicator.js +37 -3
- package/build/view/render-image/index.d.ts +2 -0
- package/build/view/render-image/index.js +42 -5
- package/build/view/render-input-accessory-view.d.ts +2 -0
- package/build/view/render-input-accessory-view.js +41 -6
- package/build/view/render-keyboard-avoiding-view.d.ts +14 -2
- package/build/view/render-keyboard-avoiding-view.js +52 -6
- package/build/view/render-modal.js +4 -2
- package/build/view/render-pressable/index.js +3 -1
- package/build/view/render-scroll-sticky.js +1 -1
- package/build/view/render-scroll-view.js +9 -3
- package/build/view/render-switch.js +11 -2
- package/build/view/render-text-input.js +6 -2
- package/build/view/render-touchable-highlight.d.ts +11 -1
- package/build/view/render-touchable-highlight.js +11 -10
- package/build/view/render-touchable-native-feedback.js +5 -1
- package/host-primitives.cjs +380 -0
- package/host-primitives.d.cts +35 -0
- package/lowering-fixtures.cjs +259 -0
- package/lowering-fixtures.d.cts +17 -0
- package/package.json +42 -5
- package/specialize-state-style.cjs +219 -0
- package/specialize-state-style.d.cts +15 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// carry. Instead we return a layout DESCRIPTION (which styles to apply and whether to nest)
|
|
10
10
|
// and the adapter builds its own element tree with its children. Mirrors RN's
|
|
11
11
|
// Libraries/Components/Keyboard/KeyboardAvoidingView.js inset/behavior logic.
|
|
12
|
-
import { isRecord } from '@symbiote-native/engine';
|
|
12
|
+
import { AccessibilityInfo, dlog, isRecord, KEYBOARD_EVENT, Platform, } from '@symbiote-native/engine';
|
|
13
13
|
// RN's default keyboardVerticalOffset (KeyboardAvoidingView.js).
|
|
14
14
|
export const DEFAULT_VERTICAL_OFFSET = 0;
|
|
15
15
|
// RN rounds nothing here; 'height' mode collapses flex so the shrunk height holds.
|
|
@@ -38,13 +38,51 @@ export function readLayoutFrame(layout) {
|
|
|
38
38
|
return undefined;
|
|
39
39
|
return { y, height };
|
|
40
40
|
}
|
|
41
|
+
export function keyboardAvoidingEventNamesFor(os) {
|
|
42
|
+
if (os === 'ios')
|
|
43
|
+
return { show: KEYBOARD_EVENT.willShow, hide: KEYBOARD_EVENT.willHide };
|
|
44
|
+
return { show: KEYBOARD_EVENT.didShow, hide: KEYBOARD_EVENT.didHide };
|
|
45
|
+
}
|
|
46
|
+
// The iOS "Prefer Cross-Fade Transitions" setting, read once per mount and fed to computeInset.
|
|
47
|
+
//
|
|
48
|
+
// AccessibilityInfo's iOS getters REJECT when the native error callback fires. That is deliberate
|
|
49
|
+
// RN parity and the engine must keep it — but this caller runs at mount and nobody awaits it, so a
|
|
50
|
+
// rejection would surface as an unhandled promise. Five adapters would each need the same `.catch`,
|
|
51
|
+
// which is five chances to forget one, so the fallback lives here instead. A failed read and "the
|
|
52
|
+
// user does not prefer cross-fade" are indistinguishable to every caller, so false is the honest
|
|
53
|
+
// answer, not a swallowed error.
|
|
54
|
+
//
|
|
55
|
+
// The query is injectable purely so both branches are unit-testable without a native-module fake;
|
|
56
|
+
// adapters call it with no arguments.
|
|
57
|
+
export function readPrefersCrossFadeTransitions(query = () => AccessibilityInfo.prefersCrossFadeTransitions()) {
|
|
58
|
+
return query().catch((error) => {
|
|
59
|
+
dlog(`KeyboardAvoidingView: prefersCrossFadeTransitions read failed -> false (${String(error)})`);
|
|
60
|
+
return false;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
41
63
|
// RN's _relativeKeyboardHeight: how far up the view must move so it no longer overlaps the
|
|
42
64
|
// keyboard. keyboardY is the keyboard's top edge minus the caller's vertical offset; the inset
|
|
43
65
|
// is the overlap of the view's bottom past that edge, clamped at 0.
|
|
44
|
-
export function computeInset(frame, keyboard, verticalOffset) {
|
|
66
|
+
export function computeInset(frame, keyboard, verticalOffset, options = {}) {
|
|
45
67
|
if (frame === undefined || keyboard === undefined)
|
|
46
68
|
return 0;
|
|
69
|
+
// With the iOS Prefer Cross-Fade Transitions setting on, the keyboard reports screenY as 0
|
|
70
|
+
// rather than its real top edge (RN:88-96). Run through the ordinary math that makes the overlap
|
|
71
|
+
// the view's ENTIRE y + height and pushes the content clean off screen, so RN answers 0.
|
|
72
|
+
const os = options.os ?? Platform.OS;
|
|
73
|
+
if (os === 'ios' &&
|
|
74
|
+
keyboard.screenY === 0 &&
|
|
75
|
+
options.prefersCrossFadeTransitions === true) {
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
47
78
|
const keyboardY = keyboard.screenY - verticalOffset;
|
|
79
|
+
// The 'height' fixpoint (RN:100-105). In this mode the wrapper is SHRUNK by the inset, so its
|
|
80
|
+
// next onLayout reports a frame shorter by exactly that much. Adding the previous inset back
|
|
81
|
+
// cancels the shrink; without it each subsequent keyboard event computes a smaller overlap and
|
|
82
|
+
// the view walks back down under the keyboard. A correction, not an accumulation.
|
|
83
|
+
if (options.behavior === 'height') {
|
|
84
|
+
return Math.max((options.previousInset ?? 0) + frame.y + frame.height - keyboardY, 0);
|
|
85
|
+
}
|
|
48
86
|
return Math.max(frame.y + frame.height - keyboardY, 0);
|
|
49
87
|
}
|
|
50
88
|
// Map the behavior + effective inset onto the wrapper/inner styles and the nesting decision:
|
|
@@ -52,7 +90,7 @@ export function computeInset(frame, keyboard, verticalOffset) {
|
|
|
52
90
|
// wrapper directly. 'height' shrinks the wrapper from its initial measured height (only while
|
|
53
91
|
// the keyboard is up, matching RN).
|
|
54
92
|
export function resolveKeyboardAvoidingLayout(params) {
|
|
55
|
-
const { behavior, effectiveInset, initialHeight, style, contentContainerStyle } = params;
|
|
93
|
+
const { behavior, effectiveInset, initialHeight, style, contentContainerStyle, } = params;
|
|
56
94
|
if (behavior === 'position') {
|
|
57
95
|
return {
|
|
58
96
|
kind: 'nested',
|
|
@@ -61,12 +99,20 @@ export function resolveKeyboardAvoidingLayout(params) {
|
|
|
61
99
|
};
|
|
62
100
|
}
|
|
63
101
|
if (behavior === 'padding') {
|
|
64
|
-
return {
|
|
102
|
+
return {
|
|
103
|
+
kind: 'wrapper',
|
|
104
|
+
wrapperStyle: [style, { paddingBottom: effectiveInset }],
|
|
105
|
+
};
|
|
65
106
|
}
|
|
66
|
-
if (behavior === 'height' &&
|
|
107
|
+
if (behavior === 'height' &&
|
|
108
|
+
effectiveInset > 0 &&
|
|
109
|
+
initialHeight !== undefined) {
|
|
67
110
|
return {
|
|
68
111
|
kind: 'wrapper',
|
|
69
|
-
wrapperStyle: [
|
|
112
|
+
wrapperStyle: [
|
|
113
|
+
style,
|
|
114
|
+
{ height: initialHeight - effectiveInset, flex: COLLAPSED_FLEX },
|
|
115
|
+
],
|
|
70
116
|
};
|
|
71
117
|
}
|
|
72
118
|
return { kind: 'wrapper', wrapperStyle: style };
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// maps to the `symbiote-modal` intrinsic the host config routes to ModalHostView, wrapping a
|
|
6
6
|
// full-screen container View that holds the user children (injected by the adapter). Shared
|
|
7
7
|
// verbatim: React and Vue both bridge this Descriptor; the keep-alive state lives in state/modal.ts.
|
|
8
|
-
import { dlog } from '@symbiote-native/engine';
|
|
8
|
+
import { dlog, } from '@symbiote-native/engine';
|
|
9
9
|
import { el } from '../descriptor.js';
|
|
10
10
|
// The full-screen box RN anchors the modal content in (Modal.js styles.container: [side]:0,
|
|
11
11
|
// top:0, flex:1, backgroundColor:'white'). It is NOT position:absolute, it is a flex child that
|
|
@@ -45,7 +45,9 @@ export function renderModal(view) {
|
|
|
45
45
|
backdropOverride,
|
|
46
46
|
];
|
|
47
47
|
const resolvedPresentationStyle = view.presentationStyle ??
|
|
48
|
-
(view.transparent === true
|
|
48
|
+
(view.transparent === true
|
|
49
|
+
? PRESENTATION_OVER_FULL_SCREEN
|
|
50
|
+
: PRESENTATION_FULL_SCREEN);
|
|
49
51
|
dlog('Modal visible -> committing ModalHostView(container View)');
|
|
50
52
|
// collapsable:false keeps the container as a real shadow node (RN sets this so the wrapper is
|
|
51
53
|
// never flattened away under the host). Empty structural children: the adapter injects the
|
|
@@ -8,7 +8,9 @@ import { dlog } from '@symbiote-native/engine';
|
|
|
8
8
|
// disabled state even if the caller passed none (Pressable.js: disabled != null ? {...state,
|
|
9
9
|
// disabled} : state). Untouched when disabled is unset.
|
|
10
10
|
export function resolveDisabledAccessibilityState(accessibilityState, disabled) {
|
|
11
|
-
return disabled !== undefined
|
|
11
|
+
return disabled !== undefined
|
|
12
|
+
? { ...accessibilityState, disabled }
|
|
13
|
+
: accessibilityState;
|
|
12
14
|
}
|
|
13
15
|
// The 3 agnostic gating predicates behind the listener bag below. Angular has no bag to spread -
|
|
14
16
|
// it binds these directly onto template event outputs (see adapters/angular/src/components/
|
|
@@ -32,7 +32,7 @@ export function readLayoutNumber(event, key) {
|
|
|
32
32
|
// layoutY and tracks 1:1 until the next header pushes it off, while the inverted branch pins
|
|
33
33
|
// at the viewport bottom (stickStartPoint) and tracks up to the collision point.
|
|
34
34
|
export function computeStickyInterpolation(params) {
|
|
35
|
-
const { measured, inverted, scrollViewHeight, layoutY, layoutHeight, nextHeaderLayoutY } = params;
|
|
35
|
+
const { measured, inverted, scrollViewHeight, layoutY, layoutHeight, nextHeaderLayoutY, } = params;
|
|
36
36
|
const inputRange = [-1, 0];
|
|
37
37
|
const outputRange = [0, 0];
|
|
38
38
|
if (measured) {
|
|
@@ -26,7 +26,7 @@ export function resolveDecelerationRate(rate) {
|
|
|
26
26
|
// select() types as `number | undefined`; the always-present `default` makes the
|
|
27
27
|
// `??` fallback unreachable, but it narrows the return to a plain `number` (no cast).
|
|
28
28
|
if (rate === 'normal')
|
|
29
|
-
return Platform.select({ ios: 0.998, android: 0.985, default: 0.998 }) ?? 0.998;
|
|
29
|
+
return (Platform.select({ ios: 0.998, android: 0.985, default: 0.998 }) ?? 0.998);
|
|
30
30
|
return Platform.select({ ios: 0.99, android: 0.9, default: 0.99 }) ?? 0.99;
|
|
31
31
|
}
|
|
32
32
|
// RN applies a base style to the scroll-view NODE itself, per axis (ScrollView.js
|
|
@@ -69,7 +69,12 @@ export function selectScrollIntrinsics(isHorizontal, contentContainerStyle) {
|
|
|
69
69
|
const contentStyle = isHorizontal
|
|
70
70
|
? [contentContainerStyle, { flexDirection: 'row' }]
|
|
71
71
|
: contentContainerStyle;
|
|
72
|
-
return {
|
|
72
|
+
return {
|
|
73
|
+
scrollViewIntrinsic,
|
|
74
|
+
contentIntrinsic,
|
|
75
|
+
scrollViewBaseStyle,
|
|
76
|
+
contentStyle,
|
|
77
|
+
};
|
|
73
78
|
}
|
|
74
79
|
// When sticky headers are active the scroll offset must reach the AnimatedValue; RN raises the scroll
|
|
75
80
|
// event rate for it (ScrollView.js:1798): throttle 1 on the native driver (it can afford every frame),
|
|
@@ -81,7 +86,8 @@ export function resolveScrollForwarding(inputs) {
|
|
|
81
86
|
// maintainVisibleContentPosition (and Android snapToAlignment) anchor against MOUNTED cell views;
|
|
82
87
|
// Android Fabric view-flattens layout-only cells away, so RN keeps them as real views with
|
|
83
88
|
// collapsableChildren={false} on the content container (ScrollView.js preserveChildren). No-op on iOS.
|
|
84
|
-
const collapsableChildren = inputs.maintainVisibleContentPosition !== undefined ||
|
|
89
|
+
const collapsableChildren = inputs.maintainVisibleContentPosition !== undefined ||
|
|
90
|
+
inputs.snapToAlignment !== undefined;
|
|
85
91
|
if (!inputs.hasStickyHeaders) {
|
|
86
92
|
return {
|
|
87
93
|
mode: 'plain',
|
|
@@ -8,13 +8,22 @@ import { dlog } from '@symbiote-native/engine';
|
|
|
8
8
|
import { el } from '../descriptor.js';
|
|
9
9
|
// RN rounds the iOS background pill to this radius when ios_backgroundColor is set.
|
|
10
10
|
const IOS_BACKGROUND_BORDER_RADIUS = 16;
|
|
11
|
+
// The `-managed` spelling, NOT the plain one the engine's Switch behavior registers under
|
|
12
|
+
// (`core/components/src/behaviors/switch.ts`) — the wrapper still runs its own lifecycle
|
|
13
|
+
// (lastNativeReport, the snap-back effect), so it may not share a tag the behavior registry would
|
|
14
|
+
// also attach to; see that behavior's module header and `.claude/rules/host-primitive-tier.md`,
|
|
15
|
+
// "A lowered element and its wrapper must not share an intrinsic tag" (the TextInput precedent).
|
|
16
|
+
const SWITCH_MANAGED_INTRINSIC = 'symbiote-switch-managed';
|
|
11
17
|
// Fold ios_backgroundColor into the style, matching RN's iOS branch: it paints the
|
|
12
18
|
// background that shows through the shrunken track. Untouched when unset, so a caller's own
|
|
13
19
|
// backgroundColor wins by simply not passing ios_backgroundColor.
|
|
14
20
|
function foldIosBackground(style, color) {
|
|
15
21
|
if (color === undefined)
|
|
16
22
|
return style;
|
|
17
|
-
return [
|
|
23
|
+
return [
|
|
24
|
+
style,
|
|
25
|
+
{ backgroundColor: color, borderRadius: IOS_BACKGROUND_BORDER_RADIUS },
|
|
26
|
+
];
|
|
18
27
|
}
|
|
19
28
|
export function renderSwitch(view, platform) {
|
|
20
29
|
dlog(`Switch render value=${view.value} disabled=${String(view.disabled)}`);
|
|
@@ -29,5 +38,5 @@ export function renderSwitch(view, platform) {
|
|
|
29
38
|
thumbTintColor: view.thumbColor,
|
|
30
39
|
style: foldIosBackground(view.style, view.ios_backgroundColor),
|
|
31
40
|
};
|
|
32
|
-
return el(
|
|
41
|
+
return el(SWITCH_MANAGED_INTRINSIC, props);
|
|
33
42
|
}
|
|
@@ -8,8 +8,12 @@ import { dlog } from '@symbiote-native/engine';
|
|
|
8
8
|
import { el } from '../descriptor.js';
|
|
9
9
|
// One host element per native input class. Text carries the only non-trivial nesting elsewhere;
|
|
10
10
|
// here the choice is binary and runtime (the `multiline` prop), so the module stays flat.
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// The `-managed` spelling, NOT the plain one the lowering transforms emit. Both resolve to the
|
|
12
|
+
// same native view; the tag is what decides whether the engine's TextInput machine attaches, and
|
|
13
|
+
// on this path the adapter's own lifecycle already owns that state. Sharing the tag would run both
|
|
14
|
+
// copies at once. Full reasoning in `component-names/shared.ts`.
|
|
15
|
+
const SINGLELINE_INTRINSIC = 'symbiote-text-input-managed';
|
|
16
|
+
const MULTILINE_INTRINSIC = 'symbiote-text-input-multiline-managed';
|
|
13
17
|
export function renderTextInput(view) {
|
|
14
18
|
const intrinsic = view.multiline ? MULTILINE_INTRINSIC : SINGLELINE_INTRINSIC;
|
|
15
19
|
dlog(`TextInput render multiline=${String(view.multiline)} ` +
|
|
@@ -1,2 +1,12 @@
|
|
|
1
1
|
import type { IViewStyle } from '@symbiote-native/engine';
|
|
2
|
-
export
|
|
2
|
+
export interface ITouchableHighlightExtraStyles {
|
|
3
|
+
underlay: IViewStyle;
|
|
4
|
+
child: IViewStyle;
|
|
5
|
+
}
|
|
6
|
+
export interface ITouchableHighlightUnderlayView {
|
|
7
|
+
shown: boolean;
|
|
8
|
+
hasPressHandler: boolean;
|
|
9
|
+
underlayColor?: string;
|
|
10
|
+
activeOpacity?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function resolveHighlightExtraStyles(view: ITouchableHighlightUnderlayView): ITouchableHighlightExtraStyles | undefined;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// TouchableHighlight underlay: the shared render decision (framework-agnostic). RN drives the
|
|
2
|
-
// highlight with setState
|
|
3
|
-
// opacity
|
|
4
|
-
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
// highlight with setState, not Animated — while shown it paints underlayColor and lowers the child
|
|
3
|
+
// opacity; at rest it is the bare style (TouchableHighlight.js).
|
|
4
|
+
import { DEFAULT_HIGHLIGHT_CHILD_OPACITY, DEFAULT_UNDERLAY_COLOR, } from '../state/touchable.js';
|
|
5
|
+
// undefined = paint nothing extra, which is RN's `extraStyles: null` state.
|
|
6
|
+
export function resolveHighlightExtraStyles(view) {
|
|
7
|
+
if (!view.shown || !view.hasPressHandler)
|
|
8
|
+
return undefined;
|
|
9
|
+
return {
|
|
10
|
+
underlay: { backgroundColor: view.underlayColor ?? DEFAULT_UNDERLAY_COLOR },
|
|
11
|
+
child: { opacity: view.activeOpacity ?? DEFAULT_HIGHLIGHT_CHILD_OPACITY },
|
|
12
|
+
};
|
|
12
13
|
}
|
|
@@ -14,7 +14,11 @@ export function canUseNativeForeground() {
|
|
|
14
14
|
Platform.Version >= ANDROID_FOREGROUND_MIN_VERSION);
|
|
15
15
|
}
|
|
16
16
|
export function selectableBackground(rippleRadius) {
|
|
17
|
-
return {
|
|
17
|
+
return {
|
|
18
|
+
type: 'ThemeAttrAndroid',
|
|
19
|
+
attribute: 'selectableItemBackground',
|
|
20
|
+
rippleRadius,
|
|
21
|
+
};
|
|
18
22
|
}
|
|
19
23
|
export function selectableBackgroundBorderless(rippleRadius) {
|
|
20
24
|
return {
|