interestfor 1.0.4 → 1.0.5

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/COPYRIGHT ADDED
@@ -0,0 +1,7 @@
1
+ // Copyright (c) 2025, Mason Freed
2
+ // All rights reserved.
3
+ //
4
+ // This source code is licensed under the BSD-style license found in the
5
+ // LICENSE file in the root directory of this source tree.
6
+
7
+
package/README.md CHANGED
@@ -29,18 +29,21 @@ upon receiving and losing interest.
29
29
  Again, this is a quick summary. For the complete details, please [read the
30
30
  explainer](https://open-ui.org/components/interest-invokers.explainer).
31
31
 
32
- ## Touchscreen not supported
33
-
34
- This polyfill will not provide touchscreen users with any way to show interest
35
- in the invoker, because on touchscreen, long-press is the user-understood way to
36
- "show interest" in an element. On today's browsers, long-pressing a link element
37
- brings up a useful context menu that contains items like "Share" and "Open in
38
- new tab". In order for the polyfill to support interest on mobile, that context
39
- menu would need to be surpressed completely. There is currently no way to
40
- provide access to the long-press context menu and *also* provide a way to show
41
- interest. Since the context menu contains items that users often use,
42
- eliminating access to the context menu is not what most developers want. So
43
- this polyfill does nothing on mobile touchscreen.
32
+ ## Touchscreen support
33
+
34
+ This polyfill provides limited touchscreen support. On a touchscreen,
35
+ long-pressing a `<button>` element that has an `interestfor` attribute will show
36
+ interest in the target.
37
+
38
+ However, long-pressing a link (`<a>` element) is **not** supported. This is
39
+ because on today's browsers, long-pressing a link element brings up a useful
40
+ context menu that contains items like "Share" and "Open in new tab". In order
41
+ for the polyfill to support interest on links, that context menu would need to
42
+ be surpressed completely. There is currently no way to provide access to the
43
+ long-press context menu and *also* provide a way to show interest. Since the
44
+ context menu contains items that users often use, eliminating access to the
45
+ context menu is not what most people want. So this polyfill will not support
46
+ long-press on links.
44
47
 
45
48
  This is one of the primary motivations for Chromium championing this as a
46
49
  native API in all browsers: to support all users, including those using
@@ -74,6 +77,18 @@ native feature:
74
77
  interest-delay-end: 200ms;
75
78
  --interest-delay-end: 200ms;
76
79
  }
80
+ /* Or, use the shorthand: */
81
+ [interestfor] {
82
+ interest-delay: 400ms 200ms;
83
+ --interest-delay: 400ms 200ms;
84
+ }
85
+ /* The keyword `normal` can also be used to specify the default delay: */
86
+ [interestfor].no-show-delay {
87
+ interest-delay-start: 0s;
88
+ --interest-delay-start: 0s;
89
+ interest-delay-end: normal;
90
+ --interest-delay-end: normal;
91
+ }
77
92
  ```
78
93
 
79
94
  - The CSS pseudo classes for detecting invoker and target interest states are
@@ -104,13 +119,6 @@ polyfill (optionally minified) or the native feature. You can run tests directly
104
119
  from this repo,
105
120
  [here](https://mfreed7.github.io/interestfor/test/test.html).
106
121
 
107
- ## Things that don't (yet?) work
108
-
109
- These things are currently not handled correctly by this polyfill:
110
-
111
- - There is no support for the `--interest-delay` shorthand.
112
- - There is no touchscreen support, as mentioned above.
113
-
114
122
  ## Improvements / Bugs
115
123
 
116
124
  If you find issues with the polyfill, feel free to file them [here](https://github.com/mfreed7/interestfor/issues).
package/build.sh ADDED
@@ -0,0 +1,20 @@
1
+ #!/bin/bash
2
+
3
+ # Exit on any error
4
+ set -e
5
+
6
+ BASE_DIR=$(pwd)
7
+ SRC_DIR="${BASE_DIR}/src"
8
+ BUILD_DIR="${BASE_DIR}/dist"
9
+ SOURCE_FILE="${SRC_DIR}/interestfor.js"
10
+ MINIFIED_FILE="${BUILD_DIR}/interestfor.min.js"
11
+ OUTPUT_FILE="${SRC_DIR}/interestfor.min.js"
12
+
13
+ # Minify the combined file
14
+ terser "${SOURCE_FILE}" --compress --mangle --output "${MINIFIED_FILE}"
15
+
16
+ cat COPYRIGHT "${MINIFIED_FILE}" > "${OUTPUT_FILE}"
17
+
18
+ rm "${MINIFIED_FILE}"
19
+
20
+ echo "--- Minified output to ${OUTPUT_FILE} ---"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "interestfor",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
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",
@@ -11,11 +11,13 @@
11
11
  const attributeName = "interestfor";
12
12
  const interestEventName = "interest";
13
13
  const loseInterestEventName = "loseinterest";
14
- const showDelayProp = "--interest-delay-start";
15
- const hideDelayProp = "--interest-delay-end";
14
+ const interestDelayStartProp = "--interest-delay-start";
15
+ const interestDelayEndProp = "--interest-delay-end";
16
+ const interestDelayProp = "--interest-delay";
16
17
  const dataField = "__interestForData";
17
18
  const targetDataField = "__interestForTargetData";
18
19
  const invokersWithInterest = new Set();
20
+ let touchInProgress = false;
19
21
 
20
22
  // Feature detection
21
23
  if (window.interestForPolyfillInstalled) {
@@ -53,6 +55,7 @@
53
55
  DeHover: "dehover",
54
56
  Focus: "focus",
55
57
  Blur: "blur",
58
+ Touch: "touch",
56
59
  };
57
60
 
58
61
  // Gain or lose interest
@@ -100,7 +103,7 @@
100
103
 
101
104
  // Schedule tasks
102
105
  function ScheduleInterestGainedTask(invoker, newState) {
103
- const delay = getDelaySeconds(invoker, showDelayProp) * 1000;
106
+ const delay = getDelaySeconds(invoker, interestDelayStartProp) * 1000;
104
107
  if (!isFinite(delay) || delay < 0) {
105
108
  return;
106
109
  }
@@ -111,7 +114,7 @@
111
114
  }
112
115
 
113
116
  function ScheduleInterestLostTask(invoker) {
114
- const delay = getDelaySeconds(invoker, hideDelayProp) * 1000;
117
+ const delay = getDelaySeconds(invoker, interestDelayEndProp) * 1000;
115
118
  if (!isFinite(delay) || delay < 0) {
116
119
  return;
117
120
  }
@@ -137,13 +140,63 @@
137
140
  return document.getElementById(id);
138
141
  }
139
142
 
143
+ function onPopoverToggle(e) {
144
+ const popover = e.target;
145
+ if (e.newState === 'closed') {
146
+ const invoker = popover[targetDataField]?.invoker;
147
+ if (invoker) {
148
+ GainOrLoseInterest(invoker, popover, InterestState.NoInterest);
149
+ }
150
+ }
151
+ }
152
+
153
+ function parseTimeValue(val) {
154
+ const s = String(val).trim();
155
+ const m_s = s.match(/^([\d.]+)s$/);
156
+ if (m_s) {
157
+ return parseFloat(m_s[1]);
158
+ }
159
+ const m_ms = s.match(/^([\d.]+)ms$/);
160
+ if (m_ms) {
161
+ return parseFloat(m_ms[1]) / 1000;
162
+ }
163
+ return parseFloat(s) || 0;
164
+ }
165
+
140
166
  function getDelaySeconds(el, prop) {
141
- const raw = getComputedStyle(el).getPropertyValue(prop).trim();
142
- const m = raw.match(/^([\d.]+)s$/);
143
- if (m) {
144
- return parseFloat(m[1]);
167
+ const style = getComputedStyle(el);
168
+ const longhandValue = style.getPropertyValue(prop).trim();
169
+
170
+ // Longhand has priority. If it's not 'normal', use it.
171
+ if (longhandValue.toLowerCase() !== 'normal') {
172
+ return parseTimeValue(longhandValue);
173
+ }
174
+
175
+ // Longhand is 'normal', so check shorthand. This isn't exactly │
176
+ // correct, since the longhand might have been explicitly set to │
177
+ // 'normal'.
178
+ const shorthand = style.getPropertyValue(interestDelayProp).trim();
179
+ if (shorthand && shorthand.toLowerCase() !== 'normal') {
180
+ const parts = shorthand.split(/\s+/).filter((s) => s.length > 0);
181
+ if (parts.length > 0) {
182
+ const firstValue = parts[0];
183
+ const secondValue = parts.length > 1 ? parts[1] : firstValue;
184
+ let valueFromShorthand;
185
+ if (prop === interestDelayStartProp) {
186
+ valueFromShorthand = firstValue;
187
+ } else { // prop === interestDelayEndProp
188
+ valueFromShorthand = secondValue;
189
+ }
190
+
191
+ // If the value from the shorthand is not 'normal', use it.
192
+ if (valueFromShorthand.toLowerCase() !== 'normal') {
193
+ return parseTimeValue(valueFromShorthand);
194
+ }
195
+ }
145
196
  }
146
- return parseFloat(raw) || 0;
197
+
198
+ // If we got here, the effective value is 'normal'. Return the default.
199
+ return prop === interestDelayStartProp ? 0.5 : 0.25;
147
200
  }
148
201
 
149
202
  // Actual state transitions
@@ -164,16 +217,22 @@
164
217
  target[targetDataField] = {};
165
218
  }
166
219
  target[targetDataField].invoker = invoker;
220
+ if (target.hasAttribute('popover')) {
221
+ target[targetDataField].toggleListener = onPopoverToggle;
222
+ target.addEventListener('toggle', onPopoverToggle);
223
+ }
167
224
  invokersWithInterest.add(invoker);
168
225
  invoker.classList.add("interest-source");
169
226
  target.classList.add("interest-target");
227
+ if (!isPlainHint(target)) {
228
+ invoker.setAttribute("aria-expanded", "true");
229
+ }
170
230
  const anchorName = `--interest-anchor-${Math.random()
171
231
  .toString(36)
172
232
  .substring(2)}`;
173
233
  invoker.style.anchorName = anchorName;
174
234
  target.style.positionAnchor = anchorName;
175
235
  data.anchorName = anchorName;
176
- restoreFocusable(target);
177
236
  break;
178
237
  default:
179
238
  throw new Error("Invalid state");
@@ -191,16 +250,21 @@
191
250
  try {
192
251
  target.hidePopover();
193
252
  } catch {}
253
+ if (target[targetDataField]?.toggleListener) {
254
+ target.removeEventListener('toggle', target[targetDataField].toggleListener);
255
+ }
194
256
  target[targetDataField] = null;
195
257
  invokersWithInterest.delete(invoker);
196
258
  invoker.classList.remove("interest-source");
197
259
  target.classList.remove("interest-target");
260
+ if (!isPlainHint(target)) {
261
+ invoker.setAttribute("aria-expanded", "false");
262
+ }
198
263
  if (data.anchorName) {
199
264
  invoker.style.anchorName = "";
200
265
  target.style.positionAnchor = "";
201
266
  data.anchorName = null;
202
267
  }
203
- restoreFocusable(target);
204
268
  data.state = InterestState.NoInterest;
205
269
  }
206
270
  }
@@ -219,29 +283,71 @@
219
283
  "[contenteditable]",
220
284
  '[tabindex]:not([tabindex="-1"])',
221
285
  ].join(",");
222
- function disableFocusable(root) {
223
- root.querySelectorAll(focusableSelector).forEach((el) => {
224
- if (el.hasAttribute("data-original-tabindex")) {
225
- return;
286
+
287
+ // Accessibility utilities
288
+ function isPlainHint(target) {
289
+ if (target.getAttribute("popover")?.toLowerCase() !== "hint") {
290
+ return false;
291
+ }
292
+ // Anything focusable is not plain.
293
+ if (target.querySelector(focusableSelector)) {
294
+ return false;
295
+ }
296
+ // Common structural/semantic elements that are not focusable.
297
+ const structuralSelector =
298
+ "h1,h2,h3,h4,h5,h6,ul,ol,li,table,nav,header,footer,main,aside,article,section,form,blockquote,details,summary,dialog";
299
+ if (target.querySelector(structuralSelector)) {
300
+ return false;
301
+ }
302
+ // Check for ARIA roles.
303
+ const elementsWithRoles = target.querySelectorAll("[role]");
304
+ for (const el of elementsWithRoles) {
305
+ const role = el.getAttribute("role").toLowerCase();
306
+ // Allowed roles are those that don't add semantics for a screen reader to explore.
307
+ if (!["presentation", "none", "generic", "image"].includes(role)) {
308
+ return false;
226
309
  }
227
- const orig = el.getAttribute("tabindex");
228
- el.setAttribute("data-original-tabindex", orig === null ? "none" : orig);
229
- el.setAttribute("tabindex", "-1");
230
- });
310
+ }
311
+ return true;
231
312
  }
232
- function restoreFocusable(root) {
233
- root.querySelectorAll("[data-original-tabindex]").forEach((el) => {
234
- const orig = el.getAttribute("data-original-tabindex");
235
- if (orig === "none") {
236
- el.removeAttribute("tabindex");
237
- } else {
238
- el.setAttribute("tabindex", orig);
313
+
314
+ function setupAccessibility(invoker, target) {
315
+ if (isPlainHint(target)) {
316
+ invoker.setAttribute("aria-describedby", target.id);
317
+ } else {
318
+ // Rich hint
319
+ invoker.setAttribute("aria-details", target.id);
320
+ invoker.setAttribute("aria-expanded", "false");
321
+ if (!target.hasAttribute("role")) {
322
+ target.setAttribute("role", "tooltip");
239
323
  }
240
- el.removeAttribute("data-original-tabindex");
241
- });
324
+ }
325
+ }
326
+
327
+ function initializeDataField(el) {
328
+ if (el[dataField]) return;
329
+ el[dataField] = {
330
+ state: InterestState.NoInterest,
331
+ gainedTimer: null,
332
+ lostTimer: null,
333
+ longPressTimer: null,
334
+ clearGainedTask() {
335
+ clearTimeout(this.gainedTimer);
336
+ },
337
+ clearLostTask() {
338
+ clearTimeout(this.lostTimer);
339
+ },
340
+ };
341
+ const target = GetInterestForTarget(el);
342
+ if (target) {
343
+ setupAccessibility(el, target);
344
+ }
242
345
  }
243
346
 
244
347
  function HandleInterestHoverOrFocus(el, source) {
348
+ if (touchInProgress) {
349
+ return;
350
+ }
245
351
  if (!el.isConnected) {
246
352
  return;
247
353
  }
@@ -262,21 +368,8 @@
262
368
  }
263
369
  return;
264
370
  }
265
- let data = el[dataField];
266
- if (!data) {
267
- el[dataField] = {
268
- state: InterestState.NoInterest,
269
- gainedTimer: null,
270
- lostTimer: null,
271
- clearGainedTask() {
272
- clearTimeout(this.gainedTimer);
273
- },
274
- clearLostTask() {
275
- clearTimeout(this.lostTimer);
276
- },
277
- };
278
- data = el[dataField];
279
- }
371
+ initializeDataField(el);
372
+ const data = el[dataField];
280
373
  const upstreamInvoker = GetInterestInvoker(el);
281
374
 
282
375
  // Hover or focus
@@ -318,18 +411,43 @@
318
411
  document.body.addEventListener("keydown", (e) => {
319
412
  if (e.key === "Escape") {
320
413
  invokersWithInterest.forEach((invoker) => {
321
- e.preventDefault();
322
414
  clearState(invoker);
323
415
  });
324
416
  }
325
417
  });
418
+ // Touch support
419
+ const longPressTime = 500;
420
+ document.body.addEventListener("touchstart", (e) => {
421
+ touchInProgress = true;
422
+ const invoker = e.target.closest("button[interestfor]");
423
+ if (invoker) {
424
+ initializeDataField(invoker);
425
+ invoker[dataField].longPressTimer = setTimeout(() => {
426
+ GainOrLoseInterest(invoker, GetInterestForTarget(invoker), InterestState.FullInterest);
427
+ invoker[dataField].longPressTimer = null;
428
+ }, longPressTime);
429
+ }
430
+ });
431
+ const cancelLongPress = (e) => {
432
+ const invoker = e.target.closest("button[interestfor]");
433
+ if (invoker && invoker[dataField]?.longPressTimer) {
434
+ clearTimeout(invoker[dataField].longPressTimer);
435
+ invoker[dataField].longPressTimer = null;
436
+ }
437
+ };
438
+ document.body.addEventListener("touchend", (e) => {
439
+ cancelLongPress(e);
440
+ touchInProgress = false;
441
+ });
442
+ document.body.addEventListener("touchmove", cancelLongPress);
326
443
  }
327
444
 
328
445
  // CSS registration
329
446
  function registerCustomProperties() {
330
447
  const style = document.createElement("style");
331
- style.textContent = `@property ${showDelayProp} {syntax: "<time>"; inherits: false; initial-value: 0.5s;}
332
- @property ${hideDelayProp} {syntax: "<time>"; inherits: false; initial-value: 0.5s;}`;
448
+ style.textContent = `@property ${interestDelayStartProp} {syntax: "normal | <time>"; inherits: false; initial-value: normal;}
449
+ @property ${interestDelayEndProp} {syntax: "normal | <time>"; inherits: false; initial-value: normal;}
450
+ @property ${interestDelayProp} {syntax: "[ normal | <time> ]{1,2}"; inherits: false; initial-value: normal;}`;
333
451
  document.head.appendChild(style);
334
452
  document[dataField] = { globalPropsStyle: style };
335
453
  }
@@ -1 +1,8 @@
1
- !function(){const e="interestfor",t="interest",n="loseinterest",r="--interest-delay-start",o="--interest-delay-end",s="__interestForData",i="__interestForTargetData",a=new Set;if(window.interestForPolyfillInstalled)return;window.interestForPolyfillInstalled=!0;const l=HTMLButtonElement.prototype.hasOwnProperty("interestForElement");if(l&&!window.interestForUsePolyfillAlways)return;if(l){const e=e=>{e.isTrusted&&(e.preventDefault(),e.stopImmediatePropagation())};document.body.addEventListener(t,e,{capture:!0}),document.body.addEventListener(n,e,{capture:!0})}const d={NoInterest:"none",FullInterest:"full"},c={Hover:"hover",DeHover:"dehover",Focus:"focus",Blur:"blur"};function u(e,n,r){if(!e||!n)return!1;if(!e.isConnected||v(e)!==n||r===d.NoInterest&&m(n)!==e)return!1;if(r!==d.NoInterest){const o=m(n);if(o){if(o===e)return o[s].clearLostTask(),!1;if(!u(o,n,d.NoInterest))return!1;if(!e.isConnected||v(e)!==n)return!1}return function(e,n){const r=e[s],o=v(e);if(n!==d.FullInterest)throw new Error("Invalid state");if(r.state!==d.NoInterest)throw new Error("Invalid state");o.dispatchEvent(new Event(t));try{o.showPopover({source:e})}catch{}r.state=d.FullInterest,o[i]||(o[i]={}),o[i].invoker=e,a.add(e),e.classList.add("interest-source"),o.classList.add("interest-target"),p(o);return!0}(e,r)}return b(e),!0}function f(e){const t=1e3*y(e,o);!isFinite(t)||t<0||(e[s].clearLostTask(),e[s].lostTimer=setTimeout((()=>{u(e,v(e),d.NoInterest)}),t))}function m(e){const t=e[i]?.invoker||null;return t&&t[s]?.state!==d.NoInterest?t:null}function v(t){const n=t.getAttribute(e);return document.getElementById(n)}function y(e,t){const n=getComputedStyle(e).getPropertyValue(t).trim(),r=n.match(/^([\d.]+)s$/);return r?parseFloat(r[1]):parseFloat(n)||0}function b(e){const t=e[s];if(clearTimeout(t.gainedTimer),clearTimeout(t.lostTimer),t.state!==d.NoInterest){const r=v(e);r.dispatchEvent(new Event(n));try{r.hidePopover()}catch{}r[i]=null,a.delete(e),e.classList.remove("interest-source"),r.classList.remove("interest-target"),p(r),t.state=d.NoInterest}}["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 p(e){e.querySelectorAll("[data-original-tabindex]").forEach((e=>{const t=e.getAttribute("data-original-tabindex");"none"===t?e.removeAttribute("tabindex"):e.setAttribute("tabindex",t),e.removeAttribute("data-original-tabindex")}))}function T(e,t){if(!e.isConnected)return;if(!v(e))return;let n=e[s];n||(e[s]={state:d.NoInterest,gainedTimer:null,lostTimer:null,clearGainedTask(){clearTimeout(this.gainedTimer)},clearLostTask(){clearTimeout(this.lostTimer)}},n=e[s]);const o=m(e);t===c.Hover||t===c.Focus?(n.clearLostTask&&n.clearLostTask(),o&&o[s].clearLostTask(),function(e,t){const n=1e3*y(e,r);!isFinite(n)||n<0||(e[s].clearGainedTask(),e[s].gainedTimer=setTimeout((()=>{u(e,v(e),t)}),n))}(e,d.FullInterest)):(n.clearGainedTask&&n.clearGainedTask(),n.state!==d.NoInterest&&f(e),o&&(o[s].clearGainedTask(),t!==c.Blur&&e.matches(":hover")||f(o)))}function h(){!function(){const e=document.createElement("style");e.textContent=`@property ${r} {syntax: "<time>"; inherits: false; initial-value: 0.5s;}\n @property ${o} {syntax: "<time>"; inherits: false; initial-value: 0.5s;}`,document.head.appendChild(e),document[s]={globalPropsStyle:e}}(),document.body.addEventListener("mouseover",(e=>T(e.target,c.Hover))),document.body.addEventListener("mouseout",(e=>T(e.target,c.DeHover))),document.body.addEventListener("focusin",(e=>T(e.target,c.Focus))),document.body.addEventListener("focusout",(e=>T(e.target,c.Blur))),document.body.addEventListener("keydown",(e=>{"Escape"===e.key&&a.forEach((t=>{e.preventDefault(),b(t)}))})),console.log(`interestfor polyfill installed (native: ${l}).`)}"complete"===document.readyState?h():window.addEventListener("load",h)}();
1
+ // Copyright (c) 2025, Mason Freed
2
+ // All rights reserved.
3
+ //
4
+ // This source code is licensed under the BSD-style license found in the
5
+ // LICENSE file in the root directory of this source tree.
6
+
7
+
8
+ !function(){const e="interest",t="loseinterest",n="--interest-delay-start",r="--interest-delay-end",o="--interest-delay",i="__interestForData",s="__interestForTargetData",a=new Set;let l=!1;if(window.interestForPolyfillInstalled)return;window.interestForPolyfillInstalled=!0;const c=HTMLButtonElement.prototype.hasOwnProperty("interestForElement");if(c&&!window.interestForUsePolyfillAlways)return;if(c){const n=e=>{e.isTrusted&&(e.preventDefault(),e.stopImmediatePropagation())};document.body.addEventListener(e,n,{capture:!0}),document.body.addEventListener(t,n,{capture:!0})}const d="none",u="full",m="hover",f="dehover",h="focus",g="blur";function y(t,n,r){if(!t||!n)return!1;if(!t.isConnected||b(t)!==n||r===d&&v(n)!==t)return!1;if(r!==d){const o=v(n);if(o){if(o===t)return o[i].clearLostTask(),!1;if(!y(o,n,d))return!1;if(!t.isConnected||b(t)!==n)return!1}return function(t,n){const r=t[i],o=b(t);if(n!==u)throw new Error("Invalid state");{if(r.state!==d)throw new Error("Invalid state");o.dispatchEvent(new Event(e));try{o.showPopover({source:t})}catch{}r.state=u,o[s]||(o[s]={}),o[s].invoker=t,o.hasAttribute("popover")&&(o[s].toggleListener=T,o.addEventListener("toggle",T)),a.add(t),t.classList.add("interest-source"),o.classList.add("interest-target"),P(o)||t.setAttribute("aria-expanded","true");const n=`--interest-anchor-${Math.random().toString(36).substring(2)}`;t.style.anchorName=n,o.style.positionAnchor=n,r.anchorName=n}return!0}(t,r)}return E(t),!0}function p(e){const t=1e3*w(e,r);!isFinite(t)||t<0||(e[i].clearLostTask(),e[i].lostTimer=setTimeout(()=>{y(e,b(e),d)},t))}function v(e){const t=e[s]?.invoker||null;return t&&t[i]?.state!==d?t:null}function b(e){const t=e.getAttribute("interestfor");return document.getElementById(t)}function T(e){const t=e.target;if("closed"===e.newState){const e=t[s]?.invoker;e&&y(e,t,d)}}function L(e){const t=String(e).trim(),n=t.match(/^([\d.]+)s$/);if(n)return parseFloat(n[1]);const r=t.match(/^([\d.]+)ms$/);return r?parseFloat(r[1])/1e3:parseFloat(t)||0}function w(e,t){const r=getComputedStyle(e),i=r.getPropertyValue(t).trim();if("normal"!==i.toLowerCase())return L(i);const s=r.getPropertyValue(o).trim();if(s&&"normal"!==s.toLowerCase()){const e=s.split(/\s+/).filter(e=>e.length>0);if(e.length>0){const r=e[0],o=e.length>1?e[1]:r;let i;if(i=t===n?r:o,"normal"!==i.toLowerCase())return L(i)}}return t===n?.5:.25}function E(e){const n=e[i];if(clearTimeout(n.gainedTimer),clearTimeout(n.lostTimer),n.state!==d){const r=b(e);r.dispatchEvent(new Event(t));try{r.hidePopover()}catch{}r[s]?.toggleListener&&r.removeEventListener("toggle",r[s].toggleListener),r[s]=null,a.delete(e),e.classList.remove("interest-source"),r.classList.remove("interest-target"),P(r)||e.setAttribute("aria-expanded","false"),n.anchorName&&(e.style.anchorName="",r.style.positionAnchor="",n.anchorName=null),n.state=d}}const k=["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 P(e){if("hint"!==e.getAttribute("popover")?.toLowerCase())return!1;if(e.querySelector(k))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 A(e){if(e[i])return;e[i]={state:d,gainedTimer:null,lostTimer:null,longPressTimer:null,clearGainedTask(){clearTimeout(this.gainedTimer)},clearLostTask(){clearTimeout(this.lostTimer)}};const t=b(e);t&&function(e,t){P(t)?e.setAttribute("aria-describedby",t.id):(e.setAttribute("aria-details",t.id),e.setAttribute("aria-expanded","false"),t.hasAttribute("role")||t.setAttribute("role","tooltip"))}(e,t)}function C(e,t){if(l)return;if(!e.isConnected)return;if(!b(e)){const n=e.closest(".interest-target");if(n){const r=v(n);r&&(t===m||t===h?r[i].clearLostTask():t!==g&&e.matches(":hover")||p(r))}return}A(e);const r=e[i],o=v(e);t===m||t===h?(r.clearLostTask&&r.clearLostTask(),o&&o[i].clearLostTask(),function(e,t){const r=1e3*w(e,n);!isFinite(r)||r<0||(e[i].clearGainedTask(),e[i].gainedTimer=setTimeout(()=>{y(e,b(e),t)},r))}(e,u)):(r.clearGainedTask&&r.clearGainedTask(),r.state!==d&&p(e),o&&(o[i].clearGainedTask(),t!==g&&e.matches(":hover")||p(o)))}function F(){!function(){const e=document.createElement("style");e.textContent=`@property ${n} {syntax: "normal | <time>"; inherits: false; initial-value: normal;}\n @property ${r} {syntax: "normal | <time>"; inherits: false; initial-value: normal;}\n @property ${o} {syntax: "[ normal | <time> ]{1,2}"; inherits: false; initial-value: normal;}`,document.head.appendChild(e),document[i]={globalPropsStyle:e}}(),function(){document.body.addEventListener("mouseover",e=>C(e.target,m)),document.body.addEventListener("mouseout",e=>C(e.target,f)),document.body.addEventListener("focusin",e=>C(e.target,h)),document.body.addEventListener("focusout",e=>C(e.target,g)),document.body.addEventListener("keydown",e=>{"Escape"===e.key&&a.forEach(e=>{E(e)})}),document.body.addEventListener("touchstart",e=>{l=!0;const t=e.target.closest("button[interestfor]");t&&(A(t),t[i].longPressTimer=setTimeout(()=>{y(t,b(t),u),t[i].longPressTimer=null},500))});const e=e=>{const t=e.target.closest("button[interestfor]");t&&t[i]?.longPressTimer&&(clearTimeout(t[i].longPressTimer),t[i].longPressTimer=null)};document.body.addEventListener("touchend",t=>{e(t),l=!1}),document.body.addEventListener("touchmove",e)}(),console.log(`interestfor polyfill installed (native: ${c}).`)}"complete"===document.readyState?F():window.addEventListener("load",F)}();
@@ -3,4 +3,20 @@
3
3
  <script src="../src/interestfor.js" async></script>
4
4
 
5
5
  <a interestfor=foo href=bar>Link - hover me, don't click me</a>
6
+ <button interestfor=foo>Button - hover me, don't click me</a>
6
7
  <div popover id=foo>Popover</div>
8
+
9
+ <style>
10
+ [interestfor] {
11
+ interest-delay: 600ms 300ms;
12
+ --interest-delay: 600ms 300ms;
13
+ }
14
+ :is([interestfor]:interest-source),
15
+ [interestfor].interest-source {
16
+ background-color: lightgreen;
17
+ }
18
+ :is([popover]:interest-target),
19
+ [popover].interest-target {
20
+ background-color: lightblue;
21
+ }
22
+ </style>
package/test/test.html CHANGED
@@ -224,13 +224,22 @@ async function waitForHoverOrFocus(type,element,target,timeMilliseconds) {
224
224
  return {midEvents, midTargetOpen, endEvents: [...events], endTargetOpen: target.matches(':popover-open')};
225
225
  }
226
226
 
227
- async function testOne(invoker, target, type, description) {
227
+ async function testOne(invoker, target, type, description, useShorthand = false) {
228
228
  assert_true(true,'---' + description,true);
229
229
  assert_false(target.matches(':popover-open'),'popover should start closed',true);
230
- invoker.style.setProperty('--interest-delay-start',hoverTimeMilliseconds + 'ms');
231
- invoker.style.setProperty('interest-delay-start',hoverTimeMilliseconds + 'ms');
232
- invoker.style.setProperty('--interest-delay-end',hoverTimeMilliseconds + 'ms');
233
- invoker.style.setProperty('interest-delay-end',hoverTimeMilliseconds + 'ms');
230
+ if (useShorthand) {
231
+ invoker.style.setProperty('--interest-delay', `${hoverTimeMilliseconds}ms ${hoverTimeMilliseconds}ms`);
232
+ invoker.style.setProperty('interest-delay', `${hoverTimeMilliseconds}ms ${hoverTimeMilliseconds}ms`);
233
+ invoker.style.removeProperty('--interest-delay-start');
234
+ invoker.style.removeProperty('interest-delay-start');
235
+ invoker.style.removeProperty('--interest-delay-end');
236
+ invoker.style.removeProperty('interest-delay-end');
237
+ } else {
238
+ invoker.style.setProperty('--interest-delay-start',hoverTimeMilliseconds + 'ms');
239
+ invoker.style.setProperty('interest-delay-start',hoverTimeMilliseconds + 'ms');
240
+ invoker.style.setProperty('--interest-delay-end',hoverTimeMilliseconds + 'ms');
241
+ invoker.style.setProperty('interest-delay-end',hoverTimeMilliseconds + 'ms');
242
+ }
234
243
  invoker.classList.remove('hide');
235
244
 
236
245
  // Show interest in the invoker
@@ -293,6 +302,20 @@ async function testHarness() {
293
302
  await testOne(dynamic_invoker,dynamic_target,type,'Dynamically-added elements');
294
303
  dynamic_invoker.remove();
295
304
  dynamic_target.remove();
305
+
306
+ // Dynamically add elements for shorthand test:
307
+ const shorthand_invoker = document.createElement('a');
308
+ shorthand_invoker.href = 'foo';
309
+ shorthand_invoker.setAttribute('interestfor','shorthand_target');
310
+ shorthand_invoker.classList='hide test-element';
311
+ const shorthand_target = document.createElement('div');
312
+ shorthand_target.popover = 'auto';
313
+ shorthand_target.id = 'shorthand_target';
314
+ shorthand_target.classList = 'hide';
315
+ targets.append(shorthand_invoker,shorthand_target);
316
+ await testOne(shorthand_invoker,shorthand_target,type,'Dynamically-added elements (shorthand)', true);
317
+ shorthand_invoker.remove();
318
+ shorthand_target.remove();
296
319
  }
297
320
 
298
321
  // Test complete