@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AdwComboOption,
|
|
1
|
+
import type { AdwComboOption, ComboWatchableModel } from '@gjsify/adwaita-core';
|
|
2
2
|
import { TooltipElement } from '../widget-element.js';
|
|
3
3
|
export declare class AdwComboRow extends TooltipElement {
|
|
4
4
|
private _select;
|
|
@@ -9,19 +9,36 @@ export declare class AdwComboRow extends TooltipElement {
|
|
|
9
9
|
private _subtitleEl;
|
|
10
10
|
/** The headless options list + selectedIndex↔selectedValue state machine (ADR 0004). */
|
|
11
11
|
private readonly _state;
|
|
12
|
+
/**
|
|
13
|
+
* The list the MARKUP doors fill — the `model` ATTRIBUTE is JSON text and a
|
|
14
|
+
* `<gtk-string-list slot="model">` child is strings, and a real `Gio.ListModel` has no
|
|
15
|
+
* spelling in either (ADR 0122 § 5). Held for the element's whole life so re-parsing the
|
|
16
|
+
* attribute splices the list the element already points at rather than replacing it, which
|
|
17
|
+
* would reset the selection.
|
|
18
|
+
*/
|
|
19
|
+
private readonly _authored;
|
|
12
20
|
private _initialized;
|
|
13
21
|
static get observedAttributes(): string[];
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
/**
|
|
23
|
+
* The list model (`Adw.ComboRow:model`) — the OBJECT that was assigned, by identity, as
|
|
24
|
+
* `row.model === model` is measured to hold in libadwaita (ADR 0122 § 4). The normalised
|
|
25
|
+
* descriptors are {@link options}. Assigning splices the `<select>` and re-runs autoselect.
|
|
26
|
+
*/
|
|
27
|
+
get model(): ComboWatchableModel | null;
|
|
28
|
+
set model(value: ComboWatchableModel | string | null);
|
|
29
|
+
/** The held model's items as `{ value, label }` descriptors. */
|
|
30
|
+
get options(): AdwComboOption[];
|
|
17
31
|
/** `adw_combo_row_set_model`. */
|
|
18
|
-
set_model(model:
|
|
32
|
+
set_model(model: ComboWatchableModel | null): void;
|
|
33
|
+
/** `adw_combo_row_get_model`. */
|
|
34
|
+
get_model(): ComboWatchableModel | null;
|
|
19
35
|
/**
|
|
20
|
-
* The selected index (Adw.ComboRow:selected).
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
36
|
+
* The selected index (Adw.ComboRow:selected). An index past the end is IGNORED — the
|
|
37
|
+
* selection stays where it was, which is what libadwaita is measured to do (ADR 0122 § 4,
|
|
38
|
+
* `combo-probe2.js`) and what `<gtk-drop-down>` already did. This element used to ACCEPT
|
|
39
|
+
* such a set and read back an empty {@link selectedValue}, the one row of
|
|
40
|
+
* `COMBO_SELECTION_VECTORS` the two selectors answered differently; the permissive half
|
|
41
|
+
* was ours, not GTK's, and it is gone. `ComboState.hasIndex` owns the bounds.
|
|
25
42
|
*/
|
|
26
43
|
get selected(): number;
|
|
27
44
|
set selected(value: number);
|
|
@@ -38,13 +55,15 @@ export declare class AdwComboRow extends TooltipElement {
|
|
|
38
55
|
get selectedValue(): string;
|
|
39
56
|
set selectedValue(value: string);
|
|
40
57
|
/**
|
|
41
|
-
* The selected
|
|
58
|
+
* The selected ITEM, or `null` (Adw.ComboRow:selected-item).
|
|
42
59
|
*
|
|
43
|
-
* Read-only, as the GObject property is
|
|
44
|
-
*
|
|
45
|
-
*
|
|
60
|
+
* Read-only, as the GObject property is, and the MODEL'S OWN object rather than a
|
|
61
|
+
* descriptor, so the identity comparison a `selected-item` consumer writes holds (ADR
|
|
62
|
+
* 0122 § 4). `null` and not a blank item: the sentinel and an index past the end both
|
|
63
|
+
* address no option, and inventing an empty one would make "nothing is selected"
|
|
64
|
+
* indistinguishable from an option with no text.
|
|
46
65
|
*/
|
|
47
|
-
get selectedItem():
|
|
66
|
+
get selectedItem(): unknown;
|
|
48
67
|
connectedCallback(): void;
|
|
49
68
|
attributeChangedCallback(name: string, _old: string | null, value: string | null): void;
|
|
50
69
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import './gtk-popover.js';
|
|
2
|
-
import type { AdwComboOption,
|
|
2
|
+
import type { AdwComboOption, ComboWatchableModel } from '@gjsify/adwaita-core';
|
|
3
3
|
import type { DispatchedSignals } from '../signals.js';
|
|
4
4
|
import { WidgetElement } from '../widget-element.js';
|
|
5
5
|
/**
|
|
@@ -19,13 +19,31 @@ export declare class GtkDropDown extends WidgetElement {
|
|
|
19
19
|
private _itemButtons;
|
|
20
20
|
/** The headless options list + selectedIndex↔selectedValue machine (ADR 0004). */
|
|
21
21
|
private readonly _state;
|
|
22
|
+
/**
|
|
23
|
+
* The list the MARKUP doors fill — the `model` ATTRIBUTE is JSON text and a
|
|
24
|
+
* `<gtk-string-list slot="model">` child is strings, and a real `Gio.ListModel` has no
|
|
25
|
+
* spelling in either (ADR 0122 § 5). Held for the element's whole life so re-parsing the
|
|
26
|
+
* attribute splices the list the element already points at rather than replacing it, which
|
|
27
|
+
* would reset the selection.
|
|
28
|
+
*/
|
|
29
|
+
private readonly _authored;
|
|
22
30
|
private _initialized;
|
|
23
31
|
private _typeAheadBuffer;
|
|
24
32
|
private _typeAheadTimer;
|
|
25
33
|
static get observedAttributes(): string[];
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
/**
|
|
35
|
+
* The list model (`Gtk.DropDown:model`) — the OBJECT that was assigned, by identity, as
|
|
36
|
+
* `dropdown.model === model` is measured to hold in GTK (ADR 0122 § 4). The normalised
|
|
37
|
+
* descriptors are {@link options}. Assigning splices the popover and re-runs autoselect.
|
|
38
|
+
*/
|
|
39
|
+
get model(): ComboWatchableModel | null;
|
|
40
|
+
set model(value: ComboWatchableModel | string | null);
|
|
41
|
+
/** The held model's items as `{ value, label }` descriptors. */
|
|
42
|
+
get options(): AdwComboOption[];
|
|
43
|
+
/** `gtk_drop_down_set_model`. */
|
|
44
|
+
set_model(model: ComboWatchableModel | null): void;
|
|
45
|
+
/** `gtk_drop_down_get_model`. */
|
|
46
|
+
get_model(): ComboWatchableModel | null;
|
|
29
47
|
/** The selected index. Out-of-range values are ignored (Gtk.DropDown:selected). */
|
|
30
48
|
get selected(): number;
|
|
31
49
|
set selected(value: number);
|
|
@@ -36,7 +54,15 @@ export declare class GtkDropDown extends WidgetElement {
|
|
|
36
54
|
/** The selected option's value, or '' when empty (Gtk.DropDown:selected-item). */
|
|
37
55
|
get selectedValue(): string;
|
|
38
56
|
set selectedValue(value: string);
|
|
39
|
-
|
|
57
|
+
/**
|
|
58
|
+
* The selected ITEM, or `null` (Gtk.DropDown:selected-item).
|
|
59
|
+
*
|
|
60
|
+
* The MODEL'S OWN object rather than a descriptor, so the identity comparison a
|
|
61
|
+
* `selected-item` consumer writes holds (ADR 0122 § 4).
|
|
62
|
+
*/
|
|
63
|
+
get selectedItem(): unknown;
|
|
64
|
+
/** `gtk_drop_down_get_selected_item`. */
|
|
65
|
+
get_selected_item(): unknown;
|
|
40
66
|
get enableSearch(): boolean;
|
|
41
67
|
set enableSearch(value: boolean);
|
|
42
68
|
get active(): boolean;
|
|
@@ -8,4 +8,4 @@ export declare const ContentProvider: Pick<typeof import("@gjsify/adwaita-core")
|
|
|
8
8
|
export declare const ContentFormats: typeof import("@gjsify/adwaita-core").ContentFormats;
|
|
9
9
|
export declare const AppLaunchContext: typeof import("@gjsify/adwaita-core").GdkAppLaunchContext;
|
|
10
10
|
export declare const CURRENT_TIME = 0;
|
|
11
|
-
export declare const KEY_space: 32, KEY_a: 97, KEY_d: 100, KEY_e: 101, KEY_q: 113, KEY_s: 115, KEY_w: 119, KEY_Return: 65293, KEY_Escape: 65307, KEY_Left: 65361, KEY_Up: 65362, KEY_Right: 65363, KEY_Down: 65364;
|
|
11
|
+
export declare const KEY_space: 32, KEY_a: 97, KEY_d: 100, KEY_e: 101, KEY_q: 113, KEY_s: 115, KEY_w: 119, KEY_Return: 65293, KEY_Escape: 65307, KEY_Left: 65361, KEY_Up: 65362, KEY_Right: 65363, KEY_Down: 65364, KEY_Back: 269025062;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import '../file-chooser-host.js';
|
|
2
2
|
export { GioMenu as Menu, GioMenuItem as MenuItem } from '@gjsify/adwaita-core';
|
|
3
|
+
export { ListModel } from '@gjsify/adwaita-core';
|
|
3
4
|
export { GioFile as File, GioListStore as ListStore, GioOutputStream as OutputStream, GioFileOutputStream as FileOutputStream, FileCreateFlags, IOErrorEnum, promisify as _promisify, } from '@gjsify/adwaita-core';
|
|
4
5
|
export { SimpleAction, SimpleActionGroup, ApplicationFlags } from '@gjsify/adwaita-core';
|
|
5
6
|
export { GioResource as Resource, ResourceError, ResourceLookupFlags, resources_enumerate_children, resources_get_info, resources_lookup_data, resources_register, resources_unregister, } from '@gjsify/adwaita-core';
|
|
@@ -65,7 +65,7 @@ export { GtkStack as Stack } from '../elements/gtk-stack.js';
|
|
|
65
65
|
export { GtkStackPage as StackPage } from '../elements/gtk-stack-page.js';
|
|
66
66
|
export { GtkStackSidebar as StackSidebar } from '../elements/gtk-stack-sidebar.js';
|
|
67
67
|
export { GtkStackSwitcher as StackSwitcher } from '../elements/gtk-stack-switcher.js';
|
|
68
|
-
export { GtkStringList as StringList } from '@gjsify/adwaita-core';
|
|
68
|
+
export { GtkStringList as StringList, GtkStringObject as StringObject } from '@gjsify/adwaita-core';
|
|
69
69
|
export { GtkSwitch as Switch } from '../elements/gtk-switch.js';
|
|
70
70
|
export { GtkText as Text } from '../elements/gtk-text.js';
|
|
71
71
|
export { GtkTextView as TextView } from '../elements/gtk-text-view.js';
|
|
@@ -34,6 +34,8 @@ interface BuildRecord {
|
|
|
34
34
|
menus?: SharedTreeNode['menus'];
|
|
35
35
|
/** Present when the tree is the template of a registered class (ADR 0096): handlers and binds go through it. */
|
|
36
36
|
template?: TemplateScope;
|
|
37
|
+
/** A template's handler connections, made once every id exists: `object=` may name a node built later. */
|
|
38
|
+
connects?: (() => void)[];
|
|
37
39
|
}
|
|
38
40
|
/** Calls `onSize` with the host's measured size, now and on every change; returns its disposer. */
|
|
39
41
|
export type SizeSource = (element: Element, onSize: (size: BreakpointSize) => void) => () => void;
|
|
@@ -141,6 +143,11 @@ export interface BuiltTemplateTree {
|
|
|
141
143
|
* to their handlers and binds through `scope`, none appended and nothing written to `host`'s
|
|
142
144
|
* attributes, because `host` is still inside its constructor. `finish` and the caller's own append
|
|
143
145
|
* belong to the first `connectedCallback`.
|
|
146
|
+
*
|
|
147
|
+
* Objects written beside the template (`Adw.AlertDialog dialog { }`, `$Learn learn { }`) are built in
|
|
148
|
+
* the same id scope and named in `objects`, which is what `InternalChildren` and `Children` read —
|
|
149
|
+
* `gtk_widget_class_bind_template_child` reaches any object id of the template's builder. They are
|
|
150
|
+
* not in `children`: a sibling is an independent object, placed by the class or not at all.
|
|
144
151
|
*/
|
|
145
152
|
export declare function buildTemplateTree(tree: SharedTreeNode, scope: TemplateScope, host: HTMLElement): BuiltTemplateTree;
|
|
146
153
|
/** A tree {@link mountSharedTree} built and connected. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/adwaita-web",
|
|
3
|
-
"version": "0.64.12
|
|
3
|
+
"version": "0.64.12",
|
|
4
4
|
"description": "Adwaita/Libadwaita web components for browser targets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -58,14 +58,14 @@
|
|
|
58
58
|
"@codemirror/language": "^6.12.4",
|
|
59
59
|
"@codemirror/state": "^6.7.0",
|
|
60
60
|
"@codemirror/view": "^6.43.4",
|
|
61
|
-
"@gjsify/adwaita-core": "0.64.12
|
|
62
|
-
"@gjsify/adwaita-fonts": "0.64.12
|
|
63
|
-
"@gjsify/adwaita-icons": "0.64.12
|
|
64
|
-
"@gjsify/gtksource-core": "0.64.12
|
|
61
|
+
"@gjsify/adwaita-core": "^0.64.12",
|
|
62
|
+
"@gjsify/adwaita-fonts": "^0.64.12",
|
|
63
|
+
"@gjsify/adwaita-icons": "^0.64.12",
|
|
64
|
+
"@gjsify/gtksource-core": "^0.64.12",
|
|
65
65
|
"@lezer/highlight": "^1.2.3"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
|
-
"@gjsify/app-settings": "0.64.12
|
|
68
|
+
"@gjsify/app-settings": "^0.64.12"
|
|
69
69
|
},
|
|
70
70
|
"peerDependenciesMeta": {
|
|
71
71
|
"@gjsify/app-settings": {
|
|
@@ -73,11 +73,11 @@
|
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@gjsify/app-settings": "0.64.12
|
|
77
|
-
"@gjsify/blueprint": "0.64.12
|
|
78
|
-
"@gjsify/cli": "0.64.12
|
|
79
|
-
"@gjsify/unit": "0.64.12
|
|
80
|
-
"@gjsify/vite-plugin-blueprint": "0.64.12
|
|
76
|
+
"@gjsify/app-settings": "^0.64.12",
|
|
77
|
+
"@gjsify/blueprint": "^0.64.12",
|
|
78
|
+
"@gjsify/cli": "^0.64.12",
|
|
79
|
+
"@gjsify/unit": "^0.64.12",
|
|
80
|
+
"@gjsify/vite-plugin-blueprint": "^0.64.12",
|
|
81
81
|
"sass": "^1.101.0",
|
|
82
82
|
"typescript": "^6.0.3"
|
|
83
83
|
},
|
|
@@ -114,6 +114,5 @@
|
|
|
114
114
|
"bugs": {
|
|
115
115
|
"url": "https://github.com/gjsify/gjsify/issues"
|
|
116
116
|
},
|
|
117
|
-
"homepage": "https://github.com/gjsify/gjsify/tree/main/packages/web/adwaita-web#readme"
|
|
118
|
-
"gitHead": "0a9185d91127072fce6aa54dc126e87e6ecd0c3d"
|
|
117
|
+
"homepage": "https://github.com/gjsify/gjsify/tree/main/packages/web/adwaita-web#readme"
|
|
119
118
|
}
|
|
@@ -13,15 +13,18 @@ import {
|
|
|
13
13
|
ADJUSTMENT_AUTHORED_VECTORS,
|
|
14
14
|
ADJUSTMENT_PARSE_VECTORS,
|
|
15
15
|
COMBO_CHOOSER_VECTORS,
|
|
16
|
+
COMBO_ITEMS_CHANGED_VECTORS,
|
|
16
17
|
COMBO_SELECTION_VECTORS,
|
|
17
18
|
LIST_ITEMS_CHANGED_VECTORS,
|
|
18
19
|
LIST_MODEL_OWNERSHIP_VECTORS,
|
|
19
20
|
LIST_NORMALIZE_VECTORS,
|
|
20
21
|
LIST_PARSE_VECTORS,
|
|
21
|
-
|
|
22
|
+
applyListMutation,
|
|
22
23
|
applyListReadback,
|
|
24
|
+
listModelForInput,
|
|
23
25
|
} from '@gjsify/adwaita-core/conformance';
|
|
24
26
|
import type { ComboSelectionStep } from '@gjsify/adwaita-core/conformance';
|
|
27
|
+
import { GjsifyOptionList } from '@gjsify/adwaita-core';
|
|
25
28
|
|
|
26
29
|
import type { AdwComboRow } from './elements/adw-combo-row.js';
|
|
27
30
|
import type { AdwExpanderRow } from './elements/adw-expander-row.js';
|
|
@@ -51,17 +54,24 @@ function record(el: HTMLElement, event: string): { details: unknown[]; stop: ()
|
|
|
51
54
|
*
|
|
52
55
|
* The row can drive the table at all only because the model and the select-by-value are
|
|
53
56
|
* now published; the conformance header names that as the condition for it inheriting
|
|
54
|
-
* these rows. It drives EVERY row
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
57
|
+
* these rows. It drives EVERY row with no carve-out — `an index past the end` used to be
|
|
58
|
+
* the one the two browser selectors answered differently and ADR 0122 measured it instead,
|
|
59
|
+
* so the permissive half this element carried is gone.
|
|
60
|
+
*
|
|
61
|
+
* Every `setModel` builds a FRESH list, which is what makes `setModelAgain` — assigning the
|
|
62
|
+
* identity the row already holds — a distinct step rather than a repeat of the one above it.
|
|
58
63
|
*/
|
|
59
64
|
function applyComboStep(row: AdwComboRow, step: ComboSelectionStep): void {
|
|
60
65
|
const select = row.querySelector('select') as HTMLSelectElement;
|
|
61
66
|
switch (step.op) {
|
|
62
67
|
case 'setModel':
|
|
63
|
-
row.model = step.model;
|
|
68
|
+
row.model = GjsifyOptionList.new([...step.model]);
|
|
69
|
+
return;
|
|
70
|
+
case 'setModelAgain': {
|
|
71
|
+
const held = row.model;
|
|
72
|
+
row.model = held;
|
|
64
73
|
return;
|
|
74
|
+
}
|
|
65
75
|
case 'setSelectedIndex':
|
|
66
76
|
row.selected = step.index;
|
|
67
77
|
return;
|
|
@@ -155,7 +165,7 @@ export const AdwRowStateTest = async () => {
|
|
|
155
165
|
|
|
156
166
|
// Through the PUBLISHED path a consumer has — the same `model` property
|
|
157
167
|
// `<gtk-drop-down>` publishes, not a reach into the composed state.
|
|
158
|
-
row.model = ['One', 'Two'];
|
|
168
|
+
row.model = GjsifyOptionList.new(['One', 'Two']);
|
|
159
169
|
expect(select.disabled).toBe(false);
|
|
160
170
|
expect(row.dataset.presentsChooser).toBe('true');
|
|
161
171
|
|
|
@@ -177,10 +187,10 @@ export const AdwRowStateTest = async () => {
|
|
|
177
187
|
const value = row.querySelector('.adw-row-value') as HTMLSpanElement;
|
|
178
188
|
expect(select.options.length).toBe(2);
|
|
179
189
|
|
|
180
|
-
// The PROPERTY, with the plain-string vocabulary
|
|
181
|
-
row.model = ['Cyan', 'Magenta', 'Yellow'];
|
|
190
|
+
// The PROPERTY, with the plain-string vocabulary `GjsifyOptionList` accepts.
|
|
191
|
+
row.model = GjsifyOptionList.new(['Cyan', 'Magenta', 'Yellow']);
|
|
182
192
|
expect([...select.options].map((o) => o.textContent).join('|')).toBe('Cyan|Magenta|Yellow');
|
|
183
|
-
expect(row.
|
|
193
|
+
expect(row.options.map((o) => o.label).join('|')).toBe('Cyan|Magenta|Yellow');
|
|
184
194
|
// The model was replaced under an index the new one still has, so autoselect
|
|
185
195
|
// keeps 0 and the inline label has to follow the NEW option at that index.
|
|
186
196
|
expect(value.textContent).toBe('Cyan');
|
|
@@ -205,12 +215,12 @@ export const AdwRowStateTest = async () => {
|
|
|
205
215
|
);
|
|
206
216
|
const select = row.querySelector('select') as HTMLSelectElement;
|
|
207
217
|
|
|
208
|
-
row.model = [
|
|
218
|
+
row.model = GjsifyOptionList.new([
|
|
209
219
|
{ value: 'r', label: 'Red' },
|
|
210
220
|
{ value: 'g', label: 'Green' },
|
|
211
|
-
];
|
|
221
|
+
]);
|
|
212
222
|
expect([...select.options].map((o) => o.textContent).join('|')).toBe('Red|Green');
|
|
213
|
-
expect(row.
|
|
223
|
+
expect(row.options.map((o) => o.value).join('|')).toBe('r|g');
|
|
214
224
|
|
|
215
225
|
// And selection by VALUE, the drop-down's other published setter.
|
|
216
226
|
row.selectedValue = 'g';
|
|
@@ -223,42 +233,50 @@ export const AdwRowStateTest = async () => {
|
|
|
223
233
|
// The row publishes the drop-down's property set apart from `enableSearch` and
|
|
224
234
|
// `active`, which are that element's popover chrome. `selectedItem` is NOT chrome —
|
|
225
235
|
// it is `Adw.ComboRow:selected-item` — so it is here, read-only as the GObject
|
|
226
|
-
// property is, and `null` rather than a blank
|
|
227
|
-
|
|
236
|
+
// property is, and `null` rather than a blank item when nothing is addressed.
|
|
237
|
+
//
|
|
238
|
+
// THE MODEL'S OWN ITEM, by identity (ADR 0122 § 4): `row.selectedItem ===
|
|
239
|
+
// model.get_item(row.selected)`, which is the comparison a `selected-item` consumer
|
|
240
|
+
// writes and the one a normalised descriptor would have failed.
|
|
241
|
+
await it('reads the selected item back by identity, and null when nothing is addressed', async () => {
|
|
228
242
|
const { el: row, host } = parse<AdwComboRow>(
|
|
229
243
|
`<adw-combo-row title="Pick"></adw-combo-row>`,
|
|
230
244
|
'adw-combo-row',
|
|
231
245
|
);
|
|
232
246
|
|
|
247
|
+
// No model at all — `null` and not an item of an empty list.
|
|
248
|
+
expect(row.model).toBeNull();
|
|
233
249
|
expect(row.selectedItem).toBeNull();
|
|
234
250
|
|
|
235
|
-
|
|
251
|
+
const model = GjsifyOptionList.new([
|
|
236
252
|
{ value: 'r', label: 'Red' },
|
|
237
253
|
{ value: 'g', label: 'Green' },
|
|
238
|
-
];
|
|
239
|
-
|
|
254
|
+
]);
|
|
255
|
+
row.model = model;
|
|
256
|
+
expect(row.model === model).toBe(true);
|
|
257
|
+
expect(row.selectedItem === model.get_item(0)).toBe(true);
|
|
240
258
|
row.selected = 1;
|
|
241
|
-
expect(row.selectedItem
|
|
259
|
+
expect(row.selectedItem === model.get_item(1)).toBe(true);
|
|
242
260
|
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
261
|
+
// An index past the end is IGNORED, as measured in libadwaita — the selection
|
|
262
|
+
// stays where it was, and nothing reads empty. This element used to ACCEPT the
|
|
263
|
+
// set and read back blank, the one row of the shared table the two browser
|
|
264
|
+
// selectors answered differently; the permissive half was ours, not GTK's.
|
|
247
265
|
row.selected = 9;
|
|
248
|
-
expect(row.selected).toBe(
|
|
249
|
-
expect(row.
|
|
250
|
-
expect(row.
|
|
251
|
-
expect((row.querySelector('.adw-row-value') as HTMLSpanElement).textContent).toBe('');
|
|
266
|
+
expect(row.selected).toBe(1);
|
|
267
|
+
expect(row.selectedValue).toBe('g');
|
|
268
|
+
expect((row.querySelector('.adw-row-value') as HTMLSpanElement).textContent).toBe('Green');
|
|
252
269
|
|
|
253
270
|
host.remove();
|
|
254
271
|
});
|
|
255
272
|
});
|
|
256
273
|
|
|
257
|
-
// The portable list model (ADR 0046), driven through this row's own `model`
|
|
274
|
+
// The portable list model (ADR 0046), driven through this row's own `model` doors — the
|
|
275
|
+
// property for a real model, the attribute for markup.
|
|
258
276
|
//
|
|
259
277
|
// The ITEMS-CHANGED rows are the ones that could not be asserted anywhere else: they
|
|
260
|
-
// compare `<option>` node IDENTITY across a
|
|
261
|
-
// between splicing the
|
|
278
|
+
// compare `<option>` node IDENTITY across a markup update, which is the difference
|
|
279
|
+
// between splicing the list and rebuilding it — a difference no assertion about the
|
|
262
280
|
// model, the labels or the selection can see. Before the splice landed, every row with
|
|
263
281
|
// a non-zero `survivors` scored 0.
|
|
264
282
|
await describe('adw-combo-row list model (portable list-model vectors)', async () => {
|
|
@@ -268,8 +286,10 @@ export const AdwRowStateTest = async () => {
|
|
|
268
286
|
`<adw-combo-row title="Pick"></adw-combo-row>`,
|
|
269
287
|
'adw-combo-row',
|
|
270
288
|
);
|
|
271
|
-
row
|
|
272
|
-
|
|
289
|
+
// `listModelForInput` is the table's own answer to which door a row reaches
|
|
290
|
+
// through now that `model` takes no array (ADR 0122 § 4).
|
|
291
|
+
row.model = listModelForInput(vector.input);
|
|
292
|
+
expect(row.options).toStrictEqual([...vector.model]);
|
|
273
293
|
const select = row.querySelector('select') as HTMLSelectElement;
|
|
274
294
|
expect([...select.options].map((option) => option.textContent)).toStrictEqual(
|
|
275
295
|
vector.model.map((item) => item.label),
|
|
@@ -287,7 +307,7 @@ export const AdwRowStateTest = async () => {
|
|
|
287
307
|
// Set AFTER connect, so the attribute reaches the callback rather than the
|
|
288
308
|
// seeding path: a total parser has to be total on the live door too.
|
|
289
309
|
row.setAttribute('model', vector.attribute);
|
|
290
|
-
expect(row.
|
|
310
|
+
expect(row.options).toStrictEqual([...vector.model]);
|
|
291
311
|
host.remove();
|
|
292
312
|
});
|
|
293
313
|
}
|
|
@@ -299,10 +319,16 @@ export const AdwRowStateTest = async () => {
|
|
|
299
319
|
'adw-combo-row',
|
|
300
320
|
);
|
|
301
321
|
const select = row.querySelector('select') as HTMLSelectElement;
|
|
302
|
-
|
|
322
|
+
// Driven through the MARKUP door, which is the one that still computes a
|
|
323
|
+
// splice: the attribute reconciles the element's own authored list (ADR 0122
|
|
324
|
+
// amendment A4), keeping its identity, so the survivors these rows count are
|
|
325
|
+
// the `<option>` nodes a minimal splice left alone. A `model` PROPERTY write
|
|
326
|
+
// cannot answer them any more — a different list is a replacement, and a
|
|
327
|
+
// replacement carries nothing over by definition.
|
|
328
|
+
row.setAttribute('model', JSON.stringify(vector.previous));
|
|
303
329
|
const before = [...select.options];
|
|
304
330
|
|
|
305
|
-
row.model
|
|
331
|
+
row.setAttribute('model', JSON.stringify(vector.next));
|
|
306
332
|
|
|
307
333
|
const after = [...select.options];
|
|
308
334
|
expect(after.map((option) => option.textContent)).toStrictEqual(vector.next.map((item) => item.label));
|
|
@@ -311,10 +337,10 @@ export const AdwRowStateTest = async () => {
|
|
|
311
337
|
});
|
|
312
338
|
}
|
|
313
339
|
|
|
314
|
-
// WHO OWNS the
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
// `<option>` list shows
|
|
340
|
+
// WHO OWNS the options on either side of the model, and what the model's own API
|
|
341
|
+
// moves. The DOM is what makes these rows worth driving at the element: on the
|
|
342
|
+
// caller-side rows the descriptors still read correctly from the model itself, and
|
|
343
|
+
// only the `<option>` list shows whether a splice was emitted for them.
|
|
318
344
|
for (const vector of LIST_MODEL_OWNERSHIP_VECTORS) {
|
|
319
345
|
await it(`ownership — ${vector.rule}`, async () => {
|
|
320
346
|
const { el: row, host } = parse<AdwComboRow>(
|
|
@@ -323,39 +349,23 @@ export const AdwRowStateTest = async () => {
|
|
|
323
349
|
);
|
|
324
350
|
const select = row.querySelector('select') as HTMLSelectElement;
|
|
325
351
|
const input = vector.initial.map((item) => ({ ...item }));
|
|
326
|
-
|
|
352
|
+
const list = GjsifyOptionList.new(input);
|
|
353
|
+
row.model = list;
|
|
327
354
|
const before = [...select.options];
|
|
328
355
|
|
|
329
|
-
|
|
330
|
-
|
|
356
|
+
if (vector.source === 'model') applyListMutation(list, vector.op);
|
|
357
|
+
else applyListReadback(vector.source === 'input' ? input : row.options, vector.op);
|
|
331
358
|
|
|
332
|
-
expect(row.
|
|
359
|
+
expect(row.options).toStrictEqual([...vector.model]);
|
|
360
|
+
// None of these ops is a replacement, so the element must still be pointing
|
|
361
|
+
// at the list it was handed.
|
|
362
|
+
expect(row.model).toBe(list);
|
|
333
363
|
const after = [...select.options];
|
|
334
364
|
expect(after.map((option) => option.textContent)).toStrictEqual(vector.model.map((item) => item.label));
|
|
335
365
|
expect(before.filter((option) => after.includes(option)).length).toBe(vector.survivors);
|
|
336
366
|
host.remove();
|
|
337
367
|
});
|
|
338
368
|
}
|
|
339
|
-
|
|
340
|
-
for (const vector of LIST_SELECTION_CLAMP_VECTORS) {
|
|
341
|
-
await it(`autoselect — ${vector.rule}`, async () => {
|
|
342
|
-
const { el: row, host } = parse<AdwComboRow>(
|
|
343
|
-
`<adw-combo-row title="Pick"></adw-combo-row>`,
|
|
344
|
-
'adw-combo-row',
|
|
345
|
-
);
|
|
346
|
-
// Seed a model long enough to hold the starting index, then replace it with
|
|
347
|
-
// one of the vector's length. NARROWER THAN THE VECTOR ON ONE ROW, and it
|
|
348
|
-
// says so: this element cannot HOLD a non-finite index — `setSelectedIndex`
|
|
349
|
-
// folds it to 0 before the clamp is reached — so that row asserts the same
|
|
350
|
-
// outcome by a different route here, and the non-finite input itself is
|
|
351
|
-
// driven in the core suite.
|
|
352
|
-
row.model = Array.from({ length: 8 }, (_, i) => `item ${i}`);
|
|
353
|
-
row.setAttribute('selected', String(vector.selected));
|
|
354
|
-
row.model = Array.from({ length: vector.length }, (_, i) => `next ${i}`);
|
|
355
|
-
expect(row.selected).toBe(vector.result);
|
|
356
|
-
host.remove();
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
369
|
});
|
|
360
370
|
|
|
361
371
|
// The SAME table `<gtk-drop-down>` and the NativeScript renderer are held to
|
|
@@ -394,6 +404,53 @@ export const AdwRowStateTest = async () => {
|
|
|
394
404
|
}
|
|
395
405
|
});
|
|
396
406
|
|
|
407
|
+
// The model MOVING under a live row — the axis that only exists since `model` holds a
|
|
408
|
+
// real `Gio.ListModel` (ADR 0122 § 6). Nothing re-assigns `model` here: the list is
|
|
409
|
+
// spliced through its own API and the row has to follow, which is what separates this
|
|
410
|
+
// from every replacement row above it.
|
|
411
|
+
//
|
|
412
|
+
// What a DOM surface can show is the OUTCOME — `selected`, `selectedValue`, the inline
|
|
413
|
+
// label and the rebuilt `<option>` list — plus the silence: a model moving underneath is
|
|
414
|
+
// not a user pick, so `notify::selected` must not fire. The vectors' `notified` column is
|
|
415
|
+
// the GObject half this element has no spelling for and the core suite asserts alone.
|
|
416
|
+
await describe('adw-combo-row items-changed on the held model (GtkSingleSelection)', async () => {
|
|
417
|
+
for (const vector of COMBO_ITEMS_CHANGED_VECTORS) {
|
|
418
|
+
await it(`${vector.name} — ${vector.rule}`, async () => {
|
|
419
|
+
const { el: row, host } = parse<AdwComboRow>(
|
|
420
|
+
`<adw-combo-row title="Pick"></adw-combo-row>`,
|
|
421
|
+
'adw-combo-row',
|
|
422
|
+
);
|
|
423
|
+
const select = row.querySelector('select') as HTMLSelectElement;
|
|
424
|
+
const value = row.querySelector('.adw-row-value') as HTMLSpanElement;
|
|
425
|
+
|
|
426
|
+
const list = GjsifyOptionList.new([...vector.initial]);
|
|
427
|
+
row.model = list;
|
|
428
|
+
if (vector.selected >= 0) row.selected = vector.selected;
|
|
429
|
+
expect(row.selected, 'the row set up the selection it says it did').toBe(vector.selected);
|
|
430
|
+
|
|
431
|
+
const events = record(row, 'notify::selected');
|
|
432
|
+
list.splice(vector.splice.position, vector.splice.removed, [...vector.splice.added]);
|
|
433
|
+
|
|
434
|
+
const expected = [...vector.initial];
|
|
435
|
+
expected.splice(vector.splice.position, vector.splice.removed, ...vector.splice.added);
|
|
436
|
+
|
|
437
|
+
expect(row.selected).toBe(vector.result);
|
|
438
|
+
expect(row.selectedValue).toBe(vector.value);
|
|
439
|
+
expect(value.textContent).toBe(expected[vector.result]?.label ?? '');
|
|
440
|
+
// The drawn list follows the MODEL: the row was never re-assigned, so a
|
|
441
|
+
// `<select>` still showing the old options is exactly the bug the
|
|
442
|
+
// `items-changed` channel exists to prevent.
|
|
443
|
+
expect([...select.options].map((option) => option.textContent)).toStrictEqual(
|
|
444
|
+
expected.map((option) => option.label),
|
|
445
|
+
);
|
|
446
|
+
expect(events.details).toStrictEqual([]);
|
|
447
|
+
|
|
448
|
+
events.stop();
|
|
449
|
+
host.remove();
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
397
454
|
await describe('adw-spin-row (SpinState)', async () => {
|
|
398
455
|
// THE SHARED TABLES, driven through the ELEMENT and its markup door — the same rows
|
|
399
456
|
// the core suite and the NativeScript suite assert, so a range authored in one
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type ConstructVector,
|
|
10
10
|
type SharedTreeNode,
|
|
11
11
|
} from '@gjsify/adwaita-core/conformance';
|
|
12
|
+
import { GjsifyOptionList } from '@gjsify/adwaita-core';
|
|
12
13
|
import { describe, expect, it } from '@gjsify/unit';
|
|
13
14
|
|
|
14
15
|
import type { AdwAlertDialog } from './elements/adw-alert-dialog.js';
|
|
@@ -280,7 +281,7 @@ export const AdwConstructVectorsTest = async () => {
|
|
|
280
281
|
});
|
|
281
282
|
await it('dispatches DropDown `notify::selected-item` after `notify::selected`, on a real change only', () => {
|
|
282
283
|
const dropDown = document.createElement('gtk-drop-down') as HTMLElement & {
|
|
283
|
-
model:
|
|
284
|
+
model: unknown;
|
|
284
285
|
selected: number;
|
|
285
286
|
};
|
|
286
287
|
const seen: string[] = [];
|
|
@@ -288,7 +289,8 @@ export const AdwConstructVectorsTest = async () => {
|
|
|
288
289
|
dropDown.addEventListener(name, () => seen.push(name));
|
|
289
290
|
document.body.append(dropDown);
|
|
290
291
|
try {
|
|
291
|
-
|
|
292
|
+
// `model` takes a real list model, never an array (ADR 0122 § 4).
|
|
293
|
+
dropDown.model = GjsifyOptionList.new(['a', 'b']);
|
|
292
294
|
seen.length = 0;
|
|
293
295
|
dropDown.selected = 1;
|
|
294
296
|
dropDown.selected = 1;
|