@symbiote-native/components 1.0.0 → 3.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 +11 -13
- package/build/accessibility-props.d.ts +1 -1
- package/build/accessibility-props.js +2 -2
- package/build/behaviors/activity-indicator/index.android.d.ts +1 -0
- package/build/behaviors/activity-indicator/index.android.js +16 -0
- package/build/behaviors/activity-indicator/index.d.ts +3 -0
- package/build/behaviors/activity-indicator/index.ios.d.ts +1 -0
- package/build/behaviors/activity-indicator/index.ios.js +14 -0
- package/build/behaviors/activity-indicator/index.js +5 -0
- package/build/behaviors/activity-indicator/shared.d.ts +18 -0
- package/build/behaviors/activity-indicator/shared.js +120 -0
- package/build/behaviors/button.d.ts +13 -0
- package/build/behaviors/button.js +340 -0
- package/build/behaviors/image-background.d.ts +3 -0
- package/build/behaviors/image-background.js +155 -0
- package/build/behaviors/image.d.ts +1 -2
- package/build/behaviors/image.js +25 -106
- package/build/behaviors/input-accessory-view.d.ts +1 -2
- package/build/behaviors/input-accessory-view.js +49 -55
- package/build/behaviors/pressable.d.ts +59 -1
- package/build/behaviors/pressable.js +142 -96
- package/build/behaviors/refresh-control.d.ts +2 -0
- package/build/behaviors/refresh-control.js +96 -0
- package/build/behaviors/scroll-view/index.android.d.ts +1 -0
- package/build/behaviors/scroll-view/index.android.js +40 -0
- package/build/behaviors/scroll-view/index.d.ts +3 -0
- package/build/behaviors/scroll-view/index.ios.d.ts +1 -0
- package/build/behaviors/scroll-view/index.ios.js +10 -0
- package/build/behaviors/scroll-view/index.js +8 -0
- package/build/behaviors/scroll-view/responder.d.ts +4 -0
- package/build/behaviors/scroll-view/responder.js +202 -0
- package/build/behaviors/scroll-view/shared.d.ts +9 -0
- package/build/behaviors/scroll-view/shared.js +291 -0
- package/build/behaviors/scroll-view/sticky.d.ts +18 -0
- package/build/behaviors/scroll-view/sticky.js +581 -0
- package/build/behaviors/switch.d.ts +1 -1
- package/build/behaviors/switch.js +49 -88
- package/build/behaviors/text-input.d.ts +2 -2
- package/build/behaviors/text-input.js +244 -104
- package/build/behaviors/touchable-highlight.d.ts +9 -0
- package/build/behaviors/touchable-highlight.js +205 -0
- package/build/behaviors/touchable-native-feedback.d.ts +20 -0
- package/build/behaviors/touchable-native-feedback.js +254 -0
- package/build/behaviors/touchable-opacity.d.ts +12 -0
- package/build/behaviors/touchable-opacity.js +239 -0
- package/build/behaviors/touchable-without-feedback.d.ts +2 -0
- package/build/behaviors/touchable-without-feedback.js +231 -0
- package/build/component-names/index.android.js +31 -25
- package/build/component-names/index.ios.js +28 -23
- package/build/component-names/shared.d.ts +2 -1
- package/build/component-names/shared.js +16 -6
- package/build/descriptor.js +4 -4
- package/build/index.d.ts +26 -26
- package/build/index.js +56 -27
- package/build/register.d.ts +1 -0
- package/build/register.js +55 -0
- package/build/resolve-intrinsic.js +3 -9
- package/build/scroll-view-commands.d.ts +1 -5
- package/build/scroll-view-commands.js +23 -85
- package/build/state/flat-list.d.ts +2 -2
- package/build/state/flat-list.js +10 -2
- package/build/state/pressable.d.ts +6 -1
- package/build/state/pressable.js +63 -28
- package/build/state/section-list.d.ts +2 -0
- package/build/state/section-list.js +14 -7
- package/build/state/text-input.d.ts +10 -40
- package/build/state/text-input.js +17 -186
- package/build/state/touchable.d.ts +1 -0
- package/build/state/touchable.js +11 -8
- package/build/state/virtualized-list-reducer.d.ts +2 -2
- package/build/state/virtualized-list.d.ts +6 -6
- package/build/state/virtualized-list.js +71 -37
- package/build/text-props.d.ts +0 -8
- package/build/text-props.js +14 -25
- package/build/view/render-button.d.ts +11 -4
- package/build/view/render-button.js +74 -22
- package/build/view/render-image/index.d.ts +14 -1
- package/build/view/render-image/index.js +22 -147
- package/build/view/render-input-accessory-view.d.ts +1 -5
- package/build/view/render-input-accessory-view.js +26 -48
- package/build/view/render-keyboard-avoiding-view.d.ts +7 -1
- package/build/view/render-keyboard-avoiding-view.js +40 -1
- package/build/view/render-modal.d.ts +1 -1
- package/build/view/render-modal.js +18 -8
- package/build/view/render-pressable/index.d.ts +3 -0
- package/build/view/render-pressable/index.js +28 -0
- package/build/view/render-scroll-view.d.ts +1 -4
- package/build/view/render-scroll-view.js +17 -54
- package/build/view/render-switch.d.ts +4 -15
- package/build/view/render-switch.js +4 -42
- package/build/view/render-touchable-highlight.d.ts +1 -0
- package/build/view/render-touchable-native-feedback.d.ts +19 -1
- package/build/view/render-touchable-native-feedback.js +34 -9
- package/host-primitives.cjs +178 -280
- package/host-primitives.d.cts +0 -3
- package/package.json +8 -21
- package/build/fold-host-bag.d.ts +0 -15
- package/build/fold-host-bag.js +0 -99
- package/build/state-style.d.ts +0 -15
- package/build/state-style.js +0 -47
- package/build/view/render-activity-indicator.d.ts +0 -25
- package/build/view/render-activity-indicator.js +0 -88
- package/build/view/render-image-background.d.ts +0 -9
- package/build/view/render-image-background.js +0 -48
- package/build/view/render-text-input.d.ts +0 -11
- package/build/view/render-text-input.js +0 -39
- package/lowering-fixtures.cjs +0 -259
- package/lowering-fixtures.d.cts +0 -17
- package/specialize-state-style.cjs +0 -219
- package/specialize-state-style.d.cts +0 -15
|
@@ -1,53 +1,31 @@
|
|
|
1
|
-
// InputAccessoryView: the
|
|
2
|
-
// RCTInputAccessoryView, that docks its content above the keyboard
|
|
3
|
-
// `nativeID`, which a TextInput points at through its `inputAccessoryViewID` prop
|
|
4
|
-
// the two by id. There is no JS-side translation
|
|
5
|
-
// onto the intrinsic
|
|
6
|
-
// verbatim across adapters: React and Vue both bridge this Descriptor.
|
|
7
|
-
import { dlog, } from '@symbiote-native/engine';
|
|
8
|
-
import { el } from '../descriptor.js';
|
|
9
|
-
// The names this fold CONSUMES, exported so an adapter splitting props before calling the render fn
|
|
10
|
-
// reads the list instead of copying it — the drift `render-image.ts` records paying for once.
|
|
11
|
-
export const INPUT_ACCESSORY_VIEW_PROP_NAMES = [
|
|
12
|
-
'nativeID',
|
|
13
|
-
'backgroundColor',
|
|
14
|
-
'style',
|
|
15
|
-
];
|
|
16
|
-
// The whole mapping, so the wrapper path and the lowered path share ONE implementation. There is no
|
|
17
|
-
// aliasing here at all: every consumed name leaves under the same name, which is what makes the
|
|
18
|
-
// fold idempotent by construction (asserted in `behaviors/input-accessory-view.test.ts`, not
|
|
19
|
-
// assumed).
|
|
1
|
+
// InputAccessoryView: the framework-agnostic prop SHAPE (iOS). The tag commits a real Fabric host
|
|
2
|
+
// node, RCTInputAccessoryView, that docks its content above the keyboard; it is referenced by
|
|
3
|
+
// `nativeID`, which a TextInput points at through its `inputAccessoryViewID` prop, and native pairs
|
|
4
|
+
// the two by id. There is no JS-side translation — style / nativeID / backgroundColor map straight
|
|
5
|
+
// onto the intrinsic.
|
|
20
6
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
7
|
+
// THE MAPPING FUNCTION IS GONE (2026-09-18) and only the type is left, which is the honest residue:
|
|
8
|
+
// `mapInputAccessoryViewProps` took the bag apart and put it back together unchanged, so the
|
|
9
|
+
// behavior stopped calling it and nothing else ever did. Angular still names the type for its
|
|
10
|
+
// `@Input()` declarations (`adapters/angular/src/elements.ts`), so the shape stays; the fold does
|
|
11
|
+
// not. Why it was never a rule, and the numeric `backgroundColor` it used to drop:
|
|
12
|
+
// `core/components/src/behaviors/input-accessory-view.ts`.
|
|
13
|
+
export {};
|
|
14
|
+
// WHAT THE DELETED FOLD KNEW THAT THIS FILE NO LONGER HAS TO, kept because the trap is a property
|
|
15
|
+
// of the engine and outlives the function that hit it. The `undefined` guards on `nativeID` /
|
|
16
|
+
// `backgroundColor` were LOAD-BEARING, and not for the reason they look it:
|
|
23
17
|
//
|
|
24
|
-
//
|
|
25
|
-
// states — `setProp` collapses an undefined value to an absent key, so a conditional write in a
|
|
26
|
-
// shared render fn is cosmetic — and that reasoning was checked against a payload carrying no `id`,
|
|
27
|
-
// which is exactly the case where it holds. The equivalence oracle caught it on its first run:
|
|
28
|
-
//
|
|
29
|
-
// authored <InputAccessoryView id="p" testID="p" />
|
|
18
|
+
// authored <input-accessory-view id="p" testID="p">
|
|
30
19
|
// guarded RCTInputAccessoryView{testID, nativeID:"p"}
|
|
31
20
|
// unguarded RCTInputAccessoryView{testID} <- the alias result, deleted
|
|
32
21
|
//
|
|
33
|
-
// `
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (view.nativeID !== undefined)
|
|
44
|
-
out.nativeID = view.nativeID;
|
|
45
|
-
if (view.backgroundColor !== undefined)
|
|
46
|
-
out.backgroundColor = view.backgroundColor;
|
|
47
|
-
return out;
|
|
48
|
-
}
|
|
49
|
-
export function renderInputAccessoryView(view) {
|
|
50
|
-
dlog('InputAccessoryView -> RCTInputAccessoryView');
|
|
51
|
-
// Empty structural children: the adapter appends the user children directly under the host.
|
|
52
|
-
return el('symbiote-input-accessory-view', mapInputAccessoryViewProps(view), []);
|
|
53
|
-
}
|
|
22
|
+
// `setProp` collapses an undefined value to an absent key, so a conditional write is normally
|
|
23
|
+
// cosmetic (`.claude/rules/fabric-boolean-event-gates.md`) — it is destructive precisely when the
|
|
24
|
+
// key has an ALIAS SOURCE. `id` arrives, something renames it to `nativeID`, and a
|
|
25
|
+
// `nativeID: undefined` written afterwards deletes what the rename just produced. Removing the
|
|
26
|
+
// guards on that reasoning cost a day on 2026-09-01.
|
|
27
|
+
//
|
|
28
|
+
// It cannot recur here, and that is the point: the rename is `foldIdAlias` in
|
|
29
|
+
// `SymbioteFabricProps.cpp` now, one rule at the end of the payload build with nothing downstream
|
|
30
|
+
// of it to overwrite the result. Anything that reintroduces a JS-side alias for this tag
|
|
31
|
+
// reintroduces the trap with it.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IKeyboardEventName, IPlatformOSType, IStyleProp, IViewStyle } from '@symbiote-native/engine';
|
|
1
|
+
import type { IKeyboardEventName, ILayoutAnimationConfig, IPlatformOSType, IStyleProp, IViewStyle } from '@symbiote-native/engine';
|
|
2
2
|
export type IKeyboardAvoidingBehavior = 'height' | 'position' | 'padding';
|
|
3
3
|
export declare const DEFAULT_VERTICAL_OFFSET = 0;
|
|
4
4
|
export interface IMeasuredFrame {
|
|
@@ -17,6 +17,12 @@ export type IKeyboardAvoidingEventNames = {
|
|
|
17
17
|
};
|
|
18
18
|
export declare function keyboardAvoidingEventNamesFor(os: IPlatformOSType): IKeyboardAvoidingEventNames;
|
|
19
19
|
export declare function readPrefersCrossFadeTransitions(query?: () => Promise<boolean>): Promise<boolean>;
|
|
20
|
+
export interface IKeyboardAnimationTiming {
|
|
21
|
+
duration: number;
|
|
22
|
+
easing: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function readKeyboardAnimationTiming(payload: unknown): IKeyboardAnimationTiming | undefined;
|
|
25
|
+
export declare function configureKeyboardAvoidingAnimation(timing: IKeyboardAnimationTiming | undefined, enabled: boolean, configure?: (config: ILayoutAnimationConfig) => void): void;
|
|
20
26
|
export type IComputeInsetOptions = {
|
|
21
27
|
behavior?: IKeyboardAvoidingBehavior;
|
|
22
28
|
previousInset?: number;
|
|
@@ -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 { AccessibilityInfo, dlog, isRecord, KEYBOARD_EVENT, Platform, } from '@symbiote-native/engine';
|
|
12
|
+
import { AccessibilityInfo, dlog, isRecord, KEYBOARD_EVENT, LayoutAnimation, 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.
|
|
@@ -60,6 +60,45 @@ export function readPrefersCrossFadeTransitions(query = () => AccessibilityInfo.
|
|
|
60
60
|
return false;
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
|
+
// RCTLayoutAnimation.m's minimal accepted duration — RN's own comment at the call site
|
|
64
|
+
// (`KeyboardAvoidingView.js:172-173`): "We have to pass the duration equal to minimal accepted
|
|
65
|
+
// duration defined here".
|
|
66
|
+
const MIN_LAYOUT_ANIMATION_DURATION = 10;
|
|
67
|
+
// The keyboard event's own transition curve, read off the SAME raw payload `readKeyboardFrame`
|
|
68
|
+
// reads `endCoordinates` from — `duration`/`easing` sit one level up, beside it.
|
|
69
|
+
export function readKeyboardAnimationTiming(payload) {
|
|
70
|
+
if (!isRecord(payload))
|
|
71
|
+
return undefined;
|
|
72
|
+
const { duration, easing } = payload;
|
|
73
|
+
if (typeof duration !== 'number' || typeof easing !== 'string')
|
|
74
|
+
return undefined;
|
|
75
|
+
return { duration, easing };
|
|
76
|
+
}
|
|
77
|
+
// RN's `_updateBottomIfNecessary` (`:169-179`): once the inset actually changes, arm the NEXT
|
|
78
|
+
// commit to animate over the keyboard's own transition curve instead of snapping. Gated on
|
|
79
|
+
// `enabled` and on the event carrying BOTH a duration and an easing name — Android's
|
|
80
|
+
// DID_SHOW/DID_HIDE notifications carry neither, so this is correctly a no-op there, same as
|
|
81
|
+
// vendor's `if (enabled && duration && easing)`.
|
|
82
|
+
//
|
|
83
|
+
// `configure` is injectable purely for testability, the same shape `readPrefersCrossFadeTransitions`
|
|
84
|
+
// takes its `query`; callers pass nothing and get the real `LayoutAnimation.configureNext`.
|
|
85
|
+
export function configureKeyboardAvoidingAnimation(timing, enabled, configure = config => LayoutAnimation.configureNext(config)) {
|
|
86
|
+
if (!enabled || timing === undefined)
|
|
87
|
+
return;
|
|
88
|
+
const { duration, easing } = timing;
|
|
89
|
+
if (!duration || !easing)
|
|
90
|
+
return;
|
|
91
|
+
const flooredDuration = duration > MIN_LAYOUT_ANIMATION_DURATION
|
|
92
|
+
? duration
|
|
93
|
+
: MIN_LAYOUT_ANIMATION_DURATION;
|
|
94
|
+
configure({
|
|
95
|
+
duration: flooredDuration,
|
|
96
|
+
update: {
|
|
97
|
+
duration: flooredDuration,
|
|
98
|
+
type: LayoutAnimation.coerceType(easing),
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
63
102
|
// RN's _relativeKeyboardHeight: how far up the view must move so it no longer overlaps the
|
|
64
103
|
// keyboard. keyboardY is the keyboard's top edge minus the caller's vertical offset; the inset
|
|
65
104
|
// is the overlap of the view's bottom past that edge, clamped at 0.
|
|
@@ -20,4 +20,4 @@ export type IModalViewProps = {
|
|
|
20
20
|
style?: IStyleProp<IViewStyle>;
|
|
21
21
|
passthrough: Record<string, unknown>;
|
|
22
22
|
};
|
|
23
|
-
export declare function renderModal(view: IModalViewProps): IDescriptor;
|
|
23
|
+
export declare function renderModal(view: IModalViewProps, isRTL?: boolean): IDescriptor;
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// node: it lives in the SAME childSet and commits through the SAME completeRoot as the rest of
|
|
3
3
|
// the tree. The native iOS/Android view presents its own window internally; there is no second
|
|
4
4
|
// root or second surface on the JS side. So this is a thin render exactly like the others: it
|
|
5
|
-
// maps to the `
|
|
5
|
+
// maps to the `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, I18nManager, } 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
|
|
@@ -13,8 +13,11 @@ import { el } from '../descriptor.js';
|
|
|
13
13
|
// (ModalHostViewComponentDescriptor sets the node size to screenSize). An absolute container with
|
|
14
14
|
// only top/left would collapse to its content instead. The backdrop color is layered on at render
|
|
15
15
|
// time so transparent/backdropColor win.
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
//
|
|
17
|
+
// `[side]` is NOT always 'left' — vendor computes it once at module load from
|
|
18
|
+
// `I18nManager.getConstants().isRTL` (`Modal.js:372`: `isRTL ? 'right' : 'left'`), so a modal in an
|
|
19
|
+
// RTL layout pins its container to the RIGHT edge. `renderModal`'s own `isRTL` parameter supplies it.
|
|
20
|
+
const CONTAINER_STYLE_BASE = {
|
|
18
21
|
top: 0,
|
|
19
22
|
flex: 1,
|
|
20
23
|
};
|
|
@@ -30,7 +33,10 @@ const DEFAULT_ANIMATION_TYPE = 'none';
|
|
|
30
33
|
// 'overFullScreen').
|
|
31
34
|
const PRESENTATION_FULL_SCREEN = 'fullScreen';
|
|
32
35
|
const PRESENTATION_OVER_FULL_SCREEN = 'overFullScreen';
|
|
33
|
-
|
|
36
|
+
// `isRTL` is injectable purely for testability, the same shape `computeInset`'s `os` option
|
|
37
|
+
// takes: `I18nManager`'s constants are resolved once at module load with no setter, so a test
|
|
38
|
+
// exercising the RTL branch cannot toggle the real module and must pass the value in.
|
|
39
|
+
export function renderModal(view, isRTL = I18nManager.isRTL) {
|
|
34
40
|
// Only override backgroundColor when transparent or backdropColor are explicitly set, so these
|
|
35
41
|
// Modal-specific props take precedence over the generic style prop (Modal.js: containerStyles
|
|
36
42
|
// composed LAST in [styles.container, props.style, containerStyles]).
|
|
@@ -40,7 +46,11 @@ export function renderModal(view) {
|
|
|
40
46
|
? { backgroundColor: view.backdropColor }
|
|
41
47
|
: {};
|
|
42
48
|
const containerStyle = [
|
|
43
|
-
{
|
|
49
|
+
{
|
|
50
|
+
...CONTAINER_STYLE_BASE,
|
|
51
|
+
...(isRTL ? { right: 0 } : { left: 0 }),
|
|
52
|
+
backgroundColor: OPAQUE_BACKDROP,
|
|
53
|
+
},
|
|
44
54
|
view.style,
|
|
45
55
|
backdropOverride,
|
|
46
56
|
];
|
|
@@ -52,8 +62,8 @@ export function renderModal(view) {
|
|
|
52
62
|
// collapsable:false keeps the container as a real shadow node (RN sets this so the wrapper is
|
|
53
63
|
// never flattened away under the host). Empty structural children: the adapter injects the
|
|
54
64
|
// user children UNDER this container, never as a direct sibling of the host.
|
|
55
|
-
const container = el('
|
|
56
|
-
return el('
|
|
65
|
+
const container = el('view', { style: containerStyle, collapsable: false }, []);
|
|
66
|
+
return el('modal', {
|
|
57
67
|
...view.passthrough,
|
|
58
68
|
style: MODAL_HOST_STYLE,
|
|
59
69
|
transparent: view.transparent,
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { IAccessibilityStateValue } from '../../accessibility-props';
|
|
2
2
|
import type { IPressHandlers } from '../../state/pressable';
|
|
3
3
|
export declare function resolveDisabledAccessibilityState(accessibilityState: IAccessibilityStateValue | undefined, disabled: boolean | undefined): IAccessibilityStateValue | undefined;
|
|
4
|
+
export declare function resolvePressableFocusable(focusable: boolean | undefined): boolean;
|
|
5
|
+
export declare function resolveTouchableFocusable(focusable: boolean | undefined, hasOnPress: boolean, disabled: boolean | undefined): boolean;
|
|
4
6
|
export declare function shouldSuppressPress(disabled: boolean | undefined): boolean;
|
|
5
7
|
export declare function shouldClaimResponder(disabled: boolean | undefined): boolean;
|
|
6
8
|
export declare function isTerminationAllowed(cancelable: boolean | undefined): boolean;
|
|
7
9
|
export declare function buildPressableListeners(handlers: IPressHandlers, options: {
|
|
8
10
|
disabled?: boolean;
|
|
9
11
|
cancelable?: boolean;
|
|
12
|
+
blockNativeResponder?: boolean;
|
|
10
13
|
}): Record<string, unknown>;
|
|
11
14
|
export declare function noteHoverNoop(onHoverIn: unknown, onHoverOut: unknown): void;
|
|
@@ -12,6 +12,30 @@ export function resolveDisabledAccessibilityState(accessibilityState, disabled)
|
|
|
12
12
|
? { ...accessibilityState, disabled }
|
|
13
13
|
: accessibilityState;
|
|
14
14
|
}
|
|
15
|
+
// RN computes `focusable` in all five touchables and we computed it nowhere until 2026-09-09, so a
|
|
16
|
+
// DISABLED control stayed focusable — a keyboard, a TV remote or switch control could land on
|
|
17
|
+
// something that cannot be pressed. There are TWO formulas, split by primitive and not by platform;
|
|
18
|
+
// do not collapse them.
|
|
19
|
+
//
|
|
20
|
+
// Pressable defaults it ON (Pressable.js:258). `!== false`, not `?? true`: only a literal `false`
|
|
21
|
+
// opts out, the same shape `accessible` uses one file over.
|
|
22
|
+
export function resolvePressableFocusable(focusable) {
|
|
23
|
+
return focusable !== false;
|
|
24
|
+
}
|
|
25
|
+
// The four Touchable* additionally require a press handler and a non-disabled state —
|
|
26
|
+
// TouchableOpacity.js:336-340, TouchableHighlight.js:370-374,
|
|
27
|
+
// TouchableWithoutFeedback.js:263-266, TouchableNativeFeedback.js:369-372, one expression verbatim.
|
|
28
|
+
//
|
|
29
|
+
// `hasOnPress` is RN's `onPress !== undefined` and NOT `hasTouchablePressHandler`, which also counts
|
|
30
|
+
// onPressIn/onPressOut/onLongPress — that gate is the underlay's, and reusing it here would make a
|
|
31
|
+
// press-in-only Touchable focusable where RN leaves it out of the focus order.
|
|
32
|
+
//
|
|
33
|
+
// A Touchable that composes our Pressable passes the result DOWN as the `focusable` prop; Pressable
|
|
34
|
+
// then applies `!== false` to a value that is already boolean, which is why the two compose without
|
|
35
|
+
// either side knowing about the other.
|
|
36
|
+
export function resolveTouchableFocusable(focusable, hasOnPress, disabled) {
|
|
37
|
+
return (resolvePressableFocusable(focusable) && hasOnPress && disabled !== true);
|
|
38
|
+
}
|
|
15
39
|
// The 3 agnostic gating predicates behind the listener bag below. Angular has no bag to spread -
|
|
16
40
|
// it binds these directly onto template event outputs (see adapters/angular/src/components/
|
|
17
41
|
// pressable/index.ts), so they're exported and shared rather than folded back into
|
|
@@ -48,6 +72,10 @@ export function buildPressableListeners(handlers, options) {
|
|
|
48
72
|
onPressOut: handlers.handlePressOut,
|
|
49
73
|
onStartShouldSetResponder: () => shouldClaimResponder(options.disabled),
|
|
50
74
|
onResponderMove: handlers.handleResponderMove,
|
|
75
|
+
// Pressability.js:479 — returned from onResponderGrant unconditionally, so native learns
|
|
76
|
+
// whether to stand down the moment the gesture is claimed. Unset resolves to false, matching
|
|
77
|
+
// the engine's own fallback for an absent listener (`events/index.ts`).
|
|
78
|
+
onResponderGrant: () => options.blockNativeResponder === true,
|
|
51
79
|
};
|
|
52
80
|
if (options.cancelable !== undefined) {
|
|
53
81
|
listeners.onResponderTerminationRequest = () => isTerminationAllowed(options.cancelable);
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import type { IStyleProp, ISymbioteEvent, IViewStyle } from '@symbiote-native/engine';
|
|
2
2
|
import type { ISymbioteIntrinsic } from '../component-names/shared';
|
|
3
3
|
export declare function readLayoutDimension(event: ISymbioteEvent, key: 'width' | 'height'): number | undefined;
|
|
4
|
-
export declare function resolveDecelerationRate(rate: 'normal' | 'fast' | number): number;
|
|
5
|
-
export declare const SCROLL_VIEW_BASE_HORIZONTAL: IViewStyle;
|
|
6
|
-
export declare const SCROLL_VIEW_BASE_VERTICAL: IViewStyle;
|
|
7
4
|
export type IScrollIntrinsics = {
|
|
8
5
|
scrollViewIntrinsic: ISymbioteIntrinsic;
|
|
9
6
|
contentIntrinsic: ISymbioteIntrinsic;
|
|
10
|
-
scrollViewBaseStyle: IViewStyle;
|
|
11
7
|
contentStyle: IStyleProp<IViewStyle>;
|
|
12
8
|
};
|
|
13
9
|
export declare function selectScrollIntrinsics(isHorizontal: boolean, contentContainerStyle: IStyleProp<IViewStyle> | undefined): IScrollIntrinsics;
|
|
@@ -26,6 +22,7 @@ export interface IScrollForwarding {
|
|
|
26
22
|
capturesViewportHeight: boolean;
|
|
27
23
|
collapsableChildren: boolean;
|
|
28
24
|
}
|
|
25
|
+
export declare function preservesContentChildren(maintainVisibleContentPosition: unknown, snapToAlignment: unknown): boolean;
|
|
29
26
|
export declare function resolveScrollForwarding(inputs: IScrollForwardingInputs): IScrollForwarding;
|
|
30
27
|
export type IContentSize = {
|
|
31
28
|
width: number;
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
// adapter owns the lifecycle (refs/state/effects) and the element assembly; it calls these pure
|
|
6
6
|
// helpers from prepareScrollView. What diverges per platform, and how a RefreshControl
|
|
7
7
|
// integrates, stays in the adapter's .ios/.android files.
|
|
8
|
-
import { Platform } from '@symbiote-native/engine';
|
|
9
8
|
import { readLayoutField } from './layout-event.js';
|
|
10
9
|
// Thin re-export kept for the existing public surface (adapters import this name from
|
|
11
10
|
// `@symbiote-native/components`); the actual field read is shared with render-scroll-sticky's
|
|
@@ -13,66 +12,22 @@ import { readLayoutField } from './layout-event.js';
|
|
|
13
12
|
export function readLayoutDimension(event, key) {
|
|
14
13
|
return readLayoutField(event, key);
|
|
15
14
|
}
|
|
16
|
-
// 'normal'/'fast' resolve to DIFFERENT friction constants per platform: RN's
|
|
17
|
-
// processDecelerationRate.js Platform.select()s them: iOS glides longer (0.998/0.99),
|
|
18
|
-
// Android sooner (0.985/0.9). Hardcoding the iOS pair made Android momentum scroll
|
|
19
|
-
// glide far too long on 'fast'. This is the file's one Platform read: the header's
|
|
20
|
-
// "no Platform.OS" rule governs component-intrinsic selection, not a value transform
|
|
21
|
-
// RN itself platform-branches. `default` mirrors iOS so any non-ios/android host stays
|
|
22
|
-
// defined (select would otherwise yield undefined). Numeric rates pass through unchanged.
|
|
23
|
-
export function resolveDecelerationRate(rate) {
|
|
24
|
-
if (typeof rate === 'number')
|
|
25
|
-
return rate;
|
|
26
|
-
// select() types as `number | undefined`; the always-present `default` makes the
|
|
27
|
-
// `??` fallback unreachable, but it narrows the return to a plain `number` (no cast).
|
|
28
|
-
if (rate === 'normal')
|
|
29
|
-
return (Platform.select({ ios: 0.998, android: 0.985, default: 0.998 }) ?? 0.998);
|
|
30
|
-
return Platform.select({ ios: 0.99, android: 0.9, default: 0.99 }) ?? 0.99;
|
|
31
|
-
}
|
|
32
|
-
// RN applies a base style to the scroll-view NODE itself, per axis (ScrollView.js
|
|
33
|
-
// styles.baseHorizontal/baseVertical). Two parts carry weight:
|
|
34
|
-
// - `overflow: 'scroll'`: clips content to the scroll view's frame. On iOS Fabric the
|
|
35
|
-
// node only clips when this is set; without it a fixed-height ScrollView lets its
|
|
36
|
-
// content bleed out over siblings (Android's native ViewGroup clips regardless, which
|
|
37
|
-
// is why the bug showed only on iOS). RN sets it on BOTH axes, so we do too.
|
|
38
|
-
// - `flexDirection: 'row'` (horizontal only): makes the single content child a MAIN-axis
|
|
39
|
-
// item, so Yoga sizes it to its content width and the view overflows and scrolls.
|
|
40
|
-
// Without it the content is a CROSS-axis item, stretched to the viewport, nothing to
|
|
41
|
-
// scroll. Vertical keeps the default `column`.
|
|
42
|
-
// Both axes match RN's baseHorizontal/baseVertical exactly. Composed UNDER the user style,
|
|
43
|
-
// so an explicit value still wins.
|
|
44
|
-
export const SCROLL_VIEW_BASE_HORIZONTAL = {
|
|
45
|
-
flexGrow: 1,
|
|
46
|
-
flexShrink: 1,
|
|
47
|
-
flexDirection: 'row',
|
|
48
|
-
overflow: 'scroll',
|
|
49
|
-
};
|
|
50
|
-
export const SCROLL_VIEW_BASE_VERTICAL = {
|
|
51
|
-
flexGrow: 1,
|
|
52
|
-
flexShrink: 1,
|
|
53
|
-
flexDirection: 'column',
|
|
54
|
-
overflow: 'scroll',
|
|
55
|
-
};
|
|
56
15
|
export function selectScrollIntrinsics(isHorizontal, contentContainerStyle) {
|
|
57
16
|
// Horizontal scroll resolves to a different native component on Android (its own
|
|
58
17
|
// ViewManager, not RCTScrollView+flag); on iOS both intrinsics map back to RCTScrollView.
|
|
59
18
|
// The name table does the per-platform mapping; here we only pick the intrinsic.
|
|
60
19
|
const scrollViewIntrinsic = isHorizontal
|
|
61
|
-
? '
|
|
62
|
-
: '
|
|
20
|
+
? 'horizontal-scroll-view'
|
|
21
|
+
: 'scroll-view';
|
|
63
22
|
const contentIntrinsic = isHorizontal
|
|
64
|
-
? '
|
|
65
|
-
: '
|
|
66
|
-
const scrollViewBaseStyle = isHorizontal
|
|
67
|
-
? SCROLL_VIEW_BASE_HORIZONTAL
|
|
68
|
-
: SCROLL_VIEW_BASE_VERTICAL;
|
|
23
|
+
? 'horizontal-scroll-content'
|
|
24
|
+
: 'scroll-content';
|
|
69
25
|
const contentStyle = isHorizontal
|
|
70
26
|
? [contentContainerStyle, { flexDirection: 'row' }]
|
|
71
27
|
: contentContainerStyle;
|
|
72
28
|
return {
|
|
73
29
|
scrollViewIntrinsic,
|
|
74
30
|
contentIntrinsic,
|
|
75
|
-
scrollViewBaseStyle,
|
|
76
31
|
contentStyle,
|
|
77
32
|
};
|
|
78
33
|
}
|
|
@@ -82,12 +37,20 @@ export function selectScrollIntrinsics(isHorizontal, contentContainerStyle) {
|
|
|
82
37
|
// throttle passes through untouched. These two magic numbers were copied into all three adapters.
|
|
83
38
|
const STICKY_NATIVE_SCROLL_THROTTLE = 1;
|
|
84
39
|
const STICKY_JS_SCROLL_THROTTLE = 16;
|
|
40
|
+
// maintainVisibleContentPosition (and Android snapToAlignment) anchor against MOUNTED cell views;
|
|
41
|
+
// Android Fabric view-flattens layout-only cells away, so RN keeps them as real views with
|
|
42
|
+
// collapsableChildren={false} on the content container (ScrollView.js:1731 `preserveChildren`).
|
|
43
|
+
// No-op on iOS.
|
|
44
|
+
//
|
|
45
|
+
// Named and exported rather than inlined because the host behavior's slot fold needs the same
|
|
46
|
+
// answer from a different place — it has the owner's raw props and none of the sticky inputs
|
|
47
|
+
// `resolveScrollForwarding` also takes. One exported function is what keeps the two from drifting.
|
|
48
|
+
export function preservesContentChildren(maintainVisibleContentPosition, snapToAlignment) {
|
|
49
|
+
return (maintainVisibleContentPosition !== undefined ||
|
|
50
|
+
snapToAlignment !== undefined);
|
|
51
|
+
}
|
|
85
52
|
export function resolveScrollForwarding(inputs) {
|
|
86
|
-
|
|
87
|
-
// Android Fabric view-flattens layout-only cells away, so RN keeps them as real views with
|
|
88
|
-
// collapsableChildren={false} on the content container (ScrollView.js preserveChildren). No-op on iOS.
|
|
89
|
-
const collapsableChildren = inputs.maintainVisibleContentPosition !== undefined ||
|
|
90
|
-
inputs.snapToAlignment !== undefined;
|
|
53
|
+
const collapsableChildren = preservesContentChildren(inputs.maintainVisibleContentPosition, inputs.snapToAlignment);
|
|
91
54
|
if (!inputs.hasStickyHeaders) {
|
|
92
55
|
return {
|
|
93
56
|
mode: 'plain',
|
|
@@ -1,29 +1,18 @@
|
|
|
1
1
|
import type { IStyleProp, IViewStyle, ISymbioteEvent } from '@symbiote-native/engine';
|
|
2
|
-
import type { IDescriptor } from '../descriptor';
|
|
3
2
|
import type { IAccessibilityProps, IAriaProps } from '../accessibility-props';
|
|
4
3
|
export type ISwitchTrackColor = {
|
|
5
4
|
false?: string;
|
|
6
5
|
true?: string;
|
|
7
6
|
};
|
|
7
|
+
export type ISwitchChangeEvent = ISymbioteEvent & {
|
|
8
|
+
value: boolean;
|
|
9
|
+
};
|
|
8
10
|
export interface ISwitchProps extends IAccessibilityProps, IAriaProps {
|
|
9
11
|
value?: boolean;
|
|
10
|
-
onValueChange?: (
|
|
12
|
+
onValueChange?: (event: ISwitchChangeEvent) => void;
|
|
11
13
|
disabled?: boolean;
|
|
12
14
|
trackColor?: ISwitchTrackColor;
|
|
13
15
|
thumbColor?: string;
|
|
14
16
|
ios_backgroundColor?: string;
|
|
15
17
|
style?: IStyleProp<IViewStyle>;
|
|
16
18
|
}
|
|
17
|
-
export type ISwitchPlatform = {
|
|
18
|
-
trackColorProps: (value: boolean, trackColor?: ISwitchTrackColor) => Record<string, unknown>;
|
|
19
|
-
};
|
|
20
|
-
export type ISwitchViewProps = {
|
|
21
|
-
value: boolean;
|
|
22
|
-
disabled?: boolean;
|
|
23
|
-
trackColor?: ISwitchTrackColor;
|
|
24
|
-
thumbColor?: string;
|
|
25
|
-
ios_backgroundColor?: string;
|
|
26
|
-
style?: IStyleProp<IViewStyle>;
|
|
27
|
-
passthrough: Record<string, unknown>;
|
|
28
|
-
};
|
|
29
|
-
export declare function renderSwitch(view: ISwitchViewProps, platform: ISwitchPlatform): IDescriptor;
|
|
@@ -1,42 +1,4 @@
|
|
|
1
|
-
// Switch: the
|
|
2
|
-
// `
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
// ios_backgroundColor folds into the style as the pill that shows through the shrunken track.
|
|
6
|
-
// Pure and prop-driven; no hooks, no events. The adapter owns those.
|
|
7
|
-
import { dlog } from '@symbiote-native/engine';
|
|
8
|
-
import { el } from '../descriptor.js';
|
|
9
|
-
// RN rounds the iOS background pill to this radius when ios_backgroundColor is set.
|
|
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';
|
|
17
|
-
// Fold ios_backgroundColor into the style, matching RN's iOS branch: it paints the
|
|
18
|
-
// background that shows through the shrunken track. Untouched when unset, so a caller's own
|
|
19
|
-
// backgroundColor wins by simply not passing ios_backgroundColor.
|
|
20
|
-
function foldIosBackground(style, color) {
|
|
21
|
-
if (color === undefined)
|
|
22
|
-
return style;
|
|
23
|
-
return [
|
|
24
|
-
style,
|
|
25
|
-
{ backgroundColor: color, borderRadius: IOS_BACKGROUND_BORDER_RADIUS },
|
|
26
|
-
];
|
|
27
|
-
}
|
|
28
|
-
export function renderSwitch(view, platform) {
|
|
29
|
-
dlog(`Switch render value=${view.value} disabled=${String(view.disabled)}`);
|
|
30
|
-
// These color props reach Fabric as ordinary props: the shared ViewConfig declares
|
|
31
|
-
// Switch's only event as `change`, so routeProp sends the on*-prefixed color names through
|
|
32
|
-
// setProp rather than mistaking them for listeners.
|
|
33
|
-
const props = {
|
|
34
|
-
...view.passthrough,
|
|
35
|
-
value: view.value,
|
|
36
|
-
disabled: view.disabled,
|
|
37
|
-
...platform.trackColorProps(view.value, view.trackColor),
|
|
38
|
-
thumbTintColor: view.thumbColor,
|
|
39
|
-
style: foldIosBackground(view.style, view.ios_backgroundColor),
|
|
40
|
-
};
|
|
41
|
-
return el(SWITCH_MANAGED_INTRINSIC, props);
|
|
42
|
-
}
|
|
1
|
+
// Switch: the framework-agnostic prop surface. The painting moved to the `switch` tag's own
|
|
2
|
+
// behavior (`behaviors/switch.ts`) when the wrapper path was retired; what stays here is the
|
|
3
|
+
// public prop type every adapter re-exports and the event shape it fires with.
|
|
4
|
+
export {};
|
|
@@ -8,5 +8,6 @@ export interface ITouchableHighlightUnderlayView {
|
|
|
8
8
|
hasPressHandler: boolean;
|
|
9
9
|
underlayColor?: string;
|
|
10
10
|
activeOpacity?: number;
|
|
11
|
+
testOnly_pressed?: boolean;
|
|
11
12
|
}
|
|
12
13
|
export declare function resolveHighlightExtraStyles(view: ITouchableHighlightUnderlayView): ITouchableHighlightExtraStyles | undefined;
|
|
@@ -14,4 +14,22 @@ export declare function canUseNativeForeground(): boolean;
|
|
|
14
14
|
export declare function selectableBackground(rippleRadius?: number): IThemeAttrBackground;
|
|
15
15
|
export declare function selectableBackgroundBorderless(rippleRadius?: number): IThemeAttrBackground;
|
|
16
16
|
export declare function rippleBackground(color: string, borderless: boolean, rippleRadius?: number): IRippleBackground;
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* RN's four statics, under RN's own spelling — `TouchableNativeFeedback.Ripple(color, borderless)`.
|
|
19
|
+
*
|
|
20
|
+
* A NAMESPACE OBJECT rather than a component value, and that is the whole of what survived the
|
|
21
|
+
* wrapper: the element is a tag now, and a tag is a string, which cannot carry properties. Shared
|
|
22
|
+
* rather than copied five times because every member is a pure dict producer with no framework in
|
|
23
|
+
* it; each adapter re-exports it verbatim, as it does the background types beside it.
|
|
24
|
+
*
|
|
25
|
+
* NOT the spelling for NEW code, and not redundant either. RN's docs point at `Pressable`'s
|
|
26
|
+
* declarative `android_ripple={{ color, borderless }}`, which the pressable behavior resolves
|
|
27
|
+
* (`behaviors/pressable.ts`, `asRippleConfig`). These four stay because the
|
|
28
|
+
* `touchable-native-feedback` tag folds `background` and nothing else produces that value.
|
|
29
|
+
*/
|
|
30
|
+
export declare const TouchableNativeFeedback: {
|
|
31
|
+
SelectableBackground: typeof selectableBackground;
|
|
32
|
+
SelectableBackgroundBorderless: typeof selectableBackgroundBorderless;
|
|
33
|
+
Ripple: typeof rippleBackground;
|
|
34
|
+
canUseNativeForeground: typeof canUseNativeForeground;
|
|
35
|
+
};
|
|
@@ -32,12 +32,37 @@ export function selectableBackgroundBorderless(rippleRadius) {
|
|
|
32
32
|
export function rippleBackground(color, borderless, rippleRadius) {
|
|
33
33
|
return { type: 'RippleAndroid', color, borderless, rippleRadius };
|
|
34
34
|
}
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
// `backgroundProps` WAS HERE AND IS GONE (2026-09-18). It mapped the resolved background plus
|
|
36
|
+
// `useForeground` onto the native slot Android reads, and that is the `#ifdef ANDROID` tail of
|
|
37
|
+
// `foldCloneOntoChild` in `SymbioteFabricProps.cpp` — api-level gate included, since
|
|
38
|
+
// `Platform.Version` on Android IS the api level and `androidApiLevel()` reads the same number.
|
|
39
|
+
//
|
|
40
|
+
// A JS COPY OF A RULE THAT RUNS IN C++, with no runtime caller and no test, reachable only through
|
|
41
|
+
// the package barrel. Found by asking what each `Platform.OS` branch still left in
|
|
42
|
+
// `core/components` is FOR. Its C++ twin is covered on both arms —
|
|
43
|
+
// `android-rules.android.itest.ts` asserts the foreground slot and `clone-onto-child-payload.itest.ts`
|
|
44
|
+
// asserts its absence off Android — and break-testing the gate fires exactly one of them.
|
|
45
|
+
//
|
|
46
|
+
// `canUseNativeForeground` below stays, and the distinction is the reusable half: it is a QUESTION
|
|
47
|
+
// an app asks the platform (`TouchableNativeFeedback.canUseNativeForeground()` is RN's own public
|
|
48
|
+
// static), not a rule that decides a payload. Same class as the slider reading a folded
|
|
49
|
+
// `accessibilityState` — asking is not reimplementing.
|
|
50
|
+
/**
|
|
51
|
+
* RN's four statics, under RN's own spelling — `TouchableNativeFeedback.Ripple(color, borderless)`.
|
|
52
|
+
*
|
|
53
|
+
* A NAMESPACE OBJECT rather than a component value, and that is the whole of what survived the
|
|
54
|
+
* wrapper: the element is a tag now, and a tag is a string, which cannot carry properties. Shared
|
|
55
|
+
* rather than copied five times because every member is a pure dict producer with no framework in
|
|
56
|
+
* it; each adapter re-exports it verbatim, as it does the background types beside it.
|
|
57
|
+
*
|
|
58
|
+
* NOT the spelling for NEW code, and not redundant either. RN's docs point at `Pressable`'s
|
|
59
|
+
* declarative `android_ripple={{ color, borderless }}`, which the pressable behavior resolves
|
|
60
|
+
* (`behaviors/pressable.ts`, `asRippleConfig`). These four stay because the
|
|
61
|
+
* `touchable-native-feedback` tag folds `background` and nothing else produces that value.
|
|
62
|
+
*/
|
|
63
|
+
export const TouchableNativeFeedback = {
|
|
64
|
+
SelectableBackground: selectableBackground,
|
|
65
|
+
SelectableBackgroundBorderless: selectableBackgroundBorderless,
|
|
66
|
+
Ripple: rippleBackground,
|
|
67
|
+
canUseNativeForeground,
|
|
68
|
+
};
|