react-x11 2.22.8 → 2.22.10

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.
@@ -318,13 +318,22 @@ export class NodePaint {
318
318
  return false;
319
319
  }
320
320
 
321
- _paintChildren(ctx) {
321
+ /**
322
+ * The children, in paint order — or those up to and including `upTo`: an
323
+ * X11 pane over a `<glarea>` holds its own child and what is under it,
324
+ * never what is over it (src/gloverlay.js, `_paintPane`).
325
+ */
326
+ _paintChildren(ctx, upTo = null) {
322
327
  // A retained presenter replays a node's `paint` into a visual of that
323
328
  // node's own — its children have visuals of their own, so it sets this
324
329
  // for the duration of the call (src/cocoa/presenter.js, `paintSelf`).
325
330
  // Never set on the X11 or surface paths, where a frame is one walk.
326
331
  if (this._ownPaintOnly) return;
327
- const order = this.paintOrder();
332
+ let order = this.paintOrder();
333
+ if (upTo !== null) {
334
+ const at = order.indexOf(upTo);
335
+ if (at !== -1) order = order.slice(0, at + 1);
336
+ }
328
337
  if (order.length === 0) return;
329
338
  if (!this.clipsChildren()) {
330
339
  this._paintChildList(ctx, order);
@@ -104,8 +104,9 @@ export const mainAxisOf = (node) => {
104
104
  /**
105
105
  * Whether `child` is one the floors are **written on** along `axis`: a flex
106
106
  * item on its container's main axis whose author left the minimum to the
107
- * content. The same test `writeFloors` applies, asked ahead of time — it is
108
- * what decides whether a stale extent is one anybody will read.
107
+ * content, and which flex could squeeze below it. The same test
108
+ * `writeFloors` applies, asked ahead of time — it is what decides whether a
109
+ * stale extent is one anybody will read.
109
110
  */
110
111
  export function receivesFloor(child, axis) {
111
112
  const parent = child.parent;
@@ -118,7 +119,37 @@ export function receivesFloor(child, axis) {
118
119
  return false;
119
120
  }
120
121
  const own = axis === 'width' ? 'minWidth' : 'minHeight';
121
- return typeof child.style[own] !== 'number';
122
+ return typeof child.style[own] !== 'number' && !floorIsNamedSize(child, axis);
123
+ }
124
+
125
+ /**
126
+ * Whether this item is its named size on `axis` in every pass, so that the
127
+ * floor a measurement would write is that same number and can never bind:
128
+ * a number for its size along the axis, as its flex basis (`flexBasis` unset
129
+ * or `'auto'`), `flexShrink: 0` and no `flexGrow`. Flex then takes its basis
130
+ * from that number and neither shrinks nor grows it, and measuring it is work
131
+ * nobody reads. A table's cells, at their columns' widths, are the case that
132
+ * paid: every row that scrolled into a `<Table>` was laid out alone for their
133
+ * floors before its frame could be, over half of a fling's frame time.
134
+ *
135
+ * A differential run over random trees, laid out with and without the
136
+ * rule, found why each clause is there. An auto basis is yoga's own
137
+ * measurement of the content, which can come out short of what the content
138
+ * needs, and the floor is what holds the item open. A named size that can
139
+ * grow is written as the size it grew to in the measuring pass, which the
140
+ * floor then keeps. And a `flexBasis` of its own is where the basis comes
141
+ * from instead of the size, and a percentage one is a different length in a
142
+ * measuring pass than in the real one.
143
+ */
144
+ function floorIsNamedSize(child, axis) {
145
+ const style = child.style;
146
+ const basis = style.flexBasis;
147
+ return (
148
+ style.flexShrink === 0 &&
149
+ !(style.flexGrow > 0) &&
150
+ (basis === undefined || basis === 'auto') &&
151
+ typeof (axis === 'width' ? style.width : style.height) === 'number'
152
+ );
122
153
  }
123
154
 
124
155
  /**
@@ -848,7 +879,12 @@ export function writeFloors(node, axis) {
848
879
  // A floor of 0 is what yoga does anyway, and an author who named their
849
880
  // own `minWidth`/`minHeight` has already answered — overwriting it would
850
881
  // put a measurement of ours above a number they wrote.
851
- if (axisIsMain && inFlow(child) && typeof child.style[own] !== 'number') {
882
+ if (
883
+ axisIsMain &&
884
+ inFlow(child) &&
885
+ typeof child.style[own] !== 'number' &&
886
+ !floorIsNamedSize(child, axis)
887
+ ) {
852
888
  const extent = horizontal ? child._floorW : child._floorH;
853
889
  if (extent > 0) floor = extent;
854
890
  }
package/src/win32/a11y.js CHANGED
@@ -33,24 +33,21 @@ import {
33
33
  } from '../a11y.js';
34
34
  import { synthesizeClick } from '../events.js';
35
35
  import { onApp } from '../trace-registry.js';
36
+ import { A11yPacer } from '../a11ypace.js';
36
37
 
37
38
  const TRACE = process.env.REACT_X11_TRACE_A11Y === '1';
38
39
 
39
- /**
40
- * How often commits may push the mirror, in ms. A commit that follows a
41
- * quiet spell pushes at once; a stream of them — a pan, a drag, a value
42
- * ticking — pushes once per interval, the last of them catching up.
43
- *
44
- * UIA's clients are almost always listening on a desktop Windows (the
45
- * touch keyboard, the text services and others ask for the tree), so the
46
- * push is not the rare case the listening gate makes it elsewhere: a walk
47
- * of the window and a marshalled diff, on every commit. For a graph pane
48
- * whose every item moves on every step of a pan that was 1.6 ms of each
49
- * frame. A screen reader reads positions on demand and does not need
50
- * sixty of them a second; a focus change, which it is waiting on, still
51
- * pushes at once (`hooks.focus`), as does an announcement.
52
- */
53
- const COMMIT_PUSH_MS = 100;
40
+ // How often commits may push the mirror is src/a11ypace.js's to say, for
41
+ // this bridge and the AT-SPI one alike: at once after a quiet spell, and for
42
+ // a stream of commits (a pan, a drag, a value ticking) once per interval,
43
+ // the last push catching up. UIA's clients are almost always listening on a
44
+ // desktop Windows (the touch keyboard, the text services and others ask for
45
+ // the tree), so pushing is not the rare case the listening gate makes it
46
+ // elsewhere. For a graph pane whose every item moves on every step of a pan,
47
+ // a push per commit was 1.6 ms of each frame. A screen reader reads positions
48
+ // on demand and does not need sixty of them a second. What it does wait on
49
+ // is pushed at once: a focus change (`hooks.focus`), an announcement, and a
50
+ // change to the focused element itself (`_touch`).
54
51
 
55
52
  // --------------------------------------------------------------------------
56
53
  // The one translation that is Windows' own
@@ -316,9 +313,12 @@ export class Win32Accessibility {
316
313
  this._pushed = new Set();
317
314
  this._unsubscribes = [];
318
315
  this.dead = false;
319
- /** When a commit last pushed, and the push a stream of commits owes. */
320
- this._lastCommitPush = -Infinity;
321
- this._trailing = null;
316
+ /** When commits may push (src/a11ypace.js). */
317
+ this._pacer = new A11yPacer();
318
+ /** The focused node, and whether a commit changed it: its own changes
319
+ * are what a screen reader is waiting to hear, and are not paced. */
320
+ this._focused = null;
321
+ this._urgentPending = false;
322
322
  }
323
323
 
324
324
  _idOf(node) {
@@ -454,23 +454,21 @@ export class Win32Accessibility {
454
454
  }
455
455
  }
456
456
 
457
- /** A commit's push, at most once per `COMMIT_PUSH_MS`. */
457
+ /** A commit's push, paced (src/a11ypace.js), and at once where it
458
+ * changed the focused element. */
458
459
  _commitFlush() {
459
460
  if (this.dead || this._dirty.size === 0) return;
460
- const now = performance.now();
461
- const wait = this._lastCommitPush + COMMIT_PUSH_MS - now;
462
- if (wait <= 0) {
463
- this._lastCommitPush = now;
464
- this.flush();
465
- return;
461
+ const urgent = this._urgentPending;
462
+ this._urgentPending = false;
463
+ if (urgent) this._pacer.cancel();
464
+ if (urgent || this._pacer.ready(() => this._pacedFlush())) {
465
+ this._pacedFlush();
466
466
  }
467
- if (this._trailing) return;
468
- this._trailing = setTimeout(() => {
469
- this._trailing = null;
470
- this._lastCommitPush = performance.now();
471
- this.flush();
472
- }, wait);
473
- this._trailing.unref?.();
467
+ }
468
+
469
+ _pacedFlush() {
470
+ this._pacer.pushed();
471
+ this.flush();
474
472
  }
475
473
 
476
474
  /** A window's HWND exists now, so what is owed for it can be pushed. */
@@ -501,9 +499,19 @@ export class Win32Accessibility {
501
499
  return root && this.toplevels.includes(root) ? root : null;
502
500
  }
503
501
 
504
- _touch(node) {
502
+ /** `own`: the change is the node's own (its props or text), which is
503
+ * urgent where the node is, or is inside, the focused element. */
504
+ _touch(node, own = false) {
505
505
  const win = this._toplevelOf(node);
506
- if (win) this._dirty.add(win);
506
+ if (!win) return;
507
+ this._dirty.add(win);
508
+ if (!own || this._focused === null || this._urgentPending) return;
509
+ for (let n = node; n; n = n.parent) {
510
+ if (n === this._focused) {
511
+ this._urgentPending = true;
512
+ return;
513
+ }
514
+ }
507
515
  }
508
516
 
509
517
  // ---- what the shell asks for ------------------------------------------
@@ -594,10 +602,11 @@ export class Win32Accessibility {
594
602
  };
595
603
  hooks.attached = (parent) => this._touch(parent);
596
604
  hooks.detach = (parent) => this._touch(parent);
597
- hooks.propsChanged = (node) => this._touch(node);
598
- hooks.textContent = (chunk) => this._touch(chunk.parent ?? chunk);
599
- hooks.textState = (node) => this._touch(node);
605
+ hooks.propsChanged = (node) => this._touch(node, true);
606
+ hooks.textContent = (chunk) => this._touch(chunk.parent ?? chunk, true);
607
+ hooks.textState = (node) => this._touch(node, true);
600
608
  hooks.focus = (previous, next) => {
609
+ this._focused = next ?? null;
601
610
  const win = this._toplevelOf(next ?? previous);
602
611
  if (!win) return;
603
612
  this._dirty.add(win);
@@ -637,8 +646,7 @@ export class Win32Accessibility {
637
646
  bury() {
638
647
  if (this.dead) return;
639
648
  this.dead = true;
640
- if (this._trailing) clearTimeout(this._trailing);
641
- this._trailing = null;
649
+ this._pacer.cancel();
642
650
  for (const key of Object.keys(hooks)) hooks[key] = null;
643
651
  for (const unsubscribe of this._unsubscribes) unsubscribe();
644
652
  this._unsubscribes = [];