@ng-native/fabric 0.18.1 → 0.19.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/dist/engine.d.ts CHANGED
@@ -228,12 +228,8 @@ export interface EngineNode extends HostNode {
228
228
  nodeText: string;
229
229
  childList: EngineNode[];
230
230
  parent: EngineNode | null;
231
- /**
232
- * A child of this node that holds what is put in it: `appendChild` and `insertBefore` on this
233
- * node put the child there instead, so a component's template lands inside a view its owner
234
- * placed in the host element. Only the view itself goes in this node.
235
- */
236
- contentView?: EngineNode;
231
+ /** Set by `Engine.wrapChildren`: the view this node's children are committed in. */
232
+ wrapper?: Wrapper;
237
233
  listeners: Map<string, Set<(event: unknown) => void>> | null;
238
234
  /** CSS classes on this node, for selector matching. */
239
235
  classes: Set<string> | null;
@@ -311,11 +307,11 @@ export interface EngineNode extends HostNode {
311
307
  scrollsOverflow?: true;
312
308
  /** Set while this element is one: see `Engine.settleScrolling`. */
313
309
  scrollView?: true;
314
- /** The view a scrolling element's children are in, as it was last committed. */
315
- inside?: {
316
- handle: FabricNode;
317
- childHandles: FabricNode[];
318
- };
310
+ /**
311
+ * The view a scrolling element's children are in, or a wrapped one's (`wrapper`), as it was
312
+ * last committed: a wrapper's with the props it was committed with.
313
+ */
314
+ inside?: Inside;
319
315
  /** Set while `settleLine` has given an always-inline element a line for the boxes it holds. */
320
316
  lined?: true;
321
317
  /** Set while an always-inline element is a flex box or a grid by its own style. */
@@ -545,6 +541,21 @@ export declare function markPage(node: EngineNode): void;
545
541
  * and its `overflow` is layout alone.
546
542
  */
547
543
  export declare function scrollsOverflow(node: EngineNode): void;
544
+ /**
545
+ * A view the engine commits an element's children in, which is no node of the tree: see
546
+ * `Engine.wrapChildren`.
547
+ */
548
+ export interface Wrapper {
549
+ readonly viewName: string;
550
+ readonly props: Readonly<Record<string, unknown>>;
551
+ }
552
+ /** The view an element's children are committed in, as `inside` keeps it. */
553
+ interface Inside {
554
+ handle: FabricNode;
555
+ childHandles: FabricNode[];
556
+ /** A wrapper's props, as it was committed with them. */
557
+ props?: Record<string, unknown>;
558
+ }
548
559
  /**
549
560
  * Mark an element as the host of a component. The platform adapter calls this from
550
561
  * `RendererFactory2.createRenderer`, which Angular calls once for every component it mounts,
@@ -1172,12 +1183,30 @@ export declare class Engine implements HostEngine {
1172
1183
  * keyboard's top edge, for a field the keyboard would otherwise hide.
1173
1184
  *
1174
1185
  * Measured in the window, all of it, so the scroll view's offset needs no tracking: it is how
1175
- * far its content's top edge is above its own.
1186
+ * far its content's top edge is above its own. In a sheet on iOS the window is the sheet's: see
1187
+ * `sheetTop`.
1188
+ *
1189
+ * A scroll view under a navigation bar rests scrolled above zero, by the inset the bar adds, and
1190
+ * is not scrolled up past where it rests, nor past zero from below it: how far above zero it can
1191
+ * go is known only while it is there.
1176
1192
  */
1177
1193
  reveal(node: EngineNode, options?: {
1178
1194
  visibleBottom?: number;
1179
1195
  margin?: number;
1180
1196
  }): void;
1197
+ /**
1198
+ * How far down the window the iOS sheet a node is presented in starts, where that is known, so
1199
+ * the keyboard's `visibleBottom` can be brought into the sheet. React Native measures a view in a
1200
+ * presented sheet from the sheet's own top: the sheet's place on the screen is UIKit's, and the
1201
+ * shadow tree has the sheet at the top of the window.
1202
+ *
1203
+ * A sheet as wide as the window is attached to its bottom edge, so it starts the window's height
1204
+ * less its own down. One that is narrower (an iPad's form sheet, or one floating at a partial
1205
+ * detent) could be anywhere, and one no taller than the room above the keyboard may have risen
1206
+ * over it, so for those nothing is known and nothing is taken off. On Android the sheet is laid
1207
+ * out in the shadow tree, so nothing is either.
1208
+ */
1209
+ private sheetTop;
1181
1210
  /**
1182
1211
  * The view a scroll view's content is in: an element that scrolls its overflow holds its
1183
1212
  * children in one of its own, and any other scroll view's first child is it.
@@ -1344,8 +1373,8 @@ export declare class Engine implements HostEngine {
1344
1373
  /** Angular's `@if`/`@for`/`ViewContainerRef` markers. Ordered, never committed. */
1345
1374
  createAnchor(): EngineNode;
1346
1375
  createText(value: string): EngineNode;
1347
- appendChild(into: EngineNode, child: EngineNode): void;
1348
- insertBefore(into: EngineNode, child: EngineNode, ref: EngineNode | null): void;
1376
+ appendChild(parent: EngineNode, child: EngineNode): void;
1377
+ insertBefore(parent: EngineNode, child: EngineNode, ref: EngineNode | null): void;
1349
1378
  /**
1350
1379
  * A node coming under a parent other than the one it is under, or under any once it has been
1351
1380
  * taken out: what is over it may be other than it was styled under. The resolver sees that for
@@ -1354,6 +1383,19 @@ export declare class Engine implements HostEngine {
1354
1383
  */
1355
1384
  private markAdopted;
1356
1385
  removeChild(parent: EngineNode | null, child: EngineNode): void;
1386
+ /**
1387
+ * Commit an element's children inside a view of the engine's own, `wrapper.viewName` with
1388
+ * `wrapper.props`, and the element's own view around it; null commits them in the element
1389
+ * again. For a native view that wants its content in a child of a kind it names, where the
1390
+ * element is a component's host: the tree keeps the children in the element, so the
1391
+ * component's `:host` rules, a `:host >` selector and what is inherited reach them as written,
1392
+ * and how the element places its children (`flex-direction`, `justify-content`, `align-items`,
1393
+ * `gap`) is committed on the wrapper, which holds them. What is hoisted into the element and
1394
+ * its fixed boxes stay its own, after the wrapper. Called again with another wrapper's props,
1395
+ * the wrapper is cloned with them; another view name makes the element and what it holds again,
1396
+ * as native gives a view one parent for as long as it lives.
1397
+ */
1398
+ wrapChildren(node: EngineNode, wrapper: Wrapper | null): void;
1357
1399
  /**
1358
1400
  * A child came or went under a view that takes its text as a prop: that prop changed. A span
1359
1401
  * changes it too, by keeping or dropping the space before it.
@@ -3096,6 +3138,18 @@ export declare class Engine implements HostEngine {
3096
3138
  * for a content that was cloned.
3097
3139
  */
3098
3140
  private reconcileScroller;
3141
+ /**
3142
+ * `reconcile` for an element whose children are in a wrapper (`wrapChildren`): its own view,
3143
+ * holding the wrapper and then `beside`, and the wrapper holding its children. The wrapper is
3144
+ * created with the element and cloned for what changed of its own, as a scroll view's content
3145
+ * is.
3146
+ */
3147
+ private reconcileWrapped;
3148
+ /** `noteClone` for a wrapped element, whose children are counted in its wrapper. */
3149
+ private noteWrappedClone;
3150
+ /** A wrapper cloned for what changed of its props, and for children that moved. */
3151
+ private clonedWrapper;
3152
+ private createWrapped;
3099
3153
  /** A view cloned for the props and the children that changed, or itself where neither did. */
3100
3154
  private clonedFor;
3101
3155
  private createScroller;
@@ -3233,7 +3287,8 @@ export declare class Engine implements HostEngine {
3233
3287
  /**
3234
3288
  * `reconcileChildren` for a node other nodes hoist into: its own children, each followed by
3235
3289
  * the hoisted nodes written inside it, then those written in it directly, then the views it
3236
- * keeps for hoisted nodes that are gone.
3290
+ * keeps for hoisted nodes that are gone. A wrapped node's hoisted nodes and kept views go in
3291
+ * `beside`, beside its wrapper and not in it, in the same order.
3237
3292
  */
3238
3293
  private reconcileHoistTarget;
3239
3294
  /**
@@ -3531,8 +3586,8 @@ export declare class Engine implements HostEngine {
3531
3586
  * `unstable_*EventPriority` constants) is read into the facade but not acted on. Angular's
3532
3587
  * zoneless scheduler already flushes on a microtask, which is soon enough for taps. It stops
3533
3588
  * being soon enough for a controlled TextInput, where the echo has to beat the next
3534
- * keystroke. `mostRecentEventCount` covers that today by letting native reject a stale write;
3535
- * a synchronous flush for discrete priority is the fix if it ever stops being enough.
3589
+ * keystroke. The text input's event count covers that today by letting native reject a stale
3590
+ * write; a synchronous flush for discrete priority is the fix if it ever stops being enough.
3536
3591
  *
3537
3592
  * **Nothing thrown here may escape.** Fabric calls this synchronously from C++
3538
3593
  * (`UIManagerBinding::dispatchEventToJS`), often from inside a mounting transaction, and a throw
package/dist/engine.js CHANGED
@@ -341,6 +341,8 @@ const ENGINE_PROPS = new Set([
341
341
  'disabled',
342
342
  'intrinsicSize',
343
343
  'styleOverride',
344
+ // A text field's latest event count, committed as `mostRecentEventCount`: see `echoedEventCount`.
345
+ 'latestEventCount',
344
346
  ...Object.values(EVENT_OPT_IN_PROPS),
345
347
  ...Object.values(VIEW_EVENT_OPT_IN_PROPS).flatMap((props) => Object.values(props)),
346
348
  ]);
@@ -459,15 +461,33 @@ export function markPage(node) {
459
461
  export function scrollsOverflow(node) {
460
462
  node.scrollsOverflow = true;
461
463
  }
464
+ /** How a flex container places its children, which the view they are in takes. */
465
+ const CHILD_PLACING_KEYS = [
466
+ ...['flexDirection', 'flexWrap', 'alignItems', 'alignContent', 'justifyContent'],
467
+ ...['gap', 'rowGap', 'columnGap'],
468
+ ];
462
469
  /** What a scrolling element lays out what it holds by, which its content view takes. */
463
470
  const CONTENT_KEYS = new Set([
464
471
  ...['padding', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
465
472
  ...['paddingHorizontal', 'paddingVertical', 'paddingStart', 'paddingEnd'],
466
473
  ...['paddingInline', 'paddingBlock', 'paddingInlineStart', 'paddingInlineEnd'],
467
474
  ...['paddingBlockStart', 'paddingBlockEnd'],
468
- ...['flexDirection', 'flexWrap', 'alignItems', 'alignContent', 'justifyContent'],
469
- ...['gap', 'rowGap', 'columnGap'],
475
+ ...CHILD_PLACING_KEYS,
470
476
  ]);
477
+ const CHILD_PLACING = new Set(CHILD_PLACING_KEYS);
478
+ /**
479
+ * A wrapped element's props split between its two views: the box it is, on its own view, and how
480
+ * its children are placed, on the wrapper, after the wrapper's own props. Its padding stays on
481
+ * the element, outside what the wrapper insets by.
482
+ */
483
+ function splitWrapped(props, own = {}) {
484
+ const box = {};
485
+ const content = { ...own };
486
+ for (const [key, value] of Object.entries(props)) {
487
+ (CHILD_PLACING.has(key) ? content : box)[key] = value;
488
+ }
489
+ return { box, content };
490
+ }
471
491
  /** A scrolling element's props split between its two views: see `scrollerProps`. */
472
492
  function splitScroller(props) {
473
493
  const box = {};
@@ -3442,6 +3462,65 @@ function alignMultiline(viewName, props) {
3442
3462
  return;
3443
3463
  props['textAlignVertical'] ??= 'top';
3444
3464
  }
3465
+ /** The props React Native builds a text field's text attributes from, as `BaseTextProps` reads them. */
3466
+ const TEXT_ATTRIBUTES = [
3467
+ 'color',
3468
+ 'backgroundColor',
3469
+ 'opacity',
3470
+ 'fontFamily',
3471
+ 'fontSize',
3472
+ 'fontStyle',
3473
+ 'fontWeight',
3474
+ 'fontVariant',
3475
+ 'fontVariationSettings',
3476
+ 'allowFontScaling',
3477
+ 'maxFontSizeMultiplier',
3478
+ 'dynamicTypeRamp',
3479
+ 'letterSpacing',
3480
+ 'lineHeight',
3481
+ 'textAlign',
3482
+ 'writingDirection',
3483
+ 'direction',
3484
+ 'lineBreakStrategyIOS',
3485
+ 'lineBreakModeIOS',
3486
+ 'textDecorationLine',
3487
+ 'textDecorationColor',
3488
+ 'textDecorationStyle',
3489
+ 'textShadowColor',
3490
+ 'textShadowOffset',
3491
+ 'textShadowRadius',
3492
+ 'textTransform',
3493
+ ];
3494
+ /**
3495
+ * The prop a text field that echoes its text puts its latest event count in, for
3496
+ * `echoedEventCount` to commit as `mostRecentEventCount`. It never reaches native.
3497
+ */
3498
+ const LATEST_EVENT_COUNT = 'latestEventCount';
3499
+ /**
3500
+ * The event count a text field that echoes its text is committed with: the latest on Android, and
3501
+ * on iOS 0, the count native starts at, but for a commit that changes a multiline field's text
3502
+ * style, which carries the latest.
3503
+ *
3504
+ * iOS takes the `text` prop only with the count it holds, and then replaces the field's text unless
3505
+ * the two compare equal, attributes and all. UIKit gives an empty field's first typed text a
3506
+ * paragraph style whose line break strategy differs from the one RN compares it with, so an echo
3507
+ * of that very text replaces it, and the keystrokes queued behind the replace are lost. Text from
3508
+ * code goes as `setTextAndSelection` instead, which carries the count. A restyle still needs the
3509
+ * replace on a multiline field: a single-line one restyles its text from its new default
3510
+ * attributes, a multiline one only takes them for what is typed next.
3511
+ *
3512
+ * A field that sets `mostRecentEventCount` itself, and never echoes, is left as it is.
3513
+ */
3514
+ function echoedEventCount(viewName, props, previous) {
3515
+ const latest = props[LATEST_EVENT_COUNT];
3516
+ if (latest === undefined)
3517
+ return;
3518
+ delete props[LATEST_EVENT_COUNT];
3519
+ const restyled = props['multiline'] === true &&
3520
+ previous !== undefined &&
3521
+ TEXT_ATTRIBUTES.some((key) => !Object.is(previous[key], props[key]));
3522
+ props['mostRecentEventCount'] = viewName !== 'TextInput' || restyled ? latest : 0;
3523
+ }
3445
3524
  /** The props of a run of text that change how tall its line is drawn. */
3446
3525
  const SIZES_A_RUN = new Set(['style', 'allowFontScaling', 'maxFontSizeMultiplier']);
3447
3526
  /**
@@ -3631,6 +3710,12 @@ function seenByCss(merged) {
3631
3710
  /** A screen of a native stack, which is what the swipe back is a gesture of. */
3632
3711
  const STACK_SCREEN = 'RNSScreen';
3633
3712
  const FIXED_BOUNDARIES = new Set([MODAL_HOST, STACK_SCREEN, 'RNSFullWindowOverlay']);
3713
+ /**
3714
+ * The presentations that can leave a sheet short of the top of the screen on iOS: a screen's
3715
+ * `stackPresentation` (`modal` is a page sheet from iOS 13) or a modal host's `presentationStyle`.
3716
+ * The others cover the screen, or what they are presented over.
3717
+ */
3718
+ const SHEETS = new Set(['modal', 'formSheet', 'pageSheet']);
3634
3719
  /** What an element's `touch-action` is: bound on it, or from the rules it matches. */
3635
3720
  function touchActionOf(node) {
3636
3721
  return inlineOf(node)['touchAction'] ?? node.styleCache?.style['touchAction'];
@@ -4726,6 +4811,7 @@ class RetainedNode {
4726
4811
  inlineInherits = undefined;
4727
4812
  line = undefined;
4728
4813
  inside = undefined;
4814
+ wrapper = undefined;
4729
4815
  transitions;
4730
4816
  playing;
4731
4817
  scrolled;
@@ -4945,7 +5031,7 @@ const ENGINE_NODE_FIELDS = {
4945
5031
  structureDirty: true,
4946
5032
  subtreeDirty: true,
4947
5033
  inside: true,
4948
- contentView: true,
5034
+ wrapper: true,
4949
5035
  scrollsOverflow: true,
4950
5036
  engine: true,
4951
5037
  };
@@ -6011,7 +6097,12 @@ export class Engine {
6011
6097
  * keyboard's top edge, for a field the keyboard would otherwise hide.
6012
6098
  *
6013
6099
  * Measured in the window, all of it, so the scroll view's offset needs no tracking: it is how
6014
- * far its content's top edge is above its own.
6100
+ * far its content's top edge is above its own. In a sheet on iOS the window is the sheet's: see
6101
+ * `sheetTop`.
6102
+ *
6103
+ * A scroll view under a navigation bar rests scrolled above zero, by the inset the bar adds, and
6104
+ * is not scrolled up past where it rests, nor past zero from below it: how far above zero it can
6105
+ * go is known only while it is there.
6015
6106
  */
6016
6107
  reveal(node, options = {}) {
6017
6108
  const scroll = scrollingDown(node);
@@ -6019,23 +6110,68 @@ export class Engine {
6019
6110
  if (!scroll || !content)
6020
6111
  return;
6021
6112
  const margin = options.margin ?? 16;
6113
+ const { visibleBottom } = options;
6114
+ const covered = visibleBottom === undefined ? Infinity : visibleBottom - this.sheetTop(node, visibleBottom);
6022
6115
  this.measure(scroll, (view) => {
6023
6116
  this.measureHandle(content, (inner) => {
6024
6117
  this.measure(node, (target) => {
6025
6118
  const offset = view.y - inner.y;
6026
6119
  const top = view.y + margin;
6027
- const bottom = Math.min(view.y + view.height, options.visibleBottom ?? Infinity) - margin;
6120
+ const bottom = Math.min(view.y + view.height, covered) - margin;
6028
6121
  let by = 0;
6029
6122
  if (target.y + target.height > bottom)
6030
6123
  by = target.y + target.height - bottom;
6031
6124
  else if (target.y < top)
6032
6125
  by = target.y - top;
6033
6126
  if (by !== 0)
6034
- this.dispatchCommand(scroll, 'scrollTo', [0, Math.max(0, offset + by), true]);
6127
+ this.dispatchCommand(scroll, 'scrollTo', [
6128
+ 0,
6129
+ Math.max(Math.min(0, offset), offset + by),
6130
+ true,
6131
+ ]);
6035
6132
  });
6036
6133
  });
6037
6134
  });
6038
6135
  }
6136
+ /**
6137
+ * How far down the window the iOS sheet a node is presented in starts, where that is known, so
6138
+ * the keyboard's `visibleBottom` can be brought into the sheet. React Native measures a view in a
6139
+ * presented sheet from the sheet's own top: the sheet's place on the screen is UIKit's, and the
6140
+ * shadow tree has the sheet at the top of the window.
6141
+ *
6142
+ * A sheet as wide as the window is attached to its bottom edge, so it starts the window's height
6143
+ * less its own down. One that is narrower (an iPad's form sheet, or one floating at a partial
6144
+ * detent) could be anywhere, and one no taller than the room above the keyboard may have risen
6145
+ * over it, so for those nothing is known and nothing is taken off. On Android the sheet is laid
6146
+ * out in the shadow tree, so nothing is either.
6147
+ */
6148
+ sheetTop(node, keyboardTop) {
6149
+ if (platformOS !== 'ios')
6150
+ return 0;
6151
+ for (let up = node.parent; up; up = up.parent) {
6152
+ if (up.nodeKind !== 'element')
6153
+ continue;
6154
+ const viewName = viewNameOf(up);
6155
+ // React Native's own modal measures what it holds from its own top, wherever it is written.
6156
+ const modal = viewName === MODAL_HOST;
6157
+ const presented = modal
6158
+ ? up.props['presentationStyle']
6159
+ : viewName === STACK_SCREEN && up.props['stackPresentation'];
6160
+ // A screen pushed in a stack is measured in whatever it is pushed in.
6161
+ if (!modal && (!presented || presented === 'push'))
6162
+ continue;
6163
+ if (!SHEETS.has(String(presented)))
6164
+ return 0;
6165
+ let top = 0;
6166
+ this.measure(up, (sheet) => {
6167
+ if (sheet.width < this.viewportSize.width || sheet.height <= keyboardTop)
6168
+ return;
6169
+ top = Math.max(0, this.viewportSize.height - (modal ? 0 : sheet.y) - sheet.height);
6170
+ });
6171
+ return top;
6172
+ }
6173
+ return 0;
6174
+ }
6039
6175
  /**
6040
6176
  * The view a scroll view's content is in: an element that scrolls its overflow holds its
6041
6177
  * children in one of its own, and any other scroll view's first child is it.
@@ -6496,8 +6632,7 @@ export class Engine {
6496
6632
  node.nodeText = value;
6497
6633
  return node;
6498
6634
  }
6499
- appendChild(into, child) {
6500
- const parent = into.contentView && into.contentView !== child ? into.contentView : into;
6635
+ appendChild(parent, child) {
6501
6636
  this.markAdopted(child, parent);
6502
6637
  this.detach(child);
6503
6638
  this.markReturned(child);
@@ -6512,8 +6647,7 @@ export class Engine {
6512
6647
  this.markStructure(parent, child, parent.childList.length - 1);
6513
6648
  this.markTextContent(parent, child);
6514
6649
  }
6515
- insertBefore(into, child, ref) {
6516
- const parent = into.contentView && into.contentView !== child ? into.contentView : into;
6650
+ insertBefore(parent, child, ref) {
6517
6651
  this.markAdopted(child, parent);
6518
6652
  this.detach(child);
6519
6653
  this.markReturned(child);
@@ -6557,6 +6691,25 @@ export class Engine {
6557
6691
  this.markStructure(target, child, at);
6558
6692
  this.markTextContent(target, child);
6559
6693
  }
6694
+ /**
6695
+ * Commit an element's children inside a view of the engine's own, `wrapper.viewName` with
6696
+ * `wrapper.props`, and the element's own view around it; null commits them in the element
6697
+ * again. For a native view that wants its content in a child of a kind it names, where the
6698
+ * element is a component's host: the tree keeps the children in the element, so the
6699
+ * component's `:host` rules, a `:host >` selector and what is inherited reach them as written,
6700
+ * and how the element places its children (`flex-direction`, `justify-content`, `align-items`,
6701
+ * `gap`) is committed on the wrapper, which holds them. What is hoisted into the element and
6702
+ * its fixed boxes stay its own, after the wrapper. Called again with another wrapper's props,
6703
+ * the wrapper is cloned with them; another view name makes the element and what it holds again,
6704
+ * as native gives a view one parent for as long as it lives.
6705
+ */
6706
+ wrapChildren(node, wrapper) {
6707
+ const was = node.wrapper;
6708
+ node.wrapper = wrapper ?? undefined;
6709
+ if (was?.viewName !== wrapper?.viewName && node.committed)
6710
+ this.forgetCommitted(node);
6711
+ this.markProps(node, false);
6712
+ }
6560
6713
  /**
6561
6714
  * A child came or went under a view that takes its text as a prop: that prop changed. A span
6562
6715
  * changes it too, by keeping or dropping the space before it.
@@ -10323,6 +10476,8 @@ export class Engine {
10323
10476
  // On iOS: Android's field says the baseline of the line it is sized and centred by.
10324
10477
  if (field)
10325
10478
  centreSingleLine(viewName, merged, this.fontScale, isOnBaseline(node, viewName));
10479
+ if (field)
10480
+ echoedEventCount(viewName, merged, node.committed?.props);
10326
10481
  // After a transition, which eases the basis as the basis it was written as.
10327
10482
  basisAsSize(node, merged);
10328
10483
  // Each asked of the key it reads first, here and in `engineLayout`: it is asked of every node
@@ -12636,15 +12791,19 @@ export class Engine {
12636
12791
  this.forgetRenamed(node, viewName);
12637
12792
  this.noteBaselineFields(node);
12638
12793
  this.freshBaselines(node);
12639
- const childHandles = this.reconcileChildren(node, viewName, style);
12794
+ // A wrapped node's own views beside the wrapper: what is hoisted into it, and its fixed boxes.
12795
+ const beside = node.wrapper && [];
12796
+ const childHandles = this.reconcileChildren(node, viewName, style, beside);
12640
12797
  if (this.heldMoves.size)
12641
12798
  this.heldHandles(node, childHandles);
12642
12799
  if (this.fixedIn.size)
12643
- this.fixedHandles(node, childHandles);
12800
+ this.fixedHandles(node, beside ?? childHandles);
12644
12801
  this.raise(node, viewName);
12645
12802
  noteTouches(node, style);
12646
12803
  if (scroller)
12647
12804
  return this.reconcileScroller(node, viewName, childHandles, styleChanged);
12805
+ if (beside)
12806
+ return this.reconcileWrapped(node, viewName, childHandles, beside, styleChanged);
12648
12807
  const previous = node.committed;
12649
12808
  this.notePresented(node, viewName);
12650
12809
  if (!previous)
@@ -12744,6 +12903,62 @@ export class Engine {
12744
12903
  this.clearFlags(node);
12745
12904
  return handle;
12746
12905
  }
12906
+ /**
12907
+ * `reconcile` for an element whose children are in a wrapper (`wrapChildren`): its own view,
12908
+ * holding the wrapper and then `beside`, and the wrapper holding its children. The wrapper is
12909
+ * created with the element and cloned for what changed of its own, as a scroll view's content
12910
+ * is.
12911
+ */
12912
+ reconcileWrapped(node, viewName, childHandles, beside, styleChanged) {
12913
+ const previous = node.committed;
12914
+ const held = node.inside;
12915
+ if (!previous || !held?.props)
12916
+ return this.createWrapped(node, viewName, childHandles, beside);
12917
+ const propsMoved = node.propsDirty || styleChanged;
12918
+ const props = propsMoved ? this.mergeProps(node, viewName) : previous.props;
12919
+ const payload = propsMoved ? this.propsPayload(node, previous.props, props) : null;
12920
+ if (this.asked & LAYOUTS)
12921
+ this.noteHow(node, payload);
12922
+ const moved = this.childrenMoved(node, childHandles, held.childHandles);
12923
+ this.noteWrappedClone(node, payload, moved, childHandles, previous, held);
12924
+ const content = propsMoved ? splitWrapped(props, node.wrapper.props).content : held.props;
12925
+ const inside = this.clonedWrapper(held, content, moved ? childHandles : null);
12926
+ const around = [inside, ...beside];
12927
+ const aroundMoved = !sameHandles(around, previous.childHandles);
12928
+ const box = payload && someProps(splitWrapped(payload).box);
12929
+ const boxPayload = this.paintNatively(node, viewName, props, box, aroundMoved);
12930
+ const handle = this.clonedFor(previous.handle, boxPayload, aroundMoved ? around : null);
12931
+ node.committed = { handle, tag: previous.tag, props, childHandles: around, viewName };
12932
+ node.inside = { handle: inside, childHandles, props: content };
12933
+ this.clearFlags(node);
12934
+ return handle;
12935
+ }
12936
+ /** `noteClone` for a wrapped element, whose children are counted in its wrapper. */
12937
+ noteWrappedClone(node, payload, moved, childHandles, previous, held) {
12938
+ if (!this.notesMoves)
12939
+ return;
12940
+ const before = { ...previous, childHandles: held.childHandles };
12941
+ this.noteClone(node, payload, moved, childHandles, before);
12942
+ }
12943
+ /** A wrapper cloned for what changed of its props, and for children that moved. */
12944
+ clonedWrapper(held, props, children) {
12945
+ const payload = props === held.props ? null : diffProps(held.props, props);
12946
+ return this.clonedFor(held.handle, payload, children);
12947
+ }
12948
+ createWrapped(node, viewName, childHandles, beside) {
12949
+ const wrapper = node.wrapper;
12950
+ const props = this.mergeProps(node, viewName);
12951
+ const { box, content } = splitWrapped(props, wrapper.props);
12952
+ // A touch on the wrapper lands on the element, as one on a scroll view's content does.
12953
+ const inside = this.fabric.createNode(nextTag(), wrapper.viewName, this.rootTag, this.processed(content, false), node);
12954
+ this.stats.createdNodes++;
12955
+ for (const child of childHandles)
12956
+ this.fabric.appendChild(inside, child);
12957
+ const handle = this.create(node, viewName, box, [inside, ...beside]);
12958
+ node.committed = { ...node.committed, props };
12959
+ node.inside = { handle: inside, childHandles, props: content };
12960
+ return handle;
12961
+ }
12747
12962
  /** A view cloned for the props and the children that changed, or itself where neither did. */
12748
12963
  clonedFor(handle, props, children) {
12749
12964
  if (!props && !children)
@@ -12803,12 +13018,12 @@ export class Engine {
12803
13018
  // Exempt from the complexity limit as `mergeProps` is: run for every node with children, where
12804
13019
  // the loop in a function of its own was a call more a node in `pnpm bench`.
12805
13020
  // eslint-disable-next-line complexity
12806
- reconcileChildren(node, viewName, style) {
13021
+ reconcileChildren(node, viewName, style, beside) {
12807
13022
  const context = style?.context;
12808
13023
  if (HOIST_ONLY.has(node.tag))
12809
13024
  return this.reconcileOnlyHoisted(node);
12810
13025
  if (HOIST_TARGETS.has(node.tag))
12811
- return this.reconcileHoistTarget(node, context);
13026
+ return this.reconcileHoistTarget(node, context, beside);
12812
13027
  if (node.flow !== undefined)
12813
13028
  return this.reconcileInFlow(node, node.flow, viewName, context);
12814
13029
  // A node with no flow of its own holds nothing in a line: each child is its own view under it.
@@ -13206,10 +13421,12 @@ export class Engine {
13206
13421
  /**
13207
13422
  * `reconcileChildren` for a node other nodes hoist into: its own children, each followed by
13208
13423
  * the hoisted nodes written inside it, then those written in it directly, then the views it
13209
- * keeps for hoisted nodes that are gone.
13424
+ * keeps for hoisted nodes that are gone. A wrapped node's hoisted nodes and kept views go in
13425
+ * `beside`, beside its wrapper and not in it, in the same order.
13210
13426
  */
13211
- reconcileHoistTarget(node, context) {
13427
+ reconcileHoistTarget(node, context, beside) {
13212
13428
  const handles = [];
13429
+ const landing = beside ?? handles;
13213
13430
  const hoisted = this.hoistedInto(node);
13214
13431
  const landed = new Set();
13215
13432
  // One written directly in the node goes after its siblings: a page writes its header first,
@@ -13227,13 +13444,13 @@ export class Engine {
13227
13444
  const moved = hoisted?.get(child);
13228
13445
  for (const written of moved ?? []) {
13229
13446
  if (!this.undisplayedUnder(node, written))
13230
- handles.push(this.land(node, written, landed));
13447
+ landing.push(this.land(node, written, landed));
13231
13448
  }
13232
13449
  }
13233
13450
  for (const child of direct)
13234
- handles.push(this.land(node, child, landed));
13451
+ landing.push(this.land(node, child, landed));
13235
13452
  if (node.kept)
13236
- this.standIn(node, landed, handles);
13453
+ this.standIn(node, landed, landing);
13237
13454
  return handles;
13238
13455
  }
13239
13456
  /**
@@ -14006,8 +14223,8 @@ export class Engine {
14006
14223
  * `unstable_*EventPriority` constants) is read into the facade but not acted on. Angular's
14007
14224
  * zoneless scheduler already flushes on a microtask, which is soon enough for taps. It stops
14008
14225
  * being soon enough for a controlled TextInput, where the echo has to beat the next
14009
- * keystroke. `mostRecentEventCount` covers that today by letting native reject a stale write;
14010
- * a synchronous flush for discrete priority is the fix if it ever stops being enough.
14226
+ * keystroke. The text input's event count covers that today by letting native reject a stale
14227
+ * write; a synchronous flush for discrete priority is the fix if it ever stops being enough.
14011
14228
  *
14012
14229
  * **Nothing thrown here may escape.** Fabric calls this synchronously from C++
14013
14230
  * (`UIManagerBinding::dispatchEventToJS`), often from inside a mounting transaction, and a throw