interestfor 1.0.8 → 1.0.9
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 +98 -3
- package/package.json +1 -1
- package/src/interestfor.js +616 -90
- package/src/interestfor.min.js +1 -1
- package/test/basic_test.html +13 -1
package/README.md
CHANGED
|
@@ -27,14 +27,84 @@ can be done by hovering the element with the mouse, or focusing it with the
|
|
|
27
27
|
keyboard. If the target is a popover, it will automatically be shown and hidden
|
|
28
28
|
upon receiving and losing interest.
|
|
29
29
|
|
|
30
|
+
A few more details:
|
|
31
|
+
|
|
32
|
+
- Only `<button>` (when not disabled), `<a href>`, `<area href>`, and SVG
|
|
33
|
+
`<a href>` elements can be interest invokers. The `interestfor` attribute has
|
|
34
|
+
no effect on other elements.
|
|
35
|
+
- The `interest` and `loseinterest` events are cancelable. Calling
|
|
36
|
+
`preventDefault()` on `interest` prevents interest from being shown, and on
|
|
37
|
+
`loseinterest` keeps interest (and the popover) in place.
|
|
38
|
+
- Pressing Escape makes all interest invokers lose interest. These
|
|
39
|
+
`loseinterest` events are not cancelable.
|
|
40
|
+
- If the target popover was already open when interest was gained, it is not
|
|
41
|
+
hidden when interest is lost.
|
|
42
|
+
- If the target popover is hidden some other way (e.g. light dismiss), the
|
|
43
|
+
invoker loses interest.
|
|
44
|
+
- Popovers can be nested: hovering an invoker inside a popover that was shown
|
|
45
|
+
via interest keeps the outer popover open.
|
|
46
|
+
- The `interestForElement` property is available on `HTMLButtonElement`,
|
|
47
|
+
`HTMLAnchorElement`, `HTMLAreaElement`, and `SVGAElement`. It reflects the
|
|
48
|
+
`interestfor` attribute as an element reference.
|
|
49
|
+
- The optional `::interest-button` pseudo element provides a button that shows
|
|
50
|
+
interest immediately when clicked or tapped. See below.
|
|
51
|
+
|
|
30
52
|
Again, this is a quick summary. For the complete details, please [read the
|
|
31
|
-
explainer](https://open-ui.org/components/interest-invokers.explainer)
|
|
53
|
+
OpenUI explainer](https://open-ui.org/components/interest-invokers.explainer),
|
|
54
|
+
the [HTML spec PR](https://github.com/whatwg/html/pull/11006), and the CSSWG
|
|
55
|
+
specs for [the pseudo classes](https://drafts.csswg.org/selectors-5/#interest-pseudos)
|
|
56
|
+
and [CSS properties](https://drafts.csswg.org/css-ui-4/#interest).
|
|
57
|
+
|
|
58
|
+
## The `::interest-button` pseudo element
|
|
59
|
+
|
|
60
|
+
An interest invoker can have an `::interest-button` pseudo element, which is
|
|
61
|
+
a small button rendered just after the invoker. Clicking (or tapping) it shows
|
|
62
|
+
interest immediately, without waiting for any delay. Interest shown this way is
|
|
63
|
+
not lost when the mouse leaves the invoker or the invoker loses focus. It is
|
|
64
|
+
lost when the user clicks outside the popover, presses Escape, or hovers and
|
|
65
|
+
then leaves the target popover. The pseudo element is only rendered if its
|
|
66
|
+
`content` property is set.
|
|
67
|
+
|
|
68
|
+
The polyfill can't add a real pseudo element, so it inserts a
|
|
69
|
+
`<span class="interest-button" aria-hidden="true">` element as the next sibling
|
|
70
|
+
of the invoker. Set the `--interest-button-content` custom property on the
|
|
71
|
+
invoker to the value you'd use for `content`, and use `+ .interest-button` to
|
|
72
|
+
style it. These must be separate rules, because a browser that doesn't
|
|
73
|
+
recognize `::interest-button` will drop the entire rule containing it:
|
|
74
|
+
|
|
75
|
+
```css
|
|
76
|
+
/* Native */
|
|
77
|
+
[interestfor]::interest-button {
|
|
78
|
+
content: "?";
|
|
79
|
+
}
|
|
80
|
+
/* Polyfill */
|
|
81
|
+
[interestfor] {
|
|
82
|
+
--interest-button-content: "?";
|
|
83
|
+
}
|
|
84
|
+
[interestfor] + .interest-button {
|
|
85
|
+
/* Additional polyfill-only styles */
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Both the native pseudo element and the polyfill element get these default
|
|
90
|
+
styles, which can be overridden: `display: inline-block`,
|
|
91
|
+
`vertical-align: middle`, `min-inline-size: 24px`,
|
|
92
|
+
`min-block-size: max(24px, 1lh)`, `cursor: help`, a transparent background, and
|
|
93
|
+
a slightly tinted background on `:hover` and `:active`.
|
|
94
|
+
|
|
95
|
+
Because the interest button can be tapped, it provides a way for touchscreen
|
|
96
|
+
users to show interest in links (see below). Note that the polyfill implements
|
|
97
|
+
this as a `<span>` with a click handler on purpose - that makes it non-focusable,
|
|
98
|
+
which is correct, because the invoker itself will show interest if focused, so
|
|
99
|
+
there should not be two focus stops. See [this section of the explainer](https://open-ui.org/components/interest-invokers.explainer/#rich-hints-aka-hovercards-or-other:~:text=Importantly%2C%20this%20new%20pseudo%20element%20is%20not%20keyboard%20focusable)
|
|
100
|
+
for more details.
|
|
32
101
|
|
|
33
102
|
## Touchscreen support
|
|
34
103
|
|
|
35
104
|
This polyfill provides limited touchscreen support. On a touchscreen,
|
|
36
105
|
long-pressing a `<button>` element that has an `interestfor` attribute will show
|
|
37
|
-
interest in the target.
|
|
106
|
+
interest in the target. As with the interest button, this interest is not lost
|
|
107
|
+
when the button loses hover or focus.
|
|
38
108
|
|
|
39
109
|
However, long-pressing a link (`<a>` element) is **not** supported. This is
|
|
40
110
|
because on today's browsers, long-pressing a link element brings up a useful
|
|
@@ -44,7 +114,8 @@ be surpressed completely. There is currently no way to provide access to the
|
|
|
44
114
|
long-press context menu and *also* provide a way to show interest. Since the
|
|
45
115
|
context menu contains items that users often use, eliminating access to the
|
|
46
116
|
context menu is not what most people want. So this polyfill will not support
|
|
47
|
-
long-press on links.
|
|
117
|
+
long-press on links. To let touchscreen users show interest in a link, give it
|
|
118
|
+
an interest button (see the `::interest-button` section above).
|
|
48
119
|
|
|
49
120
|
This is one of the primary motivations for Chromium championing this as a
|
|
50
121
|
native API in all browsers: to support all users, including those using
|
|
@@ -108,6 +179,30 @@ native feature:
|
|
|
108
179
|
class names. This is to avoid the entire selector being invalidated if the
|
|
109
180
|
browser does not recognize the native pseudo class (e.g. `:interest-source`).
|
|
110
181
|
|
|
182
|
+
- The `::interest-button` pseudo element is implemented as a real
|
|
183
|
+
`<span class="interest-button">` element, inserted after the invoker, and
|
|
184
|
+
configured with the `--interest-button-content` custom property (see above).
|
|
185
|
+
Because it is a real element:
|
|
186
|
+
- It inherits styles from the invoker's parent, not from the invoker.
|
|
187
|
+
- It can affect sibling selectors such as `+` and `:nth-child()`.
|
|
188
|
+
- Click listeners on the invoker don't see clicks on the interest button.
|
|
189
|
+
- It is not implicitly anchored to the invoker for anchor positioning.
|
|
190
|
+
- The polyfill only re-reads `--interest-button-content` when the DOM or the
|
|
191
|
+
`interestfor`, `id`, `href`, or `disabled` attributes change. Changes caused
|
|
192
|
+
only by styles (e.g. class changes, `:hover`, media queries) are not
|
|
193
|
+
detected.
|
|
194
|
+
- Interest buttons are not created for invokers inside shadow roots, or for
|
|
195
|
+
`<area>` or SVG `<a>` invokers.
|
|
196
|
+
- Clicking the interest button while the target popover is open causes the
|
|
197
|
+
popover to be light dismissed and then shown again, which fires an extra
|
|
198
|
+
pair of `loseinterest` and `interest` events.
|
|
199
|
+
- The polyfill does not run in browsers that natively support `interestfor`.
|
|
200
|
+
In browsers that support `interestfor` but not `::interest-button`, no
|
|
201
|
+
interest button is shown.
|
|
202
|
+
- The polyfill installs a `MutationObserver` on the document to keep interest
|
|
203
|
+
buttons up to date, and prepends a `<style>` element to `<head>` containing
|
|
204
|
+
the default styles for interest invokers and interest buttons.
|
|
205
|
+
|
|
111
206
|
|
|
112
207
|
## Tests
|
|
113
208
|
|
package/package.json
CHANGED
package/src/interestfor.js
CHANGED
|
@@ -14,9 +14,24 @@
|
|
|
14
14
|
const interestDelayStartProp = "--interest-delay-start";
|
|
15
15
|
const interestDelayEndProp = "--interest-delay-end";
|
|
16
16
|
const interestDelayProp = "--interest-delay";
|
|
17
|
+
// Stands in for the `content` property of the `::interest-button` pseudo
|
|
18
|
+
// element. It is set on the invoker.
|
|
19
|
+
const interestButtonContentProp = "--interest-button-content";
|
|
20
|
+
// An unregistered (and therefore inherited) copy of the content value, which
|
|
21
|
+
// is set on the polyfilled interest button, so that its ::before pseudo
|
|
22
|
+
// element can render it.
|
|
23
|
+
const interestButtonInternalContentProp =
|
|
24
|
+
"--interest-button-polyfill-content";
|
|
25
|
+
const interestButtonClass = "interest-button";
|
|
17
26
|
const dataField = "__interestForData";
|
|
18
27
|
const targetDataField = "__interestForTargetData";
|
|
28
|
+
const buttonField = "__interestForButton";
|
|
29
|
+
const buttonInvokerField = "__interestForButtonInvoker";
|
|
30
|
+
const explicitTargetField = "__interestForExplicitTarget";
|
|
31
|
+
const xlinkNamespace = "http://www.w3.org/1999/xlink";
|
|
19
32
|
const invokersWithInterest = new Set();
|
|
33
|
+
const invokersWithButtons = new Set();
|
|
34
|
+
const invokersMissingTarget = new Set();
|
|
20
35
|
let touchInProgress = false;
|
|
21
36
|
|
|
22
37
|
// Feature detection
|
|
@@ -77,7 +92,12 @@
|
|
|
77
92
|
// Enum-like state and source
|
|
78
93
|
const InterestState = {
|
|
79
94
|
NoInterest: "none",
|
|
95
|
+
// Interest gained via hover or focus, after a delay. This is lost when
|
|
96
|
+
// the invoker is de-hovered or blurred.
|
|
80
97
|
FullInterest: "full",
|
|
98
|
+
// Interest gained immediately via long-press or the `::interest-button`.
|
|
99
|
+
// This is not lost when the invoker itself is de-hovered or blurred.
|
|
100
|
+
ExplicitInterest: "explicit",
|
|
81
101
|
};
|
|
82
102
|
const Source = {
|
|
83
103
|
Hover: "hover",
|
|
@@ -96,7 +116,7 @@
|
|
|
96
116
|
}
|
|
97
117
|
|
|
98
118
|
// Gain or lose interest
|
|
99
|
-
function GainOrLoseInterest(invoker, target, newState) {
|
|
119
|
+
function GainOrLoseInterest(invoker, target, newState, options = {}) {
|
|
100
120
|
if (!invoker || !target) {
|
|
101
121
|
return false;
|
|
102
122
|
}
|
|
@@ -111,9 +131,21 @@
|
|
|
111
131
|
|
|
112
132
|
// If gaining, handle existing invoker
|
|
113
133
|
if (newState !== InterestState.NoInterest) {
|
|
134
|
+
initializeDataField(invoker);
|
|
135
|
+
if (
|
|
136
|
+
invoker[dataField].state !== InterestState.NoInterest &&
|
|
137
|
+
invoker[dataField].target !== target
|
|
138
|
+
) {
|
|
139
|
+
// The `interestfor` attribute changed while this invoker had interest,
|
|
140
|
+
// and the mutation observer hasn't run yet.
|
|
141
|
+
resetInterestState(invoker);
|
|
142
|
+
}
|
|
114
143
|
const existing = GetInterestInvoker(target);
|
|
115
144
|
if (existing) {
|
|
116
145
|
if (existing === invoker) {
|
|
146
|
+
if (newState === InterestState.ExplicitInterest) {
|
|
147
|
+
existing[dataField].state = InterestState.ExplicitInterest;
|
|
148
|
+
}
|
|
117
149
|
existing[dataField].clearLostTask();
|
|
118
150
|
return false;
|
|
119
151
|
} else {
|
|
@@ -130,73 +162,182 @@
|
|
|
130
162
|
}
|
|
131
163
|
}
|
|
132
164
|
// finally apply interest
|
|
133
|
-
return applyState(invoker, newState);
|
|
165
|
+
return applyState(invoker, target, newState);
|
|
134
166
|
}
|
|
135
167
|
|
|
136
168
|
// losing interest
|
|
137
|
-
clearState(invoker);
|
|
138
|
-
return true;
|
|
169
|
+
return clearState(invoker, options);
|
|
139
170
|
}
|
|
140
171
|
|
|
141
172
|
// Schedule tasks
|
|
142
173
|
function ScheduleInterestGainedTask(invoker, newState) {
|
|
174
|
+
const target = GetInterestForTarget(invoker);
|
|
175
|
+
if (!target) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
143
178
|
const delay = getDelaySeconds(invoker, interestDelayStartProp) * 1000;
|
|
144
|
-
if (!
|
|
179
|
+
if (!isSchedulableDelay(delay)) {
|
|
145
180
|
return;
|
|
146
181
|
}
|
|
147
182
|
invoker[dataField].clearGainedTask();
|
|
148
183
|
invoker[dataField].gainedTimer = setTimeout(() => {
|
|
149
|
-
GainOrLoseInterest(invoker,
|
|
184
|
+
GainOrLoseInterest(invoker, target, newState);
|
|
150
185
|
}, delay);
|
|
151
186
|
}
|
|
152
187
|
|
|
153
188
|
function ScheduleInterestLostTask(invoker) {
|
|
154
189
|
const delay = getDelaySeconds(invoker, interestDelayEndProp) * 1000;
|
|
155
|
-
if (!
|
|
190
|
+
if (!isSchedulableDelay(delay)) {
|
|
156
191
|
return;
|
|
157
192
|
}
|
|
158
|
-
invoker[dataField]
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
InterestState.NoInterest
|
|
164
|
-
);
|
|
193
|
+
const data = invoker[dataField];
|
|
194
|
+
data.clearLostTask();
|
|
195
|
+
const target = data.target;
|
|
196
|
+
data.lostTimer = setTimeout(() => {
|
|
197
|
+
GainOrLoseInterest(invoker, target, InterestState.NoInterest);
|
|
165
198
|
}, delay);
|
|
166
199
|
}
|
|
167
200
|
|
|
168
201
|
// Helpers
|
|
169
202
|
function GetInterestInvoker(target) {
|
|
170
203
|
const inv = target[targetDataField]?.invoker || null;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
204
|
+
if (!inv) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
const data = inv[dataField];
|
|
208
|
+
if (
|
|
209
|
+
!data ||
|
|
210
|
+
data.state === InterestState.NoInterest ||
|
|
211
|
+
data.target !== target ||
|
|
212
|
+
GetInterestForTarget(inv) !== target
|
|
213
|
+
) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
return inv;
|
|
174
217
|
}
|
|
218
|
+
|
|
219
|
+
// Returns the target of a valid interest invoker, or null.
|
|
175
220
|
function GetInterestForTarget(el) {
|
|
221
|
+
if (
|
|
222
|
+
!el.hasAttribute(attributeName) ||
|
|
223
|
+
!el.isConnected ||
|
|
224
|
+
!isValidInterestInvoker(el)
|
|
225
|
+
) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
const target = getAttrAssociatedElement(el);
|
|
229
|
+
return target && target.isConnected ? target : null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Only some elements can be interest invokers. This mirrors the
|
|
233
|
+
// IsValidInterestInvoker() overrides in Chromium.
|
|
234
|
+
function isValidInterestInvoker(el) {
|
|
235
|
+
if (el instanceof HTMLButtonElement) {
|
|
236
|
+
return !el.matches(":disabled");
|
|
237
|
+
}
|
|
238
|
+
if (el instanceof HTMLAnchorElement || el instanceof HTMLAreaElement) {
|
|
239
|
+
return el.hasAttribute("href");
|
|
240
|
+
}
|
|
241
|
+
if (window.SVGAElement && el instanceof SVGAElement) {
|
|
242
|
+
return el.hasAttribute("href") || el.hasAttributeNS(xlinkNamespace, "href");
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Resolves the `interestfor` attribute (or an element set via the
|
|
248
|
+
// `interestForElement` property) within the invoker's tree scope.
|
|
249
|
+
function getAttrAssociatedElement(el) {
|
|
176
250
|
const id = el.getAttribute(attributeName);
|
|
177
|
-
|
|
251
|
+
if (id === null) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
const explicitTarget = el[explicitTargetField];
|
|
255
|
+
if (explicitTarget && id === "") {
|
|
256
|
+
return isInShadowIncludingAncestorTree(explicitTarget, el)
|
|
257
|
+
? explicitTarget
|
|
258
|
+
: null;
|
|
259
|
+
}
|
|
260
|
+
if (id === "") {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
const root = getRootNode(el);
|
|
264
|
+
return typeof root.getElementById === "function"
|
|
265
|
+
? root.getElementById(id)
|
|
266
|
+
: null;
|
|
178
267
|
}
|
|
179
268
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
269
|
+
// Returns true if `candidate` is in the same tree as `el`, or in the tree of
|
|
270
|
+
// one of `el`'s shadow hosts.
|
|
271
|
+
function isInShadowIncludingAncestorTree(candidate, el) {
|
|
272
|
+
const candidateRoot = getRootNode(candidate);
|
|
273
|
+
let root = getRootNode(el);
|
|
274
|
+
while (root) {
|
|
275
|
+
if (root === candidateRoot) {
|
|
276
|
+
return true;
|
|
186
277
|
}
|
|
278
|
+
root = root instanceof ShadowRoot ? getRootNode(root.host) : null;
|
|
279
|
+
}
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isPopoverOpen(el) {
|
|
284
|
+
try {
|
|
285
|
+
return el.matches(":popover-open");
|
|
286
|
+
} catch {
|
|
287
|
+
return false;
|
|
187
288
|
}
|
|
188
289
|
}
|
|
189
290
|
|
|
291
|
+
// If the target popover is hidden by something other than the loss of
|
|
292
|
+
// interest (e.g. light dismiss), the invoker loses interest. This uses
|
|
293
|
+
// `beforetoggle`, since in Chromium, the loss of interest happens
|
|
294
|
+
// synchronously as the popover begins to hide.
|
|
295
|
+
function onPopoverBeforeToggle(e) {
|
|
296
|
+
if (e.newState !== "closed") {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const popover = e.currentTarget;
|
|
300
|
+
const invoker = GetInterestInvoker(popover);
|
|
301
|
+
if (!invoker) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (popover.contains(document.activeElement)) {
|
|
305
|
+
// Focus will be returned to the previously focused element after the
|
|
306
|
+
// popover hides. If that is the invoker, don't show interest again
|
|
307
|
+
// because of that focus.
|
|
308
|
+
const data = invoker[dataField];
|
|
309
|
+
data.suppressNextFocusInterest = true;
|
|
310
|
+
setTimeout(() => {
|
|
311
|
+
data.suppressNextFocusInterest = false;
|
|
312
|
+
}, 0);
|
|
313
|
+
}
|
|
314
|
+
// The popover is already hiding, so don't try to hide it again.
|
|
315
|
+
GainOrLoseInterest(invoker, popover, InterestState.NoInterest, {
|
|
316
|
+
closePopovers: false,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Delays longer than setTimeout() supports (which would fire immediately)
|
|
321
|
+
// are treated as infinite, i.e. interest is never gained or lost.
|
|
322
|
+
function isSchedulableDelay(delayMs) {
|
|
323
|
+
return delayMs >= 0 && delayMs <= 2147483647;
|
|
324
|
+
}
|
|
325
|
+
|
|
190
326
|
function parseTimeValue(val) {
|
|
191
327
|
const s = String(val).trim();
|
|
192
|
-
const m_s = s.match(/^([\d.]+)s$/);
|
|
328
|
+
const m_s = s.match(/^([\d.]+(?:e[+-]?\d+)?)s$/i);
|
|
193
329
|
if (m_s) {
|
|
194
330
|
return parseFloat(m_s[1]);
|
|
195
331
|
}
|
|
196
|
-
const m_ms = s.match(/^([\d.]+)ms$/);
|
|
332
|
+
const m_ms = s.match(/^([\d.]+(?:e[+-]?\d+)?)ms$/i);
|
|
197
333
|
if (m_ms) {
|
|
198
334
|
return parseFloat(m_ms[1]) / 1000;
|
|
199
335
|
}
|
|
336
|
+
// Values such as `calc(infinity * 1s)` mean that interest is never
|
|
337
|
+
// gained or lost via hover or focus.
|
|
338
|
+
if (/\b(infinity|nan)\b/i.test(s)) {
|
|
339
|
+
return Infinity;
|
|
340
|
+
}
|
|
200
341
|
return parseFloat(s) || 0;
|
|
201
342
|
}
|
|
202
343
|
|
|
@@ -232,32 +373,44 @@
|
|
|
232
373
|
}
|
|
233
374
|
}
|
|
234
375
|
|
|
235
|
-
// If we got here, the effective value is 'normal'. Return the default.
|
|
236
|
-
return prop === interestDelayStartProp ? 0.
|
|
376
|
+
// If we got here, the effective value is 'normal'. Return the Chromium default.
|
|
377
|
+
return prop === interestDelayStartProp ? 0.25 : 0.15;
|
|
237
378
|
}
|
|
238
379
|
|
|
239
380
|
// Actual state transitions
|
|
240
|
-
function applyState(invoker, newState) {
|
|
381
|
+
function applyState(invoker, target, newState) {
|
|
241
382
|
const data = invoker[dataField];
|
|
242
|
-
const target = GetInterestForTarget(invoker);
|
|
243
383
|
switch (newState) {
|
|
244
384
|
case InterestState.FullInterest:
|
|
385
|
+
case InterestState.ExplicitInterest:
|
|
245
386
|
if (data.state !== InterestState.NoInterest) {
|
|
246
387
|
throw new Error("Invalid state");
|
|
247
388
|
}
|
|
248
|
-
const shouldContinue = target.dispatchEvent(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
389
|
+
const shouldContinue = target.dispatchEvent(
|
|
390
|
+
new InterestEvent(interestEventName, {
|
|
391
|
+
source: invoker,
|
|
392
|
+
cancelable: true,
|
|
393
|
+
})
|
|
394
|
+
);
|
|
395
|
+
if (!shouldContinue) return false;
|
|
396
|
+
data.state = newState;
|
|
397
|
+
data.target = target;
|
|
398
|
+
data.clearGainedTask();
|
|
254
399
|
if (!target[targetDataField]) {
|
|
255
400
|
target[targetDataField] = {};
|
|
256
401
|
}
|
|
257
402
|
target[targetDataField].invoker = invoker;
|
|
258
403
|
if (target.hasAttribute('popover')) {
|
|
259
|
-
target
|
|
260
|
-
|
|
404
|
+
target.addEventListener('beforetoggle', onPopoverBeforeToggle);
|
|
405
|
+
// Only show the popover if it isn't already open. Only popovers that
|
|
406
|
+
// were shown here get hidden when interest is lost.
|
|
407
|
+
target[targetDataField].shownByInterest = false;
|
|
408
|
+
if (!isPopoverOpen(target)) {
|
|
409
|
+
try {
|
|
410
|
+
target.showPopover({ source: invoker });
|
|
411
|
+
} catch {}
|
|
412
|
+
target[targetDataField].shownByInterest = isPopoverOpen(target);
|
|
413
|
+
}
|
|
261
414
|
}
|
|
262
415
|
invokersWithInterest.add(invoker);
|
|
263
416
|
invoker.classList.add("interest-source");
|
|
@@ -281,33 +434,88 @@
|
|
|
281
434
|
return true;
|
|
282
435
|
}
|
|
283
436
|
|
|
284
|
-
|
|
437
|
+
// Fires the `loseinterest` event, and if not cancelled, removes interest
|
|
438
|
+
// and hides the target popover (if it was shown via interest). If `force` is
|
|
439
|
+
// true, the event is not cancelable.
|
|
440
|
+
function clearState(invoker, { force = false, closePopovers = true } = {}) {
|
|
285
441
|
const data = invoker[dataField];
|
|
286
442
|
clearTimeout(data.gainedTimer);
|
|
287
443
|
clearTimeout(data.lostTimer);
|
|
288
|
-
if (data.state
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
444
|
+
if (data.state === InterestState.NoInterest) {
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
const target = data.target;
|
|
448
|
+
const shouldContinue = target.dispatchEvent(
|
|
449
|
+
new InterestEvent(loseInterestEventName, {
|
|
450
|
+
source: invoker,
|
|
451
|
+
cancelable: !force,
|
|
452
|
+
})
|
|
453
|
+
);
|
|
454
|
+
if (!shouldContinue) return false;
|
|
455
|
+
const targetData = target[targetDataField];
|
|
456
|
+
const shouldHide =
|
|
457
|
+
closePopovers &&
|
|
458
|
+
targetData?.invoker === invoker &&
|
|
459
|
+
targetData.shownByInterest;
|
|
460
|
+
resetInterestState(invoker);
|
|
461
|
+
if (shouldHide && isPopoverOpen(target)) {
|
|
462
|
+
hideTargetPopover(invoker, target);
|
|
463
|
+
}
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Removes interest without firing events, or hiding the target. This
|
|
468
|
+
// mirrors ChangeInterestState(kNoInterest) in Chromium, and is used directly
|
|
469
|
+
// when the invoker or target is removed, or the `interestfor` attribute
|
|
470
|
+
// changes.
|
|
471
|
+
function resetInterestState(invoker) {
|
|
472
|
+
const data = invoker[dataField];
|
|
473
|
+
if (!data) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
clearTimeout(data.gainedTimer);
|
|
477
|
+
clearTimeout(data.lostTimer);
|
|
478
|
+
if (data.state === InterestState.NoInterest) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const target = data.target;
|
|
482
|
+
invokersWithInterest.delete(invoker);
|
|
483
|
+
invoker.classList.remove("interest-source");
|
|
484
|
+
if (target) {
|
|
485
|
+
if (target[targetDataField]?.invoker === invoker) {
|
|
486
|
+
target.removeEventListener('beforetoggle', onPopoverBeforeToggle);
|
|
487
|
+
target[targetDataField] = null;
|
|
488
|
+
target.classList.remove("interest-target");
|
|
489
|
+
}
|
|
302
490
|
if (!isPlainHint(target)) {
|
|
303
491
|
invoker.setAttribute("aria-expanded", "false");
|
|
304
492
|
}
|
|
305
|
-
|
|
306
|
-
|
|
493
|
+
}
|
|
494
|
+
if (data.anchorName) {
|
|
495
|
+
invoker.style.anchorName = "";
|
|
496
|
+
if (target) {
|
|
307
497
|
target.style.positionAnchor = "";
|
|
308
|
-
data.anchorName = null;
|
|
309
498
|
}
|
|
310
|
-
data.
|
|
499
|
+
data.anchorName = null;
|
|
500
|
+
}
|
|
501
|
+
data.state = InterestState.NoInterest;
|
|
502
|
+
data.target = null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function hideTargetPopover(invoker, target) {
|
|
506
|
+
// If focus is within the popover, hiding it returns focus to the
|
|
507
|
+
// previously focused element. If that is the invoker, don't show interest
|
|
508
|
+
// again because of that focus.
|
|
509
|
+
const data = invoker[dataField];
|
|
510
|
+
const focusInside = target.contains(document.activeElement);
|
|
511
|
+
if (focusInside) {
|
|
512
|
+
data.suppressNextFocusInterest = true;
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
target.hidePopover();
|
|
516
|
+
} catch {}
|
|
517
|
+
if (focusInside) {
|
|
518
|
+
data.suppressNextFocusInterest = false;
|
|
311
519
|
}
|
|
312
520
|
}
|
|
313
521
|
|
|
@@ -370,9 +578,11 @@
|
|
|
370
578
|
if (el[dataField]) return;
|
|
371
579
|
el[dataField] = {
|
|
372
580
|
state: InterestState.NoInterest,
|
|
581
|
+
target: null,
|
|
373
582
|
gainedTimer: null,
|
|
374
583
|
lostTimer: null,
|
|
375
584
|
longPressTimer: null,
|
|
585
|
+
suppressNextFocusInterest: false,
|
|
376
586
|
clearGainedTask() {
|
|
377
587
|
clearTimeout(this.gainedTimer);
|
|
378
588
|
},
|
|
@@ -386,6 +596,49 @@
|
|
|
386
596
|
}
|
|
387
597
|
}
|
|
388
598
|
|
|
599
|
+
// Returns the flat tree parent of `el`. The polyfilled `::interest-button`
|
|
600
|
+
// is treated as a child of its invoker, for tree traversal purposes, as
|
|
601
|
+
// the real pseudo element would be. (It is treated as a sibling for layout
|
|
602
|
+
// tree traversal purposes only.)
|
|
603
|
+
function getParentForInterest(el) {
|
|
604
|
+
if (el[buttonInvokerField]) {
|
|
605
|
+
return el[buttonInvokerField];
|
|
606
|
+
}
|
|
607
|
+
if (el.assignedSlot) {
|
|
608
|
+
return el.assignedSlot;
|
|
609
|
+
}
|
|
610
|
+
if (el.parentElement) {
|
|
611
|
+
return el.parentElement;
|
|
612
|
+
}
|
|
613
|
+
const parent = el.parentNode;
|
|
614
|
+
return parent instanceof ShadowRoot ? parent.host : null;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Returns all invokers that have interest in `el`, or in any of its
|
|
618
|
+
// ancestors, and (recursively) all invokers that have interest in those
|
|
619
|
+
// invokers or their ancestors. This mirrors AllSourceInterestInvokers() in
|
|
620
|
+
// Chromium, and allows nested interest popovers to stay open while the inner
|
|
621
|
+
// ones are hovered.
|
|
622
|
+
function allSourceInterestInvokers(el) {
|
|
623
|
+
const sources = new Set();
|
|
624
|
+
if (invokersWithInterest.size === 0) {
|
|
625
|
+
return sources;
|
|
626
|
+
}
|
|
627
|
+
const visit = (node) => {
|
|
628
|
+
const upstream = GetInterestInvoker(node);
|
|
629
|
+
if (upstream && !sources.has(upstream)) {
|
|
630
|
+
sources.add(upstream);
|
|
631
|
+
visit(upstream);
|
|
632
|
+
}
|
|
633
|
+
const parent = getParentForInterest(node);
|
|
634
|
+
if (parent && !sources.has(parent)) {
|
|
635
|
+
visit(parent);
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
visit(el);
|
|
639
|
+
return sources;
|
|
640
|
+
}
|
|
641
|
+
|
|
389
642
|
function HandleInterestHoverOrFocus(el, source) {
|
|
390
643
|
if (touchInProgress) {
|
|
391
644
|
return;
|
|
@@ -394,52 +647,235 @@
|
|
|
394
647
|
return;
|
|
395
648
|
}
|
|
396
649
|
const target = GetInterestForTarget(el);
|
|
397
|
-
if (
|
|
398
|
-
|
|
399
|
-
if (containingTarget) {
|
|
400
|
-
const upstreamInvoker = GetInterestInvoker(containingTarget);
|
|
401
|
-
if (upstreamInvoker) {
|
|
402
|
-
if (source === Source.Hover || source === Source.Focus) {
|
|
403
|
-
upstreamInvoker[dataField].clearLostTask();
|
|
404
|
-
} else {
|
|
405
|
-
if (source === Source.Blur || !el.matches(":hover")) {
|
|
406
|
-
ScheduleInterestLostTask(upstreamInvoker);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
return;
|
|
650
|
+
if (target) {
|
|
651
|
+
initializeDataField(el);
|
|
412
652
|
}
|
|
413
|
-
initializeDataField(el);
|
|
414
653
|
const data = el[dataField];
|
|
415
|
-
const upstreamInvoker = GetInterestInvoker(el);
|
|
416
654
|
|
|
417
655
|
// Hover or focus
|
|
418
656
|
if (source === Source.Hover || source === Source.Focus) {
|
|
419
|
-
|
|
420
|
-
if (
|
|
657
|
+
let suppressFocusInterest = false;
|
|
658
|
+
if (data) {
|
|
659
|
+
data.clearLostTask();
|
|
660
|
+
suppressFocusInterest =
|
|
661
|
+
source === Source.Focus && data.suppressNextFocusInterest;
|
|
662
|
+
data.suppressNextFocusInterest = false;
|
|
663
|
+
}
|
|
664
|
+
for (const upstreamInvoker of allSourceInterestInvokers(el)) {
|
|
421
665
|
upstreamInvoker[dataField].clearLostTask();
|
|
422
666
|
}
|
|
423
|
-
|
|
667
|
+
if (
|
|
668
|
+
target &&
|
|
669
|
+
!suppressFocusInterest &&
|
|
670
|
+
data.state === InterestState.NoInterest
|
|
671
|
+
) {
|
|
672
|
+
ScheduleInterestGainedTask(el, InterestState.FullInterest);
|
|
673
|
+
}
|
|
424
674
|
} else {
|
|
425
675
|
// Dehover or blur
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
676
|
+
if (data) {
|
|
677
|
+
data.clearGainedTask();
|
|
678
|
+
// Explicit interest is not lost by de-hovering or blurring the
|
|
679
|
+
// invoker itself.
|
|
680
|
+
if (data.state === InterestState.FullInterest) {
|
|
681
|
+
ScheduleInterestLostTask(el);
|
|
682
|
+
}
|
|
429
683
|
}
|
|
430
|
-
if (
|
|
431
|
-
upstreamInvoker
|
|
432
|
-
if (source === Source.Blur || !el.matches(":hover")) {
|
|
684
|
+
if (source === Source.Blur || !el.matches(":hover")) {
|
|
685
|
+
for (const upstreamInvoker of allSourceInterestInvokers(el)) {
|
|
433
686
|
ScheduleInterestLostTask(upstreamInvoker);
|
|
434
687
|
}
|
|
435
688
|
}
|
|
436
689
|
}
|
|
437
690
|
}
|
|
438
691
|
|
|
692
|
+
// `::interest-button` support. The pseudo element is polyfilled with a real
|
|
693
|
+
// element, inserted as the next sibling of the invoker. It is only created
|
|
694
|
+
// if the invoker has a `--interest-button-content` value other than `none`
|
|
695
|
+
// or `normal`, which stands in for the `content` property on the real pseudo
|
|
696
|
+
// element.
|
|
697
|
+
function wantsInterestButton(invoker) {
|
|
698
|
+
return (
|
|
699
|
+
invoker instanceof HTMLElement &&
|
|
700
|
+
invoker.localName !== "area" &&
|
|
701
|
+
!!invoker.parentNode &&
|
|
702
|
+
!!GetInterestForTarget(invoker)
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function getInterestButtonContent(invoker) {
|
|
707
|
+
const value = getComputedStyle(invoker)
|
|
708
|
+
.getPropertyValue(interestButtonContentProp)
|
|
709
|
+
.trim();
|
|
710
|
+
if (!value || value === "none" || value === "normal") {
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
return value;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function onInterestButtonClick() {
|
|
717
|
+
// Activating the interest button shows interest immediately, without
|
|
718
|
+
// delays.
|
|
719
|
+
const invoker = this[buttonInvokerField];
|
|
720
|
+
if (invoker) {
|
|
721
|
+
GainOrLoseInterest(
|
|
722
|
+
invoker,
|
|
723
|
+
GetInterestForTarget(invoker),
|
|
724
|
+
InterestState.ExplicitInterest
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function setInterestButton(invoker, content) {
|
|
730
|
+
let button = invoker[buttonField];
|
|
731
|
+
if (!content) {
|
|
732
|
+
if (button) {
|
|
733
|
+
button.remove();
|
|
734
|
+
invoker[buttonField] = null;
|
|
735
|
+
invokersWithButtons.delete(invoker);
|
|
736
|
+
}
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (!button) {
|
|
740
|
+
button = invoker.ownerDocument.createElement("span");
|
|
741
|
+
button.className = interestButtonClass;
|
|
742
|
+
// The real pseudo element is not exposed to accessibility, and is not
|
|
743
|
+
// focusable.
|
|
744
|
+
button.setAttribute("aria-hidden", "true");
|
|
745
|
+
button[buttonInvokerField] = invoker;
|
|
746
|
+
button.addEventListener("click", onInterestButtonClick);
|
|
747
|
+
invoker[buttonField] = button;
|
|
748
|
+
invokersWithButtons.add(invoker);
|
|
749
|
+
}
|
|
750
|
+
if (
|
|
751
|
+
button.style.getPropertyValue(interestButtonInternalContentProp) !==
|
|
752
|
+
content
|
|
753
|
+
) {
|
|
754
|
+
button.style.setProperty(interestButtonInternalContentProp, content);
|
|
755
|
+
}
|
|
756
|
+
if (invoker.nextSibling !== button) {
|
|
757
|
+
invoker.after(button);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function updateInterestButtons(invokers) {
|
|
762
|
+
// Read all styles first, and then modify the DOM, to avoid forcing a style
|
|
763
|
+
// recalc for each invoker.
|
|
764
|
+
const updates = [];
|
|
765
|
+
for (const invoker of invokers) {
|
|
766
|
+
let content = null;
|
|
767
|
+
if (invoker.isConnected && invoker.hasAttribute(attributeName)) {
|
|
768
|
+
if (wantsInterestButton(invoker)) {
|
|
769
|
+
invokersMissingTarget.delete(invoker);
|
|
770
|
+
content = getInterestButtonContent(invoker);
|
|
771
|
+
} else {
|
|
772
|
+
// The target might be added (or become valid) later.
|
|
773
|
+
invokersMissingTarget.add(invoker);
|
|
774
|
+
}
|
|
775
|
+
} else {
|
|
776
|
+
invokersMissingTarget.delete(invoker);
|
|
777
|
+
}
|
|
778
|
+
updates.push([invoker, content]);
|
|
779
|
+
}
|
|
780
|
+
for (const [invoker, content] of updates) {
|
|
781
|
+
setInterestButton(invoker, content);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function handleMutations(records) {
|
|
786
|
+
const candidates = new Set();
|
|
787
|
+
let recheckMissingTargets = false;
|
|
788
|
+
const addInvokers = (node) => {
|
|
789
|
+
if (node.nodeType !== Node.ELEMENT_NODE || node[buttonInvokerField]) {
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (node.hasAttribute(attributeName)) {
|
|
793
|
+
candidates.add(node);
|
|
794
|
+
}
|
|
795
|
+
for (const el of node.querySelectorAll(`[${attributeName}]`)) {
|
|
796
|
+
candidates.add(el);
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
for (const record of records) {
|
|
800
|
+
if (record.type === "childList") {
|
|
801
|
+
for (const node of record.addedNodes) {
|
|
802
|
+
if (node.nodeType === Node.ELEMENT_NODE && !node[buttonInvokerField]) {
|
|
803
|
+
recheckMissingTargets = true;
|
|
804
|
+
addInvokers(node);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
const el = record.target;
|
|
810
|
+
switch (record.attributeName) {
|
|
811
|
+
case attributeName:
|
|
812
|
+
if (record.oldValue !== null) {
|
|
813
|
+
// Changing the `interestfor` attribute removes interest, without
|
|
814
|
+
// firing events.
|
|
815
|
+
resetInterestState(el);
|
|
816
|
+
}
|
|
817
|
+
if (el.getAttribute(attributeName) !== "") {
|
|
818
|
+
el[explicitTargetField] = null;
|
|
819
|
+
}
|
|
820
|
+
candidates.add(el);
|
|
821
|
+
break;
|
|
822
|
+
case "id":
|
|
823
|
+
recheckMissingTargets = true;
|
|
824
|
+
break;
|
|
825
|
+
default:
|
|
826
|
+
// `href` or `disabled`, which affect invoker validity.
|
|
827
|
+
addInvokers(el);
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
// Invokers or targets that were removed from the document lose interest,
|
|
832
|
+
// without firing events.
|
|
833
|
+
for (const invoker of [...invokersWithInterest]) {
|
|
834
|
+
const target = invoker[dataField].target;
|
|
835
|
+
if (!invoker.isConnected || !target || !target.isConnected) {
|
|
836
|
+
resetInterestState(invoker);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (recheckMissingTargets) {
|
|
840
|
+
for (const invoker of invokersMissingTarget) {
|
|
841
|
+
candidates.add(invoker);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
// Keep existing interest buttons next to their invokers, and remove them
|
|
845
|
+
// if their invoker is no longer valid. This avoids reading styles.
|
|
846
|
+
for (const invoker of [...invokersWithButtons]) {
|
|
847
|
+
if (candidates.has(invoker)) {
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
if (!wantsInterestButton(invoker)) {
|
|
851
|
+
setInterestButton(invoker, null);
|
|
852
|
+
} else if (invoker.nextSibling !== invoker[buttonField]) {
|
|
853
|
+
invoker.after(invoker[buttonField]);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
updateInterestButtons(candidates);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function observeMutations() {
|
|
860
|
+
new MutationObserver(handleMutations).observe(document, {
|
|
861
|
+
childList: true,
|
|
862
|
+
subtree: true,
|
|
863
|
+
attributes: true,
|
|
864
|
+
attributeOldValue: true,
|
|
865
|
+
attributeFilter: [attributeName, "id", "href", "disabled"],
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
|
|
439
869
|
// Attach listeners
|
|
440
870
|
function addEventHandlers() {
|
|
441
871
|
const handler = (e, source) => {
|
|
442
|
-
|
|
872
|
+
// Use the composed path, so that invokers in open shadow roots work.
|
|
873
|
+
const path = e.composedPath();
|
|
874
|
+
const start = path.length ? path[0] : e.target;
|
|
875
|
+
if (!(start instanceof Element)) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
for (let el = start; el; el = getParentForInterest(el)) {
|
|
443
879
|
HandleInterestHoverOrFocus(el, source);
|
|
444
880
|
}
|
|
445
881
|
};
|
|
@@ -448,9 +884,16 @@
|
|
|
448
884
|
document.body.addEventListener("focusin", (e) => handler(e, Source.Focus));
|
|
449
885
|
document.body.addEventListener("focusout", (e) => handler(e, Source.Blur));
|
|
450
886
|
document.body.addEventListener("keydown", (e) => {
|
|
451
|
-
if (e.key === "Escape") {
|
|
452
|
-
|
|
453
|
-
|
|
887
|
+
if (e.key === "Escape" && !e.defaultPrevented) {
|
|
888
|
+
// Lose interest in all invokers, most recent first. These
|
|
889
|
+
// `loseinterest` events are not cancelable.
|
|
890
|
+
[...invokersWithInterest].reverse().forEach((invoker) => {
|
|
891
|
+
GainOrLoseInterest(
|
|
892
|
+
invoker,
|
|
893
|
+
invoker[dataField].target,
|
|
894
|
+
InterestState.NoInterest,
|
|
895
|
+
{ force: true }
|
|
896
|
+
);
|
|
454
897
|
});
|
|
455
898
|
}
|
|
456
899
|
});
|
|
@@ -462,7 +905,15 @@
|
|
|
462
905
|
if (invoker) {
|
|
463
906
|
initializeDataField(invoker);
|
|
464
907
|
invoker[dataField].longPressTimer = setTimeout(() => {
|
|
465
|
-
|
|
908
|
+
// Long-press shows interest immediately, and that interest is not
|
|
909
|
+
// lost by de-hovering or blurring the invoker.
|
|
910
|
+
if (invoker[dataField].state === InterestState.NoInterest) {
|
|
911
|
+
GainOrLoseInterest(
|
|
912
|
+
invoker,
|
|
913
|
+
GetInterestForTarget(invoker),
|
|
914
|
+
InterestState.ExplicitInterest
|
|
915
|
+
);
|
|
916
|
+
}
|
|
466
917
|
invoker[dataField].longPressTimer = null;
|
|
467
918
|
}, longPressTime);
|
|
468
919
|
}
|
|
@@ -486,15 +937,90 @@
|
|
|
486
937
|
const style = document.createElement("style");
|
|
487
938
|
style.textContent = `@property ${interestDelayStartProp} {syntax: "normal | <time>"; inherits: false; initial-value: normal;}
|
|
488
939
|
@property ${interestDelayEndProp} {syntax: "normal | <time>"; inherits: false; initial-value: normal;}
|
|
489
|
-
@property ${interestDelayProp} {syntax: "[ normal | <time> ]{1,2}"; inherits: false; initial-value: normal;}
|
|
940
|
+
@property ${interestDelayProp} {syntax: "[ normal | <time> ]{1,2}"; inherits: false; initial-value: normal;}
|
|
941
|
+
@property ${interestButtonContentProp} {syntax: "*"; inherits: false;}`;
|
|
490
942
|
document.head.appendChild(style);
|
|
491
|
-
|
|
943
|
+
// Equivalents of the UA stylesheet rules for interest invokers. These are
|
|
944
|
+
// inserted first, and use :where(), so that author styles take precedence.
|
|
945
|
+
const button = `span.${interestButtonClass}[aria-hidden]`;
|
|
946
|
+
const uaStyle = document.createElement("style");
|
|
947
|
+
uaStyle.textContent = `:where(button[${attributeName}]) {
|
|
948
|
+
-webkit-user-select: none;
|
|
949
|
+
user-select: none;
|
|
950
|
+
}
|
|
951
|
+
:where(${button}) {
|
|
952
|
+
display: inline-block;
|
|
953
|
+
vertical-align: middle;
|
|
954
|
+
color: inherit;
|
|
955
|
+
background-color: transparent;
|
|
956
|
+
box-sizing: border-box;
|
|
957
|
+
min-inline-size: 24px;
|
|
958
|
+
min-block-size: 24px;
|
|
959
|
+
min-block-size: max(24px, 1lh);
|
|
960
|
+
align-content: center;
|
|
961
|
+
text-align: center;
|
|
962
|
+
-webkit-user-select: none;
|
|
963
|
+
user-select: none;
|
|
964
|
+
cursor: help;
|
|
965
|
+
}
|
|
966
|
+
:where(${button}:hover) {
|
|
967
|
+
background-color: color-mix(in oklab, currentColor 10%, transparent);
|
|
968
|
+
}
|
|
969
|
+
:where(${button}:active) {
|
|
970
|
+
background-color: color-mix(in oklab, currentColor 20%, transparent);
|
|
971
|
+
}
|
|
972
|
+
:where(${button})::before {
|
|
973
|
+
content: var(${interestButtonInternalContentProp});
|
|
974
|
+
}`;
|
|
975
|
+
document.head.prepend(uaStyle);
|
|
976
|
+
document[dataField] = { globalPropsStyle: style, uaStyle };
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// The `interestForElement` IDL attribute, which reflects the `interestfor`
|
|
980
|
+
// content attribute as an element reference.
|
|
981
|
+
function installInterestForElement() {
|
|
982
|
+
const descriptor = {
|
|
983
|
+
configurable: true,
|
|
984
|
+
enumerable: true,
|
|
985
|
+
get() {
|
|
986
|
+
return getAttrAssociatedElement(this);
|
|
987
|
+
},
|
|
988
|
+
set(value) {
|
|
989
|
+
if (value === null || value === undefined) {
|
|
990
|
+
this[explicitTargetField] = null;
|
|
991
|
+
this.removeAttribute(attributeName);
|
|
992
|
+
} else if (value instanceof Element) {
|
|
993
|
+
this[explicitTargetField] = value;
|
|
994
|
+
this.setAttribute(attributeName, "");
|
|
995
|
+
} else {
|
|
996
|
+
throw new TypeError(
|
|
997
|
+
"Failed to set the 'interestForElement' property: The provided value is not of type 'Element'."
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
},
|
|
1001
|
+
};
|
|
1002
|
+
[
|
|
1003
|
+
window.HTMLButtonElement,
|
|
1004
|
+
window.HTMLAnchorElement,
|
|
1005
|
+
window.HTMLAreaElement,
|
|
1006
|
+
window.SVGAElement,
|
|
1007
|
+
].forEach((iface) => {
|
|
1008
|
+
if (
|
|
1009
|
+
iface &&
|
|
1010
|
+
!Object.prototype.hasOwnProperty.call(iface.prototype, "interestForElement")
|
|
1011
|
+
) {
|
|
1012
|
+
Object.defineProperty(iface.prototype, "interestForElement", descriptor);
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
492
1015
|
}
|
|
1016
|
+
installInterestForElement();
|
|
493
1017
|
|
|
494
1018
|
// Initialize
|
|
495
1019
|
function init() {
|
|
496
1020
|
registerCustomProperties();
|
|
497
1021
|
addEventHandlers();
|
|
1022
|
+
observeMutations();
|
|
1023
|
+
updateInterestButtons(document.querySelectorAll(`[${attributeName}]`));
|
|
498
1024
|
console.log(`interestfor polyfill installed (native: ${nativeSupported}).`);
|
|
499
1025
|
}
|
|
500
1026
|
if (document.readyState === "complete") {
|
package/src/interestfor.min.js
CHANGED
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
// LICENSE file in the root directory of this source tree.
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
!function(){const e="interest",t="loseinterest",n="--interest-delay-start",r="--interest-delay-end",o="--interest-delay",s="__interestForData",i="__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})}window.InterestEvent||(window.InterestEvent=class extends Event{#e;constructor(e,t={}){super(e,t);const{source:n}=t;if(null!=n&&!(n instanceof Element))throw new TypeError("source must be an element");this.#e=n}get[Symbol.toStringTag](){return"InterestEvent"}get source(){const e=this.#e;if(!e)return null;const t=p(e);return t!==p(this.target||document)?t.host:e}});const u="none",d="full",m="hover",f="dehover",h="focus",g="blur";function p(e){return e&&"function"==typeof e.getRootNode?e.getRootNode():e&&e.parentNode?p(e.parentNode):e}function y(t,n,r){if(!t||!n)return!1;if(!t.isConnected||T(t)!==n||r===u&&b(n)!==t)return!1;if(r!==u){const o=b(n);if(o){if(o===t)return o[s].clearLostTask(),!1;if(!y(o,n,u))return!1;if(!t.isConnected||T(t)!==n)return!1}return function(t,n){const r=t[s],o=T(t);if(n!==d)throw new Error("Invalid state");if(r.state!==u)throw new Error("Invalid state");if(!o.dispatchEvent(new InterestEvent(e,{source:t})))return;try{o.showPopover({source:t})}catch{}if(r.state=d,o[i]||(o[i]={}),o[i].invoker=t,o.hasAttribute("popover")&&(o[i].toggleListener=L,o.addEventListener("toggle",L)),a.add(t),t.classList.add("interest-source"),o.classList.add("interest-target"),P(o)||t.setAttribute("aria-expanded","true"),"none"===getComputedStyle(t).anchorName&&"auto"===getComputedStyle(o).positionAnchor){const e=`--interest-anchor-${Math.random().toString(36).substring(2)}`;t.style.anchorName=e,o.style.positionAnchor=e,r.anchorName=e}return!0}(t,r)}return k(t),!0}function v(e){const t=1e3*w(e,r);!isFinite(t)||t<0||(e[s].clearLostTask(),e[s].lostTimer=setTimeout(()=>{y(e,T(e),u)},t))}function b(e){const t=e[i]?.invoker||null;return t&&t[s]?.state!==u?t:null}function T(e){const t=e.getAttribute("interestfor");return document.getElementById(t)}function L(e){const t=e.target;if("closed"===e.newState){const e=t[i]?.invoker;e&&y(e,t,u)}}function E(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),s=r.getPropertyValue(t).trim();if("normal"!==s.toLowerCase())return E(s);const i=r.getPropertyValue(o).trim();if(i&&"normal"!==i.toLowerCase()){const e=i.split(/\s+/).filter(e=>e.length>0);if(e.length>0){const r=e[0],o=e.length>1?e[1]:r;let s;if(s=t===n?r:o,"normal"!==s.toLowerCase())return E(s)}}return t===n?.5:.25}function k(e,n=!1){const r=e[s];if(clearTimeout(r.gainedTimer),clearTimeout(r.lostTimer),r.state!==u){const o=T(e),s=o.dispatchEvent(new InterestEvent(t,{source:e}));if(!n&&!s)return;try{o.hidePopover()}catch{}o[i]?.toggleListener&&o.removeEventListener("toggle",o[i].toggleListener),o[i]=null,a.delete(e),e.classList.remove("interest-source"),o.classList.remove("interest-target"),P(o)||e.setAttribute("aria-expanded","false"),r.anchorName&&(e.style.anchorName="",o.style.positionAnchor="",r.anchorName=null),r.state=u}}const A=["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(A))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 S(e){if(e[s])return;e[s]={state:u,gainedTimer:null,lostTimer:null,longPressTimer:null,clearGainedTask(){clearTimeout(this.gainedTimer)},clearLostTask(){clearTimeout(this.lostTimer)}};const t=T(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(!T(e)){const n=e.closest(".interest-target");if(n){const r=b(n);r&&(t===m||t===h?r[s].clearLostTask():t!==g&&e.matches(":hover")||v(r))}return}S(e);const r=e[s],o=b(e);t===m||t===h?(r.clearLostTask&&r.clearLostTask(),o&&o[s].clearLostTask(),function(e,t){const r=1e3*w(e,n);!isFinite(r)||r<0||(e[s].clearGainedTask(),e[s].gainedTimer=setTimeout(()=>{y(e,T(e),t)},r))}(e,d)):(r.clearGainedTask&&r.clearGainedTask(),r.state!==u&&v(e),o&&(o[s].clearGainedTask(),t!==g&&e.matches(":hover")||v(o)))}function x(){!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[s]={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=>{k(e,!0)})}),document.body.addEventListener("touchstart",e=>{l=!0;const t=e.target.closest("button[interestfor]");t&&(S(t),t[s].longPressTimer=setTimeout(()=>{y(t,T(t),d),t[s].longPressTimer=null},500))});const t=e=>{const t=e.target.closest("button[interestfor]");t&&t[s]?.longPressTimer&&(clearTimeout(t[s].longPressTimer),t[s].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?x():window.addEventListener("load",x)}();
|
|
8
|
+
!function(){const e="interestfor",t="interest",n="loseinterest",r="--interest-delay-start",o="--interest-delay-end",s="--interest-delay",i="--interest-button-content",a="--interest-button-polyfill-content",l="interest-button",c="__interestForData",u="__interestForTargetData",d="__interestForButton",f="__interestForButtonInvoker",m="__interestForExplicitTarget",p="http://www.w3.org/1999/xlink",h=new Set,b=new Set,y=new Set;let g=!1;if(window.interestForPolyfillInstalled)return;window.interestForPolyfillInstalled=!0;const w=HTMLButtonElement.prototype.hasOwnProperty("interestForElement");if(w&&!window.interestForUsePolyfillAlways)return;if(w){const e=e=>{e.isTrusted&&(e.preventDefault(),e.stopImmediatePropagation())};document.body.addEventListener(t,e,{capture:!0}),document.body.addEventListener(n,e,{capture:!0})}window.InterestEvent||(window.InterestEvent=class extends Event{#e;constructor(e,t={}){super(e,t);const{source:n}=t;if(null!=n&&!(n instanceof Element))throw new TypeError("source must be an element");this.#e=n}get[Symbol.toStringTag](){return"InterestEvent"}get source(){const e=this.#e;if(!e)return null;const t=S(e);return t!==S(this.target||document)?t.host:e}});const E="none",v="full",T="explicit",L="hover",A="dehover",x="focus",k="blur";function S(e){return e&&"function"==typeof e.getRootNode?e.getRootNode():e&&e.parentNode?S(e.parentNode):e}function N(e,r,o,s={}){if(!e||!r)return!1;if(!e.isConnected||I(e)!==r||o===E&&F(r)!==e)return!1;if(o!==E){q(e),e[c].state!==E&&e[c].target!==r&&H(e);const n=F(r);if(n){if(n===e)return o===T&&(n[c].state=T),n[c].clearLostTask(),!1;if(!N(n,r,E))return!1;if(!e.isConnected||I(e)!==r)return!1}return function(e,n,r){const o=e[c];switch(r){case v:case T:if(o.state!==E)throw new Error("Invalid state");if(!n.dispatchEvent(new InterestEvent(t,{source:e,cancelable:!0})))return!1;if(o.state=r,o.target=n,o.clearGainedTask(),n[u]||(n[u]={}),n[u].invoker=e,n.hasAttribute("popover")&&(n.addEventListener("beforetoggle",M),n[u].shownByInterest=!1,!$(n))){try{n.showPopover({source:e})}catch{}n[u].shownByInterest=$(n)}if(h.add(e),e.classList.add("interest-source"),n.classList.add("interest-target"),G(n)||e.setAttribute("aria-expanded","true"),"none"===getComputedStyle(e).anchorName&&"auto"===getComputedStyle(n).positionAnchor){const t=`--interest-anchor-${Math.random().toString(36).substring(2)}`;e.style.anchorName=t,n.style.positionAnchor=t,o.anchorName=t}break;default:throw new Error("Invalid state")}return!0}(e,r,o)}return function(e,{force:t=!1,closePopovers:r=!0}={}){const o=e[c];if(clearTimeout(o.gainedTimer),clearTimeout(o.lostTimer),o.state===E)return!0;const s=o.target,i=s.dispatchEvent(new InterestEvent(n,{source:e,cancelable:!t}));if(!i)return!1;const a=s[u],l=r&&a?.invoker===e&&a.shownByInterest;H(e),l&&$(s)&&function(e,t){const n=e[c],r=t.contains(document.activeElement);r&&(n.suppressNextFocusInterest=!0);try{t.hidePopover()}catch{}r&&(n.suppressNextFocusInterest=!1)}(e,s);return!0}(e,s)}function P(e){const t=1e3*V(e,o);if(!_(t))return;const n=e[c];n.clearLostTask();const r=n.target;n.lostTimer=setTimeout(()=>{N(e,r,E)},t)}function F(e){const t=e[u]?.invoker||null;if(!t)return null;const n=t[c];return n&&n.state!==E&&n.target===e&&I(t)===e?t:null}function I(t){if(!t.hasAttribute(e)||!t.isConnected||!function(e){if(e instanceof HTMLButtonElement)return!e.matches(":disabled");if(e instanceof HTMLAnchorElement||e instanceof HTMLAreaElement)return e.hasAttribute("href");if(window.SVGAElement&&e instanceof SVGAElement)return e.hasAttribute("href")||e.hasAttributeNS(p,"href");return!1}(t))return null;const n=C(t);return n&&n.isConnected?n:null}function C(t){const n=t.getAttribute(e);if(null===n)return null;const r=t[m];if(r&&""===n)return function(e,t){const n=S(e);let r=S(t);for(;r;){if(r===n)return!0;r=r instanceof ShadowRoot?S(r.host):null}return!1}(r,t)?r:null;if(""===n)return null;const o=S(t);return"function"==typeof o.getElementById?o.getElementById(n):null}function $(e){try{return e.matches(":popover-open")}catch{return!1}}function M(e){if("closed"!==e.newState)return;const t=e.currentTarget,n=F(t);if(n){if(t.contains(document.activeElement)){const e=n[c];e.suppressNextFocusInterest=!0,setTimeout(()=>{e.suppressNextFocusInterest=!1},0)}N(n,t,E,{closePopovers:!1})}}function _(e){return e>=0&&e<=2147483647}function B(e){const t=String(e).trim(),n=t.match(/^([\d.]+(?:e[+-]?\d+)?)s$/i);if(n)return parseFloat(n[1]);const r=t.match(/^([\d.]+(?:e[+-]?\d+)?)ms$/i);return r?parseFloat(r[1])/1e3:/\b(infinity|nan)\b/i.test(t)?1/0:parseFloat(t)||0}function V(e,t){const n=getComputedStyle(e),o=n.getPropertyValue(t).trim();if("normal"!==o.toLowerCase())return B(o);const i=n.getPropertyValue(s).trim();if(i&&"normal"!==i.toLowerCase()){const e=i.split(/\s+/).filter(e=>e.length>0);if(e.length>0){const n=e[0],o=e.length>1?e[1]:n;let s;if(s=t===r?n:o,"normal"!==s.toLowerCase())return B(s)}}return t===r?.25:.15}function H(e){const t=e[c];if(!t)return;if(clearTimeout(t.gainedTimer),clearTimeout(t.lostTimer),t.state===E)return;const n=t.target;h.delete(e),e.classList.remove("interest-source"),n&&(n[u]?.invoker===e&&(n.removeEventListener("beforetoggle",M),n[u]=null,n.classList.remove("interest-target")),G(n)||e.setAttribute("aria-expanded","false")),t.anchorName&&(e.style.anchorName="",n&&(n.style.positionAnchor=""),t.anchorName=null),t.state=E,t.target=null}const O=["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 G(e){if("hint"!==e.getAttribute("popover")?.toLowerCase())return!1;if(e.querySelector(O))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 q(e){if(e[c])return;e[c]={state:E,target:null,gainedTimer:null,lostTimer:null,longPressTimer:null,suppressNextFocusInterest:!1,clearGainedTask(){clearTimeout(this.gainedTimer)},clearLostTask(){clearTimeout(this.lostTimer)}};const t=I(e);t&&function(e,t){G(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 D(e){if(e[f])return e[f];if(e.assignedSlot)return e.assignedSlot;if(e.parentElement)return e.parentElement;const t=e.parentNode;return t instanceof ShadowRoot?t.host:null}function z(e){const t=new Set;if(0===h.size)return t;const n=e=>{const r=F(e);r&&!t.has(r)&&(t.add(r),n(r));const o=D(e);o&&!t.has(o)&&n(o)};return n(e),t}function j(e,t){if(g)return;if(!e.isConnected)return;const n=I(e);n&&q(e);const o=e[c];if(t===L||t===x){let s=!1;o&&(o.clearLostTask(),s=t===x&&o.suppressNextFocusInterest,o.suppressNextFocusInterest=!1);for(const t of z(e))t[c].clearLostTask();n&&!s&&o.state===E&&function(e,t){const n=I(e);if(!n)return;const o=1e3*V(e,r);_(o)&&(e[c].clearGainedTask(),e[c].gainedTimer=setTimeout(()=>{N(e,n,t)},o))}(e,v)}else if(o&&(o.clearGainedTask(),o.state===v&&P(e)),t===k||!e.matches(":hover"))for(const t of z(e))P(t)}function R(e){return e instanceof HTMLElement&&"area"!==e.localName&&!!e.parentNode&&!!I(e)}function U(e){const t=getComputedStyle(e).getPropertyValue(i).trim();return t&&"none"!==t&&"normal"!==t?t:null}function J(){const e=this[f];e&&N(e,I(e),T)}function K(e,t){let n=e[d];t?(n||(n=e.ownerDocument.createElement("span"),n.className=l,n.setAttribute("aria-hidden","true"),n[f]=e,n.addEventListener("click",J),e[d]=n,b.add(e)),n.style.getPropertyValue(a)!==t&&n.style.setProperty(a,t),e.nextSibling!==n&&e.after(n)):n&&(n.remove(),e[d]=null,b.delete(e))}function Q(t){const n=[];for(const r of t){let t=null;r.isConnected&&r.hasAttribute(e)?R(r)?(y.delete(r),t=U(r)):y.add(r):y.delete(r),n.push([r,t])}for(const[e,t]of n)K(e,t)}function W(t){const n=new Set;let r=!1;const o=t=>{if(t.nodeType===Node.ELEMENT_NODE&&!t[f]){t.hasAttribute(e)&&n.add(t);for(const r of t.querySelectorAll(`[${e}]`))n.add(r)}};for(const s of t){if("childList"===s.type){for(const e of s.addedNodes)e.nodeType!==Node.ELEMENT_NODE||e[f]||(r=!0,o(e));continue}const t=s.target;switch(s.attributeName){case e:null!==s.oldValue&&H(t),""!==t.getAttribute(e)&&(t[m]=null),n.add(t);break;case"id":r=!0;break;default:o(t)}}for(const e of[...h]){const t=e[c].target;e.isConnected&&t&&t.isConnected||H(e)}if(r)for(const e of y)n.add(e);for(const e of[...b])n.has(e)||(R(e)?e.nextSibling!==e[d]&&e.after(e[d]):K(e,null));Q(n)}function X(){!function(){const t=document.createElement("style");t.textContent=`@property ${r} {syntax: "normal | <time>"; inherits: false; initial-value: normal;}\n @property ${o} {syntax: "normal | <time>"; inherits: false; initial-value: normal;}\n @property ${s} {syntax: "[ normal | <time> ]{1,2}"; inherits: false; initial-value: normal;}\n @property ${i} {syntax: "*"; inherits: false;}`,document.head.appendChild(t);const n=`span.${l}[aria-hidden]`,u=document.createElement("style");u.textContent=`:where(button[${e}]) {\n -webkit-user-select: none;\n user-select: none;\n }\n :where(${n}) {\n display: inline-block;\n vertical-align: middle;\n color: inherit;\n background-color: transparent;\n box-sizing: border-box;\n min-inline-size: 24px;\n min-block-size: 24px;\n min-block-size: max(24px, 1lh);\n align-content: center;\n text-align: center;\n -webkit-user-select: none;\n user-select: none;\n cursor: help;\n }\n :where(${n}:hover) {\n background-color: color-mix(in oklab, currentColor 10%, transparent);\n }\n :where(${n}:active) {\n background-color: color-mix(in oklab, currentColor 20%, transparent);\n }\n :where(${n})::before {\n content: var(${a});\n }`,document.head.prepend(u),document[c]={globalPropsStyle:t,uaStyle:u}}(),function(){const e=(e,t)=>{const n=e.composedPath(),r=n.length?n[0]:e.target;if(r instanceof Element)for(let e=r;e;e=D(e))j(e,t)};document.body.addEventListener("mouseover",t=>e(t,L)),document.body.addEventListener("mouseout",t=>e(t,A)),document.body.addEventListener("focusin",t=>e(t,x)),document.body.addEventListener("focusout",t=>e(t,k)),document.body.addEventListener("keydown",e=>{"Escape"!==e.key||e.defaultPrevented||[...h].reverse().forEach(e=>{N(e,e[c].target,E,{force:!0})})}),document.body.addEventListener("touchstart",e=>{g=!0;const t=e.target.closest("button[interestfor]");t&&(q(t),t[c].longPressTimer=setTimeout(()=>{t[c].state===E&&N(t,I(t),T),t[c].longPressTimer=null},500))});const t=e=>{const t=e.target.closest("button[interestfor]");t&&t[c]?.longPressTimer&&(clearTimeout(t[c].longPressTimer),t[c].longPressTimer=null)};document.body.addEventListener("touchend",e=>{t(e),g=!1}),document.body.addEventListener("touchmove",t)}(),new MutationObserver(W).observe(document,{childList:!0,subtree:!0,attributes:!0,attributeOldValue:!0,attributeFilter:[e,"id","href","disabled"]}),Q(document.querySelectorAll(`[${e}]`)),console.log(`interestfor polyfill installed (native: ${w}).`)}!function(){const t={configurable:!0,enumerable:!0,get(){return C(this)},set(t){if(null==t)this[m]=null,this.removeAttribute(e);else{if(!(t instanceof Element))throw new TypeError("Failed to set the 'interestForElement' property: The provided value is not of type 'Element'.");this[m]=t,this.setAttribute(e,"")}}};[window.HTMLButtonElement,window.HTMLAnchorElement,window.HTMLAreaElement,window.SVGAElement].forEach(e=>{e&&!Object.prototype.hasOwnProperty.call(e.prototype,"interestForElement")&&Object.defineProperty(e.prototype,"interestForElement",t)})}(),"complete"===document.readyState?X():window.addEventListener("load",X)}();
|
package/test/basic_test.html
CHANGED
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
<a interestfor=foo href=bar>Link - hover me, don't click me</a>
|
|
6
6
|
<button interestfor=foo>
|
|
7
|
-
<span style="margin:10px;">Button - hover me, don't click me</span></
|
|
7
|
+
<span style="margin:10px;">Button - hover me, don't click me</span></button>
|
|
8
8
|
<div popover id=foo>Popover</div>
|
|
9
|
+
<p>
|
|
10
|
+
<a interestfor=bar href=bar class=with-button>Link with an interest button - click the "?"</a>
|
|
11
|
+
</p>
|
|
12
|
+
<div popover id=bar>Popover shown by the interest button</div>
|
|
9
13
|
|
|
10
14
|
<style>
|
|
11
15
|
[interestfor] {
|
|
@@ -23,4 +27,12 @@
|
|
|
23
27
|
[popover] {
|
|
24
28
|
position-area: right center;
|
|
25
29
|
}
|
|
30
|
+
/* Native ::interest-button */
|
|
31
|
+
.with-button::interest-button {
|
|
32
|
+
content: "?";
|
|
33
|
+
}
|
|
34
|
+
/* Polyfilled ::interest-button */
|
|
35
|
+
.with-button {
|
|
36
|
+
--interest-button-content: "?";
|
|
37
|
+
}
|
|
26
38
|
</style>
|