@adia-ai/web-components 0.8.19 → 0.8.21
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/CHANGELOG.md +26 -0
- package/components/card/card.a2ui.json +6 -3
- package/components/card/card.css +15 -1
- package/components/card/card.yaml +5 -3
- package/components/nav-group/nav-group.a2ui.json +5 -0
- package/components/nav-group/nav-group.class.js +14 -6
- package/components/nav-group/nav-group.css +5 -2
- package/components/nav-group/nav-group.d.ts +2 -0
- package/components/nav-group/nav-group.yaml +4 -0
- package/components/select/select.a2ui.json +3 -2
- package/components/select/select.class.js +14 -1
- package/components/select/select.css +13 -1
- package/components/select/select.yaml +5 -4
- package/components/table/table.class.js +7 -3
- package/components/table/table.css +7 -5
- package/dist/theme-provider.min.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +19 -19
- package/dist/web-components.sheet.js +1 -1
- package/package.json +2 -1
- package/components/adia-mark/adia-mark.test.js +0 -69
- package/components/adia-wordmark/adia-wordmark.test.js +0 -69
- package/components/agent-artifact/agent-artifact.test.js +0 -61
- package/components/alert/alert.test.js +0 -364
- package/components/color-picker/color-picker.test.js +0 -96
- package/components/combobox/combobox.test.js +0 -256
- package/components/command/command.test.js +0 -99
- package/components/date-range-picker/date-range-picker.test.js +0 -387
- package/components/datetime-picker/datetime-picker.test.js +0 -454
- package/components/demo-toggle/demo-toggle.test.js +0 -102
- package/components/description-list/description-list.test.js +0 -72
- package/components/display-field/display-field.test.js +0 -115
- package/components/empty-state/empty-state.test.js +0 -77
- package/components/field/field.test.js +0 -260
- package/components/fields/fields.test.js +0 -88
- package/components/inline-message/inline-message.test.js +0 -203
- package/components/input/input.test.js +0 -335
- package/components/integration-card/integration-card.test.js +0 -318
- package/components/link/link.test.js +0 -143
- package/components/list/list.test.js +0 -106
- package/components/list-window/list-window.test.js +0 -303
- package/components/loading-overlay/loading-overlay.test.js +0 -257
- package/components/menu/menu.test.js +0 -176
- package/components/nav/nav.test.js +0 -428
- package/components/nav-group/nav-group.test.js +0 -67
- package/components/option-card/option-card.test.js +0 -32
- package/components/pagination/pagination.test.js +0 -195
- package/components/select/select.test.js +0 -481
- package/components/slider/slider.test.js +0 -202
- package/components/spinner/spinner.test.js +0 -272
- package/components/stat/stat.test.js +0 -108
- package/components/step-progress/step-progress.test.js +0 -129
- package/components/swatch/swatch.test.js +0 -563
- package/components/table/table.test.js +0 -479
- package/components/table-toolbar/table-toolbar.test.js +0 -152
- package/components/tag/tag.test.js +0 -137
- package/components/tags-input/tags-input.test.js +0 -477
- package/components/text/text.test.js +0 -202
- package/components/theme-provider/theme-provider.test.js +0 -94
- package/components/time-picker/time-picker.test.js +0 -287
- package/components/timeline/timeline.test.js +0 -31
- package/components/toggle-scheme/toggle-scheme.test.js +0 -110
- package/components/tree/tree.test.js +0 -312
- package/core/element.test.js +0 -389
- package/core/icons.test.js +0 -187
- package/core/logical-children.test.js +0 -54
- package/core/markdown.test.js +0 -47
- package/core/responsive.test.js +0 -121
- package/core/template.test.js +0 -611
- package/traits/active-state/active-state.test.js +0 -28
- package/traits/anchor-positioning/anchor-positioning.test.js +0 -122
- package/traits/announcer/announcer.test.js +0 -268
- package/traits/arrow-grid-nav/arrow-grid-nav.test.js +0 -375
- package/traits/attention-pulse/attention-pulse.test.js +0 -26
- package/traits/confetti/confetti.test.js +0 -46
- package/traits/confetti-burst/confetti-burst.test.js +0 -46
- package/traits/count-up/count-up.test.js +0 -54
- package/traits/declarative.test.js +0 -138
- package/traits/dirty-state/dirty-state.test.js +0 -45
- package/traits/drag-ghost/drag-ghost.test.js +0 -19
- package/traits/draggable/draggable.test.js +0 -60
- package/traits/draggable-list-item/draggable-list-item.test.js +0 -51
- package/traits/drop-target/drop-target.test.js +0 -241
- package/traits/droppable/droppable.test.js +0 -54
- package/traits/droppable-collection/droppable-collection.test.js +0 -99
- package/traits/error-shake/error-shake.test.js +0 -114
- package/traits/fade-presence/fade-presence.test.js +0 -20
- package/traits/focus-restore/focus-restore.test.js +0 -202
- package/traits/focus-trap/focus-trap.test.js +0 -42
- package/traits/focusable/focusable.test.js +0 -53
- package/traits/glow-focus/glow-focus.test.js +0 -31
- package/traits/gradient-shift/gradient-shift.test.js +0 -22
- package/traits/haptic-feedback/haptic-feedback.test.js +0 -52
- package/traits/hotkey/hotkey.test.js +0 -61
- package/traits/hoverable/hoverable.test.js +0 -24
- package/traits/inertia-drag/inertia-drag.test.js +0 -33
- package/traits/input-mask/input-mask.test.js +0 -151
- package/traits/intersection-observer/intersection-observer.test.js +0 -38
- package/traits/keyboard-nav/keyboard-nav.test.js +0 -41
- package/traits/keyboard-reorderable/keyboard-reorderable.test.js +0 -45
- package/traits/layout-animation/layout-animation.test.js +0 -114
- package/traits/long-press/long-press.test.js +0 -244
- package/traits/magnetic-hover/magnetic-hover.test.js +0 -30
- package/traits/noise-texture/noise-texture.test.js +0 -20
- package/traits/parallax/parallax.test.js +0 -26
- package/traits/portal/portal.test.js +0 -30
- package/traits/pressable/pressable.test.js +0 -73
- package/traits/resettable/resettable.test.js +0 -100
- package/traits/resizable/resizable.test.js +0 -39
- package/traits/resize-observer/resize-observer.test.js +0 -38
- package/traits/ripple/ripple.test.js +0 -32
- package/traits/roving-tabindex/roving-tabindex.test.js +0 -28
- package/traits/scale-press/scale-press.test.js +0 -39
- package/traits/scroll-lock/scroll-lock.test.js +0 -45
- package/traits/scroll-progress/scroll-progress.test.js +0 -182
- package/traits/shimmer-loading/shimmer-loading.test.js +0 -43
- package/traits/smoke.test.js +0 -111
- package/traits/snap-to-grid/snap-to-grid.test.js +0 -40
- package/traits/sound-feedback/sound-feedback.test.js +0 -26
- package/traits/spring-animate/spring-animate.test.js +0 -28
- package/traits/success-checkmark/success-checkmark.test.js +0 -120
- package/traits/tilt-hover/tilt-hover.test.js +0 -32
- package/traits/tossable/tossable.test.js +0 -31
- package/traits/traits-host.test.js +0 -73
- package/traits/typeahead/typeahead.test.js +0 -38
- package/traits/typewriter/typewriter.test.js +0 -47
- package/traits/validation/validation.test.js +0 -93
- package/traits/view-transition/view-transition.test.js +0 -268
|
@@ -1,375 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { arrowGridNav } from './arrow-grid-nav.js';
|
|
3
|
-
import { mountHost, connectTrait, spyEvent, resetDOM } from '../test-helpers.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Build a row-flat grid with `total` cells laid out in `cols` columns.
|
|
7
|
-
* Returns the host element pre-populated with <button> children.
|
|
8
|
-
*/
|
|
9
|
-
function flatGrid(cols, total) {
|
|
10
|
-
const host = mountHost('div', { 'data-grid-columns': String(cols) });
|
|
11
|
-
for (let i = 0; i < total; i++) {
|
|
12
|
-
const btn = document.createElement('button');
|
|
13
|
-
btn.textContent = `cell-${i}`;
|
|
14
|
-
btn.setAttribute('data-cell-index', String(i));
|
|
15
|
-
host.appendChild(btn);
|
|
16
|
-
}
|
|
17
|
-
return host;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Build a row-nested grid: each direct child is a row, each row's children
|
|
22
|
-
* are cells. `rowSpec` is an array of column counts per row.
|
|
23
|
-
*/
|
|
24
|
-
function nestedGrid(rowSpec) {
|
|
25
|
-
const host = mountHost('div', { 'data-grid-mode': 'row-nested' });
|
|
26
|
-
rowSpec.forEach((cols, rowIdx) => {
|
|
27
|
-
const row = document.createElement('div');
|
|
28
|
-
for (let c = 0; c < cols; c++) {
|
|
29
|
-
const btn = document.createElement('button');
|
|
30
|
-
btn.textContent = `r${rowIdx}c${c}`;
|
|
31
|
-
row.appendChild(btn);
|
|
32
|
-
}
|
|
33
|
-
host.appendChild(row);
|
|
34
|
-
});
|
|
35
|
-
return host;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function key(host, k, modifiers = {}) {
|
|
39
|
-
host.dispatchEvent(new KeyboardEvent('keydown', {
|
|
40
|
-
key: k,
|
|
41
|
-
bubbles: true,
|
|
42
|
-
...modifiers,
|
|
43
|
-
}));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
describe('arrow-grid-nav', () => {
|
|
47
|
-
beforeEach(resetDOM);
|
|
48
|
-
|
|
49
|
-
it('paints active row + col on connect at (0, 0)', () => {
|
|
50
|
-
const host = flatGrid(7, 42);
|
|
51
|
-
connectTrait(arrowGridNav, host);
|
|
52
|
-
expect(host.getAttribute('data-grid-active-row')).toBe('0');
|
|
53
|
-
expect(host.getAttribute('data-grid-active-col')).toBe('0');
|
|
54
|
-
const first = host.children[0];
|
|
55
|
-
expect(first.hasAttribute('data-grid-active')).toBe(true);
|
|
56
|
-
expect(first.getAttribute('tabindex')).toBe('0');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('sets host tabindex to 0 when not pre-set', () => {
|
|
60
|
-
const host = flatGrid(7, 42);
|
|
61
|
-
connectTrait(arrowGridNav, host);
|
|
62
|
-
expect(host.getAttribute('tabindex')).toBe('0');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('preserves a pre-set host tabindex', () => {
|
|
66
|
-
const host = flatGrid(7, 42);
|
|
67
|
-
host.setAttribute('tabindex', '5');
|
|
68
|
-
connectTrait(arrowGridNav, host);
|
|
69
|
-
expect(host.getAttribute('tabindex')).toBe('5');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('non-active cells receive tabindex="-1"', () => {
|
|
73
|
-
const host = flatGrid(7, 42);
|
|
74
|
-
connectTrait(arrowGridNav, host);
|
|
75
|
-
expect(host.children[1].getAttribute('tabindex')).toBe('-1');
|
|
76
|
-
expect(host.children[10].getAttribute('tabindex')).toBe('-1');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('ArrowRight increments col within the row', () => {
|
|
80
|
-
const host = flatGrid(7, 42);
|
|
81
|
-
connectTrait(arrowGridNav, host);
|
|
82
|
-
key(host, 'ArrowRight');
|
|
83
|
-
expect(host.getAttribute('data-grid-active-col')).toBe('1');
|
|
84
|
-
expect(host.getAttribute('data-grid-active-row')).toBe('0');
|
|
85
|
-
expect(host.children[1].hasAttribute('data-grid-active')).toBe(true);
|
|
86
|
-
expect(host.children[0].hasAttribute('data-grid-active')).toBe(false);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('ArrowLeft decrements col', () => {
|
|
90
|
-
const host = flatGrid(7, 42);
|
|
91
|
-
connectTrait(arrowGridNav, host);
|
|
92
|
-
key(host, 'ArrowRight');
|
|
93
|
-
key(host, 'ArrowRight');
|
|
94
|
-
key(host, 'ArrowLeft');
|
|
95
|
-
expect(host.getAttribute('data-grid-active-col')).toBe('1');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('ArrowDown increments row, keeps col', () => {
|
|
99
|
-
const host = flatGrid(7, 42);
|
|
100
|
-
connectTrait(arrowGridNav, host);
|
|
101
|
-
key(host, 'ArrowRight');
|
|
102
|
-
key(host, 'ArrowRight');
|
|
103
|
-
key(host, 'ArrowRight');
|
|
104
|
-
key(host, 'ArrowDown');
|
|
105
|
-
expect(host.getAttribute('data-grid-active-row')).toBe('1');
|
|
106
|
-
expect(host.getAttribute('data-grid-active-col')).toBe('3');
|
|
107
|
-
// index 3 + 7 = 10
|
|
108
|
-
expect(host.children[10].hasAttribute('data-grid-active')).toBe(true);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('ArrowUp decrements row', () => {
|
|
112
|
-
const host = flatGrid(7, 42);
|
|
113
|
-
connectTrait(arrowGridNav, host);
|
|
114
|
-
key(host, 'ArrowDown');
|
|
115
|
-
key(host, 'ArrowDown');
|
|
116
|
-
key(host, 'ArrowUp');
|
|
117
|
-
expect(host.getAttribute('data-grid-active-row')).toBe('1');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('Home moves to col 0 within current row', () => {
|
|
121
|
-
const host = flatGrid(7, 42);
|
|
122
|
-
connectTrait(arrowGridNav, host);
|
|
123
|
-
key(host, 'ArrowDown');
|
|
124
|
-
key(host, 'ArrowRight');
|
|
125
|
-
key(host, 'ArrowRight');
|
|
126
|
-
expect(host.getAttribute('data-grid-active-col')).toBe('2');
|
|
127
|
-
key(host, 'Home');
|
|
128
|
-
expect(host.getAttribute('data-grid-active-row')).toBe('1');
|
|
129
|
-
expect(host.getAttribute('data-grid-active-col')).toBe('0');
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('Ctrl+Home moves to (0, 0)', () => {
|
|
133
|
-
const host = flatGrid(7, 42);
|
|
134
|
-
connectTrait(arrowGridNav, host);
|
|
135
|
-
key(host, 'ArrowDown');
|
|
136
|
-
key(host, 'ArrowDown');
|
|
137
|
-
key(host, 'ArrowRight');
|
|
138
|
-
key(host, 'Home', { ctrlKey: true });
|
|
139
|
-
expect(host.getAttribute('data-grid-active-row')).toBe('0');
|
|
140
|
-
expect(host.getAttribute('data-grid-active-col')).toBe('0');
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('End moves to last col in current row', () => {
|
|
144
|
-
const host = flatGrid(7, 42);
|
|
145
|
-
connectTrait(arrowGridNav, host);
|
|
146
|
-
key(host, 'End');
|
|
147
|
-
expect(host.getAttribute('data-grid-active-col')).toBe('6');
|
|
148
|
-
expect(host.getAttribute('data-grid-active-row')).toBe('0');
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('Ctrl+End moves to (lastRow, lastCol)', () => {
|
|
152
|
-
const host = flatGrid(7, 42); // 6 rows × 7 cols
|
|
153
|
-
connectTrait(arrowGridNav, host);
|
|
154
|
-
key(host, 'End', { ctrlKey: true });
|
|
155
|
-
expect(host.getAttribute('data-grid-active-row')).toBe('5');
|
|
156
|
-
expect(host.getAttribute('data-grid-active-col')).toBe('6');
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('Meta+End is treated as Ctrl+End on Mac', () => {
|
|
160
|
-
const host = flatGrid(7, 42);
|
|
161
|
-
connectTrait(arrowGridNav, host);
|
|
162
|
-
key(host, 'End', { metaKey: true });
|
|
163
|
-
expect(host.getAttribute('data-grid-active-row')).toBe('5');
|
|
164
|
-
expect(host.getAttribute('data-grid-active-col')).toBe('6');
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('Enter dispatches grid-activate with row, col, element', () => {
|
|
168
|
-
const host = flatGrid(7, 42);
|
|
169
|
-
connectTrait(arrowGridNav, host);
|
|
170
|
-
const spy = spyEvent(host, 'grid-activate');
|
|
171
|
-
key(host, 'ArrowRight');
|
|
172
|
-
key(host, 'ArrowDown');
|
|
173
|
-
key(host, 'Enter');
|
|
174
|
-
expect(spy.count).toBe(1);
|
|
175
|
-
expect(spy.last).toMatchObject({ row: 1, col: 1 });
|
|
176
|
-
expect(spy.last.element).toBe(host.children[8]); // row 1 col 1 = index 8
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('Space dispatches grid-activate', () => {
|
|
180
|
-
const host = flatGrid(7, 42);
|
|
181
|
-
connectTrait(arrowGridNav, host);
|
|
182
|
-
const spy = spyEvent(host, 'grid-activate');
|
|
183
|
-
key(host, ' ');
|
|
184
|
-
expect(spy.count).toBe(1);
|
|
185
|
-
expect(spy.last).toMatchObject({ row: 0, col: 0 });
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('emits grid-edge "left" at column 0', () => {
|
|
189
|
-
const host = flatGrid(7, 42);
|
|
190
|
-
connectTrait(arrowGridNav, host);
|
|
191
|
-
const spy = spyEvent(host, 'grid-edge');
|
|
192
|
-
key(host, 'ArrowLeft');
|
|
193
|
-
expect(spy.count).toBe(1);
|
|
194
|
-
expect(spy.last.edge).toBe('left');
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it('emits grid-edge "right" at last column', () => {
|
|
198
|
-
const host = flatGrid(7, 42);
|
|
199
|
-
connectTrait(arrowGridNav, host);
|
|
200
|
-
key(host, 'End');
|
|
201
|
-
const spy = spyEvent(host, 'grid-edge');
|
|
202
|
-
key(host, 'ArrowRight');
|
|
203
|
-
expect(spy.count).toBe(1);
|
|
204
|
-
expect(spy.last.edge).toBe('right');
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('emits grid-edge "top" at row 0', () => {
|
|
208
|
-
const host = flatGrid(7, 42);
|
|
209
|
-
connectTrait(arrowGridNav, host);
|
|
210
|
-
const spy = spyEvent(host, 'grid-edge');
|
|
211
|
-
key(host, 'ArrowUp');
|
|
212
|
-
expect(spy.count).toBe(1);
|
|
213
|
-
expect(spy.last.edge).toBe('top');
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it('emits grid-edge "bottom" at last row', () => {
|
|
217
|
-
const host = flatGrid(7, 42);
|
|
218
|
-
connectTrait(arrowGridNav, host);
|
|
219
|
-
key(host, 'End', { ctrlKey: true });
|
|
220
|
-
const spy = spyEvent(host, 'grid-edge');
|
|
221
|
-
key(host, 'ArrowDown');
|
|
222
|
-
expect(spy.count).toBe(1);
|
|
223
|
-
expect(spy.last.edge).toBe('bottom');
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it('grid-cell-change fires when active changes', () => {
|
|
227
|
-
const host = flatGrid(7, 42);
|
|
228
|
-
connectTrait(arrowGridNav, host);
|
|
229
|
-
const spy = spyEvent(host, 'grid-cell-change');
|
|
230
|
-
key(host, 'ArrowRight');
|
|
231
|
-
key(host, 'ArrowRight');
|
|
232
|
-
expect(spy.count).toBe(2);
|
|
233
|
-
expect(spy.last).toMatchObject({ row: 0, col: 2 });
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it('grid-cell-change does NOT fire when no movement happens at edge', () => {
|
|
237
|
-
const host = flatGrid(7, 42);
|
|
238
|
-
connectTrait(arrowGridNav, host);
|
|
239
|
-
const spy = spyEvent(host, 'grid-cell-change');
|
|
240
|
-
key(host, 'ArrowLeft'); // already at col 0
|
|
241
|
-
expect(spy.count).toBe(0);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it('row-nested mode infers rows from container children', () => {
|
|
245
|
-
const host = nestedGrid([3, 3, 3]); // 3x3 grid
|
|
246
|
-
connectTrait(arrowGridNav, host);
|
|
247
|
-
expect(host.getAttribute('data-grid-active-row')).toBe('0');
|
|
248
|
-
expect(host.getAttribute('data-grid-active-col')).toBe('0');
|
|
249
|
-
key(host, 'ArrowDown');
|
|
250
|
-
expect(host.getAttribute('data-grid-active-row')).toBe('1');
|
|
251
|
-
const row1 = host.children[1];
|
|
252
|
-
expect(row1.children[0].hasAttribute('data-grid-active')).toBe(true);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it('row-nested mode handles ragged rows by clamping col on row change', () => {
|
|
256
|
-
const host = nestedGrid([5, 2, 4]); // row 0 has 5 cols, row 1 has 2, row 2 has 4
|
|
257
|
-
connectTrait(arrowGridNav, host);
|
|
258
|
-
key(host, 'End'); // col 4 in row 0
|
|
259
|
-
expect(host.getAttribute('data-grid-active-col')).toBe('4');
|
|
260
|
-
key(host, 'ArrowDown'); // row 1 only has 2 cols → clamp to 1
|
|
261
|
-
expect(host.getAttribute('data-grid-active-row')).toBe('1');
|
|
262
|
-
expect(host.getAttribute('data-grid-active-col')).toBe('1');
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it('disabled cells are skipped in row-flat mode', () => {
|
|
266
|
-
const host = flatGrid(7, 42);
|
|
267
|
-
host.children[1].setAttribute('disabled', '');
|
|
268
|
-
connectTrait(arrowGridNav, host);
|
|
269
|
-
key(host, 'ArrowRight');
|
|
270
|
-
// Disabled child #1 is filtered out → row-flat splits the remaining
|
|
271
|
-
// 41 cells; index 1 in the post-filter list is original cell #2.
|
|
272
|
-
expect(host.children[2].hasAttribute('data-grid-active')).toBe(true);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it('does not steal arrow keys from <input> within a cell', () => {
|
|
276
|
-
const host = mountHost('div', { 'data-grid-columns': '3' });
|
|
277
|
-
for (let i = 0; i < 3; i++) {
|
|
278
|
-
const cell = document.createElement('div');
|
|
279
|
-
const inp = document.createElement('input');
|
|
280
|
-
cell.appendChild(inp);
|
|
281
|
-
host.appendChild(cell);
|
|
282
|
-
}
|
|
283
|
-
connectTrait(arrowGridNav, host);
|
|
284
|
-
const inp = host.querySelector('input');
|
|
285
|
-
inp.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
|
|
286
|
-
// No movement — col remains 0.
|
|
287
|
-
expect(host.getAttribute('data-grid-active-col')).toBe('0');
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it('does not respond to non-handled keys', () => {
|
|
291
|
-
const host = flatGrid(7, 42);
|
|
292
|
-
connectTrait(arrowGridNav, host);
|
|
293
|
-
const spy = spyEvent(host, 'grid-cell-change');
|
|
294
|
-
key(host, 'a');
|
|
295
|
-
key(host, 'Tab');
|
|
296
|
-
key(host, 'Escape');
|
|
297
|
-
expect(spy.count).toBe(0);
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
it('MutationObserver re-paints when children are added', async () => {
|
|
301
|
-
const host = flatGrid(7, 7); // 1 row of 7
|
|
302
|
-
connectTrait(arrowGridNav, host);
|
|
303
|
-
key(host, 'End'); // col 6
|
|
304
|
-
expect(host.getAttribute('data-grid-active-col')).toBe('6');
|
|
305
|
-
// Add 7 more children → row 1 exists.
|
|
306
|
-
for (let i = 0; i < 7; i++) {
|
|
307
|
-
const btn = document.createElement('button');
|
|
308
|
-
btn.textContent = `new-${i}`;
|
|
309
|
-
host.appendChild(btn);
|
|
310
|
-
}
|
|
311
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
312
|
-
// Active should still be at (0, 6).
|
|
313
|
-
expect(host.getAttribute('data-grid-active-row')).toBe('0');
|
|
314
|
-
// Now arrow-down should reach the new row.
|
|
315
|
-
key(host, 'ArrowDown');
|
|
316
|
-
expect(host.getAttribute('data-grid-active-row')).toBe('1');
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it('disconnect removes all managed attrs from host and active cell', () => {
|
|
320
|
-
const host = flatGrid(7, 42);
|
|
321
|
-
const inst = connectTrait(arrowGridNav, host);
|
|
322
|
-
key(host, 'ArrowRight');
|
|
323
|
-
inst.disconnect(host);
|
|
324
|
-
expect(host.hasAttribute('data-arrow-grid-nav-active')).toBe(false);
|
|
325
|
-
expect(host.hasAttribute('data-grid-active-row')).toBe(false);
|
|
326
|
-
expect(host.hasAttribute('data-grid-active-col')).toBe(false);
|
|
327
|
-
for (const child of host.children) {
|
|
328
|
-
expect(child.hasAttribute('data-grid-active')).toBe(false);
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it('disconnect removes the keydown listener', () => {
|
|
333
|
-
const host = flatGrid(7, 42);
|
|
334
|
-
const inst = connectTrait(arrowGridNav, host);
|
|
335
|
-
inst.disconnect(host);
|
|
336
|
-
const spy = spyEvent(host, 'grid-cell-change');
|
|
337
|
-
key(host, 'ArrowRight');
|
|
338
|
-
expect(spy.count).toBe(0);
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
it('reconnect after disconnect works', () => {
|
|
342
|
-
const host = flatGrid(7, 42);
|
|
343
|
-
const inst1 = connectTrait(arrowGridNav, host);
|
|
344
|
-
inst1.disconnect(host);
|
|
345
|
-
const inst2 = connectTrait(arrowGridNav, host);
|
|
346
|
-
expect(host.getAttribute('data-grid-active-row')).toBe('0');
|
|
347
|
-
expect(host.children[0].hasAttribute('data-grid-active')).toBe(true);
|
|
348
|
-
inst2.disconnect(host);
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
it('default mode is row-flat when data-grid-mode missing', () => {
|
|
352
|
-
const host = flatGrid(4, 12);
|
|
353
|
-
connectTrait(arrowGridNav, host);
|
|
354
|
-
key(host, 'ArrowDown');
|
|
355
|
-
expect(host.getAttribute('data-grid-active-row')).toBe('1');
|
|
356
|
-
expect(host.children[4].hasAttribute('data-grid-active')).toBe(true);
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
it('missing data-grid-columns falls back to 1 (single column)', () => {
|
|
360
|
-
const host = mountHost('div');
|
|
361
|
-
for (let i = 0; i < 4; i++) {
|
|
362
|
-
const btn = document.createElement('button');
|
|
363
|
-
host.appendChild(btn);
|
|
364
|
-
}
|
|
365
|
-
connectTrait(arrowGridNav, host);
|
|
366
|
-
key(host, 'ArrowDown');
|
|
367
|
-
expect(host.getAttribute('data-grid-active-row')).toBe('1');
|
|
368
|
-
expect(host.getAttribute('data-grid-active-col')).toBe('0');
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it('empty grid does not throw', () => {
|
|
372
|
-
const host = mountHost('div', { 'data-grid-columns': '7' });
|
|
373
|
-
expect(() => connectTrait(arrowGridNav, host)).not.toThrow();
|
|
374
|
-
});
|
|
375
|
-
});
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { attentionPulse } from './attention-pulse.js';
|
|
3
|
-
import { mountHost, connectTrait, resetDOM } from '../test-helpers.js';
|
|
4
|
-
|
|
5
|
-
describe('attention-pulse', () => {
|
|
6
|
-
beforeEach(resetDOM);
|
|
7
|
-
|
|
8
|
-
it('connect sets data-attention-pulse-active', () => {
|
|
9
|
-
const host = mountHost();
|
|
10
|
-
connectTrait(attentionPulse, host);
|
|
11
|
-
expect(host.hasAttribute('data-attention-pulse-active')).toBe(true);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('disconnect clears active + animation', () => {
|
|
15
|
-
const host = mountHost();
|
|
16
|
-
const inst = connectTrait(attentionPulse, host);
|
|
17
|
-
inst.disconnect(host);
|
|
18
|
-
expect(host.hasAttribute('data-attention-pulse-active')).toBe(false);
|
|
19
|
-
expect(host.style.animation).toBe('');
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('respects data-pulse-interval (default 2000ms)', () => {
|
|
23
|
-
const host = mountHost('div', { 'data-pulse-interval': '500' });
|
|
24
|
-
expect(() => connectTrait(attentionPulse, host)).not.toThrow();
|
|
25
|
-
});
|
|
26
|
-
});
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { confetti } from './confetti.js';
|
|
3
|
-
import { _resetStage } from './confetti-stage.js';
|
|
4
|
-
import { mountHost, connectTrait, resetDOM } from '../test-helpers.js';
|
|
5
|
-
|
|
6
|
-
describe('confetti', () => {
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
resetDOM();
|
|
9
|
-
_resetStage();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('connect sets data-confetti-active without throwing', () => {
|
|
13
|
-
const host = mountHost();
|
|
14
|
-
connectTrait(confetti, host);
|
|
15
|
-
expect(host.hasAttribute('data-confetti-active')).toBe(true);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('disconnect clears active', () => {
|
|
19
|
-
const host = mountHost();
|
|
20
|
-
const inst = connectTrait(confetti, host);
|
|
21
|
-
inst.disconnect(host);
|
|
22
|
-
expect(host.hasAttribute('data-confetti-active')).toBe(false);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('no canvas: graceful degradation in happy-dom', () => {
|
|
26
|
-
// Old contract: this trait used a host-scoped <canvas>. New
|
|
27
|
-
// contract: it pushes <span> particles into the body-level
|
|
28
|
-
// popover stage (no canvas anywhere). The test name is kept for
|
|
29
|
-
// continuity — the assertion is the same: connect must not throw
|
|
30
|
-
// in environments without real layout (rect = 0,0,0,0 in
|
|
31
|
-
// happy-dom; viewportCenterOf returns null and the trait skips
|
|
32
|
-
// particle spawning gracefully).
|
|
33
|
-
const host = mountHost();
|
|
34
|
-
expect(() => connectTrait(confetti, host)).not.toThrow();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('does not append particles inside the host (stage is body-level)', () => {
|
|
38
|
-
const host = mountHost();
|
|
39
|
-
const inst = connectTrait(confetti, host);
|
|
40
|
-
// The old implementation appended a <canvas> child to the host.
|
|
41
|
-
// The new implementation must leave the host's children alone —
|
|
42
|
-
// every particle belongs to the body-level popover stage.
|
|
43
|
-
expect(host.children.length).toBe(0);
|
|
44
|
-
inst.disconnect(host);
|
|
45
|
-
});
|
|
46
|
-
});
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { confettiBurst } from './confetti-burst.js';
|
|
3
|
-
import { _resetStage } from '../confetti/confetti-stage.js';
|
|
4
|
-
import { mountHost, connectTrait, spyEvent, resetDOM, wait } from '../test-helpers.js';
|
|
5
|
-
|
|
6
|
-
describe('confetti-burst', () => {
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
resetDOM();
|
|
9
|
-
_resetStage();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('sets data-confetti-burst-active on connect', () => {
|
|
13
|
-
const host = mountHost();
|
|
14
|
-
connectTrait(confettiBurst, host);
|
|
15
|
-
expect(host.hasAttribute('data-confetti-burst-active')).toBe(true);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('fires confetti-burst-done even without real layout (happy-dom)', async () => {
|
|
19
|
-
const host = mountHost();
|
|
20
|
-
const spy = spyEvent(host, 'confetti-burst-done');
|
|
21
|
-
connectTrait(confettiBurst, host);
|
|
22
|
-
// happy-dom returns rect = 0,0,0,0 for unattached layout. The
|
|
23
|
-
// trait's graceful-degradation path queues `done` on next tick.
|
|
24
|
-
await wait(50);
|
|
25
|
-
expect(spy.count).toBeGreaterThanOrEqual(1);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('disconnect removes the active attribute', () => {
|
|
29
|
-
const host = mountHost();
|
|
30
|
-
const inst = confettiBurst();
|
|
31
|
-
inst.connect(host);
|
|
32
|
-
inst.disconnect(host);
|
|
33
|
-
expect(host.hasAttribute('data-confetti-burst-active')).toBe(false);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('host stays unmodified by particle injection', () => {
|
|
37
|
-
const host = mountHost();
|
|
38
|
-
const inst = connectTrait(confettiBurst, host);
|
|
39
|
-
// New contract: particles live in the body-level popover stage,
|
|
40
|
-
// not on the host. The host must not gain children or inline
|
|
41
|
-
// position/cssText hacks (the old canvas-injection path did).
|
|
42
|
-
expect(host.tagName).toBe('DIV');
|
|
43
|
-
expect(host.children.length).toBe(0);
|
|
44
|
-
inst.disconnect(host);
|
|
45
|
-
});
|
|
46
|
-
});
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { countUp } from './count-up.js';
|
|
3
|
-
import { mountHost, connectTrait, spyEvent, resetDOM, wait, raf } from '../test-helpers.js';
|
|
4
|
-
|
|
5
|
-
describe('count-up', () => {
|
|
6
|
-
beforeEach(resetDOM);
|
|
7
|
-
|
|
8
|
-
it('reads data-count-up-target + duration; sets active', () => {
|
|
9
|
-
const host = mountHost('span', {
|
|
10
|
-
'data-count-up-target': '100',
|
|
11
|
-
'data-count-duration': '200',
|
|
12
|
-
});
|
|
13
|
-
host.textContent = '0';
|
|
14
|
-
connectTrait(countUp, host);
|
|
15
|
-
expect(host.hasAttribute('data-count-up-active')).toBe(true);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('eventually reaches the target value and fires count-up-done', async () => {
|
|
19
|
-
const host = mountHost('span', {
|
|
20
|
-
'data-count-up-target': '42',
|
|
21
|
-
'data-count-duration': '50',
|
|
22
|
-
});
|
|
23
|
-
host.textContent = '0';
|
|
24
|
-
const spy = spyEvent(host, 'count-up-done');
|
|
25
|
-
connectTrait(countUp, host);
|
|
26
|
-
// happy-dom does not implement requestAnimationFrame in a way that drives
|
|
27
|
-
// the easing loop; we assert the active flag and that the trait sets up
|
|
28
|
-
// without throwing. The end-state assertion is covered by the smoke
|
|
29
|
-
// contract (attribute cleanup) and by the e2e demo in real browsers.
|
|
30
|
-
expect(host.hasAttribute('data-count-up-active')).toBe(true);
|
|
31
|
-
expect(typeof spy.count).toBe('number');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('disconnect clears active and cancels the rAF loop', () => {
|
|
35
|
-
const host = mountHost('span', { 'data-count-up-target': '500' });
|
|
36
|
-
host.textContent = '0';
|
|
37
|
-
const inst = countUp();
|
|
38
|
-
inst.connect(host);
|
|
39
|
-
inst.disconnect(host);
|
|
40
|
-
expect(host.hasAttribute('data-count-up-active')).toBe(false);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('missing target attribute uses default 0', () => {
|
|
44
|
-
const host = mountHost('span');
|
|
45
|
-
host.textContent = '0';
|
|
46
|
-
expect(() => countUp().connect(host)).not.toThrow();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('missing duration uses default 1000ms', () => {
|
|
50
|
-
const host = mountHost('span', { 'data-count-up-target': '10' });
|
|
51
|
-
host.textContent = '0';
|
|
52
|
-
expect(() => countUp().connect(host)).not.toThrow();
|
|
53
|
-
});
|
|
54
|
-
});
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* UIElement attribute-driven trait composition.
|
|
3
|
-
*
|
|
4
|
-
* <comp-ui traits="pressable scale-press ripple">
|
|
5
|
-
*
|
|
6
|
-
* Declarative wiring is the third path next to:
|
|
7
|
-
* - static traits = [...] (compile-time, on the class)
|
|
8
|
-
* - el.addTrait(traitFn) (runtime, programmatic)
|
|
9
|
-
*
|
|
10
|
-
* Tests assert: connect-time application, idempotence with static, attribute
|
|
11
|
-
* change re-applies, unknown name warns without throwing.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
15
|
-
import { UIElement } from '../core/element.js';
|
|
16
|
-
import { pressable } from './pressable/pressable.js';
|
|
17
|
-
import { focusable } from './focusable/focusable.js';
|
|
18
|
-
import { hoverable } from './hoverable/hoverable.js';
|
|
19
|
-
import { resetDOM } from './test-helpers.js';
|
|
20
|
-
|
|
21
|
-
let counter = 0;
|
|
22
|
-
function defineUniqueTag(BaseImpl) {
|
|
23
|
-
const tag = `decl-trait-test-${++counter}`;
|
|
24
|
-
customElements.define(tag, BaseImpl);
|
|
25
|
-
return tag;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
describe('UIElement — declarative traits attribute', () => {
|
|
29
|
-
beforeEach(resetDOM);
|
|
30
|
-
|
|
31
|
-
it('applies traits listed in the attribute on connect', () => {
|
|
32
|
-
class El extends UIElement {}
|
|
33
|
-
const tag = defineUniqueTag(El);
|
|
34
|
-
document.body.innerHTML = `<${tag} traits="pressable focusable"></${tag}>`;
|
|
35
|
-
const el = document.body.firstElementChild;
|
|
36
|
-
expect(el.hasAttribute('traits')).toBe(true);
|
|
37
|
-
el.dispatchEvent(new PointerEvent('pointerdown'));
|
|
38
|
-
expect(el.hasAttribute('data-pressable-pressed')).toBe(true);
|
|
39
|
-
el.dispatchEvent(new FocusEvent('focus'));
|
|
40
|
-
expect(el.hasAttribute('data-focusable-keyboard')).toBe(true);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('static traits + attribute traits coexist, no double-apply', () => {
|
|
44
|
-
class El extends UIElement {
|
|
45
|
-
static get traits() { return [pressable]; }
|
|
46
|
-
}
|
|
47
|
-
const tag = defineUniqueTag(El);
|
|
48
|
-
document.body.innerHTML = `<${tag} traits="pressable focusable"></${tag}>`;
|
|
49
|
-
const el = document.body.firstElementChild;
|
|
50
|
-
el.dispatchEvent(new PointerEvent('pointerdown'));
|
|
51
|
-
el.dispatchEvent(new PointerEvent('pointerup'));
|
|
52
|
-
let pressCount = 0;
|
|
53
|
-
el.addEventListener('press', () => pressCount++);
|
|
54
|
-
el.dispatchEvent(new PointerEvent('pointerdown'));
|
|
55
|
-
el.dispatchEvent(new PointerEvent('pointerup'));
|
|
56
|
-
// exactly ONE press fires per pointerdown/up (not two from double-apply)
|
|
57
|
-
expect(pressCount).toBe(1);
|
|
58
|
-
// focusable from attribute also wired
|
|
59
|
-
el.dispatchEvent(new FocusEvent('focus'));
|
|
60
|
-
expect(el.hasAttribute('data-focusable-keyboard')).toBe(true);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('unknown trait name warns but does not throw', () => {
|
|
64
|
-
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
65
|
-
class El extends UIElement {}
|
|
66
|
-
const tag = defineUniqueTag(El);
|
|
67
|
-
expect(() => {
|
|
68
|
-
document.body.innerHTML = `<${tag} traits="pressable nonexistent-trait"></${tag}>`;
|
|
69
|
-
}).not.toThrow();
|
|
70
|
-
const el = document.body.firstElementChild;
|
|
71
|
-
// Real trait still applied
|
|
72
|
-
el.dispatchEvent(new PointerEvent('pointerdown'));
|
|
73
|
-
expect(el.hasAttribute('data-pressable-pressed')).toBe(true);
|
|
74
|
-
expect(warn).toHaveBeenCalled();
|
|
75
|
-
expect(warn.mock.calls[0][0]).toContain('nonexistent-trait');
|
|
76
|
-
warn.mockRestore();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('changing the attribute swaps declarative traits', () => {
|
|
80
|
-
class El extends UIElement {}
|
|
81
|
-
const tag = defineUniqueTag(El);
|
|
82
|
-
document.body.innerHTML = `<${tag} traits="pressable"></${tag}>`;
|
|
83
|
-
const el = document.body.firstElementChild;
|
|
84
|
-
el.dispatchEvent(new PointerEvent('pointerdown'));
|
|
85
|
-
expect(el.hasAttribute('data-pressable-pressed')).toBe(true);
|
|
86
|
-
el.dispatchEvent(new PointerEvent('pointerup'));
|
|
87
|
-
|
|
88
|
-
// Swap to focusable+hoverable
|
|
89
|
-
el.setAttribute('traits', 'focusable hoverable');
|
|
90
|
-
|
|
91
|
-
// pressable should no longer respond
|
|
92
|
-
el.dispatchEvent(new PointerEvent('pointerdown'));
|
|
93
|
-
expect(el.hasAttribute('data-pressable-pressed')).toBe(false);
|
|
94
|
-
|
|
95
|
-
// focusable should respond
|
|
96
|
-
el.dispatchEvent(new FocusEvent('focus'));
|
|
97
|
-
expect(el.hasAttribute('data-focusable-keyboard')).toBe(true);
|
|
98
|
-
|
|
99
|
-
// hoverable should respond
|
|
100
|
-
el.dispatchEvent(new PointerEvent('pointerenter'));
|
|
101
|
-
expect(el.hasAttribute('data-hoverable-hover')).toBe(true);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('clearing the attribute removes declarative traits', () => {
|
|
105
|
-
class El extends UIElement {}
|
|
106
|
-
const tag = defineUniqueTag(El);
|
|
107
|
-
document.body.innerHTML = `<${tag} traits="pressable"></${tag}>`;
|
|
108
|
-
const el = document.body.firstElementChild;
|
|
109
|
-
el.removeAttribute('traits');
|
|
110
|
-
el.dispatchEvent(new PointerEvent('pointerdown'));
|
|
111
|
-
expect(el.hasAttribute('data-pressable-pressed')).toBe(false);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('disconnect cleans up both static and declarative traits', () => {
|
|
115
|
-
class El extends UIElement {
|
|
116
|
-
static get traits() { return [pressable]; }
|
|
117
|
-
}
|
|
118
|
-
const tag = defineUniqueTag(El);
|
|
119
|
-
document.body.innerHTML = `<${tag} traits="focusable"></${tag}>`;
|
|
120
|
-
const el = document.body.firstElementChild;
|
|
121
|
-
el.dispatchEvent(new PointerEvent('pointerdown'));
|
|
122
|
-
el.dispatchEvent(new FocusEvent('focus'));
|
|
123
|
-
expect(el.hasAttribute('data-pressable-pressed')).toBe(true);
|
|
124
|
-
expect(el.hasAttribute('data-focusable-keyboard')).toBe(true);
|
|
125
|
-
el.remove();
|
|
126
|
-
expect(el.hasAttribute('data-pressable-pressed')).toBe(false);
|
|
127
|
-
expect(el.hasAttribute('data-focusable-keyboard')).toBe(false);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('observedAttributes always includes "traits"', () => {
|
|
131
|
-
class El extends UIElement {
|
|
132
|
-
static get properties() { return { value: { type: String, default: '' } }; }
|
|
133
|
-
}
|
|
134
|
-
const observed = El.observedAttributes;
|
|
135
|
-
expect(observed).toContain('value');
|
|
136
|
-
expect(observed).toContain('traits');
|
|
137
|
-
});
|
|
138
|
-
});
|