@adia-ai/web-components 0.2.3 → 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/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 +21 -0
- package/components/textarea/textarea.js +10 -0
- package/core/icons.js +12 -1
- package/package.json +1 -1
- package/styles/components.css +1 -0
- package/styles/typography.css +1 -1
- package/traits/_catalog.json +257 -4
- 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 +67 -63
- 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 +43 -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.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
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { defineTrait } from './define.js';
|
|
2
|
+
|
|
3
|
+
export const longPress = defineTrait({
|
|
4
|
+
name: 'long-press',
|
|
5
|
+
category: 'input-interaction',
|
|
6
|
+
description: 'Press-and-hold trigger: fires after configurable duration with progress events',
|
|
7
|
+
attributes: [
|
|
8
|
+
'data-long-press-active',
|
|
9
|
+
'data-long-press-progress',
|
|
10
|
+
'data-long-press-fired',
|
|
11
|
+
],
|
|
12
|
+
events: ['long-press', 'long-press-cancelled', 'long-press-progress'],
|
|
13
|
+
config: [
|
|
14
|
+
'data-long-press-duration',
|
|
15
|
+
'data-long-press-tolerance',
|
|
16
|
+
'data-long-press-progress-interval',
|
|
17
|
+
],
|
|
18
|
+
setup({ host }) {
|
|
19
|
+
let active = false;
|
|
20
|
+
let fired = false;
|
|
21
|
+
let startX = 0;
|
|
22
|
+
let startY = 0;
|
|
23
|
+
let startedAt = 0;
|
|
24
|
+
let timerId = null;
|
|
25
|
+
let intervalId = null;
|
|
26
|
+
let suppressClick = false;
|
|
27
|
+
|
|
28
|
+
function readDuration() {
|
|
29
|
+
const v = parseInt(host.getAttribute('data-long-press-duration'), 10);
|
|
30
|
+
return Number.isFinite(v) && v > 0 ? v : 600;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readTolerance() {
|
|
34
|
+
const v = parseInt(host.getAttribute('data-long-press-tolerance'), 10);
|
|
35
|
+
return Number.isFinite(v) && v >= 0 ? v : 8;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readProgressInterval() {
|
|
39
|
+
const v = parseInt(host.getAttribute('data-long-press-progress-interval'), 10);
|
|
40
|
+
return Number.isFinite(v) && v > 0 ? v : 50;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isDisabled() {
|
|
44
|
+
return host.hasAttribute('disabled');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function clearTimers() {
|
|
48
|
+
if (timerId !== null) {
|
|
49
|
+
clearTimeout(timerId);
|
|
50
|
+
timerId = null;
|
|
51
|
+
}
|
|
52
|
+
if (intervalId !== null) {
|
|
53
|
+
clearInterval(intervalId);
|
|
54
|
+
intervalId = null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function reset() {
|
|
59
|
+
active = false;
|
|
60
|
+
clearTimers();
|
|
61
|
+
host.removeAttribute('data-long-press-active');
|
|
62
|
+
host.removeAttribute('data-long-press-progress');
|
|
63
|
+
host.removeAttribute('data-long-press-fired');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function cancel(reason) {
|
|
67
|
+
if (!active) return;
|
|
68
|
+
const wasFired = fired;
|
|
69
|
+
reset();
|
|
70
|
+
fired = false;
|
|
71
|
+
// Only the truly-cancelled press emits the cancelled event. A press
|
|
72
|
+
// that already committed (long-press fired) is a successful close,
|
|
73
|
+
// not a cancellation, even though the pointerup handler tears down
|
|
74
|
+
// the same state.
|
|
75
|
+
if (!wasFired) {
|
|
76
|
+
host.dispatchEvent(new CustomEvent('long-press-cancelled', {
|
|
77
|
+
bubbles: true,
|
|
78
|
+
detail: { reason },
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function commit() {
|
|
84
|
+
if (!active || fired) return;
|
|
85
|
+
fired = true;
|
|
86
|
+
// Final progress = 1 so consumers reading the attribute see the
|
|
87
|
+
// completed state before the fired flag flips.
|
|
88
|
+
host.setAttribute('data-long-press-progress', '1');
|
|
89
|
+
host.setAttribute('data-long-press-fired', '');
|
|
90
|
+
// Suppress the next click so consumers don't double-fire on the
|
|
91
|
+
// pointerup that completes the press.
|
|
92
|
+
suppressClick = true;
|
|
93
|
+
host.dispatchEvent(new CustomEvent('long-press', {
|
|
94
|
+
bubbles: true,
|
|
95
|
+
detail: { duration: performance.now() - startedAt },
|
|
96
|
+
}));
|
|
97
|
+
// Clear interval — duration timer already fired itself.
|
|
98
|
+
if (intervalId !== null) {
|
|
99
|
+
clearInterval(intervalId);
|
|
100
|
+
intervalId = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function onPointerDown(e) {
|
|
105
|
+
if (isDisabled()) return;
|
|
106
|
+
if (active) return;
|
|
107
|
+
active = true;
|
|
108
|
+
fired = false;
|
|
109
|
+
startX = e.clientX ?? 0;
|
|
110
|
+
startY = e.clientY ?? 0;
|
|
111
|
+
startedAt = performance.now();
|
|
112
|
+
const duration = readDuration();
|
|
113
|
+
const interval = readProgressInterval();
|
|
114
|
+
|
|
115
|
+
host.setAttribute('data-long-press-active', '');
|
|
116
|
+
host.setAttribute('data-long-press-progress', '0');
|
|
117
|
+
|
|
118
|
+
timerId = setTimeout(commit, duration);
|
|
119
|
+
intervalId = setInterval(() => {
|
|
120
|
+
if (!active || fired) return;
|
|
121
|
+
const elapsed = performance.now() - startedAt;
|
|
122
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
123
|
+
host.setAttribute('data-long-press-progress', String(progress));
|
|
124
|
+
host.dispatchEvent(new CustomEvent('long-press-progress', {
|
|
125
|
+
bubbles: true,
|
|
126
|
+
detail: { progress },
|
|
127
|
+
}));
|
|
128
|
+
}, interval);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function onPointerUp() {
|
|
132
|
+
if (!active) return;
|
|
133
|
+
if (fired) {
|
|
134
|
+
// Successful long-press just completed — reset state silently
|
|
135
|
+
// without firing cancelled.
|
|
136
|
+
reset();
|
|
137
|
+
fired = false;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
cancel('release');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function onPointerLeave() {
|
|
144
|
+
if (!active) return;
|
|
145
|
+
if (fired) {
|
|
146
|
+
// Already committed; pointerleave during the post-fire frame is
|
|
147
|
+
// benign — clean up without dispatching cancelled.
|
|
148
|
+
reset();
|
|
149
|
+
fired = false;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
cancel('leave');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function onPointerMove(e) {
|
|
156
|
+
if (!active || fired) return;
|
|
157
|
+
const tolerance = readTolerance();
|
|
158
|
+
const dx = (e.clientX ?? 0) - startX;
|
|
159
|
+
const dy = (e.clientY ?? 0) - startY;
|
|
160
|
+
if (Math.hypot(dx, dy) > tolerance) {
|
|
161
|
+
cancel('move');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function onPointerCancel() {
|
|
166
|
+
if (!active) return;
|
|
167
|
+
cancel('pointer-cancel');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function onClick(e) {
|
|
171
|
+
// The click that follows a successful long-press is suppressed so
|
|
172
|
+
// consumers wired to both `long-press` and `click` don't double-fire.
|
|
173
|
+
if (suppressClick) {
|
|
174
|
+
suppressClick = false;
|
|
175
|
+
e.stopImmediatePropagation();
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function onContextMenu(e) {
|
|
181
|
+
// While a long-press is in flight (typical on touch), suppress the
|
|
182
|
+
// browser's native context menu so the trait's commit lands cleanly.
|
|
183
|
+
if (active) {
|
|
184
|
+
e.preventDefault();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
host.addEventListener('pointerdown', onPointerDown);
|
|
189
|
+
host.addEventListener('pointerup', onPointerUp);
|
|
190
|
+
host.addEventListener('pointerleave', onPointerLeave);
|
|
191
|
+
host.addEventListener('pointermove', onPointerMove);
|
|
192
|
+
host.addEventListener('pointercancel', onPointerCancel);
|
|
193
|
+
// `click` runs in capture so we beat any consumer-attached click
|
|
194
|
+
// listener and stop the event before it propagates.
|
|
195
|
+
host.addEventListener('click', onClick, true);
|
|
196
|
+
host.addEventListener('contextmenu', onContextMenu);
|
|
197
|
+
|
|
198
|
+
return () => {
|
|
199
|
+
host.removeEventListener('pointerdown', onPointerDown);
|
|
200
|
+
host.removeEventListener('pointerup', onPointerUp);
|
|
201
|
+
host.removeEventListener('pointerleave', onPointerLeave);
|
|
202
|
+
host.removeEventListener('pointermove', onPointerMove);
|
|
203
|
+
host.removeEventListener('pointercancel', onPointerCancel);
|
|
204
|
+
host.removeEventListener('click', onClick, true);
|
|
205
|
+
host.removeEventListener('contextmenu', onContextMenu);
|
|
206
|
+
clearTimers();
|
|
207
|
+
host.removeAttribute('data-long-press-active');
|
|
208
|
+
host.removeAttribute('data-long-press-progress');
|
|
209
|
+
host.removeAttribute('data-long-press-fired');
|
|
210
|
+
};
|
|
211
|
+
},
|
|
212
|
+
});
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { longPress } from './long-press.js';
|
|
3
|
+
import { mountHost, connectTrait, spyEvent, resetDOM, wait } from './test-helpers.js';
|
|
4
|
+
|
|
5
|
+
function pointerDown(host, x = 0, y = 0) {
|
|
6
|
+
host.dispatchEvent(new PointerEvent('pointerdown', { clientX: x, clientY: y, bubbles: true }));
|
|
7
|
+
}
|
|
8
|
+
function pointerUp(host, x = 0, y = 0) {
|
|
9
|
+
host.dispatchEvent(new PointerEvent('pointerup', { clientX: x, clientY: y, bubbles: true }));
|
|
10
|
+
}
|
|
11
|
+
function pointerMove(host, x, y) {
|
|
12
|
+
host.dispatchEvent(new PointerEvent('pointermove', { clientX: x, clientY: y, bubbles: true }));
|
|
13
|
+
}
|
|
14
|
+
function pointerLeave(host) {
|
|
15
|
+
host.dispatchEvent(new PointerEvent('pointerleave', { bubbles: true }));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('long-press', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
resetDOM();
|
|
21
|
+
vi.useFakeTimers();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('sets data-long-press-active on pointerdown', () => {
|
|
25
|
+
const host = mountHost('button', {
|
|
26
|
+
'data-long-press-duration': '100',
|
|
27
|
+
'data-long-press-progress-interval': '20',
|
|
28
|
+
});
|
|
29
|
+
connectTrait(longPress, host);
|
|
30
|
+
pointerDown(host);
|
|
31
|
+
expect(host.hasAttribute('data-long-press-active')).toBe(true);
|
|
32
|
+
expect(host.getAttribute('data-long-press-progress')).toBe('0');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('fires "long-press" after the configured duration elapses', () => {
|
|
36
|
+
const host = mountHost('button', {
|
|
37
|
+
'data-long-press-duration': '100',
|
|
38
|
+
'data-long-press-progress-interval': '20',
|
|
39
|
+
});
|
|
40
|
+
connectTrait(longPress, host);
|
|
41
|
+
const spy = spyEvent(host, 'long-press');
|
|
42
|
+
pointerDown(host);
|
|
43
|
+
vi.advanceTimersByTime(100);
|
|
44
|
+
expect(spy.count).toBe(1);
|
|
45
|
+
expect(host.hasAttribute('data-long-press-fired')).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('emits "long-press-progress" at intervals while held', () => {
|
|
49
|
+
const host = mountHost('button', {
|
|
50
|
+
'data-long-press-duration': '100',
|
|
51
|
+
'data-long-press-progress-interval': '20',
|
|
52
|
+
});
|
|
53
|
+
connectTrait(longPress, host);
|
|
54
|
+
const spy = spyEvent(host, 'long-press-progress');
|
|
55
|
+
pointerDown(host);
|
|
56
|
+
vi.advanceTimersByTime(60); // 3 ticks at 20ms
|
|
57
|
+
expect(spy.count).toBeGreaterThanOrEqual(2);
|
|
58
|
+
// Each tick should report progress in [0, 1].
|
|
59
|
+
for (const detail of spy.captures) {
|
|
60
|
+
expect(detail.progress).toBeGreaterThanOrEqual(0);
|
|
61
|
+
expect(detail.progress).toBeLessThanOrEqual(1);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('fires "long-press-cancelled" when pointerup arrives early', () => {
|
|
66
|
+
const host = mountHost('button', {
|
|
67
|
+
'data-long-press-duration': '200',
|
|
68
|
+
'data-long-press-progress-interval': '50',
|
|
69
|
+
});
|
|
70
|
+
connectTrait(longPress, host);
|
|
71
|
+
const fireSpy = spyEvent(host, 'long-press');
|
|
72
|
+
const cancelSpy = spyEvent(host, 'long-press-cancelled');
|
|
73
|
+
pointerDown(host);
|
|
74
|
+
vi.advanceTimersByTime(80); // well short of 200
|
|
75
|
+
pointerUp(host);
|
|
76
|
+
expect(fireSpy.count).toBe(0);
|
|
77
|
+
expect(cancelSpy.count).toBe(1);
|
|
78
|
+
expect(cancelSpy.last.reason).toBe('release');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('fires "long-press-cancelled" on pointerleave', () => {
|
|
82
|
+
const host = mountHost('button', {
|
|
83
|
+
'data-long-press-duration': '200',
|
|
84
|
+
});
|
|
85
|
+
connectTrait(longPress, host);
|
|
86
|
+
const cancelSpy = spyEvent(host, 'long-press-cancelled');
|
|
87
|
+
pointerDown(host);
|
|
88
|
+
vi.advanceTimersByTime(40);
|
|
89
|
+
pointerLeave(host);
|
|
90
|
+
expect(cancelSpy.count).toBe(1);
|
|
91
|
+
expect(cancelSpy.last.reason).toBe('leave');
|
|
92
|
+
expect(host.hasAttribute('data-long-press-active')).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('cancels when pointermove exceeds the tolerance', () => {
|
|
96
|
+
const host = mountHost('button', {
|
|
97
|
+
'data-long-press-duration': '300',
|
|
98
|
+
'data-long-press-tolerance': '8',
|
|
99
|
+
});
|
|
100
|
+
connectTrait(longPress, host);
|
|
101
|
+
const cancelSpy = spyEvent(host, 'long-press-cancelled');
|
|
102
|
+
pointerDown(host, 0, 0);
|
|
103
|
+
pointerMove(host, 20, 0); // > 8px
|
|
104
|
+
expect(cancelSpy.count).toBe(1);
|
|
105
|
+
expect(cancelSpy.last.reason).toBe('move');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('does NOT cancel when pointermove stays within tolerance', () => {
|
|
109
|
+
const host = mountHost('button', {
|
|
110
|
+
'data-long-press-duration': '100',
|
|
111
|
+
'data-long-press-tolerance': '8',
|
|
112
|
+
});
|
|
113
|
+
connectTrait(longPress, host);
|
|
114
|
+
const cancelSpy = spyEvent(host, 'long-press-cancelled');
|
|
115
|
+
const fireSpy = spyEvent(host, 'long-press');
|
|
116
|
+
pointerDown(host, 0, 0);
|
|
117
|
+
pointerMove(host, 5, 5); // hypot ~7.07, under 8
|
|
118
|
+
vi.advanceTimersByTime(100);
|
|
119
|
+
expect(cancelSpy.count).toBe(0);
|
|
120
|
+
expect(fireSpy.count).toBe(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('respects [disabled] — no listeners fire, no attributes set', () => {
|
|
124
|
+
const host = mountHost('button', {
|
|
125
|
+
disabled: '',
|
|
126
|
+
'data-long-press-duration': '50',
|
|
127
|
+
});
|
|
128
|
+
connectTrait(longPress, host);
|
|
129
|
+
const fireSpy = spyEvent(host, 'long-press');
|
|
130
|
+
pointerDown(host);
|
|
131
|
+
expect(host.hasAttribute('data-long-press-active')).toBe(false);
|
|
132
|
+
vi.advanceTimersByTime(100);
|
|
133
|
+
expect(fireSpy.count).toBe(0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('suppresses the synthetic click that follows a committed long-press', () => {
|
|
137
|
+
const host = mountHost('button', {
|
|
138
|
+
'data-long-press-duration': '100',
|
|
139
|
+
});
|
|
140
|
+
connectTrait(longPress, host);
|
|
141
|
+
let clickHits = 0;
|
|
142
|
+
host.addEventListener('click', () => clickHits++);
|
|
143
|
+
pointerDown(host);
|
|
144
|
+
vi.advanceTimersByTime(100);
|
|
145
|
+
pointerUp(host);
|
|
146
|
+
host.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
147
|
+
expect(clickHits).toBe(0);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('does NOT suppress click after a cancelled press', () => {
|
|
151
|
+
const host = mountHost('button', {
|
|
152
|
+
'data-long-press-duration': '300',
|
|
153
|
+
});
|
|
154
|
+
connectTrait(longPress, host);
|
|
155
|
+
let clickHits = 0;
|
|
156
|
+
host.addEventListener('click', () => clickHits++);
|
|
157
|
+
pointerDown(host);
|
|
158
|
+
vi.advanceTimersByTime(60);
|
|
159
|
+
pointerUp(host); // early release → cancelled
|
|
160
|
+
host.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
161
|
+
expect(clickHits).toBe(1);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('uses default duration (600ms) when attribute missing', () => {
|
|
165
|
+
const host = mountHost('button');
|
|
166
|
+
connectTrait(longPress, host);
|
|
167
|
+
const spy = spyEvent(host, 'long-press');
|
|
168
|
+
pointerDown(host);
|
|
169
|
+
vi.advanceTimersByTime(599);
|
|
170
|
+
expect(spy.count).toBe(0);
|
|
171
|
+
vi.advanceTimersByTime(2);
|
|
172
|
+
expect(spy.count).toBe(1);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('uses default tolerance (8px) when attribute missing', () => {
|
|
176
|
+
const host = mountHost('button', { 'data-long-press-duration': '300' });
|
|
177
|
+
connectTrait(longPress, host);
|
|
178
|
+
const cancelSpy = spyEvent(host, 'long-press-cancelled');
|
|
179
|
+
pointerDown(host, 0, 0);
|
|
180
|
+
pointerMove(host, 7, 0); // under default 8
|
|
181
|
+
expect(cancelSpy.count).toBe(0);
|
|
182
|
+
pointerMove(host, 9, 0); // over default 8
|
|
183
|
+
expect(cancelSpy.count).toBe(1);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('disconnect clears active timers + attributes mid-press', () => {
|
|
187
|
+
const host = mountHost('button', {
|
|
188
|
+
'data-long-press-duration': '200',
|
|
189
|
+
'data-long-press-progress-interval': '50',
|
|
190
|
+
});
|
|
191
|
+
const inst = connectTrait(longPress, host);
|
|
192
|
+
pointerDown(host);
|
|
193
|
+
expect(host.hasAttribute('data-long-press-active')).toBe(true);
|
|
194
|
+
inst.disconnect(host);
|
|
195
|
+
expect(host.hasAttribute('data-long-press-active')).toBe(false);
|
|
196
|
+
expect(host.hasAttribute('data-long-press-progress')).toBe(false);
|
|
197
|
+
// After disconnect, advancing time must not fire long-press.
|
|
198
|
+
const fireSpy = spyEvent(host, 'long-press');
|
|
199
|
+
vi.advanceTimersByTime(500);
|
|
200
|
+
expect(fireSpy.count).toBe(0);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('reconnect after disconnect picks up fresh state', () => {
|
|
204
|
+
const host = mountHost('button', {
|
|
205
|
+
'data-long-press-duration': '100',
|
|
206
|
+
});
|
|
207
|
+
const inst = connectTrait(longPress, host);
|
|
208
|
+
pointerDown(host);
|
|
209
|
+
inst.disconnect(host);
|
|
210
|
+
|
|
211
|
+
connectTrait(longPress, host);
|
|
212
|
+
const spy = spyEvent(host, 'long-press');
|
|
213
|
+
pointerDown(host);
|
|
214
|
+
vi.advanceTimersByTime(100);
|
|
215
|
+
expect(spy.count).toBe(1);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('schema declares the input-interaction category and full attr/event/config sets', () => {
|
|
219
|
+
expect(longPress.schema.name).toBe('long-press');
|
|
220
|
+
expect(longPress.schema.category).toBe('input-interaction');
|
|
221
|
+
expect(longPress.schema.attributes).toContain('data-long-press-active');
|
|
222
|
+
expect(longPress.schema.attributes).toContain('data-long-press-progress');
|
|
223
|
+
expect(longPress.schema.attributes).toContain('data-long-press-fired');
|
|
224
|
+
expect(longPress.schema.events).toEqual(
|
|
225
|
+
expect.arrayContaining(['long-press', 'long-press-cancelled', 'long-press-progress']),
|
|
226
|
+
);
|
|
227
|
+
expect(longPress.schema.config).toEqual(
|
|
228
|
+
expect.arrayContaining([
|
|
229
|
+
'data-long-press-duration',
|
|
230
|
+
'data-long-press-tolerance',
|
|
231
|
+
'data-long-press-progress-interval',
|
|
232
|
+
]),
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('contextmenu is suppressed while a press is in flight', () => {
|
|
237
|
+
const host = mountHost('button', { 'data-long-press-duration': '200' });
|
|
238
|
+
connectTrait(longPress, host);
|
|
239
|
+
pointerDown(host);
|
|
240
|
+
const ev = new Event('contextmenu', { bubbles: true, cancelable: true });
|
|
241
|
+
host.dispatchEvent(ev);
|
|
242
|
+
expect(ev.defaultPrevented).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
});
|
package/traits/magnetic-hover.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { magneticHover } from './magnetic-hover.js';
|
|
3
|
-
import { mountHost, connectTrait, resetDOM } from './
|
|
3
|
+
import { mountHost, connectTrait, resetDOM } from './test-helpers.js';
|
|
4
4
|
|
|
5
5
|
describe('magnetic-hover', () => {
|
|
6
6
|
beforeEach(resetDOM);
|
package/traits/noise-texture.js
CHANGED
|
@@ -6,9 +6,14 @@ export const noiseTexture = defineTrait({
|
|
|
6
6
|
description: 'Procedural grain overlay',
|
|
7
7
|
attributes: ['data-noise-texture-active'],
|
|
8
8
|
events: [],
|
|
9
|
-
config: [],
|
|
9
|
+
config: ['data-noise-strength'],
|
|
10
10
|
setup({ host }) {
|
|
11
|
-
|
|
11
|
+
// Strength governs the SVG rect's opacity (0..1). Default 0.15 — visibly
|
|
12
|
+
// textured without dominating the underlying surface. Earlier defaults
|
|
13
|
+
// multiplied the rect opacity (0.08) by the overlay's CSS opacity (0.5)
|
|
14
|
+
// for an effective 0.04, which read as no-grain.
|
|
15
|
+
const strength = Math.max(0, Math.min(1, parseFloat(host.getAttribute('data-noise-strength')) || 0.15));
|
|
16
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/></filter><rect width="100%" height="100%" filter="url(#n)" opacity="${strength}"/></svg>`;
|
|
12
17
|
const encoded = `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
|
|
13
18
|
|
|
14
19
|
const overlay = document.createElement('div');
|
|
@@ -17,7 +22,6 @@ export const noiseTexture = defineTrait({
|
|
|
17
22
|
background-image: ${encoded};
|
|
18
23
|
background-repeat: repeat;
|
|
19
24
|
border-radius: inherit;
|
|
20
|
-
opacity: 0.5;
|
|
21
25
|
`;
|
|
22
26
|
|
|
23
27
|
host.style.position = host.style.position || 'relative';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { noiseTexture } from './noise-texture.js';
|
|
3
|
-
import { mountHost, connectTrait, resetDOM } from './
|
|
3
|
+
import { mountHost, connectTrait, resetDOM } from './test-helpers.js';
|
|
4
4
|
|
|
5
5
|
describe('noise-texture', () => {
|
|
6
6
|
beforeEach(resetDOM);
|
package/traits/parallax.js
CHANGED
package/traits/parallax.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { parallax } from './parallax.js';
|
|
3
|
-
import { mountHost, connectTrait, resetDOM } from './
|
|
3
|
+
import { mountHost, connectTrait, resetDOM } from './test-helpers.js';
|
|
4
4
|
|
|
5
5
|
describe('parallax', () => {
|
|
6
6
|
beforeEach(resetDOM);
|
package/traits/portal.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { portal } from './portal.js';
|
|
3
|
-
import { mountHost, connectTrait, resetDOM } from './
|
|
3
|
+
import { mountHost, connectTrait, resetDOM } from './test-helpers.js';
|
|
4
4
|
|
|
5
5
|
describe('portal', () => {
|
|
6
6
|
beforeEach(resetDOM);
|
package/traits/pressable.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { pressable } from './pressable.js';
|
|
3
|
-
import { mountHost, connectTrait, spyEvent, resetDOM } from './
|
|
3
|
+
import { mountHost, connectTrait, spyEvent, resetDOM } from './test-helpers.js';
|
|
4
4
|
|
|
5
5
|
describe('pressable', () => {
|
|
6
6
|
beforeEach(resetDOM);
|
package/traits/resettable.js
CHANGED
|
@@ -17,12 +17,38 @@ export const resettable = defineTrait({
|
|
|
17
17
|
events: ['reset-applied'],
|
|
18
18
|
config: [],
|
|
19
19
|
setup({ host }) {
|
|
20
|
-
|
|
20
|
+
// Boolean controls (switch-ui, check-ui, radio-ui) carry their state on
|
|
21
|
+
// a `checked` reactive prop, not `value`. Detect at connect time so the
|
|
22
|
+
// captured initial + reset write target the right field.
|
|
23
|
+
//
|
|
24
|
+
// Native HTMLInputElement always exposes a `checked` getter regardless
|
|
25
|
+
// of type, so we can't bareword-check `'checked' in host`. Discriminate
|
|
26
|
+
// by tag name + native-input type. Custom AdiaUI elements only declare
|
|
27
|
+
// `checked` when it's their primary state — switch-ui, check-ui,
|
|
28
|
+
// radio-ui — so the bareword check is safe there.
|
|
29
|
+
const tag = host.tagName?.toLowerCase?.() || '';
|
|
30
|
+
const type = host.getAttribute?.('type') || '';
|
|
31
|
+
const isNativeInput = tag === 'input';
|
|
32
|
+
const isNativeCheckable = isNativeInput && (type === 'checkbox' || type === 'radio');
|
|
33
|
+
const isCustomCheckable = !isNativeInput
|
|
34
|
+
&& 'checked' in host
|
|
35
|
+
&& typeof host.checked === 'boolean';
|
|
36
|
+
const isCheckable = isNativeCheckable || isCustomCheckable;
|
|
37
|
+
|
|
38
|
+
const initialValue = isCheckable
|
|
39
|
+
? host.checked
|
|
40
|
+
: (host.value ?? host.getAttribute('value') ?? '');
|
|
41
|
+
|
|
21
42
|
const form = host.closest?.('form');
|
|
22
43
|
|
|
23
44
|
function onReset() {
|
|
24
|
-
if (
|
|
25
|
-
|
|
45
|
+
if (isCheckable) {
|
|
46
|
+
host.checked = initialValue;
|
|
47
|
+
} else if ('value' in host) {
|
|
48
|
+
host.value = initialValue;
|
|
49
|
+
} else {
|
|
50
|
+
host.setAttribute('value', initialValue);
|
|
51
|
+
}
|
|
26
52
|
host.dispatchEvent(new CustomEvent('reset-applied', {
|
|
27
53
|
bubbles: true,
|
|
28
54
|
detail: { initialValue },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { resettable } from './resettable.js';
|
|
3
|
-
import { mountHost, connectTrait, spyEvent, resetDOM } from './
|
|
3
|
+
import { mountHost, connectTrait, spyEvent, resetDOM } from './test-helpers.js';
|
|
4
4
|
|
|
5
5
|
function mountInForm(initialValue = 'initial') {
|
|
6
6
|
const form = document.createElement('form');
|
|
@@ -64,4 +64,37 @@ describe('resettable', () => {
|
|
|
64
64
|
expect(a.value).toBe('A');
|
|
65
65
|
expect(b.value).toBe('B');
|
|
66
66
|
});
|
|
67
|
+
|
|
68
|
+
it('boolean controls (host.checked) restore their initial checked state', () => {
|
|
69
|
+
// Synthesize a switch-like host: the only state is a boolean `checked`
|
|
70
|
+
// reactive property — no `value`. Mirrors switch-ui's API.
|
|
71
|
+
const form = document.createElement('form');
|
|
72
|
+
document.body.appendChild(form);
|
|
73
|
+
const host = document.createElement('div');
|
|
74
|
+
host.checked = true;
|
|
75
|
+
form.appendChild(host);
|
|
76
|
+
|
|
77
|
+
connectTrait(resettable, host);
|
|
78
|
+
const spy = spyEvent(host, 'reset-applied');
|
|
79
|
+
|
|
80
|
+
host.checked = false; // user toggles it off
|
|
81
|
+
form.dispatchEvent(new Event('reset', { bubbles: true }));
|
|
82
|
+
|
|
83
|
+
expect(host.checked).toBe(true);
|
|
84
|
+
expect(spy.count).toBe(1);
|
|
85
|
+
expect(spy.last.initialValue).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('boolean controls starting unchecked stay unchecked after reset', () => {
|
|
89
|
+
const form = document.createElement('form');
|
|
90
|
+
document.body.appendChild(form);
|
|
91
|
+
const host = document.createElement('div');
|
|
92
|
+
host.checked = false;
|
|
93
|
+
form.appendChild(host);
|
|
94
|
+
|
|
95
|
+
connectTrait(resettable, host);
|
|
96
|
+
host.checked = true;
|
|
97
|
+
form.dispatchEvent(new Event('reset', { bubbles: true }));
|
|
98
|
+
expect(host.checked).toBe(false);
|
|
99
|
+
});
|
|
67
100
|
});
|
package/traits/resizable.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { resizable } from './resizable.js';
|
|
3
|
-
import { mountHost, connectTrait, resetDOM } from './
|
|
3
|
+
import { mountHost, connectTrait, resetDOM } from './test-helpers.js';
|
|
4
4
|
|
|
5
5
|
describe('resizable', () => {
|
|
6
6
|
beforeEach(resetDOM);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
2
|
import { resizeObserver } from './resize-observer.js';
|
|
3
|
-
import { mountHost, connectTrait, resetDOM } from './
|
|
3
|
+
import { mountHost, connectTrait, resetDOM } from './test-helpers.js';
|
|
4
4
|
|
|
5
5
|
describe('resize-observer', () => {
|
|
6
6
|
let originalRO;
|
package/traits/ripple.js
CHANGED
package/traits/ripple.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { ripple } from './ripple.js';
|
|
3
|
-
import { mountHost, connectTrait, resetDOM } from './
|
|
3
|
+
import { mountHost, connectTrait, resetDOM } from './test-helpers.js';
|
|
4
4
|
|
|
5
5
|
describe('ripple', () => {
|
|
6
6
|
beforeEach(resetDOM);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { rovingTabindex } from './roving-tabindex.js';
|
|
3
|
-
import { mountHost, connectTrait, resetDOM } from './
|
|
3
|
+
import { mountHost, connectTrait, resetDOM } from './test-helpers.js';
|
|
4
4
|
|
|
5
5
|
function tabbableChild(host) {
|
|
6
6
|
const btn = document.createElement('button');
|