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 +7 -0
- package/README.md +27 -19
- package/build.sh +20 -0
- package/package.json +1 -1
- package/src/interestfor.js +164 -46
- package/src/interestfor.min.js +8 -1
- package/test/basic_test.html +16 -0
- package/test/test.html +28 -5
package/COPYRIGHT
ADDED
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
|
|
33
|
-
|
|
34
|
-
This polyfill
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
package/src/interestfor.js
CHANGED
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
const attributeName = "interestfor";
|
|
12
12
|
const interestEventName = "interest";
|
|
13
13
|
const loseInterestEventName = "loseinterest";
|
|
14
|
-
const
|
|
15
|
-
const
|
|
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,
|
|
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,
|
|
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
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
el.setAttribute("tabindex", "-1");
|
|
230
|
-
});
|
|
310
|
+
}
|
|
311
|
+
return true;
|
|
231
312
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
266
|
-
|
|
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 ${
|
|
332
|
-
@property ${
|
|
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
|
}
|
package/src/interestfor.min.js
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
|
-
|
|
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)}();
|
package/test/basic_test.html
CHANGED
|
@@ -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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|