@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 @@
|
|
|
1
|
+
export declare const AdwWidgetVisibleTest: () => Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const WindowMaximizeTest: () => Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/adwaita-web",
|
|
3
|
-
"version": "0.64.
|
|
3
|
+
"version": "0.64.2-next.252",
|
|
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": "
|
|
62
|
-
"@gjsify/adwaita-fonts": "
|
|
63
|
-
"@gjsify/adwaita-icons": "
|
|
64
|
-
"@gjsify/gtksource-core": "
|
|
61
|
+
"@gjsify/adwaita-core": "0.64.2-next.252",
|
|
62
|
+
"@gjsify/adwaita-fonts": "0.64.2-next.252",
|
|
63
|
+
"@gjsify/adwaita-icons": "0.64.2-next.252",
|
|
64
|
+
"@gjsify/gtksource-core": "0.64.2-next.252",
|
|
65
65
|
"@lezer/highlight": "^1.2.3"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
|
-
"@gjsify/app-settings": "
|
|
68
|
+
"@gjsify/app-settings": "0.64.2-next.252"
|
|
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": "
|
|
77
|
-
"@gjsify/blueprint": "
|
|
78
|
-
"@gjsify/cli": "
|
|
79
|
-
"@gjsify/unit": "
|
|
80
|
-
"@gjsify/vite-plugin-blueprint": "
|
|
76
|
+
"@gjsify/app-settings": "0.64.2-next.252",
|
|
77
|
+
"@gjsify/blueprint": "0.64.2-next.252",
|
|
78
|
+
"@gjsify/cli": "0.64.2-next.252",
|
|
79
|
+
"@gjsify/unit": "0.64.2-next.252",
|
|
80
|
+
"@gjsify/vite-plugin-blueprint": "0.64.2-next.252",
|
|
81
81
|
"sass": "^1.101.0",
|
|
82
82
|
"typescript": "^6.0.3"
|
|
83
83
|
},
|
|
@@ -114,5 +114,6 @@
|
|
|
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"
|
|
117
|
+
"homepage": "https://github.com/gjsify/gjsify/tree/main/packages/web/adwaita-web#readme",
|
|
118
|
+
"gitHead": "3853c65f772d30460e3894ff08a6079a70d7b206"
|
|
118
119
|
}
|
package/scss/_bin.scss
CHANGED
|
@@ -14,13 +14,16 @@
|
|
|
14
14
|
// Modifications: Adapted for @gjsify/adwaita-web Web Components.
|
|
15
15
|
|
|
16
16
|
adw-bin {
|
|
17
|
-
display:
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
18
19
|
|
|
19
20
|
// One child, and it fills — `gtk_bin_layout_allocate` allocates the whole widget to
|
|
20
21
|
// its single child. `:only-child` rather than `> *` because a bin keeps ONE
|
|
21
22
|
// (`adw_bin_set_child` unparents the one it replaces), so the case where the selector
|
|
22
|
-
// could match two is one the element has already refused.
|
|
23
|
+
// could match two is one the element has already refused. Flex stretch rather than
|
|
24
|
+
// `block-size: 100%`: a percentage needs a definite parent height, which a bin that is
|
|
25
|
+
// itself a flex item (a stack page) only has by the stretch algorithm.
|
|
23
26
|
> :only-child {
|
|
24
|
-
|
|
27
|
+
flex: 1 1 auto;
|
|
25
28
|
}
|
|
26
29
|
}
|
package/scss/_overlay.scss
CHANGED
|
@@ -38,13 +38,23 @@
|
|
|
38
38
|
// Modifications: Adapted for @gjsify/adwaita-web Web Components.
|
|
39
39
|
|
|
40
40
|
gtk-overlay {
|
|
41
|
-
|
|
41
|
+
// A column flex box rather than a block: `gtk_overlay_size_allocate` hands the main child
|
|
42
|
+
// the overlay's WHOLE allocation (gtkoverlay.c), and a block would give it only its content
|
|
43
|
+
// height. With an auto height the overlay still measures as its child (the child's
|
|
44
|
+
// `flex-basis: auto` is the content), with a definite one the child grows to fill it.
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
42
47
|
position: relative;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
.adw-overlay-child {
|
|
46
51
|
// The main child, in the host's own flow — see the header.
|
|
47
52
|
display: contents;
|
|
53
|
+
|
|
54
|
+
// Fill the allocation on the main axis; the cross axis stretches by default.
|
|
55
|
+
> * {
|
|
56
|
+
flex: 1 1 auto;
|
|
57
|
+
}
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
.adw-overlay-layer {
|
package/scss/_reset.scss
CHANGED
|
@@ -48,7 +48,7 @@ $adw-components:
|
|
|
48
48
|
gtk-link-button, gtk-list-box, gtk-list-box-row, gtk-list-view, gtk-media-controls, gtk-menu-button, gtk-overlay, gtk-page-setup-unix-dialog, gtk-paned,
|
|
49
49
|
gtk-password-entry, gtk-picture, gtk-popover, gtk-popover-bin, gtk-popover-menu, gtk-popover-menu-bar, gtk-print-unix-dialog,
|
|
50
50
|
gtk-progress-bar, gtk-revealer, gtk-scale, gtk-scale-button, gtk-scrollbar, gtk-scrolled-window, gtk-search-bar, gtk-search-entry, gtk-separator, gtk-source-view,
|
|
51
|
-
gtk-spin-button, gtk-spinner, gtk-stack, gtk-stack-sidebar, gtk-stack-switcher, gtk-switch, gtk-text, gtk-text-view, gtk-toggle-button, gtk-tree-expander, gtk-video, gtk-viewport,
|
|
51
|
+
gtk-spin-button, gtk-spinner, gtk-stack, gtk-stack-page, gtk-stack-sidebar, gtk-stack-switcher, gtk-switch, gtk-text, gtk-text-view, gtk-toggle-button, gtk-tree-expander, gtk-video, gtk-viewport,
|
|
52
52
|
gtk-window, gtk-window-controls, gtk-window-handle;
|
|
53
53
|
|
|
54
54
|
// Roots: re-root the inherited typographic properties (and the box model) so a
|
package/scss/_stack.scss
CHANGED
|
@@ -60,13 +60,19 @@ gtk-stack {
|
|
|
60
60
|
// looks at the VISIBLE child alone on that axis (gtkstack.c:2899-2905) — the stack is
|
|
61
61
|
// as big as the current page. Turned off, the stack is as big as the LARGEST page,
|
|
62
62
|
// which is `max-content` on the cross axis and `flex: 1 1 auto` along the main one.
|
|
63
|
-
|
|
63
|
+
//
|
|
64
|
+
// The element reflects the flag as an ABSENT attribute or `=""` for TRUE and
|
|
65
|
+
// `="false"` for FALSE (`_flag`: `raw !== 'false'`, absent → the default TRUE), so
|
|
66
|
+
// only the literal `'false'` is the non-homogeneous state. `:not([hhomogeneous])`
|
|
67
|
+
// read a missing attribute as false and gave every default stack's page
|
|
68
|
+
// `width: max-content`, a zero-width page for a child with no intrinsic width.
|
|
69
|
+
&[hhomogeneous='false'] > .adw-stack-page {
|
|
64
70
|
flex: 0 1 auto;
|
|
65
71
|
width: max-content;
|
|
66
72
|
max-width: 100%;
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
|
|
75
|
+
&[vhomogeneous='false'] > .adw-stack-page {
|
|
70
76
|
height: max-content;
|
|
71
77
|
max-height: 100%;
|
|
72
78
|
}
|
package/scss/_toast.scss
CHANGED
|
@@ -12,12 +12,22 @@
|
|
|
12
12
|
|
|
13
13
|
adw-toast-overlay {
|
|
14
14
|
position: relative;
|
|
15
|
-
display:
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
16
17
|
|
|
17
|
-
// The wrapped content (Adw.ToastOverlay :child) occupies the overlay's box
|
|
18
|
+
// The wrapped content (Adw.ToastOverlay :child) occupies the overlay's box: AdwToastOverlay
|
|
19
|
+
// allocates its child the full size (adw-toast-overlay.c size_allocate). A percentage height
|
|
20
|
+
// resolves against an indefinite one inside a block parent, so the fill is flex stretch.
|
|
18
21
|
.adw-toast-overlay-content {
|
|
22
|
+
display: flex;
|
|
23
|
+
flex: 1 1 auto;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
min-height: 0;
|
|
19
26
|
width: 100%;
|
|
20
|
-
|
|
27
|
+
|
|
28
|
+
> * {
|
|
29
|
+
flex: 1 1 auto;
|
|
30
|
+
}
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
// The toast slot: layered over the bottom edge of the overlay and centered,
|
package/scss/_window.scss
CHANGED
|
@@ -43,6 +43,15 @@ adw-application-window {
|
|
|
43
43
|
box-shadow: none;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// A maximized or fullscreen window ignores its default size and fills the work area
|
|
47
|
+
// (gtkwindow.c); on a page that is the containing block. The element drops its inline
|
|
48
|
+
// `default-width`/`default-height` in these states so this can apply, and keeps the minimum.
|
|
49
|
+
&[maximized]:not([maximized="false"]),
|
|
50
|
+
&[fullscreened]:not([fullscreened="false"]) {
|
|
51
|
+
width: 100%;
|
|
52
|
+
height: 100%;
|
|
53
|
+
}
|
|
54
|
+
|
|
46
55
|
// `present()` and `hide-on-close` toggle `hidden`, which `display: flex` would override.
|
|
47
56
|
&[hidden] {
|
|
48
57
|
display: none;
|
|
@@ -110,6 +119,13 @@ gtk-application-window {
|
|
|
110
119
|
box-shadow: none;
|
|
111
120
|
}
|
|
112
121
|
|
|
122
|
+
// The same fill as `adw-window` above: the default size is only the restored size.
|
|
123
|
+
&[maximized]:not([maximized="false"]),
|
|
124
|
+
&[fullscreened]:not([fullscreened="false"]) {
|
|
125
|
+
width: 100%;
|
|
126
|
+
height: 100%;
|
|
127
|
+
}
|
|
128
|
+
|
|
113
129
|
// The strip: an application's `titlebar` slot, or the two window-control groups and the
|
|
114
130
|
// window's own title. `min-width: 0` on the title is what lets a long one ellipsize
|
|
115
131
|
// instead of pushing the end controls out of the frame.
|
|
@@ -124,8 +124,8 @@ export const AdwAlertDialogTest = async () => {
|
|
|
124
124
|
// directly would assert a contract this element does not have.
|
|
125
125
|
const { el, host } = mount({ 'close-response': 'cancel', open: '' });
|
|
126
126
|
let seen: string | null = null;
|
|
127
|
-
el.
|
|
128
|
-
seen =
|
|
127
|
+
el.connect('response', (_dialog, response: string) => {
|
|
128
|
+
seen = response;
|
|
129
129
|
});
|
|
130
130
|
(el.querySelector('.adw-alert-dialog-scrim') as HTMLElement).click();
|
|
131
131
|
expect(seen).toBe('cancel');
|
|
@@ -147,9 +147,9 @@ export const AdwAlertDialogTest = async () => {
|
|
|
147
147
|
await describe('adw-alert-dialog responses before the first connect', async () => {
|
|
148
148
|
await it('buttons added while detached appear once connected', () => {
|
|
149
149
|
const el = document.createElement('adw-alert-dialog') as AdwAlertDialog;
|
|
150
|
-
el.
|
|
151
|
-
el.
|
|
152
|
-
expect(el.
|
|
150
|
+
el.add_response('cancel', 'Cancel');
|
|
151
|
+
el.add_response('ok', 'OK');
|
|
152
|
+
expect(el.get_response_label('ok')).toBe('OK');
|
|
153
153
|
const host = document.createElement('div');
|
|
154
154
|
document.body.appendChild(host);
|
|
155
155
|
host.appendChild(el);
|
|
@@ -158,4 +158,70 @@ export const AdwAlertDialogTest = async () => {
|
|
|
158
158
|
host.remove();
|
|
159
159
|
});
|
|
160
160
|
});
|
|
161
|
+
|
|
162
|
+
// The calls a GJS app makes (Learn6502's `notification.service.ts`), run as written.
|
|
163
|
+
await describe('adw-alert-dialog in the GJS construct-props form', async () => {
|
|
164
|
+
const Dialog = customElements.get('adw-alert-dialog') as unknown as new (
|
|
165
|
+
props?: Record<string, unknown>,
|
|
166
|
+
) => AdwAlertDialog;
|
|
167
|
+
|
|
168
|
+
const confirmDialog = () => {
|
|
169
|
+
const dialog = new Dialog({
|
|
170
|
+
heading: 'Reset?',
|
|
171
|
+
body: 'This clears the memory.',
|
|
172
|
+
close_response: 'cancel',
|
|
173
|
+
default_response: 'confirm',
|
|
174
|
+
});
|
|
175
|
+
dialog.add_response('cancel', 'Cancel');
|
|
176
|
+
dialog.add_response('confirm', 'Confirm');
|
|
177
|
+
return dialog;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
await it('takes the snake_case bag', () => {
|
|
181
|
+
const dialog = confirmDialog();
|
|
182
|
+
expect(dialog.heading).toBe('Reset?');
|
|
183
|
+
expect(dialog.body).toBe('This clears the memory.');
|
|
184
|
+
expect(dialog.get_close_response()).toBe('cancel');
|
|
185
|
+
expect(dialog.get_default_response()).toBe('confirm');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await it('refuses a key that is no Adw.AlertDialog property', () => {
|
|
189
|
+
expect(() => new Dialog({ no_such_prop: 1 })).toThrow();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
await it('present() attaches it, a click emits response with the id, and closing detaches it', () => {
|
|
193
|
+
const dialog = confirmDialog();
|
|
194
|
+
const seen: unknown[][] = [];
|
|
195
|
+
dialog.connect('response', (emitter, response: string) => seen.push([emitter, response]));
|
|
196
|
+
dialog.present(null);
|
|
197
|
+
expect(dialog.isConnected).toBe(true);
|
|
198
|
+
expect(dialog.open).toBe(true);
|
|
199
|
+
const buttons = [...dialog.querySelectorAll('.adw-alert-dialog-response')] as HTMLButtonElement[];
|
|
200
|
+
buttons.find((b) => b.textContent === 'Confirm')!.click();
|
|
201
|
+
expect(seen.length).toBe(1);
|
|
202
|
+
expect(seen[0]![0]).toBe(dialog);
|
|
203
|
+
expect(seen[0]![1]).toBe('confirm');
|
|
204
|
+
expect(dialog.open).toBe(false);
|
|
205
|
+
expect(dialog.isConnected).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
await it('a dismissal emits the close_response once, not the clicked one too', () => {
|
|
209
|
+
const dialog = confirmDialog();
|
|
210
|
+
const seen: string[] = [];
|
|
211
|
+
dialog.connect('response', (_dialog, response: string) => seen.push(response));
|
|
212
|
+
dialog.present(null);
|
|
213
|
+
(dialog.querySelector('.adw-alert-dialog-scrim') as HTMLElement).click();
|
|
214
|
+
expect(seen.join(',')).toBe('cancel');
|
|
215
|
+
expect(dialog.isConnected).toBe(false);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
await it('remove_response drops the response and its button', () => {
|
|
219
|
+
const dialog = confirmDialog();
|
|
220
|
+
dialog.present(null);
|
|
221
|
+
dialog.remove_response('confirm');
|
|
222
|
+
expect(dialog.has_response('confirm')).toBe(false);
|
|
223
|
+
expect(dialog.querySelectorAll('.adw-alert-dialog-response').length).toBe(1);
|
|
224
|
+
dialog.remove();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
161
227
|
};
|
package/src/adw-bin.spec.ts
CHANGED
|
@@ -61,12 +61,13 @@ export const AdwBinTest = async () => {
|
|
|
61
61
|
other.remove();
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
await it('
|
|
64
|
+
await it('a bin is a column flex box, and its only child fills it', () => {
|
|
65
65
|
const { el, host } = mount([document.createElement('div')]);
|
|
66
66
|
// A bin takes the size it is GIVEN and hands all of it to the child, which
|
|
67
|
-
// `GtkBinLayout` does on both axes (gtkbinlayout.c:81).
|
|
67
|
+
// `GtkBinLayout` does on both axes (gtkbinlayout.c:81). Flex stretch, not a
|
|
68
|
+
// percentage, so it also holds when the bin is itself a flex item.
|
|
68
69
|
el.style.height = '120px';
|
|
69
|
-
expect(getComputedStyle(el).display).toBe('
|
|
70
|
+
expect(getComputedStyle(el).display).toBe('flex');
|
|
70
71
|
const child = el.firstElementChild as HTMLElement;
|
|
71
72
|
expect(child.getBoundingClientRect().height).toBe(120);
|
|
72
73
|
host.remove();
|
|
@@ -233,6 +233,24 @@ export const AdwHeaderBarTest = async () => {
|
|
|
233
233
|
|
|
234
234
|
// `windowtitle .title, .subtitle { padding: 0 12px }` (_header-bar.scss:214-227).
|
|
235
235
|
// Measured beside a GTK render of the gallery's window-title `.blp`: 27px narrower here.
|
|
236
|
+
await describe('<adw-header-bar> packing', async () => {
|
|
237
|
+
await it('packs an untyped child from the start, declared or appended', async () => {
|
|
238
|
+
// adw-header-bar.c:1040-1041 — `type == NULL` is pack_start. A Blueprint `[start]`
|
|
239
|
+
// tags only the next child, so the buttons after it arrive untyped.
|
|
240
|
+
const tagged = document.createElement('button');
|
|
241
|
+
tagged.setAttribute('slot', 'start');
|
|
242
|
+
const declared = document.createElement('button');
|
|
243
|
+
const bar = mountHeaderBar({ title: 'T' }, [tagged, declared]);
|
|
244
|
+
const late = document.createElement('button');
|
|
245
|
+
bar.appendChild(late);
|
|
246
|
+
await Promise.resolve();
|
|
247
|
+
const start = bar.querySelector('.adw-header-bar-start');
|
|
248
|
+
const report = [tagged, declared, late].map((el) => el.parentElement === start);
|
|
249
|
+
unmountAll();
|
|
250
|
+
expect(report).toStrictEqual([true, true, true]);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
236
254
|
await describe('adw-window-title label sides', async () => {
|
|
237
255
|
await it('pads its title and subtitle 12px on each side', () => {
|
|
238
256
|
const title = document.createElement('adw-window-title');
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// DOM-level behaviour tests for <adw-toast-overlay>, in a real browser via the
|
|
2
2
|
// @gjsify/adwaita-web browser test axis.
|
|
3
3
|
//
|
|
4
|
-
// `Adw.ToastOverlay` shows exactly ONE toast at a time and queues the rest
|
|
4
|
+
// `Adw.ToastOverlay` shows exactly ONE toast at a time and queues the rest,
|
|
5
5
|
// advancing only when the visible one is dismissed (manually or by its timeout) — the
|
|
6
|
-
// policy `
|
|
6
|
+
// policy `ToastOverlayQueue` owns in `@gjsify/adwaita-core` and this element composes.
|
|
7
|
+
// A toast is an `Adw.Toast`, added with `add_toast(toast)` as on GTK.
|
|
7
8
|
//
|
|
8
9
|
// Timing runs through core's injected `ToastScheduler` seam (a virtual clock), so the
|
|
9
10
|
// auto-dismiss lifecycle is deterministic with no real timers.
|
|
11
|
+
import { Toast } from '@gjsify/adwaita-core';
|
|
10
12
|
import { describe, expect, it } from '@gjsify/unit';
|
|
11
13
|
|
|
12
14
|
import type { AdwToastOverlay, ToastScheduler, ToastTimerHandle } from './elements/adw-toast-overlay.js';
|
|
@@ -65,12 +67,10 @@ export const AdwToastOverlayTest = async () => {
|
|
|
65
67
|
await it('shows exactly one toast at a time, in FIFO order', async () => {
|
|
66
68
|
const { overlay, clock, host } = mountOverlay();
|
|
67
69
|
|
|
68
|
-
overlay.
|
|
69
|
-
overlay.
|
|
70
|
-
overlay.
|
|
70
|
+
overlay.add_toast(new Toast({ title: 'First', timeout: 3 }));
|
|
71
|
+
overlay.add_toast(new Toast({ title: 'Second', timeout: 3 }));
|
|
72
|
+
overlay.add_toast(new Toast({ title: 'Third', timeout: 3 }));
|
|
71
73
|
|
|
72
|
-
// BEFORE the fix this was ['First', 'Second', 'Third'] — all three
|
|
73
|
-
// strips mounted at once.
|
|
74
74
|
expect(toastTitles(overlay)).toStrictEqual(['First']);
|
|
75
75
|
expect(overlay.pendingToasts).toBe(2);
|
|
76
76
|
expect(overlay.currentToast?.title).toBe('First');
|
|
@@ -96,8 +96,9 @@ export const AdwToastOverlayTest = async () => {
|
|
|
96
96
|
await it('holds the queue while a sticky toast (timeout 0) is shown', async () => {
|
|
97
97
|
const { overlay, clock, host } = mountOverlay();
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
overlay.
|
|
99
|
+
const sticky = new Toast({ title: 'Sticky', timeout: 0 });
|
|
100
|
+
overlay.add_toast(sticky);
|
|
101
|
+
overlay.add_toast(new Toast({ title: 'Next', timeout: 3 }));
|
|
101
102
|
|
|
102
103
|
// No timer was scheduled for the sticky toast, so time does not advance it.
|
|
103
104
|
clock.advance(60_000);
|
|
@@ -105,7 +106,7 @@ export const AdwToastOverlayTest = async () => {
|
|
|
105
106
|
expect(overlay.pendingToasts).toBe(1);
|
|
106
107
|
|
|
107
108
|
// Dismissing it hands the slot to the queued toast.
|
|
108
|
-
|
|
109
|
+
sticky.dismiss();
|
|
109
110
|
expect(toastTitles(overlay)).toStrictEqual(['Next']);
|
|
110
111
|
expect(overlay.pendingToasts).toBe(0);
|
|
111
112
|
|
|
@@ -115,30 +116,34 @@ export const AdwToastOverlayTest = async () => {
|
|
|
115
116
|
await it('advances the queue when the close button is pressed', async () => {
|
|
116
117
|
const { overlay, host } = mountOverlay();
|
|
117
118
|
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
const first = new Toast({ title: 'First', timeout: 3 });
|
|
120
|
+
let dismissed = 0;
|
|
121
|
+
first.connect('dismissed', () => {
|
|
122
|
+
dismissed += 1;
|
|
123
|
+
});
|
|
124
|
+
overlay.add_toast(first);
|
|
125
|
+
overlay.add_toast(new Toast({ title: 'Second', timeout: 3 }));
|
|
120
126
|
|
|
121
127
|
const close = overlay.querySelector('.adw-toast-close-button') as HTMLButtonElement;
|
|
122
128
|
close.click();
|
|
123
129
|
|
|
130
|
+
expect(dismissed).toBe(1);
|
|
124
131
|
expect(toastTitles(overlay)).toStrictEqual(['Second']);
|
|
125
132
|
expect(overlay.pendingToasts).toBe(0);
|
|
126
133
|
|
|
127
134
|
host.remove();
|
|
128
135
|
});
|
|
129
136
|
|
|
130
|
-
await it('
|
|
137
|
+
await it('emits button-clicked and advances the queue', async () => {
|
|
131
138
|
const { overlay, host } = mountOverlay();
|
|
132
139
|
|
|
140
|
+
const toast = new Toast({ title: 'File moved to Trash', timeout: 3, button_label: 'Undo' });
|
|
133
141
|
let undone = 0;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
buttonLabel: 'Undo',
|
|
137
|
-
onAction: () => {
|
|
138
|
-
undone += 1;
|
|
139
|
-
},
|
|
142
|
+
toast.connect('button-clicked', () => {
|
|
143
|
+
undone += 1;
|
|
140
144
|
});
|
|
141
|
-
overlay.
|
|
145
|
+
overlay.add_toast(toast);
|
|
146
|
+
overlay.add_toast(new Toast({ title: 'Second', timeout: 3 }));
|
|
142
147
|
|
|
143
148
|
const action = overlay.querySelector('.adw-toast-action-button') as HTMLButtonElement;
|
|
144
149
|
expect(action.textContent).toBe('Undo');
|
|
@@ -150,12 +155,51 @@ export const AdwToastOverlayTest = async () => {
|
|
|
150
155
|
host.remove();
|
|
151
156
|
});
|
|
152
157
|
|
|
153
|
-
await it('
|
|
158
|
+
await it('draws no action button without a button_label', async () => {
|
|
159
|
+
const { overlay, host } = mountOverlay();
|
|
160
|
+
|
|
161
|
+
overlay.add_toast(new Toast({ title: 'Saved' }));
|
|
162
|
+
expect(overlay.querySelector('.adw-toast-action-button')).toBeNull();
|
|
163
|
+
|
|
164
|
+
host.remove();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await it('renders the title as markup unless use_markup is off', async () => {
|
|
168
|
+
const { overlay, host } = mountOverlay();
|
|
169
|
+
|
|
170
|
+
overlay.add_toast(new Toast({ title: '<b>Bold</b>', timeout: 0 }));
|
|
171
|
+
expect(overlay.querySelector('.adw-toast-title b')?.textContent).toBe('Bold');
|
|
172
|
+
overlay.dismiss_all();
|
|
173
|
+
|
|
174
|
+
const { overlay: plain, host: plainHost } = mountOverlay();
|
|
175
|
+
plain.add_toast(new Toast({ title: '<b>Bold</b>', timeout: 0, use_markup: false }));
|
|
176
|
+
expect(plain.querySelector('.adw-toast-title b')).toBeNull();
|
|
177
|
+
expect(toastTitles(plain)).toStrictEqual(['<b>Bold</b>']);
|
|
178
|
+
|
|
179
|
+
host.remove();
|
|
180
|
+
plainHost.remove();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await it('shows a HIGH priority toast ahead of the current one', async () => {
|
|
184
|
+
const { overlay, host } = mountOverlay();
|
|
185
|
+
|
|
186
|
+
overlay.add_toast(new Toast({ title: 'Normal', timeout: 0 }));
|
|
187
|
+
const urgent = new Toast({ title: 'Urgent', timeout: 0, priority: 1 });
|
|
188
|
+
overlay.add_toast(urgent);
|
|
189
|
+
|
|
190
|
+
expect(toastTitles(overlay)).toStrictEqual(['Urgent']);
|
|
191
|
+
urgent.dismiss();
|
|
192
|
+
expect(toastTitles(overlay)).toStrictEqual(['Normal']);
|
|
193
|
+
|
|
194
|
+
host.remove();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await it('dismisses the visible toast and the queue on dismiss_all()', async () => {
|
|
154
198
|
const { overlay, clock, host } = mountOverlay();
|
|
155
199
|
|
|
156
|
-
overlay.
|
|
157
|
-
overlay.
|
|
158
|
-
overlay.
|
|
200
|
+
overlay.add_toast(new Toast({ title: 'First', timeout: 3 }));
|
|
201
|
+
overlay.add_toast(new Toast({ title: 'Second', timeout: 3 }));
|
|
202
|
+
overlay.dismiss_all();
|
|
159
203
|
|
|
160
204
|
expect(overlay.currentToast).toBeNull();
|
|
161
205
|
expect(overlay.pendingToasts).toBe(0);
|
package/src/application.spec.ts
CHANGED
|
@@ -95,6 +95,50 @@ export const ApplicationTest = async () => {
|
|
|
95
95
|
(window as unknown as { application: object | null }).application = null;
|
|
96
96
|
expect(press({ key: 's', ctrlKey: true })).toBe(false);
|
|
97
97
|
});
|
|
98
|
+
await it('inhibit(LOGOUT) cancels beforeunload until uninhibit', () => {
|
|
99
|
+
const app = new Adw.Application({ application_id: 'org.gjsify.vec.WebInhibit' });
|
|
100
|
+
const unload = (): boolean => {
|
|
101
|
+
const event = new Event('beforeunload', { cancelable: true });
|
|
102
|
+
window.dispatchEvent(event);
|
|
103
|
+
return event.defaultPrevented;
|
|
104
|
+
};
|
|
105
|
+
expect(unload()).toBe(false);
|
|
106
|
+
const cookie = app.inhibit(null, Gtk.ApplicationInhibitFlags.LOGOUT, 'unsaved changes');
|
|
107
|
+
expect(cookie > 0).toBe(true);
|
|
108
|
+
expect(unload()).toBe(true);
|
|
109
|
+
app.uninhibit(cookie);
|
|
110
|
+
expect(unload()).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
await it('every window class has application, get_application and set_application', () => {
|
|
113
|
+
const app = new Adw.Application({ application_id: 'org.gjsify.vec.WebWindowApp' });
|
|
114
|
+
const other = new Adw.Application({ application_id: 'org.gjsify.vec.WebWindowOther' });
|
|
115
|
+
for (const tag of ['gtk-window', 'gtk-application-window', 'adw-window', 'adw-application-window']) {
|
|
116
|
+
const window = document.createElement(tag) as unknown as Gtk.WindowCommon<Element>;
|
|
117
|
+
const notified: unknown[] = [];
|
|
118
|
+
window.connect('notify::application', () => notified.push(window.application));
|
|
119
|
+
expect(window.get_application()).toBe(null);
|
|
120
|
+
window.set_application(app as never);
|
|
121
|
+
expect(window.get_application()).toBe(app);
|
|
122
|
+
expect(window.application).toBe(app);
|
|
123
|
+
expect(app.get_windows().includes(window)).toBe(true);
|
|
124
|
+
window.set_application(other as never);
|
|
125
|
+
expect(app.get_windows().includes(window)).toBe(false);
|
|
126
|
+
expect(other.get_windows().includes(window)).toBe(true);
|
|
127
|
+
window.application = null;
|
|
128
|
+
expect(window.get_application()).toBe(null);
|
|
129
|
+
expect(other.get_windows().includes(window)).toBe(false);
|
|
130
|
+
expect(notified).toStrictEqual([app, other, null]);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
await it('a window reaches inhibit through get_application()', () => {
|
|
134
|
+
const app = new Adw.Application({ application_id: 'org.gjsify.vec.WebWindowInhibit' });
|
|
135
|
+
const window = document.createElement('gtk-window') as unknown as Gtk.WindowCommon<Element>;
|
|
136
|
+
window.set_application(app as never);
|
|
137
|
+
const cookie = window.get_application()?.inhibit(window, Gtk.ApplicationInhibitFlags.LOGOUT, 'unsaved');
|
|
138
|
+
expect((cookie ?? 0) > 0).toBe(true);
|
|
139
|
+
window.get_application()?.uninhibit(cookie ?? 0);
|
|
140
|
+
window.set_application(null);
|
|
141
|
+
});
|
|
98
142
|
await it('quit() takes the windows it added off the page', async () => {
|
|
99
143
|
const app = new Adw.Application({ application_id: 'org.gjsify.vec.WebQuit' });
|
|
100
144
|
const window = scene.window(app as never) as Element;
|
package/src/application.ts
CHANGED
|
@@ -10,10 +10,9 @@ import {
|
|
|
10
10
|
withGtkApplication,
|
|
11
11
|
activateWidgetAction as activateIn,
|
|
12
12
|
activateMenuItem,
|
|
13
|
-
|
|
13
|
+
defineWindowApplicationMembers,
|
|
14
14
|
dispatchAccelerator,
|
|
15
15
|
menuActionsFor,
|
|
16
|
-
installActionMap,
|
|
17
16
|
type AdwMenuItem,
|
|
18
17
|
type AdwMenuActions,
|
|
19
18
|
type AdwMenuModel,
|
|
@@ -40,8 +39,18 @@ export const webHost: ApplicationHost = {
|
|
|
40
39
|
},
|
|
41
40
|
exit() {},
|
|
42
41
|
iconTheme: () => IconTheme.get_for_display(null),
|
|
42
|
+
guardUnload(active) {
|
|
43
|
+
if (active) window.addEventListener('beforeunload', confirmUnload);
|
|
44
|
+
else window.removeEventListener('beforeunload', confirmUnload);
|
|
45
|
+
},
|
|
43
46
|
};
|
|
44
47
|
|
|
48
|
+
/** The only hook a page has on Ctrl+Q, closing the tab and reload; the browser words the prompt. */
|
|
49
|
+
function confirmUnload(event: BeforeUnloadEvent): void {
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
event.returnValue = '';
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
/** An element's parent for action resolution; a popover is rooted at the widget that owns it. */
|
|
46
55
|
export function parentOfElement(node: object): object | null {
|
|
47
56
|
if (!(node instanceof Element)) return null;
|
|
@@ -88,23 +97,9 @@ function followAccelerators(window: object, application: WindowApplication | nul
|
|
|
88
97
|
}
|
|
89
98
|
|
|
90
99
|
/**
|
|
91
|
-
* `Gtk.Window:application` and
|
|
100
|
+
* `Gtk.Window:application`, `get_application()` and `set_application()` on a window element class.
|
|
92
101
|
* Setting the property is `add_window`, as on GTK; setting it to `null` is `remove_window`.
|
|
93
102
|
*/
|
|
94
103
|
export function defineWindowApplication(proto: object): void {
|
|
95
|
-
|
|
96
|
-
Object.defineProperty(proto, 'application', {
|
|
97
|
-
configurable: true,
|
|
98
|
-
enumerable: true,
|
|
99
|
-
get(this: object) {
|
|
100
|
-
return applicationOfWindow(this);
|
|
101
|
-
},
|
|
102
|
-
set(this: object, value: WindowApplication | null) {
|
|
103
|
-
const current = applicationOfWindow(this) as unknown as WindowApplication | null;
|
|
104
|
-
if (current === value) return;
|
|
105
|
-
current?.remove_window(this);
|
|
106
|
-
value?.add_window(this);
|
|
107
|
-
followAccelerators(this, value);
|
|
108
|
-
},
|
|
109
|
-
});
|
|
104
|
+
defineWindowApplicationMembers(proto, followAccelerators);
|
|
110
105
|
}
|
package/src/capabilities.mjs
CHANGED
|
@@ -12,7 +12,5 @@ export const capabilities = {
|
|
|
12
12
|
signal: 'implemented',
|
|
13
13
|
bind: 'implemented',
|
|
14
14
|
breakpoint: 'implemented',
|
|
15
|
-
'sibling-object':
|
|
16
|
-
refused: 'the web builder mounts one root element and has no consumer for a second root yet (ADR 0093)',
|
|
17
|
-
},
|
|
15
|
+
'sibling-object': 'implemented',
|
|
18
16
|
};
|