interestfor 1.0.4 → 1.0.6
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 +29 -21
- package/build.sh +20 -0
- package/package.json +1 -1
- package/src/interestfor.js +173 -58
- package/src/interestfor.min.js +8 -1
- package/test/basic_test.html +17 -0
- package/test/test.html +28 -5
package/COPYRIGHT
ADDED
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# `interestfor` Polyfill
|
|
2
2
|
|
|
3
|
-
A polyfill for the `interestfor` attribute. This is
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
A polyfill for the `interestfor` attribute. This is a new API for "interest
|
|
4
|
+
interactions" that is in the process of being implemented in browsers. It will
|
|
5
|
+
[likely ship soon in
|
|
6
|
+
Chrome](https://groups.google.com/a/chromium.org/g/blink-dev/c/bX1G_yDt6W4?e=48417069).
|
|
7
|
+
See the explainer for all of the details for the "real" feature:
|
|
6
8
|
|
|
7
9
|
https://open-ui.org/components/interest-invokers.explainer/
|
|
8
10
|
|
|
@@ -29,18 +31,21 @@ upon receiving and losing interest.
|
|
|
29
31
|
Again, this is a quick summary. For the complete details, please [read the
|
|
30
32
|
explainer](https://open-ui.org/components/interest-invokers.explainer).
|
|
31
33
|
|
|
32
|
-
## Touchscreen
|
|
34
|
+
## Touchscreen support
|
|
33
35
|
|
|
34
|
-
This polyfill
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
This polyfill provides limited touchscreen support. On a touchscreen,
|
|
37
|
+
long-pressing a `<button>` element that has an `interestfor` attribute will show
|
|
38
|
+
interest in the target.
|
|
39
|
+
|
|
40
|
+
However, long-pressing a link (`<a>` element) is **not** supported. This is
|
|
41
|
+
because on today's browsers, long-pressing a link element brings up a useful
|
|
42
|
+
context menu that contains items like "Share" and "Open in new tab". In order
|
|
43
|
+
for the polyfill to support interest on links, that context menu would need to
|
|
44
|
+
be surpressed completely. There is currently no way to provide access to the
|
|
45
|
+
long-press context menu and *also* provide a way to show interest. Since the
|
|
46
|
+
context menu contains items that users often use, eliminating access to the
|
|
47
|
+
context menu is not what most people want. So this polyfill will not support
|
|
48
|
+
long-press on links.
|
|
44
49
|
|
|
45
50
|
This is one of the primary motivations for Chromium championing this as a
|
|
46
51
|
native API in all browsers: to support all users, including those using
|
|
@@ -74,6 +79,16 @@ native feature:
|
|
|
74
79
|
interest-delay-end: 200ms;
|
|
75
80
|
--interest-delay-end: 200ms;
|
|
76
81
|
}
|
|
82
|
+
/* Or, use the shorthand: */
|
|
83
|
+
[interestfor] {
|
|
84
|
+
interest-delay: 400ms 200ms;
|
|
85
|
+
--interest-delay: 400ms 200ms;
|
|
86
|
+
}
|
|
87
|
+
/* The keyword `normal` can also be used to specify the default delay: */
|
|
88
|
+
[interestfor] {
|
|
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);
|
|
145
173
|
}
|
|
146
|
-
|
|
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
|
+
}
|
|
196
|
+
}
|
|
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
|
|
@@ -303,33 +396,55 @@
|
|
|
303
396
|
|
|
304
397
|
// Attach listeners
|
|
305
398
|
function addEventHandlers() {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
);
|
|
312
|
-
document.body.addEventListener("
|
|
313
|
-
|
|
314
|
-
);
|
|
315
|
-
document.body.addEventListener("focusout", (e) =>
|
|
316
|
-
HandleInterestHoverOrFocus(e.target, Source.Blur)
|
|
317
|
-
);
|
|
399
|
+
const handler = (e, source) => {
|
|
400
|
+
for (let el = e.target; el; el = el.parentElement) {
|
|
401
|
+
HandleInterestHoverOrFocus(el, source);
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
document.body.addEventListener("mouseover", (e) => handler(e, Source.Hover));
|
|
405
|
+
document.body.addEventListener("mouseout", (e) => handler(e, Source.DeHover));
|
|
406
|
+
document.body.addEventListener("focusin", (e) => handler(e, Source.Focus));
|
|
407
|
+
document.body.addEventListener("focusout", (e) => handler(e, Source.Blur));
|
|
318
408
|
document.body.addEventListener("keydown", (e) => {
|
|
319
409
|
if (e.key === "Escape") {
|
|
320
410
|
invokersWithInterest.forEach((invoker) => {
|
|
321
|
-
e.preventDefault();
|
|
322
411
|
clearState(invoker);
|
|
323
412
|
});
|
|
324
413
|
}
|
|
325
414
|
});
|
|
415
|
+
// Touch support
|
|
416
|
+
const longPressTime = 500;
|
|
417
|
+
document.body.addEventListener("touchstart", (e) => {
|
|
418
|
+
touchInProgress = true;
|
|
419
|
+
const invoker = e.target.closest("button[interestfor]");
|
|
420
|
+
if (invoker) {
|
|
421
|
+
initializeDataField(invoker);
|
|
422
|
+
invoker[dataField].longPressTimer = setTimeout(() => {
|
|
423
|
+
GainOrLoseInterest(invoker, GetInterestForTarget(invoker), InterestState.FullInterest);
|
|
424
|
+
invoker[dataField].longPressTimer = null;
|
|
425
|
+
}, longPressTime);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
const cancelLongPress = (e) => {
|
|
429
|
+
const invoker = e.target.closest("button[interestfor]");
|
|
430
|
+
if (invoker && invoker[dataField]?.longPressTimer) {
|
|
431
|
+
clearTimeout(invoker[dataField].longPressTimer);
|
|
432
|
+
invoker[dataField].longPressTimer = null;
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
document.body.addEventListener("touchend", (e) => {
|
|
436
|
+
cancelLongPress(e);
|
|
437
|
+
touchInProgress = false;
|
|
438
|
+
});
|
|
439
|
+
document.body.addEventListener("touchmove", cancelLongPress);
|
|
326
440
|
}
|
|
327
441
|
|
|
328
442
|
// CSS registration
|
|
329
443
|
function registerCustomProperties() {
|
|
330
444
|
const style = document.createElement("style");
|
|
331
|
-
style.textContent = `@property ${
|
|
332
|
-
@property ${
|
|
445
|
+
style.textContent = `@property ${interestDelayStartProp} {syntax: "normal | <time>"; inherits: false; initial-value: normal;}
|
|
446
|
+
@property ${interestDelayEndProp} {syntax: "normal | <time>"; inherits: false; initial-value: normal;}
|
|
447
|
+
@property ${interestDelayProp} {syntax: "[ normal | <time> ]{1,2}"; inherits: false; initial-value: normal;}`;
|
|
333
448
|
document.head.appendChild(style);
|
|
334
449
|
document[dataField] = { globalPropsStyle: style };
|
|
335
450
|
}
|
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 w(t),!0}function p(e){const t=1e3*E(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 E(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 w(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*E(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(){const e=(e,t)=>{for(let n=e.target;n;n=n.parentElement)C(n,t)};document.body.addEventListener("mouseover",t=>e(t,m)),document.body.addEventListener("mouseout",t=>e(t,f)),document.body.addEventListener("focusin",t=>e(t,h)),document.body.addEventListener("focusout",t=>e(t,g)),document.body.addEventListener("keydown",e=>{"Escape"===e.key&&a.forEach(e=>{w(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 t=e=>{const t=e.target.closest("button[interestfor]");t&&t[i]?.longPressTimer&&(clearTimeout(t[i].longPressTimer),t[i].longPressTimer=null)};document.body.addEventListener("touchend",e=>{t(e),l=!1}),document.body.addEventListener("touchmove",t)}(),console.log(`interestfor polyfill installed (native: ${c}).`)}"complete"===document.readyState?F():window.addEventListener("load",F)}();
|
package/test/basic_test.html
CHANGED
|
@@ -3,4 +3,21 @@
|
|
|
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>
|
|
7
|
+
<span style="margin:10px;">Button - hover me, don't click me</span></a>
|
|
6
8
|
<div popover id=foo>Popover</div>
|
|
9
|
+
|
|
10
|
+
<style>
|
|
11
|
+
[interestfor] {
|
|
12
|
+
interest-delay: 600ms 300ms;
|
|
13
|
+
--interest-delay: 600ms 300ms;
|
|
14
|
+
}
|
|
15
|
+
:is([interestfor]:interest-source),
|
|
16
|
+
[interestfor].interest-source {
|
|
17
|
+
background-color: lightgreen;
|
|
18
|
+
}
|
|
19
|
+
:is([popover]:interest-target),
|
|
20
|
+
[popover].interest-target {
|
|
21
|
+
background-color: lightblue;
|
|
22
|
+
}
|
|
23
|
+
</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
|