@gjsify/adwaita-web 0.64.12-next.280 → 0.64.12
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/lib/types/elements/adw-combo-row.d.ts +34 -15
- package/lib/types/elements/gtk-drop-down.d.ts +31 -5
- package/lib/types/namespace/gdk.d.ts +1 -1
- package/lib/types/namespace/gio.d.ts +1 -0
- package/lib/types/namespace/gtk.d.ts +1 -1
- package/lib/types/shared-tree-builder.d.ts +7 -0
- package/package.json +12 -13
- package/src/adw-row-state.spec.ts +120 -63
- package/src/construct-vectors.spec.ts +4 -2
- package/src/elements/adw-combo-row.ts +93 -47
- package/src/elements/gtk-drop-down.ts +111 -35
- package/src/elements/gtk-page-setup-unix-dialog.ts +8 -6
- package/src/file-chooser-host.ts +2 -1
- package/src/gobject-door.spec.ts +57 -0
- package/src/gtk-drop-down.spec.ts +91 -75
- package/src/gtk-value-doors.spec.ts +70 -32
- package/src/namespace/gdk.ts +1 -0
- package/src/namespace/gio.ts +3 -0
- package/src/namespace/gtk.ts +13 -9
- package/src/shared-tree-builder.ts +42 -23
- package/src/value-lists.spec.ts +2 -2
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
// composes the same `ComboState` and drives the same table from its own DOM
|
|
9
9
|
// (`adw-row-state.spec.ts`) — it could not while two of its four step ops had no DOM
|
|
10
10
|
// spelling there, its model arriving through an attribute at connect time only (#1525).
|
|
11
|
-
// The two
|
|
11
|
+
// The two used to part on ONE row — `an index past the end`, which this element rejected
|
|
12
|
+
// and the row accepted — and that carve-out is GONE: ADR 0122 § 4 measured libadwaita,
|
|
13
|
+
// found the rejection right, and the row adopted it. Both selectors now drive every row.
|
|
12
14
|
import { describe, it, expect } from '@gjsify/unit';
|
|
13
15
|
|
|
14
16
|
import {
|
|
@@ -17,9 +19,13 @@ import {
|
|
|
17
19
|
LIST_MODEL_OWNERSHIP_VECTORS,
|
|
18
20
|
LIST_NORMALIZE_VECTORS,
|
|
19
21
|
LIST_PARSE_VECTORS,
|
|
22
|
+
applyListMutation,
|
|
20
23
|
applyListReadback,
|
|
24
|
+
listModelForInput,
|
|
21
25
|
} from '@gjsify/adwaita-core/conformance';
|
|
22
|
-
import type { ComboSelectionStep
|
|
26
|
+
import type { ComboSelectionStep } from '@gjsify/adwaita-core/conformance';
|
|
27
|
+
import { GjsifyOptionList } from '@gjsify/adwaita-core';
|
|
28
|
+
import type { AdwComboOptionInput } from '@gjsify/adwaita-core';
|
|
23
29
|
|
|
24
30
|
import type { GtkDropDown } from './elements/gtk-drop-down.js';
|
|
25
31
|
|
|
@@ -29,6 +35,20 @@ function makeDropDown(): GtkDropDown {
|
|
|
29
35
|
return el;
|
|
30
36
|
}
|
|
31
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Assign a model built out of an authored array.
|
|
40
|
+
*
|
|
41
|
+
* The array itself no longer goes to the property — ADR 0122 § 4 narrowed `model` to a real
|
|
42
|
+
* `Gio.ListModel` — so every test below that is about something OTHER than the door says so
|
|
43
|
+
* in one place. The tests that ARE about the door (`LIST_NORMALIZE_VECTORS` and the array
|
|
44
|
+
* refusal in `gtk-value-doors.spec.ts`) write to `model` directly.
|
|
45
|
+
*/
|
|
46
|
+
function setModel(dd: GtkDropDown, options: readonly AdwComboOptionInput[]): GjsifyOptionList {
|
|
47
|
+
const list = GjsifyOptionList.new(options);
|
|
48
|
+
dd.model = list;
|
|
49
|
+
return list;
|
|
50
|
+
}
|
|
51
|
+
|
|
32
52
|
/** The text the closed button shows — the rendered `selectedLabel`. */
|
|
33
53
|
function labelText(dd: GtkDropDown): string {
|
|
34
54
|
return dd.querySelector('.adw-drop-down-label')?.textContent ?? '';
|
|
@@ -42,12 +62,21 @@ function openChooser(dd: GtkDropDown): void {
|
|
|
42
62
|
/**
|
|
43
63
|
* Replay one vector step against the real element — property sets for the
|
|
44
64
|
* programmatic ops, a popover gesture for the interactive one.
|
|
65
|
+
*
|
|
66
|
+
* Every `setModel` builds a FRESH list, which is what makes `setModelAgain` — assigning the
|
|
67
|
+
* identity the element already holds — a distinct step rather than a repeat of the one above
|
|
68
|
+
* it.
|
|
45
69
|
*/
|
|
46
70
|
function applyStep(dd: GtkDropDown, step: ComboSelectionStep): void {
|
|
47
71
|
switch (step.op) {
|
|
48
72
|
case 'setModel':
|
|
49
|
-
dd
|
|
73
|
+
setModel(dd, [...step.model]);
|
|
74
|
+
return;
|
|
75
|
+
case 'setModelAgain': {
|
|
76
|
+
const held = dd.model;
|
|
77
|
+
dd.model = held;
|
|
50
78
|
return;
|
|
79
|
+
}
|
|
51
80
|
case 'setSelectedIndex':
|
|
52
81
|
dd.selected = step.index;
|
|
53
82
|
return;
|
|
@@ -67,23 +96,6 @@ function applyStep(dd: GtkDropDown, step: ComboSelectionStep): void {
|
|
|
67
96
|
}
|
|
68
97
|
}
|
|
69
98
|
|
|
70
|
-
/**
|
|
71
|
-
* The ONE row this element answers differently, on purpose: its published `selected`
|
|
72
|
-
* contract REJECTS an out-of-range set (`_selectIndex` gates on `ComboState.hasIndex`)
|
|
73
|
-
* where the core state accepts it and reports an empty value — the split
|
|
74
|
-
* `ComboState.hasIndex`'s doc records and `<adw-combo-row>` takes the other side of. The
|
|
75
|
-
* row is still driven below, against the element's answer, so neither policy can change
|
|
76
|
-
* silently.
|
|
77
|
-
*/
|
|
78
|
-
const DROP_DOWN_REJECTS = 'an index past the end';
|
|
79
|
-
|
|
80
|
-
/** The named vector — a hard failure if the table renamed it out from under the carve-out. */
|
|
81
|
-
function vectorNamed(name: string): ComboSelectionVector {
|
|
82
|
-
const vector = COMBO_SELECTION_VECTORS.find((v) => v.name === name);
|
|
83
|
-
if (!vector) throw new Error(`COMBO_SELECTION_VECTORS has no row "${name}" — this spec's carve-out is stale`);
|
|
84
|
-
return vector;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
99
|
/** Mount a drop-down and record every `change` detail it dispatches. */
|
|
88
100
|
function mountRecording(): { dd: GtkDropDown; changes: unknown[] } {
|
|
89
101
|
const dd = makeDropDown();
|
|
@@ -95,8 +107,6 @@ function mountRecording(): { dd: GtkDropDown; changes: unknown[] } {
|
|
|
95
107
|
export const GtkDropDownTest = async () => {
|
|
96
108
|
await describe('gtk-drop-down selection (libadwaita conformance vectors)', async () => {
|
|
97
109
|
for (const vector of COMBO_SELECTION_VECTORS) {
|
|
98
|
-
if (vector.name === DROP_DOWN_REJECTS) continue;
|
|
99
|
-
|
|
100
110
|
await it(`${vector.name} — ${vector.rule}`, async () => {
|
|
101
111
|
const { dd, changes } = mountRecording();
|
|
102
112
|
for (const step of vector.steps) applyStep(dd, step);
|
|
@@ -115,43 +125,24 @@ export const GtkDropDownTest = async () => {
|
|
|
115
125
|
dd.remove();
|
|
116
126
|
});
|
|
117
127
|
}
|
|
118
|
-
|
|
119
|
-
await it(`${DROP_DOWN_REJECTS} — rejected here: the element's published contract, not the state's`, async () => {
|
|
120
|
-
const vector = vectorNamed(DROP_DOWN_REJECTS);
|
|
121
|
-
const { dd, changes } = mountRecording();
|
|
122
|
-
|
|
123
|
-
for (const step of vector.steps.slice(0, -1)) applyStep(dd, step);
|
|
124
|
-
const before = { selected: dd.selected, value: dd.selectedValue, label: labelText(dd) };
|
|
125
|
-
// The out-of-range set itself, which the core state would accept and report
|
|
126
|
-
// as nothing-to-draw (`value`/`label` both empty below).
|
|
127
|
-
expect(vector.value).toBe('');
|
|
128
|
-
expect(vector.label).toBe('');
|
|
129
|
-
applyStep(dd, vector.steps[vector.steps.length - 1]);
|
|
130
|
-
|
|
131
|
-
// The element refused it: the selection did not move, and a refusal
|
|
132
|
-
// is not a change, so nothing was dispatched either.
|
|
133
|
-
expect({ selected: dd.selected, value: dd.selectedValue, label: labelText(dd) }).toStrictEqual(before);
|
|
134
|
-
expect(changes).toStrictEqual([]);
|
|
135
|
-
dd.remove();
|
|
136
|
-
});
|
|
137
128
|
});
|
|
138
129
|
|
|
139
130
|
await describe('gtk-drop-down model', async () => {
|
|
140
131
|
await it('accepts a string[] (value === label)', async () => {
|
|
141
132
|
const dd = makeDropDown();
|
|
142
|
-
dd
|
|
143
|
-
expect(dd.
|
|
144
|
-
expect(dd.
|
|
133
|
+
setModel(dd, ['One', 'Two', 'Three']);
|
|
134
|
+
expect(dd.options.length).toBe(3);
|
|
135
|
+
expect(dd.options[0]).toStrictEqual({ value: 'One', label: 'One' });
|
|
145
136
|
expect(dd.querySelector('.adw-drop-down-label')?.textContent).toBe('One');
|
|
146
137
|
dd.remove();
|
|
147
138
|
});
|
|
148
139
|
|
|
149
140
|
await it('accepts {value,label}[] and shows the selected label', async () => {
|
|
150
141
|
const dd = makeDropDown();
|
|
151
|
-
dd
|
|
142
|
+
setModel(dd, [
|
|
152
143
|
{ value: 'a', label: 'Apple' },
|
|
153
144
|
{ value: 'b', label: 'Banana' },
|
|
154
|
-
];
|
|
145
|
+
]);
|
|
155
146
|
dd.selected = 1;
|
|
156
147
|
expect(dd.querySelector('.adw-drop-down-label')?.textContent).toBe('Banana');
|
|
157
148
|
expect(dd.selectedValue).toBe('b');
|
|
@@ -163,7 +154,7 @@ export const GtkDropDownTest = async () => {
|
|
|
163
154
|
dd.setAttribute('model', '[{"value":"x","label":"Ex"},{"value":"y","label":"Why"}]');
|
|
164
155
|
dd.setAttribute('selected', '1');
|
|
165
156
|
document.body.appendChild(dd);
|
|
166
|
-
expect(dd.
|
|
157
|
+
expect(dd.options.length).toBe(2);
|
|
167
158
|
expect(dd.selected).toBe(1);
|
|
168
159
|
expect(dd.selectedValue).toBe('y');
|
|
169
160
|
dd.remove();
|
|
@@ -171,7 +162,7 @@ export const GtkDropDownTest = async () => {
|
|
|
171
162
|
|
|
172
163
|
await it('renders one popover item per option', async () => {
|
|
173
164
|
const dd = makeDropDown();
|
|
174
|
-
dd
|
|
165
|
+
setModel(dd, ['a', 'b', 'c']);
|
|
175
166
|
expect(dd.querySelectorAll('.adw-drop-down-item').length).toBe(3);
|
|
176
167
|
dd.remove();
|
|
177
168
|
});
|
|
@@ -179,8 +170,10 @@ export const GtkDropDownTest = async () => {
|
|
|
179
170
|
for (const vector of LIST_NORMALIZE_VECTORS) {
|
|
180
171
|
await it(`normalize — ${vector.rule}`, async () => {
|
|
181
172
|
const dd = makeDropDown();
|
|
182
|
-
|
|
183
|
-
|
|
173
|
+
// `listModelForInput` is the table's own answer to which door a row reaches
|
|
174
|
+
// through now that `model` takes no array (ADR 0122 § 4).
|
|
175
|
+
dd.model = listModelForInput(vector.input);
|
|
176
|
+
expect(dd.options).toStrictEqual([...vector.model]);
|
|
184
177
|
expect(
|
|
185
178
|
[...dd.querySelectorAll('.adw-drop-down-item-label')].map((label) => label.textContent),
|
|
186
179
|
).toStrictEqual(vector.model.map((item) => item.label));
|
|
@@ -198,21 +191,26 @@ export const GtkDropDownTest = async () => {
|
|
|
198
191
|
// Set AFTER connect, so the attribute reaches the callback rather than the
|
|
199
192
|
// seeding path: a total parser has to be total on the live door too.
|
|
200
193
|
dd.setAttribute('model', vector.attribute);
|
|
201
|
-
expect(dd.
|
|
194
|
+
expect(dd.options).toStrictEqual([...vector.model]);
|
|
202
195
|
dd.remove();
|
|
203
196
|
});
|
|
204
197
|
}
|
|
205
198
|
|
|
206
|
-
// The half no pure test reaches: a
|
|
199
|
+
// The half no pure test reaches: a markup update must SPLICE the row buttons, not
|
|
207
200
|
// rebuild them. Identity is what says which happened — and it is what a user feels,
|
|
208
201
|
// because a rebuilt row loses its focus and the popover its scroll position.
|
|
202
|
+
//
|
|
203
|
+
// Driven through the ATTRIBUTE, which is the door that still computes a splice: it
|
|
204
|
+
// reconciles the element's own authored list (ADR 0122 amendment A4) and so keeps its
|
|
205
|
+
// identity. A `model` PROPERTY write cannot answer these rows any more — a different
|
|
206
|
+
// list is a replacement, and a replacement carries nothing over by definition.
|
|
209
207
|
for (const vector of LIST_ITEMS_CHANGED_VECTORS) {
|
|
210
208
|
await it(`items-changed — ${vector.rule}`, async () => {
|
|
211
209
|
const dd = makeDropDown();
|
|
212
|
-
dd.model
|
|
210
|
+
dd.setAttribute('model', JSON.stringify(vector.previous));
|
|
213
211
|
const before = [...dd.querySelectorAll('.adw-drop-down-item')];
|
|
214
212
|
|
|
215
|
-
dd.model
|
|
213
|
+
dd.setAttribute('model', JSON.stringify(vector.next));
|
|
216
214
|
|
|
217
215
|
const after = [...dd.querySelectorAll('.adw-drop-down-item')];
|
|
218
216
|
expect(after.map((item) => item.querySelector('.adw-drop-down-item-label')?.textContent)).toStrictEqual(
|
|
@@ -223,19 +221,23 @@ export const GtkDropDownTest = async () => {
|
|
|
223
221
|
});
|
|
224
222
|
}
|
|
225
223
|
|
|
226
|
-
// WHO OWNS the
|
|
227
|
-
// the reason `adw-row-state.spec.ts` gives
|
|
224
|
+
// WHO OWNS the options either side of the model, and what the model's own API moves
|
|
225
|
+
// — the row buttons are what shows it, for the reason `adw-row-state.spec.ts` gives
|
|
226
|
+
// on the same table.
|
|
228
227
|
for (const vector of LIST_MODEL_OWNERSHIP_VECTORS) {
|
|
229
228
|
await it(`ownership — ${vector.rule}`, async () => {
|
|
230
229
|
const dd = makeDropDown();
|
|
231
230
|
const input = vector.initial.map((item) => ({ ...item }));
|
|
232
|
-
|
|
231
|
+
const list = setModel(dd, input);
|
|
233
232
|
const before = [...dd.querySelectorAll('.adw-drop-down-item')];
|
|
234
233
|
|
|
235
|
-
|
|
236
|
-
|
|
234
|
+
if (vector.source === 'model') applyListMutation(list, vector.op);
|
|
235
|
+
else applyListReadback(vector.source === 'input' ? input : dd.options, vector.op);
|
|
237
236
|
|
|
238
|
-
expect(dd.
|
|
237
|
+
expect(dd.options).toStrictEqual([...vector.model]);
|
|
238
|
+
// None of these ops is a replacement, so the element must still be pointing
|
|
239
|
+
// at the list it was handed.
|
|
240
|
+
expect(dd.model).toBe(list);
|
|
239
241
|
const after = [...dd.querySelectorAll('.adw-drop-down-item')];
|
|
240
242
|
expect(after.map((item) => item.querySelector('.adw-drop-down-item-label')?.textContent)).toStrictEqual(
|
|
241
243
|
vector.model.map((item) => item.label),
|
|
@@ -250,9 +252,10 @@ export const GtkDropDownTest = async () => {
|
|
|
250
252
|
// not, so the handler asks the list where the row is now.
|
|
251
253
|
await it('a row that a splice shifted still selects itself', async () => {
|
|
252
254
|
const dd = makeDropDown();
|
|
253
|
-
|
|
255
|
+
// Through the attribute door, the one that splices — see the items-changed loop.
|
|
256
|
+
dd.setAttribute('model', '["b","c"]');
|
|
254
257
|
const rowForB = dd.querySelectorAll('.adw-drop-down-item')[0] as HTMLButtonElement;
|
|
255
|
-
dd.model
|
|
258
|
+
dd.setAttribute('model', '["a","b","c"]');
|
|
256
259
|
expect(dd.querySelectorAll('.adw-drop-down-item')[1]).toBe(rowForB);
|
|
257
260
|
|
|
258
261
|
(dd.querySelector('.adw-drop-down-button') as HTMLButtonElement).click();
|
|
@@ -265,11 +268,11 @@ export const GtkDropDownTest = async () => {
|
|
|
265
268
|
await describe('gtk-drop-down selected ↔ value sync', async () => {
|
|
266
269
|
await it('selectedValue setter finds the index by value', async () => {
|
|
267
270
|
const dd = makeDropDown();
|
|
268
|
-
dd
|
|
271
|
+
setModel(dd, [
|
|
269
272
|
{ value: 'r', label: 'Red' },
|
|
270
273
|
{ value: 'g', label: 'Green' },
|
|
271
274
|
{ value: 'b', label: 'Blue' },
|
|
272
|
-
];
|
|
275
|
+
]);
|
|
273
276
|
dd.selectedValue = 'b';
|
|
274
277
|
expect(dd.selected).toBe(2);
|
|
275
278
|
expect(dd.querySelector('.adw-drop-down-label')?.textContent).toBe('Blue');
|
|
@@ -278,7 +281,7 @@ export const GtkDropDownTest = async () => {
|
|
|
278
281
|
|
|
279
282
|
await it('get_selected / set_selected are the GTK spelling of `selected`', async () => {
|
|
280
283
|
const dd = makeDropDown();
|
|
281
|
-
dd
|
|
284
|
+
setModel(dd, ['a', 'b', 'c']);
|
|
282
285
|
(dd as unknown as { set_selected(n: number): void }).set_selected(2);
|
|
283
286
|
expect(dd.selected).toBe(2);
|
|
284
287
|
expect((dd as unknown as { get_selected(): number }).get_selected()).toBe(2);
|
|
@@ -289,7 +292,7 @@ export const GtkDropDownTest = async () => {
|
|
|
289
292
|
|
|
290
293
|
await it('selected reflects to the attribute + marks the row', async () => {
|
|
291
294
|
const dd = makeDropDown();
|
|
292
|
-
dd
|
|
295
|
+
setModel(dd, ['a', 'b']);
|
|
293
296
|
dd.selected = 1;
|
|
294
297
|
expect(dd.getAttribute('selected')).toBe('1');
|
|
295
298
|
const items = dd.querySelectorAll('.adw-drop-down-item');
|
|
@@ -298,10 +301,23 @@ export const GtkDropDownTest = async () => {
|
|
|
298
301
|
dd.remove();
|
|
299
302
|
});
|
|
300
303
|
|
|
301
|
-
|
|
304
|
+
// `selected-item` is the MODEL'S OWN item, by identity (ADR 0122 § 4) — not a
|
|
305
|
+
// `{ value, label }` descriptor, which is what this read used to hand back. The
|
|
306
|
+
// identity is the point: it is what every `selected-item` consumer compares.
|
|
307
|
+
await it('selectedItem is the model’s own item, by identity', async () => {
|
|
308
|
+
const dd = makeDropDown();
|
|
309
|
+
const list = setModel(dd, [{ value: 'k', label: 'Key' }]);
|
|
310
|
+
expect(dd.selectedItem).toBe(list.get_item(0));
|
|
311
|
+
expect((dd as unknown as { get_selected_item(): unknown }).get_selected_item()).toBe(list.get_item(0));
|
|
312
|
+
dd.remove();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await it('selectedItem is null with no model and with no selection', async () => {
|
|
302
316
|
const dd = makeDropDown();
|
|
303
|
-
dd.model
|
|
304
|
-
expect(dd.selectedItem).
|
|
317
|
+
expect(dd.model).toBeNull();
|
|
318
|
+
expect(dd.selectedItem).toBeNull();
|
|
319
|
+
setModel(dd, []);
|
|
320
|
+
expect(dd.selectedItem).toBeNull();
|
|
305
321
|
dd.remove();
|
|
306
322
|
});
|
|
307
323
|
|
|
@@ -309,7 +325,7 @@ export const GtkDropDownTest = async () => {
|
|
|
309
325
|
// The property setters can run BEFORE connectedCallback builds the DOM, where
|
|
310
326
|
// _updateLabel would touch an undefined label element.
|
|
311
327
|
const dd = document.createElement('gtk-drop-down') as GtkDropDown;
|
|
312
|
-
dd
|
|
328
|
+
setModel(dd, ['a', 'b', 'c']);
|
|
313
329
|
dd.selected = 2;
|
|
314
330
|
expect(dd.isConnected).toBe(false);
|
|
315
331
|
document.body.appendChild(dd);
|
|
@@ -323,7 +339,7 @@ export const GtkDropDownTest = async () => {
|
|
|
323
339
|
await describe('gtk-drop-down notify-on-change', async () => {
|
|
324
340
|
await it('a programmatic set fires notify::selected but NOT change (DOM <select> semantics)', async () => {
|
|
325
341
|
const dd = makeDropDown();
|
|
326
|
-
dd
|
|
342
|
+
setModel(dd, ['a', 'b', 'c']);
|
|
327
343
|
let notifyCount = 0;
|
|
328
344
|
let changeCount = 0;
|
|
329
345
|
dd.addEventListener('notify::selected', () => notifyCount++);
|
|
@@ -346,7 +362,7 @@ export const GtkDropDownTest = async () => {
|
|
|
346
362
|
|
|
347
363
|
await it('a popover item click selects, fires change + closes', async () => {
|
|
348
364
|
const dd = makeDropDown();
|
|
349
|
-
dd
|
|
365
|
+
setModel(dd, ['a', 'b', 'c']);
|
|
350
366
|
let change: unknown = null;
|
|
351
367
|
dd.addEventListener('change', (e) => {
|
|
352
368
|
change = (e as CustomEvent).detail;
|
|
@@ -365,7 +381,7 @@ export const GtkDropDownTest = async () => {
|
|
|
365
381
|
await describe('gtk-drop-down popover behaviour', async () => {
|
|
366
382
|
await it('opens on button click and closes on outside pointerdown', async () => {
|
|
367
383
|
const dd = makeDropDown();
|
|
368
|
-
dd
|
|
384
|
+
setModel(dd, ['a', 'b']);
|
|
369
385
|
const button = dd.querySelector('.adw-drop-down-button') as HTMLButtonElement;
|
|
370
386
|
const popover = dd.querySelector('.adw-drop-down-popover') as HTMLElement;
|
|
371
387
|
expect(popover.hidden).toBe(true);
|
|
@@ -381,7 +397,7 @@ export const GtkDropDownTest = async () => {
|
|
|
381
397
|
|
|
382
398
|
await it('a disabled dropdown does not open', async () => {
|
|
383
399
|
const dd = makeDropDown();
|
|
384
|
-
dd
|
|
400
|
+
setModel(dd, ['a', 'b']);
|
|
385
401
|
dd.setAttribute('disabled', '');
|
|
386
402
|
(dd.querySelector('.adw-drop-down-button') as HTMLButtonElement).click();
|
|
387
403
|
expect(dd.active).toBe(false);
|
|
@@ -392,7 +408,7 @@ export const GtkDropDownTest = async () => {
|
|
|
392
408
|
await describe('gtk-drop-down search', async () => {
|
|
393
409
|
await it('renders a search entry and filters the list', async () => {
|
|
394
410
|
const dd = makeDropDown();
|
|
395
|
-
dd
|
|
411
|
+
setModel(dd, ['Apple', 'Banana', 'Cherry']);
|
|
396
412
|
dd.enableSearch = true;
|
|
397
413
|
const search = dd.querySelector('.adw-drop-down-search') as HTMLInputElement;
|
|
398
414
|
expect(search).toBeTruthy();
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
// `Gtk.StringList` and `Gtk.Adjustment` AT THE ELEMENT, held against the spelling they
|
|
2
2
|
// replace.
|
|
3
3
|
//
|
|
4
|
-
// THE CLAIM UNDER TEST is
|
|
5
|
-
// cannot make
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// `el.model = new Gtk.StringList({ strings: ['Blue', 'Teal'] })` leave `<adw-combo-row>` in
|
|
9
|
-
// the same state, down to the `<option>` nodes it renders, and likewise for the two
|
|
10
|
-
// adjustment spellings on `<adw-spin-row>`. Four spellings, each asserted against what the
|
|
11
|
-
// element shows.
|
|
4
|
+
// THE CLAIM UNDER TEST is what each property's value IS at the ELEMENT, and it is the one
|
|
5
|
+
// claim a core-side spec cannot make: `@gjsify/adwaita-core` proves what the two classes
|
|
6
|
+
// hold, what a reader needs to know is what the element does with them — down to the
|
|
7
|
+
// `<option>` nodes it renders.
|
|
12
8
|
//
|
|
13
|
-
// The
|
|
9
|
+
// The two properties answer DIFFERENTLY, which is why both are here. `adjustment` is still a
|
|
10
|
+
// convenience door: `el.adjustment = FONT_SIZE` and `el.adjustment = new Gtk.Adjustment(…)`
|
|
11
|
+
// leave `<adw-spin-row>` in the same state, and the object literal is the shorthand. `model`
|
|
12
|
+
// is not, since ADR 0122: it is an object property over `Gio.ListModel`, it holds the list BY
|
|
13
|
+
// IDENTITY, and `el.model = ['Blue', 'Teal']` is a `TypeError` naming the type rather than a
|
|
14
|
+
// shorthand for one. The array door it replaces is asserted here as the refusal it became —
|
|
15
|
+
// this file is where a reader looking for `el.model = [...]` arrives, so this is where the
|
|
16
|
+
// answer belongs. The `model` ATTRIBUTE is a separate, untouched door; `value-lists.spec.ts`
|
|
17
|
+
// and `adw-row-state.spec.ts` hold that one.
|
|
18
|
+
//
|
|
19
|
+
// The NativeScript port asserts the same doors at its own widget seam
|
|
14
20
|
// (`gtk-value-doors.spec.ts` there), so the two renderers cannot part on this without one
|
|
15
21
|
// of them failing.
|
|
16
22
|
//
|
|
@@ -43,24 +49,29 @@ function mount<T extends HTMLElement>(tag: string): T {
|
|
|
43
49
|
|
|
44
50
|
/** Everything a `model` write leaves behind, the rendered options included. */
|
|
45
51
|
const modelState = (el: AdwComboRow | GtkDropDown, options: string) =>
|
|
46
|
-
JSON.stringify({
|
|
52
|
+
JSON.stringify({ options: el.options, selected: el.selected, value: el.selectedValue, painted: options });
|
|
47
53
|
|
|
48
54
|
/** The option labels the row actually paints, which is what a reader sees. */
|
|
49
55
|
const comboOptions = (el: HTMLElement) =>
|
|
50
56
|
[...el.querySelectorAll('option, .adw-drop-down-item-label')].map((node) => node.textContent).join('|');
|
|
51
57
|
|
|
52
58
|
export const GtkValueDoorsTest = async () => {
|
|
53
|
-
await describe('<adw-combo-row
|
|
54
|
-
await it('
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
await describe('<adw-combo-row>.model takes a Gtk.StringList and refuses an array', async () => {
|
|
60
|
+
await it('holds the list by identity and paints what it holds', () => {
|
|
61
|
+
const list = new GtkStringList({ strings: COLOURS });
|
|
62
|
+
const row = mount<AdwComboRow>('adw-combo-row');
|
|
63
|
+
row.model = list;
|
|
64
|
+
row.selected = 1;
|
|
65
|
+
|
|
66
|
+
expect(row.model === list).toBe(true);
|
|
67
|
+
expect(modelState(row, comboOptions(row))).toBe(
|
|
68
|
+
JSON.stringify({
|
|
69
|
+
options: COLOURS.map((colour) => ({ value: colour, label: colour })),
|
|
70
|
+
selected: 1,
|
|
71
|
+
value: 'Teal',
|
|
72
|
+
painted: COLOURS.join('|'),
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
64
75
|
});
|
|
65
76
|
|
|
66
77
|
await it('follows a list that was appended to before it was assigned', () => {
|
|
@@ -72,19 +83,46 @@ export const GtkValueDoorsTest = async () => {
|
|
|
72
83
|
|
|
73
84
|
expect(row.selectedValue).toBe('Teal');
|
|
74
85
|
});
|
|
86
|
+
|
|
87
|
+
await it('follows a list appended to WHILE the row holds it', () => {
|
|
88
|
+
const strings = new GtkStringList({ strings: ['Blue'] });
|
|
89
|
+
const row = mount<AdwComboRow>('adw-combo-row');
|
|
90
|
+
row.model = strings;
|
|
91
|
+
strings.append('Teal');
|
|
92
|
+
row.selected = 1;
|
|
93
|
+
|
|
94
|
+
expect(row.selectedValue).toBe('Teal');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// The array door this file used to assert, as the refusal it became: `model` is an
|
|
98
|
+
// object property over `Gio.ListModel`, so the message is the property's own.
|
|
99
|
+
await it('refuses a plain array by naming the type', () => {
|
|
100
|
+
const row = mount<AdwComboRow>('adw-combo-row');
|
|
101
|
+
|
|
102
|
+
expect(() => {
|
|
103
|
+
(row as unknown as { model: unknown }).model = COLOURS;
|
|
104
|
+
}).toThrow("Object is not a subclass of GObject_Object, it's a Array");
|
|
105
|
+
});
|
|
75
106
|
});
|
|
76
107
|
|
|
77
|
-
await describe('<gtk-drop-down
|
|
78
|
-
await it('
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
expect(
|
|
87
|
-
|
|
108
|
+
await describe('<gtk-drop-down>.model takes a Gtk.StringList and refuses an array', async () => {
|
|
109
|
+
await it('holds the list by identity and paints what it holds', () => {
|
|
110
|
+
const list = new GtkStringList({ strings: COLOURS });
|
|
111
|
+
const dropDown = mount<GtkDropDown>('gtk-drop-down');
|
|
112
|
+
dropDown.model = list;
|
|
113
|
+
dropDown.selected = 2;
|
|
114
|
+
|
|
115
|
+
expect(dropDown.model === list).toBe(true);
|
|
116
|
+
expect(dropDown.selectedValue).toBe('Green');
|
|
117
|
+
expect(comboOptions(dropDown)).toBe(COLOURS.join('|'));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await it('refuses a plain array by naming the type', () => {
|
|
121
|
+
const dropDown = mount<GtkDropDown>('gtk-drop-down');
|
|
122
|
+
|
|
123
|
+
expect(() => {
|
|
124
|
+
(dropDown as unknown as { model: unknown }).model = COLOURS;
|
|
125
|
+
}).toThrow("Object is not a subclass of GObject_Object, it's a Array");
|
|
88
126
|
});
|
|
89
127
|
});
|
|
90
128
|
|
package/src/namespace/gdk.ts
CHANGED
package/src/namespace/gio.ts
CHANGED
|
@@ -29,6 +29,9 @@ import { createAppInfo } from '@gjsify/adwaita-core';
|
|
|
29
29
|
import { webPlatformHost } from '../platform-host.js';
|
|
30
30
|
|
|
31
31
|
export { GioMenu as Menu, GioMenuItem as MenuItem } from '@gjsify/adwaita-core';
|
|
32
|
+
// ADR 0122 § 2: the interface a `Gtk.StringList` implements and an `Implements: [Gio.ListModel]`
|
|
33
|
+
// names. It is a token, not a class — the whole of the subset's interface support.
|
|
34
|
+
export { ListModel } from '@gjsify/adwaita-core';
|
|
32
35
|
export {
|
|
33
36
|
GioFile as File,
|
|
34
37
|
GioListStore as ListStore,
|
package/src/namespace/gtk.ts
CHANGED
|
@@ -11,14 +11,18 @@
|
|
|
11
11
|
// which is what `<adw-radio>`'s own `why` in the ledger says. So the plain form takes the
|
|
12
12
|
// GIR name and the grouped one keeps its flat export in `src/index.ts`.
|
|
13
13
|
//
|
|
14
|
-
//
|
|
15
|
-
// `adjustment` and a `model` property take.
|
|
16
|
-
// clause-2 namespace carry them,
|
|
17
|
-
// where each one is declared, and the
|
|
18
|
-
// `generated/value-types.mts`. They are
|
|
19
|
-
// where the portable list and adjustment
|
|
20
|
-
//
|
|
21
|
-
//
|
|
14
|
+
// THREE MEMBERS HERE ARE NOT WIDGETS — `Gtk.Adjustment`, `Gtk.StringList` and the
|
|
15
|
+
// `Gtk.StringObject` the list holds, the values an `adjustment` and a `model` property take.
|
|
16
|
+
// ADR 0034 § Amendment 19 is what lets a clause-2 namespace carry them,
|
|
17
|
+
// `CONSTRUCTIBLE_VALUES` in `scripts/value-types.mjs` is where each one is declared, and the
|
|
18
|
+
// GIR answers for all three in gtk-host's committed `generated/value-types.mts`. They are
|
|
19
|
+
// re-exported from `@gjsify/adwaita-core`, which is where the portable list and adjustment
|
|
20
|
+
// already live.
|
|
21
|
+
//
|
|
22
|
+
// `Gtk.StringList` IS A LIST MODEL, NOT AN ARRAY (ADR 0122). The first version of it was an
|
|
23
|
+
// `Array` subclass, which made `model: ['a','b']` and `model: new Gtk.StringList(…)` the same
|
|
24
|
+
// write; it is now what GTK ships — `Implements: [Gio.ListModel]`, items that are
|
|
25
|
+
// `Gtk.StringObject`s, read by `get_n_items` / `get_item` and never by `length` or `[0]`.
|
|
22
26
|
|
|
23
27
|
export { GtkAdjustment as Adjustment } from '@gjsify/adwaita-core';
|
|
24
28
|
// ADR 0100: the file dialog is no element either — `present`-less, it asks the driver
|
|
@@ -89,7 +93,7 @@ export { GtkStack as Stack } from '../elements/gtk-stack.js';
|
|
|
89
93
|
export { GtkStackPage as StackPage } from '../elements/gtk-stack-page.js';
|
|
90
94
|
export { GtkStackSidebar as StackSidebar } from '../elements/gtk-stack-sidebar.js';
|
|
91
95
|
export { GtkStackSwitcher as StackSwitcher } from '../elements/gtk-stack-switcher.js';
|
|
92
|
-
export { GtkStringList as StringList } from '@gjsify/adwaita-core';
|
|
96
|
+
export { GtkStringList as StringList, GtkStringObject as StringObject } from '@gjsify/adwaita-core';
|
|
93
97
|
export { GtkSwitch as Switch } from '../elements/gtk-switch.js';
|
|
94
98
|
export { GtkText as Text } from '../elements/gtk-text.js';
|
|
95
99
|
export { GtkTextView as TextView } from '../elements/gtk-text-view.js';
|