@reticlehq/browser 3.9.0 → 3.10.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 +14 -0
- package/dist/actions/action-report.js +95 -22
- package/dist/actions/actions-dom.d.ts +16 -6
- package/dist/actions/actions-dom.js +94 -46
- package/dist/actions/actions-press.d.ts +10 -0
- package/dist/actions/actions-press.js +146 -2
- package/dist/actions/actions.js +52 -22
- package/dist/actions/insert-text.d.ts +3 -0
- package/dist/actions/insert-text.js +90 -0
- package/dist/actions/scroll.d.ts +1 -1
- package/dist/actions/scroll.js +25 -1
- package/dist/actions/value-input.d.ts +2 -0
- package/dist/actions/value-input.js +30 -9
- package/dist/commands/commands.js +82 -6
- package/dist/connect-options.d.ts +11 -1
- package/dist/connect-options.js +27 -1
- package/dist/dom/a11y.js +4 -3
- package/dist/dom/chart-geometry.d.ts +7 -0
- package/dist/dom/chart-geometry.js +39 -0
- package/dist/dom/occlusion.js +21 -0
- package/dist/dom/query-finders.d.ts +1 -1
- package/dist/dom/query-finders.js +10 -7
- package/dist/dom/query.d.ts +1 -0
- package/dist/dom/query.js +78 -51
- package/dist/dom/scope-roots.d.ts +8 -0
- package/dist/dom/scope-roots.js +44 -0
- package/dist/dom/snapshot.d.ts +3 -0
- package/dist/dom/snapshot.js +9 -2
- package/dist/dom/text-match.js +4 -3
- package/dist/dom/theme.js +6 -7
- package/dist/dom/visibility.d.ts +2 -0
- package/dist/dom/visibility.js +40 -8
- package/dist/observers/canvas.d.ts +1 -0
- package/dist/observers/canvas.js +10 -1
- package/dist/observers/console.js +46 -35
- package/dist/observers/dom.js +28 -4
- package/dist/observers/net-detail/resource-timing.d.ts +12 -0
- package/dist/observers/net-detail/resource-timing.js +69 -0
- package/dist/observers/network.js +12 -45
- package/dist/observers/perf.js +15 -1
- package/dist/presenter/chrome/presenter-verbs.js +1 -0
- package/dist/presenter/presenter-config.d.ts +1 -0
- package/dist/presenter/presenter-config.js +1 -0
- package/dist/presenter/presenter-shell.d.ts +1 -0
- package/dist/presenter/presenter-shell.js +6 -3
- package/dist/presenter/tour/tour.js +4 -0
- package/dist/recorder/recorder.js +82 -39
- package/dist/reticle-inject.js +597 -597
- package/dist/reticle.d.ts +3 -19
- package/dist/reticle.js +74 -108
- package/dist/security/serialization.js +35 -0
- package/dist/storage-keys.js +2 -1
- package/dist/timers/native/native-timers.js +21 -4
- package/dist/transport/bridge-url.d.ts +8 -0
- package/dist/transport/bridge-url.js +52 -0
- package/package.json +10 -10
|
@@ -31,7 +31,12 @@ export interface ActionResult {
|
|
|
31
31
|
effect: ActionEffect;
|
|
32
32
|
testid?: string;
|
|
33
33
|
testidOf?: number;
|
|
34
|
+
testidNth?: number;
|
|
34
35
|
component?: string;
|
|
36
|
+
componentNth?: number;
|
|
37
|
+
componentOf?: number;
|
|
38
|
+
ownText?: string;
|
|
39
|
+
within?: string[];
|
|
35
40
|
role?: string;
|
|
36
41
|
name?: string;
|
|
37
42
|
nth?: number;
|
|
@@ -48,17 +53,26 @@ export interface ActionResult {
|
|
|
48
53
|
export interface CapturedAnchor {
|
|
49
54
|
testid?: string;
|
|
50
55
|
testidOf?: number;
|
|
56
|
+
testidNth?: number;
|
|
51
57
|
component?: string;
|
|
52
58
|
source?: {
|
|
53
59
|
file: string;
|
|
54
60
|
line: number;
|
|
55
61
|
column?: number;
|
|
56
62
|
};
|
|
63
|
+
componentNth?: number;
|
|
64
|
+
componentOf?: number;
|
|
65
|
+
ownText?: string;
|
|
57
66
|
role?: string;
|
|
58
67
|
name?: string;
|
|
59
68
|
nth?: number;
|
|
60
69
|
of?: number;
|
|
70
|
+
within?: string[];
|
|
61
71
|
secret?: true;
|
|
62
72
|
}
|
|
73
|
+
export declare function positionAmongNamesakes(el: Element, role: string, name: string): {
|
|
74
|
+
nth: number;
|
|
75
|
+
of: number;
|
|
76
|
+
} | null;
|
|
63
77
|
export declare function anchorOf(el: Element): CapturedAnchor;
|
|
64
78
|
export declare const result: (ref: string, action: string, effect: ActionEffect, settled: boolean, settleReason: SettleReason | null, anchor: CapturedAnchor, warning?: string) => ActionResult;
|
|
@@ -1,36 +1,80 @@
|
|
|
1
1
|
import { readTestId } from '../dom/addressing/testid-attr.js';
|
|
2
2
|
import { QueryBy } from '@reticlehq/core';
|
|
3
3
|
import { elementsMatching } from '../dom/query.js';
|
|
4
|
+
import { ownWords } from '../dom/query-finders.js';
|
|
4
5
|
import { getAccessibleName, getRole, isSensitiveField } from '../dom/a11y.js';
|
|
5
6
|
import { identifyComponent } from '../registry/stores/adapters.js';
|
|
6
7
|
import { isReticleUi } from '../dom/dom-ignore.js';
|
|
8
|
+
import { isInViewport } from '../dom/visibility.js';
|
|
7
9
|
const CANVAS_SELECTOR = 'canvas';
|
|
8
10
|
export function visibleCanvases(doc) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
return Array.from(doc.querySelectorAll(CANVAS_SELECTOR)).filter((canvas) => !isReticleUi(canvas) && isInViewport(canvas)).length;
|
|
12
|
+
}
|
|
13
|
+
export function positionAmongNamesakes(el, role, name) {
|
|
14
|
+
const same = name.length > 0
|
|
15
|
+
? elementsMatching({ by: QueryBy.ROLE, value: role, name })
|
|
16
|
+
: elementsMatching({ by: QueryBy.ROLE, value: role }).filter((candidate) => 0 === getAccessibleName(candidate).length);
|
|
17
|
+
const at = same.indexOf(el);
|
|
18
|
+
return 1 < same.length && 0 <= at ? { nth: at, of: same.length } : null;
|
|
19
|
+
}
|
|
20
|
+
function positionInComponentAnchor(el, component, source, role) {
|
|
21
|
+
if (component === undefined && source === undefined) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const same = elementsMatching({
|
|
25
|
+
by: QueryBy.COMPONENT,
|
|
26
|
+
value: component ?? '',
|
|
27
|
+
...(component === undefined ? {} : { component }),
|
|
28
|
+
...(source === undefined ? {} : { source }),
|
|
29
|
+
...(role.length > 0 ? { role } : {}),
|
|
30
|
+
});
|
|
31
|
+
const at = same.indexOf(el);
|
|
32
|
+
return 1 < same.length && 0 <= at ? { nth: at, of: same.length } : null;
|
|
33
|
+
}
|
|
34
|
+
const CONTAINER_ROLES = new Set([
|
|
35
|
+
'alert',
|
|
36
|
+
'alertdialog',
|
|
37
|
+
'dialog',
|
|
38
|
+
'form',
|
|
39
|
+
'region',
|
|
40
|
+
'navigation',
|
|
41
|
+
'menu',
|
|
42
|
+
'menubar',
|
|
43
|
+
'listbox',
|
|
44
|
+
'list',
|
|
45
|
+
'listitem',
|
|
46
|
+
'tabpanel',
|
|
47
|
+
'table',
|
|
48
|
+
'grid',
|
|
49
|
+
'row',
|
|
50
|
+
'group',
|
|
51
|
+
'toolbar',
|
|
52
|
+
'tree',
|
|
53
|
+
'search',
|
|
54
|
+
]);
|
|
55
|
+
const WITHIN_MAX = 2;
|
|
56
|
+
function containerRoles(el) {
|
|
57
|
+
const out = [];
|
|
58
|
+
for (let a = el.parentElement; a !== null && out.length < WITHIN_MAX; a = a.parentElement) {
|
|
59
|
+
const role = getRole(a);
|
|
60
|
+
const landmarkNeedsName = 'form' === role || 'region' === role;
|
|
61
|
+
if (CONTAINER_ROLES.has(role) && (!landmarkNeedsName || getAccessibleName(a).length > 0)) {
|
|
62
|
+
out.push(role);
|
|
21
63
|
}
|
|
22
64
|
}
|
|
23
|
-
return
|
|
65
|
+
return out.length > 0 ? out : undefined;
|
|
24
66
|
}
|
|
67
|
+
const OWN_TEXT_MAX = 80;
|
|
25
68
|
export function anchorOf(el) {
|
|
26
69
|
const testid = readTestId(el) ?? undefined;
|
|
27
70
|
const info = identifyComponent(el);
|
|
28
71
|
const out = {};
|
|
29
72
|
if (testid !== undefined) {
|
|
30
73
|
out.testid = testid;
|
|
31
|
-
const shared = elementsMatching({ by: QueryBy.TESTID, value: testid })
|
|
32
|
-
if (1 < shared) {
|
|
33
|
-
out.testidOf = shared;
|
|
74
|
+
const shared = elementsMatching({ by: QueryBy.TESTID, value: testid });
|
|
75
|
+
if (1 < shared.length) {
|
|
76
|
+
out.testidOf = shared.length;
|
|
77
|
+
out.testidNth = shared.findIndex((match) => match === el);
|
|
34
78
|
}
|
|
35
79
|
}
|
|
36
80
|
const component = info?.componentStack[0];
|
|
@@ -42,21 +86,35 @@ export function anchorOf(el) {
|
|
|
42
86
|
}
|
|
43
87
|
const role = getRole(el);
|
|
44
88
|
const name = getAccessibleName(el);
|
|
89
|
+
const position = positionInComponentAnchor(el, out.component, out.source, role);
|
|
90
|
+
if (position !== null) {
|
|
91
|
+
out.componentNth = position.nth;
|
|
92
|
+
out.componentOf = position.of;
|
|
93
|
+
}
|
|
45
94
|
if (role.length > 0) {
|
|
46
95
|
out.role = role;
|
|
47
96
|
}
|
|
48
97
|
if (name.length > 0) {
|
|
49
98
|
out.name = name;
|
|
99
|
+
const within = containerRoles(el);
|
|
100
|
+
if (within !== undefined) {
|
|
101
|
+
out.within = within;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (out.component !== undefined || out.source !== undefined) {
|
|
105
|
+
const words = ownWords(el);
|
|
106
|
+
if (0 < words.length && words.length <= OWN_TEXT_MAX) {
|
|
107
|
+
out.ownText = words;
|
|
108
|
+
}
|
|
50
109
|
}
|
|
51
110
|
if (isSensitiveField(el)) {
|
|
52
111
|
out.secret = true;
|
|
53
112
|
}
|
|
54
|
-
if (role.length > 0
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
out.
|
|
59
|
-
out.of = same.length;
|
|
113
|
+
if (role.length > 0) {
|
|
114
|
+
const position = positionAmongNamesakes(el, role, name);
|
|
115
|
+
if (position !== null) {
|
|
116
|
+
out.nth = position.nth;
|
|
117
|
+
out.of = position.of;
|
|
60
118
|
}
|
|
61
119
|
}
|
|
62
120
|
return out;
|
|
@@ -77,12 +135,27 @@ export const result = (ref, action, effect, settled, settleReason, anchor, warni
|
|
|
77
135
|
if (anchor.testidOf !== undefined) {
|
|
78
136
|
base.testidOf = anchor.testidOf;
|
|
79
137
|
}
|
|
138
|
+
if (anchor.testidNth !== undefined) {
|
|
139
|
+
base.testidNth = anchor.testidNth;
|
|
140
|
+
}
|
|
80
141
|
if (anchor.component !== undefined) {
|
|
81
142
|
base.component = anchor.component;
|
|
82
143
|
}
|
|
83
144
|
if (anchor.source !== undefined) {
|
|
84
145
|
base.source = anchor.source;
|
|
85
146
|
}
|
|
147
|
+
if (anchor.componentNth !== undefined) {
|
|
148
|
+
base.componentNth = anchor.componentNth;
|
|
149
|
+
}
|
|
150
|
+
if (anchor.componentOf !== undefined) {
|
|
151
|
+
base.componentOf = anchor.componentOf;
|
|
152
|
+
}
|
|
153
|
+
if (anchor.ownText !== undefined) {
|
|
154
|
+
base.ownText = anchor.ownText;
|
|
155
|
+
}
|
|
156
|
+
if (anchor.within !== undefined) {
|
|
157
|
+
base.within = anchor.within;
|
|
158
|
+
}
|
|
86
159
|
if (anchor.role !== undefined) {
|
|
87
160
|
base.role = anchor.role;
|
|
88
161
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ActionTarget } from '../dom/realm.js';
|
|
2
|
-
import { ClickButton } from '@reticlehq/core';
|
|
2
|
+
import { ClickButton, type BoxFraction } from '@reticlehq/core';
|
|
3
3
|
import { type ModifierFlags } from './actions-press.js';
|
|
4
4
|
interface ClickGeometry {
|
|
5
5
|
occluded: boolean;
|
|
@@ -21,21 +21,31 @@ export declare function fireClickSequence(el: ActionTarget, hold?: {
|
|
|
21
21
|
ms: number;
|
|
22
22
|
sleep: (ms: number) => Promise<void>;
|
|
23
23
|
now: () => number;
|
|
24
|
-
}, detail?: number, input?: ClickInput): Promise<{
|
|
24
|
+
}, detail?: number, input?: ClickInput, point?: BoxFraction): Promise<{
|
|
25
25
|
prevented: boolean;
|
|
26
26
|
heldMs: number;
|
|
27
27
|
}>;
|
|
28
28
|
export declare function scrollIntoViewNow(el: Element, options?: ScrollIntoViewOptions): void;
|
|
29
29
|
export declare function isOffViewport(el: Element, rect: DOMRect): boolean;
|
|
30
30
|
export declare function clickGeometry(el: ActionTarget): ClickGeometry;
|
|
31
|
-
export declare function firePointer(el: Element, type: string, relatedTarget?: EventTarget | null): void;
|
|
32
|
-
export declare function
|
|
33
|
-
export declare function
|
|
31
|
+
export declare function firePointer(el: Element, type: string, relatedTarget?: EventTarget | null, coords?: MouseEventInit): void;
|
|
32
|
+
export declare function leavePreviousHover(next: Element): EventTarget;
|
|
33
|
+
export declare function firePointerNonBubbling(el: Element, type: string, relatedTarget?: EventTarget | null, coords?: MouseEventInit): void;
|
|
34
|
+
interface Point {
|
|
35
|
+
x: number;
|
|
36
|
+
y: number;
|
|
37
|
+
}
|
|
38
|
+
export declare function coordsAt(p: Point): MouseEventInit;
|
|
39
|
+
export declare function coordsIn(el: ActionTarget, at: BoxFraction | undefined): MouseEventInit;
|
|
40
|
+
export declare function dragElement(source: ActionTarget, target: ActionTarget | null, data: unknown, points?: {
|
|
41
|
+
at?: BoxFraction | undefined;
|
|
42
|
+
to?: BoxFraction | undefined;
|
|
43
|
+
}, input?: ClickInput): Promise<boolean>;
|
|
34
44
|
export declare function fireTapSequence(el: ActionTarget, hold: {
|
|
35
45
|
ms: number;
|
|
36
46
|
sleep: (ms: number) => Promise<void>;
|
|
37
47
|
now: () => number;
|
|
38
|
-
} | undefined): Promise<{
|
|
48
|
+
} | undefined, point?: BoxFraction): Promise<{
|
|
39
49
|
prevented: boolean;
|
|
40
50
|
heldMs: number;
|
|
41
51
|
}>;
|
|
@@ -4,7 +4,7 @@ import { getAccessibleName, getRole } from '../dom/a11y.js';
|
|
|
4
4
|
import { hitTestOccluder } from '../dom/occlusion.js';
|
|
5
5
|
import { nativeFrame } from '../timers/native/native-timers.js';
|
|
6
6
|
import { asSyntheticInput } from './synthetic/synthetic-input.js';
|
|
7
|
-
import { ClickButton, clickButtonFromArgs } from '@reticlehq/core';
|
|
7
|
+
import { ClickButton, clickButtonFromArgs, pointInBox } from '@reticlehq/core';
|
|
8
8
|
import { pressModifiers } from './actions-press.js';
|
|
9
9
|
export const NO_GEOMETRY = {
|
|
10
10
|
occluded: false,
|
|
@@ -55,10 +55,10 @@ function fireAt(el, type, at, buttons, related, detail = 0, input = PLAIN_CLICK)
|
|
|
55
55
|
? pointerEventFor(el, type, { ...init, pointerId: 1, isPrimary: true })
|
|
56
56
|
: mouseEventFor(el, type, init));
|
|
57
57
|
}
|
|
58
|
-
export async function fireClickSequence(el, hold, detail, input = PLAIN_CLICK) {
|
|
58
|
+
export async function fireClickSequence(el, hold, detail, input = PLAIN_CLICK, point) {
|
|
59
59
|
const doc = el.ownerDocument;
|
|
60
60
|
const from = doc.activeElement ?? doc.body;
|
|
61
|
-
const at =
|
|
61
|
+
const at = pointOf(el, point);
|
|
62
62
|
const d = detail ?? 0;
|
|
63
63
|
const held = BUTTON_CODES[input.button].bit;
|
|
64
64
|
const isLeft = ClickButton.LEFT === input.button;
|
|
@@ -119,15 +119,36 @@ function hasNoSize(el) {
|
|
|
119
119
|
return false;
|
|
120
120
|
}
|
|
121
121
|
const rects = Array.from(el.getClientRects());
|
|
122
|
-
|
|
122
|
+
if (0 === rects.length) {
|
|
123
|
+
return isMeasurable(el.ownerDocument.documentElement.getBoundingClientRect());
|
|
124
|
+
}
|
|
125
|
+
return rects.every((r) => 0 === r.width || 0 === r.height);
|
|
126
|
+
}
|
|
127
|
+
function sizedLabelOf(el) {
|
|
128
|
+
const labels = 'labels' in el ? el.labels : null;
|
|
129
|
+
for (const label of labels ?? []) {
|
|
130
|
+
const box = label.getBoundingClientRect();
|
|
131
|
+
if (0 < box.width && 0 < box.height) {
|
|
132
|
+
return label;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
123
136
|
}
|
|
124
137
|
export function clickGeometry(el) {
|
|
125
138
|
if (typeof el.getBoundingClientRect !== 'function') {
|
|
126
139
|
return NO_GEOMETRY;
|
|
127
140
|
}
|
|
128
141
|
let rect = el.getBoundingClientRect();
|
|
142
|
+
const sizeless = isMeasurable(rect) ? 0 === rect.width || 0 === rect.height : hasNoSize(el);
|
|
143
|
+
if (sizeless) {
|
|
144
|
+
const label = sizedLabelOf(el);
|
|
145
|
+
if (label !== null) {
|
|
146
|
+
return clickGeometry(label);
|
|
147
|
+
}
|
|
148
|
+
return { ...NO_GEOMETRY, noSize: true };
|
|
149
|
+
}
|
|
129
150
|
if (!isMeasurable(rect)) {
|
|
130
|
-
return
|
|
151
|
+
return NO_GEOMETRY;
|
|
131
152
|
}
|
|
132
153
|
let scrolledIntoView = false;
|
|
133
154
|
if (isOffViewport(el, rect) && 'function' === typeof el.scrollIntoView) {
|
|
@@ -138,11 +159,30 @@ export function clickGeometry(el) {
|
|
|
138
159
|
const stillOff = scrolledIntoView && isOffViewport(el, rect);
|
|
139
160
|
return { ...hitTest(el, rect), scrolledIntoView, ...(stillOff ? { offViewport: true } : {}) };
|
|
140
161
|
}
|
|
141
|
-
export function firePointer(el, type, relatedTarget = null) {
|
|
142
|
-
asSyntheticInput(() => el.dispatchEvent(pointerEventFor(el, type, { bubbles: true, cancelable: true, relatedTarget })));
|
|
162
|
+
export function firePointer(el, type, relatedTarget = null, coords = {}) {
|
|
163
|
+
asSyntheticInput(() => el.dispatchEvent(pointerEventFor(el, type, { bubbles: true, cancelable: true, relatedTarget, ...coords })));
|
|
164
|
+
}
|
|
165
|
+
const hoveredIn = new WeakMap();
|
|
166
|
+
export function leavePreviousHover(next) {
|
|
167
|
+
const doc = next.ownerDocument;
|
|
168
|
+
const prev = hoveredIn.get(doc);
|
|
169
|
+
hoveredIn.set(doc, next);
|
|
170
|
+
if (prev === undefined || !prev.isConnected) {
|
|
171
|
+
return doc.activeElement ?? doc.body;
|
|
172
|
+
}
|
|
173
|
+
if (prev === next) {
|
|
174
|
+
return prev;
|
|
175
|
+
}
|
|
176
|
+
firePointer(prev, 'pointerout', next);
|
|
177
|
+
prev.dispatchEvent(mouseEventFor(prev, 'mouseout', { bubbles: true, relatedTarget: next }));
|
|
178
|
+
for (let el = prev; el !== null && !el.contains(next); el = el.parentElement) {
|
|
179
|
+
firePointerNonBubbling(el, 'pointerleave', next);
|
|
180
|
+
el.dispatchEvent(mouseEventFor(el, 'mouseleave', { relatedTarget: next }));
|
|
181
|
+
}
|
|
182
|
+
return prev;
|
|
143
183
|
}
|
|
144
|
-
export function firePointerNonBubbling(el, type, relatedTarget = null) {
|
|
145
|
-
asSyntheticInput(() => el.dispatchEvent(pointerEventFor(el, type, { bubbles: false, cancelable: true, relatedTarget })));
|
|
184
|
+
export function firePointerNonBubbling(el, type, relatedTarget = null, coords = {}) {
|
|
185
|
+
asSyntheticInput(() => el.dispatchEvent(pointerEventFor(el, type, { bubbles: false, cancelable: true, relatedTarget, ...coords })));
|
|
146
186
|
}
|
|
147
187
|
function makeDataTransfer(data) {
|
|
148
188
|
if (typeof DataTransfer !== 'function') {
|
|
@@ -165,18 +205,31 @@ function centreOf(el) {
|
|
|
165
205
|
const box = el.getBoundingClientRect();
|
|
166
206
|
return { x: box.left + box.width / 2, y: box.top + box.height / 2 };
|
|
167
207
|
}
|
|
208
|
+
function pointOf(el, at) {
|
|
209
|
+
if (at === undefined) {
|
|
210
|
+
return centreOf(el);
|
|
211
|
+
}
|
|
212
|
+
const box = el.getBoundingClientRect();
|
|
213
|
+
return pointInBox({ x: box.left, y: box.top, width: box.width, height: box.height }, at);
|
|
214
|
+
}
|
|
215
|
+
export function coordsAt(p) {
|
|
216
|
+
return { clientX: p.x, clientY: p.y, screenX: p.x, screenY: p.y };
|
|
217
|
+
}
|
|
218
|
+
export function coordsIn(el, at) {
|
|
219
|
+
return at === undefined ? {} : coordsAt(pointOf(el, at));
|
|
220
|
+
}
|
|
168
221
|
function lerp(a, b, t) {
|
|
169
222
|
return { x: Math.round(a.x + (b.x - a.x) * t), y: Math.round(a.y + (b.y - a.y) * t) };
|
|
170
223
|
}
|
|
171
|
-
export async function dragElement(source, target, data) {
|
|
224
|
+
export async function dragElement(source, target, data, points = {}, input = PLAIN_CLICK) {
|
|
172
225
|
const dest = target ?? source;
|
|
173
|
-
const from =
|
|
174
|
-
const to =
|
|
226
|
+
const from = pointOf(source, points.at);
|
|
227
|
+
const to = pointOf(dest, points.to);
|
|
175
228
|
const sourceRect = source.getBoundingClientRect();
|
|
176
229
|
const destRect = target !== null ? dest.getBoundingClientRect() : null;
|
|
177
230
|
const crosses = (rect, p) => p.x >= rect.left && p.x <= rect.right && p.y >= rect.top && p.y <= rect.bottom;
|
|
178
|
-
fireAt(source, 'pointerdown', from, BUTTON_HELD);
|
|
179
|
-
fireAt(source, 'mousedown', from, BUTTON_HELD);
|
|
231
|
+
fireAt(source, 'pointerdown', from, BUTTON_HELD, undefined, 0, input);
|
|
232
|
+
fireAt(source, 'mousedown', from, BUTTON_HELD, undefined, 0, input);
|
|
180
233
|
await nativeFrame();
|
|
181
234
|
let leftSource = false;
|
|
182
235
|
let enteredDest = false;
|
|
@@ -184,30 +237,30 @@ export async function dragElement(source, target, data) {
|
|
|
184
237
|
const at = lerp(from, to, step / DRAG_STEPS);
|
|
185
238
|
if (!leftSource && !crosses(sourceRect, at)) {
|
|
186
239
|
leftSource = true;
|
|
187
|
-
fireAt(source, 'pointerout', at, BUTTON_HELD, dest);
|
|
188
|
-
fireAt(source, 'mouseout', at, BUTTON_HELD, dest);
|
|
189
|
-
fireAt(source, 'pointerleave', at, BUTTON_HELD, dest);
|
|
190
|
-
fireAt(source, 'mouseleave', at, BUTTON_HELD, dest);
|
|
240
|
+
fireAt(source, 'pointerout', at, BUTTON_HELD, dest, 0, input);
|
|
241
|
+
fireAt(source, 'mouseout', at, BUTTON_HELD, dest, 0, input);
|
|
242
|
+
fireAt(source, 'pointerleave', at, BUTTON_HELD, dest, 0, input);
|
|
243
|
+
fireAt(source, 'mouseleave', at, BUTTON_HELD, dest, 0, input);
|
|
191
244
|
}
|
|
192
245
|
if (leftSource && !enteredDest && destRect !== null && crosses(destRect, at)) {
|
|
193
246
|
enteredDest = true;
|
|
194
|
-
fireAt(dest, 'pointerover', at, BUTTON_HELD, source);
|
|
195
|
-
fireAt(dest, 'mouseover', at, BUTTON_HELD, source);
|
|
196
|
-
fireAt(dest, 'pointerenter', at, BUTTON_HELD, source);
|
|
197
|
-
fireAt(dest, 'mouseenter', at, BUTTON_HELD, source);
|
|
247
|
+
fireAt(dest, 'pointerover', at, BUTTON_HELD, source, 0, input);
|
|
248
|
+
fireAt(dest, 'mouseover', at, BUTTON_HELD, source, 0, input);
|
|
249
|
+
fireAt(dest, 'pointerenter', at, BUTTON_HELD, source, 0, input);
|
|
250
|
+
fireAt(dest, 'mouseenter', at, BUTTON_HELD, source, 0, input);
|
|
198
251
|
}
|
|
199
|
-
fireAt(dest, 'pointermove', at, BUTTON_HELD);
|
|
200
|
-
fireAt(dest, 'mousemove', at, BUTTON_HELD);
|
|
252
|
+
fireAt(dest, 'pointermove', at, BUTTON_HELD, undefined, 0, input);
|
|
253
|
+
fireAt(dest, 'mousemove', at, BUTTON_HELD, undefined, 0, input);
|
|
201
254
|
await nativeFrame();
|
|
202
255
|
}
|
|
203
256
|
if (!enteredDest && destRect !== null) {
|
|
204
|
-
fireAt(dest, 'pointerover', to, BUTTON_HELD, source);
|
|
205
|
-
fireAt(dest, 'mouseover', to, BUTTON_HELD, source);
|
|
206
|
-
fireAt(dest, 'pointerenter', to, BUTTON_HELD, source);
|
|
207
|
-
fireAt(dest, 'mouseenter', to, BUTTON_HELD, source);
|
|
257
|
+
fireAt(dest, 'pointerover', to, BUTTON_HELD, source, 0, input);
|
|
258
|
+
fireAt(dest, 'mouseover', to, BUTTON_HELD, source, 0, input);
|
|
259
|
+
fireAt(dest, 'pointerenter', to, BUTTON_HELD, source, 0, input);
|
|
260
|
+
fireAt(dest, 'mouseenter', to, BUTTON_HELD, source, 0, input);
|
|
208
261
|
}
|
|
209
|
-
fireAt(dest, 'pointerup', to, BUTTON_RELEASED);
|
|
210
|
-
fireAt(dest, 'mouseup', to, BUTTON_RELEASED);
|
|
262
|
+
fireAt(dest, 'pointerup', to, BUTTON_RELEASED, undefined, 0, input);
|
|
263
|
+
fireAt(dest, 'mouseup', to, BUTTON_RELEASED, undefined, 0, input);
|
|
211
264
|
let dropPrevented = false;
|
|
212
265
|
if ('function' === typeof DragEvent) {
|
|
213
266
|
const dataTransfer = makeDataTransfer(data);
|
|
@@ -225,9 +278,10 @@ export async function dragElement(source, target, data) {
|
|
|
225
278
|
}
|
|
226
279
|
return dropPrevented;
|
|
227
280
|
}
|
|
228
|
-
export async function fireTapSequence(el, hold) {
|
|
229
|
-
const touches = touchListFor(el);
|
|
230
|
-
|
|
281
|
+
export async function fireTapSequence(el, hold, point) {
|
|
282
|
+
const touches = touchListFor(el, point);
|
|
283
|
+
const coords = coordsIn(el, point);
|
|
284
|
+
firePointerTouch(el, 'pointerdown', coords);
|
|
231
285
|
asSyntheticInput(() => el.dispatchEvent(makeTouchEvent('touchstart', touches)));
|
|
232
286
|
if (el.tabIndex >= 0 && 'function' === typeof el.focus) {
|
|
233
287
|
el.focus();
|
|
@@ -239,11 +293,11 @@ export async function fireTapSequence(el, hold) {
|
|
|
239
293
|
heldMs = hold.now() - started;
|
|
240
294
|
}
|
|
241
295
|
asSyntheticInput(() => el.dispatchEvent(makeTouchEvent('touchend', [])));
|
|
242
|
-
firePointerTouch(el, 'pointerup');
|
|
243
|
-
const clicked = asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'click', { bubbles: true, cancelable: true })));
|
|
296
|
+
firePointerTouch(el, 'pointerup', coords);
|
|
297
|
+
const clicked = asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'click', { bubbles: true, cancelable: true, ...coords })));
|
|
244
298
|
return { prevented: !clicked, heldMs };
|
|
245
299
|
}
|
|
246
|
-
function firePointerTouch(el, type) {
|
|
300
|
+
function firePointerTouch(el, type, coords) {
|
|
247
301
|
asSyntheticInput(() => el.dispatchEvent(pointerEventFor(el, type, {
|
|
248
302
|
bubbles: true,
|
|
249
303
|
cancelable: true,
|
|
@@ -251,22 +305,16 @@ function firePointerTouch(el, type) {
|
|
|
251
305
|
isPrimary: true,
|
|
252
306
|
width: 23,
|
|
253
307
|
height: 23,
|
|
308
|
+
...coords,
|
|
254
309
|
})));
|
|
255
310
|
}
|
|
256
|
-
function touchListFor(el) {
|
|
311
|
+
function touchListFor(el, point) {
|
|
257
312
|
if ('function' !== typeof Touch || 'function' !== typeof el.getBoundingClientRect) {
|
|
258
313
|
return [];
|
|
259
314
|
}
|
|
260
|
-
const
|
|
315
|
+
const at = pointOf(el, point);
|
|
261
316
|
try {
|
|
262
|
-
return [
|
|
263
|
-
new Touch({
|
|
264
|
-
identifier: 0,
|
|
265
|
-
target: el,
|
|
266
|
-
clientX: rect.left + rect.width / 2,
|
|
267
|
-
clientY: rect.top + rect.height / 2,
|
|
268
|
-
}),
|
|
269
|
-
];
|
|
317
|
+
return [new Touch({ identifier: 0, target: el, clientX: at.x, clientY: at.y })];
|
|
270
318
|
}
|
|
271
319
|
catch {
|
|
272
320
|
return [];
|
|
@@ -19,3 +19,13 @@ export declare function keyboardEvent(type: string, init: KeyboardEventInit & {
|
|
|
19
19
|
export declare function holdKey(el: ActionTarget, key: string, code: string, mods: ModifierFlags, ms: number): Promise<void>;
|
|
20
20
|
export declare function pressCombo(el: ActionTarget, keys: readonly string[], mods: ModifierFlags, holdMs: number): Promise<boolean>;
|
|
21
21
|
export declare function closeModalOnEscape(el: ActionTarget, key: string, keydownProceeded: boolean): void;
|
|
22
|
+
export declare const SPACE_KEY = " ";
|
|
23
|
+
interface Held {
|
|
24
|
+
metaKey: boolean;
|
|
25
|
+
ctrlKey: boolean;
|
|
26
|
+
altKey: boolean;
|
|
27
|
+
shiftKey: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare function runKeydownDefault(el: Element, key: string, held: Held): void;
|
|
30
|
+
export declare function runKeyupDefault(el: Element, key: string, held: Held): void;
|
|
31
|
+
export {};
|
|
@@ -7,7 +7,21 @@ function asString(value, fallback = '') {
|
|
|
7
7
|
return 'string' === typeof value ? value : fallback;
|
|
8
8
|
}
|
|
9
9
|
export function pressKey(args) {
|
|
10
|
-
return pressKeyFromArgs(args);
|
|
10
|
+
return splitChord(pressKeyFromArgs(args)).key;
|
|
11
|
+
}
|
|
12
|
+
function splitChord(raw) {
|
|
13
|
+
const at = raw.lastIndexOf('+', raw.length - 2);
|
|
14
|
+
const flags = 0 < at
|
|
15
|
+
? raw
|
|
16
|
+
.slice(0, at)
|
|
17
|
+
.split('+')
|
|
18
|
+
.map((name) => MODIFIER_FLAG_ALIASES[name.toLowerCase()])
|
|
19
|
+
: [];
|
|
20
|
+
if (0 === flags.length || flags.some((flag) => flag === undefined)) {
|
|
21
|
+
return { key: CODE_KEYS[raw] ?? raw, flags: [] };
|
|
22
|
+
}
|
|
23
|
+
const key = raw.slice(at + 1);
|
|
24
|
+
return { key: CODE_KEYS[key] ?? key, flags: flags.filter((flag) => flag !== undefined) };
|
|
11
25
|
}
|
|
12
26
|
const MODIFIER_FLAG_KEYS = [
|
|
13
27
|
'metaKey',
|
|
@@ -37,6 +51,9 @@ export function pressModifiers(args) {
|
|
|
37
51
|
if (flag !== undefined)
|
|
38
52
|
flags[flag] = true;
|
|
39
53
|
}
|
|
54
|
+
for (const flag of splitChord(pressKeyFromArgs(args)).flags) {
|
|
55
|
+
flags[flag] = true;
|
|
56
|
+
}
|
|
40
57
|
return flags;
|
|
41
58
|
}
|
|
42
59
|
function modifierFlagFor(key) {
|
|
@@ -53,7 +70,7 @@ export function pressCode(args, key) {
|
|
|
53
70
|
return `Key${key.toUpperCase()}`;
|
|
54
71
|
if (/[0-9]/.test(key))
|
|
55
72
|
return `Digit${key}`;
|
|
56
|
-
return '';
|
|
73
|
+
return PUNCTUATION_CODES[key] ?? '';
|
|
57
74
|
}
|
|
58
75
|
return /^[A-Z][A-Za-z0-9]*$/.test(key) && KNOWN_NAMED_KEYS.has(key) ? key : '';
|
|
59
76
|
}
|
|
@@ -74,6 +91,53 @@ export function dispatchKeypress(el, key, code, mods, repeat = false) {
|
|
|
74
91
|
}
|
|
75
92
|
asSyntheticInput(() => el.dispatchEvent(event));
|
|
76
93
|
}
|
|
94
|
+
const PUNCTUATION_CODES = {
|
|
95
|
+
'/': 'Slash',
|
|
96
|
+
'?': 'Slash',
|
|
97
|
+
'.': 'Period',
|
|
98
|
+
'>': 'Period',
|
|
99
|
+
',': 'Comma',
|
|
100
|
+
'<': 'Comma',
|
|
101
|
+
';': 'Semicolon',
|
|
102
|
+
':': 'Semicolon',
|
|
103
|
+
"'": 'Quote',
|
|
104
|
+
'"': 'Quote',
|
|
105
|
+
'[': 'BracketLeft',
|
|
106
|
+
'{': 'BracketLeft',
|
|
107
|
+
']': 'BracketRight',
|
|
108
|
+
'}': 'BracketRight',
|
|
109
|
+
'\\': 'Backslash',
|
|
110
|
+
'|': 'Backslash',
|
|
111
|
+
'-': 'Minus',
|
|
112
|
+
_: 'Minus',
|
|
113
|
+
'=': 'Equal',
|
|
114
|
+
'+': 'Equal',
|
|
115
|
+
'`': 'Backquote',
|
|
116
|
+
'~': 'Backquote',
|
|
117
|
+
'!': 'Digit1',
|
|
118
|
+
'@': 'Digit2',
|
|
119
|
+
'#': 'Digit3',
|
|
120
|
+
$: 'Digit4',
|
|
121
|
+
'%': 'Digit5',
|
|
122
|
+
'^': 'Digit6',
|
|
123
|
+
'&': 'Digit7',
|
|
124
|
+
'*': 'Digit8',
|
|
125
|
+
'(': 'Digit9',
|
|
126
|
+
')': 'Digit0',
|
|
127
|
+
};
|
|
128
|
+
const CODE_KEYS = {
|
|
129
|
+
Slash: '/',
|
|
130
|
+
Period: '.',
|
|
131
|
+
Comma: ',',
|
|
132
|
+
Semicolon: ';',
|
|
133
|
+
Quote: "'",
|
|
134
|
+
BracketLeft: '[',
|
|
135
|
+
BracketRight: ']',
|
|
136
|
+
Backslash: '\\',
|
|
137
|
+
Minus: '-',
|
|
138
|
+
Equal: '=',
|
|
139
|
+
Backquote: '`',
|
|
140
|
+
};
|
|
77
141
|
const ENTER_KEY = 'Enter';
|
|
78
142
|
const ENTER_CHAR_CODE = 13;
|
|
79
143
|
const LEGACY_KEY_CODE_FIELDS = ['keyCode', 'charCode', 'which'];
|
|
@@ -233,3 +297,83 @@ export function closeModalOnEscape(el, key, keydownProceeded) {
|
|
|
233
297
|
return;
|
|
234
298
|
}
|
|
235
299
|
}
|
|
300
|
+
export const SPACE_KEY = ' ';
|
|
301
|
+
const ENTER = 'Enter';
|
|
302
|
+
const TAB = 'Tab';
|
|
303
|
+
const ENTER_ACTIVATES = 'button, a[href], area[href], summary, input[type="submit"], input[type="button"], input[type="reset"], input[type="image"]';
|
|
304
|
+
const SPACE_ACTIVATES = 'button, summary, input[type="submit"], input[type="button"], input[type="reset"], input[type="image"], input[type="checkbox"], input[type="radio"]';
|
|
305
|
+
const IMPLICIT_SUBMIT_TYPES = new Set([
|
|
306
|
+
'text',
|
|
307
|
+
'search',
|
|
308
|
+
'url',
|
|
309
|
+
'tel',
|
|
310
|
+
'email',
|
|
311
|
+
'password',
|
|
312
|
+
'date',
|
|
313
|
+
'month',
|
|
314
|
+
'week',
|
|
315
|
+
'time',
|
|
316
|
+
'datetime-local',
|
|
317
|
+
'number',
|
|
318
|
+
]);
|
|
319
|
+
const FOCUSABLE = 'a[href], area[href], button, input, select, textarea, summary, iframe, [tabindex], [contenteditable=""], [contenteditable="true"]';
|
|
320
|
+
const shortcut = (held) => held.metaKey || held.ctrlKey || held.altKey;
|
|
321
|
+
export function runKeydownDefault(el, key, held) {
|
|
322
|
+
if (shortcut(held)) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (ENTER === key && !held.shiftKey) {
|
|
326
|
+
enterDefault(el);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (TAB === key) {
|
|
330
|
+
moveFocus(el, held.shiftKey ? -1 : 1);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
export function runKeyupDefault(el, key, held) {
|
|
334
|
+
if (SPACE_KEY !== key || shortcut(held) || held.shiftKey) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (el.matches(SPACE_ACTIVATES) && !isDisabled(el)) {
|
|
338
|
+
el.click();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function enterDefault(el) {
|
|
342
|
+
if (el.matches(ENTER_ACTIVATES)) {
|
|
343
|
+
if (!isDisabled(el)) {
|
|
344
|
+
el.click();
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const type = 'input' === el.localName ? (el.getAttribute('type') ?? 'text').toLowerCase() : '';
|
|
349
|
+
if (!IMPLICIT_SUBMIT_TYPES.has(type)) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const form = el.form;
|
|
353
|
+
form?.requestSubmit();
|
|
354
|
+
}
|
|
355
|
+
function isDisabled(el) {
|
|
356
|
+
return el.matches(':disabled');
|
|
357
|
+
}
|
|
358
|
+
function tabbable(el) {
|
|
359
|
+
if (isDisabled(el) || null !== el.closest('[inert], [hidden]')) {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
const html = el;
|
|
363
|
+
if (html.tabIndex < 0) {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
return 'hidden' !== el.type;
|
|
367
|
+
}
|
|
368
|
+
function moveFocus(from, step) {
|
|
369
|
+
const doc = from.ownerDocument;
|
|
370
|
+
const all = [...doc.querySelectorAll(FOCUSABLE)].filter(tabbable);
|
|
371
|
+
const positive = all.filter((el) => el.tabIndex > 0).sort((a, b) => a.tabIndex - b.tabIndex);
|
|
372
|
+
const order = [...positive, ...all.filter((el) => 0 === el.tabIndex)];
|
|
373
|
+
let at = order.indexOf(from);
|
|
374
|
+
if (at < 0) {
|
|
375
|
+
at = order.findIndex((el) => from.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING);
|
|
376
|
+
at = at < 0 ? order.length : at - (1 === step ? 1 : 0);
|
|
377
|
+
}
|
|
378
|
+
order[at + step]?.focus();
|
|
379
|
+
}
|