@symbiote-native/components 0.5.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 +23 -10
- package/build/accessibility-props.d.ts +11 -0
- package/build/accessibility-props.js +30 -118
- 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 +149 -0
- package/build/behaviors/button.d.ts +2 -0
- package/build/behaviors/button.js +328 -0
- package/build/behaviors/image-background.d.ts +2 -0
- package/build/behaviors/image-background.js +139 -0
- 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 +60 -0
- package/build/behaviors/pressable.js +385 -0
- package/build/behaviors/refresh-control.d.ts +2 -0
- package/build/behaviors/refresh-control.js +83 -0
- package/build/behaviors/scroll-view/index.android.d.ts +1 -0
- package/build/behaviors/scroll-view/index.android.js +52 -0
- package/build/behaviors/scroll-view/index.d.ts +2 -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 +5 -0
- package/build/behaviors/scroll-view/shared.d.ts +11 -0
- package/build/behaviors/scroll-view/shared.js +291 -0
- package/build/behaviors/scroll-view/sticky.d.ts +17 -0
- package/build/behaviors/scroll-view/sticky.js +568 -0
- package/build/behaviors/switch.d.ts +2 -0
- package/build/behaviors/switch.js +186 -0
- package/build/behaviors/text-input.d.ts +14 -0
- package/build/behaviors/text-input.js +319 -0
- package/build/behaviors/touchable-highlight.d.ts +9 -0
- package/build/behaviors/touchable-highlight.js +192 -0
- package/build/behaviors/touchable-native-feedback.d.ts +20 -0
- package/build/behaviors/touchable-native-feedback.js +333 -0
- package/build/behaviors/touchable-opacity.d.ts +12 -0
- package/build/behaviors/touchable-opacity.js +227 -0
- package/build/behaviors/touchable-without-feedback.d.ts +2 -0
- package/build/behaviors/touchable-without-feedback.js +296 -0
- package/build/component-names/index.android.js +47 -15
- package/build/component-names/index.ios.js +35 -15
- package/build/component-names/shared.d.ts +2 -1
- package/build/component-names/shared.js +49 -3
- package/build/descriptor.js +4 -4
- package/build/fold-host-bag.d.ts +15 -0
- package/build/fold-host-bag.js +99 -0
- package/build/index.d.ts +23 -12
- package/build/index.js +50 -9
- package/build/register.d.ts +1 -0
- package/build/register.js +55 -0
- package/build/resolve-intrinsic.d.ts +7 -0
- package/build/resolve-intrinsic.js +49 -0
- package/build/scroll-view-commands.d.ts +4 -0
- package/build/scroll-view-commands.js +30 -31
- package/build/state/pressable.d.ts +9 -0
- package/build/state/pressable.js +120 -34
- package/build/state/text-input.d.ts +10 -2
- package/build/state/text-input.js +11 -0
- package/build/text-props.js +2 -1
- package/build/view/render-button.d.ts +36 -1
- package/build/view/render-button.js +101 -12
- package/build/view/render-image/index.d.ts +2 -0
- package/build/view/render-image/index.js +29 -2
- package/build/view/render-input-accessory-view.d.ts +2 -0
- package/build/view/render-input-accessory-view.js +37 -5
- package/build/view/render-modal.js +3 -3
- package/build/view/render-pressable/index.d.ts +2 -0
- package/build/view/render-pressable/index.js +24 -0
- package/build/view/render-scroll-view.d.ts +1 -0
- package/build/view/render-scroll-view.js +18 -9
- package/build/view/render-switch.d.ts +4 -1
- package/build/view/render-switch.js +8 -2
- package/build/view/render-text-input.js +6 -2
- package/build/view/render-touchable-native-feedback.d.ts +19 -0
- package/build/view/render-touchable-native-feedback.js +19 -0
- package/host-primitives.cjs +432 -0
- package/host-primitives.d.cts +33 -0
- package/package.json +33 -5
- package/build/view/render-activity-indicator.d.ts +0 -25
- package/build/view/render-activity-indicator.js +0 -62
- package/build/view/render-image-background.d.ts +0 -9
- package/build/view/render-image-background.js +0 -48
package/README.md
CHANGED
|
@@ -30,8 +30,7 @@ supplies only the third:
|
|
|
30
30
|
bridges the `Descriptor` to `React.createElement`; Vue wires it through `ref`/`watch` and
|
|
31
31
|
bridges to `h()`. This is the ONLY part a new adapter has to write.
|
|
32
32
|
|
|
33
|
-
`Switch` is the canonical reference for a full three-layer component
|
|
34
|
-
canonical render-only reference (no state machine needed).
|
|
33
|
+
`Switch` is the canonical reference for a full three-layer component.
|
|
35
34
|
|
|
36
35
|
### Install
|
|
37
36
|
|
|
@@ -86,19 +85,33 @@ host node untouched — the render function never names a framework type.
|
|
|
86
85
|
canonical `accessibility*` transform, shared so every adapter folds identically.
|
|
87
86
|
- **Components with a full state + render split** — `Switch`, `Modal` (its reducer gates the iOS
|
|
88
87
|
keep-alive frame).
|
|
89
|
-
- **Render-only components** (no state machine) — `
|
|
90
|
-
`
|
|
91
|
-
- **Pure logic/plumbing without a `Descriptor`** — `Pressable`'s press state machine
|
|
92
|
-
(`createPressHandlers` / `createPressRuntime`
|
|
93
|
-
`
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
88
|
+
- **Render-only components** (no state machine) — `Image`, `ImageBackground`,
|
|
89
|
+
`InputAccessoryView`.
|
|
90
|
+
- **Pure logic/plumbing without a full `Descriptor`** — `Pressable`'s press state machine
|
|
91
|
+
(`createPressHandlers` / `createPressRuntime` in `state/pressable`) plus its render-decision
|
|
92
|
+
helpers (`buildPressableListeners`, `resolveDisabledAccessibilityState`, `shouldClaimResponder`,
|
|
93
|
+
… in `view/render-pressable`), the `Touchable*` timing constants, `Button`'s shared text-style
|
|
94
|
+
fold, `TextInput`'s controlled-value/event-count handshake (`resolveTextInputProps`, `foldText`,
|
|
95
|
+
`eventCountFromChange`, …), `KeyboardAvoidingView`'s inset math, `ScrollView`'s
|
|
96
|
+
intrinsics/sticky-header math (no full 3-layer split — the adapter owns the element assembly).
|
|
97
|
+
- **Two more shared reducers, driven by every adapter's reactive glue**: the sticky-header effect
|
|
98
|
+
machine (`reduceSticky` / `createInitialStickyState`, the zero-swallow gate + debounce pick every
|
|
99
|
+
sticky-header consumer needs) and the list orchestration reducer (`reduceList` /
|
|
100
|
+
`createInitialListState`, the after-commit effect skeleton — window recompute → edge →
|
|
101
|
+
viewability → initial-scroll → MVCP — every list adapter runs).
|
|
97
102
|
- **The `VirtualizedList` family's windowing engine** — `computeWindow`, `buildListPlan`,
|
|
98
103
|
viewability tracking (`computeViewableSet`, `diffViewable`), and the `FlatList`/`SectionList`
|
|
99
104
|
row/section folding helpers. Lists have no `view/render-*.ts` (a cell's content is the
|
|
100
105
|
framework's own children) — the shared layer here is pure state/logic, reused verbatim by every
|
|
101
106
|
adapter.
|
|
107
|
+
- **Host behaviors (`src/behaviors/*.ts`)** — tier-2 host-primitive lowering: a primitive's state
|
|
108
|
+
machine and prop folds registered directly on the engine node (`registerPressableBehavior`,
|
|
109
|
+
`registerSwitchBehavior`, `registerImageBehavior`, `registerTextInputBehavior`,
|
|
110
|
+
`registerInputAccessoryViewBehavior`, plus the folds a lowered element still needs —
|
|
111
|
+
`foldImagePayload`, `foldInputAccessoryViewPayload`, `buildTextInputHandle`) so a
|
|
112
|
+
`Pressable`/`Switch`/`TextInput`/`Image`/`InputAccessoryView` can compile to a bare intrinsic tag
|
|
113
|
+
instead of a framework component. Built on `@symbiote-native/engine`'s `registerHostBehavior`
|
|
114
|
+
seam; see `.claude/rules/host-primitive-tier.md`.
|
|
102
115
|
|
|
103
116
|
## What it does NOT do
|
|
104
117
|
|
|
@@ -61,4 +61,15 @@ export interface IAriaProps {
|
|
|
61
61
|
'aria-valuenow'?: number;
|
|
62
62
|
'aria-valuetext'?: string;
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* The typed entry point adapters import. The FOLD itself now lives in `@symbiote-native/engine`
|
|
66
|
+
* (`core/engine/src/accessibility-props.ts`) so it runs at the layer every path goes through —
|
|
67
|
+
* including a LOWERED element, which has no component wrapper to run it. This function stays here
|
|
68
|
+
* because the public types do, and because it keeps the typed gate: `hasAnyAriaKey` is checked
|
|
69
|
+
* before anything is allocated, so the ~99% of nodes carrying no alias cost the same as before.
|
|
70
|
+
*
|
|
71
|
+
* Idempotent by construction, and that is load-bearing now that the engine folds too: pass 1 blanks
|
|
72
|
+
* every alias, so a wrapper still calling this after the engine has run finds nothing and returns
|
|
73
|
+
* by identity.
|
|
74
|
+
*/
|
|
64
75
|
export declare function resolveAccessibilityProps<T extends IAccessibilityProps & IAriaProps>(props: T): T;
|
|
@@ -8,43 +8,11 @@
|
|
|
8
8
|
//
|
|
9
9
|
// Framework-agnostic (imports only @symbiote-native/engine), so every adapter (React,
|
|
10
10
|
// Vue, and the next) folds aria/role into accessibility* identically.
|
|
11
|
-
import {
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
alert: 'alert',
|
|
17
|
-
button: 'button',
|
|
18
|
-
checkbox: 'checkbox',
|
|
19
|
-
combobox: 'combobox',
|
|
20
|
-
grid: 'grid',
|
|
21
|
-
heading: 'header',
|
|
22
|
-
img: 'image',
|
|
23
|
-
link: 'link',
|
|
24
|
-
list: 'list',
|
|
25
|
-
listitem: 'list',
|
|
26
|
-
menu: 'menu',
|
|
27
|
-
menubar: 'menubar',
|
|
28
|
-
menuitem: 'menuitem',
|
|
29
|
-
none: 'none',
|
|
30
|
-
presentation: 'none',
|
|
31
|
-
progressbar: 'progressbar',
|
|
32
|
-
radio: 'radio',
|
|
33
|
-
radiogroup: 'radiogroup',
|
|
34
|
-
scrollbar: 'scrollbar',
|
|
35
|
-
searchbox: 'search',
|
|
36
|
-
slider: 'adjustable',
|
|
37
|
-
spinbutton: 'spinbutton',
|
|
38
|
-
summary: 'summary',
|
|
39
|
-
switch: 'switch',
|
|
40
|
-
tab: 'tab',
|
|
41
|
-
tablist: 'tablist',
|
|
42
|
-
timer: 'timer',
|
|
43
|
-
toolbar: 'toolbar',
|
|
44
|
-
};
|
|
45
|
-
function accessibilityRoleFromRole(role) {
|
|
46
|
-
return ROLE_TO_ACCESSIBILITY_ROLE[role] ?? role;
|
|
47
|
-
}
|
|
11
|
+
import { foldAriaProps } from '@symbiote-native/engine';
|
|
12
|
+
// The gate's key list, typed against `IAriaProps` so a new alias added to that interface and
|
|
13
|
+
// forgotten here is a type error. The engine carries its own untyped copy for the lowered path;
|
|
14
|
+
// `core/engine/src/accessibility-props.ts` is the single implementation of the FOLD, this is only
|
|
15
|
+
// the cheap probe that decides whether to call it.
|
|
48
16
|
const ARIA_KEYS = [
|
|
49
17
|
'role',
|
|
50
18
|
'aria-label',
|
|
@@ -62,90 +30,34 @@ const ARIA_KEYS = [
|
|
|
62
30
|
'aria-valuenow',
|
|
63
31
|
'aria-valuetext',
|
|
64
32
|
];
|
|
33
|
+
// An indexed loop rather than `.some(key => …)`: the callback captures `props`, so the closure is
|
|
34
|
+
// allocated on every call, and this runs once per accessibility-bearing component instance — 8 000
|
|
35
|
+
// of them on one benchmark create. The fifteen property reads it replaces the closure with are
|
|
36
|
+
// cheaper than the allocation.
|
|
65
37
|
function hasAnyAriaKey(props) {
|
|
66
|
-
|
|
38
|
+
for (let index = 0; index < ARIA_KEYS.length; index += 1) {
|
|
39
|
+
if (props[ARIA_KEYS[index]] !== undefined)
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
67
43
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
44
|
+
/**
|
|
45
|
+
* The typed entry point adapters import. The FOLD itself now lives in `@symbiote-native/engine`
|
|
46
|
+
* (`core/engine/src/accessibility-props.ts`) so it runs at the layer every path goes through —
|
|
47
|
+
* including a LOWERED element, which has no component wrapper to run it. This function stays here
|
|
48
|
+
* because the public types do, and because it keeps the typed gate: `hasAnyAriaKey` is checked
|
|
49
|
+
* before anything is allocated, so the ~99% of nodes carrying no alias cost the same as before.
|
|
50
|
+
*
|
|
51
|
+
* Idempotent by construction, and that is load-bearing now that the engine folds too: pass 1 blanks
|
|
52
|
+
* every alias, so a wrapper still calling this after the engine has run finds nothing and returns
|
|
53
|
+
* by identity.
|
|
54
|
+
*/
|
|
75
55
|
export function resolveAccessibilityProps(props) {
|
|
76
56
|
if (!hasAnyAriaKey(props))
|
|
77
57
|
return props;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
'aria-label': undefined,
|
|
84
|
-
'aria-labelledby': undefined,
|
|
85
|
-
'aria-live': undefined,
|
|
86
|
-
'aria-hidden': undefined,
|
|
87
|
-
'aria-busy': undefined,
|
|
88
|
-
'aria-checked': undefined,
|
|
89
|
-
'aria-disabled': undefined,
|
|
90
|
-
'aria-expanded': undefined,
|
|
91
|
-
'aria-selected': undefined,
|
|
92
|
-
'aria-modal': undefined,
|
|
93
|
-
'aria-valuemax': undefined,
|
|
94
|
-
'aria-valuemin': undefined,
|
|
95
|
-
'aria-valuenow': undefined,
|
|
96
|
-
'aria-valuetext': undefined,
|
|
97
|
-
};
|
|
98
|
-
if (ariaLabelledBy !== undefined &&
|
|
99
|
-
next.accessibilityLabelledBy === undefined) {
|
|
100
|
-
next.accessibilityLabelledBy = ariaLabelledBy.split(/\s*,\s*/g);
|
|
101
|
-
}
|
|
102
|
-
if (ariaLabel !== undefined && next.accessibilityLabel === undefined) {
|
|
103
|
-
next.accessibilityLabel = ariaLabel;
|
|
104
|
-
}
|
|
105
|
-
if (ariaLive !== undefined && next.accessibilityLiveRegion === undefined) {
|
|
106
|
-
next.accessibilityLiveRegion = ariaLive === 'off' ? 'none' : ariaLive;
|
|
107
|
-
}
|
|
108
|
-
if (ariaHidden !== undefined) {
|
|
109
|
-
if (next.accessibilityElementsHidden === undefined) {
|
|
110
|
-
next.accessibilityElementsHidden = ariaHidden;
|
|
111
|
-
}
|
|
112
|
-
if (ariaHidden === true && next.importantForAccessibility === undefined) {
|
|
113
|
-
next.importantForAccessibility = 'no-hide-descendants';
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (ariaModal !== undefined && next.accessibilityViewIsModal === undefined) {
|
|
117
|
-
next.accessibilityViewIsModal = ariaModal;
|
|
118
|
-
}
|
|
119
|
-
if (role !== undefined && next.accessibilityRole === undefined) {
|
|
120
|
-
next.accessibilityRole = accessibilityRoleFromRole(role);
|
|
121
|
-
}
|
|
122
|
-
const existingState = props.accessibilityState;
|
|
123
|
-
if (existingState !== undefined ||
|
|
124
|
-
ariaBusy !== undefined ||
|
|
125
|
-
ariaChecked !== undefined ||
|
|
126
|
-
ariaDisabled !== undefined ||
|
|
127
|
-
ariaExpanded !== undefined ||
|
|
128
|
-
ariaSelected !== undefined) {
|
|
129
|
-
next.accessibilityState = {
|
|
130
|
-
busy: ariaBusy ?? existingState?.busy,
|
|
131
|
-
checked: ariaChecked ?? existingState?.checked,
|
|
132
|
-
disabled: ariaDisabled ?? existingState?.disabled,
|
|
133
|
-
expanded: ariaExpanded ?? existingState?.expanded,
|
|
134
|
-
selected: ariaSelected ?? existingState?.selected,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
const existingValue = props.accessibilityValue;
|
|
138
|
-
if (existingValue !== undefined ||
|
|
139
|
-
ariaValueMax !== undefined ||
|
|
140
|
-
ariaValueMin !== undefined ||
|
|
141
|
-
ariaValueNow !== undefined ||
|
|
142
|
-
ariaValueText !== undefined) {
|
|
143
|
-
next.accessibilityValue = {
|
|
144
|
-
max: ariaValueMax ?? existingValue?.max,
|
|
145
|
-
min: ariaValueMin ?? existingValue?.min,
|
|
146
|
-
now: ariaValueNow ?? existingValue?.now,
|
|
147
|
-
text: ariaValueText ?? existingValue?.text,
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
return next;
|
|
58
|
+
// `Object.entries` rather than a spread: an interface has no index signature, so a spread of `T`
|
|
59
|
+
// is not assignable to `Record<string, unknown>` and the alternative would be a cast. Paid only
|
|
60
|
+
// on the folding path, never on the gate.
|
|
61
|
+
const folded = foldAriaProps(Object.fromEntries(Object.entries(props)));
|
|
62
|
+
return Object.assign({}, props, folded);
|
|
151
63
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function registerActivityIndicatorBehavior(): void;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// ActivityIndicator's behavior on Android, where the spinner is `AndroidProgressBar` and needs two
|
|
2
|
+
// props RN's iOS branch never sends (`ActivityIndicator.js:106`, spread only at `:118`):
|
|
3
|
+
//
|
|
4
|
+
// styleAttr drives ProgressBar.setStyle(); without it the view throws "setStyle() not called"
|
|
5
|
+
// indeterminate the spinner has no determinate mode here
|
|
6
|
+
//
|
|
7
|
+
// and where the default colour is the THEME — expressed as null so the fold omits the key entirely
|
|
8
|
+
// rather than handing Fabric's colour parser a null it rejects.
|
|
9
|
+
import { registerActivityIndicatorBehaviors } from './shared.js';
|
|
10
|
+
const ANDROID_STYLE_ATTR = 'Normal';
|
|
11
|
+
export function registerActivityIndicatorBehavior() {
|
|
12
|
+
registerActivityIndicatorBehaviors({
|
|
13
|
+
defaultColor: null,
|
|
14
|
+
nativeExtras: { styleAttr: ANDROID_STYLE_ATTR, indeterminate: true },
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function registerActivityIndicatorBehavior(): void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// ActivityIndicator's behavior on iOS: `ActivityIndicatorView` takes the size enum and a GRAY
|
|
2
|
+
// default colour, and needs no extra native props. That is the entire platform half.
|
|
3
|
+
//
|
|
4
|
+
// This file is also the base the folder's `index.ts` re-exports for headless, matching every other
|
|
5
|
+
// platform-split module in this tree.
|
|
6
|
+
import { registerActivityIndicatorBehaviors } from './shared.js';
|
|
7
|
+
// RN's iOS default spinner colour (`ActivityIndicator.js:25`, GRAY).
|
|
8
|
+
const IOS_DEFAULT_COLOR = '#999999';
|
|
9
|
+
export function registerActivityIndicatorBehavior() {
|
|
10
|
+
registerActivityIndicatorBehaviors({
|
|
11
|
+
defaultColor: IOS_DEFAULT_COLOR,
|
|
12
|
+
nativeExtras: {},
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// The base of the folder-as-module group: Metro picks `index.ios` / `index.android` per platform,
|
|
2
|
+
// and everything else (tsx, vitest, headless) lands here. iOS is the default, matching every other
|
|
3
|
+
// platform-split module in this tree.
|
|
4
|
+
export { registerActivityIndicatorBehavior } from './index.ios.js';
|
|
5
|
+
export { ACTIVITY_INDICATOR_SPINNER_TAG, ACTIVITY_INDICATOR_TAG, } from './shared.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type IStyleProp, type ISymbioteEvent, type IViewStyle } from '@symbiote-native/engine';
|
|
2
|
+
import type { IAccessibilityProps, IAriaProps } from '../../accessibility-props';
|
|
3
|
+
export declare const ACTIVITY_INDICATOR_TAG = "activity-indicator";
|
|
4
|
+
export declare const ACTIVITY_INDICATOR_SPINNER_TAG = "activity-indicator-spinner";
|
|
5
|
+
export type IActivityIndicatorSize = 'small' | 'large' | number;
|
|
6
|
+
export interface IActivityIndicatorProps extends IAccessibilityProps, IAriaProps {
|
|
7
|
+
animating?: boolean;
|
|
8
|
+
color?: string;
|
|
9
|
+
size?: IActivityIndicatorSize;
|
|
10
|
+
hidesWhenStopped?: boolean;
|
|
11
|
+
style?: IStyleProp<IViewStyle>;
|
|
12
|
+
onLayout?: (event: ISymbioteEvent) => void;
|
|
13
|
+
}
|
|
14
|
+
export type IActivityIndicatorPlatform = {
|
|
15
|
+
defaultColor: string | null;
|
|
16
|
+
nativeExtras: Readonly<Record<string, unknown>>;
|
|
17
|
+
};
|
|
18
|
+
export declare function registerActivityIndicatorBehaviors(platform: IActivityIndicatorPlatform): void;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// ActivityIndicator's host behavior: the composition and the prop fold, below the framework, so the
|
|
2
|
+
// primitive is a bare `activity-indicator` tag and not five wrapper components.
|
|
3
|
+
//
|
|
4
|
+
// THE TWO-NODE SHAPE IS RN'S, not ours to collapse. `ActivityIndicator.js:112` opens a centering
|
|
5
|
+
// `<View>` around the native spinner, so the tag is that View and `buildStructure` builds
|
|
6
|
+
// `activity-indicator-spinner` under it. The container cannot be folded INTO the spinner either: it
|
|
7
|
+
// carries `alignItems`/`justifyContent`, which centre the spinner inside the space it was given,
|
|
8
|
+
// and moved onto the spinner they would centre its children, of which it has none.
|
|
9
|
+
//
|
|
10
|
+
// THE PLATFORM HALF IS THE SPINNER'S DEFAULTS AND NOTHING ELSE. iOS defaults the colour to RN's GRAY
|
|
11
|
+
// and needs no extra native props; Android's default is the theme, which means OMITTING the key
|
|
12
|
+
// rather than sending null (Fabric's colour parser rejects a null), plus `styleAttr` and
|
|
13
|
+
// `indeterminate` — without the first, AndroidProgressBar throws "setStyle() not called".
|
|
14
|
+
// `index.ios` / `index.android` supply them, the same file split `behaviors/scroll-view` uses.
|
|
15
|
+
//
|
|
16
|
+
// WHERE THE APP'S PROPS GO. `slotPropsExcept` is the COMPLEMENT of a rename map: everything an app
|
|
17
|
+
// writes on the tag routes to the spinner under its own name except `ACTIVITY_INDICATOR_HOST_PROPS`,
|
|
18
|
+
// which is RN's own split (`ActivityIndicator.js:99` spreads `...restProps` onto the spinner; `:113`
|
|
19
|
+
// keeps `onLayout` and `style` on the View). The set that moves is OPEN — every aria alias, every
|
|
20
|
+
// accessibility prop, whatever an app writes next — so a name map cannot express it.
|
|
21
|
+
//
|
|
22
|
+
// THE SIZE TRANSLATION IS PLATFORM-INVARIANT and lives here beside the fold that applies it: RN maps
|
|
23
|
+
// 'small'/'large' to a native size enum AND a fixed box style, while a NUMBER never reaches native
|
|
24
|
+
// at all (it sizes the spinner through style alone).
|
|
25
|
+
//
|
|
26
|
+
// Registered by all five adapters since 2026-09-09, in the same commit that deleted the five
|
|
27
|
+
// wrappers — the registry is keyed by TAG, so registering while a wrapper still painted its own
|
|
28
|
+
// spinner would have given every indicator two.
|
|
29
|
+
import { appendChild, createElement, registerHostBehavior, } from '@symbiote-native/engine';
|
|
30
|
+
import { descriptorFor } from '../../component-names';
|
|
31
|
+
export const ACTIVITY_INDICATOR_TAG = 'activity-indicator';
|
|
32
|
+
// The NATIVE spinner — `ActivityIndicatorView` on iOS, `AndroidProgressBar` on Android. Built by
|
|
33
|
+
// `buildStructure` below and by nothing else; no app writes it.
|
|
34
|
+
export const ACTIVITY_INDICATOR_SPINNER_TAG = 'activity-indicator-spinner';
|
|
35
|
+
// Fixed pixel boxes RN gives the two named sizes (styles.sizeSmall/sizeLarge).
|
|
36
|
+
const SIZE_SMALL_PX = 20;
|
|
37
|
+
const SIZE_LARGE_PX = 36;
|
|
38
|
+
// RN's own default when the app writes no `size` (ActivityIndicator.js:72).
|
|
39
|
+
const DEFAULT_SIZE = 'small';
|
|
40
|
+
// Centering wrapper RN puts around the spinner (styles.container).
|
|
41
|
+
const CONTAINER_STYLE = {
|
|
42
|
+
alignItems: 'center',
|
|
43
|
+
justifyContent: 'center',
|
|
44
|
+
};
|
|
45
|
+
// The props that stay on the centering host instead of travelling to the spinner: RN's own two
|
|
46
|
+
// (`ActivityIndicator.js:113`) plus the spellings the ENGINE resolves against a node's own style.
|
|
47
|
+
const ACTIVITY_INDICATOR_HOST_PROPS = [
|
|
48
|
+
// A layout callback measures the box the spinner is centred IN, which is this node.
|
|
49
|
+
'onLayout',
|
|
50
|
+
// The composed `StyleSheet.compose(styles.container, style)` array (`:114`).
|
|
51
|
+
'style',
|
|
52
|
+
// A class NAME resolves to a style (`routeProp`'s class branch), so a class written on the tag has
|
|
53
|
+
// to reach the centering view — the node `style` lands on — or the app's rule paints a spinner it
|
|
54
|
+
// was never written for.
|
|
55
|
+
'class',
|
|
56
|
+
'className',
|
|
57
|
+
];
|
|
58
|
+
function resolveSize(size) {
|
|
59
|
+
if (size === 'small') {
|
|
60
|
+
return {
|
|
61
|
+
sizeStyle: { width: SIZE_SMALL_PX, height: SIZE_SMALL_PX },
|
|
62
|
+
sizeProp: 'small',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (size === 'large') {
|
|
66
|
+
return {
|
|
67
|
+
sizeStyle: { width: SIZE_LARGE_PX, height: SIZE_LARGE_PX },
|
|
68
|
+
sizeProp: 'large',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return { sizeStyle: { width: size, height: size } };
|
|
72
|
+
}
|
|
73
|
+
// The HOST's fold: RN's `StyleSheet.compose(styles.container, style)` (ActivityIndicator.js:114).
|
|
74
|
+
// Base first, so an app style still wins.
|
|
75
|
+
const hostFold = props => ({
|
|
76
|
+
...props,
|
|
77
|
+
style: [CONTAINER_STYLE, props.style],
|
|
78
|
+
});
|
|
79
|
+
function isActivityIndicatorSize(value) {
|
|
80
|
+
return value === 'small' || value === 'large' || typeof value === 'number';
|
|
81
|
+
}
|
|
82
|
+
// The SPINNER's fold — RN's own body (`ActivityIndicator.js:99-118`) applied to the node the app
|
|
83
|
+
// never names.
|
|
84
|
+
function spinnerFold(platform) {
|
|
85
|
+
return props => {
|
|
86
|
+
const size = isActivityIndicatorSize(props.size)
|
|
87
|
+
? props.size
|
|
88
|
+
: DEFAULT_SIZE;
|
|
89
|
+
const { sizeStyle, sizeProp } = resolveSize(size);
|
|
90
|
+
const next = {
|
|
91
|
+
...props,
|
|
92
|
+
// RN defaults both to true and every wrapper spelled that `!== false`. A tag has no
|
|
93
|
+
// destructuring default, so the fold is where the default has to live.
|
|
94
|
+
animating: props.animating !== false,
|
|
95
|
+
hidesWhenStopped: props.hidesWhenStopped !== false,
|
|
96
|
+
style: sizeStyle,
|
|
97
|
+
};
|
|
98
|
+
// A NUMBER never reaches native: it sizes the spinner through style alone, and the native enum
|
|
99
|
+
// takes 'small'/'large' only. So the key has to leave, not merely go unwritten.
|
|
100
|
+
if (sizeProp === undefined)
|
|
101
|
+
delete next.size;
|
|
102
|
+
else
|
|
103
|
+
next.size = sizeProp;
|
|
104
|
+
const color = typeof props.color === 'string' ? props.color : platform.defaultColor;
|
|
105
|
+
// Omitted rather than sent as null — Android's theme default is null and Fabric's colour parser
|
|
106
|
+
// rejects one.
|
|
107
|
+
if (color === null)
|
|
108
|
+
delete next.color;
|
|
109
|
+
else
|
|
110
|
+
next.color = color;
|
|
111
|
+
return next;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// The composition. Returns the spinner as the slot because the prop redirect is gated on
|
|
115
|
+
// `childHost` being set — the redirect is what this slot is FOR, and NOT where children go: RN's
|
|
116
|
+
// ActivityIndicator renders only the spinner (`ActivityIndicator.js:112-118`) and takes no children
|
|
117
|
+
// at all. Hence `slotTakesNoChildren` below; without it a stray child would mount INSIDE the native
|
|
118
|
+
// spinner, which on Android is a `ProgressBar` and not a `ViewGroup` — the `addView` crash
|
|
119
|
+
// `IHostBehavior.slotTakesNoChildren` records for ImageBackground's Image.
|
|
120
|
+
function buildSpinner(platform) {
|
|
121
|
+
return (node) => {
|
|
122
|
+
const descriptor = descriptorFor(ACTIVITY_INDICATOR_SPINNER_TAG);
|
|
123
|
+
const spinner = createElement(descriptor.component, descriptor.isText, ACTIVITY_INDICATOR_SPINNER_TAG);
|
|
124
|
+
// Constants of the platform, never a function of a prop, so they are seeded at build time the
|
|
125
|
+
// way ScrollView seeds `collapsable: false` — empty on iOS, AndroidProgressBar's two
|
|
126
|
+
// requirements on Android.
|
|
127
|
+
spinner.props = { ...platform.nativeExtras };
|
|
128
|
+
spinner.payloadFold = spinnerFold(platform);
|
|
129
|
+
appendChild(node, spinner);
|
|
130
|
+
return spinner;
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function activityIndicatorBehavior(platform) {
|
|
134
|
+
return {
|
|
135
|
+
slotPropsExcept: ACTIVITY_INDICATOR_HOST_PROPS,
|
|
136
|
+
slotTakesNoChildren: true,
|
|
137
|
+
buildStructure: buildSpinner(platform),
|
|
138
|
+
foldPayload: hostFold,
|
|
139
|
+
// Required by the interface and deliberately empty: this primitive owns no timer, no listener
|
|
140
|
+
// and no native handshake. Written out rather than shared with a `noop` so the emptiness reads
|
|
141
|
+
// as a decision.
|
|
142
|
+
attach() { },
|
|
143
|
+
detach() { },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// Called by the platform files; nothing else should.
|
|
147
|
+
export function registerActivityIndicatorBehaviors(platform) {
|
|
148
|
+
registerHostBehavior(ACTIVITY_INDICATOR_TAG, activityIndicatorBehavior(platform));
|
|
149
|
+
}
|