@reticlehq/browser 3.5.0 → 3.7.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/action-report.d.ts +54 -0
- package/dist/actions/action-report.js +60 -0
- package/dist/actions/actions-dom.d.ts +1 -0
- package/dist/actions/actions-dom.js +56 -43
- package/dist/actions/actions-press.d.ts +11 -4
- package/dist/actions/actions-press.js +132 -17
- package/dist/actions/actions.d.ts +1 -35
- package/dist/actions/actions.js +32 -65
- package/dist/actions/appeared-text.js +3 -0
- package/dist/actions/danger-context.d.ts +2 -0
- package/dist/actions/danger-context.js +29 -3
- package/dist/actions/value-input.d.ts +1 -0
- package/dist/actions/value-input.js +11 -0
- package/dist/commands/commands.js +4 -0
- package/dist/connect-options.d.ts +1 -0
- package/dist/dom/a11y.js +264 -14
- package/dist/dom/addressing/coverage-markers.d.ts +6 -0
- package/dist/dom/addressing/coverage-markers.js +13 -0
- 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/dom-ignore.d.ts +2 -0
- package/dist/dom/dom-ignore.js +15 -2
- package/dist/dom/query.d.ts +2 -1
- package/dist/dom/query.js +116 -27
- package/dist/dom/render-page.d.ts +12 -0
- package/dist/dom/render-page.js +135 -0
- package/dist/dom/snapshot.js +56 -7
- package/dist/observers/departure.js +51 -9
- package/dist/observers/dom.js +35 -18
- package/dist/observers/field.js +5 -21
- 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 +57 -8
- package/dist/observers/perf.js +53 -1
- package/dist/observers/route.js +6 -5
- package/dist/observers/storage.js +6 -1
- package/dist/presenter/carousel/carousel.d.ts +5 -2
- package/dist/presenter/carousel/carousel.js +30 -8
- package/dist/presenter/carousel/panel-slides.d.ts +3 -4
- package/dist/presenter/carousel/panel-slides.js +48 -9
- 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 +13 -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 +12 -4
- package/dist/presenter/chrome/presenter-log.js +100 -10
- package/dist/presenter/chrome/presenter-morph.d.ts +1 -0
- package/dist/presenter/chrome/presenter-morph.js +34 -0
- package/dist/presenter/chrome/presenter-report-copy.d.ts +28 -5
- package/dist/presenter/chrome/presenter-report-copy.js +40 -5
- package/dist/presenter/hud-telemetry.js +83 -3
- 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-styles.js +3 -3
- package/dist/presenter/presenter-account.d.ts +31 -8
- package/dist/presenter/presenter-account.js +105 -15
- package/dist/presenter/presenter-agent-link.d.ts +9 -0
- package/dist/presenter/presenter-agent-link.js +62 -0
- package/dist/presenter/presenter-chat-views.d.ts +43 -0
- package/dist/presenter/presenter-chat-views.js +622 -0
- package/dist/presenter/presenter-config.d.ts +5 -2
- package/dist/presenter/presenter-config.js +5 -3
- package/dist/presenter/presenter-controls.d.ts +3 -2
- package/dist/presenter/presenter-controls.js +163 -61
- 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 +187 -0
- package/dist/presenter/presenter-harness-row.d.ts +67 -0
- package/dist/presenter/presenter-harness-row.js +209 -0
- package/dist/presenter/presenter-personas.d.ts +35 -0
- package/dist/presenter/presenter-personas.js +95 -0
- package/dist/presenter/presenter-plan.d.ts +10 -0
- package/dist/presenter/presenter-plan.js +186 -0
- package/dist/presenter/presenter-report-styles.js +46 -21
- package/dist/presenter/presenter-report.d.ts +2 -1
- package/dist/presenter/presenter-report.js +123 -20
- package/dist/presenter/presenter-settings-styles.js +18 -4
- package/dist/presenter/presenter-settings.d.ts +8 -2
- package/dist/presenter/presenter-settings.js +134 -33
- package/dist/presenter/presenter-shell-styles.js +48 -42
- package/dist/presenter/presenter-shell.d.ts +17 -4
- package/dist/presenter/presenter-shell.js +139 -79
- package/dist/presenter/presenter-styles.js +9 -3
- package/dist/presenter/presenter-workspace.js +10 -3
- package/dist/presenter/presenter.d.ts +6 -4
- package/dist/presenter/presenter.js +171 -22
- 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 +4 -0
- package/dist/reticle-inject.js +938 -494
- package/dist/reticle.d.ts +10 -1
- package/dist/reticle.js +109 -11
- 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 +15 -0
- package/dist/storage-keys.js +21 -0
- package/dist/transport/transport.js +5 -3
- package/dist/transport/unreachable-message.d.ts +1 -1
- package/dist/transport/unreachable-message.js +3 -5
- package/package.json +3 -2
- package/dist/presenter/carousel/offer-card.d.ts +0 -21
- package/dist/presenter/carousel/offer-card.js +0 -55
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type SettleReason } from '@reticlehq/core';
|
|
2
|
+
export interface ActionEffect {
|
|
3
|
+
dispatched: true;
|
|
4
|
+
targetMatched: boolean;
|
|
5
|
+
visible: boolean;
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
defaultPrevented: boolean;
|
|
8
|
+
focusMoved: string | null;
|
|
9
|
+
valueChanged: boolean;
|
|
10
|
+
autofillSafe?: true;
|
|
11
|
+
domMutatedWithin: number;
|
|
12
|
+
appeared?: string;
|
|
13
|
+
occluded: boolean;
|
|
14
|
+
occludedBy: string | null;
|
|
15
|
+
scrolledIntoView: boolean;
|
|
16
|
+
heldMs?: number;
|
|
17
|
+
alreadyAtValue?: true;
|
|
18
|
+
}
|
|
19
|
+
export interface ActionResult {
|
|
20
|
+
ok: true;
|
|
21
|
+
ref: string;
|
|
22
|
+
action: string;
|
|
23
|
+
dispatched: boolean;
|
|
24
|
+
settled: boolean;
|
|
25
|
+
settleReason: SettleReason | null;
|
|
26
|
+
effect: ActionEffect;
|
|
27
|
+
testid?: string;
|
|
28
|
+
component?: string;
|
|
29
|
+
role?: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
nth?: number;
|
|
32
|
+
of?: number;
|
|
33
|
+
source?: {
|
|
34
|
+
file: string;
|
|
35
|
+
line: number;
|
|
36
|
+
column?: number;
|
|
37
|
+
};
|
|
38
|
+
warning?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface CapturedAnchor {
|
|
41
|
+
testid?: string;
|
|
42
|
+
component?: string;
|
|
43
|
+
source?: {
|
|
44
|
+
file: string;
|
|
45
|
+
line: number;
|
|
46
|
+
column?: number;
|
|
47
|
+
};
|
|
48
|
+
role?: string;
|
|
49
|
+
name?: string;
|
|
50
|
+
nth?: number;
|
|
51
|
+
of?: number;
|
|
52
|
+
}
|
|
53
|
+
export declare function anchorOf(el: Element): CapturedAnchor;
|
|
54
|
+
export declare const result: (ref: string, action: string, effect: ActionEffect, settled: boolean, settleReason: SettleReason | null, anchor: CapturedAnchor, warning?: string) => ActionResult;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readTestId } from '../dom/addressing/testid-attr.js';
|
|
2
|
+
import { QueryBy } from '@reticlehq/core';
|
|
3
|
+
import { elementsMatching } from '../dom/query.js';
|
|
4
|
+
import { getAccessibleName, getRole } from '../dom/a11y.js';
|
|
5
|
+
import { identifyComponent } from '../registry/stores/adapters.js';
|
|
6
|
+
export function anchorOf(el) {
|
|
7
|
+
const testid = readTestId(el) ?? undefined;
|
|
8
|
+
const info = identifyComponent(el);
|
|
9
|
+
const out = {};
|
|
10
|
+
if (testid !== undefined)
|
|
11
|
+
out.testid = testid;
|
|
12
|
+
const component = info?.componentStack[0];
|
|
13
|
+
if (component !== undefined)
|
|
14
|
+
out.component = component;
|
|
15
|
+
if (info?.source !== undefined)
|
|
16
|
+
out.source = info.source;
|
|
17
|
+
const role = getRole(el);
|
|
18
|
+
const name = getAccessibleName(el);
|
|
19
|
+
if (role.length > 0)
|
|
20
|
+
out.role = role;
|
|
21
|
+
if (name.length > 0)
|
|
22
|
+
out.name = name;
|
|
23
|
+
if (role.length > 0 && name.length > 0) {
|
|
24
|
+
const same = elementsMatching({ by: QueryBy.ROLE, value: role, name });
|
|
25
|
+
const at = same.indexOf(el);
|
|
26
|
+
if (1 < same.length && 0 <= at) {
|
|
27
|
+
out.nth = at;
|
|
28
|
+
out.of = same.length;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
export const result = (ref, action, effect, settled, settleReason, anchor, warning) => {
|
|
34
|
+
const base = {
|
|
35
|
+
ok: true,
|
|
36
|
+
ref,
|
|
37
|
+
action,
|
|
38
|
+
dispatched: true,
|
|
39
|
+
settled,
|
|
40
|
+
settleReason,
|
|
41
|
+
effect,
|
|
42
|
+
};
|
|
43
|
+
if (anchor.testid !== undefined)
|
|
44
|
+
base.testid = anchor.testid;
|
|
45
|
+
if (anchor.component !== undefined)
|
|
46
|
+
base.component = anchor.component;
|
|
47
|
+
if (anchor.source !== undefined)
|
|
48
|
+
base.source = anchor.source;
|
|
49
|
+
if (anchor.role !== undefined)
|
|
50
|
+
base.role = anchor.role;
|
|
51
|
+
if (anchor.name !== undefined)
|
|
52
|
+
base.name = anchor.name;
|
|
53
|
+
if (anchor.nth !== undefined)
|
|
54
|
+
base.nth = anchor.nth;
|
|
55
|
+
if (anchor.of !== undefined)
|
|
56
|
+
base.of = anchor.of;
|
|
57
|
+
if (warning !== undefined)
|
|
58
|
+
base.warning = warning;
|
|
59
|
+
return base;
|
|
60
|
+
};
|
|
@@ -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);
|
|
@@ -230,3 +231,15 @@ function makeTouchEvent(type, touches) {
|
|
|
230
231
|
}
|
|
231
232
|
return new Event(type, { bubbles: true, cancelable: true });
|
|
232
233
|
}
|
|
234
|
+
const POPUP_ROLES = new Set(['menu', 'menubar', 'listbox', 'dialog', 'alertdialog', 'tooltip']);
|
|
235
|
+
const CLOSED_STATE = 'closed';
|
|
236
|
+
export function assertNotLeaving(el) {
|
|
237
|
+
for (let up = el.parentElement; up !== null; up = up.parentElement) {
|
|
238
|
+
if (CLOSED_STATE !== up.getAttribute('data-state'))
|
|
239
|
+
continue;
|
|
240
|
+
if (!POPUP_ROLES.has(up.getAttribute('role') ?? ''))
|
|
241
|
+
continue;
|
|
242
|
+
throw new Error(`'${up.getAttribute('role') ?? 'popup'}' holding this element is closing (data-state="closed"), ` +
|
|
243
|
+
'so a person could not click it: look again once it has gone and act on what is open');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -1,14 +1,21 @@
|
|
|
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;
|
|
11
|
+
export declare function dispatchKeypress(el: ActionTarget, key: string, code: string, mods: ModifierFlags, repeat?: boolean): void;
|
|
10
12
|
export declare function focusedOrDocument(): HTMLElement;
|
|
11
13
|
export declare function isReflessDocumentPress(ref: string, action: string, args: Record<string, unknown>): boolean;
|
|
12
14
|
export declare function pressKeys(args: Record<string, unknown>): string[];
|
|
13
|
-
export declare
|
|
14
|
-
export declare function
|
|
15
|
+
export declare const legacyKeyCode: (key: string) => number;
|
|
16
|
+
export declare function keyboardEvent(type: string, init: KeyboardEventInit & {
|
|
17
|
+
key: string;
|
|
18
|
+
}): KeyboardEvent;
|
|
19
|
+
export declare function holdKey(el: ActionTarget, key: string, code: string, mods: ModifierFlags, ms: number): Promise<void>;
|
|
20
|
+
export declare function pressCombo(el: ActionTarget, keys: readonly string[], mods: ModifierFlags, holdMs: number): Promise<boolean>;
|
|
21
|
+
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'];
|
|
@@ -35,6 +57,26 @@ export function pressCode(args, key) {
|
|
|
35
57
|
}
|
|
36
58
|
return /^[A-Z][A-Za-z0-9]*$/.test(key) && KNOWN_NAMED_KEYS.has(key) ? key : '';
|
|
37
59
|
}
|
|
60
|
+
export function dispatchKeypress(el, key, code, mods, repeat = false) {
|
|
61
|
+
if (ENTER_KEY !== key && 1 !== key.length)
|
|
62
|
+
return;
|
|
63
|
+
const event = new KeyboardEvent('keypress', {
|
|
64
|
+
key,
|
|
65
|
+
code,
|
|
66
|
+
bubbles: true,
|
|
67
|
+
cancelable: true,
|
|
68
|
+
repeat,
|
|
69
|
+
...mods,
|
|
70
|
+
});
|
|
71
|
+
const legacy = ENTER_KEY === key ? ENTER_CHAR_CODE : key.charCodeAt(0);
|
|
72
|
+
for (const field of LEGACY_KEY_CODE_FIELDS) {
|
|
73
|
+
Object.defineProperty(event, field, { value: legacy });
|
|
74
|
+
}
|
|
75
|
+
asSyntheticInput(() => el.dispatchEvent(event));
|
|
76
|
+
}
|
|
77
|
+
const ENTER_KEY = 'Enter';
|
|
78
|
+
const ENTER_CHAR_CODE = 13;
|
|
79
|
+
const LEGACY_KEY_CODE_FIELDS = ['keyCode', 'charCode', 'which'];
|
|
38
80
|
const KNOWN_NAMED_KEYS = new Set([
|
|
39
81
|
'Enter',
|
|
40
82
|
'Escape',
|
|
@@ -79,19 +121,45 @@ export function isReflessDocumentPress(ref, action, args) {
|
|
|
79
121
|
return 0 === ref.length && isGlobalPress(args) && ActionType.PRESS === action;
|
|
80
122
|
}
|
|
81
123
|
export function pressKeys(args) {
|
|
82
|
-
|
|
83
|
-
if (!Array.isArray(raw))
|
|
84
|
-
return [];
|
|
85
|
-
return raw.map((k) => asString(k)).filter((k) => k.length > 0);
|
|
124
|
+
return pressKeysFromArgs(args);
|
|
86
125
|
}
|
|
87
126
|
const KEY_REPEAT_MS = 33;
|
|
88
127
|
const KEY_REPEAT_DELAY_MS = 500;
|
|
128
|
+
const LEGACY_KEY_CODES = {
|
|
129
|
+
Backspace: 8,
|
|
130
|
+
Tab: 9,
|
|
131
|
+
Enter: 13,
|
|
132
|
+
Shift: 16,
|
|
133
|
+
Control: 17,
|
|
134
|
+
Alt: 18,
|
|
135
|
+
Escape: 27,
|
|
136
|
+
' ': 32,
|
|
137
|
+
PageUp: 33,
|
|
138
|
+
PageDown: 34,
|
|
139
|
+
End: 35,
|
|
140
|
+
Home: 36,
|
|
141
|
+
ArrowLeft: 37,
|
|
142
|
+
ArrowUp: 38,
|
|
143
|
+
ArrowRight: 39,
|
|
144
|
+
ArrowDown: 40,
|
|
145
|
+
Delete: 46,
|
|
146
|
+
Meta: 91,
|
|
147
|
+
};
|
|
148
|
+
const SINGLE_ALNUM = /^[a-z0-9]$/i;
|
|
149
|
+
export const legacyKeyCode = (key) => LEGACY_KEY_CODES[key] ?? (SINGLE_ALNUM.test(key) ? key.toUpperCase().charCodeAt(0) : 0);
|
|
150
|
+
export function keyboardEvent(type, init) {
|
|
151
|
+
const event = new KeyboardEvent(type, init);
|
|
152
|
+
const code = legacyKeyCode(init.key);
|
|
153
|
+
for (const legacy of ['keyCode', 'which'])
|
|
154
|
+
Object.defineProperty(event, legacy, { get: () => code });
|
|
155
|
+
return event;
|
|
156
|
+
}
|
|
89
157
|
export async function holdKey(el, key, code, mods, ms) {
|
|
90
158
|
const started = Date.now();
|
|
91
159
|
if (ms > KEY_REPEAT_DELAY_MS)
|
|
92
160
|
await sleep(KEY_REPEAT_DELAY_MS);
|
|
93
161
|
while (Date.now() - started < ms) {
|
|
94
|
-
asSyntheticInput(() => el.dispatchEvent(
|
|
162
|
+
const down = asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', {
|
|
95
163
|
key,
|
|
96
164
|
code,
|
|
97
165
|
bubbles: true,
|
|
@@ -99,22 +167,69 @@ export async function holdKey(el, key, code, mods, ms) {
|
|
|
99
167
|
repeat: true,
|
|
100
168
|
...mods,
|
|
101
169
|
})));
|
|
170
|
+
if (down)
|
|
171
|
+
dispatchKeypress(el, key, code, mods, true);
|
|
102
172
|
await sleep(Math.min(KEY_REPEAT_MS, Math.max(0, ms - (Date.now() - started))));
|
|
103
173
|
}
|
|
104
174
|
}
|
|
105
175
|
export async function pressCombo(el, keys, mods, holdMs) {
|
|
176
|
+
const held = { ...mods };
|
|
177
|
+
const dispatch = (type, key) => {
|
|
178
|
+
const code = pressCode({}, key);
|
|
179
|
+
return asSyntheticInput(() => el.dispatchEvent(keyboardEvent(type, { key, code, bubbles: true, cancelable: true, ...held })));
|
|
180
|
+
};
|
|
181
|
+
const presses = new Map();
|
|
182
|
+
for (const flag of MODIFIER_FLAG_KEYS) {
|
|
183
|
+
if (held[flag])
|
|
184
|
+
presses.set(flag, 1);
|
|
185
|
+
}
|
|
186
|
+
const pressFlag = (flag) => {
|
|
187
|
+
presses.set(flag, (presses.get(flag) ?? 0) + 1);
|
|
188
|
+
held[flag] = true;
|
|
189
|
+
};
|
|
190
|
+
const releaseFlag = (flag) => {
|
|
191
|
+
const left = (presses.get(flag) ?? 1) - 1;
|
|
192
|
+
presses.set(flag, left);
|
|
193
|
+
if (left <= 0)
|
|
194
|
+
held[flag] = false;
|
|
195
|
+
};
|
|
106
196
|
let prevented = false;
|
|
107
197
|
for (const key of keys) {
|
|
108
|
-
const
|
|
109
|
-
|
|
198
|
+
const flag = modifierFlagFor(key);
|
|
199
|
+
if (flag !== undefined)
|
|
200
|
+
pressFlag(flag);
|
|
201
|
+
const ok = dispatch('keydown', key);
|
|
110
202
|
if (!ok)
|
|
111
203
|
prevented = true;
|
|
204
|
+
if (ok)
|
|
205
|
+
dispatchKeypress(el, key, pressCode({}, key), held);
|
|
206
|
+
closeModalOnEscape(el, key, ok);
|
|
112
207
|
}
|
|
113
208
|
if (holdMs > 0)
|
|
114
209
|
await sleep(holdMs);
|
|
115
210
|
for (const key of [...keys].reverse()) {
|
|
116
|
-
const
|
|
117
|
-
|
|
211
|
+
const flag = modifierFlagFor(key);
|
|
212
|
+
if (flag !== undefined)
|
|
213
|
+
releaseFlag(flag);
|
|
214
|
+
dispatch('keyup', key);
|
|
118
215
|
}
|
|
119
216
|
return prevented;
|
|
120
217
|
}
|
|
218
|
+
export function closeModalOnEscape(el, key, keydownProceeded) {
|
|
219
|
+
if ('Escape' !== key || !keydownProceeded)
|
|
220
|
+
return;
|
|
221
|
+
for (let dialog = el.closest('dialog'); null !== dialog; dialog = dialog.parentElement?.closest('dialog') ?? null) {
|
|
222
|
+
try {
|
|
223
|
+
if (!dialog.open || !dialog.matches(':modal'))
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if ('function' === typeof dialog.requestClose)
|
|
230
|
+
dialog.requestClose();
|
|
231
|
+
else if (dialog.dispatchEvent(new Event('cancel', { cancelable: true })))
|
|
232
|
+
dialog.close();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -1,39 +1,5 @@
|
|
|
1
1
|
import { SettleReason } from '@reticlehq/core';
|
|
2
|
-
|
|
3
|
-
dispatched: true;
|
|
4
|
-
targetMatched: boolean;
|
|
5
|
-
visible: boolean;
|
|
6
|
-
enabled: boolean;
|
|
7
|
-
defaultPrevented: boolean;
|
|
8
|
-
focusMoved: string | null;
|
|
9
|
-
valueChanged: boolean;
|
|
10
|
-
domMutatedWithin: number;
|
|
11
|
-
appeared?: string;
|
|
12
|
-
occluded: boolean;
|
|
13
|
-
occludedBy: string | null;
|
|
14
|
-
scrolledIntoView: boolean;
|
|
15
|
-
heldMs?: number;
|
|
16
|
-
alreadyAtValue?: true;
|
|
17
|
-
}
|
|
18
|
-
interface ActionResult {
|
|
19
|
-
ok: true;
|
|
20
|
-
ref: string;
|
|
21
|
-
action: string;
|
|
22
|
-
dispatched: boolean;
|
|
23
|
-
settled: boolean;
|
|
24
|
-
settleReason: SettleReason | null;
|
|
25
|
-
effect: ActionEffect;
|
|
26
|
-
testid?: string;
|
|
27
|
-
component?: string;
|
|
28
|
-
role?: string;
|
|
29
|
-
name?: string;
|
|
30
|
-
source?: {
|
|
31
|
-
file: string;
|
|
32
|
-
line: number;
|
|
33
|
-
column?: number;
|
|
34
|
-
};
|
|
35
|
-
warning?: string;
|
|
36
|
-
}
|
|
2
|
+
import { type ActionEffect, type ActionResult } from './action-report.js';
|
|
37
3
|
export declare const ZOOM_NEEDS_REAL_BROWSER_MSG: string;
|
|
38
4
|
export declare function executeAction(ref: string, action: string, args?: Record<string, unknown>): Promise<ActionResult>;
|
|
39
5
|
export interface ActionStep {
|