@ng-native/fabric 0.14.0 → 0.16.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.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * because a persistent parent holds references to specific child handles. That bubble is
8
8
  * inherent and is what React's own Fabric renderer does.
9
9
  */
10
- import { boundDeclaration, boundRelativeLength, CSS_SPACE, isNegativeLineHeight, faded, fills, inlineInherited, setsInherited, StyleResolver, usesActive, elementsAtEnd, hasAbove, placeReadElsewhere, siblingReach, } from "./css.js";
10
+ import { boundDeclaration, boundRelativeLength, CSS_SPACE, isNegativeLineHeight, faded, fills, inlineInherited, setsInherited, StyleResolver, usesActive, elementsAtEnd, hasAbove, joinSubjects, placeReadElsewhere, siblingReach, } from "./css.js";
11
11
  import { applyAria } from "./aria-props.js";
12
12
  import { STYLED_ELEMENTS } from "./element-styles.js";
13
13
  import { aimsElsewhere, animationEvent, bezier, interpolate, readsInheritedColour, sample, step, steppedKeys, tracksOf, } from "./transition.js";
@@ -189,6 +189,13 @@ const VIEW_EVENT_OPT_IN_PROPS = {
189
189
  topError: 'shouldNotifyLoadEvents',
190
190
  topProgress: 'shouldNotifyLoadEvents',
191
191
  },
192
+ // `ReactTextInputManager` watches each of these only behind a prop of the handler's name,
193
+ // which `TextInput.js` sets by passing the handler through. iOS's field sends them unasked.
194
+ AndroidTextInput: {
195
+ topContentSizeChange: 'onContentSizeChange',
196
+ topScroll: 'onScroll',
197
+ topKeyPress: 'onKeyPress',
198
+ },
192
199
  };
193
200
  /**
194
201
  * Props the engine itself writes or reads off a node, so never a misspelling: the style, the
@@ -461,10 +468,14 @@ const blockEdge = (props, side, logical) => [
461
468
  /**
462
469
  * A multiline field's `rows` as its height, where it was given none: that many lines of its
463
470
  * line height at the system text size, inside its padding and border, as a browser sizes a `<textarea>`. A native field
464
- * is as tall as its text, which is one line before anything is typed.
471
+ * is as tall as its text, which is one line before anything is typed. Taken off the props once
472
+ * read: neither platform's native field has a prop of that name.
465
473
  */
466
- function rowsTall(props, fontScale) {
474
+ function rowsTall(viewName, props, fontScale) {
475
+ if (!TEXT_INPUTS.has(viewName))
476
+ return;
467
477
  const rows = Number(props['rows']);
478
+ delete props['rows'];
468
479
  const line = props['lineHeight'];
469
480
  if (props['multiline'] !== true || !(rows > 0) || typeof line !== 'number')
470
481
  return;
@@ -837,6 +848,12 @@ function placeholderFaded(props) {
837
848
  else if (opacity <= 0)
838
849
  props['placeholderTextColor'] = 'rgba(0, 0, 0, 0)';
839
850
  }
851
+ /**
852
+ * What a removed prop travels as: null, which native resets the prop on, except `pointerEvents`,
853
+ * which react-native-svg's Android root reads as a string and stops the app on a null. `auto` is
854
+ * what no value means.
855
+ */
856
+ const cleared = (key) => (key === 'pointerEvents' ? 'auto' : null);
840
857
  /** The least alpha iOS sends a touch to a view at: under a hundredth it is passed over. */
841
858
  const TOUCHED_ALPHA = 0.011;
842
859
  /**
@@ -1779,6 +1796,8 @@ const EASED_COLOURS = new Set([
1779
1796
  'borderLeftColor',
1780
1797
  ]);
1781
1798
  const NATIVELY_EASED = new Set(['opacity', 'transform', ...EASED_COLOURS]);
1799
+ /** What a node that hands its background colour over has handed so far: nothing. */
1800
+ const UNREPORTED = Symbol('unreported');
1782
1801
  /** The style keys that are a view's background. */
1783
1802
  const PAINT_KEYS = [
1784
1803
  'backgroundColor',
@@ -1787,6 +1806,15 @@ const PAINT_KEYS = [
1787
1806
  'experimental_backgroundPosition',
1788
1807
  'experimental_backgroundRepeat',
1789
1808
  ];
1809
+ /** Note a prop written on a node that keeps `writes`: see there. */
1810
+ function noteWrite(node, key, value) {
1811
+ if (!node.writes)
1812
+ return;
1813
+ if (value === undefined || value === null)
1814
+ node.writes.delete(key);
1815
+ else
1816
+ node.writes.add(key);
1817
+ }
1790
1818
  /**
1791
1819
  * A node's own props onto what it commits with. No native prop has a hyphen: `data-*` and `aria-*`
1792
1820
  * attributes stay on the node for selectors to match, and an `aria-*` one is mapped to the prop
@@ -1953,9 +1981,14 @@ function reportHover(host) {
1953
1981
  hoverReported = true;
1954
1982
  console.warn('[angular-native] @defer (on hover) fires when a pointer enters the trigger, which takes a trackpad or a mouse (an iPad, or an Android device with one), or when the trigger takes focus. On a phone it waits for focus alone; add another trigger, such as on interaction or on viewport.');
1955
1983
  }
1956
- /** A bound value kept as a declaration the cascade settles: a `var()` or a relative length. */
1984
+ /** A bound value the cascade works out from tokens: a `var()`, a `color-mix()`, a relative colour. */
1985
+ const BOUND_TOKEN = /^[ \t\n\r\f]*(?:var|color-mix)\(|^[ \t\n\r\f]*[a-z]+\([ \t\n\r\f]*from[ \t\n\r\f]/i;
1986
+ /**
1987
+ * A bound value kept as a declaration the cascade settles: a `var()`, a colour made of tokens, or
1988
+ * a relative length.
1989
+ */
1957
1990
  function boundFrom(prop, text, on) {
1958
- return /^\s*var\(/i.test(text)
1991
+ return BOUND_TOKEN.test(text)
1959
1992
  ? boundDeclaration(prop, tokenFromValue(text), on)
1960
1993
  : boundRelativeLength(prop, text, on);
1961
1994
  }
@@ -2344,6 +2377,26 @@ function applyIntrinsicSize(props, size) {
2344
2377
  }
2345
2378
  }
2346
2379
  const now = () => globalThis.performance?.now?.() ?? Date.now();
2380
+ /** How many boxes `node` is under. */
2381
+ /**
2382
+ * How deep a node is, with the depth of every box on its way up kept in `known`: the boxes that
2383
+ * changed beneath are a node and its ancestors, so each is counted once, not once a box.
2384
+ */
2385
+ function depthOf(node, known) {
2386
+ const path = [];
2387
+ let depth = -1;
2388
+ for (let up = node; up; up = up.parent) {
2389
+ const found = known.get(up);
2390
+ if (found !== undefined) {
2391
+ depth = found;
2392
+ break;
2393
+ }
2394
+ path.push(up);
2395
+ }
2396
+ for (let i = path.length - 1; i >= 0; i--)
2397
+ known.set(path[i], ++depth);
2398
+ return depth;
2399
+ }
2347
2400
  function sameHandles(a, b) {
2348
2401
  return a.length === b.length && a.every((handle, i) => handle === b[i]);
2349
2402
  }
@@ -2366,6 +2419,11 @@ function couldBe(compound, node) {
2366
2419
  return (compound.is ?? []).every((alternatives) => alternatives.some((option) => couldBe(option, node)));
2367
2420
  }
2368
2421
  const FRAME_BUDGET_MS = 8;
2422
+ /** A solid brush with no colour in it, which stops react-native-svg. */
2423
+ const emptyBrush = (value) => typeof value === 'object' &&
2424
+ value !== null &&
2425
+ value.type === 0 &&
2426
+ value.payload == null;
2369
2427
  export class Engine {
2370
2428
  /** Scopes the CSS resolver's memo. Bumped once per commit. */
2371
2429
  styleEpoch = 0;
@@ -2819,7 +2877,7 @@ export class Engine {
2819
2877
  if (sheet === undefined || node.sheet === sheet)
2820
2878
  return;
2821
2879
  node.sheet = sheet;
2822
- this.styles.noteSheet(sheet);
2880
+ this.styles.noteSheet(sheet, true);
2823
2881
  this.watchStructure(sheet);
2824
2882
  this.watchHas(sheet);
2825
2883
  this.watchActive(sheet);
@@ -2855,7 +2913,7 @@ export class Engine {
2855
2913
  createElement(name, sheet = null) {
2856
2914
  const node = new RetainedNode('element', name, this);
2857
2915
  node.sheet = sheet;
2858
- this.styles.noteSheet(sheet);
2916
+ this.styles.noteSheet(sheet, true);
2859
2917
  const html = HTML_ELEMENTS.get(name);
2860
2918
  if (html !== undefined) {
2861
2919
  if (html.role)
@@ -2885,8 +2943,10 @@ export class Engine {
2885
2943
  * element matched again, and one named as what a rule's element is inside, `.animating .close`
2886
2944
  * or `.close:is(.animating *)`, the elements under the node that such rules are for. What is
2887
2945
  * under any of those is styled again from the rules it matched, for what it inherits: a `dark`
2888
- * class on the root finds no rule again for an element no `dark` rule is for. Any other class
2889
- * has the node and everything under it matched again, as any other change to it does.
2946
+ * class on the root finds no rule again for an element no `dark` rule is for. One named in
2947
+ * `:has()`, or answering a test of the class attribute there, has the boxes above the node
2948
+ * asked again, and one named only in `:has(> )` the box right above it. Any other class has the node and everything under it matched again, as any
2949
+ * other change to it does.
2890
2950
  *
2891
2951
  * A sheet that names a class later restyles what it is for as it arrives, and a node not yet
2892
2952
  * committed is styled when it is, with the class as it is then.
@@ -2905,6 +2965,11 @@ export class Engine {
2905
2965
  return;
2906
2966
  if (reach.own)
2907
2967
  node.ownDirty = true;
2968
+ if (this.hasSheets && reach.beneath)
2969
+ this.markBeneath(node.parent);
2970
+ // `:has(> .on)`: only the box right above the node can answer it.
2971
+ else if (this.hasSheets && reach.parentHas)
2972
+ this.markBeneath(node.parent, node.parent?.parent);
2908
2973
  if (this.styles.reachesUnder(reach)) {
2909
2974
  this.classesAbove += 1;
2910
2975
  this.markUnder(node, reach);
@@ -2962,8 +3027,9 @@ export class Engine {
2962
3027
  if (node.hostSheet === sheet)
2963
3028
  return;
2964
3029
  node.hostSheet = sheet;
2965
- this.styles.noteSheet(sheet);
3030
+ this.styles.noteSheet(sheet, true);
2966
3031
  this.watchStructure(sheet);
3032
+ this.watchHas(sheet);
2967
3033
  this.watchActive(sheet);
2968
3034
  this.markProps(node);
2969
3035
  }
@@ -3253,6 +3319,7 @@ export class Engine {
3253
3319
  setProp(node, key, value) {
3254
3320
  if (!this.eases)
3255
3321
  this.noteEasingProp(key, value);
3322
+ noteWrite(node, key, value);
3256
3323
  // A prop written again is a binding's, and is committed as any other is.
3257
3324
  if (node.attributeOnly?.delete(key))
3258
3325
  this.markProps(node, false);
@@ -3278,7 +3345,9 @@ export class Engine {
3278
3345
  // And only a prop some selector names: `aria-valuenow` on every tick of a progress bar, a
3279
3346
  // field's text on every key, match nothing they did not.
3280
3347
  const read = key === 'style' ? this.inlineReachesStyle(node) : this.styles.reads(key);
3281
- this.markProps(node, read || heirs);
3348
+ // And restyles the elements after it only where a rule asks the prop of an element before
3349
+ // another, `[data-on] ~ .x`: a point of a chart set as a finger moves leaves those after it.
3350
+ this.markProps(node, read || heirs, this.styles.readsBeside(key));
3282
3351
  if (SIZES_A_RUN.has(key))
3283
3352
  this.markParagraph(node);
3284
3353
  }
@@ -3304,6 +3373,40 @@ export class Engine {
3304
3373
  this.markProps(node, false);
3305
3374
  this.markProps(child, false);
3306
3375
  }
3376
+ /**
3377
+ * Hand a node's background colour to `listener` instead of painting it: what its stylesheets,
3378
+ * tokens and inline style resolve `background-color` to, or undefined for none. Called after
3379
+ * each commit that changes it, the first one included. What a component needs whose colour is
3380
+ * drawn by a native view the node is not, from a prop of that view's, as a bottom sheet's is.
3381
+ * The function returned stops it, and the node paints its own background again.
3382
+ */
3383
+ takePaint(node, listener) {
3384
+ node.paintTaker = listener;
3385
+ node.paintTaken = UNREPORTED;
3386
+ this.markProps(node, false);
3387
+ return () => {
3388
+ if (node.paintTaker !== listener)
3389
+ return;
3390
+ node.paintTaker = undefined;
3391
+ this.paintReports.delete(node);
3392
+ this.markProps(node, false);
3393
+ };
3394
+ }
3395
+ /** Nodes whose background colour is to be handed over once the commit running ends. */
3396
+ paintReports = new Set();
3397
+ reportPaint() {
3398
+ const nodes = [...this.paintReports];
3399
+ this.paintReports.clear();
3400
+ // One listener that throws stops neither the rest nor what the commit does after them.
3401
+ for (const node of nodes) {
3402
+ try {
3403
+ node.paintTaker?.(node.paintTaken);
3404
+ }
3405
+ catch (error) {
3406
+ this.reportEventError(error, 'paint');
3407
+ }
3408
+ }
3409
+ }
3307
3410
  /**
3308
3411
  * Keep a prop the node already has as an attribute: on the node for a selector such as
3309
3412
  * `:host([variant='primary'])` to match, and left out of what is committed to the native view.
@@ -3372,7 +3475,9 @@ export class Engine {
3372
3475
  else
3373
3476
  byProp.delete(prop);
3374
3477
  node.boundStyle = byProp.size ? [...byProp.values()] : null;
3375
- this.markProps(node);
3478
+ // Settled by the cascade as the node is styled, and asked about by no selector: nothing after
3479
+ // the node matches otherwise for it.
3480
+ this.markProps(node, true, false);
3376
3481
  return bound !== null || refused;
3377
3482
  }
3378
3483
  /** Whether a bound value is one CSS has none of, which is dropped with a warning. */
@@ -3436,7 +3541,7 @@ export class Engine {
3436
3541
  return () => set.delete(fn);
3437
3542
  }
3438
3543
  // --- dirty marking ----------------------------------------------------
3439
- markProps(node, affectsStyle = true) {
3544
+ markProps(node, affectsStyle = true, beside = true) {
3440
3545
  node.propsDirty = true;
3441
3546
  // Classes, `nativeID` and anything else a selector can read live in props, so any of them
3442
3547
  // changing can change what this node matches, and through it what its whole subtree does.
@@ -3446,7 +3551,8 @@ export class Engine {
3446
3551
  node.styleDirty = true;
3447
3552
  // `.peer:focus ~ .label`: what a later sibling matches can hang on this node's classes and
3448
3553
  // state, though nothing about the sibling moved. Only sheets that ask about siblings pay.
3449
- if (this.structuralSheets)
3554
+ // Not for a change `beside` says no rule asks of an element before another.
3555
+ if (this.structuralSheets && beside)
3450
3556
  this.markLaterSiblings(node);
3451
3557
  if (this.hasSheets)
3452
3558
  this.markBeneath(node.parent);
@@ -3474,11 +3580,12 @@ export class Engine {
3474
3580
  }
3475
3581
  /**
3476
3582
  * Something at or beneath `from` changed: every node from there up may match a `:has()` rule
3477
- * it did not, or stop matching one. They are matched again rather than marked for restyling,
3478
- * which would restyle everything under them. Only sheets that use `:has()` pay.
3583
+ * it did not, or stop matching one, up to `upTo` and not it. They are matched again rather than
3584
+ * marked for restyling, which would restyle everything under them. Only sheets that use
3585
+ * `:has()` pay.
3479
3586
  */
3480
- markBeneath(from) {
3481
- for (let node = from; node; node = node.parent) {
3587
+ markBeneath(from, upTo = null) {
3588
+ for (let node = from; node && node !== upTo; node = node.parent) {
3482
3589
  node.hasDirty = true;
3483
3590
  if (this.hasReaders.size)
3484
3591
  this.changedBeneath.add(node);
@@ -3495,20 +3602,49 @@ export class Engine {
3495
3602
  * Have matched again what is under each box that changed beneath and that such a rule could
3496
3603
  * be asking: the nodes the rule is for, and no other. Once for a commit, however many changes
3497
3604
  * there were beneath the box: a table gains its rows one at a time.
3605
+ *
3606
+ * A rule asked of a box above is walked under the highest box that asks it, which reaches what
3607
+ * is under the boxes beneath that one as well: a rule `:has()` asks of any box is asked by every
3608
+ * box from the change up to the root, and walked once, from the root. The rules a box asks are
3609
+ * walked together, the tree beneath it once.
3498
3610
  */
3499
3611
  markUnderHas() {
3500
3612
  if (!this.changedBeneath.size)
3501
3613
  return;
3502
- for (const node of this.changedBeneath) {
3503
- for (const readers of this.hasReaders.values()) {
3504
- for (const { classes, subjects } of readers) {
3505
- if (classes.every((name) => node.classes?.has(name)))
3506
- this.markUnder(node, subjects);
3507
- }
3508
- }
3614
+ const readers = [...this.hasReaders.values()].flat();
3615
+ const depths = new Map();
3616
+ for (const node of this.changedBeneath)
3617
+ depthOf(node, depths);
3618
+ const boxes = [...this.changedBeneath].sort((a, b) => depths.get(a) - depths.get(b));
3619
+ // By box walked, the rules walked under it and under every box above it.
3620
+ const walked = new Map();
3621
+ for (const node of boxes) {
3622
+ let above;
3623
+ for (let up = node.parent; up && !above; up = up.parent)
3624
+ above = walked.get(up);
3625
+ const all = this.markUnderBox(node, readers, above);
3626
+ if (all)
3627
+ walked.set(node, all);
3509
3628
  }
3510
3629
  this.changedBeneath.clear();
3511
3630
  }
3631
+ /**
3632
+ * `markUnderHas` for one box: the rules it asks that no box above it was walked for, walked
3633
+ * together. What was walked under it and above it, which is `above` where it asks no more.
3634
+ */
3635
+ markUnderBox(node, readers, above) {
3636
+ const asked = [];
3637
+ for (const reader of readers) {
3638
+ if (above?.has(reader) || !reader.classes.every((name) => node.classes?.has(name)))
3639
+ continue;
3640
+ asked.push(reader);
3641
+ }
3642
+ if (!asked.length)
3643
+ return above;
3644
+ const subjects = asked.map((reader) => reader.subjects);
3645
+ this.markUnder(node, subjects.length === 1 ? subjects[0] : joinSubjects(subjects));
3646
+ return new Set([...(above ?? []), ...asked]);
3647
+ }
3512
3648
  /**
3513
3649
  * What `node` matches changed, which the elements after it can read: as many of them as a rule
3514
3650
  * steps along from one, every one for a `~`, and none where no sheet steps at all.
@@ -3694,6 +3830,7 @@ export class Engine {
3694
3830
  this.stats.slowCommits++;
3695
3831
  }
3696
3832
  this.flushTransitionEvents();
3833
+ this.reportPaint();
3697
3834
  this.playSized();
3698
3835
  this.scrollDriver?.afterCommit();
3699
3836
  // Read first: the commit that settles keyframes starts by clearing it.
@@ -3735,14 +3872,14 @@ export class Engine {
3735
3872
  /** The first commit whose new views have not been drawn yet, while `commitUnseen` runs. */
3736
3873
  unseen = Infinity;
3737
3874
  /**
3738
- * A commit for what changed in the same turn as earlier commits, before a frame was drawn.
3739
- * `since` is how many commits there had been before the first of them (`stats.commits`).
3875
+ * A commit for what changed after earlier commits, before a frame has drawn them. `since` is
3876
+ * how many commits there had been before the first of them (`stats.commits`).
3740
3877
  *
3741
3878
  * A view those commits created has not been seen in the style they gave it, so what this one
3742
- * gives it is where it starts, and no transition runs toward it. That is what a browser
3743
- * does with a class added to an element in the turn that inserted it, and what `animate.enter`
3744
- * is built on: Angular adds the enter class after the render pass that created the element, and
3745
- * takes it off a frame later, which is the one change the transition is for.
3879
+ * gives it is where it starts, and no transition runs toward it. That is what a browser does
3880
+ * with a class added to an element before the frame after it was inserted, and what
3881
+ * `animate.enter` is built on: Angular adds the enter class after the render pass that created
3882
+ * the element, and takes it off a frame later, which is the one change the transition is for.
3746
3883
  */
3747
3884
  commitUnseen(since) {
3748
3885
  this.unseen = since;
@@ -3902,7 +4039,6 @@ export class Engine {
3902
4039
  const resolved = props['pointerEvents'];
3903
4040
  writeOwnProps(node.props, props, node.attributeOnly);
3904
4041
  withTextContent(node, viewName, props);
3905
- renameFonts(viewName, props, this.postScriptName);
3906
4042
  truncatedAs(node, props);
3907
4043
  const cascaded = props['transform'];
3908
4044
  const inline = inlineOf(node);
@@ -3920,6 +4056,8 @@ export class Engine {
3920
4056
  flattenStyle(node.props[STYLE_OVERRIDE], style);
3921
4057
  this.fontFaces.apply(style);
3922
4058
  holdLoadingFamily(node, style);
4059
+ // Once the face is matched: a family a view's own font names is the face its weight chose.
4060
+ renameFonts(viewName, style, this.postScriptName);
3923
4061
  if (viewName === PARAGRAPH) {
3924
4062
  alignText(style, this.directionOf(node, style));
3925
4063
  this.wholeWords(node, style);
@@ -3928,7 +4066,7 @@ export class Engine {
3928
4066
  if (this.fontsRefreshed)
3929
4067
  this.capForFonts(node, style);
3930
4068
  alignMultiline(viewName, style);
3931
- rowsTall(style, this.fontScale);
4069
+ rowsTall(viewName, style, this.fontScale);
3932
4070
  const merged = composeTransform(node, this.eased(node, style));
3933
4071
  // On iOS: Android's field says the baseline of the line it is sized and centred by.
3934
4072
  // Still under the row that marked it: a row taken away is not committed again to unmark it.
@@ -4055,6 +4193,11 @@ export class Engine {
4055
4193
  * its parent's: the cascade's, and over it the parent's inline style.
4056
4194
  */
4057
4195
  movePaint(node, merged) {
4196
+ if (node.paintTaker) {
4197
+ // Only the colour is handed over: a background image stays painted on the node.
4198
+ this.takenPaint(node, merged['backgroundColor']);
4199
+ delete merged['backgroundColor'];
4200
+ }
4058
4201
  if (node.paintsOn)
4059
4202
  for (const key of PAINT_KEYS)
4060
4203
  delete merged[key];
@@ -4066,6 +4209,13 @@ export class Engine {
4066
4209
  if (paint[key] !== undefined)
4067
4210
  merged[key] = paint[key];
4068
4211
  }
4212
+ /** Note the background colour a node that hands it over resolves to, where it changed. */
4213
+ takenPaint(node, color) {
4214
+ if (color === node.paintTaken)
4215
+ return;
4216
+ node.paintTaken = color;
4217
+ this.paintReports.add(node);
4218
+ }
4069
4219
  /**
4070
4220
  * A container whose direction or alignment changed since a child with a `fit-content` size
4071
4221
  * read it, or whose height stopped or started being one a percentage is taken of since a child
@@ -4890,13 +5040,13 @@ export class Engine {
4890
5040
  /**
4891
5041
  * Commit where it ends each transition of a size or a place that just started, for native to
4892
5042
  * move the view there: a layout animation, which the commit asks for once it is known that
4893
- * nothing else in it moves. One still on its way is committed where it ends too.
5043
+ * nothing else in it moves. One still on its way is committed where it ends too. On a host
5044
+ * with no layout animation they are eased from JavaScript, but for one sent on again and again.
4894
5045
  *
4895
5046
  * A width eased from JavaScript is a commit a frame, and a commit lays the whole screen out.
4896
5047
  */
4897
5048
  layEased(node, state, started, props, now) {
4898
- if (!this.fabric.configureNextLayoutAnimation)
4899
- return;
5049
+ const native = this.fabric.configureNextLayoutAnimation !== undefined;
4900
5050
  for (const key of started) {
4901
5051
  const eased = state.get(key);
4902
5052
  if (!LAID_OUT.has(key) || eased.start > now || eased.duration <= 0)
@@ -4908,6 +5058,8 @@ export class Engine {
4908
5058
  props[key] = eased.to;
4909
5059
  continue;
4910
5060
  }
5061
+ if (!native)
5062
+ continue;
4911
5063
  eased.laid = true;
4912
5064
  this.laying.push({ node, key, eased });
4913
5065
  }
@@ -4920,10 +5072,11 @@ export class Engine {
4920
5072
  /**
4921
5073
  * Whether a transition of a size is one sent on again and again, each before the last had
4922
5074
  * ended: a progress bar that steps every 50ms and eases each step over 150. Native moves the
4923
- * view, and lays the screen out, every frame while a layout animation runs, so one started at
4924
- * every step is one that never stops. The first, and one sent back before it ends, are
4925
- * played; from the second sent on in a row, each is committed where it ends, and the steps
4926
- * are the motion. Played again once the size has been left alone for as long as it lasts.
5075
+ * view, and lays the screen out, every frame while a layout animation runs, and JavaScript
5076
+ * commits every frame while it eases one, so one started at every step is one that never
5077
+ * stops. The first, and one sent back before it ends, are played; from the second sent on in
5078
+ * a row, each is committed where it ends, and the steps are the motion. Played again once the
5079
+ * size has been left alone for as long as it lasts.
4927
5080
  */
4928
5081
  sentOnAgain(node, key, eased, now) {
4929
5082
  const starts = this.laidStarts.get(node) ?? this.laidStarts.set(node, new Map()).get(node);
@@ -5323,15 +5476,21 @@ export class Engine {
5323
5476
  }
5324
5477
  return this.animating;
5325
5478
  }
5326
- /** The host's conversions, applied to a copy. A removed prop travels as null and stays null. */
5327
- processed(props) {
5479
+ /**
5480
+ * The host's conversions, applied to a copy. A removed prop travels as `cleared` says, null for
5481
+ * nearly every one. On an update a colour dropped as unreadable is null too: JSI leaves out an
5482
+ * undefined key, and Fabric keeps what a clone leaves out.
5483
+ */
5484
+ processed(props, update) {
5328
5485
  const out = {};
5329
5486
  for (const key of Object.keys(props)) {
5330
5487
  const value = props[key];
5331
- if (value === null || value === undefined)
5488
+ if (value === null)
5489
+ out[key] = cleared(key);
5490
+ else if (value === undefined)
5332
5491
  out[key] = value;
5333
5492
  else if (isColorProp(key))
5334
- out[key] = this.painted(key, value);
5493
+ out[key] = this.colourProp(key, value, update);
5335
5494
  // A list of sources has already been resolved by whoever built it, as RN's resolver
5336
5495
  // also assumes: it passes any object through and only turns a number into one.
5337
5496
  else if (ASSET_PROPS.has(key) && !Array.isArray(value))
@@ -5493,7 +5652,7 @@ export class Engine {
5493
5652
  this.noteMoved(node, propsPayload, childHandles.length !== previous.childHandles.length, previous.props['text']);
5494
5653
  let handle = previous.handle;
5495
5654
  if (childrenChanged || propsPayload) {
5496
- handle = this.clone(previous.handle, propsPayload && this.processed(propsPayload), childrenChanged ? childHandles : null);
5655
+ handle = this.clone(previous.handle, propsPayload && this.processed(propsPayload, true), childrenChanged ? childHandles : null);
5497
5656
  // A clone is the same view, so it keeps the tag it was created with, not the newest tag
5498
5657
  // handed out, which is some other node's.
5499
5658
  node.committed = { handle, tag: previous.tag, props, childHandles, viewName };
@@ -5838,7 +5997,7 @@ export class Engine {
5838
5997
  ...STAND_INS[name],
5839
5998
  };
5840
5999
  const payload = diffProps(kept.committed.props, props);
5841
- const handle = this.clone(kept.committed.handle, payload && this.processed(payload), []);
6000
+ const handle = this.clone(kept.committed.handle, payload && this.processed(payload, true), []);
5842
6001
  kept.committed = { ...kept.committed, handle, props, childHandles: [] };
5843
6002
  kept.node = null;
5844
6003
  }
@@ -5936,7 +6095,7 @@ export class Engine {
5936
6095
  const tag = nextTag();
5937
6096
  this.stats.createdNodes++;
5938
6097
  // instanceHandle is the retained node itself: it is what Fabric hands back on events.
5939
- const handle = this.fabric.createNode(tag, viewName, this.rootTag, this.processed(props), node);
6098
+ const handle = this.fabric.createNode(tag, viewName, this.rootTag, this.processed(props, false), node);
5940
6099
  for (const child of childHandles)
5941
6100
  this.fabric.appendChild(handle, child);
5942
6101
  node.committed = { handle, tag, props, childHandles, viewName };
@@ -6228,17 +6387,29 @@ export class Engine {
6228
6387
  resolveAsset(value) {
6229
6388
  return this.resolveAssetSource(value);
6230
6389
  }
6390
+ /** A colour prop as `processed` sends it: one dropped as unreadable is null on an update. */
6391
+ colourProp(key, value, update) {
6392
+ const painted = this.painted(key, value);
6393
+ return painted === undefined && update ? null : painted;
6394
+ }
6231
6395
  /**
6232
6396
  * A colour prop as native takes it. A shape's `fill` or `stroke` the cascade settled from a
6233
- * token is the solid brush react-native-svg takes, and `none` no paint at all.
6397
+ * token is the solid brush react-native-svg takes, `none` no paint at all, and one the host
6398
+ * reads no colour in no paint of the shape's own.
6234
6399
  */
6235
6400
  painted(key, value) {
6236
6401
  if (!BRUSH_PROPS.has(key))
6237
6402
  return this.color(value);
6238
- // Already a brush, as the icon wrote it: only one the cascade settled is still text.
6403
+ // Already a brush, as the icon wrote it: only one the cascade settled is still text. An
6404
+ // icon's solid brush holds whatever the host read, which is nothing for an unreadable colour.
6239
6405
  if (typeof value !== 'string')
6240
- return value;
6241
- return value === 'none' ? null : { type: 0, payload: this.color(value) };
6406
+ return emptyBrush(value) ? undefined : value;
6407
+ if (value === 'none')
6408
+ return null;
6409
+ // A colour the host cannot read is dropped, as a browser drops it: a solid brush with no
6410
+ // colour in it stops the app.
6411
+ const payload = this.color(value);
6412
+ return payload == null ? undefined : { type: 0, payload };
6242
6413
  }
6243
6414
  /**
6244
6415
  * The host's `processColor`, and the one place a platform colour becomes one.