@gjsify/adwaita-web 0.64.1 → 0.64.2-next.252
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/README.md +12 -8
- package/dist/adwaita-web.css +39 -9
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/application.d.ts +1 -1
- package/lib/types/child-slot.spec.d.ts +1 -0
- package/lib/types/container-fill.spec.d.ts +1 -0
- package/lib/types/elements/adw-alert-dialog.d.ts +77 -17
- package/lib/types/elements/adw-toast-overlay.d.ts +8 -29
- package/lib/types/elements/adw-window-features.d.ts +8 -0
- package/lib/types/elements/gtk-application-window.d.ts +1 -4
- package/lib/types/elements/gtk-emoji-chooser.d.ts +2 -0
- package/lib/types/elements/gtk-popover-menu.d.ts +2 -0
- package/lib/types/elements/gtk-popover.d.ts +5 -0
- package/lib/types/elements/gtk-stack-page.d.ts +28 -0
- package/lib/types/elements/gtk-stack.d.ts +11 -0
- package/lib/types/elements/gtk-window.d.ts +16 -1
- package/lib/types/gobject-elements.d.ts +9 -0
- package/lib/types/index.d.ts +2 -2
- package/lib/types/learn6502-window.spec.d.ts +1 -0
- package/lib/types/namespace/adw.d.ts +1 -1
- package/lib/types/namespace/gtk.d.ts +2 -0
- package/lib/types/shared-tree-builder.d.ts +31 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/lib/types/widget-visible.spec.d.ts +1 -0
- package/lib/types/window-maximize.spec.d.ts +1 -0
- package/package.json +13 -12
- package/scss/_bin.scss +6 -3
- package/scss/_overlay.scss +11 -1
- package/scss/_reset.scss +1 -1
- package/scss/_stack.scss +8 -2
- package/scss/_toast.scss +13 -3
- package/scss/_window.scss +16 -0
- package/src/adw-alert-dialog.spec.ts +71 -5
- package/src/adw-bin.spec.ts +4 -3
- package/src/adw-header-bar.spec.ts +18 -0
- package/src/adw-toast-overlay.spec.ts +68 -24
- package/src/application.spec.ts +44 -0
- package/src/application.ts +13 -18
- package/src/capabilities.mjs +1 -3
- package/src/child-slot.spec.ts +71 -0
- package/src/construct-vectors.spec.ts +51 -6
- package/src/container-fill.spec.ts +70 -0
- package/src/elements/adw-alert-dialog.ts +174 -44
- package/src/elements/adw-application-window.ts +0 -3
- package/src/elements/adw-breakpoint-bin.ts +3 -0
- package/src/elements/adw-dialog.ts +3 -1
- package/src/elements/adw-header-bar.ts +4 -0
- package/src/elements/adw-toast-overlay.ts +35 -69
- package/src/elements/adw-window-features.ts +14 -2
- package/src/elements/gtk-application-window.ts +3 -8
- package/src/elements/gtk-drag-icon.ts +4 -0
- package/src/elements/gtk-emoji-chooser.ts +5 -0
- package/src/elements/gtk-flow-box-child.ts +3 -1
- package/src/elements/gtk-graphics-offload.ts +3 -1
- package/src/elements/gtk-list-box-row.ts +3 -1
- package/src/elements/gtk-overlay.ts +5 -4
- package/src/elements/gtk-paned.ts +7 -1
- package/src/elements/gtk-popover-bin.ts +3 -0
- package/src/elements/gtk-popover-menu.ts +5 -0
- package/src/elements/gtk-popover.ts +12 -0
- package/src/elements/gtk-search-bar.ts +5 -5
- package/src/elements/gtk-stack-page.ts +111 -0
- package/src/elements/gtk-stack.ts +50 -5
- package/src/elements/gtk-tree-expander.ts +3 -0
- package/src/elements/gtk-viewport.ts +4 -0
- package/src/elements/gtk-window-handle.ts +4 -0
- package/src/elements/gtk-window.ts +55 -6
- package/src/gobject-elements.ts +28 -0
- package/src/gtk-stack.spec.ts +170 -0
- package/src/index.ts +2 -2
- package/src/keyboard-operable.spec.ts +3 -3
- package/src/learn6502-window.spec.ts +113 -0
- package/src/namespace/adw.ts +1 -1
- package/src/namespace/gtk.ts +2 -0
- package/src/shared-tree-builder.ts +105 -10
- package/src/shared-trees.spec.ts +15 -1
- package/src/slotted-children.spec.ts +24 -0
- package/src/slotted-children.ts +18 -2
- package/src/styles.generated.ts +1 -1
- package/src/test.browser.mts +10 -0
- package/src/value-lists.spec.ts +3 -3
- package/src/widget-visible.spec.ts +104 -0
- package/src/window-maximize.spec.ts +93 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// A `.blp`'s `child:` (and every other widget-valued property) becomes `slot: '<property>'` in the
|
|
2
|
+
// shared tree (`packages/infra/blueprint/src/project.mjs`). An element that routed only its
|
|
3
|
+
// DEFAULT slot refused that name on mount, and before the refusal existed it left the child
|
|
4
|
+
// beside the structure it had built — Learn6502's `Overlay { child: Adw.ToastOverlay { … } }`
|
|
5
|
+
// was the measured case. Every element whose GTK class has such a property is mounted here
|
|
6
|
+
// with it, and the child has to land where the element's own binding says that slot goes.
|
|
7
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
8
|
+
import type { SharedTreeNode } from '@gjsify/adwaita-core/conformance';
|
|
9
|
+
|
|
10
|
+
import { mountSharedTree } from './shared-tree-builder.js';
|
|
11
|
+
import { slottedChildrenOf } from './slotted-children.js';
|
|
12
|
+
|
|
13
|
+
/** The GIR class, and the widget-valued property the tree writes as its child's slot. */
|
|
14
|
+
const CASES: ReadonlyArray<readonly [string, string]> = [
|
|
15
|
+
['GtkOverlay', 'child'],
|
|
16
|
+
['AdwToastOverlay', 'child'],
|
|
17
|
+
['AdwDialog', 'child'],
|
|
18
|
+
['AdwAlertDialog', 'extra-child'],
|
|
19
|
+
['GtkListBoxRow', 'child'],
|
|
20
|
+
['GtkFlowBoxChild', 'child'],
|
|
21
|
+
['GtkGraphicsOffload', 'child'],
|
|
22
|
+
['GtkPaned', 'start-child'],
|
|
23
|
+
['GtkPaned', 'end-child'],
|
|
24
|
+
['GtkWindow', 'child'],
|
|
25
|
+
['GtkWindowHandle', 'child'],
|
|
26
|
+
['GtkPopover', 'child'],
|
|
27
|
+
['AdwBreakpointBin', 'child'],
|
|
28
|
+
['GtkPopoverBin', 'child'],
|
|
29
|
+
['GtkViewport', 'child'],
|
|
30
|
+
['GtkTreeExpander', 'child'],
|
|
31
|
+
['GtkDragIcon', 'child'],
|
|
32
|
+
['GtkSearchBar', 'child'],
|
|
33
|
+
['AdwBin', 'child'],
|
|
34
|
+
['AdwClamp', 'child'],
|
|
35
|
+
['GtkScrolledWindow', 'child'],
|
|
36
|
+
['GtkRevealer', 'child'],
|
|
37
|
+
['GtkFrame', 'child'],
|
|
38
|
+
['GtkExpander', 'child'],
|
|
39
|
+
['AdwStatusPage', 'child'],
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/** Where `child` ended up, read against the slot the parent itself declares. */
|
|
43
|
+
function report(root: HTMLElement, child: Element, slot: string): string {
|
|
44
|
+
const declared = slottedChildrenOf(root)?.slots.find((candidate) => candidate.name === slot);
|
|
45
|
+
if (declared === undefined) return 'undeclared';
|
|
46
|
+
if (!root.contains(child)) return 'outside';
|
|
47
|
+
if (!('into' in declared)) return 'consumed';
|
|
48
|
+
return child.parentElement === declared.into ? 'routed' : `beside, under <${child.parentElement?.localName}>`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const AdwChildSlotTest = async () => {
|
|
52
|
+
await describe('adwaita-web: a widget-valued property in a tree is the slot it names', async () => {
|
|
53
|
+
for (const [tag, slot] of CASES) {
|
|
54
|
+
await it(`${tag} { ${slot}: … }`, () => {
|
|
55
|
+
const tree: SharedTreeNode = {
|
|
56
|
+
tag,
|
|
57
|
+
children: [{ tag: 'GtkBox', id: 'probe', slot }],
|
|
58
|
+
};
|
|
59
|
+
let outcome: string;
|
|
60
|
+
try {
|
|
61
|
+
const { root, unmount } = mountSharedTree(tree);
|
|
62
|
+
outcome = report(root, root.querySelector('#probe')!, slot);
|
|
63
|
+
unmount();
|
|
64
|
+
} catch (error) {
|
|
65
|
+
outcome = `refused: ${(error as Error).message}`;
|
|
66
|
+
}
|
|
67
|
+
expect(`${tag} ${slot}: ${outcome}`).toBe(`${tag} ${slot}: routed`);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
};
|
|
@@ -13,7 +13,7 @@ import { describe, expect, it } from '@gjsify/unit';
|
|
|
13
13
|
|
|
14
14
|
import type { AdwAlertDialog } from './elements/adw-alert-dialog.js';
|
|
15
15
|
import { capabilities } from './capabilities.mjs';
|
|
16
|
-
import { buildSharedTree, mountSharedTree } from './shared-tree-builder.js';
|
|
16
|
+
import { buildSharedTree, buildWithSiblings, mountSharedTree } from './shared-tree-builder.js';
|
|
17
17
|
import { registerTemplateClass } from './template-classes.js';
|
|
18
18
|
|
|
19
19
|
const vector = (kind: string) => CONSTRUCT_VECTORS.find((each) => each.kind === kind)!.tree;
|
|
@@ -24,8 +24,21 @@ registerTemplateClass(EXTERN_VECTOR_CLASS, 'corpus-extern');
|
|
|
24
24
|
|
|
25
25
|
function observe(vector: ConstructVector): unknown {
|
|
26
26
|
if (vector.kind === 'sibling-object') {
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
let feed: ((size: { width: number; height: number }) => void) | undefined;
|
|
28
|
+
const { root, siblings } = buildWithSiblings(vector.tree, {
|
|
29
|
+
observeSize: (_element, onSize) => {
|
|
30
|
+
feed = onSize;
|
|
31
|
+
return () => {};
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
const beside = siblings.get('beside')!;
|
|
35
|
+
return {
|
|
36
|
+
insideRoot: root.contains(beside) || beside.parentElement !== null,
|
|
37
|
+
label: BREAKPOINT_VECTOR_SIZES.map((size) => {
|
|
38
|
+
feed!(size);
|
|
39
|
+
return beside.getAttribute('label');
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
29
42
|
}
|
|
30
43
|
if (vector.kind === 'layout') {
|
|
31
44
|
// CSS Grid lines count from 1, GTK cells from 0: `2 / span 2` is column 1, columnSpan 2.
|
|
@@ -99,9 +112,9 @@ function observe(vector: ConstructVector): unknown {
|
|
|
99
112
|
const dialog = root as unknown as AdwAlertDialog;
|
|
100
113
|
return (vector.tree.extensions?.responses ?? []).map(({ id }) => ({
|
|
101
114
|
id,
|
|
102
|
-
label: dialog.
|
|
103
|
-
appearance: dialog.
|
|
104
|
-
enabled: dialog.
|
|
115
|
+
label: dialog.get_response_label(id),
|
|
116
|
+
appearance: dialog.get_response_appearance(id),
|
|
117
|
+
enabled: dialog.get_response_enabled(id),
|
|
105
118
|
}));
|
|
106
119
|
} finally {
|
|
107
120
|
unmount();
|
|
@@ -331,6 +344,38 @@ export const AdwConstructVectorsTest = async () => {
|
|
|
331
344
|
expect(() => mountSharedTree(withBreakpoint('template'))).toThrow("id 'template'");
|
|
332
345
|
expect(() => mountSharedTree(withBreakpoint('caption', 'wide please'))).toThrow('breakpoint condition');
|
|
333
346
|
});
|
|
347
|
+
await it('hands a sibling back by id, a dialog included, and builds none of them into the root', () => {
|
|
348
|
+
const tree: SharedTreeNode = {
|
|
349
|
+
tag: 'GtkBox',
|
|
350
|
+
children: [{ tag: 'GtkLabel', id: 'inside' }],
|
|
351
|
+
siblings: [
|
|
352
|
+
{ tag: 'AdwAlertDialog', id: 'confirm', props: { heading: 'Save?' } },
|
|
353
|
+
{ tag: EXTERN_VECTOR_CLASS, extern: true, id: 'screen' },
|
|
354
|
+
],
|
|
355
|
+
};
|
|
356
|
+
const { root, siblings, objects } = buildWithSiblings(tree);
|
|
357
|
+
expect([...siblings.keys()].join()).toBe('confirm,screen');
|
|
358
|
+
expect(siblings.get('screen') instanceof CorpusExtern).toBe(true);
|
|
359
|
+
expect(siblings.get('confirm')!.localName).toBe('adw-alert-dialog');
|
|
360
|
+
expect(siblings.get('confirm')!.getAttribute('heading')).toBe('Save?');
|
|
361
|
+
expect(root.querySelector('#screen')).toBe(null);
|
|
362
|
+
expect([...objects.keys()].sort().join()).toBe('confirm,inside,screen');
|
|
363
|
+
});
|
|
364
|
+
await it('refuses siblings through every one-root door, a sibling without an id, and a duplicate id', () => {
|
|
365
|
+
const withSiblings = (siblings: SharedTreeNode[]): SharedTreeNode => ({
|
|
366
|
+
tag: 'GtkBox',
|
|
367
|
+
children: [{ tag: 'GtkLabel', id: 'taken' }],
|
|
368
|
+
siblings,
|
|
369
|
+
});
|
|
370
|
+
const labelled = withSiblings([{ tag: 'GtkLabel', id: 'beside' }]);
|
|
371
|
+
expect(() => buildSharedTree(labelled)).toThrow('buildWithSiblings');
|
|
372
|
+
expect(() => mountSharedTree(labelled)).toThrow('buildWithSiblings');
|
|
373
|
+
expect(() => buildWithSiblings(withSiblings([{ tag: 'GtkLabel' }]))).toThrow('has no id');
|
|
374
|
+
expect(() => buildWithSiblings(withSiblings([{ tag: 'GtkLabel', id: 'taken' }]))).toThrow("id 'taken'");
|
|
375
|
+
});
|
|
376
|
+
await it('refuses a breakpoint setter that names a sibling the tree does not carry', () => {
|
|
377
|
+
expect(() => buildWithSiblings({ ...vector('sibling-object'), siblings: [] })).toThrow("id 'beside'");
|
|
378
|
+
});
|
|
334
379
|
await it('does not read a registered name for a node that is not extern', () => {
|
|
335
380
|
const el = buildSharedTree({ tag: 'GtkBox', children: [{ tag: 'GtkLabel' }] });
|
|
336
381
|
expect(el.children[0]!.localName).toBe('gtk-label');
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Single-child containers hand their child the WHOLE allocation, on both axes: GtkOverlay's main
|
|
2
|
+
// child (gtkoverlay.c size_allocate), AdwToastOverlay's child, GtkStack's visible page
|
|
3
|
+
// (gtkstack.c:2778-2831) and AdwBin's child (gtkbinlayout.c:81). A block box gives a child only
|
|
4
|
+
// its content height, and a percentage resolves against an indefinite one — these specs pin the
|
|
5
|
+
// chain a real window builds: overlay > toast-overlay > stack > bin > view-stack.
|
|
6
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
7
|
+
|
|
8
|
+
function mount(html: string, width = 400, height = 300): { host: HTMLElement; box: (sel: string) => DOMRect } {
|
|
9
|
+
const host = document.createElement('div');
|
|
10
|
+
host.style.cssText = `width:${width}px;height:${height}px;display:flex;flex-direction:column`;
|
|
11
|
+
host.innerHTML = html;
|
|
12
|
+
document.body.appendChild(host);
|
|
13
|
+
const box = (sel: string) => (host.querySelector(sel) as HTMLElement).getBoundingClientRect();
|
|
14
|
+
return { host, box };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const ContainerFillTest = async () => {
|
|
18
|
+
await describe('single-child containers fill', async () => {
|
|
19
|
+
await it('<gtk-overlay> gives its main child the whole sized overlay', () => {
|
|
20
|
+
const { host, box } = mount('<gtk-overlay style="flex:1"><div id="main"></div></gtk-overlay>');
|
|
21
|
+
expect(box('#main').width).toBe(400);
|
|
22
|
+
expect(box('#main').height).toBe(300);
|
|
23
|
+
host.remove();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await it('<gtk-overlay> still measures as its main child when unsized', () => {
|
|
27
|
+
const { host, box } = mount('<gtk-overlay id="o"><div style="height:70px"></div></gtk-overlay>');
|
|
28
|
+
expect(box('#o').height).toBe(70);
|
|
29
|
+
host.remove();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await it('<adw-toast-overlay> gives its child the whole overlay', () => {
|
|
33
|
+
const { host, box } = mount('<adw-toast-overlay style="flex:1"><div id="c"></div></adw-toast-overlay>');
|
|
34
|
+
expect(box('#c').width).toBe(400);
|
|
35
|
+
expect(box('#c').height).toBe(300);
|
|
36
|
+
host.remove();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await it('a default <gtk-stack> page fills both axes, even with a zero-intrinsic child', () => {
|
|
40
|
+
const { host, box } = mount(
|
|
41
|
+
'<gtk-stack style="flex:1"><div id="p" name="a"><div id="z"></div></div></gtk-stack>',
|
|
42
|
+
);
|
|
43
|
+
expect(box('#p').width).toBe(400);
|
|
44
|
+
expect(box('#p').height).toBe(300);
|
|
45
|
+
host.remove();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await it('hhomogeneous="false" sizes a page to max-content', () => {
|
|
49
|
+
const { host, box } = mount(
|
|
50
|
+
'<gtk-stack hhomogeneous="false" style="flex:1"><div id="p" name="a" style="width:max-content"><span>hi</span></div></gtk-stack>',
|
|
51
|
+
);
|
|
52
|
+
expect(box('#p').width < 400).toBe(true);
|
|
53
|
+
host.remove();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await it('overlay > toast-overlay > stack > bin > view-stack fills the box', async () => {
|
|
57
|
+
const { host, box } = mount(
|
|
58
|
+
`<gtk-overlay style="flex:1"><adw-toast-overlay><gtk-stack>
|
|
59
|
+
<adw-bin id="b" name="a"><adw-view-stack id="v" style="flex:1"></adw-view-stack></adw-bin>
|
|
60
|
+
</gtk-stack></adw-toast-overlay></gtk-overlay>`,
|
|
61
|
+
);
|
|
62
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
63
|
+
for (const sel of ['adw-toast-overlay', 'gtk-stack', '#b', '#v']) {
|
|
64
|
+
expect(box(sel).width).toBe(400);
|
|
65
|
+
expect(box(sel).height).toBe(300);
|
|
66
|
+
}
|
|
67
|
+
host.remove();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
};
|
|
@@ -5,11 +5,20 @@
|
|
|
5
5
|
// with a unique string ID and a label; one may carry the suggested or
|
|
6
6
|
// destructive appearance.
|
|
7
7
|
//
|
|
8
|
-
// Responses are declared imperatively via add_response()
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
8
|
+
// Responses are declared imperatively via add_response() (the Adw.AlertDialog API),
|
|
9
|
+
// or via <adw-alert-response> children consumed at connect time. The dialog is
|
|
10
|
+
// hidden until `present()` (or setting the `open` attribute) reveals it; Escape /
|
|
11
|
+
// scrim-click dismiss it — respecting the `close-response` — unless can-close is
|
|
12
|
+
// suppressed by the close response.
|
|
13
|
+
//
|
|
14
|
+
// THE GJS FORM RUNS AS WRITTEN: `new Adw.AlertDialog({ heading, close_response })`,
|
|
15
|
+
// `add_response(id, label)`, `connect('response', (dialog, id) => …)`, `present(parent)`.
|
|
16
|
+
// The response verbs carry libadwaita's snake_case names only — the camelCase
|
|
17
|
+
// `addResponse` / `setDefaultResponse` … this element used to publish had no GIR
|
|
18
|
+
// counterpart. `present()` attaches a detached dialog to the document: libadwaita adds it
|
|
19
|
+
// to the dialog host of `parent`'s ROOT window, and the page is that root here. A dialog
|
|
20
|
+
// `present()` attached leaves the document again when it closes, as `adw_dialog_close`
|
|
21
|
+
// unparents it from the host; one placed in markup stays where its author put it.
|
|
13
22
|
//
|
|
14
23
|
// Attributes:
|
|
15
24
|
// heading (the bold heading text)
|
|
@@ -35,12 +44,12 @@
|
|
|
35
44
|
// sizes) and laid out horizontally when prefer-wide-layout is set and there are no more
|
|
36
45
|
// than two of them — the libadwaita measure/allocate heuristic in spirit, not to the px.
|
|
37
46
|
//
|
|
38
|
-
//
|
|
39
|
-
// `response
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// `notify::heading` / `notify::body`
|
|
43
|
-
//
|
|
47
|
+
// Signals (declared in `static signals`, reached with `connect`):
|
|
48
|
+
// `response (dialog, id)` when a response button is activated OR the dialog is
|
|
49
|
+
// dismissed (then `id` is the close-response) — Adw.AlertDialog::response. It
|
|
50
|
+
// arrives as a non-bubbling `response` DOM event whose `detail` is `[id]`.
|
|
51
|
+
// `notify::heading` / `notify::body` (CustomEvent, bubbles) mirroring the matching
|
|
52
|
+
// GObject properties; `notify::open` is the web-only `open` attribute's.
|
|
44
53
|
//
|
|
45
54
|
// The RESPONSE MODEL — the registry (id/label/appearance/enabled), the
|
|
46
55
|
// default/close-response semantics and the resolve-to-chosen-id contract — is HEADLESS
|
|
@@ -57,8 +66,11 @@
|
|
|
57
66
|
|
|
58
67
|
import { AdwAlertResponses } from '@gjsify/adwaita-core';
|
|
59
68
|
import type { AdwResponseAppearance } from '@gjsify/adwaita-core';
|
|
69
|
+
import { propertyOf } from '@gjsify/adwaita-core/tags';
|
|
60
70
|
|
|
71
|
+
import { dispatchElement, installElementApi } from '../gobject-elements.js';
|
|
61
72
|
import { renderPangoMarkup } from '../pango-markup.js';
|
|
73
|
+
import type { DispatchedSignals } from '../signals.js';
|
|
62
74
|
import { bindEmptySections } from '../empty-sections.js';
|
|
63
75
|
import { bindSlottedChildren } from '../slotted-children.js';
|
|
64
76
|
import { AdwModalSurface } from './modal-surface.js';
|
|
@@ -79,8 +91,55 @@ export class AdwAlertResponse extends HTMLElement {
|
|
|
79
91
|
}
|
|
80
92
|
}
|
|
81
93
|
|
|
94
|
+
/** The `Adw.AlertDialog` construct properties, in GJS's snake_case or the camelCase GJS also takes. */
|
|
95
|
+
export interface AdwAlertDialogProps {
|
|
96
|
+
heading?: string;
|
|
97
|
+
body?: string;
|
|
98
|
+
heading_use_markup?: boolean;
|
|
99
|
+
headingUseMarkup?: boolean;
|
|
100
|
+
body_use_markup?: boolean;
|
|
101
|
+
bodyUseMarkup?: boolean;
|
|
102
|
+
close_response?: string;
|
|
103
|
+
closeResponse?: string;
|
|
104
|
+
default_response?: string | null;
|
|
105
|
+
defaultResponse?: string | null;
|
|
106
|
+
prefer_wide_layout?: boolean;
|
|
107
|
+
preferWideLayout?: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
82
110
|
export class AdwAlertDialog extends HTMLElement {
|
|
111
|
+
/** The GTK signals this element dispatches, each with the DOM event it arrives as (ADR 0093). */
|
|
112
|
+
static readonly signals: DispatchedSignals = {
|
|
113
|
+
response: 'response',
|
|
114
|
+
...Object.fromEntries(
|
|
115
|
+
[
|
|
116
|
+
'heading',
|
|
117
|
+
'body',
|
|
118
|
+
'heading-use-markup',
|
|
119
|
+
'body-use-markup',
|
|
120
|
+
'close-response',
|
|
121
|
+
'default-response',
|
|
122
|
+
'prefer-wide-layout',
|
|
123
|
+
].map((name) => [`notify::${name}`, `notify::${name}`]),
|
|
124
|
+
),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** `GObject.Object.connect`: installed per instance by `installElementApi`; refuses an undeclared signal. */
|
|
128
|
+
declare connect: (signal: string, handler: (emitter: this, ...args: any[]) => unknown) => number;
|
|
129
|
+
/** `GObject.Object.disconnect`: throws for an id that is not connected. */
|
|
130
|
+
declare disconnect: (id: number) => void;
|
|
131
|
+
/** `GObject.Object.emit`. */
|
|
132
|
+
declare emit: (signal: string, ...args: unknown[]) => void;
|
|
133
|
+
/** `GObject.Object.notify`. */
|
|
134
|
+
declare notify: (name: string) => void;
|
|
135
|
+
/** `GObject.Object.get_property`, by GTK's dashed name. */
|
|
136
|
+
declare get_property: (name: string) => unknown;
|
|
137
|
+
/** `GObject.Object.set_property`. */
|
|
138
|
+
declare set_property: (name: string, value: unknown) => void;
|
|
139
|
+
|
|
83
140
|
private _initialized = false;
|
|
141
|
+
/** Set when `present()` attached the dialog, so closing detaches it again. */
|
|
142
|
+
private _attachedByPresent = false;
|
|
84
143
|
private _scrimEl!: HTMLDivElement;
|
|
85
144
|
private _dialogEl!: HTMLDivElement;
|
|
86
145
|
private _headingEl!: HTMLHeadingElement;
|
|
@@ -97,6 +156,22 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
97
156
|
// also fire the close-response (AdwAlertDialog's `block_close_response`).
|
|
98
157
|
private _blockCloseResponse = false;
|
|
99
158
|
|
|
159
|
+
/**
|
|
160
|
+
* `new Adw.AlertDialog({ heading, body, close_response, default_response })`, the GJS
|
|
161
|
+
* construct bag. Only the `new` form passes one: `document.createElement` and the
|
|
162
|
+
* parser call this with nothing, which is why writing attributes here is allowed.
|
|
163
|
+
*/
|
|
164
|
+
constructor(props?: AdwAlertDialogProps) {
|
|
165
|
+
super();
|
|
166
|
+
installElementApi(this);
|
|
167
|
+
for (const [key, value] of Object.entries(props ?? {})) {
|
|
168
|
+
if (value === undefined) continue;
|
|
169
|
+
const member = propertyOf(key);
|
|
170
|
+
if (!(member in this)) throw new Error(`No property ${key} on <adw-alert-dialog>`);
|
|
171
|
+
(this as unknown as Record<string, unknown>)[member] = value;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
100
175
|
static get observedAttributes() {
|
|
101
176
|
return [
|
|
102
177
|
'heading',
|
|
@@ -171,7 +246,7 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
171
246
|
}
|
|
172
247
|
|
|
173
248
|
set defaultResponse(value: string | null) {
|
|
174
|
-
this.
|
|
249
|
+
this.set_default_response(value);
|
|
175
250
|
}
|
|
176
251
|
|
|
177
252
|
/** The ID passed to `response` when the dialog is dismissed. */
|
|
@@ -180,7 +255,7 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
180
255
|
}
|
|
181
256
|
|
|
182
257
|
set closeResponse(value: string) {
|
|
183
|
-
this.
|
|
258
|
+
this.set_close_response(value);
|
|
184
259
|
}
|
|
185
260
|
|
|
186
261
|
connectedCallback() {
|
|
@@ -239,6 +314,7 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
239
314
|
claims: (node) => node instanceof Element && node.localName === 'adw-alert-response',
|
|
240
315
|
consume: (node) => this._adoptResponse(node as AdwAlertResponse),
|
|
241
316
|
},
|
|
317
|
+
{ name: 'extra-child', into: this._childEl },
|
|
242
318
|
{ into: this._childEl },
|
|
243
319
|
]).install(this._scrimEl, this._dialogEl);
|
|
244
320
|
|
|
@@ -261,7 +337,9 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
261
337
|
// two live in the model and would simply be dropped.
|
|
262
338
|
const closeResponse = this.getAttribute('close-response');
|
|
263
339
|
if (closeResponse !== null) this._model.closeResponse = closeResponse;
|
|
264
|
-
|
|
340
|
+
// An absent attribute leaves a `default_response` the construct bag already set.
|
|
341
|
+
const defaultResponse = this.getAttribute('default-response');
|
|
342
|
+
if (defaultResponse !== null) this._model.defaultResponse = defaultResponse;
|
|
265
343
|
|
|
266
344
|
this._render();
|
|
267
345
|
}
|
|
@@ -275,11 +353,11 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
275
353
|
const id = el.getAttribute('id');
|
|
276
354
|
if (!id) return;
|
|
277
355
|
const appearance = (el.getAttribute('appearance') ?? 'default') as AdwResponseAppearance;
|
|
278
|
-
this.
|
|
279
|
-
if (appearance !== 'default') this.
|
|
356
|
+
this.add_response(id, (el.textContent ?? '').trim());
|
|
357
|
+
if (appearance !== 'default') this.set_response_appearance(id, appearance);
|
|
280
358
|
// `enabled` is a value-based boolean attribute (GTK property defaulting to TRUE).
|
|
281
359
|
// Use the shared reader instead of a hand-rolled `=== 'false'` comparison.
|
|
282
|
-
if (!booleanAttribute(el.getAttribute('enabled'), true)) this.
|
|
360
|
+
if (!booleanAttribute(el.getAttribute('enabled'), true)) this.set_response_enabled(id, false);
|
|
283
361
|
}
|
|
284
362
|
|
|
285
363
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
|
|
@@ -301,22 +379,38 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
301
379
|
// header) — the attribute is a markup entry point that feeds it, so the
|
|
302
380
|
// setter is not mirrored back and there is no reflection loop. An absent
|
|
303
381
|
// attribute means "leave the model's default", not "reset to `close`".
|
|
304
|
-
if (newValue !== null) this.
|
|
382
|
+
if (newValue !== null) this.set_close_response(newValue);
|
|
305
383
|
} else if (name === 'default-response') {
|
|
306
|
-
this.
|
|
384
|
+
this.set_default_response(newValue);
|
|
307
385
|
} else if (name === 'open') {
|
|
308
386
|
this.dispatchEvent(new CustomEvent('notify::open', { bubbles: true, detail: { open: this.open } }));
|
|
309
|
-
if (this.open)
|
|
310
|
-
|
|
387
|
+
if (this.open) {
|
|
388
|
+
this._modal.present();
|
|
389
|
+
} else {
|
|
390
|
+
this._modal.dismiss();
|
|
391
|
+
if (this._attachedByPresent) {
|
|
392
|
+
this._attachedByPresent = false;
|
|
393
|
+
this.remove();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
311
396
|
}
|
|
312
397
|
}
|
|
313
398
|
|
|
314
|
-
/**
|
|
399
|
+
/**
|
|
400
|
+
* Present the dialog (`adw_dialog_present`). A detached dialog is attached to the document
|
|
401
|
+
* first and detached again when it closes; `parent` names the window it belongs to, and the
|
|
402
|
+
* page is that window's root here, so it does not change where the dialog goes.
|
|
403
|
+
*/
|
|
315
404
|
present(_parent?: unknown): void {
|
|
405
|
+
if (!this.isConnected) {
|
|
406
|
+
document.body.appendChild(this);
|
|
407
|
+
this._attachedByPresent = true;
|
|
408
|
+
}
|
|
316
409
|
this.open = true;
|
|
317
410
|
}
|
|
318
411
|
|
|
319
|
-
|
|
412
|
+
/** `adw_alert_dialog_add_response`: a duplicate id is refused, as libadwaita's g_critical does. */
|
|
413
|
+
add_response(id: string, label: string): void {
|
|
320
414
|
// The core registry UPDATES a duplicate id in place; AdwAlertDialog refuses it,
|
|
321
415
|
// so the guard stays here.
|
|
322
416
|
if (this._model.hasResponse(id)) {
|
|
@@ -339,15 +433,14 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
339
433
|
this._render();
|
|
340
434
|
}
|
|
341
435
|
|
|
342
|
-
/**
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
436
|
+
/** `adw_alert_dialog_remove_response`. */
|
|
437
|
+
remove_response(id: string): void {
|
|
438
|
+
if (!this._model.hasResponse(id)) {
|
|
439
|
+
console.warn(
|
|
440
|
+
`[adw-alert-dialog] Trying to remove a response with id '${id}', but such a response does not exist`,
|
|
441
|
+
);
|
|
442
|
+
return;
|
|
346
443
|
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
removeResponse(id: string): void {
|
|
350
|
-
if (!this._model.hasResponse(id)) return;
|
|
351
444
|
this._buttons.get(id)?.remove();
|
|
352
445
|
this._buttons.delete(id);
|
|
353
446
|
// `AdwAlertResponses` exposes no removal (a gap against
|
|
@@ -367,15 +460,18 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
367
460
|
this._render();
|
|
368
461
|
}
|
|
369
462
|
|
|
370
|
-
|
|
463
|
+
/** `adw_alert_dialog_has_response`. */
|
|
464
|
+
has_response(id: string): boolean {
|
|
371
465
|
return this._model.hasResponse(id);
|
|
372
466
|
}
|
|
373
467
|
|
|
374
|
-
|
|
468
|
+
/** `adw_alert_dialog_get_response_label`; `null` for an unknown id, as the C returns NULL. */
|
|
469
|
+
get_response_label(id: string): string | null {
|
|
375
470
|
return this._model.responses.find((response) => response.id === id)?.label ?? null;
|
|
376
471
|
}
|
|
377
472
|
|
|
378
|
-
|
|
473
|
+
/** `adw_alert_dialog_set_response_label`. */
|
|
474
|
+
set_response_label(id: string, label: string): void {
|
|
379
475
|
const button = this._buttons.get(id);
|
|
380
476
|
if (!button) return;
|
|
381
477
|
// Re-adding a known id updates it in place (the core registry contract).
|
|
@@ -383,11 +479,13 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
383
479
|
button.textContent = label;
|
|
384
480
|
}
|
|
385
481
|
|
|
386
|
-
|
|
482
|
+
/** `adw_alert_dialog_get_response_appearance`; `null` for an unknown id. */
|
|
483
|
+
get_response_appearance(id: string): AdwResponseAppearance | null {
|
|
387
484
|
return this._model.hasResponse(id) ? this._model.getResponseAppearance(id) : null;
|
|
388
485
|
}
|
|
389
486
|
|
|
390
|
-
|
|
487
|
+
/** `adw_alert_dialog_set_response_appearance`. */
|
|
488
|
+
set_response_appearance(id: string, appearance: AdwResponseAppearance): void {
|
|
391
489
|
const button = this._buttons.get(id);
|
|
392
490
|
if (!button) return;
|
|
393
491
|
this._model.setResponseAppearance(id, appearance);
|
|
@@ -395,31 +493,63 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
395
493
|
button.classList.toggle('destructive-action', appearance === 'destructive');
|
|
396
494
|
}
|
|
397
495
|
|
|
398
|
-
|
|
496
|
+
/** `adw_alert_dialog_get_response_enabled`; `false` for an unknown id, as the C returns FALSE. */
|
|
497
|
+
get_response_enabled(id: string): boolean {
|
|
399
498
|
return this._model.hasResponse(id) ? this._model.getResponseEnabled(id) : false;
|
|
400
499
|
}
|
|
401
500
|
|
|
402
|
-
/**
|
|
403
|
-
|
|
501
|
+
/** `adw_alert_dialog_set_response_enabled`: a disabled button cannot be activated. */
|
|
502
|
+
set_response_enabled(id: string, enabled: boolean): void {
|
|
404
503
|
const button = this._buttons.get(id);
|
|
405
504
|
if (!button) return;
|
|
406
505
|
this._model.setResponseEnabled(id, enabled);
|
|
407
506
|
button.disabled = !enabled;
|
|
408
507
|
}
|
|
409
508
|
|
|
410
|
-
/**
|
|
411
|
-
|
|
509
|
+
/** `adw_alert_dialog_get_default_response`. */
|
|
510
|
+
get_default_response(): string | null {
|
|
511
|
+
return this._model.defaultResponse;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** `adw_alert_dialog_set_default_response`: its button is highlighted and takes the focus. */
|
|
515
|
+
set_default_response(id: string | null): void {
|
|
412
516
|
this._model.defaultResponse = id;
|
|
413
517
|
this._render();
|
|
414
518
|
}
|
|
415
519
|
|
|
416
|
-
/**
|
|
417
|
-
|
|
520
|
+
/** `adw_alert_dialog_get_close_response`. */
|
|
521
|
+
get_close_response(): string {
|
|
522
|
+
return this._model.closeResponse;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** `adw_alert_dialog_set_close_response`: emitted on dismissal (Escape / scrim click). */
|
|
526
|
+
set_close_response(id: string): void {
|
|
418
527
|
this._model.closeResponse = id;
|
|
419
528
|
}
|
|
420
529
|
|
|
530
|
+
/** `adw_alert_dialog_get_heading`. */
|
|
531
|
+
get_heading(): string {
|
|
532
|
+
return this.heading;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** `adw_alert_dialog_set_heading`. */
|
|
536
|
+
set_heading(heading: string): void {
|
|
537
|
+
this.heading = heading;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** `adw_alert_dialog_get_body`. */
|
|
541
|
+
get_body(): string {
|
|
542
|
+
return this.body;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** `adw_alert_dialog_set_body`. */
|
|
546
|
+
set_body(body: string): void {
|
|
547
|
+
this.body = body;
|
|
548
|
+
}
|
|
549
|
+
|
|
421
550
|
private _activateResponse(id: string): void {
|
|
422
551
|
if (!this._model.getResponseEnabled(id)) return;
|
|
552
|
+
// `emit_response`: block the close-response, close, then emit the chosen one.
|
|
423
553
|
this._blockCloseResponse = true;
|
|
424
554
|
this.open = false;
|
|
425
555
|
this._blockCloseResponse = false;
|
|
@@ -438,7 +568,7 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
438
568
|
}
|
|
439
569
|
|
|
440
570
|
private _emitResponse(response: string): void {
|
|
441
|
-
this
|
|
571
|
+
dispatchElement(this, 'response', [response]);
|
|
442
572
|
}
|
|
443
573
|
|
|
444
574
|
/**
|
|
@@ -45,7 +45,6 @@ import type { AdwBreakpoint } from '@gjsify/adwaita-core';
|
|
|
45
45
|
import type { AdwSlot } from '../slotted-children.js';
|
|
46
46
|
import { AdwWindowFeatures } from './adw-window-features.js';
|
|
47
47
|
import { GtkApplicationWindow } from './gtk-application-window.js';
|
|
48
|
-
import { defineWindowApplication } from '../application.js';
|
|
49
48
|
|
|
50
49
|
export class AdwApplicationWindow extends GtkApplicationWindow {
|
|
51
50
|
private readonly _features = new AdwWindowFeatures(this);
|
|
@@ -232,5 +231,3 @@ export class AdwApplicationWindow extends GtkApplicationWindow {
|
|
|
232
231
|
}
|
|
233
232
|
|
|
234
233
|
customElements.define('adw-application-window', AdwApplicationWindow);
|
|
235
|
-
|
|
236
|
-
defineWindowApplication(AdwApplicationWindow.prototype);
|
|
@@ -67,6 +67,7 @@ import { BreakpointBinState } from '@gjsify/adwaita-core';
|
|
|
67
67
|
import type { BreakpointSetter } from '@gjsify/adwaita-core';
|
|
68
68
|
|
|
69
69
|
import { observeAdaptiveSize } from '../breakpoints.js';
|
|
70
|
+
import { bindSlottedChildren } from '../slotted-children.js';
|
|
70
71
|
|
|
71
72
|
/** One authored setter, as it reads in the `breakpoints` attribute. */
|
|
72
73
|
interface AuthoredSetter {
|
|
@@ -157,6 +158,8 @@ export class AdwBreakpointBin extends HTMLElement {
|
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
connectedCallback() {
|
|
161
|
+
// The NAME only, for the reason `adw-bin.ts` gives: the child already IS the content.
|
|
162
|
+
bindSlottedChildren(this, [{ name: 'child', into: this }]);
|
|
160
163
|
this.setAttribute('role', 'group');
|
|
161
164
|
// A GTK widget is a box and an unknown custom element is `inline`, which measures
|
|
162
165
|
// zero — and a bin measuring zero matches every condition forever. libadwaita
|
|
@@ -196,7 +196,9 @@ export class AdwDialog extends HTMLElement {
|
|
|
196
196
|
// Everything the author places inside is the dialog content — LIVE, because
|
|
197
197
|
// `Adw.Dialog:child` is a property and a dialog whose body is filled in after it
|
|
198
198
|
// was created is the ordinary case. `src/slotted-children.ts` has the incident.
|
|
199
|
-
bindSlottedChildren(this, [{ into: this._contentEl }]).install(
|
|
199
|
+
bindSlottedChildren(this, [{ name: 'child', into: this._contentEl }, { into: this._contentEl }]).install(
|
|
200
|
+
this._scrimEl,
|
|
201
|
+
);
|
|
200
202
|
|
|
201
203
|
this._render();
|
|
202
204
|
}
|
|
@@ -119,6 +119,10 @@ export class AdwHeaderBar extends HTMLElement {
|
|
|
119
119
|
// gallery's own fences are written in.
|
|
120
120
|
{ name: 'title-widget', into: this._centerEl },
|
|
121
121
|
{ name: 'end', into: this._endEl },
|
|
122
|
+
// An untyped `<child>` packs from the start (adw-header-bar.c:1040-1041). A
|
|
123
|
+
// Blueprint `[start]` tags only the NEXT child, so every button after it
|
|
124
|
+
// arrives untyped and had no destination at all.
|
|
125
|
+
{ into: this._startEl },
|
|
122
126
|
],
|
|
123
127
|
(_node, slot) => {
|
|
124
128
|
if ('into' in slot && slot.into === this._centerEl) this._dropDerivedTitle();
|