@gjsify/adwaita-web 0.15.1 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adwaita-web.css +408 -0
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/adw-data-grid.spec.d.ts +1 -0
- package/lib/types/adw-dialog.spec.d.ts +1 -0
- package/lib/types/adw-drop-down.spec.d.ts +1 -0
- package/lib/types/elements/adw-data-grid.d.ts +60 -0
- package/lib/types/elements/adw-dialog.d.ts +51 -0
- package/lib/types/elements/adw-drop-down.d.ts +63 -0
- package/lib/types/index.d.ts +6 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/package.json +13 -11
- package/scss/_data_grid.scss +144 -0
- package/scss/_dialog.scss +220 -0
- package/scss/_drop_down.scss +185 -0
- package/scss/_icons.generated.scss +221 -0
- package/scss/_theme.scss +2 -0
- package/scss/_variables.scss +4 -0
- package/scss/adwaita-skin.scss +3 -0
- package/src/adw-data-grid.spec.ts +243 -0
- package/src/adw-dialog.spec.ts +169 -0
- package/src/adw-drop-down.spec.ts +186 -0
- package/src/elements/adw-data-grid.ts +335 -0
- package/src/elements/adw-dialog.ts +329 -0
- package/src/elements/adw-drop-down.ts +405 -0
- package/src/index.ts +11 -0
- package/src/styles.generated.ts +4 -0
- package/src/test.browser.mts +14 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// DOM-level behaviour tests for <adw-dialog>. Runs in a real browser via the
|
|
2
|
+
// @gjsify/adwaita-web browser test axis (tests/browser Playwright harness). The
|
|
3
|
+
// custom elements are registered by importing the package root in test.browser.mts;
|
|
4
|
+
// these specs create elements via document.createElement and assert their DOM
|
|
5
|
+
// behaviour (present/close events, can-close gating, header/content, focus return).
|
|
6
|
+
import { describe, it, expect } from '@gjsify/unit';
|
|
7
|
+
|
|
8
|
+
import type { AdwDialog } from './elements/adw-dialog.js';
|
|
9
|
+
|
|
10
|
+
function makeDialog(html = '<p>Body</p>'): AdwDialog {
|
|
11
|
+
const el = document.createElement('adw-dialog') as AdwDialog;
|
|
12
|
+
el.innerHTML = html;
|
|
13
|
+
document.body.appendChild(el);
|
|
14
|
+
return el;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const AdwDialogTest = async () => {
|
|
18
|
+
await describe('adw-dialog present/close', async () => {
|
|
19
|
+
await it('present() reveals + fires notify::open(true)', async () => {
|
|
20
|
+
const dialog = makeDialog();
|
|
21
|
+
let notified: boolean | null = null;
|
|
22
|
+
dialog.addEventListener('notify::open', (e) => {
|
|
23
|
+
notified = (e as CustomEvent).detail.open;
|
|
24
|
+
});
|
|
25
|
+
expect(dialog.open).toBe(false);
|
|
26
|
+
dialog.present();
|
|
27
|
+
expect(dialog.open).toBe(true);
|
|
28
|
+
expect(dialog.classList.contains('open')).toBe(true);
|
|
29
|
+
expect(notified).toBe(true);
|
|
30
|
+
dialog.remove();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await it('close() hides + fires notify::open(false) then closed', async () => {
|
|
34
|
+
const dialog = makeDialog();
|
|
35
|
+
dialog.present();
|
|
36
|
+
const order: string[] = [];
|
|
37
|
+
dialog.addEventListener('notify::open', (e) => {
|
|
38
|
+
order.push(`notify:${(e as CustomEvent).detail.open}`);
|
|
39
|
+
});
|
|
40
|
+
dialog.addEventListener('closed', () => order.push('closed'));
|
|
41
|
+
dialog.close();
|
|
42
|
+
expect(dialog.open).toBe(false);
|
|
43
|
+
expect(order).toStrictEqual(['notify:false', 'closed']);
|
|
44
|
+
dialog.remove();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await it('present() on a disconnected element attaches it to the body', async () => {
|
|
48
|
+
const dialog = document.createElement('adw-dialog') as AdwDialog;
|
|
49
|
+
expect(dialog.isConnected).toBe(false);
|
|
50
|
+
dialog.present();
|
|
51
|
+
expect(dialog.isConnected).toBe(true);
|
|
52
|
+
expect(dialog.open).toBe(true);
|
|
53
|
+
dialog.remove();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await describe('adw-dialog can-close gating', async () => {
|
|
58
|
+
await it('close() while locked raises close-attempt and stays open', async () => {
|
|
59
|
+
const dialog = makeDialog();
|
|
60
|
+
dialog.canClose = false;
|
|
61
|
+
dialog.present();
|
|
62
|
+
let attempts = 0;
|
|
63
|
+
let closed = 0;
|
|
64
|
+
dialog.addEventListener('close-attempt', () => attempts++);
|
|
65
|
+
dialog.addEventListener('closed', () => closed++);
|
|
66
|
+
dialog.close();
|
|
67
|
+
expect(dialog.open).toBe(true);
|
|
68
|
+
expect(attempts).toBe(1);
|
|
69
|
+
expect(closed).toBe(0);
|
|
70
|
+
dialog.remove();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await it('forceClose() closes even while locked', async () => {
|
|
74
|
+
const dialog = makeDialog();
|
|
75
|
+
dialog.canClose = false;
|
|
76
|
+
dialog.present();
|
|
77
|
+
let closed = 0;
|
|
78
|
+
dialog.addEventListener('closed', () => closed++);
|
|
79
|
+
dialog.forceClose();
|
|
80
|
+
expect(dialog.open).toBe(false);
|
|
81
|
+
expect(closed).toBe(1);
|
|
82
|
+
dialog.remove();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await it('Escape on the box dismisses when unlocked, is gated when locked', async () => {
|
|
86
|
+
const dialog = makeDialog();
|
|
87
|
+
dialog.present();
|
|
88
|
+
const box = dialog.querySelector('.adw-dialog-box') as HTMLElement;
|
|
89
|
+
box.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
90
|
+
expect(dialog.open).toBe(false);
|
|
91
|
+
|
|
92
|
+
const locked = makeDialog();
|
|
93
|
+
locked.canClose = false;
|
|
94
|
+
locked.present();
|
|
95
|
+
const lockedBox = locked.querySelector('.adw-dialog-box') as HTMLElement;
|
|
96
|
+
let attempts = 0;
|
|
97
|
+
locked.addEventListener('close-attempt', () => attempts++);
|
|
98
|
+
lockedBox.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
99
|
+
expect(locked.open).toBe(true);
|
|
100
|
+
expect(attempts).toBe(1);
|
|
101
|
+
dialog.remove();
|
|
102
|
+
locked.remove();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await describe('adw-dialog content + header', async () => {
|
|
107
|
+
await it('moves author markup into the content area', async () => {
|
|
108
|
+
const dialog = makeDialog('<p id="hello">Hello</p>');
|
|
109
|
+
const content = dialog.querySelector('.adw-dialog-content');
|
|
110
|
+
expect(content?.querySelector('#hello')?.textContent).toBe('Hello');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await it('renders the header only when a title is set', async () => {
|
|
114
|
+
const dialog = makeDialog();
|
|
115
|
+
const header = dialog.querySelector('.adw-dialog-header') as HTMLElement;
|
|
116
|
+
expect(header.hidden).toBe(true);
|
|
117
|
+
dialog.title = 'Settings';
|
|
118
|
+
expect(header.hidden).toBe(false);
|
|
119
|
+
expect(dialog.querySelector('.adw-dialog-title')?.textContent).toBe('Settings');
|
|
120
|
+
dialog.remove();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await it('reflects presentation-mode + content-width', async () => {
|
|
124
|
+
const dialog = makeDialog();
|
|
125
|
+
expect(dialog.dataset.presentation).toBe('auto');
|
|
126
|
+
dialog.presentationMode = 'bottom-sheet';
|
|
127
|
+
expect(dialog.dataset.presentation).toBe('bottom-sheet');
|
|
128
|
+
dialog.contentWidth = 500;
|
|
129
|
+
const box = dialog.querySelector('.adw-dialog-box') as HTMLElement;
|
|
130
|
+
expect(box.style.getPropertyValue('--adw-dialog-content-width')).toBe('500px');
|
|
131
|
+
dialog.remove();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await it('the close button reflects the locked state', async () => {
|
|
135
|
+
const dialog = makeDialog();
|
|
136
|
+
dialog.title = 'X';
|
|
137
|
+
const closeBtn = dialog.querySelector('.adw-dialog-close') as HTMLButtonElement;
|
|
138
|
+
expect(closeBtn.disabled).toBe(false);
|
|
139
|
+
dialog.canClose = false;
|
|
140
|
+
expect(closeBtn.disabled).toBe(true);
|
|
141
|
+
expect(closeBtn.classList.contains('locked')).toBe(true);
|
|
142
|
+
dialog.remove();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await describe('adw-dialog focus handling', async () => {
|
|
147
|
+
await it('returns focus to the opener on close', async () => {
|
|
148
|
+
const opener = document.createElement('button');
|
|
149
|
+
opener.textContent = 'Open';
|
|
150
|
+
document.body.appendChild(opener);
|
|
151
|
+
opener.focus();
|
|
152
|
+
expect(document.activeElement).toBe(opener);
|
|
153
|
+
|
|
154
|
+
const dialog = makeDialog('<button id="inner">Inner</button>');
|
|
155
|
+
dialog.present();
|
|
156
|
+
// Move focus into the dialog explicitly (present() also does this, but
|
|
157
|
+
// focusing a just-shown element in the same tick is layout-timing
|
|
158
|
+
// sensitive under headless Chrome — so drive it deterministically here).
|
|
159
|
+
(dialog.querySelector('#inner') as HTMLButtonElement).focus();
|
|
160
|
+
// Close must return focus to the opener (never leave it trapped inside
|
|
161
|
+
// the dialog — the regression this guards).
|
|
162
|
+
dialog.close();
|
|
163
|
+
expect(dialog.contains(document.activeElement)).toBe(false);
|
|
164
|
+
expect(document.activeElement).toBe(opener);
|
|
165
|
+
dialog.remove();
|
|
166
|
+
opener.remove();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// DOM-level behaviour tests for <adw-drop-down>. Runs in a real browser via the
|
|
2
|
+
// @gjsify/adwaita-web browser test axis. Asserts option parsing, selected↔value
|
|
3
|
+
// sync, notify-on-change semantics, popover open/close, item selection, search
|
|
4
|
+
// filtering and the disabled guard.
|
|
5
|
+
import { describe, it, expect } from '@gjsify/unit';
|
|
6
|
+
|
|
7
|
+
import type { AdwDropDown } from './elements/adw-drop-down.js';
|
|
8
|
+
|
|
9
|
+
function makeDropDown(): AdwDropDown {
|
|
10
|
+
const el = document.createElement('adw-drop-down') as AdwDropDown;
|
|
11
|
+
document.body.appendChild(el);
|
|
12
|
+
return el;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const AdwDropDownTest = async () => {
|
|
16
|
+
await describe('adw-drop-down options', async () => {
|
|
17
|
+
await it('accepts a string[] (value === label)', async () => {
|
|
18
|
+
const dd = makeDropDown();
|
|
19
|
+
dd.options = ['One', 'Two', 'Three'];
|
|
20
|
+
expect(dd.options.length).toBe(3);
|
|
21
|
+
expect(dd.options[0]).toStrictEqual({ value: 'One', label: 'One' });
|
|
22
|
+
expect(dd.querySelector('.adw-drop-down-label')?.textContent).toBe('One');
|
|
23
|
+
dd.remove();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await it('accepts {value,label}[] and shows the selected label', async () => {
|
|
27
|
+
const dd = makeDropDown();
|
|
28
|
+
dd.options = [
|
|
29
|
+
{ value: 'a', label: 'Apple' },
|
|
30
|
+
{ value: 'b', label: 'Banana' },
|
|
31
|
+
];
|
|
32
|
+
dd.selected = 1;
|
|
33
|
+
expect(dd.querySelector('.adw-drop-down-label')?.textContent).toBe('Banana');
|
|
34
|
+
expect(dd.selectedValue).toBe('b');
|
|
35
|
+
dd.remove();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await it('parses options from the JSON attribute', async () => {
|
|
39
|
+
const dd = document.createElement('adw-drop-down') as AdwDropDown;
|
|
40
|
+
dd.setAttribute('options', '[{"value":"x","label":"Ex"},{"value":"y","label":"Why"}]');
|
|
41
|
+
dd.setAttribute('selected', '1');
|
|
42
|
+
document.body.appendChild(dd);
|
|
43
|
+
expect(dd.options.length).toBe(2);
|
|
44
|
+
expect(dd.selected).toBe(1);
|
|
45
|
+
expect(dd.selectedValue).toBe('y');
|
|
46
|
+
dd.remove();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await it('renders one popover item per option', async () => {
|
|
50
|
+
const dd = makeDropDown();
|
|
51
|
+
dd.options = ['a', 'b', 'c'];
|
|
52
|
+
expect(dd.querySelectorAll('.adw-drop-down-item').length).toBe(3);
|
|
53
|
+
dd.remove();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await describe('adw-drop-down selected ↔ value sync', async () => {
|
|
58
|
+
await it('selectedValue setter finds the index by value', async () => {
|
|
59
|
+
const dd = makeDropDown();
|
|
60
|
+
dd.options = [
|
|
61
|
+
{ value: 'r', label: 'Red' },
|
|
62
|
+
{ value: 'g', label: 'Green' },
|
|
63
|
+
{ value: 'b', label: 'Blue' },
|
|
64
|
+
];
|
|
65
|
+
dd.selectedValue = 'b';
|
|
66
|
+
expect(dd.selected).toBe(2);
|
|
67
|
+
expect(dd.querySelector('.adw-drop-down-label')?.textContent).toBe('Blue');
|
|
68
|
+
dd.remove();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await it('selected reflects to the attribute + marks the row', async () => {
|
|
72
|
+
const dd = makeDropDown();
|
|
73
|
+
dd.options = ['a', 'b'];
|
|
74
|
+
dd.selected = 1;
|
|
75
|
+
expect(dd.getAttribute('selected')).toBe('1');
|
|
76
|
+
const items = dd.querySelectorAll('.adw-drop-down-item');
|
|
77
|
+
expect(items[1].classList.contains('selected')).toBe(true);
|
|
78
|
+
expect(items[1].getAttribute('aria-selected')).toBe('true');
|
|
79
|
+
dd.remove();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await it('selectedItem returns the descriptor', async () => {
|
|
83
|
+
const dd = makeDropDown();
|
|
84
|
+
dd.options = [{ value: 'k', label: 'Key' }];
|
|
85
|
+
expect(dd.selectedItem).toStrictEqual({ value: 'k', label: 'Key' });
|
|
86
|
+
dd.remove();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await it('setting options + selected before connect does not crash + applies on connect', async () => {
|
|
90
|
+
// Regression: the property setters ran before connectedCallback built
|
|
91
|
+
// the DOM, so _updateLabel touched an undefined label element.
|
|
92
|
+
const dd = document.createElement('adw-drop-down') as AdwDropDown;
|
|
93
|
+
dd.options = ['a', 'b', 'c'];
|
|
94
|
+
dd.selected = 2;
|
|
95
|
+
expect(dd.isConnected).toBe(false);
|
|
96
|
+
document.body.appendChild(dd);
|
|
97
|
+
expect(dd.selected).toBe(2);
|
|
98
|
+
expect(dd.querySelector('.adw-drop-down-label')?.textContent).toBe('c');
|
|
99
|
+
expect(dd.querySelectorAll('.adw-drop-down-item').length).toBe(3);
|
|
100
|
+
dd.remove();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await describe('adw-drop-down notify-on-change', async () => {
|
|
105
|
+
await it('fires notify::selected + change on a real change only', async () => {
|
|
106
|
+
const dd = makeDropDown();
|
|
107
|
+
dd.options = ['a', 'b', 'c'];
|
|
108
|
+
let notifyCount = 0;
|
|
109
|
+
let lastChange: unknown = null;
|
|
110
|
+
dd.addEventListener('notify::selected', () => notifyCount++);
|
|
111
|
+
dd.addEventListener('change', (e) => {
|
|
112
|
+
lastChange = (e as CustomEvent).detail;
|
|
113
|
+
});
|
|
114
|
+
dd.selected = 2;
|
|
115
|
+
expect(notifyCount).toBe(1);
|
|
116
|
+
expect(lastChange).toStrictEqual({ index: 2, value: 'c', label: 'c' });
|
|
117
|
+
// Re-selecting the same index must NOT fire again.
|
|
118
|
+
dd.selected = 2;
|
|
119
|
+
expect(notifyCount).toBe(1);
|
|
120
|
+
dd.remove();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await it('a popover item click selects, fires change + closes', async () => {
|
|
124
|
+
const dd = makeDropDown();
|
|
125
|
+
dd.options = ['a', 'b', 'c'];
|
|
126
|
+
let change: unknown = null;
|
|
127
|
+
dd.addEventListener('change', (e) => {
|
|
128
|
+
change = (e as CustomEvent).detail;
|
|
129
|
+
});
|
|
130
|
+
(dd.querySelector('.adw-drop-down-button') as HTMLButtonElement).click();
|
|
131
|
+
expect(dd.active).toBe(true);
|
|
132
|
+
const items = dd.querySelectorAll('.adw-drop-down-item');
|
|
133
|
+
(items[1] as HTMLButtonElement).click();
|
|
134
|
+
expect(dd.selected).toBe(1);
|
|
135
|
+
expect(change).toStrictEqual({ index: 1, value: 'b', label: 'b' });
|
|
136
|
+
expect(dd.active).toBe(false);
|
|
137
|
+
dd.remove();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await describe('adw-drop-down popover behaviour', async () => {
|
|
142
|
+
await it('opens on button click and closes on outside pointerdown', async () => {
|
|
143
|
+
const dd = makeDropDown();
|
|
144
|
+
dd.options = ['a', 'b'];
|
|
145
|
+
const button = dd.querySelector('.adw-drop-down-button') as HTMLButtonElement;
|
|
146
|
+
const popover = dd.querySelector('.adw-drop-down-popover') as HTMLElement;
|
|
147
|
+
expect(popover.hidden).toBe(true);
|
|
148
|
+
button.click();
|
|
149
|
+
expect(dd.active).toBe(true);
|
|
150
|
+
expect(popover.hidden).toBe(false);
|
|
151
|
+
// Outside pointerdown dismisses.
|
|
152
|
+
document.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }));
|
|
153
|
+
expect(dd.active).toBe(false);
|
|
154
|
+
expect(popover.hidden).toBe(true);
|
|
155
|
+
dd.remove();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await it('a disabled dropdown does not open', async () => {
|
|
159
|
+
const dd = makeDropDown();
|
|
160
|
+
dd.options = ['a', 'b'];
|
|
161
|
+
dd.setAttribute('disabled', '');
|
|
162
|
+
(dd.querySelector('.adw-drop-down-button') as HTMLButtonElement).click();
|
|
163
|
+
expect(dd.active).toBe(false);
|
|
164
|
+
dd.remove();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
await describe('adw-drop-down search', async () => {
|
|
169
|
+
await it('renders a search entry and filters the list', async () => {
|
|
170
|
+
const dd = makeDropDown();
|
|
171
|
+
dd.options = ['Apple', 'Banana', 'Cherry'];
|
|
172
|
+
dd.enableSearch = true;
|
|
173
|
+
const search = dd.querySelector('.adw-drop-down-search') as HTMLInputElement;
|
|
174
|
+
expect(search).toBeTruthy();
|
|
175
|
+
(dd.querySelector('.adw-drop-down-button') as HTMLButtonElement).click();
|
|
176
|
+
search.value = 'ban';
|
|
177
|
+
search.dispatchEvent(new Event('input', { bubbles: true }));
|
|
178
|
+
const visible = Array.from(dd.querySelectorAll('.adw-drop-down-item')).filter(
|
|
179
|
+
(b) => !(b as HTMLElement).hidden,
|
|
180
|
+
);
|
|
181
|
+
expect(visible.length).toBe(1);
|
|
182
|
+
expect(visible[0].textContent).toContain('Banana');
|
|
183
|
+
dd.remove();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
};
|