interestfor 1.0.12 → 1.0.13

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.13",
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
  }
@@ -989,13 +989,13 @@
989
989
  touchInProgress = false;
990
990
  }
991
991
  };
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) => {
992
+ document.addEventListener("pointerover", clearTouch);
993
+ document.addEventListener("pointerdown", clearTouch);
994
+ document.addEventListener("mouseover", (e) => handler(e, Source.Hover));
995
+ document.addEventListener("mouseout", (e) => handler(e, Source.DeHover));
996
+ document.addEventListener("focusin", (e) => handler(e, Source.Focus));
997
+ document.addEventListener("focusout", (e) => handler(e, Source.Blur));
998
+ document.addEventListener("keydown", (e) => {
999
999
  touchInProgress = false;
1000
1000
  if (e.key === "Escape" && !e.defaultPrevented) {
1001
1001
  // Lose interest in all invokers, most recent first. These
@@ -1010,31 +1010,46 @@
1010
1010
  });
1011
1011
  }
1012
1012
  });
1013
+
1014
+ // WebKit (iOS) long-press workarounds:
1015
+ // 1. On Android, holding for ~500ms fires `pointercancel` and `touchcancel`,
1016
+ // so the popover can open immediately when `longPressTimer` fires while
1017
+ // the finger is still down. On iOS WebKit, holding a button with
1018
+ // `user-select: none` does not cancel the touch stream; when the finger
1019
+ // is later lifted, iOS fires `pointerup` (which triggers WebKit's native
1020
+ // popover light-dismiss because the popover was not open at
1021
+ // `pointerdown`) followed by `touchend` and synthesized mouse/`click`
1022
+ // events. On iOS only, record that the 500ms hold succeeded
1023
+ // (`longPressActive`), and on `touchend` (which fires after `pointerup`),
1024
+ // call `preventDefault()` to suppress the synthesized `click` and open
1025
+ // the popover then.
1026
+ // 2. Unlike Android (which applies a native touch slop before firing
1027
+ // `touchmove`), iOS WebKit dispatches `touchmove` on tiny finger jitter
1028
+ // during a stationary hold. On iOS only, ignore `touchmove` events within
1029
+ // 10px of `touchstart` (matching UIKit's default `allowableMovement` on
1030
+ // `UILongPressGestureRecognizer`).
1031
+ const longPressMoveSlop = 10;
1032
+ const isIOS = () => /iPad|iPhone|iPod/.test(navigator.userAgent);
1033
+
1034
+
1013
1035
  // Touch support
1014
1036
  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) => {
1037
+ document.addEventListener("touchstart", (e) => {
1028
1038
  touchInProgress = true;
1029
1039
  const invoker = e.target.closest("button[interestfor]");
1030
1040
  if (invoker) {
1031
1041
  initializeDataField(invoker);
1042
+ const ios = isIOS();
1043
+ if (ios && e.touches?.length === 1) {
1044
+ invoker[dataField].touchStartX = e.touches[0].clientX;
1045
+ invoker[dataField].touchStartY = e.touches[0].clientY;
1046
+ }
1032
1047
  invoker[dataField].longPressActive = false;
1033
1048
  invoker[dataField].longPressTimer = setTimeout(() => {
1034
1049
  // Long-press shows interest immediately, and that interest is not
1035
1050
  // lost by de-hovering or blurring the invoker.
1036
1051
  if (invoker[dataField].state === InterestState.NoInterest) {
1037
- if (isIOS) {
1052
+ if (ios) {
1038
1053
  invoker[dataField].longPressActive = true;
1039
1054
  } else {
1040
1055
  GainOrLoseInterest(
@@ -1051,13 +1066,20 @@
1051
1066
  const cancelLongPress = (e) => {
1052
1067
  const invoker = e.target.closest("button[interestfor]");
1053
1068
  if (invoker && invoker[dataField]) {
1069
+ if (isIOS() && e.type === "touchmove" && e.touches?.length === 1) {
1070
+ const dx = e.touches[0].clientX - invoker[dataField].touchStartX;
1071
+ const dy = e.touches[0].clientY - invoker[dataField].touchStartY;
1072
+ if (Math.hypot(dx, dy) <= longPressMoveSlop) {
1073
+ return;
1074
+ }
1075
+ }
1054
1076
  clearTimeout(invoker[dataField].longPressTimer);
1055
1077
  invoker[dataField].longPressTimer = null;
1056
1078
  invoker[dataField].longPressActive = false;
1057
1079
  }
1058
1080
  };
1059
- document.body.addEventListener("touchmove", cancelLongPress);
1060
- document.body.addEventListener(
1081
+ document.addEventListener("touchmove", cancelLongPress);
1082
+ document.addEventListener(
1061
1083
  "touchend",
1062
1084
  (e) => {
1063
1085
  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",h="http://www.w3.org/1999/xlink",p=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.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=x(e);return t!==x(this.target||document)?t.host:e}});const w="none",v="full",T="explicit",A="hover",L="dehover",N="focus",S="blur";function x(e){return e&&"function"==typeof e.getRootNode?e.getRootNode():e&&e.parentNode?x(e.parentNode):e}function P(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(!P(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",M),n[u].shownByInterest=!1,!$(n))){try{n.showPopover({source:e})}catch{}n[u].shownByInterest=$(n)}if(p.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 k(e){const t=1e3*O(e,o);if(!_(t))return;const n=e[c];n.clearLostTask();const r=n.target;n.lostTimer=setTimeout(()=>{P(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(h,"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=x(e);let r=x(t);for(;r;){if(r===n)return!0;r=r instanceof ShadowRoot?x(r.host):null}return!1}(r,t)?r:null;if(""===n)return null;const o=x(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,w,{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 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;p.delete(e),e.classList.remove("interest-source"),n&&(n[u]?.invoker===e&&(n.removeEventListener("beforetoggle",M),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=x(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 X(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 R(e){const t=new Set;if(0===p.size)return t;const n=e=>{const r=F(e);r&&!t.has(r)&&(t.add(r),n(r));const o=X(e);o&&!t.has(o)&&n(o)};return n(e),t}function Y(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 R(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);_(o)&&(e[c].clearGainedTask(),e[c].gainedTimer=setTimeout(()=>{P(e,n,t)},o))}(e,v)}else if(o&&(o.clearGainedTask(),o.state===v&&k(e)),t===S||!e.matches(":hover"))for(const t of R(e))k(t)}function U(e){return e instanceof HTMLElement&&"area"!==e.localName&&!!e.parentNode&&!!I(e)}function J(e){const t=getComputedStyle(e).getPropertyValue(i).trim();return t&&"none"!==t&&"normal"!==t?t:null}function K(){const e=this[f];e&&P(e,I(e),T)}function Q(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",K),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 W(t){const n=[];for(const r of t){let t=null;const o=I(r);o?(g.delete(r),U(r)&&(t=J(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),Q(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[...p]){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)||(U(e)?e.nextSibling!==e[d]&&e.after(e[d]):Q(e,null));W(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=X(e))Y(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,N)),document.addEventListener("focusout",t=>e(t,S)),document.addEventListener("keydown",e=>{y=!1,"Escape"!==e.key||e.defaultPrevented||[...p].reverse().forEach(e=>{P(e,e[c].target,w,{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){z(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===w&&(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===w&&P(t,I(t),T))},{passive:!1})}(),new MutationObserver(Z).observe(document,{childList:!0,subtree:!0,attributes:!0,attributeOldValue:!0,attributeFilter:[e,"id","href","disabled"]}),W(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)}();
@@ -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
@@ -572,6 +572,87 @@ async function runTests() {
572
572
  await wait(25);
573
573
  assert_true(target.matches(":popover-open"), "mouse pointerover re-enables hover interest");
574
574
  });
575
+
576
+ await test("iOS long-press ignores touchmove within 10px slop and cancels beyond 10px", async () => {
577
+ const origUA = Object.getOwnPropertyDescriptor(Navigator.prototype, "userAgent");
578
+ Object.defineProperty(navigator, "userAgent", {
579
+ value: "Mozilla/5.0 (iPhone; CPU iPhone OS 27_0 like Mac OS X) AppleWebKit/605.1.15",
580
+ configurable: true,
581
+ });
582
+ const dispatchTouch = (el, type, x, y) => {
583
+ const ev = new Event(type, { bubbles: true, cancelable: true });
584
+ ev.touches = type === "touchend" ? [] : [{ clientX: x, clientY: y }];
585
+ el.dispatchEvent(ev);
586
+ return ev;
587
+ };
588
+ try {
589
+ const { invoker, target } = createPair();
590
+ // 1. Jitter within 10px (e.g. 6, 8 -> distance 10) does not cancel long press:
591
+ dispatchTouch(invoker, "touchstart", 100, 100);
592
+ dispatchTouch(invoker, "touchmove", 106, 108);
593
+ await wait(520);
594
+ assert_false(target.matches(":popover-open"), "on iOS, popover waits for touchend");
595
+ // Jitter after the 500ms timer has fired (still within 10px) also does not cancel:
596
+ dispatchTouch(invoker, "touchmove", 95, 100);
597
+ const endEv = dispatchTouch(invoker, "touchend", 95, 100);
598
+ assert_true(endEv.defaultPrevented, "touchend prevented default to suppress click");
599
+ assert_true(target.matches(":popover-open"), "long press with <=10px jitter opened popover on touchend");
600
+
601
+ pressEscape();
602
+ assert_false(target.matches(":popover-open"), "closed via Escape");
603
+
604
+ // 2. Movement >10px (e.g. 7, 8 -> distance ~10.63) cancels long press:
605
+ dispatchTouch(invoker, "touchstart", 100, 100);
606
+ dispatchTouch(invoker, "touchmove", 107, 108);
607
+ await wait(520);
608
+ const endEv2 = dispatchTouch(invoker, "touchend", 107, 108);
609
+ assert_false(endEv2.defaultPrevented, "canceled long press does not preventDefault on touchend");
610
+ assert_false(target.matches(":popover-open"), "long press with >10px movement did not open popover");
611
+ } finally {
612
+ if (origUA) {
613
+ Object.defineProperty(navigator, "userAgent", origUA);
614
+ } else {
615
+ delete navigator.userAgent;
616
+ }
617
+ // Reset touchInProgress for any subsequent tests
618
+ document.body.dispatchEvent(new PointerEvent("pointerover", { bubbles: true, pointerType: "mouse" }));
619
+ }
620
+ });
621
+
622
+ // =========================================================================
623
+ section("6. Document Body Replacement (e.g. Turbo Drive)");
624
+ // =========================================================================
625
+
626
+ await test("Hover, focus, and Escape still work after document.body is replaced", async () => {
627
+ const originalBody = document.body;
628
+ const newBody = document.createElement("body");
629
+ newBody.innerHTML =
630
+ '<button id="swap_invoker" interestfor="swap_target">Invoker</button>' +
631
+ '<div id="swap_target" popover="auto">Target</div>';
632
+ originalBody.replaceWith(newBody);
633
+ try {
634
+ const invoker = document.getElementById("swap_invoker");
635
+ const target = document.getElementById("swap_target");
636
+ invoker.style.setProperty("--interest-delay-start", "10ms");
637
+ invoker.style.setProperty("--interest-delay-end", "10ms");
638
+ await nextTick();
639
+
640
+ hover(invoker);
641
+ await wait(25);
642
+ assert_true(target.matches(":popover-open"), "hover opens popover in replaced body");
643
+ dehover(invoker);
644
+ await wait(25);
645
+ assert_false(target.matches(":popover-open"), "dehover closes popover in replaced body");
646
+
647
+ focusIn(invoker);
648
+ await wait(25);
649
+ assert_true(target.matches(":popover-open"), "focus opens popover in replaced body");
650
+ pressEscape();
651
+ assert_false(target.matches(":popover-open"), "Escape closes popover in replaced body");
652
+ } finally {
653
+ newBody.replaceWith(originalBody);
654
+ }
655
+ });
575
656
  }
576
657
  </script>
577
658