react-x11 2.11.0 → 2.12.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 +278 -129
- package/package.json +10 -3
- package/src/Reconciler.js +15 -17
- package/src/a11y.js +2 -2
- package/src/anchor.js +7 -5
- package/src/bootstrap.js +14 -0
- package/src/clientmessage.js +1 -1
- package/src/cocoa/app.js +292 -49
- package/src/cocoa/bezels.js +175 -30
- package/src/cocoa/dnd.js +27 -13
- package/src/cocoa/fonts.js +3 -3
- package/src/cocoa/glarea.js +20 -3
- package/src/cocoa/main.d.ts +8 -0
- package/src/cocoa/main.js +43 -0
- package/src/cocoa/panehost.js +15 -5
- package/src/cocoa/presenter.js +13 -9
- package/src/cocoa/promotion.js +4 -7
- package/src/cocoa/relaunch.js +207 -0
- package/src/cocoa/threaded.js +246 -0
- package/src/cocoa/window.js +256 -42
- package/src/components/Select.js +2 -2
- package/src/components/anchor.js +3 -3
- package/src/components/native.js +12 -7
- package/src/components/theme.js +2 -2
- package/src/debug.js +1 -1
- package/src/decorations.js +1 -1
- package/src/editmenu.js +2 -2
- package/src/errors.js +46 -0
- package/src/events.js +6 -6
- package/src/foreignnodes.js +3 -2
- package/src/frames.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1653 -0
- package/src/host.d.ts +230 -0
- package/src/host.js +11 -3
- package/src/imagesource.js +1 -1
- package/src/index.d.ts +21 -4
- package/src/index.js +9 -1
- package/src/layouts.js +721 -0
- package/src/node.d.ts +4 -2
- package/src/node.js +19 -21
- package/src/nodes/animation.js +644 -0
- package/src/nodes/box.js +21 -0
- package/src/nodes/boxpaint.js +473 -0
- package/src/nodes/canvas.js +269 -0
- package/src/nodes/cascade.js +600 -0
- package/src/nodes/damage.js +183 -0
- package/src/nodes/edithistory.js +124 -0
- package/src/nodes/editmenupopup.js +260 -0
- package/src/nodes/hittest.js +185 -0
- package/src/nodes/image.js +266 -0
- package/src/nodes/install.js +75 -0
- package/src/nodes/invalidate.js +465 -0
- package/src/nodes/kinds.js +31 -0
- package/src/nodes/layout.js +439 -0
- package/src/nodes/layouthost.js +949 -0
- package/src/nodes/node.js +868 -0
- package/src/nodes/paint.js +466 -0
- package/src/nodes/position.js +366 -0
- package/src/nodes/preedit.js +127 -0
- package/src/nodes/queries.js +330 -0
- package/src/nodes/rects.js +102 -0
- package/src/nodes/scrollable.js +891 -0
- package/src/nodes/scrollbars.js +138 -0
- package/src/nodes/scrollblit.js +1034 -0
- package/src/nodes/selectable.js +142 -0
- package/src/nodes/styling.js +225 -0
- package/src/nodes/text.js +649 -0
- package/src/nodes/textarea.js +391 -0
- package/src/nodes/textinput.js +1146 -0
- package/src/nodes/util.js +17 -0
- package/src/nodes/window/anchoring.js +161 -0
- package/src/nodes/window/capabilities.js +190 -0
- package/src/nodes/window/debugpaint.js +83 -0
- package/src/nodes/window/droptarget.js +145 -0
- package/src/nodes/window/floors.js +577 -0
- package/src/nodes/window/flush.js +334 -0
- package/src/nodes/window/hints.js +482 -0
- package/src/nodes/window/listeners.js +222 -0
- package/src/nodes/window/popup.js +71 -0
- package/src/nodes/window/size.js +591 -0
- package/src/nodes/window/window.js +945 -0
- package/src/palette.js +1 -1
- package/src/registry.js +7 -3
- package/src/styles.js +137 -15
- package/src/svgnodes.js +2 -1
- package/src/testing/harness.js +2 -2
- package/src/textselection.js +5 -3
- package/src/trace-registry.js +1 -1
- package/src/types/components.d.ts +38 -6
- package/src/types/elements.d.ts +11 -1
- package/src/types/nodes.d.ts +17 -2
- package/src/types/style.d.ts +94 -3
- package/src/windowstate.js +1 -1
- package/src/yoga.js +1 -1
- package/src/nodes.js +0 -13120
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
// The retained node tree's base class: one lightweight JS node per host
|
|
2
|
+
// element, one yoga node per drawn element, painted into the owning
|
|
3
|
+
// <window>'s single 2d context on ntk's frame clock. Only <window> owns a
|
|
4
|
+
// real X11 window (see NEXT_STEPS.md §4 for the rationale).
|
|
5
|
+
//
|
|
6
|
+
// This file is what every node is: construction, props, the child list and
|
|
7
|
+
// its yoga tree, focus and the default actions. The rest of Node lives with
|
|
8
|
+
// the concern each method serves — styling.js, cascade.js, layout.js,
|
|
9
|
+
// paint.js and the others beside this file — and installMethods() at the
|
|
10
|
+
// bottom puts it on the prototype. install.js explains the arrangement.
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
applyLayoutStyle,
|
|
14
|
+
applyLayoutDefaults,
|
|
15
|
+
createLayoutNode,
|
|
16
|
+
paintPropsChanged,
|
|
17
|
+
isEventProp,
|
|
18
|
+
} from '../styles.js';
|
|
19
|
+
import { Yoga } from '../yoga.js';
|
|
20
|
+
import { isPlaced } from '../layouts.js';
|
|
21
|
+
import { synthesizeClick } from '../events.js';
|
|
22
|
+
import { hasDropProps } from '../dnd.js';
|
|
23
|
+
import {
|
|
24
|
+
hooks as a11yHooks,
|
|
25
|
+
devCheckA11yProps,
|
|
26
|
+
hasClickHandler,
|
|
27
|
+
} from '../a11y.js';
|
|
28
|
+
import { XK_RETURN, XK_KP_ENTER, XK_SPACE } from '../keysyms.js';
|
|
29
|
+
import { dropVisibleSelection, selectionSurfaceOf } from '../textselection.js';
|
|
30
|
+
import { NodeAnimation } from './animation.js';
|
|
31
|
+
import { NodeBoxPaint } from './boxpaint.js';
|
|
32
|
+
import { NodeCascade } from './cascade.js';
|
|
33
|
+
import { NO_DAMAGE } from './damage.js';
|
|
34
|
+
import { NodeHitTest } from './hittest.js';
|
|
35
|
+
import { installMethods } from './install.js';
|
|
36
|
+
import { NodeInvalidate } from './invalidate.js';
|
|
37
|
+
import { CUSTOM_SELF_DAMAGED } from './kinds.js';
|
|
38
|
+
import { NodeLayout } from './layout.js';
|
|
39
|
+
import { NodeLayoutHost } from './layouthost.js';
|
|
40
|
+
import { NodePaint } from './paint.js';
|
|
41
|
+
import { NodePosition } from './position.js';
|
|
42
|
+
import { NodeQueries } from './queries.js';
|
|
43
|
+
import { NodeScrollBlit } from './scrollblit.js';
|
|
44
|
+
import { NodeSelectable } from './selectable.js';
|
|
45
|
+
import { NodeStyling } from './styling.js';
|
|
46
|
+
import { DEV } from './util.js';
|
|
47
|
+
|
|
48
|
+
// DevTools' measureHostInstance dereferences instance.ownerDocument
|
|
49
|
+
// unconditionally once getClientRects exists; a null documentElement and
|
|
50
|
+
// defaultView give it zero scroll offsets and no crash.
|
|
51
|
+
export const DEVTOOLS_FAKE_DOCUMENT = {
|
|
52
|
+
documentElement: null,
|
|
53
|
+
defaultView: null,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// And the default for the other declaration: an element that has not said
|
|
57
|
+
// otherwise claims nothing for itself, so every prop it holds is core's to
|
|
58
|
+
// be conservative about.
|
|
59
|
+
const NO_SELF_DAMAGED = new Set();
|
|
60
|
+
|
|
61
|
+
/** The half of `Node._joinsYoga` that is about the child alone — a real X
|
|
62
|
+
* window (`<window>`, `<popup>`) or a node built without a box at all (a text
|
|
63
|
+
* chunk) sits outside whatever parent it lands in. This is what
|
|
64
|
+
* `_nonYogaKids` counts, so the count stays right for a parent that has no
|
|
65
|
+
* box of its own either. */
|
|
66
|
+
const outsideYoga = (child) => !child.yoga || child.isWindow;
|
|
67
|
+
|
|
68
|
+
export class Node {
|
|
69
|
+
get ownerDocument() {
|
|
70
|
+
return DEVTOOLS_FAKE_DOCUMENT;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
constructor(kind, props, app, { yoga = true } = {}) {
|
|
74
|
+
this.kind = kind;
|
|
75
|
+
this.props = props;
|
|
76
|
+
this.app = app;
|
|
77
|
+
this.parent = null;
|
|
78
|
+
this.children = [];
|
|
79
|
+
// Where this node sits in `parent.children`, and how many of *this*
|
|
80
|
+
// node's children sit outside its yoga tree. Both are bookkeeping that
|
|
81
|
+
// turns the scans `insertBefore` used to do over the whole child list
|
|
82
|
+
// into constant work, which is what stops a commit that mounts a
|
|
83
|
+
// virtualized list's window from costing O(rows x pane) (issue #397).
|
|
84
|
+
// The index is a hint — `_indexOfChild` proves it before using it — and
|
|
85
|
+
// the count is exact, maintained by the three places `children` is
|
|
86
|
+
// spliced.
|
|
87
|
+
this._childIndex = -1;
|
|
88
|
+
this._nonYogaKids = 0;
|
|
89
|
+
// The pre-mutation bounds this frame already claimed for this node, so
|
|
90
|
+
// that a second mutation reuses the rect instead of walking the subtree
|
|
91
|
+
// again. Lives exactly as long as membership in `root._reflowed`.
|
|
92
|
+
this._reflowBefore = null;
|
|
93
|
+
// The automatic minimum size (#249), cached per node: what this node
|
|
94
|
+
// contributes to the box around it on each axis (`contentSpan`), the
|
|
95
|
+
// width the height was measured for (`probeHeightFloors`), and the
|
|
96
|
+
// floor yoga currently holds from us on each axis (`writeFloors`).
|
|
97
|
+
// `undefined` on an extent means it has to be measured again; `null`
|
|
98
|
+
// on a written floor means yoga's minimum may not be ours any more.
|
|
99
|
+
this._floorW = undefined;
|
|
100
|
+
this._floorH = undefined;
|
|
101
|
+
this._floorAtW = undefined;
|
|
102
|
+
this._floorMinW = undefined;
|
|
103
|
+
this._floorMinH = undefined;
|
|
104
|
+
// the width mode yoga last measured this leaf in with no height on
|
|
105
|
+
// offer — the question `_heightForWidth` repeats
|
|
106
|
+
this._floorMeasureMode = null;
|
|
107
|
+
this.root = null; // owning WindowNode once attached
|
|
108
|
+
this.hidden = false;
|
|
109
|
+
// Composited on a layer of its own above the window's bitmap, by a
|
|
110
|
+
// presenter that can (src/cocoa/promotion.js): the paint walk leaves a
|
|
111
|
+
// hole where it is, and the presenter draws it — the `<glarea>` idiom.
|
|
112
|
+
this._promoted = false;
|
|
113
|
+
this.destroyed = false;
|
|
114
|
+
// absolute rect within the owning window, filled by absolutize()
|
|
115
|
+
this.abs = { x: 0, y: 0, width: 0, height: 0 };
|
|
116
|
+
// node states that style blocks can react to, owned by EventManager
|
|
117
|
+
this.states = {
|
|
118
|
+
':hover': false,
|
|
119
|
+
':focus-within': false,
|
|
120
|
+
':focus': false,
|
|
121
|
+
':focus-visible': false,
|
|
122
|
+
':active': false,
|
|
123
|
+
':drag-over': false,
|
|
124
|
+
':dragging': false,
|
|
125
|
+
};
|
|
126
|
+
// in-flight animations: prop -> {from, to, start, duration}. Transitions
|
|
127
|
+
// delete themselves as they land; a loop entry (`loop: true`) is removed
|
|
128
|
+
// by `_updateLoops` and by nothing else
|
|
129
|
+
this._anim = null;
|
|
130
|
+
// False until the first frame places this node (`absolutize`). Read by
|
|
131
|
+
// `_retarget`: a style can be re-resolved several times between
|
|
132
|
+
// construction and that first frame — the attach-time theme merge is the
|
|
133
|
+
// common one, replacing a detached resolution against the desktop
|
|
134
|
+
// palette with one against the app's own — and none of those is a
|
|
135
|
+
// *change* the user saw, so no transition may start from it.
|
|
136
|
+
this._placed = false;
|
|
137
|
+
// the loops this node's style declares, whether or not they are running
|
|
138
|
+
this._loops = null;
|
|
139
|
+
// `resolvedTextStyle()`'s cache: this node's own text style over what it
|
|
140
|
+
// inherits. Undefined means "never asked", which is load-bearing — see
|
|
141
|
+
// `_retext`
|
|
142
|
+
this._resolvedText = undefined;
|
|
143
|
+
// `inheritedTextStyle`'s cache at a *scale boundary* — a node whose
|
|
144
|
+
// `scale` prop puts it in a different unit from its parent, so the
|
|
145
|
+
// device size it inherits has to be re-expressed. Null everywhere else,
|
|
146
|
+
// which is every node in a tree with no `scale` prop in it.
|
|
147
|
+
this._textScaled = null;
|
|
148
|
+
// `direction`'s cache, same contract as `_resolvedText`'s: undefined means
|
|
149
|
+
// "never asked", which is what lets `_redirectSubtree` stop at a node
|
|
150
|
+
// nothing below has resolved through
|
|
151
|
+
this._direction = undefined;
|
|
152
|
+
// hot pointer-path caches (issue #188): the children in paint order,
|
|
153
|
+
// re-verified against the live children on every read, and the
|
|
154
|
+
// subtree's hit reach, invalidated through _clearHitBounds()
|
|
155
|
+
this._paintOrderCache = null;
|
|
156
|
+
this._hitBoundsCache = null;
|
|
157
|
+
// a `$token` the theme does not define, held for `commitMount` to throw
|
|
158
|
+
// on this node's own fiber — see `_tokenProblem`. Strict mode only.
|
|
159
|
+
// `null` is "commitMount is still to come", `false` is "it has been and
|
|
160
|
+
// gone", and an Error is one waiting for it
|
|
161
|
+
this._tokenError = null;
|
|
162
|
+
// `'@container …'` blocks (styles.js), on the nodes that carry them and
|
|
163
|
+
// null on every other node: the style the record was built for, the
|
|
164
|
+
// container names it asks about (`''` for the unnamed ones), the sizes
|
|
165
|
+
// and answers the blocks last resolved to, and the pin an oscillating
|
|
166
|
+
// design is held at — see WindowNode._resolveContainerQueries. Before
|
|
167
|
+
// `_syncStyle`, which reads and writes it.
|
|
168
|
+
this._cq = null;
|
|
169
|
+
// A position placed after layout (`sticky`, or one registered with
|
|
170
|
+
// `registerPosition`): the paint reach this node was left at by the last
|
|
171
|
+
// pass that placed it — where its pixels are, which the next placement
|
|
172
|
+
// claims when it moves them (WindowNode._placeNodes) — the request its
|
|
173
|
+
// style resolved to, cached per style, and the definition that threw,
|
|
174
|
+
// which is not asked again until the style names another. Null on every
|
|
175
|
+
// node without one.
|
|
176
|
+
this._placedShown = null;
|
|
177
|
+
this._placementCache = null;
|
|
178
|
+
this._placementFailed = null;
|
|
179
|
+
// A layout algorithm (`layout`, docs/styling.md "Custom layouts"): the
|
|
180
|
+
// host state on a node that arranges its children with one — its
|
|
181
|
+
// children are yoga trees of their own then, and it a measured leaf —
|
|
182
|
+
// and, on each of those children, where the last placement put its
|
|
183
|
+
// margin box from the host's border box, whether it is one of the
|
|
184
|
+
// absolutely positioned ones, the handle the algorithm is given for it
|
|
185
|
+
// and its `layoutItem` resolved. Null everywhere else.
|
|
186
|
+
this._host = null;
|
|
187
|
+
this._hostSlot = null;
|
|
188
|
+
this._hostAbsolute = false;
|
|
189
|
+
this._handle = null;
|
|
190
|
+
this._itemCache = null;
|
|
191
|
+
// …and the sizes the algorithm was told it takes, for as long as nothing
|
|
192
|
+
// inside it changes — it asks the same questions every run, several runs
|
|
193
|
+
// a frame — and whether a placement ever gave it less height than its
|
|
194
|
+
// content, which from then on keeps its height floors measured
|
|
195
|
+
this._hostSizes = null;
|
|
196
|
+
this._hostSqueezed = false;
|
|
197
|
+
// the rect its tree was last laid out at by a placement, while nothing
|
|
198
|
+
// has laid it out since — the next placement at the same rect has
|
|
199
|
+
// nothing to do
|
|
200
|
+
this._hostLaidAt = null;
|
|
201
|
+
// the layout that threw, with the style value that named it: flexbox
|
|
202
|
+
// until the style names a different one
|
|
203
|
+
this._layoutAbandoned = null;
|
|
204
|
+
this._syncStyle(props);
|
|
205
|
+
this.yoga = yoga ? createLayoutNode() : null;
|
|
206
|
+
if (this.yoga) {
|
|
207
|
+
applyLayoutDefaults(this.yoga);
|
|
208
|
+
applyLayoutStyle(this.yoga, this.style);
|
|
209
|
+
// An element with a size of its own says so by implementing
|
|
210
|
+
// `measureContent`, and the base class is what wires it to layout —
|
|
211
|
+
// so a third-party element reaches everything that asks a leaf for its
|
|
212
|
+
// size, including the content floor `minWidth: 'auto'` is measured
|
|
213
|
+
// with (#248), without knowing either of them exists.
|
|
214
|
+
if (typeof this.measureContent === 'function') this._useMeasureContent();
|
|
215
|
+
if (this.style.layout != null || this.style.display === 'grid') {
|
|
216
|
+
this._syncLayoutHost();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// the document selection: the state when this element is a `selectable`
|
|
220
|
+
// surface, and the part of somebody else's that lands on this one
|
|
221
|
+
this._textSelection = null;
|
|
222
|
+
this._selRange = null;
|
|
223
|
+
// an element with a selection of its own — `<textinput>` — whose subtree
|
|
224
|
+
// a document around it skips whole rather than lighting up half of what
|
|
225
|
+
// the user is editing
|
|
226
|
+
this.hasOwnSelection = false;
|
|
227
|
+
this._syncSelectable(props);
|
|
228
|
+
if (DEV) devCheckA11yProps(this);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
get isWindow() {
|
|
232
|
+
return this.kind === 'window';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Prop names whose damage this element's own `applyProps` claims, and
|
|
237
|
+
* which `paintChanged` therefore does not claim the whole node for.
|
|
238
|
+
*
|
|
239
|
+
* Empty for everything that has not said otherwise, which is what keeps
|
|
240
|
+
* the default conservative. Registered elements declare theirs to
|
|
241
|
+
* `registerElement`, so the common case needs no subclass; an element
|
|
242
|
+
* whose answer depends on the *values* rather than the names overrides
|
|
243
|
+
* `paintChanged` instead.
|
|
244
|
+
*/
|
|
245
|
+
get selfDamagedProps() {
|
|
246
|
+
return CUSTOM_SELF_DAMAGED.get(this.kind) ?? NO_SELF_DAMAGED;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Number of yoga-bearing children before `index` (window children and
|
|
250
|
+
* text spans/chunks do not join the parent's yoga tree). */
|
|
251
|
+
_yogaIndexAt(index) {
|
|
252
|
+
// A list of ordinary boxes — a scroll pane's rows, which is the list
|
|
253
|
+
// this is asked about a hundred times in one commit — has every child in
|
|
254
|
+
// the yoga tree, and then the yoga index *is* the child index. Counting
|
|
255
|
+
// the exceptions as they arrive turns that answer into a read instead of
|
|
256
|
+
// a walk of every sibling in front of the new row (issue #397).
|
|
257
|
+
if (this._nonYogaKids === 0) return index;
|
|
258
|
+
let n = 0;
|
|
259
|
+
for (let i = 0; i < index; i++) {
|
|
260
|
+
if (this._joinsYoga(this.children[i])) n++;
|
|
261
|
+
}
|
|
262
|
+
return n;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
_joinsYoga(child) {
|
|
266
|
+
return Boolean(
|
|
267
|
+
this.yoga && child.yoga && !child.isWindow && this._host === null,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Where `child` sits in `this.children`.
|
|
273
|
+
*
|
|
274
|
+
* The cached slot is checked rather than trusted: a node appears in the
|
|
275
|
+
* list once, so `children[i] === child` *is* the proof that `i` is its
|
|
276
|
+
* index, and a cache that has gone stale costs a scan rather than a wrong
|
|
277
|
+
* answer. `_spliceChild` refreshes the two slots it knows — the child it
|
|
278
|
+
* placed and the sibling it pushed along — which is what keeps a run of
|
|
279
|
+
* inserts in front of the same trailing sibling (every virtualized list's
|
|
280
|
+
* commit) off the scan entirely.
|
|
281
|
+
*/
|
|
282
|
+
_indexOfChild(child) {
|
|
283
|
+
if (this.children[child._childIndex] === child) return child._childIndex;
|
|
284
|
+
const i = this.children.indexOf(child);
|
|
285
|
+
child._childIndex = i;
|
|
286
|
+
return i;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
appendChild(child) {
|
|
290
|
+
this.insertBefore(child, null);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Splice `child` in front of `beforeChild` (end of the list when that is
|
|
294
|
+
* null), first taking it out of its old slot: React reorders a keyed list
|
|
295
|
+
* by calling insertBefore with a child that is *already* mounted here, and
|
|
296
|
+
* without the removal it would appear twice. Returns the new index. */
|
|
297
|
+
_spliceChild(child, beforeChild) {
|
|
298
|
+
// `parent === this` is the cheap form of "already in this list" — the
|
|
299
|
+
// two are set and cleared together — so a child arriving for the first
|
|
300
|
+
// time, which is every node of a freshly mounted subtree, pays no scan
|
|
301
|
+
// at all for the question.
|
|
302
|
+
const from = child.parent === this ? this._indexOfChild(child) : -1;
|
|
303
|
+
if (from !== -1) this.children.splice(from, 1);
|
|
304
|
+
else if (outsideYoga(child)) this._nonYogaKids++;
|
|
305
|
+
const before = beforeChild == null ? -1 : this._indexOfChild(beforeChild);
|
|
306
|
+
const index = before === -1 ? this.children.length : before;
|
|
307
|
+
this.children.splice(index, 0, child);
|
|
308
|
+
// The two slots this splice knows. Every other cached index at or after
|
|
309
|
+
// `index` has shifted by one and will be caught by the check in
|
|
310
|
+
// `_indexOfChild`; these two are the ones a run of inserts in front of
|
|
311
|
+
// the same sibling asks about again on the very next call.
|
|
312
|
+
child._childIndex = index;
|
|
313
|
+
if (beforeChild != null) beforeChild._childIndex = index + 1;
|
|
314
|
+
return index;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
insertBefore(child, beforeChild) {
|
|
318
|
+
if (child.isPopup) {
|
|
319
|
+
// popups live anywhere in the JSX tree but are independent
|
|
320
|
+
// override-redirect windows: bookkeeping only, no yoga, no paint —
|
|
321
|
+
// but they do inherit the theme of where they are written
|
|
322
|
+
const mounting = child.parent == null;
|
|
323
|
+
this._spliceChild(child, beforeChild);
|
|
324
|
+
child.parent = this;
|
|
325
|
+
if (this.theme || child.props.theme) child._themeChanged(mounting);
|
|
326
|
+
a11yHooks.attached?.(this, child);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (child.isWindow) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`react-x11: <window> cannot be nested inside <${this.kind}>; ` +
|
|
332
|
+
'windows may only appear at the root or inside another <window>.',
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
// A registered element that declared childrenAllowed: false says so
|
|
336
|
+
// here, rather than laying out a child that will never paint. The flag
|
|
337
|
+
// is set on the instance by the registry, so this stays one property
|
|
338
|
+
// read and src/nodes/ keeps not importing it.
|
|
339
|
+
if (this._childrenAllowed === false) {
|
|
340
|
+
throw new Error(
|
|
341
|
+
`react-x11: <${this.kind}> takes no children (registered with ` +
|
|
342
|
+
`childrenAllowed: false), but <${child.kind}> is inside it.`,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
// A node's size comes from its measure function or from its children,
|
|
346
|
+
// never both — and yoga does not merely refuse the second one, it aborts
|
|
347
|
+
// the WebAssembly module, which takes the process down naming nothing
|
|
348
|
+
// the developer wrote. The built-ins reach it too: `<text>` is turned
|
|
349
|
+
// away earlier by `createInstance`, which knows what its content is, but
|
|
350
|
+
// `<image>`, `<svg>`, `<textinput>` and `<textarea>` arrive here.
|
|
351
|
+
if (this._measureFn && this._joinsYoga(child)) {
|
|
352
|
+
throw new Error(
|
|
353
|
+
`react-x11: <${this.kind}> measures its own content, so it cannot ` +
|
|
354
|
+
`contain <${child.kind}> — layout sizes such an element from its ` +
|
|
355
|
+
`measure function and gives its children no say. Render <${child.kind}> ` +
|
|
356
|
+
`beside it rather than inside it; or, if <${this.kind}> is meant to ` +
|
|
357
|
+
'arrange children, remove its measureContent() and let flexbox size ' +
|
|
358
|
+
'it from what is inside it.',
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
// captured before the child joins, so it covers the arrangement that is
|
|
362
|
+
// about to be replaced (see _childListChanged). A viewport mid-blit has
|
|
363
|
+
// nothing vacating — the child being added had no pixels — and the
|
|
364
|
+
// layout diff claims where it lands, so it names no region at all.
|
|
365
|
+
const before = this._blitLedgerOpen() ? null : this._childListBefore();
|
|
366
|
+
// a move has to leave the yoga tree too — yoga aborts on insertChild of
|
|
367
|
+
// a node that still has a parent
|
|
368
|
+
if (child.parent === this && this._joinsYoga(child)) {
|
|
369
|
+
this.yoga.removeChild(child.yoga);
|
|
370
|
+
}
|
|
371
|
+
// no parent means never attached: this insert is a mount, and the theme
|
|
372
|
+
// walk resolves without claiming — a keyed reorder arrives here too, with
|
|
373
|
+
// its parent still set, and that one keeps the claims (issue #402)
|
|
374
|
+
const mounting = child.parent == null;
|
|
375
|
+
const index = this._spliceChild(child, beforeChild);
|
|
376
|
+
child.parent = this;
|
|
377
|
+
if (this._joinsYoga(child)) {
|
|
378
|
+
this.yoga.insertChild(child.yoga, this._yogaIndexAt(index));
|
|
379
|
+
} else if (this._host !== null && child.yoga && !child.isWindow) {
|
|
380
|
+
// a layout's child is a tree of its own; a keyed reorder only moves
|
|
381
|
+
// it in the list, which is the algorithm's to read
|
|
382
|
+
if (mounting) this._adoptHostChild(child);
|
|
383
|
+
this._markHostDirty();
|
|
384
|
+
}
|
|
385
|
+
child._setRoot(this.root);
|
|
386
|
+
child._registerSizeQueries();
|
|
387
|
+
// it can see its ancestors now, so any token in its style can resolve.
|
|
388
|
+
// With no theme anywhere there is nothing to resolve and nothing to walk
|
|
389
|
+
if (this.theme || child.props.theme) child._themeChanged(mounting);
|
|
390
|
+
// …and the same for the scale: a subtree styled while detached resolved
|
|
391
|
+
// against the app's, and only now can see the `scale` props above it.
|
|
392
|
+
child._rescaleSubtree(mounting);
|
|
393
|
+
this._textContentChanged();
|
|
394
|
+
this._childListChanged(before);
|
|
395
|
+
a11yHooks.attached?.(this, child);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* This node's paint bounds from before a child-list mutation — the `before`
|
|
400
|
+
* half of `_childListChanged`'s protocol, captured while a departing child
|
|
401
|
+
* is still attached.
|
|
402
|
+
*
|
|
403
|
+
* Walked once per node per frame rather than once per mutation. A commit
|
|
404
|
+
* that mounts a virtualized list's window inserts a hundred rows into one
|
|
405
|
+
* pane, one `insertBefore` at a time, and a walk of the whole pane per row
|
|
406
|
+
* is what made that commit O(rows x pane) (issue #397).
|
|
407
|
+
*
|
|
408
|
+
* Reusing the first walk's answer is not an approximation. Nothing is laid
|
|
409
|
+
* out or painted between two mutations in the same frame, so every child
|
|
410
|
+
* still carries the rect it was last painted at, and a child that leaves
|
|
411
|
+
* later in the frame was in the list — and so inside the rect — when the
|
|
412
|
+
* first walk ran. `root._reflowed` is the marker for "this frame already
|
|
413
|
+
* has one", which is exactly its lifetime: joined at the first claim,
|
|
414
|
+
* cleared by `flush()`.
|
|
415
|
+
*/
|
|
416
|
+
_childListBefore() {
|
|
417
|
+
const root = this.root;
|
|
418
|
+
// A subtree still being built off-tree claims nothing — this is the
|
|
419
|
+
// `appendInitialChild` path, which is most of a mount, and where the
|
|
420
|
+
// walk used to be thrown away by `_childListChanged`'s `!root` return.
|
|
421
|
+
if (!root) return null;
|
|
422
|
+
if (root._reflowed.has(this) && this._reflowBefore) {
|
|
423
|
+
return this._reflowBefore;
|
|
424
|
+
}
|
|
425
|
+
// NO_DAMAGE, not null, when a blitting viewport above clips this node
|
|
426
|
+
// away entirely (issue #398): null here would read as "somewhere" and
|
|
427
|
+
// repaint the window.
|
|
428
|
+
return (this._reflowBefore = this._claimBounds() ?? NO_DAMAGE);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* A child was inserted or removed. `before` is this node's paint bounds from
|
|
433
|
+
* *before* the mutation, which the caller has to capture while the departing
|
|
434
|
+
* child is still attached.
|
|
435
|
+
*
|
|
436
|
+
* The damage is this subtree before the mutation unioned with the same
|
|
437
|
+
* subtree after layout. The second half is not measurable yet — an
|
|
438
|
+
* inserted child has no rect until layout runs — so the node is queued
|
|
439
|
+
* for the root to re-measure once it has. Siblings the reflow displaces
|
|
440
|
+
* outside this subtree (this node growing taller, say) claim themselves
|
|
441
|
+
* through the layout diff in flush(), which is what lets this claim stay
|
|
442
|
+
* bounded without requiring the node's own size to be pinned.
|
|
443
|
+
*/
|
|
444
|
+
_childListChanged(before) {
|
|
445
|
+
// belt for a subtree attached imperatively with its rect already laid
|
|
446
|
+
// out — nothing then re-runs _assignAbs to notice the reach grew
|
|
447
|
+
this._clearHitBounds();
|
|
448
|
+
const root = this.root;
|
|
449
|
+
if (!root) return;
|
|
450
|
+
// A viewport keeping a ledger this frame (issue #398) says both halves
|
|
451
|
+
// of the protocol finer: `before` is the departing child's own rect
|
|
452
|
+
// rather than this node's box, and the "after" half comes from the
|
|
453
|
+
// shifted layout diff, which claims an entering child where it lands
|
|
454
|
+
// and says nothing about the ones that only rode the scroll. Joining
|
|
455
|
+
// `_reflowed` would undo both — its post-layout claim is this node's
|
|
456
|
+
// box, the whole band the blit is about to move.
|
|
457
|
+
if (this._blitLedgerOpen()) {
|
|
458
|
+
root.invalidate(true, before ?? NO_DAMAGE, 'child-list');
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
root.invalidate(true, before, 'child-list');
|
|
462
|
+
root._reflowed.add(this);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
removeChild(child) {
|
|
466
|
+
const index = this._indexOfChild(child);
|
|
467
|
+
if (index === -1) return;
|
|
468
|
+
// told while the child is still wired, so the bridge can compute the
|
|
469
|
+
// index the AT will see the removal at
|
|
470
|
+
a11yHooks.detach?.(this, child);
|
|
471
|
+
// captured while the child is still attached, so it covers the rect the
|
|
472
|
+
// child is about to stop occupying — the child's own, for a viewport
|
|
473
|
+
// mid-blit, where this node's box is the whole scrolled band
|
|
474
|
+
const before = this._blitLedgerOpen()
|
|
475
|
+
? (child._claimBounds() ?? NO_DAMAGE)
|
|
476
|
+
: this._childListBefore();
|
|
477
|
+
this.children.splice(index, 1);
|
|
478
|
+
if (outsideYoga(child)) this._nonYogaKids--;
|
|
479
|
+
if (this._joinsYoga(child)) {
|
|
480
|
+
this.yoga.removeChild(child.yoga);
|
|
481
|
+
} else if (this._host !== null && child.yoga && !child.isWindow) {
|
|
482
|
+
if (child._hostAbsolute) {
|
|
483
|
+
this._host.absolute.removeChild(child.yoga);
|
|
484
|
+
child._hostAbsolute = false;
|
|
485
|
+
}
|
|
486
|
+
this._markHostDirty();
|
|
487
|
+
}
|
|
488
|
+
child.parent = null;
|
|
489
|
+
child.destroySubtree();
|
|
490
|
+
if (child.yoga && !child.isWindow) {
|
|
491
|
+
child.yoga.freeRecursive();
|
|
492
|
+
child.yoga = null;
|
|
493
|
+
}
|
|
494
|
+
this._textContentChanged();
|
|
495
|
+
this._childListChanged(before);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Destroy real resources (X windows) in this subtree. Yoga nodes are
|
|
499
|
+
* freed by the caller via freeRecursive on the subtree top. */
|
|
500
|
+
destroySubtree() {
|
|
501
|
+
this.destroyed = true;
|
|
502
|
+
// a loop outlives nothing: the window drops it from the set that keeps
|
|
503
|
+
// its frame clock alive, and stops watching visibility with the last one
|
|
504
|
+
this.root?._forgetLoopNode(this);
|
|
505
|
+
// …and out of the animating set in the same breath rather than on the
|
|
506
|
+
// next tick, so a spinner that unmounts leaves the clock idle even if
|
|
507
|
+
// nothing else ever asks for a frame
|
|
508
|
+
this.root?._animating.delete(this);
|
|
509
|
+
this.root?._opaqueNodes?.delete(this);
|
|
510
|
+
// a surface that goes away takes its selection with it, and the app-wide
|
|
511
|
+
// claim on being the one showing one goes with it too
|
|
512
|
+
this._textSelection?.destroy();
|
|
513
|
+
if (this.hasOwnSelection) dropVisibleSelection(this);
|
|
514
|
+
for (const child of this.children) child.destroySubtree();
|
|
515
|
+
// A layout host's children's trees are roots of their own, which the
|
|
516
|
+
// `freeRecursive` that takes this node's box does not reach.
|
|
517
|
+
if (this._host !== null) this._freeHostTrees();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
_setRoot(root) {
|
|
521
|
+
if (this.root === root) return;
|
|
522
|
+
// a layout host is found through its window's registry, like a placed
|
|
523
|
+
// node
|
|
524
|
+
if (this._host !== null) {
|
|
525
|
+
this.root?._layoutHosts?.delete(this);
|
|
526
|
+
root?._layoutHosts?.add(this);
|
|
527
|
+
}
|
|
528
|
+
// an element answering `opaqueRect()` is one the window asks per pass
|
|
529
|
+
const opaque = this.opaqueRect !== Node.prototype.opaqueRect;
|
|
530
|
+
if (opaque) this.root?._opaqueNodes?.delete(this);
|
|
531
|
+
this.root = root;
|
|
532
|
+
if (opaque) root?._opaqueNodes?.add(this);
|
|
533
|
+
// styled before it had a window, so this is where a placed node is
|
|
534
|
+
// first registered (WindowNode._placeNodes)
|
|
535
|
+
if (this.style && isPlaced(this.style)) root?._placedNodes?.add(this);
|
|
536
|
+
// A node is styled in its constructor, before it has a window — so this
|
|
537
|
+
// is where a loop declared by the very first style finds a frame clock
|
|
538
|
+
// to run on.
|
|
539
|
+
if (this._loops) this._updateLoops();
|
|
540
|
+
for (const child of this.children) {
|
|
541
|
+
if (!child.isWindow) child._setRoot(root);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** Called when descendant text content may have changed; overridden by
|
|
546
|
+
* TextNode, forwarded upward by spans/chunks. */
|
|
547
|
+
_textContentChanged() {}
|
|
548
|
+
|
|
549
|
+
applyProps(newProps, oldProps) {
|
|
550
|
+
const prev = this.props;
|
|
551
|
+
const prevStyle = this.style;
|
|
552
|
+
const themeChanged = newProps.theme !== prev.theme;
|
|
553
|
+
this.props = newProps;
|
|
554
|
+
if (themeChanged) this._themeChanged();
|
|
555
|
+
// Ahead of the `_syncStyle` below, because the funnel that runs
|
|
556
|
+
// multiplies by `this.scale` and this is what makes it read the new
|
|
557
|
+
// factor. The walk restyles this node too, so the call after it hits
|
|
558
|
+
// the identity check and costs nothing twice.
|
|
559
|
+
if (newProps.scale !== prev.scale) this._rescaleSubtree();
|
|
560
|
+
const style = this._syncStyle(newProps);
|
|
561
|
+
let layoutChanged = false;
|
|
562
|
+
// hoisted styles hit the identity check and skip the whole update
|
|
563
|
+
if (this.yoga && style !== prevStyle) {
|
|
564
|
+
layoutChanged = applyLayoutStyle(this.yoga, style, prevStyle);
|
|
565
|
+
}
|
|
566
|
+
if (Boolean(newProps.trapFocus) !== Boolean((oldProps ?? prev).trapFocus)) {
|
|
567
|
+
this._syncFocusScope();
|
|
568
|
+
}
|
|
569
|
+
// and so does being a selection surface
|
|
570
|
+
this._syncSelectable(newProps);
|
|
571
|
+
// drop-target registration follows the props edge, like trapFocus
|
|
572
|
+
if (hasDropProps(newProps) !== hasDropProps(oldProps ?? prev)) {
|
|
573
|
+
const root = this.root;
|
|
574
|
+
if (root?._registerDropTarget) {
|
|
575
|
+
if (hasDropProps(newProps)) root._registerDropTarget(this);
|
|
576
|
+
else root._forgetDropTarget(this);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// This is how every React update arrives, so it is where partial
|
|
580
|
+
// painting pays for itself. `applyLayoutStyle` has just told us whether
|
|
581
|
+
// anything can have *moved*: if so, this subtree's before/after rects
|
|
582
|
+
// plus the layout diff bound the frame; if not, this node's own region
|
|
583
|
+
// bounds what changed — a new colour, a new label, a different border.
|
|
584
|
+
// And if nothing it draws changed at all, it contributes no damage,
|
|
585
|
+
// which is what keeps a commit from widening the region to every node
|
|
586
|
+
// it touched.
|
|
587
|
+
if (layoutChanged) {
|
|
588
|
+
this._invalidateLayout('props');
|
|
589
|
+
} else {
|
|
590
|
+
// The style half is asked here rather than inside `paintChanged`, and
|
|
591
|
+
// stays core's answer: what a style change moves is the background,
|
|
592
|
+
// the border and the clip that `Node.paint` draws, so an element is
|
|
593
|
+
// not in a position to excuse one.
|
|
594
|
+
const styleChanged =
|
|
595
|
+
style !== prevStyle && paintPropsChanged(style, prevStyle);
|
|
596
|
+
this.root?.invalidate(
|
|
597
|
+
false,
|
|
598
|
+
styleChanged || this.paintChanged(newProps, prev) ? this : NO_DAMAGE,
|
|
599
|
+
'props',
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
if (DEV) devCheckA11yProps(this);
|
|
603
|
+
a11yHooks.propsChanged?.(this);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Did anything this node *draws* change? Answering true damages the whole
|
|
608
|
+
* node; answering false contributes no damage at all.
|
|
609
|
+
*
|
|
610
|
+
* Deliberately conservative, because the cost of a wrong "no" is a stale
|
|
611
|
+
* pixel that nothing will come back to fix. A prop that is not equal to
|
|
612
|
+
* the one it replaced is "yes it changed", which is what makes the default
|
|
613
|
+
* safe without knowing about subclasses: `<image src>`, `<canvas onDraw>`,
|
|
614
|
+
* a `value`, a `placeholder`, a `caretColor` — any prop a subclass paints
|
|
615
|
+
* from is a prop, so a change to it lands here as an inequality and damages
|
|
616
|
+
* the node. Three kinds are skipped because they cannot affect this node's
|
|
617
|
+
* own drawing:
|
|
618
|
+
*
|
|
619
|
+
* - `children`, which the reconciler mutates through appendChild /
|
|
620
|
+
* removeChild / commitTextUpdate, each of which invalidates on its own;
|
|
621
|
+
* - event handlers, rebuilt every render and never painted;
|
|
622
|
+
* - `style`, compared by value by the caller — so a style object React
|
|
623
|
+
* rebuilt with the same contents costs nothing, which is the whole
|
|
624
|
+
* point, since React rebuilds sibling styles on every render and a
|
|
625
|
+
* commit would otherwise damage every node it walked.
|
|
626
|
+
*
|
|
627
|
+
* **The seam (issue #301).** "The node" is the wrong granularity for an
|
|
628
|
+
* element that draws a *scene*: a graph view handed a new `nodes` array
|
|
629
|
+
* every drag step has already claimed the box the dragged node moved
|
|
630
|
+
* through, and this answering "yes" over the top widens that to the whole
|
|
631
|
+
* pane and throws the scoped work away. Such an element either names those
|
|
632
|
+
* props in `selfDamagedProps` — the declarative form, and what
|
|
633
|
+
* `registerElement({ selfDamagedProps })` fills — or overrides this method
|
|
634
|
+
* when the answer depends on the values rather than the names:
|
|
635
|
+
*
|
|
636
|
+
* ```js
|
|
637
|
+
* paintChanged(next, prev) {
|
|
638
|
+
* // my own applyProps diffed these and claimed exactly what moved
|
|
639
|
+
* if (onlyPositionsMoved(next.nodes, prev.nodes)) return false;
|
|
640
|
+
* return super.paintChanged(next, prev); // everything else is core's
|
|
641
|
+
* }
|
|
642
|
+
* ```
|
|
643
|
+
*
|
|
644
|
+
* An override that answers wrong shows stale pixels, so the part it does
|
|
645
|
+
* not know about has to reach `super` — a new `aria-label`, a prop the
|
|
646
|
+
* element grows next year.
|
|
647
|
+
*/
|
|
648
|
+
paintChanged(newProps, prev) {
|
|
649
|
+
const claimed = this.selfDamagedProps;
|
|
650
|
+
const keys = new Set([...Object.keys(newProps), ...Object.keys(prev)]);
|
|
651
|
+
for (const key of keys) {
|
|
652
|
+
if (key === 'children' || key === 'style' || isEventProp(key)) continue;
|
|
653
|
+
if (claimed.has(key)) continue;
|
|
654
|
+
if (newProps[key] !== prev[key]) return true;
|
|
655
|
+
}
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
setHidden(hidden) {
|
|
660
|
+
// claimed before the yoga flip so the bound covers the arrangement
|
|
661
|
+
// being vacated; the reveal is the after-layout re-claim
|
|
662
|
+
this._invalidateLayout('props');
|
|
663
|
+
this.hidden = hidden;
|
|
664
|
+
if (this.yoga) {
|
|
665
|
+
this.yoga.setDisplay(
|
|
666
|
+
hidden || this.style.display === 'none'
|
|
667
|
+
? Yoga.DISPLAY_NONE
|
|
668
|
+
: Yoga.DISPLAY_FLEX,
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
this._visibilityChanged(!hidden);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Whether this subtree is on screen just changed, so focus has to follow
|
|
676
|
+
* it — released when it goes, handed back when it returns. The rule and
|
|
677
|
+
* the reasoning live on the focus manager (`subtreeHidden`, events.js);
|
|
678
|
+
* this is the funnel every route to it comes through: the `hidden` flag
|
|
679
|
+
* React sets for `<Suspense>`/`<Activity>`, and `display: 'none'` from a
|
|
680
|
+
* style, a state block or a size query (`_retarget`).
|
|
681
|
+
*/
|
|
682
|
+
_visibilityChanged(visible) {
|
|
683
|
+
// Same rule, and the reason it shares this funnel: a loop inside a
|
|
684
|
+
// subtree that just went off the screen is drawing frames for nobody,
|
|
685
|
+
// whichever of the three routes hid it. Re-evaluated for the whole
|
|
686
|
+
// window rather than for this subtree — the set is the handful of nodes
|
|
687
|
+
// that declare an `animation`, and each one answers for itself.
|
|
688
|
+
if (this.root?._loopNodes?.size) this.root._refreshLoops();
|
|
689
|
+
const events = this._focusManager();
|
|
690
|
+
if (!events) return;
|
|
691
|
+
if (visible) events.subtreeRevealed(this);
|
|
692
|
+
else events.subtreeHidden(this);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Focus this node, as clicking it would: the owning window's focus moves
|
|
697
|
+
* here, `onBlur` fires on whatever had it, `onFocus` here. Also pulls the
|
|
698
|
+
* X input focus to the window if the window manager gave it away.
|
|
699
|
+
*/
|
|
700
|
+
focus() {
|
|
701
|
+
this._focusManager()?.focus(this);
|
|
702
|
+
return this;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/** Give up focus, leaving the window with nothing focused. */
|
|
706
|
+
blur() {
|
|
707
|
+
const events = this._focusManager();
|
|
708
|
+
if (events?.focused === this) events.focus(null);
|
|
709
|
+
return this;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** Whether this node has the owning window's focus. */
|
|
713
|
+
get focused() {
|
|
714
|
+
return this._focusManager()?.focused === this;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** Whether focus is on this node or inside it — CSS `:focus-within`. A
|
|
718
|
+
* `<popup>` counts as inside the node it hangs off in the JSX tree, which
|
|
719
|
+
* is what a modal needs to know before taking focus itself. */
|
|
720
|
+
get focusWithin() {
|
|
721
|
+
const focused = this._focusManager()?.focused;
|
|
722
|
+
return Boolean(focused) && this.contains(focused);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/** Whether `node` is this node or a descendant of it (DOM `contains`). */
|
|
726
|
+
contains(node) {
|
|
727
|
+
for (let n = node; n; n = n.parent) {
|
|
728
|
+
if (n === this) return true;
|
|
729
|
+
}
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* The text this element reports through `a11yTextState()` may have moved
|
|
735
|
+
* — an edit, a caret move, a selection change, a composition (#257). The
|
|
736
|
+
* same notification `<textinput>`'s `_repaint` makes, and the reason an
|
|
737
|
+
* assistive technology hears a third-party editor at all: the state is
|
|
738
|
+
* *pulled* when this says it is worth pulling.
|
|
739
|
+
*
|
|
740
|
+
* Free when nobody is listening — one property read, the hook slots being
|
|
741
|
+
* null until a bridge or the test spy fills them — so an element may call
|
|
742
|
+
* it on every edit without asking whether accessibility is on.
|
|
743
|
+
*/
|
|
744
|
+
notifyA11yTextChanged() {
|
|
745
|
+
a11yHooks.textState?.(this);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* The scene this element reports through `a11yScene()` has changed — an
|
|
750
|
+
* item added or removed, one selected, the element's own cursor moved
|
|
751
|
+
* onto another one (#304). The children an assistive technology is
|
|
752
|
+
* holding are re-read and the difference announced.
|
|
753
|
+
*
|
|
754
|
+
* A scene that is a function of the props needs no call: a commit already
|
|
755
|
+
* re-reads it. This is for everything the element does on its own —
|
|
756
|
+
* a drag, an animation, its own arrow keys.
|
|
757
|
+
*
|
|
758
|
+
* Free when nobody is listening, the same one property read
|
|
759
|
+
* `notifyA11yTextChanged()` costs.
|
|
760
|
+
*/
|
|
761
|
+
notifyA11ySceneChanged() {
|
|
762
|
+
a11yHooks.propsChanged?.(this);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/** Where focus for this node lives: its own window's EventManager, or —
|
|
766
|
+
* inside a `<popup>`, which never receives the X input focus — the owner
|
|
767
|
+
* window's (see EventManager.focusManager). */
|
|
768
|
+
_focusManager() {
|
|
769
|
+
return this.root?.events?.focusManager ?? null;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/** Register or drop this node's focus scope to match the `trapFocus` prop.
|
|
773
|
+
* Idempotent: called at mount (commitMount) and on every prop update. */
|
|
774
|
+
_syncFocusScope() {
|
|
775
|
+
const events = this._focusManager();
|
|
776
|
+
if (!events) return;
|
|
777
|
+
if (this.props.trapFocus) events.pushScope(this);
|
|
778
|
+
else events.popScope(this);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// The pointer and the keys a selection is made with. They are default
|
|
782
|
+
// actions on the *base* class because the press lands on whatever is under
|
|
783
|
+
// the pointer — a `<text>`, an `<image>`, the gap between two paragraphs —
|
|
784
|
+
// and every one of them has to reach the surface above it. An element that
|
|
785
|
+
// takes presses of its own overrides these and is, by that alone, not part
|
|
786
|
+
// of a document; one that wants both calls `super`.
|
|
787
|
+
defaultMouseDown(ev) {
|
|
788
|
+
selectionSurfaceOf(this)?.press(ev);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
defaultMouseDrag(ev) {
|
|
792
|
+
selectionSurfaceOf(this)?.drag(ev);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
defaultMouseUp(ev) {
|
|
796
|
+
selectionSurfaceOf(this)?.release(ev);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* The selection keys, and then **Space or Enter on anything clickable**.
|
|
801
|
+
*
|
|
802
|
+
* A focusable node with an `onClick` used to take focus, draw a ring, be
|
|
803
|
+
* reachable by Tab and be activatable by a screen reader — and do nothing
|
|
804
|
+
* at all when the keyboard pressed it (issue #329). It looked operable and
|
|
805
|
+
* was not, which is the failure mode a focus ring makes *worse*: the ring
|
|
806
|
+
* is a promise. Every control an application builds out of a `<box>`
|
|
807
|
+
* rather than out of `Button` had it, silently.
|
|
808
|
+
*
|
|
809
|
+
* It is the click itself, not a second definition of one: `synthesizeClick`
|
|
810
|
+
* is the function an AT's `DoAction("activate")` already went through, so
|
|
811
|
+
* a control that acts on the press hears the press either way and the two
|
|
812
|
+
* paths cannot drift. The rule for *what* is activatable is the same one
|
|
813
|
+
* the bridge writes down — there is an `onClick` here — minus the bridge's
|
|
814
|
+
* role clause, which advertises an action to something that cannot press a
|
|
815
|
+
* key (a11y.js, `hasClickHandler`).
|
|
816
|
+
*
|
|
817
|
+
* **One key rule, no role table.** The web gives `checkbox` Space and not
|
|
818
|
+
* Enter, and a link Enter and not Space, because on the web those keys are
|
|
819
|
+
* already spoken for — Space scrolls the page, Enter submits the form.
|
|
820
|
+
* Neither is true here: a default action runs on the focused node, so the
|
|
821
|
+
* scroll pane a row sits in never sees the row's Space, and there is no
|
|
822
|
+
* implicit submit. All a role table could buy, then, is *fewer* keys
|
|
823
|
+
* working on a control that draws a focus ring — which is the bug.
|
|
824
|
+
*
|
|
825
|
+
* The two ways out, both ordinary: `preventDefault()` in the element's own
|
|
826
|
+
* `onKeyDown` (the seam an application uses — a `<box>` that wants Enter
|
|
827
|
+
* for something else), and overriding this method (the seam an element
|
|
828
|
+
* uses). A scroll pane that is *itself* clickable takes the third: its
|
|
829
|
+
* `defaultKeyDown` answers Space with a page and never reaches here, so
|
|
830
|
+
* paging keeps the key it has always had and Enter activates.
|
|
831
|
+
*/
|
|
832
|
+
defaultKeyDown(ev) {
|
|
833
|
+
this._textSelection?.keyDown(ev);
|
|
834
|
+
if (ev.defaultPrevented) return;
|
|
835
|
+
const enter = ev.keysym === XK_RETURN || ev.keysym === XK_KP_ENTER;
|
|
836
|
+
// Space by either name: `XK_space` *is* code point 32 — a Latin-1 keysym
|
|
837
|
+
// and its character are the same number — and both fields are read
|
|
838
|
+
// because a synthetic event may carry only one of them, the way the
|
|
839
|
+
// scroll keys next door read the keysym and every widget read the code
|
|
840
|
+
// point. A key an open composition took reaches no default action at all.
|
|
841
|
+
const space = ev.keysym === XK_SPACE || ev.codepoint === 32;
|
|
842
|
+
if (!enter && !space) return;
|
|
843
|
+
if (!hasClickHandler(this)) return;
|
|
844
|
+
// consumed, said the way every default action says it: what it prevents
|
|
845
|
+
// is the default action after this one
|
|
846
|
+
ev.preventDefault();
|
|
847
|
+
synthesizeClick(this, this.abs, ev.nativeEvent);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// The rest of Node's methods live with the concern they serve, one file
|
|
852
|
+
// per concern; install.js explains the arrangement.
|
|
853
|
+
installMethods(
|
|
854
|
+
Node,
|
|
855
|
+
NodeStyling,
|
|
856
|
+
NodeCascade,
|
|
857
|
+
NodeQueries,
|
|
858
|
+
NodeAnimation,
|
|
859
|
+
NodeLayout,
|
|
860
|
+
NodeLayoutHost,
|
|
861
|
+
NodePosition,
|
|
862
|
+
NodeHitTest,
|
|
863
|
+
NodeInvalidate,
|
|
864
|
+
NodeScrollBlit,
|
|
865
|
+
NodePaint,
|
|
866
|
+
NodeBoxPaint,
|
|
867
|
+
NodeSelectable,
|
|
868
|
+
);
|