@reticlehq/browser 3.8.0 → 3.8.2

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.
@@ -0,0 +1,216 @@
1
+ import { DATA_RETICLE_SOURCE_ATTR, QueryBy } from '@reticlehq/core';
2
+ import { isInput, isSelect, isTextArea } from './realm.js';
3
+ import { getAccessibleName, getRole } from './a11y.js';
4
+ import { isVisible } from './visibility.js';
5
+ import { identifyComponent } from '../registry/stores/adapters.js';
6
+ import { readTestId, testIdSelector } from './addressing/testid-attr.js';
7
+ import { exactVisibleText, fuzzyVisibleText, comparableText, paintedCase, textMatches, } from './text-match.js';
8
+ const SOURCE_ATTR = DATA_RETICLE_SOURCE_ATTR;
9
+ const MAX_COMPONENT_CANDIDATES = 2000;
10
+ const componentCandidateSelector = () => `[${SOURCE_ATTR}], ${testIdSelector()}, button, a, input, select, textarea, [role]`;
11
+ export function elementsUnder(container) {
12
+ const self = Node.ELEMENT_NODE === container.nodeType ? [container] : [];
13
+ return [...self, ...Array.from(container.querySelectorAll('*'))];
14
+ }
15
+ const NON_RENDERED_TAGS = ['script', 'style', 'template', 'noscript'];
16
+ export function isNonRendered(el) {
17
+ return NON_RENDERED_TAGS.includes(el.tagName.toLowerCase());
18
+ }
19
+ function directText(el) {
20
+ if (isInput(el)) {
21
+ const type = el.type.toLowerCase();
22
+ if ('submit' === type || 'button' === type || 'reset' === type) {
23
+ return el.value;
24
+ }
25
+ }
26
+ if (isNonRendered(el)) {
27
+ return '';
28
+ }
29
+ return Array.from(el.childNodes)
30
+ .filter((node) => Node.TEXT_NODE === node.nodeType)
31
+ .map((node) => node.textContent ?? '')
32
+ .join('');
33
+ }
34
+ function semanticNameTarget(el) {
35
+ if (isInput(el) || isTextArea(el) || isSelect(el)) {
36
+ return true;
37
+ }
38
+ const tag = el.tagName.toLowerCase();
39
+ return ('button' === tag ||
40
+ 'meter' === tag ||
41
+ 'output' === tag ||
42
+ 'progress' === tag ||
43
+ el.hasAttribute('aria-label') ||
44
+ el.hasAttribute('aria-labelledby'));
45
+ }
46
+ function findBySource(container, source) {
47
+ const prefix = `${source.file}:${source.line}:`.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
48
+ try {
49
+ return Array.from(container.querySelectorAll(`[${SOURCE_ATTR}^="${prefix}"]`));
50
+ }
51
+ catch {
52
+ return [];
53
+ }
54
+ }
55
+ function findByComponentName(container, component) {
56
+ const out = [];
57
+ let scanned = 0;
58
+ for (const el of Array.from(container.querySelectorAll(componentCandidateSelector()))) {
59
+ if (scanned >= MAX_COMPONENT_CANDIDATES) {
60
+ break;
61
+ }
62
+ scanned += 1;
63
+ const info = identifyComponent(el);
64
+ if (info !== null && info.componentStack[0] === component) {
65
+ out.push(el);
66
+ }
67
+ }
68
+ return out;
69
+ }
70
+ function findByComponent(container, query) {
71
+ if (query.source !== undefined) {
72
+ const bySource = findBySource(container, query.source);
73
+ if (bySource.length > 0) {
74
+ return bySource;
75
+ }
76
+ }
77
+ if (query.component !== undefined && query.component.length > 0) {
78
+ return findByComponentName(container, query.component);
79
+ }
80
+ return [];
81
+ }
82
+ function matchesRole(actual, queried) {
83
+ if (actual === queried) {
84
+ return true;
85
+ }
86
+ if ('textbox' === queried && 'searchbox' === actual) {
87
+ return true;
88
+ }
89
+ return false;
90
+ }
91
+ const OPTION_ROLE = 'option';
92
+ const HTML_OPTION_TAG = 'option';
93
+ const HTML_SELECT_TAG = 'select';
94
+ const VISUALLY_HIDDEN_MAX_PX = 1;
95
+ export function isHiddenNativeOption(el) {
96
+ if (HTML_OPTION_TAG !== el.localName) {
97
+ return false;
98
+ }
99
+ const select = el.closest(HTML_SELECT_TAG);
100
+ if (!isSelect(select)) {
101
+ return false;
102
+ }
103
+ if (!isVisible(select)) {
104
+ return true;
105
+ }
106
+ return isVisuallyHiddenBox(select);
107
+ }
108
+ function isVisuallyHiddenBox(select) {
109
+ const box = select.getBoundingClientRect();
110
+ return (box.width > 0 &&
111
+ box.height > 0 &&
112
+ box.width <= VISUALLY_HIDDEN_MAX_PX &&
113
+ box.height <= VISUALLY_HIDDEN_MAX_PX);
114
+ }
115
+ function preferVisibleActionableOption(role, matches) {
116
+ if (OPTION_ROLE !== role) {
117
+ return matches;
118
+ }
119
+ const kept = matches.filter((el) => !isHiddenNativeOption(el));
120
+ return kept.length === matches.length ? matches : kept;
121
+ }
122
+ export function queryByRoleAndName(container, role, name) {
123
+ const matched = elementsUnder(container).filter((el) => matchesRole(getRole(el), role) &&
124
+ (name === undefined || exactVisibleText(getAccessibleName(el), name)));
125
+ return preferVisibleActionableOption(role, matched);
126
+ }
127
+ function queryByText(container, value, query) {
128
+ return elementsUnder(container).filter((el) => {
129
+ const own = directText(el);
130
+ if (textMatches(own, query, value)) {
131
+ return true;
132
+ }
133
+ if (true !== query.matchCase || !fuzzyVisibleText(own, value)) {
134
+ return false;
135
+ }
136
+ const painted = paintedCase(el, own);
137
+ return painted !== undefined && textMatches(painted, query, value);
138
+ });
139
+ }
140
+ function queryByLabel(container, value) {
141
+ const expected = comparableText(value).toLowerCase();
142
+ const exact = [];
143
+ const fuzzy = [];
144
+ for (const el of elementsUnder(container)) {
145
+ if (!semanticNameTarget(el)) {
146
+ continue;
147
+ }
148
+ const name = comparableText(getAccessibleName(el)).toLowerCase();
149
+ if (!name.includes(expected)) {
150
+ continue;
151
+ }
152
+ (name === expected ? exact : fuzzy).push(el);
153
+ }
154
+ return exact.length > 0 ? exact : fuzzy;
155
+ }
156
+ function queryByPlaceholder(container, value) {
157
+ return elementsUnder(container).filter((el) => {
158
+ const placeholder = el.getAttribute('placeholder');
159
+ return placeholder !== null && fuzzyVisibleText(placeholder, value);
160
+ });
161
+ }
162
+ function queryByTestId(container, value) {
163
+ return elementsUnder(container).filter((el) => readTestId(el) === value);
164
+ }
165
+ function queryByAlt(container, value) {
166
+ return elementsUnder(container).filter((el) => {
167
+ const alt = el.getAttribute('alt');
168
+ return alt !== null && fuzzyVisibleText(alt, value);
169
+ });
170
+ }
171
+ export function findIn(container, query) {
172
+ const by = query.by;
173
+ const value = query.value;
174
+ if (by !== undefined && value !== undefined) {
175
+ switch (by) {
176
+ case QueryBy.ROLE:
177
+ return queryByRoleAndName(container, value, query.name);
178
+ case QueryBy.TEXT:
179
+ return queryByText(container, value, query);
180
+ case QueryBy.LABEL:
181
+ return queryByLabel(container, value);
182
+ case QueryBy.PLACEHOLDER:
183
+ return queryByPlaceholder(container, value);
184
+ case QueryBy.TESTID:
185
+ return queryByTestId(container, value);
186
+ case QueryBy.ALT:
187
+ return queryByAlt(container, value);
188
+ case QueryBy.COMPONENT:
189
+ return findByComponent(container, { ...query, component: query.component ?? value });
190
+ default:
191
+ throw new Error(`unsupported query strategy '${String(by)}' - use one of: ${Object.values(QueryBy).join(', ')}`);
192
+ }
193
+ }
194
+ if (query.component !== undefined || query.source !== undefined) {
195
+ return findByComponent(container, query);
196
+ }
197
+ if (query.role !== undefined) {
198
+ return queryByRoleAndName(container, query.role, query.name);
199
+ }
200
+ if (query.text !== undefined) {
201
+ return queryByText(container, query.text, query);
202
+ }
203
+ if (query.label !== undefined) {
204
+ return queryByLabel(container, query.label);
205
+ }
206
+ if (query.placeholder !== undefined) {
207
+ return queryByPlaceholder(container, query.placeholder);
208
+ }
209
+ if (query.testid !== undefined) {
210
+ return queryByTestId(container, query.testid);
211
+ }
212
+ if (query.alt !== undefined) {
213
+ return queryByAlt(container, query.alt);
214
+ }
215
+ return [];
216
+ }
@@ -1,5 +1,4 @@
1
1
  import { ElementState, type ElementQuery, type MatchResult, type QueryResult } from '@reticlehq/core';
2
- export declare function isHiddenNativeOption(el: HTMLElement): boolean;
3
2
  export declare const ATTR_VALUE_MAX = 512;
4
3
  export declare function matchQuery(query: ElementQuery, state?: ElementState, limit?: number, wholePhrase?: boolean, diagnose?: boolean): MatchResult;
5
4
  export declare function elementsMatching(query: ElementQuery): HTMLElement[];