@gjsify/adwaita-web 0.64.13-next.283 → 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-bin.d.ts +13 -0
- 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/lib/types/widget-element.d.ts +8 -2
- package/package.json +12 -13
- package/src/adw-application-window.spec.ts +5 -2
- package/src/adw-bin.spec.ts +20 -0
- package/src/application.ts +4 -4
- package/src/elements/adw-application-window.ts +17 -21
- package/src/elements/adw-bin.ts +20 -1
- 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/pango-markup.spec.ts +11 -0
- package/src/pango-markup.ts +5 -2
- package/src/shared-tree-builder.ts +6 -1
- package/src/split-button.spec.ts +25 -8
- package/src/widget-element.ts +28 -7
- package/src/widget-members.spec.ts +42 -3
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';
|
package/src/pango-markup.spec.ts
CHANGED
|
@@ -73,6 +73,17 @@ export const PangoMarkupTest = async () => {
|
|
|
73
73
|
expect(target.querySelector('i > u')?.textContent).toBe('b');
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
+
await it("sub and sup carry pango's 1 / 1.2 font_scale fallback", () => {
|
|
77
|
+
const { target } = render('<sub>d</sub><sup>e</sup>');
|
|
78
|
+
// Compared as a computed ratio: browsers serialise the `calc()` differently
|
|
79
|
+
// (Firefox gives `calc(0.833333em)`).
|
|
80
|
+
const base = Number.parseFloat(getComputedStyle(target).fontSize);
|
|
81
|
+
for (const el of target.children) {
|
|
82
|
+
const ratio = Number.parseFloat(getComputedStyle(el).fontSize) / base;
|
|
83
|
+
expect(Math.abs(ratio - 1 / 1.2)).toBeLessThan(0.01);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
76
87
|
await it("<markup> draws its children — it is pango's implicit root, not a box", async () => {
|
|
77
88
|
const { target } = render('<markup><b>x</b></markup>');
|
|
78
89
|
expect(target.querySelector('b')?.textContent).toBe('x');
|
package/src/pango-markup.ts
CHANGED
|
@@ -59,8 +59,11 @@ export const TAG_STYLES: Readonly<Record<string, { element: string; style?: Read
|
|
|
59
59
|
tt: { element: 'span', style: { 'font-family': 'monospace' } },
|
|
60
60
|
big: { element: 'span', style: { 'font-size': 'larger' } },
|
|
61
61
|
small: { element: 'span', style: { 'font-size': 'smaller' } },
|
|
62
|
-
sub
|
|
63
|
-
|
|
62
|
+
// pango's `font_scale` for `<sub>`/`<sup>` falls back to `1 / 1.2` when the font's OpenType
|
|
63
|
+
// metrics carry no script size (pango/itemize.c, `PANGO_FONT_SCALE_SUBSCRIPT`/`SUPERSCRIPT`);
|
|
64
|
+
// spelled out so it does not depend on each UA's `font-size: smaller` ratio.
|
|
65
|
+
sub: { element: 'sub', style: { 'font-size': 'calc(1em / 1.2)' } },
|
|
66
|
+
sup: { element: 'sup', style: { 'font-size': 'calc(1em / 1.2)' } },
|
|
64
67
|
markup: { element: 'span', style: { display: 'contents' } },
|
|
65
68
|
span: { element: 'span' },
|
|
66
69
|
};
|
|
@@ -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 () => {
|
package/src/widget-element.ts
CHANGED
|
@@ -16,10 +16,25 @@
|
|
|
16
16
|
// Reference: GTK gtk/gtkwidget.c (gtk_widget_set_visible, gtk_widget_set_tooltip_text)
|
|
17
17
|
// Copyright (c) GNOME contributors (GTK). LGPLv2.1+.
|
|
18
18
|
|
|
19
|
+
import { isWidgetNode } from '@gjsify/adwaita-core';
|
|
20
|
+
|
|
19
21
|
import { WIDGET_VISIBLE_GETTERS, installElementApi, markLazyElementApi, notifyElement } from './gobject-elements.js';
|
|
20
22
|
import { dispatchedSignalsOf, type DispatchedSignals } from './signals.js';
|
|
21
23
|
import { isMapped } from './widget-mapping.js';
|
|
22
24
|
|
|
25
|
+
/** Element links, walked until one lands on a widget: `isWidgetNode` says which those are. */
|
|
26
|
+
type ElementLink =
|
|
27
|
+
| 'firstElementChild'
|
|
28
|
+
| 'lastElementChild'
|
|
29
|
+
| 'nextElementSibling'
|
|
30
|
+
| 'previousElementSibling'
|
|
31
|
+
| 'parentElement';
|
|
32
|
+
|
|
33
|
+
function widgetChild(from: Element, first: ElementLink, next: ElementLink): Element | null {
|
|
34
|
+
for (let at = from[first]; at !== null; at = at[next]) if (isWidgetNode(at)) return at;
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
export class WidgetElement extends HTMLElement {
|
|
24
39
|
/** `Gtk.Widget::map` / `::unmap`, which every subclass inherits (`dispatchedSignalsOf`). */
|
|
25
40
|
static readonly signals: DispatchedSignals = { map: 'map', unmap: 'unmap' };
|
|
@@ -41,25 +56,31 @@ export class WidgetElement extends HTMLElement {
|
|
|
41
56
|
set_visible(visible: boolean): void {
|
|
42
57
|
this.visible = visible;
|
|
43
58
|
}
|
|
44
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* `gtk_widget_get_first_child`: the first child WIDGET, `null` for a leaf.
|
|
61
|
+
*
|
|
62
|
+
* The five walkers skip the elements a port builds for its own insides — `isWidgetNode`
|
|
63
|
+
* in `@gjsify/adwaita-core` carries the rule and the incident behind it, and the
|
|
64
|
+
* registered-class copy in `widgetTreeMembers()` answers from the same function.
|
|
65
|
+
*/
|
|
45
66
|
get_first_child(): Element | null {
|
|
46
|
-
return this
|
|
67
|
+
return widgetChild(this, 'firstElementChild', 'nextElementSibling');
|
|
47
68
|
}
|
|
48
69
|
/** `gtk_widget_get_last_child`. */
|
|
49
70
|
get_last_child(): Element | null {
|
|
50
|
-
return this
|
|
71
|
+
return widgetChild(this, 'lastElementChild', 'previousElementSibling');
|
|
51
72
|
}
|
|
52
73
|
/** `gtk_widget_get_next_sibling`. */
|
|
53
74
|
get_next_sibling(): Element | null {
|
|
54
|
-
return this
|
|
75
|
+
return widgetChild(this, 'nextElementSibling', 'nextElementSibling');
|
|
55
76
|
}
|
|
56
77
|
/** `gtk_widget_get_prev_sibling`. */
|
|
57
78
|
get_prev_sibling(): Element | null {
|
|
58
|
-
return this
|
|
79
|
+
return widgetChild(this, 'previousElementSibling', 'previousElementSibling');
|
|
59
80
|
}
|
|
60
|
-
/** `gtk_widget_get_parent
|
|
81
|
+
/** `gtk_widget_get_parent`: the nearest ancestor that is a widget, not a port's own wrapper. */
|
|
61
82
|
get_parent(): Element | null {
|
|
62
|
-
return this
|
|
83
|
+
return widgetChild(this, 'parentElement', 'parentElement');
|
|
63
84
|
}
|
|
64
85
|
/** `gtk_widget_get_width`: the layout box in CSS px; 0 until the widget is rendered. */
|
|
65
86
|
get_width(): number {
|
|
@@ -60,10 +60,10 @@ export const WidgetMembersTest = async () => {
|
|
|
60
60
|
expect(() => widget.set_direction(7)).toThrow();
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
await it('walks the
|
|
63
|
+
await it('walks the widget tree with the GTK link names', () => {
|
|
64
64
|
const parent = make('WidgetMembersParent');
|
|
65
|
-
const a =
|
|
66
|
-
const b =
|
|
65
|
+
const a = new Gtk.Label() as unknown as WidgetLike;
|
|
66
|
+
const b = new Gtk.Label() as unknown as WidgetLike;
|
|
67
67
|
parent.append(a, b);
|
|
68
68
|
expect(parent.get_first_child()).toBe(a);
|
|
69
69
|
expect(parent.get_last_child()).toBe(b);
|
|
@@ -73,6 +73,45 @@ export const WidgetMembersTest = async () => {
|
|
|
73
73
|
expect(parent.get_parent()).toBe(null);
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
+
await it("steps over the plain elements a port builds for a widget's insides", () => {
|
|
77
|
+
// `gtk_widget_get_first_child` hands back a GtkWidget, always. In a DOM port the
|
|
78
|
+
// same `children` list also holds what the port made for itself — the `<span>` a
|
|
79
|
+
// label's markup renderer writes, the `<div>` a row lays its slots out in — and a
|
|
80
|
+
// caller walking the tree would get one of those and find no `get_first_child` on
|
|
81
|
+
// it at all. The custom-element hyphen separates the two kinds (`isWidgetNode`).
|
|
82
|
+
const parent = make('WidgetMembersInsides');
|
|
83
|
+
const a = new Gtk.Label() as unknown as WidgetLike;
|
|
84
|
+
const b = new Gtk.Label() as unknown as WidgetLike;
|
|
85
|
+
parent.append(
|
|
86
|
+
document.createElement('span'),
|
|
87
|
+
a,
|
|
88
|
+
document.createElement('div'),
|
|
89
|
+
b,
|
|
90
|
+
document.createElement('span'),
|
|
91
|
+
);
|
|
92
|
+
expect(parent.get_first_child()).toBe(a);
|
|
93
|
+
expect(parent.get_last_child()).toBe(b);
|
|
94
|
+
expect(a.get_next_sibling()).toBe(b);
|
|
95
|
+
expect(b.get_prev_sibling()).toBe(a);
|
|
96
|
+
expect(b.get_next_sibling()).toBe(null);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await it('a widget holding only its own markup is a LEAF', () => {
|
|
100
|
+
const label = make('WidgetMembersLeaf');
|
|
101
|
+
label.append(document.createElement('span'), document.createElement('span'));
|
|
102
|
+
expect(label.get_first_child()).toBe(null);
|
|
103
|
+
expect(label.get_last_child()).toBe(null);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await it('get_parent reports the nearest ancestor that is a widget', () => {
|
|
107
|
+
const box = new Gtk.Box() as unknown as WidgetLike;
|
|
108
|
+
const wrapper = document.createElement('div');
|
|
109
|
+
const label = new Gtk.Label() as unknown as WidgetLike;
|
|
110
|
+
box.append(wrapper);
|
|
111
|
+
wrapper.append(label);
|
|
112
|
+
expect(label.get_parent()).toBe(box);
|
|
113
|
+
});
|
|
114
|
+
|
|
76
115
|
await it('walks the element tree of a built-in widget with the GTK link names', () => {
|
|
77
116
|
const box = new Gtk.Box() as unknown as WidgetLike;
|
|
78
117
|
const a = new Gtk.Label() as unknown as WidgetLike;
|