@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.
@@ -1,4 +1,4 @@
1
- import { OutwardAction, isDangerousActionText, isInertHref, isDangerousNavigation, isPaymentField, outwardActionOf, outwardRefusal, } from '@reticlehq/core';
1
+ import { OutwardAction, isDangerousActionText, isDangerousNavigation, isInertHref, isPaymentField, outwardActionOf, outwardRefusal, withoutFilenameTokens, } from '@reticlehq/core';
2
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([
@@ -8,34 +8,39 @@ const LABEL_VALUED_INPUT_TYPES = new Set([
8
8
  'image',
9
9
  ]);
10
10
  function holdsUserText(el) {
11
- if (isHtmlElement(el) && el.isContentEditable)
11
+ if (isHtmlElement(el) && el.isContentEditable) {
12
12
  return true;
13
- if (el instanceof HTMLTextAreaElement)
13
+ }
14
+ if (el instanceof HTMLTextAreaElement) {
14
15
  return true;
15
- if (el instanceof HTMLInputElement)
16
+ }
17
+ if (el instanceof HTMLInputElement) {
16
18
  return !LABEL_VALUED_INPUT_TYPES.has(el.type.toLowerCase());
19
+ }
17
20
  return false;
18
21
  }
19
22
  const ANCHOR_TAG = 'A';
20
23
  function navigatingHref(el) {
21
- if (ANCHOR_TAG !== el.tagName)
24
+ if (ANCHOR_TAG !== el.tagName) {
22
25
  return undefined;
26
+ }
23
27
  const href = el.getAttribute('href');
24
28
  return null === href || isInertHref(href) ? undefined : href;
25
29
  }
26
30
  export function dangerousActionContext(el) {
27
31
  const href = navigatingHref(el);
28
- if (href !== undefined)
32
+ if (href !== undefined) {
29
33
  return href;
34
+ }
30
35
  const form = el.closest('form');
31
36
  const ownText = holdsUserText(el)
32
37
  ? ['', '']
33
38
  : [el.textContent ?? '', el.getAttribute('value') ?? ''];
34
39
  return [
35
- getAccessibleName(el),
36
- ...ownText,
37
- el.getAttribute('title') ?? '',
38
- el.getAttribute('aria-label') ?? '',
40
+ withoutFilenameTokens(getAccessibleName(el)),
41
+ withoutFilenameTokens(ownText.join(' ')),
42
+ withoutFilenameTokens(el.getAttribute('title') ?? ''),
43
+ withoutFilenameTokens(el.getAttribute('aria-label') ?? ''),
39
44
  el.getAttribute('href') ?? '',
40
45
  form?.getAttribute('action') ?? '',
41
46
  ].join(' ');
@@ -47,8 +52,9 @@ export function submitControlFor(el) {
47
52
  }
48
53
  export function requiresDangerousConfirmation(el) {
49
54
  const href = navigatingHref(el);
50
- if (href !== undefined)
55
+ if (href !== undefined) {
51
56
  return isDangerousNavigation(href);
57
+ }
52
58
  return isDangerousActionText(dangerousActionContext(el), getRole(el));
53
59
  }
54
60
  let outwardAllowed;
@@ -56,19 +62,23 @@ export function setOutwardPolicy(allowed) {
56
62
  outwardAllowed = allowed;
57
63
  }
58
64
  export function assertOutwardAllowed(targets) {
59
- if (outwardAllowed === undefined)
65
+ if (outwardAllowed === undefined) {
60
66
  return;
67
+ }
61
68
  for (const el of targets) {
62
- if (null === el)
69
+ if (null === el) {
63
70
  continue;
71
+ }
64
72
  const kind = outwardActionOf(dangerousActionContext(el), getRole(el));
65
- if (kind !== undefined && !outwardAllowed.includes(kind))
73
+ if (kind !== undefined && !outwardAllowed.includes(kind)) {
66
74
  throw new Error(outwardRefusal(kind));
75
+ }
67
76
  }
68
77
  }
69
78
  export function assertPaymentEntryAllowed(el) {
70
- if (outwardAllowed === undefined || outwardAllowed.includes(OutwardAction.PAY))
79
+ if (outwardAllowed === undefined || outwardAllowed.includes(OutwardAction.PAY)) {
71
80
  return;
81
+ }
72
82
  const facts = {
73
83
  autocomplete: el.getAttribute('autocomplete') ?? undefined,
74
84
  name: el.getAttribute('name') ?? undefined,
@@ -76,6 +86,7 @@ export function assertPaymentEntryAllowed(el) {
76
86
  inputMode: el.getAttribute('inputmode') ?? undefined,
77
87
  label: `${getAccessibleName(el)} ${el.getAttribute('placeholder') ?? ''}`,
78
88
  };
79
- if (isPaymentField(facts))
89
+ if (isPaymentField(facts)) {
80
90
  throw new Error(outwardRefusal(OutwardAction.PAY));
91
+ }
81
92
  }
@@ -11,6 +11,8 @@ interface ScrollResult {
11
11
  atLeftEnd: boolean;
12
12
  atRightEnd: boolean;
13
13
  }
14
+ export declare const SCROLL_FRACTION_RANGE_ERROR_PREFIX = "scroll fraction must be between 0 and 1, got ";
15
+ export declare function fractionOutOfRangeMessage(fraction: number): string;
14
16
  export declare function scrollContainer(ref: string | undefined, dy: number | undefined, fraction?: number, dx?: number): ScrollResult;
15
17
  export declare function scrollFrom(base: Element | undefined, dy: number | undefined, fraction?: number, dx?: number): ScrollResult;
16
18
  export {};
@@ -1,6 +1,10 @@
1
1
  import { refs } from '../dom/addressing/refs.js';
2
2
  const FALLBACK_STEP_PX = 400;
3
3
  const VIEWPORT_FRACTION = 0.8;
4
+ export const SCROLL_FRACTION_RANGE_ERROR_PREFIX = 'scroll fraction must be between 0 and 1, got ';
5
+ export function fractionOutOfRangeMessage(fraction) {
6
+ return `${SCROLL_FRACTION_RANGE_ERROR_PREFIX}${fraction}`;
7
+ }
4
8
  function nearestScrollable(el, axis) {
5
9
  let cur = el;
6
10
  while (cur !== null && cur !== document.body) {
@@ -18,18 +22,22 @@ export function scrollContainer(ref, dy, fraction, dx) {
18
22
  return scrollFrom(base instanceof Element ? base : undefined, dy, fraction, dx);
19
23
  }
20
24
  export function scrollFrom(base, dy, fraction, dx) {
25
+ if (fraction !== undefined && (fraction < 0 || fraction > 1 || Number.isNaN(fraction))) {
26
+ throw new RangeError(fractionOutOfRangeMessage(fraction));
27
+ }
21
28
  const axis = dx !== undefined && dx !== 0 && (dy === undefined || 0 === dy) ? 'x' : 'y';
22
29
  const target = base !== undefined
23
30
  ? nearestScrollable(base, axis)
24
31
  : (document.scrollingElement ?? document.documentElement);
25
32
  const before = target.scrollTop;
26
33
  const beforeLeft = target.scrollLeft;
27
- if (fraction !== undefined && fraction >= 0 && fraction <= 1) {
34
+ if (fraction !== undefined) {
28
35
  target.scrollTop = Math.round(target.scrollHeight * fraction);
29
36
  }
30
37
  else {
31
- if (dx !== undefined && dx !== 0)
38
+ if (dx !== undefined && dx !== 0) {
32
39
  target.scrollLeft = beforeLeft + dx;
40
+ }
33
41
  if (dx === undefined || (dy !== undefined && 0 !== dy)) {
34
42
  const step = dy ?? (Math.round(target.clientHeight * VIEWPORT_FRACTION) || FALLBACK_STEP_PX);
35
43
  target.scrollTop = before + step;
@@ -0,0 +1,6 @@
1
+ import type { ActionTarget } from '../dom/realm.js';
2
+ type ClickFn = (el: ActionTarget) => Promise<{
3
+ prevented: boolean;
4
+ }>;
5
+ export declare function selectInCombobox(trigger: ActionTarget, wanted: string, click: ClickFn, noPopupMessage: string): Promise<boolean>;
6
+ export {};
@@ -0,0 +1,69 @@
1
+ import { echoRef } from '../dom/addressing/refs.js';
2
+ import { getAccessibleName } from '../dom/a11y.js';
3
+ import { isReticleUi } from '../dom/dom-ignore.js';
4
+ import { isVisible } from '../dom/visibility.js';
5
+ import { nativeFrame } from '../timers/native/native-timers.js';
6
+ const OPEN_WAIT_FRAMES = 30;
7
+ const ROLE_OPTION = 'option';
8
+ function optionName(option) {
9
+ return getAccessibleName(option).trim();
10
+ }
11
+ function visibleOptions(trigger) {
12
+ const doc = trigger.ownerDocument;
13
+ const ids = (trigger.getAttribute('aria-controls') ?? trigger.getAttribute('aria-owns') ?? '')
14
+ .split(/\s+/)
15
+ .filter((id) => 0 < id.length);
16
+ const popups = ids
17
+ .map((id) => doc.getElementById(id))
18
+ .filter((popup) => null !== popup);
19
+ const roots = 0 < popups.length ? popups : [doc];
20
+ return roots
21
+ .flatMap((root) => Array.from(root.querySelectorAll(`[role="${ROLE_OPTION}"]`)))
22
+ .filter((option) => !isReticleUi(option) && isVisible(option));
23
+ }
24
+ async function waitForOptions(trigger) {
25
+ for (let frame = 0; frame < OPEN_WAIT_FRAMES; frame += 1) {
26
+ const options = visibleOptions(trigger);
27
+ if (0 < options.length) {
28
+ return options;
29
+ }
30
+ await nativeFrame();
31
+ }
32
+ return [];
33
+ }
34
+ function matches(option, wanted) {
35
+ const asked = wanted.trim();
36
+ return (optionName(option) === asked ||
37
+ option.getAttribute('data-value') === wanted ||
38
+ option.getAttribute('value') === wanted);
39
+ }
40
+ export async function selectInCombobox(trigger, wanted, click, noPopupMessage) {
41
+ const openedHere = 'true' !== trigger.getAttribute('aria-expanded');
42
+ if (openedHere) {
43
+ await click(trigger);
44
+ }
45
+ const options = await waitForOptions(trigger);
46
+ const found = options.filter((option) => matches(option, wanted));
47
+ const [match] = found;
48
+ const problem = 0 === options.length ? noPopupMessage : problemWith(found, options, wanted);
49
+ if (match === undefined || problem !== undefined) {
50
+ if (openedHere) {
51
+ await click(trigger);
52
+ }
53
+ throw new Error(problem ?? 'no option');
54
+ }
55
+ return (await click(match)).prevented;
56
+ }
57
+ function problemWith(found, options, wanted) {
58
+ const [match] = found;
59
+ if (match === undefined) {
60
+ return `no option '${echoRef(wanted)}' — available: ${options.map(optionName).join(', ')}`;
61
+ }
62
+ if (1 < found.length) {
63
+ return `${found.length} options match '${echoRef(wanted)}' — name one by its exact label`;
64
+ }
65
+ if ('true' === match.getAttribute('aria-disabled')) {
66
+ return `cannot select a disabled option '${echoRef(wanted)}' — a real user could not, so neither will Reticle`;
67
+ }
68
+ return undefined;
69
+ }
@@ -4,8 +4,4 @@ export declare function getAccessibleName(el: Element): string;
4
4
  export declare function getStates(el: Element, visible?: boolean): ElementState[];
5
5
  export declare function isSensitiveField(el: Element): boolean;
6
6
  export declare function getValue(el: Element): string | undefined;
7
- export declare function isHiddenByAriaOnly(el: Element): boolean;
8
- export declare function isRenderedOutsideAriaHidden(el: Element): boolean;
9
- export declare function isInViewport(el: Element, memo?: Map<Element, boolean>): boolean;
10
- export declare function isVisible(el: Element, memo?: Map<Element, boolean>): boolean;
11
7
  export declare function describe(el: Element, memo?: Map<Element, boolean>): ElementDescriptor;
package/dist/dom/a11y.js CHANGED
@@ -1,15 +1,11 @@
1
1
  import { readTestId } from './addressing/testid-attr.js';
2
2
  import { DEFAULT_TESTID_ATTR, ElementState, REDACTED_VALUE, } from '@reticlehq/core';
3
- import { isButton, isHtmlElement, isImage, isInput, isMeter, isOutput, isProgress, isSelect, isTextArea, } from './realm.js';
3
+ import { isButton, isImage, isInput, isMeter, isOutput, isProgress, isSelect, isTextArea, } from './realm.js';
4
4
  import { refs } from './addressing/refs.js';
5
- import { parentAcrossShadowBoundary } from './shadow-registry.js';
6
- import { clippingBox, overflowAxes, scrolledInto, } from './paint-context.js';
7
5
  import { inspectChart } from './chart.js';
8
6
  import { isSensitiveKey } from '../security/serialization.js';
9
7
  import { formatSource, sourceFromDom } from './addressing/source.js';
10
- const HTML_DETAILS_TAG = 'details';
11
- const HTML_DETAILS_OPEN_ATTRIBUTE = 'open';
12
- const HTML_SUMMARY_TAG = 'summary';
8
+ import { HTML_DETAILS_OPEN_ATTRIBUTE, HTML_DETAILS_TAG, HTML_SUMMARY_TAG, isVisible, } from './visibility.js';
13
9
  const NAME_FROM_CONTENT = new Set([
14
10
  'button',
15
11
  'link',
@@ -364,294 +360,6 @@ export function getValue(el) {
364
360
  const valueNow = el.getAttribute('aria-valuenow');
365
361
  return valueNow ?? undefined;
366
362
  }
367
- function hiddenInsideClosedDetails(el) {
368
- const parent = el.parentElement;
369
- if (null === parent) {
370
- return false;
371
- }
372
- const details = parent.closest(HTML_DETAILS_TAG);
373
- if (null === details || details.hasAttribute(HTML_DETAILS_OPEN_ATTRIBUTE)) {
374
- return false;
375
- }
376
- const summary = Array.from(details.children).find((child) => HTML_SUMMARY_TAG === child.localName);
377
- return summary === undefined || !summary.contains(el);
378
- }
379
- function stalledFade(el) {
380
- if (!el.ownerDocument.hidden || !('getAnimations' in el)) {
381
- return false;
382
- }
383
- return el.getAnimations().some((a) => 'running' === a.playState || a.pending);
384
- }
385
- const OVERLAY_ARIA_HIDDEN = 'data-aria-hidden';
386
- function hiddenByAnOverlay(el) {
387
- return el.hasAttribute(OVERLAY_ARIA_HIDDEN);
388
- }
389
- function selfCssHidden(el, style) {
390
- if (isHtmlElement(el) && el.hidden) {
391
- return true;
392
- }
393
- if (hiddenInsideClosedDetails(el)) {
394
- return true;
395
- }
396
- if (style !== null) {
397
- if ('none' === style.display ||
398
- 'hidden' === style.visibility ||
399
- 'collapse' === style.visibility) {
400
- return true;
401
- }
402
- if (0 === Number.parseFloat(style.opacity || '1') && !stalledFade(el)) {
403
- return true;
404
- }
405
- }
406
- return false;
407
- }
408
- function selfHidden(el, style) {
409
- if ('true' === el.getAttribute('aria-hidden') && !hiddenByAnOverlay(el)) {
410
- return true;
411
- }
412
- return selfCssHidden(el, style);
413
- }
414
- export function isHiddenByAriaOnly(el) {
415
- let foundAriaHidden = false;
416
- let current = el;
417
- while (null !== current) {
418
- if ('true' === current.getAttribute('aria-hidden')) {
419
- foundAriaHidden = true;
420
- }
421
- const style = current.ownerDocument.defaultView?.getComputedStyle(current) ?? null;
422
- if (selfCssHidden(current, style)) {
423
- return false;
424
- }
425
- current = parentAcrossShadowBoundary(current);
426
- }
427
- return foundAriaHidden;
428
- }
429
- export function isRenderedOutsideAriaHidden(el) {
430
- let current = el;
431
- while (null !== current) {
432
- if ('true' === current.getAttribute('aria-hidden')) {
433
- return false;
434
- }
435
- const style = current.ownerDocument.defaultView?.getComputedStyle(current) ?? null;
436
- if (selfCssHidden(current, style)) {
437
- return false;
438
- }
439
- current = parentAcrossShadowBoundary(current);
440
- }
441
- return true;
442
- }
443
- const CLIPPING_OVERFLOW = new Set(['hidden', 'clip']);
444
- const SCROLLING_OVERFLOW = new Set(['auto', 'scroll']);
445
- const NON_CLIPPING_DISPLAY = new Set([
446
- 'contents',
447
- 'table-row',
448
- 'table-row-group',
449
- 'table-header-group',
450
- 'table-footer-group',
451
- 'table-column',
452
- 'table-column-group',
453
- ]);
454
- const TRANSFORM_CONTAINING_BLOCK_PROPERTIES = new Set([
455
- 'transform',
456
- 'translate',
457
- 'rotate',
458
- 'scale',
459
- 'perspective',
460
- ]);
461
- const CONTAINING_BLOCK_PROPERTIES = new Set([
462
- ...TRANSFORM_CONTAINING_BLOCK_PROPERTIES,
463
- 'filter',
464
- 'backdrop-filter',
465
- ]);
466
- const CONTAINING_BLOCK_CONTAIN = new Set(['layout', 'paint', 'strict', 'content']);
467
- const REPLACED_INLINE_TAGS = new Set([
468
- 'img',
469
- 'video',
470
- 'audio',
471
- 'canvas',
472
- 'iframe',
473
- 'embed',
474
- 'object',
475
- 'input',
476
- 'textarea',
477
- 'select',
478
- ]);
479
- const visibilityInfoMemos = new WeakMap();
480
- function visibilityInfoMemo(memo) {
481
- if (memo === undefined) {
482
- return new Map();
483
- }
484
- let info = visibilityInfoMemos.get(memo);
485
- if (info === undefined) {
486
- info = new Map();
487
- visibilityInfoMemos.set(memo, info);
488
- }
489
- return info;
490
- }
491
- function ownVisibilityInfo(el, memo) {
492
- let info = memo.get(el);
493
- if (info === undefined) {
494
- const style = el.ownerDocument.defaultView?.getComputedStyle(el) ?? null;
495
- if (selfHidden(el, style)) {
496
- info = {
497
- hidden: true,
498
- style,
499
- parent: null,
500
- clipX: false,
501
- clipY: false,
502
- scrollX: false,
503
- scrollY: false,
504
- hasClipping: false,
505
- };
506
- memo.set(el, info);
507
- return info;
508
- }
509
- const parent = parentAcrossShadowBoundary(el);
510
- const inherited = null === parent ? undefined : ownVisibilityInfo(parent, memo);
511
- const [overflowX, overflowY] = overflowAxes(style);
512
- const root = el.ownerDocument.documentElement;
513
- const viewportOverflow = el === root ||
514
- (el === el.ownerDocument.body &&
515
- overflowAxes(ownVisibilityInfo(root, memo).style).every((axis) => 'visible' === axis));
516
- const hasClipBox = !viewportOverflow &&
517
- !NON_CLIPPING_DISPLAY.has(style?.display ?? '') &&
518
- !(isHtmlElement(el) && 'inline' === style?.display);
519
- const clipX = hasClipBox && CLIPPING_OVERFLOW.has(overflowX);
520
- const clipY = hasClipBox && CLIPPING_OVERFLOW.has(overflowY);
521
- info = {
522
- hidden: true === inherited?.hidden,
523
- style,
524
- parent,
525
- clipX,
526
- clipY,
527
- scrollX: hasClipBox && SCROLLING_OVERFLOW.has(overflowX),
528
- scrollY: hasClipBox && SCROLLING_OVERFLOW.has(overflowY),
529
- hasClipping: clipX || clipY || true === inherited?.hasClipping,
530
- };
531
- memo.set(el, info);
532
- }
533
- return info;
534
- }
535
- function positionedContainingBlock(el, fixed, memo) {
536
- for (let parent = ownVisibilityInfo(el, memo).parent; parent !== null; parent = ownVisibilityInfo(parent, memo).parent) {
537
- const style = ownVisibilityInfo(parent, memo).style;
538
- if (null === style || 'contents' === style.display) {
539
- continue;
540
- }
541
- if (!fixed && '' !== style.position && 'static' !== style.position) {
542
- return parent;
543
- }
544
- const transformable = !('inline' === style.display &&
545
- isHtmlElement(parent) &&
546
- !REPLACED_INLINE_TAGS.has(parent.localName)) &&
547
- 'table-column' !== style.display &&
548
- 'table-column-group' !== style.display;
549
- const applies = (property) => transformable || !TRANSFORM_CONTAINING_BLOCK_PROPERTIES.has(property);
550
- if ([...CONTAINING_BLOCK_PROPERTIES].some((property) => {
551
- const value = style.getPropertyValue(property);
552
- return applies(property) && '' !== value && 'none' !== value;
553
- }) ||
554
- style.willChange
555
- .split(',')
556
- .some((property) => CONTAINING_BLOCK_PROPERTIES.has(property.trim()) && applies(property.trim())) ||
557
- (transformable &&
558
- !NON_CLIPPING_DISPLAY.has(style.display) &&
559
- (style.contain.split(/\s+/).some((value) => CONTAINING_BLOCK_CONTAIN.has(value)) ||
560
- 'auto' === style.contentVisibility))) {
561
- return parent;
562
- }
563
- }
564
- return null;
565
- }
566
- function visibleBounds(el, memo, initial, throughScrollers = false) {
567
- let bounds = initial;
568
- const target = ownVisibilityInfo(el, memo);
569
- if (target.hidden) {
570
- return null;
571
- }
572
- if (null === target.parent || !ownVisibilityInfo(target.parent, memo).hasClipping) {
573
- return bounds;
574
- }
575
- let clipFrom;
576
- for (let current = el; current !== null; current = ownVisibilityInfo(current, memo).parent) {
577
- const info = ownVisibilityInfo(current, memo);
578
- if (info.hidden) {
579
- return null;
580
- }
581
- if (current === clipFrom) {
582
- clipFrom = undefined;
583
- }
584
- const style = info.style;
585
- if (throughScrollers &&
586
- current !== el &&
587
- undefined === clipFrom &&
588
- (info.scrollX || info.scrollY)) {
589
- bounds ??= el.getBoundingClientRect();
590
- if (bounds.width > 0 && bounds.height > 0) {
591
- bounds = scrolledInto(bounds, clippingBox(current, info), info.scrollX, info.scrollY);
592
- }
593
- }
594
- if (current !== el && undefined === clipFrom && (info.clipX || info.clipY)) {
595
- bounds ??= el.getBoundingClientRect();
596
- if (bounds.width > 0 && bounds.height > 0) {
597
- const clip = clippingBox(current, info);
598
- const left = info.clipX ? Math.max(bounds.left, clip.left) : bounds.left;
599
- const right = info.clipX ? Math.min(bounds.right, clip.right) : bounds.right;
600
- const top = info.clipY ? Math.max(bounds.top, clip.top) : bounds.top;
601
- const bottom = info.clipY ? Math.min(bounds.bottom, clip.bottom) : bounds.bottom;
602
- if (right <= left || bottom <= top) {
603
- return null;
604
- }
605
- bounds = { left, right, top, bottom, width: right - left, height: bottom - top };
606
- }
607
- }
608
- if (clipFrom === undefined && ('absolute' === style?.position || 'fixed' === style?.position)) {
609
- clipFrom = positionedContainingBlock(current, 'fixed' === style?.position, memo);
610
- }
611
- }
612
- return bounds;
613
- }
614
- export function isInViewport(el, memo) {
615
- if (!isVisible(el, memo)) {
616
- return false;
617
- }
618
- const view = el.ownerDocument.defaultView;
619
- if (null === view) {
620
- return false;
621
- }
622
- const r = visibleBounds(el, visibilityInfoMemo(memo), el.getBoundingClientRect());
623
- if (null === r || undefined === r) {
624
- return false;
625
- }
626
- if (r.width <= 0 || r.height <= 0) {
627
- return false;
628
- }
629
- return r.bottom > 0 && r.right > 0 && r.top < view.innerHeight && r.left < view.innerWidth;
630
- }
631
- function collapsed(el, memo) {
632
- const own = ownVisibilityInfo(el, memo);
633
- if (!own.clipX && !own.clipY) {
634
- return false;
635
- }
636
- const rects = Array.from(el.getClientRects());
637
- return (rects.length > 0 &&
638
- rects.every((r) => (own.clipX && 0 === r.width) || (own.clipY && 0 === r.height)));
639
- }
640
- export function isVisible(el, memo) {
641
- if (!el.isConnected) {
642
- return false;
643
- }
644
- const cached = memo?.get(el);
645
- if (cached !== undefined) {
646
- return cached;
647
- }
648
- const info = visibilityInfoMemo(memo);
649
- const result = visibleBounds(el, info, undefined, true) !== null && !collapsed(el, info);
650
- if (memo !== undefined) {
651
- memo.set(el, result);
652
- }
653
- return result;
654
- }
655
363
  const MAX_TEXT = 80;
656
364
  function getVisibleText(el) {
657
365
  const text = collapse(el.textContent ?? '');
@@ -1,5 +1,6 @@
1
1
  import { RETICLE_OVERLAY_SELECTOR } from '@reticlehq/core';
2
2
  import { capturedRootOf } from './shadow-registry.js';
3
+ import { isVisible } from './visibility.js';
3
4
  export const RETICLE_OVERLAY = RETICLE_OVERLAY_SELECTOR;
4
5
  const DEV_OVERLAYS = '[data-agentation],#__next-build-watcher,nextjs-portal,[data-nextjs-dialog],[data-nextjs-toast]';
5
6
  let extraIgnore = '';
@@ -23,8 +24,9 @@ export function isReticleOverlay(el) {
23
24
  }
24
25
  const HUD_DOCK = '[data-reticle-dock]';
25
26
  export function isUnobserved(el) {
26
- if (!isReticleOverlay(el))
27
+ if (!isReticleOverlay(el)) {
27
28
  return false;
29
+ }
28
30
  return !exposed() || null === el.closest(HUD_DOCK);
29
31
  }
30
32
  export function isReticleUi(node) {
@@ -48,30 +50,38 @@ const NEXT_DIALOG = '[data-nextjs-dialog]';
48
50
  const VITE_OVERLAY = 'vite-error-overlay';
49
51
  function openFrameworkErrorDialog(doc) {
50
52
  const vite = doc.querySelector(VITE_OVERLAY);
51
- if (vite !== null)
53
+ if (vite !== null) {
52
54
  return { name: FrameworkErrorDialog.VITE, host: vite };
55
+ }
53
56
  for (const portal of Array.from(doc.querySelectorAll(NEXT_PORTAL))) {
54
57
  const root = portal.shadowRoot ?? capturedRootOf(portal);
55
- if (null !== (root ?? portal).querySelector(NEXT_DIALOG)) {
58
+ const dialog = (root ?? portal).querySelector(NEXT_DIALOG);
59
+ if (null !== dialog && isVisible(dialog)) {
56
60
  return { name: FrameworkErrorDialog.NEXT, host: portal };
57
61
  }
58
62
  }
59
63
  const light = doc.querySelector(NEXT_DIALOG);
60
- return null === light ? undefined : { name: FrameworkErrorDialog.NEXT, host: light };
64
+ if (null === light || !isVisible(light)) {
65
+ return undefined;
66
+ }
67
+ return { name: FrameworkErrorDialog.NEXT, host: light };
61
68
  }
62
69
  export function dialogCovering(matches) {
63
70
  const first = matches[0];
64
- if (first === undefined)
71
+ if (first === undefined) {
65
72
  return undefined;
73
+ }
66
74
  const dialog = openFrameworkErrorDialog(first.ownerDocument);
67
- if (dialog === undefined)
75
+ if (dialog === undefined) {
68
76
  return undefined;
77
+ }
69
78
  return matches.some((el) => !isInsideHost(el, dialog.host)) ? dialog.name : undefined;
70
79
  }
71
80
  function isInsideHost(el, host) {
72
81
  for (let node = el; node !== null;) {
73
- if (node === host)
82
+ if (node === host) {
74
83
  return true;
84
+ }
75
85
  const root = node.getRootNode();
76
86
  node = node.parentNode ?? (root instanceof ShadowRoot ? root.host : null);
77
87
  }
@@ -80,12 +90,15 @@ function isInsideHost(el, host) {
80
90
  const APP_MODAL = 'dialog[open], [aria-modal="true"]';
81
91
  export function appModalOpen(doc) {
82
92
  for (const el of Array.from(doc.querySelectorAll(APP_MODAL))) {
83
- if (el.closest(RETICLE_OVERLAY) !== null)
93
+ if (el.closest(RETICLE_OVERLAY) !== null) {
84
94
  continue;
85
- if (el instanceof HTMLElement && (el.hidden || null !== el.closest('[hidden]')))
95
+ }
96
+ if (el instanceof HTMLElement && (el.hidden || null !== el.closest('[hidden]'))) {
86
97
  continue;
87
- if (el instanceof HTMLElement && 'none' === getComputedStyle(el).display)
98
+ }
99
+ if (el instanceof HTMLElement && 'none' === getComputedStyle(el).display) {
88
100
  continue;
101
+ }
89
102
  return true;
90
103
  }
91
104
  return false;
@@ -0,0 +1,6 @@
1
+ import { type ElementQuery } from '@reticlehq/core';
2
+ export declare function elementsUnder(container: HTMLElement): HTMLElement[];
3
+ export declare function isNonRendered(el: Element): boolean;
4
+ export declare function isHiddenNativeOption(el: HTMLElement): boolean;
5
+ export declare function queryByRoleAndName(container: HTMLElement, role: string, name: string | undefined): HTMLElement[];
6
+ export declare function findIn(container: HTMLElement, query: ElementQuery): HTMLElement[];