interestfor 1.0.3 → 1.0.4

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
@@ -54,7 +54,7 @@ browsers:
54
54
 
55
55
  ## Implementation
56
56
  The polyfill adds event listeners to document.body that monitor for hover
57
- or keyboard events relevant fo the `interestfor` API. When interesting
57
+ or keyboard events relevant for the `interestfor` API. When interesting
58
58
  events happen to elements with the `interestfor` attribute or elements that
59
59
  are the targets of those elements, it fires `interest` and `loseinterest`
60
60
  events on the target of the `interestfor` attribute. If the
@@ -69,10 +69,10 @@ native feature:
69
69
 
70
70
  ```css
71
71
  [interestfor] {
72
- interest-show-delay: 400ms;
73
- --interest-show-delay: 400ms;
74
- interest-hide-delay: 200ms;
75
- --interest-hide-delay: 200ms;
72
+ interest-delay-start: 400ms;
73
+ --interest-delay-start: 400ms;
74
+ interest-delay-end: 200ms;
75
+ --interest-delay-end: 200ms;
76
76
  }
77
77
  ```
78
78
 
@@ -80,19 +80,19 @@ native feature:
80
80
  implemented as regular class names, so again add both to your stylesheet:
81
81
 
82
82
  ```css
83
- :is([interestfor]:has-interest),
84
- [interestfor].has-interest {
83
+ :is([interestfor]:interest-source),
84
+ [interestfor].interest-source {
85
85
  /* invoker styles */
86
86
  }
87
- :is([popover]:target-of-interest),
88
- [popover].target-of-interest {
87
+ :is([popover]:interest-target),
88
+ [popover].interest-target {
89
89
  /* target popover styles */
90
90
  }
91
91
  ```
92
92
 
93
93
  Note the `:is()` wrapped around the "normal" selector for the real pseudo
94
94
  class names. This is to avoid the entire selector being invalidated if the
95
- browser does not recognize the native pseudo class (e.g. `:has-interest`).
95
+ browser does not recognize the native pseudo class (e.g. `:interest-source`).
96
96
 
97
97
 
98
98
  ## Tests
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "interestfor",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
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,19 +11,19 @@
11
11
  const attributeName = "interestfor";
12
12
  const interestEventName = "interest";
13
13
  const loseInterestEventName = "loseinterest";
14
- const showDelayProp = "--interest-show-delay";
15
- const hideDelayProp = "--interest-hide-delay";
14
+ const showDelayProp = "--interest-delay-start";
15
+ const hideDelayProp = "--interest-delay-end";
16
16
  const dataField = "__interestForData";
17
17
  const targetDataField = "__interestForTargetData";
18
+ const invokersWithInterest = new Set();
18
19
 
19
20
  // Feature detection
20
21
  if (window.interestForPolyfillInstalled) {
21
22
  return;
22
23
  }
23
24
  window.interestForPolyfillInstalled = true;
24
- const nativeSupported = HTMLButtonElement.prototype.hasOwnProperty(
25
- "interestForElement"
26
- );
25
+ const nativeSupported =
26
+ HTMLButtonElement.prototype.hasOwnProperty("interestForElement");
27
27
  if (nativeSupported && !window.interestForUsePolyfillAlways) {
28
28
  return;
29
29
  }
@@ -34,15 +34,18 @@
34
34
  e.preventDefault();
35
35
  e.stopImmediatePropagation();
36
36
  }
37
- }
38
- document.body.addEventListener(interestEventName,cancel,{capture:true});
39
- document.body.addEventListener(loseInterestEventName,cancel,{capture:true});
37
+ };
38
+ document.body.addEventListener(interestEventName, cancel, {
39
+ capture: true,
40
+ });
41
+ document.body.addEventListener(loseInterestEventName, cancel, {
42
+ capture: true,
43
+ });
40
44
  }
41
45
 
42
46
  // Enum-like state and source
43
47
  const InterestState = {
44
48
  NoInterest: "none",
45
- PartialInterest: "partial",
46
49
  FullInterest: "full",
47
50
  };
48
51
  const Source = {
@@ -78,7 +81,10 @@
78
81
  return false;
79
82
  }
80
83
  // re-check preconditions
81
- if (!invoker.isConnected || GetInterestForTarget(invoker) !== target) {
84
+ if (
85
+ !invoker.isConnected ||
86
+ GetInterestForTarget(invoker) !== target
87
+ ) {
82
88
  return false;
83
89
  }
84
90
  }
@@ -145,47 +151,32 @@
145
151
  const data = invoker[dataField];
146
152
  const target = GetInterestForTarget(invoker);
147
153
  switch (newState) {
148
- case InterestState.PartialInterest:
149
- if (data.state === InterestState.NoInterest) {
150
- if (!target.dispatchEvent(new Event(interestEventName))) {
151
- return;
152
- }
153
- try {
154
- target.showPopover();
155
- } catch {}
156
- }
157
- data.state = InterestState.PartialInterest;
158
- if (!target[targetDataField]) {
159
- target[targetDataField] = {};
160
- }
161
- target[targetDataField].invoker = invoker;
162
- invoker.classList.add("has-partial-interest", "has-interest");
163
- target.classList.add(
164
- "target-of-partial-interest",
165
- "target-of-interest"
166
- );
167
- disableFocusable(target);
168
- break;
169
154
  case InterestState.FullInterest:
170
- if (data.state !== InterestState.PartialInterest) {
171
- if (!target.dispatchEvent(new Event(interestEventName))) {
172
- return;
173
- }
174
- try {
175
- target.showPopover();
176
- } catch {}
155
+ if (data.state !== InterestState.NoInterest) {
156
+ throw new Error("Invalid state");
177
157
  }
158
+ target.dispatchEvent(new Event(interestEventName));
159
+ try {
160
+ target.showPopover({ source: invoker });
161
+ } catch {}
178
162
  data.state = InterestState.FullInterest;
179
163
  if (!target[targetDataField]) {
180
164
  target[targetDataField] = {};
181
165
  }
182
166
  target[targetDataField].invoker = invoker;
183
- invoker.classList.remove("has-partial-interest");
184
- invoker.classList.add("has-interest");
185
- target.classList.remove("target-of-partial-interest");
186
- target.classList.add("target-of-interest");
167
+ invokersWithInterest.add(invoker);
168
+ invoker.classList.add("interest-source");
169
+ target.classList.add("interest-target");
170
+ const anchorName = `--interest-anchor-${Math.random()
171
+ .toString(36)
172
+ .substring(2)}`;
173
+ invoker.style.anchorName = anchorName;
174
+ target.style.positionAnchor = anchorName;
175
+ data.anchorName = anchorName;
187
176
  restoreFocusable(target);
188
177
  break;
178
+ default:
179
+ throw new Error("Invalid state");
189
180
  }
190
181
  return true;
191
182
  }
@@ -196,18 +187,19 @@
196
187
  clearTimeout(data.lostTimer);
197
188
  if (data.state !== InterestState.NoInterest) {
198
189
  const target = GetInterestForTarget(invoker);
199
- if (!target.dispatchEvent(new Event(loseInterestEventName))) {
200
- return;
201
- }
190
+ target.dispatchEvent(new Event(loseInterestEventName));
202
191
  try {
203
192
  target.hidePopover();
204
193
  } catch {}
205
194
  target[targetDataField] = null;
206
- invoker.classList.remove("has-partial-interest", "has-interest");
207
- target.classList.remove(
208
- "target-of-partial-interest",
209
- "target-of-interest"
210
- );
195
+ invokersWithInterest.delete(invoker);
196
+ invoker.classList.remove("interest-source");
197
+ target.classList.remove("interest-target");
198
+ if (data.anchorName) {
199
+ invoker.style.anchorName = "";
200
+ target.style.positionAnchor = "";
201
+ data.anchorName = null;
202
+ }
211
203
  restoreFocusable(target);
212
204
  data.state = InterestState.NoInterest;
213
205
  }
@@ -255,6 +247,19 @@
255
247
  }
256
248
  const target = GetInterestForTarget(el);
257
249
  if (!target) {
250
+ const containingTarget = el.closest(".interest-target");
251
+ if (containingTarget) {
252
+ const upstreamInvoker = GetInterestInvoker(containingTarget);
253
+ if (upstreamInvoker) {
254
+ if (source === Source.Hover || source === Source.Focus) {
255
+ upstreamInvoker[dataField].clearLostTask();
256
+ } else {
257
+ if (source === Source.Blur || !el.matches(":hover")) {
258
+ ScheduleInterestLostTask(upstreamInvoker);
259
+ }
260
+ }
261
+ }
262
+ }
258
263
  return;
259
264
  }
260
265
  let data = el[dataField];
@@ -279,21 +284,8 @@
279
284
  data.clearLostTask && data.clearLostTask();
280
285
  if (upstreamInvoker) {
281
286
  upstreamInvoker[dataField].clearLostTask();
282
- if (
283
- upstreamInvoker[dataField].state === InterestState.PartialInterest
284
- ) {
285
- applyState(upstreamInvoker, InterestState.FullInterest);
286
- }
287
287
  }
288
- const needsPartialInterest =
289
- source === Source.Focus &&
290
- target.matches(":has(" + focusableSelector + ")");
291
- ScheduleInterestGainedTask(
292
- el,
293
- needsPartialInterest
294
- ? InterestState.PartialInterest
295
- : InterestState.FullInterest
296
- );
288
+ ScheduleInterestGainedTask(el, InterestState.FullInterest);
297
289
  } else {
298
290
  // Dehover or blur
299
291
  data.clearGainedTask && data.clearGainedTask();
@@ -324,24 +316,11 @@
324
316
  HandleInterestHoverOrFocus(e.target, Source.Blur)
325
317
  );
326
318
  document.body.addEventListener("keydown", (e) => {
327
- let data = e.target[dataField];
328
- if (!data) {
329
- return;
330
- }
331
- if (data.state !== InterestState.PartialInterest) {
332
- return;
333
- }
334
- if (
335
- e.key === "ArrowUp" &&
336
- e.altKey &&
337
- data.state === InterestState.PartialInterest
338
- ) {
339
- e.preventDefault();
340
- applyState(e.target, InterestState.FullInterest);
341
- }
342
- if (e.key === "Escape" && data.state !== InterestState.NoInterest) {
343
- e.preventDefault();
344
- clearState(e.target);
319
+ if (e.key === "Escape") {
320
+ invokersWithInterest.forEach((invoker) => {
321
+ e.preventDefault();
322
+ clearState(invoker);
323
+ });
345
324
  }
346
325
  });
347
326
  }
@@ -354,21 +333,12 @@
354
333
  document.head.appendChild(style);
355
334
  document[dataField] = { globalPropsStyle: style };
356
335
  }
357
- function injectStyles() {
358
- const style = document.createElement("style");
359
- style.textContent = `.target-of-partial-interest::after { content: "{Press Alt+UpArrow to activate}"; display: block; color: darkgrey; font-size: 0.8em;}`;
360
- document.head.appendChild(style);
361
- document[dataField].globalHintStyle = style;
362
- }
363
336
 
364
337
  // Initialize
365
338
  function init() {
366
339
  registerCustomProperties();
367
- injectStyles();
368
340
  addEventHandlers();
369
- console.log(
370
- `interestfor polyfill installed (native: ${nativeSupported}).`
371
- );
341
+ console.log(`interestfor polyfill installed (native: ${nativeSupported}).`);
372
342
  }
373
343
  if (document.readyState === "complete") {
374
344
  init();
@@ -1,2 +1 @@
1
- !function(){let t="interest",e="loseinterest",r="--interest-show-delay",a="--interest-hide-delay",n="__interestForData",i="__interestForTargetData";if(window.interestForPolyfillInstalled)return;window.interestForPolyfillInstalled=!0;let s=HTMLButtonElement.prototype.hasOwnProperty("interestForElement");if(s&&!window.interestForUsePolyfillAlways)return;if(s){let l=t=>{t.isTrusted&&(t.preventDefault(),t.stopImmediatePropagation())};document.body.addEventListener(t,l,{capture:!0}),document.body.addEventListener(e,l,{capture:!0})}let o={NoInterest:"none",PartialInterest:"partial",FullInterest:"full"},d={Hover:"hover",DeHover:"dehover",Focus:"focus",Blur:"blur"};function u(t,e,r){if(!t||!e||!t.isConnected||p(t)!==e||r===o.NoInterest&&f(e)!==t)return!1;if(r!==o.NoInterest){let a=f(e);if(a){if(a===t)return a[n].clearLostTask(),!1;if(!u(a,e,o.NoInterest)||!t.isConnected||p(t)!==e)return!1}return h(t,r)}return v(t),!0}function c(t){let e=1e3*b(t,a);isFinite(e)&&!(e<0)&&(t[n].clearLostTask(),t[n].lostTimer=setTimeout(()=>{u(t,p(t),o.NoInterest)},e))}function f(t){let e=t[i]?.invoker||null;return e&&e[n]?.state!==o.NoInterest?e:null}function p(t){let e=t.getAttribute("interestfor");return document.getElementById(e)}function b(t,e){let r=getComputedStyle(t).getPropertyValue(e).trim(),a=r.match(/^([\d.]+)s$/);return a?parseFloat(a[1]):parseFloat(r)||0}function h(e,r){let a=e[n],s=p(e);switch(r){case o.PartialInterest:if(a.state===o.NoInterest){if(!s.dispatchEvent(new Event(t)))return;try{s.showPopover()}catch{}}a.state=o.PartialInterest,s[i]||(s[i]={}),s[i].invoker=e,e.classList.add("has-partial-interest","has-interest"),s.classList.add("target-of-partial-interest","target-of-interest"),function t(e){e.querySelectorAll(y).forEach(t=>{if(t.hasAttribute("data-original-tabindex"))return;let e=t.getAttribute("tabindex");t.setAttribute("data-original-tabindex",null===e?"none":e),t.setAttribute("tabindex","-1")})}(s);break;case o.FullInterest:if(a.state!==o.PartialInterest){if(!s.dispatchEvent(new Event(t)))return;try{s.showPopover()}catch{}}a.state=o.FullInterest,s[i]||(s[i]={}),s[i].invoker=e,e.classList.remove("has-partial-interest"),e.classList.add("has-interest"),s.classList.remove("target-of-partial-interest"),s.classList.add("target-of-interest"),g(s)}return!0}function v(t){let r=t[n];if(clearTimeout(r.gainedTimer),clearTimeout(r.lostTimer),r.state!==o.NoInterest){let a=p(t);if(!a.dispatchEvent(new Event(e)))return;try{a.hidePopover()}catch{}a[i]=null,t.classList.remove("has-partial-interest","has-interest"),a.classList.remove("target-of-partial-interest","target-of-interest"),g(a),r.state=o.NoInterest}}let y='a[href],area[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),button:not([disabled]),iframe,object,embed,[contenteditable],[tabindex]:not([tabindex="-1"])';function g(t){t.querySelectorAll("[data-original-tabindex]").forEach(t=>{let e=t.getAttribute("data-original-tabindex");"none"===e?t.removeAttribute("tabindex"):t.setAttribute("tabindex",e),t.removeAttribute("data-original-tabindex")})}function m(t,e){if(!t.isConnected)return;let a=p(t);if(!a)return;let i=t[n];i||(t[n]={state:o.NoInterest,gainedTimer:null,lostTimer:null,clearGainedTask(){clearTimeout(this.gainedTimer)},clearLostTask(){clearTimeout(this.lostTimer)}},i=t[n]);let s=f(t);if(e===d.Hover||e===d.Focus){i.clearLostTask&&i.clearLostTask(),s&&(s[n].clearLostTask(),s[n].state===o.PartialInterest&&h(s,o.FullInterest));let l=e===d.Focus&&a.matches(":has("+y+")");!function t(e,a){let i=1e3*b(e,r);isFinite(i)&&!(i<0)&&(e[n].clearGainedTask(),e[n].gainedTimer=setTimeout(()=>{u(e,p(e),a)},i))}(t,l?o.PartialInterest:o.FullInterest)}else i.clearGainedTask&&i.clearGainedTask(),i.state!==o.NoInterest&&c(t),s&&(s[n].clearGainedTask(),e!==d.Blur&&t.matches(":hover")||c(s))}function I(){!function t(){let e=document.createElement("style");e.textContent=`@property ${r} {syntax: "<time>"; inherits: false; initial-value: 0.5s;}
2
- @property ${a} {syntax: "<time>"; inherits: false; initial-value: 0.5s;}`,document.head.appendChild(e),document[n]={globalPropsStyle:e}}(),function t(){let e=document.createElement("style");e.textContent='.target-of-partial-interest::after { content: "{Press Alt+UpArrow to activate}"; display: block; color: darkgrey; font-size: 0.8em;}',document.head.appendChild(e),document[n].globalHintStyle=e}(),document.body.addEventListener("mouseover",t=>m(t.target,d.Hover)),document.body.addEventListener("mouseout",t=>m(t.target,d.DeHover)),document.body.addEventListener("focusin",t=>m(t.target,d.Focus)),document.body.addEventListener("focusout",t=>m(t.target,d.Blur)),document.body.addEventListener("keydown",t=>{let e=t.target[n];e&&e.state===o.PartialInterest&&("ArrowUp"===t.key&&t.altKey&&e.state===o.PartialInterest&&(t.preventDefault(),h(t.target,o.FullInterest)),"Escape"===t.key&&e.state!==o.NoInterest&&(t.preventDefault(),v(t.target)))}),console.log(`interestfor polyfill installed (native: ${s}).`)}"complete"===document.readyState?I():window.addEventListener("load",I)}();
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,6 +1,6 @@
1
1
  <!DOCTYPE html>
2
2
 
3
- <script src="interestfor.min.js" async></script>
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
6
  <div popover id=foo>Popover</div>
package/test/test.html CHANGED
@@ -199,7 +199,8 @@ async function waitForHoverOrFocus(type,element,target,timeMilliseconds) {
199
199
  const timeout = new Promise(resolve => setTimeout(() => {
200
200
  gotTimeout=true;
201
201
  resolve();
202
- },timeMilliseconds));
202
+ },timeMilliseconds*1.5));
203
+ // Make sure the user doesn't leave too early:
203
204
  switch (type) {
204
205
  case 'hover':
205
206
  const mouseLeft = new Promise(resolve => element.addEventListener('mouseleave',resolve, {once:true}));
@@ -226,8 +227,10 @@ async function waitForHoverOrFocus(type,element,target,timeMilliseconds) {
226
227
  async function testOne(invoker, target, type, description) {
227
228
  assert_true(true,'---' + description,true);
228
229
  assert_false(target.matches(':popover-open'),'popover should start closed',true);
229
- invoker.style.setProperty('--interest-show-delay',hoverTimeMilliseconds + 'ms');
230
- invoker.style.setProperty('--interest-hide-delay',hoverTimeMilliseconds + 'ms');
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');
231
234
  invoker.classList.remove('hide');
232
235
 
233
236
  // Show interest in the invoker