interestfor 1.0.7 → 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 +102 -8
- package/build.sh +1 -1
- package/package.json +2 -4
- package/src/interestfor.js +653 -88
- package/src/interestfor.min.js +1 -1
- package/test/basic_test.html +13 -1
package/README.md
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
# `interestfor` Polyfill
|
|
2
2
|
|
|
3
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
See the explainer for all of the details for the "real" feature:
|
|
4
|
+
interactions" that is in the process of being implemented in browsers. It [shipped](https://chromestatus.com/feature/4530756656562176)
|
|
5
|
+
in Chromium browsers (e.g. Chrome, Edge) in version 142 (October 2025).
|
|
6
|
+
See the MDN documentation for more details:
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
https://developer.mozilla.org/en-US/docs/Web/API/Popover_API/Using_interest_invokers
|
|
10
9
|
|
|
11
10
|
## Usage
|
|
12
11
|
To use this polyfill, simply load it:
|
|
@@ -28,14 +27,84 @@ can be done by hovering the element with the mouse, or focusing it with the
|
|
|
28
27
|
keyboard. If the target is a popover, it will automatically be shown and hidden
|
|
29
28
|
upon receiving and losing interest.
|
|
30
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
|
+
|
|
31
52
|
Again, this is a quick summary. For the complete details, please [read the
|
|
32
|
-
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.
|
|
33
101
|
|
|
34
102
|
## Touchscreen support
|
|
35
103
|
|
|
36
104
|
This polyfill provides limited touchscreen support. On a touchscreen,
|
|
37
105
|
long-pressing a `<button>` element that has an `interestfor` attribute will show
|
|
38
|
-
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.
|
|
39
108
|
|
|
40
109
|
However, long-pressing a link (`<a>` element) is **not** supported. This is
|
|
41
110
|
because on today's browsers, long-pressing a link element brings up a useful
|
|
@@ -45,7 +114,8 @@ be surpressed completely. There is currently no way to provide access to the
|
|
|
45
114
|
long-press context menu and *also* provide a way to show interest. Since the
|
|
46
115
|
context menu contains items that users often use, eliminating access to the
|
|
47
116
|
context menu is not what most people want. So this polyfill will not support
|
|
48
|
-
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).
|
|
49
119
|
|
|
50
120
|
This is one of the primary motivations for Chromium championing this as a
|
|
51
121
|
native API in all browsers: to support all users, including those using
|
|
@@ -109,6 +179,30 @@ native feature:
|
|
|
109
179
|
class names. This is to avoid the entire selector being invalidated if the
|
|
110
180
|
browser does not recognize the native pseudo class (e.g. `:interest-source`).
|
|
111
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
|
+
|
|
112
206
|
|
|
113
207
|
## Tests
|
|
114
208
|
|
package/build.sh
CHANGED
|
@@ -11,7 +11,7 @@ MINIFIED_FILE="${BUILD_DIR}/interestfor.min.js"
|
|
|
11
11
|
OUTPUT_FILE="${SRC_DIR}/interestfor.min.js"
|
|
12
12
|
|
|
13
13
|
# Minify the combined file
|
|
14
|
-
terser "${SOURCE_FILE}" --compress --mangle --output "${MINIFIED_FILE}"
|
|
14
|
+
npx terser "${SOURCE_FILE}" --compress --mangle --output "${MINIFIED_FILE}"
|
|
15
15
|
|
|
16
16
|
cat COPYRIGHT "${MINIFIED_FILE}" > "${OUTPUT_FILE}"
|
|
17
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "interestfor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "A polyfill for the `interestfor` API",
|
|
5
5
|
"keywords": "interestfor, interesttarget, polyfill, hover-triggering",
|
|
6
6
|
"homepage": "https://github.com/mfreed7/interestfor#readme",
|
|
@@ -11,7 +11,5 @@
|
|
|
11
11
|
},
|
|
12
12
|
"author": "Mason Freed <mfreed@mfreed.com>",
|
|
13
13
|
"license": "BSD-3-Clause",
|
|
14
|
-
"main": "src/interestfor.min.js"
|
|
15
|
-
"dependencies": {}
|
|
14
|
+
"main": "src/interestfor.min.js"
|
|
16
15
|
}
|
|
17
|
-
|
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
|
|
@@ -44,11 +59,45 @@
|
|
|
44
59
|
capture: true,
|
|
45
60
|
});
|
|
46
61
|
}
|
|
62
|
+
|
|
63
|
+
if(!window.InterestEvent) {
|
|
64
|
+
window.InterestEvent = class InterestEvent extends Event {
|
|
65
|
+
#source;
|
|
66
|
+
constructor(type, eventInit = {}) {
|
|
67
|
+
super(type, eventInit);
|
|
68
|
+
const { source } = eventInit;
|
|
69
|
+
if (source != null && !(source instanceof Element)) {
|
|
70
|
+
throw new TypeError(`source must be an element`);
|
|
71
|
+
}
|
|
72
|
+
this.#source = source;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get [Symbol.toStringTag]() {
|
|
76
|
+
return "InterestEvent";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get source() {
|
|
80
|
+
const source = this.#source;
|
|
81
|
+
if (!source) return null;
|
|
82
|
+
// Retarget `source` to the current event's root:
|
|
83
|
+
const sourceRoot = getRootNode(source);
|
|
84
|
+
if (sourceRoot !== getRootNode(this.target || document)) {
|
|
85
|
+
return sourceRoot.host;
|
|
86
|
+
}
|
|
87
|
+
return source;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
47
91
|
|
|
48
92
|
// Enum-like state and source
|
|
49
93
|
const InterestState = {
|
|
50
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.
|
|
51
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",
|
|
52
101
|
};
|
|
53
102
|
const Source = {
|
|
54
103
|
Hover: "hover",
|
|
@@ -58,8 +107,16 @@
|
|
|
58
107
|
Touch: "touch",
|
|
59
108
|
};
|
|
60
109
|
|
|
110
|
+
function getRootNode(node) {
|
|
111
|
+
if (node && typeof node.getRootNode === "function") {
|
|
112
|
+
return node.getRootNode();
|
|
113
|
+
}
|
|
114
|
+
if (node && node.parentNode) return getRootNode(node.parentNode);
|
|
115
|
+
return node;
|
|
116
|
+
}
|
|
117
|
+
|
|
61
118
|
// Gain or lose interest
|
|
62
|
-
function GainOrLoseInterest(invoker, target, newState) {
|
|
119
|
+
function GainOrLoseInterest(invoker, target, newState, options = {}) {
|
|
63
120
|
if (!invoker || !target) {
|
|
64
121
|
return false;
|
|
65
122
|
}
|
|
@@ -74,9 +131,21 @@
|
|
|
74
131
|
|
|
75
132
|
// If gaining, handle existing invoker
|
|
76
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
|
+
}
|
|
77
143
|
const existing = GetInterestInvoker(target);
|
|
78
144
|
if (existing) {
|
|
79
145
|
if (existing === invoker) {
|
|
146
|
+
if (newState === InterestState.ExplicitInterest) {
|
|
147
|
+
existing[dataField].state = InterestState.ExplicitInterest;
|
|
148
|
+
}
|
|
80
149
|
existing[dataField].clearLostTask();
|
|
81
150
|
return false;
|
|
82
151
|
} else {
|
|
@@ -93,73 +162,182 @@
|
|
|
93
162
|
}
|
|
94
163
|
}
|
|
95
164
|
// finally apply interest
|
|
96
|
-
return applyState(invoker, newState);
|
|
165
|
+
return applyState(invoker, target, newState);
|
|
97
166
|
}
|
|
98
167
|
|
|
99
168
|
// losing interest
|
|
100
|
-
clearState(invoker);
|
|
101
|
-
return true;
|
|
169
|
+
return clearState(invoker, options);
|
|
102
170
|
}
|
|
103
171
|
|
|
104
172
|
// Schedule tasks
|
|
105
173
|
function ScheduleInterestGainedTask(invoker, newState) {
|
|
174
|
+
const target = GetInterestForTarget(invoker);
|
|
175
|
+
if (!target) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
106
178
|
const delay = getDelaySeconds(invoker, interestDelayStartProp) * 1000;
|
|
107
|
-
if (!
|
|
179
|
+
if (!isSchedulableDelay(delay)) {
|
|
108
180
|
return;
|
|
109
181
|
}
|
|
110
182
|
invoker[dataField].clearGainedTask();
|
|
111
183
|
invoker[dataField].gainedTimer = setTimeout(() => {
|
|
112
|
-
GainOrLoseInterest(invoker,
|
|
184
|
+
GainOrLoseInterest(invoker, target, newState);
|
|
113
185
|
}, delay);
|
|
114
186
|
}
|
|
115
187
|
|
|
116
188
|
function ScheduleInterestLostTask(invoker) {
|
|
117
189
|
const delay = getDelaySeconds(invoker, interestDelayEndProp) * 1000;
|
|
118
|
-
if (!
|
|
190
|
+
if (!isSchedulableDelay(delay)) {
|
|
119
191
|
return;
|
|
120
192
|
}
|
|
121
|
-
invoker[dataField]
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
InterestState.NoInterest
|
|
127
|
-
);
|
|
193
|
+
const data = invoker[dataField];
|
|
194
|
+
data.clearLostTask();
|
|
195
|
+
const target = data.target;
|
|
196
|
+
data.lostTimer = setTimeout(() => {
|
|
197
|
+
GainOrLoseInterest(invoker, target, InterestState.NoInterest);
|
|
128
198
|
}, delay);
|
|
129
199
|
}
|
|
130
200
|
|
|
131
201
|
// Helpers
|
|
132
202
|
function GetInterestInvoker(target) {
|
|
133
203
|
const inv = target[targetDataField]?.invoker || null;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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;
|
|
137
217
|
}
|
|
218
|
+
|
|
219
|
+
// Returns the target of a valid interest invoker, or null.
|
|
138
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) {
|
|
139
250
|
const id = el.getAttribute(attributeName);
|
|
140
|
-
|
|
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;
|
|
141
267
|
}
|
|
142
268
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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;
|
|
149
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;
|
|
150
288
|
}
|
|
151
289
|
}
|
|
152
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
|
+
|
|
153
326
|
function parseTimeValue(val) {
|
|
154
327
|
const s = String(val).trim();
|
|
155
|
-
const m_s = s.match(/^([\d.]+)s$/);
|
|
328
|
+
const m_s = s.match(/^([\d.]+(?:e[+-]?\d+)?)s$/i);
|
|
156
329
|
if (m_s) {
|
|
157
330
|
return parseFloat(m_s[1]);
|
|
158
331
|
}
|
|
159
|
-
const m_ms = s.match(/^([\d.]+)ms$/);
|
|
332
|
+
const m_ms = s.match(/^([\d.]+(?:e[+-]?\d+)?)ms$/i);
|
|
160
333
|
if (m_ms) {
|
|
161
334
|
return parseFloat(m_ms[1]) / 1000;
|
|
162
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
|
+
}
|
|
163
341
|
return parseFloat(s) || 0;
|
|
164
342
|
}
|
|
165
343
|
|
|
@@ -195,31 +373,44 @@
|
|
|
195
373
|
}
|
|
196
374
|
}
|
|
197
375
|
|
|
198
|
-
// If we got here, the effective value is 'normal'. Return the default.
|
|
199
|
-
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;
|
|
200
378
|
}
|
|
201
379
|
|
|
202
380
|
// Actual state transitions
|
|
203
|
-
function applyState(invoker, newState) {
|
|
381
|
+
function applyState(invoker, target, newState) {
|
|
204
382
|
const data = invoker[dataField];
|
|
205
|
-
const target = GetInterestForTarget(invoker);
|
|
206
383
|
switch (newState) {
|
|
207
384
|
case InterestState.FullInterest:
|
|
385
|
+
case InterestState.ExplicitInterest:
|
|
208
386
|
if (data.state !== InterestState.NoInterest) {
|
|
209
387
|
throw new Error("Invalid state");
|
|
210
388
|
}
|
|
211
|
-
target.dispatchEvent(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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();
|
|
216
399
|
if (!target[targetDataField]) {
|
|
217
400
|
target[targetDataField] = {};
|
|
218
401
|
}
|
|
219
402
|
target[targetDataField].invoker = invoker;
|
|
220
403
|
if (target.hasAttribute('popover')) {
|
|
221
|
-
target
|
|
222
|
-
|
|
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
|
+
}
|
|
223
414
|
}
|
|
224
415
|
invokersWithInterest.add(invoker);
|
|
225
416
|
invoker.classList.add("interest-source");
|
|
@@ -243,32 +434,88 @@
|
|
|
243
434
|
return true;
|
|
244
435
|
}
|
|
245
436
|
|
|
246
|
-
|
|
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 } = {}) {
|
|
441
|
+
const data = invoker[dataField];
|
|
442
|
+
clearTimeout(data.gainedTimer);
|
|
443
|
+
clearTimeout(data.lostTimer);
|
|
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) {
|
|
247
472
|
const data = invoker[dataField];
|
|
473
|
+
if (!data) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
248
476
|
clearTimeout(data.gainedTimer);
|
|
249
477
|
clearTimeout(data.lostTimer);
|
|
250
|
-
if (data.state
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
target.classList.remove("interest-target");
|
|
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
|
+
}
|
|
263
490
|
if (!isPlainHint(target)) {
|
|
264
491
|
invoker.setAttribute("aria-expanded", "false");
|
|
265
492
|
}
|
|
266
|
-
|
|
267
|
-
|
|
493
|
+
}
|
|
494
|
+
if (data.anchorName) {
|
|
495
|
+
invoker.style.anchorName = "";
|
|
496
|
+
if (target) {
|
|
268
497
|
target.style.positionAnchor = "";
|
|
269
|
-
data.anchorName = null;
|
|
270
498
|
}
|
|
271
|
-
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;
|
|
272
519
|
}
|
|
273
520
|
}
|
|
274
521
|
|
|
@@ -331,9 +578,11 @@
|
|
|
331
578
|
if (el[dataField]) return;
|
|
332
579
|
el[dataField] = {
|
|
333
580
|
state: InterestState.NoInterest,
|
|
581
|
+
target: null,
|
|
334
582
|
gainedTimer: null,
|
|
335
583
|
lostTimer: null,
|
|
336
584
|
longPressTimer: null,
|
|
585
|
+
suppressNextFocusInterest: false,
|
|
337
586
|
clearGainedTask() {
|
|
338
587
|
clearTimeout(this.gainedTimer);
|
|
339
588
|
},
|
|
@@ -347,6 +596,49 @@
|
|
|
347
596
|
}
|
|
348
597
|
}
|
|
349
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
|
+
|
|
350
642
|
function HandleInterestHoverOrFocus(el, source) {
|
|
351
643
|
if (touchInProgress) {
|
|
352
644
|
return;
|
|
@@ -355,52 +647,235 @@
|
|
|
355
647
|
return;
|
|
356
648
|
}
|
|
357
649
|
const target = GetInterestForTarget(el);
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
if (containingTarget) {
|
|
361
|
-
const upstreamInvoker = GetInterestInvoker(containingTarget);
|
|
362
|
-
if (upstreamInvoker) {
|
|
363
|
-
if (source === Source.Hover || source === Source.Focus) {
|
|
364
|
-
upstreamInvoker[dataField].clearLostTask();
|
|
365
|
-
} else {
|
|
366
|
-
if (source === Source.Blur || !el.matches(":hover")) {
|
|
367
|
-
ScheduleInterestLostTask(upstreamInvoker);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
return;
|
|
650
|
+
if (target) {
|
|
651
|
+
initializeDataField(el);
|
|
373
652
|
}
|
|
374
|
-
initializeDataField(el);
|
|
375
653
|
const data = el[dataField];
|
|
376
|
-
const upstreamInvoker = GetInterestInvoker(el);
|
|
377
654
|
|
|
378
655
|
// Hover or focus
|
|
379
656
|
if (source === Source.Hover || source === Source.Focus) {
|
|
380
|
-
|
|
381
|
-
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)) {
|
|
382
665
|
upstreamInvoker[dataField].clearLostTask();
|
|
383
666
|
}
|
|
384
|
-
|
|
667
|
+
if (
|
|
668
|
+
target &&
|
|
669
|
+
!suppressFocusInterest &&
|
|
670
|
+
data.state === InterestState.NoInterest
|
|
671
|
+
) {
|
|
672
|
+
ScheduleInterestGainedTask(el, InterestState.FullInterest);
|
|
673
|
+
}
|
|
385
674
|
} else {
|
|
386
675
|
// Dehover or blur
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
+
}
|
|
390
683
|
}
|
|
391
|
-
if (
|
|
392
|
-
upstreamInvoker
|
|
393
|
-
if (source === Source.Blur || !el.matches(":hover")) {
|
|
684
|
+
if (source === Source.Blur || !el.matches(":hover")) {
|
|
685
|
+
for (const upstreamInvoker of allSourceInterestInvokers(el)) {
|
|
394
686
|
ScheduleInterestLostTask(upstreamInvoker);
|
|
395
687
|
}
|
|
396
688
|
}
|
|
397
689
|
}
|
|
398
690
|
}
|
|
399
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
|
+
|
|
400
869
|
// Attach listeners
|
|
401
870
|
function addEventHandlers() {
|
|
402
871
|
const handler = (e, source) => {
|
|
403
|
-
|
|
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)) {
|
|
404
879
|
HandleInterestHoverOrFocus(el, source);
|
|
405
880
|
}
|
|
406
881
|
};
|
|
@@ -409,9 +884,16 @@
|
|
|
409
884
|
document.body.addEventListener("focusin", (e) => handler(e, Source.Focus));
|
|
410
885
|
document.body.addEventListener("focusout", (e) => handler(e, Source.Blur));
|
|
411
886
|
document.body.addEventListener("keydown", (e) => {
|
|
412
|
-
if (e.key === "Escape") {
|
|
413
|
-
|
|
414
|
-
|
|
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
|
+
);
|
|
415
897
|
});
|
|
416
898
|
}
|
|
417
899
|
});
|
|
@@ -423,7 +905,15 @@
|
|
|
423
905
|
if (invoker) {
|
|
424
906
|
initializeDataField(invoker);
|
|
425
907
|
invoker[dataField].longPressTimer = setTimeout(() => {
|
|
426
|
-
|
|
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
|
+
}
|
|
427
917
|
invoker[dataField].longPressTimer = null;
|
|
428
918
|
}, longPressTime);
|
|
429
919
|
}
|
|
@@ -447,15 +937,90 @@
|
|
|
447
937
|
const style = document.createElement("style");
|
|
448
938
|
style.textContent = `@property ${interestDelayStartProp} {syntax: "normal | <time>"; inherits: false; initial-value: normal;}
|
|
449
939
|
@property ${interestDelayEndProp} {syntax: "normal | <time>"; inherits: false; initial-value: normal;}
|
|
450
|
-
@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;}`;
|
|
451
942
|
document.head.appendChild(style);
|
|
452
|
-
|
|
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
|
+
});
|
|
453
1015
|
}
|
|
1016
|
+
installInterestForElement();
|
|
454
1017
|
|
|
455
1018
|
// Initialize
|
|
456
1019
|
function init() {
|
|
457
1020
|
registerCustomProperties();
|
|
458
1021
|
addEventHandlers();
|
|
1022
|
+
observeMutations();
|
|
1023
|
+
updateInterestButtons(document.querySelectorAll(`[${attributeName}]`));
|
|
459
1024
|
console.log(`interestfor polyfill installed (native: ${nativeSupported}).`);
|
|
460
1025
|
}
|
|
461
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",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)}();
|
|
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>
|