react-x11 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- package/src/scene3d.js +0 -683
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Testing-Library-shaped queries over the retained node tree.
|
|
2
|
+
//
|
|
3
|
+
// The tree is right here and fully ours — `node.kind`, `node.props`,
|
|
4
|
+
// `node.children` — so the queries walk it directly rather than going
|
|
5
|
+
// through React's test selectors. That keeps the failure messages ours (a
|
|
6
|
+
// miss prints what *was* there), and keeps the queries independent of
|
|
7
|
+
// react-reconciler's internal host-config contract.
|
|
8
|
+
//
|
|
9
|
+
// Naming follows Testing Library, because that is the vocabulary React
|
|
10
|
+
// developers already have:
|
|
11
|
+
//
|
|
12
|
+
// getBy* exactly one, or throw
|
|
13
|
+
// queryBy* one or null, never throws — the way to assert absence
|
|
14
|
+
// getAllBy* one or more, or throw
|
|
15
|
+
// findBy* getBy*, retried until it appears or the timeout expires
|
|
16
|
+
|
|
17
|
+
import { queryAllByComponent, componentInventory } from './components.js';
|
|
18
|
+
import { roleNameOf } from '../a11y.js';
|
|
19
|
+
|
|
20
|
+
const DEFAULT_TIMEOUT = 1000;
|
|
21
|
+
|
|
22
|
+
/** Depth-first, in paint order, including `<popup>` subtrees. */
|
|
23
|
+
function walk(node, visit) {
|
|
24
|
+
if (!node || node.destroyed) return;
|
|
25
|
+
visit(node);
|
|
26
|
+
for (const child of node.children ?? []) walk(child, visit);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function collect(root, predicate) {
|
|
30
|
+
const found = [];
|
|
31
|
+
walk(root, (node) => {
|
|
32
|
+
if (predicate(node)) found.push(node);
|
|
33
|
+
});
|
|
34
|
+
return found;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The text a node presents, joined across its spans and chunks — so a
|
|
39
|
+
* `<text>` built from several styled spans still matches as one string,
|
|
40
|
+
* which is what a reader sees.
|
|
41
|
+
*/
|
|
42
|
+
export function textOf(node) {
|
|
43
|
+
let out = '';
|
|
44
|
+
walk(node, (n) => {
|
|
45
|
+
if (typeof n.text === 'string') out += n.text;
|
|
46
|
+
});
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function matches(actual, expected, exact) {
|
|
51
|
+
if (expected instanceof RegExp) return expected.test(actual);
|
|
52
|
+
if (typeof expected === 'function') return Boolean(expected(actual));
|
|
53
|
+
const a = exact ? actual : actual.trim().toLowerCase();
|
|
54
|
+
const b = exact ? String(expected) : String(expected).trim().toLowerCase();
|
|
55
|
+
return exact ? a === b : a.includes(b);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The role a node reports: the `role` prop, else the same kind → role-name
|
|
60
|
+
* table the AT-SPI bridge reads (src/a11y.js), so what a test selects by
|
|
61
|
+
* and what a screen reader hears cannot drift apart. Kinds with no
|
|
62
|
+
* accessibility default (`box`, `canvas`, …) answer their kind, which is
|
|
63
|
+
* what queries were written against.
|
|
64
|
+
*/
|
|
65
|
+
export function roleOf(node) {
|
|
66
|
+
return roleNameOf(node) ?? node.kind;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function describe(node) {
|
|
70
|
+
const text = textOf(node).trim();
|
|
71
|
+
const name = node.props?.['data-testname'];
|
|
72
|
+
return (
|
|
73
|
+
`<${node.kind}${name ? ` data-testname="${name}"` : ''}>` +
|
|
74
|
+
(text ? ` ${JSON.stringify(text.slice(0, 40))}` : '')
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function inventory(root, limit = 12) {
|
|
79
|
+
const lines = [];
|
|
80
|
+
walk(root, (n) => {
|
|
81
|
+
if (n.kind === 'textchunk' || n.kind === 'textspan') return;
|
|
82
|
+
if (lines.length < limit) lines.push(` ${describe(n)}`);
|
|
83
|
+
});
|
|
84
|
+
return lines.join('\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function one(root, found, what, printTree = inventory) {
|
|
88
|
+
if (found.length === 1) return found[0];
|
|
89
|
+
if (found.length === 0) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`react-x11/test: no element found for ${what}.\nThe tree holds:\n${printTree(root)}`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
throw new Error(
|
|
95
|
+
`react-x11/test: found ${found.length} elements for ${what}, expected one:\n` +
|
|
96
|
+
found.map((n) => ` ${describe(n)}`).join('\n'),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function retry(fn, { timeout = DEFAULT_TIMEOUT, interval = 20 } = {}) {
|
|
101
|
+
const deadline = Date.now() + timeout;
|
|
102
|
+
let last;
|
|
103
|
+
for (;;) {
|
|
104
|
+
try {
|
|
105
|
+
return fn();
|
|
106
|
+
} catch (err) {
|
|
107
|
+
last = err;
|
|
108
|
+
if (Date.now() >= deadline) throw last;
|
|
109
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The queries, bound to a subtree. `within(node)` is this with a different
|
|
116
|
+
* root; `screen` is this bound to whatever `renderX11` mounted last.
|
|
117
|
+
*/
|
|
118
|
+
export function within(root) {
|
|
119
|
+
const q = {
|
|
120
|
+
/** Every node under here, in paint order — the escape hatch. */
|
|
121
|
+
all: (predicate = () => true) => collect(root, predicate),
|
|
122
|
+
|
|
123
|
+
queryAllByText: (text, { exact = false, selector = null } = {}) =>
|
|
124
|
+
collect(
|
|
125
|
+
root,
|
|
126
|
+
(n) =>
|
|
127
|
+
// only leaf-ish text holders, or every ancestor would match too
|
|
128
|
+
(n.kind === 'text' ||
|
|
129
|
+
n.kind === 'textinput' ||
|
|
130
|
+
n.kind === 'textarea') &&
|
|
131
|
+
(!selector || n.kind === selector) &&
|
|
132
|
+
matches(
|
|
133
|
+
n.kind === 'text'
|
|
134
|
+
? textOf(n)
|
|
135
|
+
: (n.value ?? n.props?.placeholder ?? ''),
|
|
136
|
+
text,
|
|
137
|
+
exact,
|
|
138
|
+
),
|
|
139
|
+
),
|
|
140
|
+
|
|
141
|
+
queryAllByRole: (role, { name = null, exact = false } = {}) =>
|
|
142
|
+
collect(
|
|
143
|
+
root,
|
|
144
|
+
(n) =>
|
|
145
|
+
roleOf(n) === role &&
|
|
146
|
+
(name == null || matches(textOf(n), name, exact)),
|
|
147
|
+
),
|
|
148
|
+
|
|
149
|
+
queryAllByTestName: (name) =>
|
|
150
|
+
collect(root, (n) => n.props?.['data-testname'] === name),
|
|
151
|
+
|
|
152
|
+
queryAllByPlaceholder: (text, { exact = false } = {}) =>
|
|
153
|
+
collect(
|
|
154
|
+
root,
|
|
155
|
+
(n) =>
|
|
156
|
+
typeof n.props?.placeholder === 'string' &&
|
|
157
|
+
matches(n.props.placeholder, text, exact),
|
|
158
|
+
),
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Host nodes by the component that created them (the JSX owner): for
|
|
162
|
+
* each rendered instance, the topmost host nodes of its output — so
|
|
163
|
+
* `within(getByComponent('TaskRow'))` scopes the other queries to that
|
|
164
|
+
* instance. Exact name, RegExp, or predicate over the name; needs
|
|
165
|
+
* development React, which records the owner on every element.
|
|
166
|
+
*/
|
|
167
|
+
queryAllByComponent: (component) => queryAllByComponent(root, component),
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// getBy / queryBy / getAllBy / findBy are all mechanical from queryAllBy
|
|
171
|
+
for (const [key, queryAll] of Object.entries(q)) {
|
|
172
|
+
if (!key.startsWith('queryAll')) continue;
|
|
173
|
+
const suffix = key.slice('queryAll'.length); // 'ByText', 'ByRole', …
|
|
174
|
+
// a ByComponent miss lists the components in the tree — the caller was
|
|
175
|
+
// trying to spell one of those, not an element kind
|
|
176
|
+
const printTree = suffix === 'ByComponent' ? componentInventory : inventory;
|
|
177
|
+
const label = (arg) =>
|
|
178
|
+
`${suffix.replace(/^By/, '')} ${
|
|
179
|
+
arg instanceof RegExp
|
|
180
|
+
? String(arg)
|
|
181
|
+
: typeof arg === 'function'
|
|
182
|
+
? arg.name || '(predicate)'
|
|
183
|
+
: JSON.stringify(arg)
|
|
184
|
+
}`;
|
|
185
|
+
q[`getAll${suffix}`] = (...args) => {
|
|
186
|
+
const found = queryAll(...args);
|
|
187
|
+
if (found.length === 0) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`react-x11/test: no element found for ${label(args[0])}.\n` +
|
|
190
|
+
`The tree holds:\n${printTree(root)}`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return found;
|
|
194
|
+
};
|
|
195
|
+
q[`get${suffix}`] = (...args) =>
|
|
196
|
+
one(root, queryAll(...args), label(args[0]), printTree);
|
|
197
|
+
q[`query${suffix}`] = (...args) => {
|
|
198
|
+
const found = queryAll(...args);
|
|
199
|
+
if (found.length > 1) return one(root, found, label(args[0]));
|
|
200
|
+
return found[0] ?? null;
|
|
201
|
+
};
|
|
202
|
+
q[`find${suffix}`] = (...args) => {
|
|
203
|
+
const options = args.length > 1 ? args[args.length - 1] : undefined;
|
|
204
|
+
return retry(() => q[`get${suffix}`](...args), options);
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return q;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** `within` bound lazily to the most recent `renderX11`. */
|
|
211
|
+
export function screenFor(getRoot) {
|
|
212
|
+
const target = {};
|
|
213
|
+
return new Proxy(target, {
|
|
214
|
+
get(_t, prop) {
|
|
215
|
+
const root = getRoot();
|
|
216
|
+
if (!root) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
'react-x11/test: `screen` has nothing to query — call renderX11() first.',
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return within(root)[prop];
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
}
|
package/src/textrange.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Ranges over text, in the units the caret is in.
|
|
2
|
+
//
|
|
3
|
+
// Everything here is pure and takes an array of code points, because that is
|
|
4
|
+
// the index space ntk's `TextLayout.caretPosition`/`indexAt` speak and
|
|
5
|
+
// therefore the one every caret and every selection in this renderer is in
|
|
6
|
+
// (a code point, not a UTF-16 unit — an emoji is one position, not two).
|
|
7
|
+
//
|
|
8
|
+
// It is its own module because two unrelated things need the same answers:
|
|
9
|
+
// `<textinput>`'s editing keys, and the selection a reader drags across a
|
|
10
|
+
// document (textselection.js). A double click has to pick the same word in
|
|
11
|
+
// both, and Ctrl+Left has to stop where a double click would start.
|
|
12
|
+
|
|
13
|
+
/** The code points of a string, which is the index space of every caret. */
|
|
14
|
+
export function codePoints(text) {
|
|
15
|
+
return Array.from(text ?? '');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Code point index -> UTF-16 offset, with one extra entry for the end.
|
|
20
|
+
*
|
|
21
|
+
* ntk's line runs report their extent in **code units** while its caret API
|
|
22
|
+
* speaks code points, so anything that reads run geometry has to translate
|
|
23
|
+
* between them; this is the table that does it.
|
|
24
|
+
*/
|
|
25
|
+
export function codeUnitOffsets(text) {
|
|
26
|
+
const offsets = [];
|
|
27
|
+
const s = text ?? '';
|
|
28
|
+
for (let i = 0; i < s.length;) {
|
|
29
|
+
offsets.push(i);
|
|
30
|
+
i += s.codePointAt(i) > 0xffff ? 2 : 1;
|
|
31
|
+
}
|
|
32
|
+
offsets.push(s.length);
|
|
33
|
+
return offsets;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The code point index for a UTF-16 offset, rounded down to a whole
|
|
37
|
+
* character. Binary search over `codeUnitOffsets`. */
|
|
38
|
+
export function codePointAtOffset(offsets, offset) {
|
|
39
|
+
let lo = 0;
|
|
40
|
+
let hi = offsets.length - 1;
|
|
41
|
+
while (lo < hi) {
|
|
42
|
+
const mid = (lo + hi + 1) >> 1;
|
|
43
|
+
if (offsets[mid] <= offset) lo = mid;
|
|
44
|
+
else hi = mid - 1;
|
|
45
|
+
}
|
|
46
|
+
return lo;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The word around an index, the way a double click selects one:
|
|
51
|
+
* whitespace-delimited, and a click in the space between two words takes the
|
|
52
|
+
* word before it rather than nothing.
|
|
53
|
+
*/
|
|
54
|
+
export function wordRangeAt(chars, index) {
|
|
55
|
+
if (chars.length === 0) return [0, 0];
|
|
56
|
+
let i = Math.min(Math.max(0, index), chars.length - 1);
|
|
57
|
+
const isSpace = (c) => /\s/.test(c);
|
|
58
|
+
if (isSpace(chars[i]) && i > 0) i--;
|
|
59
|
+
let a = i;
|
|
60
|
+
let b = i;
|
|
61
|
+
while (a > 0 && !isSpace(chars[a - 1])) a--;
|
|
62
|
+
while (b < chars.length && !isSpace(chars[b])) b++;
|
|
63
|
+
return [a, b];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The index one word away, the way Ctrl+arrow moves in a text editor: skip
|
|
68
|
+
* any run of non-word characters, then the word itself. Word characters are
|
|
69
|
+
* letters, digits and underscore, so "foo-bar" is two words and "foo_bar" is
|
|
70
|
+
* one.
|
|
71
|
+
*/
|
|
72
|
+
export function wordBoundary(chars, from, dir) {
|
|
73
|
+
const isWord = (c) => /[\p{L}\p{N}_]/u.test(c);
|
|
74
|
+
let i = Math.max(0, Math.min(from, chars.length));
|
|
75
|
+
if (dir > 0) {
|
|
76
|
+
while (i < chars.length && !isWord(chars[i])) i++;
|
|
77
|
+
while (i < chars.length && isWord(chars[i])) i++;
|
|
78
|
+
} else {
|
|
79
|
+
while (i > 0 && !isWord(chars[i - 1])) i--;
|
|
80
|
+
while (i > 0 && isWord(chars[i - 1])) i--;
|
|
81
|
+
}
|
|
82
|
+
return i;
|
|
83
|
+
}
|