@reticlehq/browser 3.3.0 → 3.5.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.d.ts +5 -4
- package/dist/actions/actions-press.d.ts +3 -3
- package/dist/actions/actions-press.js +0 -3
- package/dist/actions/actions.d.ts +0 -1
- package/dist/actions/actions.js +13 -21
- package/dist/actions/check-state.d.ts +5 -0
- package/dist/actions/check-state.js +24 -0
- package/dist/actions/danger-context.d.ts +3 -2
- package/dist/actions/danger-context.js +2 -1
- package/dist/actions/value-input.d.ts +2 -1
- package/dist/actions/value-input.js +2 -2
- package/dist/commands/commands.js +2 -6
- package/dist/dom/a11y.js +70 -7
- package/dist/dom/occlusion.js +5 -1
- package/dist/dom/query.js +4 -4
- package/dist/dom/realm.d.ts +6 -0
- package/dist/dom/realm.js +5 -0
- package/dist/dom/snapshot.js +8 -2
- package/dist/observers/network.js +14 -1
- package/dist/observers/state.js +4 -1
- package/dist/registry/store-adapters.js +32 -2
- package/dist/registry/stores/adapters.js +8 -2
- package/dist/reticle-inject.js +332 -332
- package/dist/review/annotator.d.ts +0 -1
- package/dist/review/annotator.js +0 -3
- package/package.json +9 -9
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ActionTarget } from '../dom/realm.js';
|
|
1
2
|
interface ClickGeometry {
|
|
2
3
|
occluded: boolean;
|
|
3
4
|
occludedBy: string | null;
|
|
@@ -5,7 +6,7 @@ interface ClickGeometry {
|
|
|
5
6
|
}
|
|
6
7
|
export declare const NO_GEOMETRY: ClickGeometry;
|
|
7
8
|
export declare function mouseEventFor(el: Element, type: string, init?: MouseEventInit): MouseEvent;
|
|
8
|
-
export declare function fireClickSequence(el:
|
|
9
|
+
export declare function fireClickSequence(el: ActionTarget, hold?: {
|
|
9
10
|
ms: number;
|
|
10
11
|
sleep: (ms: number) => Promise<void>;
|
|
11
12
|
now: () => number;
|
|
@@ -13,11 +14,11 @@ export declare function fireClickSequence(el: HTMLElement, hold?: {
|
|
|
13
14
|
prevented: boolean;
|
|
14
15
|
heldMs: number;
|
|
15
16
|
}>;
|
|
16
|
-
export declare function clickGeometry(el:
|
|
17
|
+
export declare function clickGeometry(el: ActionTarget): ClickGeometry;
|
|
17
18
|
export declare function firePointer(el: Element, type: string, relatedTarget?: EventTarget | null): void;
|
|
18
19
|
export declare function firePointerNonBubbling(el: Element, type: string, relatedTarget?: EventTarget | null): void;
|
|
19
|
-
export declare function dragElement(source:
|
|
20
|
-
export declare function fireTapSequence(el:
|
|
20
|
+
export declare function dragElement(source: ActionTarget, target: ActionTarget | null, data: unknown): Promise<boolean>;
|
|
21
|
+
export declare function fireTapSequence(el: ActionTarget, hold: {
|
|
21
22
|
ms: number;
|
|
22
23
|
sleep: (ms: number) => Promise<void>;
|
|
23
24
|
now: () => number;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ActionTarget } from '../dom/realm.js';
|
|
1
2
|
export declare function pressKey(args: Record<string, unknown>): string;
|
|
2
3
|
export declare function pressModifiers(args: Record<string, unknown>): {
|
|
3
4
|
metaKey: boolean;
|
|
@@ -9,6 +10,5 @@ export declare function pressCode(args: Record<string, unknown>, key: string): s
|
|
|
9
10
|
export declare function focusedOrDocument(): HTMLElement;
|
|
10
11
|
export declare function isReflessDocumentPress(ref: string, action: string, args: Record<string, unknown>): boolean;
|
|
11
12
|
export declare function pressKeys(args: Record<string, unknown>): string[];
|
|
12
|
-
export declare function holdKey(el:
|
|
13
|
-
export declare function pressCombo(el:
|
|
14
|
-
export declare const ZOOM_NEEDS_REAL_BROWSER_MSG: string;
|
|
13
|
+
export declare function holdKey(el: ActionTarget, key: string, code: string, mods: Record<string, boolean>, ms: number): Promise<void>;
|
|
14
|
+
export declare function pressCombo(el: ActionTarget, keys: readonly string[], mods: Record<string, boolean>, holdMs: number): Promise<boolean>;
|
|
@@ -118,6 +118,3 @@ export async function pressCombo(el, keys, mods, holdMs) {
|
|
|
118
118
|
}
|
|
119
119
|
return prevented;
|
|
120
120
|
}
|
|
121
|
-
export const ZOOM_NEEDS_REAL_BROWSER_MSG = 'cannot zoom from inside the page — CSS zoom changes how it LOOKS without changing the layout ' +
|
|
122
|
-
'viewport, visualViewport or media queries, so a layout that breaks at 200% would be reported as ' +
|
|
123
|
-
'checked and passing. Drive a real browser (reticle drive <url>, or reticle_lease) and zoom there.';
|
|
@@ -36,7 +36,6 @@ interface ActionResult {
|
|
|
36
36
|
}
|
|
37
37
|
export declare const ZOOM_NEEDS_REAL_BROWSER_MSG: string;
|
|
38
38
|
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
39
|
export interface ActionStep {
|
|
41
40
|
ref: string;
|
|
42
41
|
action: string;
|
package/dist/actions/actions.js
CHANGED
|
@@ -5,9 +5,10 @@ import { dangerousActionContext, requiresDangerousConfirmation, submitControlFor
|
|
|
5
5
|
import { assertEditable, assertNotRichText, setNativeValue } from './value-input.js';
|
|
6
6
|
import { getAccessibleName, getRole, isVisible, getStates } from '../dom/a11y.js';
|
|
7
7
|
import { elementHasHoverHandlers, identifyComponent } from '../registry/stores/adapters.js';
|
|
8
|
-
import {
|
|
8
|
+
import { isActionTarget, isForm, isInput, isSelect, isTextArea, } from '../dom/realm.js';
|
|
9
9
|
import { nativeSetTimeout, settle } from '../timers/native/native-timers.js';
|
|
10
10
|
import { AppearedText } from './appeared-text.js';
|
|
11
|
+
import { alreadyAtCheckedState, assertAriaToggleLanded, isAriaToggle } from './check-state.js';
|
|
11
12
|
import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, } from './actions-press.js';
|
|
12
13
|
function asString(value, fallback = '') {
|
|
13
14
|
return 'string' === typeof value ? value : fallback;
|
|
@@ -16,8 +17,8 @@ function requireElement(ref) {
|
|
|
16
17
|
const el = refs.resolve(ref);
|
|
17
18
|
if (null === el)
|
|
18
19
|
throw new Error(`ref '${echoRef(ref)}' no longer resolves to an element`);
|
|
19
|
-
if (!
|
|
20
|
-
throw new Error(`ref '${echoRef(ref)}' is not an
|
|
20
|
+
if (!isActionTarget(el))
|
|
21
|
+
throw new Error(`ref '${echoRef(ref)}' is not an HTML or SVG element`);
|
|
21
22
|
return el;
|
|
22
23
|
}
|
|
23
24
|
function anchorOf(el) {
|
|
@@ -76,11 +77,6 @@ const CLICK_LIKE = new Set([
|
|
|
76
77
|
ActionType.CHECK,
|
|
77
78
|
ActionType.UNCHECK,
|
|
78
79
|
]);
|
|
79
|
-
function alreadyAtCheckedState(el, action) {
|
|
80
|
-
if (action !== ActionType.CHECK && action !== ActionType.UNCHECK)
|
|
81
|
-
return false;
|
|
82
|
-
return isInput(el) && el.checked === (action === ActionType.CHECK);
|
|
83
|
-
}
|
|
84
80
|
function dragTargetRef(args) {
|
|
85
81
|
const named = asString(args['toRef']);
|
|
86
82
|
return '' !== named ? named : asString(args['target']);
|
|
@@ -116,7 +112,7 @@ function assertActionAllowed(el, action, args) {
|
|
|
116
112
|
const sourceDangerous = requiresDangerousConfirmation(dangerousActionContext(el), getRole(el)) ||
|
|
117
113
|
(submitter !== null &&
|
|
118
114
|
requiresDangerousConfirmation(dangerousActionContext(submitter), getRole(submitter)));
|
|
119
|
-
const targetDangerous =
|
|
115
|
+
const targetDangerous = isActionTarget(dragTarget) &&
|
|
120
116
|
requiresDangerousConfirmation(dangerousActionContext(dragTarget), getRole(dragTarget));
|
|
121
117
|
if (canTrigger &&
|
|
122
118
|
(sourceDangerous || targetDangerous) &&
|
|
@@ -244,11 +240,16 @@ async function dispatchOther(el, action, args) {
|
|
|
244
240
|
throw new Error(`cannot select on a <${el.tagName.toLowerCase()}>`);
|
|
245
241
|
case ActionType.CHECK:
|
|
246
242
|
case ActionType.UNCHECK: {
|
|
247
|
-
|
|
243
|
+
const aria = isAriaToggle(el);
|
|
244
|
+
if (!isInput(el) && !aria) {
|
|
248
245
|
throw new Error(`cannot (un)check a <${el.tagName.toLowerCase()}>`);
|
|
246
|
+
}
|
|
249
247
|
if (!enabledOf(el)) {
|
|
250
248
|
throw new Error(`cannot ${action} a disabled control — a real user could not, so neither will Reticle`);
|
|
251
249
|
}
|
|
250
|
+
if (!isInput(el)) {
|
|
251
|
+
return alreadyAtCheckedState(el, action) ? false : (await fireClickSequence(el)).prevented;
|
|
252
|
+
}
|
|
252
253
|
if ('radio' === el.type && action === ActionType.UNCHECK) {
|
|
253
254
|
throw new Error('cannot uncheck a radio button — a real user could not; select another radio in the group');
|
|
254
255
|
}
|
|
@@ -324,7 +325,7 @@ async function dispatchOther(el, action, args) {
|
|
|
324
325
|
if ('' === toRef)
|
|
325
326
|
return await dragElement(el, null, args['data']);
|
|
326
327
|
const resolved = refs.resolve(toRef);
|
|
327
|
-
if (!
|
|
328
|
+
if (!isActionTarget(resolved)) {
|
|
328
329
|
throw new Error(`drag target '${echoRef(toRef)}' did not resolve to an element — pass a ref from ` +
|
|
329
330
|
'reticle_snapshot or reticle_query as args.toRef (alias: args.target)');
|
|
330
331
|
}
|
|
@@ -380,6 +381,7 @@ export async function executeAction(ref, action, args = {}) {
|
|
|
380
381
|
settleReason = outcome.settled ? null : SettleReason.TIMEOUT;
|
|
381
382
|
obs.disconnect();
|
|
382
383
|
}
|
|
384
|
+
assertAriaToggleLanded(el, action, alreadyAtValue);
|
|
383
385
|
const valueAfter = valueOf(el);
|
|
384
386
|
const nextFocus = activeRef(el);
|
|
385
387
|
const effect = {
|
|
@@ -408,16 +410,6 @@ export async function executeAction(ref, action, args = {}) {
|
|
|
408
410
|
return result(ref, action, effect, settled, settleReason, anchor, warning);
|
|
409
411
|
}
|
|
410
412
|
const sleep = (ms) => new Promise((r) => nativeSetTimeout(r, ms));
|
|
411
|
-
export async function dispatchWebMcp(tool, params, confirmDangerous = false) {
|
|
412
|
-
if (requiresDangerousConfirmation(tool) && !confirmDangerous) {
|
|
413
|
-
throw new Error(`potentially destructive WebMCP tool blocked; retry with ${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
|
|
414
|
-
}
|
|
415
|
-
const mc = navigator.modelContext;
|
|
416
|
-
if (mc === undefined || typeof mc.callTool !== 'function') {
|
|
417
|
-
throw new Error('WebMCP (navigator.modelContext) not available on this page');
|
|
418
|
-
}
|
|
419
|
-
return await mc.callTool(tool, params);
|
|
420
|
-
}
|
|
421
413
|
export async function executeSequence(steps) {
|
|
422
414
|
const effects = [];
|
|
423
415
|
const stepResults = [];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type ActionTarget } from '../dom/realm.js';
|
|
2
|
+
export declare const isAriaToggle: (el: Element) => boolean;
|
|
3
|
+
export declare const ariaChecked: (el: Element) => boolean;
|
|
4
|
+
export declare function assertAriaToggleLanded(el: Element, action: string, skipped: boolean): void;
|
|
5
|
+
export declare function alreadyAtCheckedState(el: ActionTarget, action: string): boolean;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ActionType } from '@reticlehq/core';
|
|
2
|
+
import { isInput } from '../dom/realm.js';
|
|
3
|
+
const ARIA_TOGGLE_ROLES = new Set(['checkbox', 'switch']);
|
|
4
|
+
const ARIA_CHECKED = 'aria-checked';
|
|
5
|
+
export const isAriaToggle = (el) => !isInput(el) && ARIA_TOGGLE_ROLES.has(el.getAttribute('role') ?? '');
|
|
6
|
+
export const ariaChecked = (el) => 'true' === el.getAttribute(ARIA_CHECKED);
|
|
7
|
+
export function assertAriaToggleLanded(el, action, skipped) {
|
|
8
|
+
if (skipped || !isAriaToggle(el))
|
|
9
|
+
return;
|
|
10
|
+
if (action !== ActionType.CHECK && action !== ActionType.UNCHECK)
|
|
11
|
+
return;
|
|
12
|
+
const wanted = action === ActionType.CHECK;
|
|
13
|
+
if (ariaChecked(el) === wanted)
|
|
14
|
+
return;
|
|
15
|
+
throw new Error(`${action} clicked the ${el.getAttribute('role') ?? ''} but ${ARIA_CHECKED} is still "${String(!wanted)}" — the app did not toggle it`);
|
|
16
|
+
}
|
|
17
|
+
export function alreadyAtCheckedState(el, action) {
|
|
18
|
+
if (action !== ActionType.CHECK && action !== ActionType.UNCHECK)
|
|
19
|
+
return false;
|
|
20
|
+
const wanted = action === ActionType.CHECK;
|
|
21
|
+
if (isInput(el))
|
|
22
|
+
return el.checked === wanted;
|
|
23
|
+
return isAriaToggle(el) && ariaChecked(el) === wanted;
|
|
24
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function
|
|
1
|
+
import { type ActionTarget } from '../dom/realm.js';
|
|
2
|
+
export declare function dangerousActionContext(el: ActionTarget): string;
|
|
3
|
+
export declare function submitControlFor(el: ActionTarget): HTMLElement | null;
|
|
3
4
|
export declare function requiresDangerousConfirmation(text: string, role?: string): boolean;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isDangerousActionText } from '@reticlehq/core';
|
|
2
2
|
import { getAccessibleName } from '../dom/a11y.js';
|
|
3
|
+
import { isHtmlElement } from '../dom/realm.js';
|
|
3
4
|
const LABEL_VALUED_INPUT_TYPES = new Set([
|
|
4
5
|
'submit',
|
|
5
6
|
'button',
|
|
@@ -7,7 +8,7 @@ const LABEL_VALUED_INPUT_TYPES = new Set([
|
|
|
7
8
|
'image',
|
|
8
9
|
]);
|
|
9
10
|
function holdsUserText(el) {
|
|
10
|
-
if (el.isContentEditable)
|
|
11
|
+
if (isHtmlElement(el) && el.isContentEditable)
|
|
11
12
|
return true;
|
|
12
13
|
if (el instanceof HTMLTextAreaElement)
|
|
13
14
|
return true;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ActionTarget } from '../dom/realm.js';
|
|
1
2
|
export declare function setNativeValue(el: HTMLInputElement | HTMLTextAreaElement, value: string): boolean;
|
|
2
|
-
export declare function assertNotRichText(el:
|
|
3
|
+
export declare function assertNotRichText(el: ActionTarget, action: string): void;
|
|
3
4
|
export declare function assertEditable(el: HTMLInputElement | HTMLTextAreaElement, action: string): void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { captureValueSetter } from '../patching/capture-method.js';
|
|
2
|
-
import { valuePrototypeOf } from '../dom/realm.js';
|
|
2
|
+
import { isHtmlElement, valuePrototypeOf } from '../dom/realm.js';
|
|
3
3
|
export function setNativeValue(el, value) {
|
|
4
4
|
const proto = valuePrototypeOf(el);
|
|
5
5
|
const setter = proto === undefined ? undefined : captureValueSetter(proto);
|
|
@@ -15,7 +15,7 @@ export function setNativeValue(el, value) {
|
|
|
15
15
|
}
|
|
16
16
|
function isRichText(el) {
|
|
17
17
|
const flag = el.getAttribute('contenteditable');
|
|
18
|
-
if (el.isContentEditable || (null !== flag && 'false' !== flag))
|
|
18
|
+
if ((isHtmlElement(el) && el.isContentEditable) || (null !== flag && 'false' !== flag))
|
|
19
19
|
return true;
|
|
20
20
|
if ('textbox' === el.getAttribute('role'))
|
|
21
21
|
return true;
|
|
@@ -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,9 +1,13 @@
|
|
|
1
1
|
import { ElementState, REDACTED_VALUE } from '@reticlehq/core';
|
|
2
|
-
import { isButton, isHtmlElement, isImage, isInput, isSelect, isTextArea } from './realm.js';
|
|
2
|
+
import { isButton, isHtmlElement, isImage, isInput, isMeter, isOutput, isProgress, isSelect, isTextArea, } from './realm.js';
|
|
3
3
|
import { refs } from './addressing/refs.js';
|
|
4
|
+
import { capturedRootOf } from './shadow-registry.js';
|
|
4
5
|
import { inspectChart } from './chart.js';
|
|
5
6
|
import { isSensitiveKey } from '../security/serialization.js';
|
|
6
7
|
import { formatSource, sourceFromDom } from './addressing/source.js';
|
|
8
|
+
const HTML_DETAILS_TAG = 'details';
|
|
9
|
+
const HTML_DETAILS_OPEN_ATTRIBUTE = 'open';
|
|
10
|
+
const HTML_SUMMARY_TAG = 'summary';
|
|
7
11
|
const NAME_FROM_CONTENT = new Set([
|
|
8
12
|
'button',
|
|
9
13
|
'link',
|
|
@@ -170,13 +174,19 @@ function textWithoutHidden(node) {
|
|
|
170
174
|
const alt = el.getAttribute('alt');
|
|
171
175
|
return null === alt ? '' : alt;
|
|
172
176
|
}
|
|
173
|
-
|
|
177
|
+
let result = '';
|
|
178
|
+
let lastWasElement = false;
|
|
174
179
|
for (const child of el.childNodes) {
|
|
175
180
|
const piece = textWithoutHidden(child);
|
|
176
|
-
if (piece.length
|
|
177
|
-
|
|
181
|
+
if (0 === piece.length)
|
|
182
|
+
continue;
|
|
183
|
+
const isEl = Node.ELEMENT_NODE === child.nodeType;
|
|
184
|
+
if (result.length > 0 && (isEl || lastWasElement))
|
|
185
|
+
result += ' ';
|
|
186
|
+
result += piece;
|
|
187
|
+
lastWasElement = isEl;
|
|
178
188
|
}
|
|
179
|
-
return
|
|
189
|
+
return result;
|
|
180
190
|
}
|
|
181
191
|
export function getAccessibleName(el) {
|
|
182
192
|
const labelled = labelledByText(el);
|
|
@@ -190,7 +200,14 @@ export function getAccessibleName(el) {
|
|
|
190
200
|
if (alt !== null)
|
|
191
201
|
return alt.trim();
|
|
192
202
|
}
|
|
193
|
-
|
|
203
|
+
const isLabelable = isInput(el) ||
|
|
204
|
+
isTextArea(el) ||
|
|
205
|
+
isSelect(el) ||
|
|
206
|
+
isButton(el) ||
|
|
207
|
+
isMeter(el) ||
|
|
208
|
+
isOutput(el) ||
|
|
209
|
+
isProgress(el);
|
|
210
|
+
if (isLabelable) {
|
|
194
211
|
const labels = el.labels;
|
|
195
212
|
if (labels !== null && labels.length > 0) {
|
|
196
213
|
const text = [...labels]
|
|
@@ -200,6 +217,16 @@ export function getAccessibleName(el) {
|
|
|
200
217
|
if (text.length > 0)
|
|
201
218
|
return text;
|
|
202
219
|
}
|
|
220
|
+
}
|
|
221
|
+
if ('fieldset' === el.tagName.toLowerCase()) {
|
|
222
|
+
const legend = [...el.children].find((child) => 'legend' === child.tagName.toLowerCase());
|
|
223
|
+
if (legend !== undefined) {
|
|
224
|
+
const text = collapse(textWithoutHidden(legend));
|
|
225
|
+
if (text.length > 0)
|
|
226
|
+
return text;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (isInput(el) || isTextArea(el) || isSelect(el)) {
|
|
203
230
|
if (isInput(el)) {
|
|
204
231
|
const type = el.type.toLowerCase();
|
|
205
232
|
if ('submit' === type || 'button' === type || 'reset' === type) {
|
|
@@ -241,6 +268,8 @@ export function getStates(el, visible = isVisible(el)) {
|
|
|
241
268
|
states.push(ElementState.CHECKED);
|
|
242
269
|
if (true === ariaBool(el, 'aria-expanded'))
|
|
243
270
|
states.push(ElementState.EXPANDED);
|
|
271
|
+
if (true === ariaBool(el, 'aria-pressed'))
|
|
272
|
+
states.push(ElementState.PRESSED);
|
|
244
273
|
if (el.ownerDocument.activeElement === el)
|
|
245
274
|
states.push(ElementState.FOCUSED);
|
|
246
275
|
return states;
|
|
@@ -270,11 +299,23 @@ export function getValue(el) {
|
|
|
270
299
|
const valueNow = el.getAttribute('aria-valuenow');
|
|
271
300
|
return valueNow ?? undefined;
|
|
272
301
|
}
|
|
302
|
+
function hiddenInsideClosedDetails(el) {
|
|
303
|
+
const parent = el.parentElement;
|
|
304
|
+
if (null === parent)
|
|
305
|
+
return false;
|
|
306
|
+
const details = parent.closest(HTML_DETAILS_TAG);
|
|
307
|
+
if (null === details || details.hasAttribute(HTML_DETAILS_OPEN_ATTRIBUTE))
|
|
308
|
+
return false;
|
|
309
|
+
const summary = Array.from(details.children).find((child) => HTML_SUMMARY_TAG === child.localName);
|
|
310
|
+
return summary === undefined || !summary.contains(el);
|
|
311
|
+
}
|
|
273
312
|
function selfHidden(el) {
|
|
274
313
|
if ('true' === el.getAttribute('aria-hidden'))
|
|
275
314
|
return true;
|
|
276
315
|
if (isHtmlElement(el) && el.hidden)
|
|
277
316
|
return true;
|
|
317
|
+
if (hiddenInsideClosedDetails(el))
|
|
318
|
+
return true;
|
|
278
319
|
const view = el.ownerDocument.defaultView;
|
|
279
320
|
if (view !== null) {
|
|
280
321
|
const style = view.getComputedStyle(el);
|
|
@@ -288,6 +329,28 @@ function selfHidden(el) {
|
|
|
288
329
|
}
|
|
289
330
|
return false;
|
|
290
331
|
}
|
|
332
|
+
function slotInCapturedClosedRoot(el) {
|
|
333
|
+
const host = el.parentElement;
|
|
334
|
+
if (null === host || null !== host.shadowRoot)
|
|
335
|
+
return null;
|
|
336
|
+
const root = capturedRootOf(host);
|
|
337
|
+
if (null === root)
|
|
338
|
+
return null;
|
|
339
|
+
for (const slot of Array.from(root.querySelectorAll('slot'))) {
|
|
340
|
+
if (slot.assignedElements().includes(el))
|
|
341
|
+
return slot;
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
function parentAcrossShadowBoundary(el) {
|
|
346
|
+
const slot = el.assignedSlot ?? slotInCapturedClosedRoot(el);
|
|
347
|
+
if (null !== slot)
|
|
348
|
+
return slot;
|
|
349
|
+
if (null !== el.parentElement)
|
|
350
|
+
return el.parentElement;
|
|
351
|
+
const host = el.getRootNode().host;
|
|
352
|
+
return host ?? null;
|
|
353
|
+
}
|
|
291
354
|
export function isInViewport(el, memo) {
|
|
292
355
|
if (!isVisible(el, memo))
|
|
293
356
|
return false;
|
|
@@ -305,7 +368,7 @@ export function isVisible(el, memo) {
|
|
|
305
368
|
const cached = memo?.get(el);
|
|
306
369
|
if (cached !== undefined)
|
|
307
370
|
return cached;
|
|
308
|
-
const parent = el
|
|
371
|
+
const parent = parentAcrossShadowBoundary(el);
|
|
309
372
|
const result = !selfHidden(el) && (null === parent || isVisible(parent, memo));
|
|
310
373
|
if (memo !== undefined)
|
|
311
374
|
memo.set(el, result);
|
package/dist/dom/occlusion.js
CHANGED
|
@@ -14,9 +14,13 @@ export function hitTestOccluder(el, rect) {
|
|
|
14
14
|
if (typeof doc.elementFromPoint !== 'function')
|
|
15
15
|
return null;
|
|
16
16
|
const withStack = doc;
|
|
17
|
+
const isOursButNotTheTarget = (candidate) => isReticleUi(candidate) &&
|
|
18
|
+
candidate !== el &&
|
|
19
|
+
!el.contains(candidate) &&
|
|
20
|
+
!candidate.contains(el);
|
|
17
21
|
const topAt = (x, y) => {
|
|
18
22
|
const found = withStack.elementsFromPoint?.(x, y) ?? [doc.elementFromPoint(x, y)];
|
|
19
|
-
return found.find((c) => null !== c && !
|
|
23
|
+
return found.find((c) => null !== c && !isOursButNotTheTarget(c)) ?? null;
|
|
20
24
|
};
|
|
21
25
|
const blockers = new Map();
|
|
22
26
|
for (const [fx, fy] of SAMPLE_POINTS) {
|
package/dist/dom/query.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DATA_RETICLE_SOURCE_ATTR, ElementState, QueryBy, REDACTED_VALUE, TRANSPORT_LIMITS, } from '@reticlehq/core';
|
|
2
|
-
import { isElement, isFrame,
|
|
2
|
+
import { isActionTarget, isElement, isFrame, isInput, isSelect, isTextArea } from './realm.js';
|
|
3
3
|
import { capturedRootOf } from './shadow-registry.js';
|
|
4
4
|
import { getAccessibleName, getRole, describe, getStates, isInViewport, isVisible, } from './a11y.js';
|
|
5
5
|
import { isIgnored, isReticleOverlay } from './dom-ignore.js';
|
|
@@ -71,11 +71,11 @@ function resolveContainer(scope) {
|
|
|
71
71
|
if (scope === undefined)
|
|
72
72
|
return { container: document.body, scopeMissing: false };
|
|
73
73
|
const byRef = refs.resolve(scope);
|
|
74
|
-
if (
|
|
74
|
+
if (isActionTarget(byRef))
|
|
75
75
|
return { container: byRef, scopeMissing: false };
|
|
76
76
|
try {
|
|
77
77
|
const found = document.querySelector(scope);
|
|
78
|
-
if (
|
|
78
|
+
if (isActionTarget(found))
|
|
79
79
|
return { container: found, scopeMissing: false };
|
|
80
80
|
}
|
|
81
81
|
catch {
|
|
@@ -269,7 +269,7 @@ export function matchQuery(query, state, limit = MAX_DESCRIBED) {
|
|
|
269
269
|
const filtered = state === undefined ? elements : elements.filter((el) => inState(el, state, visMemo));
|
|
270
270
|
const attrs = query.attrs;
|
|
271
271
|
const described = filtered.slice(0, Math.max(0, Math.min(limit, MAX_DESCRIBED)));
|
|
272
|
-
const stampViewport = described.length > 1;
|
|
272
|
+
const stampViewport = described.length > 1 || ElementState.IN_VIEWPORT === state;
|
|
273
273
|
const descriptors = described.map((el) => {
|
|
274
274
|
let base = describe(el, visMemo);
|
|
275
275
|
if (stampViewport &&
|
package/dist/dom/realm.d.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
export declare const isElement: (n: unknown) => n is Element;
|
|
2
2
|
export declare const isImage: (n: unknown) => n is HTMLImageElement;
|
|
3
3
|
export declare const isHtmlElement: (n: unknown) => n is HTMLElement;
|
|
4
|
+
export declare const isSvgElement: (n: unknown) => n is SVGElement;
|
|
5
|
+
export type ActionTarget = HTMLElement | SVGElement;
|
|
6
|
+
export declare const isActionTarget: (n: unknown) => n is ActionTarget;
|
|
4
7
|
export declare const isInput: (n: unknown) => n is HTMLInputElement;
|
|
5
8
|
export declare const isTextArea: (n: unknown) => n is HTMLTextAreaElement;
|
|
6
9
|
export declare const isSelect: (n: unknown) => n is HTMLSelectElement;
|
|
7
10
|
export declare const isButton: (n: unknown) => n is HTMLButtonElement;
|
|
11
|
+
export declare const isMeter: (n: unknown) => n is HTMLMeterElement;
|
|
12
|
+
export declare const isOutput: (n: unknown) => n is HTMLOutputElement;
|
|
13
|
+
export declare const isProgress: (n: unknown) => n is HTMLProgressElement;
|
|
8
14
|
export declare const isForm: (n: unknown) => n is HTMLFormElement;
|
|
9
15
|
export declare const isFrame: (n: unknown) => n is HTMLIFrameElement;
|
|
10
16
|
export declare function valuePrototypeOf(el: Element): object | undefined;
|
package/dist/dom/realm.js
CHANGED
|
@@ -14,10 +14,15 @@ function isIn(node, name, ambient) {
|
|
|
14
14
|
export const isElement = (n) => isIn(n, 'Element', 'undefined' === typeof Element ? undefined : Element);
|
|
15
15
|
export const isImage = (n) => isIn(n, 'HTMLImageElement', 'undefined' === typeof HTMLImageElement ? undefined : HTMLImageElement);
|
|
16
16
|
export const isHtmlElement = (n) => isIn(n, 'HTMLElement', 'undefined' === typeof HTMLElement ? undefined : HTMLElement);
|
|
17
|
+
export const isSvgElement = (n) => isIn(n, 'SVGElement', 'undefined' === typeof SVGElement ? undefined : SVGElement);
|
|
18
|
+
export const isActionTarget = (n) => isHtmlElement(n) || isSvgElement(n);
|
|
17
19
|
export const isInput = (n) => isIn(n, 'HTMLInputElement', 'undefined' === typeof HTMLInputElement ? undefined : HTMLInputElement);
|
|
18
20
|
export const isTextArea = (n) => isIn(n, 'HTMLTextAreaElement', 'undefined' === typeof HTMLTextAreaElement ? undefined : HTMLTextAreaElement);
|
|
19
21
|
export const isSelect = (n) => isIn(n, 'HTMLSelectElement', 'undefined' === typeof HTMLSelectElement ? undefined : HTMLSelectElement);
|
|
20
22
|
export const isButton = (n) => isIn(n, 'HTMLButtonElement', 'undefined' === typeof HTMLButtonElement ? undefined : HTMLButtonElement);
|
|
23
|
+
export const isMeter = (n) => isIn(n, 'HTMLMeterElement', 'undefined' === typeof HTMLMeterElement ? undefined : HTMLMeterElement);
|
|
24
|
+
export const isOutput = (n) => isIn(n, 'HTMLOutputElement', 'undefined' === typeof HTMLOutputElement ? undefined : HTMLOutputElement);
|
|
25
|
+
export const isProgress = (n) => isIn(n, 'HTMLProgressElement', 'undefined' === typeof HTMLProgressElement ? undefined : HTMLProgressElement);
|
|
21
26
|
export const isForm = (n) => isIn(n, 'HTMLFormElement', 'undefined' === typeof HTMLFormElement ? undefined : HTMLFormElement);
|
|
22
27
|
export const isFrame = (n) => isIn(n, 'HTMLIFrameElement', 'undefined' === typeof HTMLIFrameElement ? undefined : HTMLIFrameElement);
|
|
23
28
|
export function valuePrototypeOf(el) {
|
package/dist/dom/snapshot.js
CHANGED
|
@@ -54,6 +54,7 @@ function stateSuffix(el) {
|
|
|
54
54
|
const states = getStates(el).filter((s) => s === ElementState.DISABLED ||
|
|
55
55
|
s === ElementState.CHECKED ||
|
|
56
56
|
s === ElementState.EXPANDED ||
|
|
57
|
+
s === ElementState.PRESSED ||
|
|
57
58
|
s === ElementState.FOCUSED);
|
|
58
59
|
return states.length > 0 ? ` [${states.join(',')}]` : '';
|
|
59
60
|
}
|
|
@@ -142,9 +143,14 @@ function visit(child, depth, ctx, inLive) {
|
|
|
142
143
|
}
|
|
143
144
|
}
|
|
144
145
|
function walk(parent, depth, ctx, inLive = false) {
|
|
145
|
-
if (depth > ctx.maxDepth)
|
|
146
|
-
return;
|
|
147
146
|
const children = pierceChildren(parent);
|
|
147
|
+
if (depth > ctx.maxDepth) {
|
|
148
|
+
if (children.length > 0) {
|
|
149
|
+
ctx.truncated = true;
|
|
150
|
+
recordUnread([parent], ctx);
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
148
154
|
for (let index = 0; index < children.length; index += 1) {
|
|
149
155
|
if (ctx.nodes >= ctx.maxNodes) {
|
|
150
156
|
ctx.truncated = true;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BlindSpotKind, EventType, REQUEST_SHAPE_FIELD, REQUEST_SHAPE_NONE, RETICLE_WS_PATH, StreamTransport, StreamDirection, fnv1a, } from '@reticlehq/core';
|
|
1
|
+
import { BlindSpotKind, EventType, NEXT_ACTION_FIELD, NEXT_ACTION_HEADER, REQUEST_SHAPE_FIELD, REQUEST_SHAPE_NONE, RETICLE_WS_PATH, StreamTransport, StreamDirection, fnv1a, } from '@reticlehq/core';
|
|
2
2
|
import { captureMethod } from '../patching/capture-method.js';
|
|
3
3
|
import { observeSafely, observeValue } from './types.js';
|
|
4
4
|
import { isCapturableType, projectBody, projectErrorBody, shouldCaptureErrorBody, withBodyDeadline, } from './net-detail/network-body.js';
|
|
@@ -120,6 +120,15 @@ function methodOf(input, init) {
|
|
|
120
120
|
return input.method.toUpperCase();
|
|
121
121
|
return 'GET';
|
|
122
122
|
}
|
|
123
|
+
function nextActionOf(input, init) {
|
|
124
|
+
if (init !== undefined && init.headers !== undefined) {
|
|
125
|
+
return observeValue(() => new Headers(init.headers).get(NEXT_ACTION_HEADER)) ?? undefined;
|
|
126
|
+
}
|
|
127
|
+
if (input instanceof Request) {
|
|
128
|
+
return observeValue(() => input.headers.get(NEXT_ACTION_HEADER)) ?? undefined;
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
123
132
|
const NON_APP_FRAME = /@reticlehq|reticle\.ts|network\.ts|transport\.ts|<anonymous>|new Promise|node:internal/i;
|
|
124
133
|
const DEPENDENCY_FRAME = /node_modules|\/\.vite\/deps\//i;
|
|
125
134
|
export function firstAppFrame(stack) {
|
|
@@ -191,6 +200,8 @@ export function installNetwork(emit, opts = {}) {
|
|
|
191
200
|
const id = nextId();
|
|
192
201
|
const start = performance.now();
|
|
193
202
|
const method = methodOf(input, init);
|
|
203
|
+
const nextAction = nextActionOf(input, init);
|
|
204
|
+
const nextActionFields = nextAction === undefined ? {} : { [NEXT_ACTION_FIELD]: nextAction };
|
|
194
205
|
const urlFields = netUrlFields(rawUrl);
|
|
195
206
|
const url = urlFields.url;
|
|
196
207
|
const initiatorStack = observeValue(() => initiatorFrame());
|
|
@@ -226,6 +237,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
226
237
|
...resourceTiming(rawUrl),
|
|
227
238
|
...netResponseMeta(res.statusText, contentType, res.headers.get('content-length')),
|
|
228
239
|
...projectRequestBody(init?.body, captureBodies),
|
|
240
|
+
...nextActionFields,
|
|
229
241
|
...responseBodyFields,
|
|
230
242
|
...(reinterpret?.(url, (name) => res.headers.get(name)) ?? {}),
|
|
231
243
|
});
|
|
@@ -282,6 +294,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
282
294
|
durationMs: Math.round(performance.now() - start),
|
|
283
295
|
initiator: 'fetch',
|
|
284
296
|
...initiatorFields,
|
|
297
|
+
...nextActionFields,
|
|
285
298
|
});
|
|
286
299
|
});
|
|
287
300
|
throw error;
|
package/dist/observers/state.js
CHANGED
|
@@ -54,7 +54,10 @@ export function installStoreState(emit) {
|
|
|
54
54
|
for (const change of changes) {
|
|
55
55
|
const value = project(change.path, change.new);
|
|
56
56
|
const old = project(change.path, change.old);
|
|
57
|
-
|
|
57
|
+
const same = isSensitiveKey(change.path)
|
|
58
|
+
? samePresentedValue(sanitizeForTransport(change.old), sanitizeForTransport(change.new))
|
|
59
|
+
: samePresentedValue(old, value);
|
|
60
|
+
if (same)
|
|
58
61
|
continue;
|
|
59
62
|
observeSafely(() => emit(EventType.STATE_CHANGE, { name, path: change.path, value, old }));
|
|
60
63
|
}
|
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
import { nativeWarn } from '../timers/console/native-console.js';
|
|
2
2
|
import { markAdapterSource } from './stores.js';
|
|
3
|
+
function snapshotLive(value) {
|
|
4
|
+
try {
|
|
5
|
+
return copyPlain(value, new WeakMap());
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function copyPlain(value, seen) {
|
|
12
|
+
if ('object' !== typeof value || null === value)
|
|
13
|
+
return value;
|
|
14
|
+
const done = seen.get(value);
|
|
15
|
+
if (done !== undefined)
|
|
16
|
+
return done;
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
const out = [];
|
|
19
|
+
seen.set(value, out);
|
|
20
|
+
for (const item of value)
|
|
21
|
+
out.push(copyPlain(item, seen));
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
const proto = Object.getPrototypeOf(value);
|
|
25
|
+
if (proto !== Object.prototype && proto !== null)
|
|
26
|
+
return value;
|
|
27
|
+
const out = {};
|
|
28
|
+
seen.set(value, out);
|
|
29
|
+
for (const [key, item] of Object.entries(value))
|
|
30
|
+
out[key] = copyPlain(item, seen);
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
3
33
|
export function tanstackQueryStore(client) {
|
|
4
34
|
const store = {
|
|
5
35
|
getState: () => {
|
|
@@ -106,7 +136,7 @@ export function svelteStore(readable, warn = nativeWarn) {
|
|
|
106
136
|
let value;
|
|
107
137
|
let called = false;
|
|
108
138
|
stopSubscription(readable.subscribe((next) => {
|
|
109
|
-
value = next;
|
|
139
|
+
value = snapshotLive(next);
|
|
110
140
|
called = true;
|
|
111
141
|
}));
|
|
112
142
|
if (!called && !warnedAboutLazyStore) {
|
|
@@ -132,7 +162,7 @@ export function svelteStore(readable, warn = nativeWarn) {
|
|
|
132
162
|
const PINIA_SUBSCRIBE_OPTIONS = { detached: true, flush: 'sync' };
|
|
133
163
|
export function piniaStore(store) {
|
|
134
164
|
return {
|
|
135
|
-
getState: () => store.$state,
|
|
165
|
+
getState: () => snapshotLive(store.$state),
|
|
136
166
|
subscribe: (listener) => store.$subscribe(() => listener(), { ...PINIA_SUBSCRIBE_OPTIONS }),
|
|
137
167
|
};
|
|
138
168
|
}
|
|
@@ -6,8 +6,14 @@ export function registerAdapter(adapter) {
|
|
|
6
6
|
}
|
|
7
7
|
export function identifyComponent(el) {
|
|
8
8
|
for (const adapter of adapters) {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
let info;
|
|
10
|
+
try {
|
|
11
|
+
info = adapter.identify(el);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (info !== null && info !== undefined)
|
|
11
17
|
return info;
|
|
12
18
|
}
|
|
13
19
|
return null;
|