interestfor 1.0.12 → 1.0.14

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/README.md CHANGED
@@ -140,7 +140,7 @@ browsers:
140
140
 
141
141
 
142
142
  ## Implementation
143
- The polyfill adds event listeners to document.body that monitor for hover
143
+ The polyfill adds event listeners to the document that monitor for hover
144
144
  or keyboard events relevant for the `interestfor` API. When interesting
145
145
  events happen to elements with the `interestfor` attribute or elements that
146
146
  are the targets of those elements, it fires `interest` and `loseinterest`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "interestfor",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "A polyfill for the `interestfor` API",
5
5
  "keywords": "interestfor, interesttarget, polyfill, hover-triggering",
6
6
  "homepage": "https://github.com/mfreed7/interestfor#readme",
@@ -52,10 +52,10 @@
52
52
  e.stopImmediatePropagation();
53
53
  }
54
54
  };
55
- document.body.addEventListener(interestEventName, cancel, {
55
+ document.addEventListener(interestEventName, cancel, {
56
56
  capture: true,
57
57
  });
58
- document.body.addEventListener(loseInterestEventName, cancel, {
58
+ document.addEventListener(loseInterestEventName, cancel, {
59
59
  capture: true,
60
60
  });
61
61
  }
@@ -414,9 +414,7 @@
414
414
  invokersWithInterest.add(invoker);
415
415
  invoker.classList.add("interest-source");
416
416
  target.classList.add("interest-target");
417
- if (!isPlainHint(target)) {
418
- invoker.setAttribute("aria-expanded", "true");
419
- }
417
+ syncAriaExpanded(invoker);
420
418
  if (getComputedStyle(invoker).anchorName === 'none' &&
421
419
  getComputedStyle(target).positionAnchor === 'auto') {
422
420
  const anchorName = `--interest-anchor-${Math.random()
@@ -459,6 +457,7 @@
459
457
  resetInterestState(invoker);
460
458
  if (shouldHide && isPopoverOpen(target)) {
461
459
  hideTargetPopover(invoker, target);
460
+ syncAriaExpanded(invoker);
462
461
  }
463
462
  return true;
464
463
  }
@@ -486,9 +485,6 @@
486
485
  target[targetDataField] = null;
487
486
  target.classList.remove("interest-target");
488
487
  }
489
- if (!isPlainHint(target)) {
490
- invoker.setAttribute("aria-expanded", "false");
491
- }
492
488
  }
493
489
  if (data.anchorName) {
494
490
  invoker.style.anchorName = "";
@@ -499,6 +495,7 @@
499
495
  }
500
496
  data.state = InterestState.NoInterest;
501
497
  data.target = null;
498
+ syncAriaExpanded(invoker);
502
499
  }
503
500
 
504
501
  function hideTargetPopover(invoker, target) {
@@ -534,10 +531,9 @@
534
531
  ].join(",");
535
532
 
536
533
  // Accessibility utilities
534
+ // Plain vs. rich hints are distinguished by the contents of the target,
535
+ // regardless of the popover type.
537
536
  function isPlainHint(target) {
538
- if (target.getAttribute("popover")?.toLowerCase() !== "hint") {
539
- return false;
540
- }
541
537
  // Anything focusable is not plain.
542
538
  if (target.querySelector(focusableSelector)) {
543
539
  return false;
@@ -553,7 +549,16 @@
553
549
  for (const el of elementsWithRoles) {
554
550
  const role = el.getAttribute("role").toLowerCase();
555
551
  // Allowed roles are those that don't add semantics for a screen reader to explore.
556
- if (!["presentation", "none", "generic", "image"].includes(role)) {
552
+ if (
553
+ ![
554
+ "presentation",
555
+ "none",
556
+ "generic",
557
+ "image",
558
+ "tooltip",
559
+ "paragraph",
560
+ ].includes(role)
561
+ ) {
557
562
  return false;
558
563
  }
559
564
  }
@@ -638,13 +643,56 @@
638
643
  return normalizeText(el.getAttribute("title"));
639
644
  }
640
645
 
646
+ function idrefListIncludes(el, attr, id) {
647
+ const value = el.getAttribute(attr) || "";
648
+ return value.trim().split(/\s+/).includes(id);
649
+ }
650
+
651
+ // Returns true if `invoker` references `target` via `popovertarget` or
652
+ // `commandfor`, i.e. activating the invoker (also) toggles the target.
653
+ function activationTogglesTarget(invoker, target) {
654
+ const refersTo = (attr, prop) => {
655
+ if (prop in invoker) {
656
+ return invoker[prop] === target;
657
+ }
658
+ return !!target.id && invoker.getAttribute(attr) === target.id;
659
+ };
660
+ return (
661
+ refersTo("popovertarget", "popoverTargetElement") ||
662
+ refersTo("commandfor", "commandForElement")
663
+ );
664
+ }
665
+
666
+ function isLink(el) {
667
+ return (
668
+ el instanceof HTMLAnchorElement ||
669
+ el instanceof HTMLAreaElement ||
670
+ (!!window.SVGAElement && el instanceof SVGAElement)
671
+ );
672
+ }
673
+
674
+ // Sets `aria-expanded` on the invoker, if it is managed by the polyfill. For
675
+ // popover targets, this reflects whether the popover is open (which might
676
+ // be because of something other than interest, e.g. `popovertarget`). For
677
+ // other targets, it reflects whether the invoker has interest.
678
+ function syncAriaExpanded(invoker) {
679
+ const data = invoker[dataField];
680
+ if (!data || !data.managesAriaExpanded || !data.a11yTarget) {
681
+ return;
682
+ }
683
+ const target = data.a11yTarget;
684
+ const expanded = target.hasAttribute("popover")
685
+ ? isPopoverOpen(target)
686
+ : data.state !== InterestState.NoInterest && data.target === target;
687
+ invoker.setAttribute("aria-expanded", expanded ? "true" : "false");
688
+ }
689
+
641
690
  function setupAccessibility(invoker, target) {
642
691
  if (!target.id) {
643
692
  return;
644
693
  }
645
694
  if (isPlainHint(target)) {
646
- const labelledBy = invoker.getAttribute("aria-labelledby") || "";
647
- if (labelledBy.trim().split(/\s+/).includes(target.id)) {
695
+ if (idrefListIncludes(invoker, "aria-labelledby", target.id)) {
648
696
  return;
649
697
  }
650
698
  const name = getAccessibleName(invoker);
@@ -656,16 +704,36 @@
656
704
  } else if (
657
705
  hintText &&
658
706
  hintText !== name &&
659
- !invoker.hasAttribute("aria-describedby")
707
+ !invoker.hasAttribute("aria-describedby") &&
708
+ // For toggletips, where activating the invoker reveals the hint,
709
+ // describing the invoker with the hint would read the hint twice.
710
+ !invoker.hasAttribute("aria-expanded") &&
711
+ !activationTogglesTarget(invoker, target)
660
712
  ) {
661
713
  invoker.setAttribute("aria-describedby", target.id);
662
714
  }
663
715
  } else {
664
716
  // Rich hint
665
- if (!invoker.hasAttribute("aria-details")) {
717
+ const data = invoker[dataField];
718
+ // If the author already related the target to the invoker, don't add
719
+ // more relationships.
720
+ const authorRelated =
721
+ idrefListIncludes(invoker, "aria-describedby", target.id) ||
722
+ idrefListIncludes(invoker, "aria-labelledby", target.id);
723
+ if (!authorRelated && !invoker.hasAttribute("aria-details")) {
666
724
  invoker.setAttribute("aria-details", target.id);
667
725
  }
668
- invoker.setAttribute("aria-expanded", "false");
726
+ // Don't overwrite an author-provided `aria-expanded`. Also, activating
727
+ // a link navigates rather than expanding the hint, so only links whose
728
+ // hint contains focusable content get `aria-expanded`.
729
+ data.managesAriaExpanded =
730
+ !authorRelated &&
731
+ !invoker.hasAttribute("aria-expanded") &&
732
+ (!isLink(invoker) || !!target.querySelector(focusableSelector));
733
+ if (data.managesAriaExpanded) {
734
+ target.addEventListener("toggle", data.onTargetToggle);
735
+ syncAriaExpanded(invoker);
736
+ }
669
737
  if (!target.hasAttribute("role")) {
670
738
  target.setAttribute("role", "tooltip");
671
739
  }
@@ -678,6 +746,8 @@
678
746
  state: InterestState.NoInterest,
679
747
  target: null,
680
748
  a11yTarget: null,
749
+ managesAriaExpanded: false,
750
+ onTargetToggle: () => syncAriaExpanded(el),
681
751
  gainedTimer: null,
682
752
  lostTimer: null,
683
753
  longPressTimer: null,
@@ -690,9 +760,16 @@
690
760
  },
691
761
  };
692
762
  }
763
+ const data = el[dataField];
693
764
  const target = GetInterestForTarget(el);
694
- if (target && el[dataField].a11yTarget !== target) {
695
- el[dataField].a11yTarget = target;
765
+ if (target && data.a11yTarget !== target) {
766
+ if (data.managesAriaExpanded) {
767
+ // Stop managing `aria-expanded` for the previous target.
768
+ data.a11yTarget?.removeEventListener("toggle", data.onTargetToggle);
769
+ el.removeAttribute("aria-expanded");
770
+ data.managesAriaExpanded = false;
771
+ }
772
+ data.a11yTarget = target;
696
773
  setupAccessibility(el, target);
697
774
  }
698
775
  }
@@ -989,13 +1066,13 @@
989
1066
  touchInProgress = false;
990
1067
  }
991
1068
  };
992
- document.body.addEventListener("pointerover", clearTouch);
993
- document.body.addEventListener("pointerdown", clearTouch);
994
- document.body.addEventListener("mouseover", (e) => handler(e, Source.Hover));
995
- document.body.addEventListener("mouseout", (e) => handler(e, Source.DeHover));
996
- document.body.addEventListener("focusin", (e) => handler(e, Source.Focus));
997
- document.body.addEventListener("focusout", (e) => handler(e, Source.Blur));
998
- document.body.addEventListener("keydown", (e) => {
1069
+ document.addEventListener("pointerover", clearTouch);
1070
+ document.addEventListener("pointerdown", clearTouch);
1071
+ document.addEventListener("mouseover", (e) => handler(e, Source.Hover));
1072
+ document.addEventListener("mouseout", (e) => handler(e, Source.DeHover));
1073
+ document.addEventListener("focusin", (e) => handler(e, Source.Focus));
1074
+ document.addEventListener("focusout", (e) => handler(e, Source.Blur));
1075
+ document.addEventListener("keydown", (e) => {
999
1076
  touchInProgress = false;
1000
1077
  if (e.key === "Escape" && !e.defaultPrevented) {
1001
1078
  // Lose interest in all invokers, most recent first. These
@@ -1010,31 +1087,46 @@
1010
1087
  });
1011
1088
  }
1012
1089
  });
1090
+
1091
+ // WebKit (iOS) long-press workarounds:
1092
+ // 1. On Android, holding for ~500ms fires `pointercancel` and `touchcancel`,
1093
+ // so the popover can open immediately when `longPressTimer` fires while
1094
+ // the finger is still down. On iOS WebKit, holding a button with
1095
+ // `user-select: none` does not cancel the touch stream; when the finger
1096
+ // is later lifted, iOS fires `pointerup` (which triggers WebKit's native
1097
+ // popover light-dismiss because the popover was not open at
1098
+ // `pointerdown`) followed by `touchend` and synthesized mouse/`click`
1099
+ // events. On iOS only, record that the 500ms hold succeeded
1100
+ // (`longPressActive`), and on `touchend` (which fires after `pointerup`),
1101
+ // call `preventDefault()` to suppress the synthesized `click` and open
1102
+ // the popover then.
1103
+ // 2. Unlike Android (which applies a native touch slop before firing
1104
+ // `touchmove`), iOS WebKit dispatches `touchmove` on tiny finger jitter
1105
+ // during a stationary hold. On iOS only, ignore `touchmove` events within
1106
+ // 10px of `touchstart` (matching UIKit's default `allowableMovement` on
1107
+ // `UILongPressGestureRecognizer`).
1108
+ const longPressMoveSlop = 10;
1109
+ const isIOS = () => /iPad|iPhone|iPod/.test(navigator.userAgent);
1110
+
1111
+
1013
1112
  // Touch support
1014
1113
  const longPressTime = 500;
1015
- // WebKit (iOS) long-press workaround:
1016
- // On Android, holding for ~500ms fires `pointercancel` and `touchcancel`,
1017
- // so the popover can open immediately when `longPressTimer` fires while the
1018
- // finger is still down. On iOS WebKit, holding a button with
1019
- // `user-select: none` does not cancel the touch stream; when the finger is
1020
- // later lifted, iOS fires `pointerup` (which triggers WebKit's native
1021
- // popover light-dismiss because the popover was not open at `pointerdown`)
1022
- // followed by `touchend` and synthesized mouse/`click` events.
1023
- // On iOS only, record that the 500ms hold succeeded (`longPressActive`),
1024
- // and on `touchend` (which fires after `pointerup`), call `preventDefault()`
1025
- // to suppress the synthesized `click` and open the popover then.
1026
- const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
1027
- document.body.addEventListener("touchstart", (e) => {
1114
+ document.addEventListener("touchstart", (e) => {
1028
1115
  touchInProgress = true;
1029
1116
  const invoker = e.target.closest("button[interestfor]");
1030
1117
  if (invoker) {
1031
1118
  initializeDataField(invoker);
1119
+ const ios = isIOS();
1120
+ if (ios && e.touches?.length === 1) {
1121
+ invoker[dataField].touchStartX = e.touches[0].clientX;
1122
+ invoker[dataField].touchStartY = e.touches[0].clientY;
1123
+ }
1032
1124
  invoker[dataField].longPressActive = false;
1033
1125
  invoker[dataField].longPressTimer = setTimeout(() => {
1034
1126
  // Long-press shows interest immediately, and that interest is not
1035
1127
  // lost by de-hovering or blurring the invoker.
1036
1128
  if (invoker[dataField].state === InterestState.NoInterest) {
1037
- if (isIOS) {
1129
+ if (ios) {
1038
1130
  invoker[dataField].longPressActive = true;
1039
1131
  } else {
1040
1132
  GainOrLoseInterest(
@@ -1051,13 +1143,20 @@
1051
1143
  const cancelLongPress = (e) => {
1052
1144
  const invoker = e.target.closest("button[interestfor]");
1053
1145
  if (invoker && invoker[dataField]) {
1146
+ if (isIOS() && e.type === "touchmove" && e.touches?.length === 1) {
1147
+ const dx = e.touches[0].clientX - invoker[dataField].touchStartX;
1148
+ const dy = e.touches[0].clientY - invoker[dataField].touchStartY;
1149
+ if (Math.hypot(dx, dy) <= longPressMoveSlop) {
1150
+ return;
1151
+ }
1152
+ }
1054
1153
  clearTimeout(invoker[dataField].longPressTimer);
1055
1154
  invoker[dataField].longPressTimer = null;
1056
1155
  invoker[dataField].longPressActive = false;
1057
1156
  }
1058
1157
  };
1059
- document.body.addEventListener("touchmove", cancelLongPress);
1060
- document.body.addEventListener(
1158
+ document.addEventListener("touchmove", cancelLongPress);
1159
+ document.addEventListener(
1061
1160
  "touchend",
1062
1161
  (e) => {
1063
1162
  const invoker = e.target.closest("button[interestfor]");
@@ -5,4 +5,4 @@
5
5
  // LICENSE file in the root directory of this source tree.
6
6
 
7
7
 
8
- !function(){const e="interestfor",t="interest",n="loseinterest",r="--interest-delay-start",o="--interest-delay-end",s="--interest-delay",i="--interest-button-content",a="--interest-button-polyfill-content",l="interest-button",c="__interestForData",u="__interestForTargetData",d="__interestForButton",f="__interestForButtonInvoker",m="__interestForExplicitTarget",p="http://www.w3.org/1999/xlink",h=new Set,b=new Set,g=new Set;let y=!1;if(window.interestForPolyfillInstalled)return;window.interestForPolyfillInstalled=!0;const E=HTMLButtonElement.prototype.hasOwnProperty("interestForElement");if(E&&!window.interestForUsePolyfillAlways)return;if(E){const e=e=>{e.isTrusted&&(e.preventDefault(),e.stopImmediatePropagation())};document.body.addEventListener(t,e,{capture:!0}),document.body.addEventListener(n,e,{capture:!0})}window.InterestEvent&&!window.interestForUsePolyfillAlways||(window.InterestEvent=class extends Event{#e;constructor(e,t={}){super(e,t);const{source:n}=t;if(null!=n&&!(n instanceof Element))throw new TypeError("source must be an element");this.#e=n}get[Symbol.toStringTag](){return"InterestEvent"}get source(){const e=this.#e;if(!e)return null;const t=P(e);return t!==P(this.target||document)?t.host:e}});const w="none",v="full",T="explicit",A="hover",L="dehover",N="focus",x="blur";function P(e){return e&&"function"==typeof e.getRootNode?e.getRootNode():e&&e.parentNode?P(e.parentNode):e}function k(e,r,o,s={}){if(!e||!r)return!1;if(!e.isConnected||I(e)!==r||o===w&&F(r)!==e)return!1;if(o!==w){z(e),e[c].state!==w&&e[c].target!==r&&D(e);const n=F(r);if(n){if(n===e)return o===T&&(n[c].state=T),n[c].clearLostTask(),!1;if(!k(n,r,w))return!1;if(!e.isConnected||I(e)!==r)return!1}return function(e,n,r){const o=e[c];switch(r){case v:case T:if(o.state!==w)throw new Error("Invalid state");if(!n.dispatchEvent(new InterestEvent(t,{source:e,cancelable:!0})))return!1;if(o.state=r,o.target=n,o.clearGainedTask(),n[u]||(n[u]={}),n[u].invoker=e,n.hasAttribute("popover")&&(n.addEventListener("beforetoggle",_),n[u].shownByInterest=!1,!$(n))){try{n.showPopover({source:e})}catch{}n[u].shownByInterest=$(n)}if(h.add(e),e.classList.add("interest-source"),n.classList.add("interest-target"),H(n)||e.setAttribute("aria-expanded","true"),"none"===getComputedStyle(e).anchorName&&"auto"===getComputedStyle(n).positionAnchor){const t=`--interest-anchor-${Math.random().toString(36).substring(2)}`;e.style.anchorName=t,n.style.positionAnchor=t,o.anchorName=t}break;default:throw new Error("Invalid state")}return!0}(e,r,o)}return function(e,{force:t=!1,closePopovers:r=!0}={}){const o=e[c];if(clearTimeout(o.gainedTimer),clearTimeout(o.lostTimer),o.state===w)return!0;const s=o.target,i=s.dispatchEvent(new InterestEvent(n,{source:e,cancelable:!t}));if(!i)return!1;const a=s[u],l=r&&a?.invoker===e&&a.shownByInterest;D(e),l&&$(s)&&function(e,t){const n=e[c],r=t.contains(document.activeElement);r&&(n.suppressNextFocusInterest=!0);try{t.hidePopover()}catch{}r&&(n.suppressNextFocusInterest=!1)}(e,s);return!0}(e,s)}function S(e){const t=1e3*O(e,o);if(!M(t))return;const n=e[c];n.clearLostTask();const r=n.target;n.lostTimer=setTimeout(()=>{k(e,r,w)},t)}function F(e){const t=e[u]?.invoker||null;if(!t)return null;const n=t[c];return n&&n.state!==w&&n.target===e&&I(t)===e?t:null}function I(t){if(!t.hasAttribute(e)||!t.isConnected||!function(e){if(e instanceof HTMLButtonElement)return!e.matches(":disabled");if(e instanceof HTMLAnchorElement||e instanceof HTMLAreaElement)return e.hasAttribute("href");if(window.SVGAElement&&e instanceof SVGAElement)return e.hasAttribute("href")||e.hasAttributeNS(p,"href");return!1}(t))return null;const n=C(t);return n&&n.isConnected?n:null}function C(t){const n=t.getAttribute(e);if(null===n)return null;const r=t[m];if(r&&""===n)return function(e,t){const n=P(e);let r=P(t);for(;r;){if(r===n)return!0;r=r instanceof ShadowRoot?P(r.host):null}return!1}(r,t)?r:null;if(""===n)return null;const o=P(t);return"function"==typeof o.getElementById?o.getElementById(n):null}function $(e){try{return e.matches(":popover-open")}catch{return!1}}function _(e){if("closed"!==e.newState)return;const t=e.currentTarget,n=F(t);if(n){if(t.contains(document.activeElement)){const e=n[c];e.suppressNextFocusInterest=!0,setTimeout(()=>{e.suppressNextFocusInterest=!1},0)}k(n,t,w,{closePopovers:!1})}}function M(e){return e>=0&&e<=2147483647}function B(e){const t=String(e).trim(),n=t.match(/^([\d.]+(?:e[+-]?\d+)?)s$/i);if(n)return parseFloat(n[1]);const r=t.match(/^([\d.]+(?:e[+-]?\d+)?)ms$/i);return r?parseFloat(r[1])/1e3:/\b(infinity|nan)\b/i.test(t)?1/0:parseFloat(t)||0}function O(e,t){const n=getComputedStyle(e),o=n.getPropertyValue(t).trim();if("normal"!==o.toLowerCase())return B(o);const i=n.getPropertyValue(s).trim();if(i&&"normal"!==i.toLowerCase()){const e=i.split(/\s+/).filter(e=>e.length>0);if(e.length>0){const n=e[0],o=e.length>1?e[1]:n;let s;if(s=t===r?n:o,"normal"!==s.toLowerCase())return B(s)}}return t===r?.25:.15}function D(e){const t=e[c];if(!t)return;if(clearTimeout(t.gainedTimer),clearTimeout(t.lostTimer),t.state===w)return;const n=t.target;h.delete(e),e.classList.remove("interest-source"),n&&(n[u]?.invoker===e&&(n.removeEventListener("beforetoggle",_),n[u]=null,n.classList.remove("interest-target")),H(n)||e.setAttribute("aria-expanded","false")),t.anchorName&&(e.style.anchorName="",n&&(n.style.positionAnchor=""),t.anchorName=null),t.state=w,t.target=null}const V=["a[href]","area[href]","input:not([disabled])","select:not([disabled])","textarea:not([disabled])","button:not([disabled])","iframe","object","embed","[contenteditable]",'[tabindex]:not([tabindex="-1"])'].join(",");function H(e){if("hint"!==e.getAttribute("popover")?.toLowerCase())return!1;if(e.querySelector(V))return!1;if(e.querySelector("h1,h2,h3,h4,h5,h6,ul,ol,li,table,nav,header,footer,main,aside,article,section,form,blockquote,details,summary,dialog"))return!1;const t=e.querySelectorAll("[role]");for(const e of t){const t=e.getAttribute("role").toLowerCase();if(!["presentation","none","generic","image"].includes(t))return!1}return!0}function G(e){return e?e.replace(/\s+/g," ").trim():""}function j(e,t=!1){if(e.nodeType===Node.TEXT_NODE)return e.data;if(e.nodeType!==Node.ELEMENT_NODE)return"";if(!t&&(e.hasAttribute("hidden")||"true"===e.getAttribute("aria-hidden")||"script"===e.localName||"style"===e.localName))return"";const n=e.getAttribute("aria-label");if(n)return n;if("img"===e.localName||"area"===e.localName)return e.getAttribute("alt")||"";let r="";for(const t of e.childNodes)r+=j(t);return r}function q(e,t){if(t.id)if(H(t)){if((e.getAttribute("aria-labelledby")||"").trim().split(/\s+/).includes(t.id))return;const n=function(e){const t=e.getAttribute("aria-labelledby");if(t){const n=P(e);if("function"==typeof n.getElementById){const e=[];for(const r of t.trim().split(/\s+/)){const t=r?n.getElementById(r):null;t&&e.push(G(j(t,!0)))}const r=G(e.join(" "));if(r)return r}}const n=G(e.getAttribute("aria-label"));if(n)return n;if(e.labels?.length){const t=G([...e.labels].map(e=>j(e,!0)).join(" "));if(t)return t}return G(j(e,!0))||G(e.getAttribute("title"))}(e),r=G(j(t,!0));n?r&&r!==n&&!e.hasAttribute("aria-describedby")&&e.setAttribute("aria-describedby",t.id):r&&e.setAttribute("aria-labelledby",t.id)}else e.hasAttribute("aria-details")||e.setAttribute("aria-details",t.id),e.setAttribute("aria-expanded","false"),t.hasAttribute("role")||t.setAttribute("role","tooltip")}function z(e){e[c]||(e[c]={state:w,target:null,a11yTarget:null,gainedTimer:null,lostTimer:null,longPressTimer:null,suppressNextFocusInterest:!1,clearGainedTask(){clearTimeout(this.gainedTimer)},clearLostTask(){clearTimeout(this.lostTimer)}});const t=I(e);t&&e[c].a11yTarget!==t&&(e[c].a11yTarget=t,q(e,t))}function R(e){if(e[f])return e[f];if(e.assignedSlot)return e.assignedSlot;if(e.parentElement)return e.parentElement;const t=e.parentNode;return t instanceof ShadowRoot?t.host:null}function U(e){const t=new Set;if(0===h.size)return t;const n=e=>{const r=F(e);r&&!t.has(r)&&(t.add(r),n(r));const o=R(e);o&&!t.has(o)&&n(o)};return n(e),t}function X(e,t){if(y)return;if(!e.isConnected)return;const n=I(e);n&&z(e);const o=e[c];if(t===A||t===N){let s=!1;o&&(o.clearLostTask(),s=t===N&&o.suppressNextFocusInterest,o.suppressNextFocusInterest=!1);for(const t of U(e))t[c].clearLostTask();n&&!s&&o.state===w&&function(e,t){const n=I(e);if(!n)return;const o=1e3*O(e,r);M(o)&&(e[c].clearGainedTask(),e[c].gainedTimer=setTimeout(()=>{k(e,n,t)},o))}(e,v)}else if(o&&(o.clearGainedTask(),o.state===v&&S(e)),t===x||!e.matches(":hover"))for(const t of U(e))S(t)}function J(e){return e instanceof HTMLElement&&"area"!==e.localName&&!!e.parentNode&&!!I(e)}function K(e){const t=getComputedStyle(e).getPropertyValue(i).trim();return t&&"none"!==t&&"normal"!==t?t:null}function Q(){const e=this[f];e&&k(e,I(e),T)}function W(e,t){let n=e[d];t?(n||(n=e.ownerDocument.createElement("span"),n.className=l,n.setAttribute("aria-hidden","true"),n[f]=e,n.addEventListener("click",Q),e[d]=n,b.add(e)),n.style.getPropertyValue(a)!==t&&n.style.setProperty(a,t),e.nextSibling!==n&&e.after(n)):n&&(n.remove(),e[d]=null,b.delete(e))}function Y(t){const n=[];for(const r of t){let t=null;const o=I(r);o?(g.delete(r),J(r)&&(t=K(r))):r.isConnected&&r.hasAttribute(e)?g.add(r):g.delete(r),n.push([r,o,t])}for(const[e,t,r]of n)t&&z(e),W(e,r)}function Z(t){const n=new Set;let r=!1;const o=t=>{if(t.nodeType===Node.ELEMENT_NODE&&!t[f]){t.hasAttribute(e)&&n.add(t);for(const r of t.querySelectorAll(`[${e}]`))n.add(r)}};for(const s of t){if("childList"===s.type){for(const e of s.addedNodes)e.nodeType!==Node.ELEMENT_NODE||e[f]||(r=!0,o(e));continue}const t=s.target;switch(s.attributeName){case e:null!==s.oldValue&&D(t),""!==t.getAttribute(e)&&(t[m]=null),n.add(t);break;case"id":r=!0;break;default:o(t)}}for(const e of[...h]){const t=e[c].target;e.isConnected&&t&&t.isConnected||D(e)}if(r)for(const e of g)n.add(e);for(const e of[...b])n.has(e)||(J(e)?e.nextSibling!==e[d]&&e.after(e[d]):W(e,null));Y(n)}function ee(){!function(){const t=document.createElement("style");t.textContent=`@property ${r} {syntax: "normal | <time>"; inherits: false; initial-value: normal;}\n @property ${o} {syntax: "normal | <time>"; inherits: false; initial-value: normal;}\n @property ${s} {syntax: "*"; inherits: false;}\n @property ${i} {syntax: "*"; inherits: false;}`,document.head.appendChild(t);const n=`span.${l}[aria-hidden]`,u=document.createElement("style");u.textContent=`:where(button[${e}]) {\n -webkit-user-select: none;\n user-select: none;\n }\n :where(${n}) {\n display: inline-block;\n vertical-align: middle;\n color: inherit;\n background-color: transparent;\n box-sizing: border-box;\n min-inline-size: 24px;\n min-block-size: 24px;\n min-block-size: max(24px, 1lh);\n align-content: center;\n text-align: center;\n -webkit-user-select: none;\n user-select: none;\n cursor: help;\n }\n :where(${n}:hover) {\n background-color: color-mix(in oklab, currentColor 10%, transparent);\n }\n :where(${n}:active) {\n background-color: color-mix(in oklab, currentColor 20%, transparent);\n }\n :where(${n})::before {\n content: var(${a});\n }`,document.head.prepend(u),document[c]={globalPropsStyle:t,uaStyle:u}}(),function(){const e=(e,t)=>{const n=e.composedPath(),r=n.length?n[0]:e.target;if(r instanceof Element)for(let e=r;e;e=R(e))X(e,t)},t=e=>{"touch"!==e.pointerType&&(y=!1)};document.body.addEventListener("pointerover",t),document.body.addEventListener("pointerdown",t),document.body.addEventListener("mouseover",t=>e(t,A)),document.body.addEventListener("mouseout",t=>e(t,L)),document.body.addEventListener("focusin",t=>e(t,N)),document.body.addEventListener("focusout",t=>e(t,x)),document.body.addEventListener("keydown",e=>{y=!1,"Escape"!==e.key||e.defaultPrevented||[...h].reverse().forEach(e=>{k(e,e[c].target,w,{force:!0})})});const n=/iPad|iPhone|iPod/.test(navigator.userAgent);document.body.addEventListener("touchstart",e=>{y=!0;const t=e.target.closest("button[interestfor]");t&&(z(t),t[c].longPressActive=!1,t[c].longPressTimer=setTimeout(()=>{t[c].state===w&&(n?t[c].longPressActive=!0:k(t,I(t),T)),t[c].longPressTimer=null},500))});const r=e=>{const t=e.target.closest("button[interestfor]");t&&t[c]&&(clearTimeout(t[c].longPressTimer),t[c].longPressTimer=null,t[c].longPressActive=!1)};document.body.addEventListener("touchmove",r),document.body.addEventListener("touchend",e=>{const t=e.target.closest("button[interestfor]"),n=t?.[c]?.longPressActive;r(e),n&&(e.cancelable&&e.preventDefault(),t[c].state===w&&k(t,I(t),T))},{passive:!1})}(),new MutationObserver(Z).observe(document,{childList:!0,subtree:!0,attributes:!0,attributeOldValue:!0,attributeFilter:[e,"id","href","disabled"]}),Y(document.querySelectorAll(`[${e}]`)),console.log(`interestfor polyfill installed (native: ${E}).`)}!function(){const t={configurable:!0,enumerable:!0,get(){return C(this)},set(t){if(null==t)this[m]=null,this.removeAttribute(e);else{if(!(t instanceof Element))throw new TypeError("Failed to set the 'interestForElement' property: The provided value is not of type 'Element'.");this[m]=t,this.setAttribute(e,"")}}};[window.HTMLButtonElement,window.HTMLAnchorElement,window.HTMLAreaElement,window.SVGAElement].forEach(e=>{!e||Object.prototype.hasOwnProperty.call(e.prototype,"interestForElement")&&!window.interestForUsePolyfillAlways||Object.defineProperty(e.prototype,"interestForElement",t)})}(),"complete"===document.readyState?ee():window.addEventListener("load",ee)}();
8
+ !function(){const e="interestfor",t="interest",n="loseinterest",r="--interest-delay-start",o="--interest-delay-end",s="--interest-delay",i="--interest-button-content",a="--interest-button-polyfill-content",l="interest-button",c="__interestForData",u="__interestForTargetData",d="__interestForButton",f="__interestForButtonInvoker",m="__interestForExplicitTarget",p="http://www.w3.org/1999/xlink",h=new Set,g=new Set,b=new Set;let y=!1;if(window.interestForPolyfillInstalled)return;window.interestForPolyfillInstalled=!0;const E=HTMLButtonElement.prototype.hasOwnProperty("interestForElement");if(E&&!window.interestForUsePolyfillAlways)return;if(E){const e=e=>{e.isTrusted&&(e.preventDefault(),e.stopImmediatePropagation())};document.addEventListener(t,e,{capture:!0}),document.addEventListener(n,e,{capture:!0})}window.InterestEvent&&!window.interestForUsePolyfillAlways||(window.InterestEvent=class extends Event{#e;constructor(e,t={}){super(e,t);const{source:n}=t;if(null!=n&&!(n instanceof Element))throw new TypeError("source must be an element");this.#e=n}get[Symbol.toStringTag](){return"InterestEvent"}get source(){const e=this.#e;if(!e)return null;const t=S(e);return t!==S(this.target||document)?t.host:e}});const v="none",w="full",T="explicit",A="hover",L="dehover",x="focus",N="blur";function S(e){return e&&"function"==typeof e.getRootNode?e.getRootNode():e&&e.parentNode?S(e.parentNode):e}function P(e,r,o,s={}){if(!e||!r)return!1;if(!e.isConnected||I(e)!==r||o===v&&F(r)!==e)return!1;if(o!==v){X(e),e[c].state!==v&&e[c].target!==r&&H(e);const n=F(r);if(n){if(n===e)return o===T&&(n[c].state=T),n[c].clearLostTask(),!1;if(!P(n,r,v))return!1;if(!e.isConnected||I(e)!==r)return!1}return function(e,n,r){const o=e[c];switch(r){case w:case T:if(o.state!==v)throw new Error("Invalid state");if(!n.dispatchEvent(new InterestEvent(t,{source:e,cancelable:!0})))return!1;if(o.state=r,o.target=n,o.clearGainedTask(),n[u]||(n[u]={}),n[u].invoker=e,n.hasAttribute("popover")&&(n.addEventListener("beforetoggle",M),n[u].shownByInterest=!1,!$(n))){try{n.showPopover({source:e})}catch{}n[u].shownByInterest=$(n)}if(h.add(e),e.classList.add("interest-source"),n.classList.add("interest-target"),j(e),"none"===getComputedStyle(e).anchorName&&"auto"===getComputedStyle(n).positionAnchor){const t=`--interest-anchor-${Math.random().toString(36).substring(2)}`;e.style.anchorName=t,n.style.positionAnchor=t,o.anchorName=t}break;default:throw new Error("Invalid state")}return!0}(e,r,o)}return function(e,{force:t=!1,closePopovers:r=!0}={}){const o=e[c];if(clearTimeout(o.gainedTimer),clearTimeout(o.lostTimer),o.state===v)return!0;const s=o.target,i=s.dispatchEvent(new InterestEvent(n,{source:e,cancelable:!t}));if(!i)return!1;const a=s[u],l=r&&a?.invoker===e&&a.shownByInterest;H(e),l&&$(s)&&(!function(e,t){const n=e[c],r=t.contains(document.activeElement);r&&(n.suppressNextFocusInterest=!0);try{t.hidePopover()}catch{}r&&(n.suppressNextFocusInterest=!1)}(e,s),j(e));return!0}(e,s)}function k(e){const t=1e3*V(e,o);if(!_(t))return;const n=e[c];n.clearLostTask();const r=n.target;n.lostTimer=setTimeout(()=>{P(e,r,v)},t)}function F(e){const t=e[u]?.invoker||null;if(!t)return null;const n=t[c];return n&&n.state!==v&&n.target===e&&I(t)===e?t:null}function I(t){if(!t.hasAttribute(e)||!t.isConnected||!function(e){if(e instanceof HTMLButtonElement)return!e.matches(":disabled");if(e instanceof HTMLAnchorElement||e instanceof HTMLAreaElement)return e.hasAttribute("href");if(window.SVGAElement&&e instanceof SVGAElement)return e.hasAttribute("href")||e.hasAttributeNS(p,"href");return!1}(t))return null;const n=C(t);return n&&n.isConnected?n:null}function C(t){const n=t.getAttribute(e);if(null===n)return null;const r=t[m];if(r&&""===n)return function(e,t){const n=S(e);let r=S(t);for(;r;){if(r===n)return!0;r=r instanceof ShadowRoot?S(r.host):null}return!1}(r,t)?r:null;if(""===n)return null;const o=S(t);return"function"==typeof o.getElementById?o.getElementById(n):null}function $(e){try{return e.matches(":popover-open")}catch{return!1}}function M(e){if("closed"!==e.newState)return;const t=e.currentTarget,n=F(t);if(n){if(t.contains(document.activeElement)){const e=n[c];e.suppressNextFocusInterest=!0,setTimeout(()=>{e.suppressNextFocusInterest=!1},0)}P(n,t,v,{closePopovers:!1})}}function _(e){return e>=0&&e<=2147483647}function B(e){const t=String(e).trim(),n=t.match(/^([\d.]+(?:e[+-]?\d+)?)s$/i);if(n)return parseFloat(n[1]);const r=t.match(/^([\d.]+(?:e[+-]?\d+)?)ms$/i);return r?parseFloat(r[1])/1e3:/\b(infinity|nan)\b/i.test(t)?1/0:parseFloat(t)||0}function V(e,t){const n=getComputedStyle(e),o=n.getPropertyValue(t).trim();if("normal"!==o.toLowerCase())return B(o);const i=n.getPropertyValue(s).trim();if(i&&"normal"!==i.toLowerCase()){const e=i.split(/\s+/).filter(e=>e.length>0);if(e.length>0){const n=e[0],o=e.length>1?e[1]:n;let s;if(s=t===r?n:o,"normal"!==s.toLowerCase())return B(s)}}return t===r?.25:.15}function H(e){const t=e[c];if(!t)return;if(clearTimeout(t.gainedTimer),clearTimeout(t.lostTimer),t.state===v)return;const n=t.target;h.delete(e),e.classList.remove("interest-source"),n&&n[u]?.invoker===e&&(n.removeEventListener("beforetoggle",M),n[u]=null,n.classList.remove("interest-target")),t.anchorName&&(e.style.anchorName="",n&&(n.style.positionAnchor=""),t.anchorName=null),t.state=v,t.target=null,j(e)}const O=["a[href]","area[href]","input:not([disabled])","select:not([disabled])","textarea:not([disabled])","button:not([disabled])","iframe","object","embed","[contenteditable]",'[tabindex]:not([tabindex="-1"])'].join(",");function D(e){return e?e.replace(/\s+/g," ").trim():""}function G(e,t=!1){if(e.nodeType===Node.TEXT_NODE)return e.data;if(e.nodeType!==Node.ELEMENT_NODE)return"";if(!t&&(e.hasAttribute("hidden")||"true"===e.getAttribute("aria-hidden")||"script"===e.localName||"style"===e.localName))return"";const n=e.getAttribute("aria-label");if(n)return n;if("img"===e.localName||"area"===e.localName)return e.getAttribute("alt")||"";let r="";for(const t of e.childNodes)r+=G(t);return r}function q(e,t,n){return(e.getAttribute(t)||"").trim().split(/\s+/).includes(n)}function j(e){const t=e[c];if(!t||!t.managesAriaExpanded||!t.a11yTarget)return;const n=t.a11yTarget,r=n.hasAttribute("popover")?$(n):t.state!==v&&t.target===n;e.setAttribute("aria-expanded",r?"true":"false")}function z(e,t){var n;if(t.id)if(function(e){if(e.querySelector(O))return!1;if(e.querySelector("h1,h2,h3,h4,h5,h6,ul,ol,li,table,nav,header,footer,main,aside,article,section,form,blockquote,details,summary,dialog"))return!1;const t=e.querySelectorAll("[role]");for(const e of t){const t=e.getAttribute("role").toLowerCase();if(!["presentation","none","generic","image","tooltip","paragraph"].includes(t))return!1}return!0}(t)){if(q(e,"aria-labelledby",t.id))return;const n=function(e){const t=e.getAttribute("aria-labelledby");if(t){const n=S(e);if("function"==typeof n.getElementById){const e=[];for(const r of t.trim().split(/\s+/)){const t=r?n.getElementById(r):null;t&&e.push(D(G(t,!0)))}const r=D(e.join(" "));if(r)return r}}const n=D(e.getAttribute("aria-label"));if(n)return n;if(e.labels?.length){const t=D([...e.labels].map(e=>G(e,!0)).join(" "));if(t)return t}return D(G(e,!0))||D(e.getAttribute("title"))}(e),r=D(G(t,!0));n?!r||r===n||e.hasAttribute("aria-describedby")||e.hasAttribute("aria-expanded")||function(e,t){const n=(n,r)=>r in e?e[r]===t:!!t.id&&e.getAttribute(n)===t.id;return n("popovertarget","popoverTargetElement")||n("commandfor","commandForElement")}(e,t)||e.setAttribute("aria-describedby",t.id):r&&e.setAttribute("aria-labelledby",t.id)}else{const r=e[c],o=q(e,"aria-describedby",t.id)||q(e,"aria-labelledby",t.id);o||e.hasAttribute("aria-details")||e.setAttribute("aria-details",t.id),r.managesAriaExpanded=!(o||e.hasAttribute("aria-expanded")||(n=e,(n instanceof HTMLAnchorElement||n instanceof HTMLAreaElement||window.SVGAElement&&n instanceof SVGAElement)&&!t.querySelector(O))),r.managesAriaExpanded&&(t.addEventListener("toggle",r.onTargetToggle),j(e)),t.hasAttribute("role")||t.setAttribute("role","tooltip")}}function X(e){e[c]||(e[c]={state:v,target:null,a11yTarget:null,managesAriaExpanded:!1,onTargetToggle:()=>j(e),gainedTimer:null,lostTimer:null,longPressTimer:null,suppressNextFocusInterest:!1,clearGainedTask(){clearTimeout(this.gainedTimer)},clearLostTask(){clearTimeout(this.lostTimer)}});const t=e[c],n=I(e);n&&t.a11yTarget!==n&&(t.managesAriaExpanded&&(t.a11yTarget?.removeEventListener("toggle",t.onTargetToggle),e.removeAttribute("aria-expanded"),t.managesAriaExpanded=!1),t.a11yTarget=n,z(e,n))}function R(e){if(e[f])return e[f];if(e.assignedSlot)return e.assignedSlot;if(e.parentElement)return e.parentElement;const t=e.parentNode;return t instanceof ShadowRoot?t.host:null}function Y(e){const t=new Set;if(0===h.size)return t;const n=e=>{const r=F(e);r&&!t.has(r)&&(t.add(r),n(r));const o=R(e);o&&!t.has(o)&&n(o)};return n(e),t}function U(e,t){if(y)return;if(!e.isConnected)return;const n=I(e);n&&X(e);const o=e[c];if(t===A||t===x){let s=!1;o&&(o.clearLostTask(),s=t===x&&o.suppressNextFocusInterest,o.suppressNextFocusInterest=!1);for(const t of Y(e))t[c].clearLostTask();n&&!s&&o.state===v&&function(e,t){const n=I(e);if(!n)return;const o=1e3*V(e,r);_(o)&&(e[c].clearGainedTask(),e[c].gainedTimer=setTimeout(()=>{P(e,n,t)},o))}(e,w)}else if(o&&(o.clearGainedTask(),o.state===w&&k(e)),t===N||!e.matches(":hover"))for(const t of Y(e))k(t)}function J(e){return e instanceof HTMLElement&&"area"!==e.localName&&!!e.parentNode&&!!I(e)}function K(e){const t=getComputedStyle(e).getPropertyValue(i).trim();return t&&"none"!==t&&"normal"!==t?t:null}function Q(){const e=this[f];e&&P(e,I(e),T)}function W(e,t){let n=e[d];t?(n||(n=e.ownerDocument.createElement("span"),n.className=l,n.setAttribute("aria-hidden","true"),n[f]=e,n.addEventListener("click",Q),e[d]=n,g.add(e)),n.style.getPropertyValue(a)!==t&&n.style.setProperty(a,t),e.nextSibling!==n&&e.after(n)):n&&(n.remove(),e[d]=null,g.delete(e))}function Z(t){const n=[];for(const r of t){let t=null;const o=I(r);o?(b.delete(r),J(r)&&(t=K(r))):r.isConnected&&r.hasAttribute(e)?b.add(r):b.delete(r),n.push([r,o,t])}for(const[e,t,r]of n)t&&X(e),W(e,r)}function ee(t){const n=new Set;let r=!1;const o=t=>{if(t.nodeType===Node.ELEMENT_NODE&&!t[f]){t.hasAttribute(e)&&n.add(t);for(const r of t.querySelectorAll(`[${e}]`))n.add(r)}};for(const s of t){if("childList"===s.type){for(const e of s.addedNodes)e.nodeType!==Node.ELEMENT_NODE||e[f]||(r=!0,o(e));continue}const t=s.target;switch(s.attributeName){case e:null!==s.oldValue&&H(t),""!==t.getAttribute(e)&&(t[m]=null),n.add(t);break;case"id":r=!0;break;default:o(t)}}for(const e of[...h]){const t=e[c].target;e.isConnected&&t&&t.isConnected||H(e)}if(r)for(const e of b)n.add(e);for(const e of[...g])n.has(e)||(J(e)?e.nextSibling!==e[d]&&e.after(e[d]):W(e,null));Z(n)}function te(){!function(){const t=document.createElement("style");t.textContent=`@property ${r} {syntax: "normal | <time>"; inherits: false; initial-value: normal;}\n @property ${o} {syntax: "normal | <time>"; inherits: false; initial-value: normal;}\n @property ${s} {syntax: "*"; inherits: false;}\n @property ${i} {syntax: "*"; inherits: false;}`,document.head.appendChild(t);const n=`span.${l}[aria-hidden]`,u=document.createElement("style");u.textContent=`:where(button[${e}]) {\n -webkit-user-select: none;\n user-select: none;\n }\n :where(${n}) {\n display: inline-block;\n vertical-align: middle;\n color: inherit;\n background-color: transparent;\n box-sizing: border-box;\n min-inline-size: 24px;\n min-block-size: 24px;\n min-block-size: max(24px, 1lh);\n align-content: center;\n text-align: center;\n -webkit-user-select: none;\n user-select: none;\n cursor: help;\n }\n :where(${n}:hover) {\n background-color: color-mix(in oklab, currentColor 10%, transparent);\n }\n :where(${n}:active) {\n background-color: color-mix(in oklab, currentColor 20%, transparent);\n }\n :where(${n})::before {\n content: var(${a});\n }`,document.head.prepend(u),document[c]={globalPropsStyle:t,uaStyle:u}}(),function(){const e=(e,t)=>{const n=e.composedPath(),r=n.length?n[0]:e.target;if(r instanceof Element)for(let e=r;e;e=R(e))U(e,t)},t=e=>{"touch"!==e.pointerType&&(y=!1)};document.addEventListener("pointerover",t),document.addEventListener("pointerdown",t),document.addEventListener("mouseover",t=>e(t,A)),document.addEventListener("mouseout",t=>e(t,L)),document.addEventListener("focusin",t=>e(t,x)),document.addEventListener("focusout",t=>e(t,N)),document.addEventListener("keydown",e=>{y=!1,"Escape"!==e.key||e.defaultPrevented||[...h].reverse().forEach(e=>{P(e,e[c].target,v,{force:!0})})});const n=()=>/iPad|iPhone|iPod/.test(navigator.userAgent);document.addEventListener("touchstart",e=>{y=!0;const t=e.target.closest("button[interestfor]");if(t){X(t);const r=n();r&&1===e.touches?.length&&(t[c].touchStartX=e.touches[0].clientX,t[c].touchStartY=e.touches[0].clientY),t[c].longPressActive=!1,t[c].longPressTimer=setTimeout(()=>{t[c].state===v&&(r?t[c].longPressActive=!0:P(t,I(t),T)),t[c].longPressTimer=null},500)}});const r=e=>{const t=e.target.closest("button[interestfor]");if(t&&t[c]){if(n()&&"touchmove"===e.type&&1===e.touches?.length){const n=e.touches[0].clientX-t[c].touchStartX,r=e.touches[0].clientY-t[c].touchStartY;if(Math.hypot(n,r)<=10)return}clearTimeout(t[c].longPressTimer),t[c].longPressTimer=null,t[c].longPressActive=!1}};document.addEventListener("touchmove",r),document.addEventListener("touchend",e=>{const t=e.target.closest("button[interestfor]"),n=t?.[c]?.longPressActive;r(e),n&&(e.cancelable&&e.preventDefault(),t[c].state===v&&P(t,I(t),T))},{passive:!1})}(),new MutationObserver(ee).observe(document,{childList:!0,subtree:!0,attributes:!0,attributeOldValue:!0,attributeFilter:[e,"id","href","disabled"]}),Z(document.querySelectorAll(`[${e}]`)),console.log(`interestfor polyfill installed (native: ${E}).`)}!function(){const t={configurable:!0,enumerable:!0,get(){return C(this)},set(t){if(null==t)this[m]=null,this.removeAttribute(e);else{if(!(t instanceof Element))throw new TypeError("Failed to set the 'interestForElement' property: The provided value is not of type 'Element'.");this[m]=t,this.setAttribute(e,"")}}};[window.HTMLButtonElement,window.HTMLAnchorElement,window.HTMLAreaElement,window.SVGAElement].forEach(e=>{!e||Object.prototype.hasOwnProperty.call(e.prototype,"interestForElement")&&!window.interestForUsePolyfillAlways||Object.defineProperty(e.prototype,"interestForElement",t)})}(),"complete"===document.readyState?te():window.addEventListener("load",te)}();
@@ -0,0 +1,298 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Interest Invokers, Command Invokers &amp; Hint Popovers Test Page</title>
6
+ <style>
7
+ body { font-family: sans-serif; line-height: 1.6; margin: 16px; }
8
+ section { border-bottom: 1px solid #ccc; padding: 12px 0; }
9
+ h2 { margin: 0 0 8px; font-size: 1rem; }
10
+ /* Minimal implicit-anchor offset so concurrent popovers do not completely occlude triggers */
11
+ [popover] { inset: auto; margin: 4px; position-area: block-end; }
12
+ [popover="hint"] { position-area: inline-end; }
13
+ </style>
14
+ <script src="../src/interestfor.js"></script>
15
+ </head>
16
+ <body>
17
+
18
+ <h1>Invoker &amp; Popover Permutations Test Suite</h1>
19
+
20
+ <!-- ===================================================================== -->
21
+ <!-- 1. Interest invokers pointing to plain hints -->
22
+ <!-- ===================================================================== -->
23
+ <section id="test-1">
24
+ <h2>1. Plain Hints (<code>interestfor</code> + non-interactive content)</h2>
25
+
26
+ <button id="btn-plain-text" interestfor="hint-plain-text">
27
+ 1a. Plain text hint
28
+ </button>
29
+ <div id="hint-plain-text" popover="hint">
30
+ Basic plain text inside a hint popover.
31
+ </div>
32
+
33
+ <button id="btn-plain-p" interestfor="hint-plain-p">
34
+ 1b. Paragraph hint (&lt;p&gt; / role="paragraph")
35
+ </button>
36
+ <div id="hint-plain-p" popover="hint">
37
+ <p role="paragraph">Paragraph node with explicit role="paragraph" inside a plain hint.</p>
38
+ <p>Second standard &lt;p&gt; element with implicit paragraph semantics.</p>
39
+ </div>
40
+
41
+ <button id="btn-plain-tooltip" interestfor="hint-plain-tooltip">
42
+ 1c. Explicit role="tooltip" plain hint
43
+ </button>
44
+ <div id="hint-plain-tooltip" popover="hint" role="tooltip">
45
+ Plain hint with explicit role="tooltip".
46
+ </div>
47
+ </section>
48
+
49
+ <!-- ===================================================================== -->
50
+ <!-- 2. Interest invokers pointing to rich hints -->
51
+ <!-- ===================================================================== -->
52
+ <section id="test-2">
53
+ <h2>2. Rich Hints (<code>interestfor</code> + interactive/structured popovers)</h2>
54
+
55
+ <!-- Adjacent in DOM: tests whether aria-details is omitted when popover is next sibling -->
56
+ <button id="btn-rich-adjacent" interestfor="hint-rich-adjacent">
57
+ 2a. Rich hint (DOM-adjacent)
58
+ </button>
59
+ <div id="hint-rich-adjacent" popover="hint">
60
+ <h3>Rich Hovercard (Adjacent)</h3>
61
+ <p>Contains interactive elements to trigger rich-hint semantics.</p>
62
+ <a href="#test-2">Explore details</a>
63
+ <button type="button">Action inside rich hint</button>
64
+ </div>
65
+
66
+ <!-- Non-adjacent in DOM: tests automatic aria-details + aria-expanded wiring -->
67
+ <button id="btn-rich-separated" interestfor="hint-rich-separated">
68
+ 2b. Rich hint (Non-adjacent in DOM)
69
+ </button>
70
+
71
+ <span>Spacer sibling between trigger and target popover.</span>
72
+
73
+ <div id="hint-rich-separated" popover="hint">
74
+ <h3>Rich Hovercard (Separated in DOM)</h3>
75
+ <p>Tests browser-managed <code>aria-expanded</code> and <code>aria-details</code> relationships.</p>
76
+ <input type="text" placeholder="Focusable input in rich hint">
77
+ <button type="button">Save</button>
78
+ </div>
79
+ </section>
80
+
81
+ <!-- ===================================================================== -->
82
+ <!-- 3. Dual-purpose button invokers -->
83
+ <!-- ===================================================================== -->
84
+ <section id="test-3">
85
+ <h2>3. Dual-Purpose Button Invokers (<code>interestfor</code> + <code>commandfor</code> / <code>popovertarget</code>)</h2>
86
+
87
+ <!-- 3a. interestfor (hint) + commandfor (secondary auto popover) -->
88
+ <button id="btn-dual-command-popover"
89
+ interestfor="hint-dual-1"
90
+ commandfor="popover-secondary-action"
91
+ command="toggle-popover">
92
+ 3a. Hover for hint / Click to toggle secondary popover (commandfor)
93
+ </button>
94
+ <div id="hint-dual-1" popover="hint" role="tooltip">
95
+ Preview tooltip via interestfor.
96
+ </div>
97
+ <div id="popover-secondary-action" popover="auto">
98
+ <p>Secondary action popover opened via <code>commandfor</code> + <code>command="toggle-popover"</code>.</p>
99
+ <button type="button">Inner control</button>
100
+ </div>
101
+
102
+ <!-- 3b. interestfor (hint) + commandfor (modal dialog) -->
103
+ <button id="btn-dual-command-dialog"
104
+ interestfor="hint-dual-2"
105
+ commandfor="dialog-action"
106
+ command="show-modal">
107
+ 3b. Hover for hint / Click to open modal dialog (commandfor)
108
+ </button>
109
+ <div id="hint-dual-2" popover="hint" role="tooltip">
110
+ Opens modal dialog when activated.
111
+ </div>
112
+ <dialog id="dialog-action">
113
+ <p>Dialog opened via <code>commandfor="dialog-action" command="show-modal"</code>.</p>
114
+ <button commandfor="dialog-action" command="close">Close Dialog</button>
115
+ </dialog>
116
+
117
+ <!-- 3c. interestfor + commandfor pointing to the SAME popover (hover to preview, click to pin) -->
118
+ <button id="btn-dual-same-target"
119
+ interestfor="hint-shared-target"
120
+ commandfor="hint-shared-target"
121
+ command="show-popover">
122
+ 3c. Same target: Hover to preview / Click to keep open
123
+ </button>
124
+ <div id="hint-shared-target" popover="hint">
125
+ <p>Shared rich hint target for both <code>interestfor</code> and <code>commandfor</code>.</p>
126
+ <button type="button">Interactive child</button>
127
+ </div>
128
+
129
+ <!-- 3d. interestfor + legacy popovertarget -->
130
+ <button id="btn-dual-popovertarget"
131
+ interestfor="hint-dual-3"
132
+ popovertarget="popover-legacy-target"
133
+ popovertargetaction="toggle">
134
+ 3d. Hover for hint / Click via popovertarget
135
+ </button>
136
+ <div id="hint-dual-3" popover="hint" role="tooltip">
137
+ Hint on a button using popovertarget.
138
+ </div>
139
+ <div id="popover-legacy-target" popover="auto">
140
+ <p>Secondary popover controlled by <code>popovertarget</code>.</p>
141
+ </div>
142
+ </section>
143
+
144
+ <!-- ===================================================================== -->
145
+ <!-- 4. Nested interest invokers inside their own popover -->
146
+ <!-- ===================================================================== -->
147
+ <section id="test-4">
148
+ <h2>4. Nested Interest Invokers Inside Their Own Popover</h2>
149
+
150
+ <button id="btn-open-outer-rich" interestfor="popover-self-host" commandfor="popover-self-host" command="toggle-popover">
151
+ 4a. Open / Interest Outer Rich Hint (#popover-self-host)
152
+ </button>
153
+
154
+ <div id="popover-self-host" popover="hint">
155
+ <h3>Outer Rich Hint (#popover-self-host)</h3>
156
+ <p>Contains an invoker pointing back to this enclosing popover, and another pointing to a second rich hint:</p>
157
+
158
+ <!-- Invoker placed INSIDE the very popover it controls -->
159
+ <button id="btn-self-nested" interestfor="popover-self-host">
160
+ 4b. Self-referencing invoker inside #popover-self-host
161
+ </button>
162
+
163
+ <!-- Invoker inside #popover-self-host interacting with another rich hint -->
164
+ <button id="btn-nested-to-second-rich" interestfor="popover-nested-rich-2">
165
+ 4c. Invoker inside #popover-self-host -&gt; #popover-nested-rich-2
166
+ </button>
167
+ </div>
168
+
169
+ <div id="popover-nested-rich-2" popover="hint">
170
+ <h3>Second Rich Hint (#popover-nested-rich-2)</h3>
171
+ <p>Triggered from inside #popover-self-host.</p>
172
+ <!-- Also includes a self-referencing invoker inside itself -->
173
+ <button id="btn-self-nested-2" interestfor="popover-nested-rich-2">
174
+ 4d. Self-referencing invoker inside #popover-nested-rich-2
175
+ </button>
176
+ <a href="#test-4">Nested interactive link</a>
177
+ </div>
178
+ </section>
179
+
180
+ <!-- ===================================================================== -->
181
+ <!-- 5. Hyperlink interest invokers (<a href> + interestfor) -->
182
+ <!-- ===================================================================== -->
183
+ <section id="test-5">
184
+ <h2>5. Hyperlink Interest Invokers (<code>&lt;a href&gt;</code> + <code>interestfor</code>)</h2>
185
+
186
+ <p>
187
+ Hover or focus the profile link for
188
+ <a id="link-rich-hovercard" href="#profile-destination" interestfor="hovercard-profile">@mfreed7</a>
189
+ or the documentation link for
190
+ <a id="link-plain-hint" href="#docs-destination" interestfor="hovercard-plain">Popover API Spec</a>.
191
+ </p>
192
+
193
+ <div id="hovercard-profile" popover="hint">
194
+ <h3>Mason (@mfreed7)</h3>
195
+ <p>Browser Engineer working on Blink / Open UI.</p>
196
+ <a href="#follow">Follow</a>
197
+ <button type="button">Message</button>
198
+ </div>
199
+
200
+ <div id="hovercard-plain" popover="hint" role="tooltip">
201
+ Plain tooltip preview for a standard navigable hyperlink.
202
+ </div>
203
+ </section>
204
+
205
+ <!-- ===================================================================== -->
206
+ <!-- 6. Author-defined ARIA states combined with interest invokers -->
207
+ <!-- ===================================================================== -->
208
+ <section id="test-6">
209
+ <h2>6. Author-Defined ARIA States + <code>interestfor</code></h2>
210
+
211
+ <p id="author-desc">Author-supplied description text via aria-describedby.</p>
212
+ <div id="author-details">Author-supplied extra details node via aria-details.</div>
213
+
214
+ <!-- Explicit aria-expanded="false" and aria-describedby on a button pointing to a rich hint -->
215
+ <button id="btn-author-aria-false"
216
+ interestfor="hint-author-rich"
217
+ aria-expanded="false"
218
+ aria-describedby="author-desc">
219
+ 6a. Button with explicit aria-expanded="false" + aria-describedby
220
+ </button>
221
+
222
+ <!-- Explicit aria-expanded="true" and aria-details on a button pointing to a rich hint -->
223
+ <button id="btn-author-aria-true"
224
+ interestfor="hint-author-rich"
225
+ aria-expanded="true"
226
+ aria-details="author-details">
227
+ 6b. Button with explicit aria-expanded="true" + aria-details
228
+ </button>
229
+
230
+ <!-- Explicit aria-expanded and aria-describedby on a hyperlink invoker -->
231
+ <a id="link-author-aria"
232
+ href="#section-6"
233
+ interestfor="hint-author-plain"
234
+ aria-expanded="false"
235
+ aria-describedby="author-desc">
236
+ 6c. Link with explicit aria-expanded="false" + aria-describedby
237
+ </a>
238
+
239
+ <div id="hint-author-rich" popover="hint">
240
+ <h3>Rich Hint Target for Author-Defined ARIA Triggers</h3>
241
+ <button type="button">Interactive Button</button>
242
+ </div>
243
+
244
+ <div id="hint-author-plain" popover="hint" role="tooltip">
245
+ Plain hint target for author-ARIA link trigger.
246
+ </div>
247
+ </section>
248
+
249
+ <!-- ===================================================================== -->
250
+ <!-- 7. Interest invokers combined with has-interest-for state -->
251
+ <!-- ===================================================================== -->
252
+ <section id="test-7">
253
+ <h2>7. Interest Invokers + <code>has-interest-for</code> &amp; <code>aria-expanded</code></h2>
254
+
255
+ <!-- Boolean has-interest-for alongside primary disclosure aria-expanded="false" -->
256
+ <button id="btn-has-interest-collapsed"
257
+ interestfor="hint-has-interest-rich"
258
+ commandfor="panel-primary-disclosure"
259
+ command="toggle-popover"
260
+ has-interest-for
261
+ aria-expanded="false">
262
+ 7a. Control with has-interest-for + aria-expanded="false" (Primary Collapsed)
263
+ </button>
264
+
265
+ <!-- Explicit ID/value has-interest-for alongside primary disclosure aria-expanded="true" -->
266
+ <button id="btn-has-interest-expanded"
267
+ interestfor="hint-has-interest-plain"
268
+ has-interest-for="hint-has-interest-plain"
269
+ aria-expanded="true">
270
+ 7b. Control with has-interest-for="hint-has-interest-plain" + aria-expanded="true" (Primary Expanded)
271
+ </button>
272
+
273
+ <!-- Hyperlink with has-interest-for + aria-expanded -->
274
+ <a id="link-has-interest"
275
+ href="#section-7"
276
+ interestfor="hint-has-interest-rich"
277
+ has-interest-for
278
+ aria-expanded="false">
279
+ 7c. Link with has-interest-for + aria-expanded="false"
280
+ </a>
281
+
282
+ <div id="hint-has-interest-rich" popover="hint">
283
+ <h3>Rich Hint on a <code>has-interest-for</code> Control</h3>
284
+ <p>Tests disambiguation between primary disclosure state and interest popover state.</p>
285
+ <button type="button">Hint Action</button>
286
+ </div>
287
+
288
+ <div id="hint-has-interest-plain" popover="hint" role="tooltip">
289
+ Plain hint on a control with has-interest-for and aria-expanded="true".
290
+ </div>
291
+
292
+ <div id="panel-primary-disclosure" popover="auto">
293
+ <p>Primary disclosure popover controlled by 7a.</p>
294
+ </div>
295
+ </section>
296
+
297
+ </body>
298
+ </html>
package/test/test.html CHANGED
@@ -192,6 +192,191 @@ async function runTests() {
192
192
  assert_equals(invoker.getAttribute("aria-labelledby"), "delayed_target");
193
193
  });
194
194
 
195
+ await test("Plain vs. rich classification ignores popover type, and allows role=tooltip/paragraph", async () => {
196
+ const { invoker: autoInv, target: autoTip } = createPair({
197
+ invokerHTML: "Help",
198
+ popoverType: "auto",
199
+ targetHTML: "Plain text in an auto popover",
200
+ });
201
+ const { invoker: manualInv, target: manualTip } = createPair({
202
+ invokerHTML: "Help",
203
+ popoverType: "manual",
204
+ targetHTML: "Plain text in a manual popover",
205
+ });
206
+ const { invoker: roleInv, target: roleTip } = createPair({
207
+ invokerHTML: "Help",
208
+ popoverType: "hint",
209
+ targetHTML: '<div role="tooltip"><span role="paragraph">Some text</span></div>',
210
+ });
211
+ const { invoker: richAutoInv, target: richAutoTip } = createPair({
212
+ invokerHTML: "Help",
213
+ popoverType: "hint",
214
+ targetHTML: "<ul><li>Rich</li></ul>",
215
+ });
216
+ await nextTick();
217
+ for (const [inv, tip, label] of [
218
+ [autoInv, autoTip, "popover=auto"],
219
+ [manualInv, manualTip, "popover=manual"],
220
+ [roleInv, roleTip, "role=tooltip/paragraph"],
221
+ ]) {
222
+ assert_equals(inv.getAttribute("aria-describedby"), tip.id, `${label} -> aria-describedby`);
223
+ assert_equals(inv.getAttribute("aria-details"), null, `${label} -> no aria-details`);
224
+ assert_equals(inv.getAttribute("aria-expanded"), null, `${label} -> no aria-expanded`);
225
+ }
226
+ assert_equals(richAutoInv.getAttribute("aria-details"), richAutoTip.id, "rich hint popover -> aria-details");
227
+ assert_equals(richAutoInv.getAttribute("aria-expanded"), "false", "rich hint popover -> aria-expanded");
228
+ });
229
+
230
+ await test("Plain-hint toggletips (popovertarget, commandfor, author aria-expanded) don't get aria-describedby", async () => {
231
+ const targetId1 = "toggletip_1";
232
+ const { invoker: ptInv } = createPair({
233
+ targetId: targetId1,
234
+ invokerAttrs: { "aria-label": "More info", popovertarget: targetId1, popovertargetaction: "show" },
235
+ invokerHTML: "?",
236
+ targetHTML: "Some helpful text",
237
+ });
238
+ const targetId2 = "toggletip_2";
239
+ const { invoker: cfInv } = createPair({
240
+ targetId: targetId2,
241
+ invokerAttrs: { "aria-label": "More info", commandfor: targetId2, command: "show-popover" },
242
+ invokerHTML: "?",
243
+ targetHTML: "Some helpful text",
244
+ });
245
+ const { invoker: expInv } = createPair({
246
+ invokerAttrs: { "aria-label": "More info", "aria-expanded": "false" },
247
+ invokerHTML: "?",
248
+ targetHTML: "Some helpful text",
249
+ });
250
+ // Control: popovertarget pointing elsewhere still gets aria-describedby.
251
+ const other = document.createElement("div");
252
+ other.id = "toggletip_other";
253
+ other.setAttribute("popover", "auto");
254
+ sandbox.appendChild(other);
255
+ const { invoker: ctlInv, target: ctlTip } = createPair({
256
+ invokerAttrs: { "aria-label": "More info", popovertarget: other.id },
257
+ invokerHTML: "?",
258
+ targetHTML: "Some helpful text",
259
+ });
260
+ await nextTick();
261
+ assert_equals(ptInv.getAttribute("aria-describedby"), null, "popovertarget");
262
+ assert_equals(cfInv.getAttribute("aria-describedby"), null, "commandfor");
263
+ assert_equals(expInv.getAttribute("aria-describedby"), null, "author aria-expanded");
264
+ assert_equals(expInv.getAttribute("aria-expanded"), "false", "author aria-expanded unchanged");
265
+ assert_equals(ctlInv.getAttribute("aria-describedby"), ctlTip.id, "popovertarget to another element");
266
+ });
267
+
268
+ await test("Rich hint: author aria-expanded is preserved through interest changes", async () => {
269
+ const { invoker, target } = createPair({
270
+ invokerAttrs: { "aria-expanded": "true" },
271
+ targetHTML: '<a href="#">Rich</a>',
272
+ });
273
+ await nextTick();
274
+ assert_equals(invoker.getAttribute("aria-details"), target.id, "aria-details still added");
275
+ assert_equals(invoker.getAttribute("aria-expanded"), "true", "after init");
276
+ hover(invoker);
277
+ await wait(30);
278
+ assert_true(target.matches(":popover-open"), "popover opened");
279
+ dehover(invoker);
280
+ await wait(30);
281
+ assert_false(target.matches(":popover-open"), "popover closed");
282
+ assert_equals(invoker.getAttribute("aria-expanded"), "true", "after losing interest");
283
+ });
284
+
285
+ await test("Rich hint: author aria-describedby/aria-labelledby referencing target suppresses aria-details and aria-expanded", async () => {
286
+ const id1 = "rich_described";
287
+ const { invoker: descInv, target: descTip } = createPair({
288
+ targetId: id1,
289
+ invokerAttrs: { "aria-describedby": `other ${id1}` },
290
+ targetHTML: '<a href="#">Rich</a>',
291
+ });
292
+ const id2 = "rich_labelled";
293
+ const { invoker: lblInv } = createPair({
294
+ targetId: id2,
295
+ invokerAttrs: { "aria-labelledby": id2 },
296
+ targetHTML: '<a href="#">Rich</a>',
297
+ });
298
+ await nextTick();
299
+ for (const [inv, label] of [[descInv, "describedby"], [lblInv, "labelledby"]]) {
300
+ assert_equals(inv.getAttribute("aria-details"), null, `${label} -> no aria-details`);
301
+ assert_equals(inv.getAttribute("aria-expanded"), null, `${label} -> no aria-expanded`);
302
+ }
303
+ hover(descInv);
304
+ await wait(30);
305
+ assert_true(descTip.matches(":popover-open"), "popover opened");
306
+ assert_equals(descInv.getAttribute("aria-expanded"), null, "not set on interest");
307
+ });
308
+
309
+ await test("Rich hint on <a href>: aria-expanded only if the hint contains focusable content", async () => {
310
+ const { invoker: plainLink, target: plainLinkTip } = createPair({
311
+ invokerTag: "a",
312
+ invokerHTML: "@user",
313
+ targetHTML: "<h3>User</h3><p>Bio</p>",
314
+ });
315
+ const { invoker: focusLink } = createPair({
316
+ invokerTag: "a",
317
+ invokerHTML: "@user",
318
+ targetHTML: '<h3>User</h3><button>Follow</button>',
319
+ });
320
+ await nextTick();
321
+ assert_equals(plainLink.getAttribute("aria-details"), plainLinkTip.id, "aria-details still set");
322
+ assert_equals(plainLink.getAttribute("aria-expanded"), null, "non-focusable rich hint -> no aria-expanded");
323
+ hover(plainLink);
324
+ await wait(30);
325
+ assert_true(plainLinkTip.matches(":popover-open"), "popover opened");
326
+ assert_equals(plainLink.getAttribute("aria-expanded"), null, "not set on interest");
327
+ assert_equals(focusLink.getAttribute("aria-expanded"), "false", "focusable rich hint -> aria-expanded");
328
+ });
329
+
330
+ await test("Rich hint: aria-expanded tracks the popover state, even when opened via popovertarget", async () => {
331
+ const targetId = "rich_pt";
332
+ const { invoker, target } = createPair({
333
+ targetId,
334
+ invokerAttrs: { popovertarget: targetId },
335
+ targetHTML: '<a href="#">Rich</a>',
336
+ });
337
+ await nextTick();
338
+ assert_equals(invoker.getAttribute("aria-expanded"), "false", "initial");
339
+ invoker.click();
340
+ assert_true(target.matches(":popover-open"), "opened by click");
341
+ await wait(10);
342
+ assert_equals(invoker.getAttribute("aria-expanded"), "true", "after click");
343
+ hover(invoker);
344
+ await wait(30);
345
+ dehover(invoker);
346
+ await wait(30);
347
+ assert_true(target.matches(":popover-open"), "popover kept open after losing interest");
348
+ assert_equals(invoker.getAttribute("aria-expanded"), "true", "still expanded after losing interest");
349
+ target.hidePopover();
350
+ await wait(10);
351
+ assert_equals(invoker.getAttribute("aria-expanded"), "false", "after hidePopover");
352
+
353
+ // Interest-shown popover that is hidden by light dismiss / script.
354
+ hover(invoker);
355
+ await wait(30);
356
+ assert_equals(invoker.getAttribute("aria-expanded"), "true", "after gaining interest");
357
+ target.hidePopover();
358
+ await wait(10);
359
+ assert_equals(invoker.getAttribute("aria-expanded"), "false", "after hiding while interest shown");
360
+ });
361
+
362
+ await test("Changing interestfor from a rich to a plain hint removes polyfill-managed aria-expanded", async () => {
363
+ const { invoker } = createPair({
364
+ invokerAttrs: { "aria-label": "Info" },
365
+ targetHTML: '<a href="#">Rich</a>',
366
+ });
367
+ const plain = document.createElement("div");
368
+ plain.id = "switch_plain";
369
+ plain.setAttribute("popover", "hint");
370
+ plain.textContent = "Plain description";
371
+ sandbox.appendChild(plain);
372
+ await nextTick();
373
+ assert_equals(invoker.getAttribute("aria-expanded"), "false", "rich -> managed");
374
+ invoker.setAttribute("interestfor", plain.id);
375
+ await nextTick();
376
+ assert_equals(invoker.getAttribute("aria-expanded"), null, "plain -> removed");
377
+ assert_equals(invoker.getAttribute("aria-describedby"), plain.id, "plain -> aria-describedby");
378
+ });
379
+
195
380
  // =========================================================================
196
381
  section("2. Element Validity & interestForElement IDL");
197
382
  // =========================================================================
@@ -295,7 +480,11 @@ async function runTests() {
295
480
  // =========================================================================
296
481
 
297
482
  await test("Hover gains interest after delay, opens popover, sets classes, and dehover loses interest", async () => {
298
- const { invoker, target } = createPair({ delayStart: "20ms", delayEnd: "20ms" });
483
+ const { invoker, target } = createPair({
484
+ delayStart: "20ms",
485
+ delayEnd: "20ms",
486
+ targetHTML: '<a href="#">Rich content</a>',
487
+ });
299
488
  const events = [];
300
489
  target.addEventListener("interest", (e) => events.push(`interest:${e.source.tagName}`));
301
490
  target.addEventListener("loseinterest", (e) => events.push(`loseinterest:${e.source.tagName}`));
@@ -572,6 +761,87 @@ async function runTests() {
572
761
  await wait(25);
573
762
  assert_true(target.matches(":popover-open"), "mouse pointerover re-enables hover interest");
574
763
  });
764
+
765
+ await test("iOS long-press ignores touchmove within 10px slop and cancels beyond 10px", async () => {
766
+ const origUA = Object.getOwnPropertyDescriptor(Navigator.prototype, "userAgent");
767
+ Object.defineProperty(navigator, "userAgent", {
768
+ value: "Mozilla/5.0 (iPhone; CPU iPhone OS 27_0 like Mac OS X) AppleWebKit/605.1.15",
769
+ configurable: true,
770
+ });
771
+ const dispatchTouch = (el, type, x, y) => {
772
+ const ev = new Event(type, { bubbles: true, cancelable: true });
773
+ ev.touches = type === "touchend" ? [] : [{ clientX: x, clientY: y }];
774
+ el.dispatchEvent(ev);
775
+ return ev;
776
+ };
777
+ try {
778
+ const { invoker, target } = createPair();
779
+ // 1. Jitter within 10px (e.g. 6, 8 -> distance 10) does not cancel long press:
780
+ dispatchTouch(invoker, "touchstart", 100, 100);
781
+ dispatchTouch(invoker, "touchmove", 106, 108);
782
+ await wait(520);
783
+ assert_false(target.matches(":popover-open"), "on iOS, popover waits for touchend");
784
+ // Jitter after the 500ms timer has fired (still within 10px) also does not cancel:
785
+ dispatchTouch(invoker, "touchmove", 95, 100);
786
+ const endEv = dispatchTouch(invoker, "touchend", 95, 100);
787
+ assert_true(endEv.defaultPrevented, "touchend prevented default to suppress click");
788
+ assert_true(target.matches(":popover-open"), "long press with <=10px jitter opened popover on touchend");
789
+
790
+ pressEscape();
791
+ assert_false(target.matches(":popover-open"), "closed via Escape");
792
+
793
+ // 2. Movement >10px (e.g. 7, 8 -> distance ~10.63) cancels long press:
794
+ dispatchTouch(invoker, "touchstart", 100, 100);
795
+ dispatchTouch(invoker, "touchmove", 107, 108);
796
+ await wait(520);
797
+ const endEv2 = dispatchTouch(invoker, "touchend", 107, 108);
798
+ assert_false(endEv2.defaultPrevented, "canceled long press does not preventDefault on touchend");
799
+ assert_false(target.matches(":popover-open"), "long press with >10px movement did not open popover");
800
+ } finally {
801
+ if (origUA) {
802
+ Object.defineProperty(navigator, "userAgent", origUA);
803
+ } else {
804
+ delete navigator.userAgent;
805
+ }
806
+ // Reset touchInProgress for any subsequent tests
807
+ document.body.dispatchEvent(new PointerEvent("pointerover", { bubbles: true, pointerType: "mouse" }));
808
+ }
809
+ });
810
+
811
+ // =========================================================================
812
+ section("6. Document Body Replacement (e.g. Turbo Drive)");
813
+ // =========================================================================
814
+
815
+ await test("Hover, focus, and Escape still work after document.body is replaced", async () => {
816
+ const originalBody = document.body;
817
+ const newBody = document.createElement("body");
818
+ newBody.innerHTML =
819
+ '<button id="swap_invoker" interestfor="swap_target">Invoker</button>' +
820
+ '<div id="swap_target" popover="auto">Target</div>';
821
+ originalBody.replaceWith(newBody);
822
+ try {
823
+ const invoker = document.getElementById("swap_invoker");
824
+ const target = document.getElementById("swap_target");
825
+ invoker.style.setProperty("--interest-delay-start", "10ms");
826
+ invoker.style.setProperty("--interest-delay-end", "10ms");
827
+ await nextTick();
828
+
829
+ hover(invoker);
830
+ await wait(25);
831
+ assert_true(target.matches(":popover-open"), "hover opens popover in replaced body");
832
+ dehover(invoker);
833
+ await wait(25);
834
+ assert_false(target.matches(":popover-open"), "dehover closes popover in replaced body");
835
+
836
+ focusIn(invoker);
837
+ await wait(25);
838
+ assert_true(target.matches(":popover-open"), "focus opens popover in replaced body");
839
+ pressEscape();
840
+ assert_false(target.matches(":popover-open"), "Escape closes popover in replaced body");
841
+ } finally {
842
+ newBody.replaceWith(originalBody);
843
+ }
844
+ });
575
845
  }
576
846
  </script>
577
847