@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
package/README.md
CHANGED
|
@@ -51,14 +51,22 @@ types and wires the reducer/render pair from here. Calling the render function d
|
|
|
51
51
|
adapter itself does, to build its lifecycle wrapper:
|
|
52
52
|
|
|
53
53
|
```ts
|
|
54
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
renderSwitch,
|
|
56
|
+
createInitialSwitchState,
|
|
57
|
+
switchReducer,
|
|
58
|
+
} from '@symbiote-native/components';
|
|
55
59
|
|
|
56
60
|
// inside an adapter's own hook/composable:
|
|
57
61
|
const state = createInitialSwitchState();
|
|
58
62
|
const next = switchReducer(state, { type: 'native-reported', value: true });
|
|
59
63
|
const descriptor = renderSwitch(
|
|
60
64
|
{ value: true, disabled: false, passthrough: { onChange, ref } },
|
|
61
|
-
{
|
|
65
|
+
{
|
|
66
|
+
trackColorProps: (value, trackColor) => ({
|
|
67
|
+
/* platform-specific prop names */
|
|
68
|
+
}),
|
|
69
|
+
},
|
|
62
70
|
);
|
|
63
71
|
// descriptor is then handed to the adapter's own descriptorToReact / descriptorToVue bridge
|
|
64
72
|
```
|
|
@@ -80,17 +88,31 @@ host node untouched — the render function never names a framework type.
|
|
|
80
88
|
keep-alive frame).
|
|
81
89
|
- **Render-only components** (no state machine) — `ActivityIndicator`, `Image`,
|
|
82
90
|
`ImageBackground`, `InputAccessoryView`.
|
|
83
|
-
- **Pure logic/plumbing without a `Descriptor`** — `Pressable`'s press state machine
|
|
84
|
-
(`createPressHandlers` / `createPressRuntime`
|
|
85
|
-
`
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
- **Pure logic/plumbing without a full `Descriptor`** — `Pressable`'s press state machine
|
|
92
|
+
(`createPressHandlers` / `createPressRuntime` in `state/pressable`) plus its render-decision
|
|
93
|
+
helpers (`buildPressableListeners`, `resolveDisabledAccessibilityState`, `shouldClaimResponder`,
|
|
94
|
+
… in `view/render-pressable`), the `Touchable*` timing constants, `Button`'s shared text-style
|
|
95
|
+
fold, `TextInput`'s controlled-value/event-count handshake (`resolveTextInputProps`, `foldText`,
|
|
96
|
+
`eventCountFromChange`, …), `KeyboardAvoidingView`'s inset math, `ScrollView`'s
|
|
97
|
+
intrinsics/sticky-header math (no full 3-layer split — the adapter owns the element assembly).
|
|
98
|
+
- **Two more shared reducers, driven by every adapter's reactive glue**: the sticky-header effect
|
|
99
|
+
machine (`reduceSticky` / `createInitialStickyState`, the zero-swallow gate + debounce pick every
|
|
100
|
+
sticky-header consumer needs) and the list orchestration reducer (`reduceList` /
|
|
101
|
+
`createInitialListState`, the after-commit effect skeleton — window recompute → edge →
|
|
102
|
+
viewability → initial-scroll → MVCP — every list adapter runs).
|
|
89
103
|
- **The `VirtualizedList` family's windowing engine** — `computeWindow`, `buildListPlan`,
|
|
90
104
|
viewability tracking (`computeViewableSet`, `diffViewable`), and the `FlatList`/`SectionList`
|
|
91
105
|
row/section folding helpers. Lists have no `view/render-*.ts` (a cell's content is the
|
|
92
106
|
framework's own children) — the shared layer here is pure state/logic, reused verbatim by every
|
|
93
107
|
adapter.
|
|
108
|
+
- **Host behaviors (`src/behaviors/*.ts`)** — tier-2 host-primitive lowering: a primitive's state
|
|
109
|
+
machine and prop folds registered directly on the engine node (`registerPressableBehavior`,
|
|
110
|
+
`registerSwitchBehavior`, `registerImageBehavior`, `registerTextInputBehavior`,
|
|
111
|
+
`registerInputAccessoryViewBehavior`, plus the folds a lowered element still needs —
|
|
112
|
+
`foldImagePayload`, `foldInputAccessoryViewPayload`, `buildTextInputHandle`) so a
|
|
113
|
+
`Pressable`/`Switch`/`TextInput`/`Image`/`InputAccessoryView` can compile to a bare intrinsic tag
|
|
114
|
+
instead of a framework component. Built on `@symbiote-native/engine`'s `registerHostBehavior`
|
|
115
|
+
seam; see `.claude/rules/host-primitive-tier.md`.
|
|
94
116
|
|
|
95
117
|
## What it does NOT do
|
|
96
118
|
|
|
@@ -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,89 +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 && next.accessibilityLabelledBy === undefined) {
|
|
99
|
-
next.accessibilityLabelledBy = ariaLabelledBy.split(/\s*,\s*/g);
|
|
100
|
-
}
|
|
101
|
-
if (ariaLabel !== undefined && next.accessibilityLabel === undefined) {
|
|
102
|
-
next.accessibilityLabel = ariaLabel;
|
|
103
|
-
}
|
|
104
|
-
if (ariaLive !== undefined && next.accessibilityLiveRegion === undefined) {
|
|
105
|
-
next.accessibilityLiveRegion = ariaLive === 'off' ? 'none' : ariaLive;
|
|
106
|
-
}
|
|
107
|
-
if (ariaHidden !== undefined) {
|
|
108
|
-
if (next.accessibilityElementsHidden === undefined) {
|
|
109
|
-
next.accessibilityElementsHidden = ariaHidden;
|
|
110
|
-
}
|
|
111
|
-
if (ariaHidden === true && next.importantForAccessibility === undefined) {
|
|
112
|
-
next.importantForAccessibility = 'no-hide-descendants';
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
if (ariaModal !== undefined && next.accessibilityViewIsModal === undefined) {
|
|
116
|
-
next.accessibilityViewIsModal = ariaModal;
|
|
117
|
-
}
|
|
118
|
-
if (role !== undefined && next.accessibilityRole === undefined) {
|
|
119
|
-
next.accessibilityRole = accessibilityRoleFromRole(role);
|
|
120
|
-
}
|
|
121
|
-
const existingState = props.accessibilityState;
|
|
122
|
-
if (existingState !== undefined ||
|
|
123
|
-
ariaBusy !== undefined ||
|
|
124
|
-
ariaChecked !== undefined ||
|
|
125
|
-
ariaDisabled !== undefined ||
|
|
126
|
-
ariaExpanded !== undefined ||
|
|
127
|
-
ariaSelected !== undefined) {
|
|
128
|
-
next.accessibilityState = {
|
|
129
|
-
busy: ariaBusy ?? existingState?.busy,
|
|
130
|
-
checked: ariaChecked ?? existingState?.checked,
|
|
131
|
-
disabled: ariaDisabled ?? existingState?.disabled,
|
|
132
|
-
expanded: ariaExpanded ?? existingState?.expanded,
|
|
133
|
-
selected: ariaSelected ?? existingState?.selected,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
const existingValue = props.accessibilityValue;
|
|
137
|
-
if (existingValue !== undefined ||
|
|
138
|
-
ariaValueMax !== undefined ||
|
|
139
|
-
ariaValueMin !== undefined ||
|
|
140
|
-
ariaValueNow !== undefined ||
|
|
141
|
-
ariaValueText !== undefined) {
|
|
142
|
-
next.accessibilityValue = {
|
|
143
|
-
max: ariaValueMax ?? existingValue?.max,
|
|
144
|
-
min: ariaValueMin ?? existingValue?.min,
|
|
145
|
-
now: ariaValueNow ?? existingValue?.now,
|
|
146
|
-
text: ariaValueText ?? existingValue?.text,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
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);
|
|
150
63
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Image's host behavior, and it carries NOTHING but a prop fold — no listeners, no timers, no
|
|
2
|
+
// commit hook. That is what a "fold-only" primitive means: the wrapper's whole body was prop
|
|
3
|
+
// mapping, so its lowered form owes exactly that and nothing else.
|
|
4
|
+
//
|
|
5
|
+
// The fold itself is not written here. `mapImageProps` in `../view/render-image` is the one
|
|
6
|
+
// implementation, and this file only narrows a flat prop bag into the typed view it takes — the
|
|
7
|
+
// same shape `behaviors/text-input.ts` uses (`stringOf` / `booleanOf` guards, then hand the shared
|
|
8
|
+
// resolver a typed object). A second copy of the mapping is the exact drift this primitive has
|
|
9
|
+
// already paid for once: `adapters/svelte/src/components/image/image-logic.ts` reproduces it by
|
|
10
|
+
// hand and says so in its own header, because nothing was exported to call.
|
|
11
|
+
//
|
|
12
|
+
// WHY THIS MAY SHARE THE WRAPPER'S TAG, where TextInput needed `-managed`. A behavior fold is keyed
|
|
13
|
+
// on the tag, and `renderImage` emits `symbiote-image` too — so on a wrapper-built node this fold
|
|
14
|
+
// runs on ALREADY-FOLDED props. That is safe here and only here, because the mapping is idempotent:
|
|
15
|
+
// every alias it consumes (`src`, `srcSet`, `alt`, `width`, `height`, …) is absent from its own
|
|
16
|
+
// output, `source` comes back in the array shape `normalizeSource` guarantees, and
|
|
17
|
+
// `loadingIndicatorSource` leaves under a DIFFERENT name (`loadingIndicatorSrc`), so a second pass
|
|
18
|
+
// finds nothing left to fold. Idempotence is asserted in `image.test.ts` rather than assumed — the
|
|
19
|
+
// audit rule's point is that a double fold is invisible precisely when it happens to be harmless,
|
|
20
|
+
// so the property has to be pinned or it is an accident waiting to be broken.
|
|
21
|
+
import { registerHostBehavior, } from '@symbiote-native/engine';
|
|
22
|
+
import { IMAGE_VIEW_PROP_NAMES, mapImageProps, } from '../view/render-image/index.js';
|
|
23
|
+
export const IMAGE_TAG = 'symbiote-image';
|
|
24
|
+
const RESIZE_MODES = new Set([
|
|
25
|
+
'cover',
|
|
26
|
+
'contain',
|
|
27
|
+
'stretch',
|
|
28
|
+
'repeat',
|
|
29
|
+
'center',
|
|
30
|
+
]);
|
|
31
|
+
const CROSS_ORIGINS = new Set([
|
|
32
|
+
'anonymous',
|
|
33
|
+
'use-credentials',
|
|
34
|
+
]);
|
|
35
|
+
const CONSUMED = new Set(IMAGE_VIEW_PROP_NAMES);
|
|
36
|
+
function stringOf(value) {
|
|
37
|
+
return typeof value === 'string' ? value : undefined;
|
|
38
|
+
}
|
|
39
|
+
function numberOf(value) {
|
|
40
|
+
return typeof value === 'number' ? value : undefined;
|
|
41
|
+
}
|
|
42
|
+
function resizeModeOf(value) {
|
|
43
|
+
if (typeof value !== 'string' || !RESIZE_MODES.has(value))
|
|
44
|
+
return undefined;
|
|
45
|
+
// Narrowed by the set membership above, then re-stated as the union through a switch rather than
|
|
46
|
+
// a cast: the set and the type are two declarations of one list, and only this makes them agree.
|
|
47
|
+
switch (value) {
|
|
48
|
+
case 'cover':
|
|
49
|
+
case 'contain':
|
|
50
|
+
case 'stretch':
|
|
51
|
+
case 'repeat':
|
|
52
|
+
case 'center':
|
|
53
|
+
return value;
|
|
54
|
+
default:
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function crossOriginOf(value) {
|
|
59
|
+
if (typeof value !== 'string' || !CROSS_ORIGINS.has(value))
|
|
60
|
+
return undefined;
|
|
61
|
+
return value === 'anonymous' ? 'anonymous' : 'use-credentials';
|
|
62
|
+
}
|
|
63
|
+
// A source is an asset id (number), a `{uri}` object, or an array of those. Anything else cannot be
|
|
64
|
+
// resolved and is dropped rather than forwarded — a malformed source reaching Fabric paints nothing
|
|
65
|
+
// and reports nothing, which is worse than an image that is simply absent.
|
|
66
|
+
function sourceOf(value) {
|
|
67
|
+
if (typeof value === 'number')
|
|
68
|
+
return value;
|
|
69
|
+
if (typeof value !== 'object' || value === null)
|
|
70
|
+
return undefined;
|
|
71
|
+
if (Array.isArray(value))
|
|
72
|
+
return value;
|
|
73
|
+
if (typeof Reflect.get(value, 'uri') === 'string')
|
|
74
|
+
return { ...value };
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
// A StyleProp is an object, an array of them, or a registered class array — all of which
|
|
78
|
+
// `flattenStyle` already handles. The only thing to exclude is a scalar.
|
|
79
|
+
function styleOf(value) {
|
|
80
|
+
if (typeof value !== 'object' || value === null)
|
|
81
|
+
return undefined;
|
|
82
|
+
if (Array.isArray(value))
|
|
83
|
+
return value;
|
|
84
|
+
return { ...value };
|
|
85
|
+
}
|
|
86
|
+
export function foldImagePayload(props) {
|
|
87
|
+
const passthrough = {};
|
|
88
|
+
for (const key of Object.keys(props)) {
|
|
89
|
+
if (!CONSUMED.has(key))
|
|
90
|
+
passthrough[key] = props[key];
|
|
91
|
+
}
|
|
92
|
+
return mapImageProps({
|
|
93
|
+
source: sourceOf(props.source),
|
|
94
|
+
defaultSource: sourceOf(props.defaultSource),
|
|
95
|
+
loadingIndicatorSource: sourceOf(props.loadingIndicatorSource),
|
|
96
|
+
style: styleOf(props.style),
|
|
97
|
+
resizeMode: resizeModeOf(props.resizeMode),
|
|
98
|
+
tintColor: stringOf(props.tintColor),
|
|
99
|
+
src: stringOf(props.src),
|
|
100
|
+
srcSet: stringOf(props.srcSet),
|
|
101
|
+
alt: stringOf(props.alt),
|
|
102
|
+
width: numberOf(props.width),
|
|
103
|
+
height: numberOf(props.height),
|
|
104
|
+
crossOrigin: crossOriginOf(props.crossOrigin),
|
|
105
|
+
referrerPolicy: stringOf(props.referrerPolicy),
|
|
106
|
+
passthrough,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// Required by IHostBehavior and deliberately empty: Image owns no per-node runtime. Written out
|
|
110
|
+
// rather than shared with a `noop` helper so the emptiness reads as a decision.
|
|
111
|
+
function attach(_node) {
|
|
112
|
+
// nothing to set up
|
|
113
|
+
}
|
|
114
|
+
function detach(_node) {
|
|
115
|
+
// nothing to release
|
|
116
|
+
}
|
|
117
|
+
export function registerImageBehavior() {
|
|
118
|
+
registerHostBehavior(IMAGE_TAG, {
|
|
119
|
+
attach,
|
|
120
|
+
detach,
|
|
121
|
+
foldPayload: foldImagePayload,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// InputAccessoryView's host behavior — a prop fold and nothing else. No listeners, no timers, no
|
|
2
|
+
// commit hook: the wrapper's whole body was prop mapping, so its lowered form owes exactly that.
|
|
3
|
+
//
|
|
4
|
+
// The fold is NOT written here. `mapInputAccessoryViewProps` in
|
|
5
|
+
// `../view/render-input-accessory-view` is the one implementation and this file only narrows a flat
|
|
6
|
+
// bag into the typed view it takes, the `stringOf` / `styleOf` guard idiom `behaviors/image.ts`
|
|
7
|
+
// uses. Counted before starting, which is step one of the fold-only recipe: unlike Image, which had
|
|
8
|
+
// THREE implementations, all five adapters already call the shared render fn and Svelte's
|
|
9
|
+
// `input-accessory-view-props.ts` is a type declaration rather than a second mapping. So there was
|
|
10
|
+
// one implementation to extract, not three to collapse.
|
|
11
|
+
//
|
|
12
|
+
// SHARES THE WRAPPER'S TAG, no `-managed` twin. The two questions the recipe separates both answer
|
|
13
|
+
// no: the fold is idempotent (there is no aliasing at all — every consumed name leaves under the
|
|
14
|
+
// same name, so a second pass rewrites the same values), and the behavior carries no machine, so
|
|
15
|
+
// nothing can end up with two owners on one node. Idempotence is asserted rather than reasoned.
|
|
16
|
+
//
|
|
17
|
+
// PLATFORM. This is the only primitive in its group that is not platform-invariant in what it
|
|
18
|
+
// COMMITS TO: `symbiote-input-accessory-view` resolves to `RCTInputAccessoryView` on iOS and to a
|
|
19
|
+
// plain `RCTView` on Android. The fold itself is platform-invariant on purpose — it reproduces the
|
|
20
|
+
// wrapper's mapping exactly, on both platforms, so the lowered and wrapped paths cannot diverge
|
|
21
|
+
// per-platform. What it does NOT do is fix the pre-existing Android divergence underneath it:
|
|
22
|
+
// upstream RN renders NOTHING there (`InputAccessoryView.js` — `console.warn('<InputAccessoryView>
|
|
23
|
+
// is only supported on iOS.'); return null`), while we commit an RCTView, and `backgroundColor` is
|
|
24
|
+
// a declared prop of the iOS view but a style key on RCTView. Both predate this behavior and are
|
|
25
|
+
// identical on both paths; neither is in scope here, and a fold that "fixed" one silently would
|
|
26
|
+
// make the two paths disagree.
|
|
27
|
+
import { registerHostBehavior, } from '@symbiote-native/engine';
|
|
28
|
+
import { INPUT_ACCESSORY_VIEW_PROP_NAMES, mapInputAccessoryViewProps, } from '../view/render-input-accessory-view.js';
|
|
29
|
+
export const INPUT_ACCESSORY_VIEW_TAG = 'symbiote-input-accessory-view';
|
|
30
|
+
const CONSUMED = new Set(INPUT_ACCESSORY_VIEW_PROP_NAMES);
|
|
31
|
+
function stringOf(value) {
|
|
32
|
+
return typeof value === 'string' ? value : undefined;
|
|
33
|
+
}
|
|
34
|
+
// A StyleProp is an object, an array of them, or a registered class array — all shapes
|
|
35
|
+
// `flattenStyle` already handles. Only a scalar has to be excluded.
|
|
36
|
+
function styleOf(value) {
|
|
37
|
+
if (typeof value !== 'object' || value === null)
|
|
38
|
+
return undefined;
|
|
39
|
+
if (Array.isArray(value))
|
|
40
|
+
return value;
|
|
41
|
+
return { ...value };
|
|
42
|
+
}
|
|
43
|
+
export function foldInputAccessoryViewPayload(props) {
|
|
44
|
+
const passthrough = {};
|
|
45
|
+
for (const key of Object.keys(props)) {
|
|
46
|
+
if (!CONSUMED.has(key))
|
|
47
|
+
passthrough[key] = props[key];
|
|
48
|
+
}
|
|
49
|
+
return mapInputAccessoryViewProps({
|
|
50
|
+
nativeID: stringOf(props.nativeID),
|
|
51
|
+
backgroundColor: stringOf(props.backgroundColor),
|
|
52
|
+
style: styleOf(props.style),
|
|
53
|
+
passthrough,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// Required by IHostBehavior and deliberately empty: this primitive owns no per-node runtime.
|
|
57
|
+
// Written out rather than shared with a `noop` so the emptiness reads as a decision.
|
|
58
|
+
function attach(_node) {
|
|
59
|
+
// nothing to set up
|
|
60
|
+
}
|
|
61
|
+
function detach(_node) {
|
|
62
|
+
// nothing to release
|
|
63
|
+
}
|
|
64
|
+
export function registerInputAccessoryViewBehavior() {
|
|
65
|
+
registerHostBehavior(INPUT_ACCESSORY_VIEW_TAG, {
|
|
66
|
+
attach,
|
|
67
|
+
detach,
|
|
68
|
+
foldPayload: foldInputAccessoryViewPayload,
|
|
69
|
+
});
|
|
70
|
+
}
|