@c9up/nebula 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/tailwind.d.ts +7 -0
- package/dist/adapters/tailwind.js +32 -1
- package/dist/adapters/unocss.js +32 -3
- package/dist/atoms/NativeSelect.js +27 -2
- package/dist/lib/motion.d.ts +25 -16
- package/dist/lib/motion.js +30 -24
- package/dist/organisms/Dialog.d.ts +11 -4
- package/dist/organisms/Dialog.js +2 -2
- package/dist/organisms/Sidebar.d.ts +9 -0
- package/dist/organisms/Sidebar.js +5 -2
- package/dist/primitives/floatingSurface.js +17 -8
- package/dist/primitives/presence.js +401 -5
- package/nebula.css +1 -1
- package/package.json +8 -6
- package/src/adapters/tailwind.ts +32 -1
- package/src/adapters/unocss.ts +32 -3
- package/src/atoms/NativeSelect.ts +27 -2
- package/src/lib/motion.ts +30 -26
- package/src/organisms/Dialog.ts +13 -6
- package/src/organisms/Sidebar.ts +15 -2
- package/src/primitives/floatingSurface.ts +18 -9
- package/src/primitives/presence.ts +428 -5
- package/theme.css +0 -35
|
@@ -19,7 +19,12 @@ export function presence(initiallyOpen = false) {
|
|
|
19
19
|
const state = signal(initiallyOpen ? "open" : "closed");
|
|
20
20
|
let element = null;
|
|
21
21
|
let pendingUnmount = false;
|
|
22
|
+
let deadline;
|
|
22
23
|
function finishClose() {
|
|
24
|
+
if (deadline !== undefined) {
|
|
25
|
+
clearTimeout(deadline);
|
|
26
|
+
deadline = undefined;
|
|
27
|
+
}
|
|
23
28
|
if (!pendingUnmount)
|
|
24
29
|
return;
|
|
25
30
|
pendingUnmount = false;
|
|
@@ -34,9 +39,38 @@ export function presence(initiallyOpen = false) {
|
|
|
34
39
|
function onAnimationEnd(event) {
|
|
35
40
|
if (event.target !== element)
|
|
36
41
|
return;
|
|
42
|
+
// The FIRST event used to end the close, so a surface running a fade and
|
|
43
|
+
// a slide together was unmounted when the shorter one finished — the
|
|
44
|
+
// other visibly cut off. Each declared name reports for itself, and the
|
|
45
|
+
// close waits until none is outstanding.
|
|
46
|
+
const name = reportedName(event);
|
|
47
|
+
if (typeof name === "string" && outstanding.size > 0) {
|
|
48
|
+
reportName(outstanding, name);
|
|
49
|
+
if (outstanding.size > 0)
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
37
52
|
finishClose();
|
|
38
53
|
}
|
|
54
|
+
/** Declared animations and transitions still waiting to report. */
|
|
55
|
+
let outstanding = new Map();
|
|
56
|
+
/**
|
|
57
|
+
* Arm the wait for `el`'s exit, replacing whatever the last one armed.
|
|
58
|
+
*
|
|
59
|
+
* Overwriting `deadline` without clearing it left the older timer running:
|
|
60
|
+
* it fired mid-way through a LATER close and unmounted a surface that was
|
|
61
|
+
* still animating, cutting the exit off at the previous close's schedule.
|
|
62
|
+
*/
|
|
63
|
+
function armDeadline(el) {
|
|
64
|
+
if (deadline !== undefined)
|
|
65
|
+
clearTimeout(deadline);
|
|
66
|
+
outstanding = declaredNames(el);
|
|
67
|
+
deadline = setTimeout(finishClose, declaredDuration(el) + SAFETY_MARGIN_MS);
|
|
68
|
+
}
|
|
39
69
|
function detach() {
|
|
70
|
+
if (deadline !== undefined) {
|
|
71
|
+
clearTimeout(deadline);
|
|
72
|
+
deadline = undefined;
|
|
73
|
+
}
|
|
40
74
|
if (element === null)
|
|
41
75
|
return;
|
|
42
76
|
element.removeEventListener("animationend", onAnimationEnd);
|
|
@@ -49,7 +83,16 @@ export function presence(initiallyOpen = false) {
|
|
|
49
83
|
mounted,
|
|
50
84
|
state,
|
|
51
85
|
open() {
|
|
86
|
+
// The close in progress is CANCELLED, not just un-pended. Clearing
|
|
87
|
+
// `pendingUnmount` alone left its deadline armed and its names
|
|
88
|
+
// outstanding, so the timer from a close the user had already undone
|
|
89
|
+
// went on to unmount the NEXT one part-way through.
|
|
52
90
|
pendingUnmount = false;
|
|
91
|
+
if (deadline !== undefined) {
|
|
92
|
+
clearTimeout(deadline);
|
|
93
|
+
deadline = undefined;
|
|
94
|
+
}
|
|
95
|
+
outstanding.clear();
|
|
53
96
|
mounted(true);
|
|
54
97
|
state("open");
|
|
55
98
|
},
|
|
@@ -64,14 +107,35 @@ export function presence(initiallyOpen = false) {
|
|
|
64
107
|
return;
|
|
65
108
|
}
|
|
66
109
|
pendingUnmount = true;
|
|
67
|
-
if (!isAnimating(element))
|
|
110
|
+
if (!isAnimating(element)) {
|
|
68
111
|
finishClose();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// The same deadline `onExitFinished` has, and for the same reason:
|
|
115
|
+
// `animationend` is not promised by anything. Without it a declared
|
|
116
|
+
// animation the browser never runs left this mounted for good —
|
|
117
|
+
// `onExitFinished` was bounded and the public presence API was not.
|
|
118
|
+
armDeadline(element);
|
|
69
119
|
},
|
|
70
120
|
attach(next) {
|
|
121
|
+
const wasClosing = pendingUnmount;
|
|
71
122
|
detach();
|
|
72
123
|
element = next;
|
|
73
|
-
if (element === null)
|
|
124
|
+
if (element === null) {
|
|
125
|
+
// Nothing left to wait for, and nothing to wait WITH: a close
|
|
126
|
+
// still pending would otherwise never complete.
|
|
127
|
+
if (wasClosing)
|
|
128
|
+
finishClose();
|
|
74
129
|
return;
|
|
130
|
+
}
|
|
131
|
+
if (wasClosing) {
|
|
132
|
+
// `detach()` cleared the deadline. Handing over a new element
|
|
133
|
+
// mid-close without arming another left the surface mounted for
|
|
134
|
+
// good — the exact failure the deadline exists to prevent,
|
|
135
|
+
// reintroduced by the handover.
|
|
136
|
+
pendingUnmount = true;
|
|
137
|
+
armDeadline(element);
|
|
138
|
+
}
|
|
75
139
|
element.addEventListener("animationend", onAnimationEnd);
|
|
76
140
|
element.addEventListener("animationcancel", onAnimationEnd);
|
|
77
141
|
element.addEventListener("transitionend", onAnimationEnd);
|
|
@@ -98,19 +162,49 @@ export function onExitFinished(element, done) {
|
|
|
98
162
|
done();
|
|
99
163
|
return () => { };
|
|
100
164
|
}
|
|
165
|
+
// Which declared animations and transitions have yet to report. The FIRST
|
|
166
|
+
// event used to end the wait, so a surface running a fade and a slide
|
|
167
|
+
// together had its node removed when the shorter one finished and the other
|
|
168
|
+
// was visibly cut off. `presence()` already waited for all of them; the
|
|
169
|
+
// portalled surfaces — Dialog, Popover, Select, Tooltip — go through here
|
|
170
|
+
// instead, and did not.
|
|
171
|
+
const outstanding = declaredNames(element);
|
|
101
172
|
function finish(event) {
|
|
102
173
|
// Bubbled events from children would cut the parent's exit short.
|
|
103
174
|
if (event.target !== element)
|
|
104
175
|
return;
|
|
176
|
+
const name = reportedName(event);
|
|
177
|
+
if (typeof name === "string" && outstanding.size > 0) {
|
|
178
|
+
reportName(outstanding, name);
|
|
179
|
+
if (outstanding.size > 0)
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
105
182
|
cancel();
|
|
106
183
|
done();
|
|
107
184
|
}
|
|
108
185
|
function cancel() {
|
|
186
|
+
if (safety !== undefined)
|
|
187
|
+
clearTimeout(safety);
|
|
109
188
|
element.removeEventListener("animationend", finish);
|
|
110
189
|
element.removeEventListener("animationcancel", finish);
|
|
111
190
|
element.removeEventListener("transitionend", finish);
|
|
112
191
|
element.removeEventListener("transitioncancel", finish);
|
|
113
192
|
}
|
|
193
|
+
// A deadline, because `animationName` is a DECLARATION, not a promise.
|
|
194
|
+
//
|
|
195
|
+
// The computed style reads back the declared name whether or not those
|
|
196
|
+
// keyframes exist anywhere — an application that has not imported the
|
|
197
|
+
// stylesheet, or that scopes it away, declares an animation the browser
|
|
198
|
+
// will never run. `animationend` then never fires, `done()` never runs, and
|
|
199
|
+
// the node stays in the document: every closed Dialog, Select, Popover and
|
|
200
|
+
// Tooltip piles up as an invisible layer swallowing the clicks underneath.
|
|
201
|
+
//
|
|
202
|
+
// That turns a missing stylesheet — cosmetic — into a page that stops
|
|
203
|
+
// responding, which reads as "the floating layer does not work".
|
|
204
|
+
const safety = setTimeout(() => {
|
|
205
|
+
cancel();
|
|
206
|
+
done();
|
|
207
|
+
}, declaredDuration(element) + SAFETY_MARGIN_MS);
|
|
114
208
|
element.addEventListener("animationend", finish);
|
|
115
209
|
element.addEventListener("animationcancel", finish);
|
|
116
210
|
element.addEventListener("transitionend", finish);
|
|
@@ -125,13 +219,315 @@ export function onExitFinished(element, done) {
|
|
|
125
219
|
* mean there is nothing to wait for, and waiting anyway would strand the node
|
|
126
220
|
* in the DOM forever — the failure mode this check exists to prevent.
|
|
127
221
|
*/
|
|
222
|
+
/**
|
|
223
|
+
* Slack added to the declared duration before the deadline fires.
|
|
224
|
+
*
|
|
225
|
+
* Long enough that a real animation always wins the race — the listener is what
|
|
226
|
+
* should end the wait — and short enough that a stuck overlay clears within a
|
|
227
|
+
* frame or two of when it should have.
|
|
228
|
+
*/
|
|
229
|
+
const SAFETY_MARGIN_MS = 100;
|
|
230
|
+
/**
|
|
231
|
+
* How long the element SAYS its exit lasts: the longest declared animation or
|
|
232
|
+
* transition, plus its delay. Capped, because a stylesheet is free to declare
|
|
233
|
+
* minutes and the deadline exists to bound the wait, not to honour it.
|
|
234
|
+
*/
|
|
235
|
+
function declaredDuration(element) {
|
|
236
|
+
if (typeof getComputedStyle !== "function")
|
|
237
|
+
return 0;
|
|
238
|
+
const style = getComputedStyle(element);
|
|
239
|
+
let longest = 0;
|
|
240
|
+
const consider = (total) => {
|
|
241
|
+
if (total > longest)
|
|
242
|
+
longest = total;
|
|
243
|
+
};
|
|
244
|
+
// PER ENTRY. The longest duration and the longest delay used to be taken
|
|
245
|
+
// independently, so a short-but-late animation beside a long-but-immediate
|
|
246
|
+
// one produced a deadline neither of them needed — and the iteration count
|
|
247
|
+
// was not read at all, so 120ms played twice was cut off at 220.
|
|
248
|
+
const names = splitList(style.animationName);
|
|
249
|
+
const durations = splitList(style.animationDuration);
|
|
250
|
+
const delays = splitList(style.animationDelay);
|
|
251
|
+
const counts = splitList(style.animationIterationCount);
|
|
252
|
+
for (const [index, name] of names.entries()) {
|
|
253
|
+
if (name === "" || name === "none")
|
|
254
|
+
continue;
|
|
255
|
+
consider(parseTime(atIndex(durations, index)) *
|
|
256
|
+
parseIterations(atIndex(counts, index)) +
|
|
257
|
+
parseTime(atIndex(delays, index)));
|
|
258
|
+
}
|
|
259
|
+
// A property named twice in `transition-property` is ONE transition — the
|
|
260
|
+
// last entry is the one that runs (CSS Transitions Level 1) — so its
|
|
261
|
+
// duration and delay are read from that index.
|
|
262
|
+
const properties = splitList(style.transitionProperty);
|
|
263
|
+
const transitionDurations = splitList(style.transitionDuration);
|
|
264
|
+
const transitionDelays = splitList(style.transitionDelay);
|
|
265
|
+
const lastEntry = new Map();
|
|
266
|
+
for (const [index, property] of properties.entries()) {
|
|
267
|
+
if (property === "" || property === "none")
|
|
268
|
+
continue;
|
|
269
|
+
lastEntry.set(property, index);
|
|
270
|
+
}
|
|
271
|
+
for (const index of lastEntry.values()) {
|
|
272
|
+
consider(parseTime(atIndex(transitionDurations, index)) +
|
|
273
|
+
parseTime(atIndex(transitionDelays, index)));
|
|
274
|
+
}
|
|
275
|
+
return Math.min(longest, 5000);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* A comma-separated CSS list, entry by entry.
|
|
279
|
+
*
|
|
280
|
+
* Tolerates an absent longhand: a computed style is not always the complete
|
|
281
|
+
* one — a test double carries what its test cares about, and not every engine
|
|
282
|
+
* exposes every longhand.
|
|
283
|
+
*/
|
|
284
|
+
function splitList(value) {
|
|
285
|
+
if (typeof value !== "string")
|
|
286
|
+
return [];
|
|
287
|
+
return value.split(",").map((part) => part.trim());
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* The entry at `index`, the way CSS reads one: a list shorter than the
|
|
291
|
+
* animation list repeats to cover it.
|
|
292
|
+
*/
|
|
293
|
+
function atIndex(list, index) {
|
|
294
|
+
if (list.length === 0)
|
|
295
|
+
return "";
|
|
296
|
+
return list[index % list.length] ?? "";
|
|
297
|
+
}
|
|
298
|
+
/** One CSS time, in milliseconds. */
|
|
299
|
+
function parseTime(value) {
|
|
300
|
+
const numeric = Number.parseFloat(value);
|
|
301
|
+
if (Number.isNaN(numeric))
|
|
302
|
+
return 0;
|
|
303
|
+
return value.endsWith("ms") ? numeric : numeric * 1000;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* How many times one animation plays.
|
|
307
|
+
*
|
|
308
|
+
* `infinite` counts as one: it never fires `animationend`, so the deadline is
|
|
309
|
+
* the only way out and there is nothing to be gained by waiting longer.
|
|
310
|
+
*/
|
|
311
|
+
function parseIterations(value) {
|
|
312
|
+
const numeric = Number.parseFloat(value);
|
|
313
|
+
if (!Number.isFinite(numeric) || numeric <= 0)
|
|
314
|
+
return 1;
|
|
315
|
+
return numeric;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Is a `@keyframes` of this name defined anywhere in the document?
|
|
319
|
+
*
|
|
320
|
+
* `animationName` is a DECLARATION. It reads back whatever the stylesheet said,
|
|
321
|
+
* whether or not the keyframes behind it exist — so an application that has not
|
|
322
|
+
* imported nebula's stylesheet declares animations the browser will never run,
|
|
323
|
+
* and the only honest answer comes from looking for the rule itself.
|
|
324
|
+
*
|
|
325
|
+
* Answers `true` when it cannot tell. A cross-origin stylesheet throws on
|
|
326
|
+
* `cssRules`, and refusing to animate because a font sheet was unreadable would
|
|
327
|
+
* be a worse trade than waiting: the deadline in `onExitFinished` already bounds
|
|
328
|
+
* the cost of being wrong here.
|
|
329
|
+
*/
|
|
330
|
+
function keyframesExist(doc, name) {
|
|
331
|
+
const cache = cacheFor(doc);
|
|
332
|
+
const cached = cache.get(name);
|
|
333
|
+
if (cached !== undefined)
|
|
334
|
+
return cached;
|
|
335
|
+
let found = false;
|
|
336
|
+
let readable = false;
|
|
337
|
+
for (const sheet of Array.from(doc.styleSheets)) {
|
|
338
|
+
let rules;
|
|
339
|
+
try {
|
|
340
|
+
const own = sheet.cssRules;
|
|
341
|
+
if (own === null)
|
|
342
|
+
continue;
|
|
343
|
+
rules = own;
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// Cross-origin: not ours to read, and not evidence of anything.
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
readable = true;
|
|
350
|
+
if (containsKeyframes(rules, name)) {
|
|
351
|
+
found = true;
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const answer = found || !readable;
|
|
356
|
+
// Cached only when it was FOUND. A negative is re-checked, because a
|
|
357
|
+
// stylesheet arriving later is exactly what turns it positive.
|
|
358
|
+
if (answer)
|
|
359
|
+
cache.set(name, true);
|
|
360
|
+
return answer;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Walk a rule list, descending into group rules.
|
|
364
|
+
*
|
|
365
|
+
* `@keyframes` inside `@media`, `@supports` or `@layer` is a nested rule, not a
|
|
366
|
+
* top-level one — a flat scan of the sheet reported it missing and the caller
|
|
367
|
+
* concluded the stylesheet was absent.
|
|
368
|
+
*/
|
|
369
|
+
function containsKeyframes(rules, name) {
|
|
370
|
+
for (const rule of Array.from(rules)) {
|
|
371
|
+
if (isKeyframesNamed(rule, name))
|
|
372
|
+
return true;
|
|
373
|
+
// `cssRules` on a group rule (media, supports, layer); absent on others.
|
|
374
|
+
const nested = Reflect.get(rule, "cssRules");
|
|
375
|
+
if (nested !== null &&
|
|
376
|
+
typeof nested === "object" &&
|
|
377
|
+
typeof Reflect.get(nested, "length") === "number" &&
|
|
378
|
+
containsKeyframes(nested, name)) {
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Are all the animations this element declares actually defined?
|
|
386
|
+
*
|
|
387
|
+
* `animation-name` is a LIST: `fade-out, slide-out` is two names, and looking
|
|
388
|
+
* the whole string up as one found nothing and reported both missing. Every
|
|
389
|
+
* name has to resolve, because waiting on any undefined one is what strands the
|
|
390
|
+
* node.
|
|
391
|
+
*/
|
|
392
|
+
function splitNames(animationName) {
|
|
393
|
+
return animationName
|
|
394
|
+
.split(",")
|
|
395
|
+
.map((part) => part.trim())
|
|
396
|
+
.filter((part) => part !== "" && part !== "none");
|
|
397
|
+
}
|
|
398
|
+
/** Which of the declared animations have no `@keyframes` anywhere. */
|
|
399
|
+
function undefinedKeyframes(doc, animationName) {
|
|
400
|
+
return splitNames(animationName).filter((name) => !keyframesExist(doc, name));
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Per-DOCUMENT memo, invalidated when the stylesheets change.
|
|
404
|
+
*
|
|
405
|
+
* A single global map was wrong three ways. An answer cached before the
|
|
406
|
+
* stylesheet finished loading stayed wrong for the life of the page; an iframe
|
|
407
|
+
* and its parent share neither styles nor documents but shared the cache; and
|
|
408
|
+
* nothing ever expired, so a sheet added later never took effect.
|
|
409
|
+
*
|
|
410
|
+
* Keyed on the document and on how many sheets it had when the answer was
|
|
411
|
+
* computed: a new stylesheet changes the count and the answers are recomputed.
|
|
412
|
+
* A `WeakMap` so a detached document does not keep its cache alive.
|
|
413
|
+
*/
|
|
414
|
+
const KEYFRAME_CACHE = new WeakMap();
|
|
415
|
+
/**
|
|
416
|
+
* The memo for this document.
|
|
417
|
+
*
|
|
418
|
+
* Only POSITIVE answers are kept. A "found" is durable — keyframes do not
|
|
419
|
+
* usually disappear — while a "missing" is exactly the answer a later
|
|
420
|
+
* stylesheet can change, and keying on the sheet COUNT missed every way that
|
|
421
|
+
* happens without one being added: `insertRule`, `replaceSync`, HMR, or editing
|
|
422
|
+
* an existing `<style>`. Re-walking on a negative costs a scan only when
|
|
423
|
+
* something is already wrong.
|
|
424
|
+
*/
|
|
425
|
+
function cacheFor(doc) {
|
|
426
|
+
const existing = KEYFRAME_CACHE.get(doc);
|
|
427
|
+
if (existing !== undefined)
|
|
428
|
+
return existing;
|
|
429
|
+
const answers = new Map();
|
|
430
|
+
KEYFRAME_CACHE.set(doc, answers);
|
|
431
|
+
return answers;
|
|
432
|
+
}
|
|
433
|
+
function isKeyframesNamed(rule, name) {
|
|
434
|
+
// `instanceof CSSKeyframesRule` is unreliable across documents (an iframe
|
|
435
|
+
// has its own constructors), so the shape is checked instead.
|
|
436
|
+
const named = Reflect.get(rule, "name");
|
|
437
|
+
return typeof named === "string" && named === name;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Say once that the stylesheet is missing, and what to do about it.
|
|
441
|
+
*
|
|
442
|
+
* The symptom without this is not "my overlays do not animate" — which would
|
|
443
|
+
* point straight at a missing sheet — but "the floating layer behaves oddly",
|
|
444
|
+
* which points everywhere else. Warned rather than thrown: a missing stylesheet
|
|
445
|
+
* is a cosmetic dependency, and taking an application down over one is a worse
|
|
446
|
+
* trade than a line in the console.
|
|
447
|
+
*/
|
|
448
|
+
function warnMissingKeyframes(name) {
|
|
449
|
+
if (WARNED.has(name))
|
|
450
|
+
return;
|
|
451
|
+
WARNED.add(name);
|
|
452
|
+
console.warn(`[nebula] the animation '${name}' is declared but its @keyframes are defined nowhere, so this element closes without animating. Overlay animations come from tw-animate-css (UnoCSS: unocss-preset-animations) — check your stylesheet imports it, or generate one with \`nebula init\`.`);
|
|
453
|
+
}
|
|
454
|
+
const WARNED = new Set();
|
|
455
|
+
/** Mark one report against `name`, if it is one we are waiting for. */
|
|
456
|
+
function reportName(outstanding, name) {
|
|
457
|
+
const left = outstanding.get(name);
|
|
458
|
+
if (left === undefined)
|
|
459
|
+
return;
|
|
460
|
+
if (left <= 1)
|
|
461
|
+
outstanding.delete(name);
|
|
462
|
+
else
|
|
463
|
+
outstanding.set(name, left - 1);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Which animation or transition an end event is reporting for.
|
|
467
|
+
*
|
|
468
|
+
* `undefined` when the event carries neither — jsdom's plain `Event`, and any
|
|
469
|
+
* synthetic one — in which case the caller falls back to treating it as the end
|
|
470
|
+
* of the whole exit.
|
|
471
|
+
*/
|
|
472
|
+
function reportedName(event) {
|
|
473
|
+
if ("animationName" in event)
|
|
474
|
+
return event.animationName;
|
|
475
|
+
if ("propertyName" in event)
|
|
476
|
+
return event.propertyName;
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
function declaredNames(element) {
|
|
480
|
+
const names = new Map();
|
|
481
|
+
if (typeof getComputedStyle !== "function")
|
|
482
|
+
return names;
|
|
483
|
+
const style = getComputedStyle(element);
|
|
484
|
+
// An animation named twice IS two animations, and reports twice.
|
|
485
|
+
for (const name of splitList(style.animationName)) {
|
|
486
|
+
if (name === "" || name === "none" || name === "all")
|
|
487
|
+
continue;
|
|
488
|
+
names.set(name, (names.get(name) ?? 0) + 1);
|
|
489
|
+
}
|
|
490
|
+
// A transition property named twice is ONE transition — the last entry
|
|
491
|
+
// wins (CSS Transitions Level 1) — so it reports once. Counting the
|
|
492
|
+
// duplicate made every such exit run to the deadline instead of ending
|
|
493
|
+
// when the browser said it had.
|
|
494
|
+
if (parseDuration(style.transitionDuration) > 0) {
|
|
495
|
+
const seen = new Set();
|
|
496
|
+
for (const property of splitList(style.transitionProperty)) {
|
|
497
|
+
if (property === "" || property === "none" || property === "all") {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
if (seen.has(property))
|
|
501
|
+
continue;
|
|
502
|
+
seen.add(property);
|
|
503
|
+
names.set(property, (names.get(property) ?? 0) + 1);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return names;
|
|
507
|
+
}
|
|
128
508
|
function isAnimating(element) {
|
|
129
509
|
if (typeof getComputedStyle !== "function")
|
|
130
510
|
return false;
|
|
131
511
|
const style = getComputedStyle(element);
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
512
|
+
const declared = style.animationName;
|
|
513
|
+
const hasAnimation = declared !== "" && declared !== "none";
|
|
514
|
+
if (hasAnimation) {
|
|
515
|
+
// Declared is not the same as defined. Waiting on an animation whose
|
|
516
|
+
// keyframes exist nowhere is what left every closed overlay in the
|
|
517
|
+
// document; the deadline now bounds that, but there is no reason to
|
|
518
|
+
// wait at all when the answer is knowable — and every reason to say so.
|
|
519
|
+
// ANY defined animation is a reason to wait: refusing because a second
|
|
520
|
+
// one is missing truncated the first, which was running perfectly well.
|
|
521
|
+
// The missing ones are still named, because they are still a mistake.
|
|
522
|
+
const missing = undefinedKeyframes(element.ownerDocument, declared);
|
|
523
|
+
if (missing.length < splitNames(declared).length) {
|
|
524
|
+
for (const name of missing)
|
|
525
|
+
warnMissingKeyframes(name);
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
warnMissingKeyframes(declared);
|
|
529
|
+
return parseDuration(style.transitionDuration) > 0;
|
|
530
|
+
}
|
|
135
531
|
return parseDuration(style.transitionDuration) > 0;
|
|
136
532
|
}
|
|
137
533
|
/** Longest duration in a comma-separated CSS time list, in milliseconds. */
|