@adia-ai/web-components 0.2.2 → 0.2.4
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/components/agent-trace/agent-trace.css +24 -3
- package/components/button/button.js +3 -0
- package/components/demo-toggle/demo-toggle.a2ui.json +144 -0
- package/components/demo-toggle/demo-toggle.css +120 -0
- package/components/demo-toggle/demo-toggle.js +144 -0
- package/components/demo-toggle/demo-toggle.test.js +102 -0
- package/components/demo-toggle/demo-toggle.yaml +144 -0
- package/components/index.js +1 -0
- package/components/input/input.js +11 -0
- package/components/list/list.css +66 -3
- package/components/nav-group/nav-group.a2ui.json +1 -1
- package/components/nav-group/nav-group.css +5 -5
- package/components/nav-group/nav-group.yaml +1 -1
- package/components/nav-item/nav-item.a2ui.json +1 -1
- package/components/nav-item/nav-item.css +3 -4
- package/components/nav-item/nav-item.yaml +1 -1
- package/components/textarea/textarea.js +10 -0
- package/core/icons.js +13 -1
- package/package.json +1 -1
- package/styles/components.css +1 -0
- package/styles/typography.css +1 -1
- package/traits/_catalog.json +258 -5
- package/traits/active-state.test.js +1 -1
- package/traits/anchor-positioning.js +205 -52
- package/traits/anchor-positioning.test.js +77 -4
- package/traits/announcer-stage.js +157 -0
- package/traits/announcer.js +145 -0
- package/traits/announcer.test.js +268 -0
- package/traits/arrow-grid-nav.js +234 -0
- package/traits/arrow-grid-nav.test.js +375 -0
- package/traits/attention-pulse.js +1 -1
- package/traits/attention-pulse.test.js +1 -1
- package/traits/confetti-burst.js +90 -60
- package/traits/confetti-burst.test.js +16 -8
- package/traits/confetti-stage.js +143 -0
- package/traits/confetti.js +44 -47
- package/traits/confetti.test.js +24 -5
- package/traits/count-up.js +31 -6
- package/traits/count-up.test.js +1 -1
- package/traits/declarative.test.js +1 -1
- package/traits/dirty-state.test.js +1 -1
- package/traits/drag-ghost.js +55 -3
- package/traits/drag-ghost.test.js +1 -1
- package/traits/draggable-list-item.js +279 -0
- package/traits/draggable-list-item.test.js +51 -0
- package/traits/draggable.js +14 -4
- package/traits/draggable.test.js +1 -1
- package/traits/drop-target.js +223 -0
- package/traits/drop-target.test.js +241 -0
- package/traits/droppable-collection.js +89 -0
- package/traits/droppable-collection.test.js +99 -0
- package/traits/droppable.js +125 -0
- package/traits/droppable.test.js +54 -0
- package/traits/error-shake.js +157 -0
- package/traits/error-shake.test.js +114 -0
- package/traits/fade-presence.test.js +1 -1
- package/traits/focus-restore.js +135 -0
- package/traits/focus-restore.test.js +202 -0
- package/traits/focus-trap.test.js +1 -1
- package/traits/focusable.test.js +1 -1
- package/traits/glow-focus.js +1 -1
- package/traits/glow-focus.test.js +1 -1
- package/traits/gradient-shift.js +1 -1
- package/traits/gradient-shift.test.js +1 -1
- package/traits/haptic-feedback.test.js +1 -1
- package/traits/hotkey.test.js +1 -1
- package/traits/hoverable.test.js +1 -1
- package/traits/index.js +15 -0
- package/traits/inertia-drag.js +9 -0
- package/traits/inertia-drag.test.js +1 -1
- package/traits/input-mask.js +328 -0
- package/traits/input-mask.test.js +151 -0
- package/traits/intersection-observer.test.js +1 -1
- package/traits/keyboard-nav.test.js +1 -1
- package/traits/keyboard-reorderable.js +254 -0
- package/traits/keyboard-reorderable.test.js +45 -0
- package/traits/layout-animation.js +229 -0
- package/traits/layout-animation.test.js +114 -0
- package/traits/long-press.js +212 -0
- package/traits/long-press.test.js +244 -0
- package/traits/magnetic-hover.js +1 -1
- package/traits/magnetic-hover.test.js +1 -1
- package/traits/noise-texture.js +7 -3
- package/traits/noise-texture.test.js +1 -1
- package/traits/parallax.js +1 -1
- package/traits/parallax.test.js +1 -1
- package/traits/portal.test.js +1 -1
- package/traits/pressable.test.js +1 -1
- package/traits/resettable.js +29 -3
- package/traits/resettable.test.js +34 -1
- package/traits/resizable.test.js +1 -1
- package/traits/resize-observer.test.js +1 -1
- package/traits/ripple.js +1 -1
- package/traits/ripple.test.js +1 -1
- package/traits/roving-tabindex.test.js +1 -1
- package/traits/scale-press.test.js +1 -1
- package/traits/scroll-lock.test.js +1 -1
- package/traits/scroll-progress.js +201 -0
- package/traits/scroll-progress.test.js +182 -0
- package/traits/shimmer-loading.js +1 -1
- package/traits/shimmer-loading.test.js +1 -1
- package/traits/{_smoke.test.js → smoke.test.js} +1 -1
- package/traits/snap-to-grid.test.js +1 -1
- package/traits/sound-feedback.test.js +1 -1
- package/traits/spring-animate.js +8 -3
- package/traits/spring-animate.test.js +1 -1
- package/traits/success-checkmark.js +222 -0
- package/traits/success-checkmark.test.js +120 -0
- package/traits/tilt-hover.js +1 -1
- package/traits/tilt-hover.test.js +1 -1
- package/traits/tossable.js +9 -0
- package/traits/tossable.test.js +1 -1
- package/traits/traits-host.test.js +1 -1
- package/traits/typeahead.test.js +1 -1
- package/traits/typewriter.js +1 -1
- package/traits/typewriter.test.js +1 -1
- package/traits/validation.test.js +1 -1
- package/traits/view-transition.js +140 -0
- package/traits/view-transition.test.js +268 -0
- /package/traits/{_motion.js → motion.js} +0 -0
- /package/traits/{_test-helpers.js → test-helpers.js} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { scalePress } from './scale-press.js';
|
|
3
|
-
import { mountHost, connectTrait, resetDOM } from './
|
|
3
|
+
import { mountHost, connectTrait, resetDOM } from './test-helpers.js';
|
|
4
4
|
|
|
5
5
|
describe('scale-press', () => {
|
|
6
6
|
beforeEach(resetDOM);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { scrollLock } from './scroll-lock.js';
|
|
3
|
-
import { mountHost, connectTrait, resetDOM } from './
|
|
3
|
+
import { mountHost, connectTrait, resetDOM } from './test-helpers.js';
|
|
4
4
|
|
|
5
5
|
describe('scroll-lock', () => {
|
|
6
6
|
beforeEach(() => {
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { defineTrait } from './define.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scroll-progress — surfaces page or element scroll progress as a 0..1
|
|
5
|
+
* attribute, CSS custom property, and event. Building block for sticky
|
|
6
|
+
* headers, ToC highlighting, scroll-linked animations, and reading-time
|
|
7
|
+
* indicators.
|
|
8
|
+
*
|
|
9
|
+
* Modes (via `data-scroll-progress-mode`):
|
|
10
|
+
* - "in-view" (default) — 0 = host top edge meets viewport bottom;
|
|
11
|
+
* 1 = host bottom edge crosses viewport top. Useful for fade-in /
|
|
12
|
+
* parallax-on-scroll patterns.
|
|
13
|
+
* - "page" — 0 = page at top, 1 = page fully scrolled. Independent of
|
|
14
|
+
* host position.
|
|
15
|
+
* - "scrolled" — 0 = host content scrollTop is 0, 1 = fully scrolled.
|
|
16
|
+
* For overflow:auto containers.
|
|
17
|
+
*
|
|
18
|
+
* Container override (`data-scroll-container`) accepts a CSS selector;
|
|
19
|
+
* resolves to the scrolling ancestor explicitly. Falls back to the
|
|
20
|
+
* nearest auto/scroll overflow ancestor or `window`.
|
|
21
|
+
*
|
|
22
|
+
* Throttling — scroll events fire frequently. The trait coalesces
|
|
23
|
+
* updates via rAF: at most one progress update per frame. Cleanup
|
|
24
|
+
* cancels any pending rAF + removes the listener.
|
|
25
|
+
*/
|
|
26
|
+
export const scrollProgress = defineTrait({
|
|
27
|
+
name: 'scroll-progress',
|
|
28
|
+
category: 'layout-measurement',
|
|
29
|
+
description: 'Page or element scroll progress as 0..1 attribute + CSS variable + event',
|
|
30
|
+
attributes: ['data-scroll-progress-active', 'data-scroll-progress'],
|
|
31
|
+
events: ['scroll-progress'],
|
|
32
|
+
config: ['data-scroll-progress-mode', 'data-scroll-container'],
|
|
33
|
+
setup({ host }) {
|
|
34
|
+
const mode = host.getAttribute('data-scroll-progress-mode') || 'in-view';
|
|
35
|
+
const containerSelector = host.getAttribute('data-scroll-container');
|
|
36
|
+
|
|
37
|
+
// Resolve scroll container — explicit selector wins; otherwise walk
|
|
38
|
+
// up looking for an overflow:auto/scroll ancestor; fall back to window.
|
|
39
|
+
function resolveContainer() {
|
|
40
|
+
if (containerSelector) {
|
|
41
|
+
try {
|
|
42
|
+
const root = host.getRootNode?.() || document;
|
|
43
|
+
const node = root.querySelector?.(containerSelector)
|
|
44
|
+
|| document.querySelector(containerSelector);
|
|
45
|
+
if (node) return node;
|
|
46
|
+
} catch {
|
|
47
|
+
// Invalid selector — fall through.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
let node = host.parentElement;
|
|
51
|
+
while (node) {
|
|
52
|
+
const style = (typeof getComputedStyle === 'function')
|
|
53
|
+
? getComputedStyle(node)
|
|
54
|
+
: null;
|
|
55
|
+
if (style) {
|
|
56
|
+
const flow = style.overflow + style.overflowY + style.overflowX;
|
|
57
|
+
if (/(auto|scroll)/.test(flow)) return node;
|
|
58
|
+
}
|
|
59
|
+
node = node.parentElement;
|
|
60
|
+
}
|
|
61
|
+
return (typeof window !== 'undefined') ? window : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const container = resolveContainer();
|
|
65
|
+
if (!container) {
|
|
66
|
+
// No place to listen — mark active and bail. Keeps cleanup
|
|
67
|
+
// contract intact (attribute is still removed on disconnect).
|
|
68
|
+
host.setAttribute('data-scroll-progress-active', '');
|
|
69
|
+
return () => {
|
|
70
|
+
host.removeAttribute('data-scroll-progress-active');
|
|
71
|
+
host.removeAttribute('data-scroll-progress');
|
|
72
|
+
host.style.removeProperty('--scroll-progress');
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const isWindow = container === (typeof window !== 'undefined' ? window : null);
|
|
77
|
+
|
|
78
|
+
function clamp01(v) {
|
|
79
|
+
if (!Number.isFinite(v)) return 0;
|
|
80
|
+
if (v < 0) return 0;
|
|
81
|
+
if (v > 1) return 1;
|
|
82
|
+
return v;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function compute() {
|
|
86
|
+
// "page" — scrollY / (scrollHeight - innerHeight)
|
|
87
|
+
if (mode === 'page') {
|
|
88
|
+
if (isWindow) {
|
|
89
|
+
const max = (document.documentElement.scrollHeight || 0) - (window.innerHeight || 0);
|
|
90
|
+
if (max <= 0) return 0;
|
|
91
|
+
return clamp01((window.scrollY || 0) / max);
|
|
92
|
+
}
|
|
93
|
+
const max = (container.scrollHeight || 0) - (container.clientHeight || 0);
|
|
94
|
+
if (max <= 0) return 0;
|
|
95
|
+
return clamp01((container.scrollTop || 0) / max);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// "scrolled" — host's own scrollTop / max-scroll
|
|
99
|
+
if (mode === 'scrolled') {
|
|
100
|
+
const max = (host.scrollHeight || 0) - (host.clientHeight || 0);
|
|
101
|
+
if (max <= 0) return 0;
|
|
102
|
+
return clamp01((host.scrollTop || 0) / max);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// "in-view" (default) — 0 when host top meets viewport bottom,
|
|
106
|
+
// 1 when host bottom crosses viewport top.
|
|
107
|
+
// We measure the host rect against the container's viewport.
|
|
108
|
+
if (typeof host.getBoundingClientRect !== 'function') return 0;
|
|
109
|
+
const rect = host.getBoundingClientRect();
|
|
110
|
+
let viewTop;
|
|
111
|
+
let viewBottom;
|
|
112
|
+
if (isWindow) {
|
|
113
|
+
viewTop = 0;
|
|
114
|
+
viewBottom = window.innerHeight || 0;
|
|
115
|
+
} else if (typeof container.getBoundingClientRect === 'function') {
|
|
116
|
+
const cRect = container.getBoundingClientRect();
|
|
117
|
+
viewTop = cRect.top;
|
|
118
|
+
viewBottom = cRect.bottom;
|
|
119
|
+
} else {
|
|
120
|
+
viewTop = 0;
|
|
121
|
+
viewBottom = window.innerHeight || 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const viewHeight = viewBottom - viewTop;
|
|
125
|
+
const total = viewHeight + (rect.height || 0);
|
|
126
|
+
if (total <= 0) return 0;
|
|
127
|
+
|
|
128
|
+
// Distance from "host fully below view" to current position.
|
|
129
|
+
// When rect.top === viewBottom → 0. When rect.bottom === viewTop → 1.
|
|
130
|
+
const distance = viewBottom - rect.top;
|
|
131
|
+
return clamp01(distance / total);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let rafId = null;
|
|
135
|
+
let lastWritten = -1;
|
|
136
|
+
|
|
137
|
+
function update() {
|
|
138
|
+
rafId = null;
|
|
139
|
+
const progress = compute();
|
|
140
|
+
// De-dup writes when nothing meaningful changed (3-decimal precision
|
|
141
|
+
// is enough for any visual or reading-progress consumer).
|
|
142
|
+
const rounded = Math.round(progress * 1000) / 1000;
|
|
143
|
+
if (rounded === lastWritten) return;
|
|
144
|
+
lastWritten = rounded;
|
|
145
|
+
|
|
146
|
+
const str = rounded.toFixed(3);
|
|
147
|
+
host.setAttribute('data-scroll-progress', str);
|
|
148
|
+
host.style.setProperty('--scroll-progress', str);
|
|
149
|
+
host.dispatchEvent(new CustomEvent('scroll-progress', {
|
|
150
|
+
bubbles: true,
|
|
151
|
+
detail: { progress: rounded },
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function schedule() {
|
|
156
|
+
if (rafId !== null) return;
|
|
157
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
158
|
+
rafId = requestAnimationFrame(update);
|
|
159
|
+
} else {
|
|
160
|
+
// Fallback for environments without rAF — write synchronously.
|
|
161
|
+
update();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const target = container;
|
|
166
|
+
const listenerOpts = { passive: true };
|
|
167
|
+
|
|
168
|
+
target.addEventListener?.('scroll', schedule, listenerOpts);
|
|
169
|
+
// For "scrolled" mode the host itself is the scroll surface — also
|
|
170
|
+
// listen on host so progress updates without requiring the resolved
|
|
171
|
+
// container to be the host.
|
|
172
|
+
if (mode === 'scrolled' && target !== host) {
|
|
173
|
+
host.addEventListener?.('scroll', schedule, listenerOpts);
|
|
174
|
+
}
|
|
175
|
+
// Window resize affects in-view + page math; update on resize too.
|
|
176
|
+
if (typeof window !== 'undefined') {
|
|
177
|
+
window.addEventListener?.('resize', schedule, listenerOpts);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
host.setAttribute('data-scroll-progress-active', '');
|
|
181
|
+
// Seed the initial value on next frame so first paint has the value.
|
|
182
|
+
schedule();
|
|
183
|
+
|
|
184
|
+
return () => {
|
|
185
|
+
if (rafId !== null && typeof cancelAnimationFrame === 'function') {
|
|
186
|
+
cancelAnimationFrame(rafId);
|
|
187
|
+
}
|
|
188
|
+
rafId = null;
|
|
189
|
+
target.removeEventListener?.('scroll', schedule, listenerOpts);
|
|
190
|
+
if (mode === 'scrolled' && target !== host) {
|
|
191
|
+
host.removeEventListener?.('scroll', schedule, listenerOpts);
|
|
192
|
+
}
|
|
193
|
+
if (typeof window !== 'undefined') {
|
|
194
|
+
window.removeEventListener?.('resize', schedule, listenerOpts);
|
|
195
|
+
}
|
|
196
|
+
host.removeAttribute('data-scroll-progress-active');
|
|
197
|
+
host.removeAttribute('data-scroll-progress');
|
|
198
|
+
host.style.removeProperty('--scroll-progress');
|
|
199
|
+
};
|
|
200
|
+
},
|
|
201
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
|
+
import { scrollProgress } from './scroll-progress.js';
|
|
3
|
+
import { mountHost, connectTrait, spyEvent, resetDOM } from './test-helpers.js';
|
|
4
|
+
|
|
5
|
+
describe('scroll-progress', () => {
|
|
6
|
+
let originalRAF;
|
|
7
|
+
let originalCAF;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
resetDOM();
|
|
11
|
+
// Synchronous rAF — the trait's scheduler calls back immediately so
|
|
12
|
+
// we can assert post-update side-effects without awaiting frames.
|
|
13
|
+
originalRAF = globalThis.requestAnimationFrame;
|
|
14
|
+
originalCAF = globalThis.cancelAnimationFrame;
|
|
15
|
+
globalThis.requestAnimationFrame = (fn) => { fn(performance.now?.() ?? 0); return 1; };
|
|
16
|
+
globalThis.cancelAnimationFrame = vi.fn();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
globalThis.requestAnimationFrame = originalRAF;
|
|
21
|
+
globalThis.cancelAnimationFrame = originalCAF;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('connect sets data-scroll-progress-active', () => {
|
|
25
|
+
const host = mountHost();
|
|
26
|
+
connectTrait(scrollProgress, host);
|
|
27
|
+
expect(host.hasAttribute('data-scroll-progress-active')).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('disconnect clears active + progress + CSS variable', () => {
|
|
31
|
+
const host = mountHost();
|
|
32
|
+
const inst = connectTrait(scrollProgress, host);
|
|
33
|
+
inst.disconnect(host);
|
|
34
|
+
expect(host.hasAttribute('data-scroll-progress-active')).toBe(false);
|
|
35
|
+
expect(host.hasAttribute('data-scroll-progress')).toBe(false);
|
|
36
|
+
expect(host.style.getPropertyValue('--scroll-progress')).toBe('');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('writes data-scroll-progress as a 0..1 string after connect', () => {
|
|
40
|
+
const host = mountHost();
|
|
41
|
+
connectTrait(scrollProgress, host);
|
|
42
|
+
// Synchronous rAF means the seed update has already run.
|
|
43
|
+
const v = host.getAttribute('data-scroll-progress');
|
|
44
|
+
expect(v).not.toBeNull();
|
|
45
|
+
const n = parseFloat(v);
|
|
46
|
+
expect(n).toBeGreaterThanOrEqual(0);
|
|
47
|
+
expect(n).toBeLessThanOrEqual(1);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('writes the --scroll-progress CSS custom property', () => {
|
|
51
|
+
const host = mountHost();
|
|
52
|
+
connectTrait(scrollProgress, host);
|
|
53
|
+
const css = host.style.getPropertyValue('--scroll-progress');
|
|
54
|
+
expect(css).not.toBe('');
|
|
55
|
+
const n = parseFloat(css);
|
|
56
|
+
expect(n).toBeGreaterThanOrEqual(0);
|
|
57
|
+
expect(n).toBeLessThanOrEqual(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('dispatches a scroll-progress event with { progress } detail', () => {
|
|
61
|
+
const host = mountHost();
|
|
62
|
+
const spy = spyEvent(host, 'scroll-progress');
|
|
63
|
+
connectTrait(scrollProgress, host);
|
|
64
|
+
// Initial seed fires once via synchronous rAF.
|
|
65
|
+
expect(spy.count).toBeGreaterThanOrEqual(1);
|
|
66
|
+
expect(typeof spy.last.progress).toBe('number');
|
|
67
|
+
expect(spy.last.progress).toBeGreaterThanOrEqual(0);
|
|
68
|
+
expect(spy.last.progress).toBeLessThanOrEqual(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('mode="page" falls back to 0 when scroll range is zero', () => {
|
|
72
|
+
const host = mountHost('div', { 'data-scroll-progress-mode': 'page' });
|
|
73
|
+
connectTrait(scrollProgress, host);
|
|
74
|
+
// happy-dom reports zero scroll geometry by default.
|
|
75
|
+
expect(host.getAttribute('data-scroll-progress')).toBe('0.000');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('mode="scrolled" reports 0..1 from host scrollTop / max', () => {
|
|
79
|
+
const host = mountHost('div', { 'data-scroll-progress-mode': 'scrolled' });
|
|
80
|
+
// Stub the geometry — happy-dom returns zeros for scroll metrics.
|
|
81
|
+
Object.defineProperty(host, 'scrollHeight', { configurable: true, value: 1000 });
|
|
82
|
+
Object.defineProperty(host, 'clientHeight', { configurable: true, value: 200 });
|
|
83
|
+
Object.defineProperty(host, 'scrollTop', { configurable: true, value: 200, writable: true });
|
|
84
|
+
connectTrait(scrollProgress, host);
|
|
85
|
+
const v = parseFloat(host.getAttribute('data-scroll-progress'));
|
|
86
|
+
// 200 / (1000 - 200) = 0.25
|
|
87
|
+
expect(v).toBeCloseTo(0.25, 2);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('mode="in-view" clamps to [0,1] for offscreen rects', () => {
|
|
91
|
+
const host = mountHost();
|
|
92
|
+
// Far below the viewport.
|
|
93
|
+
host.getBoundingClientRect = () => ({ top: 99999, bottom: 100099, left: 0, right: 0, width: 0, height: 100, x: 0, y: 99999, toJSON() { return {}; } });
|
|
94
|
+
connectTrait(scrollProgress, host);
|
|
95
|
+
expect(parseFloat(host.getAttribute('data-scroll-progress'))).toBe(0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('mode="in-view" reports 1 for rects fully scrolled past', () => {
|
|
99
|
+
const host = mountHost();
|
|
100
|
+
// Far above the viewport.
|
|
101
|
+
host.getBoundingClientRect = () => ({ top: -2000, bottom: -1900, left: 0, right: 0, width: 0, height: 100, x: 0, y: -2000, toJSON() { return {}; } });
|
|
102
|
+
// Make sure window.innerHeight is sane.
|
|
103
|
+
Object.defineProperty(window, 'innerHeight', { configurable: true, value: 800 });
|
|
104
|
+
connectTrait(scrollProgress, host);
|
|
105
|
+
expect(parseFloat(host.getAttribute('data-scroll-progress'))).toBe(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('data-scroll-container selector resolves an explicit ancestor', () => {
|
|
109
|
+
document.body.innerHTML = '<div id="custom-scroller" style="overflow:auto;"><div id="inner"></div></div>';
|
|
110
|
+
const host = document.getElementById('inner');
|
|
111
|
+
host.setAttribute('data-scroll-container', '#custom-scroller');
|
|
112
|
+
const scroller = document.getElementById('custom-scroller');
|
|
113
|
+
const addSpy = vi.spyOn(scroller, 'addEventListener');
|
|
114
|
+
connectTrait(scrollProgress, host);
|
|
115
|
+
// The trait should attach a scroll listener to the resolved container.
|
|
116
|
+
const calls = addSpy.mock.calls.filter(([type]) => type === 'scroll');
|
|
117
|
+
expect(calls.length).toBeGreaterThanOrEqual(1);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('invalid data-scroll-container selector falls through to ancestor walk', () => {
|
|
121
|
+
document.body.innerHTML = '<div id="parent" style="overflow:auto;"><div id="child"></div></div>';
|
|
122
|
+
const host = document.getElementById('child');
|
|
123
|
+
host.setAttribute('data-scroll-container', '###not-a-valid-selector');
|
|
124
|
+
expect(() => connectTrait(scrollProgress, host)).not.toThrow();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('schedules through rAF — second scroll within the same frame coalesces', () => {
|
|
128
|
+
let queued = 0;
|
|
129
|
+
let pending = null;
|
|
130
|
+
globalThis.requestAnimationFrame = (fn) => {
|
|
131
|
+
queued++;
|
|
132
|
+
pending = fn;
|
|
133
|
+
return queued;
|
|
134
|
+
};
|
|
135
|
+
globalThis.cancelAnimationFrame = vi.fn();
|
|
136
|
+
const host = mountHost();
|
|
137
|
+
const inst = connectTrait(scrollProgress, host);
|
|
138
|
+
const baseline = queued;
|
|
139
|
+
// Fire two scroll events back-to-back without flushing the rAF.
|
|
140
|
+
window.dispatchEvent(new Event('scroll'));
|
|
141
|
+
window.dispatchEvent(new Event('scroll'));
|
|
142
|
+
// Only one rAF should be queued in addition to the seed.
|
|
143
|
+
expect(queued - baseline).toBeLessThanOrEqual(1);
|
|
144
|
+
// Flush manually so disconnect has nothing pending.
|
|
145
|
+
if (pending) pending(0);
|
|
146
|
+
inst.disconnect(host);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('disconnect cancels the pending rAF', () => {
|
|
150
|
+
let pendingId = null;
|
|
151
|
+
globalThis.requestAnimationFrame = () => { pendingId = 42; return 42; };
|
|
152
|
+
const cancelSpy = vi.fn();
|
|
153
|
+
globalThis.cancelAnimationFrame = cancelSpy;
|
|
154
|
+
const host = mountHost();
|
|
155
|
+
const inst = connectTrait(scrollProgress, host);
|
|
156
|
+
inst.disconnect(host);
|
|
157
|
+
// The trait queued one rAF on connect; disconnect should cancel it.
|
|
158
|
+
expect(cancelSpy).toHaveBeenCalled();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('disconnect removes the scroll listener from the resolved container', () => {
|
|
162
|
+
document.body.innerHTML = '<div id="scr" style="overflow:auto;"><div id="h"></div></div>';
|
|
163
|
+
const host = document.getElementById('h');
|
|
164
|
+
host.setAttribute('data-scroll-container', '#scr');
|
|
165
|
+
const scroller = document.getElementById('scr');
|
|
166
|
+
const removeSpy = vi.spyOn(scroller, 'removeEventListener');
|
|
167
|
+
const inst = connectTrait(scrollProgress, host);
|
|
168
|
+
inst.disconnect(host);
|
|
169
|
+
const calls = removeSpy.mock.calls.filter(([type]) => type === 'scroll');
|
|
170
|
+
expect(calls.length).toBeGreaterThanOrEqual(1);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('reconnect after disconnect does not throw and restores the active flag', () => {
|
|
174
|
+
const host = mountHost();
|
|
175
|
+
const inst1 = connectTrait(scrollProgress, host);
|
|
176
|
+
inst1.disconnect(host);
|
|
177
|
+
const inst2 = connectTrait(scrollProgress, host);
|
|
178
|
+
expect(host.hasAttribute('data-scroll-progress-active')).toBe(true);
|
|
179
|
+
inst2.disconnect(host);
|
|
180
|
+
expect(host.hasAttribute('data-scroll-progress-active')).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { shimmerLoading } from './shimmer-loading.js';
|
|
3
|
-
import { mountHost, connectTrait, resetDOM } from './
|
|
3
|
+
import { mountHost, connectTrait, resetDOM } from './test-helpers.js';
|
|
4
4
|
|
|
5
5
|
describe('shimmer-loading', () => {
|
|
6
6
|
beforeEach(resetDOM);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
10
10
|
import * as traits from './index.js';
|
|
11
|
-
import { mountHost, connectTrait, expectValidSchema, resetDOM } from './
|
|
11
|
+
import { mountHost, connectTrait, expectValidSchema, resetDOM } from './test-helpers.js';
|
|
12
12
|
|
|
13
13
|
const KNOWN_CATEGORIES = new Set([
|
|
14
14
|
'input-interaction',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { snapToGrid } from './snap-to-grid.js';
|
|
3
|
-
import { mountHost, connectTrait, resetDOM } from './
|
|
3
|
+
import { mountHost, connectTrait, resetDOM } from './test-helpers.js';
|
|
4
4
|
|
|
5
5
|
describe('snap-to-grid', () => {
|
|
6
6
|
beforeEach(resetDOM);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { soundFeedback } from './sound-feedback.js';
|
|
3
|
-
import { mountHost, connectTrait, resetDOM } from './
|
|
3
|
+
import { mountHost, connectTrait, resetDOM } from './test-helpers.js';
|
|
4
4
|
|
|
5
5
|
describe('sound-feedback', () => {
|
|
6
6
|
beforeEach(resetDOM);
|
package/traits/spring-animate.js
CHANGED
|
@@ -14,10 +14,15 @@ export const springAnimate = defineTrait({
|
|
|
14
14
|
let rafId = null;
|
|
15
15
|
const target = 0;
|
|
16
16
|
|
|
17
|
-
// Read current translate as starting position
|
|
17
|
+
// Read current translate as starting position. The trait writes to
|
|
18
|
+
// `style.translate` (not `transform`), so read that first; fall back to
|
|
19
|
+
// the transform matrix for callers that nudged via `transform`.
|
|
18
20
|
const cs = getComputedStyle(host);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
let position = parseFloat(cs.translate) || 0;
|
|
22
|
+
if (!position) {
|
|
23
|
+
const matrix = new DOMMatrixReadOnly(cs.transform);
|
|
24
|
+
position = matrix.m41 || 0;
|
|
25
|
+
}
|
|
21
26
|
let velocity = 0;
|
|
22
27
|
|
|
23
28
|
host.setAttribute('data-spring-animate-active', '');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { springAnimate } from './spring-animate.js';
|
|
3
|
-
import { mountHost, connectTrait, resetDOM } from './
|
|
3
|
+
import { mountHost, connectTrait, resetDOM } from './test-helpers.js';
|
|
4
4
|
|
|
5
5
|
describe('spring-animate', () => {
|
|
6
6
|
beforeEach(resetDOM);
|