@reticlehq/browser 3.6.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 +12 -0
- package/dist/actions/actions-press.d.ts +1 -0
- package/dist/actions/actions-press.js +25 -1
- package/dist/actions/actions.d.ts +1 -37
- package/dist/actions/actions.js +24 -66
- package/dist/actions/appeared-text.js +3 -0
- package/dist/actions/danger-context.d.ts +2 -0
- package/dist/actions/danger-context.js +28 -2
- 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/dom/a11y.js +251 -10
- package/dist/dom/addressing/coverage-markers.d.ts +6 -0
- package/dist/dom/addressing/coverage-markers.js +13 -0
- package/dist/dom/dom-ignore.d.ts +2 -0
- package/dist/dom/dom-ignore.js +15 -2
- package/dist/dom/query.d.ts +1 -1
- package/dist/dom/query.js +78 -16
- package/dist/dom/render-page.d.ts +12 -0
- package/dist/dom/render-page.js +135 -0
- package/dist/dom/snapshot.js +55 -7
- package/dist/observers/dom.js +32 -16
- package/dist/observers/field.js +3 -21
- package/dist/observers/network.js +56 -7
- package/dist/observers/perf.js +53 -1
- package/dist/observers/route.js +6 -5
- package/dist/presenter/carousel/carousel.js +2 -7
- package/dist/presenter/carousel/panel-slides.d.ts +2 -4
- package/dist/presenter/carousel/panel-slides.js +17 -15
- package/dist/presenter/chrome/log-memory.d.ts +1 -0
- package/dist/presenter/chrome/presenter-log.d.ts +7 -0
- package/dist/presenter/chrome/presenter-log.js +48 -8
- 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 +17 -4
- package/dist/presenter/chrome/presenter-report-copy.js +26 -4
- package/dist/presenter/hud-telemetry.js +55 -4
- package/dist/presenter/presenter-account-styles.js +3 -3
- package/dist/presenter/presenter-account.d.ts +27 -5
- package/dist/presenter/presenter-account.js +83 -6
- 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 +27 -5
- package/dist/presenter/presenter-chat-views.js +312 -72
- package/dist/presenter/presenter-config.d.ts +2 -0
- package/dist/presenter/presenter-config.js +1 -0
- package/dist/presenter/presenter-controls.d.ts +0 -1
- package/dist/presenter/presenter-controls.js +13 -91
- package/dist/presenter/presenter-design-tokens.js +1 -1
- package/dist/presenter/presenter-frame.d.ts +1 -1
- package/dist/presenter/presenter-frame.js +8 -10
- 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.js +25 -24
- package/dist/presenter/presenter-report-styles.js +40 -22
- package/dist/presenter/presenter-report.d.ts +2 -1
- package/dist/presenter/presenter-report.js +83 -17
- package/dist/presenter/presenter-settings-styles.js +8 -4
- package/dist/presenter/presenter-settings.d.ts +7 -6
- package/dist/presenter/presenter-settings.js +75 -34
- package/dist/presenter/presenter-shell-styles.js +12 -3
- package/dist/presenter/presenter-shell.d.ts +10 -4
- package/dist/presenter/presenter-shell.js +31 -23
- package/dist/presenter/presenter-styles.js +0 -2
- package/dist/presenter/presenter-workspace.js +10 -3
- package/dist/presenter/presenter.d.ts +3 -2
- package/dist/presenter/presenter.js +81 -20
- package/dist/registry/declared-platform.js +2 -0
- package/dist/reticle-inject.js +777 -700
- package/dist/reticle.js +36 -2
- package/dist/storage-keys.d.ts +3 -2
- package/dist/storage-keys.js +7 -3
- 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 +2 -2
- package/dist/presenter/carousel/offer-card.d.ts +0 -21
- package/dist/presenter/carousel/offer-card.js +0 -56
|
@@ -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
|
+
};
|
|
@@ -231,3 +231,15 @@ function makeTouchEvent(type, touches) {
|
|
|
231
231
|
}
|
|
232
232
|
return new Event(type, { bubbles: true, cancelable: true });
|
|
233
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
|
+
}
|
|
@@ -8,6 +8,7 @@ export interface ModifierFlags {
|
|
|
8
8
|
}
|
|
9
9
|
export declare function pressModifiers(args: Record<string, unknown>): ModifierFlags;
|
|
10
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;
|
|
11
12
|
export declare function focusedOrDocument(): HTMLElement;
|
|
12
13
|
export declare function isReflessDocumentPress(ref: string, action: string, args: Record<string, unknown>): boolean;
|
|
13
14
|
export declare function pressKeys(args: Record<string, unknown>): string[];
|
|
@@ -57,6 +57,26 @@ export function pressCode(args, key) {
|
|
|
57
57
|
}
|
|
58
58
|
return /^[A-Z][A-Za-z0-9]*$/.test(key) && KNOWN_NAMED_KEYS.has(key) ? key : '';
|
|
59
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'];
|
|
60
80
|
const KNOWN_NAMED_KEYS = new Set([
|
|
61
81
|
'Enter',
|
|
62
82
|
'Escape',
|
|
@@ -139,7 +159,7 @@ export async function holdKey(el, key, code, mods, ms) {
|
|
|
139
159
|
if (ms > KEY_REPEAT_DELAY_MS)
|
|
140
160
|
await sleep(KEY_REPEAT_DELAY_MS);
|
|
141
161
|
while (Date.now() - started < ms) {
|
|
142
|
-
asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', {
|
|
162
|
+
const down = asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', {
|
|
143
163
|
key,
|
|
144
164
|
code,
|
|
145
165
|
bubbles: true,
|
|
@@ -147,6 +167,8 @@ export async function holdKey(el, key, code, mods, ms) {
|
|
|
147
167
|
repeat: true,
|
|
148
168
|
...mods,
|
|
149
169
|
})));
|
|
170
|
+
if (down)
|
|
171
|
+
dispatchKeypress(el, key, code, mods, true);
|
|
150
172
|
await sleep(Math.min(KEY_REPEAT_MS, Math.max(0, ms - (Date.now() - started))));
|
|
151
173
|
}
|
|
152
174
|
}
|
|
@@ -179,6 +201,8 @@ export async function pressCombo(el, keys, mods, holdMs) {
|
|
|
179
201
|
const ok = dispatch('keydown', key);
|
|
180
202
|
if (!ok)
|
|
181
203
|
prevented = true;
|
|
204
|
+
if (ok)
|
|
205
|
+
dispatchKeypress(el, key, pressCode({}, key), held);
|
|
182
206
|
closeModalOnEscape(el, key, ok);
|
|
183
207
|
}
|
|
184
208
|
if (holdMs > 0)
|
|
@@ -1,41 +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
|
-
nth?: number;
|
|
31
|
-
of?: number;
|
|
32
|
-
source?: {
|
|
33
|
-
file: string;
|
|
34
|
-
line: number;
|
|
35
|
-
column?: number;
|
|
36
|
-
};
|
|
37
|
-
warning?: string;
|
|
38
|
-
}
|
|
2
|
+
import { type ActionEffect, type ActionResult } from './action-report.js';
|
|
39
3
|
export declare const ZOOM_NEEDS_REAL_BROWSER_MSG: string;
|
|
40
4
|
export declare function executeAction(ref: string, action: string, args?: Record<string, unknown>): Promise<ActionResult>;
|
|
41
5
|
export interface ActionStep {
|
package/dist/actions/actions.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
import {
|
|
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';
|
|
1
|
+
import { ActionType, ActionWarning, clampHoldMs, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, SettleReason, } from '@reticlehq/core';
|
|
4
2
|
import { asSyntheticInput } from './synthetic/synthetic-input.js';
|
|
5
3
|
import { echoRef, refs } from '../dom/addressing/refs.js';
|
|
6
|
-
import { dangerousActionContext, requiresDangerousConfirmation, submitControlFor, } from './danger-context.js';
|
|
7
|
-
import { assertEditable, assertNotRichText, setNativeValue } from './value-input.js';
|
|
8
|
-
import {
|
|
9
|
-
import { elementHasHoverHandlers
|
|
4
|
+
import { dangerousActionContext, requiresDangerousConfirmation, assertOutwardAllowed, submitControlFor, } from './danger-context.js';
|
|
5
|
+
import { assertEditable, assertNotRichText, isAutofillSafeTarget, setNativeValue, } from './value-input.js';
|
|
6
|
+
import { getRole, isVisible, getStates } from '../dom/a11y.js';
|
|
7
|
+
import { elementHasHoverHandlers } from '../registry/stores/adapters.js';
|
|
10
8
|
import { isActionTarget, isForm, isInput, isSelect, isTextArea, } from '../dom/realm.js';
|
|
11
9
|
import { nativeSetTimeout, settle } from '../timers/native/native-timers.js';
|
|
12
10
|
import { AppearedText } from './appeared-text.js';
|
|
13
11
|
import { alreadyAtCheckedState, assertAriaToggleLanded, isAriaToggle } from './check-state.js';
|
|
14
|
-
import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, closeModalOnEscape, keyboardEvent, } from './actions-press.js';
|
|
12
|
+
import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, dispatchKeypress, closeModalOnEscape, keyboardEvent, } from './actions-press.js';
|
|
13
|
+
import { anchorOf, result } from './action-report.js';
|
|
15
14
|
function asString(value, fallback = '') {
|
|
16
15
|
return 'string' === typeof value ? value : fallback;
|
|
17
16
|
}
|
|
@@ -23,61 +22,6 @@ function requireElement(ref) {
|
|
|
23
22
|
throw new Error(`ref '${echoRef(ref)}' is not an HTML or SVG element`);
|
|
24
23
|
return el;
|
|
25
24
|
}
|
|
26
|
-
function anchorOf(el) {
|
|
27
|
-
const testid = readTestId(el) ?? undefined;
|
|
28
|
-
const info = identifyComponent(el);
|
|
29
|
-
const out = {};
|
|
30
|
-
if (testid !== undefined)
|
|
31
|
-
out.testid = testid;
|
|
32
|
-
const component = info?.componentStack[0];
|
|
33
|
-
if (component !== undefined)
|
|
34
|
-
out.component = component;
|
|
35
|
-
if (info?.source !== undefined)
|
|
36
|
-
out.source = info.source;
|
|
37
|
-
const role = getRole(el);
|
|
38
|
-
const name = getAccessibleName(el);
|
|
39
|
-
if (role.length > 0)
|
|
40
|
-
out.role = role;
|
|
41
|
-
if (name.length > 0)
|
|
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
|
-
}
|
|
51
|
-
return out;
|
|
52
|
-
}
|
|
53
|
-
const result = (ref, action, effect, settled, settleReason, anchor, warning) => {
|
|
54
|
-
const base = {
|
|
55
|
-
ok: true,
|
|
56
|
-
ref,
|
|
57
|
-
action,
|
|
58
|
-
dispatched: true,
|
|
59
|
-
settled,
|
|
60
|
-
settleReason,
|
|
61
|
-
effect,
|
|
62
|
-
};
|
|
63
|
-
if (anchor.testid !== undefined)
|
|
64
|
-
base.testid = anchor.testid;
|
|
65
|
-
if (anchor.component !== undefined)
|
|
66
|
-
base.component = anchor.component;
|
|
67
|
-
if (anchor.source !== undefined)
|
|
68
|
-
base.source = anchor.source;
|
|
69
|
-
if (anchor.role !== undefined)
|
|
70
|
-
base.role = anchor.role;
|
|
71
|
-
if (anchor.name !== undefined)
|
|
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;
|
|
77
|
-
if (warning !== undefined)
|
|
78
|
-
base.warning = warning;
|
|
79
|
-
return base;
|
|
80
|
-
};
|
|
81
25
|
const FILL_LIKE = new Set([
|
|
82
26
|
ActionType.FILL,
|
|
83
27
|
ActionType.TYPE,
|
|
@@ -123,6 +67,8 @@ function assertActionAllowed(el, action, args) {
|
|
|
123
67
|
(action === ActionType.PRESS && 'Enter' === pressKey(args));
|
|
124
68
|
const dragTarget = action === ActionType.DRAG ? refs.resolve(dragTargetRef(args)) : null;
|
|
125
69
|
const submitter = action === ActionType.PRESS && 'Enter' === pressKey(args) ? submitControlFor(el) : null;
|
|
70
|
+
if (canTrigger)
|
|
71
|
+
assertOutwardAllowed([el, submitter, isActionTarget(dragTarget) ? dragTarget : null]);
|
|
126
72
|
const sourceDangerous = requiresDangerousConfirmation(dangerousActionContext(el), getRole(el)) ||
|
|
127
73
|
(submitter !== null &&
|
|
128
74
|
requiresDangerousConfirmation(dangerousActionContext(submitter), getRole(submitter)));
|
|
@@ -134,7 +80,13 @@ function assertActionAllowed(el, action, args) {
|
|
|
134
80
|
throw new Error(`potentially destructive action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
|
|
135
81
|
}
|
|
136
82
|
}
|
|
137
|
-
|
|
83
|
+
function optionValueFor(options, named) {
|
|
84
|
+
if (options.some((option) => option.value === named))
|
|
85
|
+
return named;
|
|
86
|
+
const byLabel = options.filter((option) => option.label.trim() === named.trim());
|
|
87
|
+
return 1 === byLabel.length ? (byLabel[0]?.value ?? named) : named;
|
|
88
|
+
}
|
|
89
|
+
import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, mouseEventFor, assertNotLeaving, } from './actions-dom.js';
|
|
138
90
|
import { scrollFrom } from './scroll.js';
|
|
139
91
|
function enabledOf(el) {
|
|
140
92
|
return !getStates(el).includes(ElementState.DISABLED);
|
|
@@ -209,7 +161,8 @@ async function dispatchOther(el, action, args) {
|
|
|
209
161
|
'To empty a field on purpose use the `clear` action, or fill with an explicit "".');
|
|
210
162
|
}
|
|
211
163
|
assertEditable(el, 'fill');
|
|
212
|
-
el
|
|
164
|
+
if (!isAutofillSafeTarget(el))
|
|
165
|
+
el.focus();
|
|
213
166
|
return setNativeValue(el, args['value']);
|
|
214
167
|
}
|
|
215
168
|
assertNotRichText(el, 'fill');
|
|
@@ -235,8 +188,8 @@ async function dispatchOther(el, action, args) {
|
|
|
235
188
|
if (typeof args['value'] !== 'string') {
|
|
236
189
|
throw new Error("select requires a string `value` — pass it nested, as args: { value: '…' }.");
|
|
237
190
|
}
|
|
238
|
-
const wanted = args['value'];
|
|
239
191
|
const options = Array.from(el.options);
|
|
192
|
+
const wanted = optionValueFor(options, args['value']);
|
|
240
193
|
if (!options.some((option) => option.value === wanted)) {
|
|
241
194
|
const available = options.map((option) => `${option.value} (${option.label})`).join(', ');
|
|
242
195
|
throw new Error(`no <option> with value '${echoRef(wanted)}' — available: ${0 === options.length ? '(none)' : available}`);
|
|
@@ -287,6 +240,8 @@ async function dispatchOther(el, action, args) {
|
|
|
287
240
|
const code = pressCode(args, key);
|
|
288
241
|
const down = asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', { key, code, bubbles: true, cancelable: true, ...mods })));
|
|
289
242
|
closeModalOnEscape(el, key, down);
|
|
243
|
+
if (down)
|
|
244
|
+
dispatchKeypress(el, key, code, mods);
|
|
290
245
|
const hold = clampHoldMs(args['holdMs']);
|
|
291
246
|
if (hold > 0)
|
|
292
247
|
await holdKey(el, key, code, mods, hold);
|
|
@@ -355,6 +310,8 @@ export async function executeAction(ref, action, args = {}) {
|
|
|
355
310
|
const el = isReflessDocumentPress(ref, action, args) ? focusedOrDocument() : requireElement(ref);
|
|
356
311
|
if (named)
|
|
357
312
|
assertActionAllowed(el, action, args);
|
|
313
|
+
if (named)
|
|
314
|
+
assertNotLeaving(el);
|
|
358
315
|
const anchor = named ? anchorOf(el) : {};
|
|
359
316
|
const visible = isVisible(el);
|
|
360
317
|
const enabled = enabledOf(el);
|
|
@@ -401,6 +358,7 @@ export async function executeAction(ref, action, args = {}) {
|
|
|
401
358
|
defaultPrevented,
|
|
402
359
|
focusMoved: prevFocus !== nextFocus ? `${prevFocus ?? 'null'}->${nextFocus ?? 'null'}` : null,
|
|
403
360
|
valueChanged: isFillLike(action) ? valueBefore !== valueAfter : false,
|
|
361
|
+
...(ActionType.FILL === action && isAutofillSafeTarget(el) ? { autofillSafe: true } : {}),
|
|
404
362
|
domMutatedWithin: mutated,
|
|
405
363
|
...said.effect(valueAfter),
|
|
406
364
|
occluded: geometry.occluded,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isIgnored } from '../dom/dom-ignore.js';
|
|
2
|
+
import { isSensitiveField } from '../dom/a11y.js';
|
|
2
3
|
const APPEARED_MAX = 200;
|
|
3
4
|
const JOIN = ' | ';
|
|
4
5
|
const TEXT_NODE = 3;
|
|
@@ -46,6 +47,8 @@ export class AppearedText {
|
|
|
46
47
|
#add(raw, owner) {
|
|
47
48
|
if (null !== owner && isIgnored(owner))
|
|
48
49
|
return;
|
|
50
|
+
if (null !== owner && isSensitiveField(owner))
|
|
51
|
+
return;
|
|
49
52
|
const text = (raw ?? '').replace(/\s+/g, ' ').trim();
|
|
50
53
|
if (0 === text.length)
|
|
51
54
|
return;
|
|
@@ -2,3 +2,5 @@ import { type ActionTarget } from '../dom/realm.js';
|
|
|
2
2
|
export declare function dangerousActionContext(el: ActionTarget): string;
|
|
3
3
|
export declare function submitControlFor(el: ActionTarget): HTMLElement | null;
|
|
4
4
|
export declare function requiresDangerousConfirmation(text: string, role?: string): boolean;
|
|
5
|
+
export declare function setOutwardPolicy(allowed: readonly string[] | undefined): void;
|
|
6
|
+
export declare function assertOutwardAllowed(targets: readonly (ActionTarget | null)[]): void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { isDangerousActionText } from '@reticlehq/core';
|
|
2
|
-
import { getAccessibleName } from '../dom/a11y.js';
|
|
1
|
+
import { isDangerousActionText, outwardActionOf, outwardRefusal } from '@reticlehq/core';
|
|
2
|
+
import { getAccessibleName, getRole } from '../dom/a11y.js';
|
|
3
3
|
import { isHtmlElement } from '../dom/realm.js';
|
|
4
4
|
const LABEL_VALUED_INPUT_TYPES = new Set([
|
|
5
5
|
'submit',
|
|
@@ -16,7 +16,18 @@ function holdsUserText(el) {
|
|
|
16
16
|
return !LABEL_VALUED_INPUT_TYPES.has(el.type.toLowerCase());
|
|
17
17
|
return false;
|
|
18
18
|
}
|
|
19
|
+
const ANCHOR_TAG = 'A';
|
|
20
|
+
const INERT_HREF = /^\s*(#.*|javascript:.*)?$/i;
|
|
21
|
+
function navigatingHref(el) {
|
|
22
|
+
if (ANCHOR_TAG !== el.tagName)
|
|
23
|
+
return undefined;
|
|
24
|
+
const href = el.getAttribute('href');
|
|
25
|
+
return null === href || INERT_HREF.test(href) ? undefined : href;
|
|
26
|
+
}
|
|
19
27
|
export function dangerousActionContext(el) {
|
|
28
|
+
const href = navigatingHref(el);
|
|
29
|
+
if (href !== undefined)
|
|
30
|
+
return href;
|
|
20
31
|
const form = el.closest('form');
|
|
21
32
|
const ownText = holdsUserText(el)
|
|
22
33
|
? ['', '']
|
|
@@ -38,3 +49,18 @@ export function submitControlFor(el) {
|
|
|
38
49
|
export function requiresDangerousConfirmation(text, role) {
|
|
39
50
|
return isDangerousActionText(text, role);
|
|
40
51
|
}
|
|
52
|
+
let outwardAllowed;
|
|
53
|
+
export function setOutwardPolicy(allowed) {
|
|
54
|
+
outwardAllowed = allowed;
|
|
55
|
+
}
|
|
56
|
+
export function assertOutwardAllowed(targets) {
|
|
57
|
+
if (outwardAllowed === undefined)
|
|
58
|
+
return;
|
|
59
|
+
for (const el of targets) {
|
|
60
|
+
if (null === el)
|
|
61
|
+
continue;
|
|
62
|
+
const kind = outwardActionOf(dangerousActionContext(el), getRole(el));
|
|
63
|
+
if (kind !== undefined && !outwardAllowed.includes(kind))
|
|
64
|
+
throw new Error(outwardRefusal(kind));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ActionTarget } from '../dom/realm.js';
|
|
2
|
+
export declare function isAutofillSafeTarget(el: Element, ua?: string): boolean;
|
|
2
3
|
export declare function setNativeValue(el: HTMLInputElement | HTMLTextAreaElement, value: string): boolean;
|
|
3
4
|
export declare function assertNotRichText(el: ActionTarget, action: string): void;
|
|
4
5
|
export declare function assertEditable(el: HTMLInputElement | HTMLTextAreaElement, action: string): void;
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { captureValueSetter } from '../patching/capture-method.js';
|
|
2
2
|
import { isHtmlElement, valuePrototypeOf } from '../dom/realm.js';
|
|
3
|
+
const PASSWORD_AUTOCOMPLETE = /\b(current|new)-password\b/;
|
|
4
|
+
const NOT_WEBKIT = /Chrome|Chromium|Edg\/|Firefox|Gecko\//;
|
|
5
|
+
export function isAutofillSafeTarget(el, ua = navigator.userAgent) {
|
|
6
|
+
if (!(el instanceof HTMLInputElement))
|
|
7
|
+
return false;
|
|
8
|
+
const password = 'password' === el.type.toLowerCase() ||
|
|
9
|
+
PASSWORD_AUTOCOMPLETE.test(el.getAttribute('autocomplete') ?? '');
|
|
10
|
+
if (!password || NOT_WEBKIT.test(ua))
|
|
11
|
+
return false;
|
|
12
|
+
return 600 <= Number(/AppleWebKit\/(\d+)/.exec(ua)?.[1] ?? 0);
|
|
13
|
+
}
|
|
3
14
|
export function setNativeValue(el, value) {
|
|
4
15
|
const proto = valuePrototypeOf(el);
|
|
5
16
|
const setter = proto === undefined ? undefined : captureValueSetter(proto);
|
|
@@ -122,6 +122,9 @@ function readState(ref, store, path, depth) {
|
|
|
122
122
|
const names = storeNames();
|
|
123
123
|
if (path !== undefined || depth !== undefined) {
|
|
124
124
|
const rawStores = readStoresRaw(store);
|
|
125
|
+
if (store !== undefined && !Object.hasOwn(rawStores, store)) {
|
|
126
|
+
return { store, path, found: false, value: null, storeNames: names };
|
|
127
|
+
}
|
|
125
128
|
const base = store !== undefined ? rawStores[store] : { stores: rawStores, storeNames: names };
|
|
126
129
|
const selection = path !== undefined ? selectPath(base, path) : { found: true, value: base };
|
|
127
130
|
const selected = selection.found && depth !== undefined ? capDepth(selection.value, depth) : selection.value;
|
|
@@ -220,6 +223,7 @@ export function createCommandRegistry() {
|
|
|
220
223
|
reg.set(ReticleCommand.STORAGE_READ, (args) => readStorage(str(args['area'])));
|
|
221
224
|
reg.set(ReticleCommand.CAPABILITIES, () => getCapabilities());
|
|
222
225
|
reg.set(ReticleCommand.CAPTURE, (args) => captureDesktopWindow(true === args['fullPage']));
|
|
226
|
+
reg.set(ReticleCommand.RENDER, () => import('../dom/render-page.js').then((m) => m.renderPage()));
|
|
223
227
|
reg.set(ReticleCommand.SCROLL, (args) => {
|
|
224
228
|
const dy = args['dy'];
|
|
225
229
|
const fraction = args['fraction'];
|