@gjsify/adwaita-web 0.64.13 → 0.64.14
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/application.d.ts +3 -3
- package/lib/types/elements/adw-application-window.d.ts +4 -4
- package/lib/types/elements/adw-split-button.d.ts +19 -28
- package/lib/types/elements/adw-tab-overview.d.ts +4 -4
- package/lib/types/elements/gtk-menu-button.d.ts +30 -42
- package/lib/types/elements/gtk-popover-bin.d.ts +18 -11
- package/lib/types/elements/gtk-popover-menu-bar.d.ts +15 -8
- package/lib/types/elements/gtk-popover-menu.d.ts +13 -15
- package/lib/types/elements/menu-slot.d.ts +49 -0
- package/lib/types/elements/popover-menu.d.ts +11 -7
- package/lib/types/index.d.ts +2 -1
- package/lib/types/namespace/gio.d.ts +1 -1
- package/package.json +11 -11
- package/src/adw-application-window.spec.ts +5 -2
- package/src/application.ts +4 -4
- package/src/elements/adw-application-window.ts +17 -21
- package/src/elements/adw-split-button.ts +35 -59
- package/src/elements/adw-tab-overview.ts +18 -28
- package/src/elements/gtk-menu-button.ts +60 -82
- package/src/elements/gtk-popover-bin.ts +32 -38
- package/src/elements/gtk-popover-menu-bar.ts +41 -46
- package/src/elements/gtk-popover-menu.ts +35 -54
- package/src/elements/menu-slot.ts +139 -0
- package/src/elements/popover-menu.ts +26 -16
- package/src/gio-menu.spec.ts +173 -34
- package/src/gtk-image.spec.ts +3 -2
- package/src/gtk-menu-button.spec.ts +19 -16
- package/src/gtk-popover-bin.spec.ts +6 -5
- package/src/gtk-popover-menu-bar.spec.ts +11 -6
- package/src/gtk-popover-menu.spec.ts +22 -22
- package/src/index.ts +10 -2
- package/src/keyboard-operable.spec.ts +6 -5
- package/src/menu-actions.spec.ts +6 -1
- package/src/namespace/gio.ts +19 -2
- package/src/shared-tree-builder.ts +6 -1
- package/src/split-button.spec.ts +25 -8
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
import { describe, expect, it } from '@gjsify/unit';
|
|
11
11
|
import { MENU_LABEL_TEXT_VECTORS, MENU_REFUSAL_VECTORS } from '@gjsify/adwaita-core/conformance';
|
|
12
|
-
import { flattenMenu } from '@gjsify/adwaita-core';
|
|
13
|
-
import type {
|
|
12
|
+
import { buildMenu, flattenMenu } from '@gjsify/adwaita-core';
|
|
13
|
+
import type { AdwMenuModel, AdwMenuTree } from '@gjsify/adwaita-core';
|
|
14
14
|
|
|
15
15
|
interface PopoverMenuElement extends HTMLElement {
|
|
16
|
-
menuModel:
|
|
16
|
+
menuModel: AdwMenuModel | string | null;
|
|
17
17
|
actions: Record<string, { enabled?: boolean; state?: string }> | null;
|
|
18
18
|
visibleSubmenu: string;
|
|
19
19
|
// The three below are `GtkPopover`'s own — the reason this spec can say "inherits the
|
|
@@ -76,7 +76,7 @@ export const GtkPopoverMenuTest = async () => {
|
|
|
76
76
|
await describe('<gtk-popover-menu> the page stack', async () => {
|
|
77
77
|
await it('renders the model and reports the root page as `main`', () => {
|
|
78
78
|
const el = mount();
|
|
79
|
-
el.menuModel = NESTED;
|
|
79
|
+
el.menuModel = buildMenu(NESTED);
|
|
80
80
|
expect(labels(el)).toStrictEqual(['New', 'More']);
|
|
81
81
|
expect(el.visibleSubmenu).toBe('main');
|
|
82
82
|
unmountAll();
|
|
@@ -84,7 +84,7 @@ export const GtkPopoverMenuTest = async () => {
|
|
|
84
84
|
|
|
85
85
|
await it('opens a submenu by name and swaps the page', () => {
|
|
86
86
|
const el = mount();
|
|
87
|
-
el.menuModel = NESTED;
|
|
87
|
+
el.menuModel = buildMenu(NESTED);
|
|
88
88
|
el.openSubmenu('More');
|
|
89
89
|
// A submenu's page is the back row plus its own items — the submenu's contents
|
|
90
90
|
// are never inlined into the parent.
|
|
@@ -95,7 +95,7 @@ export const GtkPopoverMenuTest = async () => {
|
|
|
95
95
|
|
|
96
96
|
await it('`main` goes back, and so does closeSubmenus', () => {
|
|
97
97
|
const el = mount();
|
|
98
|
-
el.menuModel = NESTED;
|
|
98
|
+
el.menuModel = buildMenu(NESTED);
|
|
99
99
|
el.openSubmenu('More');
|
|
100
100
|
el.openSubmenu('main');
|
|
101
101
|
expect(labels(el)).toStrictEqual(['New', 'More']);
|
|
@@ -109,7 +109,7 @@ export const GtkPopoverMenuTest = async () => {
|
|
|
109
109
|
|
|
110
110
|
await it('a name no submenu carries leaves the open page alone', () => {
|
|
111
111
|
const el = mount();
|
|
112
|
-
el.menuModel = NESTED;
|
|
112
|
+
el.menuModel = buildMenu(NESTED);
|
|
113
113
|
el.openSubmenu('More');
|
|
114
114
|
// `gtk_stack_set_visible_child_name` on a child that is not there is a no-op in
|
|
115
115
|
// GTK, so the current page must survive rather than blank the surface.
|
|
@@ -121,7 +121,7 @@ export const GtkPopoverMenuTest = async () => {
|
|
|
121
121
|
|
|
122
122
|
await it('`visible-submenu` as an ATTRIBUTE navigates, and notifies once per page', () => {
|
|
123
123
|
const el = mount();
|
|
124
|
-
el.menuModel = NESTED;
|
|
124
|
+
el.menuModel = buildMenu(NESTED);
|
|
125
125
|
const seen: string[] = [];
|
|
126
126
|
el.addEventListener('notify::visible-submenu', (event) => {
|
|
127
127
|
seen.push((event as CustomEvent<{ visibleSubmenu: string }>).detail.visibleSubmenu);
|
|
@@ -137,9 +137,9 @@ export const GtkPopoverMenuTest = async () => {
|
|
|
137
137
|
|
|
138
138
|
await it('a CHANGED model drops the open page', () => {
|
|
139
139
|
const el = mount();
|
|
140
|
-
el.menuModel = NESTED;
|
|
140
|
+
el.menuModel = buildMenu(NESTED);
|
|
141
141
|
el.openSubmenu('More');
|
|
142
|
-
el.menuModel = [{ label: 'Only' }];
|
|
142
|
+
el.menuModel = buildMenu([{ label: 'Only' }]);
|
|
143
143
|
// The submenu the page named is gone; staying on it would leave a back row and
|
|
144
144
|
// no items behind it.
|
|
145
145
|
expect(el.visibleSubmenu).toBe('main');
|
|
@@ -149,16 +149,16 @@ export const GtkPopoverMenuTest = async () => {
|
|
|
149
149
|
|
|
150
150
|
await it('an UNCHANGED model keeps the open page', () => {
|
|
151
151
|
const el = mount();
|
|
152
|
-
el.menuModel = NESTED;
|
|
152
|
+
el.menuModel = buildMenu(NESTED);
|
|
153
153
|
el.openSubmenu('More');
|
|
154
|
-
el.menuModel = JSON.parse(JSON.stringify(NESTED))
|
|
154
|
+
el.menuModel = buildMenu(JSON.parse(JSON.stringify(NESTED)));
|
|
155
155
|
expect(el.visibleSubmenu).toBe('More');
|
|
156
156
|
unmountAll();
|
|
157
157
|
});
|
|
158
158
|
|
|
159
159
|
await it('closing the popover returns to the root page', async () => {
|
|
160
160
|
const el = mount();
|
|
161
|
-
el.menuModel = NESTED;
|
|
161
|
+
el.menuModel = buildMenu(NESTED);
|
|
162
162
|
el.openSubmenu('More');
|
|
163
163
|
el.open = true;
|
|
164
164
|
el.open = false;
|
|
@@ -175,7 +175,7 @@ export const GtkPopoverMenuTest = async () => {
|
|
|
175
175
|
const el = document.createElement('gtk-popover-menu') as PopoverMenuElement;
|
|
176
176
|
anchor.appendChild(el);
|
|
177
177
|
document.body.appendChild(anchor);
|
|
178
|
-
el.menuModel = NESTED;
|
|
178
|
+
el.menuModel = buildMenu(NESTED);
|
|
179
179
|
el.openSubmenu('More');
|
|
180
180
|
el.anchor = anchor;
|
|
181
181
|
el.open = true;
|
|
@@ -197,7 +197,7 @@ export const GtkPopoverMenuTest = async () => {
|
|
|
197
197
|
|
|
198
198
|
await it('a disabled row activates nothing', () => {
|
|
199
199
|
const el = mount();
|
|
200
|
-
el.menuModel = [{ label: 'Quit', action: 'app.quit' }];
|
|
200
|
+
el.menuModel = buildMenu([{ label: 'Quit', action: 'app.quit' }]);
|
|
201
201
|
el.actions = { 'app.quit': { enabled: false } };
|
|
202
202
|
let fired = false;
|
|
203
203
|
el.addEventListener('menu-item-activated', () => {
|
|
@@ -212,11 +212,11 @@ export const GtkPopoverMenuTest = async () => {
|
|
|
212
212
|
await describe('<gtk-popover-menu> the menu attribute', async () => {
|
|
213
213
|
await it('reads the JSON and draws the same rows `flattenMenu` names', () => {
|
|
214
214
|
const el = mount('[{"label":"New"},{"section":[{"label":"Cut"}],"label":"Edit"}]');
|
|
215
|
-
const
|
|
215
|
+
const tree: AdwMenuTree = [
|
|
216
216
|
{ kind: 'item', label: 'New' },
|
|
217
217
|
{ kind: 'section', label: 'Edit', items: [{ kind: 'item', label: 'Cut' }] },
|
|
218
218
|
];
|
|
219
|
-
expect(labels(el)).toStrictEqual(flattenMenu(
|
|
219
|
+
expect(labels(el)).toStrictEqual(flattenMenu(tree).map((row) => row.node.label));
|
|
220
220
|
unmountAll();
|
|
221
221
|
});
|
|
222
222
|
|
|
@@ -243,15 +243,15 @@ export const GtkPopoverMenuTest = async () => {
|
|
|
243
243
|
});
|
|
244
244
|
|
|
245
245
|
await it('the PROPERTY path refuses a `custom` item loudly', () => {
|
|
246
|
-
for (const {
|
|
246
|
+
for (const { tree, paths, rule } of MENU_REFUSAL_VECTORS) {
|
|
247
247
|
const el = mount();
|
|
248
248
|
if (paths.length > 0) {
|
|
249
249
|
expect(() => {
|
|
250
|
-
el.menuModel =
|
|
250
|
+
el.menuModel = buildMenu(tree);
|
|
251
251
|
}).toThrow('adwaita-web');
|
|
252
252
|
} else {
|
|
253
|
-
el.menuModel =
|
|
254
|
-
expect(labels(el)).toStrictEqual(flattenMenu(
|
|
253
|
+
el.menuModel = buildMenu(tree);
|
|
254
|
+
expect(labels(el)).toStrictEqual(flattenMenu(tree).map((row) => row.node.label));
|
|
255
255
|
}
|
|
256
256
|
expect(rule.length > 0).toBe(true);
|
|
257
257
|
el.remove();
|
|
@@ -263,7 +263,7 @@ export const GtkPopoverMenuTest = async () => {
|
|
|
263
263
|
for (const { label, text, rule } of MENU_LABEL_TEXT_VECTORS) {
|
|
264
264
|
await it(`item, submenu and back row: ${rule}`, () => {
|
|
265
265
|
const el = mount();
|
|
266
|
-
el.menuModel = [{ label }, { label, submenu: [{ label }] }];
|
|
266
|
+
el.menuModel = buildMenu([{ label }, { label, submenu: [{ label }] }]);
|
|
267
267
|
expect(labels(el)).toStrictEqual([text, text]);
|
|
268
268
|
el.openSubmenu(label);
|
|
269
269
|
expect(labels(el)).toStrictEqual([text, text]);
|
package/src/index.ts
CHANGED
|
@@ -114,8 +114,9 @@ export * as Adw from './namespace/adw.js';
|
|
|
114
114
|
export * as Gtk from './namespace/gtk.js';
|
|
115
115
|
|
|
116
116
|
// The THIRD namespace, and the one that defines no element: the GObject types an author
|
|
117
|
-
// CONSTRUCTS while building a tree. `Gio.Menu` is
|
|
118
|
-
// program here as on GJS — `./namespace/gio.ts`
|
|
117
|
+
// CONSTRUCTS while building a tree. `Gio.Menu` is a real `Gio.MenuModel` (ADR 0128), which
|
|
118
|
+
// is what makes `menuModel: menu` the same program here as on GJS — `./namespace/gio.ts`
|
|
119
|
+
// says what does not go in it.
|
|
119
120
|
export * as Gio from './namespace/gio.js';
|
|
120
121
|
|
|
121
122
|
// Gdk's clipboard (`Display`, `Clipboard`, `ContentProvider`): `./namespace/gdk.ts` says what is absent.
|
|
@@ -189,6 +190,12 @@ export type { AdwViewStackPageInfo } from './elements/adw-view-stack.js';
|
|
|
189
190
|
// The portable menu model (ADR 0042) is `@gjsify/adwaita-core`'s, re-exported so a
|
|
190
191
|
// consumer of THIS package reaches the menu types without a second dependency — one
|
|
191
192
|
// definition, two doors, never two declarations.
|
|
193
|
+
//
|
|
194
|
+
// `buildMenu` is a VALUE and not only a type, because since ADR 0128 a `menuModel`
|
|
195
|
+
// property takes a real `Gio.MenuModel` and an authored array is refused: without the
|
|
196
|
+
// builder an app depending on this package alone could not write a menu in code at all.
|
|
197
|
+
// The `menu-model` ATTRIBUTE still takes the JSON string, so markup needs none of this.
|
|
198
|
+
export { buildMenu, menuTree, watchMenuModel } from '@gjsify/adwaita-core';
|
|
192
199
|
export type {
|
|
193
200
|
AdwMenuActions,
|
|
194
201
|
AdwMenuEntryInput,
|
|
@@ -199,6 +206,7 @@ export type {
|
|
|
199
206
|
AdwMenuPath,
|
|
200
207
|
AdwMenuSection,
|
|
201
208
|
AdwMenuSubmenu,
|
|
209
|
+
AdwMenuTree,
|
|
202
210
|
} from '@gjsify/adwaita-core';
|
|
203
211
|
export type { GtkDropDownOption } from './elements/gtk-drop-down.js';
|
|
204
212
|
export type { AdwDialogPresentationMode } from './elements/adw-dialog.js';
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// A real Tab press escaping an untrapped dialog is measured one layer up, by
|
|
20
20
|
// `tests/browser/specs/adwaita-keyboard.spec.ts`.
|
|
21
21
|
import { describe, expect, it } from '@gjsify/unit';
|
|
22
|
+
import { buildMenu } from '@gjsify/adwaita-core';
|
|
22
23
|
|
|
23
24
|
import type { AdwAlertDialog } from './elements/adw-alert-dialog.js';
|
|
24
25
|
import { AdwToggle, AdwToggleGroup } from './elements/adw-toggle-group.js';
|
|
@@ -737,11 +738,11 @@ export const AdwKeyboardOperableTest = async () => {
|
|
|
737
738
|
};
|
|
738
739
|
document.body.appendChild(el);
|
|
739
740
|
el.actions = { 'app.export': { enabled: false }, 'app.new': {} };
|
|
740
|
-
el.menuModel = [
|
|
741
|
+
el.menuModel = buildMenu([
|
|
741
742
|
{ label: 'New', action: 'app.new' },
|
|
742
743
|
{ section: [{ label: 'Export', action: 'app.export' }, { label: 'Print' }] },
|
|
743
744
|
{ label: 'More', submenu: [{ label: 'Rename' }, { label: 'Duplicate' }] },
|
|
744
|
-
];
|
|
745
|
+
]);
|
|
745
746
|
(el.querySelector('.adw-menu-button-button') as HTMLElement).click();
|
|
746
747
|
return {
|
|
747
748
|
el,
|
|
@@ -828,14 +829,14 @@ export const AdwKeyboardOperableTest = async () => {
|
|
|
828
829
|
};
|
|
829
830
|
document.body.appendChild(el);
|
|
830
831
|
el.actions = { 'app.off': { enabled: false } };
|
|
831
|
-
el.menuModel = [
|
|
832
|
+
el.menuModel = buildMenu([
|
|
832
833
|
{ label: 'Dim', action: 'app.off' },
|
|
833
834
|
{ label: 'Live' },
|
|
834
835
|
{
|
|
835
836
|
label: 'More',
|
|
836
837
|
submenu: [{ label: 'SubDim', action: 'app.off' }, { label: 'SubLive' }],
|
|
837
838
|
},
|
|
838
|
-
];
|
|
839
|
+
]);
|
|
839
840
|
(el.querySelector('.adw-menu-button-button') as HTMLElement).click();
|
|
840
841
|
const rows = () => [...el.querySelectorAll<HTMLButtonElement>('.adw-popover-item')];
|
|
841
842
|
const label = () =>
|
|
@@ -867,7 +868,7 @@ export const AdwKeyboardOperableTest = async () => {
|
|
|
867
868
|
};
|
|
868
869
|
document.body.appendChild(el);
|
|
869
870
|
el.actions = { 'app.off': { enabled: false } };
|
|
870
|
-
el.menuModel = [{ label: 'More', submenu: [{ label: 'Only', action: 'app.off' }] }];
|
|
871
|
+
el.menuModel = buildMenu([{ label: 'More', submenu: [{ label: 'Only', action: 'app.off' }] }]);
|
|
871
872
|
(el.querySelector('.adw-menu-button-button') as HTMLElement).click();
|
|
872
873
|
const opener = el.querySelector('.adw-popover-item') as HTMLButtonElement;
|
|
873
874
|
opener.focus();
|
package/src/menu-actions.spec.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
registryMenuActionScene,
|
|
8
8
|
type SharedTreeNode,
|
|
9
9
|
} from '@gjsify/adwaita-core/conformance';
|
|
10
|
+
import { menuTree } from '@gjsify/adwaita-core';
|
|
10
11
|
import { describe, expect, it } from '@gjsify/unit';
|
|
11
12
|
|
|
12
13
|
import { parentOfElement } from './application.js';
|
|
@@ -39,7 +40,11 @@ export const MenuActionsTest = async () => {
|
|
|
39
40
|
return;
|
|
40
41
|
}
|
|
41
42
|
const button = buildSharedTree(node) as unknown as GtkMenuButton;
|
|
42
|
-
expect(
|
|
43
|
+
expect(
|
|
44
|
+
menuTree(button.menuModel!)
|
|
45
|
+
.map((entry) => entry.kind)
|
|
46
|
+
.join(','),
|
|
47
|
+
).toBe(result.kinds.join(','));
|
|
43
48
|
});
|
|
44
49
|
}
|
|
45
50
|
});
|
package/src/namespace/gio.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The GIO half of `@gjsify/adwaita-web`'s vocabulary — `Gio.
|
|
1
|
+
// The GIO half of `@gjsify/adwaita-web`'s vocabulary — `Gio.MenuModel` and its subclasses.
|
|
2
2
|
// `export * as Gio from './namespace/gio.js'` in `src/index.ts` makes it the namespace,
|
|
3
3
|
// the same module shape `./adw.ts` and `./gtk.ts` take and for the same reason (one list
|
|
4
4
|
// carrying the value AND the type meaning — see the header of `./adw.ts`).
|
|
@@ -28,7 +28,24 @@ import { createAppInfo } from '@gjsify/adwaita-core';
|
|
|
28
28
|
|
|
29
29
|
import { webPlatformHost } from '../platform-host.js';
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
// ADR 0128: `Gio.MenuModel` is an abstract CLASS, so the whole menu hierarchy is here and
|
|
32
|
+
// `menu instanceof Gio.MenuModel` answers the same on both runtimes. The two iterators come
|
|
33
|
+
// with it because `iterate_item_attributes` / `iterate_item_links` return them by name.
|
|
34
|
+
export {
|
|
35
|
+
GioMenu as Menu,
|
|
36
|
+
GioMenuItem as MenuItem,
|
|
37
|
+
GioMenuModel as MenuModel,
|
|
38
|
+
GioMenuAttributeIter as MenuAttributeIter,
|
|
39
|
+
GioMenuLinkIter as MenuLinkIter,
|
|
40
|
+
MENU_ATTRIBUTE_ACTION,
|
|
41
|
+
MENU_ATTRIBUTE_ACTION_NAMESPACE,
|
|
42
|
+
MENU_ATTRIBUTE_ICON,
|
|
43
|
+
MENU_ATTRIBUTE_LABEL,
|
|
44
|
+
MENU_ATTRIBUTE_TARGET,
|
|
45
|
+
MENU_EXPORTER_MAX_SECTION_SIZE,
|
|
46
|
+
MENU_LINK_SECTION,
|
|
47
|
+
MENU_LINK_SUBMENU,
|
|
48
|
+
} from '@gjsify/adwaita-core';
|
|
32
49
|
// ADR 0122 § 2: the interface a `Gtk.StringList` implements and an `Implements: [Gio.ListModel]`
|
|
33
50
|
// names. It is a token, not a class — the whole of the subset's interface support.
|
|
34
51
|
export { ListModel } from '@gjsify/adwaita-core';
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
// this small, most of which (the `SharedTreeNode` type) is erased at build anyway.
|
|
36
36
|
|
|
37
37
|
import {
|
|
38
|
+
buildMenu,
|
|
38
39
|
createBreakpointDriver,
|
|
39
40
|
menuAssignments,
|
|
40
41
|
parseBreakpointCondition,
|
|
@@ -512,8 +513,12 @@ function writeLook(el: HTMLElement, node: SharedTreeNode, menus?: SharedTreeNode
|
|
|
512
513
|
for (const [prop, value] of Object.entries(node.props ?? {})) {
|
|
513
514
|
if (prop !== 'menu-model') writeProp(el, prop, value);
|
|
514
515
|
}
|
|
516
|
+
// `buildMenu`, because since ADR 0128 `menuModel` is a real `Gio.MenuModel` and a tree
|
|
517
|
+
// written at it is refused the way GJS refuses one. `menuAssignments` stays a tree walker
|
|
518
|
+
// — it resolves a `menu-model: id` against the file's `menus` — so the model is built
|
|
519
|
+
// here, at the one place the resolved tree becomes a widget's property.
|
|
515
520
|
for (const [prop, model] of menuAssignments(node, menus)) {
|
|
516
|
-
(el as unknown as Record<string, unknown>)[propertyOf(prop)] = model;
|
|
521
|
+
(el as unknown as Record<string, unknown>)[propertyOf(prop)] = buildMenu(model);
|
|
517
522
|
}
|
|
518
523
|
// `styleClasses` is `GtkWidget:css-classes`, and this renderer's door for it is the
|
|
519
524
|
// `class` attribute — what `.title-1`, `.dimmed` and `.card` select on. Unread, a
|
package/src/split-button.spec.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
SPLIT_BUTTON_STYLE_CLASS_VECTORS,
|
|
18
18
|
SPLIT_BUTTON_TOOLTIP_VECTORS,
|
|
19
19
|
} from '@gjsify/adwaita-core/conformance';
|
|
20
|
-
import { ADW_MENU_SURFACE_WEB, flattenMenu, menuRefusals } from '@gjsify/adwaita-core';
|
|
20
|
+
import { ADW_MENU_SURFACE_WEB, buildMenu, flattenMenu, menuRefusals, menuTree } from '@gjsify/adwaita-core';
|
|
21
21
|
import type { SplitButtonProperty } from '@gjsify/adwaita-core';
|
|
22
22
|
import type { AdwSplitButton } from './elements/adw-split-button.js';
|
|
23
23
|
import { fallbackMask, maskOf } from './icon-masks.spec.js';
|
|
@@ -238,24 +238,24 @@ export const AdwSplitButtonTest = async () => {
|
|
|
238
238
|
});
|
|
239
239
|
|
|
240
240
|
await describe('adw-split-button menu', async () => {
|
|
241
|
-
for (const { input,
|
|
241
|
+
for (const { input, tree, rule } of MENU_NORMALIZE_VECTORS) {
|
|
242
242
|
await it(`normalises an authored menu — ${rule}`, () => {
|
|
243
243
|
const { el, host } = mount({ label: 'Save' });
|
|
244
244
|
// What THIS surface refuses is core's answer, not a list restated here:
|
|
245
245
|
// a `custom` item names an application widget the browser cannot host,
|
|
246
246
|
// and the setter is where that is said out loud.
|
|
247
|
-
if (menuRefusals(
|
|
247
|
+
if (menuRefusals(tree, ADW_MENU_SURFACE_WEB).length > 0) {
|
|
248
248
|
expect(() => {
|
|
249
|
-
el.menuModel = input;
|
|
249
|
+
el.menuModel = buildMenu(input);
|
|
250
250
|
}).toThrow('adwaita-web');
|
|
251
251
|
host.remove();
|
|
252
252
|
return;
|
|
253
253
|
}
|
|
254
|
-
el.menuModel = input;
|
|
255
|
-
expect(
|
|
254
|
+
el.menuModel = buildMenu(input);
|
|
255
|
+
expect(menuTree(el.menuModel!)).toStrictEqual([...tree]);
|
|
256
256
|
// Every node the model draws is a row: a section is inlined, a submenu
|
|
257
257
|
// is one row of its own.
|
|
258
|
-
expect(el.querySelectorAll('.adw-split-button-menu-item').length).toBe(flattenMenu(
|
|
258
|
+
expect(el.querySelectorAll('.adw-split-button-menu-item').length).toBe(flattenMenu(tree).length);
|
|
259
259
|
host.remove();
|
|
260
260
|
});
|
|
261
261
|
}
|
|
@@ -312,11 +312,28 @@ export const AdwSplitButtonTest = async () => {
|
|
|
312
312
|
const { el, host } = mount({ 'menu-model': '[{"label":"Print"}]' });
|
|
313
313
|
dropdownHalf(el).click();
|
|
314
314
|
expect(el.active).toBe(true);
|
|
315
|
-
|
|
315
|
+
// REMOVING the attribute is how a menu is lost since ADR 0128 — `null` is the
|
|
316
|
+
// only spelling of "no menu", and the row below is the other half of that.
|
|
317
|
+
el.removeAttribute('menu-model');
|
|
316
318
|
expect(el.active).toBe(false);
|
|
317
319
|
expect(el.dropdownEnabled).toBe(false);
|
|
318
320
|
host.remove();
|
|
319
321
|
});
|
|
322
|
+
|
|
323
|
+
await it('an EMPTY menu is still a menu, so the dropdown stays live', () => {
|
|
324
|
+
// Measured on `Adw.SplitButton`: an empty non-null `Gio.Menu` at `menu-model`
|
|
325
|
+
// still builds a `GtkPopoverMenu` and `set_menu_model(null)` is what returns
|
|
326
|
+
// the popover to null (`MENU_MODEL_DOOR_VECTORS`). `[]` therefore edits the
|
|
327
|
+
// menu this element OWNS rather than taking it away, so the open popup follows
|
|
328
|
+
// the edit instead of being closed by it.
|
|
329
|
+
const { el, host } = mount({ 'menu-model': '[{"label":"Print"}]' });
|
|
330
|
+
dropdownHalf(el).click();
|
|
331
|
+
el.setAttribute('menu-model', '[]');
|
|
332
|
+
expect(el.dropdownEnabled).toBe(true);
|
|
333
|
+
expect(el.active).toBe(true);
|
|
334
|
+
expect(el.querySelectorAll('.adw-split-button-menu-item').length).toBe(0);
|
|
335
|
+
host.remove();
|
|
336
|
+
});
|
|
320
337
|
});
|
|
321
338
|
|
|
322
339
|
await describe('adw-split-button signals + state classes', async () => {
|