@reticlehq/browser 3.4.0 → 3.6.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/actions/actions-dom.js +44 -43
- package/dist/actions/actions-press.d.ts +10 -4
- package/dist/actions/actions-press.js +108 -17
- package/dist/actions/actions.d.ts +2 -1
- package/dist/actions/actions.js +25 -26
- package/dist/actions/danger-context.js +1 -1
- package/dist/commands/commands.js +2 -6
- package/dist/connect-options.d.ts +1 -0
- package/dist/dom/a11y.js +43 -11
- package/dist/dom/addressing/refs.js +2 -1
- package/dist/dom/addressing/testid-attr.d.ts +4 -0
- package/dist/dom/addressing/testid-attr.js +14 -0
- package/dist/dom/occlusion.js +5 -1
- package/dist/dom/query.d.ts +1 -0
- package/dist/dom/query.js +42 -15
- package/dist/dom/snapshot.js +1 -0
- package/dist/observers/departure.js +51 -9
- package/dist/observers/dom.js +3 -2
- package/dist/observers/field.js +5 -3
- package/dist/observers/health.js +63 -2
- package/dist/observers/network-stream.d.ts +1 -1
- package/dist/observers/network-stream.js +3 -3
- package/dist/observers/network.js +1 -1
- package/dist/observers/storage.js +6 -1
- package/dist/presenter/carousel/carousel.d.ts +5 -2
- package/dist/presenter/carousel/carousel.js +34 -7
- package/dist/presenter/carousel/offer-card.d.ts +2 -2
- package/dist/presenter/carousel/offer-card.js +2 -1
- package/dist/presenter/carousel/panel-slides.d.ts +4 -3
- package/dist/presenter/carousel/panel-slides.js +43 -6
- package/dist/presenter/carousel/talk-card.d.ts +1 -1
- package/dist/presenter/chrome/log-kinds.d.ts +5 -0
- package/dist/presenter/chrome/log-kinds.js +1 -0
- package/dist/presenter/chrome/log-memory.d.ts +12 -0
- package/dist/presenter/chrome/log-memory.js +39 -0
- package/dist/presenter/chrome/presenter-hud-chrome.d.ts +1 -0
- package/dist/presenter/chrome/presenter-hud-chrome.js +7 -1
- package/dist/presenter/chrome/presenter-log.d.ts +5 -4
- package/dist/presenter/chrome/presenter-log.js +55 -5
- package/dist/presenter/chrome/presenter-report-copy.d.ts +11 -1
- package/dist/presenter/chrome/presenter-report-copy.js +14 -1
- package/dist/presenter/hud-telemetry.js +31 -2
- package/dist/presenter/icons/presenter-heroicons-data.d.ts +1 -0
- package/dist/presenter/icons/presenter-heroicons-data.js +4 -3
- package/dist/presenter/icons/presenter-icons.d.ts +2 -0
- package/dist/presenter/icons/presenter-icons.js +2 -0
- package/dist/presenter/presenter-account.d.ts +5 -4
- package/dist/presenter/presenter-account.js +23 -10
- package/dist/presenter/presenter-chat-views.d.ts +21 -0
- package/dist/presenter/presenter-chat-views.js +382 -0
- package/dist/presenter/presenter-config.d.ts +3 -2
- package/dist/presenter/presenter-config.js +4 -3
- package/dist/presenter/presenter-controls.d.ts +4 -2
- package/dist/presenter/presenter-controls.js +231 -51
- package/dist/presenter/presenter-design-tokens.d.ts +1 -0
- package/dist/presenter/presenter-design-tokens.js +34 -0
- package/dist/presenter/presenter-dock-layout.js +1 -1
- package/dist/presenter/presenter-frame.d.ts +12 -0
- package/dist/presenter/presenter-frame.js +189 -0
- package/dist/presenter/presenter-plan.d.ts +10 -0
- package/dist/presenter/presenter-plan.js +185 -0
- package/dist/presenter/presenter-report-styles.js +7 -0
- package/dist/presenter/presenter-report.d.ts +1 -1
- package/dist/presenter/presenter-report.js +53 -16
- package/dist/presenter/presenter-settings-styles.js +10 -0
- package/dist/presenter/presenter-settings.d.ts +5 -0
- package/dist/presenter/presenter-settings.js +71 -11
- package/dist/presenter/presenter-shell-styles.js +39 -42
- package/dist/presenter/presenter-shell.d.ts +8 -1
- package/dist/presenter/presenter-shell.js +124 -72
- package/dist/presenter/presenter-styles.js +9 -1
- package/dist/presenter/presenter.d.ts +4 -3
- package/dist/presenter/presenter.js +101 -13
- package/dist/presenter/tour/tour.d.ts +4 -1
- package/dist/presenter/tour/tour.js +6 -4
- package/dist/recorder/recorder.js +2 -2
- package/dist/registry/auto-testids.js +3 -4
- package/dist/registry/declared-platform.js +2 -0
- package/dist/reticle-inject.js +880 -513
- package/dist/reticle.d.ts +10 -1
- package/dist/reticle.js +73 -9
- package/dist/review/annotator-styles.d.ts +1 -1
- package/dist/review/annotator.d.ts +7 -0
- package/dist/review/annotator.js +20 -2
- package/dist/review/mark-anchor.js +2 -2
- package/dist/session-continuity.js +2 -1
- package/dist/storage-keys.d.ts +14 -0
- package/dist/storage-keys.js +17 -0
- package/package.json +3 -2
|
@@ -16,12 +16,32 @@ function pointerEventFor(el, type, init) {
|
|
|
16
16
|
? new PointerEvent(type, full)
|
|
17
17
|
: new MouseEvent(type, full);
|
|
18
18
|
}
|
|
19
|
+
const BUTTON_HELD = 1;
|
|
20
|
+
const BUTTON_RELEASED = 0;
|
|
21
|
+
function fireAt(el, type, at, buttons, related, detail = 0) {
|
|
22
|
+
const init = {
|
|
23
|
+
bubbles: !(type.endsWith('enter') || type.endsWith('leave')),
|
|
24
|
+
cancelable: true,
|
|
25
|
+
detail,
|
|
26
|
+
clientX: at.x,
|
|
27
|
+
clientY: at.y,
|
|
28
|
+
screenX: at.x,
|
|
29
|
+
screenY: at.y,
|
|
30
|
+
buttons,
|
|
31
|
+
button: 0,
|
|
32
|
+
...(related !== undefined ? { relatedTarget: related } : {}),
|
|
33
|
+
};
|
|
34
|
+
return el.dispatchEvent(type.startsWith('pointer')
|
|
35
|
+
? pointerEventFor(el, type, { ...init, pointerId: 1, isPrimary: true })
|
|
36
|
+
: mouseEventFor(el, type, init));
|
|
37
|
+
}
|
|
19
38
|
export async function fireClickSequence(el, hold, detail) {
|
|
20
39
|
const doc = el.ownerDocument;
|
|
21
40
|
const from = doc.activeElement ?? doc.body;
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
asSyntheticInput(() =>
|
|
41
|
+
const at = centreOf(el);
|
|
42
|
+
const d = detail ?? 0;
|
|
43
|
+
asSyntheticInput(() => fireAt(el, 'pointerdown', at, BUTTON_HELD, from, d));
|
|
44
|
+
asSyntheticInput(() => fireAt(el, 'mousedown', at, BUTTON_HELD, undefined, d));
|
|
25
45
|
if (el.tabIndex >= 0 && 'function' === typeof el.focus)
|
|
26
46
|
el.focus();
|
|
27
47
|
let heldMs = 0;
|
|
@@ -30,9 +50,9 @@ export async function fireClickSequence(el, hold, detail) {
|
|
|
30
50
|
await hold.sleep(hold.ms);
|
|
31
51
|
heldMs = hold.now() - startedAt;
|
|
32
52
|
}
|
|
33
|
-
|
|
34
|
-
asSyntheticInput(() =>
|
|
35
|
-
const notPrevented = asSyntheticInput(() =>
|
|
53
|
+
asSyntheticInput(() => fireAt(el, 'pointerup', at, BUTTON_RELEASED, from, d));
|
|
54
|
+
asSyntheticInput(() => fireAt(el, 'mouseup', at, BUTTON_RELEASED, undefined, d));
|
|
55
|
+
const notPrevented = asSyntheticInput(() => fireAt(el, 'click', at, BUTTON_RELEASED, undefined, d));
|
|
36
56
|
return { prevented: !notPrevented, heldMs };
|
|
37
57
|
}
|
|
38
58
|
function isMeasurable(rect) {
|
|
@@ -86,8 +106,6 @@ function makeDataTransfer(data) {
|
|
|
86
106
|
}
|
|
87
107
|
return dt;
|
|
88
108
|
}
|
|
89
|
-
const BUTTON_HELD = 1;
|
|
90
|
-
const BUTTON_RELEASED = 0;
|
|
91
109
|
const DRAG_STEPS = 5;
|
|
92
110
|
function centreOf(el) {
|
|
93
111
|
const box = el.getBoundingClientRect();
|
|
@@ -100,28 +118,11 @@ export async function dragElement(source, target, data) {
|
|
|
100
118
|
const dest = target ?? source;
|
|
101
119
|
const from = centreOf(source);
|
|
102
120
|
const to = centreOf(dest);
|
|
103
|
-
const fire = (el, type, at, buttons, related) => {
|
|
104
|
-
const bubbles = !(type.endsWith('enter') || type.endsWith('leave'));
|
|
105
|
-
const init = {
|
|
106
|
-
bubbles,
|
|
107
|
-
cancelable: true,
|
|
108
|
-
clientX: at.x,
|
|
109
|
-
clientY: at.y,
|
|
110
|
-
screenX: at.x,
|
|
111
|
-
screenY: at.y,
|
|
112
|
-
buttons,
|
|
113
|
-
button: 0,
|
|
114
|
-
...(related !== undefined ? { relatedTarget: related } : {}),
|
|
115
|
-
};
|
|
116
|
-
el.dispatchEvent(type.startsWith('pointer')
|
|
117
|
-
? pointerEventFor(el, type, { ...init, pointerId: 1, isPrimary: true })
|
|
118
|
-
: mouseEventFor(el, type, init));
|
|
119
|
-
};
|
|
120
121
|
const sourceRect = source.getBoundingClientRect();
|
|
121
122
|
const destRect = target !== null ? dest.getBoundingClientRect() : null;
|
|
122
123
|
const crosses = (rect, p) => p.x >= rect.left && p.x <= rect.right && p.y >= rect.top && p.y <= rect.bottom;
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
fireAt(source, 'pointerdown', from, BUTTON_HELD);
|
|
125
|
+
fireAt(source, 'mousedown', from, BUTTON_HELD);
|
|
125
126
|
await nativeFrame();
|
|
126
127
|
let leftSource = false;
|
|
127
128
|
let enteredDest = false;
|
|
@@ -129,30 +130,30 @@ export async function dragElement(source, target, data) {
|
|
|
129
130
|
const at = lerp(from, to, step / DRAG_STEPS);
|
|
130
131
|
if (!leftSource && !crosses(sourceRect, at)) {
|
|
131
132
|
leftSource = true;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
fireAt(source, 'pointerout', at, BUTTON_HELD, dest);
|
|
134
|
+
fireAt(source, 'mouseout', at, BUTTON_HELD, dest);
|
|
135
|
+
fireAt(source, 'pointerleave', at, BUTTON_HELD, dest);
|
|
136
|
+
fireAt(source, 'mouseleave', at, BUTTON_HELD, dest);
|
|
136
137
|
}
|
|
137
138
|
if (leftSource && !enteredDest && destRect !== null && crosses(destRect, at)) {
|
|
138
139
|
enteredDest = true;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
fireAt(dest, 'pointerover', at, BUTTON_HELD, source);
|
|
141
|
+
fireAt(dest, 'mouseover', at, BUTTON_HELD, source);
|
|
142
|
+
fireAt(dest, 'pointerenter', at, BUTTON_HELD, source);
|
|
143
|
+
fireAt(dest, 'mouseenter', at, BUTTON_HELD, source);
|
|
143
144
|
}
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
fireAt(dest, 'pointermove', at, BUTTON_HELD);
|
|
146
|
+
fireAt(dest, 'mousemove', at, BUTTON_HELD);
|
|
146
147
|
await nativeFrame();
|
|
147
148
|
}
|
|
148
149
|
if (!enteredDest && destRect !== null) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
fireAt(dest, 'pointerover', to, BUTTON_HELD, source);
|
|
151
|
+
fireAt(dest, 'mouseover', to, BUTTON_HELD, source);
|
|
152
|
+
fireAt(dest, 'pointerenter', to, BUTTON_HELD, source);
|
|
153
|
+
fireAt(dest, 'mouseenter', to, BUTTON_HELD, source);
|
|
153
154
|
}
|
|
154
|
-
|
|
155
|
-
|
|
155
|
+
fireAt(dest, 'pointerup', to, BUTTON_RELEASED);
|
|
156
|
+
fireAt(dest, 'mouseup', to, BUTTON_RELEASED);
|
|
156
157
|
let dropPrevented = false;
|
|
157
158
|
if ('function' === typeof DragEvent) {
|
|
158
159
|
const dataTransfer = makeDataTransfer(data);
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { type ActionTarget } from '../dom/realm.js';
|
|
2
2
|
export declare function pressKey(args: Record<string, unknown>): string;
|
|
3
|
-
export
|
|
3
|
+
export interface ModifierFlags {
|
|
4
4
|
metaKey: boolean;
|
|
5
5
|
ctrlKey: boolean;
|
|
6
6
|
shiftKey: boolean;
|
|
7
7
|
altKey: boolean;
|
|
8
|
-
}
|
|
8
|
+
}
|
|
9
|
+
export declare function pressModifiers(args: Record<string, unknown>): ModifierFlags;
|
|
9
10
|
export declare function pressCode(args: Record<string, unknown>, key: string): string;
|
|
10
11
|
export declare function focusedOrDocument(): HTMLElement;
|
|
11
12
|
export declare function isReflessDocumentPress(ref: string, action: string, args: Record<string, unknown>): boolean;
|
|
12
13
|
export declare function pressKeys(args: Record<string, unknown>): string[];
|
|
13
|
-
export declare
|
|
14
|
-
export declare function
|
|
14
|
+
export declare const legacyKeyCode: (key: string) => number;
|
|
15
|
+
export declare function keyboardEvent(type: string, init: KeyboardEventInit & {
|
|
16
|
+
key: string;
|
|
17
|
+
}): KeyboardEvent;
|
|
18
|
+
export declare function holdKey(el: ActionTarget, key: string, code: string, mods: ModifierFlags, ms: number): Promise<void>;
|
|
19
|
+
export declare function pressCombo(el: ActionTarget, keys: readonly string[], mods: ModifierFlags, holdMs: number): Promise<boolean>;
|
|
20
|
+
export declare function closeModalOnEscape(el: ActionTarget, key: string, keydownProceeded: boolean): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActionType, isGlobalPress, pressKeyFromArgs } from '@reticlehq/core';
|
|
1
|
+
import { ActionType, isGlobalPress, pressKeyFromArgs, pressKeysFromArgs } from '@reticlehq/core';
|
|
2
2
|
import { asSyntheticInput } from './synthetic/synthetic-input.js';
|
|
3
3
|
import { nativeSetTimeout } from '../timers/native/native-timers.js';
|
|
4
4
|
const sleep = (ms) => new Promise((r) => nativeSetTimeout(r, ms));
|
|
@@ -9,16 +9,38 @@ function asString(value, fallback = '') {
|
|
|
9
9
|
export function pressKey(args) {
|
|
10
10
|
return pressKeyFromArgs(args);
|
|
11
11
|
}
|
|
12
|
+
const MODIFIER_FLAG_KEYS = [
|
|
13
|
+
'metaKey',
|
|
14
|
+
'ctrlKey',
|
|
15
|
+
'shiftKey',
|
|
16
|
+
'altKey',
|
|
17
|
+
];
|
|
18
|
+
const MODIFIER_FLAG_ALIASES = {
|
|
19
|
+
meta: 'metaKey',
|
|
20
|
+
cmd: 'metaKey',
|
|
21
|
+
command: 'metaKey',
|
|
22
|
+
super: 'metaKey',
|
|
23
|
+
win: 'metaKey',
|
|
24
|
+
control: 'ctrlKey',
|
|
25
|
+
ctrl: 'ctrlKey',
|
|
26
|
+
shift: 'shiftKey',
|
|
27
|
+
alt: 'altKey',
|
|
28
|
+
option: 'altKey',
|
|
29
|
+
opt: 'altKey',
|
|
30
|
+
};
|
|
12
31
|
export function pressModifiers(args) {
|
|
13
32
|
const raw = args['modifiers'];
|
|
14
33
|
const names = Array.isArray(raw) ? raw.map((m) => asString(m).toLowerCase()) : [];
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
34
|
+
const flags = { metaKey: false, ctrlKey: false, shiftKey: false, altKey: false };
|
|
35
|
+
for (const name of names) {
|
|
36
|
+
const flag = MODIFIER_FLAG_ALIASES[name];
|
|
37
|
+
if (flag !== undefined)
|
|
38
|
+
flags[flag] = true;
|
|
39
|
+
}
|
|
40
|
+
return flags;
|
|
41
|
+
}
|
|
42
|
+
function modifierFlagFor(key) {
|
|
43
|
+
return MODIFIER_FLAG_ALIASES[key.toLowerCase()];
|
|
22
44
|
}
|
|
23
45
|
export function pressCode(args, key) {
|
|
24
46
|
const explicit = args['code'];
|
|
@@ -79,19 +101,45 @@ export function isReflessDocumentPress(ref, action, args) {
|
|
|
79
101
|
return 0 === ref.length && isGlobalPress(args) && ActionType.PRESS === action;
|
|
80
102
|
}
|
|
81
103
|
export function pressKeys(args) {
|
|
82
|
-
|
|
83
|
-
if (!Array.isArray(raw))
|
|
84
|
-
return [];
|
|
85
|
-
return raw.map((k) => asString(k)).filter((k) => k.length > 0);
|
|
104
|
+
return pressKeysFromArgs(args);
|
|
86
105
|
}
|
|
87
106
|
const KEY_REPEAT_MS = 33;
|
|
88
107
|
const KEY_REPEAT_DELAY_MS = 500;
|
|
108
|
+
const LEGACY_KEY_CODES = {
|
|
109
|
+
Backspace: 8,
|
|
110
|
+
Tab: 9,
|
|
111
|
+
Enter: 13,
|
|
112
|
+
Shift: 16,
|
|
113
|
+
Control: 17,
|
|
114
|
+
Alt: 18,
|
|
115
|
+
Escape: 27,
|
|
116
|
+
' ': 32,
|
|
117
|
+
PageUp: 33,
|
|
118
|
+
PageDown: 34,
|
|
119
|
+
End: 35,
|
|
120
|
+
Home: 36,
|
|
121
|
+
ArrowLeft: 37,
|
|
122
|
+
ArrowUp: 38,
|
|
123
|
+
ArrowRight: 39,
|
|
124
|
+
ArrowDown: 40,
|
|
125
|
+
Delete: 46,
|
|
126
|
+
Meta: 91,
|
|
127
|
+
};
|
|
128
|
+
const SINGLE_ALNUM = /^[a-z0-9]$/i;
|
|
129
|
+
export const legacyKeyCode = (key) => LEGACY_KEY_CODES[key] ?? (SINGLE_ALNUM.test(key) ? key.toUpperCase().charCodeAt(0) : 0);
|
|
130
|
+
export function keyboardEvent(type, init) {
|
|
131
|
+
const event = new KeyboardEvent(type, init);
|
|
132
|
+
const code = legacyKeyCode(init.key);
|
|
133
|
+
for (const legacy of ['keyCode', 'which'])
|
|
134
|
+
Object.defineProperty(event, legacy, { get: () => code });
|
|
135
|
+
return event;
|
|
136
|
+
}
|
|
89
137
|
export async function holdKey(el, key, code, mods, ms) {
|
|
90
138
|
const started = Date.now();
|
|
91
139
|
if (ms > KEY_REPEAT_DELAY_MS)
|
|
92
140
|
await sleep(KEY_REPEAT_DELAY_MS);
|
|
93
141
|
while (Date.now() - started < ms) {
|
|
94
|
-
asSyntheticInput(() => el.dispatchEvent(
|
|
142
|
+
asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', {
|
|
95
143
|
key,
|
|
96
144
|
code,
|
|
97
145
|
bubbles: true,
|
|
@@ -103,18 +151,61 @@ export async function holdKey(el, key, code, mods, ms) {
|
|
|
103
151
|
}
|
|
104
152
|
}
|
|
105
153
|
export async function pressCombo(el, keys, mods, holdMs) {
|
|
154
|
+
const held = { ...mods };
|
|
155
|
+
const dispatch = (type, key) => {
|
|
156
|
+
const code = pressCode({}, key);
|
|
157
|
+
return asSyntheticInput(() => el.dispatchEvent(keyboardEvent(type, { key, code, bubbles: true, cancelable: true, ...held })));
|
|
158
|
+
};
|
|
159
|
+
const presses = new Map();
|
|
160
|
+
for (const flag of MODIFIER_FLAG_KEYS) {
|
|
161
|
+
if (held[flag])
|
|
162
|
+
presses.set(flag, 1);
|
|
163
|
+
}
|
|
164
|
+
const pressFlag = (flag) => {
|
|
165
|
+
presses.set(flag, (presses.get(flag) ?? 0) + 1);
|
|
166
|
+
held[flag] = true;
|
|
167
|
+
};
|
|
168
|
+
const releaseFlag = (flag) => {
|
|
169
|
+
const left = (presses.get(flag) ?? 1) - 1;
|
|
170
|
+
presses.set(flag, left);
|
|
171
|
+
if (left <= 0)
|
|
172
|
+
held[flag] = false;
|
|
173
|
+
};
|
|
106
174
|
let prevented = false;
|
|
107
175
|
for (const key of keys) {
|
|
108
|
-
const
|
|
109
|
-
|
|
176
|
+
const flag = modifierFlagFor(key);
|
|
177
|
+
if (flag !== undefined)
|
|
178
|
+
pressFlag(flag);
|
|
179
|
+
const ok = dispatch('keydown', key);
|
|
110
180
|
if (!ok)
|
|
111
181
|
prevented = true;
|
|
182
|
+
closeModalOnEscape(el, key, ok);
|
|
112
183
|
}
|
|
113
184
|
if (holdMs > 0)
|
|
114
185
|
await sleep(holdMs);
|
|
115
186
|
for (const key of [...keys].reverse()) {
|
|
116
|
-
const
|
|
117
|
-
|
|
187
|
+
const flag = modifierFlagFor(key);
|
|
188
|
+
if (flag !== undefined)
|
|
189
|
+
releaseFlag(flag);
|
|
190
|
+
dispatch('keyup', key);
|
|
118
191
|
}
|
|
119
192
|
return prevented;
|
|
120
193
|
}
|
|
194
|
+
export function closeModalOnEscape(el, key, keydownProceeded) {
|
|
195
|
+
if ('Escape' !== key || !keydownProceeded)
|
|
196
|
+
return;
|
|
197
|
+
for (let dialog = el.closest('dialog'); null !== dialog; dialog = dialog.parentElement?.closest('dialog') ?? null) {
|
|
198
|
+
try {
|
|
199
|
+
if (!dialog.open || !dialog.matches(':modal'))
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if ('function' === typeof dialog.requestClose)
|
|
206
|
+
dialog.requestClose();
|
|
207
|
+
else if (dialog.dispatchEvent(new Event('cancel', { cancelable: true })))
|
|
208
|
+
dialog.close();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -27,6 +27,8 @@ interface ActionResult {
|
|
|
27
27
|
component?: string;
|
|
28
28
|
role?: string;
|
|
29
29
|
name?: string;
|
|
30
|
+
nth?: number;
|
|
31
|
+
of?: number;
|
|
30
32
|
source?: {
|
|
31
33
|
file: string;
|
|
32
34
|
line: number;
|
|
@@ -36,7 +38,6 @@ interface ActionResult {
|
|
|
36
38
|
}
|
|
37
39
|
export declare const ZOOM_NEEDS_REAL_BROWSER_MSG: string;
|
|
38
40
|
export declare function executeAction(ref: string, action: string, args?: Record<string, unknown>): Promise<ActionResult>;
|
|
39
|
-
export declare function dispatchWebMcp(tool: string, params: Record<string, unknown>, confirmDangerous?: boolean): Promise<unknown>;
|
|
40
41
|
export interface ActionStep {
|
|
41
42
|
ref: string;
|
|
42
43
|
action: string;
|
package/dist/actions/actions.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readTestId } from '../dom/addressing/testid-attr.js';
|
|
2
|
+
import { ActionType, ActionWarning, clampHoldMs, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, QueryBy, SettleReason, } from '@reticlehq/core';
|
|
3
|
+
import { elementsMatching } from '../dom/query.js';
|
|
2
4
|
import { asSyntheticInput } from './synthetic/synthetic-input.js';
|
|
3
5
|
import { echoRef, refs } from '../dom/addressing/refs.js';
|
|
4
6
|
import { dangerousActionContext, requiresDangerousConfirmation, submitControlFor, } from './danger-context.js';
|
|
@@ -9,7 +11,7 @@ import { isActionTarget, isForm, isInput, isSelect, isTextArea, } from '../dom/r
|
|
|
9
11
|
import { nativeSetTimeout, settle } from '../timers/native/native-timers.js';
|
|
10
12
|
import { AppearedText } from './appeared-text.js';
|
|
11
13
|
import { alreadyAtCheckedState, assertAriaToggleLanded, isAriaToggle } from './check-state.js';
|
|
12
|
-
import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, } from './actions-press.js';
|
|
14
|
+
import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, closeModalOnEscape, keyboardEvent, } from './actions-press.js';
|
|
13
15
|
function asString(value, fallback = '') {
|
|
14
16
|
return 'string' === typeof value ? value : fallback;
|
|
15
17
|
}
|
|
@@ -22,7 +24,7 @@ function requireElement(ref) {
|
|
|
22
24
|
return el;
|
|
23
25
|
}
|
|
24
26
|
function anchorOf(el) {
|
|
25
|
-
const testid = el
|
|
27
|
+
const testid = readTestId(el) ?? undefined;
|
|
26
28
|
const info = identifyComponent(el);
|
|
27
29
|
const out = {};
|
|
28
30
|
if (testid !== undefined)
|
|
@@ -38,6 +40,14 @@ function anchorOf(el) {
|
|
|
38
40
|
out.role = role;
|
|
39
41
|
if (name.length > 0)
|
|
40
42
|
out.name = name;
|
|
43
|
+
if (role.length > 0 && name.length > 0) {
|
|
44
|
+
const same = elementsMatching({ by: QueryBy.ROLE, value: role, name });
|
|
45
|
+
const at = same.indexOf(el);
|
|
46
|
+
if (1 < same.length && 0 <= at) {
|
|
47
|
+
out.nth = at;
|
|
48
|
+
out.of = same.length;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
41
51
|
return out;
|
|
42
52
|
}
|
|
43
53
|
const result = (ref, action, effect, settled, settleReason, anchor, warning) => {
|
|
@@ -60,6 +70,10 @@ const result = (ref, action, effect, settled, settleReason, anchor, warning) =>
|
|
|
60
70
|
base.role = anchor.role;
|
|
61
71
|
if (anchor.name !== undefined)
|
|
62
72
|
base.name = anchor.name;
|
|
73
|
+
if (anchor.nth !== undefined)
|
|
74
|
+
base.nth = anchor.nth;
|
|
75
|
+
if (anchor.of !== undefined)
|
|
76
|
+
base.of = anchor.of;
|
|
63
77
|
if (warning !== undefined)
|
|
64
78
|
base.warning = warning;
|
|
65
79
|
return base;
|
|
@@ -137,19 +151,13 @@ function activeRef(el) {
|
|
|
137
151
|
return null;
|
|
138
152
|
return refs.refFor(active);
|
|
139
153
|
}
|
|
140
|
-
const MAX_HOLD_MS = 30_000;
|
|
141
|
-
function clampHold(raw) {
|
|
142
|
-
if ('number' !== typeof raw || !Number.isFinite(raw) || raw <= 0)
|
|
143
|
-
return 0;
|
|
144
|
-
return Math.min(raw, MAX_HOLD_MS);
|
|
145
|
-
}
|
|
146
154
|
async function dispatchFor(el, action, args) {
|
|
147
155
|
if (ActionType.CLICK === action) {
|
|
148
|
-
const hold =
|
|
156
|
+
const hold = clampHoldMs(args['holdMs']);
|
|
149
157
|
return await fireClickSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() });
|
|
150
158
|
}
|
|
151
159
|
if (ActionType.TAP === action) {
|
|
152
|
-
const hold =
|
|
160
|
+
const hold = clampHoldMs(args['holdMs']);
|
|
153
161
|
return await fireTapSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() });
|
|
154
162
|
}
|
|
155
163
|
return { prevented: await dispatchOther(el, action, args), heldMs: 0 };
|
|
@@ -175,7 +183,7 @@ async function dispatchOther(el, action, args) {
|
|
|
175
183
|
el.dispatchEvent(mouseEventFor(el, 'mouseenter', { relatedTarget: from }));
|
|
176
184
|
firePointer(el, 'pointermove', from);
|
|
177
185
|
const moved = el.dispatchEvent(mouseEventFor(el, 'mousemove', { bubbles: true, cancelable: true }));
|
|
178
|
-
const holdMs =
|
|
186
|
+
const holdMs = clampHoldMs(args['holdMs']);
|
|
179
187
|
if (holdMs > 0)
|
|
180
188
|
await sleep(holdMs);
|
|
181
189
|
return !moved;
|
|
@@ -274,14 +282,15 @@ async function dispatchOther(el, action, args) {
|
|
|
274
282
|
const mods = pressModifiers(args);
|
|
275
283
|
const combo = pressKeys(args);
|
|
276
284
|
if (combo.length > 0)
|
|
277
|
-
return await pressCombo(el, combo, mods,
|
|
285
|
+
return await pressCombo(el, combo, mods, clampHoldMs(args['holdMs']));
|
|
278
286
|
const key = pressKey(args);
|
|
279
287
|
const code = pressCode(args, key);
|
|
280
|
-
const down = asSyntheticInput(() => el.dispatchEvent(
|
|
281
|
-
|
|
288
|
+
const down = asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', { key, code, bubbles: true, cancelable: true, ...mods })));
|
|
289
|
+
closeModalOnEscape(el, key, down);
|
|
290
|
+
const hold = clampHoldMs(args['holdMs']);
|
|
282
291
|
if (hold > 0)
|
|
283
292
|
await holdKey(el, key, code, mods, hold);
|
|
284
|
-
asSyntheticInput(() => el.dispatchEvent(
|
|
293
|
+
asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keyup', { key, code, bubbles: true, ...mods })));
|
|
285
294
|
return !down;
|
|
286
295
|
}
|
|
287
296
|
case ActionType.SCROLL_INTO_VIEW:
|
|
@@ -410,16 +419,6 @@ export async function executeAction(ref, action, args = {}) {
|
|
|
410
419
|
return result(ref, action, effect, settled, settleReason, anchor, warning);
|
|
411
420
|
}
|
|
412
421
|
const sleep = (ms) => new Promise((r) => nativeSetTimeout(r, ms));
|
|
413
|
-
export async function dispatchWebMcp(tool, params, confirmDangerous = false) {
|
|
414
|
-
if (requiresDangerousConfirmation(tool) && !confirmDangerous) {
|
|
415
|
-
throw new Error(`potentially destructive WebMCP tool blocked; retry with ${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
|
|
416
|
-
}
|
|
417
|
-
const mc = navigator.modelContext;
|
|
418
|
-
if (mc === undefined || typeof mc.callTool !== 'function') {
|
|
419
|
-
throw new Error('WebMCP (navigator.modelContext) not available on this page');
|
|
420
|
-
}
|
|
421
|
-
return await mc.callTool(tool, params);
|
|
422
|
-
}
|
|
423
422
|
export async function executeSequence(steps) {
|
|
424
423
|
const effects = [];
|
|
425
424
|
const stepResults = [];
|
|
@@ -33,7 +33,7 @@ export function dangerousActionContext(el) {
|
|
|
33
33
|
const SUBMIT_CONTROL_SELECTOR = 'button[type="submit"], input[type="submit"], button:not([type]):not([type=""])';
|
|
34
34
|
export function submitControlFor(el) {
|
|
35
35
|
const found = el.closest('form')?.querySelector(SUBMIT_CONTROL_SELECTOR);
|
|
36
|
-
return found
|
|
36
|
+
return isHtmlElement(found) ? found : null;
|
|
37
37
|
}
|
|
38
38
|
export function requiresDangerousConfirmation(text, role) {
|
|
39
39
|
return isDangerousActionText(text, role);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ComponentStateReason, ElementQuerySchema, ReticleCommand, SnapshotMode, TRANSPORT_LIMITS, selectPath, capDepth, } from '@reticlehq/core';
|
|
2
2
|
import { buildSnapshot } from '../dom/snapshot.js';
|
|
3
3
|
import { paintContextOf } from '../dom/paint-context.js';
|
|
4
4
|
import { matchQuery, runQuery } from '../dom/query.js';
|
|
5
|
-
import { executeAction, executeSequence
|
|
5
|
+
import { executeAction, executeSequence } from '../actions/actions.js';
|
|
6
6
|
import { describe } from '../dom/a11y.js';
|
|
7
7
|
import { documentHasSourceStamps, sourceFor, formatSource } from '../dom/addressing/source.js';
|
|
8
8
|
import { themeReport } from '../dom/theme.js';
|
|
@@ -198,10 +198,6 @@ export function createCommandRegistry() {
|
|
|
198
198
|
reg.set(ReticleCommand.MATCH, (args) => matchQuery(ElementQuerySchema.parse(record(args['query'])), str(args['state'])));
|
|
199
199
|
reg.set(ReticleCommand.ACT, (args) => {
|
|
200
200
|
const action = str(args['action']) ?? '';
|
|
201
|
-
if (action === ActionType.WEBMCP) {
|
|
202
|
-
const inner = record(args['args']);
|
|
203
|
-
return dispatchWebMcp(str(inner['tool']) ?? '', record(inner['params']), true === inner[DANGEROUS_ACTION_CONFIRM_ARG]);
|
|
204
|
-
}
|
|
205
201
|
return executeAction(str(args['ref']) ?? '', action, record(args['args']));
|
|
206
202
|
});
|
|
207
203
|
reg.set(ReticleCommand.ACT_SEQUENCE, (args) => executeSequence((Array.isArray(args['steps']) ? args['steps'] : [])));
|
package/dist/dom/a11y.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readTestId } from './addressing/testid-attr.js';
|
|
2
|
+
import { DEFAULT_TESTID_ATTR, ElementState, REDACTED_VALUE, } from '@reticlehq/core';
|
|
2
3
|
import { isButton, isHtmlElement, isImage, isInput, isMeter, isOutput, isProgress, isSelect, isTextArea, } from './realm.js';
|
|
3
4
|
import { refs } from './addressing/refs.js';
|
|
5
|
+
import { capturedRootOf } from './shadow-registry.js';
|
|
4
6
|
import { inspectChart } from './chart.js';
|
|
5
7
|
import { isSensitiveKey } from '../security/serialization.js';
|
|
6
8
|
import { formatSource, sourceFromDom } from './addressing/source.js';
|
|
7
9
|
const HTML_DETAILS_TAG = 'details';
|
|
8
10
|
const HTML_DETAILS_OPEN_ATTRIBUTE = 'open';
|
|
9
|
-
const
|
|
11
|
+
const HTML_SUMMARY_TAG = 'summary';
|
|
10
12
|
const NAME_FROM_CONTENT = new Set([
|
|
11
13
|
'button',
|
|
12
14
|
'link',
|
|
@@ -30,9 +32,11 @@ const NAME_FROM_CONTENT = new Set([
|
|
|
30
32
|
'status',
|
|
31
33
|
'alert',
|
|
32
34
|
]);
|
|
33
|
-
const INPUT_TEXT_TYPES = new Set(['text', 'email', 'tel', 'url', '
|
|
35
|
+
const INPUT_TEXT_TYPES = new Set(['text', 'email', 'tel', 'url', 'password', '']);
|
|
34
36
|
function inputRole(input) {
|
|
35
37
|
const type = input.type.toLowerCase();
|
|
38
|
+
if ('search' === type)
|
|
39
|
+
return 'searchbox';
|
|
36
40
|
if (INPUT_TEXT_TYPES.has(type))
|
|
37
41
|
return 'textbox';
|
|
38
42
|
if ('checkbox' === type)
|
|
@@ -173,13 +177,19 @@ function textWithoutHidden(node) {
|
|
|
173
177
|
const alt = el.getAttribute('alt');
|
|
174
178
|
return null === alt ? '' : alt;
|
|
175
179
|
}
|
|
176
|
-
|
|
180
|
+
let result = '';
|
|
181
|
+
let lastWasElement = false;
|
|
177
182
|
for (const child of el.childNodes) {
|
|
178
183
|
const piece = textWithoutHidden(child);
|
|
179
|
-
if (piece.length
|
|
180
|
-
|
|
184
|
+
if (0 === piece.length)
|
|
185
|
+
continue;
|
|
186
|
+
const isEl = Node.ELEMENT_NODE === child.nodeType;
|
|
187
|
+
if (result.length > 0 && (isEl || lastWasElement))
|
|
188
|
+
result += ' ';
|
|
189
|
+
result += piece;
|
|
190
|
+
lastWasElement = isEl;
|
|
181
191
|
}
|
|
182
|
-
return
|
|
192
|
+
return result;
|
|
183
193
|
}
|
|
184
194
|
export function getAccessibleName(el) {
|
|
185
195
|
const labelled = labelledByText(el);
|
|
@@ -275,7 +285,8 @@ export function isSensitiveField(el) {
|
|
|
275
285
|
const identifiers = [
|
|
276
286
|
el.getAttribute('name') ?? '',
|
|
277
287
|
el.id,
|
|
278
|
-
el
|
|
288
|
+
readTestId(el) ?? '',
|
|
289
|
+
el.getAttribute(DEFAULT_TESTID_ATTR) ?? '',
|
|
279
290
|
el.getAttribute('aria-label') ?? '',
|
|
280
291
|
];
|
|
281
292
|
const sensitiveAutocomplete = /current-password|new-password|cc-number|cc-csc|one-time-code/i.test(autocomplete);
|
|
@@ -299,8 +310,13 @@ function hiddenInsideClosedDetails(el) {
|
|
|
299
310
|
const details = parent.closest(HTML_DETAILS_TAG);
|
|
300
311
|
if (null === details || details.hasAttribute(HTML_DETAILS_OPEN_ATTRIBUTE))
|
|
301
312
|
return false;
|
|
302
|
-
const summary = details.
|
|
303
|
-
return
|
|
313
|
+
const summary = Array.from(details.children).find((child) => HTML_SUMMARY_TAG === child.localName);
|
|
314
|
+
return summary === undefined || !summary.contains(el);
|
|
315
|
+
}
|
|
316
|
+
function stalledFade(el) {
|
|
317
|
+
if (!el.ownerDocument.hidden || !('getAnimations' in el))
|
|
318
|
+
return false;
|
|
319
|
+
return el.getAnimations().some((a) => 'running' === a.playState || a.pending);
|
|
304
320
|
}
|
|
305
321
|
function selfHidden(el) {
|
|
306
322
|
if ('true' === el.getAttribute('aria-hidden'))
|
|
@@ -317,12 +333,28 @@ function selfHidden(el) {
|
|
|
317
333
|
'collapse' === style.visibility) {
|
|
318
334
|
return true;
|
|
319
335
|
}
|
|
320
|
-
if (0 === Number.parseFloat(style.opacity || '1'))
|
|
336
|
+
if (0 === Number.parseFloat(style.opacity || '1') && !stalledFade(el))
|
|
321
337
|
return true;
|
|
322
338
|
}
|
|
323
339
|
return false;
|
|
324
340
|
}
|
|
341
|
+
function slotInCapturedClosedRoot(el) {
|
|
342
|
+
const host = el.parentElement;
|
|
343
|
+
if (null === host || null !== host.shadowRoot)
|
|
344
|
+
return null;
|
|
345
|
+
const root = capturedRootOf(host);
|
|
346
|
+
if (null === root)
|
|
347
|
+
return null;
|
|
348
|
+
for (const slot of Array.from(root.querySelectorAll('slot'))) {
|
|
349
|
+
if (slot.assignedElements().includes(el))
|
|
350
|
+
return slot;
|
|
351
|
+
}
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
325
354
|
function parentAcrossShadowBoundary(el) {
|
|
355
|
+
const slot = el.assignedSlot ?? slotInCapturedClosedRoot(el);
|
|
356
|
+
if (null !== slot)
|
|
357
|
+
return slot;
|
|
326
358
|
if (null !== el.parentElement)
|
|
327
359
|
return el.parentElement;
|
|
328
360
|
const host = el.getRootNode().host;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TRANSPORT_LIMITS, asRef } from '@reticlehq/core';
|
|
2
|
+
import { ReticleStorageKey } from '../../storage-keys.js';
|
|
2
3
|
export const MAX_TRACKED_REFS = 10000;
|
|
3
4
|
const REF_ELISION = '…';
|
|
4
5
|
const REF_PREFIX = 'e';
|
|
@@ -7,7 +8,7 @@ export function echoRef(ref) {
|
|
|
7
8
|
return max >= ref.length ? ref : `${ref.slice(0, max)}${REF_ELISION}`;
|
|
8
9
|
}
|
|
9
10
|
const SWEEP_EVERY_MINTS = 1000;
|
|
10
|
-
const REF_BASE_KEY =
|
|
11
|
+
const REF_BASE_KEY = ReticleStorageKey.REF_BASE;
|
|
11
12
|
export const REF_BLOCK = 100;
|
|
12
13
|
function readBase(store) {
|
|
13
14
|
try {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { DEFAULT_TESTID_ATTR, TESTID_ATTR_PATTERN } from '@reticlehq/core';
|
|
2
|
+
let testIdAttr = DEFAULT_TESTID_ATTR;
|
|
3
|
+
export function getTestIdAttr() {
|
|
4
|
+
return testIdAttr;
|
|
5
|
+
}
|
|
6
|
+
export function testIdSelector() {
|
|
7
|
+
return `[${testIdAttr}]`;
|
|
8
|
+
}
|
|
9
|
+
export function readTestId(el) {
|
|
10
|
+
return el.getAttribute(testIdAttr);
|
|
11
|
+
}
|
|
12
|
+
export function setTestIdAttr(name) {
|
|
13
|
+
testIdAttr = undefined !== name && TESTID_ATTR_PATTERN.test(name) ? name : DEFAULT_TESTID_ATTR;
|
|
14
|
+
}
|