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 +10 -10
- package/package.json +1 -1
- package/src/interestfor.js +62 -92
- package/src/interestfor.min.js +1 -2
- package/test/basic_test.html +1 -1
- package/test/test.html +6 -3
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
|
|
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-
|
|
73
|
-
--interest-
|
|
74
|
-
interest-
|
|
75
|
-
--interest-
|
|
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]:
|
|
84
|
-
[interestfor].
|
|
83
|
+
:is([interestfor]:interest-source),
|
|
84
|
+
[interestfor].interest-source {
|
|
85
85
|
/* invoker styles */
|
|
86
86
|
}
|
|
87
|
-
:is([popover]:target
|
|
88
|
-
[popover].target
|
|
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. `:
|
|
95
|
+
browser does not recognize the native pseudo class (e.g. `:interest-source`).
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
## Tests
|
package/package.json
CHANGED
package/src/interestfor.js
CHANGED
|
@@ -11,19 +11,19 @@
|
|
|
11
11
|
const attributeName = "interestfor";
|
|
12
12
|
const interestEventName = "interest";
|
|
13
13
|
const loseInterestEventName = "loseinterest";
|
|
14
|
-
const showDelayProp = "--interest-
|
|
15
|
-
const hideDelayProp = "--interest-
|
|
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 =
|
|
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,{
|
|
39
|
-
|
|
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 (
|
|
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.
|
|
171
|
-
|
|
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
|
-
|
|
184
|
-
invoker.classList.add("
|
|
185
|
-
target.classList.
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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();
|
package/src/interestfor.min.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
!function(){
|
|
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)}();
|
package/test/basic_test.html
CHANGED
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-
|
|
230
|
-
invoker.style.setProperty('
|
|
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
|