@symbiote-native/components 1.0.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 +3 -4
- 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 +1 -1
- package/build/behaviors/image.js +2 -2
- package/build/behaviors/input-accessory-view.d.ts +1 -1
- package/build/behaviors/input-accessory-view.js +2 -2
- package/build/behaviors/pressable.d.ts +59 -1
- package/build/behaviors/pressable.js +93 -18
- 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 +1 -1
- package/build/behaviors/switch.js +9 -5
- package/build/behaviors/text-input.d.ts +2 -2
- package/build/behaviors/text-input.js +43 -15
- 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 +29 -19
- package/build/component-names/index.ios.js +31 -19
- 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/fold-host-bag.js +2 -2
- package/build/index.d.ts +17 -11
- package/build/index.js +44 -8
- package/build/register.d.ts +1 -0
- package/build/register.js +55 -0
- package/build/scroll-view-commands.d.ts +4 -0
- package/build/scroll-view-commands.js +30 -31
- package/build/state/text-input.d.ts +4 -1
- package/build/view/render-button.d.ts +36 -1
- package/build/view/render-button.js +101 -12
- package/build/view/render-image/index.js +2 -2
- package/build/view/render-input-accessory-view.js +1 -1
- 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 +2 -2
- package/build/view/render-text-input.js +2 -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 +202 -150
- package/host-primitives.d.cts +0 -2
- package/package.json +8 -17
- 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/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
|
@@ -24,8 +24,8 @@ import { eventCountFromChange, foldText, INITIAL_EVENT_COUNT, SELECTION_NONE, re
|
|
|
24
24
|
// Both spellings, because `multiline` picks between two Fabric views and a lowering transform
|
|
25
25
|
// resolves that statically. The behavior is registered for both so it does not care which one the
|
|
26
26
|
// transform emitted.
|
|
27
|
-
export const TEXT_INPUT_TAG = '
|
|
28
|
-
export const TEXT_INPUT_MULTILINE_TAG = '
|
|
27
|
+
export const TEXT_INPUT_TAG = 'text-input';
|
|
28
|
+
export const TEXT_INPUT_MULTILINE_TAG = 'text-input-multiline';
|
|
29
29
|
const states = new WeakMap();
|
|
30
30
|
function stateOf(node) {
|
|
31
31
|
return states.get(node);
|
|
@@ -53,7 +53,7 @@ function callAppListener(node, name, event) {
|
|
|
53
53
|
if (typeof listener === 'function')
|
|
54
54
|
listener(event);
|
|
55
55
|
}
|
|
56
|
-
// `onValueChange(
|
|
56
|
+
// `onValueChange(event)` is NOT a Fabric event — it is a fold the component wrapper used to do
|
|
57
57
|
// over the raw `change` payload, so it lives in `node.props` as a plain function key and
|
|
58
58
|
// `fabricProps` drops it on the way to native. A lowered element has no wrapper to run that fold, so
|
|
59
59
|
// before this the callback was simply never called: the field echoed keystrokes natively (native
|
|
@@ -64,10 +64,17 @@ function callAppListener(node, name, event) {
|
|
|
64
64
|
// fork, where all five adapters inherit it. Refusing to lower an element carrying the prop was the
|
|
65
65
|
// other candidate and is strictly worse — it makes the optimisation opt out of the idiom the
|
|
66
66
|
// ecosystem actually writes, to avoid a fold the runtime can do in three lines.
|
|
67
|
+
//
|
|
68
|
+
// The listener takes ONE argument, `text` carried on the event itself (`ITextInputChangeEvent`),
|
|
69
|
+
// not `(text, event)` — Svelte's compiler forces every individual `on*` attribute through a native
|
|
70
|
+
// listener wrapper that calls with exactly one argument, always a real object, so a second
|
|
71
|
+
// argument is silently dropped and a bare string as the sole argument crashes.
|
|
67
72
|
function callValueChange(node, text, event) {
|
|
68
73
|
const listener = node.props.onValueChange;
|
|
69
|
-
if (typeof listener
|
|
70
|
-
|
|
74
|
+
if (typeof listener !== 'function')
|
|
75
|
+
return;
|
|
76
|
+
const changeEvent = Object.assign(event, { text });
|
|
77
|
+
listener(changeEvent);
|
|
71
78
|
}
|
|
72
79
|
// The W3C/legacy alias fold the WRAPPER runs in its component body — `inputMode` -> `keyboardType`,
|
|
73
80
|
// `enterKeyHint` -> `returnKeyType`, `readOnly` -> inverted `editable`, `blurOnSubmit` ->
|
|
@@ -91,7 +98,29 @@ function stringOf(value) {
|
|
|
91
98
|
function booleanOf(value) {
|
|
92
99
|
return typeof value === 'boolean' ? value : undefined;
|
|
93
100
|
}
|
|
94
|
-
|
|
101
|
+
// `multiline` picks between TWO Fabric views, so the TAG decides it and no later prop write moves a
|
|
102
|
+
// node between them. Two of the three paths that build the node resolve it before the engine ever
|
|
103
|
+
// sees the prop — the wrapper CONSUMES it to pick its intrinsic, a lowering transform reads a
|
|
104
|
+
// literal at compile time — and the third, an author writing the tag by hand, has neither. That
|
|
105
|
+
// leaves two silent, device-only divergences, measured on the committed payload:
|
|
106
|
+
//
|
|
107
|
+
// <text-input-multiline value="a" /> RCTMultilineTextInputView, folded as SINGLE-line:
|
|
108
|
+
// submitBehavior 'blurAndSubmit', so Return blurs instead
|
|
109
|
+
// of inserting a newline
|
|
110
|
+
// <text-input multiline value="b" /> RCTSinglelineTextInputView carrying the multiline fold
|
|
111
|
+
//
|
|
112
|
+
// So the tag is the authority here, and a prop that contradicts it throws rather than being
|
|
113
|
+
// quietly overridden — an ignored `multiline` is a wrong native view with nothing to read.
|
|
114
|
+
// Found on Solid, fixed here because all five adapters produce the same two divergences: a
|
|
115
|
+
// decision the wrapper used to make by CONSUMING a prop has no owner once the author writes the
|
|
116
|
+
// tag directly.
|
|
117
|
+
// The COMPLAINT cannot live here, only the correction. `foldPayload` runs inside the commit, so a
|
|
118
|
+
// throw from it surfaces as an uncaught exception a tick after the author's write with no frame
|
|
119
|
+
// naming the call site — measured: a test awaiting the mount sees `nothing committed` instead of
|
|
120
|
+
// the error. Refusing a contradicting prop therefore stays in each adapter's own prop-write path,
|
|
121
|
+
// where the author's stack still exists (Solid's `renderer.ts` is the reference); this file only
|
|
122
|
+
// guarantees that whatever the props say, the payload matches the TAG.
|
|
123
|
+
function foldPayload(props, isMultilineTag) {
|
|
95
124
|
const folded = resolveTextInputProps({
|
|
96
125
|
inputMode: stringOf(props.inputMode),
|
|
97
126
|
keyboardType: stringOf(props.keyboardType),
|
|
@@ -101,10 +130,7 @@ function foldPayload(props) {
|
|
|
101
130
|
editable: booleanOf(props.editable),
|
|
102
131
|
submitBehavior: stringOf(props.submitBehavior),
|
|
103
132
|
blurOnSubmit: booleanOf(props.blurOnSubmit),
|
|
104
|
-
|
|
105
|
-
// with one object, so the prop is the only thing it can read. Absent reads as single-line,
|
|
106
|
-
// which is the tag the transform emits when `multiline` is absent — the two agree.
|
|
107
|
-
multiline: props.multiline === true,
|
|
133
|
+
multiline: isMultilineTag,
|
|
108
134
|
cursorColor: stringOf(props.cursorColor),
|
|
109
135
|
selectionColor: stringOf(props.selectionColor),
|
|
110
136
|
selectionHandleColor: stringOf(props.selectionHandleColor),
|
|
@@ -276,16 +302,18 @@ export function buildTextInputHandle(node) {
|
|
|
276
302
|
// Idempotent: an adapter entry may be imported more than once in a bundle, and re-registering the
|
|
277
303
|
// same tag with an equivalent behavior must not double-install anything.
|
|
278
304
|
export function registerTextInputBehavior() {
|
|
279
|
-
const
|
|
305
|
+
const behaviorFor = (isMultilineTag) => ({
|
|
280
306
|
attach,
|
|
281
307
|
attachAfterCommit,
|
|
282
308
|
afterCommit,
|
|
283
309
|
detach,
|
|
284
|
-
|
|
310
|
+
// The one thing the two registrations do NOT share: the tag is what answers `multiline`, so
|
|
311
|
+
// each closes over its own answer. Everything else is the same machine.
|
|
312
|
+
foldPayload: (props) => foldPayload(props, isMultilineTag),
|
|
285
313
|
// The three the machine needs as INPUTS. Without the stash the app's own `onChange` would
|
|
286
314
|
// evict the machine from the very event the controlled handshake runs on.
|
|
287
315
|
ownedListeners: ['change', 'focus', 'blur'],
|
|
288
|
-
};
|
|
289
|
-
registerHostBehavior(TEXT_INPUT_TAG,
|
|
290
|
-
registerHostBehavior(TEXT_INPUT_MULTILINE_TAG,
|
|
316
|
+
});
|
|
317
|
+
registerHostBehavior(TEXT_INPUT_TAG, behaviorFor(false));
|
|
318
|
+
registerHostBehavior(TEXT_INPUT_MULTILINE_TAG, behaviorFor(true));
|
|
291
319
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type IHostBehavior } from '@symbiote-native/engine';
|
|
2
|
+
import { type IDisabledResolver } from './pressable';
|
|
3
|
+
export declare const TOUCHABLE_HIGHLIGHT_TAG = "touchable-highlight";
|
|
4
|
+
/**
|
|
5
|
+
* The behavior as PARTS, mirroring `./touchable-opacity`'s shape — a tag that is a
|
|
6
|
+
* TouchableHighlight plus something composes this instead of re-implementing the underlay.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createTouchableHighlightBehavior(disabledOf?: IDisabledResolver): IHostBehavior;
|
|
9
|
+
export declare function registerTouchableHighlightBehavior(): void;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// TouchableHighlight as an ENGINE-NODE behavior, so it can be an intrinsic tag instead of a
|
|
2
|
+
// framework component (`.claude/rules/host-primitive-tier.md`, tier 2).
|
|
3
|
+
//
|
|
4
|
+
// ONE NODE, THE SAME SIMPLIFICATION EVERY WRAPPER ALREADY SHIPS. RN's own TouchableHighlight
|
|
5
|
+
// renders a container View (the responder, the underlay backgroundColor, the whole accessibility
|
|
6
|
+
// fold) and CLONES an extra opacity style onto its single child (TouchableHighlight.js:281-320,
|
|
7
|
+
// `_createExtraStyles` + `cloneElement`) — closer to TouchableNativeFeedback's clone-onto-child
|
|
8
|
+
// shape than to TouchableOpacity's true single node. Every wrapper (Svelte's own comment: "ITEM 7
|
|
9
|
+
// IS DELIBERATELY NOT FIXED HERE... exactly as Solid and Angular decided") folds BOTH the underlay
|
|
10
|
+
// and the child opacity onto the ONE node instead, because splitting them needs a child to target
|
|
11
|
+
// and a framework component holding an opaque children snippet/slot cannot reach one safely. This
|
|
12
|
+
// port keeps that already-shipped, already-cross-adapter simplification rather than reopening it —
|
|
13
|
+
// `render-touchable-highlight.ts`'s own header says the shared layer "takes no position on where
|
|
14
|
+
// they land", so this is a legitimate placement choice, not a new shortcut.
|
|
15
|
+
//
|
|
16
|
+
// WHAT IS SHARED AND WHAT IS NEW. The underlay show/hide state machine
|
|
17
|
+
// (createHighlightUnderlayHandlers/createHighlightUnderlayRuntime, `../state/touchable`) is already
|
|
18
|
+
// framework-agnostic — every wrapper already calls it. New here is only WHERE `shown` lives (a
|
|
19
|
+
// WeakMap keyed by node, the same shape `./touchable-opacity`'s `states` map uses) and how a flip
|
|
20
|
+
// reaches Fabric: `markPropsDirty` + `requestCommitFor`, the same primitives `./pressable`'s
|
|
21
|
+
// `setPressed` composer uses to re-fold after an internal state change — NOT
|
|
22
|
+
// `setAnimatedBehaviorStyle`, which exists for a continuously-driven `AnimatedValue` and would be
|
|
23
|
+
// the wrong tool for a discrete boolean swap with no easing.
|
|
24
|
+
//
|
|
25
|
+
// The press machine is `./pressable`'s, composed through `createPressBehavior` — one node may hold
|
|
26
|
+
// exactly one press machine, so this tag must not also register the plain `pressable` behavior.
|
|
27
|
+
//
|
|
28
|
+
// REGISTRATION IS THE HAZARD, not the machine — see `./pressable` for why each adapter's entry does
|
|
29
|
+
// a bare `import './register';` that the barrel does not re-export.
|
|
30
|
+
import { appListenerFor, markPropsDirty, registerHostBehavior, requestCommitFor, } from '@symbiote-native/engine';
|
|
31
|
+
import { resolveTouchableFocusable } from '../view/render-pressable/index.js';
|
|
32
|
+
import { accessibleUnlessOptedOut, booleanOr, createPressBehavior, } from './pressable.js';
|
|
33
|
+
import { createHighlightUnderlayHandlers, createHighlightUnderlayRuntime, hasTouchablePressHandler, } from '../state/touchable.js';
|
|
34
|
+
import { resolveHighlightExtraStyles } from '../view/render-touchable-highlight.js';
|
|
35
|
+
export const TOUCHABLE_HIGHLIGHT_TAG = 'touchable-highlight';
|
|
36
|
+
const states = new WeakMap();
|
|
37
|
+
function numberOr(value, fallback) {
|
|
38
|
+
return typeof value === 'number' ? value : fallback;
|
|
39
|
+
}
|
|
40
|
+
function stringOr(value, fallback) {
|
|
41
|
+
return typeof value === 'string' ? value : fallback;
|
|
42
|
+
}
|
|
43
|
+
// Runs once per GESTURE, matching `./touchable-opacity`'s `refine` — the config it reads is
|
|
44
|
+
// whatever the props hold when a finger lands, and the underlay handlers it builds read `shown`
|
|
45
|
+
// through the closure below rather than owning it, so a re-arm reads the current props.
|
|
46
|
+
const refine = (node, config) => {
|
|
47
|
+
const state = states.get(node);
|
|
48
|
+
if (state === undefined)
|
|
49
|
+
return config;
|
|
50
|
+
const hasPressHandler = hasTouchablePressHandler({
|
|
51
|
+
onPress: config.onPress,
|
|
52
|
+
onPressIn: config.onPressIn,
|
|
53
|
+
onPressOut: config.onPressOut,
|
|
54
|
+
onLongPress: config.onLongPress,
|
|
55
|
+
});
|
|
56
|
+
const underlay = createHighlightUnderlayHandlers({
|
|
57
|
+
delayPressOut: numberOr(node.props.delayPressOut, 0),
|
|
58
|
+
hasPressHandler,
|
|
59
|
+
schedule: (callback, ms) => {
|
|
60
|
+
const id = setTimeout(() => {
|
|
61
|
+
state.timers.delete(id);
|
|
62
|
+
callback();
|
|
63
|
+
}, ms);
|
|
64
|
+
state.timers.add(id);
|
|
65
|
+
return () => {
|
|
66
|
+
clearTimeout(id);
|
|
67
|
+
state.timers.delete(id);
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
}, state.runtime, {
|
|
71
|
+
setShown: (shown) => {
|
|
72
|
+
if (state.shown === shown)
|
|
73
|
+
return;
|
|
74
|
+
state.shown = shown;
|
|
75
|
+
markPropsDirty(node);
|
|
76
|
+
requestCommitFor(node);
|
|
77
|
+
},
|
|
78
|
+
onShowUnderlay: () => {
|
|
79
|
+
const onShowUnderlay = node.props.onShowUnderlay;
|
|
80
|
+
if (typeof onShowUnderlay === 'function')
|
|
81
|
+
onShowUnderlay();
|
|
82
|
+
},
|
|
83
|
+
onHideUnderlay: () => {
|
|
84
|
+
const onHideUnderlay = node.props.onHideUnderlay;
|
|
85
|
+
if (typeof onHideUnderlay === 'function')
|
|
86
|
+
onHideUnderlay();
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
return {
|
|
90
|
+
...config,
|
|
91
|
+
// TouchableHighlight.js:203, verbatim: RN hands Pressability a 0 floor here too — what holds
|
|
92
|
+
// the underlay visible is the hold TIMER above, not a press-duration floor.
|
|
93
|
+
minPressDuration: 0,
|
|
94
|
+
onPress: (event) => {
|
|
95
|
+
underlay.handlePress(event);
|
|
96
|
+
config.onPress?.(event);
|
|
97
|
+
},
|
|
98
|
+
onPressIn: (event) => {
|
|
99
|
+
underlay.handlePressIn(event);
|
|
100
|
+
config.onPressIn?.(event);
|
|
101
|
+
},
|
|
102
|
+
onPressOut: (event) => {
|
|
103
|
+
underlay.handlePressOut(event);
|
|
104
|
+
config.onPressOut?.(event);
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
// `id -> nativeID`, the fold every un-lowered wrapper's `foldHostBag` already applies. Not read off
|
|
109
|
+
// `HOST_PRIMITIVES`, matching `./touchable-opacity`'s own inline check — that spec entry is
|
|
110
|
+
// deliberately withheld until this primitive's wrapper collapses to one node everywhere.
|
|
111
|
+
const foldPayload = props => {
|
|
112
|
+
const next = { ...props };
|
|
113
|
+
if (Object.hasOwn(next, 'id')) {
|
|
114
|
+
next.nativeID = next.id;
|
|
115
|
+
delete next.id;
|
|
116
|
+
}
|
|
117
|
+
next.accessible = accessibleUnlessOptedOut(props);
|
|
118
|
+
return next;
|
|
119
|
+
};
|
|
120
|
+
function tagFold(node) {
|
|
121
|
+
return props => {
|
|
122
|
+
const next = foldPayload(props);
|
|
123
|
+
const state = states.get(node);
|
|
124
|
+
const hasPressHandler = appListenerFor(node, 'press') !== undefined ||
|
|
125
|
+
appListenerFor(node, 'pressIn') !== undefined ||
|
|
126
|
+
appListenerFor(node, 'pressOut') !== undefined ||
|
|
127
|
+
appListenerFor(node, 'longPress') !== undefined;
|
|
128
|
+
const extra = state === undefined
|
|
129
|
+
? undefined
|
|
130
|
+
: resolveHighlightExtraStyles({
|
|
131
|
+
shown: state.shown,
|
|
132
|
+
hasPressHandler,
|
|
133
|
+
underlayColor: stringOr(props.underlayColor, undefined),
|
|
134
|
+
activeOpacity: numberOr(props.activeOpacity, NaN) || undefined,
|
|
135
|
+
});
|
|
136
|
+
if (extra !== undefined) {
|
|
137
|
+
next.style = [props.style, extra.underlay, extra.child];
|
|
138
|
+
}
|
|
139
|
+
next.focusable = resolveTouchableFocusable(booleanOr(props.focusable), appListenerFor(node, 'press') !== undefined, booleanOr(props.disabled));
|
|
140
|
+
return next;
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// A listener flip changes no payload by itself, so the commit after it is a no-op and no fold
|
|
144
|
+
// re-runs (`IHostBehavior.onOwnedListenerChange`) — same reason `./touchable-opacity` carries this.
|
|
145
|
+
function onOwnedListenerChange(node, name) {
|
|
146
|
+
if (name !== 'press')
|
|
147
|
+
return;
|
|
148
|
+
markPropsDirty(node);
|
|
149
|
+
requestCommitFor(node);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The behavior as PARTS, mirroring `./touchable-opacity`'s shape — a tag that is a
|
|
153
|
+
* TouchableHighlight plus something composes this instead of re-implementing the underlay.
|
|
154
|
+
*/
|
|
155
|
+
export function createTouchableHighlightBehavior(disabledOf) {
|
|
156
|
+
const machine = createPressBehavior(refine, disabledOf);
|
|
157
|
+
return {
|
|
158
|
+
...machine,
|
|
159
|
+
foldPayload,
|
|
160
|
+
attach(node) {
|
|
161
|
+
states.set(node, {
|
|
162
|
+
shown: false,
|
|
163
|
+
runtime: createHighlightUnderlayRuntime(),
|
|
164
|
+
timers: new Set(),
|
|
165
|
+
});
|
|
166
|
+
machine.attach(node);
|
|
167
|
+
},
|
|
168
|
+
detach(node) {
|
|
169
|
+
machine.detach(node);
|
|
170
|
+
const state = states.get(node);
|
|
171
|
+
if (state === undefined)
|
|
172
|
+
return;
|
|
173
|
+
for (const id of state.timers)
|
|
174
|
+
clearTimeout(id);
|
|
175
|
+
state.timers.clear();
|
|
176
|
+
states.delete(node);
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// Idempotent: an adapter entry may be imported more than once in a bundle.
|
|
181
|
+
export function registerTouchableHighlightBehavior() {
|
|
182
|
+
const touchable = createTouchableHighlightBehavior();
|
|
183
|
+
registerHostBehavior(TOUCHABLE_HIGHLIGHT_TAG, {
|
|
184
|
+
...touchable,
|
|
185
|
+
// Only the TAG binds a per-node fold — see `./touchable-opacity`'s identical comment.
|
|
186
|
+
attach(node) {
|
|
187
|
+
touchable.attach(node);
|
|
188
|
+
node.payloadFold = tagFold(node);
|
|
189
|
+
},
|
|
190
|
+
onOwnedListenerChange,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type IPressConfigRefinement } from './pressable';
|
|
2
|
+
export declare const TOUCHABLE_NATIVE_FEEDBACK_TAG = "touchable-native-feedback";
|
|
3
|
+
/**
|
|
4
|
+
* TNF's own Pressability config, applied to whatever node carries the responder.
|
|
5
|
+
*
|
|
6
|
+
* SHARED WITH `./button`, which is what this file's arrival makes possible. Button's Android arm
|
|
7
|
+
* held a private copy with a comment saying a future `touchable-native-feedback` tag could not
|
|
8
|
+
* reuse it "while the wrappers wrap" — the tag is here now and the responder node is a parameter
|
|
9
|
+
* either way, so the copy is gone.
|
|
10
|
+
*
|
|
11
|
+
* `minPressDuration: 0` is UNCONDITIONAL in RN (:226), not Android-gated: TNF has no feedback
|
|
12
|
+
* machine of its own to hold, and the press machine's 130 ms deactivation floor would only defer
|
|
13
|
+
* `onPressOut` — which on Android leaves `setPressed(false)` ~130 ms late and the ripple lit after
|
|
14
|
+
* the finger is gone. The two view commands ARE Android-gated (:257,:276): the ripple is a drawable
|
|
15
|
+
* Android animates off the view's pressed state, and the JS responder consumes the touch before
|
|
16
|
+
* Android's own pressed handling ever runs, so without them the background is installed and never
|
|
17
|
+
* moves.
|
|
18
|
+
*/
|
|
19
|
+
export declare const nativeFeedbackRefinement: IPressConfigRefinement;
|
|
20
|
+
export declare function registerTouchableNativeFeedbackBehavior(): void;
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// TouchableNativeFeedback as an ENGINE-NODE behavior — the one primitive that must commit NO NODE
|
|
2
|
+
// OF ITS OWN (`.claude/rules/host-primitive-tier.md`, tier 2).
|
|
3
|
+
//
|
|
4
|
+
// RN'S TNF RENDERS NOTHING. `render()` takes `React.Children.only(this.props.children)`
|
|
5
|
+
// (TouchableNativeFeedback.js:289) and returns `cloneElement(element, {…}, ...children)` (:339) —
|
|
6
|
+
// the child IS the responder, carries the ripple background, and takes the whole accessibility
|
|
7
|
+
// fold. All five of our adapters render a `Pressable` around a feedback `View` instead, so every
|
|
8
|
+
// TNF in every app commits a node RN does not, and the mistake has already spread: our Pressable
|
|
9
|
+
// wrapper and our TouchableOpacity wrapper both copied that inner view from here
|
|
10
|
+
// (`.claude/rules/adapter-parity-audit.md`, "the OTHER half turned up a gap on BOTH paths").
|
|
11
|
+
//
|
|
12
|
+
// WHY THE FIX CANNOT BE PER ADAPTER. At the FRAMEWORK level React, Vue and Solid can clone a child
|
|
13
|
+
// element; Svelte's children arrive as a snippet and Angular's through `<ng-content>`, and neither
|
|
14
|
+
// can have props written onto them there. At the ENGINE level all five can reach the child node.
|
|
15
|
+
// So the clone belongs below the frameworks, once, or it is five copies of one fold.
|
|
16
|
+
//
|
|
17
|
+
// ---------------------------------------------------------------------------------------------
|
|
18
|
+
// THE SHAPE, and which existing engine seam does each half:
|
|
19
|
+
//
|
|
20
|
+
// commits nothing the tag resolves to ANCHOR_COMPONENT (component-names), so the node is
|
|
21
|
+
// BORN skipped: `renderableChildren` (commit.ts) drops it and flattens its
|
|
22
|
+
// child into the grandparent's renderable list. Not a new concept — that is
|
|
23
|
+
// precisely what an anchor is, and TNF is its third instance after Vue's
|
|
24
|
+
// fragment placeholders and Angular's non-painting component hosts.
|
|
25
|
+
// finds its child `onChildInserted`, the one hook this needed. `buildStructure` cannot do it:
|
|
26
|
+
// it runs at `attach`, inside `createElement`, and the child is the
|
|
27
|
+
// FRAMEWORK'S and does not exist yet.
|
|
28
|
+
// clones the props a `payloadFold` on the CHILD reading the owner's bag, re-run because
|
|
29
|
+
// `slotDerived` marks the child's props dirty on every owner write. The
|
|
30
|
+
// alternative — redirecting each prop onto the child through `slotPropsExcept`
|
|
31
|
+
// — was rejected: adapters disagree about ORDER (React writes props before
|
|
32
|
+
// children, Vue mounts children before props), so a redirect silently drops
|
|
33
|
+
// every prop written before the child arrives, on some adapters only.
|
|
34
|
+
// presses the press machine on the CHILD, reading the OWNER (`attachPressMachine`'s
|
|
35
|
+
// `source`). It cannot live on the owner: `bubble` (events/index.ts) skips
|
|
36
|
+
// anchors for listener lookup, and `handOverNativeResponder` has no Fabric
|
|
37
|
+
// handle to hand native for an uncommitted node. So a responder on this tag
|
|
38
|
+
// could never fire — checked before the design, not after.
|
|
39
|
+
// ---------------------------------------------------------------------------------------------
|
|
40
|
+
// KNOWN LIMITS, stated rather than left to be found:
|
|
41
|
+
//
|
|
42
|
+
// 1. EXACTLY ONE CHILD, as RN enforces with `React.Children.only`. A second child lands INSIDE the
|
|
43
|
+
// first (the ordinary `childHost` redirect) rather than throwing. Nothing in RN's surface
|
|
44
|
+
// produces that shape.
|
|
45
|
+
// 2. A child REMOVED from a still-mounted TNF leaves its press machine armed until the TNF itself
|
|
46
|
+
// unmounts (`detach` releases whatever `childHost` then holds). Unreachable through the
|
|
47
|
+
// frameworks we drive: Solid's remove-then-reinsert move and Svelte's parking both move the TNF,
|
|
48
|
+
// never its single child, and there is no sibling to reorder it against.
|
|
49
|
+
// 3. `style` is NOT cloned, matching RN — its clone list (:342-390) is closed, and a `style` on a
|
|
50
|
+
// TNF stays on a node that never commits. RN's TNF declares no style prop either.
|
|
51
|
+
//
|
|
52
|
+
// REGISTRATION IS THE HAZARD, not the machine — see `./pressable` for why each adapter entry does a
|
|
53
|
+
// bare `import './register';` that the barrel does not re-export. Registered by ALL FIVE adapters
|
|
54
|
+
// since 2026-09-09, in the same commit that deleted the five wrappers, which is what makes it safe:
|
|
55
|
+
// while a wrapper still built its own Pressable + feedback View, registering would have left every
|
|
56
|
+
// TouchableNativeFeedback with two responders. There is no `-managed` twin and none is possible —
|
|
57
|
+
// this tag commits no node, so a second spelling would have nothing to name.
|
|
58
|
+
import { ARIA_ALIAS_KEYS, appListenerFor, dispatchViewCommand, foldAriaProps, markPropsDirty, Platform, registerHostBehavior, requestCommitFor, setBehaviorListener, } from '@symbiote-native/engine';
|
|
59
|
+
import { resolveDisabledAccessibilityState, resolveTouchableFocusable, } from '../view/render-pressable/index.js';
|
|
60
|
+
import { backgroundProps, selectableBackground, } from '../view/render-touchable-native-feedback.js';
|
|
61
|
+
import { accessibleUnlessOptedOut, asAccessibilityState, attachPressMachine, booleanOr, detachPressMachine, } from './pressable.js';
|
|
62
|
+
export const TOUCHABLE_NATIVE_FEEDBACK_TAG = 'touchable-native-feedback';
|
|
63
|
+
// Read once, like `./button`'s: the platform cannot change under a running app.
|
|
64
|
+
const IS_ANDROID = Platform.OS === 'android';
|
|
65
|
+
// TouchableNativeFeedback.js:349-390, verbatim and in RN's own order. A CLOSED list, not a
|
|
66
|
+
// passthrough: RN clones exactly these and nothing else, so a prop it does not name stays behind.
|
|
67
|
+
// `accessibilityState`, `accessible`, `focusable` and `nativeID` are cloned too and are computed
|
|
68
|
+
// below; `onLayout` and `onAccessibilityAction` are cloned as LISTENERS and forwarded by
|
|
69
|
+
// `FORWARDED_LISTENERS`.
|
|
70
|
+
const CLONED_PROPS = [
|
|
71
|
+
'accessibilityHint',
|
|
72
|
+
'accessibilityLanguage',
|
|
73
|
+
'accessibilityLabel',
|
|
74
|
+
'accessibilityRole',
|
|
75
|
+
'accessibilityActions',
|
|
76
|
+
'accessibilityValue',
|
|
77
|
+
'importantForAccessibility',
|
|
78
|
+
'accessibilityViewIsModal',
|
|
79
|
+
'accessibilityLiveRegion',
|
|
80
|
+
'accessibilityElementsHidden',
|
|
81
|
+
'hasTVPreferredFocus',
|
|
82
|
+
'hitSlop',
|
|
83
|
+
'nextFocusDown',
|
|
84
|
+
'nextFocusForward',
|
|
85
|
+
'nextFocusLeft',
|
|
86
|
+
'nextFocusRight',
|
|
87
|
+
'nextFocusUp',
|
|
88
|
+
'testID',
|
|
89
|
+
];
|
|
90
|
+
// Owner props the cloned payload is COMPUTED from, on top of the verbatim list above. `focusable`
|
|
91
|
+
// also derives from the `press` LISTENER, which is not a prop and is handled by
|
|
92
|
+
// `onOwnedListenerChange` — a listener flip dirties no payload by itself.
|
|
93
|
+
const DERIVED_FROM = [
|
|
94
|
+
'background',
|
|
95
|
+
'useForeground',
|
|
96
|
+
'accessible',
|
|
97
|
+
'accessibilityState',
|
|
98
|
+
'disabled',
|
|
99
|
+
'focusable',
|
|
100
|
+
'id',
|
|
101
|
+
'nativeID',
|
|
102
|
+
];
|
|
103
|
+
// Every owner name the child's payload reads. Without it the clone is correct at mount and frozen
|
|
104
|
+
// forever after: `markPropsDirty` bubbles UP, so an owner write never reaches the child on its own.
|
|
105
|
+
// The aria half comes off the engine's own list rather than a second copy of it.
|
|
106
|
+
const SLOT_DERIVED = [
|
|
107
|
+
...CLONED_PROPS,
|
|
108
|
+
...DERIVED_FROM,
|
|
109
|
+
...ARIA_ALIAS_KEYS,
|
|
110
|
+
];
|
|
111
|
+
// The two RN clones as EVENTS rather than props (:386-387). Owned, so the app's callback stashes on
|
|
112
|
+
// the owner and a trampoline installed on the child reads it at dispatch time — which keeps a fresh
|
|
113
|
+
// closure per render free, exactly as `ownedListeners` intends.
|
|
114
|
+
//
|
|
115
|
+
// Both are Fabric BOOLEAN-GATED events (`.claude/rules/fabric-boolean-event-gates.md`), so the
|
|
116
|
+
// trampoline goes in only while the app has one wired: `setBehaviorListener` writes and clears the
|
|
117
|
+
// flag with it, and installing eagerly would light the gate on every TNF child in the tree.
|
|
118
|
+
const FORWARDED_LISTENERS = [
|
|
119
|
+
'layout',
|
|
120
|
+
'accessibilityAction',
|
|
121
|
+
];
|
|
122
|
+
const PRESS_LISTENERS = [
|
|
123
|
+
'press',
|
|
124
|
+
'pressIn',
|
|
125
|
+
'pressOut',
|
|
126
|
+
'pressMove',
|
|
127
|
+
'longPress',
|
|
128
|
+
'startShouldSetResponder',
|
|
129
|
+
'responderMove',
|
|
130
|
+
'responderTerminationRequest',
|
|
131
|
+
];
|
|
132
|
+
function stringOr(value) {
|
|
133
|
+
return typeof value === 'string' ? value : undefined;
|
|
134
|
+
}
|
|
135
|
+
// The background dicts our own factories produce, narrowed on the discriminant rather than cast.
|
|
136
|
+
// Anything else reads as "not supplied", which is RN's `background === undefined` branch.
|
|
137
|
+
function asFeedbackBackground(value) {
|
|
138
|
+
if (typeof value !== 'object' || value === null)
|
|
139
|
+
return undefined;
|
|
140
|
+
const type = Reflect.get(value, 'type');
|
|
141
|
+
if (type !== 'ThemeAttrAndroid' && type !== 'RippleAndroid')
|
|
142
|
+
return undefined;
|
|
143
|
+
const attribute = Reflect.get(value, 'attribute');
|
|
144
|
+
const rippleRadius = Reflect.get(value, 'rippleRadius');
|
|
145
|
+
const radius = typeof rippleRadius === 'number' ? rippleRadius : undefined;
|
|
146
|
+
if (type === 'ThemeAttrAndroid') {
|
|
147
|
+
return attribute === 'selectableItemBackgroundBorderless'
|
|
148
|
+
? { type, attribute, rippleRadius: radius }
|
|
149
|
+
: { type, attribute: 'selectableItemBackground', rippleRadius: radius };
|
|
150
|
+
}
|
|
151
|
+
const color = Reflect.get(value, 'color');
|
|
152
|
+
return {
|
|
153
|
+
type,
|
|
154
|
+
color: typeof color === 'string' ? color : null,
|
|
155
|
+
borderless: Reflect.get(value, 'borderless') === true,
|
|
156
|
+
rippleRadius: radius,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// TouchableNativeFeedback.js:280 — `locationX ?? 0`. The bag is raw Fabric payload, so guard.
|
|
160
|
+
function hotspotAt(nativeEvent, key) {
|
|
161
|
+
const value = nativeEvent[key];
|
|
162
|
+
return typeof value === 'number' ? value : 0;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* TNF's own Pressability config, applied to whatever node carries the responder.
|
|
166
|
+
*
|
|
167
|
+
* SHARED WITH `./button`, which is what this file's arrival makes possible. Button's Android arm
|
|
168
|
+
* held a private copy with a comment saying a future `touchable-native-feedback` tag could not
|
|
169
|
+
* reuse it "while the wrappers wrap" — the tag is here now and the responder node is a parameter
|
|
170
|
+
* either way, so the copy is gone.
|
|
171
|
+
*
|
|
172
|
+
* `minPressDuration: 0` is UNCONDITIONAL in RN (:226), not Android-gated: TNF has no feedback
|
|
173
|
+
* machine of its own to hold, and the press machine's 130 ms deactivation floor would only defer
|
|
174
|
+
* `onPressOut` — which on Android leaves `setPressed(false)` ~130 ms late and the ripple lit after
|
|
175
|
+
* the finger is gone. The two view commands ARE Android-gated (:257,:276): the ripple is a drawable
|
|
176
|
+
* Android animates off the view's pressed state, and the JS responder consumes the touch before
|
|
177
|
+
* Android's own pressed handling ever runs, so without them the background is installed and never
|
|
178
|
+
* moves.
|
|
179
|
+
*/
|
|
180
|
+
export const nativeFeedbackRefinement = (node, config) => {
|
|
181
|
+
if (!IS_ANDROID)
|
|
182
|
+
return { ...config, minPressDuration: 0 };
|
|
183
|
+
const hotspot = (event) => {
|
|
184
|
+
dispatchViewCommand(node, 'hotspotUpdate', [
|
|
185
|
+
hotspotAt(event.nativeEvent, 'locationX'),
|
|
186
|
+
hotspotAt(event.nativeEvent, 'locationY'),
|
|
187
|
+
]);
|
|
188
|
+
};
|
|
189
|
+
return {
|
|
190
|
+
...config,
|
|
191
|
+
minPressDuration: 0,
|
|
192
|
+
onPressIn(event) {
|
|
193
|
+
// RN's order: hotspot first, so the ripple starts where the finger is rather than at centre.
|
|
194
|
+
hotspot(event);
|
|
195
|
+
dispatchViewCommand(node, 'setPressed', [true]);
|
|
196
|
+
config.onPressIn?.(event);
|
|
197
|
+
},
|
|
198
|
+
onPressMove(event) {
|
|
199
|
+
hotspot(event);
|
|
200
|
+
// RN drops the app's own onPressMove here; ours forwards it, because on a tag that callback
|
|
201
|
+
// is the app's and iOS already delivers it.
|
|
202
|
+
config.onPressMove?.(event);
|
|
203
|
+
},
|
|
204
|
+
onPressOut(event) {
|
|
205
|
+
dispatchViewCommand(node, 'setPressed', [false]);
|
|
206
|
+
config.onPressOut?.(event);
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* `cloneElement(element, {…})` as a payload fold: the child's own props first, the owner's clone
|
|
212
|
+
* list over them.
|
|
213
|
+
*
|
|
214
|
+
* OVERWRITING WITH `undefined` IS DELIBERATE and is RN's behaviour, not an accident of the spread.
|
|
215
|
+
* React's `cloneElement` assigns every key of its config, so a TNF with no `accessibilityLabel`
|
|
216
|
+
* clears the child's; `fabricProps` skips undefined values, which reproduces that exactly.
|
|
217
|
+
*
|
|
218
|
+
* Pure, as `IPayloadFold` requires — the owner is read, never written.
|
|
219
|
+
*/
|
|
220
|
+
function cloneFold(owner, inner) {
|
|
221
|
+
return (props) => {
|
|
222
|
+
const next = {
|
|
223
|
+
...(inner === undefined ? props : inner(props)),
|
|
224
|
+
};
|
|
225
|
+
// The owner's aria props are on the OWNER, so the engine's own fold at `fabricProps` — which
|
|
226
|
+
// reads the node being committed — never sees them. Run it here over the source bag instead of
|
|
227
|
+
// restating any of `:349-390`'s aria half; `hasAriaAlias` is the same sticky gate `fabricProps`
|
|
228
|
+
// uses, and the fold returns its input by identity when there is nothing to do.
|
|
229
|
+
const source = owner.hasAriaAlias
|
|
230
|
+
? foldAriaProps(owner.props)
|
|
231
|
+
: owner.props;
|
|
232
|
+
for (const key of CLONED_PROPS)
|
|
233
|
+
next[key] = source[key];
|
|
234
|
+
// :369-372. `onPress` is an OWNED name, so it lives in the stash and never in `props`.
|
|
235
|
+
const disabled = booleanOr(source.disabled);
|
|
236
|
+
next.accessible = accessibleUnlessOptedOut(source);
|
|
237
|
+
next.focusable = resolveTouchableFocusable(booleanOr(source.focusable), appListenerFor(owner, 'press') !== undefined, disabled);
|
|
238
|
+
// :373. RN gives `id` unconditional priority over `nativeID`.
|
|
239
|
+
next.nativeID = stringOr(source.id) ?? stringOr(source.nativeID);
|
|
240
|
+
// :318-323 folded by the engine above, then :324-330: an explicit `disabled` overrides the
|
|
241
|
+
// aria/accessibilityState answer. Same helper every wrapper calls, so both paths agree.
|
|
242
|
+
next.accessibilityState = resolveDisabledAccessibilityState(asAccessibilityState(source.accessibilityState), disabled);
|
|
243
|
+
// :343-348 + :402. `getBackgroundProp` returns null off Android, so nothing is spread there.
|
|
244
|
+
if (IS_ANDROID) {
|
|
245
|
+
Object.assign(next, backgroundProps(asFeedbackBackground(source.background) ?? selectableBackground(), source.useForeground === true));
|
|
246
|
+
}
|
|
247
|
+
return next;
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
// Not RN's own list: RN drops these by never cloning them, and a lowered tag has no clone to omit
|
|
251
|
+
// them from — they would ride into the child's payload as keys no ViewConfig declares. Same strip
|
|
252
|
+
// `./pressable`'s fold does for the machine-only half, applied to the owner's bag instead.
|
|
253
|
+
//
|
|
254
|
+
// (`background` / `useForeground` / `disabled` / `hitSlop` never reach the child's props at all —
|
|
255
|
+
// the fold only ever ADDS to the child's own bag — so nothing needs stripping here. Kept as a
|
|
256
|
+
// comment rather than an empty constant so the next reader does not go looking for the strip.)
|
|
257
|
+
// The app's callback, read at dispatch time so a fresh closure per render costs nothing.
|
|
258
|
+
function trampolineFor(owner, name) {
|
|
259
|
+
return (event) => {
|
|
260
|
+
const listener = appListenerFor(owner, name);
|
|
261
|
+
if (typeof listener === 'function')
|
|
262
|
+
listener(event);
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function forwardListener(owner, child, name, wired) {
|
|
266
|
+
setBehaviorListener(child, name, wired ? trampolineFor(owner, name) : undefined);
|
|
267
|
+
}
|
|
268
|
+
// The machine plus the forwarded listeners. Split from adoption because a parked subtree comes back
|
|
269
|
+
// with its child and its fold intact but its machine torn down — `attach` re-runs this, and must
|
|
270
|
+
// NOT re-chain the fold.
|
|
271
|
+
function arm(owner, child) {
|
|
272
|
+
attachPressMachine(child, {
|
|
273
|
+
source: owner,
|
|
274
|
+
refine: nativeFeedbackRefinement,
|
|
275
|
+
});
|
|
276
|
+
for (const name of FORWARDED_LISTENERS) {
|
|
277
|
+
forwardListener(owner, child, name, appListenerFor(owner, name) !== undefined);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function attach(node) {
|
|
281
|
+
// First attach has no child yet; a re-attach after the sweep does. See `arm`.
|
|
282
|
+
if (node.childHost !== undefined)
|
|
283
|
+
arm(node, node.childHost);
|
|
284
|
+
}
|
|
285
|
+
function detach(node) {
|
|
286
|
+
const child = node.childHost;
|
|
287
|
+
if (child !== undefined)
|
|
288
|
+
detachPressMachine(child);
|
|
289
|
+
}
|
|
290
|
+
function onChildInserted(node, child) {
|
|
291
|
+
const previous = node.childHost;
|
|
292
|
+
if (previous === child)
|
|
293
|
+
return;
|
|
294
|
+
// A replaced child: release the machine still armed on the one that left. `removeChild` clears
|
|
295
|
+
// `childHost` (node.ts), so this only runs when a framework inserts without removing first.
|
|
296
|
+
if (previous !== undefined)
|
|
297
|
+
detachPressMachine(previous);
|
|
298
|
+
node.childHost = child;
|
|
299
|
+
child.payloadFold = cloneFold(node, child.payloadFold);
|
|
300
|
+
// The owner's props were very likely written BEFORE this child existed (React and Solid set props
|
|
301
|
+
// at createInstance), so the fold owes a run even though nothing was written since.
|
|
302
|
+
markPropsDirty(child);
|
|
303
|
+
arm(node, child);
|
|
304
|
+
}
|
|
305
|
+
// `focusable` is a function of a LISTENER, and a listener flip changes no payload by itself — so
|
|
306
|
+
// the commit after it is a no-op and no fold re-runs (`IHostBehavior.onOwnedListenerChange`).
|
|
307
|
+
function onOwnedListenerChange(node, name, wired) {
|
|
308
|
+
const child = node.childHost;
|
|
309
|
+
if (child === undefined)
|
|
310
|
+
return;
|
|
311
|
+
if (FORWARDED_LISTENERS.includes(name)) {
|
|
312
|
+
forwardListener(node, child, name, wired);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (name !== 'press')
|
|
316
|
+
return;
|
|
317
|
+
markPropsDirty(child);
|
|
318
|
+
requestCommitFor(child);
|
|
319
|
+
}
|
|
320
|
+
// Idempotent: an adapter entry may be imported more than once in a bundle.
|
|
321
|
+
export function registerTouchableNativeFeedbackBehavior() {
|
|
322
|
+
const behavior = {
|
|
323
|
+
attach,
|
|
324
|
+
detach,
|
|
325
|
+
onChildInserted,
|
|
326
|
+
onOwnedListenerChange,
|
|
327
|
+
// Owned on the OWNER, which is where the app writes them, and read from there by the machine
|
|
328
|
+
// on the child (`attachPressMachine`'s `source`) and by the trampolines above.
|
|
329
|
+
ownedListeners: [...PRESS_LISTENERS, ...FORWARDED_LISTENERS],
|
|
330
|
+
slotDerived: SLOT_DERIVED,
|
|
331
|
+
};
|
|
332
|
+
registerHostBehavior(TOUCHABLE_NATIVE_FEEDBACK_TAG, behavior);
|
|
333
|
+
}
|