interestfor 1.0.13 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "interestfor",
3
- "version": "1.0.13",
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",
@@ -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
  }
@@ -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",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)}();
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)}();
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}`));